1
0
Fork 0
mirror of https://gitlab.com/msrd0/gotham-restful.git synced 2025-02-22 20:52:27 +00:00

use openapi_type::OpenapiType for gotham_restful

This commit is contained in:
Dominic 2021-03-09 19:46:11 +01:00
parent eecd192458
commit ebea39fe0d
Signed by: msrd0
GPG key ID: DCC8C247452E98F9
27 changed files with 148 additions and 866 deletions

View file

@ -24,26 +24,23 @@ futures-core = "0.3.7"
futures-util = "0.3.7" futures-util = "0.3.7"
gotham = { git = "https://github.com/gotham-rs/gotham", default-features = false } gotham = { git = "https://github.com/gotham-rs/gotham", default-features = false }
gotham_derive = "0.5.0" gotham_derive = "0.5.0"
gotham_restful_derive = "0.2.0" gotham_restful_derive = "0.3.0-dev"
log = "0.4.8" log = "0.4.8"
mime = "0.3.16" mime = "0.3.16"
serde = { version = "1.0.110", features = ["derive"] } serde = { version = "1.0.110", features = ["derive"] }
serde_json = "1.0.58" serde_json = "1.0.58"
thiserror = "1.0" thiserror = "1.0"
# features
chrono = { version = "0.4.19", features = ["serde"], optional = true }
uuid = { version = "0.8.1", optional = true }
# non-feature optional dependencies # non-feature optional dependencies
base64 = { version = "0.13.0", optional = true } base64 = { version = "0.13.0", optional = true }
cookie = { version = "0.15", 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 } indexmap = { version = "1.3.2", optional = true }
indoc = { version = "1.0", optional = true } indoc = { version = "1.0", optional = true }
jsonwebtoken = { version = "7.1.0", optional = true } jsonwebtoken = { version = "7.1.0", optional = true }
once_cell = { version = "1.5", optional = true } once_cell = { version = "1.5", optional = true }
openapiv3 = { version = "=0.3.2", optional = true } openapiv3 = { version = "=0.3.2", optional = true }
openapi_type = { version = "0.1.0-dev", optional = true }
regex = { version = "1.4", optional = true } regex = { version = "1.4", optional = true }
sha2 = { version = "0.9.3", optional = true } sha2 = { version = "0.9.3", optional = true }
@ -58,7 +55,7 @@ trybuild = "1.0.27"
[features] [features]
default = ["cors", "errorlog", "without-openapi"] 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"] auth = ["gotham_restful_derive/auth", "base64", "cookie", "jsonwebtoken"]
cors = [] cors = []
@ -67,7 +64,7 @@ errorlog = []
# These features are exclusive - https://gitlab.com/msrd0/gotham-restful/-/issues/4 # These features are exclusive - https://gitlab.com/msrd0/gotham-restful/-/issues/4
without-openapi = [] 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] [package.metadata.docs.rs]
no-default-features = true no-default-features = true

View file

@ -100,7 +100,7 @@ use gotham_restful::gotham::hyper::Method;
struct CustomResource; struct CustomResource;
/// This type is used to parse path parameters. /// This type is used to parse path parameters.
#[derive(Deserialize, StateData, StaticResponseExtender)] #[derive(Clone, Deserialize, StateData, StaticResponseExtender)]
struct CustomPath { struct CustomPath {
name: String 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 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 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 enough type information, so all types used in the router need to implement
can be derived for almoust any type and there should be no need to implement it manually. A simple `OpenapiType`[openapi_type::OpenapiType]. This can be derived for almoust any type and there
example looks like this: should be no need to implement it manually. A simple example looks like this:
```rust ```rust
#[derive(Resource)] #[derive(Resource)]
@ -350,15 +350,15 @@ clients in different languages without worying to exactly replicate your api in
languages. languages.
However, please note that by default, the `without-openapi` feature of this crate is enabled. 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`], Disabling it in favour of the `openapi` feature will add an additional type bound,
on some of the types in [`Endpoint`] and related traits. This means that some code might only [`OpenapiType`][openapi_type::OpenapiType], on some of the types in [`Endpoint`] and related
compile on either feature, but not on both. If you are writing a library that uses gotham-restful, traits. This means that some code might only compile on either feature, but not on both. If you
it is strongly recommended to pass both features through and conditionally enable the openapi are writing a library that uses gotham-restful, it is strongly recommended to pass both features
code, like this: through and conditionally enable the openapi code, like this:
```rust ```rust
#[derive(Deserialize, Serialize)] #[derive(Deserialize, Serialize)]
#[cfg_attr(feature = "openapi", derive(OpenapiType))] #[cfg_attr(feature = "openapi", derive(openapi_type::OpenapiType))]
struct Foo; struct Foo;
``` ```

View file

@ -2,7 +2,7 @@
[package] [package]
name = "gotham_restful_derive" name = "gotham_restful_derive"
version = "0.2.0" version = "0.3.0-dev"
authors = ["Dominic Meiser <git@msrd0.de>"] authors = ["Dominic Meiser <git@msrd0.de>"]
edition = "2018" edition = "2018"
description = "Derive macros for gotham_restful" description = "Derive macros for gotham_restful"

View file

@ -128,14 +128,14 @@ impl EndpointType {
fn placeholders_ty(&self, arg_ty: Option<&Type>) -> TokenStream { fn placeholders_ty(&self, arg_ty: Option<&Type>) -> TokenStream {
match self { match self {
Self::ReadAll | Self::Search | Self::Create | Self::UpdateAll | Self::DeleteAll => { 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::Read | Self::Update | Self::Delete => quote!(::gotham_restful::private::IdPlaceholder::<#arg_ty>),
Self::Custom { .. } => { Self::Custom { .. } => {
if self.has_placeholders().value { if self.has_placeholders().value {
arg_ty.to_token_stream() arg_ty.to_token_stream()
} else { } 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 { fn params_ty(&self, arg_ty: Option<&Type>) -> TokenStream {
match self { match self {
Self::ReadAll | Self::Read | Self::Create | Self::UpdateAll | Self::Update | Self::DeleteAll | Self::Delete => { 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::Search => quote!(#arg_ty),
Self::Custom { .. } => { Self::Custom { .. } => {
if self.needs_params().value { if self.needs_params().value {
arg_ty.to_token_stream() arg_ty.to_token_stream()
} else { } else {
quote!(::gotham_restful::gotham::extractor::NoopQueryStringExtractor) quote!(::gotham_restful::NoopExtractor)
} }
}, },
} }
@ -201,7 +201,7 @@ impl EndpointType {
if self.needs_body().value { if self.needs_body().value {
arg_ty.to_token_stream() arg_ty.to_token_stream()
} else { } else {
quote!(::gotham_restful::gotham::extractor::NoopPathExtractor) quote!(())
} }
}, },
} }

View file

@ -24,11 +24,6 @@ use resource::expand_resource;
mod resource_error; mod resource_error;
use resource_error::expand_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; mod private_openapi_trait;
use private_openapi_trait::expand_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) 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))] #[proc_macro_derive(RequestBody, attributes(supported_types))]
pub fn derive_request_body(input: TokenStream) -> TokenStream { pub fn derive_request_body(input: TokenStream) -> TokenStream {
expand_derive(input, expand_request_body) expand_derive(input, expand_request_body)

View file

@ -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<TokenStream> {
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<WhereClause>) {
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<String>
}
fn to_string(lit: &Lit) -> Result<String> {
match lit {
Lit::Str(str) => Ok(str.value()),
_ => Err(Error::new(lit.span(), "Expected string literal"))
}
}
fn to_bool(lit: &Lit) -> Result<bool> {
match lit {
Lit::Bool(bool) => Ok(bool.value),
_ => Err(Error::new(lit.span(), "Expected bool"))
}
}
fn parse_attributes(input: &[Attribute]) -> Result<Attrs> {
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::<AttributeArgs>(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<TokenStream> {
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<Attribute>, input: DataEnum) -> Result<TokenStream> {
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<String> = 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<TokenStream> {
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<String> = schema.dependencies.keys().map(|k| k.to_string()).collect();
for dep in keys
{
let dep_schema = schema.dependencies.swap_remove(&dep);
if let Some(dep_schema) = dep_schema
{
dependencies.insert(dep, dep_schema);
}
}
match schema.name.clone() {
Some(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<Attribute>, input: DataStruct) -> Result<TokenStream> {
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<TokenStream> = 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<String, ReferenceOr<Box<Schema>>> = IndexMap::new();
let mut required : Vec<String> = Vec::new();
let mut dependencies : IndexMap<String, OpenapiSchema> = IndexMap::new();
#(#fields)*
let schema = SchemaKind::Type(Type::Object(ObjectType {
properties,
required,
additional_properties: None,
min_properties: None,
max_properties: None
}));
OpenapiSchema {
name: Some(#name.to_string()),
nullable: #nullable,
schema,
dependencies
}
}
}
})
}

View file

@ -26,18 +26,25 @@ fn impl_openapi_type(_ident: &Ident, _generics: &Generics) -> TokenStream {
#[cfg(feature = "openapi")] #[cfg(feature = "openapi")]
fn impl_openapi_type(ident: &Ident, generics: &Generics) -> TokenStream { fn impl_openapi_type(ident: &Ident, generics: &Generics) -> TokenStream {
let krate = super::krate(); let krate = super::krate();
let openapi = quote!(#krate::private::openapi);
quote! { 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}; #krate::private::OpenapiSchema::new(
#openapi::SchemaKind::Type(
OpenapiSchema::new(SchemaKind::Type(Type::String(StringType { #openapi::Type::String(
format: VariantOrUnknownOrEmpty::Item(StringFormat::Binary), #openapi::StringType {
..Default::default() format: #openapi::VariantOrUnknownOrEmpty::Item(
}))) #openapi::StringFormat::Binary
),
.. ::std::default::Default::default()
}
)
)
)
} }
} }
} }

View file

@ -97,7 +97,7 @@ fn str_schema(format: VariantOrUnknownOrEmpty<StringFormat>) -> OpenapiSchema {
}))) })))
} }
impl_openapi_type!(String => str_schema(VariantOrUnknownOrEmpty::Empty)); impl_openapi_type!(String, str => str_schema(VariantOrUnknownOrEmpty::Empty));
#[cfg(feature = "chrono")] #[cfg(feature = "chrono")]
impl_openapi_type!(Date<T: TimeZone>, NaiveDate => { impl_openapi_type!(Date<T: TimeZone>, NaiveDate => {

View file

@ -78,3 +78,9 @@ struct MyResponse {
pub trait OpenapiType { pub trait OpenapiType {
fn schema() -> OpenapiSchema; fn schema() -> OpenapiSchema;
} }
impl<'a, T: ?Sized + OpenapiType> OpenapiType for &'a T {
fn schema() -> OpenapiSchema {
T::schema()
}
}

View file

@ -55,7 +55,7 @@ fn expand_openapi_type(mut input: DeriveInput) -> syn::Result<TokenStream2> {
modifier: TraitBoundModifier::None, modifier: TraitBoundModifier::None,
lifetimes: None, lifetimes: None,
path: path!(::openapi_type::OpenapiType) path: path!(::openapi_type::OpenapiType)
})) }));
}); });
generics.split_for_impl() generics.split_for_impl()
}; };

View file

@ -2,11 +2,41 @@ use crate::{IntoResponse, RequestBody};
use futures_util::future::BoxFuture; use futures_util::future::BoxFuture;
use gotham::{ use gotham::{
extractor::{PathExtractor, QueryStringExtractor}, extractor::{PathExtractor, QueryStringExtractor},
hyper::{Body, Method}, hyper::{Body, Method, Response},
state::State router::response::extender::StaticResponseExtender,
state::{State, StateData}
}; };
#[cfg(feature = "openapi")]
use openapi_type::{OpenapiSchema, OpenapiType};
use serde::{Deserialize, Deserializer};
use std::borrow::Cow; 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: Deserializer<'de>>(_: D) -> Result<Self, D::Error> {
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<Body>) {}
}
// TODO: Specify default types once https://github.com/rust-lang/rust/issues/29661 lands. // TODO: Specify default types once https://github.com/rust-lang/rust/issues/29661 lands.
#[_private_openapi_trait(EndpointWithSchema)] #[_private_openapi_trait(EndpointWithSchema)]
pub trait Endpoint { pub trait Endpoint {
@ -23,19 +53,19 @@ pub trait Endpoint {
fn has_placeholders() -> bool { fn has_placeholders() -> bool {
false false
} }
/// The type that parses the URI placeholders. Use [gotham::extractor::NoopPathExtractor] /// The type that parses the URI placeholders. Use [NoopExtractor] if `has_placeholders()`
/// if `has_placeholders()` returns `false`. /// returns `false`.
#[openapi_bound("Placeholders: crate::OpenapiType")] #[openapi_bound("Placeholders: OpenapiType")]
type Placeholders: PathExtractor<Body> + Sync; type Placeholders: PathExtractor<Body> + Clone + Sync;
/// Returns `true` _iff_ the request parameters should be parsed. `false` by default. /// Returns `true` _iff_ the request parameters should be parsed. `false` by default.
fn needs_params() -> bool { fn needs_params() -> bool {
false false
} }
/// The type that parses the request parameters. Use [gotham::extractor::NoopQueryStringExtractor] /// The type that parses the request parameters. Use [NoopExtractor] if `needs_params()`
/// if `needs_params()` returns `false`. /// returns `false`.
#[openapi_bound("Params: crate::OpenapiType")] #[openapi_bound("Params: OpenapiType")]
type Params: QueryStringExtractor<Body> + Sync; type Params: QueryStringExtractor<Body> + Clone + Sync;
/// Returns `true` _iff_ the request body should be parsed. `false` by default. /// Returns `true` _iff_ the request body should be parsed. `false` by default.
fn needs_body() -> bool { fn needs_body() -> bool {

View file

@ -60,7 +60,7 @@ struct FooResource;
/// The return type of the foo read endpoint. /// The return type of the foo read endpoint.
#[derive(Serialize)] #[derive(Serialize)]
# #[cfg_attr(feature = "openapi", derive(OpenapiType))] # #[cfg_attr(feature = "openapi", derive(openapi_type::OpenapiType))]
struct Foo { struct Foo {
id: u64 id: u64
} }
@ -95,8 +95,8 @@ use gotham_restful::gotham::hyper::Method;
struct CustomResource; struct CustomResource;
/// This type is used to parse path parameters. /// This type is used to parse path parameters.
#[derive(Deserialize, StateData, StaticResponseExtender)] #[derive(Clone, Deserialize, StateData, StaticResponseExtender)]
# #[cfg_attr(feature = "openapi", derive(OpenapiType))] # #[cfg_attr(feature = "openapi", derive(openapi_type::OpenapiType))]
struct CustomPath { struct CustomPath {
name: String name: String
} }
@ -225,7 +225,7 @@ A simple example that uses only a single secret looks like this:
struct SecretResource; struct SecretResource;
#[derive(Serialize)] #[derive(Serialize)]
# #[cfg_attr(feature = "openapi", derive(OpenapiType))] # #[cfg_attr(feature = "openapi", derive(openapi_type::OpenapiType))]
struct Secret { struct Secret {
id: u64, id: u64,
intended_for: String intended_for: String
@ -331,7 +331,7 @@ A simple non-async example looks like this:
struct FooResource; struct FooResource;
#[derive(Queryable, Serialize)] #[derive(Queryable, Serialize)]
# #[cfg_attr(feature = "openapi", derive(OpenapiType))] # #[cfg_attr(feature = "openapi", derive(openapi_type::OpenapiType))]
struct Foo { struct Foo {
id: i64, id: i64,
value: String 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 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 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 enough type information, so all types used in the router need to implement
can be derived for almoust any type and there should be no need to implement it manually. A simple `OpenapiType`[openapi_type::OpenapiType]. This can be derived for almoust any type and there
example looks like this: should be no need to implement it manually. A simple example looks like this:
```rust,no_run ```rust,no_run
# #[macro_use] extern crate gotham_restful_derive; # #[macro_use] extern crate gotham_restful_derive;
@ -373,6 +373,7 @@ example looks like this:
# mod openapi_feature_enabled { # mod openapi_feature_enabled {
# use gotham::{router::builder::*, state::State}; # use gotham::{router::builder::*, state::State};
# use gotham_restful::*; # use gotham_restful::*;
# use openapi_type::OpenapiType;
# use serde::{Deserialize, Serialize}; # use serde::{Deserialize, Serialize};
#[derive(Resource)] #[derive(Resource)]
#[resource(read_all)] #[resource(read_all)]
@ -410,17 +411,17 @@ clients in different languages without worying to exactly replicate your api in
languages. languages.
However, please note that by default, the `without-openapi` feature of this crate is enabled. 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`], Disabling it in favour of the `openapi` feature will add an additional type bound,
on some of the types in [`Endpoint`] and related traits. This means that some code might only [`OpenapiType`][openapi_type::OpenapiType], on some of the types in [`Endpoint`] and related
compile on either feature, but not on both. If you are writing a library that uses gotham-restful, traits. This means that some code might only compile on either feature, but not on both. If you
it is strongly recommended to pass both features through and conditionally enable the openapi are writing a library that uses gotham-restful, it is strongly recommended to pass both features
code, like this: through and conditionally enable the openapi code, like this:
```rust ```rust
# #[macro_use] extern crate gotham_restful; # #[macro_use] extern crate gotham_restful;
# use serde::{Deserialize, Serialize}; # use serde::{Deserialize, Serialize};
#[derive(Deserialize, Serialize)] #[derive(Deserialize, Serialize)]
#[cfg_attr(feature = "openapi", derive(OpenapiType))] #[cfg_attr(feature = "openapi", derive(openapi_type::OpenapiType))]
struct Foo; struct Foo;
``` ```
@ -478,6 +479,8 @@ pub mod private {
#[cfg(feature = "openapi")] #[cfg(feature = "openapi")]
pub use indexmap::IndexMap; pub use indexmap::IndexMap;
#[cfg(feature = "openapi")] #[cfg(feature = "openapi")]
pub use openapi_type::{OpenapiSchema, OpenapiType};
#[cfg(feature = "openapi")]
pub use openapiv3 as openapi; pub use openapiv3 as openapi;
} }
@ -494,16 +497,12 @@ pub use cors::{handle_cors, CorsConfig, CorsRoute};
#[cfg(feature = "openapi")] #[cfg(feature = "openapi")]
mod openapi; mod openapi;
#[cfg(feature = "openapi")] #[cfg(feature = "openapi")]
pub use openapi::{ pub use openapi::{builder::OpenapiInfo, router::GetOpenapi};
builder::OpenapiInfo,
router::GetOpenapi,
types::{OpenapiSchema, OpenapiType}
};
mod endpoint; mod endpoint;
pub use endpoint::Endpoint;
#[cfg(feature = "openapi")] #[cfg(feature = "openapi")]
pub use endpoint::EndpointWithSchema; pub use endpoint::EndpointWithSchema;
pub use endpoint::{Endpoint, NoopExtractor};
mod response; mod response;
pub use response::{ pub use response::{

View file

@ -1,5 +1,5 @@
use crate::OpenapiSchema;
use indexmap::IndexMap; use indexmap::IndexMap;
use openapi_type::OpenapiSchema;
use openapiv3::{ use openapiv3::{
Components, OpenAPI, PathItem, ReferenceOr, Components, OpenAPI, PathItem, ReferenceOr,
ReferenceOr::{Item, Reference}, ReferenceOr::{Item, Reference},
@ -104,7 +104,7 @@ impl OpenapiBuilder {
#[allow(dead_code)] #[allow(dead_code)]
mod test { mod test {
use super::*; use super::*;
use crate::OpenapiType; use openapi_type::OpenapiType;
#[derive(OpenapiType)] #[derive(OpenapiType)]
struct Message { struct Message {

View file

@ -4,4 +4,3 @@ pub mod builder;
pub mod handler; pub mod handler;
pub mod operation; pub mod operation;
pub mod router; pub mod router;
pub mod types;

View file

@ -1,7 +1,8 @@
use super::SECURITY_NAME; 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 indexmap::IndexMap;
use mime::Mime; use mime::Mime;
use openapi_type::OpenapiSchema;
use openapiv3::{ use openapiv3::{
MediaType, Operation, Parameter, ParameterData, ParameterSchemaOrContent, ReferenceOr, ReferenceOr::Item, MediaType, Operation, Parameter, ParameterData, ParameterSchemaOrContent, ReferenceOr, ReferenceOr::Item,
RequestBody as OARequestBody, Response, Responses, Schema, SchemaKind, StatusCode, Type RequestBody as OARequestBody, Response, Responses, Schema, SchemaKind, StatusCode, Type

View file

@ -3,9 +3,10 @@ use super::{
handler::{OpenapiHandler, SwaggerUiHandler}, handler::{OpenapiHandler, SwaggerUiHandler},
operation::OperationDescription 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 gotham::{hyper::Method, pipeline::chain::PipelineHandleChain, router::builder::*};
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use openapi_type::OpenapiType;
use regex::{Captures, Regex}; use regex::{Captures, Regex};
use std::panic::RefUnwindSafe; use std::panic::RefUnwindSafe;

View file

@ -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<String>,
/// 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<String, OpenapiSchema>
}
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<FixedOffset>, Date<Local>, Date<Utc>, NaiveDate);
#[cfg(feature = "chrono")]
str_types!(
format = DateTime,
DateTime<FixedOffset>,
DateTime<Local>,
DateTime<Utc>,
NaiveDateTime
);
#[cfg(feature = "uuid")]
str_types!(format_str = "uuid", Uuid);
impl<T: OpenapiType> OpenapiType for Option<T> {
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<T: OpenapiType> OpenapiType for Vec<T> {
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<T: OpenapiType> OpenapiType for BTreeSet<T> {
fn schema() -> OpenapiSchema {
<Vec<T> as OpenapiType>::schema()
}
}
impl<T: OpenapiType, S: BuildHasher> OpenapiType for HashSet<T, S> {
fn schema() -> OpenapiSchema {
<Vec<T> as OpenapiType>::schema()
}
}
impl<K: OpenapiType, T: OpenapiType, S: BuildHasher> OpenapiType for HashMap<K, T, S> {
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 [<test_schema_ $ty:lower $($(_ $generic:lower)+)*>]()
{
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<FixedOffset> => r#"{"type":"string","format":"date"}"#);
assert_schema!(Date<Local> => r#"{"type":"string","format":"date"}"#);
assert_schema!(Date<Utc> => r#"{"type":"string","format":"date"}"#);
assert_schema!(NaiveDate => r#"{"type":"string","format":"date"}"#);
assert_schema!(DateTime<FixedOffset> => r#"{"type":"string","format":"date-time"}"#);
assert_schema!(DateTime<Local> => r#"{"type":"string","format":"date-time"}"#);
assert_schema!(DateTime<Utc> => r#"{"type":"string","format":"date-time"}"#);
assert_schema!(NaiveDateTime => r#"{"type":"string","format":"date-time"}"#);
}
assert_schema!(Option<String> => r#"{"nullable":true,"type":"string"}"#);
assert_schema!(Vec<String> => r#"{"type":"array","items":{"type":"string"}}"#);
assert_schema!(BTreeSet<String> => r#"{"type":"array","items":{"type":"string"}}"#);
assert_schema!(HashSet<String> => r#"{"type":"array","items":{"type":"string"}}"#);
assert_schema!(HashMap<i64, String> => r#"{"type":"object","properties":{"default":{"type":"integer","format":"int64"}},"required":["default"],"additionalProperties":{"type":"string"}}"#);
assert_schema!(Value => r#"{"nullable":true}"#);
}

View file

@ -1,6 +1,3 @@
#[cfg(feature = "openapi")]
use crate::OpenapiSchema;
use futures_util::future::{self, BoxFuture, FutureExt}; use futures_util::future::{self, BoxFuture, FutureExt};
use gotham::{ use gotham::{
handler::HandlerError, handler::HandlerError,
@ -10,6 +7,8 @@ use gotham::{
} }
}; };
use mime::{Mime, APPLICATION_JSON, STAR_STAR}; use mime::{Mime, APPLICATION_JSON, STAR_STAR};
#[cfg(feature = "openapi")]
use openapi_type::OpenapiSchema;
use serde::Serialize; use serde::Serialize;
use std::{ use std::{
convert::Infallible, convert::Infallible,
@ -259,7 +258,7 @@ mod test {
use thiserror::Error; use thiserror::Error;
#[derive(Debug, Default, Deserialize, Serialize)] #[derive(Debug, Default, Deserialize, Serialize)]
#[cfg_attr(feature = "openapi", derive(crate::OpenapiType))] #[cfg_attr(feature = "openapi", derive(openapi_type::OpenapiType))]
struct Msg { struct Msg {
msg: String msg: String
} }

View file

@ -1,12 +1,14 @@
use super::{handle_error, IntoResponse}; use super::{handle_error, IntoResponse};
use crate::{IntoResponseError, Response};
#[cfg(feature = "openapi")] #[cfg(feature = "openapi")]
use crate::{OpenapiSchema, OpenapiType, ResponseSchema}; use crate::ResponseSchema;
use crate::{IntoResponseError, Response};
use futures_util::{future, future::FutureExt}; use futures_util::{future, future::FutureExt};
use gotham::hyper::header::{HeaderMap, HeaderValue, IntoHeaderName}; use gotham::hyper::header::{HeaderMap, HeaderValue, IntoHeaderName};
#[cfg(feature = "openapi")] #[cfg(feature = "openapi")]
use gotham::hyper::StatusCode; use gotham::hyper::StatusCode;
use mime::Mime; use mime::Mime;
#[cfg(feature = "openapi")]
use openapi_type::{OpenapiSchema, OpenapiType};
use std::{fmt::Display, future::Future, pin::Pin}; use std::{fmt::Display, future::Future, pin::Pin};
/** /**

View file

@ -1,7 +1,9 @@
use super::{handle_error, IntoResponse, IntoResponseError}; use super::{handle_error, IntoResponse, IntoResponseError};
use crate::{FromBody, RequestBody, ResourceType, Response}; use crate::{FromBody, RequestBody, ResourceType, Response};
#[cfg(feature = "openapi")] #[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_core::future::Future;
use futures_util::{future, future::FutureExt}; use futures_util::{future, future::FutureExt};

View file

@ -1,12 +1,14 @@
use super::{handle_error, IntoResponse}; use super::{handle_error, IntoResponse};
use crate::{IntoResponseError, Response}; use crate::{IntoResponseError, Response};
#[cfg(feature = "openapi")] #[cfg(feature = "openapi")]
use crate::{NoContent, OpenapiSchema, ResponseSchema}; use crate::{NoContent, ResponseSchema};
use futures_util::future::{BoxFuture, FutureExt, TryFutureExt}; use futures_util::future::{BoxFuture, FutureExt, TryFutureExt};
use gotham::hyper::{ use gotham::hyper::{
header::{InvalidHeaderValue, LOCATION}, header::{InvalidHeaderValue, LOCATION},
Body, StatusCode Body, StatusCode
}; };
#[cfg(feature = "openapi")]
use openapi_type::OpenapiSchema;
use std::{ use std::{
error::Error as StdError, error::Error as StdError,
fmt::{Debug, Display} fmt::{Debug, Display}

View file

@ -1,7 +1,9 @@
use super::{handle_error, IntoResponse, ResourceError}; use super::{handle_error, IntoResponse, ResourceError};
#[cfg(feature = "openapi")] #[cfg(feature = "openapi")]
use crate::{OpenapiSchema, ResponseSchema}; use crate::ResponseSchema;
use crate::{Response, ResponseBody, Success}; use crate::{Response, ResponseBody, Success};
#[cfg(feature = "openapi")]
use openapi_type::OpenapiSchema;
use futures_core::future::Future; use futures_core::future::Future;
use gotham::hyper::StatusCode; use gotham::hyper::StatusCode;
@ -64,7 +66,7 @@ mod test {
use thiserror::Error; use thiserror::Error;
#[derive(Debug, Default, Deserialize, Serialize)] #[derive(Debug, Default, Deserialize, Serialize)]
#[cfg_attr(feature = "openapi", derive(crate::OpenapiType))] #[cfg_attr(feature = "openapi", derive(openapi_type::OpenapiType))]
struct Msg { struct Msg {
msg: String msg: String
} }

View file

@ -1,6 +1,6 @@
use super::IntoResponse; use super::IntoResponse;
#[cfg(feature = "openapi")] #[cfg(feature = "openapi")]
use crate::{OpenapiSchema, ResponseSchema}; use crate::ResponseSchema;
use crate::{Response, ResponseBody}; use crate::{Response, ResponseBody};
use futures_util::future::{self, FutureExt}; use futures_util::future::{self, FutureExt};
use gotham::hyper::{ use gotham::hyper::{
@ -8,6 +8,8 @@ use gotham::hyper::{
StatusCode StatusCode
}; };
use mime::{Mime, APPLICATION_JSON}; use mime::{Mime, APPLICATION_JSON};
#[cfg(feature = "openapi")]
use openapi_type::OpenapiSchema;
use std::{fmt::Debug, future::Future, pin::Pin}; use std::{fmt::Debug, future::Future, pin::Pin};
/** /**
@ -27,7 +29,7 @@ Usage example:
# struct MyResource; # struct MyResource;
# #
#[derive(Deserialize, Serialize)] #[derive(Deserialize, Serialize)]
# #[cfg_attr(feature = "openapi", derive(OpenapiType))] # #[cfg_attr(feature = "openapi", derive(openapi_type::OpenapiType))]
struct MyResponse { struct MyResponse {
message: &'static str message: &'static str
} }
@ -96,7 +98,7 @@ mod test {
use gotham::hyper::header::ACCESS_CONTROL_ALLOW_ORIGIN; use gotham::hyper::header::ACCESS_CONTROL_ALLOW_ORIGIN;
#[derive(Debug, Default, Serialize)] #[derive(Debug, Default, Serialize)]
#[cfg_attr(feature = "openapi", derive(crate::OpenapiType))] #[cfg_attr(feature = "openapi", derive(openapi_type::OpenapiType))]
struct Msg { struct Msg {
msg: String msg: String
} }

View file

@ -4,7 +4,6 @@ use crate::openapi::{
router::OpenapiRouter router::OpenapiRouter
}; };
use crate::{response::ResourceError, Endpoint, FromBody, IntoResponse, Resource, Response}; use crate::{response::ResourceError, Endpoint, FromBody, IntoResponse, Resource, Response};
#[cfg(feature = "cors")] #[cfg(feature = "cors")]
use gotham::router::route::matcher::AccessControlRequestMethodMatcher; use gotham::router::route::matcher::AccessControlRequestMethodMatcher;
use gotham::{ use gotham::{
@ -20,10 +19,12 @@ use gotham::{
state::{FromState, State} state::{FromState, State}
}; };
use mime::{Mime, APPLICATION_JSON}; 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. /// 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))] #[cfg_attr(feature = "openapi", derive(OpenapiType))]
pub struct PathExtractor<ID: RefUnwindSafe + Send + 'static> { pub struct PathExtractor<ID: RefUnwindSafe + Send + 'static> {
pub id: ID pub id: ID
@ -91,6 +92,11 @@ where
{ {
trace!("entering endpoint_handler"); trace!("entering endpoint_handler");
let placeholders = E::Placeholders::take_from(state); 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::<E::Placeholders>() == TypeId::of::<E::Params>() {
state.put(placeholders.clone());
}
let params = E::Params::take_from(state); let params = E::Params::take_from(state);
let body = match E::needs_body() { let body = match E::needs_body() {

View file

@ -1,8 +1,7 @@
#[cfg(feature = "openapi")]
use crate::OpenapiType;
use gotham::hyper::body::Bytes; use gotham::hyper::body::Bytes;
use mime::{Mime, APPLICATION_JSON}; use mime::{Mime, APPLICATION_JSON};
#[cfg(feature = "openapi")]
use openapi_type::OpenapiType;
use serde::{de::DeserializeOwned, Serialize}; use serde::{de::DeserializeOwned, Serialize};
use std::error::Error; use std::error::Error;

View file

@ -9,6 +9,8 @@ use gotham::{
}; };
use gotham_restful::*; use gotham_restful::*;
use mime::{APPLICATION_JSON, TEXT_PLAIN}; use mime::{APPLICATION_JSON, TEXT_PLAIN};
#[cfg(feature = "openapi")]
use openapi_type::OpenapiType;
use serde::Deserialize; use serde::Deserialize;
use tokio::time::{sleep, Duration}; use tokio::time::{sleep, Duration};
@ -28,7 +30,7 @@ struct FooBody {
data: String data: String
} }
#[derive(Deserialize, StateData, StaticResponseExtender)] #[derive(Clone, Deserialize, StateData, StaticResponseExtender)]
#[cfg_attr(feature = "openapi", derive(OpenapiType))] #[cfg_attr(feature = "openapi", derive(OpenapiType))]
#[allow(dead_code)] #[allow(dead_code)]
struct FooSearch { struct FooSearch {

View file

@ -4,6 +4,8 @@ extern crate gotham_derive;
use gotham::{router::builder::*, test::TestServer}; use gotham::{router::builder::*, test::TestServer};
use gotham_restful::*; use gotham_restful::*;
use mime::{APPLICATION_JSON, TEXT_PLAIN}; use mime::{APPLICATION_JSON, TEXT_PLAIN};
#[cfg(feature = "openapi")]
use openapi_type::OpenapiType;
use serde::Deserialize; use serde::Deserialize;
mod util { mod util {
@ -22,7 +24,7 @@ struct FooBody {
data: String data: String
} }
#[derive(Deserialize, StateData, StaticResponseExtender)] #[derive(Clone, Deserialize, StateData, StaticResponseExtender)]
#[cfg_attr(feature = "openapi", derive(OpenapiType))] #[cfg_attr(feature = "openapi", derive(OpenapiType))]
#[allow(dead_code)] #[allow(dead_code)]
struct FooSearch { struct FooSearch {