Initial experimental version
commit
28915bb825
@ -0,0 +1,7 @@
|
||||
.DS_Store
|
||||
|
||||
.idea
|
||||
.vscode
|
||||
|
||||
/target
|
||||
Cargo.lock
|
||||
@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "enumscribe"
|
||||
version = "0.1.0"
|
||||
authors = ["Tom Panton <pantonshire@gmail.com>"]
|
||||
edition = "2018"
|
||||
license = "MIT"
|
||||
description = "Procedural macros for converting between enums and strings"
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
syn = "1.0"
|
||||
quote = "1.0"
|
||||
@ -0,0 +1,88 @@
|
||||
use proc_macro::TokenStream;
|
||||
|
||||
use quote::quote;
|
||||
use syn::{Attribute, Data, DeriveInput, LitStr};
|
||||
|
||||
/// Derives `serde::Deserialize` for an enum with variants associated with strings.
|
||||
/// The `#[str_name("...")]` attribute is used to specify the string associated with each variant.
|
||||
///
|
||||
/// An "other" variant can be specified with `#[other]`. This variant should have a parameter
|
||||
/// which implements `From<String>` to store the string that could not be deserialized to any
|
||||
/// of the other variants.
|
||||
///
|
||||
/// If no "other" variant is specified, strings which are not associated with any of the variants
|
||||
/// will produce a deserialization error.
|
||||
///
|
||||
/// The enum may have the attribute `#[case_insensitive]`, in which case string comparisons will
|
||||
/// be done case-insensitively.
|
||||
#[proc_macro_derive(EnumStrDeserialize, attributes(case_insensitive, str_name, other))]
|
||||
pub fn derive_enum_str_de(ast: TokenStream) -> TokenStream {
|
||||
const ATTR_CASE_INSENSITIVE: &'static str = "case_insensitive";
|
||||
const ATTR_STR_NAME: &'static str = "str_name";
|
||||
const ATTR_OTHER: &'static str = "other";
|
||||
|
||||
let ast: DeriveInput = syn::parse(ast).unwrap();
|
||||
|
||||
let enum_name = &ast.ident;
|
||||
let enum_names = std::iter::repeat(enum_name);
|
||||
|
||||
let case_insensitive = find_attribute(ATTR_CASE_INSENSITIVE, &ast.attrs).is_some();
|
||||
|
||||
let enum_data = match ast.data {
|
||||
Data::Enum(e) => e,
|
||||
_ => panic!("cannot derive EnumStrDeserialize for anything other than an enum"),
|
||||
};
|
||||
|
||||
let (variant_names, variant_strings): (Vec<_>, Vec<_>) = enum_data.variants.iter()
|
||||
.map(|variant| (&variant.ident, find_attribute(ATTR_STR_NAME, &variant.attrs)))
|
||||
.filter(|(_, attribute)| attribute.is_some())
|
||||
.map(|(variant_ident, attribute)| (variant_ident, attribute
|
||||
.unwrap()
|
||||
.parse_args::<LitStr>()
|
||||
.unwrap()
|
||||
.value()))
|
||||
.map(|(variant_ident, attribute)| (variant_ident, if case_insensitive {
|
||||
attribute.to_lowercase()
|
||||
} else {
|
||||
attribute
|
||||
}))
|
||||
.unzip();
|
||||
|
||||
let other_variant = enum_data.variants.iter()
|
||||
.find(|variant| find_attribute(ATTR_OTHER, &variant.attrs).is_some());
|
||||
|
||||
let matching_string = if case_insensitive {
|
||||
quote! { deserialized_string.to_lowercase() }
|
||||
} else {
|
||||
quote! { deserialized_string }
|
||||
};
|
||||
|
||||
let (base_case_pattern, base_case_value) = if let Some(other_variant) = other_variant {
|
||||
let other_variant_name = &other_variant.ident;
|
||||
(quote! { _ }, quote! { ::core::result::Result::Ok(#enum_name::#other_variant_name(deserialized_string.into())) })
|
||||
} else {
|
||||
(quote! { s }, quote! { ::core::result::Result::Err(::serde::de::Error::unknown_variant(s, &[#(#variant_strings),*])) })
|
||||
};
|
||||
|
||||
(quote! {
|
||||
impl<'de> ::serde::Deserialize<'de> for #enum_name {
|
||||
fn deserialize<D>(deserializer: D) -> ::core::result::Result<Self, D::Error>
|
||||
where
|
||||
D: ::serde::Deserializer<'de>,
|
||||
{
|
||||
let deserialized_string = ::std::string::String::deserialize(deserializer)?;
|
||||
match #matching_string.as_str() {
|
||||
#(#variant_strings => ::core::result::Result::Ok(#enum_names::#variant_names),)*
|
||||
#base_case_pattern => #base_case_value,
|
||||
}
|
||||
}
|
||||
}
|
||||
}).into()
|
||||
}
|
||||
|
||||
fn find_attribute<'a>(name: &str, attributes: &'a [Attribute]) -> Option<&'a Attribute> {
|
||||
attributes
|
||||
.iter()
|
||||
.find(|attribute| attribute.path.is_ident(name))
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue