1
0
Fork 0
mirror of https://gitlab.com/msrd0/gotham-restful.git synced 2025-02-22 12:42:28 +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"
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

View file

@ -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;
```

View file

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

View file

@ -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!(())
}
},
}

View file

@ -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)

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")]
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()
}
)
)
)
}
}
}

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")]
impl_openapi_type!(Date<T: TimeZone>, NaiveDate => {

View file

@ -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()
}
}

View file

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

View file

@ -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: 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.
#[_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<Body> + Sync;
/// The type that parses the URI placeholders. Use [NoopExtractor] if `has_placeholders()`
/// returns `false`.
#[openapi_bound("Placeholders: OpenapiType")]
type Placeholders: PathExtractor<Body> + 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<Body> + Sync;
/// The type that parses the request parameters. Use [NoopExtractor] if `needs_params()`
/// returns `false`.
#[openapi_bound("Params: OpenapiType")]
type Params: QueryStringExtractor<Body> + Clone + Sync;
/// Returns `true` _iff_ the request body should be parsed. `false` by default.
fn needs_body() -> bool {

View file

@ -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::{

View file

@ -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 {

View file

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

View file

@ -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

View file

@ -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;

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 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
}

View file

@ -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};
/**

View file

@ -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};

View file

@ -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}

View file

@ -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
}

View file

@ -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
}

View file

@ -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<ID: RefUnwindSafe + Send + 'static> {
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::<E::Placeholders>() == TypeId::of::<E::Params>() {
state.put(placeholders.clone());
}
let params = E::Params::take_from(state);
let body = match E::needs_body() {

View file

@ -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;

View file

@ -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 {

View file

@ -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 {