diff --git a/proxmox-api/src/cli.rs b/proxmox-api/src/cli.rs new file mode 100644 index 00000000..e3d6da6d --- /dev/null +++ b/proxmox-api/src/cli.rs @@ -0,0 +1,128 @@ +//! Provides Command Line Interface to API methods + +use std::collections::HashMap; + +use super::ApiMethodInfo; + +/// A CLI root node. +pub struct App { + name: &'static str, + command: Option>, +} + +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), + } + } +} + +/// 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( + method: &'static (dyn ApiMethodInfo + Send + Sync), + positional_args: &'static [&'static str], + ) -> Self { + Command::Method(Method::new(method, positional_args)) + } + + /// Create a new empty subcommand entry. + pub fn new() -> Self { + Command::SubCommands(SubCommands::new()) + } +} + +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 + } +} + +/// 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: &'static (dyn ApiMethodInfo + Send + Sync), + pub positional_args: &'static [&'static str], +} + +impl Method { + /// Create a new reference to an API method. + pub fn new( + method: &'static (dyn ApiMethodInfo + Send + Sync), + positional_args: &'static [&'static str], + ) -> Self { + Self { + method, + positional_args, + } + } +} diff --git a/proxmox-api/src/lib.rs b/proxmox-api/src/lib.rs index 1339c31f..21bcbba0 100644 --- a/proxmox-api/src/lib.rs +++ b/proxmox-api/src/lib.rs @@ -21,6 +21,8 @@ pub use api_type::*; mod router; pub use router::*; +pub mod cli; + /// Return type of an API method. pub type ApiOutput = Result, Error>; diff --git a/proxmox-api/tests/cli.rs b/proxmox-api/tests/cli.rs new file mode 100644 index 00000000..0cb8809a --- /dev/null +++ b/proxmox-api/tests/cli.rs @@ -0,0 +1,82 @@ +#![feature(async_await)] + +use bytes::Bytes; + +use proxmox_api::cli; + +#[test] +fn simple() { + let simple_method: &proxmox_api::ApiMethod = &methods::SIMPLE_METHOD; + + let cli = cli::App::new("simple") + .subcommand("new", cli::Command::method(simple_method, &[])) + .subcommand("newfoo", cli::Command::method(simple_method, &["foo"])) + .subcommand("newbar", cli::Command::method(simple_method, &["bar"])) + .subcommand( + "newboth", + cli::Command::method(simple_method, &["foo", "bar"]), + ); +} + +mod methods { + use bytes::Bytes; + use failure::{bail, Error}; + use http::Response; + use lazy_static::lazy_static; + use serde_derive::{Deserialize, Serialize}; + use serde_json::Value; + + use proxmox_api::{ + get_type_info, ApiFuture, ApiMethod, ApiOutput, ApiType, Parameter, TypeInfo, + }; + + pub async fn simple_method(value: Value) -> ApiOutput { + let foo = value["foo"].as_str().unwrap(); + + let bar = value["bar"].as_str().unwrap(); + + let baz = value.get("baz").map(|value| value.as_str().unwrap()); + + let output = match baz { + Some(baz) => format!("{}:{}:{}", foo, bar, baz), + None => format!("{}:{}", foo, bar), + }; + + Ok(Response::builder() + .status(200) + .header("content-type", "application/json") + .body(output.into())?) + } + + lazy_static! { + static ref SIMPLE_PARAMS: Vec = { + vec![ + Parameter { + name: "foo", + description: "a test parameter", + type_info: String::type_info, + }, + Parameter { + name: "bar", + description: "another test parameter", + type_info: String::type_info, + }, + Parameter { + name: "baz", + description: "another test parameter", + type_info: Option::::type_info, + }, + ] + }; + pub static ref SIMPLE_METHOD: ApiMethod = { + ApiMethod { + description: "get some parameters back", + parameters: &SIMPLE_PARAMS, + return_type: get_type_info::(), + protected: false, + reload_timezone: false, + handler: |value: Value| -> ApiFuture { Box::pin(simple_method(value)) }, + } + }; + } +}