1
0
Fork 0
mirror of https://gitlab.com/msrd0/gotham-restful.git synced 2025-04-19 22:44:38 +00:00

Compare commits

..

18 commits

68 changed files with 1669 additions and 1390 deletions

View file

@ -14,8 +14,7 @@ check-example:
before_script:
- cargo -V
script:
- cd example
- cargo check
- cargo check --manifest-path example/Cargo.toml
cache:
key: cargo-stable-example
paths:
@ -28,6 +27,7 @@ test-default:
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

View file

@ -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 <git@msrd0.de>"]
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" }

399
README.md
View file

@ -1,399 +1,4 @@
<div align="center">
<h1>gotham-restful</h1>
</div>
<div align="center">
<a href="https://gitlab.com/msrd0/gotham-restful/pipelines">
<img alt="pipeline status" src="https://gitlab.com/msrd0/gotham-restful/badges/master/pipeline.svg"/>
</a>
<a href="https://msrd0.gitlab.io/gotham-restful/coverage.html">
<img alt="coverage report" src="https://gitlab.com/msrd0/gotham-restful/badges/master/coverage.svg"/>
</a>
<a href="https://crates.io/crates/gotham_restful">
<img alt="crates.io" src="https://img.shields.io/crates/v/gotham_restful.svg"/>
</a>
<a href="https://docs.rs/crate/gotham_restful">
<img alt="docs.rs" src="https://docs.rs/gotham_restful/badge.svg"/>
</a>
<a href="https://msrd0.gitlab.io/gotham-restful/gotham_restful/index.html">
<img alt="rustdoc" src="https://img.shields.io/badge/docs-master-blue.svg"/>
</a>
<a href="https://blog.rust-lang.org/2020/12/31/Rust-1.49.0.html">
<img alt="Minimum Rust Version" src="https://img.shields.io/badge/rustc-1.49+-orange.svg"/>
</a>
<a href="https://deps.rs/repo/gitlab/msrd0/gotham-restful">
<img alt="dependencies" src="https://deps.rs/repo/gitlab/msrd0/gotham-restful/status.svg"/>
</a>
</div>
<br/>
# 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> {
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<String> {
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<u8>,
content_type: Mime
}
#[create]
fn create(body : RawImage) -> Raw<Vec<u8>> {
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<AuthData>, id: u64) -> AuthSuccess<Secret> {
let intended_for = auth.ok()?.sub;
Ok(Secret { id, intended_for })
}
fn main() {
let auth: AuthMiddleware<AuthData, _> = 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::<SecretResource>("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::<FooResource>("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<Vec<Foo>> {
foo::table.load(conn)
}
type Repo = gotham_middleware_diesel::Repo<PgConnection>;
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::<FooResource>("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> {
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::<FooResource>("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.
```

View file

@ -1,18 +1,10 @@
<div align="center">
<h1>gotham-restful</h1>
</div>
<div align="center">
<br/>
<div>
<a href="https://gitlab.com/msrd0/gotham-restful/pipelines">
<img alt="pipeline status" src="https://gitlab.com/msrd0/gotham-restful/badges/master/pipeline.svg"/>
</a>
<a href="https://msrd0.gitlab.io/gotham-restful/coverage.html">
<img alt="coverage report" src="https://gitlab.com/msrd0/gotham-restful/badges/master/coverage.svg"/>
</a>
<a href="https://crates.io/crates/gotham_restful">
<img alt="crates.io" src="https://img.shields.io/crates/v/gotham_restful.svg"/>
</a>
<a href="https://docs.rs/crate/gotham_restful">
<img alt="docs.rs" src="https://docs.rs/gotham_restful/badge.svg"/>
</a>
<a href="https://msrd0.gitlab.io/gotham-restful/gotham_restful/index.html">
<img alt="rustdoc" src="https://img.shields.io/badge/docs-master-blue.svg"/>
@ -26,6 +18,23 @@
</div>
<br/>
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

View file

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

View file

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

View file

@ -24,11 +24,6 @@ use resource::expand_resource;
mod resource_error;
use resource_error::expand_resource_error;
#[cfg(feature = "openapi")]
mod openapi_type;
#[cfg(feature = "openapi")]
use openapi_type::expand_openapi_type;
mod private_openapi_trait;
use private_openapi_trait::expand_private_openapi_trait;
@ -66,12 +61,6 @@ pub fn derive_from_body(input: TokenStream) -> TokenStream {
expand_derive(input, expand_from_body)
}
#[cfg(feature = "openapi")]
#[proc_macro_derive(OpenapiType, attributes(openapi))]
pub fn derive_openapi_type(input: TokenStream) -> TokenStream {
expand_derive(input, expand_openapi_type)
}
#[proc_macro_derive(RequestBody, attributes(supported_types))]
pub fn derive_request_body(input: TokenStream) -> TokenStream {
expand_derive(input, expand_request_body)

View file

@ -1,289 +0,0 @@
use crate::util::{remove_parens, CollectToResult};
use proc_macro2::{Ident, TokenStream};
use quote::quote;
use syn::{
parse_macro_input, spanned::Spanned, Attribute, AttributeArgs, Data, DataEnum, DataStruct, DeriveInput, Error, Field,
Fields, GenericParam, Generics, Lit, LitStr, Meta, NestedMeta, Path, PathSegment, PredicateType, Result, TraitBound,
TraitBoundModifier, Type, TypeParamBound, TypePath, Variant, WhereClause, WherePredicate
};
pub fn expand_openapi_type(input: DeriveInput) -> Result<TokenStream> {
match (input.ident, input.generics, input.attrs, input.data) {
(ident, generics, attrs, Data::Enum(inum)) => expand_enum(ident, generics, attrs, inum),
(ident, generics, attrs, Data::Struct(strukt)) => expand_struct(ident, generics, attrs, strukt),
(_, _, _, Data::Union(uni)) => Err(Error::new(
uni.union_token.span(),
"#[derive(OpenapiType)] only works for structs and enums"
))
}
}
fn update_generics(generics: &Generics, where_clause: &mut Option<WhereClause>) {
if generics.params.is_empty() {
return;
}
if where_clause.is_none() {
*where_clause = Some(WhereClause {
where_token: Default::default(),
predicates: Default::default()
});
}
let where_clause = where_clause.as_mut().unwrap();
for param in &generics.params {
if let GenericParam::Type(ty_param) = param {
where_clause.predicates.push(WherePredicate::Type(PredicateType {
lifetimes: None,
bounded_ty: Type::Path(TypePath {
qself: None,
path: Path {
leading_colon: None,
segments: vec![PathSegment {
ident: ty_param.ident.clone(),
arguments: Default::default()
}]
.into_iter()
.collect()
}
}),
colon_token: Default::default(),
bounds: vec![TypeParamBound::Trait(TraitBound {
paren_token: None,
modifier: TraitBoundModifier::None,
lifetimes: None,
path: syn::parse_str("::gotham_restful::OpenapiType").unwrap()
})]
.into_iter()
.collect()
}));
}
}
}
#[derive(Debug, Default)]
struct Attrs {
nullable: bool,
rename: Option<String>
}
fn to_string(lit: &Lit) -> Result<String> {
match lit {
Lit::Str(str) => Ok(str.value()),
_ => Err(Error::new(lit.span(), "Expected string literal"))
}
}
fn to_bool(lit: &Lit) -> Result<bool> {
match lit {
Lit::Bool(bool) => Ok(bool.value),
_ => Err(Error::new(lit.span(), "Expected bool"))
}
}
fn parse_attributes(input: &[Attribute]) -> Result<Attrs> {
let mut parsed = Attrs::default();
for attr in input {
if attr.path.segments.iter().last().map(|segment| segment.ident.to_string()) == Some("openapi".to_owned()) {
let tokens = remove_parens(attr.tokens.clone());
// TODO this is not public api but syn currently doesn't offer another convenient way to parse AttributeArgs
let nested = parse_macro_input::parse::<AttributeArgs>(tokens.into())?;
for meta in nested {
match &meta {
NestedMeta::Meta(Meta::NameValue(kv)) => match kv.path.segments.last().map(|s| s.ident.to_string()) {
Some(key) => match key.as_ref() {
"nullable" => parsed.nullable = to_bool(&kv.lit)?,
"rename" => parsed.rename = Some(to_string(&kv.lit)?),
_ => return Err(Error::new(kv.path.span(), "Unknown key"))
},
_ => return Err(Error::new(meta.span(), "Unexpected token"))
},
_ => return Err(Error::new(meta.span(), "Unexpected token"))
}
}
}
}
Ok(parsed)
}
fn expand_variant(variant: &Variant) -> Result<TokenStream> {
if !matches!(variant.fields, Fields::Unit) {
return Err(Error::new(
variant.span(),
"#[derive(OpenapiType)] does not support enum variants with fields"
));
}
let ident = &variant.ident;
let attrs = parse_attributes(&variant.attrs)?;
let name = match attrs.rename {
Some(rename) => rename,
None => ident.to_string()
};
Ok(quote! {
enumeration.push(#name.to_string());
})
}
fn expand_enum(ident: Ident, generics: Generics, attrs: Vec<Attribute>, input: DataEnum) -> Result<TokenStream> {
let krate = super::krate();
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
let mut where_clause = where_clause.cloned();
update_generics(&generics, &mut where_clause);
let attrs = parse_attributes(&attrs)?;
let nullable = attrs.nullable;
let name = match attrs.rename {
Some(rename) => rename,
None => ident.to_string()
};
let variants = input.variants.iter().map(expand_variant).collect_to_result()?;
Ok(quote! {
impl #impl_generics #krate::OpenapiType for #ident #ty_generics
#where_clause
{
fn schema() -> #krate::OpenapiSchema
{
use #krate::{private::openapi::*, OpenapiSchema};
let mut enumeration : Vec<String> = Vec::new();
#(#variants)*
let schema = SchemaKind::Type(Type::String(StringType {
format: VariantOrUnknownOrEmpty::Empty,
enumeration,
..Default::default()
}));
OpenapiSchema {
name: Some(#name.to_string()),
nullable: #nullable,
schema,
dependencies: Default::default()
}
}
}
})
}
fn expand_field(field: &Field) -> Result<TokenStream> {
let ident = match &field.ident {
Some(ident) => ident,
None => {
return Err(Error::new(
field.span(),
"#[derive(OpenapiType)] does not support fields without an ident"
))
},
};
let ident_str = LitStr::new(&ident.to_string(), ident.span());
let ty = &field.ty;
let attrs = parse_attributes(&field.attrs)?;
let nullable = attrs.nullable;
let name = match attrs.rename {
Some(rename) => rename,
None => ident.to_string()
};
Ok(quote! {{
let mut schema = <#ty>::schema();
if schema.nullable
{
schema.nullable = false;
}
else if !#nullable
{
required.push(#ident_str.to_string());
}
let keys : Vec<String> = schema.dependencies.keys().map(|k| k.to_string()).collect();
for dep in keys
{
let dep_schema = schema.dependencies.swap_remove(&dep);
if let Some(dep_schema) = dep_schema
{
dependencies.insert(dep, dep_schema);
}
}
match schema.name.clone() {
Some(schema_name) => {
properties.insert(
#name.to_string(),
ReferenceOr::Reference { reference: format!("#/components/schemas/{}", schema_name) }
);
dependencies.insert(schema_name, schema);
},
None => {
properties.insert(
#name.to_string(),
ReferenceOr::Item(Box::new(schema.into_schema()))
);
}
}
}})
}
fn expand_struct(ident: Ident, generics: Generics, attrs: Vec<Attribute>, input: DataStruct) -> Result<TokenStream> {
let krate = super::krate();
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
let mut where_clause = where_clause.cloned();
update_generics(&generics, &mut where_clause);
let attrs = parse_attributes(&attrs)?;
let nullable = attrs.nullable;
let name = match attrs.rename {
Some(rename) => rename,
None => ident.to_string()
};
let fields: Vec<TokenStream> = match input.fields {
Fields::Named(named_fields) => named_fields.named.iter().map(expand_field).collect_to_result()?,
Fields::Unnamed(fields) => {
return Err(Error::new(
fields.span(),
"#[derive(OpenapiType)] does not support unnamed fields"
))
},
Fields::Unit => Vec::new()
};
Ok(quote! {
impl #impl_generics #krate::OpenapiType for #ident #ty_generics
#where_clause
{
fn schema() -> #krate::OpenapiSchema
{
use #krate::{private::{openapi::*, IndexMap}, OpenapiSchema};
let mut properties : IndexMap<String, ReferenceOr<Box<Schema>>> = IndexMap::new();
let mut required : Vec<String> = Vec::new();
let mut dependencies : IndexMap<String, OpenapiSchema> = IndexMap::new();
#(#fields)*
let schema = SchemaKind::Type(Type::Object(ObjectType {
properties,
required,
additional_properties: None,
min_properties: None,
max_properties: None
}));
OpenapiSchema {
name: Some(#name.to_string()),
nullable: #nullable,
schema,
dependencies
}
}
}
})
}

View file

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

27
openapi_type/Cargo.toml Normal file
View file

@ -0,0 +1,27 @@
# -*- eval: (cargo-minor-mode 1) -*-
[package]
workspace = ".."
name = "openapi_type"
version = "0.1.0-dev"
authors = ["Dominic Meiser <git@msrd0.de>"]
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"

224
openapi_type/src/impls.rs Normal file
View file

@ -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<i64>, bits: Option<i64>) -> 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<StringFormat>) -> 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<T: TimeZone>, NaiveDate => {
str_schema(VariantOrUnknownOrEmpty::Item(StringFormat::Date))
});
#[cfg(feature = "chrono")]
impl_openapi_type!(DateTime<T: TimeZone>, 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<T: OpenapiType> => {
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<T: OpenapiType>(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<T: OpenapiType> => array_schema::<T>(false));
impl_openapi_type!(BTreeSet<T: OpenapiType>, IndexSet<T: OpenapiType>, HashSet<T: OpenapiType, S: BuildHasher> => {
array_schema::<T>(true)
});
#[inline]
fn map_schema<K: OpenapiType, T: OpenapiType>() -> 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<K: OpenapiType, T: OpenapiType>,
IndexMap<K: OpenapiType, T: OpenapiType>,
HashMap<K: OpenapiType, T: OpenapiType, S: BuildHasher>
=> map_schema::<K, T>()
);

86
openapi_type/src/lib.rs Normal file
View file

@ -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<String>,
/// Whether this particular schema is nullable. Note that there is no guarantee that this will
/// make it into the final specification, it might just be interpreted as a hint to make it
/// an optional parameter.
pub nullable: bool,
/// The actual OpenAPI schema.
pub schema: SchemaKind,
/// Other schemas that this schema depends on. They will be included in the final OpenAPI Spec
/// along with this schema.
pub dependencies: IndexMap<String, OpenapiSchema>
}
impl OpenapiSchema {
/// Create a new schema that has no name.
pub fn new(schema: SchemaKind) -> Self {
Self {
name: None,
nullable: false,
schema,
dependencies: IndexMap::new()
}
}
/// Convert this schema to 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()
}
}

View file

@ -0,0 +1,12 @@
use crate::OpenapiSchema;
use indexmap::IndexMap;
pub type Dependencies = IndexMap<String, OpenapiSchema>;
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);
}
}
}

View file

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

View file

@ -0,0 +1,6 @@
use openapi_type::OpenapiType;
#[derive(OpenapiType)]
enum Foo {}
fn main() {}

View file

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

View file

@ -0,0 +1,12 @@
use openapi_type::OpenapiType;
#[derive(OpenapiType)]
struct Foo {
bar: Bar
}
struct Bar;
fn main() {
Foo::schema();
}

View file

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

View file

@ -0,0 +1,12 @@
use openapi_type::OpenapiType;
#[derive(OpenapiType)]
struct Foo<T> {
bar: T
}
struct Bar;
fn main() {
<Foo<Bar>>::schema();
}

View file

@ -0,0 +1,23 @@
error[E0599]: no function or associated item named `schema` found for struct `Foo<Bar>` in the current scope
--> $DIR/not_openapitype_generics.rs:11:14
|
4 | struct Foo<T> {
| -------------
| |
| function or associated item `schema` not found for this
| doesn't satisfy `Foo<Bar>: OpenapiType`
...
8 | struct Bar;
| ----------- doesn't satisfy `Bar: OpenapiType`
...
11 | <Foo<Bar>>::schema();
| ^^^^^^ function or associated item not found in `Foo<Bar>`
|
= note: the method `schema` exists but the following trait bounds were not satisfied:
`Bar: OpenapiType`
which is required by `Foo<Bar>: OpenapiType`
`Foo<Bar>: OpenapiType`
which is required by `&Foo<Bar>: 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`

View file

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

View file

@ -0,0 +1,6 @@
use openapi_type::OpenapiType;
#[derive(OpenapiType)]
struct Foo(i64, i64);
fn main() {}

View file

@ -0,0 +1,5 @@
error: #[derive(OpenapiType)] does not support tuple structs
--> $DIR/tuple_struct.rs:4:11
|
4 | struct Foo(i64, i64);
| ^^^^^^^^^^

View file

@ -0,0 +1,8 @@
use openapi_type::OpenapiType;
#[derive(OpenapiType)]
enum Foo {
Pair(i64, i64)
}
fn main() {}

View file

@ -0,0 +1,5 @@
error: #[derive(OpenapiType)] does not support tuple variants
--> $DIR/tuple_variant.rs:5:6
|
5 | Pair(i64, i64)
| ^^^^^^^^^^

View file

@ -0,0 +1,9 @@
use openapi_type::OpenapiType;
#[derive(OpenapiType)]
union Foo {
signed: i64,
unsigned: u64
}
fn main() {}

View file

@ -0,0 +1,5 @@
error: #[derive(OpenapiType)] cannot be used on unions
--> $DIR/union.rs:4:1
|
4 | union Foo {
| ^^^^^

View file

@ -0,0 +1,7 @@
use openapi_type::OpenapiType;
#[derive(OpenapiType)]
#[openapi(pizza)]
struct Foo;
fn main() {}

View file

@ -0,0 +1,5 @@
error: Unexpected token
--> $DIR/unknown_attribute.rs:4:11
|
4 | #[openapi(pizza)]
| ^^^^^

View file

@ -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<FixedOffset>, Date<Local>, Date<Utc>, NaiveDate = {
"type": "string",
"format": "date"
});
#[cfg(feature = "chrono")]
test_type!(DateTime<FixedOffset>, DateTime<Local>, DateTime<Utc>, NaiveDateTime = {
"type": "string",
"format": "date-time"
});
// ### some std types
test_type!(Option<String> = {
"type": "string",
"nullable": true
});
test_type!(Vec<String> = {
"type": "array",
"items": {
"type": "string"
}
});
test_type!(BTreeSet<String>, IndexSet<String>, HashSet<String> = {
"type": "array",
"items": {
"type": "string"
},
"uniqueItems": true
});
test_type!(BTreeMap<isize, String>, IndexMap<isize, String>, HashMap<isize, String> = {
"type": "object",
"properties": {
"default": {
"type": "integer"
}
},
"required": ["default"],
"additionalProperties": {
"type": "string"
}
});

View file

@ -0,0 +1,7 @@
use trybuild::TestCases;
#[test]
fn trybuild() {
let t = TestCases::new();
t.compile_fail("tests/fail/*.rs");
}

View file

@ -0,0 +1,19 @@
# -*- eval: (cargo-minor-mode 1) -*-
[package]
workspace = ".."
name = "openapi_type_derive"
version = "0.1.0-dev"
authors = ["Dominic Meiser <git@msrd0.de>"]
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"

View file

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

View file

@ -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<TokenStream2> {
// 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
}
}
}
})
}

View file

@ -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<LitStr>),
Alternatives(Vec<ParseData>),
Unit
}
fn parse_named_fields(named_fields: &FieldsNamed) -> syn::Result<ParseData> {
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<ParseData> {
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<ParseData> {
let mut strings: Vec<LitStr> = 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::<syn::Result<Vec<_>>>()?
))
};
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<ParseData> {
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<LitStr>,
pub(super) rename_all: Option<LitStr>,
pub(super) tag: Option<LitStr>,
pub(super) content: Option<LitStr>,
pub(super) untagged: bool
}
pub(super) fn parse_container_attrs(
input: &Attribute,
attrs: &mut ContainerAttributes,
error_on_unknown: bool
) -> syn::Result<()> {
let tokens: Punctuated<Meta, Token![,]> = 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(())
}

View file

@ -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<LitStr>;
}
impl ExpectLit for Lit {
fn expect_str(self) -> syn::Result<LitStr> {
match self {
Self::Str(str) => Ok(str),
_ => Err(syn::Error::new(self.span(), "Expected string literal"))
}
}
}

View file

@ -2,11 +2,41 @@ use crate::{IntoResponse, RequestBody};
use futures_util::future::BoxFuture;
use gotham::{
extractor::{PathExtractor, QueryStringExtractor},
hyper::{Body, Method},
state::State
hyper::{Body, Method, Response},
router::response::extender::StaticResponseExtender,
state::{State, StateData}
};
#[cfg(feature = "openapi")]
use openapi_type::{OpenapiSchema, OpenapiType};
use serde::{Deserialize, Deserializer};
use std::borrow::Cow;
/// A no-op extractor that can be used as a default type for [Endpoint::Placeholders] and
/// [Endpoint::Params].
#[derive(Debug, Clone, Copy)]
pub struct NoopExtractor;
impl<'de> Deserialize<'de> for NoopExtractor {
fn deserialize<D: Deserializer<'de>>(_: D) -> Result<Self, D::Error> {
Ok(Self)
}
}
#[cfg(feature = "openapi")]
impl OpenapiType for NoopExtractor {
fn schema() -> OpenapiSchema {
warn!("You're asking for the OpenAPI Schema for gotham_restful::NoopExtractor. This is probably not what you want.");
<() as OpenapiType>::schema()
}
}
impl StateData for NoopExtractor {}
impl StaticResponseExtender for NoopExtractor {
type ResBody = Body;
fn extend(_: &mut State, _: &mut Response<Body>) {}
}
// TODO: Specify default types once https://github.com/rust-lang/rust/issues/29661 lands.
#[_private_openapi_trait(EndpointWithSchema)]
pub trait Endpoint {
@ -23,19 +53,19 @@ pub trait Endpoint {
fn has_placeholders() -> bool {
false
}
/// The type that parses the URI placeholders. Use [gotham::extractor::NoopPathExtractor]
/// if `has_placeholders()` returns `false`.
#[openapi_bound("Placeholders: crate::OpenapiType")]
type Placeholders: PathExtractor<Body> + Sync;
/// The type that parses the URI placeholders. Use [NoopExtractor] if `has_placeholders()`
/// returns `false`.
#[openapi_bound("Placeholders: OpenapiType")]
type Placeholders: PathExtractor<Body> + Clone + Sync;
/// Returns `true` _iff_ the request parameters should be parsed. `false` by default.
fn needs_params() -> bool {
false
}
/// The type that parses the request parameters. Use [gotham::extractor::NoopQueryStringExtractor]
/// if `needs_params()` returns `false`.
#[openapi_bound("Params: crate::OpenapiType")]
type Params: QueryStringExtractor<Body> + Sync;
/// The type that parses the request parameters. Use [NoopExtractor] if `needs_params()`
/// returns `false`.
#[openapi_bound("Params: OpenapiType")]
type Params: QueryStringExtractor<Body> + Clone + Sync;
/// Returns `true` _iff_ the request body should be parsed. `false` by default.
fn needs_body() -> bool {

View file

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

View file

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

View file

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

View file

@ -1,7 +1,8 @@
use super::SECURITY_NAME;
use crate::{response::OrAllTypes, EndpointWithSchema, IntoResponse, OpenapiSchema, RequestBody, ResponseSchema};
use crate::{response::OrAllTypes, EndpointWithSchema, IntoResponse, RequestBody, ResponseSchema};
use indexmap::IndexMap;
use mime::Mime;
use openapi_type::OpenapiSchema;
use openapiv3::{
MediaType, Operation, Parameter, ParameterData, ParameterSchemaOrContent, ReferenceOr, ReferenceOr::Item,
RequestBody as OARequestBody, Response, Responses, Schema, SchemaKind, StatusCode, Type

View file

@ -3,9 +3,10 @@ use super::{
handler::{OpenapiHandler, SwaggerUiHandler},
operation::OperationDescription
};
use crate::{routing::*, EndpointWithSchema, OpenapiType, ResourceWithSchema, ResponseSchema};
use crate::{routing::*, EndpointWithSchema, ResourceWithSchema, ResponseSchema};
use gotham::{hyper::Method, pipeline::chain::PipelineHandleChain, router::builder::*};
use once_cell::sync::Lazy;
use openapi_type::OpenapiType;
use regex::{Captures, Regex};
use std::panic::RefUnwindSafe;

View file

@ -1,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<String>,
/// Whether this particular schema is nullable. Note that there is no guarantee that this will
/// make it into the final specification, it might just be interpreted as a hint to make it
/// an optional parameter.
pub nullable: bool,
/// The actual OpenAPI schema.
pub schema: SchemaKind,
/// Other schemas that this schema depends on. They will be included in the final OpenAPI Spec
/// along with this schema.
pub dependencies: IndexMap<String, OpenapiSchema>
}
impl OpenapiSchema {
/// Create a new schema that has no name.
pub fn new(schema: SchemaKind) -> Self {
Self {
name: None,
nullable: false,
schema,
dependencies: IndexMap::new()
}
}
/// Convert this schema to an [openapiv3::Schema] that can be serialized to the OpenAPI Spec.
pub fn into_schema(self) -> Schema {
Schema {
schema_data: SchemaData {
nullable: self.nullable,
title: self.name,
..Default::default()
},
schema_kind: self.schema
}
}
}
/**
This trait needs to be implemented by every type that is being used in the OpenAPI Spec. It gives
access to the [OpenapiSchema] of this type. It is provided for primitive types, String and the
like. For use on your own types, there is a derive macro:
```
# #[macro_use] extern crate gotham_restful_derive;
#
#[derive(OpenapiType)]
struct MyResponse {
message: String
}
```
*/
pub trait OpenapiType {
fn schema() -> OpenapiSchema;
}
impl OpenapiType for () {
fn schema() -> OpenapiSchema {
OpenapiSchema::new(SchemaKind::Type(Type::Object(ObjectType {
additional_properties: Some(AdditionalProperties::Any(false)),
..Default::default()
})))
}
}
impl OpenapiType for NoopPathExtractor {
fn schema() -> OpenapiSchema {
warn!("You're asking for the OpenAPI Schema for gotham::extractor::NoopPathExtractor. This is probably not what you want.");
<()>::schema()
}
}
impl OpenapiType for NoopQueryStringExtractor {
fn schema() -> OpenapiSchema {
warn!("You're asking for the OpenAPI Schema for gotham::extractor::NoopQueryStringExtractor. This is probably not what you want.");
<()>::schema()
}
}
impl OpenapiType for bool {
fn schema() -> OpenapiSchema {
OpenapiSchema::new(SchemaKind::Type(Type::Boolean {}))
}
}
macro_rules! int_types {
($($int_ty:ty),*) => {$(
impl OpenapiType for $int_ty
{
fn schema() -> OpenapiSchema
{
OpenapiSchema::new(SchemaKind::Type(Type::Integer(IntegerType::default())))
}
}
)*};
(unsigned $($int_ty:ty),*) => {$(
impl OpenapiType for $int_ty
{
fn schema() -> OpenapiSchema
{
OpenapiSchema::new(SchemaKind::Type(Type::Integer(IntegerType {
minimum: Some(0),
..Default::default()
})))
}
}
)*};
(gtzero $($int_ty:ty),*) => {$(
impl OpenapiType for $int_ty
{
fn schema() -> OpenapiSchema
{
OpenapiSchema::new(SchemaKind::Type(Type::Integer(IntegerType {
minimum: Some(1),
..Default::default()
})))
}
}
)*};
(bits = $bits:expr, $($int_ty:ty),*) => {$(
impl OpenapiType for $int_ty
{
fn schema() -> OpenapiSchema
{
OpenapiSchema::new(SchemaKind::Type(Type::Integer(IntegerType {
format: VariantOrUnknownOrEmpty::Unknown(format!("int{}", $bits)),
..Default::default()
})))
}
}
)*};
(unsigned bits = $bits:expr, $($int_ty:ty),*) => {$(
impl OpenapiType for $int_ty
{
fn schema() -> OpenapiSchema
{
OpenapiSchema::new(SchemaKind::Type(Type::Integer(IntegerType {
format: VariantOrUnknownOrEmpty::Unknown(format!("int{}", $bits)),
minimum: Some(0),
..Default::default()
})))
}
}
)*};
(gtzero bits = $bits:expr, $($int_ty:ty),*) => {$(
impl OpenapiType for $int_ty
{
fn schema() -> OpenapiSchema
{
OpenapiSchema::new(SchemaKind::Type(Type::Integer(IntegerType {
format: VariantOrUnknownOrEmpty::Unknown(format!("int{}", $bits)),
minimum: Some(1),
..Default::default()
})))
}
}
)*};
}
int_types!(isize);
int_types!(unsigned usize);
int_types!(gtzero NonZeroUsize);
int_types!(bits = 8, i8);
int_types!(unsigned bits = 8, u8);
int_types!(gtzero bits = 8, NonZeroU8);
int_types!(bits = 16, i16);
int_types!(unsigned bits = 16, u16);
int_types!(gtzero bits = 16, NonZeroU16);
int_types!(bits = 32, i32);
int_types!(unsigned bits = 32, u32);
int_types!(gtzero bits = 32, NonZeroU32);
int_types!(bits = 64, i64);
int_types!(unsigned bits = 64, u64);
int_types!(gtzero bits = 64, NonZeroU64);
int_types!(bits = 128, i128);
int_types!(unsigned bits = 128, u128);
int_types!(gtzero bits = 128, NonZeroU128);
macro_rules! num_types {
($($num_ty:ty = $num_fmt:ident),*) => {$(
impl OpenapiType for $num_ty
{
fn schema() -> OpenapiSchema
{
OpenapiSchema::new(SchemaKind::Type(Type::Number(NumberType {
format: VariantOrUnknownOrEmpty::Item(NumberFormat::$num_fmt),
..Default::default()
})))
}
}
)*}
}
num_types!(f32 = Float, f64 = Double);
macro_rules! str_types {
($($str_ty:ty),*) => {$(
impl OpenapiType for $str_ty
{
fn schema() -> OpenapiSchema
{
OpenapiSchema::new(SchemaKind::Type(Type::String(StringType::default())))
}
}
)*};
(format = $format:ident, $($str_ty:ty),*) => {$(
impl OpenapiType for $str_ty
{
fn schema() -> OpenapiSchema
{
use openapiv3::StringFormat;
OpenapiSchema::new(SchemaKind::Type(Type::String(StringType {
format: VariantOrUnknownOrEmpty::Item(StringFormat::$format),
..Default::default()
})))
}
}
)*};
(format_str = $format:expr, $($str_ty:ty),*) => {$(
impl OpenapiType for $str_ty
{
fn schema() -> OpenapiSchema
{
OpenapiSchema::new(SchemaKind::Type(Type::String(StringType {
format: VariantOrUnknownOrEmpty::Unknown($format.to_string()),
..Default::default()
})))
}
}
)*};
}
str_types!(String, &str);
#[cfg(feature = "chrono")]
str_types!(format = Date, Date<FixedOffset>, Date<Local>, Date<Utc>, NaiveDate);
#[cfg(feature = "chrono")]
str_types!(
format = DateTime,
DateTime<FixedOffset>,
DateTime<Local>,
DateTime<Utc>,
NaiveDateTime
);
#[cfg(feature = "uuid")]
str_types!(format_str = "uuid", Uuid);
impl<T: OpenapiType> OpenapiType for Option<T> {
fn schema() -> OpenapiSchema {
let schema = T::schema();
let mut dependencies = schema.dependencies.clone();
let schema = match schema.name.clone() {
Some(name) => {
let reference = Reference {
reference: format!("#/components/schemas/{}", name)
};
dependencies.insert(name, schema);
SchemaKind::AllOf { all_of: vec![reference] }
},
None => schema.schema
};
OpenapiSchema {
nullable: true,
name: None,
schema,
dependencies
}
}
}
impl<T: OpenapiType> OpenapiType for Vec<T> {
fn schema() -> OpenapiSchema {
let schema = T::schema();
let mut dependencies = schema.dependencies.clone();
let items = match schema.name.clone() {
Some(name) => {
let reference = Reference {
reference: format!("#/components/schemas/{}", name)
};
dependencies.insert(name, schema);
reference
},
None => Item(Box::new(schema.into_schema()))
};
OpenapiSchema {
nullable: false,
name: None,
schema: SchemaKind::Type(Type::Array(ArrayType {
items,
min_items: None,
max_items: None,
unique_items: false
})),
dependencies
}
}
}
impl<T: OpenapiType> OpenapiType for BTreeSet<T> {
fn schema() -> OpenapiSchema {
<Vec<T> as OpenapiType>::schema()
}
}
impl<T: OpenapiType, S: BuildHasher> OpenapiType for HashSet<T, S> {
fn schema() -> OpenapiSchema {
<Vec<T> as OpenapiType>::schema()
}
}
impl<K: OpenapiType, T: OpenapiType, S: BuildHasher> OpenapiType for HashMap<K, T, S> {
fn schema() -> OpenapiSchema {
let key_schema = K::schema();
let mut dependencies = key_schema.dependencies.clone();
let keys = match key_schema.name.clone() {
Some(name) => {
let reference = Reference {
reference: format!("#/components/schemas/{}", name)
};
dependencies.insert(name, key_schema);
reference
},
None => Item(Box::new(key_schema.into_schema()))
};
let schema = T::schema();
dependencies.extend(schema.dependencies.iter().map(|(k, v)| (k.clone(), v.clone())));
let items = Box::new(match schema.name.clone() {
Some(name) => {
let reference = Reference {
reference: format!("#/components/schemas/{}", name)
};
dependencies.insert(name, schema);
reference
},
None => Item(schema.into_schema())
});
let mut properties = IndexMap::new();
properties.insert("default".to_owned(), keys);
OpenapiSchema {
nullable: false,
name: None,
schema: SchemaKind::Type(Type::Object(ObjectType {
properties,
required: vec!["default".to_owned()],
additional_properties: Some(AdditionalProperties::Schema(items)),
..Default::default()
})),
dependencies
}
}
}
impl OpenapiType for serde_json::Value {
fn schema() -> OpenapiSchema {
OpenapiSchema {
nullable: true,
name: None,
schema: SchemaKind::Any(Default::default()),
dependencies: Default::default()
}
}
}
#[cfg(test)]
mod test {
use super::*;
use serde_json::Value;
type Unit = ();
macro_rules! assert_schema {
($ty:ident $(<$($generic:ident),+>)* => $json:expr) => {
paste::item! {
#[test]
fn [<test_schema_ $ty:lower $($(_ $generic:lower)+)*>]()
{
let schema = <$ty $(<$($generic),+>)* as OpenapiType>::schema().into_schema();
let schema_json = serde_json::to_string(&schema).expect(&format!("Unable to serialize schema for {}", stringify!($ty)));
assert_eq!(schema_json, $json);
}
}
};
}
assert_schema!(Unit => r#"{"type":"object","additionalProperties":false}"#);
assert_schema!(bool => r#"{"type":"boolean"}"#);
assert_schema!(isize => r#"{"type":"integer"}"#);
assert_schema!(usize => r#"{"type":"integer","minimum":0}"#);
assert_schema!(i8 => r#"{"type":"integer","format":"int8"}"#);
assert_schema!(u8 => r#"{"type":"integer","format":"int8","minimum":0}"#);
assert_schema!(i16 => r#"{"type":"integer","format":"int16"}"#);
assert_schema!(u16 => r#"{"type":"integer","format":"int16","minimum":0}"#);
assert_schema!(i32 => r#"{"type":"integer","format":"int32"}"#);
assert_schema!(u32 => r#"{"type":"integer","format":"int32","minimum":0}"#);
assert_schema!(i64 => r#"{"type":"integer","format":"int64"}"#);
assert_schema!(u64 => r#"{"type":"integer","format":"int64","minimum":0}"#);
assert_schema!(i128 => r#"{"type":"integer","format":"int128"}"#);
assert_schema!(u128 => r#"{"type":"integer","format":"int128","minimum":0}"#);
assert_schema!(NonZeroUsize => r#"{"type":"integer","minimum":1}"#);
assert_schema!(NonZeroU8 => r#"{"type":"integer","format":"int8","minimum":1}"#);
assert_schema!(NonZeroU16 => r#"{"type":"integer","format":"int16","minimum":1}"#);
assert_schema!(NonZeroU32 => r#"{"type":"integer","format":"int32","minimum":1}"#);
assert_schema!(NonZeroU64 => r#"{"type":"integer","format":"int64","minimum":1}"#);
assert_schema!(NonZeroU128 => r#"{"type":"integer","format":"int128","minimum":1}"#);
assert_schema!(f32 => r#"{"type":"number","format":"float"}"#);
assert_schema!(f64 => r#"{"type":"number","format":"double"}"#);
assert_schema!(String => r#"{"type":"string"}"#);
#[cfg(feature = "uuid")]
assert_schema!(Uuid => r#"{"type":"string","format":"uuid"}"#);
#[cfg(feature = "chrono")]
mod chrono {
use super::*;
assert_schema!(Date<FixedOffset> => r#"{"type":"string","format":"date"}"#);
assert_schema!(Date<Local> => r#"{"type":"string","format":"date"}"#);
assert_schema!(Date<Utc> => r#"{"type":"string","format":"date"}"#);
assert_schema!(NaiveDate => r#"{"type":"string","format":"date"}"#);
assert_schema!(DateTime<FixedOffset> => r#"{"type":"string","format":"date-time"}"#);
assert_schema!(DateTime<Local> => r#"{"type":"string","format":"date-time"}"#);
assert_schema!(DateTime<Utc> => r#"{"type":"string","format":"date-time"}"#);
assert_schema!(NaiveDateTime => r#"{"type":"string","format":"date-time"}"#);
}
assert_schema!(Option<String> => r#"{"nullable":true,"type":"string"}"#);
assert_schema!(Vec<String> => r#"{"type":"array","items":{"type":"string"}}"#);
assert_schema!(BTreeSet<String> => r#"{"type":"array","items":{"type":"string"}}"#);
assert_schema!(HashSet<String> => r#"{"type":"array","items":{"type":"string"}}"#);
assert_schema!(HashMap<i64, String> => r#"{"type":"object","properties":{"default":{"type":"integer","format":"int64"}},"required":["default"],"additionalProperties":{"type":"string"}}"#);
assert_schema!(Value => r#"{"nullable":true}"#);
}

View file

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

View file

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

View file

@ -1,7 +1,9 @@
use super::{handle_error, IntoResponse, IntoResponseError};
use crate::{FromBody, RequestBody, ResourceType, Response};
#[cfg(feature = "openapi")]
use crate::{IntoResponseWithSchema, OpenapiSchema, OpenapiType, ResponseSchema};
use crate::{IntoResponseWithSchema, ResponseSchema};
#[cfg(feature = "openapi")]
use openapi_type::{OpenapiSchema, OpenapiType};
use futures_core::future::Future;
use futures_util::{future, future::FutureExt};

View file

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

View file

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

View file

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

View file

@ -4,7 +4,6 @@ use crate::openapi::{
router::OpenapiRouter
};
use crate::{response::ResourceError, Endpoint, FromBody, IntoResponse, Resource, Response};
#[cfg(feature = "cors")]
use gotham::router::route::matcher::AccessControlRequestMethodMatcher;
use gotham::{
@ -20,10 +19,12 @@ use gotham::{
state::{FromState, State}
};
use mime::{Mime, APPLICATION_JSON};
use std::panic::RefUnwindSafe;
#[cfg(feature = "openapi")]
use openapi_type::OpenapiType;
use std::{any::TypeId, panic::RefUnwindSafe};
/// Allow us to extract an id from a path.
#[derive(Debug, Deserialize, StateData, StaticResponseExtender)]
#[derive(Clone, Copy, Debug, Deserialize, StateData, StaticResponseExtender)]
#[cfg_attr(feature = "openapi", derive(OpenapiType))]
pub struct PathExtractor<ID: RefUnwindSafe + Send + 'static> {
pub id: ID
@ -91,6 +92,11 @@ where
{
trace!("entering endpoint_handler");
let placeholders = E::Placeholders::take_from(state);
// workaround for E::Placeholders and E::Param being the same type
// when fixed remove `Clone` requirement on endpoint
if TypeId::of::<E::Placeholders>() == TypeId::of::<E::Params>() {
state.put(placeholders.clone());
}
let params = E::Params::take_from(state);
let body = match E::needs_body() {

View file

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

View file

@ -9,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::<HeaderMap>();
delay_for(Duration::from_nanos(1)).await;
sleep(Duration::from_nanos(1)).await;
Raw::new(STATE_TEST_RESPONSE, TEXT_PLAIN)
}

View file

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

View file

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

View file

@ -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<Body> + Sync;
| type Params: QueryStringExtractor<Body> + 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<Body> + Sync;
| type Params: QueryStringExtractor<Body> + 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<Body> + Sync;
| type Params: QueryStringExtractor<Body> + 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<Body> + Clone + Sync;
| ----- required by this bound in `gotham_restful::EndpointWithSchema::Params`

View file

@ -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<Body> + Sync;
| type Placeholders: PathExtractor<Body> + 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<Body> + Sync;
| type Placeholders: PathExtractor<Body> + 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<Body> + Sync;
| type Placeholders: PathExtractor<Body> + 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<Body> + Clone + Sync;
| ----- required by this bound in `gotham_restful::EndpointWithSchema::Placeholders`

View file

@ -1,12 +0,0 @@
#[macro_use]
extern crate gotham_restful;
#[derive(OpenapiType)]
enum Food {
Pasta,
Pizza { pineapple: bool },
Rice,
Other(String)
}
fn main() {}

View file

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

View file

@ -1,10 +0,0 @@
#[macro_use]
extern crate gotham_restful;
#[derive(OpenapiType)]
struct Foo {
#[openapi(nullable = "yes, please")]
bar: String
}
fn main() {}

View file

@ -1,5 +0,0 @@
error: Expected bool
--> $DIR/nullable_non_bool.rs:6:23
|
6 | #[openapi(nullable = "yes, please")]
| ^^^^^^^^^^^^^

View file

@ -1,10 +0,0 @@
#[macro_use]
extern crate gotham_restful;
#[derive(OpenapiType)]
struct Foo {
#[openapi(rename = 42)]
bar: String
}
fn main() {}

View file

@ -1,5 +0,0 @@
error: Expected string literal
--> $DIR/rename_non_string.rs:6:21
|
6 | #[openapi(rename = 42)]
| ^^

View file

@ -1,7 +0,0 @@
#[macro_use]
extern crate gotham_restful;
#[derive(OpenapiType)]
struct Foo(String);
fn main() {}

View file

@ -1,5 +0,0 @@
error: #[derive(OpenapiType)] does not support unnamed fields
--> $DIR/tuple_struct.rs:5:11
|
5 | struct Foo(String);
| ^^^^^^^^

View file

@ -1,10 +0,0 @@
#[macro_use]
extern crate gotham_restful;
#[derive(OpenapiType)]
union IntOrPointer {
int: u64,
pointer: *mut String
}
fn main() {}

View file

@ -1,5 +0,0 @@
error: #[derive(OpenapiType)] only works for structs and enums
--> $DIR/union.rs:5:1
|
5 | union IntOrPointer {
| ^^^^^

View file

@ -1,10 +0,0 @@
#[macro_use]
extern crate gotham_restful;
#[derive(OpenapiType)]
struct Foo {
#[openapi(like = "pizza")]
bar: String
}
fn main() {}

View file

@ -1,5 +0,0 @@
error: Unknown key
--> $DIR/unknown_key.rs:6:12
|
6 | #[openapi(like = "pizza")]
| ^^^^