Skip to main content

better_auth_entity_codegen/
attributes.rs

1use super::*;
2/// Parsed `#[auth(...)]` container attributes.
3#[derive(Clone, Debug)]
4pub struct AuthAttributes {
5    pub role: EntityRole,
6    pub secondary_storage: bool,
7    pub table: Option<String>,
8    /// Application ID factory; returns a String compatible with existing columns.
9    pub id_generator: Option<syn::Path>,
10}
11
12/// Parse the role, optional ID factory and secondary-storage configuration.
13/// SQLx also permits an explicit table name.
14///
15/// # Errors
16///
17/// Returns a spanned error for an unknown key or role, or a missing role.
18pub fn parse_auth_attributes(
19    input: &DeriveInput,
20    allow_table: bool,
21) -> syn::Result<AuthAttributes> {
22    let mut parsed = None;
23    let mut secondary = false;
24    let mut table = None;
25    let mut id_generator = None;
26    for attr in &input.attrs {
27        if !attr.path().is_ident("auth") {
28            continue;
29        }
30        attr.parse_nested_meta(|meta| {
31            if meta.path.is_ident("role") {
32                let value = meta.value()?;
33                let role: LitStr = value.parse()?;
34                parsed = Some(match role.value().as_str() {
35                    "user" => EntityRole::User,
36                    "session" => EntityRole::Session,
37                    "account" => EntityRole::Account,
38                    "verification" => EntityRole::Verification,
39                    _ => {
40                        return Err(syn::Error::new_spanned(
41                            role,
42                            "unsupported auth role; expected user, session, account, or verification",
43                        ));
44                    }
45                });
46                Ok(())
47            } else if meta.path.is_ident("id_generator") {
48                let path = meta.value()?.parse::<LitStr>()?;
49                id_generator = Some(path.parse()?);
50                Ok(())
51            } else if meta.path.is_ident("secondary_storage") {
52                secondary = true;
53                Ok(())
54            } else if allow_table && meta.path.is_ident("table") {
55                table = Some(meta.value()?.parse::<LitStr>()?.value());
56                Ok(())
57            } else if allow_table {
58                Err(meta.error(
59                    "expected `role = \"...\"`, `table = \"...\"`, `id_generator = \"...\"` or `secondary_storage`",
60                ))
61            } else {
62                Err(meta.error("expected `role = \"...\"` or `secondary_storage`"))
63            }
64        })?;
65    }
66
67    parsed
68        .map(|role| AuthAttributes {
69            role,
70            secondary_storage: secondary,
71            table,
72            id_generator,
73        })
74        .ok_or_else(|| {
75            syn::Error::new_spanned(
76                input,
77                "missing #[auth(role = \"...\")] attribute for AuthEntity",
78            )
79        })
80}
81
82/// The derive input's named fields.
83///
84/// # Errors
85///
86/// Returns a spanned error for enums, unions, tuple and unit structs.
87pub fn named_fields(input: &DeriveInput) -> syn::Result<&FieldsNamed> {
88    match &input.data {
89        Data::Struct(data) => match &data.fields {
90            Fields::Named(fields) => Ok(fields),
91            Fields::Unnamed(_) | Fields::Unit => Err(syn::Error::new_spanned(
92                &input.ident,
93                "AuthEntity requires a struct with named fields",
94            )),
95        },
96        Data::Enum(_) | Data::Union(_) => Err(syn::Error::new_spanned(
97            &input.ident,
98            "AuthEntity requires a struct",
99        )),
100    }
101}
102
103/// Require every core field of the role.
104///
105/// # Errors
106///
107/// Returns a spanned error naming the first missing core field.
108pub fn validate_core_fields(
109    input: &DeriveInput,
110    role: EntityRole,
111    fields: &FieldsNamed,
112) -> syn::Result<()> {
113    if let Some(missing) = registry::core_field_names(role)
114        .iter()
115        .find(|required| !has_field(fields, required))
116    {
117        return Err(syn::Error::new_spanned(
118            &input.ident,
119            format!("missing required auth field `{missing}` for this role"),
120        ));
121    }
122    Ok(())
123}