Skip to main content

appdb_macros/
lib.rs

1use proc_macro::TokenStream;
2use quote::{format_ident, quote};
3use std::collections::HashSet;
4use syn::{
5    parse_macro_input, Attribute, Data, DeriveInput, Error, Field, Fields, GenericArgument,
6    Meta, PathArguments, Type, TypePath,
7};
8
9#[proc_macro_derive(Sensitive, attributes(secure, crypto))]
10pub fn derive_sensitive(input: TokenStream) -> TokenStream {
11    match derive_sensitive_impl(parse_macro_input!(input as DeriveInput)) {
12        Ok(tokens) => tokens.into(),
13        Err(err) => err.to_compile_error().into(),
14    }
15}
16
17#[proc_macro_derive(Store, attributes(unique, secure, foreign, table_as, crypto))]
18pub fn derive_store(input: TokenStream) -> TokenStream {
19    match derive_store_impl(parse_macro_input!(input as DeriveInput)) {
20        Ok(tokens) => tokens.into(),
21        Err(err) => err.to_compile_error().into(),
22    }
23}
24
25#[proc_macro_derive(Relation, attributes(relation))]
26pub fn derive_relation(input: TokenStream) -> TokenStream {
27    match derive_relation_impl(parse_macro_input!(input as DeriveInput)) {
28        Ok(tokens) => tokens.into(),
29        Err(err) => err.to_compile_error().into(),
30    }
31}
32
33#[proc_macro_derive(Bridge)]
34pub fn derive_bridge(input: TokenStream) -> TokenStream {
35    match derive_bridge_impl(parse_macro_input!(input as DeriveInput)) {
36        Ok(tokens) => tokens.into(),
37        Err(err) => err.to_compile_error().into(),
38    }
39}
40
41fn derive_store_impl(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
42    let struct_ident = input.ident;
43    let vis = input.vis.clone();
44    let table_alias = table_alias_target(&input.attrs)?;
45
46    let named_fields = match input.data {
47        Data::Struct(data) => match data.fields {
48            Fields::Named(fields) => fields.named,
49            _ => {
50                return Err(Error::new_spanned(
51                    struct_ident,
52                    "Store can only be derived for structs with named fields",
53                ))
54            }
55        },
56        _ => {
57            return Err(Error::new_spanned(
58                struct_ident,
59                "Store can only be derived for structs",
60            ))
61        }
62    };
63
64    let id_fields = named_fields
65        .iter()
66        .filter(|field| is_id_type(&field.ty))
67        .map(|field| field.ident.clone().expect("named field"))
68        .collect::<Vec<_>>();
69
70    let secure_fields = named_fields
71        .iter()
72        .filter(|field| has_secure_attr(&field.attrs))
73        .map(|field| field.ident.clone().expect("named field"))
74        .collect::<Vec<_>>();
75
76    let unique_fields = named_fields
77        .iter()
78        .filter(|field| has_unique_attr(&field.attrs))
79        .map(|field| field.ident.clone().expect("named field"))
80        .collect::<Vec<_>>();
81
82    if id_fields.len() > 1 {
83        return Err(Error::new_spanned(
84            struct_ident,
85            "Store supports at most one `Id` field for automatic HasId generation",
86        ));
87    }
88
89    if let Some(invalid_field) = named_fields
90        .iter()
91        .find(|field| has_secure_attr(&field.attrs) && has_unique_attr(&field.attrs))
92    {
93        let ident = invalid_field.ident.as_ref().expect("named field");
94        return Err(Error::new_spanned(
95            ident,
96            "#[secure] fields cannot be used as #[unique] lookup keys",
97        ));
98    }
99
100    let foreign_fields = named_fields
101        .iter()
102        .filter_map(|field| match field_foreign_attr(field) {
103            Ok(Some(attr)) => Some(parse_foreign_field(field, attr)),
104            Ok(None) => None,
105            Err(err) => Some(Err(err)),
106        })
107        .collect::<syn::Result<Vec<_>>>()?;
108
109    if let Some(non_store_child) = foreign_fields
110        .iter()
111        .find_map(|field| invalid_foreign_leaf_type(&field.kind.original_ty))
112    {
113        return Err(Error::new_spanned(
114            non_store_child,
115            BINDREF_BRIDGE_STORE_ONLY,
116        ));
117    }
118
119    if let Some(invalid_field) = named_fields.iter().find(|field| {
120        field_foreign_attr(field).ok().flatten().is_some() && has_unique_attr(&field.attrs)
121    }) {
122        let ident = invalid_field.ident.as_ref().expect("named field");
123        return Err(Error::new_spanned(
124            ident,
125            "#[foreign] fields cannot be used as #[unique] lookup keys",
126        ));
127    }
128
129    let auto_has_id_impl = id_fields.first().map(|field| {
130        quote! {
131            impl ::appdb::model::meta::HasId for #struct_ident {
132                fn id(&self) -> ::surrealdb::types::RecordId {
133                    ::surrealdb::types::RecordId::new(
134                        <Self as ::appdb::model::meta::ModelMeta>::storage_table(),
135                        self.#field.clone(),
136                    )
137                }
138            }
139        }
140    });
141
142    let resolve_record_id_impl = if let Some(field) = id_fields.first() {
143        quote! {
144            #[::async_trait::async_trait]
145            impl ::appdb::model::meta::ResolveRecordId for #struct_ident {
146                async fn resolve_record_id(&self) -> ::anyhow::Result<::surrealdb::types::RecordId> {
147                    Ok(::surrealdb::types::RecordId::new(
148                        <Self as ::appdb::model::meta::ModelMeta>::storage_table(),
149                        self.#field.clone(),
150                    ))
151                }
152            }
153        }
154    } else {
155        quote! {
156            #[::async_trait::async_trait]
157            impl ::appdb::model::meta::ResolveRecordId for #struct_ident {
158                async fn resolve_record_id(&self) -> ::anyhow::Result<::surrealdb::types::RecordId> {
159                    ::appdb::repository::Repo::<Self>::find_unique_id_for(self).await
160                }
161            }
162        }
163    };
164
165    let resolved_table_name_expr = if let Some(target_ty) = &table_alias {
166        quote! { <#target_ty as ::appdb::model::meta::ModelMeta>::table_name() }
167    } else {
168        quote! {
169            {
170                let table = ::appdb::model::meta::default_table_name(stringify!(#struct_ident));
171                ::appdb::model::meta::register_table(stringify!(#struct_ident), table)
172            }
173        }
174    };
175
176    let unique_schema_impls = unique_fields.iter().map(|field| {
177        let field_name = field.to_string();
178        let index_name = format!(
179            "{}_{}_unique",
180            resolved_schema_table_name(&struct_ident, table_alias.as_ref()),
181            field_name
182        );
183        let ddl = format!(
184            "DEFINE INDEX IF NOT EXISTS {index_name} ON {} FIELDS {field_name} UNIQUE;",
185            resolved_schema_table_name(&struct_ident, table_alias.as_ref())
186        );
187
188        quote! {
189            ::inventory::submit! {
190                ::appdb::model::schema::SchemaItem {
191                    ddl: #ddl,
192                }
193            }
194        }
195    });
196
197    let lookup_fields = if unique_fields.is_empty() {
198        named_fields
199            .iter()
200            .filter_map(|field| {
201                let ident = field.ident.as_ref()?;
202                if ident == "id"
203                    || secure_fields.iter().any(|secure| secure == ident)
204                    || foreign_fields.iter().any(|foreign| foreign.ident == *ident)
205                {
206                    None
207                } else {
208                    Some(ident.to_string())
209                }
210            })
211            .collect::<Vec<_>>()
212    } else {
213        unique_fields
214            .iter()
215            .map(|field| field.to_string())
216            .collect::<Vec<_>>()
217    };
218
219    let foreign_field_literals = foreign_fields
220        .iter()
221        .map(|field| field.ident.to_string())
222        .map(|field| quote! { #field });
223    if id_fields.is_empty() && lookup_fields.is_empty() {
224        return Err(Error::new_spanned(
225            struct_ident,
226            "Store requires an `Id` field or at least one non-secure lookup field for automatic record resolution",
227        ));
228    }
229    let lookup_field_literals = lookup_fields.iter().map(|field| quote! { #field });
230
231    let stored_model_impl = if !foreign_fields.is_empty() {
232        quote! {}
233    } else if secure_field_count(&named_fields) > 0 {
234        quote! {
235            impl ::appdb::StoredModel for #struct_ident {
236                type Stored = <Self as ::appdb::Sensitive>::Encrypted;
237
238                fn into_stored(self) -> ::anyhow::Result<Self::Stored> {
239                    <Self as ::appdb::Sensitive>::encrypt_with_runtime_resolver(&self)
240                        .map_err(::anyhow::Error::from)
241                }
242
243                fn from_stored(stored: Self::Stored) -> ::anyhow::Result<Self> {
244                    <Self as ::appdb::Sensitive>::decrypt_with_runtime_resolver(&stored)
245                        .map_err(::anyhow::Error::from)
246                }
247
248                fn supports_create_return_id() -> bool {
249                    false
250                }
251            }
252        }
253    } else {
254        quote! {
255            impl ::appdb::StoredModel for #struct_ident {
256                type Stored = Self;
257
258                fn into_stored(self) -> ::anyhow::Result<Self::Stored> {
259                    ::std::result::Result::Ok(self)
260                }
261
262                fn from_stored(stored: Self::Stored) -> ::anyhow::Result<Self> {
263                    ::std::result::Result::Ok(stored)
264                }
265            }
266        }
267    };
268
269    let stored_fields = named_fields.iter().map(|field| {
270        let ident = field.ident.clone().expect("named field");
271        let ty = stored_field_type(field, &foreign_fields);
272        if is_record_id_type(&ty) {
273            quote! {
274                #[serde(deserialize_with = "::appdb::serde_utils::id::deserialize_record_id_or_compat_string")]
275                #ident: #ty
276            }
277        } else {
278            quote! { #ident: #ty }
279        }
280    });
281
282    let into_stored_assignments = named_fields.iter().map(|field| {
283        let ident = field.ident.clone().expect("named field");
284        match foreign_field_kind(&ident, &foreign_fields) {
285            Some(ForeignFieldKind { original_ty, .. }) => quote! {
286                #ident: <#original_ty as ::appdb::ForeignShape>::persist_foreign_shape(value.#ident).await?
287            },
288            None => quote! { #ident: value.#ident },
289        }
290    });
291
292    let from_stored_assignments = named_fields.iter().map(|field| {
293        let ident = field.ident.clone().expect("named field");
294        match foreign_field_kind(&ident, &foreign_fields) {
295            Some(ForeignFieldKind { original_ty, .. }) => quote! {
296                #ident: <#original_ty as ::appdb::ForeignShape>::hydrate_foreign_shape(stored.#ident).await?
297            },
298            None => quote! { #ident: stored.#ident },
299        }
300    });
301
302    let decode_foreign_fields = foreign_fields.iter().map(|field| {
303        let ident = field.ident.to_string();
304        quote! {
305            if let ::std::option::Option::Some(value) = map.get_mut(#ident) {
306                ::appdb::decode_stored_record_links(value);
307            }
308        }
309    });
310
311    let foreign_model_impl = if foreign_fields.is_empty() {
312        quote! {
313            impl ::appdb::ForeignModel for #struct_ident {
314                async fn persist_foreign(value: Self) -> ::anyhow::Result<Self::Stored> {
315                    <Self as ::appdb::StoredModel>::into_stored(value)
316                }
317
318                async fn hydrate_foreign(stored: Self::Stored) -> ::anyhow::Result<Self> {
319                    <Self as ::appdb::StoredModel>::from_stored(stored)
320                }
321
322                fn decode_stored_row(
323                    row: ::surrealdb::types::Value,
324                ) -> ::anyhow::Result<Self::Stored>
325                where
326                    Self::Stored: ::serde::de::DeserializeOwned,
327                {
328                    Ok(::serde_json::from_value(row.into_json_value())?)
329                }
330            }
331        }
332    } else {
333        let stored_struct_ident = format_ident!("AppdbStored{}", struct_ident);
334        quote! {
335            #[derive(
336                Debug,
337                Clone,
338                ::serde::Serialize,
339                ::serde::Deserialize,
340                ::surrealdb::types::SurrealValue,
341            )]
342            #vis struct #stored_struct_ident {
343                #( #stored_fields, )*
344            }
345
346            impl ::appdb::StoredModel for #struct_ident {
347                type Stored = #stored_struct_ident;
348
349                fn into_stored(self) -> ::anyhow::Result<Self::Stored> {
350                    unreachable!("foreign fields require async persist_foreign")
351                }
352
353                fn from_stored(_stored: Self::Stored) -> ::anyhow::Result<Self> {
354                    unreachable!("foreign fields require async hydrate_foreign")
355                }
356            }
357
358            impl ::appdb::ForeignModel for #struct_ident {
359                async fn persist_foreign(value: Self) -> ::anyhow::Result<Self::Stored> {
360                    let value = value;
361                    Ok(#stored_struct_ident {
362                        #( #into_stored_assignments, )*
363                    })
364                }
365
366                async fn hydrate_foreign(stored: Self::Stored) -> ::anyhow::Result<Self> {
367                    Ok(Self {
368                        #( #from_stored_assignments, )*
369                    })
370                }
371
372                fn has_foreign_fields() -> bool {
373                    true
374                }
375
376                fn decode_stored_row(
377                    row: ::surrealdb::types::Value,
378                ) -> ::anyhow::Result<Self::Stored>
379                where
380                    Self::Stored: ::serde::de::DeserializeOwned,
381                {
382                    let mut row = row.into_json_value();
383                    if let ::serde_json::Value::Object(map) = &mut row {
384                        #( #decode_foreign_fields )*
385                    }
386                    Ok(::serde_json::from_value(row)?)
387                }
388            }
389        }
390    };
391
392    let store_marker_ident = format_ident!("AppdbStoreMarker{}", struct_ident);
393
394    Ok(quote! {
395        #[doc(hidden)]
396        #vis struct #store_marker_ident;
397
398        impl ::appdb::model::meta::ModelMeta for #struct_ident {
399            fn storage_table() -> &'static str {
400                #resolved_table_name_expr
401            }
402
403            fn table_name() -> &'static str {
404                static TABLE_NAME: ::std::sync::OnceLock<&'static str> = ::std::sync::OnceLock::new();
405                TABLE_NAME.get_or_init(|| {
406                    let table = #resolved_table_name_expr;
407                    ::appdb::model::meta::register_table(stringify!(#struct_ident), table)
408                })
409            }
410        }
411
412        impl ::appdb::model::meta::StoreModelMarker for #struct_ident {}
413        impl ::appdb::model::meta::StoreModelMarker for #store_marker_ident {}
414
415        impl ::appdb::model::meta::UniqueLookupMeta for #struct_ident {
416            fn lookup_fields() -> &'static [&'static str] {
417                &[ #( #lookup_field_literals ),* ]
418            }
419
420            fn foreign_fields() -> &'static [&'static str] {
421                &[ #( #foreign_field_literals ),* ]
422            }
423        }
424        #stored_model_impl
425        #foreign_model_impl
426
427        #auto_has_id_impl
428        #resolve_record_id_impl
429
430        #( #unique_schema_impls )*
431
432        impl ::appdb::repository::Crud for #struct_ident {}
433
434        impl #struct_ident {
435            /// Saves one value through the recommended Store CRUD surface.
436            ///
437            /// Prefer these generated model methods in application code. Lower-level
438            /// `appdb::repository::Repo` helpers exist for library internals and
439            /// advanced integration seams, not as the primary public path.
440            pub async fn save(self) -> ::anyhow::Result<Self> {
441                <Self as ::appdb::repository::Crud>::save(self).await
442            }
443
444            /// Saves many values through the recommended Store CRUD surface.
445            pub async fn save_many(data: ::std::vec::Vec<Self>) -> ::anyhow::Result<::std::vec::Vec<Self>> {
446                <Self as ::appdb::repository::Crud>::save_many(data).await
447            }
448
449            pub async fn get<T>(id: T) -> ::anyhow::Result<Self>
450            where
451                ::surrealdb::types::RecordIdKey: From<T>,
452                T: Send,
453            {
454                ::appdb::repository::Repo::<Self>::get(id).await
455            }
456
457            pub async fn list() -> ::anyhow::Result<::std::vec::Vec<Self>> {
458                ::appdb::repository::Repo::<Self>::list().await
459            }
460
461            pub async fn list_limit(count: i64) -> ::anyhow::Result<::std::vec::Vec<Self>> {
462                ::appdb::repository::Repo::<Self>::list_limit(count).await
463            }
464
465            pub async fn delete_all() -> ::anyhow::Result<()> {
466                ::appdb::repository::Repo::<Self>::delete_all().await
467            }
468
469            pub async fn find_one_id(
470                k: &str,
471                v: &str,
472            ) -> ::anyhow::Result<::surrealdb::types::RecordId> {
473                ::appdb::repository::Repo::<Self>::find_one_id(k, v).await
474            }
475
476            pub async fn list_record_ids() -> ::anyhow::Result<::std::vec::Vec<::surrealdb::types::RecordId>> {
477                ::appdb::repository::Repo::<Self>::list_record_ids().await
478            }
479
480            pub async fn create_at(
481                id: ::surrealdb::types::RecordId,
482                data: Self,
483            ) -> ::anyhow::Result<Self> {
484                ::appdb::repository::Repo::<Self>::create_at(id, data).await
485            }
486
487            pub async fn upsert_at(
488                id: ::surrealdb::types::RecordId,
489                data: Self,
490            ) -> ::anyhow::Result<Self> {
491                ::appdb::repository::Repo::<Self>::upsert_at(id, data).await
492            }
493
494            pub async fn update_at(
495                self,
496                id: ::surrealdb::types::RecordId,
497            ) -> ::anyhow::Result<Self> {
498                ::appdb::repository::Repo::<Self>::update_at(id, self).await
499            }
500
501
502            pub async fn delete<T>(id: T) -> ::anyhow::Result<()>
503            where
504                ::surrealdb::types::RecordIdKey: From<T>,
505                T: Send,
506            {
507                ::appdb::repository::Repo::<Self>::delete(id).await
508            }
509        }
510    })
511}
512
513fn derive_bridge_impl(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
514    let enum_ident = input.ident;
515
516    let variants = match input.data {
517        Data::Enum(data) => data.variants,
518        _ => {
519            return Err(Error::new_spanned(
520                enum_ident,
521                "Bridge can only be derived for enums",
522            ))
523        }
524    };
525
526    let payloads = variants
527        .iter()
528        .map(parse_bridge_variant)
529        .collect::<syn::Result<Vec<_>>>()?;
530
531    let from_impls = payloads.iter().map(|variant| {
532        let variant_ident = &variant.variant_ident;
533        let payload_ty = &variant.payload_ty;
534
535        quote! {
536            impl ::std::convert::From<#payload_ty> for #enum_ident {
537                fn from(value: #payload_ty) -> Self {
538                    Self::#variant_ident(value)
539                }
540            }
541        }
542    });
543
544    let persist_match_arms = payloads.iter().map(|variant| {
545        let variant_ident = &variant.variant_ident;
546
547        quote! {
548            Self::#variant_ident(value) => <_ as ::appdb::Bridge>::persist_foreign(value).await,
549        }
550    });
551
552    let hydrate_match_arms = payloads.iter().map(|variant| {
553        let variant_ident = &variant.variant_ident;
554        let payload_ty = &variant.payload_ty;
555
556        quote! {
557            table if table == <#payload_ty as ::appdb::model::meta::ModelMeta>::storage_table() => {
558                ::std::result::Result::Ok(Self::#variant_ident(
559                    <#payload_ty as ::appdb::Bridge>::hydrate_foreign(id).await?,
560                ))
561            }
562        }
563    });
564
565    Ok(quote! {
566        #( #from_impls )*
567
568        #[::async_trait::async_trait]
569        impl ::appdb::Bridge for #enum_ident {
570            async fn persist_foreign(self) -> ::anyhow::Result<::surrealdb::types::RecordId> {
571                match self {
572                    #( #persist_match_arms )*
573                }
574            }
575
576            async fn hydrate_foreign(
577                id: ::surrealdb::types::RecordId,
578            ) -> ::anyhow::Result<Self> {
579                match id.table.to_string().as_str() {
580                    #( #hydrate_match_arms, )*
581                    table => ::anyhow::bail!(
582                        "unsupported foreign table `{table}` for enum dispatcher `{}`",
583                        ::std::stringify!(#enum_ident)
584                    ),
585                }
586            }
587        }
588    })
589}
590
591#[derive(Clone)]
592struct BridgeVariant {
593    variant_ident: syn::Ident,
594    payload_ty: Type,
595}
596
597fn parse_bridge_variant(variant: &syn::Variant) -> syn::Result<BridgeVariant> {
598    let payload_ty = match &variant.fields {
599        Fields::Unnamed(fields) if fields.unnamed.len() == 1 => {
600            fields.unnamed.first().expect("single field").ty.clone()
601        }
602        Fields::Unnamed(_) => {
603            return Err(Error::new_spanned(
604                &variant.ident,
605                "Bridge variants must be single-field tuple variants",
606            ))
607        }
608        Fields::Unit => {
609            return Err(Error::new_spanned(
610                &variant.ident,
611                "Bridge does not support unit variants",
612            ))
613        }
614        Fields::Named(_) => {
615            return Err(Error::new_spanned(
616                &variant.ident,
617                "Bridge does not support struct variants",
618            ))
619        }
620    };
621
622    let payload_path = match &payload_ty {
623        Type::Path(path) => path,
624        _ => {
625            return Err(Error::new_spanned(
626                &payload_ty,
627                "Bridge payload must implement appdb::Bridge",
628            ))
629        }
630    };
631
632    let segment = payload_path.path.segments.last().ok_or_else(|| {
633        Error::new_spanned(&payload_ty, "Bridge payload must implement appdb::Bridge")
634    })?;
635
636    if !matches!(segment.arguments, PathArguments::None) {
637        return Err(Error::new_spanned(
638            &payload_ty,
639            "Bridge payload must implement appdb::Bridge",
640        ));
641    }
642
643    Ok(BridgeVariant {
644        variant_ident: variant.ident.clone(),
645        payload_ty,
646    })
647}
648
649fn derive_relation_impl(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
650    let struct_ident = input.ident;
651    let relation_name = relation_name_override(&input.attrs)?
652        .unwrap_or_else(|| to_snake_case(&struct_ident.to_string()));
653
654    match input.data {
655        Data::Struct(data) => {
656            match data.fields {
657                Fields::Unit | Fields::Named(_) => {}
658                _ => return Err(Error::new_spanned(
659                    struct_ident,
660                    "Relation can only be derived for unit structs or structs with named fields",
661                )),
662            }
663        }
664        _ => {
665            return Err(Error::new_spanned(
666                struct_ident,
667                "Relation can only be derived for structs",
668            ))
669        }
670    }
671
672    Ok(quote! {
673        impl ::appdb::model::relation::RelationMeta for #struct_ident {
674            fn relation_name() -> &'static str {
675                static REL_NAME: ::std::sync::OnceLock<&'static str> = ::std::sync::OnceLock::new();
676                REL_NAME.get_or_init(|| ::appdb::model::relation::register_relation(#relation_name))
677            }
678        }
679
680        impl #struct_ident {
681            pub async fn relate<A, B>(a: &A, b: &B) -> ::anyhow::Result<()>
682            where
683                A: ::appdb::model::meta::ResolveRecordId + Send + Sync,
684                B: ::appdb::model::meta::ResolveRecordId + Send + Sync,
685            {
686                ::appdb::graph::relate_at(a.resolve_record_id().await?, b.resolve_record_id().await?, <Self as ::appdb::model::relation::RelationMeta>::relation_name()).await
687            }
688
689            pub async fn unrelate<A, B>(a: &A, b: &B) -> ::anyhow::Result<()>
690            where
691                A: ::appdb::model::meta::ResolveRecordId + Send + Sync,
692                B: ::appdb::model::meta::ResolveRecordId + Send + Sync,
693            {
694                ::appdb::graph::unrelate_at(a.resolve_record_id().await?, b.resolve_record_id().await?, <Self as ::appdb::model::relation::RelationMeta>::relation_name()).await
695            }
696
697            pub async fn out_ids<A>(a: &A, out_table: &str) -> ::anyhow::Result<::std::vec::Vec<::surrealdb::types::RecordId>>
698            where
699                A: ::appdb::model::meta::ResolveRecordId + Send + Sync,
700            {
701                ::appdb::graph::out_ids(a.resolve_record_id().await?, <Self as ::appdb::model::relation::RelationMeta>::relation_name(), out_table).await
702            }
703
704            pub async fn in_ids<B>(b: &B, in_table: &str) -> ::anyhow::Result<::std::vec::Vec<::surrealdb::types::RecordId>>
705            where
706                B: ::appdb::model::meta::ResolveRecordId + Send + Sync,
707            {
708                ::appdb::graph::in_ids(b.resolve_record_id().await?, <Self as ::appdb::model::relation::RelationMeta>::relation_name(), in_table).await
709            }
710        }
711    })
712}
713
714fn derive_sensitive_impl(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
715    let struct_ident = input.ident;
716    let encrypted_ident = format_ident!("Encrypted{}", struct_ident);
717    let vis = input.vis;
718    let type_crypto_config = type_crypto_config(&input.attrs)?;
719    let named_fields = match input.data {
720        Data::Struct(data) => match data.fields {
721            Fields::Named(fields) => fields.named,
722            _ => {
723                return Err(Error::new_spanned(
724                    struct_ident,
725                    "Sensitive can only be derived for structs with named fields",
726                ))
727            }
728        },
729        _ => {
730            return Err(Error::new_spanned(
731                struct_ident,
732                "Sensitive can only be derived for structs",
733            ))
734        }
735    };
736
737    let mut secure_field_count = 0usize;
738    let mut encrypted_fields = Vec::new();
739    let mut encrypt_assignments = Vec::new();
740    let mut decrypt_assignments = Vec::new();
741    let mut runtime_encrypt_assignments = Vec::new();
742    let mut runtime_decrypt_assignments = Vec::new();
743    let mut field_tag_structs = Vec::new();
744    let mut secure_field_meta_entries = Vec::new();
745
746    for field in named_fields.iter() {
747        let ident = field.ident.clone().expect("named field");
748        let field_vis = field.vis.clone();
749        let secure = has_secure_attr(&field.attrs);
750        let field_crypto_config = field_crypto_config(&field.attrs)?;
751
752        if !secure && field_crypto_config.is_present() {
753            return Err(Error::new_spanned(
754                ident,
755                "#[crypto(...)] on a field requires #[secure] on the same field",
756            ));
757        }
758
759        if secure {
760            secure_field_count += 1;
761            let secure_kind = secure_kind(field)?;
762            let encrypted_ty = secure_kind.encrypted_type();
763            let field_tag_ident = format_ident!(
764                "AppdbSensitiveFieldTag{}{}",
765                struct_ident,
766                to_pascal_case(&ident.to_string())
767            );
768            let field_tag_literal = ident.to_string();
769            let effective_account = field_crypto_config
770                .field_account
771                .clone()
772                .or_else(|| type_crypto_config.account.clone());
773            let service_override = type_crypto_config.service.clone();
774            let account_literal = effective_account
775                .as_ref()
776                .map(|value| quote! { ::std::option::Option::Some(#value) })
777                .unwrap_or_else(|| quote! { ::std::option::Option::None });
778            let service_literal = service_override
779                .as_ref()
780                .map(|value| quote! { ::std::option::Option::Some(#value) })
781                .unwrap_or_else(|| quote! { ::std::option::Option::None });
782            let encrypt_expr = secure_kind.encrypt_with_context_expr(&ident);
783            let decrypt_expr = secure_kind.decrypt_with_context_expr(&ident);
784            let runtime_encrypt_expr =
785                secure_kind.encrypt_with_runtime_expr(&ident, &field_tag_ident);
786            let runtime_decrypt_expr =
787                secure_kind.decrypt_with_runtime_expr(&ident, &field_tag_ident);
788            encrypted_fields.push(quote! { #field_vis #ident: #encrypted_ty });
789            encrypt_assignments.push(quote! { #ident: #encrypt_expr });
790            decrypt_assignments.push(quote! { #ident: #decrypt_expr });
791            runtime_encrypt_assignments.push(quote! { #ident: #runtime_encrypt_expr });
792            runtime_decrypt_assignments.push(quote! { #ident: #runtime_decrypt_expr });
793            secure_field_meta_entries.push(quote! {
794                ::appdb::crypto::SensitiveFieldMetadata {
795                    model_tag: ::std::concat!(::std::module_path!(), "::", ::std::stringify!(#struct_ident)),
796                    field_tag: #field_tag_literal,
797                    service: #service_literal,
798                    account: #account_literal,
799                    secure_fields: &[],
800                }
801            });
802            field_tag_structs.push(quote! {
803                #[doc(hidden)]
804                #vis struct #field_tag_ident;
805
806                impl ::appdb::crypto::SensitiveFieldTag for #field_tag_ident {
807                    fn model_tag() -> &'static str {
808                        <#struct_ident as ::appdb::crypto::SensitiveModelTag>::model_tag()
809                    }
810
811                    fn field_tag() -> &'static str {
812                        #field_tag_literal
813                    }
814
815                    fn crypto_metadata() -> &'static ::appdb::crypto::SensitiveFieldMetadata {
816                        static FIELD_META: ::std::sync::OnceLock<::appdb::crypto::SensitiveFieldMetadata> = ::std::sync::OnceLock::new();
817                        FIELD_META.get_or_init(|| ::appdb::crypto::SensitiveFieldMetadata {
818                            model_tag: <#struct_ident as ::appdb::crypto::SensitiveModelTag>::model_tag(),
819                            field_tag: #field_tag_literal,
820                            service: #service_literal,
821                            account: #account_literal,
822                            secure_fields: &#struct_ident::SECURE_FIELDS,
823                        })
824                    }
825                }
826            });
827        } else {
828            let ty = field.ty.clone();
829            encrypted_fields.push(quote! { #field_vis #ident: #ty });
830            encrypt_assignments.push(quote! { #ident: self.#ident.clone() });
831            decrypt_assignments.push(quote! { #ident: encrypted.#ident.clone() });
832            runtime_encrypt_assignments.push(quote! { #ident: self.#ident.clone() });
833            runtime_decrypt_assignments.push(quote! { #ident: encrypted.#ident.clone() });
834        }
835    }
836
837    if secure_field_count == 0 {
838        return Err(Error::new_spanned(
839            struct_ident,
840            "Sensitive requires at least one #[secure] field",
841        ));
842    }
843
844    Ok(quote! {
845        #[derive(
846            Debug,
847            Clone,
848            ::serde::Serialize,
849            ::serde::Deserialize,
850            ::surrealdb::types::SurrealValue,
851        )]
852        #vis struct #encrypted_ident {
853            #( #encrypted_fields, )*
854        }
855
856        impl ::appdb::crypto::SensitiveModelTag for #struct_ident {
857            fn model_tag() -> &'static str {
858                ::std::concat!(::std::module_path!(), "::", ::std::stringify!(#struct_ident))
859            }
860        }
861
862        #( #field_tag_structs )*
863
864        impl ::appdb::Sensitive for #struct_ident {
865            type Encrypted = #encrypted_ident;
866
867            fn encrypt(
868                &self,
869                context: &::appdb::crypto::CryptoContext,
870            ) -> ::std::result::Result<Self::Encrypted, ::appdb::crypto::CryptoError> {
871                ::std::result::Result::Ok(#encrypted_ident {
872                    #( #encrypt_assignments, )*
873                })
874            }
875
876            fn decrypt(
877                encrypted: &Self::Encrypted,
878                context: &::appdb::crypto::CryptoContext,
879            ) -> ::std::result::Result<Self, ::appdb::crypto::CryptoError> {
880                ::std::result::Result::Ok(Self {
881                    #( #decrypt_assignments, )*
882                })
883            }
884
885            fn encrypt_with_runtime_resolver(
886                &self,
887            ) -> ::std::result::Result<Self::Encrypted, ::appdb::crypto::CryptoError> {
888                ::std::result::Result::Ok(#encrypted_ident {
889                    #( #runtime_encrypt_assignments, )*
890                })
891            }
892
893            fn decrypt_with_runtime_resolver(
894                encrypted: &Self::Encrypted,
895            ) -> ::std::result::Result<Self, ::appdb::crypto::CryptoError> {
896                ::std::result::Result::Ok(Self {
897                    #( #runtime_decrypt_assignments, )*
898                })
899            }
900
901            fn secure_fields() -> &'static [::appdb::crypto::SensitiveFieldMetadata] {
902                &Self::SECURE_FIELDS
903            }
904        }
905
906        impl #struct_ident {
907            pub const SECURE_FIELDS: [::appdb::crypto::SensitiveFieldMetadata; #secure_field_count] = [
908                #( #secure_field_meta_entries, )*
909            ];
910
911            pub fn encrypt(
912                &self,
913                context: &::appdb::crypto::CryptoContext,
914            ) -> ::std::result::Result<#encrypted_ident, ::appdb::crypto::CryptoError> {
915                <Self as ::appdb::Sensitive>::encrypt(self, context)
916            }
917        }
918
919        impl #encrypted_ident {
920            pub fn decrypt(
921                &self,
922                context: &::appdb::crypto::CryptoContext,
923            ) -> ::std::result::Result<#struct_ident, ::appdb::crypto::CryptoError> {
924                <#struct_ident as ::appdb::Sensitive>::decrypt(self, context)
925            }
926        }
927    })
928}
929
930fn has_secure_attr(attrs: &[Attribute]) -> bool {
931    attrs.iter().any(|attr| attr.path().is_ident("secure"))
932}
933
934fn has_unique_attr(attrs: &[Attribute]) -> bool {
935    attrs.iter().any(|attr| attr.path().is_ident("unique"))
936}
937
938#[derive(Default, Clone)]
939struct TypeCryptoConfig {
940    service: Option<String>,
941    account: Option<String>,
942}
943
944#[derive(Default, Clone)]
945struct FieldCryptoConfig {
946    field_account: Option<String>,
947}
948
949impl FieldCryptoConfig {
950    fn is_present(&self) -> bool {
951        self.field_account.is_some()
952    }
953}
954
955fn type_crypto_config(attrs: &[Attribute]) -> syn::Result<TypeCryptoConfig> {
956    let mut config = TypeCryptoConfig::default();
957    let mut seen = HashSet::new();
958
959    for attr in attrs {
960        if !attr.path().is_ident("crypto") {
961            continue;
962        }
963
964        attr.parse_nested_meta(|meta| {
965            let key = meta
966                .path
967                .get_ident()
968                .cloned()
969                .ok_or_else(|| meta.error("unsupported crypto attribute"))?;
970
971            if !seen.insert(key.to_string()) {
972                return Err(meta.error("duplicate crypto attribute key"));
973            }
974
975            let value = meta.value()?;
976            let literal: syn::LitStr = value.parse()?;
977            match key.to_string().as_str() {
978                "service" => config.service = Some(literal.value()),
979                "account" => config.account = Some(literal.value()),
980                _ => return Err(meta.error("unsupported crypto attribute; expected `service` or `account`")),
981            }
982            Ok(())
983        })?;
984    }
985
986    Ok(config)
987}
988
989fn field_crypto_config(attrs: &[Attribute]) -> syn::Result<FieldCryptoConfig> {
990    let mut config = FieldCryptoConfig::default();
991    let mut seen = HashSet::new();
992
993    for attr in attrs {
994        if attr.path().is_ident("crypto") {
995            attr.parse_nested_meta(|meta| {
996                let key = meta
997                    .path
998                    .get_ident()
999                    .cloned()
1000                    .ok_or_else(|| meta.error("unsupported crypto attribute"))?;
1001
1002                if !seen.insert(key.to_string()) {
1003                    return Err(meta.error("duplicate crypto attribute key"));
1004                }
1005
1006                let value = meta.value()?;
1007                let literal: syn::LitStr = value.parse()?;
1008                match key.to_string().as_str() {
1009                    "field_account" => config.field_account = Some(literal.value()),
1010                    _ => {
1011                        return Err(meta.error(
1012                            "unsupported field crypto attribute; expected `field_account`",
1013                        ))
1014                    }
1015                }
1016                Ok(())
1017            })?;
1018        } else if attr.path().is_ident("secure") && matches!(attr.meta, Meta::List(_)) {
1019            return Err(Error::new_spanned(
1020                attr,
1021                "#[secure] does not accept arguments; use #[crypto(field_account = \"...\")] on the field",
1022            ));
1023        }
1024    }
1025
1026    Ok(config)
1027}
1028
1029fn table_alias_target(attrs: &[Attribute]) -> syn::Result<Option<Type>> {
1030    let mut target = None;
1031
1032    for attr in attrs {
1033        if !attr.path().is_ident("table_as") {
1034            continue;
1035        }
1036
1037        if target.is_some() {
1038            return Err(Error::new_spanned(
1039                attr,
1040                "duplicate #[table_as(...)] attribute is not supported",
1041            ));
1042        }
1043
1044        let parsed: Type = attr.parse_args().map_err(|_| {
1045            Error::new_spanned(attr, "#[table_as(...)] requires exactly one target type")
1046        })?;
1047
1048        match parsed {
1049            Type::Path(TypePath { ref path, .. }) if !path.segments.is_empty() => {
1050                target = Some(parsed);
1051            }
1052            _ => {
1053                return Err(Error::new_spanned(
1054                    parsed,
1055                    "#[table_as(...)] target must be a type path",
1056                ))
1057            }
1058        }
1059    }
1060
1061    Ok(target)
1062}
1063
1064fn resolved_schema_table_name(struct_ident: &syn::Ident, table_alias: Option<&Type>) -> String {
1065    match table_alias {
1066        Some(Type::Path(type_path)) => type_path
1067            .path
1068            .segments
1069            .last()
1070            .map(|segment| to_snake_case(&segment.ident.to_string()))
1071            .unwrap_or_else(|| to_snake_case(&struct_ident.to_string())),
1072        Some(_) => to_snake_case(&struct_ident.to_string()),
1073        None => to_snake_case(&struct_ident.to_string()),
1074    }
1075}
1076
1077fn field_foreign_attr(field: &Field) -> syn::Result<Option<&Attribute>> {
1078    let mut foreign_attr = None;
1079
1080    for attr in &field.attrs {
1081        if !attr.path().is_ident("foreign") {
1082            continue;
1083        }
1084
1085        if foreign_attr.is_some() {
1086            return Err(Error::new_spanned(
1087                attr,
1088                "duplicate nested-ref attribute is not supported",
1089            ));
1090        }
1091
1092        foreign_attr = Some(attr);
1093    }
1094
1095    Ok(foreign_attr)
1096}
1097
1098fn validate_foreign_field(field: &Field, attr: &Attribute) -> syn::Result<Type> {
1099    if attr.path().is_ident("foreign") {
1100        return foreign_leaf_type(&field.ty)
1101            .ok_or_else(|| Error::new_spanned(&field.ty, BINDREF_ACCEPTED_SHAPES));
1102    }
1103
1104    Err(Error::new_spanned(attr, "unsupported foreign attribute"))
1105}
1106
1107const BINDREF_ACCEPTED_SHAPES: &str =
1108    "#[foreign] supports recursive Option<_> / Vec<_> shapes whose leaf type implements appdb::Bridge";
1109
1110const BINDREF_BRIDGE_STORE_ONLY: &str =
1111    "#[foreign] leaf types must derive Store or #[derive(Bridge)] dispatcher enums";
1112
1113#[derive(Clone)]
1114struct ForeignField {
1115    ident: syn::Ident,
1116    kind: ForeignFieldKind,
1117}
1118
1119#[derive(Clone)]
1120struct ForeignFieldKind {
1121    original_ty: Type,
1122    stored_ty: Type,
1123}
1124
1125fn parse_foreign_field(field: &Field, attr: &Attribute) -> syn::Result<ForeignField> {
1126    validate_foreign_field(field, attr)?;
1127    let ident = field.ident.clone().expect("named field");
1128
1129    let kind = ForeignFieldKind {
1130        original_ty: field.ty.clone(),
1131        stored_ty: foreign_stored_type(&field.ty)
1132            .ok_or_else(|| Error::new_spanned(&field.ty, BINDREF_ACCEPTED_SHAPES))?,
1133    };
1134
1135    Ok(ForeignField { ident, kind })
1136}
1137
1138fn foreign_field_kind<'a>(
1139    ident: &syn::Ident,
1140    fields: &'a [ForeignField],
1141) -> Option<&'a ForeignFieldKind> {
1142    fields
1143        .iter()
1144        .find(|field| field.ident == *ident)
1145        .map(|field| &field.kind)
1146}
1147
1148fn stored_field_type(field: &Field, foreign_fields: &[ForeignField]) -> Type {
1149    let ident = field.ident.as_ref().expect("named field");
1150    match foreign_field_kind(ident, foreign_fields) {
1151        Some(ForeignFieldKind { stored_ty, .. }) => stored_ty.clone(),
1152        None => field.ty.clone(),
1153    }
1154}
1155
1156fn foreign_stored_type(ty: &Type) -> Option<Type> {
1157    if let Some(inner) = option_inner_type(ty) {
1158        let inner = foreign_stored_type(inner)?;
1159        return Some(syn::parse_quote!(::std::option::Option<#inner>));
1160    }
1161
1162    if let Some(inner) = vec_inner_type(ty) {
1163        let inner = foreign_stored_type(inner)?;
1164        return Some(syn::parse_quote!(::std::vec::Vec<#inner>));
1165    }
1166
1167    direct_store_child_type(ty)
1168        .cloned()
1169        .map(|_| syn::parse_quote!(::surrealdb::types::RecordId))
1170}
1171
1172fn foreign_leaf_type(ty: &Type) -> Option<Type> {
1173    if let Some(inner) = option_inner_type(ty) {
1174        return foreign_leaf_type(inner);
1175    }
1176
1177    if let Some(inner) = vec_inner_type(ty) {
1178        return foreign_leaf_type(inner);
1179    }
1180
1181    direct_store_child_type(ty).cloned().map(Type::Path)
1182}
1183
1184fn invalid_foreign_leaf_type(ty: &Type) -> Option<Type> {
1185    let leaf = foreign_leaf_type(ty)?;
1186    match &leaf {
1187        Type::Path(type_path) => {
1188            let segment = type_path.path.segments.last()?;
1189            if matches!(segment.arguments, PathArguments::None) {
1190                None
1191            } else {
1192                Some(leaf)
1193            }
1194        }
1195        _ => Some(leaf),
1196    }
1197}
1198
1199fn direct_store_child_type(ty: &Type) -> Option<&TypePath> {
1200    let Type::Path(type_path) = ty else {
1201        return None;
1202    };
1203
1204    let segment = type_path.path.segments.last()?;
1205    if !matches!(segment.arguments, PathArguments::None) {
1206        return None;
1207    }
1208
1209    if is_id_type(ty) || is_string_type(ty) || is_common_non_store_leaf_type(ty) {
1210        return None;
1211    }
1212
1213    Some(type_path)
1214}
1215
1216fn is_common_non_store_leaf_type(ty: &Type) -> bool {
1217    matches!(
1218        ty,
1219        Type::Path(TypePath { path, .. })
1220            if path.is_ident("bool")
1221                || path.is_ident("u8")
1222                || path.is_ident("u16")
1223                || path.is_ident("u32")
1224                || path.is_ident("u64")
1225                || path.is_ident("u128")
1226                || path.is_ident("usize")
1227                || path.is_ident("i8")
1228                || path.is_ident("i16")
1229                || path.is_ident("i32")
1230                || path.is_ident("i64")
1231                || path.is_ident("i128")
1232                || path.is_ident("isize")
1233                || path.is_ident("f32")
1234                || path.is_ident("f64")
1235                || path.is_ident("char")
1236    )
1237}
1238
1239fn secure_field_count(fields: &syn::punctuated::Punctuated<Field, syn::token::Comma>) -> usize {
1240    fields
1241        .iter()
1242        .filter(|field| has_secure_attr(&field.attrs))
1243        .count()
1244}
1245
1246fn relation_name_override(attrs: &[Attribute]) -> syn::Result<Option<String>> {
1247    for attr in attrs {
1248        if !attr.path().is_ident("relation") {
1249            continue;
1250        }
1251
1252        let mut name = None;
1253        attr.parse_nested_meta(|meta| {
1254            if meta.path.is_ident("name") {
1255                let value = meta.value()?;
1256                let literal: syn::LitStr = value.parse()?;
1257                name = Some(literal.value());
1258                Ok(())
1259            } else {
1260                Err(meta.error("unsupported relation attribute"))
1261            }
1262        })?;
1263        return Ok(name);
1264    }
1265
1266    Ok(None)
1267}
1268
1269enum SecureKind {
1270    Shape(Type),
1271}
1272
1273impl SecureKind {
1274    fn encrypted_type(&self) -> proc_macro2::TokenStream {
1275        match self {
1276            SecureKind::Shape(ty) => quote! { <#ty as ::appdb::SensitiveShape>::Encrypted },
1277        }
1278    }
1279
1280    fn encrypt_with_context_expr(&self, ident: &syn::Ident) -> proc_macro2::TokenStream {
1281        match self {
1282            SecureKind::Shape(ty) => {
1283                quote! { <#ty as ::appdb::SensitiveShape>::encrypt_with_context(&self.#ident, context)? }
1284            }
1285        }
1286    }
1287
1288    fn decrypt_with_context_expr(&self, ident: &syn::Ident) -> proc_macro2::TokenStream {
1289        match self {
1290            SecureKind::Shape(ty) => {
1291                quote! { <#ty as ::appdb::SensitiveShape>::decrypt_with_context(&encrypted.#ident, context)? }
1292            }
1293        }
1294    }
1295
1296    fn encrypt_with_runtime_expr(
1297        &self,
1298        ident: &syn::Ident,
1299        field_tag_ident: &syn::Ident,
1300    ) -> proc_macro2::TokenStream {
1301        match self {
1302            SecureKind::Shape(ty) => {
1303                quote! {{
1304                    let context = ::appdb::crypto::resolve_crypto_context_for::<#field_tag_ident>()?;
1305                    <#ty as ::appdb::SensitiveShape>::encrypt_with_context(&self.#ident, context.as_ref())?
1306                }}
1307            }
1308        }
1309    }
1310
1311    fn decrypt_with_runtime_expr(
1312        &self,
1313        ident: &syn::Ident,
1314        field_tag_ident: &syn::Ident,
1315    ) -> proc_macro2::TokenStream {
1316        match self {
1317            SecureKind::Shape(ty) => {
1318                quote! {{
1319                    let context = ::appdb::crypto::resolve_crypto_context_for::<#field_tag_ident>()?;
1320                    <#ty as ::appdb::SensitiveShape>::decrypt_with_context(&encrypted.#ident, context.as_ref())?
1321                }}
1322            }
1323        }
1324    }
1325}
1326
1327fn secure_kind(field: &Field) -> syn::Result<SecureKind> {
1328    if secure_shape_supported(&field.ty) {
1329        return Ok(SecureKind::Shape(field.ty.clone()));
1330    }
1331
1332    Err(Error::new_spanned(&field.ty, secure_shape_error_message(&field.ty)))
1333}
1334
1335fn secure_shape_supported(ty: &Type) -> bool {
1336    if is_string_type(ty) {
1337        return true;
1338    }
1339
1340    if sensitive_value_wrapper_inner_type(ty).is_some() {
1341        return true;
1342    }
1343
1344    if let Some(inner) = option_inner_type(ty) {
1345        return secure_shape_supported(inner);
1346    }
1347
1348    if let Some(inner) = vec_inner_type(ty) {
1349        return secure_shape_supported(inner);
1350    }
1351
1352    direct_sensitive_child_type(ty).is_some()
1353}
1354
1355fn secure_shape_error_message(ty: &Type) -> &'static str {
1356    if invalid_secure_leaf_type(ty).is_some() {
1357        "#[secure] child shapes require a direct named Sensitive type leaf with only Option<_> and Vec<_> wrappers"
1358    } else {
1359        "#[secure] supports String, appdb::SensitiveValueOf<T>, and recursive Child / Option<Child> / Vec<Child> shapes where Child implements appdb::Sensitive"
1360    }
1361}
1362
1363fn direct_sensitive_child_type(ty: &Type) -> Option<&TypePath> {
1364    let Type::Path(type_path) = ty else {
1365        return None;
1366    };
1367
1368    let segment = type_path.path.segments.last()?;
1369    if !matches!(segment.arguments, PathArguments::None) {
1370        return None;
1371    }
1372
1373    if is_id_type(ty) || is_string_type(ty) || is_common_non_store_leaf_type(ty) {
1374        return None;
1375    }
1376
1377    Some(type_path)
1378}
1379
1380fn invalid_secure_leaf_type(ty: &Type) -> Option<Type> {
1381    if let Some(inner) = option_inner_type(ty) {
1382        return invalid_secure_leaf_type(inner);
1383    }
1384
1385    if let Some(inner) = vec_inner_type(ty) {
1386        return invalid_secure_leaf_type(inner);
1387    }
1388
1389    let leaf = direct_sensitive_child_type(ty)?.clone();
1390    let segment = leaf.path.segments.last()?;
1391    if matches!(segment.arguments, PathArguments::None) {
1392        None
1393    } else {
1394        Some(Type::Path(leaf))
1395    }
1396}
1397
1398fn is_string_type(ty: &Type) -> bool {
1399    match ty {
1400        Type::Path(TypePath { path, .. }) => path.is_ident("String"),
1401        _ => false,
1402    }
1403}
1404
1405fn is_id_type(ty: &Type) -> bool {
1406    match ty {
1407        Type::Path(TypePath { path, .. }) => path.segments.last().is_some_and(|segment| {
1408            let ident = segment.ident.to_string();
1409            ident == "Id"
1410        }),
1411        _ => false,
1412    }
1413}
1414
1415fn is_record_id_type(ty: &Type) -> bool {
1416    match ty {
1417        Type::Path(TypePath { path, .. }) => path.segments.last().is_some_and(|segment| {
1418            let ident = segment.ident.to_string();
1419            ident == "RecordId"
1420        }),
1421        _ => false,
1422    }
1423}
1424
1425fn option_inner_type(ty: &Type) -> Option<&Type> {
1426    let Type::Path(TypePath { path, .. }) = ty else {
1427        return None;
1428    };
1429    let segment = path.segments.last()?;
1430    if segment.ident != "Option" {
1431        return None;
1432    }
1433    let PathArguments::AngleBracketed(args) = &segment.arguments else {
1434        return None;
1435    };
1436    let GenericArgument::Type(inner) = args.args.first()? else {
1437        return None;
1438    };
1439    Some(inner)
1440}
1441
1442fn vec_inner_type(ty: &Type) -> Option<&Type> {
1443    let Type::Path(TypePath { path, .. }) = ty else {
1444        return None;
1445    };
1446    let segment = path.segments.last()?;
1447    if segment.ident != "Vec" {
1448        return None;
1449    }
1450    let PathArguments::AngleBracketed(args) = &segment.arguments else {
1451        return None;
1452    };
1453    let GenericArgument::Type(inner) = args.args.first()? else {
1454        return None;
1455    };
1456    Some(inner)
1457}
1458
1459fn sensitive_value_wrapper_inner_type(ty: &Type) -> Option<&Type> {
1460    let Type::Path(TypePath { path, .. }) = ty else {
1461        return None;
1462    };
1463    let segment = path.segments.last()?;
1464    if segment.ident != "SensitiveValueOf" {
1465        return None;
1466    }
1467    let PathArguments::AngleBracketed(args) = &segment.arguments else {
1468        return None;
1469    };
1470    let GenericArgument::Type(inner) = args.args.first()? else {
1471        return None;
1472    };
1473    Some(inner)
1474}
1475
1476fn to_snake_case(input: &str) -> String {
1477    let mut out = String::with_capacity(input.len() + 4);
1478    let mut prev_is_lower_or_digit = false;
1479
1480    for ch in input.chars() {
1481        if ch.is_ascii_uppercase() {
1482            if prev_is_lower_or_digit {
1483                out.push('_');
1484            }
1485            out.push(ch.to_ascii_lowercase());
1486            prev_is_lower_or_digit = false;
1487        } else {
1488            out.push(ch);
1489            prev_is_lower_or_digit = ch.is_ascii_lowercase() || ch.is_ascii_digit();
1490        }
1491    }
1492
1493    out
1494}
1495
1496fn to_pascal_case(input: &str) -> String {
1497    let mut out = String::with_capacity(input.len());
1498    let mut uppercase_next = true;
1499
1500    for ch in input.chars() {
1501        if ch == '_' || ch == '-' {
1502            uppercase_next = true;
1503            continue;
1504        }
1505
1506        if uppercase_next {
1507            out.push(ch.to_ascii_uppercase());
1508            uppercase_next = false;
1509        } else {
1510            out.push(ch);
1511        }
1512    }
1513
1514    out
1515}