From df1d11e42dafef4961d3137ae0a79c252017c230 Mon Sep 17 00:00:00 2001 From: Dominic Date: Sun, 29 Sep 2019 21:15:22 +0200 Subject: [PATCH 01/20] start adding openapi support --- Cargo.lock | 44 ++++++++++ Cargo.toml | 6 ++ src/lib.rs | 3 + src/openapi.rs | 226 +++++++++++++++++++++++++++++++++++++++++++++++++ src/routing.rs | 34 ++++++++ 5 files changed, 313 insertions(+) create mode 100644 src/openapi.rs diff --git a/Cargo.lock b/Cargo.lock index 29f0e36..c4f12c1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -194,6 +194,11 @@ dependencies = [ "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "dtoa" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "either" version = "1.5.3" @@ -332,9 +337,11 @@ dependencies = [ "gotham 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "gotham_derive 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "hyper 0.12.35 (registry+https://github.com/rust-lang/crates.io-index)", + "indexmap 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", "log4rs 0.8.3 (registry+https://github.com/rust-lang/crates.io-index)", "mime 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)", + "openapiv3 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.101 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 1.0.40 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -439,6 +446,9 @@ dependencies = [ name = "indexmap" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "serde 1.0.101 (registry+https://github.com/rust-lang/crates.io-index)", +] [[package]] name = "iovec" @@ -625,6 +635,17 @@ dependencies = [ "libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "openapiv3" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "indexmap 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.101 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.40 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_yaml 0.8.9 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "owning_ref" version = "0.4.0" @@ -999,6 +1020,17 @@ dependencies = [ "serde 1.0.101 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "serde_yaml" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "dtoa 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)", + "linked-hash-map 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.101 (registry+https://github.com/rust-lang/crates.io-index)", + "yaml-rust 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "skeptic" version = "0.13.4" @@ -1447,6 +1479,14 @@ dependencies = [ "winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "yaml-rust" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "linked-hash-map 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + [metadata] "checksum aho-corasick 0.7.6 (registry+https://github.com/rust-lang/crates.io-index)" = "58fb5e95d83b38284460a5fda7d6470aa0b8844d283a0b614b8535e880800d2d" "checksum arc-swap 0.3.11 (registry+https://github.com/rust-lang/crates.io-index)" = "bc4662175ead9cd84451d5c35070517777949a2ed84551764129cedb88384841" @@ -1472,6 +1512,7 @@ dependencies = [ "checksum crossbeam-epoch 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "fedcd6772e37f3da2a9af9bf12ebe046c0dfe657992377b4df982a2b54cd37a9" "checksum crossbeam-queue 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7c979cd6cfe72335896575c6b5688da489e420d36a27a0b9eb0c73db574b4a4b" "checksum crossbeam-utils 0.6.6 (registry+https://github.com/rust-lang/crates.io-index)" = "04973fa96e96579258a5091af6003abde64af786b860f18622b82e026cca60e6" +"checksum dtoa 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "ea57b42383d091c85abcc2706240b94ab2a8fa1fc81c10ff23c4de06e2a90b5e" "checksum either 1.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "bb1f6b1ce1c140482ea30ddd3335fc0024ac7ee112895426e0a629a6c20adfe3" "checksum error-chain 0.12.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3ab49e9dcb602294bc42f9a7dfc9bc6e936fca4418ea300dbfb84fe16de0b7d9" "checksum failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "795bd83d3abeb9220f257e597aa0080a508b27533824adf336529648f6abf7e2" @@ -1518,6 +1559,7 @@ dependencies = [ "checksum num-integer 0.1.41 (registry+https://github.com/rust-lang/crates.io-index)" = "b85e541ef8255f6cf42bbfe4ef361305c6c135d10919ecc26126c4e5ae94bc09" "checksum num-traits 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "6ba9a427cfca2be13aa6f6403b0b7e7368fe982bfa16fccc450ce74c46cd9b32" "checksum num_cpus 1.10.1 (registry+https://github.com/rust-lang/crates.io-index)" = "bcef43580c035376c0705c42792c294b66974abbfd2789b511784023f71f3273" +"checksum openapiv3 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c8776c7d6a58a03d30ba278adfd54d923eb0a24e26cbf39d2b97648306935d65" "checksum owning_ref 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "49a4b8ea2179e6a2e27411d3bca09ca6dd630821cf6894c6c7c8467a8ee7ef13" "checksum parking_lot 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "ab41b4aed082705d1056416ae4468b6ea99d52599ecf3169b00088d43113e337" "checksum parking_lot_core 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "94c8c7923936b28d546dfd14d4472eaf34c99b14e1c973a32b3e6d4eb04298c9" @@ -1562,6 +1604,7 @@ dependencies = [ "checksum serde 1.0.101 (registry+https://github.com/rust-lang/crates.io-index)" = "9796c9b7ba2ffe7a9ce53c2287dfc48080f4b2b362fcc245a259b3a7201119dd" "checksum serde_derive 1.0.101 (registry+https://github.com/rust-lang/crates.io-index)" = "4b133a43a1ecd55d4086bd5b4dc6c1751c68b1bfbeba7a5040442022c7e7c02e" "checksum serde_json 1.0.40 (registry+https://github.com/rust-lang/crates.io-index)" = "051c49229f282f7c6f3813f8286cc1e3323e8051823fce42c7ea80fe13521704" +"checksum serde_yaml 0.8.9 (registry+https://github.com/rust-lang/crates.io-index)" = "38b08a9a90e5260fe01c6480ec7c811606df6d3a660415808c3c3fa8ed95b582" "checksum skeptic 0.13.4 (registry+https://github.com/rust-lang/crates.io-index)" = "d6fb8ed853fdc19ce09752d63f3a2e5b5158aeb261520cd75eb618bd60305165" "checksum slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" "checksum smallvec 0.6.10 (registry+https://github.com/rust-lang/crates.io-index)" = "ab606a9c5e214920bb66c458cd7be8ef094f813f20fe77a54cc7dbfff220d4b7" @@ -1611,3 +1654,4 @@ dependencies = [ "checksum winapi-util 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7168bab6e1daee33b4557efd0e95d5ca70a03706d39fa5f3fe7a236f584b03c9" "checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" "checksum ws2_32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e" +"checksum yaml-rust 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "65923dd1784f44da1d2c3dbbc5e822045628c590ba72123e1c73d3c230c4434d" diff --git a/Cargo.toml b/Cargo.toml index f8071bf..e2fc8d4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,9 @@ futures = "0.1" gotham = "0.4" gotham_derive = "0.4" hyper = "0.12" +indexmap = { version = "1.0", optional = true } mime = "0.3" +openapiv3 = { version = "0.3", optional = true } serde = { version = "1", features = ["derive"] } serde_json = "1" @@ -29,3 +31,7 @@ serde_json = "1" fake = "2.2" log = "0.4" log4rs = { version = "0.8", features = ["console_appender"], default-features = false } + +[features] +default = ["openapi"] +openapi = ["indexmap", "openapiv3"] diff --git a/src/lib.rs b/src/lib.rs index 7a3cfba..e83d213 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,6 +3,9 @@ pub use hyper::StatusCode; +#[cfg(feature = "openapi")] +pub mod openapi; + mod resource; pub use resource::{ Resource, diff --git a/src/openapi.rs b/src/openapi.rs new file mode 100644 index 0000000..1adbee9 --- /dev/null +++ b/src/openapi.rs @@ -0,0 +1,226 @@ +use crate::{ + resource::*, + result::*, + routing::* +}; +use futures::future::{err, ok}; +use gotham::{ + handler::{HandlerFuture, IntoHandlerError}, + helpers::http::response::create_response, + pipeline::chain::PipelineHandleChain, + router::builder::*, + state::State +}; +use indexmap::IndexMap; +use mime::APPLICATION_JSON; +use openapiv3::{MediaType, OpenAPI, Operation, PathItem, Paths, ReferenceOr, ReferenceOr::Item, Response, Responses, StatusCode}; +use serde::de::DeserializeOwned; +use std::panic::RefUnwindSafe; + +pub struct OpenapiRouter<'a, D> +{ + route : &'a mut D, + openapi : OpenAPI +} + +impl<'a, D> OpenapiRouter<'a, D> +{ + pub fn new(route : &'a mut D, title : Title, version : Version) -> Self + { + Self { + route, + openapi: OpenAPI { + openapi: "3.0.2".to_string(), + info: openapiv3::Info { + title: title.to_string(), + description: None, + terms_of_service: None, + contact: None, + license: None, + version: version.to_string() + }, + servers: Vec::new(), + paths: Paths::new(), + components: None, + security: Vec::new(), + tags: Vec::new(), + external_docs: None + } + } + } + + /// Remove path from the OpenAPI spec, or return an empty one if not included. This is handy if you need to + /// modify the path and add it back after the modification + fn remove_path(&mut self, path : &str) -> PathItem + { + if let Some(Item(item)) = self.openapi.paths.swap_remove(path) + { + return item; + } + return PathItem { + get: None, + put: None, + post: None, + delete: None, + options: None, + head: None, + patch: None, + trace: None, + servers: Vec::new(), + parameters: Vec::new() + }; + } + + fn add_path<Path : ToString>(&mut self, path : Path, item : PathItem) + { + self.openapi.paths.insert(path.to_string(), Item(item)); + } +} + +fn handle_get_openapi(state : State, openapi : &'static OpenAPI) -> Box<HandlerFuture> +{ + let res = serde_json::to_string(openapi); + match res { + Ok(body) => { + let res = create_response(&state, hyper::StatusCode::OK, APPLICATION_JSON, body); + Box::new(ok((state, res))) + }, + Err(e) => Box::new(err((state, e.into_handler_error()))) + } +} + +macro_rules! implOpenapiRouter { + ($implType:ident) => { + + impl<'a, C, P> OpenapiRouter<'a, $implType<'a, C, P>> + where + C : PipelineHandleChain<P> + Copy + Send + Sync + 'static, + P : RefUnwindSafe + Send + Sync + 'static + { + pub fn get_openapi(&mut self, path : &str) + { + let openapi = Box::leak(Box::new(self.openapi.clone())); + self.route.get(path).to(|state| { + handle_get_openapi(state, openapi) + }); + } + } + + impl<'a, C, P> DrawResources for OpenapiRouter<'a, $implType<'a, C, P>> + where + C : PipelineHandleChain<P> + Copy + Send + Sync + 'static, + P : RefUnwindSafe + Send + Sync + 'static + { + fn resource<R : Resource, T : ToString>(&mut self, path : T) + { + R::setup((self, path.to_string())); + } + } + + impl<'a, C, P> DrawResourceRoutes for (&mut OpenapiRouter<'a, $implType<'a, C, P>>, String) + where + C : PipelineHandleChain<P> + Copy + Send + Sync + 'static, + P : RefUnwindSafe + Send + Sync + 'static + { + fn read_all<Handler, Res>(&mut self) + where + Res : ResourceResult, + Handler : ResourceReadAll<Res> + { + let path = &self.1; + let mut item = self.0.remove_path(path); + let mut content : IndexMap<String, MediaType> = IndexMap::new(); + content[&APPLICATION_JSON.to_string()] = MediaType { + schema: None, // TODO + example: None, + examples: IndexMap::new(), + encoding: IndexMap::new() + }; + let mut responses : IndexMap<StatusCode, ReferenceOr<Response>> = IndexMap::new(); + responses[&StatusCode::Code(200)] = Item(Response { + description: "OK".to_string(), + headers: IndexMap::new(), + content, + links: IndexMap::new() + }); + item.get = Some(Operation { + tags: Vec::new(), + summary: None, + description: None, + external_documentation: None, + operation_id: None, // TODO + parameters: Vec::new(), + request_body: None, + responses: Responses { + default: None, + responses + }, + deprecated: false, + security: Vec::new(), + servers: Vec::new() + }); + self.0.add_path(path, item); + + (&mut *self.0.route, self.1.to_string()).read_all::<Handler, Res>() + } + + fn read<Handler, ID, Res>(&mut self) + where + ID : DeserializeOwned + Clone + RefUnwindSafe + Send + Sync + 'static, + Res : ResourceResult, + Handler : ResourceRead<ID, Res> + { + (&mut *self.0.route, self.1.to_string()).read::<Handler, ID, Res>() + } + + fn create<Handler, Body, Res>(&mut self) + where + Body : DeserializeOwned, + Res : ResourceResult, + Handler : ResourceCreate<Body, Res> + { + (&mut *self.0.route, self.1.to_string()).create::<Handler, Body, Res>() + } + + fn update_all<Handler, Body, Res>(&mut self) + where + Body : DeserializeOwned, + Res : ResourceResult, + Handler : ResourceUpdateAll<Body, Res> + { + (&mut *self.0.route, self.1.to_string()).update_all::<Handler, Body, Res>() + } + + fn update<Handler, ID, Body, Res>(&mut self) + where + ID : DeserializeOwned + Clone + RefUnwindSafe + Send + Sync + 'static, + Body : DeserializeOwned, + Res : ResourceResult, + Handler : ResourceUpdate<ID, Body, Res> + { + (&mut *self.0.route, self.1.to_string()).update::<Handler, ID, Body, Res>() + } + + fn delete_all<Handler, Res>(&mut self) + where + Res : ResourceResult, + Handler : ResourceDeleteAll<Res> + { + (&mut *self.0.route, self.1.to_string()).delete_all::<Handler, Res>() + } + + fn delete<Handler, ID, Res>(&mut self) + where + ID : DeserializeOwned + Clone + RefUnwindSafe + Send + Sync + 'static, + Res : ResourceResult, + Handler : ResourceDelete<ID, Res> + { + (&mut *self.0.route, self.1.to_string()).delete::<Handler, ID, Res>() + } + } + + } +} + +implOpenapiRouter!(RouterBuilder); +implOpenapiRouter!(ScopeBuilder); diff --git a/src/routing.rs b/src/routing.rs index 7da5691..f4fdbd5 100644 --- a/src/routing.rs +++ b/src/routing.rs @@ -3,6 +3,9 @@ use crate::{ result::{ResourceError, ResourceResult}, StatusCode }; +#[cfg(feature = "openapi")] +use crate::openapi::OpenapiRouter; + use futures::{ future::{Future, err, ok}, stream::Stream @@ -25,6 +28,19 @@ struct PathExtractor<ID : RefUnwindSafe + Send + 'static> id : ID } +/// This trait adds the `with_openapi` method to gotham's routing. It turns the default +/// router into one that will only allow RESTful resources, but record them and generate +/// an OpenAPI specification on request. +#[cfg(feature = "openapi")] +pub trait WithOpenapi<D> +{ + fn with_openapi<F, Title, Version>(&mut self, title : Title, version : Version, block : F) + where + F : FnOnce(OpenapiRouter<D>), + Title : ToString, + Version : ToString; +} + /// This trait adds the `resource` method to gotham's routing. It allows you to register /// any RESTful `Resource` with a path. pub trait DrawResources @@ -212,6 +228,24 @@ where macro_rules! implDrawResourceRoutes { ($implType:ident) => { + + #[cfg(feature = "openapi")] + impl<'a, C, P> WithOpenapi<Self> for $implType<'a, C, P> + where + C : PipelineHandleChain<P> + Copy + Send + Sync + 'static, + P : RefUnwindSafe + Send + Sync + 'static + { + fn with_openapi<F, Title, Version>(&mut self, title : Title, version : Version, block : F) + where + F : FnOnce(OpenapiRouter<Self>), + Title : ToString, + Version : ToString + { + let router : OpenapiRouter<Self> = OpenapiRouter::new(self, title, version); + block(router); + } + } + impl<'a, C, P> DrawResources for $implType<'a, C, P> where C : PipelineHandleChain<P> + Copy + Send + Sync + 'static, From 8b0d655ebb8ba02be756748053304f3ebe11fc1f Mon Sep 17 00:00:00 2001 From: Dominic <git@msrd0.de> Date: Mon, 30 Sep 2019 17:34:48 +0200 Subject: [PATCH 02/20] registration of openapi handler --- Cargo.toml | 3 ++- src/openapi.rs | 58 +++++++++++++++++++++++++++++++++++++------------- 2 files changed, 45 insertions(+), 16 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index e2fc8d4..c798ee3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,7 @@ gotham = "0.4" gotham_derive = "0.4" hyper = "0.12" indexmap = { version = "1.0", optional = true } +log = { version = "0.4", optional = true } mime = "0.3" openapiv3 = { version = "0.3", optional = true } serde = { version = "1", features = ["derive"] } @@ -34,4 +35,4 @@ log4rs = { version = "0.8", features = ["console_appender"], default-features = [features] default = ["openapi"] -openapi = ["indexmap", "openapiv3"] +openapi = ["indexmap", "log", "openapiv3"] diff --git a/src/openapi.rs b/src/openapi.rs index 1adbee9..43e947d 100644 --- a/src/openapi.rs +++ b/src/openapi.rs @@ -3,16 +3,17 @@ use crate::{ result::*, routing::* }; -use futures::future::{err, ok}; +use futures::future::ok; use gotham::{ - handler::{HandlerFuture, IntoHandlerError}, + handler::{Handler, HandlerFuture, NewHandler}, helpers::http::response::create_response, pipeline::chain::PipelineHandleChain, router::builder::*, state::State }; use indexmap::IndexMap; -use mime::APPLICATION_JSON; +use log::error; +use mime::{APPLICATION_JSON, TEXT_PLAIN}; use openapiv3::{MediaType, OpenAPI, Operation, PathItem, Paths, ReferenceOr, ReferenceOr::Item, Response, Responses, StatusCode}; use serde::de::DeserializeOwned; use std::panic::RefUnwindSafe; @@ -77,15 +78,45 @@ impl<'a, D> OpenapiRouter<'a, D> } } -fn handle_get_openapi(state : State, openapi : &'static OpenAPI) -> Box<HandlerFuture> +#[derive(Clone)] +struct OpenapiHandler(Result<String, String>); + +// dunno what/why/whatever +impl RefUnwindSafe for OpenapiHandler {} + +impl OpenapiHandler { - let res = serde_json::to_string(openapi); - match res { - Ok(body) => { - let res = create_response(&state, hyper::StatusCode::OK, APPLICATION_JSON, body); - Box::new(ok((state, res))) - }, - Err(e) => Box::new(err((state, e.into_handler_error()))) + fn new(openapi : &OpenAPI) -> Self + { + Self(serde_json::to_string(openapi).map_err(|e| format!("{}", e))) + } +} + +impl NewHandler for OpenapiHandler +{ + type Instance = Self; + + fn new_handler(&self) -> gotham::error::Result<Self::Instance> + { + Ok(self.clone()) + } +} + +impl Handler for OpenapiHandler +{ + fn handle(self, state : State) -> Box<HandlerFuture> + { + match self.0 { + Ok(body) => { + let res = create_response(&state, hyper::StatusCode::OK, APPLICATION_JSON, body); + Box::new(ok((state, res))) + }, + Err(e) => { + error!("Unable to handle OpenAPI request due to error: {}", e); + let res = create_response(&state, hyper::StatusCode::INTERNAL_SERVER_ERROR, TEXT_PLAIN, ""); + Box::new(ok((state, res))) + } + } } } @@ -99,10 +130,7 @@ macro_rules! implOpenapiRouter { { pub fn get_openapi(&mut self, path : &str) { - let openapi = Box::leak(Box::new(self.openapi.clone())); - self.route.get(path).to(|state| { - handle_get_openapi(state, openapi) - }); + self.route.get(path).to_new_handler(OpenapiHandler::new(&self.openapi)); } } From 29af28ad8d37769a0033f46eec0e35ff42fcbb85 Mon Sep 17 00:00:00 2001 From: Dominic <git@msrd0.de> Date: Mon, 30 Sep 2019 18:18:10 +0200 Subject: [PATCH 03/20] the example compiles :party: --- examples/users.rs | 7 ++-- src/lib.rs | 4 +++ src/openapi.rs | 88 +++++++++++++++++++++++------------------------ src/routing.rs | 8 ++--- 4 files changed, 56 insertions(+), 51 deletions(-) diff --git a/examples/users.rs b/examples/users.rs index 1cfe938..fd8caac 100644 --- a/examples/users.rs +++ b/examples/users.rs @@ -125,9 +125,12 @@ fn main() .add(logging) .build() ); - + gotham::start(ADDR, build_router(chain, pipelines, |route| { - route.resource::<Users, _>("users"); + route.with_openapi("Users Example", "0.0.1", |mut route| { + route.resource::<Users, _>("users"); + route.get_openapi("openapi"); + }); })); println!("Gotham started on {} for testing", ADDR); } diff --git a/src/lib.rs b/src/lib.rs index e83d213..8bbdab3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,6 +5,8 @@ pub use hyper::StatusCode; #[cfg(feature = "openapi")] pub mod openapi; +#[cfg(feature = "openapi")] +pub use openapi::{GetOpenapi, OpenapiRouter}; mod resource; pub use resource::{ @@ -23,3 +25,5 @@ pub use result::{ResourceResult, Success}; mod routing; pub use routing::{DrawResources, DrawResourceRoutes}; +#[cfg(feature = "openapi")] +pub use routing::WithOpenapi; diff --git a/src/openapi.rs b/src/openapi.rs index 43e947d..d5aff5f 100644 --- a/src/openapi.rs +++ b/src/openapi.rs @@ -18,43 +18,36 @@ use openapiv3::{MediaType, OpenAPI, Operation, PathItem, Paths, ReferenceOr, Ref use serde::de::DeserializeOwned; use std::panic::RefUnwindSafe; -pub struct OpenapiRouter<'a, D> -{ - route : &'a mut D, - openapi : OpenAPI -} +pub struct OpenapiRouter(OpenAPI); -impl<'a, D> OpenapiRouter<'a, D> +impl OpenapiRouter { - pub fn new<Title : ToString, Version : ToString>(route : &'a mut D, title : Title, version : Version) -> Self + pub fn new<Title : ToString, Version : ToString>(title : Title, version : Version) -> Self { - Self { - route, - openapi: OpenAPI { - openapi: "3.0.2".to_string(), - info: openapiv3::Info { - title: title.to_string(), - description: None, - terms_of_service: None, - contact: None, - license: None, - version: version.to_string() - }, - servers: Vec::new(), - paths: Paths::new(), - components: None, - security: Vec::new(), - tags: Vec::new(), - external_docs: None - } - } + Self(OpenAPI { + openapi: "3.0.2".to_string(), + info: openapiv3::Info { + title: title.to_string(), + description: None, + terms_of_service: None, + contact: None, + license: None, + version: version.to_string() + }, + servers: Vec::new(), + paths: Paths::new(), + components: None, + security: Vec::new(), + tags: Vec::new(), + external_docs: None + }) } /// Remove path from the OpenAPI spec, or return an empty one if not included. This is handy if you need to /// modify the path and add it back after the modification fn remove_path(&mut self, path : &str) -> PathItem { - if let Some(Item(item)) = self.openapi.paths.swap_remove(path) + if let Some(Item(item)) = self.0.paths.swap_remove(path) { return item; } @@ -74,7 +67,7 @@ impl<'a, D> OpenapiRouter<'a, D> fn add_path<Path : ToString>(&mut self, path : Path, item : PathItem) { - self.openapi.paths.insert(path.to_string(), Item(item)); + self.0.paths.insert(path.to_string(), Item(item)); } } @@ -86,9 +79,9 @@ impl RefUnwindSafe for OpenapiHandler {} impl OpenapiHandler { - fn new(openapi : &OpenAPI) -> Self + fn new(openapi : &OpenapiRouter) -> Self { - Self(serde_json::to_string(openapi).map_err(|e| format!("{}", e))) + Self(serde_json::to_string(&openapi.0).map_err(|e| format!("{}", e))) } } @@ -120,21 +113,26 @@ impl Handler for OpenapiHandler } } +pub trait GetOpenapi +{ + fn get_openapi(&mut self, path : &str); +} + macro_rules! implOpenapiRouter { ($implType:ident) => { - impl<'a, C, P> OpenapiRouter<'a, $implType<'a, C, P>> + impl<'a, C, P> GetOpenapi for (&mut $implType<'a, C, P>, &mut OpenapiRouter) where C : PipelineHandleChain<P> + Copy + Send + Sync + 'static, P : RefUnwindSafe + Send + Sync + 'static { - pub fn get_openapi(&mut self, path : &str) + fn get_openapi(&mut self, path : &str) { - self.route.get(path).to_new_handler(OpenapiHandler::new(&self.openapi)); + self.0.get(path).to_new_handler(OpenapiHandler::new(&self.1)); } } - impl<'a, C, P> DrawResources for OpenapiRouter<'a, $implType<'a, C, P>> + impl<'a, C, P> DrawResources for (&mut $implType<'a, C, P>, &mut OpenapiRouter) where C : PipelineHandleChain<P> + Copy + Send + Sync + 'static, P : RefUnwindSafe + Send + Sync + 'static @@ -145,7 +143,7 @@ macro_rules! implOpenapiRouter { } } - impl<'a, C, P> DrawResourceRoutes for (&mut OpenapiRouter<'a, $implType<'a, C, P>>, String) + impl<'a, C, P> DrawResourceRoutes for (&mut (&mut $implType<'a, C, P>, &mut OpenapiRouter), String) where C : PipelineHandleChain<P> + Copy + Send + Sync + 'static, P : RefUnwindSafe + Send + Sync + 'static @@ -156,7 +154,7 @@ macro_rules! implOpenapiRouter { Handler : ResourceReadAll<Res> { let path = &self.1; - let mut item = self.0.remove_path(path); + let mut item = (self.0).1.remove_path(path); let mut content : IndexMap<String, MediaType> = IndexMap::new(); content[&APPLICATION_JSON.to_string()] = MediaType { schema: None, // TODO @@ -187,9 +185,9 @@ macro_rules! implOpenapiRouter { security: Vec::new(), servers: Vec::new() }); - self.0.add_path(path, item); + (self.0).1.add_path(path, item); - (&mut *self.0.route, self.1.to_string()).read_all::<Handler, Res>() + (&mut *(self.0).0, self.1.to_string()).read_all::<Handler, Res>() } fn read<Handler, ID, Res>(&mut self) @@ -198,7 +196,7 @@ macro_rules! implOpenapiRouter { Res : ResourceResult, Handler : ResourceRead<ID, Res> { - (&mut *self.0.route, self.1.to_string()).read::<Handler, ID, Res>() + (&mut *(self.0).0, self.1.to_string()).read::<Handler, ID, Res>() } fn create<Handler, Body, Res>(&mut self) @@ -207,7 +205,7 @@ macro_rules! implOpenapiRouter { Res : ResourceResult, Handler : ResourceCreate<Body, Res> { - (&mut *self.0.route, self.1.to_string()).create::<Handler, Body, Res>() + (&mut *(self.0).0, self.1.to_string()).create::<Handler, Body, Res>() } fn update_all<Handler, Body, Res>(&mut self) @@ -216,7 +214,7 @@ macro_rules! implOpenapiRouter { Res : ResourceResult, Handler : ResourceUpdateAll<Body, Res> { - (&mut *self.0.route, self.1.to_string()).update_all::<Handler, Body, Res>() + (&mut *(self.0).0, self.1.to_string()).update_all::<Handler, Body, Res>() } fn update<Handler, ID, Body, Res>(&mut self) @@ -226,7 +224,7 @@ macro_rules! implOpenapiRouter { Res : ResourceResult, Handler : ResourceUpdate<ID, Body, Res> { - (&mut *self.0.route, self.1.to_string()).update::<Handler, ID, Body, Res>() + (&mut *(self.0).0, self.1.to_string()).update::<Handler, ID, Body, Res>() } fn delete_all<Handler, Res>(&mut self) @@ -234,7 +232,7 @@ macro_rules! implOpenapiRouter { Res : ResourceResult, Handler : ResourceDeleteAll<Res> { - (&mut *self.0.route, self.1.to_string()).delete_all::<Handler, Res>() + (&mut *(self.0).0, self.1.to_string()).delete_all::<Handler, Res>() } fn delete<Handler, ID, Res>(&mut self) @@ -243,7 +241,7 @@ macro_rules! implOpenapiRouter { Res : ResourceResult, Handler : ResourceDelete<ID, Res> { - (&mut *self.0.route, self.1.to_string()).delete::<Handler, ID, Res>() + (&mut *(self.0).0, self.1.to_string()).delete::<Handler, ID, Res>() } } diff --git a/src/routing.rs b/src/routing.rs index f4fdbd5..ae073a9 100644 --- a/src/routing.rs +++ b/src/routing.rs @@ -36,7 +36,7 @@ pub trait WithOpenapi<D> { fn with_openapi<F, Title, Version>(&mut self, title : Title, version : Version, block : F) where - F : FnOnce(OpenapiRouter<D>), + F : FnOnce((&mut D, &mut OpenapiRouter)), Title : ToString, Version : ToString; } @@ -237,12 +237,12 @@ macro_rules! implDrawResourceRoutes { { fn with_openapi<F, Title, Version>(&mut self, title : Title, version : Version, block : F) where - F : FnOnce(OpenapiRouter<Self>), + F : FnOnce((&mut Self, &mut OpenapiRouter)), Title : ToString, Version : ToString { - let router : OpenapiRouter<Self> = OpenapiRouter::new(self, title, version); - block(router); + let mut router = OpenapiRouter::new(title, version); + block((self, &mut router)); } } From 4ceb1ef302e386df60e671e2e3829ffe6d0b46f7 Mon Sep 17 00:00:00 2001 From: Dominic <git@msrd0.de> Date: Mon, 30 Sep 2019 18:20:22 +0200 Subject: [PATCH 04/20] fix panic --- src/openapi.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/openapi.rs b/src/openapi.rs index d5aff5f..c67f518 100644 --- a/src/openapi.rs +++ b/src/openapi.rs @@ -156,19 +156,19 @@ macro_rules! implOpenapiRouter { let path = &self.1; let mut item = (self.0).1.remove_path(path); let mut content : IndexMap<String, MediaType> = IndexMap::new(); - content[&APPLICATION_JSON.to_string()] = MediaType { + content.insert(APPLICATION_JSON.to_string(), MediaType { schema: None, // TODO example: None, examples: IndexMap::new(), encoding: IndexMap::new() - }; + }); let mut responses : IndexMap<StatusCode, ReferenceOr<Response>> = IndexMap::new(); - responses[&StatusCode::Code(200)] = Item(Response { + responses.insert(StatusCode::Code(200), Item(Response { description: "OK".to_string(), headers: IndexMap::new(), content, links: IndexMap::new() - }); + })); item.get = Some(Operation { tags: Vec::new(), summary: None, From 6cf4b054477406a8556ee9f35a0b08ae525c5410 Mon Sep 17 00:00:00 2001 From: Dominic <git@msrd0.de> Date: Mon, 30 Sep 2019 18:41:18 +0200 Subject: [PATCH 05/20] add server data for openapi --- examples/users.rs | 2 +- src/openapi.rs | 10 +++++++--- src/routing.rs | 12 +++++++----- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/examples/users.rs b/examples/users.rs index fd8caac..077ef41 100644 --- a/examples/users.rs +++ b/examples/users.rs @@ -127,7 +127,7 @@ fn main() ); gotham::start(ADDR, build_router(chain, pipelines, |route| { - route.with_openapi("Users Example", "0.0.1", |mut route| { + route.with_openapi("Users Example", "0.0.1", ADDR, |mut route| { route.resource::<Users, _>("users"); route.get_openapi("openapi"); }); diff --git a/src/openapi.rs b/src/openapi.rs index c67f518..7d36973 100644 --- a/src/openapi.rs +++ b/src/openapi.rs @@ -14,7 +14,7 @@ use gotham::{ use indexmap::IndexMap; use log::error; use mime::{APPLICATION_JSON, TEXT_PLAIN}; -use openapiv3::{MediaType, OpenAPI, Operation, PathItem, Paths, ReferenceOr, ReferenceOr::Item, Response, Responses, StatusCode}; +use openapiv3::{MediaType, OpenAPI, Operation, PathItem, Paths, ReferenceOr, ReferenceOr::Item, Response, Responses, Server, StatusCode}; use serde::de::DeserializeOwned; use std::panic::RefUnwindSafe; @@ -22,7 +22,7 @@ pub struct OpenapiRouter(OpenAPI); impl OpenapiRouter { - pub fn new<Title : ToString, Version : ToString>(title : Title, version : Version) -> Self + pub fn new<Title : ToString, Version : ToString, Url : ToString>(title : Title, version : Version, server_url : Url) -> Self { Self(OpenAPI { openapi: "3.0.2".to_string(), @@ -34,7 +34,11 @@ impl OpenapiRouter license: None, version: version.to_string() }, - servers: Vec::new(), + servers: vec![Server { + url: server_url.to_string(), + description: None, + variables: None + }], paths: Paths::new(), components: None, security: Vec::new(), diff --git a/src/routing.rs b/src/routing.rs index ae073a9..a7ea592 100644 --- a/src/routing.rs +++ b/src/routing.rs @@ -34,11 +34,12 @@ struct PathExtractor<ID : RefUnwindSafe + Send + 'static> #[cfg(feature = "openapi")] pub trait WithOpenapi<D> { - fn with_openapi<F, Title, Version>(&mut self, title : Title, version : Version, block : F) + fn with_openapi<F, Title, Version, Url>(&mut self, title : Title, version : Version, server_url : Url, block : F) where F : FnOnce((&mut D, &mut OpenapiRouter)), Title : ToString, - Version : ToString; + Version : ToString, + Url : ToString; } /// This trait adds the `resource` method to gotham's routing. It allows you to register @@ -235,13 +236,14 @@ macro_rules! implDrawResourceRoutes { C : PipelineHandleChain<P> + Copy + Send + Sync + 'static, P : RefUnwindSafe + Send + Sync + 'static { - fn with_openapi<F, Title, Version>(&mut self, title : Title, version : Version, block : F) + fn with_openapi<F, Title, Version, Url>(&mut self, title : Title, version : Version, server_url : Url, block : F) where F : FnOnce((&mut Self, &mut OpenapiRouter)), Title : ToString, - Version : ToString + Version : ToString, + Url : ToString { - let mut router = OpenapiRouter::new(title, version); + let mut router = OpenapiRouter::new(title, version, server_url); block((self, &mut router)); } } From 681482ece308c4f86597bdd07e871bd8a82ad61b Mon Sep 17 00:00:00 2001 From: Dominic <git@msrd0.de> Date: Mon, 30 Sep 2019 20:58:15 +0200 Subject: [PATCH 06/20] introduce openapitype --- src/lib.rs | 34 ++++++++++++- src/openapi/mod.rs | 3 ++ src/{openapi.rs => openapi/router.rs} | 4 +- src/openapi/types.rs | 69 +++++++++++++++++++++++++++ src/result.rs | 20 ++++++-- src/routing.rs | 2 +- 6 files changed, 126 insertions(+), 6 deletions(-) create mode 100644 src/openapi/mod.rs rename src/{openapi.rs => openapi/router.rs} (97%) create mode 100644 src/openapi/types.rs diff --git a/src/lib.rs b/src/lib.rs index 2a12070..0506d71 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,13 +2,18 @@ #[macro_use] extern crate serde; pub use hyper::StatusCode; +#[cfg(not(feature = "openapi"))] +use serde::Serialize; pub mod helper; #[cfg(feature = "openapi")] pub mod openapi; #[cfg(feature = "openapi")] -pub use openapi::{GetOpenapi, OpenapiRouter}; +pub use openapi::{ + router::{GetOpenapi, OpenapiRouter}, + types::OpenapiType +}; mod resource; pub use resource::{ @@ -29,3 +34,30 @@ mod routing; pub use routing::{DrawResources, DrawResourceRoutes}; #[cfg(feature = "openapi")] pub use routing::WithOpenapi; + + +/// A type that can be used inside a request or response body. Implemented for every type +/// that is serializable with serde, however, it is recommended to use the rest_struct! +/// macro to create one. +#[cfg(not(feature = "openapi"))] +pub trait ResourceType : Serialize +{ +} + +#[cfg(not(feature = "openapi"))] +impl<T : Serialize> ResourceType for T +{ +} + +/// A type that can be used inside a request or response body. Implemented for every type +/// that is serializable with serde, however, it is recommended to use the rest_struct! +/// macro to create one. +#[cfg(feature = "openapi")] +pub trait ResourceType : OpenapiType +{ +} + +#[cfg(feature = "openapi")] +impl<T : OpenapiType> ResourceType for T +{ +} diff --git a/src/openapi/mod.rs b/src/openapi/mod.rs new file mode 100644 index 0000000..5c19494 --- /dev/null +++ b/src/openapi/mod.rs @@ -0,0 +1,3 @@ + +pub mod router; +pub mod types; diff --git a/src/openapi.rs b/src/openapi/router.rs similarity index 97% rename from src/openapi.rs rename to src/openapi/router.rs index 7d36973..5de695f 100644 --- a/src/openapi.rs +++ b/src/openapi/router.rs @@ -14,7 +14,9 @@ use gotham::{ use indexmap::IndexMap; use log::error; use mime::{APPLICATION_JSON, TEXT_PLAIN}; -use openapiv3::{MediaType, OpenAPI, Operation, PathItem, Paths, ReferenceOr, ReferenceOr::Item, Response, Responses, Server, StatusCode}; +use openapiv3::{ + MediaType, OpenAPI, Operation, PathItem, Paths, ReferenceOr, ReferenceOr::Item, Response, Responses, Server, StatusCode +}; use serde::de::DeserializeOwned; use std::panic::RefUnwindSafe; diff --git a/src/openapi/types.rs b/src/openapi/types.rs new file mode 100644 index 0000000..ba37f61 --- /dev/null +++ b/src/openapi/types.rs @@ -0,0 +1,69 @@ +use indexmap::IndexMap; +use openapiv3::{ + IntegerType, NumberType, ObjectType, SchemaKind, StringType, Type +}; +use serde::Serialize; + + +pub trait OpenapiType : Serialize +{ + fn to_schema() -> SchemaKind; +} + +impl OpenapiType for () +{ + fn to_schema() -> SchemaKind + { + SchemaKind::Type(Type::Object(ObjectType::default())) + } +} + +impl OpenapiType for bool +{ + fn to_schema() -> SchemaKind + { + SchemaKind::Type(Type::Boolean{}) + } +} + +macro_rules! int_types { + ($($int_ty:ty),*) => {$( + impl OpenapiType for $int_ty + { + fn to_schema() -> SchemaKind + { + SchemaKind::Type(Type::Integer(IntegerType::default())) + } + } + )*} +} + +int_types!(u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128); + +macro_rules! num_types { + ($($num_ty:ty),*) => {$( + impl OpenapiType for $num_ty + { + fn to_schema() -> SchemaKind + { + SchemaKind::Type(Type::Number(NumberType::default())) + } + } + )*} +} + +num_types!(f32, f64); + +macro_rules! str_types { + ($($str_ty:ty),*) => {$( + impl OpenapiType for $str_ty + { + fn to_schema() -> SchemaKind + { + SchemaKind::Type(Type::String(StringType::default())) + } + } + )*} +} + +str_types!(String, &str); diff --git a/src/result.rs b/src/result.rs index c75c5f6..79f42c8 100644 --- a/src/result.rs +++ b/src/result.rs @@ -1,4 +1,5 @@ -use crate::StatusCode; +use crate::{ResourceType, StatusCode}; +use openapiv3::SchemaKind; use serde::Serialize; use serde_json::error::Error as SerdeJsonError; use std::error::Error; @@ -7,6 +8,9 @@ use std::error::Error; pub trait ResourceResult { fn to_json(&self) -> Result<(StatusCode, String), SerdeJsonError>; + + #[cfg(feature = "openapi")] + fn to_schema() -> SchemaKind; } /// The default json returned on an 500 Internal Server Error. @@ -28,7 +32,7 @@ impl<T : ToString> From<T> for ResourceError } } -impl<R : Serialize, E : Error> ResourceResult for Result<R, E> +impl<R : ResourceType, E : Error> ResourceResult for Result<R, E> { fn to_json(&self) -> Result<(StatusCode, String), SerdeJsonError> { @@ -40,6 +44,11 @@ impl<R : Serialize, E : Error> ResourceResult for Result<R, E> } }) } + + fn to_schema() -> SchemaKind + { + R::to_schema() + } } /// This can be returned from a resource when there is no cause of an error. @@ -53,10 +62,15 @@ impl<T> From<T> for Success<T> } } -impl<T : Serialize> ResourceResult for Success<T> +impl<T : ResourceType> ResourceResult for Success<T> { fn to_json(&self) -> Result<(StatusCode, String), SerdeJsonError> { Ok((StatusCode::OK, serde_json::to_string(&self.0)?)) } + + fn to_schema() -> SchemaKind + { + T::to_schema() + } } diff --git a/src/routing.rs b/src/routing.rs index a7ea592..726dc85 100644 --- a/src/routing.rs +++ b/src/routing.rs @@ -4,7 +4,7 @@ use crate::{ StatusCode }; #[cfg(feature = "openapi")] -use crate::openapi::OpenapiRouter; +use crate::OpenapiRouter; use futures::{ future::{Future, err, ok}, From d9b4b22af33c15aa5945e895d715f2a70b55151f Mon Sep 17 00:00:00 2001 From: Dominic <git@msrd0.de> Date: Mon, 30 Sep 2019 23:53:55 +0200 Subject: [PATCH 07/20] first valid openapi spec generated --- src/helper.rs | 53 +++++++++++++++++++++++++++++++++++++++++++ src/openapi/router.rs | 46 ++++++++++++++++++++++++------------- src/openapi/types.rs | 28 ++++++++++++++++++++++- src/result.rs | 23 ++++++++++++++++--- 4 files changed, 130 insertions(+), 20 deletions(-) diff --git a/src/helper.rs b/src/helper.rs index 44d606b..24cf9a9 100644 --- a/src/helper.rs +++ b/src/helper.rs @@ -1,4 +1,11 @@ +#[cfg(feature = "openapi")] +pub mod openapi +{ + pub use indexmap::IndexMap; + pub use openapiv3::{ObjectType, ReferenceOr, Schema, SchemaData, SchemaKind, Type}; +} +#[cfg(not(feature = "openapi"))] #[macro_export] macro_rules! rest_struct { ($struct_name:ident { $($field_id:ident : $field_ty:ty),* }) => { @@ -10,6 +17,52 @@ macro_rules! rest_struct { } } +#[cfg(feature = "openapi")] +#[macro_export] +macro_rules! rest_struct { + ($struct_name:ident { $($field_id:ident : $field_ty:ty),* }) => { + #[derive(serde::Deserialize, serde::Serialize)] + struct $struct_name + { + $($field_id : $field_ty),* + } + + impl ::gotham_restful::OpenapiType for $struct_name + { + fn schema_name() -> Option<String> + { + Some(stringify!($struct_name).to_string()) + } + + fn to_schema() -> ::gotham_restful::helper::openapi::SchemaKind + { + use ::gotham_restful::helper::openapi::*; + + let mut properties : IndexMap<String, ReferenceOr<Box<Schema>>> = IndexMap::new(); + let mut required : Vec<String> = Vec::new(); + + $( + properties.insert( + stringify!($field_id).to_string(), + ReferenceOr::Item(Box::new(Schema { + schema_data: SchemaData::default(), + schema_kind: <$field_ty>::to_schema() + })) + ); + )* + + SchemaKind::Type(Type::Object(ObjectType { + properties, + required, + additional_properties: None, + min_properties: None, + max_properties: None + })) + } + } + } +} + #[macro_export] macro_rules! rest_resource { ($res_name:ident, $route:ident => $setup:block) => { diff --git a/src/openapi/router.rs b/src/openapi/router.rs index 5de695f..76d637c 100644 --- a/src/openapi/router.rs +++ b/src/openapi/router.rs @@ -15,7 +15,8 @@ use indexmap::IndexMap; use log::error; use mime::{APPLICATION_JSON, TEXT_PLAIN}; use openapiv3::{ - MediaType, OpenAPI, Operation, PathItem, Paths, ReferenceOr, ReferenceOr::Item, Response, Responses, Server, StatusCode + Components, MediaType, OpenAPI, Operation, PathItem, Paths, ReferenceOr, ReferenceOr::Item, ReferenceOr::Reference, + Response, Responses, Schema, SchemaData, Server, StatusCode }; use serde::de::DeserializeOwned; use std::panic::RefUnwindSafe; @@ -57,24 +58,27 @@ impl OpenapiRouter { return item; } - return PathItem { - get: None, - put: None, - post: None, - delete: None, - options: None, - head: None, - patch: None, - trace: None, - servers: Vec::new(), - parameters: Vec::new() - }; + return PathItem::default() } fn add_path<Path : ToString>(&mut self, path : Path, item : PathItem) { self.0.paths.insert(path.to_string(), Item(item)); } + + fn add_schema<Name : ToString>(&mut self, name : Name, item : Schema) + { + match &mut self.0.components { + Some(comp) => { + comp.schemas.insert(name.to_string(), Item(item)); + }, + None => { + let mut comp = Components::default(); + comp.schemas.insert(name.to_string(), Item(item)); + self.0.components = Some(comp); + } + }; + } } #[derive(Clone)] @@ -159,11 +163,21 @@ macro_rules! implOpenapiRouter { Res : ResourceResult, Handler : ResourceReadAll<Res> { - let path = &self.1; - let mut item = (self.0).1.remove_path(path); + let schema = Res::schema_name().unwrap_or_else(|| { + format!("Resource_{}_ReadAllResult", self.1) + }); + (self.0).1.add_schema(&schema, Schema { + schema_data: SchemaData::default(), + schema_kind: Res::to_schema() + }); + + let path = format!("/{}", &self.1); + let mut item = (self.0).1.remove_path(&path); let mut content : IndexMap<String, MediaType> = IndexMap::new(); content.insert(APPLICATION_JSON.to_string(), MediaType { - schema: None, // TODO + schema: Some(Reference { + reference: format!("#/components/schemas/{}", schema) + }), example: None, examples: IndexMap::new(), encoding: IndexMap::new() diff --git a/src/openapi/types.rs b/src/openapi/types.rs index ba37f61..8ed2851 100644 --- a/src/openapi/types.rs +++ b/src/openapi/types.rs @@ -1,12 +1,17 @@ use indexmap::IndexMap; use openapiv3::{ - IntegerType, NumberType, ObjectType, SchemaKind, StringType, Type + ArrayType, IntegerType, NumberType, ObjectType, ReferenceOr::Item, Schema, SchemaData, SchemaKind, StringType, Type }; use serde::Serialize; pub trait OpenapiType : Serialize { + fn schema_name() -> Option<String> + { + None + } + fn to_schema() -> SchemaKind; } @@ -67,3 +72,24 @@ macro_rules! str_types { } str_types!(String, &str); + +impl<T : OpenapiType> OpenapiType for Vec<T> +{ + fn schema_name() -> Option<String> + { + T::schema_name().map(|name| format!("{}Array", name)) + } + + fn to_schema() -> SchemaKind + { + SchemaKind::Type(Type::Array(ArrayType { + items: Item(Box::new(Schema { + schema_data: SchemaData::default(), + schema_kind: T::to_schema() + })), + min_items: None, + max_items: None, + unique_items: false + })) + } +} diff --git a/src/result.rs b/src/result.rs index 79f42c8..97d3f26 100644 --- a/src/result.rs +++ b/src/result.rs @@ -8,7 +8,10 @@ use std::error::Error; pub trait ResourceResult { fn to_json(&self) -> Result<(StatusCode, String), SerdeJsonError>; - + + #[cfg(feature = "openapi")] + fn schema_name() -> Option<String>; + #[cfg(feature = "openapi")] fn to_schema() -> SchemaKind; } @@ -44,7 +47,14 @@ impl<R : ResourceType, E : Error> ResourceResult for Result<R, E> } }) } - + + #[cfg(feature = "openapi")] + fn schema_name() -> Option<String> + { + R::schema_name() + } + + #[cfg(feature = "openapi")] fn to_schema() -> SchemaKind { R::to_schema() @@ -68,7 +78,14 @@ impl<T : ResourceType> ResourceResult for Success<T> { Ok((StatusCode::OK, serde_json::to_string(&self.0)?)) } - + + #[cfg(feature = "openapi")] + fn schema_name() -> Option<String> + { + T::schema_name() + } + + #[cfg(feature = "openapi")] fn to_schema() -> SchemaKind { T::to_schema() From 7286054a2fae89159a0774497535c17e0bc6282d Mon Sep 17 00:00:00 2001 From: Dominic <git@msrd0.de> Date: Tue, 1 Oct 2019 00:23:34 +0200 Subject: [PATCH 08/20] openapi for read method --- examples/users.rs | 2 +- src/openapi/router.rs | 133 ++++++++++++++++++++++++++++-------------- src/result.rs | 1 + 3 files changed, 92 insertions(+), 44 deletions(-) diff --git a/examples/users.rs b/examples/users.rs index 6ba9b55..1f169b8 100644 --- a/examples/users.rs +++ b/examples/users.rs @@ -118,7 +118,7 @@ fn main() ); gotham::start(ADDR, build_router(chain, pipelines, |route| { - route.with_openapi("Users Example", "0.0.1", ADDR, |mut route| { + route.with_openapi("Users Example", "0.0.1", format!("http://{}", ADDR), |mut route| { route.resource::<Users, _>("users"); route.get_openapi("openapi"); }); diff --git a/src/openapi/router.rs b/src/openapi/router.rs index 76d637c..678ad5f 100644 --- a/src/openapi/router.rs +++ b/src/openapi/router.rs @@ -1,7 +1,8 @@ use crate::{ resource::*, result::*, - routing::* + routing::*, + OpenapiType }; use futures::future::ok; use gotham::{ @@ -15,8 +16,9 @@ use indexmap::IndexMap; use log::error; use mime::{APPLICATION_JSON, TEXT_PLAIN}; use openapiv3::{ - Components, MediaType, OpenAPI, Operation, PathItem, Paths, ReferenceOr, ReferenceOr::Item, ReferenceOr::Reference, - Response, Responses, Schema, SchemaData, Server, StatusCode + Components, MediaType, OpenAPI, Operation, Parameter, ParameterData, ParameterSchemaOrContent, PathItem, + PathStyle, Paths, ReferenceOr, ReferenceOr::Item, ReferenceOr::Reference, Response, Responses, Schema, + SchemaData, Server, StatusCode }; use serde::de::DeserializeOwned; use std::panic::RefUnwindSafe; @@ -66,8 +68,24 @@ impl OpenapiRouter self.0.paths.insert(path.to_string(), Item(item)); } - fn add_schema<Name : ToString>(&mut self, name : Name, item : Schema) + fn add_schema<T : ResourceResult>(&mut self, path : &str, method : &str, desc : &str) -> String { + let name = T::schema_name().unwrap_or_else(|| format!("path_{}_{}_{}", path, method, desc)); + let item = Schema { + schema_data: SchemaData { + nullable: false, + read_only: false, + write_only: false, + deprecated: false, + external_docs: None, + example: None, + title: Some(name.to_string()), + description: None, + discriminator: None, + default: None + }, + schema_kind: T::to_schema() + }; match &mut self.0.components { Some(comp) => { comp.schemas.insert(name.to_string(), Item(item)); @@ -78,6 +96,7 @@ impl OpenapiRouter self.0.components = Some(comp); } }; + name } } @@ -128,6 +147,64 @@ pub trait GetOpenapi fn get_openapi(&mut self, path : &str); } +fn new_operation(schema : &str, path_params : Vec<&str>) -> Operation +{ + let mut content : IndexMap<String, MediaType> = IndexMap::new(); + content.insert(APPLICATION_JSON.to_string(), MediaType { + schema: Some(Reference { + reference: format!("#/components/schemas/{}", schema) + }), + example: None, + examples: IndexMap::new(), + encoding: IndexMap::new() + }); + + let mut responses : IndexMap<StatusCode, ReferenceOr<Response>> = IndexMap::new(); + responses.insert(StatusCode::Code(200), Item(Response { + description: "OK".to_string(), + headers: IndexMap::new(), + content, + links: IndexMap::new() + })); + + let mut params : Vec<ReferenceOr<Parameter>> = Vec::new(); + for param in path_params + { + params.push(Item(Parameter::Path { + parameter_data: ParameterData { + name: param.to_string(), + description: None, + required: true, + deprecated: None, + format: ParameterSchemaOrContent::Schema(Item(Schema { + schema_data: SchemaData::default(), + schema_kind: String::to_schema() + })), + example: None, + examples: IndexMap::new() + }, + style: PathStyle::default(), + })); + } + + Operation { + tags: Vec::new(), + summary: None, + description: None, + external_documentation: None, + operation_id: None, // TODO + parameters: params, + request_body: None, + responses: Responses { + default: None, + responses + }, + deprecated: false, + security: Vec::new(), + servers: Vec::new() + } +} + macro_rules! implOpenapiRouter { ($implType:ident) => { @@ -163,48 +240,11 @@ macro_rules! implOpenapiRouter { Res : ResourceResult, Handler : ResourceReadAll<Res> { - let schema = Res::schema_name().unwrap_or_else(|| { - format!("Resource_{}_ReadAllResult", self.1) - }); - (self.0).1.add_schema(&schema, Schema { - schema_data: SchemaData::default(), - schema_kind: Res::to_schema() - }); + let schema = (self.0).1.add_schema::<Res>(&self.1, "read_all", "result_body"); let path = format!("/{}", &self.1); let mut item = (self.0).1.remove_path(&path); - let mut content : IndexMap<String, MediaType> = IndexMap::new(); - content.insert(APPLICATION_JSON.to_string(), MediaType { - schema: Some(Reference { - reference: format!("#/components/schemas/{}", schema) - }), - example: None, - examples: IndexMap::new(), - encoding: IndexMap::new() - }); - let mut responses : IndexMap<StatusCode, ReferenceOr<Response>> = IndexMap::new(); - responses.insert(StatusCode::Code(200), Item(Response { - description: "OK".to_string(), - headers: IndexMap::new(), - content, - links: IndexMap::new() - })); - item.get = Some(Operation { - tags: Vec::new(), - summary: None, - description: None, - external_documentation: None, - operation_id: None, // TODO - parameters: Vec::new(), - request_body: None, - responses: Responses { - default: None, - responses - }, - deprecated: false, - security: Vec::new(), - servers: Vec::new() - }); + item.get = Some(new_operation(&schema, vec![])); (self.0).1.add_path(path, item); (&mut *(self.0).0, self.1.to_string()).read_all::<Handler, Res>() @@ -216,6 +256,13 @@ macro_rules! implOpenapiRouter { Res : ResourceResult, Handler : ResourceRead<ID, Res> { + let schema = (self.0).1.add_schema::<Res>(&self.1, "read", "result_body"); + + let path = format!("/{}/{{id}}", &self.1); + let mut item = (self.0).1.remove_path(&path); + item.get = Some(new_operation(&schema, vec!["id"])); + (self.0).1.add_path(path, item); + (&mut *(self.0).0, self.1.to_string()).read::<Handler, ID, Res>() } diff --git a/src/result.rs b/src/result.rs index 97d3f26..b67333d 100644 --- a/src/result.rs +++ b/src/result.rs @@ -1,4 +1,5 @@ use crate::{ResourceType, StatusCode}; +#[cfg(feature = "openapi")] use openapiv3::SchemaKind; use serde::Serialize; use serde_json::error::Error as SerdeJsonError; From 389740cd64d0fde28148ea01ae982f29d1ab513c Mon Sep 17 00:00:00 2001 From: Dominic <git@msrd0.de> Date: Tue, 1 Oct 2019 00:34:58 +0200 Subject: [PATCH 09/20] add openapi for other methods, so far without request bodies --- src/openapi/router.rs | 61 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 53 insertions(+), 8 deletions(-) diff --git a/src/openapi/router.rs b/src/openapi/router.rs index 678ad5f..25dc3c0 100644 --- a/src/openapi/router.rs +++ b/src/openapi/router.rs @@ -17,8 +17,8 @@ use log::error; use mime::{APPLICATION_JSON, TEXT_PLAIN}; use openapiv3::{ Components, MediaType, OpenAPI, Operation, Parameter, ParameterData, ParameterSchemaOrContent, PathItem, - PathStyle, Paths, ReferenceOr, ReferenceOr::Item, ReferenceOr::Reference, Response, Responses, Schema, - SchemaData, Server, StatusCode + PathStyle, Paths, ReferenceOr, ReferenceOr::Item, ReferenceOr::Reference, RequestBody, Response, Responses, + Schema, SchemaData, Server, StatusCode }; use serde::de::DeserializeOwned; use std::panic::RefUnwindSafe; @@ -147,7 +147,7 @@ pub trait GetOpenapi fn get_openapi(&mut self, path : &str); } -fn new_operation(schema : &str, path_params : Vec<&str>) -> Operation +fn schema_to_content(schema : &str) -> IndexMap<String, MediaType> { let mut content : IndexMap<String, MediaType> = IndexMap::new(); content.insert(APPLICATION_JSON.to_string(), MediaType { @@ -158,12 +158,16 @@ fn new_operation(schema : &str, path_params : Vec<&str>) -> Operation examples: IndexMap::new(), encoding: IndexMap::new() }); - + content +} + +fn new_operation(schema : &str, path_params : Vec<&str>, body_schema : Option<&str>) -> Operation +{ let mut responses : IndexMap<StatusCode, ReferenceOr<Response>> = IndexMap::new(); responses.insert(StatusCode::Code(200), Item(Response { description: "OK".to_string(), headers: IndexMap::new(), - content, + content: schema_to_content(schema), links: IndexMap::new() })); @@ -186,6 +190,12 @@ fn new_operation(schema : &str, path_params : Vec<&str>) -> Operation style: PathStyle::default(), })); } + + let request_body = body_schema.map(|schema| Item(RequestBody { + description: None, + content: schema_to_content(schema), + required: true + })); Operation { tags: Vec::new(), @@ -194,7 +204,7 @@ fn new_operation(schema : &str, path_params : Vec<&str>) -> Operation external_documentation: None, operation_id: None, // TODO parameters: params, - request_body: None, + request_body, responses: Responses { default: None, responses @@ -244,7 +254,7 @@ macro_rules! implOpenapiRouter { let path = format!("/{}", &self.1); let mut item = (self.0).1.remove_path(&path); - item.get = Some(new_operation(&schema, vec![])); + item.get = Some(new_operation(&schema, vec![], None)); (self.0).1.add_path(path, item); (&mut *(self.0).0, self.1.to_string()).read_all::<Handler, Res>() @@ -260,7 +270,7 @@ macro_rules! implOpenapiRouter { let path = format!("/{}/{{id}}", &self.1); let mut item = (self.0).1.remove_path(&path); - item.get = Some(new_operation(&schema, vec!["id"])); + item.get = Some(new_operation(&schema, vec!["id"], None)); (self.0).1.add_path(path, item); (&mut *(self.0).0, self.1.to_string()).read::<Handler, ID, Res>() @@ -272,6 +282,13 @@ macro_rules! implOpenapiRouter { Res : ResourceResult, Handler : ResourceCreate<Body, Res> { + let schema = (self.0).1.add_schema::<Res>(&self.1, "create", "result_body"); + + let path = format!("/{}", &self.1); + let mut item = (self.0).1.remove_path(&path); + item.post = Some(new_operation(&schema, vec![], None)); + (self.0).1.add_path(path, item); + (&mut *(self.0).0, self.1.to_string()).create::<Handler, Body, Res>() } @@ -281,6 +298,13 @@ macro_rules! implOpenapiRouter { Res : ResourceResult, Handler : ResourceUpdateAll<Body, Res> { + let schema = (self.0).1.add_schema::<Res>(&self.1, "update_all", "result_body"); + + let path = format!("/{}", &self.1); + let mut item = (self.0).1.remove_path(&path); + item.put = Some(new_operation(&schema, vec![], None)); + (self.0).1.add_path(path, item); + (&mut *(self.0).0, self.1.to_string()).update_all::<Handler, Body, Res>() } @@ -291,6 +315,13 @@ macro_rules! implOpenapiRouter { Res : ResourceResult, Handler : ResourceUpdate<ID, Body, Res> { + let schema = (self.0).1.add_schema::<Res>(&self.1, "update", "result_body"); + + let path = format!("/{}/{{id}}", &self.1); + let mut item = (self.0).1.remove_path(&path); + item.put = Some(new_operation(&schema, vec!["id"], None)); + (self.0).1.add_path(path, item); + (&mut *(self.0).0, self.1.to_string()).update::<Handler, ID, Body, Res>() } @@ -299,6 +330,13 @@ macro_rules! implOpenapiRouter { Res : ResourceResult, Handler : ResourceDeleteAll<Res> { + let schema = (self.0).1.add_schema::<Res>(&self.1, "delete_all", "result_body"); + + let path = format!("/{}", &self.1); + let mut item = (self.0).1.remove_path(&path); + item.delete = Some(new_operation(&schema, vec![], None)); + (self.0).1.add_path(path, item); + (&mut *(self.0).0, self.1.to_string()).delete_all::<Handler, Res>() } @@ -308,6 +346,13 @@ macro_rules! implOpenapiRouter { Res : ResourceResult, Handler : ResourceDelete<ID, Res> { + let schema = (self.0).1.add_schema::<Res>(&self.1, "delete", "result_body"); + + let path = format!("/{}/{{id}}", &self.1); + let mut item = (self.0).1.remove_path(&path); + item.delete = Some(new_operation(&schema, vec!["id"], None)); + (self.0).1.add_path(path, item); + (&mut *(self.0).0, self.1.to_string()).delete::<Handler, ID, Res>() } } From fe6a79008e4d4f8af883884c063454161873fef9 Mon Sep 17 00:00:00 2001 From: Dominic <git@msrd0.de> Date: Tue, 1 Oct 2019 00:49:13 +0200 Subject: [PATCH 10/20] add request bodies to openapi --- src/lib.rs | 11 +++++------ src/openapi/router.rs | 20 ++++++++++++-------- src/openapi/types.rs | 4 +--- src/resource.rs | 6 +++--- src/result.rs | 14 ++++++++++++++ src/routing.rs | 19 ++++++++++--------- 6 files changed, 45 insertions(+), 29 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 0506d71..715cb90 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,8 +2,7 @@ #[macro_use] extern crate serde; pub use hyper::StatusCode; -#[cfg(not(feature = "openapi"))] -use serde::Serialize; +use serde::{de::DeserializeOwned, Serialize}; pub mod helper; @@ -40,12 +39,12 @@ pub use routing::WithOpenapi; /// that is serializable with serde, however, it is recommended to use the rest_struct! /// macro to create one. #[cfg(not(feature = "openapi"))] -pub trait ResourceType : Serialize +pub trait ResourceType : DeserializeOwned + Serialize { } #[cfg(not(feature = "openapi"))] -impl<T : Serialize> ResourceType for T +impl<T : DeserializeOwned + Serialize> ResourceType for T { } @@ -53,11 +52,11 @@ impl<T : Serialize> ResourceType for T /// that is serializable with serde, however, it is recommended to use the rest_struct! /// macro to create one. #[cfg(feature = "openapi")] -pub trait ResourceType : OpenapiType +pub trait ResourceType : OpenapiType + DeserializeOwned + Serialize { } #[cfg(feature = "openapi")] -impl<T : OpenapiType> ResourceType for T +impl<T : OpenapiType + DeserializeOwned + Serialize> ResourceType for T { } diff --git a/src/openapi/router.rs b/src/openapi/router.rs index 25dc3c0..1539f4d 100644 --- a/src/openapi/router.rs +++ b/src/openapi/router.rs @@ -2,7 +2,8 @@ use crate::{ resource::*, result::*, routing::*, - OpenapiType + OpenapiType, + ResourceType }; use futures::future::ok; use gotham::{ @@ -68,7 +69,7 @@ impl OpenapiRouter self.0.paths.insert(path.to_string(), Item(item)); } - fn add_schema<T : ResourceResult>(&mut self, path : &str, method : &str, desc : &str) -> String + fn add_schema<T : OpenapiType>(&mut self, path : &str, method : &str, desc : &str) -> String { let name = T::schema_name().unwrap_or_else(|| format!("path_{}_{}_{}", path, method, desc)); let item = Schema { @@ -278,15 +279,16 @@ macro_rules! implOpenapiRouter { fn create<Handler, Body, Res>(&mut self) where - Body : DeserializeOwned, + Body : ResourceType, Res : ResourceResult, Handler : ResourceCreate<Body, Res> { let schema = (self.0).1.add_schema::<Res>(&self.1, "create", "result_body"); + let body_schema = (self.0).1.add_schema::<Body>(&self.1, "create", "body"); let path = format!("/{}", &self.1); let mut item = (self.0).1.remove_path(&path); - item.post = Some(new_operation(&schema, vec![], None)); + item.post = Some(new_operation(&schema, vec![], Some(&body_schema))); (self.0).1.add_path(path, item); (&mut *(self.0).0, self.1.to_string()).create::<Handler, Body, Res>() @@ -294,15 +296,16 @@ macro_rules! implOpenapiRouter { fn update_all<Handler, Body, Res>(&mut self) where - Body : DeserializeOwned, + Body : ResourceType, Res : ResourceResult, Handler : ResourceUpdateAll<Body, Res> { let schema = (self.0).1.add_schema::<Res>(&self.1, "update_all", "result_body"); + let body_schema = (self.0).1.add_schema::<Body>(&self.1, "create", "body"); let path = format!("/{}", &self.1); let mut item = (self.0).1.remove_path(&path); - item.put = Some(new_operation(&schema, vec![], None)); + item.put = Some(new_operation(&schema, vec![], Some(&body_schema))); (self.0).1.add_path(path, item); (&mut *(self.0).0, self.1.to_string()).update_all::<Handler, Body, Res>() @@ -311,15 +314,16 @@ macro_rules! implOpenapiRouter { fn update<Handler, ID, Body, Res>(&mut self) where ID : DeserializeOwned + Clone + RefUnwindSafe + Send + Sync + 'static, - Body : DeserializeOwned, + Body : ResourceType, Res : ResourceResult, Handler : ResourceUpdate<ID, Body, Res> { let schema = (self.0).1.add_schema::<Res>(&self.1, "update", "result_body"); + let body_schema = (self.0).1.add_schema::<Body>(&self.1, "create", "body"); let path = format!("/{}/{{id}}", &self.1); let mut item = (self.0).1.remove_path(&path); - item.put = Some(new_operation(&schema, vec!["id"], None)); + item.put = Some(new_operation(&schema, vec!["id"], Some(&body_schema))); (self.0).1.add_path(path, item); (&mut *(self.0).0, self.1.to_string()).update::<Handler, ID, Body, Res>() diff --git a/src/openapi/types.rs b/src/openapi/types.rs index 8ed2851..3b70003 100644 --- a/src/openapi/types.rs +++ b/src/openapi/types.rs @@ -1,11 +1,9 @@ -use indexmap::IndexMap; use openapiv3::{ ArrayType, IntegerType, NumberType, ObjectType, ReferenceOr::Item, Schema, SchemaData, SchemaKind, StringType, Type }; -use serde::Serialize; -pub trait OpenapiType : Serialize +pub trait OpenapiType { fn schema_name() -> Option<String> { diff --git a/src/resource.rs b/src/resource.rs index f82947c..d05594f 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -1,4 +1,4 @@ -use crate::{DrawResourceRoutes, ResourceResult}; +use crate::{DrawResourceRoutes, ResourceResult, ResourceType}; use gotham::state::State; use serde::de::DeserializeOwned; use std::panic::RefUnwindSafe; @@ -30,7 +30,7 @@ where } /// Handle a POST request on the Resource root. -pub trait ResourceCreate<Body : DeserializeOwned, R : ResourceResult> +pub trait ResourceCreate<Body : ResourceType, R : ResourceResult> { fn create(state : &mut State, body : Body) -> R; } @@ -42,7 +42,7 @@ pub trait ResourceUpdateAll<Body : DeserializeOwned, R : ResourceResult> } /// Handle a PUT request on the Resource with an id. -pub trait ResourceUpdate<ID, Body : DeserializeOwned, R : ResourceResult> +pub trait ResourceUpdate<ID, Body : ResourceType, R : ResourceResult> where ID : DeserializeOwned + Clone + RefUnwindSafe + Send + Sync + 'static { diff --git a/src/result.rs b/src/result.rs index b67333d..3f8eed1 100644 --- a/src/result.rs +++ b/src/result.rs @@ -17,6 +17,20 @@ pub trait ResourceResult fn to_schema() -> SchemaKind; } +#[cfg(feature = "openapi")] +impl<Res : ResourceResult> crate::OpenapiType for Res +{ + fn schema_name() -> Option<String> + { + Self::schema_name() + } + + fn to_schema() -> SchemaKind + { + Self::to_schema() + } +} + /// The default json returned on an 500 Internal Server Error. #[derive(Debug, Serialize)] pub struct ResourceError diff --git a/src/routing.rs b/src/routing.rs index 726dc85..5d720b2 100644 --- a/src/routing.rs +++ b/src/routing.rs @@ -1,6 +1,7 @@ use crate::{ resource::*, result::{ResourceError, ResourceResult}, + ResourceType, StatusCode }; #[cfg(feature = "openapi")] @@ -66,20 +67,20 @@ pub trait DrawResourceRoutes fn create<Handler, Body, Res>(&mut self) where - Body : DeserializeOwned, + Body : ResourceType, Res : ResourceResult, Handler : ResourceCreate<Body, Res>; fn update_all<Handler, Body, Res>(&mut self) where - Body : DeserializeOwned, + Body : ResourceType, Res : ResourceResult, Handler : ResourceUpdateAll<Body, Res>; fn update<Handler, ID, Body, Res>(&mut self) where ID : DeserializeOwned + Clone + RefUnwindSafe + Send + Sync + 'static, - Body : DeserializeOwned, + Body : ResourceType, Res : ResourceResult, Handler : ResourceUpdate<ID, Body, Res>; @@ -176,7 +177,7 @@ where fn create_handler<Handler, Body, Res>(state : State) -> Box<HandlerFuture> where - Body : DeserializeOwned, + Body : ResourceType, Res : ResourceResult, Handler : ResourceCreate<Body, Res> { @@ -185,7 +186,7 @@ where fn update_all_handler<Handler, Body, Res>(state : State) -> Box<HandlerFuture> where - Body : DeserializeOwned, + Body : ResourceType, Res : ResourceResult, Handler : ResourceUpdateAll<Body, Res> { @@ -195,7 +196,7 @@ where fn update_handler<Handler, ID, Body, Res>(state : State) -> Box<HandlerFuture> where ID : DeserializeOwned + Clone + RefUnwindSafe + Send + Sync + 'static, - Body : DeserializeOwned, + Body : ResourceType, Res : ResourceResult, Handler : ResourceUpdate<ID, Body, Res> { @@ -286,7 +287,7 @@ macro_rules! implDrawResourceRoutes { fn create<Handler, Body, Res>(&mut self) where - Body : DeserializeOwned, + Body : ResourceType, Res : ResourceResult, Handler : ResourceCreate<Body, Res> { @@ -296,7 +297,7 @@ macro_rules! implDrawResourceRoutes { fn update_all<Handler, Body, Res>(&mut self) where - Body : DeserializeOwned, + Body : ResourceType, Res : ResourceResult, Handler : ResourceUpdateAll<Body, Res> { @@ -307,7 +308,7 @@ macro_rules! implDrawResourceRoutes { fn update<Handler, ID, Body, Res>(&mut self) where ID : DeserializeOwned + Clone + RefUnwindSafe + Send + Sync + 'static, - Body : DeserializeOwned, + Body : ResourceType, Res : ResourceResult, Handler : ResourceUpdate<ID, Body, Res> { From 84ff8acc68b158c136bd0310f831e3b5ad7d9a22 Mon Sep 17 00:00:00 2001 From: Dominic <git@msrd0.de> Date: Tue, 1 Oct 2019 01:11:43 +0200 Subject: [PATCH 11/20] add chrono support --- Cargo.lock | 1 + Cargo.toml | 3 ++- src/openapi/types.rs | 28 ++++++++++++++++++++++++++-- 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c4f12c1..944d6ed 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -331,6 +331,7 @@ dependencies = [ name = "gotham-restful" version = "0.0.1" dependencies = [ + "chrono 0.4.9 (registry+https://github.com/rust-lang/crates.io-index)", "failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", "fake 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "futures 0.1.29 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/Cargo.toml b/Cargo.toml index c798ee3..0803aff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ repository = "https://gitlab.com/msrd0/gotham-restful" gitlab = { repository = "msrd0/gotham-restful", branch = "master" } [dependencies] +chrono = { version = "0.4", optional = true } failure = "0.1" futures = "0.1" gotham = "0.4" @@ -34,5 +35,5 @@ log = "0.4" log4rs = { version = "0.8", features = ["console_appender"], default-features = false } [features] -default = ["openapi"] +default = ["openapi", "chrono"] openapi = ["indexmap", "log", "openapiv3"] diff --git a/src/openapi/types.rs b/src/openapi/types.rs index 3b70003..1338526 100644 --- a/src/openapi/types.rs +++ b/src/openapi/types.rs @@ -1,5 +1,10 @@ +#[cfg(feature = "chrono")] +use chrono::{ + Date, DateTime, FixedOffset, Local, NaiveDate, NaiveDateTime, Utc +}; use openapiv3::{ - ArrayType, IntegerType, NumberType, ObjectType, ReferenceOr::Item, Schema, SchemaData, SchemaKind, StringType, Type + ArrayType, IntegerType, NumberType, ObjectType, ReferenceOr::Item, Schema, SchemaData, SchemaKind, + StringFormat, StringType, Type, VariantOrUnknownOrEmpty }; @@ -66,7 +71,21 @@ macro_rules! str_types { SchemaKind::Type(Type::String(StringType::default())) } } - )*} + )*}; + + (format = $format:ident, $($str_ty:ty),*) => {$( + impl OpenapiType for $str_ty + { + fn to_schema() -> SchemaKind + { + SchemaKind::Type(Type::String(StringType { + format: VariantOrUnknownOrEmpty::Item(StringFormat::$format), + pattern: None, + enumeration: Vec::new() + })) + } + } + )*}; } str_types!(String, &str); @@ -91,3 +110,8 @@ impl<T : OpenapiType> OpenapiType for Vec<T> })) } } + +#[cfg(feature = "chrono")] +str_types!(format = Date, Date<FixedOffset>, Date<Local>, Date<Utc>, NaiveDate); +#[cfg(feature = "chrono")] +str_types!(format = DateTime, DateTime<FixedOffset>, DateTime<Local>, DateTime<Utc>, NaiveDateTime); From 0619e699258983bd13c88a3b31c20f39ecb80345 Mon Sep 17 00:00:00 2001 From: Dominic <git@msrd0.de> Date: Tue, 1 Oct 2019 01:37:47 +0200 Subject: [PATCH 12/20] add more schema titles --- src/helper.rs | 13 ++++++++++++- src/openapi/types.rs | 13 ++++++++++++- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/helper.rs b/src/helper.rs index e73fe96..e34515e 100644 --- a/src/helper.rs +++ b/src/helper.rs @@ -45,7 +45,18 @@ macro_rules! rest_struct { properties.insert( stringify!($field_id).to_string(), ReferenceOr::Item(Box::new(Schema { - schema_data: SchemaData::default(), + schema_data: SchemaData { + nullable: false, + read_only: false, + write_only: false, + deprecated: false, + external_docs: None, + example: None, + title: <$field_ty>::schema_name(), + description: None, + discriminator: None, + default: None + }, schema_kind: <$field_ty>::to_schema() })) ); diff --git a/src/openapi/types.rs b/src/openapi/types.rs index 1338526..db26d67 100644 --- a/src/openapi/types.rs +++ b/src/openapi/types.rs @@ -101,7 +101,18 @@ impl<T : OpenapiType> OpenapiType for Vec<T> { SchemaKind::Type(Type::Array(ArrayType { items: Item(Box::new(Schema { - schema_data: SchemaData::default(), + schema_data: SchemaData { + nullable: false, + read_only: false, + write_only: false, + deprecated: false, + external_docs: None, + example: None, + title: T::schema_name(), + description: None, + discriminator: None, + default: None + }, schema_kind: T::to_schema() })), min_items: None, From d1c7ac5887f4c8e7336feb4fd418ae43cfec3ebd Mon Sep 17 00:00:00 2001 From: Dominic <git@msrd0.de> Date: Tue, 1 Oct 2019 15:33:05 +0200 Subject: [PATCH 13/20] introduce OpenapiSchema type --- src/helper.rs | 35 +++++--------- src/lib.rs | 2 +- src/openapi/router.rs | 26 ++-------- src/openapi/types.rs | 107 +++++++++++++++++++++++++----------------- src/result.rs | 30 ++---------- 5 files changed, 85 insertions(+), 115 deletions(-) diff --git a/src/helper.rs b/src/helper.rs index e34515e..4b2789e 100644 --- a/src/helper.rs +++ b/src/helper.rs @@ -29,14 +29,9 @@ macro_rules! rest_struct { impl ::gotham_restful::OpenapiType for $struct_name { - fn schema_name() -> Option<String> + fn to_schema() -> ::gotham_restful::OpenapiSchema { - Some(stringify!($struct_name).to_string()) - } - - fn to_schema() -> ::gotham_restful::helper::openapi::SchemaKind - { - use ::gotham_restful::helper::openapi::*; + use ::gotham_restful::{helper::openapi::*, OpenapiSchema}; let mut properties : IndexMap<String, ReferenceOr<Box<Schema>>> = IndexMap::new(); let mut required : Vec<String> = Vec::new(); @@ -44,31 +39,23 @@ macro_rules! rest_struct { $( properties.insert( stringify!($field_id).to_string(), - ReferenceOr::Item(Box::new(Schema { - schema_data: SchemaData { - nullable: false, - read_only: false, - write_only: false, - deprecated: false, - external_docs: None, - example: None, - title: <$field_ty>::schema_name(), - description: None, - discriminator: None, - default: None - }, - schema_kind: <$field_ty>::to_schema() - })) + ReferenceOr::Item(Box::new(<$field_ty>::to_schema().to_schema())) ); )* - SchemaKind::Type(Type::Object(ObjectType { + let schema = SchemaKind::Type(Type::Object(ObjectType { properties, required, additional_properties: None, min_properties: None, max_properties: None - })) + })); + + OpenapiSchema { + name: Some(stringify!($struct_name).to_string()), + nullable: false, + schema + } } } } diff --git a/src/lib.rs b/src/lib.rs index 715cb90..470d119 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,7 +11,7 @@ pub mod openapi; #[cfg(feature = "openapi")] pub use openapi::{ router::{GetOpenapi, OpenapiRouter}, - types::OpenapiType + types::{OpenapiSchema, OpenapiType} }; mod resource; diff --git a/src/openapi/router.rs b/src/openapi/router.rs index 1539f4d..1bade2e 100644 --- a/src/openapi/router.rs +++ b/src/openapi/router.rs @@ -19,7 +19,7 @@ use mime::{APPLICATION_JSON, TEXT_PLAIN}; use openapiv3::{ Components, MediaType, OpenAPI, Operation, Parameter, ParameterData, ParameterSchemaOrContent, PathItem, PathStyle, Paths, ReferenceOr, ReferenceOr::Item, ReferenceOr::Reference, RequestBody, Response, Responses, - Schema, SchemaData, Server, StatusCode + Server, StatusCode }; use serde::de::DeserializeOwned; use std::panic::RefUnwindSafe; @@ -71,22 +71,9 @@ impl OpenapiRouter fn add_schema<T : OpenapiType>(&mut self, path : &str, method : &str, desc : &str) -> String { - let name = T::schema_name().unwrap_or_else(|| format!("path_{}_{}_{}", path, method, desc)); - let item = Schema { - schema_data: SchemaData { - nullable: false, - read_only: false, - write_only: false, - deprecated: false, - external_docs: None, - example: None, - title: Some(name.to_string()), - description: None, - discriminator: None, - default: None - }, - schema_kind: T::to_schema() - }; + let schema = T::to_schema(); + let name = schema.name.clone().unwrap_or_else(|| format!("path_{}_{}_{}", path, method, desc)); + let item = schema.to_schema(); match &mut self.0.components { Some(comp) => { comp.schemas.insert(name.to_string(), Item(item)); @@ -181,10 +168,7 @@ fn new_operation(schema : &str, path_params : Vec<&str>, body_schema : Option<&s description: None, required: true, deprecated: None, - format: ParameterSchemaOrContent::Schema(Item(Schema { - schema_data: SchemaData::default(), - schema_kind: String::to_schema() - })), + format: ParameterSchemaOrContent::Schema(Item(String::to_schema().to_schema())), example: None, examples: IndexMap::new() }, diff --git a/src/openapi/types.rs b/src/openapi/types.rs index db26d67..cce241d 100644 --- a/src/openapi/types.rs +++ b/src/openapi/types.rs @@ -3,34 +3,68 @@ use chrono::{ Date, DateTime, FixedOffset, Local, NaiveDate, NaiveDateTime, Utc }; use openapiv3::{ - ArrayType, IntegerType, NumberType, ObjectType, ReferenceOr::Item, Schema, SchemaData, SchemaKind, - StringFormat, StringType, Type, VariantOrUnknownOrEmpty + ArrayType, IntegerType, NumberType, ObjectType, ReferenceOr::Item, ReferenceOr::Reference, Schema, + SchemaData, SchemaKind, StringFormat, StringType, Type, VariantOrUnknownOrEmpty }; +#[derive(Debug, Clone, PartialEq)] +pub struct OpenapiSchema +{ + /// The name of this schema. If it is None, the schema will be inlined. + pub name : Option<String>, + pub nullable : bool, + pub schema : SchemaKind +} + +impl OpenapiSchema +{ + pub fn new(schema : SchemaKind) -> Self + { + Self { + name: None, + nullable: false, + schema + } + } + + pub fn to_schema(self) -> Schema + { + Schema { + schema_data: SchemaData { + nullable: self.nullable, + read_only: false, + write_only: false, + deprecated: false, + external_docs: None, + example: None, + title: self.name, + description: None, + discriminator: None, + default: None + }, + schema_kind: self.schema + } + } +} pub trait OpenapiType { - fn schema_name() -> Option<String> - { - None - } - - fn to_schema() -> SchemaKind; + fn to_schema() -> OpenapiSchema; } impl OpenapiType for () { - fn to_schema() -> SchemaKind + fn to_schema() -> OpenapiSchema { - SchemaKind::Type(Type::Object(ObjectType::default())) + OpenapiSchema::new(SchemaKind::Type(Type::Object(ObjectType::default()))) } } impl OpenapiType for bool { - fn to_schema() -> SchemaKind + fn to_schema() -> OpenapiSchema { - SchemaKind::Type(Type::Boolean{}) + OpenapiSchema::new(SchemaKind::Type(Type::Boolean{})) } } @@ -38,9 +72,9 @@ macro_rules! int_types { ($($int_ty:ty),*) => {$( impl OpenapiType for $int_ty { - fn to_schema() -> SchemaKind + fn to_schema() -> OpenapiSchema { - SchemaKind::Type(Type::Integer(IntegerType::default())) + OpenapiSchema::new(SchemaKind::Type(Type::Integer(IntegerType::default()))) } } )*} @@ -52,9 +86,9 @@ macro_rules! num_types { ($($num_ty:ty),*) => {$( impl OpenapiType for $num_ty { - fn to_schema() -> SchemaKind + fn to_schema() -> OpenapiSchema { - SchemaKind::Type(Type::Number(NumberType::default())) + OpenapiSchema::new(SchemaKind::Type(Type::Number(NumberType::default()))) } } )*} @@ -66,9 +100,9 @@ macro_rules! str_types { ($($str_ty:ty),*) => {$( impl OpenapiType for $str_ty { - fn to_schema() -> SchemaKind + fn to_schema() -> OpenapiSchema { - SchemaKind::Type(Type::String(StringType::default())) + OpenapiSchema::new(SchemaKind::Type(Type::String(StringType::default()))) } } )*}; @@ -76,13 +110,13 @@ macro_rules! str_types { (format = $format:ident, $($str_ty:ty),*) => {$( impl OpenapiType for $str_ty { - fn to_schema() -> SchemaKind + fn to_schema() -> OpenapiSchema { - SchemaKind::Type(Type::String(StringType { + OpenapiSchema::new(SchemaKind::Type(Type::String(StringType { format: VariantOrUnknownOrEmpty::Item(StringFormat::$format), pattern: None, enumeration: Vec::new() - })) + }))) } } )*}; @@ -92,33 +126,18 @@ str_types!(String, &str); impl<T : OpenapiType> OpenapiType for Vec<T> { - fn schema_name() -> Option<String> + fn to_schema() -> OpenapiSchema { - T::schema_name().map(|name| format!("{}Array", name)) - } - - fn to_schema() -> SchemaKind - { - SchemaKind::Type(Type::Array(ArrayType { - items: Item(Box::new(Schema { - schema_data: SchemaData { - nullable: false, - read_only: false, - write_only: false, - deprecated: false, - external_docs: None, - example: None, - title: T::schema_name(), - description: None, - discriminator: None, - default: None - }, - schema_kind: T::to_schema() - })), + let schema = T::to_schema(); + OpenapiSchema::new(SchemaKind::Type(Type::Array(ArrayType { + items: match schema.name { + Some(name) => Reference { reference: format!("#/components/schemas/{}", name) }, + None => Item(Box::new(schema.to_schema())) + }, min_items: None, max_items: None, unique_items: false - })) + }))) } } diff --git a/src/result.rs b/src/result.rs index 3f8eed1..b73bee5 100644 --- a/src/result.rs +++ b/src/result.rs @@ -1,6 +1,6 @@ use crate::{ResourceType, StatusCode}; #[cfg(feature = "openapi")] -use openapiv3::SchemaKind; +use crate::OpenapiSchema; use serde::Serialize; use serde_json::error::Error as SerdeJsonError; use std::error::Error; @@ -11,21 +11,13 @@ pub trait ResourceResult fn to_json(&self) -> Result<(StatusCode, String), SerdeJsonError>; #[cfg(feature = "openapi")] - fn schema_name() -> Option<String>; - - #[cfg(feature = "openapi")] - fn to_schema() -> SchemaKind; + fn to_schema() -> OpenapiSchema; } #[cfg(feature = "openapi")] impl<Res : ResourceResult> crate::OpenapiType for Res { - fn schema_name() -> Option<String> - { - Self::schema_name() - } - - fn to_schema() -> SchemaKind + fn to_schema() -> OpenapiSchema { Self::to_schema() } @@ -64,13 +56,7 @@ impl<R : ResourceType, E : Error> ResourceResult for Result<R, E> } #[cfg(feature = "openapi")] - fn schema_name() -> Option<String> - { - R::schema_name() - } - - #[cfg(feature = "openapi")] - fn to_schema() -> SchemaKind + fn to_schema() -> OpenapiSchema { R::to_schema() } @@ -95,13 +81,7 @@ impl<T : ResourceType> ResourceResult for Success<T> } #[cfg(feature = "openapi")] - fn schema_name() -> Option<String> - { - T::schema_name() - } - - #[cfg(feature = "openapi")] - fn to_schema() -> SchemaKind + fn to_schema() -> OpenapiSchema { T::to_schema() } From 3427fb4c6fdb15a607f6fa0f64d7016005391dea Mon Sep 17 00:00:00 2001 From: Dominic <git@msrd0.de> Date: Tue, 1 Oct 2019 16:13:13 +0200 Subject: [PATCH 14/20] add dependencies to schema --- examples/users.rs | 8 ++-- src/helper.rs | 31 +++++++++++++--- src/openapi/router.rs | 86 ++++++++++++++++++++++++++++--------------- src/openapi/types.rs | 66 +++++++++++++++++++++++++++------ 4 files changed, 141 insertions(+), 50 deletions(-) diff --git a/examples/users.rs b/examples/users.rs index 1f169b8..25c6fb9 100644 --- a/examples/users.rs +++ b/examples/users.rs @@ -27,14 +27,14 @@ rest_struct!{User { username : String }} -impl ResourceReadAll<Success<Vec<User>>> for Users +impl ResourceReadAll<Success<Vec<Option<User>>>> for Users { - fn read_all(_state : &mut State) -> Success<Vec<User>> + fn read_all(_state : &mut State) -> Success<Vec<Option<User>>> { vec![Username().fake(), Username().fake()] .into_iter() - .map(|username| User { username }) - .collect::<Vec<User>>() + .map(|username| Some(User { username })) + .collect::<Vec<Option<User>>>() .into() } } diff --git a/src/helper.rs b/src/helper.rs index 4b2789e..0a5fb1e 100644 --- a/src/helper.rs +++ b/src/helper.rs @@ -35,12 +35,32 @@ macro_rules! rest_struct { let mut properties : IndexMap<String, ReferenceOr<Box<Schema>>> = IndexMap::new(); let mut required : Vec<String> = Vec::new(); + let mut dependencies : IndexMap<String, OpenapiSchema> = IndexMap::new(); $( - properties.insert( - stringify!($field_id).to_string(), - ReferenceOr::Item(Box::new(<$field_ty>::to_schema().to_schema())) - ); + { + let mut schema = <$field_ty>::to_schema(); + if let Some(name) = schema.name.clone() + { + properties.insert( + stringify!($field_id).to_string(), + ReferenceOr::Reference { reference: format!("#/components/schemas/{}", name) } + ); + if schema.nullable + { + required.push(stringify!($field_id).to_string()); + schema.nullable = false; + } + dependencies.insert(name, schema); + } + else + { + properties.insert( + stringify!($field_id).to_string(), + ReferenceOr::Item(Box::new(<$field_ty>::to_schema().to_schema())) + ); + } + } )* let schema = SchemaKind::Type(Type::Object(ObjectType { @@ -54,7 +74,8 @@ macro_rules! rest_struct { OpenapiSchema { name: Some(stringify!($struct_name).to_string()), nullable: false, - schema + schema, + dependencies } } } diff --git a/src/openapi/router.rs b/src/openapi/router.rs index 1bade2e..bc9cb60 100644 --- a/src/openapi/router.rs +++ b/src/openapi/router.rs @@ -2,6 +2,7 @@ use crate::{ resource::*, result::*, routing::*, + OpenapiSchema, OpenapiType, ResourceType }; @@ -19,7 +20,7 @@ use mime::{APPLICATION_JSON, TEXT_PLAIN}; use openapiv3::{ Components, MediaType, OpenAPI, Operation, Parameter, ParameterData, ParameterSchemaOrContent, PathItem, PathStyle, Paths, ReferenceOr, ReferenceOr::Item, ReferenceOr::Reference, RequestBody, Response, Responses, - Server, StatusCode + Schema, Server, StatusCode }; use serde::de::DeserializeOwned; use std::panic::RefUnwindSafe; @@ -69,22 +70,49 @@ impl OpenapiRouter self.0.paths.insert(path.to_string(), Item(item)); } - fn add_schema<T : OpenapiType>(&mut self, path : &str, method : &str, desc : &str) -> String + fn add_schema_impl(&mut self, name : String, mut schema : OpenapiSchema) { - let schema = T::to_schema(); - let name = schema.name.clone().unwrap_or_else(|| format!("path_{}_{}_{}", path, method, desc)); - let item = schema.to_schema(); + self.add_schema_dependencies(&mut schema.dependencies); + match &mut self.0.components { Some(comp) => { - comp.schemas.insert(name.to_string(), Item(item)); + comp.schemas.insert(name, Item(schema.to_schema())); }, None => { let mut comp = Components::default(); - comp.schemas.insert(name.to_string(), Item(item)); + comp.schemas.insert(name, Item(schema.to_schema())); self.0.components = Some(comp); } }; - name + } + + fn add_schema_dependencies(&mut self, dependencies : &mut IndexMap<String, OpenapiSchema>) + { + let keys : Vec<String> = dependencies.keys().map(|k| k.to_string()).collect(); + for dep in keys + { + let dep_schema = dependencies.swap_remove(&dep); + if let Some(dep_schema) = dep_schema + { + self.add_schema_impl(dep, dep_schema); + } + } + } + + fn add_schema<T : OpenapiType>(&mut self) -> ReferenceOr<Schema> + { + let mut schema = T::to_schema(); + if let Some(name) = schema.name.clone() + { + let reference = Reference { reference: format!("#/components/schemas/{}", name) }; + self.add_schema_impl(name, schema); + reference + } + else + { + self.add_schema_dependencies(&mut schema.dependencies); + Item(schema.to_schema()) + } } } @@ -135,13 +163,11 @@ pub trait GetOpenapi fn get_openapi(&mut self, path : &str); } -fn schema_to_content(schema : &str) -> IndexMap<String, MediaType> +fn schema_to_content(schema : ReferenceOr<Schema>) -> IndexMap<String, MediaType> { let mut content : IndexMap<String, MediaType> = IndexMap::new(); content.insert(APPLICATION_JSON.to_string(), MediaType { - schema: Some(Reference { - reference: format!("#/components/schemas/{}", schema) - }), + schema: Some(schema), example: None, examples: IndexMap::new(), encoding: IndexMap::new() @@ -149,7 +175,7 @@ fn schema_to_content(schema : &str) -> IndexMap<String, MediaType> content } -fn new_operation(schema : &str, path_params : Vec<&str>, body_schema : Option<&str>) -> Operation +fn new_operation(schema : ReferenceOr<Schema>, path_params : Vec<&str>, body_schema : Option<ReferenceOr<Schema>>) -> Operation { let mut responses : IndexMap<StatusCode, ReferenceOr<Response>> = IndexMap::new(); responses.insert(StatusCode::Code(200), Item(Response { @@ -235,11 +261,11 @@ macro_rules! implOpenapiRouter { Res : ResourceResult, Handler : ResourceReadAll<Res> { - let schema = (self.0).1.add_schema::<Res>(&self.1, "read_all", "result_body"); + let schema = (self.0).1.add_schema::<Res>(); let path = format!("/{}", &self.1); let mut item = (self.0).1.remove_path(&path); - item.get = Some(new_operation(&schema, vec![], None)); + item.get = Some(new_operation(schema, vec![], None)); (self.0).1.add_path(path, item); (&mut *(self.0).0, self.1.to_string()).read_all::<Handler, Res>() @@ -251,11 +277,11 @@ macro_rules! implOpenapiRouter { Res : ResourceResult, Handler : ResourceRead<ID, Res> { - let schema = (self.0).1.add_schema::<Res>(&self.1, "read", "result_body"); + let schema = (self.0).1.add_schema::<Res>(); let path = format!("/{}/{{id}}", &self.1); let mut item = (self.0).1.remove_path(&path); - item.get = Some(new_operation(&schema, vec!["id"], None)); + item.get = Some(new_operation(schema, vec!["id"], None)); (self.0).1.add_path(path, item); (&mut *(self.0).0, self.1.to_string()).read::<Handler, ID, Res>() @@ -267,12 +293,12 @@ macro_rules! implOpenapiRouter { Res : ResourceResult, Handler : ResourceCreate<Body, Res> { - let schema = (self.0).1.add_schema::<Res>(&self.1, "create", "result_body"); - let body_schema = (self.0).1.add_schema::<Body>(&self.1, "create", "body"); + let schema = (self.0).1.add_schema::<Res>(); + let body_schema = (self.0).1.add_schema::<Body>(); let path = format!("/{}", &self.1); let mut item = (self.0).1.remove_path(&path); - item.post = Some(new_operation(&schema, vec![], Some(&body_schema))); + item.post = Some(new_operation(schema, vec![], Some(body_schema))); (self.0).1.add_path(path, item); (&mut *(self.0).0, self.1.to_string()).create::<Handler, Body, Res>() @@ -284,12 +310,12 @@ macro_rules! implOpenapiRouter { Res : ResourceResult, Handler : ResourceUpdateAll<Body, Res> { - let schema = (self.0).1.add_schema::<Res>(&self.1, "update_all", "result_body"); - let body_schema = (self.0).1.add_schema::<Body>(&self.1, "create", "body"); + let schema = (self.0).1.add_schema::<Res>(); + let body_schema = (self.0).1.add_schema::<Body>(); let path = format!("/{}", &self.1); let mut item = (self.0).1.remove_path(&path); - item.put = Some(new_operation(&schema, vec![], Some(&body_schema))); + item.put = Some(new_operation(schema, vec![], Some(body_schema))); (self.0).1.add_path(path, item); (&mut *(self.0).0, self.1.to_string()).update_all::<Handler, Body, Res>() @@ -302,12 +328,12 @@ macro_rules! implOpenapiRouter { Res : ResourceResult, Handler : ResourceUpdate<ID, Body, Res> { - let schema = (self.0).1.add_schema::<Res>(&self.1, "update", "result_body"); - let body_schema = (self.0).1.add_schema::<Body>(&self.1, "create", "body"); + let schema = (self.0).1.add_schema::<Res>(); + let body_schema = (self.0).1.add_schema::<Body>(); let path = format!("/{}/{{id}}", &self.1); let mut item = (self.0).1.remove_path(&path); - item.put = Some(new_operation(&schema, vec!["id"], Some(&body_schema))); + item.put = Some(new_operation(schema, vec!["id"], Some(body_schema))); (self.0).1.add_path(path, item); (&mut *(self.0).0, self.1.to_string()).update::<Handler, ID, Body, Res>() @@ -318,11 +344,11 @@ macro_rules! implOpenapiRouter { Res : ResourceResult, Handler : ResourceDeleteAll<Res> { - let schema = (self.0).1.add_schema::<Res>(&self.1, "delete_all", "result_body"); + let schema = (self.0).1.add_schema::<Res>(); let path = format!("/{}", &self.1); let mut item = (self.0).1.remove_path(&path); - item.delete = Some(new_operation(&schema, vec![], None)); + item.delete = Some(new_operation(schema, vec![], None)); (self.0).1.add_path(path, item); (&mut *(self.0).0, self.1.to_string()).delete_all::<Handler, Res>() @@ -334,11 +360,11 @@ macro_rules! implOpenapiRouter { Res : ResourceResult, Handler : ResourceDelete<ID, Res> { - let schema = (self.0).1.add_schema::<Res>(&self.1, "delete", "result_body"); + let schema = (self.0).1.add_schema::<Res>(); let path = format!("/{}/{{id}}", &self.1); let mut item = (self.0).1.remove_path(&path); - item.delete = Some(new_operation(&schema, vec!["id"], None)); + item.delete = Some(new_operation(schema, vec!["id"], None)); (self.0).1.add_path(path, item); (&mut *(self.0).0, self.1.to_string()).delete::<Handler, ID, Res>() diff --git a/src/openapi/types.rs b/src/openapi/types.rs index cce241d..5e08d60 100644 --- a/src/openapi/types.rs +++ b/src/openapi/types.rs @@ -2,6 +2,7 @@ use chrono::{ Date, DateTime, FixedOffset, Local, NaiveDate, NaiveDateTime, Utc }; +use indexmap::IndexMap; use openapiv3::{ ArrayType, IntegerType, NumberType, ObjectType, ReferenceOr::Item, ReferenceOr::Reference, Schema, SchemaData, SchemaKind, StringFormat, StringType, Type, VariantOrUnknownOrEmpty @@ -13,7 +14,8 @@ pub struct OpenapiSchema /// The name of this schema. If it is None, the schema will be inlined. pub name : Option<String>, pub nullable : bool, - pub schema : SchemaKind + pub schema : SchemaKind, + pub dependencies : IndexMap<String, OpenapiSchema> } impl OpenapiSchema @@ -23,7 +25,8 @@ impl OpenapiSchema Self { name: None, nullable: false, - schema + schema, + dependencies: IndexMap::new() } } @@ -124,20 +127,61 @@ macro_rules! str_types { str_types!(String, &str); +impl<T : OpenapiType> OpenapiType for Option<T> +{ + fn to_schema() -> OpenapiSchema + { + let schema = T::to_schema(); + let mut dependencies : IndexMap<String, OpenapiSchema> = IndexMap::new(); + let refor = if let Some(name) = schema.name.clone() + { + let reference = Reference { reference: format!("#/components/schemas/{}", name) }; + dependencies.insert(name, schema); + reference + } + else + { + Item(schema.to_schema()) + }; + + OpenapiSchema { + nullable: true, + name: None, + schema: SchemaKind::AllOf { all_of: vec![refor] }, + dependencies + } + } +} + impl<T : OpenapiType> OpenapiType for Vec<T> { fn to_schema() -> OpenapiSchema { let schema = T::to_schema(); - OpenapiSchema::new(SchemaKind::Type(Type::Array(ArrayType { - items: match schema.name { - Some(name) => Reference { reference: format!("#/components/schemas/{}", name) }, - None => Item(Box::new(schema.to_schema())) - }, - min_items: None, - max_items: None, - unique_items: false - }))) + let mut dependencies : IndexMap<String, OpenapiSchema> = IndexMap::new(); + + let items = if let Some(name) = schema.name.clone() + { + let reference = Reference { reference: format!("#/components/schemas/{}", name) }; + dependencies.insert(name, schema); + reference + } + else + { + Item(Box::new(schema.to_schema())) + }; + + OpenapiSchema { + nullable: false, + name: None, + schema: SchemaKind::Type(Type::Array(ArrayType { + items, + min_items: None, + max_items: None, + unique_items: false + })), + dependencies + } } } From a4185a5665bb4cca9dda18cd1b23fc80d3542a12 Mon Sep 17 00:00:00 2001 From: Dominic <git@msrd0.de> Date: Tue, 1 Oct 2019 18:03:44 +0200 Subject: [PATCH 15/20] fix bug: required is now properly populated --- src/helper.rs | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/helper.rs b/src/helper.rs index 0a5fb1e..d4c7fcc 100644 --- a/src/helper.rs +++ b/src/helper.rs @@ -40,17 +40,26 @@ macro_rules! rest_struct { $( { let mut schema = <$field_ty>::to_schema(); + + if schema.nullable + { + schema.nullable = false; + schema.name = schema.name.map(|name| + if name.ends_with("OrNull") { + name[..(name.len()-6)].to_string() + } else { name }); + } + else + { + required.push(stringify!($field_id).to_string()); + } + if let Some(name) = schema.name.clone() { properties.insert( stringify!($field_id).to_string(), ReferenceOr::Reference { reference: format!("#/components/schemas/{}", name) } ); - if schema.nullable - { - required.push(stringify!($field_id).to_string()); - schema.nullable = false; - } dependencies.insert(name, schema); } else From 4ef216e8c8f858aee7b6f7000102bad1ab0e08d2 Mon Sep 17 00:00:00 2001 From: Dominic <git@msrd0.de> Date: Wed, 2 Oct 2019 10:59:25 +0200 Subject: [PATCH 16/20] add proc macro derive for openapitype --- Cargo.lock | 39 ++++++-- Cargo.toml | 43 ++------ example/Cargo.toml | 28 ++++++ example/LICENSE | 24 +++++ examples/users.rs => example/src/main.rs | 7 +- gotham_restful/Cargo.toml | 34 +++++++ {src => gotham_restful/src}/helper.rs | 0 {src => gotham_restful/src}/lib.rs | 0 {src => gotham_restful/src}/openapi/mod.rs | 0 {src => gotham_restful/src}/openapi/router.rs | 0 {src => gotham_restful/src}/openapi/types.rs | 0 {src => gotham_restful/src}/resource.rs | 0 {src => gotham_restful/src}/result.rs | 0 {src => gotham_restful/src}/routing.rs | 0 gotham_restful_derive/Cargo.toml | 33 +++++++ gotham_restful_derive/src/lib.rs | 13 +++ gotham_restful_derive/src/openapi_type.rs | 99 +++++++++++++++++++ 17 files changed, 273 insertions(+), 47 deletions(-) create mode 100644 example/Cargo.toml create mode 100644 example/LICENSE rename examples/users.rs => example/src/main.rs (95%) create mode 100644 gotham_restful/Cargo.toml rename {src => gotham_restful/src}/helper.rs (100%) rename {src => gotham_restful/src}/lib.rs (100%) rename {src => gotham_restful/src}/openapi/mod.rs (100%) rename {src => gotham_restful/src}/openapi/router.rs (100%) rename {src => gotham_restful/src}/openapi/types.rs (100%) rename {src => gotham_restful/src}/resource.rs (100%) rename {src => gotham_restful/src}/result.rs (100%) rename {src => gotham_restful/src}/routing.rs (100%) create mode 100644 gotham_restful_derive/Cargo.toml create mode 100644 gotham_restful_derive/src/lib.rs create mode 100644 gotham_restful_derive/src/openapi_type.rs diff --git a/Cargo.lock b/Cargo.lock index 944d6ed..0a2bcfc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -213,6 +213,19 @@ dependencies = [ "version_check 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "example" +version = "0.0.1" +dependencies = [ + "fake 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "gotham 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "gotham_restful 0.0.1", + "gotham_restful_derive 0.0.1", + "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "log4rs 0.8.3 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.101 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "failure" version = "0.1.5" @@ -328,19 +341,26 @@ dependencies = [ ] [[package]] -name = "gotham-restful" +name = "gotham_derive" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "quote 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 0.15.44 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "gotham_restful" version = "0.0.1" dependencies = [ "chrono 0.4.9 (registry+https://github.com/rust-lang/crates.io-index)", "failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", - "fake 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "futures 0.1.29 (registry+https://github.com/rust-lang/crates.io-index)", "gotham 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "gotham_derive 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "hyper 0.12.35 (registry+https://github.com/rust-lang/crates.io-index)", "indexmap 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", - "log4rs 0.8.3 (registry+https://github.com/rust-lang/crates.io-index)", "mime 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)", "openapiv3 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.101 (registry+https://github.com/rust-lang/crates.io-index)", @@ -348,12 +368,15 @@ dependencies = [ ] [[package]] -name = "gotham_derive" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" +name = "gotham_restful_derive" +version = "0.0.1" dependencies = [ - "quote 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 0.15.44 (registry+https://github.com/rust-lang/crates.io-index)", + "fake 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "log4rs 0.8.3 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 0803aff..a7b19e1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,39 +1,8 @@ # -*- eval: (cargo-minor-mode 1) -*- -[package] -name = "gotham-restful" -version = "0.0.1" -authors = ["Dominic Meiser <git@msrd0.de>"] -edition = "2018" -description = "RESTful additions for Gotham" -keywords = ["gotham", "rest", "restful"] -license = "EPL-2.0" -readme = "README.md" -include = ["src/**/*", "Cargo.toml", "LICENSE"] -repository = "https://gitlab.com/msrd0/gotham-restful" - -[badges] -gitlab = { repository = "msrd0/gotham-restful", branch = "master" } - -[dependencies] -chrono = { version = "0.4", optional = true } -failure = "0.1" -futures = "0.1" -gotham = "0.4" -gotham_derive = "0.4" -hyper = "0.12" -indexmap = { version = "1.0", optional = true } -log = { version = "0.4", optional = true } -mime = "0.3" -openapiv3 = { version = "0.3", optional = true } -serde = { version = "1", features = ["derive"] } -serde_json = "1" - -[dev-dependencies] -fake = "2.2" -log = "0.4" -log4rs = { version = "0.8", features = ["console_appender"], default-features = false } - -[features] -default = ["openapi", "chrono"] -openapi = ["indexmap", "log", "openapiv3"] +[workspace] +members = [ + "gotham_restful", + "gotham_restful_derive", + "example" +] diff --git a/example/Cargo.toml b/example/Cargo.toml new file mode 100644 index 0000000..0226424 --- /dev/null +++ b/example/Cargo.toml @@ -0,0 +1,28 @@ +# -*- eval: (cargo-minor-mode 1) -*- + +[package] +name = "example" +version = "0.0.1" +authors = ["Dominic Meiser <git@msrd0.de>"] +edition = "2018" +license = "Unlicense" +readme = "README.md" +include = ["src/**/*", "Cargo.toml", "LICENSE"] +repository = "https://gitlab.com/msrd0/gotham-restful" + +[badges] +gitlab = { repository = "msrd0/gotham-restful", branch = "master" } + +[dependencies] +fake = "2.2" +gotham = "0.4" +gotham_restful = { path = "../gotham_restful", features = ["openapi"] } +gotham_restful_derive = { path = "../gotham_restful_derive", features = ["openapi"] } +log = "0.4" +log4rs = { version = "0.8", features = ["console_appender"], default-features = false } +serde = "1" + +[dev-dependencies] +fake = "2.2" +log = "0.4" +log4rs = { version = "0.8", features = ["console_appender"], default-features = false } diff --git a/example/LICENSE b/example/LICENSE new file mode 100644 index 0000000..cf1ab25 --- /dev/null +++ b/example/LICENSE @@ -0,0 +1,24 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to <http://unlicense.org> diff --git a/examples/users.rs b/example/src/main.rs similarity index 95% rename from examples/users.rs rename to example/src/main.rs index 25c6fb9..631368e 100644 --- a/examples/users.rs +++ b/example/src/main.rs @@ -1,4 +1,5 @@ #[macro_use] extern crate log; +#[macro_use] extern crate gotham_restful_derive; use fake::{faker::internet::en::Username, Fake}; use gotham::{ @@ -14,6 +15,7 @@ use log4rs::{ config::{Appender, Config, Root}, encode::pattern::PatternEncoder }; +use serde::{Deserialize, Serialize}; rest_resource!{Users, route => { route.read_all::<Self, _>(); @@ -23,9 +25,10 @@ rest_resource!{Users, route => { route.update::<Self, _, _, _>(); }} -rest_struct!{User { +#[derive(Deserialize, OpenapiType, Serialize)] +struct User { username : String -}} +} impl ResourceReadAll<Success<Vec<Option<User>>>> for Users { diff --git a/gotham_restful/Cargo.toml b/gotham_restful/Cargo.toml new file mode 100644 index 0000000..0b6b146 --- /dev/null +++ b/gotham_restful/Cargo.toml @@ -0,0 +1,34 @@ +# -*- eval: (cargo-minor-mode 1) -*- + +[package] +name = "gotham_restful" +version = "0.0.1" +authors = ["Dominic Meiser <git@msrd0.de>"] +edition = "2018" +description = "RESTful additions for Gotham" +keywords = ["gotham", "rest", "restful"] +license = "EPL-2.0" +readme = "../README.md" +include = ["src/**/*", "Cargo.toml", "../LICENSE"] +repository = "https://gitlab.com/msrd0/gotham-restful" + +[badges] +gitlab = { repository = "msrd0/gotham-restful", branch = "master" } + +[dependencies] +chrono = { version = "0.4", optional = true } +failure = "0.1" +futures = "0.1" +gotham = "0.4" +gotham_derive = "0.4" +hyper = "0.12" +indexmap = { version = "1.0", optional = true } +log = { version = "0.4", optional = true } +mime = "0.3" +openapiv3 = { version = "0.3", optional = true } +serde = { version = "1", features = ["derive"] } +serde_json = "1" + +[features] +default = ["openapi", "chrono"] +openapi = ["indexmap", "log", "openapiv3"] diff --git a/src/helper.rs b/gotham_restful/src/helper.rs similarity index 100% rename from src/helper.rs rename to gotham_restful/src/helper.rs diff --git a/src/lib.rs b/gotham_restful/src/lib.rs similarity index 100% rename from src/lib.rs rename to gotham_restful/src/lib.rs diff --git a/src/openapi/mod.rs b/gotham_restful/src/openapi/mod.rs similarity index 100% rename from src/openapi/mod.rs rename to gotham_restful/src/openapi/mod.rs diff --git a/src/openapi/router.rs b/gotham_restful/src/openapi/router.rs similarity index 100% rename from src/openapi/router.rs rename to gotham_restful/src/openapi/router.rs diff --git a/src/openapi/types.rs b/gotham_restful/src/openapi/types.rs similarity index 100% rename from src/openapi/types.rs rename to gotham_restful/src/openapi/types.rs diff --git a/src/resource.rs b/gotham_restful/src/resource.rs similarity index 100% rename from src/resource.rs rename to gotham_restful/src/resource.rs diff --git a/src/result.rs b/gotham_restful/src/result.rs similarity index 100% rename from src/result.rs rename to gotham_restful/src/result.rs diff --git a/src/routing.rs b/gotham_restful/src/routing.rs similarity index 100% rename from src/routing.rs rename to gotham_restful/src/routing.rs diff --git a/gotham_restful_derive/Cargo.toml b/gotham_restful_derive/Cargo.toml new file mode 100644 index 0000000..0f78279 --- /dev/null +++ b/gotham_restful_derive/Cargo.toml @@ -0,0 +1,33 @@ +# -*- eval: (cargo-minor-mode 1) -*- + +[package] +name = "gotham_restful_derive" +version = "0.0.1" +authors = ["Dominic Meiser <git@msrd0.de>"] +edition = "2018" +description = "RESTful additions for Gotham - Derive" +keywords = ["gotham", "rest", "restful", "derive"] +license = "EPL-2.0" +readme = "../README.md" +include = ["src/**/*", "Cargo.toml", "../LICENSE"] +repository = "https://gitlab.com/msrd0/gotham-restful" + +[lib] +proc-macro = true + +[badges] +gitlab = { repository = "msrd0/gotham-restful", branch = "master" } + +[dependencies] +proc-macro2 = "1" +quote = "1" +syn = { version = "1", features = ["extra-traits", "full"] } + +[dev-dependencies] +fake = "2.2" +log = "0.4" +log4rs = { version = "0.8", features = ["console_appender"], default-features = false } + +[features] +default = ["openapi"] +openapi = [] diff --git a/gotham_restful_derive/src/lib.rs b/gotham_restful_derive/src/lib.rs new file mode 100644 index 0000000..3cddc5c --- /dev/null +++ b/gotham_restful_derive/src/lib.rs @@ -0,0 +1,13 @@ +extern crate proc_macro; + +use proc_macro::TokenStream; + +#[cfg(feature = "openapi")] +mod openapi_type; + +#[cfg(feature = "openapi")] +#[proc_macro_derive(OpenapiType)] +pub fn derive_openapi_type(tokens : TokenStream) -> TokenStream +{ + openapi_type::expand(tokens) +} diff --git a/gotham_restful_derive/src/openapi_type.rs b/gotham_restful_derive/src/openapi_type.rs new file mode 100644 index 0000000..d24753c --- /dev/null +++ b/gotham_restful_derive/src/openapi_type.rs @@ -0,0 +1,99 @@ +use proc_macro::TokenStream; +use proc_macro2::TokenStream as TokenStream2; +use quote::quote; +use syn::{ + Field, + Fields, + ItemStruct, + parse_macro_input +}; + +fn expand_field(field : &Field) -> TokenStream2 +{ + let ident = match &field.ident { + Some(ident) => ident, + None => panic!("Fields without ident are not supported") + }; + let ty = &field.ty; + + quote! {{ + let mut schema = <#ty>::to_schema(); + + if schema.nullable + { + schema.nullable = false; + schema.name = schema.name.map(|name| + if name.ends_with("OrNull") { name[..(name.len()-6)].to_string() } else { name }); + } + else + { + required.push(stringify!(#ident).to_string()); + } + + match schema.name.clone() { + Some(name) => { + properties.insert( + stringify!(#ident).to_string(), + ReferenceOr::Reference { reference: format!("#/components/schemas/{}", name) } + ); + dependencies.insert(name, schema); + }, + None => { + properties.insert( + stringify!(#ident).to_string(), + ReferenceOr::Item(Box::new(<#ty>::to_schema().to_schema())) + ); + } + } + }} +} + +pub fn expand(tokens : proc_macro::TokenStream) -> TokenStream +{ + let input = parse_macro_input!(tokens as ItemStruct); + + let ident = input.ident; + let generics = input.generics; + + let fields : Vec<TokenStream2> = match input.fields { + Fields::Named(fields) => { + fields.named.iter().map(|field| expand_field(field)).collect() + }, + Fields::Unnamed(_) => panic!("Unnamed fields are not supported"), + Fields::Unit => Vec::new() + }; + + let output = quote!{ + impl #generics ::gotham_restful::OpenapiType for #ident #generics + { + fn to_schema() -> ::gotham_restful::OpenapiSchema + { + use ::gotham_restful::{helper::openapi::*, OpenapiSchema}; + + let mut properties : IndexMap<String, ReferenceOr<Box<Schema>>> = IndexMap::new(); + let mut required : Vec<String> = Vec::new(); + let mut dependencies : IndexMap<String, OpenapiSchema> = IndexMap::new(); + + #(#fields)* + + let schema = SchemaKind::Type(Type::Object(ObjectType { + properties, + required, + additional_properties: None, + min_properties: None, + max_properties: None + })); + + OpenapiSchema { + name: Some(stringify!(#ident).to_string()), + nullable: false, + schema, + dependencies + } + } + } + }; + + eprintln!("output: {}", output); + output.into() +} From 0b11aaf1f9760709691580fc5197db9d4c60959f Mon Sep 17 00:00:00 2001 From: Dominic <git@msrd0.de> Date: Wed, 2 Oct 2019 11:09:10 +0200 Subject: [PATCH 17/20] less verbose --- gotham_restful_derive/src/openapi_type.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/gotham_restful_derive/src/openapi_type.rs b/gotham_restful_derive/src/openapi_type.rs index d24753c..7ab40f7 100644 --- a/gotham_restful_derive/src/openapi_type.rs +++ b/gotham_restful_derive/src/openapi_type.rs @@ -48,7 +48,7 @@ fn expand_field(field : &Field) -> TokenStream2 }} } -pub fn expand(tokens : proc_macro::TokenStream) -> TokenStream +pub fn expand(tokens : TokenStream) -> TokenStream { let input = parse_macro_input!(tokens as ItemStruct); @@ -94,6 +94,5 @@ pub fn expand(tokens : proc_macro::TokenStream) -> TokenStream } }; - eprintln!("output: {}", output); output.into() } From 50ed2411c9973d3b3ffb26790cce1bf6e07c561a Mon Sep 17 00:00:00 2001 From: Dominic <git@msrd0.de> Date: Thu, 3 Oct 2019 00:04:33 +0200 Subject: [PATCH 18/20] allow deriving enums --- example/src/main.rs | 17 ++++-- gotham_restful/src/helper.rs | 2 +- gotham_restful_derive/src/openapi_type.rs | 66 ++++++++++++++++++++--- 3 files changed, 73 insertions(+), 12 deletions(-) diff --git a/example/src/main.rs b/example/src/main.rs index 631368e..3faee12 100644 --- a/example/src/main.rs +++ b/example/src/main.rs @@ -26,8 +26,17 @@ rest_resource!{Users, route => { }} #[derive(Deserialize, OpenapiType, Serialize)] -struct User { - username : String +enum TestEnum +{ + Foo, + Bar +} + +#[derive(Deserialize, OpenapiType, Serialize)] +struct User +{ + username : String, + test : Option<TestEnum> } impl ResourceReadAll<Success<Vec<Option<User>>>> for Users @@ -36,7 +45,7 @@ impl ResourceReadAll<Success<Vec<Option<User>>>> for Users { vec![Username().fake(), Username().fake()] .into_iter() - .map(|username| Some(User { username })) + .map(|username| Some(User { username, test: None })) .collect::<Vec<Option<User>>>() .into() } @@ -47,7 +56,7 @@ impl ResourceRead<u64, Success<User>> for Users fn read(_state : &mut State, id : u64) -> Success<User> { let username : String = Username().fake(); - User { username: format!("{}{}", username, id) }.into() + User { username: format!("{}{}", username, id), test: None }.into() } } diff --git a/gotham_restful/src/helper.rs b/gotham_restful/src/helper.rs index d4c7fcc..7efb776 100644 --- a/gotham_restful/src/helper.rs +++ b/gotham_restful/src/helper.rs @@ -2,7 +2,7 @@ pub mod openapi { pub use indexmap::IndexMap; - pub use openapiv3::{ObjectType, ReferenceOr, Schema, SchemaData, SchemaKind, Type}; + pub use openapiv3::{ObjectType, ReferenceOr, Schema, SchemaData, SchemaKind, StringType, Type, VariantOrUnknownOrEmpty}; } #[cfg(not(feature = "openapi"))] diff --git a/gotham_restful_derive/src/openapi_type.rs b/gotham_restful_derive/src/openapi_type.rs index 7ab40f7..d4df178 100644 --- a/gotham_restful_derive/src/openapi_type.rs +++ b/gotham_restful_derive/src/openapi_type.rs @@ -4,10 +4,66 @@ use quote::quote; use syn::{ Field, Fields, + Item, + ItemEnum, ItemStruct, + Variant, parse_macro_input }; +pub fn expand(tokens : TokenStream) -> TokenStream +{ + let input = parse_macro_input!(tokens as Item); + + match input { + Item::Enum(item) => expand_enum(item), + Item::Struct(item) => expand_struct(item), + _ => panic!("derive(OpenapiType) not supported for this context") + }.into() +} + +fn expand_variant(variant : &Variant) -> TokenStream2 +{ + if variant.fields != Fields::Unit + { + panic!("Enum Variants with Fields not supported"); + } + + let ident = &variant.ident; + + quote! { + enumeration.push(stringify!(#ident).to_string()); + } +} + +fn expand_enum(input : ItemEnum) -> TokenStream2 +{ + let ident = input.ident; + let generics = input.generics; + + let variants : Vec<TokenStream2> = input.variants.iter().map(expand_variant).collect(); + + quote! { + impl #generics ::gotham_restful::OpenapiType for #ident #generics + { + fn to_schema() -> ::gotham_restful::OpenapiSchema + { + use ::gotham_restful::{helper::openapi::*, OpenapiSchema}; + + let mut enumeration : Vec<String> = Vec::new(); + + #(#variants)* + + OpenapiSchema::new(SchemaKind::Type(Type::String(StringType { + format: VariantOrUnknownOrEmpty::Empty, + pattern: None, + enumeration + }))) + } + } + } +} + fn expand_field(field : &Field) -> TokenStream2 { let ident = match &field.ident { @@ -48,10 +104,8 @@ fn expand_field(field : &Field) -> TokenStream2 }} } -pub fn expand(tokens : TokenStream) -> TokenStream +pub fn expand_struct(input : ItemStruct) -> TokenStream2 { - let input = parse_macro_input!(tokens as ItemStruct); - let ident = input.ident; let generics = input.generics; @@ -63,7 +117,7 @@ pub fn expand(tokens : TokenStream) -> TokenStream Fields::Unit => Vec::new() }; - let output = quote!{ + quote!{ impl #generics ::gotham_restful::OpenapiType for #ident #generics { fn to_schema() -> ::gotham_restful::OpenapiSchema @@ -92,7 +146,5 @@ pub fn expand(tokens : TokenStream) -> TokenStream } } } - }; - - output.into() + } } From 74ea1b820b67bf608c2695af3335690ba03a3d69 Mon Sep 17 00:00:00 2001 From: Dominic <git@msrd0.de> Date: Thu, 3 Oct 2019 00:11:23 +0200 Subject: [PATCH 19/20] improve option handling --- gotham_restful/src/openapi/types.rs | 20 +++++++++----------- gotham_restful_derive/src/openapi_type.rs | 4 +--- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/gotham_restful/src/openapi/types.rs b/gotham_restful/src/openapi/types.rs index 5e08d60..69f4adc 100644 --- a/gotham_restful/src/openapi/types.rs +++ b/gotham_restful/src/openapi/types.rs @@ -132,22 +132,20 @@ impl<T : OpenapiType> OpenapiType for Option<T> fn to_schema() -> OpenapiSchema { let schema = T::to_schema(); - let mut dependencies : IndexMap<String, OpenapiSchema> = IndexMap::new(); - let refor = if let Some(name) = schema.name.clone() - { - let reference = Reference { reference: format!("#/components/schemas/{}", name) }; - dependencies.insert(name, schema); - reference - } - else - { - Item(schema.to_schema()) + let mut dependencies = schema.dependencies.clone(); + let schema = match schema.name.clone() { + Some(name) => { + let reference = Reference { reference: format!("#/components/schemas/{}", name) }; + dependencies.insert(name, schema); + SchemaKind::AllOf { all_of: vec![reference] } + }, + None => schema.schema }; OpenapiSchema { nullable: true, name: None, - schema: SchemaKind::AllOf { all_of: vec![refor] }, + schema, dependencies } } diff --git a/gotham_restful_derive/src/openapi_type.rs b/gotham_restful_derive/src/openapi_type.rs index d4df178..f041d5b 100644 --- a/gotham_restful_derive/src/openapi_type.rs +++ b/gotham_restful_derive/src/openapi_type.rs @@ -78,8 +78,6 @@ fn expand_field(field : &Field) -> TokenStream2 if schema.nullable { schema.nullable = false; - schema.name = schema.name.map(|name| - if name.ends_with("OrNull") { name[..(name.len()-6)].to_string() } else { name }); } else { @@ -97,7 +95,7 @@ fn expand_field(field : &Field) -> TokenStream2 None => { properties.insert( stringify!(#ident).to_string(), - ReferenceOr::Item(Box::new(<#ty>::to_schema().to_schema())) + ReferenceOr::Item(Box::new(schema.to_schema())) ); } } From 46372dee6029b4a0ad634fc7c51c020ca6200158 Mon Sep 17 00:00:00 2001 From: Dominic <git@msrd0.de> Date: Thu, 3 Oct 2019 00:41:29 +0200 Subject: [PATCH 20/20] don't loose dependencies --- example/src/main.rs | 7 +++---- gotham_restful/src/openapi/types.rs | 2 +- gotham_restful_derive/src/openapi_type.rs | 10 ++++++++++ 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/example/src/main.rs b/example/src/main.rs index 3faee12..1612a22 100644 --- a/example/src/main.rs +++ b/example/src/main.rs @@ -26,17 +26,16 @@ rest_resource!{Users, route => { }} #[derive(Deserialize, OpenapiType, Serialize)] -enum TestEnum +struct TestStruct { - Foo, - Bar + foo : String } #[derive(Deserialize, OpenapiType, Serialize)] struct User { username : String, - test : Option<TestEnum> + test : Option<Vec<TestStruct>> } impl ResourceReadAll<Success<Vec<Option<User>>>> for Users diff --git a/gotham_restful/src/openapi/types.rs b/gotham_restful/src/openapi/types.rs index 69f4adc..f0167f0 100644 --- a/gotham_restful/src/openapi/types.rs +++ b/gotham_restful/src/openapi/types.rs @@ -156,7 +156,7 @@ impl<T : OpenapiType> OpenapiType for Vec<T> fn to_schema() -> OpenapiSchema { let schema = T::to_schema(); - let mut dependencies : IndexMap<String, OpenapiSchema> = IndexMap::new(); + let mut dependencies = schema.dependencies.clone(); let items = if let Some(name) = schema.name.clone() { diff --git a/gotham_restful_derive/src/openapi_type.rs b/gotham_restful_derive/src/openapi_type.rs index f041d5b..7138da1 100644 --- a/gotham_restful_derive/src/openapi_type.rs +++ b/gotham_restful_derive/src/openapi_type.rs @@ -84,6 +84,16 @@ fn expand_field(field : &Field) -> TokenStream2 required.push(stringify!(#ident).to_string()); } + let keys : Vec<String> = schema.dependencies.keys().map(|k| k.to_string()).collect(); + for dep in keys + { + let dep_schema = schema.dependencies.swap_remove(&dep); + if let Some(dep_schema) = dep_schema + { + dependencies.insert(dep, dep_schema); + } + } + match schema.name.clone() { Some(name) => { properties.insert(