From 8a5e0ad74e57758cb130bfd524991521cc1f3ff6 Mon Sep 17 00:00:00 2001 From: glendc Date: Sun, 31 Mar 2024 23:41:35 +0200 Subject: [PATCH] support top-level attributes for venndb derive --- README.md | 5 +- src/errors.rs | 113 ++++++++++++++++++ src/lib.rs | 17 ++- src/parse_attrs.rs | 65 ++++++++++ .../compiles/derive_struct_custom_name.rs | 24 ++++ 5 files changed, 219 insertions(+), 5 deletions(-) create mode 100644 src/parse_attrs.rs create mode 100644 venndb-usage/tests/compiles/derive_struct_custom_name.rs diff --git a/README.md b/README.md index 615f25b..2db68d2 100644 --- a/README.md +++ b/README.md @@ -114,7 +114,10 @@ without any additional terms or conditions. ### Acknowledgements -Special thanks goes to all involved in developing, maintaining and supporting [the Rust programming language](https://www.rust-lang.org/). Also a big shoutout to the ["Write Powerful Rust Macros" book by _Sam Van Overmeire_](https://www.manning.com/books/write-powerful-rust-macros), which gave the courage to develop this crate, finally. +Special thanks goes to all involved in developing, maintaining and supporting [the Rust programming language](https://www.rust-lang.org/). Also a big shoutout to the ["Write Powerful Rust Macros" book by _Sam Van Overmeire_](https://www.manning.com/books/write-powerful-rust-macros), which gave the courage to develop this crate. + +Some code was also copied/forked from [google/argh](https://github.com/google/argh), for which thank you, +we are big fans of that crate. Go use it if you want to create a CLI App. ## 💖 | Sponsors diff --git a/src/errors.rs b/src/errors.rs index 1c6c2cc..08d1dab 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -6,6 +6,38 @@ use { std::cell::RefCell, }; +/// Produce functions to expect particular literals in `syn::Expr` +macro_rules! expect_lit_fn { + ($(($fn_name:ident, $syn_type:ident, $variant:ident, $lit_name:literal),)*) => { + $( + pub fn $fn_name<'a>(&self, e: &'a syn::Expr) -> Option<&'a syn::$syn_type> { + if let syn::Expr::Lit(syn::ExprLit { lit: syn::Lit::$variant(inner), .. }) = e { + Some(inner) + } else { + self.unexpected_lit($lit_name, e); + None + } + } + )* + } +} + +/// Produce functions to expect particular variants of `syn::Meta` +macro_rules! expect_meta_fn { + ($(($fn_name:ident, $syn_type:ident, $variant:ident, $meta_name:literal),)*) => { + $( + pub fn $fn_name<'a>(&self, meta: &'a syn::Meta) -> Option<&'a syn::$syn_type> { + if let syn::Meta::$variant(inner) = meta { + Some(inner) + } else { + self.unexpected_meta($meta_name, meta); + None + } + } + )* + } +} + /// A type for collecting procedural macro errors. #[derive(Default)] pub struct Errors { @@ -13,6 +45,87 @@ pub struct Errors { } impl Errors { + expect_lit_fn![ + (expect_lit_str, LitStr, Str, "string"), + (expect_lit_char, LitChar, Char, "character"), + (expect_lit_int, LitInt, Int, "integer"), + ]; + + expect_meta_fn![ + (expect_meta_word, Path, Path, "path"), + (expect_meta_list, MetaList, List, "list"), + ( + expect_meta_name_value, + MetaNameValue, + NameValue, + "name-value pair" + ), + ]; + + fn unexpected_lit(&self, expected: &str, found: &syn::Expr) { + fn lit_kind(lit: &syn::Lit) -> &'static str { + use syn::Lit::{Bool, Byte, ByteStr, Char, Float, Int, Str, Verbatim}; + match lit { + Str(_) => "string", + ByteStr(_) => "bytestring", + Byte(_) => "byte", + Char(_) => "character", + Int(_) => "integer", + Float(_) => "float", + Bool(_) => "boolean", + Verbatim(_) => "unknown (possibly extra-large integer)", + _ => "unknown literal kind", + } + } + + if let syn::Expr::Lit(syn::ExprLit { lit, .. }) = found { + self.err( + found, + &[ + "Expected ", + expected, + " literal, found ", + lit_kind(lit), + " literal", + ] + .concat(), + ) + } else { + self.err( + found, + &[ + "Expected ", + expected, + " literal, found non-literal expression.", + ] + .concat(), + ) + } + } + + fn unexpected_meta(&self, expected: &str, found: &syn::Meta) { + fn meta_kind(meta: &syn::Meta) -> &'static str { + use syn::Meta::{List, NameValue, Path}; + match meta { + Path(_) => "path", + List(_) => "list", + NameValue(_) => "name-value pair", + } + } + + self.err( + found, + &[ + "Expected ", + expected, + " attribute, found ", + meta_kind(found), + " attribute", + ] + .concat(), + ) + } + /// Issue an error relating to a particular `Spanned` structure. pub fn err(&self, spanned: &impl syn::spanned::Spanned, msg: &str) { self.err_span(spanned.span(), msg); diff --git a/src/lib.rs b/src/lib.rs index 73a09e9..f3e00fa 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,11 +1,13 @@ #![forbid(unsafe_code)] +mod errors; +mod parse_attrs; + use errors::Errors; +use parse_attrs::TypeAttrs; use proc_macro2::TokenStream; use quote::{format_ident, quote, ToTokens}; -mod errors; - /// Entrypoint for `#[derive(VennDB)]`. #[proc_macro_derive(VennDB, attributes(venndb))] pub fn venndb(input: proc_macro::TokenStream) -> proc_macro::TokenStream { @@ -18,8 +20,11 @@ pub fn venndb(input: proc_macro::TokenStream) -> proc_macro::TokenStream { /// as well as all errors that occurred. fn impl_from_args(input: &syn::DeriveInput) -> TokenStream { let errors = &Errors::default(); + let type_attrs = &TypeAttrs::parse(errors, input); let mut output_tokens = match &input.data { - syn::Data::Struct(ds) => impl_from_args_struct(errors, &input.ident, &input.generics, ds), + syn::Data::Struct(ds) => { + impl_from_args_struct(errors, &input.ident, type_attrs, &input.generics, ds) + } syn::Data::Enum(_) => { errors.err(input, "`#[derive(VennDB)]` cannot be applied to enums"); TokenStream::new() @@ -37,6 +42,7 @@ fn impl_from_args(input: &syn::DeriveInput) -> TokenStream { fn impl_from_args_struct( errors: &Errors, name: &syn::Ident, + type_attrs: &TypeAttrs, _generic_args: &syn::Generics, ds: &syn::DataStruct, ) -> TokenStream { @@ -58,7 +64,10 @@ fn impl_from_args_struct( } }; - let name_db = format_ident!("{}DB", name); + let name_db = match &type_attrs.name { + Some(name) => format_ident!("{}", name.value()), + None => format_ident!("{}DB", name), + }; quote! { #[non_exhaustive] diff --git a/src/parse_attrs.rs b/src/parse_attrs.rs new file mode 100644 index 0000000..931e352 --- /dev/null +++ b/src/parse_attrs.rs @@ -0,0 +1,65 @@ +use crate::errors::Errors; + +/// Represents a `#[derive(VennDB)]` type's top-level attributes. +#[derive(Default)] +pub struct TypeAttrs { + pub name: Option, +} + +impl TypeAttrs { + /// Parse top-level `#[venndb(...)]` attributes + pub fn parse(errors: &Errors, derive_input: &syn::DeriveInput) -> Self { + let mut this = Self::default(); + + for attr in &derive_input.attrs { + let ml = if let Some(ml) = venndb_attr_to_meta_list(errors, attr) { + ml + } else { + continue; + }; + + for meta in ml { + let name = meta.path(); + if name.is_ident("name") { + if let Some(m) = errors.expect_meta_name_value(&meta) { + this.name = errors.expect_lit_str(&m.value).cloned(); + } + } else { + errors.err( + &meta, + concat!( + "Invalid field-level `venndb` attribute\n", + "Expected one of: `name`", + ), + ); + } + } + } + + this + } +} + +/// Filters out non-`#[venndb(...)]` attributes and converts to a sequence of `syn::Meta`. +fn venndb_attr_to_meta_list( + errors: &Errors, + attr: &syn::Attribute, +) -> Option> { + if !is_argh_attr(attr) { + return None; + } + let ml = errors.expect_meta_list(&attr.meta)?; + errors.ok(ml.parse_args_with( + syn::punctuated::Punctuated::::parse_terminated, + )) +} + +// Whether the attribute is one like `#[ ...]` +fn is_matching_attr(name: &str, attr: &syn::Attribute) -> bool { + attr.path().segments.len() == 1 && attr.path().segments[0].ident == name +} + +/// Checks for `#[venndb ...]` +fn is_argh_attr(attr: &syn::Attribute) -> bool { + is_matching_attr("venndb", attr) +} diff --git a/venndb-usage/tests/compiles/derive_struct_custom_name.rs b/venndb-usage/tests/compiles/derive_struct_custom_name.rs new file mode 100644 index 0000000..a640dca --- /dev/null +++ b/venndb-usage/tests/compiles/derive_struct_custom_name.rs @@ -0,0 +1,24 @@ +use venndb::VennDB; + +#[derive(Debug, VennDB)] +#[venndb(name = "Database")] +struct Employee { + id: u32, + name: String, + is_manager: bool, + is_admin: bool, + is_active: bool, + department: Department, +} + +#[derive(Debug)] +pub enum Department { + Engineering, + Sales, + Marketing, + HR, +} + +fn main() { + let _ = Database::new(); +}