forked from proxmox-mirrors/proxmox
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:
parent
64d9e81c76
commit
3182df96c0
128
proxmox-api/src/cli.rs
Normal file
128
proxmox-api/src/cli.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
@ -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
82
proxmox-api/tests/cli.rs
Normal 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)) },
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user