Skip to main content

alibi_entity_codegen/
attributes.rs

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