From a57f1c097da1f79038a27ac04849bd1f1da82270 Mon Sep 17 00:00:00 2001 From: Dominic Date: Tue, 9 Mar 2021 00:17:13 +0100 Subject: [PATCH] enum representations [skip ci] --- openapi_type/Cargo.toml | 1 + openapi_type/tests/custom_types.rs | 107 +++++++++++++++++++++++++- openapi_type_derive/src/lib.rs | 23 +++++- openapi_type_derive/src/parser.rs | 118 +++++++++++++++++++++++++---- openapi_type_derive/src/util.rs | 16 +++- 5 files changed, 246 insertions(+), 19 deletions(-) diff --git a/openapi_type/Cargo.toml b/openapi_type/Cargo.toml index 028c753..0513c0b 100644 --- a/openapi_type/Cargo.toml +++ b/openapi_type/Cargo.toml @@ -19,4 +19,5 @@ serde_json = "1.0" [dev-dependencies] paste = "1.0" +serde = "1.0" trybuild = "1.0" diff --git a/openapi_type/tests/custom_types.rs b/openapi_type/tests/custom_types.rs index 5119033..18cca88 100644 --- a/openapi_type/tests/custom_types.rs +++ b/openapi_type/tests/custom_types.rs @@ -43,6 +43,15 @@ test_type!(SimpleStruct = { "required": ["foo", "bar"] }); +#[derive(OpenapiType)] +#[openapi(rename = "FooBar")] +struct StructRename; +test_type!(StructRename = { + "type": "object", + "title": "FooBar", + "additionalProperties": false +}); + #[derive(OpenapiType)] enum EnumWithoutFields { Success, @@ -119,6 +128,7 @@ test_type!(EnumWithFields = { #[derive(OpenapiType)] enum EnumExternallyTagged { Success { value: isize }, + Empty, Error } test_type!(EnumExternallyTagged = { @@ -139,6 +149,101 @@ test_type!(EnumExternallyTagged = { "required": ["Success"] }, { "type": "string", - "enum": ["Error"] + "enum": ["Empty", "Error"] + }] +}); + +#[derive(OpenapiType)] +#[openapi(tag = "ty")] +enum EnumInternallyTagged { + Success { value: isize }, + Empty, + Error +} +test_type!(EnumInternallyTagged = { + "title": "EnumInternallyTagged", + "oneOf": [{ + "type": "object", + "properties": { + "value": { + "type": "integer" + }, + "ty": { + "type": "string", + "enum": ["Success"] + } + }, + "required": ["value", "ty"] + }, { + "type": "object", + "properties": { + "ty": { + "type": "string", + "enum": ["Empty", "Error"] + } + }, + "required": ["ty"] + }] +}); + +#[derive(OpenapiType)] +#[openapi(tag = "ty", content = "ct")] +enum EnumAdjacentlyTagged { + Success { value: isize }, + Empty, + Error +} +test_type!(EnumAdjacentlyTagged = { + "title": "EnumAdjacentlyTagged", + "oneOf": [{ + "type": "object", + "properties": { + "ty": { + "type": "string", + "enum": ["Success"] + }, + "ct": { + "type": "object", + "properties": { + "value": { + "type": "integer" + } + }, + "required": ["value"] + } + }, + "required": ["ty", "ct"] + }, { + "type": "object", + "properties": { + "ty": { + "type": "string", + "enum": ["Empty", "Error"] + } + }, + "required": ["ty"] + }] +}); + +#[derive(OpenapiType)] +#[openapi(untagged)] +enum EnumUntagged { + Success { value: isize }, + Empty, + Error +} +test_type!(EnumUntagged = { + "title": "EnumUntagged", + "oneOf": [{ + "type": "object", + "properties": { + "value": { + "type": "integer" + } + }, + "required": ["value"] + }, { + "type": "object", + "additionalProperties": false }] }); diff --git a/openapi_type_derive/src/lib.rs b/openapi_type_derive/src/lib.rs index e3bc9fc..6d82331 100644 --- a/openapi_type_derive/src/lib.rs +++ b/openapi_type_derive/src/lib.rs @@ -17,16 +17,33 @@ mod parser; use parser::*; /// The derive macro for [OpenapiType][openapi_type::OpenapiType]. -#[proc_macro_derive(OpenapiType)] +#[proc_macro_derive(OpenapiType, attributes(openapi))] 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() } fn expand_openapi_type(mut input: DeriveInput) -> syn::Result { + // parse #[serde] and #[openapi] attributes + let mut attrs = ContainerAttributes::default(); + for attr in &input.attrs { + if attr.path.is_ident("serde") { + parse_container_attrs(attr, &mut attrs, false)?; + } + } + for attr in &input.attrs { + if attr.path.is_ident("openapi") { + parse_container_attrs(attr, &mut attrs, true)?; + } + } + + // prepare impl block for codegen let ident = &input.ident; let name = ident.to_string(); - let name = LitStr::new(&name, ident.span()); + let mut name = LitStr::new(&name, ident.span()); + if let Some(rename) = &attrs.rename { + name = rename.clone(); + } // prepare the generics - all impl generics will get `OpenapiType` requirement let (impl_generics, ty_generics, where_clause) = { @@ -46,7 +63,7 @@ fn expand_openapi_type(mut input: DeriveInput) -> syn::Result { // parse the input data let parsed = match &input.data { Data::Struct(strukt) => parse_struct(strukt)?, - Data::Enum(inum) => parse_enum(inum)?, + Data::Enum(inum) => parse_enum(inum, &attrs)?, Data::Union(union) => parse_union(union)? }; diff --git a/openapi_type_derive/src/parser.rs b/openapi_type_derive/src/parser.rs index 8d0c15f..5e57736 100644 --- a/openapi_type_derive/src/parser.rs +++ b/openapi_type_derive/src/parser.rs @@ -1,5 +1,9 @@ -use crate::util::ToLitStr; -use syn::{spanned::Spanned as _, DataEnum, DataStruct, DataUnion, Fields, FieldsNamed, LitStr, Type}; +use crate::util::{ExpectLit, ToLitStr}; +use proc_macro2::Span; +use syn::{ + punctuated::Punctuated, spanned::Spanned as _, Attribute, DataEnum, DataStruct, DataUnion, Fields, FieldsNamed, LitStr, + Meta, Token, Type +}; pub(super) enum ParseDataType { Type(Type), @@ -36,7 +40,7 @@ pub(super) fn parse_struct(strukt: &DataStruct) -> syn::Result { } } -pub(super) fn parse_enum(inum: &DataEnum) -> syn::Result { +pub(super) fn parse_enum(inum: &DataEnum, attrs: &ContainerAttributes) -> syn::Result { let mut strings: Vec = Vec::new(); let mut types: Vec<(LitStr, ParseData)> = Vec::new(); @@ -54,19 +58,59 @@ pub(super) fn parse_enum(inum: &DataEnum) -> syn::Result { let data_strings = if strings.is_empty() { None } else { - Some(ParseData::Enum(strings)) + match (&attrs.tag, &attrs.content, attrs.untagged) { + // externally tagged (default) + (None, None, false) => Some(ParseData::Enum(strings)), + // internally tagged or adjacently tagged + (Some(tag), _, false) => Some(ParseData::Struct(vec![( + tag.clone(), + ParseDataType::Inline(ParseData::Enum(strings)) + )])), + // untagged + (None, None, true) => Some(ParseData::Unit), + // unknown + _ => return Err(syn::Error::new(Span::call_site(), "Unknown enum representation")) + } }; - 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() - )) - }; + let data_types = + if types.is_empty() { + None + } else { + Some(ParseData::Alternatives( + types + .into_iter() + .map(|(name, mut data)| { + Ok(match (&attrs.tag, &attrs.content, attrs.untagged) { + // externally tagged (default) + (None, None, false) => ParseData::Struct(vec![(name, ParseDataType::Inline(data))]), + // internally tagged + (Some(tag), None, false) => { + match &mut data { + ParseData::Struct(fields) => { + fields.push((tag.clone(), ParseDataType::Inline(ParseData::Enum(vec![name])))) + }, + _ => return Err(syn::Error::new( + tag.span(), + "#[derive(OpenapiType)] does not support tuple variants on internally tagged enums" + )) + }; + data + }, + // adjacently tagged + (Some(tag), Some(content), false) => ParseData::Struct(vec![ + (tag.clone(), ParseDataType::Inline(ParseData::Enum(vec![name]))), + (content.clone(), ParseDataType::Inline(data)), + ]), + // untagged + (None, None, true) => data, + // unknown + _ => return Err(syn::Error::new(Span::call_site(), "Unknown enum representation")) + }) + }) + .collect::>>()? + )) + }; match (data_strings, data_types) { // only variants without fields @@ -96,3 +140,49 @@ pub(super) fn parse_union(union: &DataUnion) -> syn::Result { "#[derive(OpenapiType)] cannot be used on unions" )) } + +#[derive(Default)] +pub(super) struct ContainerAttributes { + pub(super) rename: Option, + pub(super) rename_all: Option, + pub(super) tag: Option, + pub(super) content: Option, + pub(super) untagged: bool +} + +pub(super) fn parse_container_attrs( + input: &Attribute, + attrs: &mut ContainerAttributes, + error_on_unknown: bool +) -> syn::Result<()> { + let tokens: Punctuated = input.parse_args_with(Punctuated::parse_terminated)?; + for token in tokens { + match token { + Meta::NameValue(kv) if kv.path.is_ident("rename") => { + attrs.rename = Some(kv.lit.expect_str()?); + }, + + Meta::NameValue(kv) if kv.path.is_ident("rename_all") => { + attrs.rename_all = Some(kv.lit.expect_str()?); + }, + + Meta::NameValue(kv) if kv.path.is_ident("tag") => { + attrs.tag = Some(kv.lit.expect_str()?); + }, + + Meta::NameValue(kv) if kv.path.is_ident("content") => { + attrs.content = Some(kv.lit.expect_str()?); + }, + + Meta::Path(path) if path.is_ident("untagged") => { + attrs.untagged = true; + }, + + Meta::Path(path) if error_on_unknown => return Err(syn::Error::new(path.span(), "Unexpected token")), + Meta::List(list) if error_on_unknown => return Err(syn::Error::new(list.span(), "Unexpected token")), + Meta::NameValue(kv) if error_on_unknown => return Err(syn::Error::new(kv.path.span(), "Unexpected token")), + _ => {} + } + } + Ok(()) +} diff --git a/openapi_type_derive/src/util.rs b/openapi_type_derive/src/util.rs index 9470a04..2a752e0 100644 --- a/openapi_type_derive/src/util.rs +++ b/openapi_type_derive/src/util.rs @@ -1,5 +1,5 @@ use proc_macro2::Ident; -use syn::LitStr; +use syn::{Lit, LitStr}; /// Convert any literal path into a [syn::Path]. macro_rules! path { @@ -36,3 +36,17 @@ impl ToLitStr for Ident { LitStr::new(&self.to_string(), self.span()) } } + +/// Convert a [Lit] to one specific literal type. +pub(crate) trait ExpectLit { + fn expect_str(self) -> syn::Result; +} + +impl ExpectLit for Lit { + fn expect_str(self) -> syn::Result { + match self { + Self::Str(str) => Ok(str), + _ => Err(syn::Error::new(self.span(), "Expected string literal")) + } + } +}