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

gotham-restful

-
-
- - pipeline status - - - coverage report - - - crates.io - - - docs.rs - - - rustdoc - - - Minimum Rust Version - - - dependencies - -
-
+# Moved to GitHub -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. +This project has moved to GitHub: https://github.com/msrd0/gotham_restful -## 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 bd3e252..1769315 100644 --- a/README.tpl +++ b/README.tpl @@ -1,19 +1,11 @@ -
-

gotham-restful

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

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