TedDriggs / darling

A Rust proc-macro attribute parser
MIT License
1.02k stars 66 forks source link

How to validate struct & field attributes #298

Closed holmofy closed 3 months ago

holmofy commented 4 months ago

I am currently implementing the feature of extracting HTML into a struct based on CSS selectors.

#[derive(Debug, FromDeriveInput)]
#[darling(attributes(css_selector), supports(struct_named))]
struct CssSelectorScraper {
    ident: syn::Ident,
    generics: syn::Generics,
    data: Data<(), CssSelectorStructField>,
    selector: Option<String>,
}

#[derive(Debug, FromField)]
#[darling(attributes(css_selector))]
struct CssSelectorStructField {
    ident: Option<syn::Ident>,
    ty: syn::Type,
    selector: Option<String>,
    attr: Option<String>,
    default: Option<String>,
}

I need to verify whether the selector of CssSelectorStructField is valid. I found two methods in many issues: #[darling(map="path::to::function")] and #[darling(with="path::to::function")]. which one should I use?

TedDriggs commented 4 months ago

Ideally, you wouldn't need to use either.

I'd recommend doing the following:

/// Contents TBD
struct CssSelectorParseError;

impl std::error::Error for CssSelectorParseError {
    // impl trait here
}

struct CssSelector(String);

impl FromStr for CssSelector {
    type Err = CssSelectorParseError;

    fn from_str(s: &str) -> Result<Self, CssSelectorParseError> {
        // validation goes here
    }
}

impl FromMeta for CssSelector {
    fn from_string(s: &str) -> darling::Result<Self> {
        s.parse().map_err(darling::Error::from)
    }
}

Then, change the type of your selector field to be Option<CssSelector>, and you won't need to use any attributes to get the behavior you want. darling will generate code that calls the <CssSelector as FromMeta>::from_meta function, which in turn will call the provided from_string method, which in turn will call your <CssSelector as FromStr>::from_str method via the parse() method.

This approach guarantees there cannot be an invalid selector there at any time, and allows you to expose methods to work with the selector as a selector, rather than having to work with it as a string all the time.