mirror of
https://gitlab.com/msrd0/gotham-restful.git
synced 2025-02-22 20:52:27 +00:00
start openapi-type codegen
This commit is contained in:
parent
90870e3b6a
commit
d9c7f4135f
9 changed files with 241 additions and 51 deletions
12
openapi_type/tests/fail/not_openapitype.rs
Normal file
12
openapi_type/tests/fail/not_openapitype.rs
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
use openapi_type::OpenapiType;
|
||||||
|
|
||||||
|
#[derive(OpenapiType)]
|
||||||
|
struct Foo {
|
||||||
|
bar: Bar
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Bar;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
Foo::schema();
|
||||||
|
}
|
8
openapi_type/tests/fail/not_openapitype.stderr
Normal file
8
openapi_type/tests/fail/not_openapitype.stderr
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
error[E0277]: the trait bound `Bar: OpenapiType` is not satisfied
|
||||||
|
--> $DIR/not_openapitype.rs:3:10
|
||||||
|
|
|
||||||
|
3 | #[derive(OpenapiType)]
|
||||||
|
| ^^^^^^^^^^^ the trait `OpenapiType` is not implemented for `Bar`
|
||||||
|
|
|
||||||
|
= note: required by `schema`
|
||||||
|
= note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info)
|
|
@ -1,5 +1,5 @@
|
||||||
error[E0599]: no function or associated item named `schema` found for struct `Foo<Bar>` in the current scope
|
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
|
--> $DIR/not_openapitype_generics.rs:11:14
|
||||||
|
|
|
|
||||||
4 | struct Foo<T> {
|
4 | struct Foo<T> {
|
||||||
| -------------
|
| -------------
|
|
@ -10,9 +10,6 @@ description = "Implementation detail of the openapi_type crate"
|
||||||
license = "Apache-2.0"
|
license = "Apache-2.0"
|
||||||
repository = "https://gitlab.com/msrd0/gotham-restful/-/tree/master/openapi_type_derive"
|
repository = "https://gitlab.com/msrd0/gotham-restful/-/tree/master/openapi_type_derive"
|
||||||
|
|
||||||
# tests are done using trybuild exclusively
|
|
||||||
autotests = false
|
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
proc-macro = true
|
proc-macro = true
|
||||||
|
|
||||||
|
|
95
openapi_type_derive/src/codegen.rs
Normal file
95
openapi_type_derive/src/codegen.rs
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
use crate::parser::ParseData;
|
||||||
|
use proc_macro2::TokenStream;
|
||||||
|
use quote::quote;
|
||||||
|
use syn::{LitStr, Type};
|
||||||
|
|
||||||
|
impl ParseData {
|
||||||
|
pub(super) fn gen_schema(self) -> syn::Result<TokenStream> {
|
||||||
|
match self {
|
||||||
|
Self::Struct(fields) => gen_struct(fields),
|
||||||
|
Self::Unit => gen_unit(),
|
||||||
|
_ => unimplemented!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn gen_struct(fields: Vec<(LitStr, Type)>) -> syn::Result<TokenStream> {
|
||||||
|
let field_name = fields.iter().map(|(name, _)| name);
|
||||||
|
let field_ty = fields.iter().map(|(_, ty)| ty);
|
||||||
|
|
||||||
|
let openapi = path!(::openapi_type::openapi);
|
||||||
|
Ok(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_ty as ::openapi_type::OpenapiType>::schema();
|
||||||
|
add_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_unit() -> syn::Result<TokenStream> {
|
||||||
|
let openapi = path!(::openapi_type::openapi);
|
||||||
|
Ok(quote! {
|
||||||
|
#openapi::SchemaKind::Type(
|
||||||
|
#openapi::Type::Object(
|
||||||
|
#openapi::ObjectType {
|
||||||
|
additional_properties: ::std::option::Option::Some(
|
||||||
|
#openapi::AdditionalProperties::Any(false)
|
||||||
|
),
|
||||||
|
.. ::std::default::Default::default()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
|
@ -6,44 +6,27 @@
|
||||||
use proc_macro::TokenStream;
|
use proc_macro::TokenStream;
|
||||||
use proc_macro2::TokenStream as TokenStream2;
|
use proc_macro2::TokenStream as TokenStream2;
|
||||||
use quote::quote;
|
use quote::quote;
|
||||||
use syn::{
|
use syn::{parse_macro_input, Data, DeriveInput, LitStr, TraitBound, TraitBoundModifier, TypeParamBound};
|
||||||
parse_macro_input, spanned::Spanned as _, Data, DataEnum, DataStruct, DataUnion, DeriveInput, TraitBound,
|
|
||||||
TraitBoundModifier, TypeParamBound
|
|
||||||
};
|
|
||||||
|
|
||||||
|
#[macro_use]
|
||||||
|
mod util;
|
||||||
|
//use util::*;
|
||||||
|
|
||||||
|
mod codegen;
|
||||||
|
mod parser;
|
||||||
|
use parser::*;
|
||||||
|
|
||||||
|
/// The derive macro for [OpenapiType][openapi_type::OpenapiType].
|
||||||
#[proc_macro_derive(OpenapiType)]
|
#[proc_macro_derive(OpenapiType)]
|
||||||
pub fn derive_openapi_type(input: TokenStream) -> TokenStream {
|
pub fn derive_openapi_type(input: TokenStream) -> TokenStream {
|
||||||
let input = parse_macro_input!(input);
|
let input = parse_macro_input!(input);
|
||||||
expand_openapi_type(input).unwrap_or_else(|err| err.to_compile_error()).into()
|
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> {
|
fn expand_openapi_type(mut input: DeriveInput) -> syn::Result<TokenStream2> {
|
||||||
let ident = &input.ident;
|
let ident = &input.ident;
|
||||||
|
let name = ident.to_string();
|
||||||
|
let name = LitStr::new(&name, ident.span());
|
||||||
|
|
||||||
// prepare the generics - all impl generics will get `OpenapiType` requirement
|
// prepare the generics - all impl generics will get `OpenapiType` requirement
|
||||||
let (impl_generics, ty_generics, where_clause) = {
|
let (impl_generics, ty_generics, where_clause) = {
|
||||||
|
@ -61,33 +44,50 @@ fn expand_openapi_type(mut input: DeriveInput) -> syn::Result<TokenStream2> {
|
||||||
};
|
};
|
||||||
|
|
||||||
// parse the input data
|
// parse the input data
|
||||||
match &input.data {
|
let parsed = match &input.data {
|
||||||
Data::Struct(strukt) => parse_struct(strukt)?,
|
Data::Struct(strukt) => parse_struct(strukt)?,
|
||||||
Data::Enum(inum) => parse_enum(inum)?,
|
Data::Enum(inum) => parse_enum(inum)?,
|
||||||
Data::Union(union) => parse_union(union)?
|
Data::Union(union) => parse_union(union)?
|
||||||
};
|
};
|
||||||
|
|
||||||
// generate the impl code
|
// run the codegen
|
||||||
|
let schema_code = parsed.gen_schema()?;
|
||||||
|
|
||||||
|
// put the code together
|
||||||
Ok(quote! {
|
Ok(quote! {
|
||||||
|
#[allow(unused_mut)]
|
||||||
impl #impl_generics ::openapi_type::OpenapiType for #ident #ty_generics #where_clause {
|
impl #impl_generics ::openapi_type::OpenapiType for #ident #ty_generics #where_clause {
|
||||||
fn schema() -> ::openapi_type::OpenapiSchema {
|
fn schema() -> ::openapi_type::OpenapiSchema {
|
||||||
unimplemented!()
|
// prepare the dependencies
|
||||||
|
let mut dependencies = <::openapi_type::indexmap::IndexMap<
|
||||||
|
::std::string::String,
|
||||||
|
::openapi_type::OpenapiSchema
|
||||||
|
>>::new();
|
||||||
|
|
||||||
|
// this function can be used to include dependencies of dependencies
|
||||||
|
let add_dependencies = |deps: &mut ::openapi_type::indexmap::IndexMap<
|
||||||
|
::std::string::String,
|
||||||
|
::openapi_type::OpenapiSchema
|
||||||
|
>| {
|
||||||
|
while let ::std::option::Option::Some((dep_name, dep_schema)) = deps.pop() {
|
||||||
|
if !dependencies.contains_key(&dep_name) {
|
||||||
|
dependencies.insert(dep_name, dep_schema);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
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"
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
40
openapi_type_derive/src/parser.rs
Normal file
40
openapi_type_derive/src/parser.rs
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
use crate::util::ToLitStr;
|
||||||
|
use syn::{spanned::Spanned as _, DataEnum, DataStruct, DataUnion, Fields, LitStr, Type};
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub(super) enum ParseData {
|
||||||
|
Struct(Vec<(LitStr, Type)>),
|
||||||
|
Enum(Vec<LitStr>),
|
||||||
|
Alternatives(Vec<ParseData>),
|
||||||
|
Unit
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn parse_struct(strukt: &DataStruct) -> syn::Result<ParseData> {
|
||||||
|
match &strukt.fields {
|
||||||
|
Fields::Named(named_fields) => {
|
||||||
|
let mut fields: Vec<(LitStr, Type)> = 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, ty));
|
||||||
|
}
|
||||||
|
Ok(ParseData::Struct(fields))
|
||||||
|
},
|
||||||
|
Fields::Unnamed(_) => unimplemented!(),
|
||||||
|
Fields::Unit => Ok(ParseData::Unit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn parse_enum(_inum: &DataEnum) -> syn::Result<ParseData> {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
|
||||||
|
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"
|
||||||
|
))
|
||||||
|
}
|
38
openapi_type_derive/src/util.rs
Normal file
38
openapi_type_derive/src/util.rs
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
use proc_macro2::Ident;
|
||||||
|
use syn::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())
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue