mirror of
https://gitlab.com/msrd0/gotham-restful.git
synced 2025-02-22 12:42:28 +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]
|
||||
paste = "1.0"
|
||||
serde = "1.0"
|
||||
trybuild = "1.0"
|
||||
|
|
|
@ -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
|
||||
}]
|
||||
});
|
||||
|
|
|
@ -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<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 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<TokenStream2> {
|
|||
// 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)?
|
||||
};
|
||||
|
||||
|
|
|
@ -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<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 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() {
|
||||
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::<syn::Result<Vec<_>>>()?
|
||||
))
|
||||
};
|
||||
|
||||
match (data_strings, data_types) {
|
||||
// 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(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 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<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