Skip to main content

alibi_entity_codegen/
fields.rs

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