darling

repository·master·Indexed 23 days ago

https://github.com/teddriggs/darling

A procedural macro toolkit for Rust that simplifies parsing attributes into structured data. Inspired by Serde's API, it provides declarative, type-safe parsing with validation and error reporting through traits such as FromMeta, FromDeriveInput, FromField, FromVariant, and FromAttributes. Version 0.24.0.

Tokens
8.5K
Snippets
15
Records
48
Agent score
79%

What's inside darling

  1. Use darling utility modules

    master

    Darling provides several specialized modules to assist in macro development:

    • darling::ast: Provides generic types for representing the AST (e.g., NestedMeta).
    • darling::usage: Provides traits and functions to determine where type parameters and lifetimes are used within a struct or enum.
    • darling::util: Contains helper types with specialized FromMeta implementations, such as PathList and SpannedValue (for accessing source code spans).
  2. How darling's core traits work together

    master

    Darling uses a set of traits to map attribute meta-items and AST elements into structured Rust types. The primary traits are:

    1. FromMeta: Extracts values from a meta-item (e.g., key = "value" or flag) within an attribute. Similar to serde::Deserialize, it is used for individual field parsing.
    2. FromDeriveInput: The root for parsing Derive macro inputs. It provides access to the target type's identity, generics, and visibility, and allows specifying which attributes to parse or forward.
    3. FromField: Used for parsing struct fields. It provides access to the field's identity, type, and visibility.
    4. FromVariant: Used for parsing enum variants. It provides access to the variant's identity and contents.
    5. FromAttributes: A lower-level trait for non-derive proc-macros. It allows parsing any syntax element (traits, functions, etc.) by providing a meta-item extractor and error collection.
  3. Implement attribute macro argument parsing

    master

    To parse arguments for a non-derive attribute macro, follow these steps:

    1. Define an argument receiver type and derive FromMeta on it.
    2. In your #[proc_macro_attribute] function, use syn::parse to parse the args TokenStream into a syn::parse::Parse compatible type, or use darling::ast::NestedMeta::parse_meta_list to convert the TokenStream to a Vec<NestedMeta>.
    3. Call the derived from_list method on your argument receiver type to get a darling::Result<T>.

    Example implementation:

    use darling::{Error, FromMeta};
    use darling::ast::NestedMeta;
    use syn::ItemFn;
    use proc_macro::TokenStream;
    
    #[derive(Debug, FromMeta)]
    #[darling(derive_syn_parse)]
    struct MacroArgs {
        #[darling(default)]
        timeout_ms: Option<u16>,
        path: String,
    }
    
    #[proc_macro_attribute]
    pub fn your_attr(args: TokenStream, input: TokenStream) -> TokenStream {
        let _args: MacroArgs = match syn::parse(args) {
            Ok(v) => v,
            Err(e) => { return e.to_compile_error().into(); }
        };
        let _input = syn::parse_macro_input!(input as ItemFn);
    
        // do things with `args`
        unimplemented!()
    }
    use darling::{Error, FromMeta};
    use darling::ast::NestedMeta;
    use syn::ItemFn;
    use proc_macro::TokenStream;
    
    #[derive(Debug, FromMeta)]
    #[darling(derive_syn_parse)]
    struct MacroArgs {
        #[darling(default)]
        timeout_ms: Option<u16>,
        path: String,
    }
    
    #[proc_macro_attribute]
    pub fn your_attr(args: TokenStream, input: TokenStream) -> TokenStream {
        let _args: MacroArgs = match syn::parse(args) {
            Ok(v) => v,
            Err(e) => { return e.to_compile_error().into(); }
        };
        let _input = syn::parse_macro_input!(input as ItemFn);
    
        // do things with `args`
        unimplemented!()
    }
  4. Use ShapeSet to validate field structures

    master

    A ShapeSet is a collection of allowed Shape values. It is used to check if a struct or variant matches a set of permitted layouts.

    Crucially, ShapeSet understands the relationship between Newtype and Tuple: if a ShapeSet allows Tuple, it automatically allows Newtype because a newtype is effectively a single-field tuple.

    Common tasks:

    • Create a set: Use ShapeSet::new(iter) to specify allowed shapes.
    • Check a container: Use .contains(&impl AsShape) to see if a container (like syn::DataStruct or syn::Variant) matches the set.
    • Validate and error: Use .check(&impl AsShape) to return a Result. If the shape is unsupported, it returns a crate::Error describing the mismatch.
  5. Handle arbitrary or invalid expressions with `from_invalid_expr`

    master

    Sometimes you need to parse content that is not a valid Rust expression (e.g., a where clause in a trait bound). In these cases, darling will call from_invalid_expr.

    To support truly arbitrary inputs like #[example(bound = where T: Deserialize<'de>, D: 'static)], you should implement both from_expr and from_invalid_expr. from_expr handles valid Rust expressions, while from_invalid_expr handles the tokens that failed expression parsing.

    Recommended Pattern for syn::parse::Parse types: If your type implements syn::parse::Parse, you can use syn::parse2 to handle both valid and invalid expressions by converting the tokens into a TokenStream.

    fn from_expr(expr: &syn::Expr) -> darling::Result<Self> {
        match *expr {
            // Invisible delimiter when the input to the macro is passed
            // by a `macro_rules!`, but we can safely ignore it
            Expr::Group(ref group) => Self::from_expr(&group.expr),
            _ => Ok(syn::parse2(expr.into_token_stream().clone())?)
        }
        .map_err(|e| e.with_span(expr))
    }
    
    fn from_invalid_expr(value: &MetaNameValueInvalidExpr) -> darling::Result<Self> {
        syn::parse2(value.value.clone()).map_err(Into::into)
    }
  6. Accumulate multiple errors using `Accumulator`

    master

    The Accumulator is used to collect multiple errors during a single parsing pass instead of failing on the first one.

    Important: An Accumulator will panic on drop if it has not been "defused" by calling finish, finish_with, or into_inner. This prevents developers from accidentally swallowing errors.

    Use handle or handle_in to process results. If a result is Err, the error is pushed to the accumulator and the method returns None.

    # // Example of validating a list of items and collecting all errors
    fn validate_things(inputs: Vec<Thing>) -> darling::Result<Vec<Output>> {
        let mut errors = darling::Error::accumulator();
    
        let outputs = inputs
            .into_iter()
            .filter_map(|thing| errors.handle_in(|| thing.validate()))
            .collect::<Vec<_>>();
    
        errors.finish()?; // Returns Err if any errors were collected
        Ok(outputs)
    }
  7. Understand the Purpose enum for type parameter tracing

    master

    The Purpose enum defines the context in which type parameters are being traced. This distinction is critical for determining which type parameter uses need to be reported to ensure generated code compiles correctly.

    • Purpose::BoundImpl: Used when tracing is intended to generate an impl block. In this mode, uses such as syn::TypePath.qself are not returned, as they typically do not require additional bounds in a trait implementation.
    • Purpose::Declare: Used when tracing is intended to generate a new struct or enum. In this mode, all uses are returned, ensuring that any helper types or associated types referencing generic parameters are correctly identified so the generated code can reference them.
  8. How Darling works: The FromMeta design pattern

    master

    Darling is designed for declarative attribute parsing in procedural macros. It follows a pattern inspired by serde: any data structure that can be read from an attribute implements the FromMeta trait.

    To use Darling, you define a struct that represents the settings your macro expects, and then use #[derive(FromMeta)] to automatically generate the parsing logic. This allows you to map complex attribute syntax directly onto typed Rust structures.

  9. Best practices for handling errors in darling

    master

    When building proc-macros with darling, follow these three best practices to ensure high-quality compiler diagnostics:

    1. Do not simplify darling::Error: Avoid converting darling::Error into other error types like syn::Error. To surface errors to the user, use darling::Error::write_errors. This preserves all span information and suggestions.
    2. Avoid early returns for validation: Do not use the ? operator for custom validations. Instead, use an error::Accumulator to collect all errors encountered during parsing. Use Accumulator::finish to return the final result; it returns Ok only if no errors were collected.
    3. Use with_span for custom errors: When creating additional errors via darling::Error::custom, call .with_span(node) to ensure the error points to the correct location in the source code. Use darling::util::SpannedValue to keep span information available on parsed fields.
  10. Use `Override<T>` to handle optional attribute values

    master

    The Override<T> type is used when an attribute can either inherit a default value from an external source or be explicitly set by the user.

    In darling, this is commonly used for attributes like default. An attribute can take two forms:

    1. Inherit: A bare word (e.g., #[darling(default)]). This maps to Override::Inherit.
    2. Explicit: A value or list (e.g., #[darling(default="path::to::fn")]). This maps to Override::Explicit(T).

    When defining a struct to collect these attributes using FromField or FromVariant, use Option<Override<T>> to represent the presence of the attribute itself.

    use darling::{util::Override, FromField};
    
    #[derive(FromField)]
    #[darling(attributes(darling))]
    pub struct Options {
       default: Option<Override<syn::Path>>,
    }
    
    impl Options {
        fn hydrate(self) -> Option<syn::Path> {
            // If the attribute was present, use the explicit value or fall back to a default path
            self.default.map(|ov| ov.unwrap_or(syn::parse_path("::Default::default").unwrap()))
        }
    }
  11. Understand the Shape of a fields container

    master

    In darling, a Shape describes the syntactic layout of fields within a container like a struct or a variant. This is used to validate that a user's code matches the expected structure required by an attribute.

    Available shapes:

    • Named: A set of named fields, e.g., { field: String }.
    • Tuple: A list of unnamed fields, e.g., (String, u64).
    • Unit: No fields, e.g., struct Example;.
    • Newtype: A special case of Tuple with exactly one field, e.g., (String).
  12. Parse `syn::Type` and other complex types using `from_syn_parse` pattern

    master

    For types that implement syn::parse::Parse (like syn::Type, syn::Visibility, or syn::WhereClause), you can leverage the pattern used by darling to bridge syn parsing with FromMeta.

    This pattern allows the type to be parsed from:

    1. A valid syn::Expr.
    2. A string literal (where the contents are parsed).
    3. An 'invalid' expression (where the tokens are parsed directly).