diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 3db33c0..b98380d 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -14,20 +14,20 @@ check-example: before_script: - cargo -V script: - - cargo check --manifest-path example/Cargo.toml + - cd example + - cargo check cache: key: cargo-stable-example paths: - cargo/ - target/ - + test-default: stage: test image: rust:1.49-slim before_script: - cargo -V script: - - cargo test --manifest-path openapi_type/Cargo.toml -- --skip trybuild - cargo test cache: key: cargo-1-49-default @@ -43,7 +43,6 @@ test-full: - apt install -y --no-install-recommends libpq-dev - cargo -V script: - - cargo test --manifest-path openapi_type/Cargo.toml --all-features -- --skip trybuild - cargo test --no-default-features --features full cache: key: cargo-1-49-all @@ -80,7 +79,6 @@ test-trybuild-ui: - apt install -y --no-install-recommends libpq-dev - cargo -V script: - - cargo test --manifest-path openapi_type/Cargo.toml --all-features -- trybuild - cargo test --no-default-features --features full --tests -- --ignored cache: key: cargo-1-50-all @@ -109,9 +107,8 @@ rustfmt: - cargo -V - cargo fmt --version script: - - cargo fmt --all -- --check + - cargo fmt -- --check - ./tests/ui/rustfmt.sh --check - - ./openapi_type/tests/fail/rustfmt.sh --check doc: stage: build diff --git a/Cargo.toml b/Cargo.toml index 787b6da..5448ca0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,11 +1,11 @@ # -*- eval: (cargo-minor-mode 1) -*- [workspace] -members = [".", "./derive", "./example", "./openapi_type", "./openapi_type_derive"] +members = [".", "./derive", "./example"] [package] name = "gotham_restful" -version = "0.3.0-dev" +version = "0.2.1" authors = ["Dominic Meiser "] edition = "2018" description = "RESTful additions for the gotham web framework" @@ -22,25 +22,28 @@ gitlab = { repository = "msrd0/gotham-restful", branch = "master" } [dependencies] futures-core = "0.3.7" futures-util = "0.3.7" -gotham = { git = "https://github.com/gotham-rs/gotham", default-features = false } +gotham = { version = "0.5.0", default-features = false } gotham_derive = "0.5.0" -gotham_restful_derive = "0.3.0-dev" +gotham_restful_derive = "0.2.0" 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 = { git = "https://github.com/gotham-rs/gotham", optional = true } +cookie = { version = "0.14", optional = true } +gotham_middleware_diesel = { version = "0.2.0", 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 } @@ -49,13 +52,13 @@ diesel = { version = "1.4.4", features = ["postgres"] } futures-executor = "0.3.5" paste = "1.0" pretty_env_logger = "0.4" -tokio = { version = "1.0", features = ["time"], default-features = false } +tokio = { version = "0.2", features = ["time"], default-features = false } thiserror = "1.0.18" trybuild = "1.0.27" [features] default = ["cors", "errorlog", "without-openapi"] -full = ["auth", "cors", "database", "errorlog", "openapi"] +full = ["auth", "chrono", "cors", "database", "errorlog", "openapi", "uuid"] auth = ["gotham_restful_derive/auth", "base64", "cookie", "jsonwebtoken"] cors = [] @@ -64,7 +67,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", "openapi_type", "regex", "sha2"] +openapi = ["gotham_restful_derive/openapi", "base64", "indexmap", "indoc", "once_cell", "openapiv3", "regex", "sha2"] [package.metadata.docs.rs] no-default-features = true @@ -73,5 +76,3 @@ features = ["full"] [patch.crates-io] gotham_restful = { path = "." } gotham_restful_derive = { path = "./derive" } -openapi_type = { path = "./openapi_type" } -openapi_type_derive = { path = "./openapi_type_derive" } diff --git a/README.md b/README.md index 9c1e534..76da543 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,399 @@ -# Moved to GitHub +
+

gotham-restful

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

gotham-restful

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

-This repository contains the following crates: - - - **gotham_restful** - [![gotham_restful on crates.io](https://img.shields.io/crates/v/gotham_restful.svg)](https://crates.io/crates/gotham_restful) - [![gotham_restful on docs.rs](https://docs.rs/gotham_restful/badge.svg)](https://docs.rs/gotham_restful) - - **gotham_restful_derive** - [![gotham_restful_derive on crates.io](https://img.shields.io/crates/v/gotham_restful_derive.svg)](https://crates.io/crates/gotham_restful_derive) - [![gotham_restful_derive on docs.rs](https://docs.rs/gotham_restful_derive/badge.svg)](https://docs.rs/gotham_restful_derive) - - **openapi_type** - [![openapi_type on crates.io](https://img.shields.io/crates/v/openapi_type.svg)](https://crates.io/crates/openapi_type) - [![openapi_type on docs.rs](https://docs.rs/openapi_type/badge.svg)](https://docs.rs/crate/openapi_type) - - **openapi_type_derive** - [![openapi_type_derive on crates.io](https://img.shields.io/crates/v/openapi_type_derive.svg)](https://crates.io/crates/openapi_type_derive) - [![openapi_type_derive on docs.rs](https://docs.rs/openapi_type_derive/badge.svg)](https://docs.rs/crate/openapi_type_derive) - -# gotham-restful - {{readme}} ## Versioning diff --git a/derive/Cargo.toml b/derive/Cargo.toml index 58c877e..e06f2b0 100644 --- a/derive/Cargo.toml +++ b/derive/Cargo.toml @@ -2,7 +2,7 @@ [package] name = "gotham_restful_derive" -version = "0.3.0-dev" +version = "0.2.0" authors = ["Dominic Meiser "] edition = "2018" description = "Derive macros for gotham_restful" diff --git a/derive/src/endpoint.rs b/derive/src/endpoint.rs index 457f8ee..f6f143b 100644 --- a/derive/src/endpoint.rs +++ b/derive/src/endpoint.rs @@ -128,14 +128,14 @@ impl EndpointType { fn placeholders_ty(&self, arg_ty: Option<&Type>) -> TokenStream { match self { Self::ReadAll | Self::Search | Self::Create | Self::UpdateAll | Self::DeleteAll => { - quote!(::gotham_restful::NoopExtractor) + quote!(::gotham_restful::gotham::extractor::NoopPathExtractor) }, 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::NoopExtractor) + quote!(::gotham_restful::gotham::extractor::NoopPathExtractor) } }, } @@ -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::NoopExtractor) + quote!(::gotham_restful::gotham::extractor::NoopQueryStringExtractor) }, Self::Search => quote!(#arg_ty), Self::Custom { .. } => { if self.needs_params().value { arg_ty.to_token_stream() } else { - quote!(::gotham_restful::NoopExtractor) + quote!(::gotham_restful::gotham::extractor::NoopQueryStringExtractor) } }, } @@ -201,7 +201,7 @@ impl EndpointType { if self.needs_body().value { arg_ty.to_token_stream() } else { - quote!(()) + quote!(::gotham_restful::gotham::extractor::NoopPathExtractor) } }, } diff --git a/derive/src/lib.rs b/derive/src/lib.rs index 59ee8b6..39e2855 100644 --- a/derive/src/lib.rs +++ b/derive/src/lib.rs @@ -24,6 +24,11 @@ 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; @@ -61,6 +66,12 @@ pub fn derive_from_body(input: TokenStream) -> TokenStream { expand_derive(input, expand_from_body) } +#[cfg(feature = "openapi")] +#[proc_macro_derive(OpenapiType, attributes(openapi))] +pub fn derive_openapi_type(input: TokenStream) -> TokenStream { + expand_derive(input, expand_openapi_type) +} + #[proc_macro_derive(RequestBody, attributes(supported_types))] pub fn derive_request_body(input: TokenStream) -> TokenStream { expand_derive(input, expand_request_body) diff --git a/derive/src/openapi_type.rs b/derive/src/openapi_type.rs new file mode 100644 index 0000000..4b4530d --- /dev/null +++ b/derive/src/openapi_type.rs @@ -0,0 +1,289 @@ +use crate::util::{remove_parens, CollectToResult}; +use proc_macro2::{Ident, TokenStream}; +use quote::quote; +use syn::{ + parse_macro_input, spanned::Spanned, Attribute, AttributeArgs, Data, DataEnum, DataStruct, DeriveInput, Error, Field, + Fields, GenericParam, Generics, Lit, LitStr, Meta, NestedMeta, Path, PathSegment, PredicateType, Result, TraitBound, + TraitBoundModifier, Type, TypeParamBound, TypePath, Variant, WhereClause, WherePredicate +}; + +pub fn expand_openapi_type(input: DeriveInput) -> Result { + match (input.ident, input.generics, input.attrs, input.data) { + (ident, generics, attrs, Data::Enum(inum)) => expand_enum(ident, generics, attrs, inum), + (ident, generics, attrs, Data::Struct(strukt)) => expand_struct(ident, generics, attrs, strukt), + (_, _, _, Data::Union(uni)) => Err(Error::new( + uni.union_token.span(), + "#[derive(OpenapiType)] only works for structs and enums" + )) + } +} + +fn update_generics(generics: &Generics, where_clause: &mut Option) { + if generics.params.is_empty() { + return; + } + + if where_clause.is_none() { + *where_clause = Some(WhereClause { + where_token: Default::default(), + predicates: Default::default() + }); + } + let where_clause = where_clause.as_mut().unwrap(); + + for param in &generics.params { + if let GenericParam::Type(ty_param) = param { + where_clause.predicates.push(WherePredicate::Type(PredicateType { + lifetimes: None, + bounded_ty: Type::Path(TypePath { + qself: None, + path: Path { + leading_colon: None, + segments: vec![PathSegment { + ident: ty_param.ident.clone(), + arguments: Default::default() + }] + .into_iter() + .collect() + } + }), + colon_token: Default::default(), + bounds: vec![TypeParamBound::Trait(TraitBound { + paren_token: None, + modifier: TraitBoundModifier::None, + lifetimes: None, + path: syn::parse_str("::gotham_restful::OpenapiType").unwrap() + })] + .into_iter() + .collect() + })); + } + } +} + +#[derive(Debug, Default)] +struct Attrs { + nullable: bool, + rename: Option +} + +fn to_string(lit: &Lit) -> Result { + match lit { + Lit::Str(str) => Ok(str.value()), + _ => Err(Error::new(lit.span(), "Expected string literal")) + } +} + +fn to_bool(lit: &Lit) -> Result { + match lit { + Lit::Bool(bool) => Ok(bool.value), + _ => Err(Error::new(lit.span(), "Expected bool")) + } +} + +fn parse_attributes(input: &[Attribute]) -> Result { + let mut parsed = Attrs::default(); + for attr in input { + if attr.path.segments.iter().last().map(|segment| segment.ident.to_string()) == Some("openapi".to_owned()) { + let tokens = remove_parens(attr.tokens.clone()); + // TODO this is not public api but syn currently doesn't offer another convenient way to parse AttributeArgs + let nested = parse_macro_input::parse::(tokens.into())?; + for meta in nested { + match &meta { + NestedMeta::Meta(Meta::NameValue(kv)) => match kv.path.segments.last().map(|s| s.ident.to_string()) { + Some(key) => match key.as_ref() { + "nullable" => parsed.nullable = to_bool(&kv.lit)?, + "rename" => parsed.rename = Some(to_string(&kv.lit)?), + _ => return Err(Error::new(kv.path.span(), "Unknown key")) + }, + _ => return Err(Error::new(meta.span(), "Unexpected token")) + }, + _ => return Err(Error::new(meta.span(), "Unexpected token")) + } + } + } + } + Ok(parsed) +} + +fn expand_variant(variant: &Variant) -> Result { + if !matches!(variant.fields, Fields::Unit) { + return Err(Error::new( + variant.span(), + "#[derive(OpenapiType)] does not support enum variants with fields" + )); + } + + let ident = &variant.ident; + + let attrs = parse_attributes(&variant.attrs)?; + let name = match attrs.rename { + Some(rename) => rename, + None => ident.to_string() + }; + + Ok(quote! { + enumeration.push(#name.to_string()); + }) +} + +fn expand_enum(ident: Ident, generics: Generics, attrs: Vec, input: DataEnum) -> Result { + let krate = super::krate(); + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + let mut where_clause = where_clause.cloned(); + update_generics(&generics, &mut where_clause); + + let attrs = parse_attributes(&attrs)?; + let nullable = attrs.nullable; + let name = match attrs.rename { + Some(rename) => rename, + None => ident.to_string() + }; + + let variants = input.variants.iter().map(expand_variant).collect_to_result()?; + + Ok(quote! { + impl #impl_generics #krate::OpenapiType for #ident #ty_generics + #where_clause + { + fn schema() -> #krate::OpenapiSchema + { + use #krate::{private::openapi::*, OpenapiSchema}; + + let mut enumeration : Vec = Vec::new(); + + #(#variants)* + + let schema = SchemaKind::Type(Type::String(StringType { + format: VariantOrUnknownOrEmpty::Empty, + enumeration, + ..Default::default() + })); + + OpenapiSchema { + name: Some(#name.to_string()), + nullable: #nullable, + schema, + dependencies: Default::default() + } + } + } + }) +} + +fn expand_field(field: &Field) -> Result { + let ident = match &field.ident { + Some(ident) => ident, + None => { + return Err(Error::new( + field.span(), + "#[derive(OpenapiType)] does not support fields without an ident" + )) + }, + }; + let ident_str = LitStr::new(&ident.to_string(), ident.span()); + let ty = &field.ty; + + let attrs = parse_attributes(&field.attrs)?; + let nullable = attrs.nullable; + let name = match attrs.rename { + Some(rename) => rename, + None => ident.to_string() + }; + + Ok(quote! {{ + let mut schema = <#ty>::schema(); + + if schema.nullable + { + schema.nullable = false; + } + else if !#nullable + { + required.push(#ident_str.to_string()); + } + + let keys : Vec = schema.dependencies.keys().map(|k| k.to_string()).collect(); + for dep in keys + { + let dep_schema = schema.dependencies.swap_remove(&dep); + if let Some(dep_schema) = dep_schema + { + dependencies.insert(dep, dep_schema); + } + } + + match schema.name.clone() { + Some(schema_name) => { + properties.insert( + #name.to_string(), + ReferenceOr::Reference { reference: format!("#/components/schemas/{}", schema_name) } + ); + dependencies.insert(schema_name, schema); + }, + None => { + properties.insert( + #name.to_string(), + ReferenceOr::Item(Box::new(schema.into_schema())) + ); + } + } + }}) +} + +fn expand_struct(ident: Ident, generics: Generics, attrs: Vec, input: DataStruct) -> Result { + let krate = super::krate(); + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + let mut where_clause = where_clause.cloned(); + update_generics(&generics, &mut where_clause); + + let attrs = parse_attributes(&attrs)?; + let nullable = attrs.nullable; + let name = match attrs.rename { + Some(rename) => rename, + None => ident.to_string() + }; + + let fields: Vec = match input.fields { + Fields::Named(named_fields) => named_fields.named.iter().map(expand_field).collect_to_result()?, + Fields::Unnamed(fields) => { + return Err(Error::new( + fields.span(), + "#[derive(OpenapiType)] does not support unnamed fields" + )) + }, + Fields::Unit => Vec::new() + }; + + Ok(quote! { + impl #impl_generics #krate::OpenapiType for #ident #ty_generics + #where_clause + { + fn schema() -> #krate::OpenapiSchema + { + use #krate::{private::{openapi::*, IndexMap}, OpenapiSchema}; + + let mut properties : IndexMap>> = IndexMap::new(); + let mut required : Vec = Vec::new(); + let mut dependencies : IndexMap = IndexMap::new(); + + #(#fields)* + + let schema = SchemaKind::Type(Type::Object(ObjectType { + properties, + required, + additional_properties: None, + min_properties: None, + max_properties: None + })); + + OpenapiSchema { + name: Some(#name.to_string()), + nullable: #nullable, + schema, + dependencies + } + } + } + }) +} diff --git a/derive/src/request_body.rs b/derive/src/request_body.rs index c543dfa..9657b21 100644 --- a/derive/src/request_body.rs +++ b/derive/src/request_body.rs @@ -26,25 +26,18 @@ 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::private::OpenapiType for #ident #generics + impl #generics #krate::OpenapiType for #ident #generics { - fn schema() -> #krate::private::OpenapiSchema + fn schema() -> #krate::OpenapiSchema { - #krate::private::OpenapiSchema::new( - #openapi::SchemaKind::Type( - #openapi::Type::String( - #openapi::StringType { - format: #openapi::VariantOrUnknownOrEmpty::Item( - #openapi::StringFormat::Binary - ), - .. ::std::default::Default::default() - } - ) - ) - ) + use #krate::{private::openapi::*, OpenapiSchema}; + + OpenapiSchema::new(SchemaKind::Type(Type::String(StringType { + format: VariantOrUnknownOrEmpty::Item(StringFormat::Binary), + ..Default::default() + }))) } } } diff --git a/openapi_type/Cargo.toml b/openapi_type/Cargo.toml deleted file mode 100644 index 782f798..0000000 --- a/openapi_type/Cargo.toml +++ /dev/null @@ -1,27 +0,0 @@ -# -*- eval: (cargo-minor-mode 1) -*- - -[package] -workspace = ".." -name = "openapi_type" -version = "0.1.0-dev" -authors = ["Dominic Meiser "] -edition = "2018" -description = "OpenAPI type information for Rust structs and enums" -keywords = ["openapi", "type"] -license = "Apache-2.0" -repository = "https://gitlab.com/msrd0/gotham-restful/-/tree/master/openapi_type" - -[dependencies] -indexmap = "1.6" -openapi_type_derive = "0.1.0-dev" -openapiv3 = "=0.3.2" -serde_json = "1.0" - -# optional dependencies / features -chrono = { version = "0.4.19", optional = true } -uuid = { version = "0.8.2" , optional = true } - -[dev-dependencies] -paste = "1.0" -serde = "1.0" -trybuild = "1.0" diff --git a/openapi_type/src/impls.rs b/openapi_type/src/impls.rs deleted file mode 100644 index d46a922..0000000 --- a/openapi_type/src/impls.rs +++ /dev/null @@ -1,224 +0,0 @@ -use crate::{OpenapiSchema, OpenapiType}; -#[cfg(feature = "chrono")] -use chrono::{offset::TimeZone, Date, DateTime, NaiveDate, NaiveDateTime}; -use indexmap::{IndexMap, IndexSet}; -use openapiv3::{ - AdditionalProperties, ArrayType, IntegerType, NumberFormat, NumberType, ObjectType, ReferenceOr, SchemaKind, - StringFormat, StringType, Type, VariantOrUnknownOrEmpty -}; -use serde_json::Value; -use std::{ - collections::{BTreeMap, BTreeSet, HashMap, HashSet}, - hash::BuildHasher, - num::{NonZeroU128, NonZeroU16, NonZeroU32, NonZeroU64, NonZeroU8, NonZeroUsize} -}; -#[cfg(feature = "uuid")] -use uuid::Uuid; - -macro_rules! impl_openapi_type { - ($($ty:ident $(<$($generic:ident : $bound:path),+>)*),* => $schema:expr) => { - $( - impl $(<$($generic : $bound),+>)* OpenapiType for $ty $(<$($generic),+>)* { - fn schema() -> OpenapiSchema { - $schema - } - } - )* - }; -} - -type Unit = (); -impl_openapi_type!(Unit => { - OpenapiSchema::new(SchemaKind::Type(Type::Object(ObjectType { - additional_properties: Some(AdditionalProperties::Any(false)), - ..Default::default() - }))) -}); - -impl_openapi_type!(Value => { - OpenapiSchema { - nullable: true, - name: None, - schema: SchemaKind::Any(Default::default()), - dependencies: Default::default() - } -}); - -impl_openapi_type!(bool => OpenapiSchema::new(SchemaKind::Type(Type::Boolean {}))); - -#[inline] -fn int_schema(minimum: Option, bits: Option) -> OpenapiSchema { - OpenapiSchema::new(SchemaKind::Type(Type::Integer(IntegerType { - minimum, - format: bits - .map(|bits| VariantOrUnknownOrEmpty::Unknown(format!("int{}", bits))) - .unwrap_or(VariantOrUnknownOrEmpty::Empty), - ..Default::default() - }))) -} - -impl_openapi_type!(isize => int_schema(None, None)); -impl_openapi_type!(i8 => int_schema(None, Some(8))); -impl_openapi_type!(i16 => int_schema(None, Some(16))); -impl_openapi_type!(i32 => int_schema(None, Some(32))); -impl_openapi_type!(i64 => int_schema(None, Some(64))); -impl_openapi_type!(i128 => int_schema(None, Some(128))); - -impl_openapi_type!(usize => int_schema(Some(0), None)); -impl_openapi_type!(u8 => int_schema(Some(0), Some(8))); -impl_openapi_type!(u16 => int_schema(Some(0), Some(16))); -impl_openapi_type!(u32 => int_schema(Some(0), Some(32))); -impl_openapi_type!(u64 => int_schema(Some(0), Some(64))); -impl_openapi_type!(u128 => int_schema(Some(0), Some(128))); - -impl_openapi_type!(NonZeroUsize => int_schema(Some(1), None)); -impl_openapi_type!(NonZeroU8 => int_schema(Some(1), Some(8))); -impl_openapi_type!(NonZeroU16 => int_schema(Some(1), Some(16))); -impl_openapi_type!(NonZeroU32 => int_schema(Some(1), Some(32))); -impl_openapi_type!(NonZeroU64 => int_schema(Some(1), Some(64))); -impl_openapi_type!(NonZeroU128 => int_schema(Some(1), Some(128))); - -#[inline] -fn float_schema(format: NumberFormat) -> OpenapiSchema { - OpenapiSchema::new(SchemaKind::Type(Type::Number(NumberType { - format: VariantOrUnknownOrEmpty::Item(format), - ..Default::default() - }))) -} - -impl_openapi_type!(f32 => float_schema(NumberFormat::Float)); -impl_openapi_type!(f64 => float_schema(NumberFormat::Double)); - -#[inline] -fn str_schema(format: VariantOrUnknownOrEmpty) -> OpenapiSchema { - OpenapiSchema::new(SchemaKind::Type(Type::String(StringType { - format, - ..Default::default() - }))) -} - -impl_openapi_type!(String, str => str_schema(VariantOrUnknownOrEmpty::Empty)); - -#[cfg(feature = "chrono")] -impl_openapi_type!(Date, NaiveDate => { - str_schema(VariantOrUnknownOrEmpty::Item(StringFormat::Date)) -}); - -#[cfg(feature = "chrono")] -impl_openapi_type!(DateTime, NaiveDateTime => { - str_schema(VariantOrUnknownOrEmpty::Item(StringFormat::DateTime)) -}); - -#[cfg(feature = "uuid")] -impl_openapi_type!(Uuid => { - str_schema(VariantOrUnknownOrEmpty::Unknown("uuid".to_owned())) -}); - -impl_openapi_type!(Option => { - let schema = T::schema(); - let mut dependencies = schema.dependencies.clone(); - let schema = match schema.name.clone() { - Some(name) => { - let reference = ReferenceOr::Reference { - reference: format!("#/components/schemas/{}", name) - }; - dependencies.insert(name, schema); - SchemaKind::AllOf { all_of: vec![reference] } - }, - None => schema.schema - }; - - OpenapiSchema { - nullable: true, - name: None, - schema, - dependencies - } -}); - -#[inline] -fn array_schema(unique_items: bool) -> OpenapiSchema { - let schema = T::schema(); - let mut dependencies = schema.dependencies.clone(); - - let items = match schema.name.clone() { - Some(name) => { - let reference = ReferenceOr::Reference { - reference: format!("#/components/schemas/{}", name) - }; - dependencies.insert(name, schema); - reference - }, - None => ReferenceOr::Item(Box::new(schema.into_schema())) - }; - - OpenapiSchema { - nullable: false, - name: None, - schema: SchemaKind::Type(Type::Array(ArrayType { - items, - min_items: None, - max_items: None, - unique_items - })), - dependencies - } -} - -impl_openapi_type!(Vec => array_schema::(false)); -impl_openapi_type!(BTreeSet, IndexSet, HashSet => { - array_schema::(true) -}); - -#[inline] -fn map_schema() -> OpenapiSchema { - let key_schema = K::schema(); - let mut dependencies = key_schema.dependencies.clone(); - - let keys = match key_schema.name.clone() { - Some(name) => { - let reference = ReferenceOr::Reference { - reference: format!("#/components/schemas/{}", name) - }; - dependencies.insert(name, key_schema); - reference - }, - None => ReferenceOr::Item(Box::new(key_schema.into_schema())) - }; - - let schema = T::schema(); - dependencies.extend(schema.dependencies.iter().map(|(k, v)| (k.clone(), v.clone()))); - - let items = Box::new(match schema.name.clone() { - Some(name) => { - let reference = ReferenceOr::Reference { - reference: format!("#/components/schemas/{}", name) - }; - dependencies.insert(name, schema); - reference - }, - None => ReferenceOr::Item(schema.into_schema()) - }); - - let mut properties = IndexMap::new(); - properties.insert("default".to_owned(), keys); - - OpenapiSchema { - nullable: false, - name: None, - schema: SchemaKind::Type(Type::Object(ObjectType { - properties, - required: vec!["default".to_owned()], - additional_properties: Some(AdditionalProperties::Schema(items)), - ..Default::default() - })), - dependencies - } -} - -impl_openapi_type!( - BTreeMap, - IndexMap, - HashMap - => map_schema::() -); diff --git a/openapi_type/src/lib.rs b/openapi_type/src/lib.rs deleted file mode 100644 index 2933027..0000000 --- a/openapi_type/src/lib.rs +++ /dev/null @@ -1,86 +0,0 @@ -#![warn(missing_debug_implementations, rust_2018_idioms)] -#![forbid(unsafe_code)] -#![cfg_attr(feature = "cargo-clippy", allow(clippy::tabs_in_doc_comments))] -/*! -TODO -*/ - -pub use indexmap; -pub use openapi_type_derive::OpenapiType; -pub use openapiv3 as openapi; - -mod impls; -#[doc(hidden)] -pub mod private; - -use indexmap::IndexMap; -use openapi::{Schema, SchemaData, SchemaKind}; - -// TODO update the documentation -/** -This struct needs to be available for every type that can be part of an OpenAPI Spec. It is -already implemented for primitive types, String, Vec, Option and the like. To have it available -for your type, simply derive from [OpenapiType]. -*/ -#[derive(Debug, Clone, PartialEq)] -pub struct OpenapiSchema { - /// The name of this schema. If it is None, the schema will be inlined. - pub name: Option, - /// Whether this particular schema is nullable. Note that there is no guarantee that this will - /// make it into the final specification, it might just be interpreted as a hint to make it - /// an optional parameter. - pub nullable: bool, - /// The actual OpenAPI schema. - pub schema: SchemaKind, - /// Other schemas that this schema depends on. They will be included in the final OpenAPI Spec - /// along with this schema. - pub dependencies: IndexMap -} - -impl OpenapiSchema { - /// Create a new schema that has no name. - pub fn new(schema: SchemaKind) -> Self { - Self { - name: None, - nullable: false, - schema, - dependencies: IndexMap::new() - } - } - - /// Convert this schema to a [Schema] that can be serialized to the OpenAPI Spec. - pub fn into_schema(self) -> Schema { - Schema { - schema_data: SchemaData { - nullable: self.nullable, - title: self.name, - ..Default::default() - }, - schema_kind: self.schema - } - } -} - -/** -This trait needs to be implemented by every type that is being used in the OpenAPI Spec. It gives -access to the [OpenapiSchema] of this type. It is provided for primitive types, String and the -like. For use on your own types, there is a derive macro: - -``` -# #[macro_use] extern crate openapi_type_derive; -# -#[derive(OpenapiType)] -struct MyResponse { - message: String -} -``` -*/ -pub trait OpenapiType { - fn schema() -> OpenapiSchema; -} - -impl<'a, T: ?Sized + OpenapiType> OpenapiType for &'a T { - fn schema() -> OpenapiSchema { - T::schema() - } -} diff --git a/openapi_type/src/private.rs b/openapi_type/src/private.rs deleted file mode 100644 index 892b8e3..0000000 --- a/openapi_type/src/private.rs +++ /dev/null @@ -1,12 +0,0 @@ -use crate::OpenapiSchema; -use indexmap::IndexMap; - -pub type Dependencies = IndexMap; - -pub fn add_dependencies(dependencies: &mut Dependencies, other: &mut Dependencies) { - while let Some((dep_name, dep_schema)) = other.pop() { - if !dependencies.contains_key(&dep_name) { - dependencies.insert(dep_name, dep_schema); - } - } -} diff --git a/openapi_type/tests/custom_types.rs b/openapi_type/tests/custom_types.rs deleted file mode 100644 index 18cca88..0000000 --- a/openapi_type/tests/custom_types.rs +++ /dev/null @@ -1,249 +0,0 @@ -#![allow(dead_code)] -use openapi_type::OpenapiType; - -macro_rules! test_type { - ($ty:ty = $json:tt) => { - paste::paste! { - #[test] - fn [< $ty:lower >]() { - let schema = <$ty as OpenapiType>::schema(); - let schema = openapi_type::OpenapiSchema::into_schema(schema); - let schema_json = serde_json::to_value(&schema).unwrap(); - let expected = serde_json::json!($json); - assert_eq!(schema_json, expected); - } - } - }; -} - -#[derive(OpenapiType)] -struct UnitStruct; -test_type!(UnitStruct = { - "type": "object", - "title": "UnitStruct", - "additionalProperties": false -}); - -#[derive(OpenapiType)] -struct SimpleStruct { - foo: String, - bar: isize -} -test_type!(SimpleStruct = { - "type": "object", - "title": "SimpleStruct", - "properties": { - "foo": { - "type": "string" - }, - "bar": { - "type": "integer" - } - }, - "required": ["foo", "bar"] -}); - -#[derive(OpenapiType)] -#[openapi(rename = "FooBar")] -struct StructRename; -test_type!(StructRename = { - "type": "object", - "title": "FooBar", - "additionalProperties": false -}); - -#[derive(OpenapiType)] -enum EnumWithoutFields { - Success, - Error -} -test_type!(EnumWithoutFields = { - "type": "string", - "title": "EnumWithoutFields", - "enum": [ - "Success", - "Error" - ] -}); - -#[derive(OpenapiType)] -enum EnumWithOneField { - Success { value: isize } -} -test_type!(EnumWithOneField = { - "type": "object", - "title": "EnumWithOneField", - "properties": { - "Success": { - "type": "object", - "properties": { - "value": { - "type": "integer" - } - }, - "required": ["value"] - } - }, - "required": ["Success"] -}); - -#[derive(OpenapiType)] -enum EnumWithFields { - Success { value: isize }, - Error { msg: String } -} -test_type!(EnumWithFields = { - "title": "EnumWithFields", - "oneOf": [{ - "type": "object", - "properties": { - "Success": { - "type": "object", - "properties": { - "value": { - "type": "integer" - } - }, - "required": ["value"] - } - }, - "required": ["Success"] - }, { - "type": "object", - "properties": { - "Error": { - "type": "object", - "properties": { - "msg": { - "type": "string" - } - }, - "required": ["msg"] - } - }, - "required": ["Error"] - }] -}); - -#[derive(OpenapiType)] -enum EnumExternallyTagged { - Success { value: isize }, - Empty, - Error -} -test_type!(EnumExternallyTagged = { - "title": "EnumExternallyTagged", - "oneOf": [{ - "type": "object", - "properties": { - "Success": { - "type": "object", - "properties": { - "value": { - "type": "integer" - } - }, - "required": ["value"] - } - }, - "required": ["Success"] - }, { - "type": "string", - "enum": ["Empty", "Error"] - }] -}); - -#[derive(OpenapiType)] -#[openapi(tag = "ty")] -enum EnumInternallyTagged { - Success { value: isize }, - Empty, - Error -} -test_type!(EnumInternallyTagged = { - "title": "EnumInternallyTagged", - "oneOf": [{ - "type": "object", - "properties": { - "value": { - "type": "integer" - }, - "ty": { - "type": "string", - "enum": ["Success"] - } - }, - "required": ["value", "ty"] - }, { - "type": "object", - "properties": { - "ty": { - "type": "string", - "enum": ["Empty", "Error"] - } - }, - "required": ["ty"] - }] -}); - -#[derive(OpenapiType)] -#[openapi(tag = "ty", content = "ct")] -enum EnumAdjacentlyTagged { - Success { value: isize }, - Empty, - Error -} -test_type!(EnumAdjacentlyTagged = { - "title": "EnumAdjacentlyTagged", - "oneOf": [{ - "type": "object", - "properties": { - "ty": { - "type": "string", - "enum": ["Success"] - }, - "ct": { - "type": "object", - "properties": { - "value": { - "type": "integer" - } - }, - "required": ["value"] - } - }, - "required": ["ty", "ct"] - }, { - "type": "object", - "properties": { - "ty": { - "type": "string", - "enum": ["Empty", "Error"] - } - }, - "required": ["ty"] - }] -}); - -#[derive(OpenapiType)] -#[openapi(untagged)] -enum EnumUntagged { - Success { value: isize }, - Empty, - Error -} -test_type!(EnumUntagged = { - "title": "EnumUntagged", - "oneOf": [{ - "type": "object", - "properties": { - "value": { - "type": "integer" - } - }, - "required": ["value"] - }, { - "type": "object", - "additionalProperties": false - }] -}); diff --git a/openapi_type/tests/fail/enum_with_no_variants.rs b/openapi_type/tests/fail/enum_with_no_variants.rs deleted file mode 100644 index d08e223..0000000 --- a/openapi_type/tests/fail/enum_with_no_variants.rs +++ /dev/null @@ -1,6 +0,0 @@ -use openapi_type::OpenapiType; - -#[derive(OpenapiType)] -enum Foo {} - -fn main() {} diff --git a/openapi_type/tests/fail/enum_with_no_variants.stderr b/openapi_type/tests/fail/enum_with_no_variants.stderr deleted file mode 100644 index 5c6b1d1..0000000 --- a/openapi_type/tests/fail/enum_with_no_variants.stderr +++ /dev/null @@ -1,5 +0,0 @@ -error: #[derive(OpenapiType)] does not support enums with no variants - --> $DIR/enum_with_no_variants.rs:4:10 - | -4 | enum Foo {} - | ^^ diff --git a/openapi_type/tests/fail/not_openapitype.rs b/openapi_type/tests/fail/not_openapitype.rs deleted file mode 100644 index 2b5b23c..0000000 --- a/openapi_type/tests/fail/not_openapitype.rs +++ /dev/null @@ -1,12 +0,0 @@ -use openapi_type::OpenapiType; - -#[derive(OpenapiType)] -struct Foo { - bar: Bar -} - -struct Bar; - -fn main() { - Foo::schema(); -} diff --git a/openapi_type/tests/fail/not_openapitype.stderr b/openapi_type/tests/fail/not_openapitype.stderr deleted file mode 100644 index f089b15..0000000 --- a/openapi_type/tests/fail/not_openapitype.stderr +++ /dev/null @@ -1,8 +0,0 @@ -error[E0277]: the trait bound `Bar: OpenapiType` is not satisfied - --> $DIR/not_openapitype.rs:3:10 - | -3 | #[derive(OpenapiType)] - | ^^^^^^^^^^^ the trait `OpenapiType` is not implemented for `Bar` - | - = note: required by `schema` - = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/openapi_type/tests/fail/not_openapitype_generics.rs b/openapi_type/tests/fail/not_openapitype_generics.rs deleted file mode 100644 index 3d2a09d..0000000 --- a/openapi_type/tests/fail/not_openapitype_generics.rs +++ /dev/null @@ -1,12 +0,0 @@ -use openapi_type::OpenapiType; - -#[derive(OpenapiType)] -struct Foo { - bar: T -} - -struct Bar; - -fn main() { - >::schema(); -} diff --git a/openapi_type/tests/fail/not_openapitype_generics.stderr b/openapi_type/tests/fail/not_openapitype_generics.stderr deleted file mode 100644 index d33bafe..0000000 --- a/openapi_type/tests/fail/not_openapitype_generics.stderr +++ /dev/null @@ -1,23 +0,0 @@ -error[E0599]: no function or associated item named `schema` found for struct `Foo` in the current scope - --> $DIR/not_openapitype_generics.rs:11:14 - | -4 | struct Foo { - | ------------- - | | - | function or associated item `schema` not found for this - | doesn't satisfy `Foo: OpenapiType` -... -8 | struct Bar; - | ----------- doesn't satisfy `Bar: OpenapiType` -... -11 | >::schema(); - | ^^^^^^ function or associated item not found in `Foo` - | - = note: the method `schema` exists but the following trait bounds were not satisfied: - `Bar: OpenapiType` - which is required by `Foo: OpenapiType` - `Foo: OpenapiType` - which is required by `&Foo: OpenapiType` - = help: items from traits can only be used if the trait is implemented and in scope - = note: the following trait defines an item `schema`, perhaps you need to implement it: - candidate #1: `OpenapiType` diff --git a/openapi_type/tests/fail/rustfmt.sh b/openapi_type/tests/fail/rustfmt.sh deleted file mode 100755 index a93f958..0000000 --- a/openapi_type/tests/fail/rustfmt.sh +++ /dev/null @@ -1,21 +0,0 @@ -#!/bin/busybox ash -set -euo pipefail - -rustfmt=${RUSTFMT:-rustfmt} -version="$($rustfmt -V)" -case "$version" in - *nightly*) - # all good, no additional flags required - ;; - *) - # assume we're using some sort of rustup setup - rustfmt="$rustfmt +nightly" - ;; -esac - -return=0 -find "$(dirname "$0")" -name '*.rs' -type f | while read file; do - $rustfmt --config-path "$(dirname "$0")/../../../rustfmt.toml" "$@" "$file" || return=1 -done - -exit $return diff --git a/openapi_type/tests/fail/tuple_struct.rs b/openapi_type/tests/fail/tuple_struct.rs deleted file mode 100644 index 146a236..0000000 --- a/openapi_type/tests/fail/tuple_struct.rs +++ /dev/null @@ -1,6 +0,0 @@ -use openapi_type::OpenapiType; - -#[derive(OpenapiType)] -struct Foo(i64, i64); - -fn main() {} diff --git a/openapi_type/tests/fail/tuple_struct.stderr b/openapi_type/tests/fail/tuple_struct.stderr deleted file mode 100644 index b5ceb01..0000000 --- a/openapi_type/tests/fail/tuple_struct.stderr +++ /dev/null @@ -1,5 +0,0 @@ -error: #[derive(OpenapiType)] does not support tuple structs - --> $DIR/tuple_struct.rs:4:11 - | -4 | struct Foo(i64, i64); - | ^^^^^^^^^^ diff --git a/openapi_type/tests/fail/tuple_variant.rs b/openapi_type/tests/fail/tuple_variant.rs deleted file mode 100644 index 92aa8d7..0000000 --- a/openapi_type/tests/fail/tuple_variant.rs +++ /dev/null @@ -1,8 +0,0 @@ -use openapi_type::OpenapiType; - -#[derive(OpenapiType)] -enum Foo { - Pair(i64, i64) -} - -fn main() {} diff --git a/openapi_type/tests/fail/tuple_variant.stderr b/openapi_type/tests/fail/tuple_variant.stderr deleted file mode 100644 index 05573cb..0000000 --- a/openapi_type/tests/fail/tuple_variant.stderr +++ /dev/null @@ -1,5 +0,0 @@ -error: #[derive(OpenapiType)] does not support tuple variants - --> $DIR/tuple_variant.rs:5:6 - | -5 | Pair(i64, i64) - | ^^^^^^^^^^ diff --git a/openapi_type/tests/fail/union.rs b/openapi_type/tests/fail/union.rs deleted file mode 100644 index d011109..0000000 --- a/openapi_type/tests/fail/union.rs +++ /dev/null @@ -1,9 +0,0 @@ -use openapi_type::OpenapiType; - -#[derive(OpenapiType)] -union Foo { - signed: i64, - unsigned: u64 -} - -fn main() {} diff --git a/openapi_type/tests/fail/union.stderr b/openapi_type/tests/fail/union.stderr deleted file mode 100644 index f0feb48..0000000 --- a/openapi_type/tests/fail/union.stderr +++ /dev/null @@ -1,5 +0,0 @@ -error: #[derive(OpenapiType)] cannot be used on unions - --> $DIR/union.rs:4:1 - | -4 | union Foo { - | ^^^^^ diff --git a/openapi_type/tests/fail/unknown_attribute.rs b/openapi_type/tests/fail/unknown_attribute.rs deleted file mode 100644 index 70a4785..0000000 --- a/openapi_type/tests/fail/unknown_attribute.rs +++ /dev/null @@ -1,7 +0,0 @@ -use openapi_type::OpenapiType; - -#[derive(OpenapiType)] -#[openapi(pizza)] -struct Foo; - -fn main() {} diff --git a/openapi_type/tests/fail/unknown_attribute.stderr b/openapi_type/tests/fail/unknown_attribute.stderr deleted file mode 100644 index 2558768..0000000 --- a/openapi_type/tests/fail/unknown_attribute.stderr +++ /dev/null @@ -1,5 +0,0 @@ -error: Unexpected token - --> $DIR/unknown_attribute.rs:4:11 - | -4 | #[openapi(pizza)] - | ^^^^^ diff --git a/openapi_type/tests/std_types.rs b/openapi_type/tests/std_types.rs deleted file mode 100644 index e10fb89..0000000 --- a/openapi_type/tests/std_types.rs +++ /dev/null @@ -1,216 +0,0 @@ -#[cfg(feature = "chrono")] -use chrono::{Date, DateTime, FixedOffset, Local, NaiveDate, NaiveDateTime, Utc}; -use indexmap::{IndexMap, IndexSet}; -use openapi_type::OpenapiType; -use serde_json::Value; -use std::{ - collections::{BTreeMap, BTreeSet, HashMap, HashSet}, - num::{NonZeroU128, NonZeroU16, NonZeroU32, NonZeroU64, NonZeroU8, NonZeroUsize} -}; -#[cfg(feature = "uuid")] -use uuid::Uuid; - -macro_rules! test_type { - ($($ty:ident $(<$($generic:ident),+>)*),* = $json:tt) => { - paste::paste! { $( - #[test] - fn [< $ty:lower $($(_ $generic:lower)+)* >]() { - let schema = <$ty $(<$($generic),+>)* as OpenapiType>::schema(); - let schema = openapi_type::OpenapiSchema::into_schema(schema); - let schema_json = serde_json::to_value(&schema).unwrap(); - let expected = serde_json::json!($json); - assert_eq!(schema_json, expected); - } - )* } - }; -} - -type Unit = (); -test_type!(Unit = { - "type": "object", - "additionalProperties": false -}); - -test_type!(Value = { - "nullable": true -}); - -test_type!(bool = { - "type": "boolean" -}); - -// ### integer types - -test_type!(isize = { - "type": "integer" -}); - -test_type!(usize = { - "type": "integer", - "minimum": 0 -}); - -test_type!(i8 = { - "type": "integer", - "format": "int8" -}); - -test_type!(u8 = { - "type": "integer", - "format": "int8", - "minimum": 0 -}); - -test_type!(i16 = { - "type": "integer", - "format": "int16" -}); - -test_type!(u16 = { - "type": "integer", - "format": "int16", - "minimum": 0 -}); - -test_type!(i32 = { - "type": "integer", - "format": "int32" -}); - -test_type!(u32 = { - "type": "integer", - "format": "int32", - "minimum": 0 -}); - -test_type!(i64 = { - "type": "integer", - "format": "int64" -}); - -test_type!(u64 = { - "type": "integer", - "format": "int64", - "minimum": 0 -}); - -test_type!(i128 = { - "type": "integer", - "format": "int128" -}); - -test_type!(u128 = { - "type": "integer", - "format": "int128", - "minimum": 0 -}); - -// ### non-zero integer types - -test_type!(NonZeroUsize = { - "type": "integer", - "minimum": 1 -}); - -test_type!(NonZeroU8 = { - "type": "integer", - "format": "int8", - "minimum": 1 -}); - -test_type!(NonZeroU16 = { - "type": "integer", - "format": "int16", - "minimum": 1 -}); - -test_type!(NonZeroU32 = { - "type": "integer", - "format": "int32", - "minimum": 1 -}); - -test_type!(NonZeroU64 = { - "type": "integer", - "format": "int64", - "minimum": 1 -}); - -test_type!(NonZeroU128 = { - "type": "integer", - "format": "int128", - "minimum": 1 -}); - -// ### floats - -test_type!(f32 = { - "type": "number", - "format": "float" -}); - -test_type!(f64 = { - "type": "number", - "format": "double" -}); - -// ### string - -test_type!(String = { - "type": "string" -}); - -#[cfg(feature = "uuid")] -test_type!(Uuid = { - "type": "string", - "format": "uuid" -}); - -// ### date/time - -#[cfg(feature = "chrono")] -test_type!(Date, Date, Date, NaiveDate = { - "type": "string", - "format": "date" -}); - -#[cfg(feature = "chrono")] -test_type!(DateTime, DateTime, DateTime, NaiveDateTime = { - "type": "string", - "format": "date-time" -}); - -// ### some std types - -test_type!(Option = { - "type": "string", - "nullable": true -}); - -test_type!(Vec = { - "type": "array", - "items": { - "type": "string" - } -}); - -test_type!(BTreeSet, IndexSet, HashSet = { - "type": "array", - "items": { - "type": "string" - }, - "uniqueItems": true -}); - -test_type!(BTreeMap, IndexMap, HashMap = { - "type": "object", - "properties": { - "default": { - "type": "integer" - } - }, - "required": ["default"], - "additionalProperties": { - "type": "string" - } -}); diff --git a/openapi_type/tests/trybuild.rs b/openapi_type/tests/trybuild.rs deleted file mode 100644 index b76b676..0000000 --- a/openapi_type/tests/trybuild.rs +++ /dev/null @@ -1,7 +0,0 @@ -use trybuild::TestCases; - -#[test] -fn trybuild() { - let t = TestCases::new(); - t.compile_fail("tests/fail/*.rs"); -} diff --git a/openapi_type_derive/Cargo.toml b/openapi_type_derive/Cargo.toml deleted file mode 100644 index ab8e932..0000000 --- a/openapi_type_derive/Cargo.toml +++ /dev/null @@ -1,19 +0,0 @@ -# -*- eval: (cargo-minor-mode 1) -*- - -[package] -workspace = ".." -name = "openapi_type_derive" -version = "0.1.0-dev" -authors = ["Dominic Meiser "] -edition = "2018" -description = "Implementation detail of the openapi_type crate" -license = "Apache-2.0" -repository = "https://gitlab.com/msrd0/gotham-restful/-/tree/master/openapi_type_derive" - -[lib] -proc-macro = true - -[dependencies] -proc-macro2 = "1.0" -quote = "1.0" -syn = "1.0" diff --git a/openapi_type_derive/src/codegen.rs b/openapi_type_derive/src/codegen.rs deleted file mode 100644 index a56c97c..0000000 --- a/openapi_type_derive/src/codegen.rs +++ /dev/null @@ -1,143 +0,0 @@ -use crate::parser::{ParseData, ParseDataType}; -use proc_macro2::TokenStream; -use quote::quote; -use syn::LitStr; - -impl ParseData { - pub(super) fn gen_schema(&self) -> TokenStream { - match self { - Self::Struct(fields) => gen_struct(fields), - Self::Enum(variants) => gen_enum(variants), - Self::Alternatives(alt) => gen_alt(alt), - Self::Unit => gen_unit() - } - } -} - -fn gen_struct(fields: &[(LitStr, ParseDataType)]) -> TokenStream { - let field_name = fields.iter().map(|(name, _)| name); - let field_schema = fields.iter().map(|(_, ty)| match ty { - ParseDataType::Type(ty) => { - quote!(<#ty as ::openapi_type::OpenapiType>::schema()) - }, - ParseDataType::Inline(data) => { - let code = data.gen_schema(); - quote!(::openapi_type::OpenapiSchema::new(#code)) - } - }); - - let openapi = path!(::openapi_type::openapi); - quote! { - { - let mut properties = <::openapi_type::indexmap::IndexMap< - ::std::string::String, - #openapi::ReferenceOr<::std::boxed::Box<#openapi::Schema>> - >>::new(); - let mut required = <::std::vec::Vec<::std::string::String>>::new(); - - #({ - const FIELD_NAME: &::core::primitive::str = #field_name; - let mut field_schema = #field_schema; - ::openapi_type::private::add_dependencies( - &mut dependencies, - &mut field_schema.dependencies - ); - - // fields in OpenAPI are nullable by default - match field_schema.nullable { - true => field_schema.nullable = false, - false => required.push(::std::string::String::from(FIELD_NAME)) - }; - - match field_schema.name.as_ref() { - // include the field schema as reference - ::std::option::Option::Some(schema_name) => { - let mut reference = ::std::string::String::from("#/components/schemas/"); - reference.push_str(schema_name); - properties.insert( - ::std::string::String::from(FIELD_NAME), - #openapi::ReferenceOr::Reference { reference } - ); - dependencies.insert( - ::std::string::String::from(schema_name), - field_schema - ); - }, - // inline the field schema - ::std::option::Option::None => { - properties.insert( - ::std::string::String::from(FIELD_NAME), - #openapi::ReferenceOr::Item( - ::std::boxed::Box::new( - field_schema.into_schema() - ) - ) - ); - } - } - })* - - #openapi::SchemaKind::Type( - #openapi::Type::Object( - #openapi::ObjectType { - properties, - required, - .. ::std::default::Default::default() - } - ) - ) - } - } -} - -fn gen_enum(variants: &[LitStr]) -> TokenStream { - let openapi = path!(::openapi_type::openapi); - quote! { - { - let mut enumeration = <::std::vec::Vec<::std::string::String>>::new(); - #(enumeration.push(::std::string::String::from(#variants));)* - #openapi::SchemaKind::Type( - #openapi::Type::String( - #openapi::StringType { - enumeration, - .. ::std::default::Default::default() - } - ) - ) - } - } -} - -fn gen_alt(alt: &[ParseData]) -> TokenStream { - let openapi = path!(::openapi_type::openapi); - let schema = alt.iter().map(|data| data.gen_schema()); - quote! { - { - let mut alternatives = <::std::vec::Vec< - #openapi::ReferenceOr<#openapi::Schema> - >>::new(); - #(alternatives.push(#openapi::ReferenceOr::Item( - ::openapi_type::OpenapiSchema::new(#schema).into_schema() - ));)* - #openapi::SchemaKind::OneOf { - one_of: alternatives - } - } - } -} - -fn gen_unit() -> TokenStream { - let openapi = path!(::openapi_type::openapi); - quote! { - #openapi::SchemaKind::Type( - #openapi::Type::Object( - #openapi::ObjectType { - additional_properties: ::std::option::Option::Some( - #openapi::AdditionalProperties::Any(false) - ), - .. ::std::default::Default::default() - } - ) - ) - } -} diff --git a/openapi_type_derive/src/lib.rs b/openapi_type_derive/src/lib.rs deleted file mode 100644 index 0a81bec..0000000 --- a/openapi_type_derive/src/lib.rs +++ /dev/null @@ -1,95 +0,0 @@ -#![warn(missing_debug_implementations, rust_2018_idioms)] -#![deny(broken_intra_doc_links)] -#![forbid(unsafe_code)] -//! This crate defines the macros for `#[derive(OpenapiType)]`. - -use proc_macro::TokenStream; -use proc_macro2::TokenStream as TokenStream2; -use quote::quote; -use syn::{parse_macro_input, Data, DeriveInput, LitStr, TraitBound, TraitBoundModifier, TypeParamBound}; - -#[macro_use] -mod util; -//use util::*; - -mod codegen; -mod parser; -use parser::*; - -/// The derive macro for [OpenapiType](https://docs.rs/openapi_type/*/openapi_type/trait.OpenapiType.html). -#[proc_macro_derive(OpenapiType, attributes(openapi))] -pub fn derive_openapi_type(input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input); - expand_openapi_type(input).unwrap_or_else(|err| err.to_compile_error()).into() -} - -fn expand_openapi_type(mut input: DeriveInput) -> syn::Result { - // parse #[serde] and #[openapi] attributes - let mut attrs = ContainerAttributes::default(); - for attr in &input.attrs { - if attr.path.is_ident("serde") { - parse_container_attrs(attr, &mut attrs, false)?; - } - } - for attr in &input.attrs { - if attr.path.is_ident("openapi") { - parse_container_attrs(attr, &mut attrs, true)?; - } - } - - // prepare impl block for codegen - let ident = &input.ident; - let name = ident.to_string(); - let mut name = LitStr::new(&name, ident.span()); - if let Some(rename) = &attrs.rename { - name = rename.clone(); - } - - // prepare the generics - all impl generics will get `OpenapiType` requirement - let (impl_generics, ty_generics, where_clause) = { - let generics = &mut input.generics; - generics.type_params_mut().for_each(|param| { - param.colon_token.get_or_insert_with(Default::default); - param.bounds.push(TypeParamBound::Trait(TraitBound { - paren_token: None, - modifier: TraitBoundModifier::None, - lifetimes: None, - path: path!(::openapi_type::OpenapiType) - })); - }); - generics.split_for_impl() - }; - - // parse the input data - let parsed = match &input.data { - Data::Struct(strukt) => parse_struct(strukt)?, - Data::Enum(inum) => parse_enum(inum, &attrs)?, - Data::Union(union) => parse_union(union)? - }; - - // run the codegen - let schema_code = parsed.gen_schema(); - - // put the code together - Ok(quote! { - #[allow(unused_mut)] - impl #impl_generics ::openapi_type::OpenapiType for #ident #ty_generics #where_clause { - fn schema() -> ::openapi_type::OpenapiSchema { - // prepare the dependencies - let mut dependencies = ::openapi_type::private::Dependencies::new(); - - // create the schema - let schema = #schema_code; - - // return everything - const NAME: &::core::primitive::str = #name; - ::openapi_type::OpenapiSchema { - name: ::std::option::Option::Some(::std::string::String::from(NAME)), - nullable: false, - schema, - dependencies - } - } - } - }) -} diff --git a/openapi_type_derive/src/parser.rs b/openapi_type_derive/src/parser.rs deleted file mode 100644 index 350fee2..0000000 --- a/openapi_type_derive/src/parser.rs +++ /dev/null @@ -1,198 +0,0 @@ -use crate::util::{ExpectLit, ToLitStr}; -use proc_macro2::Span; -use syn::{ - punctuated::Punctuated, spanned::Spanned as _, Attribute, DataEnum, DataStruct, DataUnion, Fields, FieldsNamed, LitStr, - Meta, Token, Type -}; - -pub(super) enum ParseDataType { - Type(Type), - Inline(ParseData) -} - -#[allow(dead_code)] -pub(super) enum ParseData { - Struct(Vec<(LitStr, ParseDataType)>), - Enum(Vec), - Alternatives(Vec), - Unit -} - -fn parse_named_fields(named_fields: &FieldsNamed) -> syn::Result { - let mut fields: Vec<(LitStr, ParseDataType)> = Vec::new(); - for f in &named_fields.named { - let ident = f - .ident - .as_ref() - .ok_or_else(|| syn::Error::new(f.span(), "#[derive(OpenapiType)] does not support fields without an ident"))?; - let name = ident.to_lit_str(); - let ty = f.ty.to_owned(); - fields.push((name, ParseDataType::Type(ty))); - } - Ok(ParseData::Struct(fields)) -} - -pub(super) fn parse_struct(strukt: &DataStruct) -> syn::Result { - match &strukt.fields { - Fields::Named(named_fields) => parse_named_fields(named_fields), - Fields::Unnamed(unnamed_fields) => { - return Err(syn::Error::new( - unnamed_fields.span(), - "#[derive(OpenapiType)] does not support tuple structs" - )) - }, - Fields::Unit => Ok(ParseData::Unit) - } -} - -pub(super) fn parse_enum(inum: &DataEnum, attrs: &ContainerAttributes) -> syn::Result { - let mut strings: Vec = Vec::new(); - let mut types: Vec<(LitStr, ParseData)> = Vec::new(); - - for v in &inum.variants { - let name = v.ident.to_lit_str(); - match &v.fields { - Fields::Named(named_fields) => { - types.push((name, parse_named_fields(named_fields)?)); - }, - Fields::Unnamed(unnamed_fields) => { - return Err(syn::Error::new( - unnamed_fields.span(), - "#[derive(OpenapiType)] does not support tuple variants" - )) - }, - Fields::Unit => strings.push(name) - } - } - - let data_strings = if strings.is_empty() { - None - } else { - match (&attrs.tag, &attrs.content, attrs.untagged) { - // externally tagged (default) - (None, None, false) => Some(ParseData::Enum(strings)), - // internally tagged or adjacently tagged - (Some(tag), _, false) => Some(ParseData::Struct(vec![( - tag.clone(), - ParseDataType::Inline(ParseData::Enum(strings)) - )])), - // untagged - (None, None, true) => Some(ParseData::Unit), - // unknown - _ => return Err(syn::Error::new(Span::call_site(), "Unknown enum representation")) - } - }; - - let data_types = - if types.is_empty() { - None - } else { - Some(ParseData::Alternatives( - types - .into_iter() - .map(|(name, mut data)| { - Ok(match (&attrs.tag, &attrs.content, attrs.untagged) { - // externally tagged (default) - (None, None, false) => ParseData::Struct(vec![(name, ParseDataType::Inline(data))]), - // internally tagged - (Some(tag), None, false) => { - match &mut data { - ParseData::Struct(fields) => { - fields.push((tag.clone(), ParseDataType::Inline(ParseData::Enum(vec![name])))) - }, - _ => return Err(syn::Error::new( - tag.span(), - "#[derive(OpenapiType)] does not support tuple variants on internally tagged enums" - )) - }; - data - }, - // adjacently tagged - (Some(tag), Some(content), false) => ParseData::Struct(vec![ - (tag.clone(), ParseDataType::Inline(ParseData::Enum(vec![name]))), - (content.clone(), ParseDataType::Inline(data)), - ]), - // untagged - (None, None, true) => data, - // unknown - _ => return Err(syn::Error::new(Span::call_site(), "Unknown enum representation")) - }) - }) - .collect::>>()? - )) - }; - - match (data_strings, data_types) { - // only variants without fields - (Some(data), None) => Ok(data), - // only one variant with fields - (None, Some(ParseData::Alternatives(mut alt))) if alt.len() == 1 => Ok(alt.remove(0)), - // only variants with fields - (None, Some(data)) => Ok(data), - // variants with and without fields - (Some(data), Some(ParseData::Alternatives(mut alt))) => { - alt.push(data); - Ok(ParseData::Alternatives(alt)) - }, - // no variants - (None, None) => Err(syn::Error::new( - inum.brace_token.span, - "#[derive(OpenapiType)] does not support enums with no variants" - )), - // data_types always produces Alternatives - _ => unreachable!() - } -} - -pub(super) fn parse_union(union: &DataUnion) -> syn::Result { - Err(syn::Error::new( - union.union_token.span(), - "#[derive(OpenapiType)] cannot be used on unions" - )) -} - -#[derive(Default)] -pub(super) struct ContainerAttributes { - pub(super) rename: Option, - pub(super) rename_all: Option, - pub(super) tag: Option, - pub(super) content: Option, - pub(super) untagged: bool -} - -pub(super) fn parse_container_attrs( - input: &Attribute, - attrs: &mut ContainerAttributes, - error_on_unknown: bool -) -> syn::Result<()> { - let tokens: Punctuated = input.parse_args_with(Punctuated::parse_terminated)?; - for token in tokens { - match token { - Meta::NameValue(kv) if kv.path.is_ident("rename") => { - attrs.rename = Some(kv.lit.expect_str()?); - }, - - Meta::NameValue(kv) if kv.path.is_ident("rename_all") => { - attrs.rename_all = Some(kv.lit.expect_str()?); - }, - - Meta::NameValue(kv) if kv.path.is_ident("tag") => { - attrs.tag = Some(kv.lit.expect_str()?); - }, - - Meta::NameValue(kv) if kv.path.is_ident("content") => { - attrs.content = Some(kv.lit.expect_str()?); - }, - - Meta::Path(path) if path.is_ident("untagged") => { - attrs.untagged = true; - }, - - Meta::Path(path) if error_on_unknown => return Err(syn::Error::new(path.span(), "Unexpected token")), - Meta::List(list) if error_on_unknown => return Err(syn::Error::new(list.span(), "Unexpected token")), - Meta::NameValue(kv) if error_on_unknown => return Err(syn::Error::new(kv.path.span(), "Unexpected token")), - _ => {} - } - } - Ok(()) -} diff --git a/openapi_type_derive/src/util.rs b/openapi_type_derive/src/util.rs deleted file mode 100644 index 2a752e0..0000000 --- a/openapi_type_derive/src/util.rs +++ /dev/null @@ -1,52 +0,0 @@ -use proc_macro2::Ident; -use syn::{Lit, LitStr}; - -/// Convert any literal path into a [syn::Path]. -macro_rules! path { - (:: $($segment:ident)::*) => { - path!(@private Some(Default::default()), $($segment),*) - }; - ($($segment:ident)::*) => { - path!(@private None, $($segment),*) - }; - (@private $leading_colon:expr, $($segment:ident),*) => { - { - #[allow(unused_mut)] - let mut segments: ::syn::punctuated::Punctuated<::syn::PathSegment, _> = Default::default(); - $( - segments.push(::syn::PathSegment { - ident: ::proc_macro2::Ident::new(stringify!($segment), ::proc_macro2::Span::call_site()), - arguments: Default::default() - }); - )* - ::syn::Path { - leading_colon: $leading_colon, - segments - } - } - }; -} - -/// Convert any [Ident] into a [LitStr]. Basically `stringify!`. -pub(super) trait ToLitStr { - fn to_lit_str(&self) -> LitStr; -} -impl ToLitStr for Ident { - fn to_lit_str(&self) -> LitStr { - LitStr::new(&self.to_string(), self.span()) - } -} - -/// Convert a [Lit] to one specific literal type. -pub(crate) trait ExpectLit { - fn expect_str(self) -> syn::Result; -} - -impl ExpectLit for Lit { - fn expect_str(self) -> syn::Result { - match self { - Self::Str(str) => Ok(str), - _ => Err(syn::Error::new(self.span(), "Expected string literal")) - } - } -} diff --git a/src/endpoint.rs b/src/endpoint.rs index 2095948..d8da412 100644 --- a/src/endpoint.rs +++ b/src/endpoint.rs @@ -2,41 +2,11 @@ use crate::{IntoResponse, RequestBody}; use futures_util::future::BoxFuture; use gotham::{ extractor::{PathExtractor, QueryStringExtractor}, - hyper::{Body, Method, Response}, - router::response::extender::StaticResponseExtender, - state::{State, StateData} + hyper::{Body, Method}, + state::State }; -#[cfg(feature = "openapi")] -use openapi_type::{OpenapiSchema, OpenapiType}; -use serde::{Deserialize, Deserializer}; use std::borrow::Cow; -/// A no-op extractor that can be used as a default type for [Endpoint::Placeholders] and -/// [Endpoint::Params]. -#[derive(Debug, Clone, Copy)] -pub struct NoopExtractor; - -impl<'de> Deserialize<'de> for NoopExtractor { - fn deserialize>(_: D) -> Result { - Ok(Self) - } -} - -#[cfg(feature = "openapi")] -impl OpenapiType for NoopExtractor { - fn schema() -> OpenapiSchema { - warn!("You're asking for the OpenAPI Schema for gotham_restful::NoopExtractor. This is probably not what you want."); - <() as OpenapiType>::schema() - } -} - -impl StateData for NoopExtractor {} - -impl StaticResponseExtender for NoopExtractor { - type ResBody = Body; - fn extend(_: &mut State, _: &mut Response) {} -} - // TODO: Specify default types once https://github.com/rust-lang/rust/issues/29661 lands. #[_private_openapi_trait(EndpointWithSchema)] pub trait Endpoint { @@ -53,19 +23,19 @@ pub trait Endpoint { fn has_placeholders() -> bool { false } - /// The type that parses the URI placeholders. Use [NoopExtractor] if `has_placeholders()` - /// returns `false`. - #[openapi_bound("Placeholders: OpenapiType")] - type Placeholders: PathExtractor + Clone + Sync; + /// The type that parses the URI placeholders. Use [gotham::extractor::NoopPathExtractor] + /// if `has_placeholders()` returns `false`. + #[openapi_bound("Placeholders: crate::OpenapiType")] + type Placeholders: PathExtractor + Sync; /// 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 [NoopExtractor] if `needs_params()` - /// returns `false`. - #[openapi_bound("Params: OpenapiType")] - type Params: QueryStringExtractor + Clone + Sync; + /// The type that parses the request parameters. Use [gotham::extractor::NoopQueryStringExtractor] + /// if `needs_params()` returns `false`. + #[openapi_bound("Params: crate::OpenapiType")] + type Params: QueryStringExtractor + Sync; /// Returns `true` _iff_ the request body should be parsed. `false` by default. fn needs_body() -> bool { diff --git a/src/lib.rs b/src/lib.rs index aea56a4..36674c3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -60,7 +60,7 @@ struct FooResource; /// The return type of the foo read endpoint. #[derive(Serialize)] -# #[cfg_attr(feature = "openapi", derive(openapi_type::OpenapiType))] +# #[cfg_attr(feature = "openapi", derive(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(Clone, Deserialize, StateData, StaticResponseExtender)] -# #[cfg_attr(feature = "openapi", derive(openapi_type::OpenapiType))] +#[derive(Deserialize, StateData, StaticResponseExtender)] +# #[cfg_attr(feature = "openapi", derive(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(openapi_type::OpenapiType))] +# #[cfg_attr(feature = "openapi", derive(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(openapi_type::OpenapiType))] +# #[cfg_attr(feature = "openapi", derive(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`[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: +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: ```rust,no_run # #[macro_use] extern crate gotham_restful_derive; @@ -373,7 +373,6 @@ should be no need to implement it manually. A simple 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)] @@ -411,17 +410,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`][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: +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: ```rust # #[macro_use] extern crate gotham_restful; # use serde::{Deserialize, Serialize}; #[derive(Deserialize, Serialize)] -#[cfg_attr(feature = "openapi", derive(openapi_type::OpenapiType))] +#[cfg_attr(feature = "openapi", derive(OpenapiType))] struct Foo; ``` @@ -479,8 +478,6 @@ 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; } @@ -497,12 +494,16 @@ pub use cors::{handle_cors, CorsConfig, CorsRoute}; #[cfg(feature = "openapi")] mod openapi; #[cfg(feature = "openapi")] -pub use openapi::{builder::OpenapiInfo, router::GetOpenapi}; +pub use openapi::{ + builder::OpenapiInfo, + router::GetOpenapi, + types::{OpenapiSchema, OpenapiType} +}; mod endpoint; +pub use endpoint::Endpoint; #[cfg(feature = "openapi")] pub use endpoint::EndpointWithSchema; -pub use endpoint::{Endpoint, NoopExtractor}; mod response; pub use response::{ diff --git a/src/openapi/builder.rs b/src/openapi/builder.rs index 4fa6a0d..11f79f8 100644 --- a/src/openapi/builder.rs +++ b/src/openapi/builder.rs @@ -1,5 +1,5 @@ +use crate::OpenapiSchema; use indexmap::IndexMap; -use openapi_type::OpenapiSchema; use openapiv3::{ Components, OpenAPI, PathItem, ReferenceOr, ReferenceOr::{Item, Reference}, @@ -104,7 +104,7 @@ impl OpenapiBuilder { #[allow(dead_code)] mod test { use super::*; - use openapi_type::OpenapiType; + use crate::OpenapiType; #[derive(OpenapiType)] struct Message { diff --git a/src/openapi/mod.rs b/src/openapi/mod.rs index 5eefc1f..500d190 100644 --- a/src/openapi/mod.rs +++ b/src/openapi/mod.rs @@ -4,3 +4,4 @@ pub mod builder; pub mod handler; pub mod operation; pub mod router; +pub mod types; diff --git a/src/openapi/operation.rs b/src/openapi/operation.rs index 1823b3c..62d06d5 100644 --- a/src/openapi/operation.rs +++ b/src/openapi/operation.rs @@ -1,8 +1,7 @@ use super::SECURITY_NAME; -use crate::{response::OrAllTypes, EndpointWithSchema, IntoResponse, RequestBody, ResponseSchema}; +use crate::{response::OrAllTypes, EndpointWithSchema, IntoResponse, OpenapiSchema, RequestBody, ResponseSchema}; use indexmap::IndexMap; use mime::Mime; -use openapi_type::OpenapiSchema; use openapiv3::{ MediaType, Operation, Parameter, ParameterData, ParameterSchemaOrContent, ReferenceOr, ReferenceOr::Item, RequestBody as OARequestBody, Response, Responses, Schema, SchemaKind, StatusCode, Type diff --git a/src/openapi/router.rs b/src/openapi/router.rs index e6b3187..3ced31b 100644 --- a/src/openapi/router.rs +++ b/src/openapi/router.rs @@ -3,10 +3,9 @@ use super::{ handler::{OpenapiHandler, SwaggerUiHandler}, operation::OperationDescription }; -use crate::{routing::*, EndpointWithSchema, ResourceWithSchema, ResponseSchema}; +use crate::{routing::*, EndpointWithSchema, OpenapiType, ResourceWithSchema, ResponseSchema}; use gotham::{hyper::Method, pipeline::chain::PipelineHandleChain, router::builder::*}; use once_cell::sync::Lazy; -use openapi_type::OpenapiType; use regex::{Captures, Regex}; use std::panic::RefUnwindSafe; diff --git a/src/openapi/types.rs b/src/openapi/types.rs new file mode 100644 index 0000000..18f5be6 --- /dev/null +++ b/src/openapi/types.rs @@ -0,0 +1,477 @@ +#[cfg(feature = "chrono")] +use chrono::{Date, DateTime, FixedOffset, Local, NaiveDate, NaiveDateTime, Utc}; +use gotham::extractor::{NoopPathExtractor, NoopQueryStringExtractor}; +use indexmap::IndexMap; +use openapiv3::{ + AdditionalProperties, ArrayType, IntegerType, NumberFormat, NumberType, ObjectType, + ReferenceOr::{Item, Reference}, + Schema, SchemaData, SchemaKind, StringType, Type, VariantOrUnknownOrEmpty +}; + +use std::{ + collections::{BTreeSet, HashMap, HashSet}, + hash::BuildHasher, + num::{NonZeroU128, NonZeroU16, NonZeroU32, NonZeroU64, NonZeroU8, NonZeroUsize} +}; +#[cfg(feature = "uuid")] +use uuid::Uuid; + +/** +This struct needs to be available for every type that can be part of an OpenAPI Spec. It is +already implemented for primitive types, String, Vec, Option and the like. To have it available +for your type, simply derive from [OpenapiType]. +*/ +#[derive(Debug, Clone, PartialEq)] +pub struct OpenapiSchema { + /// The name of this schema. If it is None, the schema will be inlined. + pub name: Option, + /// Whether this particular schema is nullable. Note that there is no guarantee that this will + /// make it into the final specification, it might just be interpreted as a hint to make it + /// an optional parameter. + pub nullable: bool, + /// The actual OpenAPI schema. + pub schema: SchemaKind, + /// Other schemas that this schema depends on. They will be included in the final OpenAPI Spec + /// along with this schema. + pub dependencies: IndexMap +} + +impl OpenapiSchema { + /// Create a new schema that has no name. + pub fn new(schema: SchemaKind) -> Self { + Self { + name: None, + nullable: false, + schema, + dependencies: IndexMap::new() + } + } + + /// Convert this schema to an [openapiv3::Schema] that can be serialized to the OpenAPI Spec. + pub fn into_schema(self) -> Schema { + Schema { + schema_data: SchemaData { + nullable: self.nullable, + title: self.name, + ..Default::default() + }, + schema_kind: self.schema + } + } +} + +/** +This trait needs to be implemented by every type that is being used in the OpenAPI Spec. It gives +access to the [OpenapiSchema] of this type. It is provided for primitive types, String and the +like. For use on your own types, there is a derive macro: + +``` +# #[macro_use] extern crate gotham_restful_derive; +# +#[derive(OpenapiType)] +struct MyResponse { + message: String +} +``` +*/ +pub trait OpenapiType { + fn schema() -> OpenapiSchema; +} + +impl OpenapiType for () { + fn schema() -> OpenapiSchema { + OpenapiSchema::new(SchemaKind::Type(Type::Object(ObjectType { + additional_properties: Some(AdditionalProperties::Any(false)), + ..Default::default() + }))) + } +} + +impl OpenapiType for NoopPathExtractor { + fn schema() -> OpenapiSchema { + warn!("You're asking for the OpenAPI Schema for gotham::extractor::NoopPathExtractor. This is probably not what you want."); + <()>::schema() + } +} + +impl OpenapiType for NoopQueryStringExtractor { + fn schema() -> OpenapiSchema { + warn!("You're asking for the OpenAPI Schema for gotham::extractor::NoopQueryStringExtractor. This is probably not what you want."); + <()>::schema() + } +} + +impl OpenapiType for bool { + fn schema() -> OpenapiSchema { + OpenapiSchema::new(SchemaKind::Type(Type::Boolean {})) + } +} + +macro_rules! int_types { + ($($int_ty:ty),*) => {$( + impl OpenapiType for $int_ty + { + fn schema() -> OpenapiSchema + { + OpenapiSchema::new(SchemaKind::Type(Type::Integer(IntegerType::default()))) + } + } + )*}; + + (unsigned $($int_ty:ty),*) => {$( + impl OpenapiType for $int_ty + { + fn schema() -> OpenapiSchema + { + OpenapiSchema::new(SchemaKind::Type(Type::Integer(IntegerType { + minimum: Some(0), + ..Default::default() + }))) + } + } + )*}; + + (gtzero $($int_ty:ty),*) => {$( + impl OpenapiType for $int_ty + { + fn schema() -> OpenapiSchema + { + OpenapiSchema::new(SchemaKind::Type(Type::Integer(IntegerType { + minimum: Some(1), + ..Default::default() + }))) + } + } + )*}; + + (bits = $bits:expr, $($int_ty:ty),*) => {$( + impl OpenapiType for $int_ty + { + fn schema() -> OpenapiSchema + { + OpenapiSchema::new(SchemaKind::Type(Type::Integer(IntegerType { + format: VariantOrUnknownOrEmpty::Unknown(format!("int{}", $bits)), + ..Default::default() + }))) + } + } + )*}; + + (unsigned bits = $bits:expr, $($int_ty:ty),*) => {$( + impl OpenapiType for $int_ty + { + fn schema() -> OpenapiSchema + { + OpenapiSchema::new(SchemaKind::Type(Type::Integer(IntegerType { + format: VariantOrUnknownOrEmpty::Unknown(format!("int{}", $bits)), + minimum: Some(0), + ..Default::default() + }))) + } + } + )*}; + + (gtzero bits = $bits:expr, $($int_ty:ty),*) => {$( + impl OpenapiType for $int_ty + { + fn schema() -> OpenapiSchema + { + OpenapiSchema::new(SchemaKind::Type(Type::Integer(IntegerType { + format: VariantOrUnknownOrEmpty::Unknown(format!("int{}", $bits)), + minimum: Some(1), + ..Default::default() + }))) + } + } + )*}; +} + +int_types!(isize); +int_types!(unsigned usize); +int_types!(gtzero NonZeroUsize); +int_types!(bits = 8, i8); +int_types!(unsigned bits = 8, u8); +int_types!(gtzero bits = 8, NonZeroU8); +int_types!(bits = 16, i16); +int_types!(unsigned bits = 16, u16); +int_types!(gtzero bits = 16, NonZeroU16); +int_types!(bits = 32, i32); +int_types!(unsigned bits = 32, u32); +int_types!(gtzero bits = 32, NonZeroU32); +int_types!(bits = 64, i64); +int_types!(unsigned bits = 64, u64); +int_types!(gtzero bits = 64, NonZeroU64); +int_types!(bits = 128, i128); +int_types!(unsigned bits = 128, u128); +int_types!(gtzero bits = 128, NonZeroU128); + +macro_rules! num_types { + ($($num_ty:ty = $num_fmt:ident),*) => {$( + impl OpenapiType for $num_ty + { + fn schema() -> OpenapiSchema + { + OpenapiSchema::new(SchemaKind::Type(Type::Number(NumberType { + format: VariantOrUnknownOrEmpty::Item(NumberFormat::$num_fmt), + ..Default::default() + }))) + } + } + )*} +} + +num_types!(f32 = Float, f64 = Double); + +macro_rules! str_types { + ($($str_ty:ty),*) => {$( + impl OpenapiType for $str_ty + { + fn schema() -> OpenapiSchema + { + OpenapiSchema::new(SchemaKind::Type(Type::String(StringType::default()))) + } + } + )*}; + + (format = $format:ident, $($str_ty:ty),*) => {$( + impl OpenapiType for $str_ty + { + fn schema() -> OpenapiSchema + { + use openapiv3::StringFormat; + + OpenapiSchema::new(SchemaKind::Type(Type::String(StringType { + format: VariantOrUnknownOrEmpty::Item(StringFormat::$format), + ..Default::default() + }))) + } + } + )*}; + + (format_str = $format:expr, $($str_ty:ty),*) => {$( + impl OpenapiType for $str_ty + { + fn schema() -> OpenapiSchema + { + OpenapiSchema::new(SchemaKind::Type(Type::String(StringType { + format: VariantOrUnknownOrEmpty::Unknown($format.to_string()), + ..Default::default() + }))) + } + } + )*}; +} + +str_types!(String, &str); + +#[cfg(feature = "chrono")] +str_types!(format = Date, Date, Date, Date, NaiveDate); +#[cfg(feature = "chrono")] +str_types!( + format = DateTime, + DateTime, + DateTime, + DateTime, + NaiveDateTime +); + +#[cfg(feature = "uuid")] +str_types!(format_str = "uuid", Uuid); + +impl OpenapiType for Option { + fn schema() -> OpenapiSchema { + let schema = T::schema(); + let mut dependencies = schema.dependencies.clone(); + let schema = match schema.name.clone() { + Some(name) => { + let reference = Reference { + reference: format!("#/components/schemas/{}", name) + }; + dependencies.insert(name, schema); + SchemaKind::AllOf { all_of: vec![reference] } + }, + None => schema.schema + }; + + OpenapiSchema { + nullable: true, + name: None, + schema, + dependencies + } + } +} + +impl OpenapiType for Vec { + fn schema() -> OpenapiSchema { + let schema = T::schema(); + let mut dependencies = schema.dependencies.clone(); + + let items = match schema.name.clone() { + Some(name) => { + let reference = Reference { + reference: format!("#/components/schemas/{}", name) + }; + dependencies.insert(name, schema); + reference + }, + None => Item(Box::new(schema.into_schema())) + }; + + OpenapiSchema { + nullable: false, + name: None, + schema: SchemaKind::Type(Type::Array(ArrayType { + items, + min_items: None, + max_items: None, + unique_items: false + })), + dependencies + } + } +} + +impl OpenapiType for BTreeSet { + fn schema() -> OpenapiSchema { + as OpenapiType>::schema() + } +} + +impl OpenapiType for HashSet { + fn schema() -> OpenapiSchema { + as OpenapiType>::schema() + } +} + +impl OpenapiType for HashMap { + fn schema() -> OpenapiSchema { + let key_schema = K::schema(); + let mut dependencies = key_schema.dependencies.clone(); + + let keys = match key_schema.name.clone() { + Some(name) => { + let reference = Reference { + reference: format!("#/components/schemas/{}", name) + }; + dependencies.insert(name, key_schema); + reference + }, + None => Item(Box::new(key_schema.into_schema())) + }; + + let schema = T::schema(); + dependencies.extend(schema.dependencies.iter().map(|(k, v)| (k.clone(), v.clone()))); + + let items = Box::new(match schema.name.clone() { + Some(name) => { + let reference = Reference { + reference: format!("#/components/schemas/{}", name) + }; + dependencies.insert(name, schema); + reference + }, + None => Item(schema.into_schema()) + }); + + let mut properties = IndexMap::new(); + properties.insert("default".to_owned(), keys); + + OpenapiSchema { + nullable: false, + name: None, + schema: SchemaKind::Type(Type::Object(ObjectType { + properties, + required: vec!["default".to_owned()], + additional_properties: Some(AdditionalProperties::Schema(items)), + ..Default::default() + })), + dependencies + } + } +} + +impl OpenapiType for serde_json::Value { + fn schema() -> OpenapiSchema { + OpenapiSchema { + nullable: true, + name: None, + schema: SchemaKind::Any(Default::default()), + dependencies: Default::default() + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use serde_json::Value; + + type Unit = (); + + macro_rules! assert_schema { + ($ty:ident $(<$($generic:ident),+>)* => $json:expr) => { + paste::item! { + #[test] + fn []() + { + let schema = <$ty $(<$($generic),+>)* as OpenapiType>::schema().into_schema(); + let schema_json = serde_json::to_string(&schema).expect(&format!("Unable to serialize schema for {}", stringify!($ty))); + assert_eq!(schema_json, $json); + } + } + }; + } + + assert_schema!(Unit => r#"{"type":"object","additionalProperties":false}"#); + assert_schema!(bool => r#"{"type":"boolean"}"#); + + assert_schema!(isize => r#"{"type":"integer"}"#); + assert_schema!(usize => r#"{"type":"integer","minimum":0}"#); + assert_schema!(i8 => r#"{"type":"integer","format":"int8"}"#); + assert_schema!(u8 => r#"{"type":"integer","format":"int8","minimum":0}"#); + assert_schema!(i16 => r#"{"type":"integer","format":"int16"}"#); + assert_schema!(u16 => r#"{"type":"integer","format":"int16","minimum":0}"#); + assert_schema!(i32 => r#"{"type":"integer","format":"int32"}"#); + assert_schema!(u32 => r#"{"type":"integer","format":"int32","minimum":0}"#); + assert_schema!(i64 => r#"{"type":"integer","format":"int64"}"#); + assert_schema!(u64 => r#"{"type":"integer","format":"int64","minimum":0}"#); + assert_schema!(i128 => r#"{"type":"integer","format":"int128"}"#); + assert_schema!(u128 => r#"{"type":"integer","format":"int128","minimum":0}"#); + + assert_schema!(NonZeroUsize => r#"{"type":"integer","minimum":1}"#); + assert_schema!(NonZeroU8 => r#"{"type":"integer","format":"int8","minimum":1}"#); + assert_schema!(NonZeroU16 => r#"{"type":"integer","format":"int16","minimum":1}"#); + assert_schema!(NonZeroU32 => r#"{"type":"integer","format":"int32","minimum":1}"#); + assert_schema!(NonZeroU64 => r#"{"type":"integer","format":"int64","minimum":1}"#); + assert_schema!(NonZeroU128 => r#"{"type":"integer","format":"int128","minimum":1}"#); + + assert_schema!(f32 => r#"{"type":"number","format":"float"}"#); + assert_schema!(f64 => r#"{"type":"number","format":"double"}"#); + + assert_schema!(String => r#"{"type":"string"}"#); + + #[cfg(feature = "uuid")] + assert_schema!(Uuid => r#"{"type":"string","format":"uuid"}"#); + + #[cfg(feature = "chrono")] + mod chrono { + use super::*; + + assert_schema!(Date => r#"{"type":"string","format":"date"}"#); + assert_schema!(Date => r#"{"type":"string","format":"date"}"#); + assert_schema!(Date => r#"{"type":"string","format":"date"}"#); + assert_schema!(NaiveDate => r#"{"type":"string","format":"date"}"#); + assert_schema!(DateTime => r#"{"type":"string","format":"date-time"}"#); + assert_schema!(DateTime => r#"{"type":"string","format":"date-time"}"#); + assert_schema!(DateTime => r#"{"type":"string","format":"date-time"}"#); + assert_schema!(NaiveDateTime => r#"{"type":"string","format":"date-time"}"#); + } + + assert_schema!(Option => r#"{"nullable":true,"type":"string"}"#); + assert_schema!(Vec => r#"{"type":"array","items":{"type":"string"}}"#); + assert_schema!(BTreeSet => r#"{"type":"array","items":{"type":"string"}}"#); + assert_schema!(HashSet => r#"{"type":"array","items":{"type":"string"}}"#); + assert_schema!(HashMap => r#"{"type":"object","properties":{"default":{"type":"integer","format":"int64"}},"required":["default"],"additionalProperties":{"type":"string"}}"#); + assert_schema!(Value => r#"{"nullable":true}"#); +} diff --git a/src/response/mod.rs b/src/response/mod.rs index bdf7c66..b2796dc 100644 --- a/src/response/mod.rs +++ b/src/response/mod.rs @@ -1,3 +1,6 @@ +#[cfg(feature = "openapi")] +use crate::OpenapiSchema; + use futures_util::future::{self, BoxFuture, FutureExt}; use gotham::{ handler::HandlerError, @@ -7,8 +10,6 @@ use gotham::{ } }; use mime::{Mime, APPLICATION_JSON, STAR_STAR}; -#[cfg(feature = "openapi")] -use openapi_type::OpenapiSchema; use serde::Serialize; use std::{ convert::Infallible, @@ -258,7 +259,7 @@ mod test { use thiserror::Error; #[derive(Debug, Default, Deserialize, Serialize)] - #[cfg_attr(feature = "openapi", derive(openapi_type::OpenapiType))] + #[cfg_attr(feature = "openapi", derive(crate::OpenapiType))] struct Msg { msg: String } diff --git a/src/response/no_content.rs b/src/response/no_content.rs index 3a10b3b..73159c1 100644 --- a/src/response/no_content.rs +++ b/src/response/no_content.rs @@ -1,14 +1,12 @@ use super::{handle_error, IntoResponse}; -#[cfg(feature = "openapi")] -use crate::ResponseSchema; use crate::{IntoResponseError, Response}; +#[cfg(feature = "openapi")] +use crate::{OpenapiSchema, OpenapiType, ResponseSchema}; use futures_util::{future, future::FutureExt}; use gotham::hyper::header::{HeaderMap, HeaderValue, IntoHeaderName}; #[cfg(feature = "openapi")] use gotham::hyper::StatusCode; use mime::Mime; -#[cfg(feature = "openapi")] -use openapi_type::{OpenapiSchema, OpenapiType}; use std::{fmt::Display, future::Future, pin::Pin}; /** diff --git a/src/response/raw.rs b/src/response/raw.rs index 3722146..6c003dc 100644 --- a/src/response/raw.rs +++ b/src/response/raw.rs @@ -1,9 +1,7 @@ use super::{handle_error, IntoResponse, IntoResponseError}; use crate::{FromBody, RequestBody, ResourceType, Response}; #[cfg(feature = "openapi")] -use crate::{IntoResponseWithSchema, ResponseSchema}; -#[cfg(feature = "openapi")] -use openapi_type::{OpenapiSchema, OpenapiType}; +use crate::{IntoResponseWithSchema, OpenapiSchema, OpenapiType, ResponseSchema}; use futures_core::future::Future; use futures_util::{future, future::FutureExt}; diff --git a/src/response/redirect.rs b/src/response/redirect.rs index f1edd82..8b6e854 100644 --- a/src/response/redirect.rs +++ b/src/response/redirect.rs @@ -1,14 +1,12 @@ use super::{handle_error, IntoResponse}; use crate::{IntoResponseError, Response}; #[cfg(feature = "openapi")] -use crate::{NoContent, ResponseSchema}; +use crate::{NoContent, OpenapiSchema, ResponseSchema}; use futures_util::future::{BoxFuture, FutureExt, TryFutureExt}; use gotham::hyper::{ header::{InvalidHeaderValue, LOCATION}, Body, StatusCode }; -#[cfg(feature = "openapi")] -use openapi_type::OpenapiSchema; use std::{ error::Error as StdError, fmt::{Debug, Display} diff --git a/src/response/result.rs b/src/response/result.rs index f0ddc91..a28803f 100644 --- a/src/response/result.rs +++ b/src/response/result.rs @@ -1,9 +1,7 @@ use super::{handle_error, IntoResponse, ResourceError}; #[cfg(feature = "openapi")] -use crate::ResponseSchema; +use crate::{OpenapiSchema, ResponseSchema}; use crate::{Response, ResponseBody, Success}; -#[cfg(feature = "openapi")] -use openapi_type::OpenapiSchema; use futures_core::future::Future; use gotham::hyper::StatusCode; @@ -66,7 +64,7 @@ mod test { use thiserror::Error; #[derive(Debug, Default, Deserialize, Serialize)] - #[cfg_attr(feature = "openapi", derive(openapi_type::OpenapiType))] + #[cfg_attr(feature = "openapi", derive(crate::OpenapiType))] struct Msg { msg: String } diff --git a/src/response/success.rs b/src/response/success.rs index 31f9374..24d6b3c 100644 --- a/src/response/success.rs +++ b/src/response/success.rs @@ -1,6 +1,6 @@ use super::IntoResponse; #[cfg(feature = "openapi")] -use crate::ResponseSchema; +use crate::{OpenapiSchema, ResponseSchema}; use crate::{Response, ResponseBody}; use futures_util::future::{self, FutureExt}; use gotham::hyper::{ @@ -8,8 +8,6 @@ use gotham::hyper::{ StatusCode }; use mime::{Mime, APPLICATION_JSON}; -#[cfg(feature = "openapi")] -use openapi_type::OpenapiSchema; use std::{fmt::Debug, future::Future, pin::Pin}; /** @@ -29,7 +27,7 @@ Usage example: # struct MyResource; # #[derive(Deserialize, Serialize)] -# #[cfg_attr(feature = "openapi", derive(openapi_type::OpenapiType))] +# #[cfg_attr(feature = "openapi", derive(OpenapiType))] struct MyResponse { message: &'static str } @@ -98,7 +96,7 @@ mod test { use gotham::hyper::header::ACCESS_CONTROL_ALLOW_ORIGIN; #[derive(Debug, Default, Serialize)] - #[cfg_attr(feature = "openapi", derive(openapi_type::OpenapiType))] + #[cfg_attr(feature = "openapi", derive(crate::OpenapiType))] struct Msg { msg: String } diff --git a/src/routing.rs b/src/routing.rs index c7cd5a6..f41dc93 100644 --- a/src/routing.rs +++ b/src/routing.rs @@ -4,6 +4,7 @@ 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::{ @@ -19,12 +20,10 @@ use gotham::{ state::{FromState, State} }; use mime::{Mime, APPLICATION_JSON}; -#[cfg(feature = "openapi")] -use openapi_type::OpenapiType; -use std::{any::TypeId, panic::RefUnwindSafe}; +use std::panic::RefUnwindSafe; /// Allow us to extract an id from a path. -#[derive(Clone, Copy, Debug, Deserialize, StateData, StaticResponseExtender)] +#[derive(Debug, Deserialize, StateData, StaticResponseExtender)] #[cfg_attr(feature = "openapi", derive(OpenapiType))] pub struct PathExtractor { pub id: ID @@ -92,11 +91,6 @@ where { trace!("entering endpoint_handler"); let placeholders = E::Placeholders::take_from(state); - // workaround for E::Placeholders and E::Param being the same type - // when fixed remove `Clone` requirement on endpoint - if TypeId::of::() == TypeId::of::() { - state.put(placeholders.clone()); - } let params = E::Params::take_from(state); let body = match E::needs_body() { diff --git a/src/types.rs b/src/types.rs index 20be58d..ca08bec 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,7 +1,8 @@ +#[cfg(feature = "openapi")] +use crate::OpenapiType; + use gotham::hyper::body::Bytes; use mime::{Mime, APPLICATION_JSON}; -#[cfg(feature = "openapi")] -use openapi_type::OpenapiType; use serde::{de::DeserializeOwned, Serialize}; use std::error::Error; diff --git a/tests/async_methods.rs b/tests/async_methods.rs index 9a1669b..35ec42a 100644 --- a/tests/async_methods.rs +++ b/tests/async_methods.rs @@ -9,10 +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}; +use tokio::time::{delay_for, Duration}; mod util { include!("util/mod.rs"); @@ -30,7 +28,7 @@ struct FooBody { data: String } -#[derive(Clone, Deserialize, StateData, StaticResponseExtender)] +#[derive(Deserialize, StateData, StaticResponseExtender)] #[cfg_attr(feature = "openapi", derive(OpenapiType))] #[allow(dead_code)] struct FooSearch { @@ -88,9 +86,9 @@ async fn remove(_id: u64) -> Raw<&'static [u8]> { const STATE_TEST_RESPONSE: &[u8] = b"xxJbxOuwioqR5DfzPuVqvaqRSfpdNQGluIvHU4n1LM"; #[endpoint(method = "Method::GET", uri = "state_test")] async fn state_test(state: &mut State) -> Raw<&'static [u8]> { - sleep(Duration::from_nanos(1)).await; + delay_for(Duration::from_nanos(1)).await; state.borrow::(); - sleep(Duration::from_nanos(1)).await; + delay_for(Duration::from_nanos(1)).await; Raw::new(STATE_TEST_RESPONSE, TEXT_PLAIN) } diff --git a/tests/sync_methods.rs b/tests/sync_methods.rs index 2b440fa..4e07259 100644 --- a/tests/sync_methods.rs +++ b/tests/sync_methods.rs @@ -4,8 +4,6 @@ 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 { @@ -24,7 +22,7 @@ struct FooBody { data: String } -#[derive(Clone, Deserialize, StateData, StaticResponseExtender)] +#[derive(Deserialize, StateData, StaticResponseExtender)] #[cfg_attr(feature = "openapi", derive(OpenapiType))] #[allow(dead_code)] struct FooSearch { diff --git a/tests/trybuild_ui.rs b/tests/trybuild_ui.rs index 406ae6a..2317215 100644 --- a/tests/trybuild_ui.rs +++ b/tests/trybuild_ui.rs @@ -4,7 +4,14 @@ use trybuild::TestCases; #[ignore] fn trybuild_ui() { let t = TestCases::new(); + + // always enabled t.compile_fail("tests/ui/endpoint/*.rs"); t.compile_fail("tests/ui/from_body/*.rs"); t.compile_fail("tests/ui/resource/*.rs"); + + // require the openapi feature + if cfg!(feature = "openapi") { + t.compile_fail("tests/ui/openapi_type/*.rs"); + } } diff --git a/tests/ui/endpoint/invalid_params_ty.stderr b/tests/ui/endpoint/invalid_params_ty.stderr index de1597a..35ed700 100644 --- a/tests/ui/endpoint/invalid_params_ty.stderr +++ b/tests/ui/endpoint/invalid_params_ty.stderr @@ -1,14 +1,3 @@ -error[E0277]: the trait bound `FooParams: OpenapiType` is not satisfied - --> $DIR/invalid_params_ty.rs:15:16 - | -15 | fn endpoint(_: FooParams) { - | ^^^^^^^^^ the trait `OpenapiType` is not implemented for `FooParams` - | - ::: $WORKSPACE/src/endpoint.rs - | - | #[openapi_bound("Params: OpenapiType")] - | --------------------- required by this bound in `gotham_restful::EndpointWithSchema::Params` - error[E0277]: the trait bound `for<'de> FooParams: serde::de::Deserialize<'de>` is not satisfied --> $DIR/invalid_params_ty.rs:15:16 | @@ -17,7 +6,7 @@ error[E0277]: the trait bound `for<'de> FooParams: serde::de::Deserialize<'de>` | ::: $WORKSPACE/src/endpoint.rs | - | type Params: QueryStringExtractor + Clone + Sync; + | type Params: QueryStringExtractor + Sync; | -------------------------- required by this bound in `gotham_restful::EndpointWithSchema::Params` error[E0277]: the trait bound `FooParams: StateData` is not satisfied @@ -28,7 +17,7 @@ error[E0277]: the trait bound `FooParams: StateData` is not satisfied | ::: $WORKSPACE/src/endpoint.rs | - | type Params: QueryStringExtractor + Clone + Sync; + | type Params: QueryStringExtractor + Sync; | -------------------------- required by this bound in `gotham_restful::EndpointWithSchema::Params` error[E0277]: the trait bound `FooParams: StaticResponseExtender` is not satisfied @@ -39,16 +28,16 @@ error[E0277]: the trait bound `FooParams: StaticResponseExtender` is not satisfi | ::: $WORKSPACE/src/endpoint.rs | - | type Params: QueryStringExtractor + Clone + Sync; + | type Params: QueryStringExtractor + Sync; | -------------------------- required by this bound in `gotham_restful::EndpointWithSchema::Params` -error[E0277]: the trait bound `FooParams: Clone` is not satisfied +error[E0277]: the trait bound `FooParams: OpenapiType` is not satisfied --> $DIR/invalid_params_ty.rs:15:16 | 15 | fn endpoint(_: FooParams) { - | ^^^^^^^^^ the trait `Clone` is not implemented for `FooParams` + | ^^^^^^^^^ the trait `OpenapiType` is not implemented for `FooParams` | ::: $WORKSPACE/src/endpoint.rs | - | type Params: QueryStringExtractor + Clone + Sync; - | ----- required by this bound in `gotham_restful::EndpointWithSchema::Params` + | #[openapi_bound("Params: crate::OpenapiType")] + | ---------------------------- required by this bound in `gotham_restful::EndpointWithSchema::Params` diff --git a/tests/ui/endpoint/invalid_placeholders_ty.stderr b/tests/ui/endpoint/invalid_placeholders_ty.stderr index 58c8014..09c9bbb 100644 --- a/tests/ui/endpoint/invalid_placeholders_ty.stderr +++ b/tests/ui/endpoint/invalid_placeholders_ty.stderr @@ -1,14 +1,3 @@ -error[E0277]: the trait bound `FooPlaceholders: OpenapiType` is not satisfied - --> $DIR/invalid_placeholders_ty.rs:15:16 - | -15 | fn endpoint(_: FooPlaceholders) { - | ^^^^^^^^^^^^^^^ the trait `OpenapiType` is not implemented for `FooPlaceholders` - | - ::: $WORKSPACE/src/endpoint.rs - | - | #[openapi_bound("Placeholders: OpenapiType")] - | --------------------------- required by this bound in `gotham_restful::EndpointWithSchema::Placeholders` - error[E0277]: the trait bound `for<'de> FooPlaceholders: serde::de::Deserialize<'de>` is not satisfied --> $DIR/invalid_placeholders_ty.rs:15:16 | @@ -17,7 +6,7 @@ error[E0277]: the trait bound `for<'de> FooPlaceholders: serde::de::Deserialize< | ::: $WORKSPACE/src/endpoint.rs | - | type Placeholders: PathExtractor + Clone + Sync; + | type Placeholders: PathExtractor + Sync; | ------------------- required by this bound in `gotham_restful::EndpointWithSchema::Placeholders` error[E0277]: the trait bound `FooPlaceholders: StateData` is not satisfied @@ -28,7 +17,7 @@ error[E0277]: the trait bound `FooPlaceholders: StateData` is not satisfied | ::: $WORKSPACE/src/endpoint.rs | - | type Placeholders: PathExtractor + Clone + Sync; + | type Placeholders: PathExtractor + Sync; | ------------------- required by this bound in `gotham_restful::EndpointWithSchema::Placeholders` error[E0277]: the trait bound `FooPlaceholders: StaticResponseExtender` is not satisfied @@ -39,16 +28,16 @@ error[E0277]: the trait bound `FooPlaceholders: StaticResponseExtender` is not s | ::: $WORKSPACE/src/endpoint.rs | - | type Placeholders: PathExtractor + Clone + Sync; + | type Placeholders: PathExtractor + Sync; | ------------------- required by this bound in `gotham_restful::EndpointWithSchema::Placeholders` -error[E0277]: the trait bound `FooPlaceholders: Clone` is not satisfied +error[E0277]: the trait bound `FooPlaceholders: OpenapiType` is not satisfied --> $DIR/invalid_placeholders_ty.rs:15:16 | 15 | fn endpoint(_: FooPlaceholders) { - | ^^^^^^^^^^^^^^^ the trait `Clone` is not implemented for `FooPlaceholders` + | ^^^^^^^^^^^^^^^ the trait `OpenapiType` is not implemented for `FooPlaceholders` | ::: $WORKSPACE/src/endpoint.rs | - | type Placeholders: PathExtractor + Clone + Sync; - | ----- required by this bound in `gotham_restful::EndpointWithSchema::Placeholders` + | #[openapi_bound("Placeholders: crate::OpenapiType")] + | ---------------------------------- required by this bound in `gotham_restful::EndpointWithSchema::Placeholders` diff --git a/tests/ui/openapi_type/enum_with_fields.rs b/tests/ui/openapi_type/enum_with_fields.rs new file mode 100644 index 0000000..b07cbfa --- /dev/null +++ b/tests/ui/openapi_type/enum_with_fields.rs @@ -0,0 +1,12 @@ +#[macro_use] +extern crate gotham_restful; + +#[derive(OpenapiType)] +enum Food { + Pasta, + Pizza { pineapple: bool }, + Rice, + Other(String) +} + +fn main() {} diff --git a/tests/ui/openapi_type/enum_with_fields.stderr b/tests/ui/openapi_type/enum_with_fields.stderr new file mode 100644 index 0000000..2925a32 --- /dev/null +++ b/tests/ui/openapi_type/enum_with_fields.stderr @@ -0,0 +1,11 @@ +error: #[derive(OpenapiType)] does not support enum variants with fields + --> $DIR/enum_with_fields.rs:7:2 + | +7 | Pizza { pineapple: bool }, + | ^^^^^ + +error: #[derive(OpenapiType)] does not support enum variants with fields + --> $DIR/enum_with_fields.rs:9:2 + | +9 | Other(String) + | ^^^^^ diff --git a/tests/ui/openapi_type/nullable_non_bool.rs b/tests/ui/openapi_type/nullable_non_bool.rs new file mode 100644 index 0000000..2431e94 --- /dev/null +++ b/tests/ui/openapi_type/nullable_non_bool.rs @@ -0,0 +1,10 @@ +#[macro_use] +extern crate gotham_restful; + +#[derive(OpenapiType)] +struct Foo { + #[openapi(nullable = "yes, please")] + bar: String +} + +fn main() {} diff --git a/tests/ui/openapi_type/nullable_non_bool.stderr b/tests/ui/openapi_type/nullable_non_bool.stderr new file mode 100644 index 0000000..421d9cd --- /dev/null +++ b/tests/ui/openapi_type/nullable_non_bool.stderr @@ -0,0 +1,5 @@ +error: Expected bool + --> $DIR/nullable_non_bool.rs:6:23 + | +6 | #[openapi(nullable = "yes, please")] + | ^^^^^^^^^^^^^ diff --git a/tests/ui/openapi_type/rename_non_string.rs b/tests/ui/openapi_type/rename_non_string.rs new file mode 100644 index 0000000..83f8bd6 --- /dev/null +++ b/tests/ui/openapi_type/rename_non_string.rs @@ -0,0 +1,10 @@ +#[macro_use] +extern crate gotham_restful; + +#[derive(OpenapiType)] +struct Foo { + #[openapi(rename = 42)] + bar: String +} + +fn main() {} diff --git a/tests/ui/openapi_type/rename_non_string.stderr b/tests/ui/openapi_type/rename_non_string.stderr new file mode 100644 index 0000000..0446b21 --- /dev/null +++ b/tests/ui/openapi_type/rename_non_string.stderr @@ -0,0 +1,5 @@ +error: Expected string literal + --> $DIR/rename_non_string.rs:6:21 + | +6 | #[openapi(rename = 42)] + | ^^ diff --git a/tests/ui/openapi_type/tuple_struct.rs b/tests/ui/openapi_type/tuple_struct.rs new file mode 100644 index 0000000..7def578 --- /dev/null +++ b/tests/ui/openapi_type/tuple_struct.rs @@ -0,0 +1,7 @@ +#[macro_use] +extern crate gotham_restful; + +#[derive(OpenapiType)] +struct Foo(String); + +fn main() {} diff --git a/tests/ui/openapi_type/tuple_struct.stderr b/tests/ui/openapi_type/tuple_struct.stderr new file mode 100644 index 0000000..62a81c1 --- /dev/null +++ b/tests/ui/openapi_type/tuple_struct.stderr @@ -0,0 +1,5 @@ +error: #[derive(OpenapiType)] does not support unnamed fields + --> $DIR/tuple_struct.rs:5:11 + | +5 | struct Foo(String); + | ^^^^^^^^ diff --git a/tests/ui/openapi_type/union.rs b/tests/ui/openapi_type/union.rs new file mode 100644 index 0000000..99efd49 --- /dev/null +++ b/tests/ui/openapi_type/union.rs @@ -0,0 +1,10 @@ +#[macro_use] +extern crate gotham_restful; + +#[derive(OpenapiType)] +union IntOrPointer { + int: u64, + pointer: *mut String +} + +fn main() {} diff --git a/tests/ui/openapi_type/union.stderr b/tests/ui/openapi_type/union.stderr new file mode 100644 index 0000000..2dbe3b6 --- /dev/null +++ b/tests/ui/openapi_type/union.stderr @@ -0,0 +1,5 @@ +error: #[derive(OpenapiType)] only works for structs and enums + --> $DIR/union.rs:5:1 + | +5 | union IntOrPointer { + | ^^^^^ diff --git a/tests/ui/openapi_type/unknown_key.rs b/tests/ui/openapi_type/unknown_key.rs new file mode 100644 index 0000000..daab52a --- /dev/null +++ b/tests/ui/openapi_type/unknown_key.rs @@ -0,0 +1,10 @@ +#[macro_use] +extern crate gotham_restful; + +#[derive(OpenapiType)] +struct Foo { + #[openapi(like = "pizza")] + bar: String +} + +fn main() {} diff --git a/tests/ui/openapi_type/unknown_key.stderr b/tests/ui/openapi_type/unknown_key.stderr new file mode 100644 index 0000000..b5e9ac1 --- /dev/null +++ b/tests/ui/openapi_type/unknown_key.stderr @@ -0,0 +1,5 @@ +error: Unknown key + --> $DIR/unknown_key.rs:6:12 + | +6 | #[openapi(like = "pizza")] + | ^^^^