mirror of
https://gitlab.com/msrd0/gotham-restful.git
synced 2025-02-22 20:52:27 +00:00
basic structure for openapi_type crate
This commit is contained in:
parent
2251c29d7b
commit
90870e3b6a
9 changed files with 261 additions and 1 deletions
|
@ -1,7 +1,7 @@
|
||||||
# -*- eval: (cargo-minor-mode 1) -*-
|
# -*- eval: (cargo-minor-mode 1) -*-
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
members = [".", "./derive", "./example"]
|
members = [".", "./derive", "./example", "./openapi_type", "./openapi_type_derive"]
|
||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "gotham_restful"
|
name = "gotham_restful"
|
||||||
|
@ -76,3 +76,5 @@ 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" }
|
||||||
|
|
20
openapi_type/Cargo.toml
Normal file
20
openapi_type/Cargo.toml
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
# -*- 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"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
trybuild = "1.0"
|
76
openapi_type/src/lib.rs
Normal file
76
openapi_type/src/lib.rs
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
#![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;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
12
openapi_type/tests/fail/generics_not_openapitype.rs
Normal file
12
openapi_type/tests/fail/generics_not_openapitype.rs
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
use openapi_type::OpenapiType;
|
||||||
|
|
||||||
|
#[derive(OpenapiType)]
|
||||||
|
struct Foo<T> {
|
||||||
|
bar: T
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Bar;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
<Foo<Bar>>::schema();
|
||||||
|
}
|
21
openapi_type/tests/fail/generics_not_openapitype.stderr
Normal file
21
openapi_type/tests/fail/generics_not_openapitype.stderr
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
error[E0599]: no function or associated item named `schema` found for struct `Foo<Bar>` in the current scope
|
||||||
|
--> $DIR/generics_not_openapitype.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`
|
||||||
|
= 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`
|
6
openapi_type/tests/pass/unit_struct.rs
Normal file
6
openapi_type/tests/pass/unit_struct.rs
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
use openapi_type_derive::OpenapiType;
|
||||||
|
|
||||||
|
#[derive(OpenapiType)]
|
||||||
|
struct Foo;
|
||||||
|
|
||||||
|
fn main() {}
|
8
openapi_type/tests/trybuild.rs
Normal file
8
openapi_type/tests/trybuild.rs
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
use trybuild::TestCases;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn trybuild() {
|
||||||
|
let t = TestCases::new();
|
||||||
|
t.pass("tests/pass/*.rs");
|
||||||
|
t.compile_fail("tests/fail/*.rs");
|
||||||
|
}
|
22
openapi_type_derive/Cargo.toml
Normal file
22
openapi_type_derive/Cargo.toml
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
# -*- 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"
|
||||||
|
|
||||||
|
# tests are done using trybuild exclusively
|
||||||
|
autotests = false
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
proc-macro = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
proc-macro2 = "1.0"
|
||||||
|
quote = "1.0"
|
||||||
|
syn = "1.0"
|
93
openapi_type_derive/src/lib.rs
Normal file
93
openapi_type_derive/src/lib.rs
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
#![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, spanned::Spanned as _, Data, DataEnum, DataStruct, DataUnion, DeriveInput, TraitBound,
|
||||||
|
TraitBoundModifier, TypeParamBound
|
||||||
|
};
|
||||||
|
|
||||||
|
#[proc_macro_derive(OpenapiType)]
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn expand_openapi_type(mut input: DeriveInput) -> syn::Result<TokenStream2> {
|
||||||
|
let ident = &input.ident;
|
||||||
|
|
||||||
|
// 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
|
||||||
|
match &input.data {
|
||||||
|
Data::Struct(strukt) => parse_struct(strukt)?,
|
||||||
|
Data::Enum(inum) => parse_enum(inum)?,
|
||||||
|
Data::Union(union) => parse_union(union)?
|
||||||
|
};
|
||||||
|
|
||||||
|
// generate the impl code
|
||||||
|
Ok(quote! {
|
||||||
|
impl #impl_generics ::openapi_type::OpenapiType for #ident #ty_generics #where_clause {
|
||||||
|
fn schema() -> ::openapi_type::OpenapiSchema {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_struct(_strukt: &DataStruct) -> syn::Result<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_enum(_inum: &DataEnum) -> syn::Result<()> {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_union(union: &DataUnion) -> syn::Result<()> {
|
||||||
|
Err(syn::Error::new(
|
||||||
|
union.union_token.span(),
|
||||||
|
"#[derive(OpenapiType)] cannot be used on unions"
|
||||||
|
))
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue