api: started CLI layout

The CLI part itself needs much less info now as we'll take
as much as we can from the api methods themselves. Note that
we may still want to be able to add extra info to a cli
command in particular, for instance, for the completion
callbacks. For now this is all part of the method itself.

Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
This commit is contained in:
Wolfgang Bumiller 2019-06-14 11:13:44 +02:00
parent 64d9e81c76
commit 3182df96c0
3 changed files with 212 additions and 0 deletions

128
proxmox-api/src/cli.rs Normal file
View File

@ -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<Body: 'static> {
name: &'static str,
command: Option<Command<Body>>,
}
impl<Body: 'static> App<Body> {
/// 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<Body>) -> 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<Body>) -> 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<Body: 'static> {
Method(Method<Body>),
SubCommands(SubCommands<Body>),
}
impl<Body: 'static> Command<Body> {
/// Create a Command entry pointing to an API method
pub fn method(
method: &'static (dyn ApiMethodInfo<Body> + 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<Body: 'static> {
commands: HashMap<&'static str, Command<Body>>,
}
impl<Body: 'static> SubCommands<Body> {
/// 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<Body>) -> &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<Body>) -> 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<Body: 'static> {
pub method: &'static (dyn ApiMethodInfo<Body> + Send + Sync),
pub positional_args: &'static [&'static str],
}
impl<Body: 'static> Method<Body> {
/// Create a new reference to an API method.
pub fn new(
method: &'static (dyn ApiMethodInfo<Body> + Send + Sync),
positional_args: &'static [&'static str],
) -> Self {
Self {
method,
positional_args,
}
}
}

View File

@ -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<Body> = Result<Response<Body>, Error>;

82
proxmox-api/tests/cli.rs Normal file
View File

@ -0,0 +1,82 @@
#![feature(async_await)]
use bytes::Bytes;
use proxmox_api::cli;
#[test]
fn simple() {
let simple_method: &proxmox_api::ApiMethod<Bytes> = &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<Bytes> {
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<Parameter> = {
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::<String>::type_info,
},
]
};
pub static ref SIMPLE_METHOD: ApiMethod<Bytes> = {
ApiMethod {
description: "get some parameters back",
parameters: &SIMPLE_PARAMS,
return_type: get_type_info::<String>(),
protected: false,
reload_timezone: false,
handler: |value: Value| -> ApiFuture<Bytes> { Box::pin(simple_method(value)) },
}
};
}
}