From 60df564f739ba495c11fc3fae7f0823b8e21576c Mon Sep 17 00:00:00 2001 From: Wolfgang Bumiller Date: Sat, 8 Jun 2019 13:11:24 +0200 Subject: [PATCH] api: move router to router.rs We'll have a separate router for the command line, so the http router won't live in the root module. It is still exported at the root level, though, via proxmox::api::Router. Also move ApiType into api_type.rs, makes more sense. Signed-off-by: Wolfgang Bumiller --- proxmox-api/src/api_type.rs | 154 ++++++++++++++++++ proxmox-api/src/lib.rs | 303 +----------------------------------- proxmox-api/src/router.rs | 156 +++++++++++++++++++ 3 files changed, 313 insertions(+), 300 deletions(-) create mode 100644 proxmox-api/src/router.rs diff --git a/proxmox-api/src/api_type.rs b/proxmox-api/src/api_type.rs index 67103a38..58d773f5 100644 --- a/proxmox-api/src/api_type.rs +++ b/proxmox-api/src/api_type.rs @@ -1,5 +1,11 @@ //! This contains traits used to implement methods to be added to the `Router`. +use std::cell::Cell; +use std::sync::Once; + +use bytes::Bytes; +use failure::Error; +use http::Response; use serde_json::Value; /// Method entries in a `Router` are actually just `&dyn ApiMethodInfo` trait objects. @@ -74,3 +80,151 @@ impl ApiMethodInfo for ApiMethod { self.handler } } + +/// We're supposed to only use types in the API which implement `ApiType`, which forces types ot +/// have a `verify` method. The idea is that all parameters used in the API are documented +/// somewhere with their formats and limits, which are checked when entering and leaving API entry +/// points. +/// +/// Any API type is also required to implement `Serialize` and `DeserializeOwned`, since they're +/// read out of json `Value` types. +/// +/// While this is very useful for structural types, we sometimes to want to be able to pass a +/// simple unconstrainted type like a `String` with no restrictions, so most basic types implement +/// `ApiType` as well. +// +// FIXME: I've actually moved most of this into the types in `api_type.rs` now, so this is +// probably unused at this point? +// `verify` should be moved to `TypeInfo` (for the type related verifier), and `Parameter` should +// get an additional verify method for constraints added by *methods*. +// +// We actually have 2 layers of validation: +// When entering the API: The type validation +// obviously a `String` should also be a string in the json object... +// This does not happen when we call the method from rust-code as we have no json layer +// there. +// When entering the function: The input validation +// if the function says `Integer`, the type itself has no validation other than that it has +// to be an integer type, but the function may still say `minimum: 5, maximum: 10`. +// This should also happen for direct calls from within rust, the `#[api]` macro can take +// care of this. +// When leaving the function: The output validation +// Yep, we need to add this ;-) +pub trait ApiType { + /// API types need to provide a `TypeInfo`, providing details about the underlying type. + fn type_info() -> &'static TypeInfo; + + /// Additionally, ApiTypes must provide a way to verify their constraints! + fn verify(&self) -> Result<(), Error>; + + /// This is a workaround for when we cannot name the type but have an object available we can + /// call a method on. (We cannot call associated methods on objects without being able to write + /// out the type, and rust has some restrictions as to what types are available.) + // eg. nested generics: + // fn foo() { + // fn bar(x: &T) { + // cannot use T::method() here, but can use x.method() + // (compile error "can't use generic parameter of outer function", + // and yes, that's a stupid restriction as it is still completely static...) + // } + // } + fn get_type_info(&self) -> &'static TypeInfo { + Self::type_info() + } +} + +/// Option types are supposed to wrap their underlying types with an `optional:` text in their +/// description. +// BUT it requires some anti-static magic. And while this looks like the result of lazy_static!, +// it's not exactly the same, lazy_static! here does not actually work as it'll curiously produce +// the same error as we pointed out above in the `get_type_info` method (as it does a lot more +// extra stuff we don't need)... +impl ApiType for Option { + fn verify(&self) -> Result<(), Error> { + if let Some(inner) = self { + inner.verify()? + } + Ok(()) + } + + fn type_info() -> &'static TypeInfo { + struct Data { + info: Cell>, + once: Once, + name: Cell>, + description: Cell>, + } + unsafe impl Sync for Data {} + static DATA: Data = Data { + info: Cell::new(None), + once: Once::new(), + name: Cell::new(None), + description: Cell::new(None), + }; + DATA.once.call_once(|| { + let info = T::type_info(); + DATA.name.set(Some(format!("optional: {}", info.name))); + DATA.info.set(Some(TypeInfo { + name: unsafe { (*DATA.name.as_ptr()).as_ref().unwrap().as_str() }, + description: unsafe { (*DATA.description.as_ptr()).as_ref().unwrap().as_str() }, + complete_fn: None, + })); + }); + unsafe { (*DATA.info.as_ptr()).as_ref().unwrap() } + } +} + +/// Any `Result` of course gets the same info as `T`, since this only means that it can +/// fail... +impl ApiType for Result { + fn verify(&self) -> Result<(), Error> { + if let Ok(inner) = self { + inner.verify()? + } + Ok(()) + } + + fn type_info() -> &'static TypeInfo { + ::type_info() + } +} + +/// This is not supposed to be used, but can be if needed. This will provide an empty `ApiType` +/// declaration with no description and no verifier. +/// +/// This rarely makes sense, but sometimes a `string` is just a `string`. +#[macro_export] +macro_rules! unconstrained_api_type { + ($type:ty $(, $more:ty)*) => { + impl $crate::ApiType for $type { + fn verify(&self) -> Result<(), ::failure::Error> { + Ok(()) + } + + fn type_info() -> &'static $crate::TypeInfo { + const INFO: $crate::TypeInfo = $crate::TypeInfo { + name: stringify!($type), + description: stringify!($type), + complete_fn: None, + }; + &INFO + } + } + + $crate::unconstrained_api_type!{$($more),*} + }; + () => {}; +} + +unconstrained_api_type! {Value} // basically our API's "any" type +unconstrained_api_type! {&str} +unconstrained_api_type! {String, isize, usize, i64, u64, i32, u32, i16, u16, i8, u8, f64, f32} +unconstrained_api_type! {Vec} + +// Raw return types are also okay: +unconstrained_api_type! {Response} + +// FIXME: make const once feature(const_fn) is stable! +pub fn get_type_info() -> &'static TypeInfo { + T::type_info() +} diff --git a/proxmox-api/src/lib.rs b/proxmox-api/src/lib.rs index 5cb6f340..cb9e57b0 100644 --- a/proxmox-api/src/lib.rs +++ b/proxmox-api/src/lib.rs @@ -6,16 +6,12 @@ //! Note that you'll rarely need the [`Router`] type itself, as you'll most likely be creating them //! with the `router` macro provided by the `proxmox-api-macro` crate. -use std::cell::Cell; -use std::collections::HashMap; use std::future::Future; use std::pin::Pin; -use std::sync::Once; use bytes::Bytes; use failure::Error; use http::Response; -use serde_json::Value; mod api_output; pub use api_output::*; @@ -23,304 +19,11 @@ pub use api_output::*; mod api_type; pub use api_type::*; +mod router; +pub use router::*; + /// Return type of an API method. pub type ApiOutput = Result, Error>; /// Future type of an API method. In order to support `async fn` this is a pinned box. pub type ApiFuture = Pin>>; - -/// This enum specifies what to do when a subdirectory is requested from the current router. -/// -/// For plain subdirectories a `Directories` entry is used. -/// -/// 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 -/// point, so all method calls beneath will receive a parameter ot that particular name. -pub enum SubRoute { - /// This is used for plain subdirectories. - Directories(HashMap<&'static str, Router>), - - /// Match subdirectories as the given parameter name to the underlying router. - Parameter(&'static str, Box), -} - -/// A router is a nested structure. On the one hand it contains HTTP method entries (`GET`, `PUT`, -/// ...), and on the other hand it contains sub directories. In some cases we want to match those -/// sub directories as parameters, so the nesting uses a `SubRoute` `enum` representing which of -/// the two is the case. -#[derive(Default)] -pub struct Router { - /// The `GET` http method. - pub get: Option<&'static dyn ApiMethodInfo>, - - /// The `PUT` http method. - pub put: Option<&'static dyn ApiMethodInfo>, - - /// The `POST` http method. - pub post: Option<&'static dyn ApiMethodInfo>, - - /// The `DELETE` http method. - pub delete: Option<&'static dyn ApiMethodInfo>, - - /// Specifies the behavior of sub directories. See [`SubRoute`]. - pub subroute: Option, -} - -impl Router { - /// Create a new empty router. - pub fn new() -> Self { - Self::default() - } - - /// Lookup a path in the router. Note that this returns a tuple: the router we ended up on - /// (providing methods and subdirectories available for the given path), and optionally a json - /// value containing all the matched parameters ([`SubRoute::Parameter`] subdirectories). - pub fn lookup>(&self, path: T) -> Option<(&Self, Option)> { - self.lookup_do(path.as_ref()) - } - - // The actual implementation taking the parameter as &str - fn lookup_do(&self, path: &str) -> Option<(&Self, Option)> { - let mut matched_params = None; - - let mut this = self; - for component in path.split('/') { - if component.is_empty() { - // `foo//bar` or the first `/` in `/foo` - continue; - } - this = match &this.subroute { - Some(SubRoute::Directories(subdirs)) => subdirs.get(component)?, - Some(SubRoute::Parameter(param_name, router)) => { - let previous = matched_params - .get_or_insert_with(serde_json::Map::new) - .insert(param_name.to_string(), Value::String(component.to_string())); - if previous.is_some() { - panic!("API contains the same parameter twice in route"); - } - &*router - } - None => return None, - }; - } - - Some((this, matched_params.map(Value::Object))) - } - - /// Builder method to provide a `GET` method info. - pub fn get(mut self, method: &'static I) -> Self - where - I: ApiMethodInfo, - { - self.get = Some(method); - self - } - - /// Builder method to provide a `PUT` method info. - pub fn put(mut self, method: &'static I) -> Self - where - I: ApiMethodInfo, - { - self.put = Some(method); - self - } - - /// Builder method to provide a `POST` method info. - pub fn post(mut self, method: &'static I) -> Self - where - I: ApiMethodInfo, - { - self.post = Some(method); - self - } - - /// Builder method to provide a `DELETE` method info. - pub fn delete(mut self, method: &'static I) -> Self - where - I: ApiMethodInfo, - { - self.delete = Some(method); - self - } - - /// Builder method to make this router match the next subdirectory into a parameter. - /// - /// This is supposed to be used statically (via `lazy_static!), therefore we panic if we - /// already have a subdir entry! - pub fn parameter_subdir(mut self, parameter_name: &'static str, router: Router) -> Self { - if self.subroute.is_some() { - panic!("match_parameter can only be used once and without sub directories"); - } - self.subroute = Some(SubRoute::Parameter(parameter_name, Box::new(router))); - self - } - - /// Builder method to add a regular directory entro to this router. - /// - /// This is supposed to be used statically (via `lazy_static!), therefore we panic if we - /// already have a subdir entry! - pub fn subdir(mut self, dir_name: &'static str, router: Router) -> Self { - let previous = match self.subroute { - Some(SubRoute::Directories(ref mut map)) => map.insert(dir_name, router), - None => { - let mut map = HashMap::new(); - map.insert(dir_name, router); - self.subroute = Some(SubRoute::Directories(map)); - None - } - _ => panic!("subdir and match_parameter are mutually exclusive"), - }; - if previous.is_some() { - panic!("duplicate subdirectory: {}", dir_name); - } - self - } -} - -/// We're supposed to only use types in the API which implement `ApiType`, which forces types ot -/// have a `verify` method. The idea is that all parameters used in the API are documented -/// somewhere with their formats and limits, which are checked when entering and leaving API entry -/// points. -/// -/// Any API type is also required to implement `Serialize` and `DeserializeOwned`, since they're -/// read out of json `Value` types. -/// -/// While this is very useful for structural types, we sometimes to want to be able to pass a -/// simple unconstrainted type like a `String` with no restrictions, so most basic types implement -/// `ApiType` as well. -// -// FIXME: I've actually moved most of this into the types in `api_type.rs` now, so this is -// probably unused at this point? -// `verify` should be moved to `TypeInfo` (for the type related verifier), and `Parameter` should -// get an additional verify method for constraints added by *methods*. -// -// We actually have 2 layers of validation: -// When entering the API: The type validation -// obviously a `String` should also be a string in the json object... -// This does not happen when we call the method from rust-code as we have no json layer -// there. -// When entering the function: The input validation -// if the function says `Integer`, the type itself has no validation other than that it has -// to be an integer type, but the function may still say `minimum: 5, maximum: 10`. -// This should also happen for direct calls from within rust, the `#[api]` macro can take -// care of this. -// When leaving the function: The output validation -// Yep, we need to add this ;-) -pub trait ApiType { - /// API types need to provide a `TypeInfo`, providing details about the underlying type. - fn type_info() -> &'static TypeInfo; - - /// Additionally, ApiTypes must provide a way to verify their constraints! - fn verify(&self) -> Result<(), Error>; - - /// This is a workaround for when we cannot name the type but have an object available we can - /// call a method on. (We cannot call associated methods on objects without being able to write - /// out the type, and rust has some restrictions as to what types are available.) - // eg. nested generics: - // fn foo() { - // fn bar(x: &T) { - // cannot use T::method() here, but can use x.method() - // (compile error "can't use generic parameter of outer function", - // and yes, that's a stupid restriction as it is still completely static...) - // } - // } - fn get_type_info(&self) -> &'static TypeInfo { - Self::type_info() - } -} - -/// Option types are supposed to wrap their underlying types with an `optional:` text in their -/// description. -// BUT it requires some anti-static magic. And while this looks like the result of lazy_static!, -// it's not exactly the same, lazy_static! here does not actually work as it'll curiously produce -// the same error as we pointed out above in the `get_type_info` method (as it does a lot more -// extra stuff we don't need)... -impl ApiType for Option { - fn verify(&self) -> Result<(), Error> { - if let Some(inner) = self { - inner.verify()? - } - Ok(()) - } - - fn type_info() -> &'static TypeInfo { - struct Data { - info: Cell>, - once: Once, - name: Cell>, - description: Cell>, - } - unsafe impl Sync for Data {} - static DATA: Data = Data { - info: Cell::new(None), - once: Once::new(), - name: Cell::new(None), - description: Cell::new(None), - }; - DATA.once.call_once(|| { - let info = T::type_info(); - DATA.name.set(Some(format!("optional: {}", info.name))); - DATA.info.set(Some(TypeInfo { - name: unsafe { (*DATA.name.as_ptr()).as_ref().unwrap().as_str() }, - description: unsafe { (*DATA.description.as_ptr()).as_ref().unwrap().as_str() }, - complete_fn: None, - })); - }); - unsafe { (*DATA.info.as_ptr()).as_ref().unwrap() } - } -} - -/// Any `Result` of course gets the same info as `T`, since this only means that it can -/// fail... -impl ApiType for Result { - fn verify(&self) -> Result<(), Error> { - if let Ok(inner) = self { - inner.verify()? - } - Ok(()) - } - - fn type_info() -> &'static TypeInfo { - ::type_info() - } -} - -/// This is not supposed to be used, but can be if needed. This will provide an empty `ApiType` -/// declaration with no description and no verifier. -/// -/// This rarely makes sense, but sometimes a `string` is just a `string`. -#[macro_export] -macro_rules! unconstrained_api_type { - ($type:ty $(, $more:ty)*) => { - impl $crate::ApiType for $type { - fn verify(&self) -> Result<(), ::failure::Error> { - Ok(()) - } - - fn type_info() -> &'static $crate::TypeInfo { - const INFO: $crate::TypeInfo = $crate::TypeInfo { - name: stringify!($type), - description: stringify!($type), - complete_fn: None, - }; - &INFO - } - } - - $crate::unconstrained_api_type!{$($more),*} - }; - () => {}; -} - -unconstrained_api_type! {Value} // basically our API's "any" type -unconstrained_api_type! {&str} -unconstrained_api_type! {String, isize, usize, i64, u64, i32, u32, i16, u16, i8, u8, f64, f32} -unconstrained_api_type! {Vec} - -// Raw return types are also okay: -unconstrained_api_type! {Response} - -// FIXME: make const once feature(const_fn) is stable! -pub fn get_type_info() -> &'static TypeInfo { - T::type_info() -} diff --git a/proxmox-api/src/router.rs b/proxmox-api/src/router.rs new file mode 100644 index 00000000..c409dcdd --- /dev/null +++ b/proxmox-api/src/router.rs @@ -0,0 +1,156 @@ +//! This module provides a router used for http servers. + +use std::collections::HashMap; + +use serde_json::Value; + +use super::ApiMethodInfo; + +/// This enum specifies what to do when a subdirectory is requested from the current router. +/// +/// For plain subdirectories a `Directories` entry is used. +/// +/// 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 +/// point, so all method calls beneath will receive a parameter ot that particular name. +pub enum SubRoute { + /// This is used for plain subdirectories. + Directories(HashMap<&'static str, Router>), + + /// Match subdirectories as the given parameter name to the underlying router. + Parameter(&'static str, Box), +} + +/// A router is a nested structure. On the one hand it contains HTTP method entries (`GET`, `PUT`, +/// ...), and on the other hand it contains sub directories. In some cases we want to match those +/// sub directories as parameters, so the nesting uses a `SubRoute` `enum` representing which of +/// the two is the case. +#[derive(Default)] +pub struct Router { + /// The `GET` http method. + pub get: Option<&'static dyn ApiMethodInfo>, + + /// The `PUT` http method. + pub put: Option<&'static dyn ApiMethodInfo>, + + /// The `POST` http method. + pub post: Option<&'static dyn ApiMethodInfo>, + + /// The `DELETE` http method. + pub delete: Option<&'static dyn ApiMethodInfo>, + + /// Specifies the behavior of sub directories. See [`SubRoute`]. + pub subroute: Option, +} + +impl Router { + /// Create a new empty router. + pub fn new() -> Self { + Self::default() + } + + /// Lookup a path in the router. Note that this returns a tuple: the router we ended up on + /// (providing methods and subdirectories available for the given path), and optionally a json + /// value containing all the matched parameters ([`SubRoute::Parameter`] subdirectories). + pub fn lookup>(&self, path: T) -> Option<(&Self, Option)> { + self.lookup_do(path.as_ref()) + } + + // The actual implementation taking the parameter as &str + fn lookup_do(&self, path: &str) -> Option<(&Self, Option)> { + let mut matched_params = None; + + let mut this = self; + for component in path.split('/') { + if component.is_empty() { + // `foo//bar` or the first `/` in `/foo` + continue; + } + this = match &this.subroute { + Some(SubRoute::Directories(subdirs)) => subdirs.get(component)?, + Some(SubRoute::Parameter(param_name, router)) => { + let previous = matched_params + .get_or_insert_with(serde_json::Map::new) + .insert(param_name.to_string(), Value::String(component.to_string())); + if previous.is_some() { + panic!("API contains the same parameter twice in route"); + } + &*router + } + None => return None, + }; + } + + Some((this, matched_params.map(Value::Object))) + } + + /// Builder method to provide a `GET` method info. + pub fn get(mut self, method: &'static I) -> Self + where + I: ApiMethodInfo, + { + self.get = Some(method); + self + } + + /// Builder method to provide a `PUT` method info. + pub fn put(mut self, method: &'static I) -> Self + where + I: ApiMethodInfo, + { + self.put = Some(method); + self + } + + /// Builder method to provide a `POST` method info. + pub fn post(mut self, method: &'static I) -> Self + where + I: ApiMethodInfo, + { + self.post = Some(method); + self + } + + /// Builder method to provide a `DELETE` method info. + pub fn delete(mut self, method: &'static I) -> Self + where + I: ApiMethodInfo, + { + self.delete = Some(method); + self + } + + /// Builder method to make this router match the next subdirectory into a parameter. + /// + /// This is supposed to be used statically (via `lazy_static!), therefore we panic if we + /// already have a subdir entry! + pub fn parameter_subdir(mut self, parameter_name: &'static str, router: Router) -> Self { + if self.subroute.is_some() { + panic!("match_parameter can only be used once and without sub directories"); + } + self.subroute = Some(SubRoute::Parameter(parameter_name, Box::new(router))); + self + } + + /// Builder method to add a regular directory entro to this router. + /// + /// This is supposed to be used statically (via `lazy_static!), therefore we panic if we + /// already have a subdir entry! + pub fn subdir(mut self, dir_name: &'static str, router: Router) -> Self { + let previous = match self.subroute { + Some(SubRoute::Directories(ref mut map)) => map.insert(dir_name, router), + None => { + let mut map = HashMap::new(); + map.insert(dir_name, router); + self.subroute = Some(SubRoute::Directories(map)); + None + } + _ => panic!("subdir and match_parameter are mutually exclusive"), + }; + if previous.is_some() { + panic!("duplicate subdirectory: {}", dir_name); + } + self + } +} +