mirror of
https://gitlab.com/msrd0/gotham-restful.git
synced 2025-04-20 06:54:46 +00:00
Compare commits
1 commit
Author | SHA1 | Date | |
---|---|---|---|
59e97f5d70 |
68 changed files with 1390 additions and 1669 deletions
|
@ -14,7 +14,8 @@ check-example:
|
||||||
before_script:
|
before_script:
|
||||||
- cargo -V
|
- cargo -V
|
||||||
script:
|
script:
|
||||||
- cargo check --manifest-path example/Cargo.toml
|
- cd example
|
||||||
|
- cargo check
|
||||||
cache:
|
cache:
|
||||||
key: cargo-stable-example
|
key: cargo-stable-example
|
||||||
paths:
|
paths:
|
||||||
|
@ -27,7 +28,6 @@ test-default:
|
||||||
before_script:
|
before_script:
|
||||||
- cargo -V
|
- cargo -V
|
||||||
script:
|
script:
|
||||||
- cargo test --manifest-path openapi_type/Cargo.toml -- --skip trybuild
|
|
||||||
- cargo test
|
- cargo test
|
||||||
cache:
|
cache:
|
||||||
key: cargo-1-49-default
|
key: cargo-1-49-default
|
||||||
|
@ -43,7 +43,6 @@ test-full:
|
||||||
- apt install -y --no-install-recommends libpq-dev
|
- apt install -y --no-install-recommends libpq-dev
|
||||||
- cargo -V
|
- cargo -V
|
||||||
script:
|
script:
|
||||||
- cargo test --manifest-path openapi_type/Cargo.toml --all-features -- --skip trybuild
|
|
||||||
- cargo test --no-default-features --features full
|
- cargo test --no-default-features --features full
|
||||||
cache:
|
cache:
|
||||||
key: cargo-1-49-all
|
key: cargo-1-49-all
|
||||||
|
@ -80,7 +79,6 @@ test-trybuild-ui:
|
||||||
- apt install -y --no-install-recommends libpq-dev
|
- apt install -y --no-install-recommends libpq-dev
|
||||||
- cargo -V
|
- cargo -V
|
||||||
script:
|
script:
|
||||||
- cargo test --manifest-path openapi_type/Cargo.toml --all-features -- trybuild
|
|
||||||
- cargo test --no-default-features --features full --tests -- --ignored
|
- cargo test --no-default-features --features full --tests -- --ignored
|
||||||
cache:
|
cache:
|
||||||
key: cargo-1-50-all
|
key: cargo-1-50-all
|
||||||
|
@ -109,9 +107,8 @@ rustfmt:
|
||||||
- cargo -V
|
- cargo -V
|
||||||
- cargo fmt --version
|
- cargo fmt --version
|
||||||
script:
|
script:
|
||||||
- cargo fmt --all -- --check
|
- cargo fmt -- --check
|
||||||
- ./tests/ui/rustfmt.sh --check
|
- ./tests/ui/rustfmt.sh --check
|
||||||
- ./openapi_type/tests/fail/rustfmt.sh --check
|
|
||||||
|
|
||||||
doc:
|
doc:
|
||||||
stage: build
|
stage: build
|
||||||
|
|
25
Cargo.toml
25
Cargo.toml
|
@ -1,11 +1,11 @@
|
||||||
# -*- eval: (cargo-minor-mode 1) -*-
|
# -*- eval: (cargo-minor-mode 1) -*-
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
members = [".", "./derive", "./example", "./openapi_type", "./openapi_type_derive"]
|
members = [".", "./derive", "./example"]
|
||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "gotham_restful"
|
name = "gotham_restful"
|
||||||
version = "0.3.0-dev"
|
version = "0.2.1"
|
||||||
authors = ["Dominic Meiser <git@msrd0.de>"]
|
authors = ["Dominic Meiser <git@msrd0.de>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
description = "RESTful additions for the gotham web framework"
|
description = "RESTful additions for the gotham web framework"
|
||||||
|
@ -22,25 +22,28 @@ gitlab = { repository = "msrd0/gotham-restful", branch = "master" }
|
||||||
[dependencies]
|
[dependencies]
|
||||||
futures-core = "0.3.7"
|
futures-core = "0.3.7"
|
||||||
futures-util = "0.3.7"
|
futures-util = "0.3.7"
|
||||||
gotham = { git = "https://github.com/gotham-rs/gotham", default-features = false }
|
gotham = { version = "0.5.0", default-features = false }
|
||||||
gotham_derive = "0.5.0"
|
gotham_derive = "0.5.0"
|
||||||
gotham_restful_derive = "0.3.0-dev"
|
gotham_restful_derive = "0.2.0"
|
||||||
log = "0.4.8"
|
log = "0.4.8"
|
||||||
mime = "0.3.16"
|
mime = "0.3.16"
|
||||||
serde = { version = "1.0.110", features = ["derive"] }
|
serde = { version = "1.0.110", features = ["derive"] }
|
||||||
serde_json = "1.0.58"
|
serde_json = "1.0.58"
|
||||||
thiserror = "1.0"
|
thiserror = "1.0"
|
||||||
|
|
||||||
|
# features
|
||||||
|
chrono = { version = "0.4.19", features = ["serde"], optional = true }
|
||||||
|
uuid = { version = "0.8.1", optional = true }
|
||||||
|
|
||||||
# non-feature optional dependencies
|
# non-feature optional dependencies
|
||||||
base64 = { version = "0.13.0", optional = true }
|
base64 = { version = "0.13.0", optional = true }
|
||||||
cookie = { version = "0.15", optional = true }
|
cookie = { version = "0.14", optional = true }
|
||||||
gotham_middleware_diesel = { git = "https://github.com/gotham-rs/gotham", optional = true }
|
gotham_middleware_diesel = { version = "0.2.0", optional = true }
|
||||||
indexmap = { version = "1.3.2", optional = true }
|
indexmap = { version = "1.3.2", optional = true }
|
||||||
indoc = { version = "1.0", optional = true }
|
indoc = { version = "1.0", optional = true }
|
||||||
jsonwebtoken = { version = "7.1.0", optional = true }
|
jsonwebtoken = { version = "7.1.0", optional = true }
|
||||||
once_cell = { version = "1.5", optional = true }
|
once_cell = { version = "1.5", optional = true }
|
||||||
openapiv3 = { version = "=0.3.2", optional = true }
|
openapiv3 = { version = "=0.3.2", optional = true }
|
||||||
openapi_type = { version = "0.1.0-dev", optional = true }
|
|
||||||
regex = { version = "1.4", optional = true }
|
regex = { version = "1.4", optional = true }
|
||||||
sha2 = { version = "0.9.3", optional = true }
|
sha2 = { version = "0.9.3", optional = true }
|
||||||
|
|
||||||
|
@ -49,13 +52,13 @@ diesel = { version = "1.4.4", features = ["postgres"] }
|
||||||
futures-executor = "0.3.5"
|
futures-executor = "0.3.5"
|
||||||
paste = "1.0"
|
paste = "1.0"
|
||||||
pretty_env_logger = "0.4"
|
pretty_env_logger = "0.4"
|
||||||
tokio = { version = "1.0", features = ["time"], default-features = false }
|
tokio = { version = "0.2", features = ["time"], default-features = false }
|
||||||
thiserror = "1.0.18"
|
thiserror = "1.0.18"
|
||||||
trybuild = "1.0.27"
|
trybuild = "1.0.27"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["cors", "errorlog", "without-openapi"]
|
default = ["cors", "errorlog", "without-openapi"]
|
||||||
full = ["auth", "cors", "database", "errorlog", "openapi"]
|
full = ["auth", "chrono", "cors", "database", "errorlog", "openapi", "uuid"]
|
||||||
|
|
||||||
auth = ["gotham_restful_derive/auth", "base64", "cookie", "jsonwebtoken"]
|
auth = ["gotham_restful_derive/auth", "base64", "cookie", "jsonwebtoken"]
|
||||||
cors = []
|
cors = []
|
||||||
|
@ -64,7 +67,7 @@ errorlog = []
|
||||||
|
|
||||||
# These features are exclusive - https://gitlab.com/msrd0/gotham-restful/-/issues/4
|
# These features are exclusive - https://gitlab.com/msrd0/gotham-restful/-/issues/4
|
||||||
without-openapi = []
|
without-openapi = []
|
||||||
openapi = ["gotham_restful_derive/openapi", "base64", "indexmap", "indoc", "once_cell", "openapiv3", "openapi_type", "regex", "sha2"]
|
openapi = ["gotham_restful_derive/openapi", "base64", "indexmap", "indoc", "once_cell", "openapiv3", "regex", "sha2"]
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
[package.metadata.docs.rs]
|
||||||
no-default-features = true
|
no-default-features = true
|
||||||
|
@ -73,5 +76,3 @@ features = ["full"]
|
||||||
[patch.crates-io]
|
[patch.crates-io]
|
||||||
gotham_restful = { path = "." }
|
gotham_restful = { path = "." }
|
||||||
gotham_restful_derive = { path = "./derive" }
|
gotham_restful_derive = { path = "./derive" }
|
||||||
openapi_type = { path = "./openapi_type" }
|
|
||||||
openapi_type_derive = { path = "./openapi_type_derive" }
|
|
||||||
|
|
399
README.md
399
README.md
|
@ -1,4 +1,399 @@
|
||||||
# Moved to GitHub
|
<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/>
|
||||||
|
|
||||||
This project has moved to GitHub: https://github.com/msrd0/gotham_restful
|
This crate is an extension to the popular [gotham web framework][gotham] for Rust. It allows you to
|
||||||
|
create resources with assigned endpoints that aim to be a more convenient way of creating handlers
|
||||||
|
for requests.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Automatically parse **JSON** request and produce response bodies
|
||||||
|
- Allow using **raw** request and response bodies
|
||||||
|
- Convenient **macros** to create responses that can be registered with gotham's router
|
||||||
|
- Auto-Generate an **OpenAPI** specification for your API
|
||||||
|
- Manage **CORS** headers so you don't have to
|
||||||
|
- Manage **Authentication** with JWT
|
||||||
|
- Integrate diesel connection pools for easy **database** integration
|
||||||
|
|
||||||
|
## Safety
|
||||||
|
|
||||||
|
This crate is just as safe as you'd expect from anything written in safe Rust - and
|
||||||
|
`#![forbid(unsafe_code)]` ensures that no unsafe was used.
|
||||||
|
|
||||||
|
## Endpoints
|
||||||
|
|
||||||
|
There are a set of pre-defined endpoints that should cover the majority of REST APIs. However,
|
||||||
|
it is also possible to define your own endpoints.
|
||||||
|
|
||||||
|
### Pre-defined Endpoints
|
||||||
|
|
||||||
|
Assuming you assign `/foobar` to your resource, the following pre-defined endpoints exist:
|
||||||
|
|
||||||
|
| Endpoint Name | Required Arguments | HTTP Verb | HTTP Path |
|
||||||
|
| ------------- | ------------------ | --------- | -------------- |
|
||||||
|
| read_all | | GET | /foobar |
|
||||||
|
| read | id | GET | /foobar/:id |
|
||||||
|
| search | query | GET | /foobar/search |
|
||||||
|
| create | body | POST | /foobar |
|
||||||
|
| change_all | body | PUT | /foobar |
|
||||||
|
| change | id, body | PUT | /foobar/:id |
|
||||||
|
| remove_all | | DELETE | /foobar |
|
||||||
|
| remove | id | DELETE | /foobar/:id |
|
||||||
|
|
||||||
|
Each of those endpoints has a macro that creates the neccessary boilerplate for the Resource. A
|
||||||
|
simple example looks like this:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
/// Our RESTful resource.
|
||||||
|
#[derive(Resource)]
|
||||||
|
#[resource(read)]
|
||||||
|
struct FooResource;
|
||||||
|
|
||||||
|
/// The return type of the foo read endpoint.
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct Foo {
|
||||||
|
id: u64
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The foo read endpoint.
|
||||||
|
#[read]
|
||||||
|
fn read(id: u64) -> Success<Foo> {
|
||||||
|
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,10 +1,18 @@
|
||||||
<br/>
|
<div align="center">
|
||||||
<div>
|
<h1>gotham-restful</h1>
|
||||||
|
</div>
|
||||||
|
<div align="center">
|
||||||
<a href="https://gitlab.com/msrd0/gotham-restful/pipelines">
|
<a href="https://gitlab.com/msrd0/gotham-restful/pipelines">
|
||||||
<img alt="pipeline status" src="https://gitlab.com/msrd0/gotham-restful/badges/master/pipeline.svg"/>
|
<img alt="pipeline status" src="https://gitlab.com/msrd0/gotham-restful/badges/master/pipeline.svg"/>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://msrd0.gitlab.io/gotham-restful/coverage.html">
|
<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"/>
|
<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>
|
||||||
<a href="https://msrd0.gitlab.io/gotham-restful/gotham_restful/index.html">
|
<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"/>
|
<img alt="rustdoc" src="https://img.shields.io/badge/docs-master-blue.svg"/>
|
||||||
|
@ -18,23 +26,6 @@
|
||||||
</div>
|
</div>
|
||||||
<br/>
|
<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}}
|
{{readme}}
|
||||||
|
|
||||||
## Versioning
|
## Versioning
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "gotham_restful_derive"
|
name = "gotham_restful_derive"
|
||||||
version = "0.3.0-dev"
|
version = "0.2.0"
|
||||||
authors = ["Dominic Meiser <git@msrd0.de>"]
|
authors = ["Dominic Meiser <git@msrd0.de>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
description = "Derive macros for gotham_restful"
|
description = "Derive macros for gotham_restful"
|
||||||
|
|
|
@ -128,14 +128,14 @@ impl EndpointType {
|
||||||
fn placeholders_ty(&self, arg_ty: Option<&Type>) -> TokenStream {
|
fn placeholders_ty(&self, arg_ty: Option<&Type>) -> TokenStream {
|
||||||
match self {
|
match self {
|
||||||
Self::ReadAll | Self::Search | Self::Create | Self::UpdateAll | Self::DeleteAll => {
|
Self::ReadAll | Self::Search | Self::Create | Self::UpdateAll | Self::DeleteAll => {
|
||||||
quote!(::gotham_restful::NoopExtractor)
|
quote!(::gotham_restful::gotham::extractor::NoopPathExtractor)
|
||||||
},
|
},
|
||||||
Self::Read | Self::Update | Self::Delete => quote!(::gotham_restful::private::IdPlaceholder::<#arg_ty>),
|
Self::Read | Self::Update | Self::Delete => quote!(::gotham_restful::private::IdPlaceholder::<#arg_ty>),
|
||||||
Self::Custom { .. } => {
|
Self::Custom { .. } => {
|
||||||
if self.has_placeholders().value {
|
if self.has_placeholders().value {
|
||||||
arg_ty.to_token_stream()
|
arg_ty.to_token_stream()
|
||||||
} else {
|
} else {
|
||||||
quote!(::gotham_restful::NoopExtractor)
|
quote!(::gotham_restful::gotham::extractor::NoopPathExtractor)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -163,14 +163,14 @@ impl EndpointType {
|
||||||
fn params_ty(&self, arg_ty: Option<&Type>) -> TokenStream {
|
fn params_ty(&self, arg_ty: Option<&Type>) -> TokenStream {
|
||||||
match self {
|
match self {
|
||||||
Self::ReadAll | Self::Read | Self::Create | Self::UpdateAll | Self::Update | Self::DeleteAll | Self::Delete => {
|
Self::ReadAll | Self::Read | Self::Create | Self::UpdateAll | Self::Update | Self::DeleteAll | Self::Delete => {
|
||||||
quote!(::gotham_restful::NoopExtractor)
|
quote!(::gotham_restful::gotham::extractor::NoopQueryStringExtractor)
|
||||||
},
|
},
|
||||||
Self::Search => quote!(#arg_ty),
|
Self::Search => quote!(#arg_ty),
|
||||||
Self::Custom { .. } => {
|
Self::Custom { .. } => {
|
||||||
if self.needs_params().value {
|
if self.needs_params().value {
|
||||||
arg_ty.to_token_stream()
|
arg_ty.to_token_stream()
|
||||||
} else {
|
} else {
|
||||||
quote!(::gotham_restful::NoopExtractor)
|
quote!(::gotham_restful::gotham::extractor::NoopQueryStringExtractor)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -201,7 +201,7 @@ impl EndpointType {
|
||||||
if self.needs_body().value {
|
if self.needs_body().value {
|
||||||
arg_ty.to_token_stream()
|
arg_ty.to_token_stream()
|
||||||
} else {
|
} else {
|
||||||
quote!(())
|
quote!(::gotham_restful::gotham::extractor::NoopPathExtractor)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,6 +24,11 @@ use resource::expand_resource;
|
||||||
mod resource_error;
|
mod resource_error;
|
||||||
use resource_error::expand_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;
|
mod private_openapi_trait;
|
||||||
use private_openapi_trait::expand_private_openapi_trait;
|
use private_openapi_trait::expand_private_openapi_trait;
|
||||||
|
|
||||||
|
@ -61,6 +66,12 @@ pub fn derive_from_body(input: TokenStream) -> TokenStream {
|
||||||
expand_derive(input, expand_from_body)
|
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))]
|
#[proc_macro_derive(RequestBody, attributes(supported_types))]
|
||||||
pub fn derive_request_body(input: TokenStream) -> TokenStream {
|
pub fn derive_request_body(input: TokenStream) -> TokenStream {
|
||||||
expand_derive(input, expand_request_body)
|
expand_derive(input, expand_request_body)
|
||||||
|
|
289
derive/src/openapi_type.rs
Normal file
289
derive/src/openapi_type.rs
Normal file
|
@ -0,0 +1,289 @@
|
||||||
|
use crate::util::{remove_parens, CollectToResult};
|
||||||
|
use proc_macro2::{Ident, TokenStream};
|
||||||
|
use quote::quote;
|
||||||
|
use syn::{
|
||||||
|
parse_macro_input, spanned::Spanned, Attribute, AttributeArgs, Data, DataEnum, DataStruct, DeriveInput, Error, Field,
|
||||||
|
Fields, GenericParam, Generics, Lit, LitStr, Meta, NestedMeta, Path, PathSegment, PredicateType, Result, TraitBound,
|
||||||
|
TraitBoundModifier, Type, TypeParamBound, TypePath, Variant, WhereClause, WherePredicate
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn expand_openapi_type(input: DeriveInput) -> Result<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,25 +26,18 @@ fn impl_openapi_type(_ident: &Ident, _generics: &Generics) -> TokenStream {
|
||||||
#[cfg(feature = "openapi")]
|
#[cfg(feature = "openapi")]
|
||||||
fn impl_openapi_type(ident: &Ident, generics: &Generics) -> TokenStream {
|
fn impl_openapi_type(ident: &Ident, generics: &Generics) -> TokenStream {
|
||||||
let krate = super::krate();
|
let krate = super::krate();
|
||||||
let openapi = quote!(#krate::private::openapi);
|
|
||||||
|
|
||||||
quote! {
|
quote! {
|
||||||
impl #generics #krate::private::OpenapiType for #ident #generics
|
impl #generics #krate::OpenapiType for #ident #generics
|
||||||
{
|
{
|
||||||
fn schema() -> #krate::private::OpenapiSchema
|
fn schema() -> #krate::OpenapiSchema
|
||||||
{
|
{
|
||||||
#krate::private::OpenapiSchema::new(
|
use #krate::{private::openapi::*, OpenapiSchema};
|
||||||
#openapi::SchemaKind::Type(
|
|
||||||
#openapi::Type::String(
|
OpenapiSchema::new(SchemaKind::Type(Type::String(StringType {
|
||||||
#openapi::StringType {
|
format: VariantOrUnknownOrEmpty::Item(StringFormat::Binary),
|
||||||
format: #openapi::VariantOrUnknownOrEmpty::Item(
|
..Default::default()
|
||||||
#openapi::StringFormat::Binary
|
})))
|
||||||
),
|
|
||||||
.. ::std::default::Default::default()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,27 +0,0 @@
|
||||||
# -*- 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"
|
|
|
@ -1,224 +0,0 @@
|
||||||
use crate::{OpenapiSchema, OpenapiType};
|
|
||||||
#[cfg(feature = "chrono")]
|
|
||||||
use chrono::{offset::TimeZone, Date, DateTime, NaiveDate, NaiveDateTime};
|
|
||||||
use indexmap::{IndexMap, IndexSet};
|
|
||||||
use openapiv3::{
|
|
||||||
AdditionalProperties, ArrayType, IntegerType, NumberFormat, NumberType, ObjectType, ReferenceOr, SchemaKind,
|
|
||||||
StringFormat, StringType, Type, VariantOrUnknownOrEmpty
|
|
||||||
};
|
|
||||||
use serde_json::Value;
|
|
||||||
use std::{
|
|
||||||
collections::{BTreeMap, BTreeSet, HashMap, HashSet},
|
|
||||||
hash::BuildHasher,
|
|
||||||
num::{NonZeroU128, NonZeroU16, NonZeroU32, NonZeroU64, NonZeroU8, NonZeroUsize}
|
|
||||||
};
|
|
||||||
#[cfg(feature = "uuid")]
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
macro_rules! impl_openapi_type {
|
|
||||||
($($ty:ident $(<$($generic:ident : $bound:path),+>)*),* => $schema:expr) => {
|
|
||||||
$(
|
|
||||||
impl $(<$($generic : $bound),+>)* OpenapiType for $ty $(<$($generic),+>)* {
|
|
||||||
fn schema() -> OpenapiSchema {
|
|
||||||
$schema
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)*
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
type Unit = ();
|
|
||||||
impl_openapi_type!(Unit => {
|
|
||||||
OpenapiSchema::new(SchemaKind::Type(Type::Object(ObjectType {
|
|
||||||
additional_properties: Some(AdditionalProperties::Any(false)),
|
|
||||||
..Default::default()
|
|
||||||
})))
|
|
||||||
});
|
|
||||||
|
|
||||||
impl_openapi_type!(Value => {
|
|
||||||
OpenapiSchema {
|
|
||||||
nullable: true,
|
|
||||||
name: None,
|
|
||||||
schema: SchemaKind::Any(Default::default()),
|
|
||||||
dependencies: Default::default()
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
impl_openapi_type!(bool => OpenapiSchema::new(SchemaKind::Type(Type::Boolean {})));
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn int_schema(minimum: Option<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>()
|
|
||||||
);
|
|
|
@ -1,86 +0,0 @@
|
||||||
#![warn(missing_debug_implementations, rust_2018_idioms)]
|
|
||||||
#![forbid(unsafe_code)]
|
|
||||||
#![cfg_attr(feature = "cargo-clippy", allow(clippy::tabs_in_doc_comments))]
|
|
||||||
/*!
|
|
||||||
TODO
|
|
||||||
*/
|
|
||||||
|
|
||||||
pub use indexmap;
|
|
||||||
pub use openapi_type_derive::OpenapiType;
|
|
||||||
pub use openapiv3 as openapi;
|
|
||||||
|
|
||||||
mod impls;
|
|
||||||
#[doc(hidden)]
|
|
||||||
pub mod private;
|
|
||||||
|
|
||||||
use indexmap::IndexMap;
|
|
||||||
use openapi::{Schema, SchemaData, SchemaKind};
|
|
||||||
|
|
||||||
// TODO update the documentation
|
|
||||||
/**
|
|
||||||
This struct needs to be available for every type that can be part of an OpenAPI Spec. It is
|
|
||||||
already implemented for primitive types, String, Vec, Option and the like. To have it available
|
|
||||||
for your type, simply derive from [OpenapiType].
|
|
||||||
*/
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
|
||||||
pub struct OpenapiSchema {
|
|
||||||
/// The name of this schema. If it is None, the schema will be inlined.
|
|
||||||
pub name: Option<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()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,12 +0,0 @@
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,249 +0,0 @@
|
||||||
#![allow(dead_code)]
|
|
||||||
use openapi_type::OpenapiType;
|
|
||||||
|
|
||||||
macro_rules! test_type {
|
|
||||||
($ty:ty = $json:tt) => {
|
|
||||||
paste::paste! {
|
|
||||||
#[test]
|
|
||||||
fn [< $ty:lower >]() {
|
|
||||||
let schema = <$ty as OpenapiType>::schema();
|
|
||||||
let schema = openapi_type::OpenapiSchema::into_schema(schema);
|
|
||||||
let schema_json = serde_json::to_value(&schema).unwrap();
|
|
||||||
let expected = serde_json::json!($json);
|
|
||||||
assert_eq!(schema_json, expected);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(OpenapiType)]
|
|
||||||
struct UnitStruct;
|
|
||||||
test_type!(UnitStruct = {
|
|
||||||
"type": "object",
|
|
||||||
"title": "UnitStruct",
|
|
||||||
"additionalProperties": false
|
|
||||||
});
|
|
||||||
|
|
||||||
#[derive(OpenapiType)]
|
|
||||||
struct SimpleStruct {
|
|
||||||
foo: String,
|
|
||||||
bar: isize
|
|
||||||
}
|
|
||||||
test_type!(SimpleStruct = {
|
|
||||||
"type": "object",
|
|
||||||
"title": "SimpleStruct",
|
|
||||||
"properties": {
|
|
||||||
"foo": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"bar": {
|
|
||||||
"type": "integer"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["foo", "bar"]
|
|
||||||
});
|
|
||||||
|
|
||||||
#[derive(OpenapiType)]
|
|
||||||
#[openapi(rename = "FooBar")]
|
|
||||||
struct StructRename;
|
|
||||||
test_type!(StructRename = {
|
|
||||||
"type": "object",
|
|
||||||
"title": "FooBar",
|
|
||||||
"additionalProperties": false
|
|
||||||
});
|
|
||||||
|
|
||||||
#[derive(OpenapiType)]
|
|
||||||
enum EnumWithoutFields {
|
|
||||||
Success,
|
|
||||||
Error
|
|
||||||
}
|
|
||||||
test_type!(EnumWithoutFields = {
|
|
||||||
"type": "string",
|
|
||||||
"title": "EnumWithoutFields",
|
|
||||||
"enum": [
|
|
||||||
"Success",
|
|
||||||
"Error"
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
#[derive(OpenapiType)]
|
|
||||||
enum EnumWithOneField {
|
|
||||||
Success { value: isize }
|
|
||||||
}
|
|
||||||
test_type!(EnumWithOneField = {
|
|
||||||
"type": "object",
|
|
||||||
"title": "EnumWithOneField",
|
|
||||||
"properties": {
|
|
||||||
"Success": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"value": {
|
|
||||||
"type": "integer"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["value"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["Success"]
|
|
||||||
});
|
|
||||||
|
|
||||||
#[derive(OpenapiType)]
|
|
||||||
enum EnumWithFields {
|
|
||||||
Success { value: isize },
|
|
||||||
Error { msg: String }
|
|
||||||
}
|
|
||||||
test_type!(EnumWithFields = {
|
|
||||||
"title": "EnumWithFields",
|
|
||||||
"oneOf": [{
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"Success": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"value": {
|
|
||||||
"type": "integer"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["value"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["Success"]
|
|
||||||
}, {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"Error": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"msg": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["msg"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["Error"]
|
|
||||||
}]
|
|
||||||
});
|
|
||||||
|
|
||||||
#[derive(OpenapiType)]
|
|
||||||
enum EnumExternallyTagged {
|
|
||||||
Success { value: isize },
|
|
||||||
Empty,
|
|
||||||
Error
|
|
||||||
}
|
|
||||||
test_type!(EnumExternallyTagged = {
|
|
||||||
"title": "EnumExternallyTagged",
|
|
||||||
"oneOf": [{
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"Success": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"value": {
|
|
||||||
"type": "integer"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["value"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["Success"]
|
|
||||||
}, {
|
|
||||||
"type": "string",
|
|
||||||
"enum": ["Empty", "Error"]
|
|
||||||
}]
|
|
||||||
});
|
|
||||||
|
|
||||||
#[derive(OpenapiType)]
|
|
||||||
#[openapi(tag = "ty")]
|
|
||||||
enum EnumInternallyTagged {
|
|
||||||
Success { value: isize },
|
|
||||||
Empty,
|
|
||||||
Error
|
|
||||||
}
|
|
||||||
test_type!(EnumInternallyTagged = {
|
|
||||||
"title": "EnumInternallyTagged",
|
|
||||||
"oneOf": [{
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"value": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"ty": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": ["Success"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["value", "ty"]
|
|
||||||
}, {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"ty": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": ["Empty", "Error"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["ty"]
|
|
||||||
}]
|
|
||||||
});
|
|
||||||
|
|
||||||
#[derive(OpenapiType)]
|
|
||||||
#[openapi(tag = "ty", content = "ct")]
|
|
||||||
enum EnumAdjacentlyTagged {
|
|
||||||
Success { value: isize },
|
|
||||||
Empty,
|
|
||||||
Error
|
|
||||||
}
|
|
||||||
test_type!(EnumAdjacentlyTagged = {
|
|
||||||
"title": "EnumAdjacentlyTagged",
|
|
||||||
"oneOf": [{
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"ty": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": ["Success"]
|
|
||||||
},
|
|
||||||
"ct": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"value": {
|
|
||||||
"type": "integer"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["value"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["ty", "ct"]
|
|
||||||
}, {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"ty": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": ["Empty", "Error"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["ty"]
|
|
||||||
}]
|
|
||||||
});
|
|
||||||
|
|
||||||
#[derive(OpenapiType)]
|
|
||||||
#[openapi(untagged)]
|
|
||||||
enum EnumUntagged {
|
|
||||||
Success { value: isize },
|
|
||||||
Empty,
|
|
||||||
Error
|
|
||||||
}
|
|
||||||
test_type!(EnumUntagged = {
|
|
||||||
"title": "EnumUntagged",
|
|
||||||
"oneOf": [{
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"value": {
|
|
||||||
"type": "integer"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["value"]
|
|
||||||
}, {
|
|
||||||
"type": "object",
|
|
||||||
"additionalProperties": false
|
|
||||||
}]
|
|
||||||
});
|
|
|
@ -1,6 +0,0 @@
|
||||||
use openapi_type::OpenapiType;
|
|
||||||
|
|
||||||
#[derive(OpenapiType)]
|
|
||||||
enum Foo {}
|
|
||||||
|
|
||||||
fn main() {}
|
|
|
@ -1,5 +0,0 @@
|
||||||
error: #[derive(OpenapiType)] does not support enums with no variants
|
|
||||||
--> $DIR/enum_with_no_variants.rs:4:10
|
|
||||||
|
|
|
||||||
4 | enum Foo {}
|
|
||||||
| ^^
|
|
|
@ -1,12 +0,0 @@
|
||||||
use openapi_type::OpenapiType;
|
|
||||||
|
|
||||||
#[derive(OpenapiType)]
|
|
||||||
struct Foo {
|
|
||||||
bar: Bar
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Bar;
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
Foo::schema();
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
error[E0277]: the trait bound `Bar: OpenapiType` is not satisfied
|
|
||||||
--> $DIR/not_openapitype.rs:3:10
|
|
||||||
|
|
|
||||||
3 | #[derive(OpenapiType)]
|
|
||||||
| ^^^^^^^^^^^ the trait `OpenapiType` is not implemented for `Bar`
|
|
||||||
|
|
|
||||||
= note: required by `schema`
|
|
||||||
= note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info)
|
|
|
@ -1,12 +0,0 @@
|
||||||
use openapi_type::OpenapiType;
|
|
||||||
|
|
||||||
#[derive(OpenapiType)]
|
|
||||||
struct Foo<T> {
|
|
||||||
bar: T
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Bar;
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
<Foo<Bar>>::schema();
|
|
||||||
}
|
|
|
@ -1,23 +0,0 @@
|
||||||
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`
|
|
|
@ -1,21 +0,0 @@
|
||||||
#!/bin/busybox ash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
rustfmt=${RUSTFMT:-rustfmt}
|
|
||||||
version="$($rustfmt -V)"
|
|
||||||
case "$version" in
|
|
||||||
*nightly*)
|
|
||||||
# all good, no additional flags required
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
# assume we're using some sort of rustup setup
|
|
||||||
rustfmt="$rustfmt +nightly"
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
return=0
|
|
||||||
find "$(dirname "$0")" -name '*.rs' -type f | while read file; do
|
|
||||||
$rustfmt --config-path "$(dirname "$0")/../../../rustfmt.toml" "$@" "$file" || return=1
|
|
||||||
done
|
|
||||||
|
|
||||||
exit $return
|
|
|
@ -1,6 +0,0 @@
|
||||||
use openapi_type::OpenapiType;
|
|
||||||
|
|
||||||
#[derive(OpenapiType)]
|
|
||||||
struct Foo(i64, i64);
|
|
||||||
|
|
||||||
fn main() {}
|
|
|
@ -1,5 +0,0 @@
|
||||||
error: #[derive(OpenapiType)] does not support tuple structs
|
|
||||||
--> $DIR/tuple_struct.rs:4:11
|
|
||||||
|
|
|
||||||
4 | struct Foo(i64, i64);
|
|
||||||
| ^^^^^^^^^^
|
|
|
@ -1,8 +0,0 @@
|
||||||
use openapi_type::OpenapiType;
|
|
||||||
|
|
||||||
#[derive(OpenapiType)]
|
|
||||||
enum Foo {
|
|
||||||
Pair(i64, i64)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() {}
|
|
|
@ -1,5 +0,0 @@
|
||||||
error: #[derive(OpenapiType)] does not support tuple variants
|
|
||||||
--> $DIR/tuple_variant.rs:5:6
|
|
||||||
|
|
|
||||||
5 | Pair(i64, i64)
|
|
||||||
| ^^^^^^^^^^
|
|
|
@ -1,9 +0,0 @@
|
||||||
use openapi_type::OpenapiType;
|
|
||||||
|
|
||||||
#[derive(OpenapiType)]
|
|
||||||
union Foo {
|
|
||||||
signed: i64,
|
|
||||||
unsigned: u64
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() {}
|
|
|
@ -1,5 +0,0 @@
|
||||||
error: #[derive(OpenapiType)] cannot be used on unions
|
|
||||||
--> $DIR/union.rs:4:1
|
|
||||||
|
|
|
||||||
4 | union Foo {
|
|
||||||
| ^^^^^
|
|
|
@ -1,7 +0,0 @@
|
||||||
use openapi_type::OpenapiType;
|
|
||||||
|
|
||||||
#[derive(OpenapiType)]
|
|
||||||
#[openapi(pizza)]
|
|
||||||
struct Foo;
|
|
||||||
|
|
||||||
fn main() {}
|
|
|
@ -1,5 +0,0 @@
|
||||||
error: Unexpected token
|
|
||||||
--> $DIR/unknown_attribute.rs:4:11
|
|
||||||
|
|
|
||||||
4 | #[openapi(pizza)]
|
|
||||||
| ^^^^^
|
|
|
@ -1,216 +0,0 @@
|
||||||
#[cfg(feature = "chrono")]
|
|
||||||
use chrono::{Date, DateTime, FixedOffset, Local, NaiveDate, NaiveDateTime, Utc};
|
|
||||||
use indexmap::{IndexMap, IndexSet};
|
|
||||||
use openapi_type::OpenapiType;
|
|
||||||
use serde_json::Value;
|
|
||||||
use std::{
|
|
||||||
collections::{BTreeMap, BTreeSet, HashMap, HashSet},
|
|
||||||
num::{NonZeroU128, NonZeroU16, NonZeroU32, NonZeroU64, NonZeroU8, NonZeroUsize}
|
|
||||||
};
|
|
||||||
#[cfg(feature = "uuid")]
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
macro_rules! test_type {
|
|
||||||
($($ty:ident $(<$($generic:ident),+>)*),* = $json:tt) => {
|
|
||||||
paste::paste! { $(
|
|
||||||
#[test]
|
|
||||||
fn [< $ty:lower $($(_ $generic:lower)+)* >]() {
|
|
||||||
let schema = <$ty $(<$($generic),+>)* as OpenapiType>::schema();
|
|
||||||
let schema = openapi_type::OpenapiSchema::into_schema(schema);
|
|
||||||
let schema_json = serde_json::to_value(&schema).unwrap();
|
|
||||||
let expected = serde_json::json!($json);
|
|
||||||
assert_eq!(schema_json, expected);
|
|
||||||
}
|
|
||||||
)* }
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
type Unit = ();
|
|
||||||
test_type!(Unit = {
|
|
||||||
"type": "object",
|
|
||||||
"additionalProperties": false
|
|
||||||
});
|
|
||||||
|
|
||||||
test_type!(Value = {
|
|
||||||
"nullable": true
|
|
||||||
});
|
|
||||||
|
|
||||||
test_type!(bool = {
|
|
||||||
"type": "boolean"
|
|
||||||
});
|
|
||||||
|
|
||||||
// ### integer types
|
|
||||||
|
|
||||||
test_type!(isize = {
|
|
||||||
"type": "integer"
|
|
||||||
});
|
|
||||||
|
|
||||||
test_type!(usize = {
|
|
||||||
"type": "integer",
|
|
||||||
"minimum": 0
|
|
||||||
});
|
|
||||||
|
|
||||||
test_type!(i8 = {
|
|
||||||
"type": "integer",
|
|
||||||
"format": "int8"
|
|
||||||
});
|
|
||||||
|
|
||||||
test_type!(u8 = {
|
|
||||||
"type": "integer",
|
|
||||||
"format": "int8",
|
|
||||||
"minimum": 0
|
|
||||||
});
|
|
||||||
|
|
||||||
test_type!(i16 = {
|
|
||||||
"type": "integer",
|
|
||||||
"format": "int16"
|
|
||||||
});
|
|
||||||
|
|
||||||
test_type!(u16 = {
|
|
||||||
"type": "integer",
|
|
||||||
"format": "int16",
|
|
||||||
"minimum": 0
|
|
||||||
});
|
|
||||||
|
|
||||||
test_type!(i32 = {
|
|
||||||
"type": "integer",
|
|
||||||
"format": "int32"
|
|
||||||
});
|
|
||||||
|
|
||||||
test_type!(u32 = {
|
|
||||||
"type": "integer",
|
|
||||||
"format": "int32",
|
|
||||||
"minimum": 0
|
|
||||||
});
|
|
||||||
|
|
||||||
test_type!(i64 = {
|
|
||||||
"type": "integer",
|
|
||||||
"format": "int64"
|
|
||||||
});
|
|
||||||
|
|
||||||
test_type!(u64 = {
|
|
||||||
"type": "integer",
|
|
||||||
"format": "int64",
|
|
||||||
"minimum": 0
|
|
||||||
});
|
|
||||||
|
|
||||||
test_type!(i128 = {
|
|
||||||
"type": "integer",
|
|
||||||
"format": "int128"
|
|
||||||
});
|
|
||||||
|
|
||||||
test_type!(u128 = {
|
|
||||||
"type": "integer",
|
|
||||||
"format": "int128",
|
|
||||||
"minimum": 0
|
|
||||||
});
|
|
||||||
|
|
||||||
// ### non-zero integer types
|
|
||||||
|
|
||||||
test_type!(NonZeroUsize = {
|
|
||||||
"type": "integer",
|
|
||||||
"minimum": 1
|
|
||||||
});
|
|
||||||
|
|
||||||
test_type!(NonZeroU8 = {
|
|
||||||
"type": "integer",
|
|
||||||
"format": "int8",
|
|
||||||
"minimum": 1
|
|
||||||
});
|
|
||||||
|
|
||||||
test_type!(NonZeroU16 = {
|
|
||||||
"type": "integer",
|
|
||||||
"format": "int16",
|
|
||||||
"minimum": 1
|
|
||||||
});
|
|
||||||
|
|
||||||
test_type!(NonZeroU32 = {
|
|
||||||
"type": "integer",
|
|
||||||
"format": "int32",
|
|
||||||
"minimum": 1
|
|
||||||
});
|
|
||||||
|
|
||||||
test_type!(NonZeroU64 = {
|
|
||||||
"type": "integer",
|
|
||||||
"format": "int64",
|
|
||||||
"minimum": 1
|
|
||||||
});
|
|
||||||
|
|
||||||
test_type!(NonZeroU128 = {
|
|
||||||
"type": "integer",
|
|
||||||
"format": "int128",
|
|
||||||
"minimum": 1
|
|
||||||
});
|
|
||||||
|
|
||||||
// ### floats
|
|
||||||
|
|
||||||
test_type!(f32 = {
|
|
||||||
"type": "number",
|
|
||||||
"format": "float"
|
|
||||||
});
|
|
||||||
|
|
||||||
test_type!(f64 = {
|
|
||||||
"type": "number",
|
|
||||||
"format": "double"
|
|
||||||
});
|
|
||||||
|
|
||||||
// ### string
|
|
||||||
|
|
||||||
test_type!(String = {
|
|
||||||
"type": "string"
|
|
||||||
});
|
|
||||||
|
|
||||||
#[cfg(feature = "uuid")]
|
|
||||||
test_type!(Uuid = {
|
|
||||||
"type": "string",
|
|
||||||
"format": "uuid"
|
|
||||||
});
|
|
||||||
|
|
||||||
// ### date/time
|
|
||||||
|
|
||||||
#[cfg(feature = "chrono")]
|
|
||||||
test_type!(Date<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"
|
|
||||||
}
|
|
||||||
});
|
|
|
@ -1,7 +0,0 @@
|
||||||
use trybuild::TestCases;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn trybuild() {
|
|
||||||
let t = TestCases::new();
|
|
||||||
t.compile_fail("tests/fail/*.rs");
|
|
||||||
}
|
|
|
@ -1,19 +0,0 @@
|
||||||
# -*- 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"
|
|
|
@ -1,143 +0,0 @@
|
||||||
use crate::parser::{ParseData, ParseDataType};
|
|
||||||
use proc_macro2::TokenStream;
|
|
||||||
use quote::quote;
|
|
||||||
use syn::LitStr;
|
|
||||||
|
|
||||||
impl ParseData {
|
|
||||||
pub(super) fn gen_schema(&self) -> TokenStream {
|
|
||||||
match self {
|
|
||||||
Self::Struct(fields) => gen_struct(fields),
|
|
||||||
Self::Enum(variants) => gen_enum(variants),
|
|
||||||
Self::Alternatives(alt) => gen_alt(alt),
|
|
||||||
Self::Unit => gen_unit()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn gen_struct(fields: &[(LitStr, ParseDataType)]) -> TokenStream {
|
|
||||||
let field_name = fields.iter().map(|(name, _)| name);
|
|
||||||
let field_schema = fields.iter().map(|(_, ty)| match ty {
|
|
||||||
ParseDataType::Type(ty) => {
|
|
||||||
quote!(<#ty as ::openapi_type::OpenapiType>::schema())
|
|
||||||
},
|
|
||||||
ParseDataType::Inline(data) => {
|
|
||||||
let code = data.gen_schema();
|
|
||||||
quote!(::openapi_type::OpenapiSchema::new(#code))
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let openapi = path!(::openapi_type::openapi);
|
|
||||||
quote! {
|
|
||||||
{
|
|
||||||
let mut properties = <::openapi_type::indexmap::IndexMap<
|
|
||||||
::std::string::String,
|
|
||||||
#openapi::ReferenceOr<::std::boxed::Box<#openapi::Schema>>
|
|
||||||
>>::new();
|
|
||||||
let mut required = <::std::vec::Vec<::std::string::String>>::new();
|
|
||||||
|
|
||||||
#({
|
|
||||||
const FIELD_NAME: &::core::primitive::str = #field_name;
|
|
||||||
let mut field_schema = #field_schema;
|
|
||||||
::openapi_type::private::add_dependencies(
|
|
||||||
&mut dependencies,
|
|
||||||
&mut field_schema.dependencies
|
|
||||||
);
|
|
||||||
|
|
||||||
// fields in OpenAPI are nullable by default
|
|
||||||
match field_schema.nullable {
|
|
||||||
true => field_schema.nullable = false,
|
|
||||||
false => required.push(::std::string::String::from(FIELD_NAME))
|
|
||||||
};
|
|
||||||
|
|
||||||
match field_schema.name.as_ref() {
|
|
||||||
// include the field schema as reference
|
|
||||||
::std::option::Option::Some(schema_name) => {
|
|
||||||
let mut reference = ::std::string::String::from("#/components/schemas/");
|
|
||||||
reference.push_str(schema_name);
|
|
||||||
properties.insert(
|
|
||||||
::std::string::String::from(FIELD_NAME),
|
|
||||||
#openapi::ReferenceOr::Reference { reference }
|
|
||||||
);
|
|
||||||
dependencies.insert(
|
|
||||||
::std::string::String::from(schema_name),
|
|
||||||
field_schema
|
|
||||||
);
|
|
||||||
},
|
|
||||||
// inline the field schema
|
|
||||||
::std::option::Option::None => {
|
|
||||||
properties.insert(
|
|
||||||
::std::string::String::from(FIELD_NAME),
|
|
||||||
#openapi::ReferenceOr::Item(
|
|
||||||
::std::boxed::Box::new(
|
|
||||||
field_schema.into_schema()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})*
|
|
||||||
|
|
||||||
#openapi::SchemaKind::Type(
|
|
||||||
#openapi::Type::Object(
|
|
||||||
#openapi::ObjectType {
|
|
||||||
properties,
|
|
||||||
required,
|
|
||||||
.. ::std::default::Default::default()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn gen_enum(variants: &[LitStr]) -> TokenStream {
|
|
||||||
let openapi = path!(::openapi_type::openapi);
|
|
||||||
quote! {
|
|
||||||
{
|
|
||||||
let mut enumeration = <::std::vec::Vec<::std::string::String>>::new();
|
|
||||||
#(enumeration.push(::std::string::String::from(#variants));)*
|
|
||||||
#openapi::SchemaKind::Type(
|
|
||||||
#openapi::Type::String(
|
|
||||||
#openapi::StringType {
|
|
||||||
enumeration,
|
|
||||||
.. ::std::default::Default::default()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn gen_alt(alt: &[ParseData]) -> TokenStream {
|
|
||||||
let openapi = path!(::openapi_type::openapi);
|
|
||||||
let schema = alt.iter().map(|data| data.gen_schema());
|
|
||||||
quote! {
|
|
||||||
{
|
|
||||||
let mut alternatives = <::std::vec::Vec<
|
|
||||||
#openapi::ReferenceOr<#openapi::Schema>
|
|
||||||
>>::new();
|
|
||||||
#(alternatives.push(#openapi::ReferenceOr::Item(
|
|
||||||
::openapi_type::OpenapiSchema::new(#schema).into_schema()
|
|
||||||
));)*
|
|
||||||
#openapi::SchemaKind::OneOf {
|
|
||||||
one_of: alternatives
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn gen_unit() -> TokenStream {
|
|
||||||
let openapi = path!(::openapi_type::openapi);
|
|
||||||
quote! {
|
|
||||||
#openapi::SchemaKind::Type(
|
|
||||||
#openapi::Type::Object(
|
|
||||||
#openapi::ObjectType {
|
|
||||||
additional_properties: ::std::option::Option::Some(
|
|
||||||
#openapi::AdditionalProperties::Any(false)
|
|
||||||
),
|
|
||||||
.. ::std::default::Default::default()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,95 +0,0 @@
|
||||||
#![warn(missing_debug_implementations, rust_2018_idioms)]
|
|
||||||
#![deny(broken_intra_doc_links)]
|
|
||||||
#![forbid(unsafe_code)]
|
|
||||||
//! This crate defines the macros for `#[derive(OpenapiType)]`.
|
|
||||||
|
|
||||||
use proc_macro::TokenStream;
|
|
||||||
use proc_macro2::TokenStream as TokenStream2;
|
|
||||||
use quote::quote;
|
|
||||||
use syn::{parse_macro_input, Data, DeriveInput, LitStr, TraitBound, TraitBoundModifier, TypeParamBound};
|
|
||||||
|
|
||||||
#[macro_use]
|
|
||||||
mod util;
|
|
||||||
//use util::*;
|
|
||||||
|
|
||||||
mod codegen;
|
|
||||||
mod parser;
|
|
||||||
use parser::*;
|
|
||||||
|
|
||||||
/// The derive macro for [OpenapiType](https://docs.rs/openapi_type/*/openapi_type/trait.OpenapiType.html).
|
|
||||||
#[proc_macro_derive(OpenapiType, attributes(openapi))]
|
|
||||||
pub fn derive_openapi_type(input: TokenStream) -> TokenStream {
|
|
||||||
let input = parse_macro_input!(input);
|
|
||||||
expand_openapi_type(input).unwrap_or_else(|err| err.to_compile_error()).into()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn expand_openapi_type(mut input: DeriveInput) -> syn::Result<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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -1,198 +0,0 @@
|
||||||
use crate::util::{ExpectLit, ToLitStr};
|
|
||||||
use proc_macro2::Span;
|
|
||||||
use syn::{
|
|
||||||
punctuated::Punctuated, spanned::Spanned as _, Attribute, DataEnum, DataStruct, DataUnion, Fields, FieldsNamed, LitStr,
|
|
||||||
Meta, Token, Type
|
|
||||||
};
|
|
||||||
|
|
||||||
pub(super) enum ParseDataType {
|
|
||||||
Type(Type),
|
|
||||||
Inline(ParseData)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub(super) enum ParseData {
|
|
||||||
Struct(Vec<(LitStr, ParseDataType)>),
|
|
||||||
Enum(Vec<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(())
|
|
||||||
}
|
|
|
@ -1,52 +0,0 @@
|
||||||
use proc_macro2::Ident;
|
|
||||||
use syn::{Lit, LitStr};
|
|
||||||
|
|
||||||
/// Convert any literal path into a [syn::Path].
|
|
||||||
macro_rules! path {
|
|
||||||
(:: $($segment:ident)::*) => {
|
|
||||||
path!(@private Some(Default::default()), $($segment),*)
|
|
||||||
};
|
|
||||||
($($segment:ident)::*) => {
|
|
||||||
path!(@private None, $($segment),*)
|
|
||||||
};
|
|
||||||
(@private $leading_colon:expr, $($segment:ident),*) => {
|
|
||||||
{
|
|
||||||
#[allow(unused_mut)]
|
|
||||||
let mut segments: ::syn::punctuated::Punctuated<::syn::PathSegment, _> = Default::default();
|
|
||||||
$(
|
|
||||||
segments.push(::syn::PathSegment {
|
|
||||||
ident: ::proc_macro2::Ident::new(stringify!($segment), ::proc_macro2::Span::call_site()),
|
|
||||||
arguments: Default::default()
|
|
||||||
});
|
|
||||||
)*
|
|
||||||
::syn::Path {
|
|
||||||
leading_colon: $leading_colon,
|
|
||||||
segments
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Convert any [Ident] into a [LitStr]. Basically `stringify!`.
|
|
||||||
pub(super) trait ToLitStr {
|
|
||||||
fn to_lit_str(&self) -> LitStr;
|
|
||||||
}
|
|
||||||
impl ToLitStr for Ident {
|
|
||||||
fn to_lit_str(&self) -> LitStr {
|
|
||||||
LitStr::new(&self.to_string(), self.span())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Convert a [Lit] to one specific literal type.
|
|
||||||
pub(crate) trait ExpectLit {
|
|
||||||
fn expect_str(self) -> syn::Result<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,41 +2,11 @@ use crate::{IntoResponse, RequestBody};
|
||||||
use futures_util::future::BoxFuture;
|
use futures_util::future::BoxFuture;
|
||||||
use gotham::{
|
use gotham::{
|
||||||
extractor::{PathExtractor, QueryStringExtractor},
|
extractor::{PathExtractor, QueryStringExtractor},
|
||||||
hyper::{Body, Method, Response},
|
hyper::{Body, Method},
|
||||||
router::response::extender::StaticResponseExtender,
|
state::State
|
||||||
state::{State, StateData}
|
|
||||||
};
|
};
|
||||||
#[cfg(feature = "openapi")]
|
|
||||||
use openapi_type::{OpenapiSchema, OpenapiType};
|
|
||||||
use serde::{Deserialize, Deserializer};
|
|
||||||
use std::borrow::Cow;
|
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.
|
// TODO: Specify default types once https://github.com/rust-lang/rust/issues/29661 lands.
|
||||||
#[_private_openapi_trait(EndpointWithSchema)]
|
#[_private_openapi_trait(EndpointWithSchema)]
|
||||||
pub trait Endpoint {
|
pub trait Endpoint {
|
||||||
|
@ -53,19 +23,19 @@ pub trait Endpoint {
|
||||||
fn has_placeholders() -> bool {
|
fn has_placeholders() -> bool {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
/// The type that parses the URI placeholders. Use [NoopExtractor] if `has_placeholders()`
|
/// The type that parses the URI placeholders. Use [gotham::extractor::NoopPathExtractor]
|
||||||
/// returns `false`.
|
/// if `has_placeholders()` returns `false`.
|
||||||
#[openapi_bound("Placeholders: OpenapiType")]
|
#[openapi_bound("Placeholders: crate::OpenapiType")]
|
||||||
type Placeholders: PathExtractor<Body> + Clone + Sync;
|
type Placeholders: PathExtractor<Body> + Sync;
|
||||||
|
|
||||||
/// Returns `true` _iff_ the request parameters should be parsed. `false` by default.
|
/// Returns `true` _iff_ the request parameters should be parsed. `false` by default.
|
||||||
fn needs_params() -> bool {
|
fn needs_params() -> bool {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
/// The type that parses the request parameters. Use [NoopExtractor] if `needs_params()`
|
/// The type that parses the request parameters. Use [gotham::extractor::NoopQueryStringExtractor]
|
||||||
/// returns `false`.
|
/// if `needs_params()` returns `false`.
|
||||||
#[openapi_bound("Params: OpenapiType")]
|
#[openapi_bound("Params: crate::OpenapiType")]
|
||||||
type Params: QueryStringExtractor<Body> + Clone + Sync;
|
type Params: QueryStringExtractor<Body> + Sync;
|
||||||
|
|
||||||
/// Returns `true` _iff_ the request body should be parsed. `false` by default.
|
/// Returns `true` _iff_ the request body should be parsed. `false` by default.
|
||||||
fn needs_body() -> bool {
|
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.
|
/// The return type of the foo read endpoint.
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
# #[cfg_attr(feature = "openapi", derive(openapi_type::OpenapiType))]
|
# #[cfg_attr(feature = "openapi", derive(OpenapiType))]
|
||||||
struct Foo {
|
struct Foo {
|
||||||
id: u64
|
id: u64
|
||||||
}
|
}
|
||||||
|
@ -95,8 +95,8 @@ use gotham_restful::gotham::hyper::Method;
|
||||||
struct CustomResource;
|
struct CustomResource;
|
||||||
|
|
||||||
/// This type is used to parse path parameters.
|
/// This type is used to parse path parameters.
|
||||||
#[derive(Clone, Deserialize, StateData, StaticResponseExtender)]
|
#[derive(Deserialize, StateData, StaticResponseExtender)]
|
||||||
# #[cfg_attr(feature = "openapi", derive(openapi_type::OpenapiType))]
|
# #[cfg_attr(feature = "openapi", derive(OpenapiType))]
|
||||||
struct CustomPath {
|
struct CustomPath {
|
||||||
name: String
|
name: String
|
||||||
}
|
}
|
||||||
|
@ -225,7 +225,7 @@ A simple example that uses only a single secret looks like this:
|
||||||
struct SecretResource;
|
struct SecretResource;
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
# #[cfg_attr(feature = "openapi", derive(openapi_type::OpenapiType))]
|
# #[cfg_attr(feature = "openapi", derive(OpenapiType))]
|
||||||
struct Secret {
|
struct Secret {
|
||||||
id: u64,
|
id: u64,
|
||||||
intended_for: String
|
intended_for: String
|
||||||
|
@ -331,7 +331,7 @@ A simple non-async example looks like this:
|
||||||
struct FooResource;
|
struct FooResource;
|
||||||
|
|
||||||
#[derive(Queryable, Serialize)]
|
#[derive(Queryable, Serialize)]
|
||||||
# #[cfg_attr(feature = "openapi", derive(openapi_type::OpenapiType))]
|
# #[cfg_attr(feature = "openapi", derive(OpenapiType))]
|
||||||
struct Foo {
|
struct Foo {
|
||||||
id: i64,
|
id: i64,
|
||||||
value: String
|
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
|
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
|
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
|
enough type information, so all types used in the router need to implement `OpenapiType`. This
|
||||||
`OpenapiType`[openapi_type::OpenapiType]. This can be derived for almoust any type and there
|
can be derived for almoust any type and there should be no need to implement it manually. A simple
|
||||||
should be no need to implement it manually. A simple example looks like this:
|
example looks like this:
|
||||||
|
|
||||||
```rust,no_run
|
```rust,no_run
|
||||||
# #[macro_use] extern crate gotham_restful_derive;
|
# #[macro_use] extern crate gotham_restful_derive;
|
||||||
|
@ -373,7 +373,6 @@ should be no need to implement it manually. A simple example looks like this:
|
||||||
# mod openapi_feature_enabled {
|
# mod openapi_feature_enabled {
|
||||||
# use gotham::{router::builder::*, state::State};
|
# use gotham::{router::builder::*, state::State};
|
||||||
# use gotham_restful::*;
|
# use gotham_restful::*;
|
||||||
# use openapi_type::OpenapiType;
|
|
||||||
# use serde::{Deserialize, Serialize};
|
# use serde::{Deserialize, Serialize};
|
||||||
#[derive(Resource)]
|
#[derive(Resource)]
|
||||||
#[resource(read_all)]
|
#[resource(read_all)]
|
||||||
|
@ -411,17 +410,17 @@ clients in different languages without worying to exactly replicate your api in
|
||||||
languages.
|
languages.
|
||||||
|
|
||||||
However, please note that by default, the `without-openapi` feature of this crate is enabled.
|
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,
|
Disabling it in favour of the `openapi` feature will add an additional type bound, [`OpenapiType`],
|
||||||
[`OpenapiType`][openapi_type::OpenapiType], on some of the types in [`Endpoint`] and related
|
on some of the types in [`Endpoint`] and related traits. This means that some code might only
|
||||||
traits. This means that some code might only compile on either feature, but not on both. If you
|
compile on either feature, but not on both. If you are writing a library that uses gotham-restful,
|
||||||
are writing a library that uses gotham-restful, it is strongly recommended to pass both features
|
it is strongly recommended to pass both features through and conditionally enable the openapi
|
||||||
through and conditionally enable the openapi code, like this:
|
code, like this:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
# #[macro_use] extern crate gotham_restful;
|
# #[macro_use] extern crate gotham_restful;
|
||||||
# use serde::{Deserialize, Serialize};
|
# use serde::{Deserialize, Serialize};
|
||||||
#[derive(Deserialize, Serialize)]
|
#[derive(Deserialize, Serialize)]
|
||||||
#[cfg_attr(feature = "openapi", derive(openapi_type::OpenapiType))]
|
#[cfg_attr(feature = "openapi", derive(OpenapiType))]
|
||||||
struct Foo;
|
struct Foo;
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -479,8 +478,6 @@ pub mod private {
|
||||||
#[cfg(feature = "openapi")]
|
#[cfg(feature = "openapi")]
|
||||||
pub use indexmap::IndexMap;
|
pub use indexmap::IndexMap;
|
||||||
#[cfg(feature = "openapi")]
|
#[cfg(feature = "openapi")]
|
||||||
pub use openapi_type::{OpenapiSchema, OpenapiType};
|
|
||||||
#[cfg(feature = "openapi")]
|
|
||||||
pub use openapiv3 as openapi;
|
pub use openapiv3 as openapi;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -497,12 +494,16 @@ pub use cors::{handle_cors, CorsConfig, CorsRoute};
|
||||||
#[cfg(feature = "openapi")]
|
#[cfg(feature = "openapi")]
|
||||||
mod openapi;
|
mod openapi;
|
||||||
#[cfg(feature = "openapi")]
|
#[cfg(feature = "openapi")]
|
||||||
pub use openapi::{builder::OpenapiInfo, router::GetOpenapi};
|
pub use openapi::{
|
||||||
|
builder::OpenapiInfo,
|
||||||
|
router::GetOpenapi,
|
||||||
|
types::{OpenapiSchema, OpenapiType}
|
||||||
|
};
|
||||||
|
|
||||||
mod endpoint;
|
mod endpoint;
|
||||||
|
pub use endpoint::Endpoint;
|
||||||
#[cfg(feature = "openapi")]
|
#[cfg(feature = "openapi")]
|
||||||
pub use endpoint::EndpointWithSchema;
|
pub use endpoint::EndpointWithSchema;
|
||||||
pub use endpoint::{Endpoint, NoopExtractor};
|
|
||||||
|
|
||||||
mod response;
|
mod response;
|
||||||
pub use response::{
|
pub use response::{
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
|
use crate::OpenapiSchema;
|
||||||
use indexmap::IndexMap;
|
use indexmap::IndexMap;
|
||||||
use openapi_type::OpenapiSchema;
|
|
||||||
use openapiv3::{
|
use openapiv3::{
|
||||||
Components, OpenAPI, PathItem, ReferenceOr,
|
Components, OpenAPI, PathItem, ReferenceOr,
|
||||||
ReferenceOr::{Item, Reference},
|
ReferenceOr::{Item, Reference},
|
||||||
|
@ -104,7 +104,7 @@ impl OpenapiBuilder {
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
use openapi_type::OpenapiType;
|
use crate::OpenapiType;
|
||||||
|
|
||||||
#[derive(OpenapiType)]
|
#[derive(OpenapiType)]
|
||||||
struct Message {
|
struct Message {
|
||||||
|
|
|
@ -4,3 +4,4 @@ pub mod builder;
|
||||||
pub mod handler;
|
pub mod handler;
|
||||||
pub mod operation;
|
pub mod operation;
|
||||||
pub mod router;
|
pub mod router;
|
||||||
|
pub mod types;
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
use super::SECURITY_NAME;
|
use super::SECURITY_NAME;
|
||||||
use crate::{response::OrAllTypes, EndpointWithSchema, IntoResponse, RequestBody, ResponseSchema};
|
use crate::{response::OrAllTypes, EndpointWithSchema, IntoResponse, OpenapiSchema, RequestBody, ResponseSchema};
|
||||||
use indexmap::IndexMap;
|
use indexmap::IndexMap;
|
||||||
use mime::Mime;
|
use mime::Mime;
|
||||||
use openapi_type::OpenapiSchema;
|
|
||||||
use openapiv3::{
|
use openapiv3::{
|
||||||
MediaType, Operation, Parameter, ParameterData, ParameterSchemaOrContent, ReferenceOr, ReferenceOr::Item,
|
MediaType, Operation, Parameter, ParameterData, ParameterSchemaOrContent, ReferenceOr, ReferenceOr::Item,
|
||||||
RequestBody as OARequestBody, Response, Responses, Schema, SchemaKind, StatusCode, Type
|
RequestBody as OARequestBody, Response, Responses, Schema, SchemaKind, StatusCode, Type
|
||||||
|
|
|
@ -3,10 +3,9 @@ use super::{
|
||||||
handler::{OpenapiHandler, SwaggerUiHandler},
|
handler::{OpenapiHandler, SwaggerUiHandler},
|
||||||
operation::OperationDescription
|
operation::OperationDescription
|
||||||
};
|
};
|
||||||
use crate::{routing::*, EndpointWithSchema, ResourceWithSchema, ResponseSchema};
|
use crate::{routing::*, EndpointWithSchema, OpenapiType, ResourceWithSchema, ResponseSchema};
|
||||||
use gotham::{hyper::Method, pipeline::chain::PipelineHandleChain, router::builder::*};
|
use gotham::{hyper::Method, pipeline::chain::PipelineHandleChain, router::builder::*};
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use openapi_type::OpenapiType;
|
|
||||||
use regex::{Captures, Regex};
|
use regex::{Captures, Regex};
|
||||||
use std::panic::RefUnwindSafe;
|
use std::panic::RefUnwindSafe;
|
||||||
|
|
||||||
|
|
477
src/openapi/types.rs
Normal file
477
src/openapi/types.rs
Normal file
|
@ -0,0 +1,477 @@
|
||||||
|
#[cfg(feature = "chrono")]
|
||||||
|
use chrono::{Date, DateTime, FixedOffset, Local, NaiveDate, NaiveDateTime, Utc};
|
||||||
|
use gotham::extractor::{NoopPathExtractor, NoopQueryStringExtractor};
|
||||||
|
use indexmap::IndexMap;
|
||||||
|
use openapiv3::{
|
||||||
|
AdditionalProperties, ArrayType, IntegerType, NumberFormat, NumberType, ObjectType,
|
||||||
|
ReferenceOr::{Item, Reference},
|
||||||
|
Schema, SchemaData, SchemaKind, StringType, Type, VariantOrUnknownOrEmpty
|
||||||
|
};
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
collections::{BTreeSet, HashMap, HashSet},
|
||||||
|
hash::BuildHasher,
|
||||||
|
num::{NonZeroU128, NonZeroU16, NonZeroU32, NonZeroU64, NonZeroU8, NonZeroUsize}
|
||||||
|
};
|
||||||
|
#[cfg(feature = "uuid")]
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
/**
|
||||||
|
This struct needs to be available for every type that can be part of an OpenAPI Spec. It is
|
||||||
|
already implemented for primitive types, String, Vec, Option and the like. To have it available
|
||||||
|
for your type, simply derive from [OpenapiType].
|
||||||
|
*/
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub struct OpenapiSchema {
|
||||||
|
/// The name of this schema. If it is None, the schema will be inlined.
|
||||||
|
pub name: Option<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,3 +1,6 @@
|
||||||
|
#[cfg(feature = "openapi")]
|
||||||
|
use crate::OpenapiSchema;
|
||||||
|
|
||||||
use futures_util::future::{self, BoxFuture, FutureExt};
|
use futures_util::future::{self, BoxFuture, FutureExt};
|
||||||
use gotham::{
|
use gotham::{
|
||||||
handler::HandlerError,
|
handler::HandlerError,
|
||||||
|
@ -7,8 +10,6 @@ use gotham::{
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
use mime::{Mime, APPLICATION_JSON, STAR_STAR};
|
use mime::{Mime, APPLICATION_JSON, STAR_STAR};
|
||||||
#[cfg(feature = "openapi")]
|
|
||||||
use openapi_type::OpenapiSchema;
|
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use std::{
|
use std::{
|
||||||
convert::Infallible,
|
convert::Infallible,
|
||||||
|
@ -258,7 +259,7 @@ mod test {
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
#[derive(Debug, Default, Deserialize, Serialize)]
|
#[derive(Debug, Default, Deserialize, Serialize)]
|
||||||
#[cfg_attr(feature = "openapi", derive(openapi_type::OpenapiType))]
|
#[cfg_attr(feature = "openapi", derive(crate::OpenapiType))]
|
||||||
struct Msg {
|
struct Msg {
|
||||||
msg: String
|
msg: String
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,12 @@
|
||||||
use super::{handle_error, IntoResponse};
|
use super::{handle_error, IntoResponse};
|
||||||
#[cfg(feature = "openapi")]
|
|
||||||
use crate::ResponseSchema;
|
|
||||||
use crate::{IntoResponseError, Response};
|
use crate::{IntoResponseError, Response};
|
||||||
|
#[cfg(feature = "openapi")]
|
||||||
|
use crate::{OpenapiSchema, OpenapiType, ResponseSchema};
|
||||||
use futures_util::{future, future::FutureExt};
|
use futures_util::{future, future::FutureExt};
|
||||||
use gotham::hyper::header::{HeaderMap, HeaderValue, IntoHeaderName};
|
use gotham::hyper::header::{HeaderMap, HeaderValue, IntoHeaderName};
|
||||||
#[cfg(feature = "openapi")]
|
#[cfg(feature = "openapi")]
|
||||||
use gotham::hyper::StatusCode;
|
use gotham::hyper::StatusCode;
|
||||||
use mime::Mime;
|
use mime::Mime;
|
||||||
#[cfg(feature = "openapi")]
|
|
||||||
use openapi_type::{OpenapiSchema, OpenapiType};
|
|
||||||
use std::{fmt::Display, future::Future, pin::Pin};
|
use std::{fmt::Display, future::Future, pin::Pin};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
use super::{handle_error, IntoResponse, IntoResponseError};
|
use super::{handle_error, IntoResponse, IntoResponseError};
|
||||||
use crate::{FromBody, RequestBody, ResourceType, Response};
|
use crate::{FromBody, RequestBody, ResourceType, Response};
|
||||||
#[cfg(feature = "openapi")]
|
#[cfg(feature = "openapi")]
|
||||||
use crate::{IntoResponseWithSchema, ResponseSchema};
|
use crate::{IntoResponseWithSchema, OpenapiSchema, OpenapiType, ResponseSchema};
|
||||||
#[cfg(feature = "openapi")]
|
|
||||||
use openapi_type::{OpenapiSchema, OpenapiType};
|
|
||||||
|
|
||||||
use futures_core::future::Future;
|
use futures_core::future::Future;
|
||||||
use futures_util::{future, future::FutureExt};
|
use futures_util::{future, future::FutureExt};
|
||||||
|
|
|
@ -1,14 +1,12 @@
|
||||||
use super::{handle_error, IntoResponse};
|
use super::{handle_error, IntoResponse};
|
||||||
use crate::{IntoResponseError, Response};
|
use crate::{IntoResponseError, Response};
|
||||||
#[cfg(feature = "openapi")]
|
#[cfg(feature = "openapi")]
|
||||||
use crate::{NoContent, ResponseSchema};
|
use crate::{NoContent, OpenapiSchema, ResponseSchema};
|
||||||
use futures_util::future::{BoxFuture, FutureExt, TryFutureExt};
|
use futures_util::future::{BoxFuture, FutureExt, TryFutureExt};
|
||||||
use gotham::hyper::{
|
use gotham::hyper::{
|
||||||
header::{InvalidHeaderValue, LOCATION},
|
header::{InvalidHeaderValue, LOCATION},
|
||||||
Body, StatusCode
|
Body, StatusCode
|
||||||
};
|
};
|
||||||
#[cfg(feature = "openapi")]
|
|
||||||
use openapi_type::OpenapiSchema;
|
|
||||||
use std::{
|
use std::{
|
||||||
error::Error as StdError,
|
error::Error as StdError,
|
||||||
fmt::{Debug, Display}
|
fmt::{Debug, Display}
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
use super::{handle_error, IntoResponse, ResourceError};
|
use super::{handle_error, IntoResponse, ResourceError};
|
||||||
#[cfg(feature = "openapi")]
|
#[cfg(feature = "openapi")]
|
||||||
use crate::ResponseSchema;
|
use crate::{OpenapiSchema, ResponseSchema};
|
||||||
use crate::{Response, ResponseBody, Success};
|
use crate::{Response, ResponseBody, Success};
|
||||||
#[cfg(feature = "openapi")]
|
|
||||||
use openapi_type::OpenapiSchema;
|
|
||||||
|
|
||||||
use futures_core::future::Future;
|
use futures_core::future::Future;
|
||||||
use gotham::hyper::StatusCode;
|
use gotham::hyper::StatusCode;
|
||||||
|
@ -66,7 +64,7 @@ mod test {
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
#[derive(Debug, Default, Deserialize, Serialize)]
|
#[derive(Debug, Default, Deserialize, Serialize)]
|
||||||
#[cfg_attr(feature = "openapi", derive(openapi_type::OpenapiType))]
|
#[cfg_attr(feature = "openapi", derive(crate::OpenapiType))]
|
||||||
struct Msg {
|
struct Msg {
|
||||||
msg: String
|
msg: String
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use super::IntoResponse;
|
use super::IntoResponse;
|
||||||
#[cfg(feature = "openapi")]
|
#[cfg(feature = "openapi")]
|
||||||
use crate::ResponseSchema;
|
use crate::{OpenapiSchema, ResponseSchema};
|
||||||
use crate::{Response, ResponseBody};
|
use crate::{Response, ResponseBody};
|
||||||
use futures_util::future::{self, FutureExt};
|
use futures_util::future::{self, FutureExt};
|
||||||
use gotham::hyper::{
|
use gotham::hyper::{
|
||||||
|
@ -8,8 +8,6 @@ use gotham::hyper::{
|
||||||
StatusCode
|
StatusCode
|
||||||
};
|
};
|
||||||
use mime::{Mime, APPLICATION_JSON};
|
use mime::{Mime, APPLICATION_JSON};
|
||||||
#[cfg(feature = "openapi")]
|
|
||||||
use openapi_type::OpenapiSchema;
|
|
||||||
use std::{fmt::Debug, future::Future, pin::Pin};
|
use std::{fmt::Debug, future::Future, pin::Pin};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -29,7 +27,7 @@ Usage example:
|
||||||
# struct MyResource;
|
# struct MyResource;
|
||||||
#
|
#
|
||||||
#[derive(Deserialize, Serialize)]
|
#[derive(Deserialize, Serialize)]
|
||||||
# #[cfg_attr(feature = "openapi", derive(openapi_type::OpenapiType))]
|
# #[cfg_attr(feature = "openapi", derive(OpenapiType))]
|
||||||
struct MyResponse {
|
struct MyResponse {
|
||||||
message: &'static str
|
message: &'static str
|
||||||
}
|
}
|
||||||
|
@ -98,7 +96,7 @@ mod test {
|
||||||
use gotham::hyper::header::ACCESS_CONTROL_ALLOW_ORIGIN;
|
use gotham::hyper::header::ACCESS_CONTROL_ALLOW_ORIGIN;
|
||||||
|
|
||||||
#[derive(Debug, Default, Serialize)]
|
#[derive(Debug, Default, Serialize)]
|
||||||
#[cfg_attr(feature = "openapi", derive(openapi_type::OpenapiType))]
|
#[cfg_attr(feature = "openapi", derive(crate::OpenapiType))]
|
||||||
struct Msg {
|
struct Msg {
|
||||||
msg: String
|
msg: String
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ use crate::openapi::{
|
||||||
router::OpenapiRouter
|
router::OpenapiRouter
|
||||||
};
|
};
|
||||||
use crate::{response::ResourceError, Endpoint, FromBody, IntoResponse, Resource, Response};
|
use crate::{response::ResourceError, Endpoint, FromBody, IntoResponse, Resource, Response};
|
||||||
|
|
||||||
#[cfg(feature = "cors")]
|
#[cfg(feature = "cors")]
|
||||||
use gotham::router::route::matcher::AccessControlRequestMethodMatcher;
|
use gotham::router::route::matcher::AccessControlRequestMethodMatcher;
|
||||||
use gotham::{
|
use gotham::{
|
||||||
|
@ -19,12 +20,10 @@ use gotham::{
|
||||||
state::{FromState, State}
|
state::{FromState, State}
|
||||||
};
|
};
|
||||||
use mime::{Mime, APPLICATION_JSON};
|
use mime::{Mime, APPLICATION_JSON};
|
||||||
#[cfg(feature = "openapi")]
|
use std::panic::RefUnwindSafe;
|
||||||
use openapi_type::OpenapiType;
|
|
||||||
use std::{any::TypeId, panic::RefUnwindSafe};
|
|
||||||
|
|
||||||
/// Allow us to extract an id from a path.
|
/// Allow us to extract an id from a path.
|
||||||
#[derive(Clone, Copy, Debug, Deserialize, StateData, StaticResponseExtender)]
|
#[derive(Debug, Deserialize, StateData, StaticResponseExtender)]
|
||||||
#[cfg_attr(feature = "openapi", derive(OpenapiType))]
|
#[cfg_attr(feature = "openapi", derive(OpenapiType))]
|
||||||
pub struct PathExtractor<ID: RefUnwindSafe + Send + 'static> {
|
pub struct PathExtractor<ID: RefUnwindSafe + Send + 'static> {
|
||||||
pub id: ID
|
pub id: ID
|
||||||
|
@ -92,11 +91,6 @@ where
|
||||||
{
|
{
|
||||||
trace!("entering endpoint_handler");
|
trace!("entering endpoint_handler");
|
||||||
let placeholders = E::Placeholders::take_from(state);
|
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 params = E::Params::take_from(state);
|
||||||
|
|
||||||
let body = match E::needs_body() {
|
let body = match E::needs_body() {
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
|
#[cfg(feature = "openapi")]
|
||||||
|
use crate::OpenapiType;
|
||||||
|
|
||||||
use gotham::hyper::body::Bytes;
|
use gotham::hyper::body::Bytes;
|
||||||
use mime::{Mime, APPLICATION_JSON};
|
use mime::{Mime, APPLICATION_JSON};
|
||||||
#[cfg(feature = "openapi")]
|
|
||||||
use openapi_type::OpenapiType;
|
|
||||||
use serde::{de::DeserializeOwned, Serialize};
|
use serde::{de::DeserializeOwned, Serialize};
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
|
|
||||||
|
|
|
@ -9,10 +9,8 @@ use gotham::{
|
||||||
};
|
};
|
||||||
use gotham_restful::*;
|
use gotham_restful::*;
|
||||||
use mime::{APPLICATION_JSON, TEXT_PLAIN};
|
use mime::{APPLICATION_JSON, TEXT_PLAIN};
|
||||||
#[cfg(feature = "openapi")]
|
|
||||||
use openapi_type::OpenapiType;
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use tokio::time::{sleep, Duration};
|
use tokio::time::{delay_for, Duration};
|
||||||
|
|
||||||
mod util {
|
mod util {
|
||||||
include!("util/mod.rs");
|
include!("util/mod.rs");
|
||||||
|
@ -30,7 +28,7 @@ struct FooBody {
|
||||||
data: String
|
data: String
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Deserialize, StateData, StaticResponseExtender)]
|
#[derive(Deserialize, StateData, StaticResponseExtender)]
|
||||||
#[cfg_attr(feature = "openapi", derive(OpenapiType))]
|
#[cfg_attr(feature = "openapi", derive(OpenapiType))]
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
struct FooSearch {
|
struct FooSearch {
|
||||||
|
@ -88,9 +86,9 @@ async fn remove(_id: u64) -> Raw<&'static [u8]> {
|
||||||
const STATE_TEST_RESPONSE: &[u8] = b"xxJbxOuwioqR5DfzPuVqvaqRSfpdNQGluIvHU4n1LM";
|
const STATE_TEST_RESPONSE: &[u8] = b"xxJbxOuwioqR5DfzPuVqvaqRSfpdNQGluIvHU4n1LM";
|
||||||
#[endpoint(method = "Method::GET", uri = "state_test")]
|
#[endpoint(method = "Method::GET", uri = "state_test")]
|
||||||
async fn state_test(state: &mut State) -> Raw<&'static [u8]> {
|
async fn state_test(state: &mut State) -> Raw<&'static [u8]> {
|
||||||
sleep(Duration::from_nanos(1)).await;
|
delay_for(Duration::from_nanos(1)).await;
|
||||||
state.borrow::<HeaderMap>();
|
state.borrow::<HeaderMap>();
|
||||||
sleep(Duration::from_nanos(1)).await;
|
delay_for(Duration::from_nanos(1)).await;
|
||||||
Raw::new(STATE_TEST_RESPONSE, TEXT_PLAIN)
|
Raw::new(STATE_TEST_RESPONSE, TEXT_PLAIN)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,8 +4,6 @@ extern crate gotham_derive;
|
||||||
use gotham::{router::builder::*, test::TestServer};
|
use gotham::{router::builder::*, test::TestServer};
|
||||||
use gotham_restful::*;
|
use gotham_restful::*;
|
||||||
use mime::{APPLICATION_JSON, TEXT_PLAIN};
|
use mime::{APPLICATION_JSON, TEXT_PLAIN};
|
||||||
#[cfg(feature = "openapi")]
|
|
||||||
use openapi_type::OpenapiType;
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
mod util {
|
mod util {
|
||||||
|
@ -24,7 +22,7 @@ struct FooBody {
|
||||||
data: String
|
data: String
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Deserialize, StateData, StaticResponseExtender)]
|
#[derive(Deserialize, StateData, StaticResponseExtender)]
|
||||||
#[cfg_attr(feature = "openapi", derive(OpenapiType))]
|
#[cfg_attr(feature = "openapi", derive(OpenapiType))]
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
struct FooSearch {
|
struct FooSearch {
|
||||||
|
|
|
@ -4,7 +4,14 @@ use trybuild::TestCases;
|
||||||
#[ignore]
|
#[ignore]
|
||||||
fn trybuild_ui() {
|
fn trybuild_ui() {
|
||||||
let t = TestCases::new();
|
let t = TestCases::new();
|
||||||
|
|
||||||
|
// always enabled
|
||||||
t.compile_fail("tests/ui/endpoint/*.rs");
|
t.compile_fail("tests/ui/endpoint/*.rs");
|
||||||
t.compile_fail("tests/ui/from_body/*.rs");
|
t.compile_fail("tests/ui/from_body/*.rs");
|
||||||
t.compile_fail("tests/ui/resource/*.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,14 +1,3 @@
|
||||||
error[E0277]: the trait bound `FooParams: OpenapiType` is not satisfied
|
|
||||||
--> $DIR/invalid_params_ty.rs:15:16
|
|
||||||
|
|
|
||||||
15 | fn endpoint(_: FooParams) {
|
|
||||||
| ^^^^^^^^^ the trait `OpenapiType` is not implemented for `FooParams`
|
|
||||||
|
|
|
||||||
::: $WORKSPACE/src/endpoint.rs
|
|
||||||
|
|
|
||||||
| #[openapi_bound("Params: OpenapiType")]
|
|
||||||
| --------------------- required by this bound in `gotham_restful::EndpointWithSchema::Params`
|
|
||||||
|
|
||||||
error[E0277]: the trait bound `for<'de> FooParams: serde::de::Deserialize<'de>` is not satisfied
|
error[E0277]: the trait bound `for<'de> FooParams: serde::de::Deserialize<'de>` is not satisfied
|
||||||
--> $DIR/invalid_params_ty.rs:15:16
|
--> $DIR/invalid_params_ty.rs:15:16
|
||||||
|
|
|
|
||||||
|
@ -17,7 +6,7 @@ error[E0277]: the trait bound `for<'de> FooParams: serde::de::Deserialize<'de>`
|
||||||
|
|
|
|
||||||
::: $WORKSPACE/src/endpoint.rs
|
::: $WORKSPACE/src/endpoint.rs
|
||||||
|
|
|
|
||||||
| type Params: QueryStringExtractor<Body> + Clone + Sync;
|
| type Params: QueryStringExtractor<Body> + Sync;
|
||||||
| -------------------------- required by this bound in `gotham_restful::EndpointWithSchema::Params`
|
| -------------------------- required by this bound in `gotham_restful::EndpointWithSchema::Params`
|
||||||
|
|
||||||
error[E0277]: the trait bound `FooParams: StateData` is not satisfied
|
error[E0277]: the trait bound `FooParams: StateData` is not satisfied
|
||||||
|
@ -28,7 +17,7 @@ error[E0277]: the trait bound `FooParams: StateData` is not satisfied
|
||||||
|
|
|
|
||||||
::: $WORKSPACE/src/endpoint.rs
|
::: $WORKSPACE/src/endpoint.rs
|
||||||
|
|
|
|
||||||
| type Params: QueryStringExtractor<Body> + Clone + Sync;
|
| type Params: QueryStringExtractor<Body> + Sync;
|
||||||
| -------------------------- required by this bound in `gotham_restful::EndpointWithSchema::Params`
|
| -------------------------- required by this bound in `gotham_restful::EndpointWithSchema::Params`
|
||||||
|
|
||||||
error[E0277]: the trait bound `FooParams: StaticResponseExtender` is not satisfied
|
error[E0277]: the trait bound `FooParams: StaticResponseExtender` is not satisfied
|
||||||
|
@ -39,16 +28,16 @@ error[E0277]: the trait bound `FooParams: StaticResponseExtender` is not satisfi
|
||||||
|
|
|
|
||||||
::: $WORKSPACE/src/endpoint.rs
|
::: $WORKSPACE/src/endpoint.rs
|
||||||
|
|
|
|
||||||
| type Params: QueryStringExtractor<Body> + Clone + Sync;
|
| type Params: QueryStringExtractor<Body> + Sync;
|
||||||
| -------------------------- required by this bound in `gotham_restful::EndpointWithSchema::Params`
|
| -------------------------- required by this bound in `gotham_restful::EndpointWithSchema::Params`
|
||||||
|
|
||||||
error[E0277]: the trait bound `FooParams: Clone` is not satisfied
|
error[E0277]: the trait bound `FooParams: OpenapiType` is not satisfied
|
||||||
--> $DIR/invalid_params_ty.rs:15:16
|
--> $DIR/invalid_params_ty.rs:15:16
|
||||||
|
|
|
|
||||||
15 | fn endpoint(_: FooParams) {
|
15 | fn endpoint(_: FooParams) {
|
||||||
| ^^^^^^^^^ the trait `Clone` is not implemented for `FooParams`
|
| ^^^^^^^^^ the trait `OpenapiType` is not implemented for `FooParams`
|
||||||
|
|
|
|
||||||
::: $WORKSPACE/src/endpoint.rs
|
::: $WORKSPACE/src/endpoint.rs
|
||||||
|
|
|
|
||||||
| type Params: QueryStringExtractor<Body> + Clone + Sync;
|
| #[openapi_bound("Params: crate::OpenapiType")]
|
||||||
| ----- required by this bound in `gotham_restful::EndpointWithSchema::Params`
|
| ---------------------------- required by this bound in `gotham_restful::EndpointWithSchema::Params`
|
||||||
|
|
|
@ -1,14 +1,3 @@
|
||||||
error[E0277]: the trait bound `FooPlaceholders: OpenapiType` is not satisfied
|
|
||||||
--> $DIR/invalid_placeholders_ty.rs:15:16
|
|
||||||
|
|
|
||||||
15 | fn endpoint(_: FooPlaceholders) {
|
|
||||||
| ^^^^^^^^^^^^^^^ the trait `OpenapiType` is not implemented for `FooPlaceholders`
|
|
||||||
|
|
|
||||||
::: $WORKSPACE/src/endpoint.rs
|
|
||||||
|
|
|
||||||
| #[openapi_bound("Placeholders: OpenapiType")]
|
|
||||||
| --------------------------- required by this bound in `gotham_restful::EndpointWithSchema::Placeholders`
|
|
||||||
|
|
||||||
error[E0277]: the trait bound `for<'de> FooPlaceholders: serde::de::Deserialize<'de>` is not satisfied
|
error[E0277]: the trait bound `for<'de> FooPlaceholders: serde::de::Deserialize<'de>` is not satisfied
|
||||||
--> $DIR/invalid_placeholders_ty.rs:15:16
|
--> $DIR/invalid_placeholders_ty.rs:15:16
|
||||||
|
|
|
|
||||||
|
@ -17,7 +6,7 @@ error[E0277]: the trait bound `for<'de> FooPlaceholders: serde::de::Deserialize<
|
||||||
|
|
|
|
||||||
::: $WORKSPACE/src/endpoint.rs
|
::: $WORKSPACE/src/endpoint.rs
|
||||||
|
|
|
|
||||||
| type Placeholders: PathExtractor<Body> + Clone + Sync;
|
| type Placeholders: PathExtractor<Body> + Sync;
|
||||||
| ------------------- required by this bound in `gotham_restful::EndpointWithSchema::Placeholders`
|
| ------------------- required by this bound in `gotham_restful::EndpointWithSchema::Placeholders`
|
||||||
|
|
||||||
error[E0277]: the trait bound `FooPlaceholders: StateData` is not satisfied
|
error[E0277]: the trait bound `FooPlaceholders: StateData` is not satisfied
|
||||||
|
@ -28,7 +17,7 @@ error[E0277]: the trait bound `FooPlaceholders: StateData` is not satisfied
|
||||||
|
|
|
|
||||||
::: $WORKSPACE/src/endpoint.rs
|
::: $WORKSPACE/src/endpoint.rs
|
||||||
|
|
|
|
||||||
| type Placeholders: PathExtractor<Body> + Clone + Sync;
|
| type Placeholders: PathExtractor<Body> + Sync;
|
||||||
| ------------------- required by this bound in `gotham_restful::EndpointWithSchema::Placeholders`
|
| ------------------- required by this bound in `gotham_restful::EndpointWithSchema::Placeholders`
|
||||||
|
|
||||||
error[E0277]: the trait bound `FooPlaceholders: StaticResponseExtender` is not satisfied
|
error[E0277]: the trait bound `FooPlaceholders: StaticResponseExtender` is not satisfied
|
||||||
|
@ -39,16 +28,16 @@ error[E0277]: the trait bound `FooPlaceholders: StaticResponseExtender` is not s
|
||||||
|
|
|
|
||||||
::: $WORKSPACE/src/endpoint.rs
|
::: $WORKSPACE/src/endpoint.rs
|
||||||
|
|
|
|
||||||
| type Placeholders: PathExtractor<Body> + Clone + Sync;
|
| type Placeholders: PathExtractor<Body> + Sync;
|
||||||
| ------------------- required by this bound in `gotham_restful::EndpointWithSchema::Placeholders`
|
| ------------------- required by this bound in `gotham_restful::EndpointWithSchema::Placeholders`
|
||||||
|
|
||||||
error[E0277]: the trait bound `FooPlaceholders: Clone` is not satisfied
|
error[E0277]: the trait bound `FooPlaceholders: OpenapiType` is not satisfied
|
||||||
--> $DIR/invalid_placeholders_ty.rs:15:16
|
--> $DIR/invalid_placeholders_ty.rs:15:16
|
||||||
|
|
|
|
||||||
15 | fn endpoint(_: FooPlaceholders) {
|
15 | fn endpoint(_: FooPlaceholders) {
|
||||||
| ^^^^^^^^^^^^^^^ the trait `Clone` is not implemented for `FooPlaceholders`
|
| ^^^^^^^^^^^^^^^ the trait `OpenapiType` is not implemented for `FooPlaceholders`
|
||||||
|
|
|
|
||||||
::: $WORKSPACE/src/endpoint.rs
|
::: $WORKSPACE/src/endpoint.rs
|
||||||
|
|
|
|
||||||
| type Placeholders: PathExtractor<Body> + Clone + Sync;
|
| #[openapi_bound("Placeholders: crate::OpenapiType")]
|
||||||
| ----- required by this bound in `gotham_restful::EndpointWithSchema::Placeholders`
|
| ---------------------------------- required by this bound in `gotham_restful::EndpointWithSchema::Placeholders`
|
||||||
|
|
12
tests/ui/openapi_type/enum_with_fields.rs
Normal file
12
tests/ui/openapi_type/enum_with_fields.rs
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
#[macro_use]
|
||||||
|
extern crate gotham_restful;
|
||||||
|
|
||||||
|
#[derive(OpenapiType)]
|
||||||
|
enum Food {
|
||||||
|
Pasta,
|
||||||
|
Pizza { pineapple: bool },
|
||||||
|
Rice,
|
||||||
|
Other(String)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {}
|
11
tests/ui/openapi_type/enum_with_fields.stderr
Normal file
11
tests/ui/openapi_type/enum_with_fields.stderr
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
error: #[derive(OpenapiType)] does not support enum variants with fields
|
||||||
|
--> $DIR/enum_with_fields.rs:7:2
|
||||||
|
|
|
||||||
|
7 | Pizza { pineapple: bool },
|
||||||
|
| ^^^^^
|
||||||
|
|
||||||
|
error: #[derive(OpenapiType)] does not support enum variants with fields
|
||||||
|
--> $DIR/enum_with_fields.rs:9:2
|
||||||
|
|
|
||||||
|
9 | Other(String)
|
||||||
|
| ^^^^^
|
10
tests/ui/openapi_type/nullable_non_bool.rs
Normal file
10
tests/ui/openapi_type/nullable_non_bool.rs
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
#[macro_use]
|
||||||
|
extern crate gotham_restful;
|
||||||
|
|
||||||
|
#[derive(OpenapiType)]
|
||||||
|
struct Foo {
|
||||||
|
#[openapi(nullable = "yes, please")]
|
||||||
|
bar: String
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {}
|
5
tests/ui/openapi_type/nullable_non_bool.stderr
Normal file
5
tests/ui/openapi_type/nullable_non_bool.stderr
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
error: Expected bool
|
||||||
|
--> $DIR/nullable_non_bool.rs:6:23
|
||||||
|
|
|
||||||
|
6 | #[openapi(nullable = "yes, please")]
|
||||||
|
| ^^^^^^^^^^^^^
|
10
tests/ui/openapi_type/rename_non_string.rs
Normal file
10
tests/ui/openapi_type/rename_non_string.rs
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
#[macro_use]
|
||||||
|
extern crate gotham_restful;
|
||||||
|
|
||||||
|
#[derive(OpenapiType)]
|
||||||
|
struct Foo {
|
||||||
|
#[openapi(rename = 42)]
|
||||||
|
bar: String
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {}
|
5
tests/ui/openapi_type/rename_non_string.stderr
Normal file
5
tests/ui/openapi_type/rename_non_string.stderr
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
error: Expected string literal
|
||||||
|
--> $DIR/rename_non_string.rs:6:21
|
||||||
|
|
|
||||||
|
6 | #[openapi(rename = 42)]
|
||||||
|
| ^^
|
7
tests/ui/openapi_type/tuple_struct.rs
Normal file
7
tests/ui/openapi_type/tuple_struct.rs
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
#[macro_use]
|
||||||
|
extern crate gotham_restful;
|
||||||
|
|
||||||
|
#[derive(OpenapiType)]
|
||||||
|
struct Foo(String);
|
||||||
|
|
||||||
|
fn main() {}
|
5
tests/ui/openapi_type/tuple_struct.stderr
Normal file
5
tests/ui/openapi_type/tuple_struct.stderr
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
error: #[derive(OpenapiType)] does not support unnamed fields
|
||||||
|
--> $DIR/tuple_struct.rs:5:11
|
||||||
|
|
|
||||||
|
5 | struct Foo(String);
|
||||||
|
| ^^^^^^^^
|
10
tests/ui/openapi_type/union.rs
Normal file
10
tests/ui/openapi_type/union.rs
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
#[macro_use]
|
||||||
|
extern crate gotham_restful;
|
||||||
|
|
||||||
|
#[derive(OpenapiType)]
|
||||||
|
union IntOrPointer {
|
||||||
|
int: u64,
|
||||||
|
pointer: *mut String
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {}
|
5
tests/ui/openapi_type/union.stderr
Normal file
5
tests/ui/openapi_type/union.stderr
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
error: #[derive(OpenapiType)] only works for structs and enums
|
||||||
|
--> $DIR/union.rs:5:1
|
||||||
|
|
|
||||||
|
5 | union IntOrPointer {
|
||||||
|
| ^^^^^
|
10
tests/ui/openapi_type/unknown_key.rs
Normal file
10
tests/ui/openapi_type/unknown_key.rs
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
#[macro_use]
|
||||||
|
extern crate gotham_restful;
|
||||||
|
|
||||||
|
#[derive(OpenapiType)]
|
||||||
|
struct Foo {
|
||||||
|
#[openapi(like = "pizza")]
|
||||||
|
bar: String
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {}
|
5
tests/ui/openapi_type/unknown_key.stderr
Normal file
5
tests/ui/openapi_type/unknown_key.stderr
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
error: Unknown key
|
||||||
|
--> $DIR/unknown_key.rs:6:12
|
||||||
|
|
|
||||||
|
6 | #[openapi(like = "pizza")]
|
||||||
|
| ^^^^
|
Loading…
Add table
Add a link
Reference in a new issue