Implement transparent translation for newtypes and enums
This commit is contained in:
@@ -111,6 +111,7 @@ match_bool = "allow"
|
|||||||
new_without_default = "allow"
|
new_without_default = "allow"
|
||||||
missing_panics_doc = "warn"
|
missing_panics_doc = "warn"
|
||||||
multiple_crate_versions = "allow"
|
multiple_crate_versions = "allow"
|
||||||
|
map_unwrap_or = "allow"
|
||||||
|
|
||||||
pedantic = { level = "deny", priority = -1 }
|
pedantic = { level = "deny", priority = -1 }
|
||||||
nursery = { level = "deny", priority = -1 }
|
nursery = { level = "deny", priority = -1 }
|
||||||
@@ -142,7 +143,7 @@ float_cmp_const = "deny"
|
|||||||
fn_to_numeric_cast_any = "deny"
|
fn_to_numeric_cast_any = "deny"
|
||||||
format_push_string = "deny"
|
format_push_string = "deny"
|
||||||
get_unwrap = "deny"
|
get_unwrap = "deny"
|
||||||
if_then_some_else_none = "deny"
|
if_then_some_else_none = "allow"
|
||||||
impl_trait_in_params = "allow"
|
impl_trait_in_params = "allow"
|
||||||
indexing_slicing = "deny"
|
indexing_slicing = "deny"
|
||||||
infinite_loop = "deny"
|
infinite_loop = "deny"
|
||||||
|
|||||||
@@ -1,10 +1,107 @@
|
|||||||
use proc_macro::TokenStream;
|
use proc_macro::TokenStream;
|
||||||
use quote::quote;
|
use quote::quote;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
enum TransparentKind {
|
||||||
|
NewtypeStruct {
|
||||||
|
ty: syn::Type,
|
||||||
|
},
|
||||||
|
SimpleEnum {
|
||||||
|
variants: Vec<(syn::Ident, Option<syn::LitStr>)>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
enum Translator {
|
enum Translator {
|
||||||
Serde,
|
Serde,
|
||||||
Manual,
|
Manual,
|
||||||
|
Transparent(TransparentKind),
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_enum_attributes(attrs: &mut Vec<syn::Attribute>) -> Option<syn::LitStr> {
|
||||||
|
let index_of_tag_attribute = attrs
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter(|&(_i, attr)| attr.style == syn::AttrStyle::Outer)
|
||||||
|
.find_map(|(i, attr)| match attr.meta {
|
||||||
|
syn::Meta::List(ref meta_list) => {
|
||||||
|
if let (Some(name), 1) = (
|
||||||
|
meta_list.path.segments.first(),
|
||||||
|
meta_list.path.segments.len(),
|
||||||
|
) {
|
||||||
|
if name.ident == "tag" {
|
||||||
|
Some((i, meta_list.clone()))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
});
|
||||||
|
|
||||||
|
match index_of_tag_attribute {
|
||||||
|
Some((i, meta_list)) => {
|
||||||
|
// `i` came from `enumerate()` and is guaranteed to be in bounds
|
||||||
|
let removed_attribute = attrs.remove(i);
|
||||||
|
drop(removed_attribute);
|
||||||
|
|
||||||
|
let expr: syn::Expr =
|
||||||
|
syn::parse(meta_list.tokens.into()).expect("expected expr in macro attribute");
|
||||||
|
|
||||||
|
match expr {
|
||||||
|
syn::Expr::Assign(ref assign) => {
|
||||||
|
match *assign.left {
|
||||||
|
syn::Expr::Path(ref exprpath) => {
|
||||||
|
let segments = &exprpath.path.segments;
|
||||||
|
let (Some(segment), 1) = (segments.first(), segments.len()) else {
|
||||||
|
panic!("invalid attribute key")
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(segment.ident == "rename", "invalid attribute key");
|
||||||
|
}
|
||||||
|
_ => panic!("invalid expression in enum variant attribute, left side"),
|
||||||
|
}
|
||||||
|
|
||||||
|
match *assign.right {
|
||||||
|
syn::Expr::Lit(ref expr_lit) => match expr_lit.lit {
|
||||||
|
syn::Lit::Str(ref lit_str) => Some(lit_str.clone()),
|
||||||
|
_ => panic!("invalid literal in enum variant attribute"),
|
||||||
|
},
|
||||||
|
_ => panic!("invalid expression in enum variant attribute, right side"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => panic!("invalid expression in enum variant attribute"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_transparent_enum(mut e: syn::ItemEnum) -> (Translator, syn::Item) {
|
||||||
|
let variants = e
|
||||||
|
.variants
|
||||||
|
.iter_mut()
|
||||||
|
.map(|variant| {
|
||||||
|
assert!(
|
||||||
|
variant.discriminant.is_none(),
|
||||||
|
"variant cannot have an explicit discriminant"
|
||||||
|
);
|
||||||
|
match variant.fields {
|
||||||
|
syn::Fields::Unit => (),
|
||||||
|
_ => panic!("enum cannot have fields in variants"),
|
||||||
|
}
|
||||||
|
let rename = parse_enum_attributes(&mut variant.attrs);
|
||||||
|
|
||||||
|
(variant.ident.clone(), rename)
|
||||||
|
})
|
||||||
|
.collect::<Vec<(syn::Ident, Option<syn::LitStr>)>>();
|
||||||
|
|
||||||
|
(
|
||||||
|
Translator::Transparent(TransparentKind::SimpleEnum { variants }),
|
||||||
|
e.into(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn transform(attr: TokenStream, item: TokenStream) -> TokenStream {
|
pub(crate) fn transform(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||||
@@ -12,6 +109,8 @@ pub(crate) fn transform(attr: TokenStream, item: TokenStream) -> TokenStream {
|
|||||||
|
|
||||||
let expr: syn::Expr = syn::parse(attr).expect("expected expr in macro attribute");
|
let expr: syn::Expr = syn::parse(attr).expect("expected expr in macro attribute");
|
||||||
|
|
||||||
|
let elem = syn::parse_macro_input!(item as syn::Item);
|
||||||
|
|
||||||
let syn::Expr::Assign(assign) = expr else {
|
let syn::Expr::Assign(assign) = expr else {
|
||||||
panic!("invalid expression in macro attribute")
|
panic!("invalid expression in macro attribute")
|
||||||
};
|
};
|
||||||
@@ -28,7 +127,7 @@ pub(crate) fn transform(attr: TokenStream, item: TokenStream) -> TokenStream {
|
|||||||
_ => panic!("invalid expression in tag field attribute, left side"),
|
_ => panic!("invalid expression in tag field attribute, left side"),
|
||||||
}
|
}
|
||||||
|
|
||||||
let translator = match *assign.right {
|
let (translator, elem) = match *assign.right {
|
||||||
syn::Expr::Path(ref exprpath) => {
|
syn::Expr::Path(ref exprpath) => {
|
||||||
let segments = &exprpath.path.segments;
|
let segments = &exprpath.path.segments;
|
||||||
let (Some(segment), 1) = (segments.first(), segments.len()) else {
|
let (Some(segment), 1) = (segments.first(), segments.len()) else {
|
||||||
@@ -36,16 +135,40 @@ pub(crate) fn transform(attr: TokenStream, item: TokenStream) -> TokenStream {
|
|||||||
};
|
};
|
||||||
|
|
||||||
match segment.ident.to_string().as_str() {
|
match segment.ident.to_string().as_str() {
|
||||||
"serde" => Translator::Serde,
|
"serde" => (Translator::Serde, elem),
|
||||||
"manual" => Translator::Manual,
|
"manual" => (Translator::Manual, elem),
|
||||||
|
"transparent" => match elem {
|
||||||
|
syn::Item::Struct(ref s) => match s.fields {
|
||||||
|
syn::Fields::Unnamed(ref fields) => {
|
||||||
|
let (Some(field), 1) = (fields.unnamed.first(), fields.unnamed.len())
|
||||||
|
else {
|
||||||
|
panic!(
|
||||||
|
"transparent translation is only available for newtype-style macros"
|
||||||
|
)
|
||||||
|
};
|
||||||
|
(
|
||||||
|
Translator::Transparent(TransparentKind::NewtypeStruct {
|
||||||
|
ty: field.ty.clone(),
|
||||||
|
}),
|
||||||
|
elem,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
_ => panic!(
|
||||||
|
"transparent translation is only available for newtype-style macros"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
|
||||||
|
syn::Item::Enum(e) => parse_transparent_enum(e),
|
||||||
|
_ => {
|
||||||
|
panic!("transparent translation is only available for newtype-style macros")
|
||||||
|
}
|
||||||
|
},
|
||||||
t => panic!("invalid translator {t}"),
|
t => panic!("invalid translator {t}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => panic!("invalid expression in tag field attribute, left side"),
|
_ => panic!("invalid expression in tag field attribute, left side"),
|
||||||
};
|
};
|
||||||
|
|
||||||
let elem = syn::parse_macro_input!(item as syn::Item);
|
|
||||||
|
|
||||||
let name = match elem {
|
let name = match elem {
|
||||||
syn::Item::Struct(ref s) => &s.ident,
|
syn::Item::Struct(ref s) => &s.ident,
|
||||||
syn::Item::Enum(ref e) => &e.ident,
|
syn::Item::Enum(ref e) => &e.ident,
|
||||||
@@ -69,6 +192,82 @@ pub(crate) fn transform(attr: TokenStream, item: TokenStream) -> TokenStream {
|
|||||||
type Translator = #root::tags::TranslateManual;
|
type Translator = #root::tags::TranslateManual;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
Translator::Transparent(kind) => match kind {
|
||||||
|
TransparentKind::NewtypeStruct { ty } => quote! {
|
||||||
|
impl #root::tags::TranslatableManual for #name {}
|
||||||
|
|
||||||
|
impl #root::tags::TagValue<#name> for #name {
|
||||||
|
type Error = #root::tags::ParseTagValueError;
|
||||||
|
type Translator = #root::tags::TranslateManual;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<#root::tags::RawTagValue> for #name {
|
||||||
|
type Error = #root::tags::ParseTagValueError;
|
||||||
|
|
||||||
|
fn try_from(value: #root::tags::RawTagValue) -> Result<Self, Self::Error> {
|
||||||
|
Ok(Self(<#ty as #root::tags::TagValue<#ty>>::from_raw_tag(value)?))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<#name> for RawTagValue {
|
||||||
|
fn from(value: #name) -> Self {
|
||||||
|
<#ty as #root::tags::TagValue<#ty>>::into_raw_tag(value.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
TransparentKind::SimpleEnum { variants } => {
|
||||||
|
let (into_raw_tag_mapping, from_raw_tag_mapping): (Vec<_>, Vec<_>) = variants
|
||||||
|
.into_iter()
|
||||||
|
.map(|(variant, rename)| {
|
||||||
|
let lit = rename
|
||||||
|
.map(|r| r.value())
|
||||||
|
.unwrap_or_else(|| variant.to_string());
|
||||||
|
(
|
||||||
|
quote! {
|
||||||
|
#name::#variant => #root::tags::RawTagValue::new(#lit.to_owned()),
|
||||||
|
},
|
||||||
|
quote! {
|
||||||
|
#lit => Self::#variant,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.unzip();
|
||||||
|
|
||||||
|
quote! {
|
||||||
|
impl #root::tags::TranslatableManual for #name {}
|
||||||
|
|
||||||
|
impl #root::tags::TagValue<#name> for #name {
|
||||||
|
type Error = #root::tags::ParseTagValueError;
|
||||||
|
type Translator = #root::tags::TranslateManual;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<#name> for #root::tags::RawTagValue {
|
||||||
|
fn from(value: #name) -> Self {
|
||||||
|
match value {
|
||||||
|
#(#into_raw_tag_mapping)
|
||||||
|
*
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<#root::tags::RawTagValue> for #name {
|
||||||
|
type Error = #root::tags::ParseTagValueError;
|
||||||
|
|
||||||
|
fn try_from(value: #root::tags::RawTagValue) -> Result<Self, Self::Error> {
|
||||||
|
Ok(match value.as_str() {
|
||||||
|
#(#from_raw_tag_mapping)
|
||||||
|
*
|
||||||
|
_ => return Err(#root::tags::ParseTagValueError::InvalidValue {
|
||||||
|
value,
|
||||||
|
message: "invalid enum value".to_owned(),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
quote! {
|
quote! {
|
||||||
|
|||||||
@@ -403,4 +403,49 @@ mod tests {
|
|||||||
])
|
])
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_transparent_tag() {
|
||||||
|
#[Tag(translate = transparent)]
|
||||||
|
#[derive(Serialize, Deserialize, PartialEq, Debug)]
|
||||||
|
struct MyTag(String);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
MyTag::into_raw_tag(MyTag("test".to_owned())),
|
||||||
|
RawTagValue::new("test".to_owned())
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
MyTag::from_raw_tag(RawTagValue::new("test".to_owned())).unwrap(),
|
||||||
|
MyTag("test".to_owned())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_enums() {
|
||||||
|
#[Tag(translate = transparent)]
|
||||||
|
#[derive(PartialEq, Debug)]
|
||||||
|
enum MyCoolioTag {
|
||||||
|
A,
|
||||||
|
#[tag(rename = "C")]
|
||||||
|
B,
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
MyCoolioTag::into_raw_tag(MyCoolioTag::A),
|
||||||
|
RawTagValue::new("A".to_owned())
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
MyCoolioTag::from_raw_tag(RawTagValue::new("A".to_owned())).unwrap(),
|
||||||
|
MyCoolioTag::A
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
MyCoolioTag::into_raw_tag(MyCoolioTag::B),
|
||||||
|
RawTagValue::new("C".to_owned())
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
MyCoolioTag::from_raw_tag(RawTagValue::new("C".to_owned())).unwrap(),
|
||||||
|
MyCoolioTag::B
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user