1
0
Fork 0
mirror of https://gitlab.com/msrd0/gotham-restful.git synced 2025-02-23 04:52:28 +00:00

Merge branch 'openapi'

This commit is contained in:
Dominic 2019-10-04 17:35:42 +02:00
commit 6f0997808b
Signed by: msrd0
GPG key ID: DCC8C247452E98F9
19 changed files with 1225 additions and 124 deletions

102
Cargo.lock generated
View file

@ -194,6 +194,11 @@ dependencies = [
"lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "dtoa"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "either"
version = "1.5.3"
@ -208,6 +213,19 @@ dependencies = [
"version_check 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "example"
version = "0.0.1"
dependencies = [
"fake 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"gotham 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"gotham_restful 0.0.1",
"gotham_restful_derive 0.0.1",
"log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
"log4rs 0.8.3 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.101 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "failure"
version = "0.1.5"
@ -322,23 +340,6 @@ dependencies = [
"uuid 0.7.4 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "gotham-restful"
version = "0.0.1"
dependencies = [
"failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
"fake 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"futures 0.1.29 (registry+https://github.com/rust-lang/crates.io-index)",
"gotham 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"gotham_derive 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"hyper 0.12.35 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
"log4rs 0.8.3 (registry+https://github.com/rust-lang/crates.io-index)",
"mime 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.101 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_json 1.0.40 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "gotham_derive"
version = "0.4.0"
@ -348,6 +349,36 @@ dependencies = [
"syn 0.15.44 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "gotham_restful"
version = "0.0.1"
dependencies = [
"chrono 0.4.9 (registry+https://github.com/rust-lang/crates.io-index)",
"failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
"futures 0.1.29 (registry+https://github.com/rust-lang/crates.io-index)",
"gotham 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"gotham_derive 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"hyper 0.12.35 (registry+https://github.com/rust-lang/crates.io-index)",
"indexmap 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
"mime 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)",
"openapiv3 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.101 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_json 1.0.40 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "gotham_restful_derive"
version = "0.0.1"
dependencies = [
"fake 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
"log4rs 0.8.3 (registry+https://github.com/rust-lang/crates.io-index)",
"proc-macro2 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)",
"quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
"syn 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "h2"
version = "0.1.26"
@ -439,6 +470,9 @@ dependencies = [
name = "indexmap"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"serde 1.0.101 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "iovec"
@ -625,6 +659,17 @@ dependencies = [
"libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "openapiv3"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"indexmap 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.101 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_json 1.0.40 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_yaml 0.8.9 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "owning_ref"
version = "0.4.0"
@ -999,6 +1044,17 @@ dependencies = [
"serde 1.0.101 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "serde_yaml"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"dtoa 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)",
"linked-hash-map 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.101 (registry+https://github.com/rust-lang/crates.io-index)",
"yaml-rust 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "skeptic"
version = "0.13.4"
@ -1447,6 +1503,14 @@ dependencies = [
"winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "yaml-rust"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"linked-hash-map 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)",
]
[metadata]
"checksum aho-corasick 0.7.6 (registry+https://github.com/rust-lang/crates.io-index)" = "58fb5e95d83b38284460a5fda7d6470aa0b8844d283a0b614b8535e880800d2d"
"checksum arc-swap 0.3.11 (registry+https://github.com/rust-lang/crates.io-index)" = "bc4662175ead9cd84451d5c35070517777949a2ed84551764129cedb88384841"
@ -1472,6 +1536,7 @@ dependencies = [
"checksum crossbeam-epoch 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "fedcd6772e37f3da2a9af9bf12ebe046c0dfe657992377b4df982a2b54cd37a9"
"checksum crossbeam-queue 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7c979cd6cfe72335896575c6b5688da489e420d36a27a0b9eb0c73db574b4a4b"
"checksum crossbeam-utils 0.6.6 (registry+https://github.com/rust-lang/crates.io-index)" = "04973fa96e96579258a5091af6003abde64af786b860f18622b82e026cca60e6"
"checksum dtoa 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "ea57b42383d091c85abcc2706240b94ab2a8fa1fc81c10ff23c4de06e2a90b5e"
"checksum either 1.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "bb1f6b1ce1c140482ea30ddd3335fc0024ac7ee112895426e0a629a6c20adfe3"
"checksum error-chain 0.12.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3ab49e9dcb602294bc42f9a7dfc9bc6e936fca4418ea300dbfb84fe16de0b7d9"
"checksum failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "795bd83d3abeb9220f257e597aa0080a508b27533824adf336529648f6abf7e2"
@ -1518,6 +1583,7 @@ dependencies = [
"checksum num-integer 0.1.41 (registry+https://github.com/rust-lang/crates.io-index)" = "b85e541ef8255f6cf42bbfe4ef361305c6c135d10919ecc26126c4e5ae94bc09"
"checksum num-traits 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "6ba9a427cfca2be13aa6f6403b0b7e7368fe982bfa16fccc450ce74c46cd9b32"
"checksum num_cpus 1.10.1 (registry+https://github.com/rust-lang/crates.io-index)" = "bcef43580c035376c0705c42792c294b66974abbfd2789b511784023f71f3273"
"checksum openapiv3 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c8776c7d6a58a03d30ba278adfd54d923eb0a24e26cbf39d2b97648306935d65"
"checksum owning_ref 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "49a4b8ea2179e6a2e27411d3bca09ca6dd630821cf6894c6c7c8467a8ee7ef13"
"checksum parking_lot 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "ab41b4aed082705d1056416ae4468b6ea99d52599ecf3169b00088d43113e337"
"checksum parking_lot_core 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "94c8c7923936b28d546dfd14d4472eaf34c99b14e1c973a32b3e6d4eb04298c9"
@ -1562,6 +1628,7 @@ dependencies = [
"checksum serde 1.0.101 (registry+https://github.com/rust-lang/crates.io-index)" = "9796c9b7ba2ffe7a9ce53c2287dfc48080f4b2b362fcc245a259b3a7201119dd"
"checksum serde_derive 1.0.101 (registry+https://github.com/rust-lang/crates.io-index)" = "4b133a43a1ecd55d4086bd5b4dc6c1751c68b1bfbeba7a5040442022c7e7c02e"
"checksum serde_json 1.0.40 (registry+https://github.com/rust-lang/crates.io-index)" = "051c49229f282f7c6f3813f8286cc1e3323e8051823fce42c7ea80fe13521704"
"checksum serde_yaml 0.8.9 (registry+https://github.com/rust-lang/crates.io-index)" = "38b08a9a90e5260fe01c6480ec7c811606df6d3a660415808c3c3fa8ed95b582"
"checksum skeptic 0.13.4 (registry+https://github.com/rust-lang/crates.io-index)" = "d6fb8ed853fdc19ce09752d63f3a2e5b5158aeb261520cd75eb618bd60305165"
"checksum slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8"
"checksum smallvec 0.6.10 (registry+https://github.com/rust-lang/crates.io-index)" = "ab606a9c5e214920bb66c458cd7be8ef094f813f20fe77a54cc7dbfff220d4b7"
@ -1611,3 +1678,4 @@ dependencies = [
"checksum winapi-util 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7168bab6e1daee33b4557efd0e95d5ca70a03706d39fa5f3fe7a236f584b03c9"
"checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
"checksum ws2_32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e"
"checksum yaml-rust 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "65923dd1784f44da1d2c3dbbc5e822045628c590ba72123e1c73d3c230c4434d"

View file

@ -1,31 +1,8 @@
# -*- eval: (cargo-minor-mode 1) -*-
[package]
name = "gotham-restful"
version = "0.0.1"
authors = ["Dominic Meiser <git@msrd0.de>"]
edition = "2018"
description = "RESTful additions for Gotham"
keywords = ["gotham", "rest", "restful"]
license = "EPL-2.0"
readme = "README.md"
include = ["src/**/*", "Cargo.toml", "LICENSE"]
repository = "https://gitlab.com/msrd0/gotham-restful"
[badges]
gitlab = { repository = "msrd0/gotham-restful", branch = "master" }
[dependencies]
failure = "0.1"
futures = "0.1"
gotham = "0.4"
gotham_derive = "0.4"
hyper = "0.12"
mime = "0.3"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
[dev-dependencies]
fake = "2.2"
log = "0.4"
log4rs = { version = "0.8", features = ["console_appender"], default-features = false }
[workspace]
members = [
"gotham_restful",
"gotham_restful_derive",
"example"
]

28
example/Cargo.toml Normal file
View file

@ -0,0 +1,28 @@
# -*- eval: (cargo-minor-mode 1) -*-
[package]
name = "example"
version = "0.0.1"
authors = ["Dominic Meiser <git@msrd0.de>"]
edition = "2018"
license = "Unlicense"
readme = "README.md"
include = ["src/**/*", "Cargo.toml", "LICENSE"]
repository = "https://gitlab.com/msrd0/gotham-restful"
[badges]
gitlab = { repository = "msrd0/gotham-restful", branch = "master" }
[dependencies]
fake = "2.2"
gotham = "0.4"
gotham_restful = { path = "../gotham_restful", features = ["openapi"] }
gotham_restful_derive = { path = "../gotham_restful_derive", features = ["openapi"] }
log = "0.4"
log4rs = { version = "0.8", features = ["console_appender"], default-features = false }
serde = "1"
[dev-dependencies]
fake = "2.2"
log = "0.4"
log4rs = { version = "0.8", features = ["console_appender"], default-features = false }

24
example/LICENSE Normal file
View file

@ -0,0 +1,24 @@
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <http://unlicense.org>

View file

@ -1,4 +1,5 @@
#[macro_use] extern crate log;
#[macro_use] extern crate gotham_restful_derive;
use fake::{faker::internet::en::Username, Fake};
use gotham::{
@ -14,6 +15,7 @@ use log4rs::{
config::{Appender, Config, Root},
encode::pattern::PatternEncoder
};
use serde::{Deserialize, Serialize};
rest_resource!{Users, route => {
route.read_all::<Self, _>();
@ -23,18 +25,27 @@ rest_resource!{Users, route => {
route.update::<Self, _, _, _>();
}}
rest_struct!{User {
username : String
}}
impl ResourceReadAll<Success<Vec<User>>> for Users
#[derive(Deserialize, OpenapiType, Serialize)]
struct TestStruct
{
fn read_all(_state : &mut State) -> Success<Vec<User>>
foo : String
}
#[derive(Deserialize, OpenapiType, Serialize)]
struct User
{
username : String,
test : Option<Vec<TestStruct>>
}
impl ResourceReadAll<Success<Vec<Option<User>>>> for Users
{
fn read_all(_state : &mut State) -> Success<Vec<Option<User>>>
{
vec![Username().fake(), Username().fake()]
.into_iter()
.map(|username| User { username })
.collect::<Vec<User>>()
.map(|username| Some(User { username, test: None }))
.collect::<Vec<Option<User>>>()
.into()
}
}
@ -44,7 +55,7 @@ impl ResourceRead<u64, Success<User>> for Users
fn read(_state : &mut State, id : u64) -> Success<User>
{
let username : String = Username().fake();
User { username: format!("{}{}", username, id) }.into()
User { username: format!("{}{}", username, id), test: None }.into()
}
}
@ -118,7 +129,10 @@ fn main()
);
gotham::start(ADDR, build_router(chain, pipelines, |route| {
route.resource::<Users, _>("users");
route.with_openapi("Users Example", "0.0.1", format!("http://{}", ADDR), |mut route| {
route.resource::<Users, _>("users");
route.get_openapi("openapi");
});
}));
println!("Gotham started on {} for testing", ADDR);
}

34
gotham_restful/Cargo.toml Normal file
View file

@ -0,0 +1,34 @@
# -*- eval: (cargo-minor-mode 1) -*-
[package]
name = "gotham_restful"
version = "0.0.1"
authors = ["Dominic Meiser <git@msrd0.de>"]
edition = "2018"
description = "RESTful additions for Gotham"
keywords = ["gotham", "rest", "restful"]
license = "EPL-2.0"
readme = "../README.md"
include = ["src/**/*", "Cargo.toml", "../LICENSE"]
repository = "https://gitlab.com/msrd0/gotham-restful"
[badges]
gitlab = { repository = "msrd0/gotham-restful", branch = "master" }
[dependencies]
chrono = { version = "0.4", optional = true }
failure = "0.1"
futures = "0.1"
gotham = "0.4"
gotham_derive = "0.4"
hyper = "0.12"
indexmap = { version = "1.0", optional = true }
log = { version = "0.4", optional = true }
mime = "0.3"
openapiv3 = { version = "0.3", optional = true }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
[features]
default = ["openapi", "chrono"]
openapi = ["indexmap", "log", "openapiv3"]

View file

@ -0,0 +1,109 @@
#[cfg(feature = "openapi")]
pub mod openapi
{
pub use indexmap::IndexMap;
pub use openapiv3::{ObjectType, ReferenceOr, Schema, SchemaData, SchemaKind, StringType, Type, VariantOrUnknownOrEmpty};
}
#[cfg(not(feature = "openapi"))]
#[macro_export]
macro_rules! rest_struct {
($struct_name:ident { $($field_id:ident : $field_ty:ty),* }) => {
#[derive(serde::Deserialize, serde::Serialize)]
pub struct $struct_name
{
$($field_id : $field_ty),*
}
}
}
#[cfg(feature = "openapi")]
#[macro_export]
macro_rules! rest_struct {
($struct_name:ident { $($field_id:ident : $field_ty:ty),* }) => {
#[derive(serde::Deserialize, serde::Serialize)]
struct $struct_name
{
$($field_id : $field_ty),*
}
impl ::gotham_restful::OpenapiType for $struct_name
{
fn to_schema() -> ::gotham_restful::OpenapiSchema
{
use ::gotham_restful::{helper::openapi::*, 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();
$(
{
let mut schema = <$field_ty>::to_schema();
if schema.nullable
{
schema.nullable = false;
schema.name = schema.name.map(|name|
if name.ends_with("OrNull") {
name[..(name.len()-6)].to_string()
} else { name });
}
else
{
required.push(stringify!($field_id).to_string());
}
if let Some(name) = schema.name.clone()
{
properties.insert(
stringify!($field_id).to_string(),
ReferenceOr::Reference { reference: format!("#/components/schemas/{}", name) }
);
dependencies.insert(name, schema);
}
else
{
properties.insert(
stringify!($field_id).to_string(),
ReferenceOr::Item(Box::new(<$field_ty>::to_schema().to_schema()))
);
}
}
)*
let schema = SchemaKind::Type(Type::Object(ObjectType {
properties,
required,
additional_properties: None,
min_properties: None,
max_properties: None
}));
OpenapiSchema {
name: Some(stringify!($struct_name).to_string()),
nullable: false,
schema,
dependencies
}
}
}
}
}
#[macro_export]
macro_rules! rest_resource {
($res_name:ident, $route:ident => $setup:block) => {
pub struct $res_name;
impl ::gotham_restful::Resource for $res_name
{
fn name() -> String
{
stringify!($res_name).to_string()
}
fn setup<D : ::gotham_restful::DrawResourceRoutes>(mut $route : D) $setup
}
}
}

62
gotham_restful/src/lib.rs Normal file
View file

@ -0,0 +1,62 @@
#[macro_use] extern crate gotham_derive;
#[macro_use] extern crate serde;
pub use hyper::StatusCode;
use serde::{de::DeserializeOwned, Serialize};
pub mod helper;
#[cfg(feature = "openapi")]
pub mod openapi;
#[cfg(feature = "openapi")]
pub use openapi::{
router::{GetOpenapi, OpenapiRouter},
types::{OpenapiSchema, OpenapiType}
};
mod resource;
pub use resource::{
Resource,
ResourceReadAll,
ResourceRead,
ResourceCreate,
ResourceUpdateAll,
ResourceUpdate,
ResourceDeleteAll,
ResourceDelete
};
mod result;
pub use result::{ResourceResult, Success};
mod routing;
pub use routing::{DrawResources, DrawResourceRoutes};
#[cfg(feature = "openapi")]
pub use routing::WithOpenapi;
/// A type that can be used inside a request or response body. Implemented for every type
/// that is serializable with serde, however, it is recommended to use the rest_struct!
/// macro to create one.
#[cfg(not(feature = "openapi"))]
pub trait ResourceType : DeserializeOwned + Serialize
{
}
#[cfg(not(feature = "openapi"))]
impl<T : DeserializeOwned + Serialize> ResourceType for T
{
}
/// A type that can be used inside a request or response body. Implemented for every type
/// that is serializable with serde, however, it is recommended to use the rest_struct!
/// macro to create one.
#[cfg(feature = "openapi")]
pub trait ResourceType : OpenapiType + DeserializeOwned + Serialize
{
}
#[cfg(feature = "openapi")]
impl<T : OpenapiType + DeserializeOwned + Serialize> ResourceType for T
{
}

View file

@ -0,0 +1,3 @@
pub mod router;
pub mod types;

View file

@ -0,0 +1,378 @@
use crate::{
resource::*,
result::*,
routing::*,
OpenapiSchema,
OpenapiType,
ResourceType
};
use futures::future::ok;
use gotham::{
handler::{Handler, HandlerFuture, NewHandler},
helpers::http::response::create_response,
pipeline::chain::PipelineHandleChain,
router::builder::*,
state::State
};
use indexmap::IndexMap;
use log::error;
use mime::{APPLICATION_JSON, TEXT_PLAIN};
use openapiv3::{
Components, MediaType, OpenAPI, Operation, Parameter, ParameterData, ParameterSchemaOrContent, PathItem,
PathStyle, Paths, ReferenceOr, ReferenceOr::Item, ReferenceOr::Reference, RequestBody, Response, Responses,
Schema, Server, StatusCode
};
use serde::de::DeserializeOwned;
use std::panic::RefUnwindSafe;
pub struct OpenapiRouter(OpenAPI);
impl OpenapiRouter
{
pub fn new<Title : ToString, Version : ToString, Url : ToString>(title : Title, version : Version, server_url : Url) -> Self
{
Self(OpenAPI {
openapi: "3.0.2".to_string(),
info: openapiv3::Info {
title: title.to_string(),
description: None,
terms_of_service: None,
contact: None,
license: None,
version: version.to_string()
},
servers: vec![Server {
url: server_url.to_string(),
description: None,
variables: None
}],
paths: Paths::new(),
components: None,
security: Vec::new(),
tags: Vec::new(),
external_docs: None
})
}
/// Remove path from the OpenAPI spec, or return an empty one if not included. This is handy if you need to
/// modify the path and add it back after the modification
fn remove_path(&mut self, path : &str) -> PathItem
{
if let Some(Item(item)) = self.0.paths.swap_remove(path)
{
return item;
}
return PathItem::default()
}
fn add_path<Path : ToString>(&mut self, path : Path, item : PathItem)
{
self.0.paths.insert(path.to_string(), Item(item));
}
fn add_schema_impl(&mut self, name : String, mut schema : OpenapiSchema)
{
self.add_schema_dependencies(&mut schema.dependencies);
match &mut self.0.components {
Some(comp) => {
comp.schemas.insert(name, Item(schema.to_schema()));
},
None => {
let mut comp = Components::default();
comp.schemas.insert(name, Item(schema.to_schema()));
self.0.components = Some(comp);
}
};
}
fn add_schema_dependencies(&mut self, dependencies : &mut IndexMap<String, OpenapiSchema>)
{
let keys : Vec<String> = dependencies.keys().map(|k| k.to_string()).collect();
for dep in keys
{
let dep_schema = dependencies.swap_remove(&dep);
if let Some(dep_schema) = dep_schema
{
self.add_schema_impl(dep, dep_schema);
}
}
}
fn add_schema<T : OpenapiType>(&mut self) -> ReferenceOr<Schema>
{
let mut schema = T::to_schema();
if let Some(name) = schema.name.clone()
{
let reference = Reference { reference: format!("#/components/schemas/{}", name) };
self.add_schema_impl(name, schema);
reference
}
else
{
self.add_schema_dependencies(&mut schema.dependencies);
Item(schema.to_schema())
}
}
}
#[derive(Clone)]
struct OpenapiHandler(Result<String, String>);
// dunno what/why/whatever
impl RefUnwindSafe for OpenapiHandler {}
impl OpenapiHandler
{
fn new(openapi : &OpenapiRouter) -> Self
{
Self(serde_json::to_string(&openapi.0).map_err(|e| format!("{}", e)))
}
}
impl NewHandler for OpenapiHandler
{
type Instance = Self;
fn new_handler(&self) -> gotham::error::Result<Self::Instance>
{
Ok(self.clone())
}
}
impl Handler for OpenapiHandler
{
fn handle(self, state : State) -> Box<HandlerFuture>
{
match self.0 {
Ok(body) => {
let res = create_response(&state, hyper::StatusCode::OK, APPLICATION_JSON, body);
Box::new(ok((state, res)))
},
Err(e) => {
error!("Unable to handle OpenAPI request due to error: {}", e);
let res = create_response(&state, hyper::StatusCode::INTERNAL_SERVER_ERROR, TEXT_PLAIN, "");
Box::new(ok((state, res)))
}
}
}
}
pub trait GetOpenapi
{
fn get_openapi(&mut self, path : &str);
}
fn schema_to_content(schema : ReferenceOr<Schema>) -> IndexMap<String, MediaType>
{
let mut content : IndexMap<String, MediaType> = IndexMap::new();
content.insert(APPLICATION_JSON.to_string(), MediaType {
schema: Some(schema),
example: None,
examples: IndexMap::new(),
encoding: IndexMap::new()
});
content
}
fn new_operation(schema : ReferenceOr<Schema>, path_params : Vec<&str>, body_schema : Option<ReferenceOr<Schema>>) -> Operation
{
let mut responses : IndexMap<StatusCode, ReferenceOr<Response>> = IndexMap::new();
responses.insert(StatusCode::Code(200), Item(Response {
description: "OK".to_string(),
headers: IndexMap::new(),
content: schema_to_content(schema),
links: IndexMap::new()
}));
let mut params : Vec<ReferenceOr<Parameter>> = Vec::new();
for param in path_params
{
params.push(Item(Parameter::Path {
parameter_data: ParameterData {
name: param.to_string(),
description: None,
required: true,
deprecated: None,
format: ParameterSchemaOrContent::Schema(Item(String::to_schema().to_schema())),
example: None,
examples: IndexMap::new()
},
style: PathStyle::default(),
}));
}
let request_body = body_schema.map(|schema| Item(RequestBody {
description: None,
content: schema_to_content(schema),
required: true
}));
Operation {
tags: Vec::new(),
summary: None,
description: None,
external_documentation: None,
operation_id: None, // TODO
parameters: params,
request_body,
responses: Responses {
default: None,
responses
},
deprecated: false,
security: Vec::new(),
servers: Vec::new()
}
}
macro_rules! implOpenapiRouter {
($implType:ident) => {
impl<'a, C, P> GetOpenapi for (&mut $implType<'a, C, P>, &mut OpenapiRouter)
where
C : PipelineHandleChain<P> + Copy + Send + Sync + 'static,
P : RefUnwindSafe + Send + Sync + 'static
{
fn get_openapi(&mut self, path : &str)
{
self.0.get(path).to_new_handler(OpenapiHandler::new(&self.1));
}
}
impl<'a, C, P> DrawResources for (&mut $implType<'a, C, P>, &mut OpenapiRouter)
where
C : PipelineHandleChain<P> + Copy + Send + Sync + 'static,
P : RefUnwindSafe + Send + Sync + 'static
{
fn resource<R : Resource, T : ToString>(&mut self, path : T)
{
R::setup((self, path.to_string()));
}
}
impl<'a, C, P> DrawResourceRoutes for (&mut (&mut $implType<'a, C, P>, &mut OpenapiRouter), String)
where
C : PipelineHandleChain<P> + Copy + Send + Sync + 'static,
P : RefUnwindSafe + Send + Sync + 'static
{
fn read_all<Handler, Res>(&mut self)
where
Res : ResourceResult,
Handler : ResourceReadAll<Res>
{
let schema = (self.0).1.add_schema::<Res>();
let path = format!("/{}", &self.1);
let mut item = (self.0).1.remove_path(&path);
item.get = Some(new_operation(schema, vec![], None));
(self.0).1.add_path(path, item);
(&mut *(self.0).0, self.1.to_string()).read_all::<Handler, Res>()
}
fn read<Handler, ID, Res>(&mut self)
where
ID : DeserializeOwned + Clone + RefUnwindSafe + Send + Sync + 'static,
Res : ResourceResult,
Handler : ResourceRead<ID, Res>
{
let schema = (self.0).1.add_schema::<Res>();
let path = format!("/{}/{{id}}", &self.1);
let mut item = (self.0).1.remove_path(&path);
item.get = Some(new_operation(schema, vec!["id"], None));
(self.0).1.add_path(path, item);
(&mut *(self.0).0, self.1.to_string()).read::<Handler, ID, Res>()
}
fn create<Handler, Body, Res>(&mut self)
where
Body : ResourceType,
Res : ResourceResult,
Handler : ResourceCreate<Body, Res>
{
let schema = (self.0).1.add_schema::<Res>();
let body_schema = (self.0).1.add_schema::<Body>();
let path = format!("/{}", &self.1);
let mut item = (self.0).1.remove_path(&path);
item.post = Some(new_operation(schema, vec![], Some(body_schema)));
(self.0).1.add_path(path, item);
(&mut *(self.0).0, self.1.to_string()).create::<Handler, Body, Res>()
}
fn update_all<Handler, Body, Res>(&mut self)
where
Body : ResourceType,
Res : ResourceResult,
Handler : ResourceUpdateAll<Body, Res>
{
let schema = (self.0).1.add_schema::<Res>();
let body_schema = (self.0).1.add_schema::<Body>();
let path = format!("/{}", &self.1);
let mut item = (self.0).1.remove_path(&path);
item.put = Some(new_operation(schema, vec![], Some(body_schema)));
(self.0).1.add_path(path, item);
(&mut *(self.0).0, self.1.to_string()).update_all::<Handler, Body, Res>()
}
fn update<Handler, ID, Body, Res>(&mut self)
where
ID : DeserializeOwned + Clone + RefUnwindSafe + Send + Sync + 'static,
Body : ResourceType,
Res : ResourceResult,
Handler : ResourceUpdate<ID, Body, Res>
{
let schema = (self.0).1.add_schema::<Res>();
let body_schema = (self.0).1.add_schema::<Body>();
let path = format!("/{}/{{id}}", &self.1);
let mut item = (self.0).1.remove_path(&path);
item.put = Some(new_operation(schema, vec!["id"], Some(body_schema)));
(self.0).1.add_path(path, item);
(&mut *(self.0).0, self.1.to_string()).update::<Handler, ID, Body, Res>()
}
fn delete_all<Handler, Res>(&mut self)
where
Res : ResourceResult,
Handler : ResourceDeleteAll<Res>
{
let schema = (self.0).1.add_schema::<Res>();
let path = format!("/{}", &self.1);
let mut item = (self.0).1.remove_path(&path);
item.delete = Some(new_operation(schema, vec![], None));
(self.0).1.add_path(path, item);
(&mut *(self.0).0, self.1.to_string()).delete_all::<Handler, Res>()
}
fn delete<Handler, ID, Res>(&mut self)
where
ID : DeserializeOwned + Clone + RefUnwindSafe + Send + Sync + 'static,
Res : ResourceResult,
Handler : ResourceDelete<ID, Res>
{
let schema = (self.0).1.add_schema::<Res>();
let path = format!("/{}/{{id}}", &self.1);
let mut item = (self.0).1.remove_path(&path);
item.delete = Some(new_operation(schema, vec!["id"], None));
(self.0).1.add_path(path, item);
(&mut *(self.0).0, self.1.to_string()).delete::<Handler, ID, Res>()
}
}
}
}
implOpenapiRouter!(RouterBuilder);
implOpenapiRouter!(ScopeBuilder);

View file

@ -0,0 +1,189 @@
#[cfg(feature = "chrono")]
use chrono::{
Date, DateTime, FixedOffset, Local, NaiveDate, NaiveDateTime, Utc
};
use indexmap::IndexMap;
use openapiv3::{
ArrayType, IntegerType, NumberType, ObjectType, ReferenceOr::Item, ReferenceOr::Reference, Schema,
SchemaData, SchemaKind, StringFormat, StringType, Type, VariantOrUnknownOrEmpty
};
#[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>,
pub nullable : bool,
pub schema : SchemaKind,
pub dependencies : IndexMap<String, OpenapiSchema>
}
impl OpenapiSchema
{
pub fn new(schema : SchemaKind) -> Self
{
Self {
name: None,
nullable: false,
schema,
dependencies: IndexMap::new()
}
}
pub fn to_schema(self) -> Schema
{
Schema {
schema_data: SchemaData {
nullable: self.nullable,
read_only: false,
write_only: false,
deprecated: false,
external_docs: None,
example: None,
title: self.name,
description: None,
discriminator: None,
default: None
},
schema_kind: self.schema
}
}
}
pub trait OpenapiType
{
fn to_schema() -> OpenapiSchema;
}
impl OpenapiType for ()
{
fn to_schema() -> OpenapiSchema
{
OpenapiSchema::new(SchemaKind::Type(Type::Object(ObjectType::default())))
}
}
impl OpenapiType for bool
{
fn to_schema() -> OpenapiSchema
{
OpenapiSchema::new(SchemaKind::Type(Type::Boolean{}))
}
}
macro_rules! int_types {
($($int_ty:ty),*) => {$(
impl OpenapiType for $int_ty
{
fn to_schema() -> OpenapiSchema
{
OpenapiSchema::new(SchemaKind::Type(Type::Integer(IntegerType::default())))
}
}
)*}
}
int_types!(u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128);
macro_rules! num_types {
($($num_ty:ty),*) => {$(
impl OpenapiType for $num_ty
{
fn to_schema() -> OpenapiSchema
{
OpenapiSchema::new(SchemaKind::Type(Type::Number(NumberType::default())))
}
}
)*}
}
num_types!(f32, f64);
macro_rules! str_types {
($($str_ty:ty),*) => {$(
impl OpenapiType for $str_ty
{
fn to_schema() -> OpenapiSchema
{
OpenapiSchema::new(SchemaKind::Type(Type::String(StringType::default())))
}
}
)*};
(format = $format:ident, $($str_ty:ty),*) => {$(
impl OpenapiType for $str_ty
{
fn to_schema() -> OpenapiSchema
{
OpenapiSchema::new(SchemaKind::Type(Type::String(StringType {
format: VariantOrUnknownOrEmpty::Item(StringFormat::$format),
pattern: None,
enumeration: Vec::new()
})))
}
}
)*};
}
str_types!(String, &str);
impl<T : OpenapiType> OpenapiType for Option<T>
{
fn to_schema() -> OpenapiSchema
{
let schema = T::to_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 to_schema() -> OpenapiSchema
{
let schema = T::to_schema();
let mut dependencies = schema.dependencies.clone();
let items = if let Some(name) = schema.name.clone()
{
let reference = Reference { reference: format!("#/components/schemas/{}", name) };
dependencies.insert(name, schema);
reference
}
else
{
Item(Box::new(schema.to_schema()))
};
OpenapiSchema {
nullable: false,
name: None,
schema: SchemaKind::Type(Type::Array(ArrayType {
items,
min_items: None,
max_items: None,
unique_items: false
})),
dependencies
}
}
}
#[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);

View file

@ -1,4 +1,4 @@
use crate::{DrawResourceRoutes, ResourceResult};
use crate::{DrawResourceRoutes, ResourceResult, ResourceType};
use gotham::state::State;
use serde::de::DeserializeOwned;
use std::panic::RefUnwindSafe;
@ -30,7 +30,7 @@ where
}
/// Handle a POST request on the Resource root.
pub trait ResourceCreate<Body : DeserializeOwned, R : ResourceResult>
pub trait ResourceCreate<Body : ResourceType, R : ResourceResult>
{
fn create(state : &mut State, body : Body) -> R;
}
@ -42,7 +42,7 @@ pub trait ResourceUpdateAll<Body : DeserializeOwned, R : ResourceResult>
}
/// Handle a PUT request on the Resource with an id.
pub trait ResourceUpdate<ID, Body : DeserializeOwned, R : ResourceResult>
pub trait ResourceUpdate<ID, Body : ResourceType, R : ResourceResult>
where
ID : DeserializeOwned + Clone + RefUnwindSafe + Send + Sync + 'static
{

View file

@ -1,4 +1,6 @@
use crate::StatusCode;
use crate::{ResourceType, StatusCode};
#[cfg(feature = "openapi")]
use crate::OpenapiSchema;
use serde::Serialize;
use serde_json::error::Error as SerdeJsonError;
use std::error::Error;
@ -7,6 +9,18 @@ use std::error::Error;
pub trait ResourceResult
{
fn to_json(&self) -> Result<(StatusCode, String), SerdeJsonError>;
#[cfg(feature = "openapi")]
fn to_schema() -> OpenapiSchema;
}
#[cfg(feature = "openapi")]
impl<Res : ResourceResult> crate::OpenapiType for Res
{
fn to_schema() -> OpenapiSchema
{
Self::to_schema()
}
}
/// The default json returned on an 500 Internal Server Error.
@ -28,7 +42,7 @@ impl<T : ToString> From<T> for ResourceError
}
}
impl<R : Serialize, E : Error> ResourceResult for Result<R, E>
impl<R : ResourceType, E : Error> ResourceResult for Result<R, E>
{
fn to_json(&self) -> Result<(StatusCode, String), SerdeJsonError>
{
@ -40,6 +54,12 @@ impl<R : Serialize, E : Error> ResourceResult for Result<R, E>
}
})
}
#[cfg(feature = "openapi")]
fn to_schema() -> OpenapiSchema
{
R::to_schema()
}
}
/// This can be returned from a resource when there is no cause of an error.
@ -53,10 +73,16 @@ impl<T> From<T> for Success<T>
}
}
impl<T : Serialize> ResourceResult for Success<T>
impl<T : ResourceType> ResourceResult for Success<T>
{
fn to_json(&self) -> Result<(StatusCode, String), SerdeJsonError>
{
Ok((StatusCode::OK, serde_json::to_string(&self.0)?))
}
#[cfg(feature = "openapi")]
fn to_schema() -> OpenapiSchema
{
T::to_schema()
}
}

View file

@ -1,8 +1,12 @@
use crate::{
resource::*,
result::{ResourceError, ResourceResult},
ResourceType,
StatusCode
};
#[cfg(feature = "openapi")]
use crate::OpenapiRouter;
use futures::{
future::{Future, err, ok},
stream::Stream
@ -25,6 +29,20 @@ struct PathExtractor<ID : RefUnwindSafe + Send + 'static>
id : ID
}
/// This trait adds the `with_openapi` method to gotham's routing. It turns the default
/// router into one that will only allow RESTful resources, but record them and generate
/// an OpenAPI specification on request.
#[cfg(feature = "openapi")]
pub trait WithOpenapi<D>
{
fn with_openapi<F, Title, Version, Url>(&mut self, title : Title, version : Version, server_url : Url, block : F)
where
F : FnOnce((&mut D, &mut OpenapiRouter)),
Title : ToString,
Version : ToString,
Url : ToString;
}
/// This trait adds the `resource` method to gotham's routing. It allows you to register
/// any RESTful `Resource` with a path.
pub trait DrawResources
@ -49,20 +67,20 @@ pub trait DrawResourceRoutes
fn create<Handler, Body, Res>(&mut self)
where
Body : DeserializeOwned,
Body : ResourceType,
Res : ResourceResult,
Handler : ResourceCreate<Body, Res>;
fn update_all<Handler, Body, Res>(&mut self)
where
Body : DeserializeOwned,
Body : ResourceType,
Res : ResourceResult,
Handler : ResourceUpdateAll<Body, Res>;
fn update<Handler, ID, Body, Res>(&mut self)
where
ID : DeserializeOwned + Clone + RefUnwindSafe + Send + Sync + 'static,
Body : DeserializeOwned,
Body : ResourceType,
Res : ResourceResult,
Handler : ResourceUpdate<ID, Body, Res>;
@ -159,7 +177,7 @@ where
fn create_handler<Handler, Body, Res>(state : State) -> Box<HandlerFuture>
where
Body : DeserializeOwned,
Body : ResourceType,
Res : ResourceResult,
Handler : ResourceCreate<Body, Res>
{
@ -168,7 +186,7 @@ where
fn update_all_handler<Handler, Body, Res>(state : State) -> Box<HandlerFuture>
where
Body : DeserializeOwned,
Body : ResourceType,
Res : ResourceResult,
Handler : ResourceUpdateAll<Body, Res>
{
@ -178,7 +196,7 @@ where
fn update_handler<Handler, ID, Body, Res>(state : State) -> Box<HandlerFuture>
where
ID : DeserializeOwned + Clone + RefUnwindSafe + Send + Sync + 'static,
Body : DeserializeOwned,
Body : ResourceType,
Res : ResourceResult,
Handler : ResourceUpdate<ID, Body, Res>
{
@ -212,6 +230,25 @@ where
macro_rules! implDrawResourceRoutes {
($implType:ident) => {
#[cfg(feature = "openapi")]
impl<'a, C, P> WithOpenapi<Self> for $implType<'a, C, P>
where
C : PipelineHandleChain<P> + Copy + Send + Sync + 'static,
P : RefUnwindSafe + Send + Sync + 'static
{
fn with_openapi<F, Title, Version, Url>(&mut self, title : Title, version : Version, server_url : Url, block : F)
where
F : FnOnce((&mut Self, &mut OpenapiRouter)),
Title : ToString,
Version : ToString,
Url : ToString
{
let mut router = OpenapiRouter::new(title, version, server_url);
block((self, &mut router));
}
}
impl<'a, C, P> DrawResources for $implType<'a, C, P>
where
C : PipelineHandleChain<P> + Copy + Send + Sync + 'static,
@ -250,7 +287,7 @@ macro_rules! implDrawResourceRoutes {
fn create<Handler, Body, Res>(&mut self)
where
Body : DeserializeOwned,
Body : ResourceType,
Res : ResourceResult,
Handler : ResourceCreate<Body, Res>
{
@ -260,7 +297,7 @@ macro_rules! implDrawResourceRoutes {
fn update_all<Handler, Body, Res>(&mut self)
where
Body : DeserializeOwned,
Body : ResourceType,
Res : ResourceResult,
Handler : ResourceUpdateAll<Body, Res>
{
@ -271,7 +308,7 @@ macro_rules! implDrawResourceRoutes {
fn update<Handler, ID, Body, Res>(&mut self)
where
ID : DeserializeOwned + Clone + RefUnwindSafe + Send + Sync + 'static,
Body : DeserializeOwned,
Body : ResourceType,
Res : ResourceResult,
Handler : ResourceUpdate<ID, Body, Res>
{

View file

@ -0,0 +1,33 @@
# -*- eval: (cargo-minor-mode 1) -*-
[package]
name = "gotham_restful_derive"
version = "0.0.1"
authors = ["Dominic Meiser <git@msrd0.de>"]
edition = "2018"
description = "RESTful additions for Gotham - Derive"
keywords = ["gotham", "rest", "restful", "derive"]
license = "EPL-2.0"
readme = "../README.md"
include = ["src/**/*", "Cargo.toml", "../LICENSE"]
repository = "https://gitlab.com/msrd0/gotham-restful"
[lib]
proc-macro = true
[badges]
gitlab = { repository = "msrd0/gotham-restful", branch = "master" }
[dependencies]
proc-macro2 = "1"
quote = "1"
syn = { version = "1", features = ["extra-traits", "full"] }
[dev-dependencies]
fake = "2.2"
log = "0.4"
log4rs = { version = "0.8", features = ["console_appender"], default-features = false }
[features]
default = ["openapi"]
openapi = []

View file

@ -0,0 +1,13 @@
extern crate proc_macro;
use proc_macro::TokenStream;
#[cfg(feature = "openapi")]
mod openapi_type;
#[cfg(feature = "openapi")]
#[proc_macro_derive(OpenapiType)]
pub fn derive_openapi_type(tokens : TokenStream) -> TokenStream
{
openapi_type::expand(tokens)
}

View file

@ -0,0 +1,158 @@
use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
use syn::{
Field,
Fields,
Item,
ItemEnum,
ItemStruct,
Variant,
parse_macro_input
};
pub fn expand(tokens : TokenStream) -> TokenStream
{
let input = parse_macro_input!(tokens as Item);
match input {
Item::Enum(item) => expand_enum(item),
Item::Struct(item) => expand_struct(item),
_ => panic!("derive(OpenapiType) not supported for this context")
}.into()
}
fn expand_variant(variant : &Variant) -> TokenStream2
{
if variant.fields != Fields::Unit
{
panic!("Enum Variants with Fields not supported");
}
let ident = &variant.ident;
quote! {
enumeration.push(stringify!(#ident).to_string());
}
}
fn expand_enum(input : ItemEnum) -> TokenStream2
{
let ident = input.ident;
let generics = input.generics;
let variants : Vec<TokenStream2> = input.variants.iter().map(expand_variant).collect();
quote! {
impl #generics ::gotham_restful::OpenapiType for #ident #generics
{
fn to_schema() -> ::gotham_restful::OpenapiSchema
{
use ::gotham_restful::{helper::openapi::*, OpenapiSchema};
let mut enumeration : Vec<String> = Vec::new();
#(#variants)*
OpenapiSchema::new(SchemaKind::Type(Type::String(StringType {
format: VariantOrUnknownOrEmpty::Empty,
pattern: None,
enumeration
})))
}
}
}
}
fn expand_field(field : &Field) -> TokenStream2
{
let ident = match &field.ident {
Some(ident) => ident,
None => panic!("Fields without ident are not supported")
};
let ty = &field.ty;
quote! {{
let mut schema = <#ty>::to_schema();
if schema.nullable
{
schema.nullable = false;
}
else
{
required.push(stringify!(#ident).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(name) => {
properties.insert(
stringify!(#ident).to_string(),
ReferenceOr::Reference { reference: format!("#/components/schemas/{}", name) }
);
dependencies.insert(name, schema);
},
None => {
properties.insert(
stringify!(#ident).to_string(),
ReferenceOr::Item(Box::new(schema.to_schema()))
);
}
}
}}
}
pub fn expand_struct(input : ItemStruct) -> TokenStream2
{
let ident = input.ident;
let generics = input.generics;
let fields : Vec<TokenStream2> = match input.fields {
Fields::Named(fields) => {
fields.named.iter().map(|field| expand_field(field)).collect()
},
Fields::Unnamed(_) => panic!("Unnamed fields are not supported"),
Fields::Unit => Vec::new()
};
quote!{
impl #generics ::gotham_restful::OpenapiType for #ident #generics
{
fn to_schema() -> ::gotham_restful::OpenapiSchema
{
use ::gotham_restful::{helper::openapi::*, 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(stringify!(#ident).to_string()),
nullable: false,
schema,
dependencies
}
}
}
}
}

View file

@ -1,28 +0,0 @@
#[macro_export]
macro_rules! rest_struct {
($struct_name:ident { $($field_id:ident : $field_ty:ty),* }) => {
#[derive(serde::Deserialize, serde::Serialize)]
pub struct $struct_name
{
$($field_id : $field_ty),*
}
}
}
#[macro_export]
macro_rules! rest_resource {
($res_name:ident, $route:ident => $setup:block) => {
pub struct $res_name;
impl ::gotham_restful::Resource for $res_name
{
fn name() -> String
{
stringify!($res_name).to_string()
}
fn setup<D : ::gotham_restful::DrawResourceRoutes>(mut $route : D) $setup
}
}
}

View file

@ -1,24 +0,0 @@
#[macro_use] extern crate gotham_derive;
#[macro_use] extern crate serde;
pub use hyper::StatusCode;
pub mod helper;
mod resource;
pub use resource::{
Resource,
ResourceReadAll,
ResourceRead,
ResourceCreate,
ResourceUpdateAll,
ResourceUpdate,
ResourceDeleteAll,
ResourceDelete
};
mod result;
pub use result::{ResourceResult, Success};
mod routing;
pub use routing::{DrawResources, DrawResourceRoutes};