api: make method body an associated type

This way we do not need to carry the body type into the CLI
router and can instead just require the body to be
Into<Bytes>.

This also makes more sense, because previously a method
could in theory implement multiple ApiMethodInfo types with
different bodies which seems pointless.

Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
This commit is contained in:
Wolfgang Bumiller 2019-06-19 15:16:40 +02:00
parent dbcadda6b2
commit bd79dd8f02
6 changed files with 85 additions and 53 deletions

View File

@ -341,7 +341,9 @@ fn handle_function(
//
// Note that technically we don't need the `description` member in this trait, as this is
// mostly used at compile time for documentation!
impl ::proxmox::api::ApiMethodInfo<#body_type> for #struct_name {
impl ::proxmox::api::ApiMethodInfo for #struct_name {
type Body = #body_type;
fn description(&self) -> &'static str {
#fn_api_description
}

View File

@ -5,6 +5,7 @@ version = "0.1.0"
authors = [ "Wolfgang Bumiller <w.bumiller@proxmox.com>" ]
[dependencies]
bytes = "0.4"
failure = "0.1"
futures-preview = "0.3.0-alpha"
http = "0.1"
@ -13,5 +14,4 @@ serde_derive = "1.0"
serde_json = "1.0"
[dev-dependencies]
bytes = "0.4"
lazy_static = "1.3"

View File

@ -7,13 +7,15 @@ use serde_json::{json, Value};
/// Method entries in a `Router` are actually just `&dyn ApiMethodInfo` trait objects.
/// This contains all the info required to call, document, or command-line-complete parameters for
/// a method.
pub trait ApiMethodInfo<Body> {
pub trait ApiMethodInfo {
type Body;
fn description(&self) -> &'static str;
fn parameters(&self) -> &'static [Parameter];
fn return_type(&self) -> &'static TypeInfo;
fn protected(&self) -> bool;
fn reload_timezone(&self) -> bool;
fn handler(&self) -> fn(Value) -> super::ApiFuture<Body>;
fn handler(&self) -> fn(Value) -> super::ApiFuture<Self::Body>;
}
/// Shortcut to not having to type it out. This function signature is just a dummy and not yet
@ -84,7 +86,9 @@ pub struct ApiMethod<Body> {
pub handler: fn(Value) -> super::ApiFuture<Body>,
}
impl<Body> ApiMethodInfo<Body> for ApiMethod<Body> {
impl<Body> ApiMethodInfo for ApiMethod<Body> {
type Body = Body;
fn description(&self) -> &'static str {
self.description
}
@ -110,7 +114,7 @@ impl<Body> ApiMethodInfo<Body> for ApiMethod<Body> {
}
}
impl<Body> dyn ApiMethodInfo<Body> + Send + Sync {
impl<Body> dyn ApiMethodInfo<Body = Body> + Send + Sync {
pub fn api_dump(&self) -> Value {
let parameters = Value::Object(std::iter::FromIterator::from_iter(
self.parameters()

View File

@ -3,21 +3,22 @@
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};
type MethodInfoRef<Body> = &'static (dyn ApiMethodInfo<Body> + Send + Sync);
type MethodInfoRef = &'static dyn UnifiedApiMethod;
/// A CLI root node.
pub struct App<Body: 'static> {
pub struct App {
name: &'static str,
command: Option<Command<Body>>,
command: Option<Command>,
}
impl<Body: 'static> App<Body> {
impl App {
/// Create a new empty App instance.
pub fn new(name: &'static str) -> Self {
Self {
@ -29,7 +30,7 @@ impl<Body: 'static> App<Body> {
/// 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 {
pub fn method(mut self, method: Method) -> Self {
assert!(
self.command.is_none(),
"app {} already has a comman!",
@ -44,7 +45,7 @@ impl<Body: 'static> App<Body> {
///
/// 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 {
pub fn subcommand(mut self, name: &'static str, subcommand: Command) -> Self {
match self
.command
.get_or_insert_with(|| Command::SubCommands(SubCommands::new()))
@ -58,7 +59,7 @@ impl<Body: 'static> App<Body> {
}
/// Resolve a list of parameters to a method and a parameter json value.
pub fn resolve(&self, args: &[&str]) -> Result<(MethodInfoRef<Body>, Value), Error> {
pub fn resolve(&self, args: &[&str]) -> Result<(MethodInfoRef, Value), Error> {
self.command
.as_ref()
.ok_or_else(|| format_err!("no commands available"))?
@ -66,25 +67,29 @@ impl<Body: 'static> App<Body> {
}
/// Run a command through this command line interface.
pub fn run(&self, args: &[&str]) -> ApiOutput<Body> {
pub fn run(&self, args: &[&str]) -> ApiOutput<Bytes> {
let (method, params) = self.resolve(args)?;
let handler = method.handler();
futures::executor::block_on(handler(params))
let future = method.call(params);
futures::executor::block_on(future)
}
}
/// A node in the CLI command router. This is either
pub enum Command<Body: 'static> {
Method(Method<Body>),
SubCommands(SubCommands<Body>),
pub enum Command {
Method(Method),
SubCommands(SubCommands),
}
impl<Body: 'static> Command<Body> {
impl Command {
/// Create a Command entry pointing to an API method
pub fn method(
method: &'static (dyn ApiMethodInfo<Body> + Send + Sync),
pub fn method<T: Send + Sync>(
method: &'static T,
positional_args: &'static [&'static str],
) -> Self {
) -> Self
where
T: ApiMethodInfo,
T::Body: 'static + Into<Bytes>,
{
Command::Method(Method::new(method, positional_args))
}
@ -93,7 +98,7 @@ impl<Body: 'static> Command<Body> {
Command::SubCommands(SubCommands::new())
}
fn resolve(&self, args: std::slice::Iter<&str>) -> Result<(MethodInfoRef<Body>, Value), Error> {
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),
@ -101,11 +106,11 @@ impl<Body: 'static> Command<Body> {
}
}
pub struct SubCommands<Body: 'static> {
commands: HashMap<&'static str, Command<Body>>,
pub struct SubCommands {
commands: HashMap<&'static str, Command>,
}
impl<Body: 'static> SubCommands<Body> {
impl SubCommands {
/// Create a new empty SubCommands hash.
pub fn new() -> Self {
Self {
@ -116,7 +121,7 @@ impl<Body: 'static> SubCommands<Body> {
/// 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 {
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
@ -125,15 +130,12 @@ impl<Body: 'static> SubCommands<Body> {
/// 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 {
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<Body>, Value), Error> {
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) {
@ -144,6 +146,36 @@ impl<Body: 'static> SubCommands<Body> {
}
}
/// API methods can have different body types. For the CLI we don't care whether it is a
/// hyper::Body or a bytes::Bytes (also because we don't care for partia bodies etc.), so the
/// output needs to be wrapped to a common format. So basically the CLI will only ever see
/// `ApiOutput<Bytes>`.
pub trait UnifiedApiMethod: Send + Sync {
fn parameters(&self) -> &'static [Parameter];
fn call(&self, params: Value) -> super::ApiFuture<Bytes>;
}
impl<T: Send + Sync> UnifiedApiMethod for T
where
T: ApiMethodInfo,
T::Body: 'static + Into<Bytes>,
{
fn parameters(&self) -> &'static [Parameter] {
ApiMethodInfo::parameters(self)
}
fn call(&self, params: Value) -> super::ApiFuture<Bytes> {
//async fn real_handler(this: &Self, params: Value) -> ApiOutput<Bytes> {
// (Self::handler(this))(params)
// .await
// .map(|res| res.into())
//}
let handler = self.handler();
use futures::future::TryFutureExt;
Box::pin(handler(params).map_ok(|res| res.map(|body| body.into())))
}
}
/// 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.
@ -151,28 +183,22 @@ impl<Body: 'static> SubCommands<Body> {
/// 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: MethodInfoRef<Body>,
pub struct Method {
pub method: MethodInfoRef,
pub positional_args: &'static [&'static str],
//pub formatter: Option<()>, // TODO: output formatter
}
impl<Body: 'static> Method<Body> {
impl Method {
/// Create a new reference to an API method.
pub fn new(
method: &'static (dyn ApiMethodInfo<Body> + Send + Sync),
positional_args: &'static [&'static str],
) -> Self {
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<Body>, Value), Error> {
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();

View File

@ -32,16 +32,16 @@ pub enum SubRoute<Body: 'static> {
#[derive(Default)]
pub struct Router<Body: 'static> {
/// The `GET` http method.
pub get: Option<&'static (dyn ApiMethodInfo<Body> + Send + Sync)>,
pub get: Option<&'static (dyn ApiMethodInfo<Body = Body> + Send + Sync)>,
/// The `PUT` http method.
pub put: Option<&'static (dyn ApiMethodInfo<Body> + Send + Sync)>,
pub put: Option<&'static (dyn ApiMethodInfo<Body = Body> + Send + Sync)>,
/// The `POST` http method.
pub post: Option<&'static (dyn ApiMethodInfo<Body> + Send + Sync)>,
pub post: Option<&'static (dyn ApiMethodInfo<Body = Body> + Send + Sync)>,
/// The `DELETE` http method.
pub delete: Option<&'static (dyn ApiMethodInfo<Body> + Send + Sync)>,
pub delete: Option<&'static (dyn ApiMethodInfo<Body = Body> + Send + Sync)>,
/// Specifies the behavior of sub directories. See [`SubRoute`].
pub subroute: Option<SubRoute<Body>>,
@ -165,7 +165,7 @@ where
/// Builder method to provide a `GET` method info.
pub fn get<I>(mut self, method: &'static I) -> Self
where
I: ApiMethodInfo<Body> + Send + Sync,
I: ApiMethodInfo<Body = Body> + Send + Sync,
{
self.get = Some(method);
self
@ -174,7 +174,7 @@ where
/// Builder method to provide a `PUT` method info.
pub fn put<I>(mut self, method: &'static I) -> Self
where
I: ApiMethodInfo<Body> + Send + Sync,
I: ApiMethodInfo<Body = Body> + Send + Sync,
{
self.put = Some(method);
self
@ -183,7 +183,7 @@ where
/// Builder method to provide a `POST` method info.
pub fn post<I>(mut self, method: &'static I) -> Self
where
I: ApiMethodInfo<Body> + Send + Sync,
I: ApiMethodInfo<Body = Body> + Send + Sync,
{
self.post = Some(method);
self
@ -192,7 +192,7 @@ where
/// Builder method to provide a `DELETE` method info.
pub fn delete<I>(mut self, method: &'static I) -> Self
where
I: ApiMethodInfo<Body> + Send + Sync,
I: ApiMethodInfo<Body = Body> + Send + Sync,
{
self.delete = Some(method);
self

View File

@ -65,7 +65,7 @@ fn simple() {
);
}
fn check_cli(cli: &cli::App<Bytes>, args: &[&str], expect: Result<&str, &str>) {
fn check_cli(cli: &cli::App, args: &[&str], expect: Result<&str, &str>) {
match (cli.run(args), expect) {
(Ok(result), Ok(expect)) => {
let body = std::str::from_utf8(result.body().as_ref())