diff --git a/proxmox-api/src/api_type.rs b/proxmox-api/src/api_type.rs index 585fc143..3afd92a7 100644 --- a/proxmox-api/src/api_type.rs +++ b/proxmox-api/src/api_type.rs @@ -3,7 +3,7 @@ use std::cell::Cell; use std::sync::Once; -use failure::Error; +use failure::{bail, Error}; use http::Response; use serde_json::{json, Value}; @@ -41,6 +41,13 @@ impl Parameter { }), ) } + + /// Parse a commnd line option: if it is None, we only saw an `--option` without value, this is + /// fine for booleans. If we saw a value, we should try to parse it out into a json value. For + /// string parameters this means passing them as is, for others it means using FromStr... + pub fn parse_cli(&self, _value: Option<&str>) -> Result { + bail!("TODO"); + } } /// Bare type info. Types themselves should also have a description, even if a method's parameter diff --git a/proxmox-api/src/cli.rs b/proxmox-api/src/cli.rs index e3d6da6d..e5b1106b 100644 --- a/proxmox-api/src/cli.rs +++ b/proxmox-api/src/cli.rs @@ -2,7 +2,10 @@ use std::collections::HashMap; -use super::ApiMethodInfo; +use failure::{bail, format_err, Error}; +use serde_json::Value; + +use super::{ApiMethodInfo, ApiOutput, Parameter}; /// A CLI root node. pub struct App { @@ -49,6 +52,13 @@ impl App { _ => panic!("app {} cannot have subcommands!", self.name), } } + + pub fn resolve(&self, args: &[&str]) -> Result, Error> { + self.command + .as_ref() + .ok_or_else(|| format_err!("no commands available"))? + .resolve(args.iter()) + } } /// A node in the CLI command router. This is either @@ -70,6 +80,13 @@ impl Command { pub fn new() -> Self { Command::SubCommands(SubCommands::new()) } + + fn resolve(&self, args: std::slice::Iter<&str>) -> Result, Error> { + match self { + Command::Method(method) => method.resolve(args), + Command::SubCommands(subcmd) => subcmd.resolve(args), + } + } } pub struct SubCommands { @@ -100,6 +117,16 @@ impl SubCommands { self.add_subcommand(name, command); self } + + fn resolve(&self, mut args: std::slice::Iter<&str>) -> Result, Error> { + match args.next() { + None => bail!("missing subcommand"), + Some(arg) => match self.commands.get(arg) { + None => bail!("no such subcommand: {}", arg), + Some(cmd) => cmd.resolve(args), + }, + } + } } /// A reference to an API method. Note that when coming from the command line, it is possible to @@ -112,6 +139,7 @@ impl SubCommands { pub struct Method { pub method: &'static (dyn ApiMethodInfo + Send + Sync), pub positional_args: &'static [&'static str], + //pub formatter: Option<()>, // TODO: output formatter } impl Method { @@ -125,4 +153,102 @@ impl Method { positional_args, } } + + fn resolve(&self, mut args: std::slice::Iter<&str>) -> Result, Error> { + let mut params = serde_json::Map::new(); + let mut positionals = self.positional_args.iter(); + + let mut current_option = None; + loop { + match next_arg(&mut args) { + Some(Arg::Opt(arg)) => { + if let Some(arg) = current_option { + self.add_parameter(&mut params, arg, None)?; + } + + current_option = Some(arg); + } + Some(Arg::OptArg(arg, value)) => { + if let Some(arg) = current_option { + self.add_parameter(&mut params, arg, None)?; + } + + self.add_parameter(&mut params, arg, Some(value))?; + } + Some(Arg::Positional(value)) => match current_option { + Some(arg) => self.add_parameter(&mut params, arg, Some(value))?, + None => match positionals.next() { + Some(arg) => self.add_parameter(&mut params, arg, Some(value))?, + None => bail!("unexpected positional parameter: '{}'", value), + }, + }, + None => { + if let Some(arg) = current_option { + self.add_parameter(&mut params, arg, None)?; + } + break; + } + } + } + assert!( + current_option.is_none(), + "current_option must have been dealt with" + ); + + let missing = positionals.fold(String::new(), |mut acc, more| { + if acc.is_empty() { + more.to_string() + } else { + acc.push_str(", "); + acc.push_str(more); + acc + } + }); + if !missing.is_empty() { + bail!("missing positional parameters: {}", missing); + } + + unreachable!(); + } + + /// This should insert the parameter 'arg' with value 'value' into 'params'. + /// This means we need to verify `arg` exists in self.method, `value` deserializes to its type, + /// and then serialize it into the Value. + fn add_parameter( + &self, + params: &mut serde_json::Map, + arg: &str, + value: Option<&str>, + ) -> Result<(), Error> { + let param_def = self + .find_parameter(arg) + .ok_or_else(|| format_err!("no such parameter: '{}'", arg))?; + params.insert(arg.to_string(), param_def.parse_cli(value)?); + Ok(()) + } + + fn find_parameter(&self, name: &str) -> Option<&Parameter> { + self.method.parameters().iter().find(|p| p.name == name) + } +} + +enum Arg<'a> { + Positional(&'a str), + Opt(&'a str), + OptArg(&'a str, &'a str), +} + +fn next_arg<'a>(args: &mut std::slice::Iter<&'a str>) -> Option> { + args.next().map(|arg| { + if arg.starts_with("--") { + let arg = &arg[2..]; + + match arg.find('=') { + Some(idx) => Arg::OptArg(&arg[0..idx], &arg[idx + 1..]), + None => Arg::Opt(arg), + } + } else { + Arg::Positional(arg) + } + }) }