mirror of
https://gitlab.com/msrd0/gotham-restful.git
synced 2025-04-19 22:44:38 +00:00
Compare commits
18 commits
Author | SHA1 | Date | |
---|---|---|---|
7bac379e05 | |||
2dd3f3e21a | |||
e206ab10eb | |||
a5257608e3 | |||
9c7f681e3d | |||
63567f5480 | |||
3a3f743369 | |||
ebea39fe0d | |||
eecd192458 | |||
2a35e044db | |||
a57f1c097d | |||
5f60599c41 | |||
43d3a1cd89 | |||
667009bd22 | |||
d9c7f4135f | |||
90870e3b6a | |||
2251c29d7b | |||
960ba0e8bc |
68 changed files with 1669 additions and 1390 deletions
|
@ -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
|
||||
|
|
25
Cargo.toml
25
Cargo.toml
|
@ -1,11 +1,11 @@
|
|||
# -*- eval: (cargo-minor-mode 1) -*-
|
||||
|
||||
[workspace]
|
||||
members = [".", "./derive", "./example"]
|
||||
members = [".", "./derive", "./example", "./openapi_type", "./openapi_type_derive"]
|
||||
|
||||
[package]
|
||||
name = "gotham_restful"
|
||||
version = "0.2.1"
|
||||
version = "0.3.0-dev"
|
||||
authors = ["Dominic Meiser <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
399
README.md
|
@ -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.
|
||||
```
|
||||
|
|
29
README.tpl
29
README.tpl
|
@ -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**
|
||||
[](https://crates.io/crates/gotham_restful)
|
||||
[](https://docs.rs/gotham_restful)
|
||||
- **gotham_restful_derive**
|
||||
[](https://crates.io/crates/gotham_restful_derive)
|
||||
[](https://docs.rs/gotham_restful_derive)
|
||||
- **openapi_type**
|
||||
[](https://crates.io/crates/openapi_type)
|
||||
[](https://docs.rs/crate/openapi_type)
|
||||
- **openapi_type_derive**
|
||||
[](https://crates.io/crates/openapi_type_derive)
|
||||
[](https://docs.rs/crate/openapi_type_derive)
|
||||
|
||||
# gotham-restful
|
||||
|
||||
{{readme}}
|
||||
|
||||
## Versioning
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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!(())
|
||||
}
|
||||
},
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
|
@ -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
27
openapi_type/Cargo.toml
Normal 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
224
openapi_type/src/impls.rs
Normal 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
86
openapi_type/src/lib.rs
Normal 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()
|
||||
}
|
||||
}
|
12
openapi_type/src/private.rs
Normal file
12
openapi_type/src/private.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
249
openapi_type/tests/custom_types.rs
Normal file
249
openapi_type/tests/custom_types.rs
Normal 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
|
||||
}]
|
||||
});
|
6
openapi_type/tests/fail/enum_with_no_variants.rs
Normal file
6
openapi_type/tests/fail/enum_with_no_variants.rs
Normal file
|
@ -0,0 +1,6 @@
|
|||
use openapi_type::OpenapiType;
|
||||
|
||||
#[derive(OpenapiType)]
|
||||
enum Foo {}
|
||||
|
||||
fn main() {}
|
5
openapi_type/tests/fail/enum_with_no_variants.stderr
Normal file
5
openapi_type/tests/fail/enum_with_no_variants.stderr
Normal 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 {}
|
||||
| ^^
|
12
openapi_type/tests/fail/not_openapitype.rs
Normal file
12
openapi_type/tests/fail/not_openapitype.rs
Normal file
|
@ -0,0 +1,12 @@
|
|||
use openapi_type::OpenapiType;
|
||||
|
||||
#[derive(OpenapiType)]
|
||||
struct Foo {
|
||||
bar: Bar
|
||||
}
|
||||
|
||||
struct Bar;
|
||||
|
||||
fn main() {
|
||||
Foo::schema();
|
||||
}
|
8
openapi_type/tests/fail/not_openapitype.stderr
Normal file
8
openapi_type/tests/fail/not_openapitype.stderr
Normal 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)
|
12
openapi_type/tests/fail/not_openapitype_generics.rs
Normal file
12
openapi_type/tests/fail/not_openapitype_generics.rs
Normal file
|
@ -0,0 +1,12 @@
|
|||
use openapi_type::OpenapiType;
|
||||
|
||||
#[derive(OpenapiType)]
|
||||
struct Foo<T> {
|
||||
bar: T
|
||||
}
|
||||
|
||||
struct Bar;
|
||||
|
||||
fn main() {
|
||||
<Foo<Bar>>::schema();
|
||||
}
|
23
openapi_type/tests/fail/not_openapitype_generics.stderr
Normal file
23
openapi_type/tests/fail/not_openapitype_generics.stderr
Normal 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`
|
21
openapi_type/tests/fail/rustfmt.sh
Executable file
21
openapi_type/tests/fail/rustfmt.sh
Executable 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
|
6
openapi_type/tests/fail/tuple_struct.rs
Normal file
6
openapi_type/tests/fail/tuple_struct.rs
Normal file
|
@ -0,0 +1,6 @@
|
|||
use openapi_type::OpenapiType;
|
||||
|
||||
#[derive(OpenapiType)]
|
||||
struct Foo(i64, i64);
|
||||
|
||||
fn main() {}
|
5
openapi_type/tests/fail/tuple_struct.stderr
Normal file
5
openapi_type/tests/fail/tuple_struct.stderr
Normal file
|
@ -0,0 +1,5 @@
|
|||
error: #[derive(OpenapiType)] does not support tuple structs
|
||||
--> $DIR/tuple_struct.rs:4:11
|
||||
|
|
||||
4 | struct Foo(i64, i64);
|
||||
| ^^^^^^^^^^
|
8
openapi_type/tests/fail/tuple_variant.rs
Normal file
8
openapi_type/tests/fail/tuple_variant.rs
Normal file
|
@ -0,0 +1,8 @@
|
|||
use openapi_type::OpenapiType;
|
||||
|
||||
#[derive(OpenapiType)]
|
||||
enum Foo {
|
||||
Pair(i64, i64)
|
||||
}
|
||||
|
||||
fn main() {}
|
5
openapi_type/tests/fail/tuple_variant.stderr
Normal file
5
openapi_type/tests/fail/tuple_variant.stderr
Normal file
|
@ -0,0 +1,5 @@
|
|||
error: #[derive(OpenapiType)] does not support tuple variants
|
||||
--> $DIR/tuple_variant.rs:5:6
|
||||
|
|
||||
5 | Pair(i64, i64)
|
||||
| ^^^^^^^^^^
|
9
openapi_type/tests/fail/union.rs
Normal file
9
openapi_type/tests/fail/union.rs
Normal file
|
@ -0,0 +1,9 @@
|
|||
use openapi_type::OpenapiType;
|
||||
|
||||
#[derive(OpenapiType)]
|
||||
union Foo {
|
||||
signed: i64,
|
||||
unsigned: u64
|
||||
}
|
||||
|
||||
fn main() {}
|
5
openapi_type/tests/fail/union.stderr
Normal file
5
openapi_type/tests/fail/union.stderr
Normal file
|
@ -0,0 +1,5 @@
|
|||
error: #[derive(OpenapiType)] cannot be used on unions
|
||||
--> $DIR/union.rs:4:1
|
||||
|
|
||||
4 | union Foo {
|
||||
| ^^^^^
|
7
openapi_type/tests/fail/unknown_attribute.rs
Normal file
7
openapi_type/tests/fail/unknown_attribute.rs
Normal file
|
@ -0,0 +1,7 @@
|
|||
use openapi_type::OpenapiType;
|
||||
|
||||
#[derive(OpenapiType)]
|
||||
#[openapi(pizza)]
|
||||
struct Foo;
|
||||
|
||||
fn main() {}
|
5
openapi_type/tests/fail/unknown_attribute.stderr
Normal file
5
openapi_type/tests/fail/unknown_attribute.stderr
Normal file
|
@ -0,0 +1,5 @@
|
|||
error: Unexpected token
|
||||
--> $DIR/unknown_attribute.rs:4:11
|
||||
|
|
||||
4 | #[openapi(pizza)]
|
||||
| ^^^^^
|
216
openapi_type/tests/std_types.rs
Normal file
216
openapi_type/tests/std_types.rs
Normal 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"
|
||||
}
|
||||
});
|
7
openapi_type/tests/trybuild.rs
Normal file
7
openapi_type/tests/trybuild.rs
Normal file
|
@ -0,0 +1,7 @@
|
|||
use trybuild::TestCases;
|
||||
|
||||
#[test]
|
||||
fn trybuild() {
|
||||
let t = TestCases::new();
|
||||
t.compile_fail("tests/fail/*.rs");
|
||||
}
|
19
openapi_type_derive/Cargo.toml
Normal file
19
openapi_type_derive/Cargo.toml
Normal 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"
|
143
openapi_type_derive/src/codegen.rs
Normal file
143
openapi_type_derive/src/codegen.rs
Normal 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()
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
95
openapi_type_derive/src/lib.rs
Normal file
95
openapi_type_derive/src/lib.rs
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
198
openapi_type_derive/src/parser.rs
Normal file
198
openapi_type_derive/src/parser.rs
Normal 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(())
|
||||
}
|
52
openapi_type_derive/src/util.rs
Normal file
52
openapi_type_derive/src/util.rs
Normal 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"))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
39
src/lib.rs
39
src/lib.rs
|
@ -60,7 +60,7 @@ struct FooResource;
|
|||
|
||||
/// The return type of the foo read endpoint.
|
||||
#[derive(Serialize)]
|
||||
# #[cfg_attr(feature = "openapi", derive(OpenapiType))]
|
||||
# #[cfg_attr(feature = "openapi", derive(openapi_type::OpenapiType))]
|
||||
struct Foo {
|
||||
id: u64
|
||||
}
|
||||
|
@ -95,8 +95,8 @@ use gotham_restful::gotham::hyper::Method;
|
|||
struct CustomResource;
|
||||
|
||||
/// This type is used to parse path parameters.
|
||||
#[derive(Deserialize, StateData, StaticResponseExtender)]
|
||||
# #[cfg_attr(feature = "openapi", derive(OpenapiType))]
|
||||
#[derive(Clone, Deserialize, StateData, StaticResponseExtender)]
|
||||
# #[cfg_attr(feature = "openapi", derive(openapi_type::OpenapiType))]
|
||||
struct CustomPath {
|
||||
name: String
|
||||
}
|
||||
|
@ -225,7 +225,7 @@ A simple example that uses only a single secret looks like this:
|
|||
struct SecretResource;
|
||||
|
||||
#[derive(Serialize)]
|
||||
# #[cfg_attr(feature = "openapi", derive(OpenapiType))]
|
||||
# #[cfg_attr(feature = "openapi", derive(openapi_type::OpenapiType))]
|
||||
struct Secret {
|
||||
id: u64,
|
||||
intended_for: String
|
||||
|
@ -331,7 +331,7 @@ A simple non-async example looks like this:
|
|||
struct FooResource;
|
||||
|
||||
#[derive(Queryable, Serialize)]
|
||||
# #[cfg_attr(feature = "openapi", derive(OpenapiType))]
|
||||
# #[cfg_attr(feature = "openapi", derive(openapi_type::OpenapiType))]
|
||||
struct Foo {
|
||||
id: i64,
|
||||
value: String
|
||||
|
@ -363,9 +363,9 @@ carefully both as a binary as well as a library author to avoid unwanted suprise
|
|||
|
||||
In order to automatically create an openapi specification, gotham-restful needs knowledge over
|
||||
all routes and the types returned. `serde` does a great job at serialization but doesn't give
|
||||
enough type information, so all types used in the router need to implement `OpenapiType`. This
|
||||
can be derived for almoust any type and there should be no need to implement it manually. A simple
|
||||
example looks like this:
|
||||
enough type information, so all types used in the router need to implement
|
||||
`OpenapiType`[openapi_type::OpenapiType]. This can be derived for almoust any type and there
|
||||
should be no need to implement it manually. A simple example looks like this:
|
||||
|
||||
```rust,no_run
|
||||
# #[macro_use] extern crate gotham_restful_derive;
|
||||
|
@ -373,6 +373,7 @@ example looks like this:
|
|||
# mod openapi_feature_enabled {
|
||||
# use gotham::{router::builder::*, state::State};
|
||||
# use gotham_restful::*;
|
||||
# use openapi_type::OpenapiType;
|
||||
# use serde::{Deserialize, Serialize};
|
||||
#[derive(Resource)]
|
||||
#[resource(read_all)]
|
||||
|
@ -410,17 +411,17 @@ clients in different languages without worying to exactly replicate your api in
|
|||
languages.
|
||||
|
||||
However, please note that by default, the `without-openapi` feature of this crate is enabled.
|
||||
Disabling it in favour of the `openapi` feature will add an additional type bound, [`OpenapiType`],
|
||||
on some of the types in [`Endpoint`] and related traits. This means that some code might only
|
||||
compile on either feature, but not on both. If you are writing a library that uses gotham-restful,
|
||||
it is strongly recommended to pass both features through and conditionally enable the openapi
|
||||
code, like this:
|
||||
Disabling it in favour of the `openapi` feature will add an additional type bound,
|
||||
[`OpenapiType`][openapi_type::OpenapiType], on some of the types in [`Endpoint`] and related
|
||||
traits. This means that some code might only compile on either feature, but not on both. If you
|
||||
are writing a library that uses gotham-restful, it is strongly recommended to pass both features
|
||||
through and conditionally enable the openapi code, like this:
|
||||
|
||||
```rust
|
||||
# #[macro_use] extern crate gotham_restful;
|
||||
# use serde::{Deserialize, Serialize};
|
||||
#[derive(Deserialize, Serialize)]
|
||||
#[cfg_attr(feature = "openapi", derive(OpenapiType))]
|
||||
#[cfg_attr(feature = "openapi", derive(openapi_type::OpenapiType))]
|
||||
struct Foo;
|
||||
```
|
||||
|
||||
|
@ -478,6 +479,8 @@ pub mod private {
|
|||
#[cfg(feature = "openapi")]
|
||||
pub use indexmap::IndexMap;
|
||||
#[cfg(feature = "openapi")]
|
||||
pub use openapi_type::{OpenapiSchema, OpenapiType};
|
||||
#[cfg(feature = "openapi")]
|
||||
pub use openapiv3 as openapi;
|
||||
}
|
||||
|
||||
|
@ -494,16 +497,12 @@ pub use cors::{handle_cors, CorsConfig, CorsRoute};
|
|||
#[cfg(feature = "openapi")]
|
||||
mod openapi;
|
||||
#[cfg(feature = "openapi")]
|
||||
pub use openapi::{
|
||||
builder::OpenapiInfo,
|
||||
router::GetOpenapi,
|
||||
types::{OpenapiSchema, OpenapiType}
|
||||
};
|
||||
pub use openapi::{builder::OpenapiInfo, router::GetOpenapi};
|
||||
|
||||
mod endpoint;
|
||||
pub use endpoint::Endpoint;
|
||||
#[cfg(feature = "openapi")]
|
||||
pub use endpoint::EndpointWithSchema;
|
||||
pub use endpoint::{Endpoint, NoopExtractor};
|
||||
|
||||
mod response;
|
||||
pub use response::{
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -4,4 +4,3 @@ pub mod builder;
|
|||
pub mod handler;
|
||||
pub mod operation;
|
||||
pub mod router;
|
||||
pub mod types;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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}"#);
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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};
|
||||
|
||||
/**
|
||||
|
|
|
@ -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};
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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`
|
||||
|
|
|
@ -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`
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
#[macro_use]
|
||||
extern crate gotham_restful;
|
||||
|
||||
#[derive(OpenapiType)]
|
||||
enum Food {
|
||||
Pasta,
|
||||
Pizza { pineapple: bool },
|
||||
Rice,
|
||||
Other(String)
|
||||
}
|
||||
|
||||
fn main() {}
|
|
@ -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)
|
||||
| ^^^^^
|
|
@ -1,10 +0,0 @@
|
|||
#[macro_use]
|
||||
extern crate gotham_restful;
|
||||
|
||||
#[derive(OpenapiType)]
|
||||
struct Foo {
|
||||
#[openapi(nullable = "yes, please")]
|
||||
bar: String
|
||||
}
|
||||
|
||||
fn main() {}
|
|
@ -1,5 +0,0 @@
|
|||
error: Expected bool
|
||||
--> $DIR/nullable_non_bool.rs:6:23
|
||||
|
|
||||
6 | #[openapi(nullable = "yes, please")]
|
||||
| ^^^^^^^^^^^^^
|
|
@ -1,10 +0,0 @@
|
|||
#[macro_use]
|
||||
extern crate gotham_restful;
|
||||
|
||||
#[derive(OpenapiType)]
|
||||
struct Foo {
|
||||
#[openapi(rename = 42)]
|
||||
bar: String
|
||||
}
|
||||
|
||||
fn main() {}
|
|
@ -1,5 +0,0 @@
|
|||
error: Expected string literal
|
||||
--> $DIR/rename_non_string.rs:6:21
|
||||
|
|
||||
6 | #[openapi(rename = 42)]
|
||||
| ^^
|
|
@ -1,7 +0,0 @@
|
|||
#[macro_use]
|
||||
extern crate gotham_restful;
|
||||
|
||||
#[derive(OpenapiType)]
|
||||
struct Foo(String);
|
||||
|
||||
fn main() {}
|
|
@ -1,5 +0,0 @@
|
|||
error: #[derive(OpenapiType)] does not support unnamed fields
|
||||
--> $DIR/tuple_struct.rs:5:11
|
||||
|
|
||||
5 | struct Foo(String);
|
||||
| ^^^^^^^^
|
|
@ -1,10 +0,0 @@
|
|||
#[macro_use]
|
||||
extern crate gotham_restful;
|
||||
|
||||
#[derive(OpenapiType)]
|
||||
union IntOrPointer {
|
||||
int: u64,
|
||||
pointer: *mut String
|
||||
}
|
||||
|
||||
fn main() {}
|
|
@ -1,5 +0,0 @@
|
|||
error: #[derive(OpenapiType)] only works for structs and enums
|
||||
--> $DIR/union.rs:5:1
|
||||
|
|
||||
5 | union IntOrPointer {
|
||||
| ^^^^^
|
|
@ -1,10 +0,0 @@
|
|||
#[macro_use]
|
||||
extern crate gotham_restful;
|
||||
|
||||
#[derive(OpenapiType)]
|
||||
struct Foo {
|
||||
#[openapi(like = "pizza")]
|
||||
bar: String
|
||||
}
|
||||
|
||||
fn main() {}
|
|
@ -1,5 +0,0 @@
|
|||
error: Unknown key
|
||||
--> $DIR/unknown_key.rs:6:12
|
||||
|
|
||||
6 | #[openapi(like = "pizza")]
|
||||
| ^^^^
|
Loading…
Add table
Reference in a new issue