diff --git a/openapi_type/tests/fail/not_openapitype.rs b/openapi_type/tests/fail/not_openapitype.rs new file mode 100644 index 0000000..2b5b23c --- /dev/null +++ b/openapi_type/tests/fail/not_openapitype.rs @@ -0,0 +1,12 @@ +use openapi_type::OpenapiType; + +#[derive(OpenapiType)] +struct Foo { + bar: Bar +} + +struct Bar; + +fn main() { + Foo::schema(); +} diff --git a/openapi_type/tests/fail/not_openapitype.stderr b/openapi_type/tests/fail/not_openapitype.stderr new file mode 100644 index 0000000..f089b15 --- /dev/null +++ b/openapi_type/tests/fail/not_openapitype.stderr @@ -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) diff --git a/openapi_type/tests/fail/generics_not_openapitype.rs b/openapi_type/tests/fail/not_openapitype_generics.rs similarity index 100% rename from openapi_type/tests/fail/generics_not_openapitype.rs rename to openapi_type/tests/fail/not_openapitype_generics.rs diff --git a/openapi_type/tests/fail/generics_not_openapitype.stderr b/openapi_type/tests/fail/not_openapitype_generics.stderr similarity index 94% rename from openapi_type/tests/fail/generics_not_openapitype.stderr rename to openapi_type/tests/fail/not_openapitype_generics.stderr index 193eadb..d41098b 100644 --- a/openapi_type/tests/fail/generics_not_openapitype.stderr +++ b/openapi_type/tests/fail/not_openapitype_generics.stderr @@ -1,5 +1,5 @@ error[E0599]: no function or associated item named `schema` found for struct `Foo` in the current scope - --> $DIR/generics_not_openapitype.rs:11:14 + --> $DIR/not_openapitype_generics.rs:11:14 | 4 | struct Foo { | ------------- diff --git a/openapi_type_derive/Cargo.toml b/openapi_type_derive/Cargo.toml index c1cbedc..ab8e932 100644 --- a/openapi_type_derive/Cargo.toml +++ b/openapi_type_derive/Cargo.toml @@ -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 diff --git a/openapi_type_derive/src/codegen.rs b/openapi_type_derive/src/codegen.rs new file mode 100644 index 0000000..9ae9db7 --- /dev/null +++ b/openapi_type_derive/src/codegen.rs @@ -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 { + match self { + Self::Struct(fields) => gen_struct(fields), + Self::Unit => gen_unit(), + _ => unimplemented!() + } + } +} + +fn gen_struct(fields: Vec<(LitStr, Type)>) -> syn::Result { + 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 { + 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() + } + ) + ) + }) +} diff --git a/openapi_type_derive/src/lib.rs b/openapi_type_derive/src/lib.rs index bc049b3..af4f9be 100644 --- a/openapi_type_derive/src/lib.rs +++ b/openapi_type_derive/src/lib.rs @@ -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 { 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 { }; // 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" - )) -} diff --git a/openapi_type_derive/src/parser.rs b/openapi_type_derive/src/parser.rs new file mode 100644 index 0000000..4a71c0a --- /dev/null +++ b/openapi_type_derive/src/parser.rs @@ -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), + Alternatives(Vec), + Unit +} + +pub(super) fn parse_struct(strukt: &DataStruct) -> syn::Result { + 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 { + unimplemented!() +} + +pub(super) fn parse_union(union: &DataUnion) -> syn::Result { + Err(syn::Error::new( + union.union_token.span(), + "#[derive(OpenapiType)] cannot be used on unions" + )) +} diff --git a/openapi_type_derive/src/util.rs b/openapi_type_derive/src/util.rs new file mode 100644 index 0000000..9470a04 --- /dev/null +++ b/openapi_type_derive/src/util.rs @@ -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()) + } +}