feat(cli): add plugin (#304)

This commit is contained in:
Lucas Fernandes Nogueira
2023-04-14 11:31:28 -07:00
committed by GitHub
parent 4b229031b7
commit 0fed10fdce
16 changed files with 1739 additions and 479 deletions
+174
View File
@@ -0,0 +1,174 @@
use std::collections::HashMap;
use serde::Deserialize;
/// A CLI argument definition.
#[derive(Debug, PartialEq, Eq, Clone, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct Arg {
/// The short version of the argument, without the preceding -.
///
/// NOTE: Any leading `-` characters will be stripped, and only the first non-character will be used as the short version.
pub short: Option<char>,
/// The unique argument name
pub name: String,
/// The argument description which will be shown on the help information.
/// Typically, this is a short (one line) description of the arg.
pub description: Option<String>,
/// The argument long description which will be shown on the help information.
/// Typically this a more detailed (multi-line) message that describes the argument.
#[serde(alias = "long-description")]
pub long_description: Option<String>,
/// Specifies that the argument takes a value at run time.
///
/// NOTE: values for arguments may be specified in any of the following methods
/// - Using a space such as -o value or --option value
/// - Using an equals and no space such as -o=value or --option=value
/// - Use a short and no space such as -ovalue
#[serde(default, alias = "takes-value")]
pub takes_value: bool,
/// Specifies that the argument may have an unknown number of multiple values. Without any other settings, this argument may appear only once.
///
/// For example, --opt val1 val2 is allowed, but --opt val1 val2 --opt val3 is not.
///
/// NOTE: Setting this requires `takes_value` to be set to true.
#[serde(default)]
pub multiple: bool,
/// Specifies how many values are required to satisfy this argument. For example, if you had a
/// `-f <file>` argument where you wanted exactly 3 'files' you would set
/// `number_of_values = 3`, and this argument wouldn't be satisfied unless the user provided
/// 3 and only 3 values.
///
/// **NOTE:** Does *not* require `multiple_occurrences = true` to be set. Setting
/// `multiple_occurrences = true` would allow `-f <file> <file> <file> -f <file> <file> <file>` where
/// as *not* setting it would only allow one occurrence of this argument.
///
/// **NOTE:** implicitly sets `takes_value = true` and `multiple_values = true`.
#[serde(alias = "number-of-values")]
pub number_of_values: Option<usize>,
/// Specifies a list of possible values for this argument.
/// At runtime, the CLI verifies that only one of the specified values was used, or fails with an error message.
#[serde(alias = "possible-values")]
pub possible_values: Option<Vec<String>>,
/// Specifies the minimum number of values for this argument.
/// For example, if you had a -f `<file>` argument where you wanted at least 2 'files',
/// you would set `minValues: 2`, and this argument would be satisfied if the user provided, 2 or more values.
#[serde(alias = "min-values")]
pub min_values: Option<usize>,
/// Specifies the maximum number of values are for this argument.
/// For example, if you had a -f `<file>` argument where you wanted up to 3 'files',
/// you would set .max_values(3), and this argument would be satisfied if the user provided, 1, 2, or 3 values.
#[serde(alias = "max-values")]
pub max_values: Option<usize>,
/// Sets whether or not the argument is required by default.
///
/// - Required by default means it is required, when no other conflicting rules have been evaluated
/// - Conflicting rules take precedence over being required.
#[serde(default)]
pub required: bool,
/// Sets an arg that override this arg's required setting
/// i.e. this arg will be required unless this other argument is present.
#[serde(alias = "required-unless-present")]
pub required_unless_present: Option<String>,
/// Sets args that override this arg's required setting
/// i.e. this arg will be required unless all these other arguments are present.
#[serde(alias = "required-unless-present-all")]
pub required_unless_present_all: Option<Vec<String>>,
/// Sets args that override this arg's required setting
/// i.e. this arg will be required unless at least one of these other arguments are present.
#[serde(alias = "required-unless-present-any")]
pub required_unless_present_any: Option<Vec<String>>,
/// Sets a conflicting argument by name
/// i.e. when using this argument, the following argument can't be present and vice versa.
#[serde(alias = "conflicts-with")]
pub conflicts_with: Option<String>,
/// The same as conflictsWith but allows specifying multiple two-way conflicts per argument.
#[serde(alias = "conflicts-with-all")]
pub conflicts_with_all: Option<Vec<String>>,
/// Tets an argument by name that is required when this one is present
/// i.e. when using this argument, the following argument must be present.
pub requires: Option<String>,
/// Sts multiple arguments by names that are required when this one is present
/// i.e. when using this argument, the following arguments must be present.
#[serde(alias = "requires-all")]
pub requires_all: Option<Vec<String>>,
/// Allows a conditional requirement with the signature [arg, value]
/// the requirement will only become valid if `arg`'s value equals `${value}`.
#[serde(alias = "requires-if")]
pub requires_if: Option<(String, String)>,
/// Allows specifying that an argument is required conditionally with the signature [arg, value]
/// the requirement will only become valid if the `arg`'s value equals `${value}`.
#[serde(alias = "required-if-eq")]
pub required_if_eq: Option<(String, String)>,
/// Requires that options use the --option=val syntax
/// i.e. an equals between the option and associated value.
#[serde(alias = "requires-equals")]
pub require_equals: Option<bool>,
/// The positional argument index, starting at 1.
///
/// The index refers to position according to other positional argument.
/// It does not define position in the argument list as a whole. When utilized with multiple=true,
/// only the last positional argument may be defined as multiple (i.e. the one with the highest index).
pub index: Option<usize>,
}
/// describes a CLI configuration
#[derive(Debug, PartialEq, Eq, Clone, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct Config {
/// Command description which will be shown on the help information.
pub description: Option<String>,
/// Command long description which will be shown on the help information.
#[serde(alias = "long-description")]
pub long_description: Option<String>,
/// Adds additional help information to be displayed in addition to auto-generated help.
/// This information is displayed before the auto-generated help information.
/// This is often used for header information.
#[serde(alias = "before-help")]
pub before_help: Option<String>,
/// Adds additional help information to be displayed in addition to auto-generated help.
/// This information is displayed after the auto-generated help information.
/// This is often used to describe how to use the arguments, or caveats to be noted.
#[serde(alias = "after-help")]
pub after_help: Option<String>,
/// List of arguments for the command
pub args: Option<Vec<Arg>>,
/// List of subcommands of this command
pub subcommands: Option<HashMap<String, Config>>,
}
impl Config {
/// List of arguments for the command
pub fn args(&self) -> Option<&Vec<Arg>> {
self.args.as_ref()
}
/// List of subcommands of this command
pub fn subcommands(&self) -> Option<&HashMap<String, Config>> {
self.subcommands.as_ref()
}
/// Command description which will be shown on the help information.
pub fn description(&self) -> Option<&String> {
self.description.as_ref()
}
/// Command long description which will be shown on the help information.
pub fn long_description(&self) -> Option<&String> {
self.description.as_ref()
}
/// Adds additional help information to be displayed in addition to auto-generated help.
/// This information is displayed before the auto-generated help information.
/// This is often used for header information.
pub fn before_help(&self) -> Option<&String> {
self.before_help.as_ref()
}
/// Adds additional help information to be displayed in addition to auto-generated help.
/// This information is displayed after the auto-generated help information.
/// This is often used to describe how to use the arguments, or caveats to be noted.
pub fn after_help(&self) -> Option<&String> {
self.after_help.as_ref()
}
}
+16
View File
@@ -0,0 +1,16 @@
use serde::{Serialize, Serializer};
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("failed to parse arguments: {0}")]
ParseCli(#[from] clap::Error),
}
impl Serialize for Error {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(self.to_string().as_ref())
}
}
+46
View File
@@ -0,0 +1,46 @@
use tauri::{
plugin::{Builder, PluginApi, TauriPlugin},
AppHandle, Manager, Runtime, State,
};
mod config;
mod error;
mod parser;
use config::{Arg, Config};
use error::Error;
type Result<T> = std::result::Result<T, Error>;
// TODO: use PluginApi#app when 2.0.0-alpha.9 is released
pub struct Cli<R: Runtime>(PluginApi<R, Config>, AppHandle<R>);
impl<R: Runtime> Cli<R> {
pub fn matches(&self) -> Result<parser::Matches> {
parser::get_matches(self.0.config(), self.1.package_info())
}
}
pub trait CliExt<R: Runtime> {
fn cli(&self) -> &Cli<R>;
}
impl<R: Runtime, T: Manager<R>> CliExt<R> for T {
fn cli(&self) -> &Cli<R> {
self.state::<Cli<R>>().inner()
}
}
#[tauri::command]
fn cli_matches<R: Runtime>(_app: AppHandle<R>, cli: State<'_, Cli<R>>) -> Result<parser::Matches> {
cli.matches()
}
pub fn init<R: Runtime>() -> TauriPlugin<R, Config> {
Builder::new("cli")
.invoke_handler(tauri::generate_handler![cli_matches])
.setup(|app, api| {
app.manage(Cli(api, app.clone()));
Ok(())
})
.build()
}
+282
View File
@@ -0,0 +1,282 @@
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use clap::{
builder::{PossibleValue, PossibleValuesParser},
error::ErrorKind,
Arg as ClapArg, ArgAction, ArgMatches, Command,
};
use serde::Serialize;
use serde_json::Value;
use tauri::PackageInfo;
use crate::{Arg, Config};
use std::collections::HashMap;
#[macro_use]
mod macros;
/// The resolution of a argument match.
#[derive(Default, Debug, Serialize)]
#[non_exhaustive]
pub struct ArgData {
/// - [`Value::Bool`] if it's a flag,
/// - [`Value::Array`] if it's multiple,
/// - [`Value::String`] if it has value,
/// - [`Value::Null`] otherwise.
pub value: Value,
/// The number of occurrences of the argument.
/// e.g. `./app --arg 1 --arg 2 --arg 2 3 4` results in three occurrences.
pub occurrences: u8,
}
/// The matched subcommand.
#[derive(Default, Debug, Serialize)]
#[non_exhaustive]
pub struct SubcommandMatches {
/// The subcommand name.
pub name: String,
/// The subcommand argument matches.
pub matches: Matches,
}
/// The argument matches of a command.
#[derive(Default, Debug, Serialize)]
#[non_exhaustive]
pub struct Matches {
/// Data structure mapping each found arg with its resolution.
pub args: HashMap<String, ArgData>,
/// The matched subcommand if found.
pub subcommand: Option<Box<SubcommandMatches>>,
}
impl Matches {
/// Set a arg match.
pub(crate) fn set_arg(&mut self, name: String, value: ArgData) {
self.args.insert(name, value);
}
/// Sets the subcommand matches.
pub(crate) fn set_subcommand(&mut self, name: String, matches: Matches) {
self.subcommand = Some(Box::new(SubcommandMatches { name, matches }));
}
}
/// Gets the argument matches of the CLI definition.
///
/// This is a low level API. If the application has been built,
/// prefer [`App::get_cli_matches`](`crate::App#method.get_cli_matches`).
///
/// # Examples
///
/// ```rust,no_run
/// use tauri_plugin_cli::get_matches;
/// tauri::Builder::default()
/// .setup(|app| {
/// let matches = get_matches(app.config().tauri.cli.as_ref().unwrap(), app.package_info())?;
/// Ok(())
/// });
/// ```
pub fn get_matches(cli: &Config, package_info: &PackageInfo) -> crate::Result<Matches> {
let about = cli
.description()
.unwrap_or(&package_info.description.to_string())
.to_string();
let version = package_info.version.to_string();
let app = get_app(
package_info,
version,
package_info.name.clone(),
Some(&about),
cli,
);
match app.try_get_matches() {
Ok(matches) => Ok(get_matches_internal(cli, &matches)),
Err(e) => match e.kind() {
ErrorKind::DisplayHelp => {
let mut matches = Matches::default();
let help_text = e.to_string();
matches.args.insert(
"help".to_string(),
ArgData {
value: Value::String(help_text),
occurrences: 0,
},
);
Ok(matches)
}
ErrorKind::DisplayVersion => {
let mut matches = Matches::default();
matches
.args
.insert("version".to_string(), Default::default());
Ok(matches)
}
_ => Err(e.into()),
},
}
}
fn get_matches_internal(config: &Config, matches: &ArgMatches) -> Matches {
let mut cli_matches = Matches::default();
map_matches(config, matches, &mut cli_matches);
if let Some((subcommand_name, subcommand_matches)) = matches.subcommand() {
if let Some(subcommand_config) = config
.subcommands
.as_ref()
.and_then(|s| s.get(subcommand_name))
{
cli_matches.set_subcommand(
subcommand_name.to_string(),
get_matches_internal(subcommand_config, subcommand_matches),
);
}
}
cli_matches
}
fn map_matches(config: &Config, matches: &ArgMatches, cli_matches: &mut Matches) {
if let Some(args) = config.args() {
for arg in args {
let (occurrences, value) = if arg.takes_value {
if arg.multiple {
matches
.get_many::<String>(&arg.name)
.map(|v| {
let mut values = Vec::new();
for value in v {
values.push(Value::String(value.into()));
}
(values.len() as u8, Value::Array(values))
})
.unwrap_or((0, Value::Null))
} else {
matches
.get_one::<String>(&arg.name)
.map(|v| (1, Value::String(v.clone())))
.unwrap_or((0, Value::Null))
}
} else {
let occurrences = matches.get_count(&arg.name);
(occurrences, Value::Bool(occurrences > 0))
};
cli_matches.set_arg(arg.name.clone(), ArgData { value, occurrences });
}
}
}
fn get_app(
package_info: &PackageInfo,
version: String,
command_name: String,
about: Option<&String>,
config: &Config,
) -> Command {
let mut app = Command::new(command_name)
.author(package_info.authors)
.version(version.clone());
if let Some(about) = about {
app = app.about(about);
}
if let Some(long_description) = config.long_description() {
app = app.long_about(long_description);
}
if let Some(before_help) = config.before_help() {
app = app.before_help(before_help);
}
if let Some(after_help) = config.after_help() {
app = app.after_help(after_help);
}
if let Some(args) = config.args() {
for arg in args {
app = app.arg(get_arg(arg.name.clone(), arg));
}
}
if let Some(subcommands) = config.subcommands() {
for (subcommand_name, subcommand) in subcommands {
let clap_subcommand = get_app(
package_info,
version.clone(),
subcommand_name.to_string(),
subcommand.description(),
subcommand,
);
app = app.subcommand(clap_subcommand);
}
}
app
}
fn get_arg(arg_name: String, arg: &Arg) -> ClapArg {
let mut clap_arg = ClapArg::new(arg_name.clone());
if arg.index.is_none() {
clap_arg = clap_arg.long(arg_name);
if let Some(short) = arg.short {
clap_arg = clap_arg.short(short);
}
}
clap_arg = bind_string_arg!(arg, clap_arg, description, help);
clap_arg = bind_string_arg!(arg, clap_arg, long_description, long_help);
let action = if arg.multiple {
ArgAction::Append
} else if arg.takes_value {
ArgAction::Set
} else {
ArgAction::Count
};
clap_arg = clap_arg.action(action);
clap_arg = bind_value_arg!(arg, clap_arg, number_of_values);
if let Some(values) = &arg.possible_values {
clap_arg = clap_arg.value_parser(PossibleValuesParser::new(
values
.iter()
.map(PossibleValue::new)
.collect::<Vec<PossibleValue>>(),
));
}
clap_arg = match (arg.min_values, arg.max_values) {
(Some(min), Some(max)) => clap_arg.num_args(min..=max),
(Some(min), None) => clap_arg.num_args(min..),
(None, Some(max)) => clap_arg.num_args(0..max),
(None, None) => clap_arg,
};
clap_arg = clap_arg.required(arg.required);
clap_arg = bind_string_arg!(
arg,
clap_arg,
required_unless_present,
required_unless_present
);
clap_arg = bind_string_slice_arg!(arg, clap_arg, required_unless_present_all);
clap_arg = bind_string_slice_arg!(arg, clap_arg, required_unless_present_any);
clap_arg = bind_string_arg!(arg, clap_arg, conflicts_with, conflicts_with);
if let Some(value) = &arg.conflicts_with_all {
clap_arg = clap_arg.conflicts_with_all(value);
}
clap_arg = bind_string_arg!(arg, clap_arg, requires, requires);
if let Some(value) = &arg.requires_all {
clap_arg = clap_arg.requires_all(value);
}
clap_arg = bind_if_arg!(arg, clap_arg, requires_if);
clap_arg = bind_if_arg!(arg, clap_arg, required_if_eq);
clap_arg = bind_value_arg!(arg, clap_arg, require_equals);
clap_arg = bind_value_arg!(arg, clap_arg, index);
clap_arg
}
+47
View File
@@ -0,0 +1,47 @@
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
macro_rules! bind_string_arg {
($arg:expr, $clap_arg:expr, $arg_name:ident, $clap_field:ident) => {{
let arg = $arg;
let mut clap_arg = $clap_arg;
if let Some(value) = &arg.$arg_name {
clap_arg = clap_arg.$clap_field(value);
}
clap_arg
}};
}
macro_rules! bind_value_arg {
($arg:expr, $clap_arg:expr, $field:ident) => {{
let arg = $arg;
let mut clap_arg = $clap_arg;
if let Some(value) = arg.$field {
clap_arg = clap_arg.$field(value);
}
clap_arg
}};
}
macro_rules! bind_string_slice_arg {
($arg:expr, $clap_arg:expr, $field:ident) => {{
let arg = $arg;
let mut clap_arg = $clap_arg;
if let Some(value) = &arg.$field {
clap_arg = clap_arg.$field(value);
}
clap_arg
}};
}
macro_rules! bind_if_arg {
($arg:expr, $clap_arg:expr, $field:ident) => {{
let arg = $arg;
let mut clap_arg = $clap_arg;
if let Some((value, arg)) = &arg.$field {
clap_arg = clap_arg.$field(value, arg);
}
clap_arg
}};
}