Project restructuring, dedicated traits, TryScribeString derive

This commit is contained in:
Pantonshire committed 2021-05-19 10:16:22 +01:00
1 parent 6a2ffc19eb
commit 0c4c252be9
14 files changed
+245 -119

No files matched your search

+222
View File
@@ -0,0 +1,222 @@
use std::collections::HashMap;
use std::fmt;
use proc_macro2::Span;
use syn::{Attribute, Ident, Lit, Token};
use syn::parse::{Parse, ParseBuffer, ParseStream};
use syn::parse::discouraged::Speculative;
use syn::token::Token;
use crate::error::{MacroError, MacroResult, ValueTypeError, ValueTypeResult};
#[derive(Clone)]
pub(crate) enum Value {
None,
Lit(Lit),
Ident(Ident),
}
impl Value {
pub(crate) fn type_name(&self) -> &'static str {
match self {
Value::None => "nothing",
Value::Lit(lit) => match lit {
Lit::Str(_) => "string",
Lit::ByteStr(_) => "byte string",
Lit::Byte(_) => "byte",
Lit::Char(_) => "character",
Lit::Int(_) => "integer",
Lit::Float(_) => "float",
Lit::Bool(_) => "boolean",
Lit::Verbatim(_) => "verbatim literal",
},
Value::Ident(_) => "identifier",
}
}
/// Gets the boolean value associated with this Value. `Value::None` value is considered to
/// be true. If this value cannot represent a boolean, a `ValueTypeError` will be returned.
pub(crate) fn value_bool(&self) -> ValueTypeResult<bool> {
match self {
Value::None => Ok(true),
Value::Lit(Lit::Bool(lit_bool)) => Ok(lit_bool.value),
val => Err(ValueTypeError {
message: format!("expected boolean but found {}", val.type_name()).into()
})
}
}
/// Gets the string value associated with this Value. If this value cannot represent a string,
/// a `ValueTypeError` will be returned.
pub(crate) fn value_string(&self) -> ValueTypeResult<String> {
match self {
Value::Lit(Lit::Str(lit_str)) => Ok(lit_str.value()),
val => Err(ValueTypeError {
message: format!("expected string but found {}", val.type_name()).into()
})
}
}
}
impl fmt::Debug for Value {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Value::None => write!(f, "ε"),
Value::Lit(lit) => match lit {
Lit::Str(lit_str) => lit_str.value().fmt(f),
Lit::ByteStr(lit_byte_str) => lit_byte_str.value().fmt(f),
Lit::Byte(lit_byte) => lit_byte.value().fmt(f),
Lit::Char(lit_char) => lit_char.value().fmt(f),
Lit::Int(lit_int) => write!(f, "{}", lit_int.base10_digits()),
Lit::Float(lit_float) => write!(f, "{}", lit_float.base10_digits()),
Lit::Bool(lit_bool) => lit_bool.value.fmt(f),
Lit::Verbatim(lit_verbatim) => lit_verbatim.fmt(f),
},
Value::Ident(ident) => ident.fmt(f),
}
}
}
#[derive(Clone, Debug)]
pub(crate) struct Dict {
pub(crate) inner: HashMap<String, (Value, Span)>,
}
/// Represents the contents of a single `#[tag(...)]`.
/// The contents are parsed from `key = value` pairs, separated by commas.
#[derive(Clone, Debug)]
struct AttributeTag {
inner: Vec<(String, Value, Span)>,
}
#[derive(Clone, Debug)]
struct KeyValPair {
key: String,
val: Value,
span: Span,
}
impl Dict {
pub(crate) fn new() -> Self {
Dict { inner: HashMap::new() }
}
pub(crate) fn from_attrs(name: &str, attrs: &[Attribute]) -> MacroResult<Self> {
let mut dict = Dict::new();
let attribute_tags = attrs.iter()
.filter(|attr| attr.path.is_ident(name))
.map(|attr| attr.parse_args::<AttributeTag>());
for tag in attribute_tags {
let tag = tag.map_err(MacroError::from)?;
for (key, val, span) in tag.inner {
if dict.inner.contains_key(&key) {
return Err(MacroError::new(format!(
"key appears more than once: {}", key
), span));
}
dict.inner.insert(key, (val, span));
}
}
Ok(dict)
}
pub(crate) fn remove_typed<T, F>(&mut self, key: &str, converter: F) -> MacroResult<Option<(T, Span)>>
where
F: Fn(&Value) -> ValueTypeResult<T>
{
match self.inner.remove(key) {
None => Ok(None),
Some((val, span)) => match converter(&val) {
Ok(converted) => Ok(Some((converted, span))),
Err(ValueTypeError { message }) => Err(MacroError::new(
format!("{} for key: {}", message, key),
span,
))
}
}
}
pub(crate) fn remove_typed_or_default<T, F>(&mut self, key: &str, default: (T, Span), converter: F) -> MacroResult<(T, Span)>
where
F: Fn(&Value) -> ValueTypeResult<T>
{
match self.remove_typed(key, converter) {
Ok(Some(value)) => Ok(value),
Ok(None) => Ok(default),
Err(err) => Err(err),
}
}
pub(crate) fn assert_empty(&self) -> MacroResult<()> {
if self.inner.is_empty() {
Ok(())
} else {
let (unexpected_key, (_, unexpected_span)) = self.inner.iter().next().unwrap();
Err(MacroError::new(
format!("unexpected key: {}", unexpected_key),
*unexpected_span,
))
}
}
}
impl Parse for AttributeTag {
fn parse(input: ParseStream) -> syn::Result<Self> {
Ok(AttributeTag {
inner: input
.parse_terminated::<KeyValPair, Token![,]>(KeyValPair::parse)?
.into_iter()
.map(|pair| (pair.key, pair.val, pair.span))
.collect::<Vec<_>>()
})
}
}
impl Parse for KeyValPair {
fn parse(input: ParseStream) -> syn::Result<Self> {
let key = input
.parse::<Ident>()?;
let val = if input.peek(Token![=]) {
input.parse::<Token![=]>()?;
if let Ok(lit) = speculative_parse::<Lit>(input) {
Value::Lit(lit)
} else if let Ok(ident) = speculative_parse::<Ident>(input) {
Value::Ident(ident)
} else {
return Err(input.error(format!(
"could not parse value corresponding to key: {}", key
)));
}
} else {
Value::None
};
Ok(KeyValPair {
key: key.to_string(),
val,
span: key.span(),
})
}
}
fn speculative_parse<T>(input: ParseStream) -> syn::Result<T> where T: Parse {
match fork_and_parse(input) {
Ok((fork, parsed)) => {
input.advance_to(&fork);
Ok(parsed)
}
Err(err) => Err(err)
}
}
fn fork_and_parse<T>(input: ParseStream) -> syn::Result<(ParseBuffer, T)> where T: Parse {
let fork = input.fork();
T::parse(&fork).map(move |parsed| (fork, parsed))
}
+200
View File
@@ -0,0 +1,200 @@
use std::collections::HashSet;
use proc_macro2::{Ident, Span, TokenStream};
use quote::{quote, ToTokens};
use syn::{DataEnum, Fields};
use syn::spanned::Spanned;
use crate::{CASE_INSENSITIVE, CRATE_ATTR, IGNORE, NAME, OTHER};
use crate::attribute::{Dict, Value};
use crate::error::{MacroError, MacroResult};
#[derive(Clone)]
pub(crate) struct Enum<'a> {
pub(crate) variants: Vec<Variant<'a>>,
}
#[derive(Clone)]
pub(crate) struct Variant<'a> {
pub(crate) data: &'a syn::Variant,
pub(crate) v_type: VariantType<'a>,
pub(crate) span: Span,
}
#[derive(Clone)]
pub(crate) enum VariantType<'a> {
Ignore,
Named {
name: String,
constructor: VariantConstructor,
case_insensitive: bool,
},
Other {
field_name: Option<&'a Ident> //use {} for constructor if Some, use () if None
},
}
#[derive(Clone, Copy, Debug)]
pub(crate) enum VariantConstructor {
None,
Paren,
Brace,
}
impl<'a> Variant<'a> {
pub(crate) fn match_variant<F, G>(&self, named_fn: F, other_fn: G) -> MacroResult<Option<(TokenStream, TokenStream)>>
where
F: Fn(&str) -> MacroResult<TokenStream>,
G: Fn(TokenStream) -> MacroResult<TokenStream>
{
let variant_ident = &self.data.ident;
match &self.v_type {
VariantType::Ignore => Ok(None),
VariantType::Named { name, constructor, .. } => {
let pattern = match constructor {
VariantConstructor::None => quote! { #variant_ident },
VariantConstructor::Paren => quote! { #variant_ident () },
VariantConstructor::Brace => quote! { #variant_ident {} }
};
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! { __enumscribe_other_inner }
};
let pattern = match field_name {
Some(_) => quote! { #variant_ident { #field_name_tokens } },
None => quote! { #variant_ident (#field_name_tokens) }
};
Ok(Some((pattern, other_fn(field_name_tokens)?)))
}
}
}
}
pub(crate) fn parse_enum(data: &DataEnum) -> MacroResult<Enum> {
let mut variants = Vec::with_capacity(data.variants.len());
let mut taken_names = HashSet::new();
let mut other_variant = false;
for variant in data.variants.iter() {
let variant_span = variant.span();
// Parse the `#[enumscribe(...)]` attributes for this variant into a single Dict
let mut dict = Dict::from_attrs(CRATE_ATTR, &variant.attrs)?;
// Convert the values in the Dict to the appropriate types
let name_opt = dict.remove_typed(NAME, Value::value_string)?;
let (other, other_span) = dict.remove_typed_or_default(OTHER, (false, variant_span), Value::value_bool)?;
let (ignore, _) = dict.remove_typed_or_default(IGNORE, (false, variant_span), Value::value_bool)?;
let (case_insensitive, _) = dict.remove_typed_or_default(CASE_INSENSITIVE, (false, variant_span), Value::value_bool)?;
// Return an error if there are any unrecognised keys in the Dict
dict.assert_empty()?;
let scribe_variant = if ignore {
Variant {
data: variant,
v_type: VariantType::Ignore,
span: variant_span,
}
} else if other {
// Return an error if there is already an "other" variant for this enum
if other_variant {
return Err(MacroError::new(
format!(
"cannot have multiple variants marked as {}",
OTHER
),
other_span,
));
}
other_variant = true;
// Return an error if a str name is provided for this variant
if let Some((_, name_span)) = name_opt {
return Err(MacroError::new(
format!(
"cannot use {} for variant {} because it is marked as {}",
NAME, variant.ident.to_string(), OTHER
),
name_span,
));
}
// Return an error if this variant doesn't have exactly one field
if variant.fields.len() != 1 {
return Err(MacroError::new(
format!(
"the variant {} must have exactly one field because it is marked as {}",
variant.ident.to_string(), OTHER
),
variant_span,
));
}
// Get the name of the variant's field (or None if it is unnamed)
let field_name = variant.fields.iter().next()
.and_then(|field| field.ident.as_ref());
Variant {
data: variant,
v_type: VariantType::Other { field_name },
span: variant_span,
}
} else {
// Use the str name if one is provided, otherwise use the variant's name
let (name, name_span) = match name_opt {
Some((name, name_span)) => (name, name_span),
None => (variant.ident.to_string(), variant.ident.span())
};
// Do not allow duplicate names
if taken_names.contains(&name) {
return Err(MacroError::new(
format!("duplicate name \"{}\"", name),
name_span,
));
}
// Return an error if the variant has any fields
if variant.fields.len() != 0 {
let variant_ident = variant.ident.to_string();
return Err(MacroError::new(
format!(
"the variant {} must not have any fields\n\
hint: if you do not want to remove {}\'s fields, try using \
#[enumscribe(ignore)] for {}",
variant_ident, variant_ident, variant_ident
),
variant_span,
));
}
// The variant is allowed to have an empty constructor, so find out if it has one
// and, if so, what type of constructor (parentheses or braces)
let constructor = match variant.fields {
Fields::Named(_) => VariantConstructor::Brace,
Fields::Unnamed(_) => VariantConstructor::Paren,
Fields::Unit => VariantConstructor::None,
};
taken_names.insert(name.clone());
Variant {
data: variant,
v_type: VariantType::Named { name, constructor, case_insensitive },
span: variant_span,
}
};
variants.push(scribe_variant);
}
Ok(Enum { variants })
}
+77
View File
@@ -0,0 +1,77 @@
use std::borrow::Cow;
use std::fmt;
use std::error;
use std::result;
use proc_macro2::Span;
use quote::quote_spanned;
use syn::Error;
#[derive(Clone, Debug)]
pub(crate) struct MacroError {
pub(crate) message: Cow<'static, str>,
pub(crate) span: Span,
}
pub(crate) type MacroResult<T> = result::Result<T, MacroError>;
impl MacroError {
pub(crate) fn new<T>(message: T, span: Span) -> Self where T : Into<Cow<'static, str>> {
MacroError {
message: message.into(),
span,
}
}
pub(crate) fn to_token_stream(&self) -> proc_macro::TokenStream {
self.to_token_stream2().into()
}
pub(crate) fn to_token_stream2(&self) -> proc_macro2::TokenStream {
let message = &self.message;
quote_spanned! {
self.span => ::std::compile_error!(#message);
}
}
}
impl From<syn::Error> for MacroError {
fn from(err: Error) -> Self {
MacroError::new(err.to_string(), err.span())
}
}
impl From<MacroError> for proc_macro::TokenStream {
fn from(err: MacroError) -> Self {
err.to_token_stream()
}
}
impl From<MacroError> for proc_macro2::TokenStream {
fn from(err: MacroError) -> Self {
err.to_token_stream2()
}
}
impl fmt::Display for MacroError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.message)
}
}
impl error::Error for MacroError {}
#[derive(Clone, Debug)]
pub(crate) struct ValueTypeError {
pub(crate) message: Cow<'static, str>
}
pub(crate) type ValueTypeResult<T> = result::Result<T, ValueTypeError>;
impl fmt::Display for ValueTypeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.message)
}
}
impl error::Error for ValueTypeError {}
+138
View File
@@ -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)
}