1
0
Fork 0
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:
Dominic 2021-03-07 23:09:50 +01:00
parent 90870e3b6a
commit d9c7f4135f
Signed by: msrd0
GPG key ID: DCC8C247452E98F9
9 changed files with 241 additions and 51 deletions

View file

@ -0,0 +1,12 @@
use openapi_type::OpenapiType;
#[derive(OpenapiType)]
struct Foo {
bar: Bar
}
struct Bar;
fn main() {
Foo::schema();
}

View 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)

View file

@ -1,5 +1,5 @@
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> {
| -------------

View file

@ -10,9 +10,6 @@ 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

View 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()
}
)
)
})
}

View file

@ -6,44 +6,27 @@
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
};
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][openapi_type::OpenapiType].
#[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;
let name = ident.to_string();
let name = LitStr::new(&name, ident.span());
// prepare the generics - all impl generics will get `OpenapiType` requirement
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
match &input.data {
let parsed = 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
// 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 {
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"
))
}

View 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"
))
}

View 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())
}
}