mirror of
https://gitlab.com/msrd0/gotham-restful.git
synced 2025-02-22 20:52:27 +00:00
enum representations
[skip ci]
This commit is contained in:
parent
5f60599c41
commit
a57f1c097d
5 changed files with 246 additions and 19 deletions
|
@ -19,4 +19,5 @@ serde_json = "1.0"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
paste = "1.0"
|
paste = "1.0"
|
||||||
|
serde = "1.0"
|
||||||
trybuild = "1.0"
|
trybuild = "1.0"
|
||||||
|
|
|
@ -43,6 +43,15 @@ test_type!(SimpleStruct = {
|
||||||
"required": ["foo", "bar"]
|
"required": ["foo", "bar"]
|
||||||
});
|
});
|
||||||
|
|
||||||
|
#[derive(OpenapiType)]
|
||||||
|
#[openapi(rename = "FooBar")]
|
||||||
|
struct StructRename;
|
||||||
|
test_type!(StructRename = {
|
||||||
|
"type": "object",
|
||||||
|
"title": "FooBar",
|
||||||
|
"additionalProperties": false
|
||||||
|
});
|
||||||
|
|
||||||
#[derive(OpenapiType)]
|
#[derive(OpenapiType)]
|
||||||
enum EnumWithoutFields {
|
enum EnumWithoutFields {
|
||||||
Success,
|
Success,
|
||||||
|
@ -119,6 +128,7 @@ test_type!(EnumWithFields = {
|
||||||
#[derive(OpenapiType)]
|
#[derive(OpenapiType)]
|
||||||
enum EnumExternallyTagged {
|
enum EnumExternallyTagged {
|
||||||
Success { value: isize },
|
Success { value: isize },
|
||||||
|
Empty,
|
||||||
Error
|
Error
|
||||||
}
|
}
|
||||||
test_type!(EnumExternallyTagged = {
|
test_type!(EnumExternallyTagged = {
|
||||||
|
@ -139,6 +149,101 @@ test_type!(EnumExternallyTagged = {
|
||||||
"required": ["Success"]
|
"required": ["Success"]
|
||||||
}, {
|
}, {
|
||||||
"type": "string",
|
"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
|
||||||
}]
|
}]
|
||||||
});
|
});
|
||||||
|
|
|
@ -17,16 +17,33 @@ mod parser;
|
||||||
use parser::*;
|
use parser::*;
|
||||||
|
|
||||||
/// The derive macro for [OpenapiType][openapi_type::OpenapiType].
|
/// 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 {
|
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()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn expand_openapi_type(mut input: DeriveInput) -> syn::Result<TokenStream2> {
|
fn expand_openapi_type(mut input: DeriveInput) -> syn::Result<TokenStream2> {
|
||||||
|
// 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 ident = &input.ident;
|
||||||
let name = ident.to_string();
|
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
|
// prepare the generics - all impl generics will get `OpenapiType` requirement
|
||||||
let (impl_generics, ty_generics, where_clause) = {
|
let (impl_generics, ty_generics, where_clause) = {
|
||||||
|
@ -46,7 +63,7 @@ fn expand_openapi_type(mut input: DeriveInput) -> syn::Result<TokenStream2> {
|
||||||
// parse the input data
|
// parse the input data
|
||||||
let parsed = 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, &attrs)?,
|
||||||
Data::Union(union) => parse_union(union)?
|
Data::Union(union) => parse_union(union)?
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
use crate::util::ToLitStr;
|
use crate::util::{ExpectLit, ToLitStr};
|
||||||
use syn::{spanned::Spanned as _, DataEnum, DataStruct, DataUnion, Fields, FieldsNamed, LitStr, Type};
|
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 {
|
pub(super) enum ParseDataType {
|
||||||
Type(Type),
|
Type(Type),
|
||||||
|
@ -36,7 +40,7 @@ pub(super) fn parse_struct(strukt: &DataStruct) -> syn::Result<ParseData> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn parse_enum(inum: &DataEnum) -> syn::Result<ParseData> {
|
pub(super) fn parse_enum(inum: &DataEnum, attrs: &ContainerAttributes) -> syn::Result<ParseData> {
|
||||||
let mut strings: Vec<LitStr> = Vec::new();
|
let mut strings: Vec<LitStr> = Vec::new();
|
||||||
let mut types: Vec<(LitStr, ParseData)> = Vec::new();
|
let mut types: Vec<(LitStr, ParseData)> = Vec::new();
|
||||||
|
|
||||||
|
@ -54,19 +58,59 @@ pub(super) fn parse_enum(inum: &DataEnum) -> syn::Result<ParseData> {
|
||||||
let data_strings = if strings.is_empty() {
|
let data_strings = if strings.is_empty() {
|
||||||
None
|
None
|
||||||
} else {
|
} 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() {
|
let data_types =
|
||||||
None
|
if types.is_empty() {
|
||||||
} else {
|
None
|
||||||
Some(ParseData::Alternatives(
|
} else {
|
||||||
types
|
Some(ParseData::Alternatives(
|
||||||
.into_iter()
|
types
|
||||||
.map(|(name, data)| ParseData::Struct(vec![(name, ParseDataType::Inline(data))]))
|
.into_iter()
|
||||||
.collect()
|
.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::<syn::Result<Vec<_>>>()?
|
||||||
|
))
|
||||||
|
};
|
||||||
|
|
||||||
match (data_strings, data_types) {
|
match (data_strings, data_types) {
|
||||||
// only variants without fields
|
// only variants without fields
|
||||||
|
@ -96,3 +140,49 @@ pub(super) fn parse_union(union: &DataUnion) -> syn::Result<ParseData> {
|
||||||
"#[derive(OpenapiType)] cannot be used on unions"
|
"#[derive(OpenapiType)] cannot be used on unions"
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub(super) struct ContainerAttributes {
|
||||||
|
pub(super) rename: Option<LitStr>,
|
||||||
|
pub(super) rename_all: Option<LitStr>,
|
||||||
|
pub(super) tag: Option<LitStr>,
|
||||||
|
pub(super) content: Option<LitStr>,
|
||||||
|
pub(super) untagged: bool
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn parse_container_attrs(
|
||||||
|
input: &Attribute,
|
||||||
|
attrs: &mut ContainerAttributes,
|
||||||
|
error_on_unknown: bool
|
||||||
|
) -> syn::Result<()> {
|
||||||
|
let tokens: Punctuated<Meta, Token![,]> = 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(())
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use proc_macro2::Ident;
|
use proc_macro2::Ident;
|
||||||
use syn::LitStr;
|
use syn::{Lit, LitStr};
|
||||||
|
|
||||||
/// Convert any literal path into a [syn::Path].
|
/// Convert any literal path into a [syn::Path].
|
||||||
macro_rules! path {
|
macro_rules! path {
|
||||||
|
@ -36,3 +36,17 @@ impl ToLitStr for Ident {
|
||||||
LitStr::new(&self.to_string(), self.span())
|
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<LitStr>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ExpectLit for Lit {
|
||||||
|
fn expect_str(self) -> syn::Result<LitStr> {
|
||||||
|
match self {
|
||||||
|
Self::Str(str) => Ok(str),
|
||||||
|
_ => Err(syn::Error::new(self.span(), "Expected string literal"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue