Skip to main content

better_auth_entity_codegen/
fields.rs

1use super::*;
2#[must_use]
3pub fn has_field(fields: &FieldsNamed, name: &str) -> bool {
4    fields
5        .named
6        .iter()
7        .any(|field| field.ident.as_ref().is_some_and(|ident| ident == name))
8}
9
10#[must_use]
11pub fn is_option(ty: &Type) -> bool {
12    matches!(ty, Type::Path(path) if path.path.segments.last().is_some_and(|segment| segment.ident == "Option"))
13}
14
15/// Whether the named field exists with an `Option` type.
16#[must_use]
17pub fn optional_field(fields: &FieldsNamed, name: &str) -> bool {
18    fields.named.iter().any(|field| {
19        field.ident.as_ref().is_some_and(|ident| ident == name) && is_option(&field.ty)
20    })
21}
22
23/// Whether a field name is a core or plugin field of the role.
24#[must_use]
25pub fn is_known_field(role: EntityRole, name: &Ident) -> bool {
26    registry::core_field_names(role)
27        .iter()
28        .chain(registry::plugin_field_names(role).iter())
29        .any(|known| name == known)
30}
31
32/// The `lowerCamelCase` wire name of a snake-case Rust field.
33#[must_use]
34pub fn camel_case(name: &Ident) -> String {
35    let identifier = name.to_string();
36    let text = identifier.trim_start_matches("r#");
37    let mut components = text.split('_');
38    let mut camel = components.next().unwrap_or_default().to_owned();
39    for component in components {
40        let mut letters = component.chars();
41        if let Some(first) = letters.next() {
42            camel.extend(first.to_uppercase());
43        }
44        camel.extend(letters);
45    }
46    camel
47}
48
49/// A model field with its wire name and explicitly renamed physical column.
50#[derive(Clone)]
51pub struct EntityField {
52    pub ident: Ident,
53    pub camel: String,
54    /// The physical column when it is explicitly renamed.
55    pub physical: Option<String>,
56}
57
58/// Generate `fn additional_fields(&self)`: undeclared fields and nullable
59/// username values reach configured output transforms under their wire and
60/// physical names.
61#[must_use]
62pub fn additional_output(
63    role: EntityRole,
64    fields: &[EntityField],
65    core_root: &TokenStream,
66) -> TokenStream {
67    let mut output = Vec::new();
68    for field in fields {
69        let name = &field.ident;
70        let camel = &field.camel;
71        // Nullable username columns must reach configured output transforms as
72        // actual stored values, even though canonical wire DTOs omit None.
73        // Only declared model fields qualify; missing plugin columns stay absent.
74        let physical_output = !is_known_field(role, name)
75            || matches!(role, EntityRole::User)
76                && (name == "username" || name == "display_username");
77        if physical_output {
78            output.push(quote! {
79                if let Ok(value) = #core_root::utils::json::to_value(&self.#name) {
80                    let _ = fields.insert(#camel.into(), value);
81                }
82            });
83        }
84        if let Some(physical) = field
85            .physical
86            .as_ref()
87            .filter(|physical| *physical != camel)
88            && physical_output
89        {
90            output.push(quote! {
91                if let Ok(value) = #core_root::utils::json::to_value(&self.#name) {
92                    let _ = fields.insert(#physical.into(), value);
93                }
94            });
95        }
96    }
97    quote! {
98        fn additional_fields(&self) -> #core_root::field_policy::FieldOutput {
99            let mut fields = #core_root::field_policy::FieldOutput::new();
100            #(#output)*
101            fields
102        }
103    }
104}
105
106/// Auth date fields whose mutations arrive in the core UTC representation.
107#[must_use]
108pub fn is_auth_timestamp(name: &str) -> bool {
109    matches!(
110        name,
111        "created_at"
112            | "updated_at"
113            | "expires_at"
114            | "ban_expires"
115            | "access_token_expires_at"
116            | "refresh_token_expires_at"
117    )
118}