api: add generic Body parameter

Since we already know we'll want to be using hyper::Body and
bytes::Bytes as API output, we need to allow making routers
for each kind.

Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
This commit is contained in:
Wolfgang Bumiller 2019-06-09 14:04:53 +02:00
parent b82b14d947
commit 7d2c13da95
6 changed files with 69 additions and 47 deletions

View File

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

View File

@ -10,36 +10,41 @@ use super::{ApiOutput, ApiType};
/// wrapped in a `http::Response` with a status code of `200`, but if an API method returns a /// wrapped in a `http::Response` with a status code of `200`, but if an API method returns a
/// `http::Response`, we don't want that, our wrappers produced by the `#[api]` macro simply call /// `http::Response`, we don't want that, our wrappers produced by the `#[api]` macro simply call
/// `output.into_api_output()`, and the trait implementation decides how to proceed. /// `output.into_api_output()`, and the trait implementation decides how to proceed.
pub trait IntoApiOutput<T> { pub trait IntoApiOutput<Body, T> {
fn into_api_output(self) -> ApiOutput; fn into_api_output(self) -> ApiOutput<Body>;
} }
impl<T: ApiType + serde::Serialize> IntoApiOutput<()> for T { impl<Body, T> IntoApiOutput<Body, ()> for T
where
Body: 'static,
T: ApiType + serde::Serialize,
Body: From<String>,
{
/// By default, any serializable type is serialized into a `{"data": output}` json structure, /// By default, any serializable type is serialized into a `{"data": output}` json structure,
/// and returned as http status 200. /// and returned as http status 200.
fn into_api_output(self) -> ApiOutput { fn into_api_output(self) -> ApiOutput<Body> {
let output = serde_json::to_value(self)?; let output = serde_json::to_value(self)?;
let res = json!({ "data": output }); let res = json!({ "data": output });
let output = serde_json::to_string(&res)?; let output = serde_json::to_string(&res)?;
Ok(http::Response::builder() Ok(http::Response::builder()
.status(200) .status(200)
.header("content-type", "application/json") .header("content-type", "application/json")
.body(bytes::Bytes::from(output))?) .body(Body::from(output))?)
} }
} }
/// Methods returning `ApiOutput` (which is a `Result<http::Result<Bytes>, Error>`) don't need /// Methods returning `ApiOutput` (which is a `Result<http::Result<Bytes>, Error>`) don't need
/// anything to happen to the value anymore, return the result as is: /// anything to happen to the value anymore, return the result as is:
impl IntoApiOutput<ApiOutput> for ApiOutput { impl<Body> IntoApiOutput<Body, ApiOutput<Body>> for ApiOutput<Body> {
fn into_api_output(self) -> ApiOutput { fn into_api_output(self) -> ApiOutput<Body> {
self self
} }
} }
/// Methods returning a `http::Response` (without the `Result<_, Error>` around it) need to be /// Methods returning a `http::Response` (without the `Result<_, Error>` around it) need to be
/// wrapped in a `Result`, as we do apply a `?` operator on our methods. /// wrapped in a `Result`, as we do apply a `?` operator on our methods.
impl IntoApiOutput<ApiOutput> for http::Response<bytes::Bytes> { impl<Body> IntoApiOutput<Body, ApiOutput<Body>> for http::Response<Body> {
fn into_api_output(self) -> ApiOutput { fn into_api_output(self) -> ApiOutput<Body> {
Ok(self) Ok(self)
} }
} }

View File

@ -3,7 +3,6 @@
use std::cell::Cell; use std::cell::Cell;
use std::sync::Once; use std::sync::Once;
use bytes::Bytes;
use failure::Error; use failure::Error;
use http::Response; use http::Response;
use serde_json::Value; use serde_json::Value;
@ -11,13 +10,13 @@ use serde_json::Value;
/// Method entries in a `Router` are actually just `&dyn ApiMethodInfo` trait objects. /// 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 /// This contains all the info required to call, document, or command-line-complete parameters for
/// a method. /// a method.
pub trait ApiMethodInfo { pub trait ApiMethodInfo<Body> {
fn description(&self) -> &'static str; fn description(&self) -> &'static str;
fn parameters(&self) -> &'static [Parameter]; fn parameters(&self) -> &'static [Parameter];
fn return_type(&self) -> &'static TypeInfo; fn return_type(&self) -> &'static TypeInfo;
fn protected(&self) -> bool; fn protected(&self) -> bool;
fn reload_timezone(&self) -> bool; fn reload_timezone(&self) -> bool;
fn handler(&self) -> fn(Value) -> super::ApiFuture; fn handler(&self) -> fn(Value) -> super::ApiFuture<Body>;
} }
/// Shortcut to not having to type it out. This function signature is just a dummy and not yet /// Shortcut to not having to type it out. This function signature is just a dummy and not yet
@ -46,16 +45,16 @@ pub struct TypeInfo {
/// Otherwise this is mostly there so we can run the tests in the tests subdirectory without /// Otherwise this is mostly there so we can run the tests in the tests subdirectory without
/// depending on the api-macro crate. Tests using the macros belong into the api-macro crate itself /// depending on the api-macro crate. Tests using the macros belong into the api-macro crate itself
/// after all! /// after all!
pub struct ApiMethod { pub struct ApiMethod<Body> {
pub description: &'static str, pub description: &'static str,
pub parameters: &'static [Parameter], pub parameters: &'static [Parameter],
pub return_type: &'static TypeInfo, pub return_type: &'static TypeInfo,
pub protected: bool, pub protected: bool,
pub reload_timezone: bool, pub reload_timezone: bool,
pub handler: fn(Value) -> super::ApiFuture, pub handler: fn(Value) -> super::ApiFuture<Body>,
} }
impl ApiMethodInfo for ApiMethod { impl<Body> ApiMethodInfo<Body> for ApiMethod<Body> {
fn description(&self) -> &'static str { fn description(&self) -> &'static str {
self.description self.description
} }
@ -76,7 +75,7 @@ impl ApiMethodInfo for ApiMethod {
self.reload_timezone self.reload_timezone
} }
fn handler(&self) -> fn(Value) -> super::ApiFuture { fn handler(&self) -> fn(Value) -> super::ApiFuture<Body> {
self.handler self.handler
} }
} }
@ -222,7 +221,20 @@ unconstrained_api_type! {String, isize, usize, i64, u64, i32, u32, i16, u16, i8,
unconstrained_api_type! {Vec<String>} unconstrained_api_type! {Vec<String>}
// Raw return types are also okay: // Raw return types are also okay:
unconstrained_api_type! {Response<Bytes>} impl<Body> ApiType for Response<Body> {
fn verify(&self) -> Result<(), Error> {
Ok(())
}
fn type_info() -> &'static TypeInfo {
const INFO: TypeInfo = TypeInfo {
name: "http::Response<>",
description: "A raw http response",
complete_fn: None,
};
&INFO
}
}
// FIXME: make const once feature(const_fn) is stable! // FIXME: make const once feature(const_fn) is stable!
pub fn get_type_info<T: ApiType>() -> &'static TypeInfo { pub fn get_type_info<T: ApiType>() -> &'static TypeInfo {

View File

@ -9,7 +9,6 @@
use std::future::Future; use std::future::Future;
use std::pin::Pin; use std::pin::Pin;
use bytes::Bytes;
use failure::Error; use failure::Error;
use http::Response; use http::Response;
@ -23,7 +22,7 @@ mod router;
pub use router::*; pub use router::*;
/// Return type of an API method. /// Return type of an API method.
pub type ApiOutput = Result<Response<Bytes>, Error>; pub type ApiOutput<Body> = Result<Response<Body>, Error>;
/// Future type of an API method. In order to support `async fn` this is a pinned box. /// Future type of an API method. In order to support `async fn` this is a pinned box.
pub type ApiFuture = Pin<Box<dyn Future<Output = ApiOutput>>>; pub type ApiFuture<Body> = Pin<Box<dyn Future<Output = ApiOutput<Body>>>>;

View File

@ -13,16 +13,16 @@ use super::ApiMethodInfo;
/// When subdirectories are supposed to be passed as a `String` parameter to methods beneath the /// When subdirectories are supposed to be passed as a `String` parameter to methods beneath the
/// current directory, a `Parameter` entry is used. Note that the parameter name is fixed at this /// current directory, a `Parameter` entry is used. Note that the parameter name is fixed at this
/// point, so all method calls beneath will receive a parameter ot that particular name. /// point, so all method calls beneath will receive a parameter ot that particular name.
pub enum SubRoute { pub enum SubRoute<Body: 'static> {
/// Call this router for any further subdirectory paths, and provide the relative path via the /// Call this router for any further subdirectory paths, and provide the relative path via the
/// given parameter. /// given parameter.
Wildcard(&'static str), Wildcard(&'static str),
/// This is used for plain subdirectories. /// This is used for plain subdirectories.
Directories(HashMap<&'static str, Router>), Directories(HashMap<&'static str, Router<Body>>),
/// Match subdirectories as the given parameter name to the underlying router. /// Match subdirectories as the given parameter name to the underlying router.
Parameter(&'static str, Box<Router>), Parameter(&'static str, Box<Router<Body>>),
} }
/// A router is a nested structure. On the one hand it contains HTTP method entries (`GET`, `PUT`, /// A router is a nested structure. On the one hand it contains HTTP method entries (`GET`, `PUT`,
@ -30,24 +30,27 @@ pub enum SubRoute {
/// sub directories as parameters, so the nesting uses a `SubRoute` `enum` representing which of /// sub directories as parameters, so the nesting uses a `SubRoute` `enum` representing which of
/// the two is the case. /// the two is the case.
#[derive(Default)] #[derive(Default)]
pub struct Router { pub struct Router<Body: 'static> {
/// The `GET` http method. /// The `GET` http method.
pub get: Option<&'static dyn ApiMethodInfo>, pub get: Option<&'static dyn ApiMethodInfo<Body>>,
/// The `PUT` http method. /// The `PUT` http method.
pub put: Option<&'static dyn ApiMethodInfo>, pub put: Option<&'static dyn ApiMethodInfo<Body>>,
/// The `POST` http method. /// The `POST` http method.
pub post: Option<&'static dyn ApiMethodInfo>, pub post: Option<&'static dyn ApiMethodInfo<Body>>,
/// The `DELETE` http method. /// The `DELETE` http method.
pub delete: Option<&'static dyn ApiMethodInfo>, pub delete: Option<&'static dyn ApiMethodInfo<Body>>,
/// Specifies the behavior of sub directories. See [`SubRoute`]. /// Specifies the behavior of sub directories. See [`SubRoute`].
pub subroute: Option<SubRoute>, pub subroute: Option<SubRoute<Body>>,
} }
impl Router { impl<Body> Router<Body>
where
Self: Default,
{
/// Create a new empty router. /// Create a new empty router.
pub fn new() -> Self { pub fn new() -> Self {
Self::default() Self::default()
@ -110,7 +113,7 @@ impl Router {
/// Builder method to provide a `GET` method info. /// Builder method to provide a `GET` method info.
pub fn get<I>(mut self, method: &'static I) -> Self pub fn get<I>(mut self, method: &'static I) -> Self
where where
I: ApiMethodInfo, I: ApiMethodInfo<Body>,
{ {
self.get = Some(method); self.get = Some(method);
self self
@ -119,7 +122,7 @@ impl Router {
/// Builder method to provide a `PUT` method info. /// Builder method to provide a `PUT` method info.
pub fn put<I>(mut self, method: &'static I) -> Self pub fn put<I>(mut self, method: &'static I) -> Self
where where
I: ApiMethodInfo, I: ApiMethodInfo<Body>,
{ {
self.put = Some(method); self.put = Some(method);
self self
@ -128,7 +131,7 @@ impl Router {
/// Builder method to provide a `POST` method info. /// Builder method to provide a `POST` method info.
pub fn post<I>(mut self, method: &'static I) -> Self pub fn post<I>(mut self, method: &'static I) -> Self
where where
I: ApiMethodInfo, I: ApiMethodInfo<Body>,
{ {
self.post = Some(method); self.post = Some(method);
self self
@ -137,7 +140,7 @@ impl Router {
/// Builder method to provide a `DELETE` method info. /// Builder method to provide a `DELETE` method info.
pub fn delete<I>(mut self, method: &'static I) -> Self pub fn delete<I>(mut self, method: &'static I) -> Self
where where
I: ApiMethodInfo, I: ApiMethodInfo<Body>,
{ {
self.delete = Some(method); self.delete = Some(method);
self self
@ -147,7 +150,7 @@ impl Router {
/// ///
/// This is supposed to be used statically (via `lazy_static!), therefore we panic if we /// This is supposed to be used statically (via `lazy_static!), therefore we panic if we
/// already have a subdir entry! /// already have a subdir entry!
pub fn parameter_subdir(mut self, parameter_name: &'static str, router: Router) -> Self { pub fn parameter_subdir(mut self, parameter_name: &'static str, router: Router<Body>) -> Self {
if self.subroute.is_some() { if self.subroute.is_some() {
panic!("match_parameter can only be used once and without sub directories"); panic!("match_parameter can only be used once and without sub directories");
} }
@ -159,7 +162,7 @@ impl Router {
/// ///
/// This is supposed to be used statically (via `lazy_static!), therefore we panic if we /// This is supposed to be used statically (via `lazy_static!), therefore we panic if we
/// already have a subdir entry! /// already have a subdir entry!
pub fn subdir(mut self, dir_name: &'static str, router: Router) -> Self { pub fn subdir(mut self, dir_name: &'static str, router: Router<Body>) -> Self {
let previous = match self.subroute { let previous = match self.subroute {
Some(SubRoute::Directories(ref mut map)) => map.insert(dir_name, router), Some(SubRoute::Directories(ref mut map)) => map.insert(dir_name, router),
None => { None => {

View File

@ -2,13 +2,15 @@
use std::pin::Pin; use std::pin::Pin;
use bytes::Bytes;
use proxmox_api::Router; use proxmox_api::Router;
#[test] #[test]
fn basic() { fn basic() {
let info: &proxmox_api::ApiMethod = &methods::GET_PEOPLE; let info: &proxmox_api::ApiMethod<Bytes> = &methods::GET_PEOPLE;
let get_subpath: &proxmox_api::ApiMethod = &methods::GET_SUBPATH; let get_subpath: &proxmox_api::ApiMethod<Bytes> = &methods::GET_SUBPATH;
let router = Router::new() let router: Router<Bytes> = Router::new()
.subdir( .subdir(
"people", "people",
Router::new().parameter_subdir("person", Router::new().get(info)), Router::new().parameter_subdir("person", Router::new().get(info)),
@ -35,7 +37,7 @@ fn basic() {
} }
fn check_with_matched_params( fn check_with_matched_params(
router: &Router, router: &Router<Bytes>,
path: &str, path: &str,
param_name: &str, param_name: &str,
param_value: &str, param_value: &str,
@ -84,6 +86,7 @@ fn check_with_matched_params(
#[cfg(test)] #[cfg(test)]
mod methods { mod methods {
use bytes::Bytes;
use failure::{bail, Error}; use failure::{bail, Error};
use http::Response; use http::Response;
use lazy_static::lazy_static; use lazy_static::lazy_static;
@ -94,14 +97,14 @@ mod methods {
get_type_info, ApiFuture, ApiMethod, ApiOutput, ApiType, Parameter, TypeInfo, get_type_info, ApiFuture, ApiMethod, ApiOutput, ApiType, Parameter, TypeInfo,
}; };
pub async fn get_people(value: Value) -> ApiOutput { pub async fn get_people(value: Value) -> ApiOutput<Bytes> {
Ok(Response::builder() Ok(Response::builder()
.status(200) .status(200)
.header("content-type", "application/json") .header("content-type", "application/json")
.body(value["person"].as_str().unwrap().into())?) .body(value["person"].as_str().unwrap().into())?)
} }
pub async fn get_subpath(value: Value) -> ApiOutput { pub async fn get_subpath(value: Value) -> ApiOutput<Bytes> {
Ok(Response::builder() Ok(Response::builder()
.status(200) .status(200)
.header("content-type", "application/json") .header("content-type", "application/json")
@ -116,14 +119,14 @@ mod methods {
type_info: get_type_info::<String>(), type_info: get_type_info::<String>(),
}] }]
}; };
pub static ref GET_PEOPLE: ApiMethod = { pub static ref GET_PEOPLE: ApiMethod<Bytes> = {
ApiMethod { ApiMethod {
description: "get some people", description: "get some people",
parameters: &GET_PEOPLE_PARAMS, parameters: &GET_PEOPLE_PARAMS,
return_type: get_type_info::<String>(), return_type: get_type_info::<String>(),
protected: false, protected: false,
reload_timezone: false, reload_timezone: false,
handler: |value: Value| -> ApiFuture { Box::pin(get_people(value)) }, handler: |value: Value| -> ApiFuture<Bytes> { Box::pin(get_people(value)) },
} }
}; };
static ref GET_SUBPATH_PARAMS: Vec<Parameter> = { static ref GET_SUBPATH_PARAMS: Vec<Parameter> = {
@ -133,14 +136,14 @@ mod methods {
type_info: get_type_info::<String>(), type_info: get_type_info::<String>(),
}] }]
}; };
pub static ref GET_SUBPATH: ApiMethod = { pub static ref GET_SUBPATH: ApiMethod<Bytes> = {
ApiMethod { ApiMethod {
description: "get the 'subpath' parameter returned back", description: "get the 'subpath' parameter returned back",
parameters: &GET_SUBPATH_PARAMS, parameters: &GET_SUBPATH_PARAMS,
return_type: get_type_info::<String>(), return_type: get_type_info::<String>(),
protected: false, protected: false,
reload_timezone: false, reload_timezone: false,
handler: |value: Value| -> ApiFuture { Box::pin(get_subpath(value)) }, handler: |value: Value| -> ApiFuture<Bytes> { Box::pin(get_subpath(value)) },
} }
}; };
} }