mirror of
				https://git.proxmox.com/git/proxmox
				synced 2025-11-04 12:57:31 +00:00 
			
		
		
		
	For a T which is not directly a Body or Response<Body> type
    #[api]
    fn foo() -> T;
should not require a specific Body type.
Signed-off-by: Wolfgang Bumiller <wry.git@bumiller.com>
		
	
			
		
			
				
	
	
		
			382 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
			
		
		
	
	
			382 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
//! Provides Command Line Interface to API methods
 | 
						|
 | 
						|
use std::collections::HashMap;
 | 
						|
use std::str::FromStr;
 | 
						|
 | 
						|
use bytes::Bytes;
 | 
						|
use failure::{bail, format_err, Error};
 | 
						|
use serde::Serialize;
 | 
						|
use serde_json::Value;
 | 
						|
 | 
						|
use super::{ApiMethodInfo, ApiOutput, Parameter, UnifiedApiMethod};
 | 
						|
 | 
						|
type MethodInfoRef = &'static dyn UnifiedApiMethod<Bytes>;
 | 
						|
 | 
						|
/// A CLI root node.
 | 
						|
pub struct App {
 | 
						|
    name: &'static str,
 | 
						|
    command: Option<Command>,
 | 
						|
}
 | 
						|
 | 
						|
impl App {
 | 
						|
    /// Create a new empty App instance.
 | 
						|
    pub fn new(name: &'static str) -> Self {
 | 
						|
        Self {
 | 
						|
            name,
 | 
						|
            command: None,
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    /// Directly connect this instance to a single API method.
 | 
						|
    ///
 | 
						|
    /// This is a builder method and will panic if there's already a method registered!
 | 
						|
    pub fn method(mut self, method: Method) -> Self {
 | 
						|
        assert!(
 | 
						|
            self.command.is_none(),
 | 
						|
            "app {} already has a comman!",
 | 
						|
            self.name
 | 
						|
        );
 | 
						|
 | 
						|
        self.command = Some(Command::Method(method));
 | 
						|
        self
 | 
						|
    }
 | 
						|
 | 
						|
    /// Add a subcommand to this instance.
 | 
						|
    ///
 | 
						|
    /// This is a builder method and will panic if the subcommand already exists or no subcommands
 | 
						|
    /// may be added.
 | 
						|
    pub fn subcommand(mut self, name: &'static str, subcommand: Command) -> Self {
 | 
						|
        match self
 | 
						|
            .command
 | 
						|
            .get_or_insert_with(|| Command::SubCommands(SubCommands::new()))
 | 
						|
        {
 | 
						|
            Command::SubCommands(ref mut commands) => {
 | 
						|
                commands.add_subcommand(name, subcommand);
 | 
						|
                self
 | 
						|
            }
 | 
						|
            _ => panic!("app {} cannot have subcommands!", self.name),
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    /// Resolve a list of parameters to a method and a parameter json value.
 | 
						|
    pub fn resolve(&self, args: &[&str]) -> Result<(MethodInfoRef, Value), Error> {
 | 
						|
        self.command
 | 
						|
            .as_ref()
 | 
						|
            .ok_or_else(|| format_err!("no commands available"))?
 | 
						|
            .resolve(args.iter())
 | 
						|
    }
 | 
						|
 | 
						|
    /// Run a command through this command line interface.
 | 
						|
    pub fn run(&self, args: &[&str]) -> ApiOutput<Bytes> {
 | 
						|
        let (method, params) = self.resolve(args)?;
 | 
						|
        let future = method.call(params);
 | 
						|
        futures::executor::block_on(future)
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
/// A node in the CLI command router. This is either
 | 
						|
pub enum Command {
 | 
						|
    Method(Method),
 | 
						|
    SubCommands(SubCommands),
 | 
						|
}
 | 
						|
 | 
						|
impl Command {
 | 
						|
    /// Create a Command entry pointing to an API method
 | 
						|
    pub fn method<T: Send + Sync>(
 | 
						|
        method: &'static T,
 | 
						|
        positional_args: &'static [&'static str],
 | 
						|
    ) -> Self
 | 
						|
    where
 | 
						|
        T: ApiMethodInfo,
 | 
						|
        T::Body: 'static + Into<Bytes>,
 | 
						|
    {
 | 
						|
        Command::Method(Method::new(method, positional_args))
 | 
						|
    }
 | 
						|
 | 
						|
    /// Create a new empty subcommand entry.
 | 
						|
    pub fn new() -> Self {
 | 
						|
        Command::SubCommands(SubCommands::new())
 | 
						|
    }
 | 
						|
 | 
						|
    fn resolve(&self, args: std::slice::Iter<&str>) -> Result<(MethodInfoRef, Value), Error> {
 | 
						|
        match self {
 | 
						|
            Command::Method(method) => method.resolve(args),
 | 
						|
            Command::SubCommands(subcmd) => subcmd.resolve(args),
 | 
						|
        }
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
pub struct SubCommands {
 | 
						|
    commands: HashMap<&'static str, Command>,
 | 
						|
}
 | 
						|
 | 
						|
impl SubCommands {
 | 
						|
    /// Create a new empty SubCommands hash.
 | 
						|
    pub fn new() -> Self {
 | 
						|
        Self {
 | 
						|
            commands: HashMap::new(),
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    /// Add a subcommand.
 | 
						|
    ///
 | 
						|
    /// Note that it is illegal for the subcommand to already exist, which will cause a panic.
 | 
						|
    pub fn add_subcommand(&mut self, name: &'static str, command: Command) -> &mut Self {
 | 
						|
        let old = self.commands.insert(name, command);
 | 
						|
        assert!(old.is_none(), "subcommand '{}' already exists", name);
 | 
						|
        self
 | 
						|
    }
 | 
						|
 | 
						|
    /// Builder method to add a subcommand.
 | 
						|
    ///
 | 
						|
    /// Note that it is illegal for the subcommand to already exist, which will cause a panic.
 | 
						|
    pub fn subcommand(mut self, name: &'static str, command: Command) -> Self {
 | 
						|
        self.add_subcommand(name, command);
 | 
						|
        self
 | 
						|
    }
 | 
						|
 | 
						|
    fn resolve(&self, mut args: std::slice::Iter<&str>) -> Result<(MethodInfoRef, Value), 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
 | 
						|
/// match some parameters as positional parameters rather than argument switches, therefor this
 | 
						|
/// contains an ordered list of positional parameters.
 | 
						|
///
 | 
						|
/// Note that we currently do not support optional positional parameters.
 | 
						|
// XXX: If we want optional positional parameters - should we make an enum or just say the
 | 
						|
// parameter name should have brackets around it?
 | 
						|
pub struct Method {
 | 
						|
    pub method: MethodInfoRef,
 | 
						|
    pub positional_args: &'static [&'static str],
 | 
						|
    //pub formatter: Option<()>, // TODO: output formatter
 | 
						|
}
 | 
						|
 | 
						|
impl Method {
 | 
						|
    /// Create a new reference to an API method.
 | 
						|
    pub fn new(method: MethodInfoRef, positional_args: &'static [&'static str]) -> Self {
 | 
						|
        Self {
 | 
						|
            method,
 | 
						|
            positional_args,
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    fn resolve(&self, mut args: std::slice::Iter<&str>) -> Result<(MethodInfoRef, Value), 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.take() {
 | 
						|
                        self.add_parameter(&mut params, arg, None)?;
 | 
						|
                    }
 | 
						|
 | 
						|
                    current_option = Some(arg);
 | 
						|
                }
 | 
						|
                Some(Arg::OptArg(arg, value)) => {
 | 
						|
                    if let Some(arg) = current_option.take() {
 | 
						|
                        self.add_parameter(&mut params, arg, None)?;
 | 
						|
                    }
 | 
						|
 | 
						|
                    self.add_parameter(&mut params, arg, Some(value))?;
 | 
						|
                }
 | 
						|
                Some(Arg::Positional(value)) => match current_option.take() {
 | 
						|
                    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.take() {
 | 
						|
                        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);
 | 
						|
        }
 | 
						|
 | 
						|
        Ok((self.method, Value::Object(params)))
 | 
						|
    }
 | 
						|
 | 
						|
    /// 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<String, Value>,
 | 
						|
        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(arg, 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<Arg<'a>> {
 | 
						|
    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)
 | 
						|
        }
 | 
						|
    })
 | 
						|
}
 | 
						|
 | 
						|
pub fn parse_cli_from_str<T>(name: &str, value: Option<&str>) -> Result<Value, Error>
 | 
						|
where
 | 
						|
    T: FromStr + Serialize,
 | 
						|
    <T as FromStr>::Err: Into<Error>,
 | 
						|
{
 | 
						|
    let this: T = value
 | 
						|
        .ok_or_else(|| format_err!("missing parameter value for '{}'", name))?
 | 
						|
        .parse()
 | 
						|
        .map_err(|e: <T as FromStr>::Err| e.into())?;
 | 
						|
    Ok(serde_json::to_value(this)?)
 | 
						|
}
 | 
						|
 | 
						|
/// We use this trait so we can keep the "mass implementation macro" for the ApiType trait simple
 | 
						|
/// and specialize the CLI parameter parsing via this trait separately.
 | 
						|
pub trait ParseCli {
 | 
						|
    fn parse_cli(name: &str, value: Option<&str>) -> Result<Value, Error>;
 | 
						|
}
 | 
						|
 | 
						|
/// This is a version of ParseCli with a default implementation falling to FromStr.
 | 
						|
pub trait ParseCliFromStr
 | 
						|
where
 | 
						|
    Self: FromStr + Serialize,
 | 
						|
    <Self as FromStr>::Err: Into<Error>,
 | 
						|
{
 | 
						|
    fn parse_cli(name: &str, value: Option<&str>) -> Result<Value, Error> {
 | 
						|
        parse_cli_from_str::<Self>(name, value)
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
impl<T> ParseCliFromStr for T
 | 
						|
where
 | 
						|
    T: FromStr + Serialize,
 | 
						|
    <T as FromStr>::Err: Into<Error>,
 | 
						|
{
 | 
						|
}
 | 
						|
 | 
						|
#[macro_export]
 | 
						|
macro_rules! no_cli_type {
 | 
						|
    ($type:ty $(, $more:ty)*) => {
 | 
						|
        impl $crate::cli::ParseCli for $type {
 | 
						|
            fn parse_cli(name: &str, _value: Option<&str>) -> Result<Value, Error> {
 | 
						|
                bail!(
 | 
						|
                    "invalid type for command line interface found for parameter '{}'",
 | 
						|
                    name
 | 
						|
                );
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        $crate::impl_parse_cli_from_str!{$($more),*}
 | 
						|
    };
 | 
						|
    () => {};
 | 
						|
}
 | 
						|
 | 
						|
no_cli_type! {Vec<String>}
 | 
						|
 | 
						|
#[macro_export]
 | 
						|
macro_rules! impl_parse_cli_from_str {
 | 
						|
    ($type:ty $(, $more:ty)*) => {
 | 
						|
        impl $crate::cli::ParseCli for $type {
 | 
						|
            fn parse_cli(name: &str, value: Option<&str>) -> Result<Value, Error> {
 | 
						|
                parse_cli_from_str::<$type>(name, value)
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        $crate::impl_parse_cli_from_str!{$($more),*}
 | 
						|
    };
 | 
						|
    () => {};
 | 
						|
}
 | 
						|
 | 
						|
impl_parse_cli_from_str! {isize, usize, i64, u64, i32, u32, i16, u16, i8, u8, f64, f32}
 | 
						|
 | 
						|
impl ParseCli for bool {
 | 
						|
    fn parse_cli(name: &str, value: Option<&str>) -> Result<Value, Error> {
 | 
						|
        // for booleans, using `--arg` without an option counts as `true`:
 | 
						|
        match value {
 | 
						|
            None => Ok(Value::Bool(true)),
 | 
						|
            Some("true") | Some("yes") | Some("on") | Some("1") => Ok(Value::Bool(true)),
 | 
						|
            Some("false") | Some("no") | Some("off") | Some("0") => Ok(Value::Bool(false)),
 | 
						|
            Some(other) => bail!("parameter '{}' must be a boolean, found: '{}'", name, other),
 | 
						|
        }
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
impl ParseCli for Value {
 | 
						|
    fn parse_cli(name: &str, _value: Option<&str>) -> Result<Value, Error> {
 | 
						|
        // FIXME: we could of course allow generic json parameters...?
 | 
						|
        bail!(
 | 
						|
            "found generic json parameter ('{}') in command line...",
 | 
						|
            name
 | 
						|
        );
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
impl ParseCli for &str {
 | 
						|
    fn parse_cli(name: &str, value: Option<&str>) -> Result<Value, Error> {
 | 
						|
        Ok(Value::String(
 | 
						|
            value
 | 
						|
                .ok_or_else(|| format_err!("missing value for parameter '{}'", name))?
 | 
						|
                .to_string(),
 | 
						|
        ))
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
impl ParseCli for String {
 | 
						|
    fn parse_cli(name: &str, value: Option<&str>) -> Result<Value, Error> {
 | 
						|
        Ok(Value::String(
 | 
						|
            value
 | 
						|
                .ok_or_else(|| format_err!("missing value for parameter '{}'", name))?
 | 
						|
                .to_string(),
 | 
						|
        ))
 | 
						|
    }
 | 
						|
}
 |