From 43d3a1cd89bccd482d62399f3de47918be443dff Mon Sep 17 00:00:00 2001 From: Dominic Date: Mon, 8 Mar 2021 17:20:41 +0100 Subject: [PATCH] start implementing enums --- openapi_type/tests/custom_types.rs | 35 ++++++++ .../tests/fail/enum_with_no_variants.rs | 6 ++ .../tests/fail/enum_with_no_variants.stderr | 5 ++ openapi_type/tests/pass/unit_struct.rs | 6 -- openapi_type/tests/trybuild.rs | 1 - openapi_type_derive/src/codegen.rs | 49 +++++++--- openapi_type_derive/src/lib.rs | 2 +- openapi_type_derive/src/parser.rs | 90 +++++++++++++++---- 8 files changed, 159 insertions(+), 35 deletions(-) create mode 100644 openapi_type/tests/fail/enum_with_no_variants.rs create mode 100644 openapi_type/tests/fail/enum_with_no_variants.stderr delete mode 100644 openapi_type/tests/pass/unit_struct.rs diff --git a/openapi_type/tests/custom_types.rs b/openapi_type/tests/custom_types.rs index ba52b41..fce5e57 100644 --- a/openapi_type/tests/custom_types.rs +++ b/openapi_type/tests/custom_types.rs @@ -42,3 +42,38 @@ test_type!(SimpleStruct = { }, "required": ["foo", "bar"] }); + +#[derive(OpenapiType)] +enum EnumWithoutFields { + Success, + Error +} +test_type!(EnumWithoutFields = { + "type": "string", + "title": "EnumWithoutFields", + "enum": [ + "Success", + "Error" + ] +}); + +#[derive(OpenapiType)] +enum EnumWithOneField { + Success { value: isize } +} +test_type!(EnumWithOneField = { + "type": "object", + "title": "EnumWithOneField", + "properties": { + "Success": { + "type": "object", + "properties": { + "value": { + "type": "integer" + } + }, + "required": ["value"] + } + }, + "required": ["Success"] +}); diff --git a/openapi_type/tests/fail/enum_with_no_variants.rs b/openapi_type/tests/fail/enum_with_no_variants.rs new file mode 100644 index 0000000..d08e223 --- /dev/null +++ b/openapi_type/tests/fail/enum_with_no_variants.rs @@ -0,0 +1,6 @@ +use openapi_type::OpenapiType; + +#[derive(OpenapiType)] +enum Foo {} + +fn main() {} diff --git a/openapi_type/tests/fail/enum_with_no_variants.stderr b/openapi_type/tests/fail/enum_with_no_variants.stderr new file mode 100644 index 0000000..5c6b1d1 --- /dev/null +++ b/openapi_type/tests/fail/enum_with_no_variants.stderr @@ -0,0 +1,5 @@ +error: #[derive(OpenapiType)] does not support enums with no variants + --> $DIR/enum_with_no_variants.rs:4:10 + | +4 | enum Foo {} + | ^^ diff --git a/openapi_type/tests/pass/unit_struct.rs b/openapi_type/tests/pass/unit_struct.rs deleted file mode 100644 index 79f6443..0000000 --- a/openapi_type/tests/pass/unit_struct.rs +++ /dev/null @@ -1,6 +0,0 @@ -use openapi_type_derive::OpenapiType; - -#[derive(OpenapiType)] -struct Foo; - -fn main() {} diff --git a/openapi_type/tests/trybuild.rs b/openapi_type/tests/trybuild.rs index 28574f1..b76b676 100644 --- a/openapi_type/tests/trybuild.rs +++ b/openapi_type/tests/trybuild.rs @@ -3,6 +3,5 @@ use trybuild::TestCases; #[test] fn trybuild() { let t = TestCases::new(); - t.pass("tests/pass/*.rs"); t.compile_fail("tests/fail/*.rs"); } diff --git a/openapi_type_derive/src/codegen.rs b/openapi_type_derive/src/codegen.rs index 4aae6a3..1ad7d02 100644 --- a/openapi_type_derive/src/codegen.rs +++ b/openapi_type_derive/src/codegen.rs @@ -1,24 +1,33 @@ -use crate::parser::ParseData; +use crate::parser::{ParseData, ParseDataType}; use proc_macro2::TokenStream; use quote::quote; -use syn::{LitStr, Type}; +use syn::LitStr; impl ParseData { - pub(super) fn gen_schema(self) -> syn::Result { + pub(super) fn gen_schema(&self) -> TokenStream { match self { Self::Struct(fields) => gen_struct(fields), + Self::Enum(variants) => gen_enum(variants), Self::Unit => gen_unit(), _ => unimplemented!() } } } -fn gen_struct(fields: Vec<(LitStr, Type)>) -> syn::Result { +fn gen_struct(fields: &[(LitStr, ParseDataType)]) -> TokenStream { let field_name = fields.iter().map(|(name, _)| name); - let field_ty = fields.iter().map(|(_, ty)| ty); + let field_schema = fields.iter().map(|(_, ty)| match ty { + ParseDataType::Type(ty) => { + quote!(<#ty as ::openapi_type::OpenapiType>::schema()) + }, + ParseDataType::Inline(data) => { + let code = data.gen_schema(); + quote!(::openapi_type::OpenapiSchema::new(#code)) + } + }); let openapi = path!(::openapi_type::openapi); - Ok(quote! { + quote! { { let mut properties = <::openapi_type::indexmap::IndexMap< ::std::string::String, @@ -28,7 +37,7 @@ fn gen_struct(fields: Vec<(LitStr, Type)>) -> syn::Result { #({ const FIELD_NAME: &::core::primitive::str = #field_name; - let mut field_schema = <#field_ty as ::openapi_type::OpenapiType>::schema(); + let mut field_schema = #field_schema; ::openapi_type::private::add_dependencies( &mut dependencies, &mut field_schema.dependencies @@ -78,12 +87,30 @@ fn gen_struct(fields: Vec<(LitStr, Type)>) -> syn::Result { ) ) } - }) + } } -fn gen_unit() -> syn::Result { +fn gen_enum(variants: &[LitStr]) -> TokenStream { let openapi = path!(::openapi_type::openapi); - Ok(quote! { + quote! { + { + let mut enumeration = <::std::vec::Vec<::std::string::String>>::new(); + #(enumeration.push(::std::string::String::from(#variants));)* + #openapi::SchemaKind::Type( + #openapi::Type::String( + #openapi::StringType { + enumeration, + .. ::std::default::Default::default() + } + ) + ) + } + } +} + +fn gen_unit() -> TokenStream { + let openapi = path!(::openapi_type::openapi); + quote! { #openapi::SchemaKind::Type( #openapi::Type::Object( #openapi::ObjectType { @@ -94,5 +121,5 @@ fn gen_unit() -> syn::Result { } ) ) - }) + } } diff --git a/openapi_type_derive/src/lib.rs b/openapi_type_derive/src/lib.rs index cdbdbf3..e3bc9fc 100644 --- a/openapi_type_derive/src/lib.rs +++ b/openapi_type_derive/src/lib.rs @@ -51,7 +51,7 @@ fn expand_openapi_type(mut input: DeriveInput) -> syn::Result { }; // run the codegen - let schema_code = parsed.gen_schema()?; + let schema_code = parsed.gen_schema(); // put the code together Ok(quote! { diff --git a/openapi_type_derive/src/parser.rs b/openapi_type_derive/src/parser.rs index 4a71c0a..8d0c15f 100644 --- a/openapi_type_derive/src/parser.rs +++ b/openapi_type_derive/src/parser.rs @@ -1,35 +1,93 @@ use crate::util::ToLitStr; -use syn::{spanned::Spanned as _, DataEnum, DataStruct, DataUnion, Fields, LitStr, Type}; +use syn::{spanned::Spanned as _, DataEnum, DataStruct, DataUnion, Fields, FieldsNamed, LitStr, Type}; + +pub(super) enum ParseDataType { + Type(Type), + Inline(ParseData) +} #[allow(dead_code)] pub(super) enum ParseData { - Struct(Vec<(LitStr, Type)>), + Struct(Vec<(LitStr, ParseDataType)>), Enum(Vec), Alternatives(Vec), Unit } +fn parse_named_fields(named_fields: &FieldsNamed) -> syn::Result { + let mut fields: Vec<(LitStr, ParseDataType)> = 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, ParseDataType::Type(ty))); + } + Ok(ParseData::Struct(fields)) +} + 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::Named(named_fields) => parse_named_fields(named_fields), Fields::Unnamed(_) => unimplemented!(), Fields::Unit => Ok(ParseData::Unit) } } -pub(super) fn parse_enum(_inum: &DataEnum) -> syn::Result { - unimplemented!() +pub(super) fn parse_enum(inum: &DataEnum) -> syn::Result { + let mut strings: Vec = Vec::new(); + let mut types: Vec<(LitStr, ParseData)> = Vec::new(); + + for v in &inum.variants { + let name = v.ident.to_lit_str(); + match &v.fields { + Fields::Named(named_fields) => { + types.push((name, parse_named_fields(named_fields)?)); + }, + Fields::Unnamed(_unnamed_fields) => unimplemented!(), + Fields::Unit => strings.push(name) + } + } + + let data_strings = if strings.is_empty() { + None + } else { + Some(ParseData::Enum(strings)) + }; + + let data_types = if types.is_empty() { + None + } else { + Some(ParseData::Alternatives( + types + .into_iter() + .map(|(name, data)| ParseData::Struct(vec![(name, ParseDataType::Inline(data))])) + .collect() + )) + }; + + match (data_strings, data_types) { + // only variants without fields + (Some(data), None) => Ok(data), + // only one variant with fields + (None, Some(ParseData::Alternatives(mut alt))) if alt.len() == 1 => Ok(alt.remove(0)), + // only variants with fields + (None, Some(data)) => Ok(data), + // variants with and without fields + (Some(data), Some(ParseData::Alternatives(mut alt))) => { + alt.push(data); + Ok(ParseData::Alternatives(alt)) + }, + // no variants + (None, None) => Err(syn::Error::new( + inum.brace_token.span, + "#[derive(OpenapiType)] does not support enums with no variants" + )), + // data_types always produces Alternatives + _ => unreachable!() + } } pub(super) fn parse_union(union: &DataUnion) -> syn::Result {