From 960ba0e8bcc5bcc8bcce8e6c847766b5a9b10771 Mon Sep 17 00:00:00 2001 From: Dominic Date: Sun, 28 Feb 2021 01:59:59 +0100 Subject: [PATCH 01/18] track gotham master --- Cargo.toml | 8 ++++---- tests/async_methods.rs | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 6f81fe0..0f4621c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ members = [".", "./derive", "./example"] [package] name = "gotham_restful" -version = "0.2.0" +version = "0.3.0-dev" authors = ["Dominic Meiser "] edition = "2018" description = "RESTful additions for the gotham web framework" @@ -22,7 +22,7 @@ gitlab = { repository = "msrd0/gotham-restful", branch = "master" } [dependencies] futures-core = "0.3.7" futures-util = "0.3.7" -gotham = { version = "0.5.0", default-features = false } +gotham = { git = "https://github.com/gotham-rs/gotham", default-features = false } gotham_derive = "0.5.0" gotham_restful_derive = "0.2.0" log = "0.4.8" @@ -37,7 +37,7 @@ uuid = { version = "0.8.1", optional = true } # non-feature optional dependencies base64 = { version = "0.13.0", optional = true } -cookie = { version = "0.14", optional = true } +cookie = { version = "0.15", optional = true } gotham_middleware_diesel = { version = "0.2.0", optional = true } indexmap = { version = "1.3.2", optional = true } indoc = { version = "1.0", optional = true } @@ -52,7 +52,7 @@ diesel = { version = "1.4.4", features = ["postgres"] } futures-executor = "0.3.5" paste = "1.0" pretty_env_logger = "0.4" -tokio = { version = "0.2", features = ["time"], default-features = false } +tokio = { version = "1.0", features = ["time"], default-features = false } thiserror = "1.0.18" trybuild = "1.0.27" diff --git a/tests/async_methods.rs b/tests/async_methods.rs index 35ec42a..21a74b5 100644 --- a/tests/async_methods.rs +++ b/tests/async_methods.rs @@ -10,7 +10,7 @@ use gotham::{ use gotham_restful::*; use mime::{APPLICATION_JSON, TEXT_PLAIN}; use serde::Deserialize; -use tokio::time::{delay_for, Duration}; +use tokio::time::{sleep, Duration}; mod util { include!("util/mod.rs"); @@ -86,9 +86,9 @@ async fn remove(_id: u64) -> Raw<&'static [u8]> { const STATE_TEST_RESPONSE: &[u8] = b"xxJbxOuwioqR5DfzPuVqvaqRSfpdNQGluIvHU4n1LM"; #[endpoint(method = "Method::GET", uri = "state_test")] async fn state_test(state: &mut State) -> Raw<&'static [u8]> { - delay_for(Duration::from_nanos(1)).await; + sleep(Duration::from_nanos(1)).await; state.borrow::(); - delay_for(Duration::from_nanos(1)).await; + sleep(Duration::from_nanos(1)).await; Raw::new(STATE_TEST_RESPONSE, TEXT_PLAIN) } From 59e97f5d70ffbb6726a569d615940aaa6b71976c Mon Sep 17 00:00:00 2001 From: Dominic Date: Wed, 3 Mar 2021 23:46:46 +0100 Subject: [PATCH 02/18] release gotham_restful 0.2.1 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index f9d03c8..5448ca0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ members = [".", "./derive", "./example"] [package] name = "gotham_restful" -version = "0.2.0" +version = "0.2.1" authors = ["Dominic Meiser "] edition = "2018" description = "RESTful additions for the gotham web framework" From 90870e3b6a16bcd41859d3491bb05c16faa4afd4 Mon Sep 17 00:00:00 2001 From: Dominic Date: Sun, 7 Mar 2021 19:05:25 +0100 Subject: [PATCH 03/18] basic structure for openapi_type crate --- Cargo.toml | 4 +- openapi_type/Cargo.toml | 20 ++++ openapi_type/src/lib.rs | 76 +++++++++++++++ .../tests/fail/generics_not_openapitype.rs | 12 +++ .../fail/generics_not_openapitype.stderr | 21 +++++ openapi_type/tests/pass/unit_struct.rs | 6 ++ openapi_type/tests/trybuild.rs | 8 ++ openapi_type_derive/Cargo.toml | 22 +++++ openapi_type_derive/src/lib.rs | 93 +++++++++++++++++++ 9 files changed, 261 insertions(+), 1 deletion(-) create mode 100644 openapi_type/Cargo.toml create mode 100644 openapi_type/src/lib.rs create mode 100644 openapi_type/tests/fail/generics_not_openapitype.rs create mode 100644 openapi_type/tests/fail/generics_not_openapitype.stderr create mode 100644 openapi_type/tests/pass/unit_struct.rs create mode 100644 openapi_type/tests/trybuild.rs create mode 100644 openapi_type_derive/Cargo.toml create mode 100644 openapi_type_derive/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index 08bf06d..7c0baee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ # -*- eval: (cargo-minor-mode 1) -*- [workspace] -members = [".", "./derive", "./example"] +members = [".", "./derive", "./example", "./openapi_type", "./openapi_type_derive"] [package] name = "gotham_restful" @@ -76,3 +76,5 @@ features = ["full"] [patch.crates-io] gotham_restful = { path = "." } gotham_restful_derive = { path = "./derive" } +openapi_type = { path = "./openapi_type" } +openapi_type_derive = { path = "./openapi_type_derive" } diff --git a/openapi_type/Cargo.toml b/openapi_type/Cargo.toml new file mode 100644 index 0000000..5e2972a --- /dev/null +++ b/openapi_type/Cargo.toml @@ -0,0 +1,20 @@ +# -*- eval: (cargo-minor-mode 1) -*- + +[package] +workspace = ".." +name = "openapi_type" +version = "0.1.0-dev" +authors = ["Dominic Meiser "] +edition = "2018" +description = "OpenAPI type information for Rust structs and enums" +keywords = ["openapi", "type"] +license = "Apache-2.0" +repository = "https://gitlab.com/msrd0/gotham-restful/-/tree/master/openapi_type" + +[dependencies] +indexmap = "1.6" +openapi_type_derive = "0.1.0-dev" +openapiv3 = "=0.3.2" + +[dev-dependencies] +trybuild = "1.0" diff --git a/openapi_type/src/lib.rs b/openapi_type/src/lib.rs new file mode 100644 index 0000000..b5c0341 --- /dev/null +++ b/openapi_type/src/lib.rs @@ -0,0 +1,76 @@ +#![warn(missing_debug_implementations, rust_2018_idioms)] +#![forbid(unsafe_code)] +#![cfg_attr(feature = "cargo-clippy", allow(clippy::tabs_in_doc_comments))] +/*! +TODO +*/ + +pub use indexmap; +pub use openapi_type_derive::OpenapiType; +pub use openapiv3 as openapi; + +use indexmap::IndexMap; +use openapi::{Schema, SchemaData, SchemaKind}; + +// TODO update the documentation +/** +This struct needs to be available for every type that can be part of an OpenAPI Spec. It is +already implemented for primitive types, String, Vec, Option and the like. To have it available +for your type, simply derive from [OpenapiType]. +*/ +#[derive(Debug, Clone, PartialEq)] +pub struct OpenapiSchema { + /// The name of this schema. If it is None, the schema will be inlined. + pub name: Option, + /// Whether this particular schema is nullable. Note that there is no guarantee that this will + /// make it into the final specification, it might just be interpreted as a hint to make it + /// an optional parameter. + pub nullable: bool, + /// The actual OpenAPI schema. + pub schema: SchemaKind, + /// Other schemas that this schema depends on. They will be included in the final OpenAPI Spec + /// along with this schema. + pub dependencies: IndexMap +} + +impl OpenapiSchema { + /// Create a new schema that has no name. + pub fn new(schema: SchemaKind) -> Self { + Self { + name: None, + nullable: false, + schema, + dependencies: IndexMap::new() + } + } + + /// Convert this schema to a [Schema] that can be serialized to the OpenAPI Spec. + pub fn into_schema(self) -> Schema { + Schema { + schema_data: SchemaData { + nullable: self.nullable, + title: self.name, + ..Default::default() + }, + schema_kind: self.schema + } + } +} + +/** +This trait needs to be implemented by every type that is being used in the OpenAPI Spec. It gives +access to the [OpenapiSchema] of this type. It is provided for primitive types, String and the +like. For use on your own types, there is a derive macro: + +``` +# #[macro_use] extern crate openapi_type_derive; +# +#[derive(OpenapiType)] +struct MyResponse { + message: String +} +``` +*/ +pub trait OpenapiType { + fn schema() -> OpenapiSchema; +} diff --git a/openapi_type/tests/fail/generics_not_openapitype.rs b/openapi_type/tests/fail/generics_not_openapitype.rs new file mode 100644 index 0000000..3d2a09d --- /dev/null +++ b/openapi_type/tests/fail/generics_not_openapitype.rs @@ -0,0 +1,12 @@ +use openapi_type::OpenapiType; + +#[derive(OpenapiType)] +struct Foo { + bar: T +} + +struct Bar; + +fn main() { + >::schema(); +} diff --git a/openapi_type/tests/fail/generics_not_openapitype.stderr b/openapi_type/tests/fail/generics_not_openapitype.stderr new file mode 100644 index 0000000..193eadb --- /dev/null +++ b/openapi_type/tests/fail/generics_not_openapitype.stderr @@ -0,0 +1,21 @@ +error[E0599]: no function or associated item named `schema` found for struct `Foo` in the current scope + --> $DIR/generics_not_openapitype.rs:11:14 + | +4 | struct Foo { + | ------------- + | | + | function or associated item `schema` not found for this + | doesn't satisfy `Foo: OpenapiType` +... +8 | struct Bar; + | ----------- doesn't satisfy `Bar: OpenapiType` +... +11 | >::schema(); + | ^^^^^^ function or associated item not found in `Foo` + | + = note: the method `schema` exists but the following trait bounds were not satisfied: + `Bar: OpenapiType` + which is required by `Foo: OpenapiType` + = help: items from traits can only be used if the trait is implemented and in scope + = note: the following trait defines an item `schema`, perhaps you need to implement it: + candidate #1: `OpenapiType` diff --git a/openapi_type/tests/pass/unit_struct.rs b/openapi_type/tests/pass/unit_struct.rs new file mode 100644 index 0000000..79f6443 --- /dev/null +++ b/openapi_type/tests/pass/unit_struct.rs @@ -0,0 +1,6 @@ +use openapi_type_derive::OpenapiType; + +#[derive(OpenapiType)] +struct Foo; + +fn main() {} diff --git a/openapi_type/tests/trybuild.rs b/openapi_type/tests/trybuild.rs new file mode 100644 index 0000000..28574f1 --- /dev/null +++ b/openapi_type/tests/trybuild.rs @@ -0,0 +1,8 @@ +use trybuild::TestCases; + +#[test] +fn trybuild() { + let t = TestCases::new(); + t.pass("tests/pass/*.rs"); + t.compile_fail("tests/fail/*.rs"); +} diff --git a/openapi_type_derive/Cargo.toml b/openapi_type_derive/Cargo.toml new file mode 100644 index 0000000..c1cbedc --- /dev/null +++ b/openapi_type_derive/Cargo.toml @@ -0,0 +1,22 @@ +# -*- eval: (cargo-minor-mode 1) -*- + +[package] +workspace = ".." +name = "openapi_type_derive" +version = "0.1.0-dev" +authors = ["Dominic Meiser "] +edition = "2018" +description = "Implementation detail of the openapi_type crate" +license = "Apache-2.0" +repository = "https://gitlab.com/msrd0/gotham-restful/-/tree/master/openapi_type_derive" + +# tests are done using trybuild exclusively +autotests = false + +[lib] +proc-macro = true + +[dependencies] +proc-macro2 = "1.0" +quote = "1.0" +syn = "1.0" diff --git a/openapi_type_derive/src/lib.rs b/openapi_type_derive/src/lib.rs new file mode 100644 index 0000000..bc049b3 --- /dev/null +++ b/openapi_type_derive/src/lib.rs @@ -0,0 +1,93 @@ +#![warn(missing_debug_implementations, rust_2018_idioms)] +#![deny(broken_intra_doc_links)] +#![forbid(unsafe_code)] +//! This crate defines the macros for `#[derive(OpenapiType)]`. + +use proc_macro::TokenStream; +use proc_macro2::TokenStream as TokenStream2; +use quote::quote; +use syn::{ + parse_macro_input, spanned::Spanned as _, Data, DataEnum, DataStruct, DataUnion, DeriveInput, TraitBound, + TraitBoundModifier, TypeParamBound +}; + +#[proc_macro_derive(OpenapiType)] +pub fn derive_openapi_type(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input); + expand_openapi_type(input).unwrap_or_else(|err| err.to_compile_error()).into() +} + +macro_rules! path { + (:: $($segment:ident)::*) => { + path!(@private Some(Default::default()), $($segment),*) + }; + ($($segment:ident)::*) => { + path!(@private None, $($segment),*) + }; + (@private $leading_colon:expr, $($segment:ident),*) => { + { + #[allow(unused_mut)] + let mut segments: ::syn::punctuated::Punctuated<::syn::PathSegment, _> = Default::default(); + $( + segments.push(::syn::PathSegment { + ident: ::proc_macro2::Ident::new(stringify!($segment), ::proc_macro2::Span::call_site()), + arguments: Default::default() + }); + )* + ::syn::Path { + leading_colon: $leading_colon, + segments + } + } + }; +} + +fn expand_openapi_type(mut input: DeriveInput) -> syn::Result { + let ident = &input.ident; + + // prepare the generics - all impl generics will get `OpenapiType` requirement + let (impl_generics, ty_generics, where_clause) = { + let generics = &mut input.generics; + generics.type_params_mut().for_each(|param| { + param.colon_token.get_or_insert_with(Default::default); + param.bounds.push(TypeParamBound::Trait(TraitBound { + paren_token: None, + modifier: TraitBoundModifier::None, + lifetimes: None, + path: path!(::openapi_type::OpenapiType) + })) + }); + generics.split_for_impl() + }; + + // parse the input data + match &input.data { + Data::Struct(strukt) => parse_struct(strukt)?, + Data::Enum(inum) => parse_enum(inum)?, + Data::Union(union) => parse_union(union)? + }; + + // generate the impl code + Ok(quote! { + impl #impl_generics ::openapi_type::OpenapiType for #ident #ty_generics #where_clause { + fn schema() -> ::openapi_type::OpenapiSchema { + unimplemented!() + } + } + }) +} + +fn parse_struct(_strukt: &DataStruct) -> syn::Result<()> { + Ok(()) +} + +fn parse_enum(_inum: &DataEnum) -> syn::Result<()> { + unimplemented!() +} + +fn parse_union(union: &DataUnion) -> syn::Result<()> { + Err(syn::Error::new( + union.union_token.span(), + "#[derive(OpenapiType)] cannot be used on unions" + )) +} From d9c7f4135f1c314f5f483f5314214806c7d32db9 Mon Sep 17 00:00:00 2001 From: Dominic Date: Sun, 7 Mar 2021 23:09:50 +0100 Subject: [PATCH 04/18] start openapi-type codegen --- openapi_type/tests/fail/not_openapitype.rs | 12 +++ .../tests/fail/not_openapitype.stderr | 8 ++ ...apitype.rs => not_openapitype_generics.rs} | 0 ...stderr => not_openapitype_generics.stderr} | 2 +- openapi_type_derive/Cargo.toml | 3 - openapi_type_derive/src/codegen.rs | 95 +++++++++++++++++++ openapi_type_derive/src/lib.rs | 94 +++++++++--------- openapi_type_derive/src/parser.rs | 40 ++++++++ openapi_type_derive/src/util.rs | 38 ++++++++ 9 files changed, 241 insertions(+), 51 deletions(-) create mode 100644 openapi_type/tests/fail/not_openapitype.rs create mode 100644 openapi_type/tests/fail/not_openapitype.stderr rename openapi_type/tests/fail/{generics_not_openapitype.rs => not_openapitype_generics.rs} (100%) rename openapi_type/tests/fail/{generics_not_openapitype.stderr => not_openapitype_generics.stderr} (94%) create mode 100644 openapi_type_derive/src/codegen.rs create mode 100644 openapi_type_derive/src/parser.rs create mode 100644 openapi_type_derive/src/util.rs diff --git a/openapi_type/tests/fail/not_openapitype.rs b/openapi_type/tests/fail/not_openapitype.rs new file mode 100644 index 0000000..2b5b23c --- /dev/null +++ b/openapi_type/tests/fail/not_openapitype.rs @@ -0,0 +1,12 @@ +use openapi_type::OpenapiType; + +#[derive(OpenapiType)] +struct Foo { + bar: Bar +} + +struct Bar; + +fn main() { + Foo::schema(); +} diff --git a/openapi_type/tests/fail/not_openapitype.stderr b/openapi_type/tests/fail/not_openapitype.stderr new file mode 100644 index 0000000..f089b15 --- /dev/null +++ b/openapi_type/tests/fail/not_openapitype.stderr @@ -0,0 +1,8 @@ +error[E0277]: the trait bound `Bar: OpenapiType` is not satisfied + --> $DIR/not_openapitype.rs:3:10 + | +3 | #[derive(OpenapiType)] + | ^^^^^^^^^^^ the trait `OpenapiType` is not implemented for `Bar` + | + = note: required by `schema` + = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/openapi_type/tests/fail/generics_not_openapitype.rs b/openapi_type/tests/fail/not_openapitype_generics.rs similarity index 100% rename from openapi_type/tests/fail/generics_not_openapitype.rs rename to openapi_type/tests/fail/not_openapitype_generics.rs diff --git a/openapi_type/tests/fail/generics_not_openapitype.stderr b/openapi_type/tests/fail/not_openapitype_generics.stderr similarity index 94% rename from openapi_type/tests/fail/generics_not_openapitype.stderr rename to openapi_type/tests/fail/not_openapitype_generics.stderr index 193eadb..d41098b 100644 --- a/openapi_type/tests/fail/generics_not_openapitype.stderr +++ b/openapi_type/tests/fail/not_openapitype_generics.stderr @@ -1,5 +1,5 @@ error[E0599]: no function or associated item named `schema` found for struct `Foo` in the current scope - --> $DIR/generics_not_openapitype.rs:11:14 + --> $DIR/not_openapitype_generics.rs:11:14 | 4 | struct Foo { | ------------- diff --git a/openapi_type_derive/Cargo.toml b/openapi_type_derive/Cargo.toml index c1cbedc..ab8e932 100644 --- a/openapi_type_derive/Cargo.toml +++ b/openapi_type_derive/Cargo.toml @@ -10,9 +10,6 @@ description = "Implementation detail of the openapi_type crate" license = "Apache-2.0" repository = "https://gitlab.com/msrd0/gotham-restful/-/tree/master/openapi_type_derive" -# tests are done using trybuild exclusively -autotests = false - [lib] proc-macro = true diff --git a/openapi_type_derive/src/codegen.rs b/openapi_type_derive/src/codegen.rs new file mode 100644 index 0000000..9ae9db7 --- /dev/null +++ b/openapi_type_derive/src/codegen.rs @@ -0,0 +1,95 @@ +use crate::parser::ParseData; +use proc_macro2::TokenStream; +use quote::quote; +use syn::{LitStr, Type}; + +impl ParseData { + pub(super) fn gen_schema(self) -> syn::Result { + match self { + Self::Struct(fields) => gen_struct(fields), + Self::Unit => gen_unit(), + _ => unimplemented!() + } + } +} + +fn gen_struct(fields: Vec<(LitStr, Type)>) -> syn::Result { + let field_name = fields.iter().map(|(name, _)| name); + let field_ty = fields.iter().map(|(_, ty)| ty); + + let openapi = path!(::openapi_type::openapi); + Ok(quote! { + { + let mut properties = <::openapi_type::indexmap::IndexMap< + ::std::string::String, + #openapi::ReferenceOr<::std::boxed::Box<#openapi::Schema>> + >>::new(); + let mut required = <::std::vec::Vec<::std::string::String>>::new(); + + #({ + const FIELD_NAME: &::core::primitive::str = #field_name; + let mut field_schema = <#field_ty as ::openapi_type::OpenapiType>::schema(); + add_dependencies(&mut field_schema.dependencies); + + // fields in OpenAPI are nullable by default + match field_schema.nullable { + true => field_schema.nullable = false, + false => required.push(::std::string::String::from(FIELD_NAME)) + }; + + match field_schema.name.as_ref() { + // include the field schema as reference + ::std::option::Option::Some(schema_name) => { + let mut reference = ::std::string::String::from("#/components/schemas/"); + reference.push_str(schema_name); + properties.insert( + ::std::string::String::from(FIELD_NAME), + #openapi::ReferenceOr::Reference { reference } + ); + dependencies.insert( + ::std::string::String::from(schema_name), + field_schema + ); + }, + // inline the field schema + ::std::option::Option::None => { + properties.insert( + ::std::string::String::from(FIELD_NAME), + #openapi::ReferenceOr::Item( + ::std::boxed::Box::new( + field_schema.into_schema() + ) + ) + ); + } + } + })* + + #openapi::SchemaKind::Type( + #openapi::Type::Object( + #openapi::ObjectType { + properties, + required, + .. ::std::default::Default::default() + } + ) + ) + } + }) +} + +fn gen_unit() -> syn::Result { + let openapi = path!(::openapi_type::openapi); + Ok(quote! { + #openapi::SchemaKind::Type( + #openapi::Type::Object( + #openapi::ObjectType { + additional_properties: ::std::option::Option::Some( + #openapi::AdditionalProperties::Any(false) + ), + .. ::std::default::Default::default() + } + ) + ) + }) +} diff --git a/openapi_type_derive/src/lib.rs b/openapi_type_derive/src/lib.rs index bc049b3..af4f9be 100644 --- a/openapi_type_derive/src/lib.rs +++ b/openapi_type_derive/src/lib.rs @@ -6,44 +6,27 @@ use proc_macro::TokenStream; use proc_macro2::TokenStream as TokenStream2; use quote::quote; -use syn::{ - parse_macro_input, spanned::Spanned as _, Data, DataEnum, DataStruct, DataUnion, DeriveInput, TraitBound, - TraitBoundModifier, TypeParamBound -}; +use syn::{parse_macro_input, Data, DeriveInput, LitStr, TraitBound, TraitBoundModifier, TypeParamBound}; +#[macro_use] +mod util; +//use util::*; + +mod codegen; +mod parser; +use parser::*; + +/// The derive macro for [OpenapiType][openapi_type::OpenapiType]. #[proc_macro_derive(OpenapiType)] pub fn derive_openapi_type(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input); expand_openapi_type(input).unwrap_or_else(|err| err.to_compile_error()).into() } -macro_rules! path { - (:: $($segment:ident)::*) => { - path!(@private Some(Default::default()), $($segment),*) - }; - ($($segment:ident)::*) => { - path!(@private None, $($segment),*) - }; - (@private $leading_colon:expr, $($segment:ident),*) => { - { - #[allow(unused_mut)] - let mut segments: ::syn::punctuated::Punctuated<::syn::PathSegment, _> = Default::default(); - $( - segments.push(::syn::PathSegment { - ident: ::proc_macro2::Ident::new(stringify!($segment), ::proc_macro2::Span::call_site()), - arguments: Default::default() - }); - )* - ::syn::Path { - leading_colon: $leading_colon, - segments - } - } - }; -} - fn expand_openapi_type(mut input: DeriveInput) -> syn::Result { let ident = &input.ident; + let name = ident.to_string(); + let name = LitStr::new(&name, ident.span()); // prepare the generics - all impl generics will get `OpenapiType` requirement let (impl_generics, ty_generics, where_clause) = { @@ -61,33 +44,50 @@ fn expand_openapi_type(mut input: DeriveInput) -> syn::Result { }; // parse the input data - match &input.data { + let parsed = match &input.data { Data::Struct(strukt) => parse_struct(strukt)?, Data::Enum(inum) => parse_enum(inum)?, Data::Union(union) => parse_union(union)? }; - // generate the impl code + // run the codegen + let schema_code = parsed.gen_schema()?; + + // put the code together Ok(quote! { + #[allow(unused_mut)] impl #impl_generics ::openapi_type::OpenapiType for #ident #ty_generics #where_clause { fn schema() -> ::openapi_type::OpenapiSchema { - unimplemented!() + // prepare the dependencies + let mut dependencies = <::openapi_type::indexmap::IndexMap< + ::std::string::String, + ::openapi_type::OpenapiSchema + >>::new(); + + // this function can be used to include dependencies of dependencies + let add_dependencies = |deps: &mut ::openapi_type::indexmap::IndexMap< + ::std::string::String, + ::openapi_type::OpenapiSchema + >| { + while let ::std::option::Option::Some((dep_name, dep_schema)) = deps.pop() { + if !dependencies.contains_key(&dep_name) { + dependencies.insert(dep_name, dep_schema); + } + } + }; + + // create the schema + let schema = #schema_code; + + // return everything + const NAME: &::core::primitive::str = #name; + ::openapi_type::OpenapiSchema { + name: ::std::option::Option::Some(::std::string::String::from(NAME)), + nullable: false, + schema, + dependencies + } } } }) } - -fn parse_struct(_strukt: &DataStruct) -> syn::Result<()> { - Ok(()) -} - -fn parse_enum(_inum: &DataEnum) -> syn::Result<()> { - unimplemented!() -} - -fn parse_union(union: &DataUnion) -> syn::Result<()> { - Err(syn::Error::new( - union.union_token.span(), - "#[derive(OpenapiType)] cannot be used on unions" - )) -} diff --git a/openapi_type_derive/src/parser.rs b/openapi_type_derive/src/parser.rs new file mode 100644 index 0000000..4a71c0a --- /dev/null +++ b/openapi_type_derive/src/parser.rs @@ -0,0 +1,40 @@ +use crate::util::ToLitStr; +use syn::{spanned::Spanned as _, DataEnum, DataStruct, DataUnion, Fields, LitStr, Type}; + +#[allow(dead_code)] +pub(super) enum ParseData { + Struct(Vec<(LitStr, Type)>), + Enum(Vec), + Alternatives(Vec), + Unit +} + +pub(super) fn parse_struct(strukt: &DataStruct) -> syn::Result { + match &strukt.fields { + Fields::Named(named_fields) => { + let mut fields: Vec<(LitStr, Type)> = Vec::new(); + for f in &named_fields.named { + let ident = f.ident.as_ref().ok_or_else(|| { + syn::Error::new(f.span(), "#[derive(OpenapiType)] does not support fields without an ident") + })?; + let name = ident.to_lit_str(); + let ty = f.ty.to_owned(); + fields.push((name, ty)); + } + Ok(ParseData::Struct(fields)) + }, + Fields::Unnamed(_) => unimplemented!(), + Fields::Unit => Ok(ParseData::Unit) + } +} + +pub(super) fn parse_enum(_inum: &DataEnum) -> syn::Result { + unimplemented!() +} + +pub(super) fn parse_union(union: &DataUnion) -> syn::Result { + Err(syn::Error::new( + union.union_token.span(), + "#[derive(OpenapiType)] cannot be used on unions" + )) +} diff --git a/openapi_type_derive/src/util.rs b/openapi_type_derive/src/util.rs new file mode 100644 index 0000000..9470a04 --- /dev/null +++ b/openapi_type_derive/src/util.rs @@ -0,0 +1,38 @@ +use proc_macro2::Ident; +use syn::LitStr; + +/// Convert any literal path into a [syn::Path]. +macro_rules! path { + (:: $($segment:ident)::*) => { + path!(@private Some(Default::default()), $($segment),*) + }; + ($($segment:ident)::*) => { + path!(@private None, $($segment),*) + }; + (@private $leading_colon:expr, $($segment:ident),*) => { + { + #[allow(unused_mut)] + let mut segments: ::syn::punctuated::Punctuated<::syn::PathSegment, _> = Default::default(); + $( + segments.push(::syn::PathSegment { + ident: ::proc_macro2::Ident::new(stringify!($segment), ::proc_macro2::Span::call_site()), + arguments: Default::default() + }); + )* + ::syn::Path { + leading_colon: $leading_colon, + segments + } + } + }; +} + +/// Convert any [Ident] into a [LitStr]. Basically `stringify!`. +pub(super) trait ToLitStr { + fn to_lit_str(&self) -> LitStr; +} +impl ToLitStr for Ident { + fn to_lit_str(&self) -> LitStr { + LitStr::new(&self.to_string(), self.span()) + } +} From 667009bd228dbee456b44d2f41342b9e4dec684e Mon Sep 17 00:00:00 2001 From: Dominic Date: Mon, 8 Mar 2021 16:33:38 +0100 Subject: [PATCH 05/18] copy OpenapiType implementations and fix codegen reference --- openapi_type/Cargo.toml | 2 + openapi_type/src/impls.rs | 321 +++++++++++++++++++++++++++++ openapi_type/src/lib.rs | 4 + openapi_type/src/private.rs | 12 ++ openapi_type/tests/custom_types.rs | 44 ++++ openapi_type_derive/src/codegen.rs | 5 +- openapi_type_derive/src/lib.rs | 17 +- 7 files changed, 388 insertions(+), 17 deletions(-) create mode 100644 openapi_type/src/impls.rs create mode 100644 openapi_type/src/private.rs create mode 100644 openapi_type/tests/custom_types.rs diff --git a/openapi_type/Cargo.toml b/openapi_type/Cargo.toml index 5e2972a..028c753 100644 --- a/openapi_type/Cargo.toml +++ b/openapi_type/Cargo.toml @@ -15,6 +15,8 @@ repository = "https://gitlab.com/msrd0/gotham-restful/-/tree/master/openapi_type indexmap = "1.6" openapi_type_derive = "0.1.0-dev" openapiv3 = "=0.3.2" +serde_json = "1.0" [dev-dependencies] +paste = "1.0" trybuild = "1.0" diff --git a/openapi_type/src/impls.rs b/openapi_type/src/impls.rs new file mode 100644 index 0000000..f363818 --- /dev/null +++ b/openapi_type/src/impls.rs @@ -0,0 +1,321 @@ +use crate::{OpenapiSchema, OpenapiType}; +use indexmap::IndexMap; +use openapiv3::{ + AdditionalProperties, ArrayType, IntegerType, NumberFormat, NumberType, ObjectType, ReferenceOr, SchemaKind, StringType, + Type, VariantOrUnknownOrEmpty +}; +use std::{ + collections::{BTreeSet, HashMap, HashSet}, + hash::BuildHasher, + num::{NonZeroU128, NonZeroU16, NonZeroU32, NonZeroU64, NonZeroU8, NonZeroUsize} +}; + +impl OpenapiType for () { + fn schema() -> OpenapiSchema { + OpenapiSchema::new(SchemaKind::Type(Type::Object(ObjectType { + additional_properties: Some(AdditionalProperties::Any(false)), + ..Default::default() + }))) + } +} + +impl OpenapiType for bool { + fn schema() -> OpenapiSchema { + OpenapiSchema::new(SchemaKind::Type(Type::Boolean {})) + } +} + +macro_rules! int_types { + ($($int_ty:ty),*) => {$( + impl OpenapiType for $int_ty + { + fn schema() -> OpenapiSchema + { + OpenapiSchema::new(SchemaKind::Type(Type::Integer(IntegerType::default()))) + } + } + )*}; + + (unsigned $($int_ty:ty),*) => {$( + impl OpenapiType for $int_ty + { + fn schema() -> OpenapiSchema + { + OpenapiSchema::new(SchemaKind::Type(Type::Integer(IntegerType { + minimum: Some(0), + ..Default::default() + }))) + } + } + )*}; + + (gtzero $($int_ty:ty),*) => {$( + impl OpenapiType for $int_ty + { + fn schema() -> OpenapiSchema + { + OpenapiSchema::new(SchemaKind::Type(Type::Integer(IntegerType { + minimum: Some(1), + ..Default::default() + }))) + } + } + )*}; + + (bits = $bits:expr, $($int_ty:ty),*) => {$( + impl OpenapiType for $int_ty + { + fn schema() -> OpenapiSchema + { + OpenapiSchema::new(SchemaKind::Type(Type::Integer(IntegerType { + format: VariantOrUnknownOrEmpty::Unknown(format!("int{}", $bits)), + ..Default::default() + }))) + } + } + )*}; + + (unsigned bits = $bits:expr, $($int_ty:ty),*) => {$( + impl OpenapiType for $int_ty + { + fn schema() -> OpenapiSchema + { + OpenapiSchema::new(SchemaKind::Type(Type::Integer(IntegerType { + format: VariantOrUnknownOrEmpty::Unknown(format!("int{}", $bits)), + minimum: Some(0), + ..Default::default() + }))) + } + } + )*}; + + (gtzero bits = $bits:expr, $($int_ty:ty),*) => {$( + impl OpenapiType for $int_ty + { + fn schema() -> OpenapiSchema + { + OpenapiSchema::new(SchemaKind::Type(Type::Integer(IntegerType { + format: VariantOrUnknownOrEmpty::Unknown(format!("int{}", $bits)), + minimum: Some(1), + ..Default::default() + }))) + } + } + )*}; +} + +int_types!(isize); +int_types!(unsigned usize); +int_types!(gtzero NonZeroUsize); +int_types!(bits = 8, i8); +int_types!(unsigned bits = 8, u8); +int_types!(gtzero bits = 8, NonZeroU8); +int_types!(bits = 16, i16); +int_types!(unsigned bits = 16, u16); +int_types!(gtzero bits = 16, NonZeroU16); +int_types!(bits = 32, i32); +int_types!(unsigned bits = 32, u32); +int_types!(gtzero bits = 32, NonZeroU32); +int_types!(bits = 64, i64); +int_types!(unsigned bits = 64, u64); +int_types!(gtzero bits = 64, NonZeroU64); +int_types!(bits = 128, i128); +int_types!(unsigned bits = 128, u128); +int_types!(gtzero bits = 128, NonZeroU128); + +macro_rules! num_types { + ($($num_ty:ty = $num_fmt:ident),*) => {$( + impl OpenapiType for $num_ty + { + fn schema() -> OpenapiSchema + { + OpenapiSchema::new(SchemaKind::Type(Type::Number(NumberType { + format: VariantOrUnknownOrEmpty::Item(NumberFormat::$num_fmt), + ..Default::default() + }))) + } + } + )*} +} + +num_types!(f32 = Float, f64 = Double); + +macro_rules! str_types { + ($($str_ty:ty),*) => {$( + impl OpenapiType for $str_ty + { + fn schema() -> OpenapiSchema + { + OpenapiSchema::new(SchemaKind::Type(Type::String(StringType::default()))) + } + } + )*}; + + (format = $format:ident, $($str_ty:ty),*) => {$( + impl OpenapiType for $str_ty + { + fn schema() -> OpenapiSchema + { + use openapiv3::StringFormat; + + OpenapiSchema::new(SchemaKind::Type(Type::String(StringType { + format: VariantOrUnknownOrEmpty::Item(StringFormat::$format), + ..Default::default() + }))) + } + } + )*}; + + (format_str = $format:expr, $($str_ty:ty),*) => {$( + impl OpenapiType for $str_ty + { + fn schema() -> OpenapiSchema + { + OpenapiSchema::new(SchemaKind::Type(Type::String(StringType { + format: VariantOrUnknownOrEmpty::Unknown($format.to_string()), + ..Default::default() + }))) + } + } + )*}; +} + +str_types!(String, &str); + +#[cfg(feature = "chrono")] +str_types!(format = Date, Date, Date, Date, NaiveDate); +#[cfg(feature = "chrono")] +str_types!( + format = DateTime, + DateTime, + DateTime, + DateTime, + NaiveDateTime +); + +#[cfg(feature = "uuid")] +str_types!(format_str = "uuid", Uuid); + +impl OpenapiType for Option { + fn schema() -> OpenapiSchema { + let schema = T::schema(); + let mut dependencies = schema.dependencies.clone(); + let schema = match schema.name.clone() { + Some(name) => { + let reference = ReferenceOr::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, + dependencies + } + } +} + +impl OpenapiType for Vec { + fn schema() -> OpenapiSchema { + let schema = T::schema(); + let mut dependencies = schema.dependencies.clone(); + + let items = match schema.name.clone() { + Some(name) => { + let reference = ReferenceOr::Reference { + reference: format!("#/components/schemas/{}", name) + }; + dependencies.insert(name, schema); + reference + }, + None => ReferenceOr::Item(Box::new(schema.into_schema())) + }; + + OpenapiSchema { + nullable: false, + name: None, + schema: SchemaKind::Type(Type::Array(ArrayType { + items, + min_items: None, + max_items: None, + unique_items: false + })), + dependencies + } + } +} + +impl OpenapiType for BTreeSet { + fn schema() -> OpenapiSchema { + as OpenapiType>::schema() + } +} + +impl OpenapiType for HashSet { + fn schema() -> OpenapiSchema { + as OpenapiType>::schema() + } +} + +impl OpenapiType for HashMap { + fn schema() -> OpenapiSchema { + let key_schema = K::schema(); + let mut dependencies = key_schema.dependencies.clone(); + + let keys = match key_schema.name.clone() { + Some(name) => { + let reference = ReferenceOr::Reference { + reference: format!("#/components/schemas/{}", name) + }; + dependencies.insert(name, key_schema); + reference + }, + None => ReferenceOr::Item(Box::new(key_schema.into_schema())) + }; + + let schema = T::schema(); + dependencies.extend(schema.dependencies.iter().map(|(k, v)| (k.clone(), v.clone()))); + + let items = Box::new(match schema.name.clone() { + Some(name) => { + let reference = ReferenceOr::Reference { + reference: format!("#/components/schemas/{}", name) + }; + dependencies.insert(name, schema); + reference + }, + None => ReferenceOr::Item(schema.into_schema()) + }); + + let mut properties = IndexMap::new(); + properties.insert("default".to_owned(), keys); + + OpenapiSchema { + nullable: false, + name: None, + schema: SchemaKind::Type(Type::Object(ObjectType { + properties, + required: vec!["default".to_owned()], + additional_properties: Some(AdditionalProperties::Schema(items)), + ..Default::default() + })), + dependencies + } + } +} + +impl OpenapiType for serde_json::Value { + fn schema() -> OpenapiSchema { + OpenapiSchema { + nullable: true, + name: None, + schema: SchemaKind::Any(Default::default()), + dependencies: Default::default() + } + } +} diff --git a/openapi_type/src/lib.rs b/openapi_type/src/lib.rs index b5c0341..590800b 100644 --- a/openapi_type/src/lib.rs +++ b/openapi_type/src/lib.rs @@ -9,6 +9,10 @@ pub use indexmap; pub use openapi_type_derive::OpenapiType; pub use openapiv3 as openapi; +mod impls; +#[doc(hidden)] +pub mod private; + use indexmap::IndexMap; use openapi::{Schema, SchemaData, SchemaKind}; diff --git a/openapi_type/src/private.rs b/openapi_type/src/private.rs new file mode 100644 index 0000000..892b8e3 --- /dev/null +++ b/openapi_type/src/private.rs @@ -0,0 +1,12 @@ +use crate::OpenapiSchema; +use indexmap::IndexMap; + +pub type Dependencies = IndexMap; + +pub fn add_dependencies(dependencies: &mut Dependencies, other: &mut Dependencies) { + while let Some((dep_name, dep_schema)) = other.pop() { + if !dependencies.contains_key(&dep_name) { + dependencies.insert(dep_name, dep_schema); + } + } +} diff --git a/openapi_type/tests/custom_types.rs b/openapi_type/tests/custom_types.rs new file mode 100644 index 0000000..ba52b41 --- /dev/null +++ b/openapi_type/tests/custom_types.rs @@ -0,0 +1,44 @@ +#![allow(dead_code)] +use openapi_type::OpenapiType; + +macro_rules! test_type { + ($ty:ty = $json:tt) => { + paste::paste! { + #[test] + fn [< $ty:lower >]() { + let schema = <$ty as OpenapiType>::schema(); + let schema = openapi_type::OpenapiSchema::into_schema(schema); + let schema_json = serde_json::to_value(&schema).unwrap(); + let expected = serde_json::json!($json); + assert_eq!(schema_json, expected); + } + } + }; +} + +#[derive(OpenapiType)] +struct UnitStruct; +test_type!(UnitStruct = { + "type": "object", + "title": "UnitStruct", + "additionalProperties": false +}); + +#[derive(OpenapiType)] +struct SimpleStruct { + foo: String, + bar: isize +} +test_type!(SimpleStruct = { + "type": "object", + "title": "SimpleStruct", + "properties": { + "foo": { + "type": "string" + }, + "bar": { + "type": "integer" + } + }, + "required": ["foo", "bar"] +}); diff --git a/openapi_type_derive/src/codegen.rs b/openapi_type_derive/src/codegen.rs index 9ae9db7..4aae6a3 100644 --- a/openapi_type_derive/src/codegen.rs +++ b/openapi_type_derive/src/codegen.rs @@ -29,7 +29,10 @@ fn gen_struct(fields: Vec<(LitStr, Type)>) -> syn::Result { #({ const FIELD_NAME: &::core::primitive::str = #field_name; let mut field_schema = <#field_ty as ::openapi_type::OpenapiType>::schema(); - add_dependencies(&mut field_schema.dependencies); + ::openapi_type::private::add_dependencies( + &mut dependencies, + &mut field_schema.dependencies + ); // fields in OpenAPI are nullable by default match field_schema.nullable { diff --git a/openapi_type_derive/src/lib.rs b/openapi_type_derive/src/lib.rs index af4f9be..cdbdbf3 100644 --- a/openapi_type_derive/src/lib.rs +++ b/openapi_type_derive/src/lib.rs @@ -59,22 +59,7 @@ fn expand_openapi_type(mut input: DeriveInput) -> syn::Result { impl #impl_generics ::openapi_type::OpenapiType for #ident #ty_generics #where_clause { fn schema() -> ::openapi_type::OpenapiSchema { // prepare the dependencies - let mut dependencies = <::openapi_type::indexmap::IndexMap< - ::std::string::String, - ::openapi_type::OpenapiSchema - >>::new(); - - // this function can be used to include dependencies of dependencies - let add_dependencies = |deps: &mut ::openapi_type::indexmap::IndexMap< - ::std::string::String, - ::openapi_type::OpenapiSchema - >| { - while let ::std::option::Option::Some((dep_name, dep_schema)) = deps.pop() { - if !dependencies.contains_key(&dep_name) { - dependencies.insert(dep_name, dep_schema); - } - } - }; + let mut dependencies = ::openapi_type::private::Dependencies::new(); // create the schema let schema = #schema_code; From 43d3a1cd89bccd482d62399f3de47918be443dff Mon Sep 17 00:00:00 2001 From: Dominic Date: Mon, 8 Mar 2021 17:20:41 +0100 Subject: [PATCH 06/18] start implementing enums --- openapi_type/tests/custom_types.rs | 35 ++++++++ .../tests/fail/enum_with_no_variants.rs | 6 ++ .../tests/fail/enum_with_no_variants.stderr | 5 ++ openapi_type/tests/pass/unit_struct.rs | 6 -- openapi_type/tests/trybuild.rs | 1 - openapi_type_derive/src/codegen.rs | 49 +++++++--- openapi_type_derive/src/lib.rs | 2 +- openapi_type_derive/src/parser.rs | 90 +++++++++++++++---- 8 files changed, 159 insertions(+), 35 deletions(-) create mode 100644 openapi_type/tests/fail/enum_with_no_variants.rs create mode 100644 openapi_type/tests/fail/enum_with_no_variants.stderr delete mode 100644 openapi_type/tests/pass/unit_struct.rs diff --git a/openapi_type/tests/custom_types.rs b/openapi_type/tests/custom_types.rs index ba52b41..fce5e57 100644 --- a/openapi_type/tests/custom_types.rs +++ b/openapi_type/tests/custom_types.rs @@ -42,3 +42,38 @@ test_type!(SimpleStruct = { }, "required": ["foo", "bar"] }); + +#[derive(OpenapiType)] +enum EnumWithoutFields { + Success, + Error +} +test_type!(EnumWithoutFields = { + "type": "string", + "title": "EnumWithoutFields", + "enum": [ + "Success", + "Error" + ] +}); + +#[derive(OpenapiType)] +enum EnumWithOneField { + Success { value: isize } +} +test_type!(EnumWithOneField = { + "type": "object", + "title": "EnumWithOneField", + "properties": { + "Success": { + "type": "object", + "properties": { + "value": { + "type": "integer" + } + }, + "required": ["value"] + } + }, + "required": ["Success"] +}); diff --git a/openapi_type/tests/fail/enum_with_no_variants.rs b/openapi_type/tests/fail/enum_with_no_variants.rs new file mode 100644 index 0000000..d08e223 --- /dev/null +++ b/openapi_type/tests/fail/enum_with_no_variants.rs @@ -0,0 +1,6 @@ +use openapi_type::OpenapiType; + +#[derive(OpenapiType)] +enum Foo {} + +fn main() {} diff --git a/openapi_type/tests/fail/enum_with_no_variants.stderr b/openapi_type/tests/fail/enum_with_no_variants.stderr new file mode 100644 index 0000000..5c6b1d1 --- /dev/null +++ b/openapi_type/tests/fail/enum_with_no_variants.stderr @@ -0,0 +1,5 @@ +error: #[derive(OpenapiType)] does not support enums with no variants + --> $DIR/enum_with_no_variants.rs:4:10 + | +4 | enum Foo {} + | ^^ diff --git a/openapi_type/tests/pass/unit_struct.rs b/openapi_type/tests/pass/unit_struct.rs deleted file mode 100644 index 79f6443..0000000 --- a/openapi_type/tests/pass/unit_struct.rs +++ /dev/null @@ -1,6 +0,0 @@ -use openapi_type_derive::OpenapiType; - -#[derive(OpenapiType)] -struct Foo; - -fn main() {} diff --git a/openapi_type/tests/trybuild.rs b/openapi_type/tests/trybuild.rs index 28574f1..b76b676 100644 --- a/openapi_type/tests/trybuild.rs +++ b/openapi_type/tests/trybuild.rs @@ -3,6 +3,5 @@ use trybuild::TestCases; #[test] fn trybuild() { let t = TestCases::new(); - t.pass("tests/pass/*.rs"); t.compile_fail("tests/fail/*.rs"); } diff --git a/openapi_type_derive/src/codegen.rs b/openapi_type_derive/src/codegen.rs index 4aae6a3..1ad7d02 100644 --- a/openapi_type_derive/src/codegen.rs +++ b/openapi_type_derive/src/codegen.rs @@ -1,24 +1,33 @@ -use crate::parser::ParseData; +use crate::parser::{ParseData, ParseDataType}; use proc_macro2::TokenStream; use quote::quote; -use syn::{LitStr, Type}; +use syn::LitStr; impl ParseData { - pub(super) fn gen_schema(self) -> syn::Result { + pub(super) fn gen_schema(&self) -> TokenStream { match self { Self::Struct(fields) => gen_struct(fields), + Self::Enum(variants) => gen_enum(variants), Self::Unit => gen_unit(), _ => unimplemented!() } } } -fn gen_struct(fields: Vec<(LitStr, Type)>) -> syn::Result { +fn gen_struct(fields: &[(LitStr, ParseDataType)]) -> TokenStream { let field_name = fields.iter().map(|(name, _)| name); - let field_ty = fields.iter().map(|(_, ty)| ty); + let field_schema = fields.iter().map(|(_, ty)| match ty { + ParseDataType::Type(ty) => { + quote!(<#ty as ::openapi_type::OpenapiType>::schema()) + }, + ParseDataType::Inline(data) => { + let code = data.gen_schema(); + quote!(::openapi_type::OpenapiSchema::new(#code)) + } + }); let openapi = path!(::openapi_type::openapi); - Ok(quote! { + quote! { { let mut properties = <::openapi_type::indexmap::IndexMap< ::std::string::String, @@ -28,7 +37,7 @@ fn gen_struct(fields: Vec<(LitStr, Type)>) -> syn::Result { #({ const FIELD_NAME: &::core::primitive::str = #field_name; - let mut field_schema = <#field_ty as ::openapi_type::OpenapiType>::schema(); + let mut field_schema = #field_schema; ::openapi_type::private::add_dependencies( &mut dependencies, &mut field_schema.dependencies @@ -78,12 +87,30 @@ fn gen_struct(fields: Vec<(LitStr, Type)>) -> syn::Result { ) ) } - }) + } } -fn gen_unit() -> syn::Result { +fn gen_enum(variants: &[LitStr]) -> TokenStream { let openapi = path!(::openapi_type::openapi); - Ok(quote! { + quote! { + { + let mut enumeration = <::std::vec::Vec<::std::string::String>>::new(); + #(enumeration.push(::std::string::String::from(#variants));)* + #openapi::SchemaKind::Type( + #openapi::Type::String( + #openapi::StringType { + enumeration, + .. ::std::default::Default::default() + } + ) + ) + } + } +} + +fn gen_unit() -> TokenStream { + let openapi = path!(::openapi_type::openapi); + quote! { #openapi::SchemaKind::Type( #openapi::Type::Object( #openapi::ObjectType { @@ -94,5 +121,5 @@ fn gen_unit() -> syn::Result { } ) ) - }) + } } diff --git a/openapi_type_derive/src/lib.rs b/openapi_type_derive/src/lib.rs index cdbdbf3..e3bc9fc 100644 --- a/openapi_type_derive/src/lib.rs +++ b/openapi_type_derive/src/lib.rs @@ -51,7 +51,7 @@ fn expand_openapi_type(mut input: DeriveInput) -> syn::Result { }; // run the codegen - let schema_code = parsed.gen_schema()?; + let schema_code = parsed.gen_schema(); // put the code together Ok(quote! { diff --git a/openapi_type_derive/src/parser.rs b/openapi_type_derive/src/parser.rs index 4a71c0a..8d0c15f 100644 --- a/openapi_type_derive/src/parser.rs +++ b/openapi_type_derive/src/parser.rs @@ -1,35 +1,93 @@ use crate::util::ToLitStr; -use syn::{spanned::Spanned as _, DataEnum, DataStruct, DataUnion, Fields, LitStr, Type}; +use syn::{spanned::Spanned as _, DataEnum, DataStruct, DataUnion, Fields, FieldsNamed, LitStr, Type}; + +pub(super) enum ParseDataType { + Type(Type), + Inline(ParseData) +} #[allow(dead_code)] pub(super) enum ParseData { - Struct(Vec<(LitStr, Type)>), + Struct(Vec<(LitStr, ParseDataType)>), Enum(Vec), Alternatives(Vec), Unit } +fn parse_named_fields(named_fields: &FieldsNamed) -> syn::Result { + let mut fields: Vec<(LitStr, ParseDataType)> = Vec::new(); + for f in &named_fields.named { + let ident = f + .ident + .as_ref() + .ok_or_else(|| syn::Error::new(f.span(), "#[derive(OpenapiType)] does not support fields without an ident"))?; + let name = ident.to_lit_str(); + let ty = f.ty.to_owned(); + fields.push((name, ParseDataType::Type(ty))); + } + Ok(ParseData::Struct(fields)) +} + pub(super) fn parse_struct(strukt: &DataStruct) -> syn::Result { match &strukt.fields { - Fields::Named(named_fields) => { - let mut fields: Vec<(LitStr, Type)> = Vec::new(); - for f in &named_fields.named { - let ident = f.ident.as_ref().ok_or_else(|| { - syn::Error::new(f.span(), "#[derive(OpenapiType)] does not support fields without an ident") - })?; - let name = ident.to_lit_str(); - let ty = f.ty.to_owned(); - fields.push((name, ty)); - } - Ok(ParseData::Struct(fields)) - }, + Fields::Named(named_fields) => parse_named_fields(named_fields), Fields::Unnamed(_) => unimplemented!(), Fields::Unit => Ok(ParseData::Unit) } } -pub(super) fn parse_enum(_inum: &DataEnum) -> syn::Result { - unimplemented!() +pub(super) fn parse_enum(inum: &DataEnum) -> syn::Result { + let mut strings: Vec = Vec::new(); + let mut types: Vec<(LitStr, ParseData)> = Vec::new(); + + for v in &inum.variants { + let name = v.ident.to_lit_str(); + match &v.fields { + Fields::Named(named_fields) => { + types.push((name, parse_named_fields(named_fields)?)); + }, + Fields::Unnamed(_unnamed_fields) => unimplemented!(), + Fields::Unit => strings.push(name) + } + } + + let data_strings = if strings.is_empty() { + None + } else { + Some(ParseData::Enum(strings)) + }; + + let data_types = if types.is_empty() { + None + } else { + Some(ParseData::Alternatives( + types + .into_iter() + .map(|(name, data)| ParseData::Struct(vec![(name, ParseDataType::Inline(data))])) + .collect() + )) + }; + + match (data_strings, data_types) { + // only variants without fields + (Some(data), None) => Ok(data), + // only one variant with fields + (None, Some(ParseData::Alternatives(mut alt))) if alt.len() == 1 => Ok(alt.remove(0)), + // only variants with fields + (None, Some(data)) => Ok(data), + // variants with and without fields + (Some(data), Some(ParseData::Alternatives(mut alt))) => { + alt.push(data); + Ok(ParseData::Alternatives(alt)) + }, + // no variants + (None, None) => Err(syn::Error::new( + inum.brace_token.span, + "#[derive(OpenapiType)] does not support enums with no variants" + )), + // data_types always produces Alternatives + _ => unreachable!() + } } pub(super) fn parse_union(union: &DataUnion) -> syn::Result { From 5f60599c412b4dc71f8cbd5a066bda0938bc158c Mon Sep 17 00:00:00 2001 From: Dominic Date: Mon, 8 Mar 2021 17:33:49 +0100 Subject: [PATCH 07/18] more enum stuff [skip ci] --- openapi_type/tests/custom_types.rs | 65 ++++++++++++++++++++++++++++++ openapi_type_derive/src/codegen.rs | 22 +++++++++- 2 files changed, 85 insertions(+), 2 deletions(-) diff --git a/openapi_type/tests/custom_types.rs b/openapi_type/tests/custom_types.rs index fce5e57..5119033 100644 --- a/openapi_type/tests/custom_types.rs +++ b/openapi_type/tests/custom_types.rs @@ -77,3 +77,68 @@ test_type!(EnumWithOneField = { }, "required": ["Success"] }); + +#[derive(OpenapiType)] +enum EnumWithFields { + Success { value: isize }, + Error { msg: String } +} +test_type!(EnumWithFields = { + "title": "EnumWithFields", + "oneOf": [{ + "type": "object", + "properties": { + "Success": { + "type": "object", + "properties": { + "value": { + "type": "integer" + } + }, + "required": ["value"] + } + }, + "required": ["Success"] + }, { + "type": "object", + "properties": { + "Error": { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + }, + "required": ["msg"] + } + }, + "required": ["Error"] + }] +}); + +#[derive(OpenapiType)] +enum EnumExternallyTagged { + Success { value: isize }, + Error +} +test_type!(EnumExternallyTagged = { + "title": "EnumExternallyTagged", + "oneOf": [{ + "type": "object", + "properties": { + "Success": { + "type": "object", + "properties": { + "value": { + "type": "integer" + } + }, + "required": ["value"] + } + }, + "required": ["Success"] + }, { + "type": "string", + "enum": ["Error"] + }] +}); diff --git a/openapi_type_derive/src/codegen.rs b/openapi_type_derive/src/codegen.rs index 1ad7d02..a56c97c 100644 --- a/openapi_type_derive/src/codegen.rs +++ b/openapi_type_derive/src/codegen.rs @@ -8,8 +8,8 @@ impl ParseData { match self { Self::Struct(fields) => gen_struct(fields), Self::Enum(variants) => gen_enum(variants), - Self::Unit => gen_unit(), - _ => unimplemented!() + Self::Alternatives(alt) => gen_alt(alt), + Self::Unit => gen_unit() } } } @@ -108,6 +108,24 @@ fn gen_enum(variants: &[LitStr]) -> TokenStream { } } +fn gen_alt(alt: &[ParseData]) -> TokenStream { + let openapi = path!(::openapi_type::openapi); + let schema = alt.iter().map(|data| data.gen_schema()); + quote! { + { + let mut alternatives = <::std::vec::Vec< + #openapi::ReferenceOr<#openapi::Schema> + >>::new(); + #(alternatives.push(#openapi::ReferenceOr::Item( + ::openapi_type::OpenapiSchema::new(#schema).into_schema() + ));)* + #openapi::SchemaKind::OneOf { + one_of: alternatives + } + } + } +} + fn gen_unit() -> TokenStream { let openapi = path!(::openapi_type::openapi); quote! { From a57f1c097da1f79038a27ac04849bd1f1da82270 Mon Sep 17 00:00:00 2001 From: Dominic Date: Tue, 9 Mar 2021 00:17:13 +0100 Subject: [PATCH 08/18] enum representations [skip ci] --- openapi_type/Cargo.toml | 1 + openapi_type/tests/custom_types.rs | 107 +++++++++++++++++++++++++- openapi_type_derive/src/lib.rs | 23 +++++- openapi_type_derive/src/parser.rs | 118 +++++++++++++++++++++++++---- openapi_type_derive/src/util.rs | 16 +++- 5 files changed, 246 insertions(+), 19 deletions(-) diff --git a/openapi_type/Cargo.toml b/openapi_type/Cargo.toml index 028c753..0513c0b 100644 --- a/openapi_type/Cargo.toml +++ b/openapi_type/Cargo.toml @@ -19,4 +19,5 @@ serde_json = "1.0" [dev-dependencies] paste = "1.0" +serde = "1.0" trybuild = "1.0" diff --git a/openapi_type/tests/custom_types.rs b/openapi_type/tests/custom_types.rs index 5119033..18cca88 100644 --- a/openapi_type/tests/custom_types.rs +++ b/openapi_type/tests/custom_types.rs @@ -43,6 +43,15 @@ test_type!(SimpleStruct = { "required": ["foo", "bar"] }); +#[derive(OpenapiType)] +#[openapi(rename = "FooBar")] +struct StructRename; +test_type!(StructRename = { + "type": "object", + "title": "FooBar", + "additionalProperties": false +}); + #[derive(OpenapiType)] enum EnumWithoutFields { Success, @@ -119,6 +128,7 @@ test_type!(EnumWithFields = { #[derive(OpenapiType)] enum EnumExternallyTagged { Success { value: isize }, + Empty, Error } test_type!(EnumExternallyTagged = { @@ -139,6 +149,101 @@ test_type!(EnumExternallyTagged = { "required": ["Success"] }, { "type": "string", - "enum": ["Error"] + "enum": ["Empty", "Error"] + }] +}); + +#[derive(OpenapiType)] +#[openapi(tag = "ty")] +enum EnumInternallyTagged { + Success { value: isize }, + Empty, + Error +} +test_type!(EnumInternallyTagged = { + "title": "EnumInternallyTagged", + "oneOf": [{ + "type": "object", + "properties": { + "value": { + "type": "integer" + }, + "ty": { + "type": "string", + "enum": ["Success"] + } + }, + "required": ["value", "ty"] + }, { + "type": "object", + "properties": { + "ty": { + "type": "string", + "enum": ["Empty", "Error"] + } + }, + "required": ["ty"] + }] +}); + +#[derive(OpenapiType)] +#[openapi(tag = "ty", content = "ct")] +enum EnumAdjacentlyTagged { + Success { value: isize }, + Empty, + Error +} +test_type!(EnumAdjacentlyTagged = { + "title": "EnumAdjacentlyTagged", + "oneOf": [{ + "type": "object", + "properties": { + "ty": { + "type": "string", + "enum": ["Success"] + }, + "ct": { + "type": "object", + "properties": { + "value": { + "type": "integer" + } + }, + "required": ["value"] + } + }, + "required": ["ty", "ct"] + }, { + "type": "object", + "properties": { + "ty": { + "type": "string", + "enum": ["Empty", "Error"] + } + }, + "required": ["ty"] + }] +}); + +#[derive(OpenapiType)] +#[openapi(untagged)] +enum EnumUntagged { + Success { value: isize }, + Empty, + Error +} +test_type!(EnumUntagged = { + "title": "EnumUntagged", + "oneOf": [{ + "type": "object", + "properties": { + "value": { + "type": "integer" + } + }, + "required": ["value"] + }, { + "type": "object", + "additionalProperties": false }] }); diff --git a/openapi_type_derive/src/lib.rs b/openapi_type_derive/src/lib.rs index e3bc9fc..6d82331 100644 --- a/openapi_type_derive/src/lib.rs +++ b/openapi_type_derive/src/lib.rs @@ -17,16 +17,33 @@ mod parser; use parser::*; /// The derive macro for [OpenapiType][openapi_type::OpenapiType]. -#[proc_macro_derive(OpenapiType)] +#[proc_macro_derive(OpenapiType, attributes(openapi))] pub fn derive_openapi_type(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input); expand_openapi_type(input).unwrap_or_else(|err| err.to_compile_error()).into() } fn expand_openapi_type(mut input: DeriveInput) -> syn::Result { + // parse #[serde] and #[openapi] attributes + let mut attrs = ContainerAttributes::default(); + for attr in &input.attrs { + if attr.path.is_ident("serde") { + parse_container_attrs(attr, &mut attrs, false)?; + } + } + for attr in &input.attrs { + if attr.path.is_ident("openapi") { + parse_container_attrs(attr, &mut attrs, true)?; + } + } + + // prepare impl block for codegen let ident = &input.ident; let name = ident.to_string(); - let name = LitStr::new(&name, ident.span()); + let mut name = LitStr::new(&name, ident.span()); + if let Some(rename) = &attrs.rename { + name = rename.clone(); + } // prepare the generics - all impl generics will get `OpenapiType` requirement let (impl_generics, ty_generics, where_clause) = { @@ -46,7 +63,7 @@ fn expand_openapi_type(mut input: DeriveInput) -> syn::Result { // parse the input data let parsed = match &input.data { Data::Struct(strukt) => parse_struct(strukt)?, - Data::Enum(inum) => parse_enum(inum)?, + Data::Enum(inum) => parse_enum(inum, &attrs)?, Data::Union(union) => parse_union(union)? }; diff --git a/openapi_type_derive/src/parser.rs b/openapi_type_derive/src/parser.rs index 8d0c15f..5e57736 100644 --- a/openapi_type_derive/src/parser.rs +++ b/openapi_type_derive/src/parser.rs @@ -1,5 +1,9 @@ -use crate::util::ToLitStr; -use syn::{spanned::Spanned as _, DataEnum, DataStruct, DataUnion, Fields, FieldsNamed, LitStr, Type}; +use crate::util::{ExpectLit, ToLitStr}; +use proc_macro2::Span; +use syn::{ + punctuated::Punctuated, spanned::Spanned as _, Attribute, DataEnum, DataStruct, DataUnion, Fields, FieldsNamed, LitStr, + Meta, Token, Type +}; pub(super) enum ParseDataType { Type(Type), @@ -36,7 +40,7 @@ pub(super) fn parse_struct(strukt: &DataStruct) -> syn::Result { } } -pub(super) fn parse_enum(inum: &DataEnum) -> syn::Result { +pub(super) fn parse_enum(inum: &DataEnum, attrs: &ContainerAttributes) -> syn::Result { let mut strings: Vec = Vec::new(); let mut types: Vec<(LitStr, ParseData)> = Vec::new(); @@ -54,19 +58,59 @@ pub(super) fn parse_enum(inum: &DataEnum) -> syn::Result { let data_strings = if strings.is_empty() { None } else { - Some(ParseData::Enum(strings)) + match (&attrs.tag, &attrs.content, attrs.untagged) { + // externally tagged (default) + (None, None, false) => Some(ParseData::Enum(strings)), + // internally tagged or adjacently tagged + (Some(tag), _, false) => Some(ParseData::Struct(vec![( + tag.clone(), + ParseDataType::Inline(ParseData::Enum(strings)) + )])), + // untagged + (None, None, true) => Some(ParseData::Unit), + // unknown + _ => return Err(syn::Error::new(Span::call_site(), "Unknown enum representation")) + } }; - let data_types = if types.is_empty() { - None - } else { - Some(ParseData::Alternatives( - types - .into_iter() - .map(|(name, data)| ParseData::Struct(vec![(name, ParseDataType::Inline(data))])) - .collect() - )) - }; + let data_types = + if types.is_empty() { + None + } else { + Some(ParseData::Alternatives( + types + .into_iter() + .map(|(name, mut data)| { + Ok(match (&attrs.tag, &attrs.content, attrs.untagged) { + // externally tagged (default) + (None, None, false) => ParseData::Struct(vec![(name, ParseDataType::Inline(data))]), + // internally tagged + (Some(tag), None, false) => { + match &mut data { + ParseData::Struct(fields) => { + fields.push((tag.clone(), ParseDataType::Inline(ParseData::Enum(vec![name])))) + }, + _ => return Err(syn::Error::new( + tag.span(), + "#[derive(OpenapiType)] does not support tuple variants on internally tagged enums" + )) + }; + data + }, + // adjacently tagged + (Some(tag), Some(content), false) => ParseData::Struct(vec![ + (tag.clone(), ParseDataType::Inline(ParseData::Enum(vec![name]))), + (content.clone(), ParseDataType::Inline(data)), + ]), + // untagged + (None, None, true) => data, + // unknown + _ => return Err(syn::Error::new(Span::call_site(), "Unknown enum representation")) + }) + }) + .collect::>>()? + )) + }; match (data_strings, data_types) { // only variants without fields @@ -96,3 +140,49 @@ pub(super) fn parse_union(union: &DataUnion) -> syn::Result { "#[derive(OpenapiType)] cannot be used on unions" )) } + +#[derive(Default)] +pub(super) struct ContainerAttributes { + pub(super) rename: Option, + pub(super) rename_all: Option, + pub(super) tag: Option, + pub(super) content: Option, + pub(super) untagged: bool +} + +pub(super) fn parse_container_attrs( + input: &Attribute, + attrs: &mut ContainerAttributes, + error_on_unknown: bool +) -> syn::Result<()> { + let tokens: Punctuated = input.parse_args_with(Punctuated::parse_terminated)?; + for token in tokens { + match token { + Meta::NameValue(kv) if kv.path.is_ident("rename") => { + attrs.rename = Some(kv.lit.expect_str()?); + }, + + Meta::NameValue(kv) if kv.path.is_ident("rename_all") => { + attrs.rename_all = Some(kv.lit.expect_str()?); + }, + + Meta::NameValue(kv) if kv.path.is_ident("tag") => { + attrs.tag = Some(kv.lit.expect_str()?); + }, + + Meta::NameValue(kv) if kv.path.is_ident("content") => { + attrs.content = Some(kv.lit.expect_str()?); + }, + + Meta::Path(path) if path.is_ident("untagged") => { + attrs.untagged = true; + }, + + Meta::Path(path) if error_on_unknown => return Err(syn::Error::new(path.span(), "Unexpected token")), + Meta::List(list) if error_on_unknown => return Err(syn::Error::new(list.span(), "Unexpected token")), + Meta::NameValue(kv) if error_on_unknown => return Err(syn::Error::new(kv.path.span(), "Unexpected token")), + _ => {} + } + } + Ok(()) +} diff --git a/openapi_type_derive/src/util.rs b/openapi_type_derive/src/util.rs index 9470a04..2a752e0 100644 --- a/openapi_type_derive/src/util.rs +++ b/openapi_type_derive/src/util.rs @@ -1,5 +1,5 @@ use proc_macro2::Ident; -use syn::LitStr; +use syn::{Lit, LitStr}; /// Convert any literal path into a [syn::Path]. macro_rules! path { @@ -36,3 +36,17 @@ impl ToLitStr for Ident { LitStr::new(&self.to_string(), self.span()) } } + +/// Convert a [Lit] to one specific literal type. +pub(crate) trait ExpectLit { + fn expect_str(self) -> syn::Result; +} + +impl ExpectLit for Lit { + fn expect_str(self) -> syn::Result { + match self { + Self::Str(str) => Ok(str), + _ => Err(syn::Error::new(self.span(), "Expected string literal")) + } + } +} From 2a35e044dbc4a433f6dcded23d3a41e8898b6a03 Mon Sep 17 00:00:00 2001 From: Dominic Date: Tue, 9 Mar 2021 16:17:11 +0100 Subject: [PATCH 09/18] more error messages [skip ci] --- openapi_type/tests/fail/tuple_struct.rs | 6 ++++++ openapi_type/tests/fail/tuple_struct.stderr | 5 +++++ openapi_type/tests/fail/tuple_variant.rs | 8 ++++++++ openapi_type/tests/fail/tuple_variant.stderr | 5 +++++ openapi_type/tests/fail/union.rs | 9 +++++++++ openapi_type/tests/fail/union.stderr | 5 +++++ openapi_type/tests/fail/unknown_attribute.rs | 7 +++++++ openapi_type/tests/fail/unknown_attribute.stderr | 5 +++++ openapi_type_derive/src/parser.rs | 14 ++++++++++++-- 9 files changed, 62 insertions(+), 2 deletions(-) create mode 100644 openapi_type/tests/fail/tuple_struct.rs create mode 100644 openapi_type/tests/fail/tuple_struct.stderr create mode 100644 openapi_type/tests/fail/tuple_variant.rs create mode 100644 openapi_type/tests/fail/tuple_variant.stderr create mode 100644 openapi_type/tests/fail/union.rs create mode 100644 openapi_type/tests/fail/union.stderr create mode 100644 openapi_type/tests/fail/unknown_attribute.rs create mode 100644 openapi_type/tests/fail/unknown_attribute.stderr diff --git a/openapi_type/tests/fail/tuple_struct.rs b/openapi_type/tests/fail/tuple_struct.rs new file mode 100644 index 0000000..146a236 --- /dev/null +++ b/openapi_type/tests/fail/tuple_struct.rs @@ -0,0 +1,6 @@ +use openapi_type::OpenapiType; + +#[derive(OpenapiType)] +struct Foo(i64, i64); + +fn main() {} diff --git a/openapi_type/tests/fail/tuple_struct.stderr b/openapi_type/tests/fail/tuple_struct.stderr new file mode 100644 index 0000000..b5ceb01 --- /dev/null +++ b/openapi_type/tests/fail/tuple_struct.stderr @@ -0,0 +1,5 @@ +error: #[derive(OpenapiType)] does not support tuple structs + --> $DIR/tuple_struct.rs:4:11 + | +4 | struct Foo(i64, i64); + | ^^^^^^^^^^ diff --git a/openapi_type/tests/fail/tuple_variant.rs b/openapi_type/tests/fail/tuple_variant.rs new file mode 100644 index 0000000..92aa8d7 --- /dev/null +++ b/openapi_type/tests/fail/tuple_variant.rs @@ -0,0 +1,8 @@ +use openapi_type::OpenapiType; + +#[derive(OpenapiType)] +enum Foo { + Pair(i64, i64) +} + +fn main() {} diff --git a/openapi_type/tests/fail/tuple_variant.stderr b/openapi_type/tests/fail/tuple_variant.stderr new file mode 100644 index 0000000..05573cb --- /dev/null +++ b/openapi_type/tests/fail/tuple_variant.stderr @@ -0,0 +1,5 @@ +error: #[derive(OpenapiType)] does not support tuple variants + --> $DIR/tuple_variant.rs:5:6 + | +5 | Pair(i64, i64) + | ^^^^^^^^^^ diff --git a/openapi_type/tests/fail/union.rs b/openapi_type/tests/fail/union.rs new file mode 100644 index 0000000..d011109 --- /dev/null +++ b/openapi_type/tests/fail/union.rs @@ -0,0 +1,9 @@ +use openapi_type::OpenapiType; + +#[derive(OpenapiType)] +union Foo { + signed: i64, + unsigned: u64 +} + +fn main() {} diff --git a/openapi_type/tests/fail/union.stderr b/openapi_type/tests/fail/union.stderr new file mode 100644 index 0000000..f0feb48 --- /dev/null +++ b/openapi_type/tests/fail/union.stderr @@ -0,0 +1,5 @@ +error: #[derive(OpenapiType)] cannot be used on unions + --> $DIR/union.rs:4:1 + | +4 | union Foo { + | ^^^^^ diff --git a/openapi_type/tests/fail/unknown_attribute.rs b/openapi_type/tests/fail/unknown_attribute.rs new file mode 100644 index 0000000..70a4785 --- /dev/null +++ b/openapi_type/tests/fail/unknown_attribute.rs @@ -0,0 +1,7 @@ +use openapi_type::OpenapiType; + +#[derive(OpenapiType)] +#[openapi(pizza)] +struct Foo; + +fn main() {} diff --git a/openapi_type/tests/fail/unknown_attribute.stderr b/openapi_type/tests/fail/unknown_attribute.stderr new file mode 100644 index 0000000..2558768 --- /dev/null +++ b/openapi_type/tests/fail/unknown_attribute.stderr @@ -0,0 +1,5 @@ +error: Unexpected token + --> $DIR/unknown_attribute.rs:4:11 + | +4 | #[openapi(pizza)] + | ^^^^^ diff --git a/openapi_type_derive/src/parser.rs b/openapi_type_derive/src/parser.rs index 5e57736..350fee2 100644 --- a/openapi_type_derive/src/parser.rs +++ b/openapi_type_derive/src/parser.rs @@ -35,7 +35,12 @@ fn parse_named_fields(named_fields: &FieldsNamed) -> syn::Result { pub(super) fn parse_struct(strukt: &DataStruct) -> syn::Result { match &strukt.fields { Fields::Named(named_fields) => parse_named_fields(named_fields), - Fields::Unnamed(_) => unimplemented!(), + Fields::Unnamed(unnamed_fields) => { + return Err(syn::Error::new( + unnamed_fields.span(), + "#[derive(OpenapiType)] does not support tuple structs" + )) + }, Fields::Unit => Ok(ParseData::Unit) } } @@ -50,7 +55,12 @@ pub(super) fn parse_enum(inum: &DataEnum, attrs: &ContainerAttributes) -> syn::R Fields::Named(named_fields) => { types.push((name, parse_named_fields(named_fields)?)); }, - Fields::Unnamed(_unnamed_fields) => unimplemented!(), + Fields::Unnamed(unnamed_fields) => { + return Err(syn::Error::new( + unnamed_fields.span(), + "#[derive(OpenapiType)] does not support tuple variants" + )) + }, Fields::Unit => strings.push(name) } } From eecd1924580d958233ea8a3dd7ba32b6000572ab Mon Sep 17 00:00:00 2001 From: Dominic Date: Tue, 9 Mar 2021 17:07:16 +0100 Subject: [PATCH 10/18] redo and test openapi type implementations --- openapi_type/Cargo.toml | 4 + openapi_type/src/impls.rs | 483 +++++++++++++------------------- openapi_type/tests/std_types.rs | 216 ++++++++++++++ src/openapi/types.rs | 1 - 4 files changed, 413 insertions(+), 291 deletions(-) create mode 100644 openapi_type/tests/std_types.rs diff --git a/openapi_type/Cargo.toml b/openapi_type/Cargo.toml index 0513c0b..782f798 100644 --- a/openapi_type/Cargo.toml +++ b/openapi_type/Cargo.toml @@ -17,6 +17,10 @@ openapi_type_derive = "0.1.0-dev" openapiv3 = "=0.3.2" serde_json = "1.0" +# optional dependencies / features +chrono = { version = "0.4.19", optional = true } +uuid = { version = "0.8.2" , optional = true } + [dev-dependencies] paste = "1.0" serde = "1.0" diff --git a/openapi_type/src/impls.rs b/openapi_type/src/impls.rs index f363818..d9396fd 100644 --- a/openapi_type/src/impls.rs +++ b/openapi_type/src/impls.rs @@ -1,321 +1,224 @@ use crate::{OpenapiSchema, OpenapiType}; -use indexmap::IndexMap; +#[cfg(feature = "chrono")] +use chrono::{offset::TimeZone, Date, DateTime, NaiveDate, NaiveDateTime}; +use indexmap::{IndexMap, IndexSet}; use openapiv3::{ - AdditionalProperties, ArrayType, IntegerType, NumberFormat, NumberType, ObjectType, ReferenceOr, SchemaKind, StringType, - Type, VariantOrUnknownOrEmpty + AdditionalProperties, ArrayType, IntegerType, NumberFormat, NumberType, ObjectType, ReferenceOr, SchemaKind, + StringFormat, StringType, Type, VariantOrUnknownOrEmpty }; +use serde_json::Value; use std::{ - collections::{BTreeSet, HashMap, HashSet}, + collections::{BTreeMap, BTreeSet, HashMap, HashSet}, hash::BuildHasher, num::{NonZeroU128, NonZeroU16, NonZeroU32, NonZeroU64, NonZeroU8, NonZeroUsize} }; +#[cfg(feature = "uuid")] +use uuid::Uuid; -impl OpenapiType for () { - fn schema() -> OpenapiSchema { - OpenapiSchema::new(SchemaKind::Type(Type::Object(ObjectType { - additional_properties: Some(AdditionalProperties::Any(false)), - ..Default::default() - }))) +macro_rules! impl_openapi_type { + ($($ty:ident $(<$($generic:ident : $bound:path),+>)*),* => $schema:expr) => { + $( + impl $(<$($generic : $bound),+>)* OpenapiType for $ty $(<$($generic),+>)* { + fn schema() -> OpenapiSchema { + $schema + } + } + )* + }; +} + +type Unit = (); +impl_openapi_type!(Unit => { + OpenapiSchema::new(SchemaKind::Type(Type::Object(ObjectType { + additional_properties: Some(AdditionalProperties::Any(false)), + ..Default::default() + }))) +}); + +impl_openapi_type!(Value => { + OpenapiSchema { + nullable: true, + name: None, + schema: SchemaKind::Any(Default::default()), + dependencies: Default::default() } +}); + +impl_openapi_type!(bool => OpenapiSchema::new(SchemaKind::Type(Type::Boolean {}))); + +#[inline] +fn int_schema(minimum: Option, bits: Option) -> OpenapiSchema { + OpenapiSchema::new(SchemaKind::Type(Type::Integer(IntegerType { + minimum, + format: bits + .map(|bits| VariantOrUnknownOrEmpty::Unknown(format!("int{}", bits))) + .unwrap_or(VariantOrUnknownOrEmpty::Empty), + ..Default::default() + }))) } -impl OpenapiType for bool { - fn schema() -> OpenapiSchema { - OpenapiSchema::new(SchemaKind::Type(Type::Boolean {})) - } +impl_openapi_type!(isize => int_schema(None, None)); +impl_openapi_type!(i8 => int_schema(None, Some(8))); +impl_openapi_type!(i16 => int_schema(None, Some(16))); +impl_openapi_type!(i32 => int_schema(None, Some(32))); +impl_openapi_type!(i64 => int_schema(None, Some(64))); +impl_openapi_type!(i128 => int_schema(None, Some(128))); + +impl_openapi_type!(usize => int_schema(Some(0), None)); +impl_openapi_type!(u8 => int_schema(Some(0), Some(8))); +impl_openapi_type!(u16 => int_schema(Some(0), Some(16))); +impl_openapi_type!(u32 => int_schema(Some(0), Some(32))); +impl_openapi_type!(u64 => int_schema(Some(0), Some(64))); +impl_openapi_type!(u128 => int_schema(Some(0), Some(128))); + +impl_openapi_type!(NonZeroUsize => int_schema(Some(1), None)); +impl_openapi_type!(NonZeroU8 => int_schema(Some(1), Some(8))); +impl_openapi_type!(NonZeroU16 => int_schema(Some(1), Some(16))); +impl_openapi_type!(NonZeroU32 => int_schema(Some(1), Some(32))); +impl_openapi_type!(NonZeroU64 => int_schema(Some(1), Some(64))); +impl_openapi_type!(NonZeroU128 => int_schema(Some(1), Some(128))); + +#[inline] +fn float_schema(format: NumberFormat) -> OpenapiSchema { + OpenapiSchema::new(SchemaKind::Type(Type::Number(NumberType { + format: VariantOrUnknownOrEmpty::Item(format), + ..Default::default() + }))) } -macro_rules! int_types { - ($($int_ty:ty),*) => {$( - impl OpenapiType for $int_ty - { - fn schema() -> OpenapiSchema - { - OpenapiSchema::new(SchemaKind::Type(Type::Integer(IntegerType::default()))) - } - } - )*}; +impl_openapi_type!(f32 => float_schema(NumberFormat::Float)); +impl_openapi_type!(f64 => float_schema(NumberFormat::Double)); - (unsigned $($int_ty:ty),*) => {$( - impl OpenapiType for $int_ty - { - fn schema() -> OpenapiSchema - { - OpenapiSchema::new(SchemaKind::Type(Type::Integer(IntegerType { - minimum: Some(0), - ..Default::default() - }))) - } - } - )*}; - - (gtzero $($int_ty:ty),*) => {$( - impl OpenapiType for $int_ty - { - fn schema() -> OpenapiSchema - { - OpenapiSchema::new(SchemaKind::Type(Type::Integer(IntegerType { - minimum: Some(1), - ..Default::default() - }))) - } - } - )*}; - - (bits = $bits:expr, $($int_ty:ty),*) => {$( - impl OpenapiType for $int_ty - { - fn schema() -> OpenapiSchema - { - OpenapiSchema::new(SchemaKind::Type(Type::Integer(IntegerType { - format: VariantOrUnknownOrEmpty::Unknown(format!("int{}", $bits)), - ..Default::default() - }))) - } - } - )*}; - - (unsigned bits = $bits:expr, $($int_ty:ty),*) => {$( - impl OpenapiType for $int_ty - { - fn schema() -> OpenapiSchema - { - OpenapiSchema::new(SchemaKind::Type(Type::Integer(IntegerType { - format: VariantOrUnknownOrEmpty::Unknown(format!("int{}", $bits)), - minimum: Some(0), - ..Default::default() - }))) - } - } - )*}; - - (gtzero bits = $bits:expr, $($int_ty:ty),*) => {$( - impl OpenapiType for $int_ty - { - fn schema() -> OpenapiSchema - { - OpenapiSchema::new(SchemaKind::Type(Type::Integer(IntegerType { - format: VariantOrUnknownOrEmpty::Unknown(format!("int{}", $bits)), - minimum: Some(1), - ..Default::default() - }))) - } - } - )*}; +#[inline] +fn str_schema(format: VariantOrUnknownOrEmpty) -> OpenapiSchema { + OpenapiSchema::new(SchemaKind::Type(Type::String(StringType { + format, + ..Default::default() + }))) } -int_types!(isize); -int_types!(unsigned usize); -int_types!(gtzero NonZeroUsize); -int_types!(bits = 8, i8); -int_types!(unsigned bits = 8, u8); -int_types!(gtzero bits = 8, NonZeroU8); -int_types!(bits = 16, i16); -int_types!(unsigned bits = 16, u16); -int_types!(gtzero bits = 16, NonZeroU16); -int_types!(bits = 32, i32); -int_types!(unsigned bits = 32, u32); -int_types!(gtzero bits = 32, NonZeroU32); -int_types!(bits = 64, i64); -int_types!(unsigned bits = 64, u64); -int_types!(gtzero bits = 64, NonZeroU64); -int_types!(bits = 128, i128); -int_types!(unsigned bits = 128, u128); -int_types!(gtzero bits = 128, NonZeroU128); - -macro_rules! num_types { - ($($num_ty:ty = $num_fmt:ident),*) => {$( - impl OpenapiType for $num_ty - { - fn schema() -> OpenapiSchema - { - OpenapiSchema::new(SchemaKind::Type(Type::Number(NumberType { - format: VariantOrUnknownOrEmpty::Item(NumberFormat::$num_fmt), - ..Default::default() - }))) - } - } - )*} -} - -num_types!(f32 = Float, f64 = Double); - -macro_rules! str_types { - ($($str_ty:ty),*) => {$( - impl OpenapiType for $str_ty - { - fn schema() -> OpenapiSchema - { - OpenapiSchema::new(SchemaKind::Type(Type::String(StringType::default()))) - } - } - )*}; - - (format = $format:ident, $($str_ty:ty),*) => {$( - impl OpenapiType for $str_ty - { - fn schema() -> OpenapiSchema - { - use openapiv3::StringFormat; - - OpenapiSchema::new(SchemaKind::Type(Type::String(StringType { - format: VariantOrUnknownOrEmpty::Item(StringFormat::$format), - ..Default::default() - }))) - } - } - )*}; - - (format_str = $format:expr, $($str_ty:ty),*) => {$( - impl OpenapiType for $str_ty - { - fn schema() -> OpenapiSchema - { - OpenapiSchema::new(SchemaKind::Type(Type::String(StringType { - format: VariantOrUnknownOrEmpty::Unknown($format.to_string()), - ..Default::default() - }))) - } - } - )*}; -} - -str_types!(String, &str); +impl_openapi_type!(String => str_schema(VariantOrUnknownOrEmpty::Empty)); #[cfg(feature = "chrono")] -str_types!(format = Date, Date, Date, Date, NaiveDate); +impl_openapi_type!(Date, NaiveDate => { + str_schema(VariantOrUnknownOrEmpty::Item(StringFormat::Date)) +}); + #[cfg(feature = "chrono")] -str_types!( - format = DateTime, - DateTime, - DateTime, - DateTime, - NaiveDateTime -); +impl_openapi_type!(DateTime, NaiveDateTime => { + str_schema(VariantOrUnknownOrEmpty::Item(StringFormat::DateTime)) +}); #[cfg(feature = "uuid")] -str_types!(format_str = "uuid", Uuid); +impl_openapi_type!(Uuid => { + str_schema(VariantOrUnknownOrEmpty::Unknown("uuid".to_owned())) +}); -impl OpenapiType for Option { - fn schema() -> OpenapiSchema { - let schema = T::schema(); - let mut dependencies = schema.dependencies.clone(); - let schema = match schema.name.clone() { - Some(name) => { - let reference = ReferenceOr::Reference { - reference: format!("#/components/schemas/{}", name) - }; - dependencies.insert(name, schema); - SchemaKind::AllOf { all_of: vec![reference] } - }, - None => schema.schema - }; +impl_openapi_type!(Option => { + let schema = T::schema(); + let mut dependencies = schema.dependencies.clone(); + let schema = match schema.name.clone() { + Some(name) => { + let reference = ReferenceOr::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, - dependencies - } + OpenapiSchema { + nullable: true, + name: None, + schema, + dependencies + } +}); + +#[inline] +fn array_schema(unique_items: bool) -> OpenapiSchema { + let schema = T::schema(); + let mut dependencies = schema.dependencies.clone(); + + let items = match schema.name.clone() { + Some(name) => { + let reference = ReferenceOr::Reference { + reference: format!("#/components/schemas/{}", name) + }; + dependencies.insert(name, schema); + reference + }, + None => ReferenceOr::Item(Box::new(schema.into_schema())) + }; + + OpenapiSchema { + nullable: false, + name: None, + schema: SchemaKind::Type(Type::Array(ArrayType { + items, + min_items: None, + max_items: None, + unique_items + })), + dependencies } } -impl OpenapiType for Vec { - fn schema() -> OpenapiSchema { - let schema = T::schema(); - let mut dependencies = schema.dependencies.clone(); +impl_openapi_type!(Vec => array_schema::(false)); +impl_openapi_type!(BTreeSet, IndexSet, HashSet => { + array_schema::(true) +}); - let items = match schema.name.clone() { - Some(name) => { - let reference = ReferenceOr::Reference { - reference: format!("#/components/schemas/{}", name) - }; - dependencies.insert(name, schema); - reference - }, - None => ReferenceOr::Item(Box::new(schema.into_schema())) - }; +#[inline] +fn map_schema() -> OpenapiSchema { + let key_schema = K::schema(); + let mut dependencies = key_schema.dependencies.clone(); - OpenapiSchema { - nullable: false, - name: None, - schema: SchemaKind::Type(Type::Array(ArrayType { - items, - min_items: None, - max_items: None, - unique_items: false - })), - dependencies - } + let keys = match key_schema.name.clone() { + Some(name) => { + let reference = ReferenceOr::Reference { + reference: format!("#/components/schemas/{}", name) + }; + dependencies.insert(name, key_schema); + reference + }, + None => ReferenceOr::Item(Box::new(key_schema.into_schema())) + }; + + let schema = T::schema(); + dependencies.extend(schema.dependencies.iter().map(|(k, v)| (k.clone(), v.clone()))); + + let items = Box::new(match schema.name.clone() { + Some(name) => { + let reference = ReferenceOr::Reference { + reference: format!("#/components/schemas/{}", name) + }; + dependencies.insert(name, schema); + reference + }, + None => ReferenceOr::Item(schema.into_schema()) + }); + + let mut properties = IndexMap::new(); + properties.insert("default".to_owned(), keys); + + OpenapiSchema { + nullable: false, + name: None, + schema: SchemaKind::Type(Type::Object(ObjectType { + properties, + required: vec!["default".to_owned()], + additional_properties: Some(AdditionalProperties::Schema(items)), + ..Default::default() + })), + dependencies } } -impl OpenapiType for BTreeSet { - fn schema() -> OpenapiSchema { - as OpenapiType>::schema() - } -} - -impl OpenapiType for HashSet { - fn schema() -> OpenapiSchema { - as OpenapiType>::schema() - } -} - -impl OpenapiType for HashMap { - fn schema() -> OpenapiSchema { - let key_schema = K::schema(); - let mut dependencies = key_schema.dependencies.clone(); - - let keys = match key_schema.name.clone() { - Some(name) => { - let reference = ReferenceOr::Reference { - reference: format!("#/components/schemas/{}", name) - }; - dependencies.insert(name, key_schema); - reference - }, - None => ReferenceOr::Item(Box::new(key_schema.into_schema())) - }; - - let schema = T::schema(); - dependencies.extend(schema.dependencies.iter().map(|(k, v)| (k.clone(), v.clone()))); - - let items = Box::new(match schema.name.clone() { - Some(name) => { - let reference = ReferenceOr::Reference { - reference: format!("#/components/schemas/{}", name) - }; - dependencies.insert(name, schema); - reference - }, - None => ReferenceOr::Item(schema.into_schema()) - }); - - let mut properties = IndexMap::new(); - properties.insert("default".to_owned(), keys); - - OpenapiSchema { - nullable: false, - name: None, - schema: SchemaKind::Type(Type::Object(ObjectType { - properties, - required: vec!["default".to_owned()], - additional_properties: Some(AdditionalProperties::Schema(items)), - ..Default::default() - })), - dependencies - } - } -} - -impl OpenapiType for serde_json::Value { - fn schema() -> OpenapiSchema { - OpenapiSchema { - nullable: true, - name: None, - schema: SchemaKind::Any(Default::default()), - dependencies: Default::default() - } - } -} +impl_openapi_type!( + BTreeMap, + IndexMap, + HashMap + => map_schema::() +); diff --git a/openapi_type/tests/std_types.rs b/openapi_type/tests/std_types.rs new file mode 100644 index 0000000..e10fb89 --- /dev/null +++ b/openapi_type/tests/std_types.rs @@ -0,0 +1,216 @@ +#[cfg(feature = "chrono")] +use chrono::{Date, DateTime, FixedOffset, Local, NaiveDate, NaiveDateTime, Utc}; +use indexmap::{IndexMap, IndexSet}; +use openapi_type::OpenapiType; +use serde_json::Value; +use std::{ + collections::{BTreeMap, BTreeSet, HashMap, HashSet}, + num::{NonZeroU128, NonZeroU16, NonZeroU32, NonZeroU64, NonZeroU8, NonZeroUsize} +}; +#[cfg(feature = "uuid")] +use uuid::Uuid; + +macro_rules! test_type { + ($($ty:ident $(<$($generic:ident),+>)*),* = $json:tt) => { + paste::paste! { $( + #[test] + fn [< $ty:lower $($(_ $generic:lower)+)* >]() { + let schema = <$ty $(<$($generic),+>)* as OpenapiType>::schema(); + let schema = openapi_type::OpenapiSchema::into_schema(schema); + let schema_json = serde_json::to_value(&schema).unwrap(); + let expected = serde_json::json!($json); + assert_eq!(schema_json, expected); + } + )* } + }; +} + +type Unit = (); +test_type!(Unit = { + "type": "object", + "additionalProperties": false +}); + +test_type!(Value = { + "nullable": true +}); + +test_type!(bool = { + "type": "boolean" +}); + +// ### integer types + +test_type!(isize = { + "type": "integer" +}); + +test_type!(usize = { + "type": "integer", + "minimum": 0 +}); + +test_type!(i8 = { + "type": "integer", + "format": "int8" +}); + +test_type!(u8 = { + "type": "integer", + "format": "int8", + "minimum": 0 +}); + +test_type!(i16 = { + "type": "integer", + "format": "int16" +}); + +test_type!(u16 = { + "type": "integer", + "format": "int16", + "minimum": 0 +}); + +test_type!(i32 = { + "type": "integer", + "format": "int32" +}); + +test_type!(u32 = { + "type": "integer", + "format": "int32", + "minimum": 0 +}); + +test_type!(i64 = { + "type": "integer", + "format": "int64" +}); + +test_type!(u64 = { + "type": "integer", + "format": "int64", + "minimum": 0 +}); + +test_type!(i128 = { + "type": "integer", + "format": "int128" +}); + +test_type!(u128 = { + "type": "integer", + "format": "int128", + "minimum": 0 +}); + +// ### non-zero integer types + +test_type!(NonZeroUsize = { + "type": "integer", + "minimum": 1 +}); + +test_type!(NonZeroU8 = { + "type": "integer", + "format": "int8", + "minimum": 1 +}); + +test_type!(NonZeroU16 = { + "type": "integer", + "format": "int16", + "minimum": 1 +}); + +test_type!(NonZeroU32 = { + "type": "integer", + "format": "int32", + "minimum": 1 +}); + +test_type!(NonZeroU64 = { + "type": "integer", + "format": "int64", + "minimum": 1 +}); + +test_type!(NonZeroU128 = { + "type": "integer", + "format": "int128", + "minimum": 1 +}); + +// ### floats + +test_type!(f32 = { + "type": "number", + "format": "float" +}); + +test_type!(f64 = { + "type": "number", + "format": "double" +}); + +// ### string + +test_type!(String = { + "type": "string" +}); + +#[cfg(feature = "uuid")] +test_type!(Uuid = { + "type": "string", + "format": "uuid" +}); + +// ### date/time + +#[cfg(feature = "chrono")] +test_type!(Date, Date, Date, NaiveDate = { + "type": "string", + "format": "date" +}); + +#[cfg(feature = "chrono")] +test_type!(DateTime, DateTime, DateTime, NaiveDateTime = { + "type": "string", + "format": "date-time" +}); + +// ### some std types + +test_type!(Option = { + "type": "string", + "nullable": true +}); + +test_type!(Vec = { + "type": "array", + "items": { + "type": "string" + } +}); + +test_type!(BTreeSet, IndexSet, HashSet = { + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true +}); + +test_type!(BTreeMap, IndexMap, HashMap = { + "type": "object", + "properties": { + "default": { + "type": "integer" + } + }, + "required": ["default"], + "additionalProperties": { + "type": "string" + } +}); diff --git a/src/openapi/types.rs b/src/openapi/types.rs index 18f5be6..66bf059 100644 --- a/src/openapi/types.rs +++ b/src/openapi/types.rs @@ -7,7 +7,6 @@ use openapiv3::{ ReferenceOr::{Item, Reference}, Schema, SchemaData, SchemaKind, StringType, Type, VariantOrUnknownOrEmpty }; - use std::{ collections::{BTreeSet, HashMap, HashSet}, hash::BuildHasher, From ebea39fe0d5f9ae446dfb42bbff70a38096376ab Mon Sep 17 00:00:00 2001 From: Dominic Date: Tue, 9 Mar 2021 19:46:11 +0100 Subject: [PATCH 11/18] use openapi_type::OpenapiType for gotham_restful --- Cargo.toml | 13 +- README.md | 20 +- derive/Cargo.toml | 2 +- derive/src/endpoint.rs | 10 +- derive/src/lib.rs | 11 - derive/src/openapi_type.rs | 289 -------------------- derive/src/request_body.rs | 23 +- openapi_type/src/impls.rs | 2 +- openapi_type/src/lib.rs | 6 + openapi_type_derive/src/lib.rs | 2 +- src/endpoint.rs | 50 +++- src/lib.rs | 39 ++- src/openapi/builder.rs | 4 +- src/openapi/mod.rs | 1 - src/openapi/operation.rs | 3 +- src/openapi/router.rs | 3 +- src/openapi/types.rs | 476 --------------------------------- src/response/mod.rs | 7 +- src/response/no_content.rs | 6 +- src/response/raw.rs | 4 +- src/response/redirect.rs | 4 +- src/response/result.rs | 6 +- src/response/success.rs | 8 +- src/routing.rs | 12 +- src/types.rs | 5 +- tests/async_methods.rs | 4 +- tests/sync_methods.rs | 4 +- 27 files changed, 148 insertions(+), 866 deletions(-) delete mode 100644 derive/src/openapi_type.rs delete mode 100644 src/openapi/types.rs diff --git a/Cargo.toml b/Cargo.toml index 7c0baee..787b6da 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,26 +24,23 @@ futures-core = "0.3.7" futures-util = "0.3.7" gotham = { git = "https://github.com/gotham-rs/gotham", default-features = false } gotham_derive = "0.5.0" -gotham_restful_derive = "0.2.0" +gotham_restful_derive = "0.3.0-dev" log = "0.4.8" mime = "0.3.16" serde = { version = "1.0.110", features = ["derive"] } serde_json = "1.0.58" thiserror = "1.0" -# features -chrono = { version = "0.4.19", features = ["serde"], optional = true } -uuid = { version = "0.8.1", optional = true } - # non-feature optional dependencies base64 = { version = "0.13.0", optional = true } cookie = { version = "0.15", optional = true } -gotham_middleware_diesel = { version = "0.2.0", optional = true } +gotham_middleware_diesel = { git = "https://github.com/gotham-rs/gotham", optional = true } indexmap = { version = "1.3.2", optional = true } indoc = { version = "1.0", optional = true } jsonwebtoken = { version = "7.1.0", optional = true } once_cell = { version = "1.5", optional = true } openapiv3 = { version = "=0.3.2", optional = true } +openapi_type = { version = "0.1.0-dev", optional = true } regex = { version = "1.4", optional = true } sha2 = { version = "0.9.3", optional = true } @@ -58,7 +55,7 @@ trybuild = "1.0.27" [features] default = ["cors", "errorlog", "without-openapi"] -full = ["auth", "chrono", "cors", "database", "errorlog", "openapi", "uuid"] +full = ["auth", "cors", "database", "errorlog", "openapi"] auth = ["gotham_restful_derive/auth", "base64", "cookie", "jsonwebtoken"] cors = [] @@ -67,7 +64,7 @@ errorlog = [] # These features are exclusive - https://gitlab.com/msrd0/gotham-restful/-/issues/4 without-openapi = [] -openapi = ["gotham_restful_derive/openapi", "base64", "indexmap", "indoc", "once_cell", "openapiv3", "regex", "sha2"] +openapi = ["gotham_restful_derive/openapi", "base64", "indexmap", "indoc", "once_cell", "openapiv3", "openapi_type", "regex", "sha2"] [package.metadata.docs.rs] no-default-features = true diff --git a/README.md b/README.md index 76da543..1cb82c1 100644 --- a/README.md +++ b/README.md @@ -100,7 +100,7 @@ use gotham_restful::gotham::hyper::Method; struct CustomResource; /// This type is used to parse path parameters. -#[derive(Deserialize, StateData, StaticResponseExtender)] +#[derive(Clone, Deserialize, StateData, StaticResponseExtender)] struct CustomPath { name: String } @@ -310,9 +310,9 @@ carefully both as a binary as well as a library author to avoid unwanted suprise In order to automatically create an openapi specification, gotham-restful needs knowledge over all routes and the types returned. `serde` does a great job at serialization but doesn't give -enough type information, so all types used in the router need to implement `OpenapiType`. This -can be derived for almoust any type and there should be no need to implement it manually. A simple -example looks like this: +enough type information, so all types used in the router need to implement +`OpenapiType`[openapi_type::OpenapiType]. This can be derived for almoust any type and there +should be no need to implement it manually. A simple example looks like this: ```rust #[derive(Resource)] @@ -350,15 +350,15 @@ clients in different languages without worying to exactly replicate your api in languages. However, please note that by default, the `without-openapi` feature of this crate is enabled. -Disabling it in favour of the `openapi` feature will add an additional type bound, [`OpenapiType`], -on some of the types in [`Endpoint`] and related traits. This means that some code might only -compile on either feature, but not on both. If you are writing a library that uses gotham-restful, -it is strongly recommended to pass both features through and conditionally enable the openapi -code, like this: +Disabling it in favour of the `openapi` feature will add an additional type bound, +[`OpenapiType`][openapi_type::OpenapiType], on some of the types in [`Endpoint`] and related +traits. This means that some code might only compile on either feature, but not on both. If you +are writing a library that uses gotham-restful, it is strongly recommended to pass both features +through and conditionally enable the openapi code, like this: ```rust #[derive(Deserialize, Serialize)] -#[cfg_attr(feature = "openapi", derive(OpenapiType))] +#[cfg_attr(feature = "openapi", derive(openapi_type::OpenapiType))] struct Foo; ``` diff --git a/derive/Cargo.toml b/derive/Cargo.toml index e06f2b0..58c877e 100644 --- a/derive/Cargo.toml +++ b/derive/Cargo.toml @@ -2,7 +2,7 @@ [package] name = "gotham_restful_derive" -version = "0.2.0" +version = "0.3.0-dev" authors = ["Dominic Meiser "] edition = "2018" description = "Derive macros for gotham_restful" diff --git a/derive/src/endpoint.rs b/derive/src/endpoint.rs index f6f143b..457f8ee 100644 --- a/derive/src/endpoint.rs +++ b/derive/src/endpoint.rs @@ -128,14 +128,14 @@ impl EndpointType { fn placeholders_ty(&self, arg_ty: Option<&Type>) -> TokenStream { match self { Self::ReadAll | Self::Search | Self::Create | Self::UpdateAll | Self::DeleteAll => { - quote!(::gotham_restful::gotham::extractor::NoopPathExtractor) + quote!(::gotham_restful::NoopExtractor) }, Self::Read | Self::Update | Self::Delete => quote!(::gotham_restful::private::IdPlaceholder::<#arg_ty>), Self::Custom { .. } => { if self.has_placeholders().value { arg_ty.to_token_stream() } else { - quote!(::gotham_restful::gotham::extractor::NoopPathExtractor) + quote!(::gotham_restful::NoopExtractor) } }, } @@ -163,14 +163,14 @@ impl EndpointType { fn params_ty(&self, arg_ty: Option<&Type>) -> TokenStream { match self { Self::ReadAll | Self::Read | Self::Create | Self::UpdateAll | Self::Update | Self::DeleteAll | Self::Delete => { - quote!(::gotham_restful::gotham::extractor::NoopQueryStringExtractor) + quote!(::gotham_restful::NoopExtractor) }, Self::Search => quote!(#arg_ty), Self::Custom { .. } => { if self.needs_params().value { arg_ty.to_token_stream() } else { - quote!(::gotham_restful::gotham::extractor::NoopQueryStringExtractor) + quote!(::gotham_restful::NoopExtractor) } }, } @@ -201,7 +201,7 @@ impl EndpointType { if self.needs_body().value { arg_ty.to_token_stream() } else { - quote!(::gotham_restful::gotham::extractor::NoopPathExtractor) + quote!(()) } }, } diff --git a/derive/src/lib.rs b/derive/src/lib.rs index 39e2855..59ee8b6 100644 --- a/derive/src/lib.rs +++ b/derive/src/lib.rs @@ -24,11 +24,6 @@ use resource::expand_resource; mod resource_error; use resource_error::expand_resource_error; -#[cfg(feature = "openapi")] -mod openapi_type; -#[cfg(feature = "openapi")] -use openapi_type::expand_openapi_type; - mod private_openapi_trait; use private_openapi_trait::expand_private_openapi_trait; @@ -66,12 +61,6 @@ pub fn derive_from_body(input: TokenStream) -> TokenStream { expand_derive(input, expand_from_body) } -#[cfg(feature = "openapi")] -#[proc_macro_derive(OpenapiType, attributes(openapi))] -pub fn derive_openapi_type(input: TokenStream) -> TokenStream { - expand_derive(input, expand_openapi_type) -} - #[proc_macro_derive(RequestBody, attributes(supported_types))] pub fn derive_request_body(input: TokenStream) -> TokenStream { expand_derive(input, expand_request_body) diff --git a/derive/src/openapi_type.rs b/derive/src/openapi_type.rs deleted file mode 100644 index 4b4530d..0000000 --- a/derive/src/openapi_type.rs +++ /dev/null @@ -1,289 +0,0 @@ -use crate::util::{remove_parens, CollectToResult}; -use proc_macro2::{Ident, TokenStream}; -use quote::quote; -use syn::{ - parse_macro_input, spanned::Spanned, Attribute, AttributeArgs, Data, DataEnum, DataStruct, DeriveInput, Error, Field, - Fields, GenericParam, Generics, Lit, LitStr, Meta, NestedMeta, Path, PathSegment, PredicateType, Result, TraitBound, - TraitBoundModifier, Type, TypeParamBound, TypePath, Variant, WhereClause, WherePredicate -}; - -pub fn expand_openapi_type(input: DeriveInput) -> Result { - match (input.ident, input.generics, input.attrs, input.data) { - (ident, generics, attrs, Data::Enum(inum)) => expand_enum(ident, generics, attrs, inum), - (ident, generics, attrs, Data::Struct(strukt)) => expand_struct(ident, generics, attrs, strukt), - (_, _, _, Data::Union(uni)) => Err(Error::new( - uni.union_token.span(), - "#[derive(OpenapiType)] only works for structs and enums" - )) - } -} - -fn update_generics(generics: &Generics, where_clause: &mut Option) { - if generics.params.is_empty() { - return; - } - - if where_clause.is_none() { - *where_clause = Some(WhereClause { - where_token: Default::default(), - predicates: Default::default() - }); - } - let where_clause = where_clause.as_mut().unwrap(); - - for param in &generics.params { - if let GenericParam::Type(ty_param) = param { - where_clause.predicates.push(WherePredicate::Type(PredicateType { - lifetimes: None, - bounded_ty: Type::Path(TypePath { - qself: None, - path: Path { - leading_colon: None, - segments: vec![PathSegment { - ident: ty_param.ident.clone(), - arguments: Default::default() - }] - .into_iter() - .collect() - } - }), - colon_token: Default::default(), - bounds: vec![TypeParamBound::Trait(TraitBound { - paren_token: None, - modifier: TraitBoundModifier::None, - lifetimes: None, - path: syn::parse_str("::gotham_restful::OpenapiType").unwrap() - })] - .into_iter() - .collect() - })); - } - } -} - -#[derive(Debug, Default)] -struct Attrs { - nullable: bool, - rename: Option -} - -fn to_string(lit: &Lit) -> Result { - match lit { - Lit::Str(str) => Ok(str.value()), - _ => Err(Error::new(lit.span(), "Expected string literal")) - } -} - -fn to_bool(lit: &Lit) -> Result { - match lit { - Lit::Bool(bool) => Ok(bool.value), - _ => Err(Error::new(lit.span(), "Expected bool")) - } -} - -fn parse_attributes(input: &[Attribute]) -> Result { - let mut parsed = Attrs::default(); - for attr in input { - if attr.path.segments.iter().last().map(|segment| segment.ident.to_string()) == Some("openapi".to_owned()) { - let tokens = remove_parens(attr.tokens.clone()); - // TODO this is not public api but syn currently doesn't offer another convenient way to parse AttributeArgs - let nested = parse_macro_input::parse::(tokens.into())?; - for meta in nested { - match &meta { - NestedMeta::Meta(Meta::NameValue(kv)) => match kv.path.segments.last().map(|s| s.ident.to_string()) { - Some(key) => match key.as_ref() { - "nullable" => parsed.nullable = to_bool(&kv.lit)?, - "rename" => parsed.rename = Some(to_string(&kv.lit)?), - _ => return Err(Error::new(kv.path.span(), "Unknown key")) - }, - _ => return Err(Error::new(meta.span(), "Unexpected token")) - }, - _ => return Err(Error::new(meta.span(), "Unexpected token")) - } - } - } - } - Ok(parsed) -} - -fn expand_variant(variant: &Variant) -> Result { - if !matches!(variant.fields, Fields::Unit) { - return Err(Error::new( - variant.span(), - "#[derive(OpenapiType)] does not support enum variants with fields" - )); - } - - let ident = &variant.ident; - - let attrs = parse_attributes(&variant.attrs)?; - let name = match attrs.rename { - Some(rename) => rename, - None => ident.to_string() - }; - - Ok(quote! { - enumeration.push(#name.to_string()); - }) -} - -fn expand_enum(ident: Ident, generics: Generics, attrs: Vec, input: DataEnum) -> Result { - let krate = super::krate(); - let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); - let mut where_clause = where_clause.cloned(); - update_generics(&generics, &mut where_clause); - - let attrs = parse_attributes(&attrs)?; - let nullable = attrs.nullable; - let name = match attrs.rename { - Some(rename) => rename, - None => ident.to_string() - }; - - let variants = input.variants.iter().map(expand_variant).collect_to_result()?; - - Ok(quote! { - impl #impl_generics #krate::OpenapiType for #ident #ty_generics - #where_clause - { - fn schema() -> #krate::OpenapiSchema - { - use #krate::{private::openapi::*, OpenapiSchema}; - - let mut enumeration : Vec = Vec::new(); - - #(#variants)* - - let schema = SchemaKind::Type(Type::String(StringType { - format: VariantOrUnknownOrEmpty::Empty, - enumeration, - ..Default::default() - })); - - OpenapiSchema { - name: Some(#name.to_string()), - nullable: #nullable, - schema, - dependencies: Default::default() - } - } - } - }) -} - -fn expand_field(field: &Field) -> Result { - let ident = match &field.ident { - Some(ident) => ident, - None => { - return Err(Error::new( - field.span(), - "#[derive(OpenapiType)] does not support fields without an ident" - )) - }, - }; - let ident_str = LitStr::new(&ident.to_string(), ident.span()); - let ty = &field.ty; - - let attrs = parse_attributes(&field.attrs)?; - let nullable = attrs.nullable; - let name = match attrs.rename { - Some(rename) => rename, - None => ident.to_string() - }; - - Ok(quote! {{ - let mut schema = <#ty>::schema(); - - if schema.nullable - { - schema.nullable = false; - } - else if !#nullable - { - required.push(#ident_str.to_string()); - } - - let keys : Vec = 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(schema_name) => { - properties.insert( - #name.to_string(), - ReferenceOr::Reference { reference: format!("#/components/schemas/{}", schema_name) } - ); - dependencies.insert(schema_name, schema); - }, - None => { - properties.insert( - #name.to_string(), - ReferenceOr::Item(Box::new(schema.into_schema())) - ); - } - } - }}) -} - -fn expand_struct(ident: Ident, generics: Generics, attrs: Vec, input: DataStruct) -> Result { - let krate = super::krate(); - let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); - let mut where_clause = where_clause.cloned(); - update_generics(&generics, &mut where_clause); - - let attrs = parse_attributes(&attrs)?; - let nullable = attrs.nullable; - let name = match attrs.rename { - Some(rename) => rename, - None => ident.to_string() - }; - - let fields: Vec = match input.fields { - Fields::Named(named_fields) => named_fields.named.iter().map(expand_field).collect_to_result()?, - Fields::Unnamed(fields) => { - return Err(Error::new( - fields.span(), - "#[derive(OpenapiType)] does not support unnamed fields" - )) - }, - Fields::Unit => Vec::new() - }; - - Ok(quote! { - impl #impl_generics #krate::OpenapiType for #ident #ty_generics - #where_clause - { - fn schema() -> #krate::OpenapiSchema - { - use #krate::{private::{openapi::*, IndexMap}, OpenapiSchema}; - - let mut properties : IndexMap>> = IndexMap::new(); - let mut required : Vec = Vec::new(); - let mut dependencies : IndexMap = IndexMap::new(); - - #(#fields)* - - let schema = SchemaKind::Type(Type::Object(ObjectType { - properties, - required, - additional_properties: None, - min_properties: None, - max_properties: None - })); - - OpenapiSchema { - name: Some(#name.to_string()), - nullable: #nullable, - schema, - dependencies - } - } - } - }) -} diff --git a/derive/src/request_body.rs b/derive/src/request_body.rs index 9657b21..c543dfa 100644 --- a/derive/src/request_body.rs +++ b/derive/src/request_body.rs @@ -26,18 +26,25 @@ fn impl_openapi_type(_ident: &Ident, _generics: &Generics) -> TokenStream { #[cfg(feature = "openapi")] fn impl_openapi_type(ident: &Ident, generics: &Generics) -> TokenStream { let krate = super::krate(); + let openapi = quote!(#krate::private::openapi); quote! { - impl #generics #krate::OpenapiType for #ident #generics + impl #generics #krate::private::OpenapiType for #ident #generics { - fn schema() -> #krate::OpenapiSchema + fn schema() -> #krate::private::OpenapiSchema { - use #krate::{private::openapi::*, OpenapiSchema}; - - OpenapiSchema::new(SchemaKind::Type(Type::String(StringType { - format: VariantOrUnknownOrEmpty::Item(StringFormat::Binary), - ..Default::default() - }))) + #krate::private::OpenapiSchema::new( + #openapi::SchemaKind::Type( + #openapi::Type::String( + #openapi::StringType { + format: #openapi::VariantOrUnknownOrEmpty::Item( + #openapi::StringFormat::Binary + ), + .. ::std::default::Default::default() + } + ) + ) + ) } } } diff --git a/openapi_type/src/impls.rs b/openapi_type/src/impls.rs index d9396fd..d46a922 100644 --- a/openapi_type/src/impls.rs +++ b/openapi_type/src/impls.rs @@ -97,7 +97,7 @@ fn str_schema(format: VariantOrUnknownOrEmpty) -> OpenapiSchema { }))) } -impl_openapi_type!(String => str_schema(VariantOrUnknownOrEmpty::Empty)); +impl_openapi_type!(String, str => str_schema(VariantOrUnknownOrEmpty::Empty)); #[cfg(feature = "chrono")] impl_openapi_type!(Date, NaiveDate => { diff --git a/openapi_type/src/lib.rs b/openapi_type/src/lib.rs index 590800b..2933027 100644 --- a/openapi_type/src/lib.rs +++ b/openapi_type/src/lib.rs @@ -78,3 +78,9 @@ struct MyResponse { pub trait OpenapiType { fn schema() -> OpenapiSchema; } + +impl<'a, T: ?Sized + OpenapiType> OpenapiType for &'a T { + fn schema() -> OpenapiSchema { + T::schema() + } +} diff --git a/openapi_type_derive/src/lib.rs b/openapi_type_derive/src/lib.rs index 6d82331..10e6c0a 100644 --- a/openapi_type_derive/src/lib.rs +++ b/openapi_type_derive/src/lib.rs @@ -55,7 +55,7 @@ fn expand_openapi_type(mut input: DeriveInput) -> syn::Result { modifier: TraitBoundModifier::None, lifetimes: None, path: path!(::openapi_type::OpenapiType) - })) + })); }); generics.split_for_impl() }; diff --git a/src/endpoint.rs b/src/endpoint.rs index d8da412..2095948 100644 --- a/src/endpoint.rs +++ b/src/endpoint.rs @@ -2,11 +2,41 @@ use crate::{IntoResponse, RequestBody}; use futures_util::future::BoxFuture; use gotham::{ extractor::{PathExtractor, QueryStringExtractor}, - hyper::{Body, Method}, - state::State + hyper::{Body, Method, Response}, + router::response::extender::StaticResponseExtender, + state::{State, StateData} }; +#[cfg(feature = "openapi")] +use openapi_type::{OpenapiSchema, OpenapiType}; +use serde::{Deserialize, Deserializer}; use std::borrow::Cow; +/// A no-op extractor that can be used as a default type for [Endpoint::Placeholders] and +/// [Endpoint::Params]. +#[derive(Debug, Clone, Copy)] +pub struct NoopExtractor; + +impl<'de> Deserialize<'de> for NoopExtractor { + fn deserialize>(_: D) -> Result { + Ok(Self) + } +} + +#[cfg(feature = "openapi")] +impl OpenapiType for NoopExtractor { + fn schema() -> OpenapiSchema { + warn!("You're asking for the OpenAPI Schema for gotham_restful::NoopExtractor. This is probably not what you want."); + <() as OpenapiType>::schema() + } +} + +impl StateData for NoopExtractor {} + +impl StaticResponseExtender for NoopExtractor { + type ResBody = Body; + fn extend(_: &mut State, _: &mut Response) {} +} + // TODO: Specify default types once https://github.com/rust-lang/rust/issues/29661 lands. #[_private_openapi_trait(EndpointWithSchema)] pub trait Endpoint { @@ -23,19 +53,19 @@ pub trait Endpoint { fn has_placeholders() -> bool { false } - /// The type that parses the URI placeholders. Use [gotham::extractor::NoopPathExtractor] - /// if `has_placeholders()` returns `false`. - #[openapi_bound("Placeholders: crate::OpenapiType")] - type Placeholders: PathExtractor + Sync; + /// The type that parses the URI placeholders. Use [NoopExtractor] if `has_placeholders()` + /// returns `false`. + #[openapi_bound("Placeholders: OpenapiType")] + type Placeholders: PathExtractor + Clone + Sync; /// Returns `true` _iff_ the request parameters should be parsed. `false` by default. fn needs_params() -> bool { false } - /// The type that parses the request parameters. Use [gotham::extractor::NoopQueryStringExtractor] - /// if `needs_params()` returns `false`. - #[openapi_bound("Params: crate::OpenapiType")] - type Params: QueryStringExtractor + Sync; + /// The type that parses the request parameters. Use [NoopExtractor] if `needs_params()` + /// returns `false`. + #[openapi_bound("Params: OpenapiType")] + type Params: QueryStringExtractor + Clone + Sync; /// Returns `true` _iff_ the request body should be parsed. `false` by default. fn needs_body() -> bool { diff --git a/src/lib.rs b/src/lib.rs index 36674c3..aea56a4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -60,7 +60,7 @@ struct FooResource; /// The return type of the foo read endpoint. #[derive(Serialize)] -# #[cfg_attr(feature = "openapi", derive(OpenapiType))] +# #[cfg_attr(feature = "openapi", derive(openapi_type::OpenapiType))] struct Foo { id: u64 } @@ -95,8 +95,8 @@ use gotham_restful::gotham::hyper::Method; struct CustomResource; /// This type is used to parse path parameters. -#[derive(Deserialize, StateData, StaticResponseExtender)] -# #[cfg_attr(feature = "openapi", derive(OpenapiType))] +#[derive(Clone, Deserialize, StateData, StaticResponseExtender)] +# #[cfg_attr(feature = "openapi", derive(openapi_type::OpenapiType))] struct CustomPath { name: String } @@ -225,7 +225,7 @@ A simple example that uses only a single secret looks like this: struct SecretResource; #[derive(Serialize)] -# #[cfg_attr(feature = "openapi", derive(OpenapiType))] +# #[cfg_attr(feature = "openapi", derive(openapi_type::OpenapiType))] struct Secret { id: u64, intended_for: String @@ -331,7 +331,7 @@ A simple non-async example looks like this: struct FooResource; #[derive(Queryable, Serialize)] -# #[cfg_attr(feature = "openapi", derive(OpenapiType))] +# #[cfg_attr(feature = "openapi", derive(openapi_type::OpenapiType))] struct Foo { id: i64, value: String @@ -363,9 +363,9 @@ carefully both as a binary as well as a library author to avoid unwanted suprise In order to automatically create an openapi specification, gotham-restful needs knowledge over all routes and the types returned. `serde` does a great job at serialization but doesn't give -enough type information, so all types used in the router need to implement `OpenapiType`. This -can be derived for almoust any type and there should be no need to implement it manually. A simple -example looks like this: +enough type information, so all types used in the router need to implement +`OpenapiType`[openapi_type::OpenapiType]. This can be derived for almoust any type and there +should be no need to implement it manually. A simple example looks like this: ```rust,no_run # #[macro_use] extern crate gotham_restful_derive; @@ -373,6 +373,7 @@ example looks like this: # mod openapi_feature_enabled { # use gotham::{router::builder::*, state::State}; # use gotham_restful::*; +# use openapi_type::OpenapiType; # use serde::{Deserialize, Serialize}; #[derive(Resource)] #[resource(read_all)] @@ -410,17 +411,17 @@ clients in different languages without worying to exactly replicate your api in languages. However, please note that by default, the `without-openapi` feature of this crate is enabled. -Disabling it in favour of the `openapi` feature will add an additional type bound, [`OpenapiType`], -on some of the types in [`Endpoint`] and related traits. This means that some code might only -compile on either feature, but not on both. If you are writing a library that uses gotham-restful, -it is strongly recommended to pass both features through and conditionally enable the openapi -code, like this: +Disabling it in favour of the `openapi` feature will add an additional type bound, +[`OpenapiType`][openapi_type::OpenapiType], on some of the types in [`Endpoint`] and related +traits. This means that some code might only compile on either feature, but not on both. If you +are writing a library that uses gotham-restful, it is strongly recommended to pass both features +through and conditionally enable the openapi code, like this: ```rust # #[macro_use] extern crate gotham_restful; # use serde::{Deserialize, Serialize}; #[derive(Deserialize, Serialize)] -#[cfg_attr(feature = "openapi", derive(OpenapiType))] +#[cfg_attr(feature = "openapi", derive(openapi_type::OpenapiType))] struct Foo; ``` @@ -478,6 +479,8 @@ pub mod private { #[cfg(feature = "openapi")] pub use indexmap::IndexMap; #[cfg(feature = "openapi")] + pub use openapi_type::{OpenapiSchema, OpenapiType}; + #[cfg(feature = "openapi")] pub use openapiv3 as openapi; } @@ -494,16 +497,12 @@ pub use cors::{handle_cors, CorsConfig, CorsRoute}; #[cfg(feature = "openapi")] mod openapi; #[cfg(feature = "openapi")] -pub use openapi::{ - builder::OpenapiInfo, - router::GetOpenapi, - types::{OpenapiSchema, OpenapiType} -}; +pub use openapi::{builder::OpenapiInfo, router::GetOpenapi}; mod endpoint; -pub use endpoint::Endpoint; #[cfg(feature = "openapi")] pub use endpoint::EndpointWithSchema; +pub use endpoint::{Endpoint, NoopExtractor}; mod response; pub use response::{ diff --git a/src/openapi/builder.rs b/src/openapi/builder.rs index 11f79f8..4fa6a0d 100644 --- a/src/openapi/builder.rs +++ b/src/openapi/builder.rs @@ -1,5 +1,5 @@ -use crate::OpenapiSchema; use indexmap::IndexMap; +use openapi_type::OpenapiSchema; use openapiv3::{ Components, OpenAPI, PathItem, ReferenceOr, ReferenceOr::{Item, Reference}, @@ -104,7 +104,7 @@ impl OpenapiBuilder { #[allow(dead_code)] mod test { use super::*; - use crate::OpenapiType; + use openapi_type::OpenapiType; #[derive(OpenapiType)] struct Message { diff --git a/src/openapi/mod.rs b/src/openapi/mod.rs index 500d190..5eefc1f 100644 --- a/src/openapi/mod.rs +++ b/src/openapi/mod.rs @@ -4,4 +4,3 @@ pub mod builder; pub mod handler; pub mod operation; pub mod router; -pub mod types; diff --git a/src/openapi/operation.rs b/src/openapi/operation.rs index 62d06d5..1823b3c 100644 --- a/src/openapi/operation.rs +++ b/src/openapi/operation.rs @@ -1,7 +1,8 @@ use super::SECURITY_NAME; -use crate::{response::OrAllTypes, EndpointWithSchema, IntoResponse, OpenapiSchema, RequestBody, ResponseSchema}; +use crate::{response::OrAllTypes, EndpointWithSchema, IntoResponse, RequestBody, ResponseSchema}; use indexmap::IndexMap; use mime::Mime; +use openapi_type::OpenapiSchema; use openapiv3::{ MediaType, Operation, Parameter, ParameterData, ParameterSchemaOrContent, ReferenceOr, ReferenceOr::Item, RequestBody as OARequestBody, Response, Responses, Schema, SchemaKind, StatusCode, Type diff --git a/src/openapi/router.rs b/src/openapi/router.rs index 3ced31b..e6b3187 100644 --- a/src/openapi/router.rs +++ b/src/openapi/router.rs @@ -3,9 +3,10 @@ use super::{ handler::{OpenapiHandler, SwaggerUiHandler}, operation::OperationDescription }; -use crate::{routing::*, EndpointWithSchema, OpenapiType, ResourceWithSchema, ResponseSchema}; +use crate::{routing::*, EndpointWithSchema, ResourceWithSchema, ResponseSchema}; use gotham::{hyper::Method, pipeline::chain::PipelineHandleChain, router::builder::*}; use once_cell::sync::Lazy; +use openapi_type::OpenapiType; use regex::{Captures, Regex}; use std::panic::RefUnwindSafe; diff --git a/src/openapi/types.rs b/src/openapi/types.rs deleted file mode 100644 index 66bf059..0000000 --- a/src/openapi/types.rs +++ /dev/null @@ -1,476 +0,0 @@ -#[cfg(feature = "chrono")] -use chrono::{Date, DateTime, FixedOffset, Local, NaiveDate, NaiveDateTime, Utc}; -use gotham::extractor::{NoopPathExtractor, NoopQueryStringExtractor}; -use indexmap::IndexMap; -use openapiv3::{ - AdditionalProperties, ArrayType, IntegerType, NumberFormat, NumberType, ObjectType, - ReferenceOr::{Item, Reference}, - Schema, SchemaData, SchemaKind, StringType, Type, VariantOrUnknownOrEmpty -}; -use std::{ - collections::{BTreeSet, HashMap, HashSet}, - hash::BuildHasher, - num::{NonZeroU128, NonZeroU16, NonZeroU32, NonZeroU64, NonZeroU8, NonZeroUsize} -}; -#[cfg(feature = "uuid")] -use uuid::Uuid; - -/** -This struct needs to be available for every type that can be part of an OpenAPI Spec. It is -already implemented for primitive types, String, Vec, Option and the like. To have it available -for your type, simply derive from [OpenapiType]. -*/ -#[derive(Debug, Clone, PartialEq)] -pub struct OpenapiSchema { - /// The name of this schema. If it is None, the schema will be inlined. - pub name: Option, - /// Whether this particular schema is nullable. Note that there is no guarantee that this will - /// make it into the final specification, it might just be interpreted as a hint to make it - /// an optional parameter. - pub nullable: bool, - /// The actual OpenAPI schema. - pub schema: SchemaKind, - /// Other schemas that this schema depends on. They will be included in the final OpenAPI Spec - /// along with this schema. - pub dependencies: IndexMap -} - -impl OpenapiSchema { - /// Create a new schema that has no name. - pub fn new(schema: SchemaKind) -> Self { - Self { - name: None, - nullable: false, - schema, - dependencies: IndexMap::new() - } - } - - /// Convert this schema to an [openapiv3::Schema] that can be serialized to the OpenAPI Spec. - pub fn into_schema(self) -> Schema { - Schema { - schema_data: SchemaData { - nullable: self.nullable, - title: self.name, - ..Default::default() - }, - schema_kind: self.schema - } - } -} - -/** -This trait needs to be implemented by every type that is being used in the OpenAPI Spec. It gives -access to the [OpenapiSchema] of this type. It is provided for primitive types, String and the -like. For use on your own types, there is a derive macro: - -``` -# #[macro_use] extern crate gotham_restful_derive; -# -#[derive(OpenapiType)] -struct MyResponse { - message: String -} -``` -*/ -pub trait OpenapiType { - fn schema() -> OpenapiSchema; -} - -impl OpenapiType for () { - fn schema() -> OpenapiSchema { - OpenapiSchema::new(SchemaKind::Type(Type::Object(ObjectType { - additional_properties: Some(AdditionalProperties::Any(false)), - ..Default::default() - }))) - } -} - -impl OpenapiType for NoopPathExtractor { - fn schema() -> OpenapiSchema { - warn!("You're asking for the OpenAPI Schema for gotham::extractor::NoopPathExtractor. This is probably not what you want."); - <()>::schema() - } -} - -impl OpenapiType for NoopQueryStringExtractor { - fn schema() -> OpenapiSchema { - warn!("You're asking for the OpenAPI Schema for gotham::extractor::NoopQueryStringExtractor. This is probably not what you want."); - <()>::schema() - } -} - -impl OpenapiType for bool { - fn schema() -> OpenapiSchema { - OpenapiSchema::new(SchemaKind::Type(Type::Boolean {})) - } -} - -macro_rules! int_types { - ($($int_ty:ty),*) => {$( - impl OpenapiType for $int_ty - { - fn schema() -> OpenapiSchema - { - OpenapiSchema::new(SchemaKind::Type(Type::Integer(IntegerType::default()))) - } - } - )*}; - - (unsigned $($int_ty:ty),*) => {$( - impl OpenapiType for $int_ty - { - fn schema() -> OpenapiSchema - { - OpenapiSchema::new(SchemaKind::Type(Type::Integer(IntegerType { - minimum: Some(0), - ..Default::default() - }))) - } - } - )*}; - - (gtzero $($int_ty:ty),*) => {$( - impl OpenapiType for $int_ty - { - fn schema() -> OpenapiSchema - { - OpenapiSchema::new(SchemaKind::Type(Type::Integer(IntegerType { - minimum: Some(1), - ..Default::default() - }))) - } - } - )*}; - - (bits = $bits:expr, $($int_ty:ty),*) => {$( - impl OpenapiType for $int_ty - { - fn schema() -> OpenapiSchema - { - OpenapiSchema::new(SchemaKind::Type(Type::Integer(IntegerType { - format: VariantOrUnknownOrEmpty::Unknown(format!("int{}", $bits)), - ..Default::default() - }))) - } - } - )*}; - - (unsigned bits = $bits:expr, $($int_ty:ty),*) => {$( - impl OpenapiType for $int_ty - { - fn schema() -> OpenapiSchema - { - OpenapiSchema::new(SchemaKind::Type(Type::Integer(IntegerType { - format: VariantOrUnknownOrEmpty::Unknown(format!("int{}", $bits)), - minimum: Some(0), - ..Default::default() - }))) - } - } - )*}; - - (gtzero bits = $bits:expr, $($int_ty:ty),*) => {$( - impl OpenapiType for $int_ty - { - fn schema() -> OpenapiSchema - { - OpenapiSchema::new(SchemaKind::Type(Type::Integer(IntegerType { - format: VariantOrUnknownOrEmpty::Unknown(format!("int{}", $bits)), - minimum: Some(1), - ..Default::default() - }))) - } - } - )*}; -} - -int_types!(isize); -int_types!(unsigned usize); -int_types!(gtzero NonZeroUsize); -int_types!(bits = 8, i8); -int_types!(unsigned bits = 8, u8); -int_types!(gtzero bits = 8, NonZeroU8); -int_types!(bits = 16, i16); -int_types!(unsigned bits = 16, u16); -int_types!(gtzero bits = 16, NonZeroU16); -int_types!(bits = 32, i32); -int_types!(unsigned bits = 32, u32); -int_types!(gtzero bits = 32, NonZeroU32); -int_types!(bits = 64, i64); -int_types!(unsigned bits = 64, u64); -int_types!(gtzero bits = 64, NonZeroU64); -int_types!(bits = 128, i128); -int_types!(unsigned bits = 128, u128); -int_types!(gtzero bits = 128, NonZeroU128); - -macro_rules! num_types { - ($($num_ty:ty = $num_fmt:ident),*) => {$( - impl OpenapiType for $num_ty - { - fn schema() -> OpenapiSchema - { - OpenapiSchema::new(SchemaKind::Type(Type::Number(NumberType { - format: VariantOrUnknownOrEmpty::Item(NumberFormat::$num_fmt), - ..Default::default() - }))) - } - } - )*} -} - -num_types!(f32 = Float, f64 = Double); - -macro_rules! str_types { - ($($str_ty:ty),*) => {$( - impl OpenapiType for $str_ty - { - fn schema() -> OpenapiSchema - { - OpenapiSchema::new(SchemaKind::Type(Type::String(StringType::default()))) - } - } - )*}; - - (format = $format:ident, $($str_ty:ty),*) => {$( - impl OpenapiType for $str_ty - { - fn schema() -> OpenapiSchema - { - use openapiv3::StringFormat; - - OpenapiSchema::new(SchemaKind::Type(Type::String(StringType { - format: VariantOrUnknownOrEmpty::Item(StringFormat::$format), - ..Default::default() - }))) - } - } - )*}; - - (format_str = $format:expr, $($str_ty:ty),*) => {$( - impl OpenapiType for $str_ty - { - fn schema() -> OpenapiSchema - { - OpenapiSchema::new(SchemaKind::Type(Type::String(StringType { - format: VariantOrUnknownOrEmpty::Unknown($format.to_string()), - ..Default::default() - }))) - } - } - )*}; -} - -str_types!(String, &str); - -#[cfg(feature = "chrono")] -str_types!(format = Date, Date, Date, Date, NaiveDate); -#[cfg(feature = "chrono")] -str_types!( - format = DateTime, - DateTime, - DateTime, - DateTime, - NaiveDateTime -); - -#[cfg(feature = "uuid")] -str_types!(format_str = "uuid", Uuid); - -impl OpenapiType for Option { - fn schema() -> OpenapiSchema { - let schema = T::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, - dependencies - } - } -} - -impl OpenapiType for Vec { - fn schema() -> OpenapiSchema { - let schema = T::schema(); - let mut dependencies = schema.dependencies.clone(); - - let items = match schema.name.clone() { - Some(name) => { - let reference = Reference { - reference: format!("#/components/schemas/{}", name) - }; - dependencies.insert(name, schema); - reference - }, - None => Item(Box::new(schema.into_schema())) - }; - - OpenapiSchema { - nullable: false, - name: None, - schema: SchemaKind::Type(Type::Array(ArrayType { - items, - min_items: None, - max_items: None, - unique_items: false - })), - dependencies - } - } -} - -impl OpenapiType for BTreeSet { - fn schema() -> OpenapiSchema { - as OpenapiType>::schema() - } -} - -impl OpenapiType for HashSet { - fn schema() -> OpenapiSchema { - as OpenapiType>::schema() - } -} - -impl OpenapiType for HashMap { - fn schema() -> OpenapiSchema { - let key_schema = K::schema(); - let mut dependencies = key_schema.dependencies.clone(); - - let keys = match key_schema.name.clone() { - Some(name) => { - let reference = Reference { - reference: format!("#/components/schemas/{}", name) - }; - dependencies.insert(name, key_schema); - reference - }, - None => Item(Box::new(key_schema.into_schema())) - }; - - let schema = T::schema(); - dependencies.extend(schema.dependencies.iter().map(|(k, v)| (k.clone(), v.clone()))); - - let items = Box::new(match schema.name.clone() { - Some(name) => { - let reference = Reference { - reference: format!("#/components/schemas/{}", name) - }; - dependencies.insert(name, schema); - reference - }, - None => Item(schema.into_schema()) - }); - - let mut properties = IndexMap::new(); - properties.insert("default".to_owned(), keys); - - OpenapiSchema { - nullable: false, - name: None, - schema: SchemaKind::Type(Type::Object(ObjectType { - properties, - required: vec!["default".to_owned()], - additional_properties: Some(AdditionalProperties::Schema(items)), - ..Default::default() - })), - dependencies - } - } -} - -impl OpenapiType for serde_json::Value { - fn schema() -> OpenapiSchema { - OpenapiSchema { - nullable: true, - name: None, - schema: SchemaKind::Any(Default::default()), - dependencies: Default::default() - } - } -} - -#[cfg(test)] -mod test { - use super::*; - use serde_json::Value; - - type Unit = (); - - macro_rules! assert_schema { - ($ty:ident $(<$($generic:ident),+>)* => $json:expr) => { - paste::item! { - #[test] - fn []() - { - let schema = <$ty $(<$($generic),+>)* as OpenapiType>::schema().into_schema(); - let schema_json = serde_json::to_string(&schema).expect(&format!("Unable to serialize schema for {}", stringify!($ty))); - assert_eq!(schema_json, $json); - } - } - }; - } - - assert_schema!(Unit => r#"{"type":"object","additionalProperties":false}"#); - assert_schema!(bool => r#"{"type":"boolean"}"#); - - assert_schema!(isize => r#"{"type":"integer"}"#); - assert_schema!(usize => r#"{"type":"integer","minimum":0}"#); - assert_schema!(i8 => r#"{"type":"integer","format":"int8"}"#); - assert_schema!(u8 => r#"{"type":"integer","format":"int8","minimum":0}"#); - assert_schema!(i16 => r#"{"type":"integer","format":"int16"}"#); - assert_schema!(u16 => r#"{"type":"integer","format":"int16","minimum":0}"#); - assert_schema!(i32 => r#"{"type":"integer","format":"int32"}"#); - assert_schema!(u32 => r#"{"type":"integer","format":"int32","minimum":0}"#); - assert_schema!(i64 => r#"{"type":"integer","format":"int64"}"#); - assert_schema!(u64 => r#"{"type":"integer","format":"int64","minimum":0}"#); - assert_schema!(i128 => r#"{"type":"integer","format":"int128"}"#); - assert_schema!(u128 => r#"{"type":"integer","format":"int128","minimum":0}"#); - - assert_schema!(NonZeroUsize => r#"{"type":"integer","minimum":1}"#); - assert_schema!(NonZeroU8 => r#"{"type":"integer","format":"int8","minimum":1}"#); - assert_schema!(NonZeroU16 => r#"{"type":"integer","format":"int16","minimum":1}"#); - assert_schema!(NonZeroU32 => r#"{"type":"integer","format":"int32","minimum":1}"#); - assert_schema!(NonZeroU64 => r#"{"type":"integer","format":"int64","minimum":1}"#); - assert_schema!(NonZeroU128 => r#"{"type":"integer","format":"int128","minimum":1}"#); - - assert_schema!(f32 => r#"{"type":"number","format":"float"}"#); - assert_schema!(f64 => r#"{"type":"number","format":"double"}"#); - - assert_schema!(String => r#"{"type":"string"}"#); - - #[cfg(feature = "uuid")] - assert_schema!(Uuid => r#"{"type":"string","format":"uuid"}"#); - - #[cfg(feature = "chrono")] - mod chrono { - use super::*; - - assert_schema!(Date => r#"{"type":"string","format":"date"}"#); - assert_schema!(Date => r#"{"type":"string","format":"date"}"#); - assert_schema!(Date => r#"{"type":"string","format":"date"}"#); - assert_schema!(NaiveDate => r#"{"type":"string","format":"date"}"#); - assert_schema!(DateTime => r#"{"type":"string","format":"date-time"}"#); - assert_schema!(DateTime => r#"{"type":"string","format":"date-time"}"#); - assert_schema!(DateTime => r#"{"type":"string","format":"date-time"}"#); - assert_schema!(NaiveDateTime => r#"{"type":"string","format":"date-time"}"#); - } - - assert_schema!(Option => r#"{"nullable":true,"type":"string"}"#); - assert_schema!(Vec => r#"{"type":"array","items":{"type":"string"}}"#); - assert_schema!(BTreeSet => r#"{"type":"array","items":{"type":"string"}}"#); - assert_schema!(HashSet => r#"{"type":"array","items":{"type":"string"}}"#); - assert_schema!(HashMap => r#"{"type":"object","properties":{"default":{"type":"integer","format":"int64"}},"required":["default"],"additionalProperties":{"type":"string"}}"#); - assert_schema!(Value => r#"{"nullable":true}"#); -} diff --git a/src/response/mod.rs b/src/response/mod.rs index b2796dc..bdf7c66 100644 --- a/src/response/mod.rs +++ b/src/response/mod.rs @@ -1,6 +1,3 @@ -#[cfg(feature = "openapi")] -use crate::OpenapiSchema; - use futures_util::future::{self, BoxFuture, FutureExt}; use gotham::{ handler::HandlerError, @@ -10,6 +7,8 @@ use gotham::{ } }; use mime::{Mime, APPLICATION_JSON, STAR_STAR}; +#[cfg(feature = "openapi")] +use openapi_type::OpenapiSchema; use serde::Serialize; use std::{ convert::Infallible, @@ -259,7 +258,7 @@ mod test { use thiserror::Error; #[derive(Debug, Default, Deserialize, Serialize)] - #[cfg_attr(feature = "openapi", derive(crate::OpenapiType))] + #[cfg_attr(feature = "openapi", derive(openapi_type::OpenapiType))] struct Msg { msg: String } diff --git a/src/response/no_content.rs b/src/response/no_content.rs index 73159c1..3a10b3b 100644 --- a/src/response/no_content.rs +++ b/src/response/no_content.rs @@ -1,12 +1,14 @@ use super::{handle_error, IntoResponse}; -use crate::{IntoResponseError, Response}; #[cfg(feature = "openapi")] -use crate::{OpenapiSchema, OpenapiType, ResponseSchema}; +use crate::ResponseSchema; +use crate::{IntoResponseError, Response}; use futures_util::{future, future::FutureExt}; use gotham::hyper::header::{HeaderMap, HeaderValue, IntoHeaderName}; #[cfg(feature = "openapi")] use gotham::hyper::StatusCode; use mime::Mime; +#[cfg(feature = "openapi")] +use openapi_type::{OpenapiSchema, OpenapiType}; use std::{fmt::Display, future::Future, pin::Pin}; /** diff --git a/src/response/raw.rs b/src/response/raw.rs index 6c003dc..3722146 100644 --- a/src/response/raw.rs +++ b/src/response/raw.rs @@ -1,7 +1,9 @@ use super::{handle_error, IntoResponse, IntoResponseError}; use crate::{FromBody, RequestBody, ResourceType, Response}; #[cfg(feature = "openapi")] -use crate::{IntoResponseWithSchema, OpenapiSchema, OpenapiType, ResponseSchema}; +use crate::{IntoResponseWithSchema, ResponseSchema}; +#[cfg(feature = "openapi")] +use openapi_type::{OpenapiSchema, OpenapiType}; use futures_core::future::Future; use futures_util::{future, future::FutureExt}; diff --git a/src/response/redirect.rs b/src/response/redirect.rs index 8b6e854..f1edd82 100644 --- a/src/response/redirect.rs +++ b/src/response/redirect.rs @@ -1,12 +1,14 @@ use super::{handle_error, IntoResponse}; use crate::{IntoResponseError, Response}; #[cfg(feature = "openapi")] -use crate::{NoContent, OpenapiSchema, ResponseSchema}; +use crate::{NoContent, ResponseSchema}; use futures_util::future::{BoxFuture, FutureExt, TryFutureExt}; use gotham::hyper::{ header::{InvalidHeaderValue, LOCATION}, Body, StatusCode }; +#[cfg(feature = "openapi")] +use openapi_type::OpenapiSchema; use std::{ error::Error as StdError, fmt::{Debug, Display} diff --git a/src/response/result.rs b/src/response/result.rs index a28803f..f0ddc91 100644 --- a/src/response/result.rs +++ b/src/response/result.rs @@ -1,7 +1,9 @@ use super::{handle_error, IntoResponse, ResourceError}; #[cfg(feature = "openapi")] -use crate::{OpenapiSchema, ResponseSchema}; +use crate::ResponseSchema; use crate::{Response, ResponseBody, Success}; +#[cfg(feature = "openapi")] +use openapi_type::OpenapiSchema; use futures_core::future::Future; use gotham::hyper::StatusCode; @@ -64,7 +66,7 @@ mod test { use thiserror::Error; #[derive(Debug, Default, Deserialize, Serialize)] - #[cfg_attr(feature = "openapi", derive(crate::OpenapiType))] + #[cfg_attr(feature = "openapi", derive(openapi_type::OpenapiType))] struct Msg { msg: String } diff --git a/src/response/success.rs b/src/response/success.rs index 24d6b3c..31f9374 100644 --- a/src/response/success.rs +++ b/src/response/success.rs @@ -1,6 +1,6 @@ use super::IntoResponse; #[cfg(feature = "openapi")] -use crate::{OpenapiSchema, ResponseSchema}; +use crate::ResponseSchema; use crate::{Response, ResponseBody}; use futures_util::future::{self, FutureExt}; use gotham::hyper::{ @@ -8,6 +8,8 @@ use gotham::hyper::{ StatusCode }; use mime::{Mime, APPLICATION_JSON}; +#[cfg(feature = "openapi")] +use openapi_type::OpenapiSchema; use std::{fmt::Debug, future::Future, pin::Pin}; /** @@ -27,7 +29,7 @@ Usage example: # struct MyResource; # #[derive(Deserialize, Serialize)] -# #[cfg_attr(feature = "openapi", derive(OpenapiType))] +# #[cfg_attr(feature = "openapi", derive(openapi_type::OpenapiType))] struct MyResponse { message: &'static str } @@ -96,7 +98,7 @@ mod test { use gotham::hyper::header::ACCESS_CONTROL_ALLOW_ORIGIN; #[derive(Debug, Default, Serialize)] - #[cfg_attr(feature = "openapi", derive(crate::OpenapiType))] + #[cfg_attr(feature = "openapi", derive(openapi_type::OpenapiType))] struct Msg { msg: String } diff --git a/src/routing.rs b/src/routing.rs index f41dc93..c7cd5a6 100644 --- a/src/routing.rs +++ b/src/routing.rs @@ -4,7 +4,6 @@ use crate::openapi::{ router::OpenapiRouter }; use crate::{response::ResourceError, Endpoint, FromBody, IntoResponse, Resource, Response}; - #[cfg(feature = "cors")] use gotham::router::route::matcher::AccessControlRequestMethodMatcher; use gotham::{ @@ -20,10 +19,12 @@ use gotham::{ state::{FromState, State} }; use mime::{Mime, APPLICATION_JSON}; -use std::panic::RefUnwindSafe; +#[cfg(feature = "openapi")] +use openapi_type::OpenapiType; +use std::{any::TypeId, panic::RefUnwindSafe}; /// Allow us to extract an id from a path. -#[derive(Debug, Deserialize, StateData, StaticResponseExtender)] +#[derive(Clone, Copy, Debug, Deserialize, StateData, StaticResponseExtender)] #[cfg_attr(feature = "openapi", derive(OpenapiType))] pub struct PathExtractor { pub id: ID @@ -91,6 +92,11 @@ where { trace!("entering endpoint_handler"); let placeholders = E::Placeholders::take_from(state); + // workaround for E::Placeholders and E::Param being the same type + // when fixed remove `Clone` requirement on endpoint + if TypeId::of::() == TypeId::of::() { + state.put(placeholders.clone()); + } let params = E::Params::take_from(state); let body = match E::needs_body() { diff --git a/src/types.rs b/src/types.rs index ca08bec..20be58d 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,8 +1,7 @@ -#[cfg(feature = "openapi")] -use crate::OpenapiType; - use gotham::hyper::body::Bytes; use mime::{Mime, APPLICATION_JSON}; +#[cfg(feature = "openapi")] +use openapi_type::OpenapiType; use serde::{de::DeserializeOwned, Serialize}; use std::error::Error; diff --git a/tests/async_methods.rs b/tests/async_methods.rs index 21a74b5..9a1669b 100644 --- a/tests/async_methods.rs +++ b/tests/async_methods.rs @@ -9,6 +9,8 @@ use gotham::{ }; use gotham_restful::*; use mime::{APPLICATION_JSON, TEXT_PLAIN}; +#[cfg(feature = "openapi")] +use openapi_type::OpenapiType; use serde::Deserialize; use tokio::time::{sleep, Duration}; @@ -28,7 +30,7 @@ struct FooBody { data: String } -#[derive(Deserialize, StateData, StaticResponseExtender)] +#[derive(Clone, Deserialize, StateData, StaticResponseExtender)] #[cfg_attr(feature = "openapi", derive(OpenapiType))] #[allow(dead_code)] struct FooSearch { diff --git a/tests/sync_methods.rs b/tests/sync_methods.rs index 4e07259..2b440fa 100644 --- a/tests/sync_methods.rs +++ b/tests/sync_methods.rs @@ -4,6 +4,8 @@ extern crate gotham_derive; use gotham::{router::builder::*, test::TestServer}; use gotham_restful::*; use mime::{APPLICATION_JSON, TEXT_PLAIN}; +#[cfg(feature = "openapi")] +use openapi_type::OpenapiType; use serde::Deserialize; mod util { @@ -22,7 +24,7 @@ struct FooBody { data: String } -#[derive(Deserialize, StateData, StaticResponseExtender)] +#[derive(Clone, Deserialize, StateData, StaticResponseExtender)] #[cfg_attr(feature = "openapi", derive(OpenapiType))] #[allow(dead_code)] struct FooSearch { From 3a3f74336935f222e1c2bf0cb4b21f9ea2c77bc3 Mon Sep 17 00:00:00 2001 From: Dominic Date: Tue, 9 Mar 2021 19:55:04 +0100 Subject: [PATCH 12/18] ci: include openapi_type crate --- .gitlab-ci.yml | 11 ++++++---- .../fail/not_openapitype_generics.stderr | 2 ++ openapi_type/tests/fail/rustfmt.sh | 21 +++++++++++++++++++ 3 files changed, 30 insertions(+), 4 deletions(-) create mode 100755 openapi_type/tests/fail/rustfmt.sh diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index b98380d..27a6250 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -14,20 +14,20 @@ check-example: before_script: - cargo -V script: - - cd example - - cargo check + - cargo check --manifest-path example/Cargo.toml cache: key: cargo-stable-example paths: - cargo/ - target/ - + test-default: stage: test image: rust:1.49-slim before_script: - cargo -V script: + - cargo test --manifest-path openapi_type/Cargo.toml -- --skip trybuild - cargo test cache: key: cargo-1-49-default @@ -43,6 +43,7 @@ test-full: - apt install -y --no-install-recommends libpq-dev - cargo -V script: + - cargo test --manifest-path openapi_type/Cargo.toml --all-features -- --skip trybuild - cargo test --no-default-features --features full cache: key: cargo-1-49-all @@ -79,6 +80,7 @@ test-trybuild-ui: - apt install -y --no-install-recommends libpq-dev - cargo -V script: + - cargo test --manifest-path openapi_type/Cargo.toml --all-features -- trybuild - cargo test --no-default-features --features full --tests -- --ignored cache: key: cargo-1-50-all @@ -107,8 +109,9 @@ rustfmt: - cargo -V - cargo fmt --version script: - - cargo fmt -- --check + - cargo fmt --all -- --check - ./tests/ui/rustfmt.sh --check + - ./openapi-type/tests/fail/rustfmt.sh --check doc: stage: build diff --git a/openapi_type/tests/fail/not_openapitype_generics.stderr b/openapi_type/tests/fail/not_openapitype_generics.stderr index d41098b..d33bafe 100644 --- a/openapi_type/tests/fail/not_openapitype_generics.stderr +++ b/openapi_type/tests/fail/not_openapitype_generics.stderr @@ -16,6 +16,8 @@ error[E0599]: no function or associated item named `schema` found for struct `Fo = note: the method `schema` exists but the following trait bounds were not satisfied: `Bar: OpenapiType` which is required by `Foo: OpenapiType` + `Foo: OpenapiType` + which is required by `&Foo: OpenapiType` = help: items from traits can only be used if the trait is implemented and in scope = note: the following trait defines an item `schema`, perhaps you need to implement it: candidate #1: `OpenapiType` diff --git a/openapi_type/tests/fail/rustfmt.sh b/openapi_type/tests/fail/rustfmt.sh new file mode 100755 index 0000000..a93f958 --- /dev/null +++ b/openapi_type/tests/fail/rustfmt.sh @@ -0,0 +1,21 @@ +#!/bin/busybox ash +set -euo pipefail + +rustfmt=${RUSTFMT:-rustfmt} +version="$($rustfmt -V)" +case "$version" in + *nightly*) + # all good, no additional flags required + ;; + *) + # assume we're using some sort of rustup setup + rustfmt="$rustfmt +nightly" + ;; +esac + +return=0 +find "$(dirname "$0")" -name '*.rs' -type f | while read file; do + $rustfmt --config-path "$(dirname "$0")/../../../rustfmt.toml" "$@" "$file" || return=1 +done + +exit $return From 63567f54806cad4fdc18afe34c7d31bd1593b3e6 Mon Sep 17 00:00:00 2001 From: Dominic Date: Tue, 9 Mar 2021 20:08:12 +0100 Subject: [PATCH 13/18] ci: fix typo --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 27a6250..3db33c0 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -111,7 +111,7 @@ rustfmt: script: - cargo fmt --all -- --check - ./tests/ui/rustfmt.sh --check - - ./openapi-type/tests/fail/rustfmt.sh --check + - ./openapi_type/tests/fail/rustfmt.sh --check doc: stage: build From 9c7f681e3dd2d8f530cb9484f2406826d01f0d54 Mon Sep 17 00:00:00 2001 From: Dominic Date: Tue, 9 Mar 2021 20:51:44 +0100 Subject: [PATCH 14/18] remove outdated tests --- tests/trybuild_ui.rs | 7 ------- tests/ui/openapi_type/enum_with_fields.rs | 12 ------------ tests/ui/openapi_type/enum_with_fields.stderr | 11 ----------- tests/ui/openapi_type/nullable_non_bool.rs | 10 ---------- tests/ui/openapi_type/nullable_non_bool.stderr | 5 ----- tests/ui/openapi_type/rename_non_string.rs | 10 ---------- tests/ui/openapi_type/rename_non_string.stderr | 5 ----- tests/ui/openapi_type/tuple_struct.rs | 7 ------- tests/ui/openapi_type/tuple_struct.stderr | 5 ----- tests/ui/openapi_type/union.rs | 10 ---------- tests/ui/openapi_type/union.stderr | 5 ----- tests/ui/openapi_type/unknown_key.rs | 10 ---------- tests/ui/openapi_type/unknown_key.stderr | 5 ----- 13 files changed, 102 deletions(-) delete mode 100644 tests/ui/openapi_type/enum_with_fields.rs delete mode 100644 tests/ui/openapi_type/enum_with_fields.stderr delete mode 100644 tests/ui/openapi_type/nullable_non_bool.rs delete mode 100644 tests/ui/openapi_type/nullable_non_bool.stderr delete mode 100644 tests/ui/openapi_type/rename_non_string.rs delete mode 100644 tests/ui/openapi_type/rename_non_string.stderr delete mode 100644 tests/ui/openapi_type/tuple_struct.rs delete mode 100644 tests/ui/openapi_type/tuple_struct.stderr delete mode 100644 tests/ui/openapi_type/union.rs delete mode 100644 tests/ui/openapi_type/union.stderr delete mode 100644 tests/ui/openapi_type/unknown_key.rs delete mode 100644 tests/ui/openapi_type/unknown_key.stderr diff --git a/tests/trybuild_ui.rs b/tests/trybuild_ui.rs index 2317215..406ae6a 100644 --- a/tests/trybuild_ui.rs +++ b/tests/trybuild_ui.rs @@ -4,14 +4,7 @@ use trybuild::TestCases; #[ignore] fn trybuild_ui() { let t = TestCases::new(); - - // always enabled t.compile_fail("tests/ui/endpoint/*.rs"); t.compile_fail("tests/ui/from_body/*.rs"); t.compile_fail("tests/ui/resource/*.rs"); - - // require the openapi feature - if cfg!(feature = "openapi") { - t.compile_fail("tests/ui/openapi_type/*.rs"); - } } diff --git a/tests/ui/openapi_type/enum_with_fields.rs b/tests/ui/openapi_type/enum_with_fields.rs deleted file mode 100644 index b07cbfa..0000000 --- a/tests/ui/openapi_type/enum_with_fields.rs +++ /dev/null @@ -1,12 +0,0 @@ -#[macro_use] -extern crate gotham_restful; - -#[derive(OpenapiType)] -enum Food { - Pasta, - Pizza { pineapple: bool }, - Rice, - Other(String) -} - -fn main() {} diff --git a/tests/ui/openapi_type/enum_with_fields.stderr b/tests/ui/openapi_type/enum_with_fields.stderr deleted file mode 100644 index 2925a32..0000000 --- a/tests/ui/openapi_type/enum_with_fields.stderr +++ /dev/null @@ -1,11 +0,0 @@ -error: #[derive(OpenapiType)] does not support enum variants with fields - --> $DIR/enum_with_fields.rs:7:2 - | -7 | Pizza { pineapple: bool }, - | ^^^^^ - -error: #[derive(OpenapiType)] does not support enum variants with fields - --> $DIR/enum_with_fields.rs:9:2 - | -9 | Other(String) - | ^^^^^ diff --git a/tests/ui/openapi_type/nullable_non_bool.rs b/tests/ui/openapi_type/nullable_non_bool.rs deleted file mode 100644 index 2431e94..0000000 --- a/tests/ui/openapi_type/nullable_non_bool.rs +++ /dev/null @@ -1,10 +0,0 @@ -#[macro_use] -extern crate gotham_restful; - -#[derive(OpenapiType)] -struct Foo { - #[openapi(nullable = "yes, please")] - bar: String -} - -fn main() {} diff --git a/tests/ui/openapi_type/nullable_non_bool.stderr b/tests/ui/openapi_type/nullable_non_bool.stderr deleted file mode 100644 index 421d9cd..0000000 --- a/tests/ui/openapi_type/nullable_non_bool.stderr +++ /dev/null @@ -1,5 +0,0 @@ -error: Expected bool - --> $DIR/nullable_non_bool.rs:6:23 - | -6 | #[openapi(nullable = "yes, please")] - | ^^^^^^^^^^^^^ diff --git a/tests/ui/openapi_type/rename_non_string.rs b/tests/ui/openapi_type/rename_non_string.rs deleted file mode 100644 index 83f8bd6..0000000 --- a/tests/ui/openapi_type/rename_non_string.rs +++ /dev/null @@ -1,10 +0,0 @@ -#[macro_use] -extern crate gotham_restful; - -#[derive(OpenapiType)] -struct Foo { - #[openapi(rename = 42)] - bar: String -} - -fn main() {} diff --git a/tests/ui/openapi_type/rename_non_string.stderr b/tests/ui/openapi_type/rename_non_string.stderr deleted file mode 100644 index 0446b21..0000000 --- a/tests/ui/openapi_type/rename_non_string.stderr +++ /dev/null @@ -1,5 +0,0 @@ -error: Expected string literal - --> $DIR/rename_non_string.rs:6:21 - | -6 | #[openapi(rename = 42)] - | ^^ diff --git a/tests/ui/openapi_type/tuple_struct.rs b/tests/ui/openapi_type/tuple_struct.rs deleted file mode 100644 index 7def578..0000000 --- a/tests/ui/openapi_type/tuple_struct.rs +++ /dev/null @@ -1,7 +0,0 @@ -#[macro_use] -extern crate gotham_restful; - -#[derive(OpenapiType)] -struct Foo(String); - -fn main() {} diff --git a/tests/ui/openapi_type/tuple_struct.stderr b/tests/ui/openapi_type/tuple_struct.stderr deleted file mode 100644 index 62a81c1..0000000 --- a/tests/ui/openapi_type/tuple_struct.stderr +++ /dev/null @@ -1,5 +0,0 @@ -error: #[derive(OpenapiType)] does not support unnamed fields - --> $DIR/tuple_struct.rs:5:11 - | -5 | struct Foo(String); - | ^^^^^^^^ diff --git a/tests/ui/openapi_type/union.rs b/tests/ui/openapi_type/union.rs deleted file mode 100644 index 99efd49..0000000 --- a/tests/ui/openapi_type/union.rs +++ /dev/null @@ -1,10 +0,0 @@ -#[macro_use] -extern crate gotham_restful; - -#[derive(OpenapiType)] -union IntOrPointer { - int: u64, - pointer: *mut String -} - -fn main() {} diff --git a/tests/ui/openapi_type/union.stderr b/tests/ui/openapi_type/union.stderr deleted file mode 100644 index 2dbe3b6..0000000 --- a/tests/ui/openapi_type/union.stderr +++ /dev/null @@ -1,5 +0,0 @@ -error: #[derive(OpenapiType)] only works for structs and enums - --> $DIR/union.rs:5:1 - | -5 | union IntOrPointer { - | ^^^^^ diff --git a/tests/ui/openapi_type/unknown_key.rs b/tests/ui/openapi_type/unknown_key.rs deleted file mode 100644 index daab52a..0000000 --- a/tests/ui/openapi_type/unknown_key.rs +++ /dev/null @@ -1,10 +0,0 @@ -#[macro_use] -extern crate gotham_restful; - -#[derive(OpenapiType)] -struct Foo { - #[openapi(like = "pizza")] - bar: String -} - -fn main() {} diff --git a/tests/ui/openapi_type/unknown_key.stderr b/tests/ui/openapi_type/unknown_key.stderr deleted file mode 100644 index b5e9ac1..0000000 --- a/tests/ui/openapi_type/unknown_key.stderr +++ /dev/null @@ -1,5 +0,0 @@ -error: Unknown key - --> $DIR/unknown_key.rs:6:12 - | -6 | #[openapi(like = "pizza")] - | ^^^^ From a5257608e38a7f97a8694de7650f1538eac88479 Mon Sep 17 00:00:00 2001 From: Dominic Date: Tue, 9 Mar 2021 21:00:35 +0100 Subject: [PATCH 15/18] update readme --- README.md | 29 +++++++++++++++++++---------- README.tpl | 29 +++++++++++++++++++---------- 2 files changed, 38 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 1cb82c1..ecf0ae8 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,11 @@ -
-

gotham-restful

-
-
+
+
pipeline status coverage report - - crates.io - - - docs.rs - rustdoc @@ -26,6 +18,23 @@

+This repository contains the following crates: + + - **gotham_restful** + [![gotham_restful on crates.io](https://img.shields.io/crates/v/gotham_restful.svg)](https://crates.io/crates/gotham_restful) + [![gotham_restful on docs.rs](https://docs.rs/gotham_restful/badge.svg)](https://docs.rs/gotham_restful) + - **gotham_restful_derive** + [![gotham_restful_derive on crates.io](https://img.shields.io/crates/v/gotham_restful_derive.svg)](https://crates.io/crates/gotham_restful_derive) + [![gotham_restful_derive on docs.rs](https://docs.rs/gotham_restful_derive/badge.svg)](https://docs.rs/gotham_restful_derive) + - **openapi_type** + [![openapi_type on crates.io](https://img.shields.io/crates/v/openapi_type.svg)](https://crates.io/crates/openapi_type) + [![openapi_type on docs.rs](https://docs.rs/openapi_type/badge.svg)](https://docs.rs/crate/openapi_type) + - **openapi_type_derive** + [![openapi_type_derive on crates.io](https://img.shields.io/crates/v/openapi_type_derive.svg)](https://crates.io/crates/openapi_type_derive) + [![openapi_type_derive on docs.rs](https://docs.rs/openapi_type_derive/badge.svg)](https://docs.rs/crate/openapi_type_derive) + +# gotham-restful + This crate is an extension to the popular [gotham web framework][gotham] for Rust. It allows you to create resources with assigned endpoints that aim to be a more convenient way of creating handlers for requests. diff --git a/README.tpl b/README.tpl index bd3e252..1769315 100644 --- a/README.tpl +++ b/README.tpl @@ -1,19 +1,11 @@ -
-

gotham-restful

-
-
+
+
pipeline status coverage report - - crates.io - - - docs.rs - rustdoc @@ -26,6 +18,23 @@

+This repository contains the following crates: + + - **gotham_restful** + [![gotham_restful on crates.io](https://img.shields.io/crates/v/gotham_restful.svg)](https://crates.io/crates/gotham_restful) + [![gotham_restful on docs.rs](https://docs.rs/gotham_restful/badge.svg)](https://docs.rs/gotham_restful) + - **gotham_restful_derive** + [![gotham_restful_derive on crates.io](https://img.shields.io/crates/v/gotham_restful_derive.svg)](https://crates.io/crates/gotham_restful_derive) + [![gotham_restful_derive on docs.rs](https://docs.rs/gotham_restful_derive/badge.svg)](https://docs.rs/gotham_restful_derive) + - **openapi_type** + [![openapi_type on crates.io](https://img.shields.io/crates/v/openapi_type.svg)](https://crates.io/crates/openapi_type) + [![openapi_type on docs.rs](https://docs.rs/openapi_type/badge.svg)](https://docs.rs/crate/openapi_type) + - **openapi_type_derive** + [![openapi_type_derive on crates.io](https://img.shields.io/crates/v/openapi_type_derive.svg)](https://crates.io/crates/openapi_type_derive) + [![openapi_type_derive on docs.rs](https://docs.rs/openapi_type_derive/badge.svg)](https://docs.rs/crate/openapi_type_derive) + +# gotham-restful + {{readme}} ## Versioning From e206ab10eb1bb4eacfe418fd166cfcb55f7dd3b4 Mon Sep 17 00:00:00 2001 From: Dominic Date: Tue, 9 Mar 2021 21:23:44 +0100 Subject: [PATCH 16/18] update trybuild tests --- tests/ui/endpoint/invalid_params_ty.stderr | 25 +++++++++++++------ .../endpoint/invalid_placeholders_ty.stderr | 25 +++++++++++++------ 2 files changed, 36 insertions(+), 14 deletions(-) diff --git a/tests/ui/endpoint/invalid_params_ty.stderr b/tests/ui/endpoint/invalid_params_ty.stderr index 35ed700..de1597a 100644 --- a/tests/ui/endpoint/invalid_params_ty.stderr +++ b/tests/ui/endpoint/invalid_params_ty.stderr @@ -1,3 +1,14 @@ +error[E0277]: the trait bound `FooParams: OpenapiType` is not satisfied + --> $DIR/invalid_params_ty.rs:15:16 + | +15 | fn endpoint(_: FooParams) { + | ^^^^^^^^^ the trait `OpenapiType` is not implemented for `FooParams` + | + ::: $WORKSPACE/src/endpoint.rs + | + | #[openapi_bound("Params: OpenapiType")] + | --------------------- required by this bound in `gotham_restful::EndpointWithSchema::Params` + error[E0277]: the trait bound `for<'de> FooParams: serde::de::Deserialize<'de>` is not satisfied --> $DIR/invalid_params_ty.rs:15:16 | @@ -6,7 +17,7 @@ error[E0277]: the trait bound `for<'de> FooParams: serde::de::Deserialize<'de>` | ::: $WORKSPACE/src/endpoint.rs | - | type Params: QueryStringExtractor + Sync; + | type Params: QueryStringExtractor + Clone + Sync; | -------------------------- required by this bound in `gotham_restful::EndpointWithSchema::Params` error[E0277]: the trait bound `FooParams: StateData` is not satisfied @@ -17,7 +28,7 @@ error[E0277]: the trait bound `FooParams: StateData` is not satisfied | ::: $WORKSPACE/src/endpoint.rs | - | type Params: QueryStringExtractor + Sync; + | type Params: QueryStringExtractor + Clone + Sync; | -------------------------- required by this bound in `gotham_restful::EndpointWithSchema::Params` error[E0277]: the trait bound `FooParams: StaticResponseExtender` is not satisfied @@ -28,16 +39,16 @@ error[E0277]: the trait bound `FooParams: StaticResponseExtender` is not satisfi | ::: $WORKSPACE/src/endpoint.rs | - | type Params: QueryStringExtractor + Sync; + | type Params: QueryStringExtractor + Clone + Sync; | -------------------------- required by this bound in `gotham_restful::EndpointWithSchema::Params` -error[E0277]: the trait bound `FooParams: OpenapiType` is not satisfied +error[E0277]: the trait bound `FooParams: Clone` is not satisfied --> $DIR/invalid_params_ty.rs:15:16 | 15 | fn endpoint(_: FooParams) { - | ^^^^^^^^^ the trait `OpenapiType` is not implemented for `FooParams` + | ^^^^^^^^^ the trait `Clone` is not implemented for `FooParams` | ::: $WORKSPACE/src/endpoint.rs | - | #[openapi_bound("Params: crate::OpenapiType")] - | ---------------------------- required by this bound in `gotham_restful::EndpointWithSchema::Params` + | type Params: QueryStringExtractor + Clone + Sync; + | ----- required by this bound in `gotham_restful::EndpointWithSchema::Params` diff --git a/tests/ui/endpoint/invalid_placeholders_ty.stderr b/tests/ui/endpoint/invalid_placeholders_ty.stderr index 09c9bbb..58c8014 100644 --- a/tests/ui/endpoint/invalid_placeholders_ty.stderr +++ b/tests/ui/endpoint/invalid_placeholders_ty.stderr @@ -1,3 +1,14 @@ +error[E0277]: the trait bound `FooPlaceholders: OpenapiType` is not satisfied + --> $DIR/invalid_placeholders_ty.rs:15:16 + | +15 | fn endpoint(_: FooPlaceholders) { + | ^^^^^^^^^^^^^^^ the trait `OpenapiType` is not implemented for `FooPlaceholders` + | + ::: $WORKSPACE/src/endpoint.rs + | + | #[openapi_bound("Placeholders: OpenapiType")] + | --------------------------- required by this bound in `gotham_restful::EndpointWithSchema::Placeholders` + error[E0277]: the trait bound `for<'de> FooPlaceholders: serde::de::Deserialize<'de>` is not satisfied --> $DIR/invalid_placeholders_ty.rs:15:16 | @@ -6,7 +17,7 @@ error[E0277]: the trait bound `for<'de> FooPlaceholders: serde::de::Deserialize< | ::: $WORKSPACE/src/endpoint.rs | - | type Placeholders: PathExtractor + Sync; + | type Placeholders: PathExtractor + Clone + Sync; | ------------------- required by this bound in `gotham_restful::EndpointWithSchema::Placeholders` error[E0277]: the trait bound `FooPlaceholders: StateData` is not satisfied @@ -17,7 +28,7 @@ error[E0277]: the trait bound `FooPlaceholders: StateData` is not satisfied | ::: $WORKSPACE/src/endpoint.rs | - | type Placeholders: PathExtractor + Sync; + | type Placeholders: PathExtractor + Clone + Sync; | ------------------- required by this bound in `gotham_restful::EndpointWithSchema::Placeholders` error[E0277]: the trait bound `FooPlaceholders: StaticResponseExtender` is not satisfied @@ -28,16 +39,16 @@ error[E0277]: the trait bound `FooPlaceholders: StaticResponseExtender` is not s | ::: $WORKSPACE/src/endpoint.rs | - | type Placeholders: PathExtractor + Sync; + | type Placeholders: PathExtractor + Clone + Sync; | ------------------- required by this bound in `gotham_restful::EndpointWithSchema::Placeholders` -error[E0277]: the trait bound `FooPlaceholders: OpenapiType` is not satisfied +error[E0277]: the trait bound `FooPlaceholders: Clone` is not satisfied --> $DIR/invalid_placeholders_ty.rs:15:16 | 15 | fn endpoint(_: FooPlaceholders) { - | ^^^^^^^^^^^^^^^ the trait `OpenapiType` is not implemented for `FooPlaceholders` + | ^^^^^^^^^^^^^^^ the trait `Clone` is not implemented for `FooPlaceholders` | ::: $WORKSPACE/src/endpoint.rs | - | #[openapi_bound("Placeholders: crate::OpenapiType")] - | ---------------------------------- required by this bound in `gotham_restful::EndpointWithSchema::Placeholders` + | type Placeholders: PathExtractor + Clone + Sync; + | ----- required by this bound in `gotham_restful::EndpointWithSchema::Placeholders` From 2dd3f3e21a6f0cd15ffd74c5142935c7ea54947f Mon Sep 17 00:00:00 2001 From: Dominic Date: Tue, 9 Mar 2021 22:35:22 +0100 Subject: [PATCH 17/18] fix broken doc link --- openapi_type_derive/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openapi_type_derive/src/lib.rs b/openapi_type_derive/src/lib.rs index 10e6c0a..0a81bec 100644 --- a/openapi_type_derive/src/lib.rs +++ b/openapi_type_derive/src/lib.rs @@ -16,7 +16,7 @@ mod codegen; mod parser; use parser::*; -/// The derive macro for [OpenapiType][openapi_type::OpenapiType]. +/// The derive macro for [OpenapiType](https://docs.rs/openapi_type/*/openapi_type/trait.OpenapiType.html). #[proc_macro_derive(OpenapiType, attributes(openapi))] pub fn derive_openapi_type(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input); From 7bac379e057c26daf7b7a266d72f78a37109af2b Mon Sep 17 00:00:00 2001 From: Dominic Date: Wed, 10 Mar 2021 19:03:25 +0100 Subject: [PATCH 18/18] update readme: moved to github [skip ci] --- README.md | 408 +----------------------------------------------------- 1 file changed, 2 insertions(+), 406 deletions(-) diff --git a/README.md b/README.md index ecf0ae8..9c1e534 100644 --- a/README.md +++ b/README.md @@ -1,408 +1,4 @@ -
- -
+# Moved to GitHub -This repository contains the following crates: +This project has moved to GitHub: https://github.com/msrd0/gotham_restful - - **gotham_restful** - [![gotham_restful on crates.io](https://img.shields.io/crates/v/gotham_restful.svg)](https://crates.io/crates/gotham_restful) - [![gotham_restful on docs.rs](https://docs.rs/gotham_restful/badge.svg)](https://docs.rs/gotham_restful) - - **gotham_restful_derive** - [![gotham_restful_derive on crates.io](https://img.shields.io/crates/v/gotham_restful_derive.svg)](https://crates.io/crates/gotham_restful_derive) - [![gotham_restful_derive on docs.rs](https://docs.rs/gotham_restful_derive/badge.svg)](https://docs.rs/gotham_restful_derive) - - **openapi_type** - [![openapi_type on crates.io](https://img.shields.io/crates/v/openapi_type.svg)](https://crates.io/crates/openapi_type) - [![openapi_type on docs.rs](https://docs.rs/openapi_type/badge.svg)](https://docs.rs/crate/openapi_type) - - **openapi_type_derive** - [![openapi_type_derive on crates.io](https://img.shields.io/crates/v/openapi_type_derive.svg)](https://crates.io/crates/openapi_type_derive) - [![openapi_type_derive on docs.rs](https://docs.rs/openapi_type_derive/badge.svg)](https://docs.rs/crate/openapi_type_derive) - -# gotham-restful - -This crate is an extension to the popular [gotham web framework][gotham] for Rust. It allows you to -create resources with assigned endpoints that aim to be a more convenient way of creating handlers -for requests. - -## Features - - - Automatically parse **JSON** request and produce response bodies - - Allow using **raw** request and response bodies - - Convenient **macros** to create responses that can be registered with gotham's router - - Auto-Generate an **OpenAPI** specification for your API - - Manage **CORS** headers so you don't have to - - Manage **Authentication** with JWT - - Integrate diesel connection pools for easy **database** integration - -## Safety - -This crate is just as safe as you'd expect from anything written in safe Rust - and -`#![forbid(unsafe_code)]` ensures that no unsafe was used. - -## Endpoints - -There are a set of pre-defined endpoints that should cover the majority of REST APIs. However, -it is also possible to define your own endpoints. - -### Pre-defined Endpoints - -Assuming you assign `/foobar` to your resource, the following pre-defined endpoints exist: - -| Endpoint Name | Required Arguments | HTTP Verb | HTTP Path | -| ------------- | ------------------ | --------- | -------------- | -| read_all | | GET | /foobar | -| read | id | GET | /foobar/:id | -| search | query | GET | /foobar/search | -| create | body | POST | /foobar | -| change_all | body | PUT | /foobar | -| change | id, body | PUT | /foobar/:id | -| remove_all | | DELETE | /foobar | -| remove | id | DELETE | /foobar/:id | - -Each of those endpoints has a macro that creates the neccessary boilerplate for the Resource. A -simple example looks like this: - -```rust -/// Our RESTful resource. -#[derive(Resource)] -#[resource(read)] -struct FooResource; - -/// The return type of the foo read endpoint. -#[derive(Serialize)] -struct Foo { - id: u64 -} - -/// The foo read endpoint. -#[read] -fn read(id: u64) -> Success { - Foo { id }.into() -} -``` - -### Custom Endpoints - -Defining custom endpoints is done with the `#[endpoint]` macro. The syntax is similar to that -of the pre-defined endpoints, but you need to give it more context: - -```rust -use gotham_restful::gotham::hyper::Method; - -#[derive(Resource)] -#[resource(custom_endpoint)] -struct CustomResource; - -/// This type is used to parse path parameters. -#[derive(Clone, Deserialize, StateData, StaticResponseExtender)] -struct CustomPath { - name: String -} - -#[endpoint(uri = "custom/:name/read", method = "Method::GET", params = false, body = false)] -fn custom_endpoint(path: CustomPath) -> Success { - path.name.into() -} -``` - -## Arguments - -Some endpoints require arguments. Those should be - * **id** Should be a deserializable json-primitive like [`i64`] or [`String`]. - * **body** Should be any deserializable object, or any type implementing [`RequestBody`]. - * **query** Should be any deserializable object whose variables are json-primitives. It will - however not be parsed from json, but from HTTP GET parameters like in `search?id=1`. The - type needs to implement [`QueryStringExtractor`](gotham::extractor::QueryStringExtractor). - -Additionally, all handlers may take a reference to gotham's [`State`]. Please note that for async -handlers, it needs to be a mutable reference until rustc's lifetime checks across await bounds -improve. - -## Uploads and Downloads - -By default, every request body is parsed from json, and every respone is converted to json using -[serde_json]. However, you may also use raw bodies. This is an example where the request body -is simply returned as the response again, no json parsing involved: - -```rust -#[derive(Resource)] -#[resource(create)] -struct ImageResource; - -#[derive(FromBody, RequestBody)] -#[supported_types(mime::IMAGE_GIF, mime::IMAGE_JPEG, mime::IMAGE_PNG)] -struct RawImage { - content: Vec, - content_type: Mime -} - -#[create] -fn create(body : RawImage) -> Raw> { - Raw::new(body.content, body.content_type) -} -``` - -## Custom HTTP Headers - -You can read request headers from the state as you would in any other gotham handler, and specify -custom response headers using [Response::header]. - -```rust -#[derive(Resource)] -#[resource(read_all)] -struct FooResource; - -#[read_all] -async fn read_all(state: &mut State) -> NoContent { - let headers: &HeaderMap = state.borrow(); - let accept = &headers[ACCEPT]; - - let mut res = NoContent::default(); - res.header(VARY, "accept".parse().unwrap()); - res -} -``` - -## Features - -To make life easier for common use-cases, this create offers a few features that might be helpful -when you implement your web server. The complete feature list is - - [`auth`](#authentication-feature) Advanced JWT middleware - - `chrono` openapi support for chrono types - - `full` enables all features except `without-openapi` - - [`cors`](#cors-feature) CORS handling for all endpoint handlers - - [`database`](#database-feature) diesel middleware support - - `errorlog` log errors returned from endpoint handlers - - [`openapi`](#openapi-feature) router additions to generate an openapi spec - - `uuid` openapi support for uuid - - `without-openapi` (**default**) disables `openapi` support. - -### Authentication Feature - -In order to enable authentication support, enable the `auth` feature gate. This allows you to -register a middleware that can automatically check for the existence of an JWT authentication -token. Besides being supported by the endpoint macros, it supports to lookup the required JWT secret -with the JWT data, hence you can use several JWT secrets and decide on the fly which secret to use. -None of this is currently supported by gotham's own JWT middleware. - -A simple example that uses only a single secret looks like this: - -```rust -#[derive(Resource)] -#[resource(read)] -struct SecretResource; - -#[derive(Serialize)] -struct Secret { - id: u64, - intended_for: String -} - -#[derive(Deserialize, Clone)] -struct AuthData { - sub: String, - exp: u64 -} - -#[read] -fn read(auth: AuthStatus, id: u64) -> AuthSuccess { - let intended_for = auth.ok()?.sub; - Ok(Secret { id, intended_for }) -} - -fn main() { - let auth: AuthMiddleware = AuthMiddleware::new( - AuthSource::AuthorizationHeader, - AuthValidation::default(), - StaticAuthHandler::from_array(b"zlBsA2QXnkmpe0QTh8uCvtAEa4j33YAc") - ); - let (chain, pipelines) = single_pipeline(new_pipeline().add(auth).build()); - gotham::start("127.0.0.1:8080", build_router(chain, pipelines, |route| { - route.resource::("secret"); - })); -} -``` - -### CORS Feature - -The cors feature allows an easy usage of this web server from other origins. By default, only -the `Access-Control-Allow-Methods` header is touched. To change the behaviour, add your desired -configuration as a middleware. - -A simple example that allows authentication from every origin (note that `*` always disallows -authentication), and every content type, looks like this: - -```rust -#[derive(Resource)] -#[resource(read_all)] -struct FooResource; - -#[read_all] -fn read_all() { - // your handler -} - -fn main() { - let cors = CorsConfig { - origin: Origin::Copy, - headers: Headers::List(vec![CONTENT_TYPE]), - max_age: 0, - credentials: true - }; - let (chain, pipelines) = single_pipeline(new_pipeline().add(cors).build()); - gotham::start("127.0.0.1:8080", build_router(chain, pipelines, |route| { - route.resource::("foo"); - })); -} -``` - -The cors feature can also be used for non-resource handlers. Take a look at [`CorsRoute`] -for an example. - -### Database Feature - -The database feature allows an easy integration of [diesel] into your handler functions. Please -note however that due to the way gotham's diesel middleware implementation, it is not possible -to run async code while holding a database connection. If you need to combine async and database, -you'll need to borrow the connection from the [`State`] yourself and return a boxed future. - -A simple non-async example looks like this: - -```rust -#[derive(Resource)] -#[resource(read_all)] -struct FooResource; - -#[derive(Queryable, Serialize)] -struct Foo { - id: i64, - value: String -} - -#[read_all] -fn read_all(conn: &PgConnection) -> QueryResult> { - foo::table.load(conn) -} - -type Repo = gotham_middleware_diesel::Repo; - -fn main() { - let repo = Repo::new(&env::var("DATABASE_URL").unwrap()); - let diesel = DieselMiddleware::new(repo); - - let (chain, pipelines) = single_pipeline(new_pipeline().add(diesel).build()); - gotham::start("127.0.0.1:8080", build_router(chain, pipelines, |route| { - route.resource::("foo"); - })); -} -``` - -### OpenAPI Feature - -The OpenAPI feature is probably the most powerful one of this crate. Definitely read this section -carefully both as a binary as well as a library author to avoid unwanted suprises. - -In order to automatically create an openapi specification, gotham-restful needs knowledge over -all routes and the types returned. `serde` does a great job at serialization but doesn't give -enough type information, so all types used in the router need to implement -`OpenapiType`[openapi_type::OpenapiType]. This can be derived for almoust any type and there -should be no need to implement it manually. A simple example looks like this: - -```rust -#[derive(Resource)] -#[resource(read_all)] -struct FooResource; - -#[derive(OpenapiType, Serialize)] -struct Foo { - bar: String -} - -#[read_all] -fn read_all() -> Success { - Foo { bar: "Hello World".to_owned() }.into() -} - -fn main() { - gotham::start("127.0.0.1:8080", build_simple_router(|route| { - let info = OpenapiInfo { - title: "My Foo API".to_owned(), - version: "0.1.0".to_owned(), - urls: vec!["https://example.org/foo/api/v1".to_owned()] - }; - route.with_openapi(info, |mut route| { - route.resource::("foo"); - route.get_openapi("openapi"); - }); - })); -} -``` - -Above example adds the resource as before, but adds another endpoint that we specified as `/openapi`. -It will return the generated openapi specification in JSON format. This allows you to easily write -clients in different languages without worying to exactly replicate your api in each of those -languages. - -However, please note that by default, the `without-openapi` feature of this crate is enabled. -Disabling it in favour of the `openapi` feature will add an additional type bound, -[`OpenapiType`][openapi_type::OpenapiType], on some of the types in [`Endpoint`] and related -traits. This means that some code might only compile on either feature, but not on both. If you -are writing a library that uses gotham-restful, it is strongly recommended to pass both features -through and conditionally enable the openapi code, like this: - -```rust -#[derive(Deserialize, Serialize)] -#[cfg_attr(feature = "openapi", derive(openapi_type::OpenapiType))] -struct Foo; -``` - -## Examples - -This readme and the crate documentation contain some of example. In addition to that, there is -a collection of code in the [example] directory that might help you. Any help writing more -examples is highly appreciated. - - - [diesel]: https://diesel.rs/ - [example]: https://gitlab.com/msrd0/gotham-restful/tree/master/example - [gotham]: https://gotham.rs/ - [serde_json]: https://github.com/serde-rs/json#serde-json---- - [`State`]: gotham::state::State - -## Versioning - -Like all rust crates, this crate will follow semantic versioning guidelines. However, changing -the MSRV (minimum supported rust version) is not considered a breaking change. - -## License - -Copyright (C) 2020-2021 Dominic Meiser and [contributors](https://gitlab.com/msrd0/gotham-restful/-/graphs/master). - -``` -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - https://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -```