From 0c4c252be9ece0e8ed2de7d1af99c49539b229b3 Mon Sep 17 00:00:00 2001 From: Pantonshire Date: Wed, 19 May 2021 10:16:22 +0100 Subject: [PATCH] Project restructuring, dedicated traits, TryScribeString derive --- Cargo.toml | 26 +--- enumscribe/Cargo.toml | 10 ++ enumscribe/src/lib.rs | 34 +++++ enumscribe_derive/Cargo.toml | 19 +++ {src => enumscribe_derive/src}/attribute.rs | 0 {src => enumscribe_derive/src}/enums.rs | 14 +- {src => enumscribe_derive/src}/error.rs | 0 enumscribe_derive/src/lib.rs | 138 ++++++++++++++++++ enumscribe_examples/Cargo.toml | 10 ++ .../examples}/airports.rs | 12 +- enumscribe_examples/src/main.rs | 3 + enumscribe_tests/Cargo.toml | 8 + enumscribe_tests/src/main.rs | 3 + src/lib.rs | 87 ----------- 14 files changed, 245 insertions(+), 119 deletions(-) create mode 100644 enumscribe/Cargo.toml create mode 100644 enumscribe/src/lib.rs create mode 100644 enumscribe_derive/Cargo.toml rename {src => enumscribe_derive/src}/attribute.rs (100%) rename {src => enumscribe_derive/src}/enums.rs (94%) rename {src => enumscribe_derive/src}/error.rs (100%) create mode 100644 enumscribe_derive/src/lib.rs create mode 100644 enumscribe_examples/Cargo.toml rename {examples => enumscribe_examples/examples}/airports.rs (59%) create mode 100644 enumscribe_examples/src/main.rs create mode 100644 enumscribe_tests/Cargo.toml create mode 100644 enumscribe_tests/src/main.rs delete mode 100644 src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index 2301050..c7ee6ca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,19 +1,7 @@ -[package] -name = "enumscribe" -version = "0.1.0" -authors = ["Tom Panton "] -edition = "2018" -license = "MIT" -description = "Procedural macros for converting between enums and strings" - -[lib] -proc-macro = true - -[dependencies] -proc-macro2 = "1.0" -syn = "1.0" -quote = "1.0" - -[dev-dependencies] -serde = "1.0" -serde_json = "1.0" +[workspace] +members = [ + "enumscribe", + "enumscribe_derive", + "enumscribe_tests", + "enumscribe_examples" +] diff --git a/enumscribe/Cargo.toml b/enumscribe/Cargo.toml new file mode 100644 index 0000000..b655642 --- /dev/null +++ b/enumscribe/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "enumscribe" +version = "0.1.0" +authors = ["Tom Panton "] +edition = "2018" +license = "MIT" +description = "Procedural macros for converting between enums and strings" + +[dependencies] +enumscribe_derive = { path = "../enumscribe_derive" } diff --git a/enumscribe/src/lib.rs b/enumscribe/src/lib.rs new file mode 100644 index 0000000..3f7b398 --- /dev/null +++ b/enumscribe/src/lib.rs @@ -0,0 +1,34 @@ +#[macro_use] +extern crate enumscribe_derive; + +use std::borrow::Cow; + +pub use enumscribe_derive::*; + +//TODO +pub trait ScribeStaticStr { + fn scribe(&self) -> &'static str; +} + +//TODO +pub trait TryScribeStaticStr { + fn try_scribe(&self) -> Option<&'static str>; +} + +pub trait ScribeString { + fn scribe(&self) -> String; +} + +pub trait TryScribeString { + fn try_scribe(&self) -> Option; +} + +//TODO +pub trait ScribeCowStr { + fn scribe(&self) -> Cow<'static, str>; +} + +//TODO +pub trait TryScribeCowStr { + fn try_scribe(&self) -> Option>; +} diff --git a/enumscribe_derive/Cargo.toml b/enumscribe_derive/Cargo.toml new file mode 100644 index 0000000..b0bbe63 --- /dev/null +++ b/enumscribe_derive/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "enumscribe_derive" +version = "0.1.0" +authors = ["Tom Panton "] +edition = "2018" +license = "MIT" +description = "Procedural macros for converting between enums and strings" + +[lib] +proc-macro = true + +[dependencies] +proc-macro2 = "1.0" +syn = "1.0" +quote = "1.0" + +[dev-dependencies] +serde = "1.0" +serde_json = "1.0" diff --git a/src/attribute.rs b/enumscribe_derive/src/attribute.rs similarity index 100% rename from src/attribute.rs rename to enumscribe_derive/src/attribute.rs diff --git a/src/enums.rs b/enumscribe_derive/src/enums.rs similarity index 94% rename from src/enums.rs rename to enumscribe_derive/src/enums.rs index d08d76e..3c5ba4c 100644 --- a/src/enums.rs +++ b/enumscribe_derive/src/enums.rs @@ -42,15 +42,15 @@ pub(crate) enum VariantConstructor { } impl<'a> Variant<'a> { - pub(crate) fn match_variant(&self, named_fn: F, other_fn: G) -> Option<(TokenStream, TokenStream)> + pub(crate) fn match_variant(&self, named_fn: F, other_fn: G) -> MacroResult> where - F: Fn(&str) -> TokenStream, - G: Fn(TokenStream) -> TokenStream + F: Fn(&str) -> MacroResult, + G: Fn(TokenStream) -> MacroResult { let variant_ident = &self.data.ident; match &self.v_type { - VariantType::Ignore => None, + VariantType::Ignore => Ok(None), VariantType::Named { name, constructor, .. } => { let pattern = match constructor { @@ -58,19 +58,19 @@ impl<'a> Variant<'a> { VariantConstructor::Paren => quote! { #variant_ident () }, VariantConstructor::Brace => quote! { #variant_ident {} } }; - Some((pattern, named_fn(name))) + Ok(Some((pattern, named_fn(name)?))) } VariantType::Other { field_name } => { let field_name_tokens = match field_name { Some(field_name) => field_name.to_token_stream(), - None => quote! { __other_inner } + None => quote! { __enumscribe_other_inner } }; let pattern = match field_name { Some(_) => quote! { #variant_ident { #field_name_tokens } }, None => quote! { #variant_ident (#field_name_tokens) } }; - Some((pattern, other_fn(field_name_tokens))) + Ok(Some((pattern, other_fn(field_name_tokens)?))) } } } diff --git a/src/error.rs b/enumscribe_derive/src/error.rs similarity index 100% rename from src/error.rs rename to enumscribe_derive/src/error.rs diff --git a/enumscribe_derive/src/lib.rs b/enumscribe_derive/src/lib.rs new file mode 100644 index 0000000..1984281 --- /dev/null +++ b/enumscribe_derive/src/lib.rs @@ -0,0 +1,138 @@ +use proc_macro::TokenStream; +use std::iter; + +use quote::quote; +use syn::{Data, DataEnum, DeriveInput}; + +use error::{MacroError, MacroResult}; + +mod enums; +mod attribute; +mod error; + +const CRATE_ATTR: &'static str = "enumscribe"; + +const NAME: &'static str = "str"; +const OTHER: &'static str = "other"; +const IGNORE: &'static str = "ignore"; +const CASE_INSENSITIVE: &'static str = "case_insensitive"; + +macro_rules! proc_try { + ($x:expr) => { + match $x { + Ok(val) => val, + Err(err) => return err.into() + } + }; +} + +#[proc_macro_derive(ScribeString, attributes(enumscribe))] +pub fn derive_enum_to_string(input: TokenStream) -> TokenStream { + let input: DeriveInput = syn::parse(input) + .expect("failed to parse input"); + + let enum_data = proc_try!(get_enum_data(&input)); + let parsed_enum = proc_try!(enums::parse_enum(enum_data)); + + let enum_ident = &input.ident; + let enum_idents = iter::repeat(enum_ident); + + let mut match_patterns = Vec::with_capacity(parsed_enum.variants.len()); + let mut match_results = Vec::with_capacity(parsed_enum.variants.len()); + + for variant in parsed_enum.variants.iter() { + match variant.match_variant( + |name| Ok(quote! { <_ as ::std::borrow::ToOwned>::to_owned(#name) }), + |field| Ok(quote! { <_ as ::std::convert::Into<::std::string::String>>::into(#field) }), + ) { + Ok(Some((pattern, result))) => { + match_patterns.push(pattern); + match_results.push(result); + }, + + Ok(None) => return MacroError::new(format!( + "cannot derive ScribeString for {} because the variant {} is marked as {}\n\ + explanation: since {} is ignored, it cannot be guaranteed that the enum can \ + always be successfully converted to a String\n\ + hint: try deriving TryScribeString instead", + enum_ident.to_string(), variant.data.ident.to_string(), IGNORE, + variant.data.ident.to_string(), + ), variant.span).into(), + + Err(err) => return err.into() + } + } + + (quote! { + impl ::enumscribe::ScribeString for #enum_ident { + fn scribe(&self) -> ::std::string::String { + match self { + #(#enum_idents::#match_patterns => #match_results,)* + } + } + } + }).into() +} + +#[proc_macro_derive(TryScribeString, attributes(enumscribe))] +pub fn derive_try_enum_to_string(input: TokenStream) -> TokenStream { + let input: DeriveInput = syn::parse(input) + .expect("failed to parse input"); + + let enum_data = proc_try!(get_enum_data(&input)); + let parsed_enum = proc_try!(enums::parse_enum(enum_data)); + + let enum_ident = &input.ident; + let enum_idents = iter::repeat(enum_ident); + + let mut ignore_variant = false; + let mut match_patterns = Vec::with_capacity(parsed_enum.variants.len()); + let mut match_results = Vec::with_capacity(parsed_enum.variants.len()); + + for variant in parsed_enum.variants.iter() { + match variant.match_variant( + |name| Ok(quote! { ::std::option::Option::Some(<_ as ::std::borrow::ToOwned>::to_owned(#name)) }), + |field| Ok(quote! { ::std::option::Option::Some(<_ as ::std::convert::Into<::std::string::String>>::into(#field)) }), + ) { + Ok(Some((pattern, result))) => { + match_patterns.push(pattern); + match_results.push(result); + }, + + Ok(None) => ignore_variant = true, + + Err(err) => return err.into() + } + } + + let ignore_arm = if ignore_variant { + quote! { _ => ::std::option::Option::None, } + } else { + quote! {} + }; + + (quote! { + impl ::enumscribe::TryScribeString for #enum_ident { + fn try_scribe(&self) -> ::std::option::Option<::std::string::String> { + match self { + #(#enum_idents::#match_patterns => #match_results,)* + #ignore_arm + } + } + } + }).into() +} + +fn get_enum_data(input: &DeriveInput) -> MacroResult<&DataEnum> { + let enum_data = match &input.data { + Data::Enum(enum_data) => enum_data, + Data::Struct(_) => return Err(MacroError::new("enumscribe cannot be used for structs", input.ident.span())), + Data::Union(_) => return Err(MacroError::new("enumscribe cannot be used for unions", input.ident.span())) + }; + + if enum_data.variants.is_empty() { + return Err(MacroError::new("enumscribe cannot be used for empty enums", input.ident.span())); + } + + Ok(enum_data) +} diff --git a/enumscribe_examples/Cargo.toml b/enumscribe_examples/Cargo.toml new file mode 100644 index 0000000..a60d093 --- /dev/null +++ b/enumscribe_examples/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "enumscribe_examples" +version = "0.1.0" +authors = ["Tom Panton "] +edition = "2018" +license = "MIT" + +[dev-dependencies] +enumscribe = { path = "../enumscribe" } +enumscribe_derive = { path = "../enumscribe_derive" } diff --git a/examples/airports.rs b/enumscribe_examples/examples/airports.rs similarity index 59% rename from examples/airports.rs rename to enumscribe_examples/examples/airports.rs index 163e5fe..ceb45c5 100644 --- a/examples/airports.rs +++ b/enumscribe_examples/examples/airports.rs @@ -1,7 +1,7 @@ -#[macro_use] -extern crate enumscribe; +use enumscribe::*; -#[derive(EnumToString)] +// #[derive(ScribeString)] +#[derive(TryScribeString)] enum Airport { #[enumscribe(str = "LHR", case_insensitive)] Heathrow, @@ -9,7 +9,7 @@ enum Airport { Gatwick, #[enumscribe(str = "LTN", case_insensitive)] Luton, - #[enumscribe(str = "BHX", case_insensitive)] + #[enumscribe(str = "BHX", case_insensitive, ignore)] BirminghamInternational, #[enumscribe(other)] Other(String), @@ -17,6 +17,6 @@ enum Airport { fn main() { let luton = Airport::Luton; - let luton_str: String = luton.into(); - println!("Hello, {}!", luton_str); + let luton_string = luton.try_scribe(); + println!("Hello, {:?}!", luton_string); } diff --git a/enumscribe_examples/src/main.rs b/enumscribe_examples/src/main.rs new file mode 100644 index 0000000..e7a11a9 --- /dev/null +++ b/enumscribe_examples/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("Hello, world!"); +} diff --git a/enumscribe_tests/Cargo.toml b/enumscribe_tests/Cargo.toml new file mode 100644 index 0000000..9bde18c --- /dev/null +++ b/enumscribe_tests/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "enumscribe_tests" +version = "0.1.0" +authors = ["Tom Panton "] +edition = "2018" +license = "MIT" + +[dependencies] diff --git a/enumscribe_tests/src/main.rs b/enumscribe_tests/src/main.rs new file mode 100644 index 0000000..e7a11a9 --- /dev/null +++ b/enumscribe_tests/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("Hello, world!"); +} diff --git a/src/lib.rs b/src/lib.rs deleted file mode 100644 index a641cf6..0000000 --- a/src/lib.rs +++ /dev/null @@ -1,87 +0,0 @@ -use proc_macro::TokenStream; -use std::iter; - -use quote::quote; -use syn::{Data, DataEnum, DeriveInput}; - -use error::{MacroError, MacroResult}; - -use crate::enums::VariantType; - -mod enums; -mod attribute; -mod error; - -const CRATE_ATTR: &'static str = "enumscribe"; - -const NAME: &'static str = "str"; -const OTHER: &'static str = "other"; -const IGNORE: &'static str = "ignore"; -const CASE_INSENSITIVE: &'static str = "case_insensitive"; - -macro_rules! proc_try { - ($x:expr) => { - match $x { - Ok(val) => val, - Err(err) => return err.into() - } - }; -} - -#[proc_macro_derive(EnumToString, attributes(enumscribe))] -pub fn derive_enum_to_string(input: TokenStream) -> TokenStream { - let input: DeriveInput = syn::parse(input) - .expect("failed to parse input"); - - let enum_data = proc_try!(get_enum_data(&input)); - let parsed_enum = proc_try!(enums::parse_enum(enum_data)); - - let enum_ident = &input.ident; - let enum_idents = iter::repeat(enum_ident); - - if parsed_enum.variants.is_empty() { - return MacroError::new(format!( - "cannot derive EnumToString for {} because it has no variants", - enum_ident.to_string() - ), input.ident.span()).into(); - } - - let mut match_patterns = Vec::with_capacity(parsed_enum.variants.len()); - let mut match_results = Vec::with_capacity(parsed_enum.variants.len()); - - for variant in parsed_enum.variants.iter() { - if let Some((pattern, result)) = variant.match_variant( - |name| quote! { <_ as ::std::borrow::ToOwned>::to_owned(#name) }, - |field| quote! { <_ as ::std::convert::Into<::std::string::String>>::into(#field) } - ) { - match_patterns.push(pattern); - match_results.push(result); - } else { - return MacroError::new(format!( - "cannot derive EnumToString for {} because the variant {} is marked as {}\n\ - explanation: since {} is ignored, it cannot be guaranteed that the enum can \ - always be successfully converted to a String", //TODO: suggest another derive to use instead - enum_ident.to_string(), variant.data.ident.to_string(), IGNORE, - variant.data.ident.to_string(), - ), variant.span).into(); - } - } - - (quote! { - impl ::std::convert::From<#enum_ident> for ::std::string::String { - fn from(__enum_to_scribe: #enum_ident) -> Self { - match __enum_to_scribe { - #(#enum_idents::#match_patterns => #match_results),* - } - } - } - }).into() -} - -fn get_enum_data(input: &DeriveInput) -> MacroResult<&DataEnum> { - match &input.data { - Data::Enum(data) => Ok(data), - Data::Struct(_) => Err(MacroError::new("enumscribe cannot be used for structs", input.ident.span())), - Data::Union(_) => Err(MacroError::new("enumscribe cannot be used for unions", input.ident.span())) - } -}