Skip to content

Commit

Permalink
support top-level attributes for venndb derive
Browse files Browse the repository at this point in the history
  • Loading branch information
GlenDC committed Mar 31, 2024
1 parent 48979b2 commit 8a5e0ad
Show file tree
Hide file tree
Showing 5 changed files with 219 additions and 5 deletions.
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
113 changes: 113 additions & 0 deletions src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,126 @@ 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 {
errors: RefCell<Vec<syn::Error>>,
}

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);
Expand Down
17 changes: 13 additions & 4 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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()
Expand All @@ -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 {
Expand All @@ -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]
Expand Down
65 changes: 65 additions & 0 deletions src/parse_attrs.rs
Original file line number Diff line number Diff line change
@@ -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<syn::LitStr>,
}

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<impl IntoIterator<Item = syn::Meta>> {
if !is_argh_attr(attr) {
return None;
}
let ml = errors.expect_meta_list(&attr.meta)?;
errors.ok(ml.parse_args_with(
syn::punctuated::Punctuated::<syn::Meta, syn::Token![,]>::parse_terminated,
))
}

// Whether the attribute is one like `#[<name> ...]`
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)
}
24 changes: 24 additions & 0 deletions venndb-usage/tests/compiles/derive_struct_custom_name.rs
Original file line number Diff line number Diff line change
@@ -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();
}

0 comments on commit 8a5e0ad

Please sign in to comment.