Skip to main content

appdb_macros/
lib.rs

1use proc_macro::TokenStream;
2use quote::{format_ident, quote};
3use std::collections::HashSet;
4use syn::{
5    Attribute, Data, DeriveInput, Error, Field, Fields, GenericArgument, Meta, PathArguments, Type,
6    TypePath, parse_macro_input,
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(
18    Store,
19    attributes(unique, secure, foreign, table_as, crypto, relate, back_relate, pagin)
20)]
21pub fn derive_store(input: TokenStream) -> TokenStream {
22    match derive_store_impl(parse_macro_input!(input as DeriveInput)) {
23        Ok(tokens) => tokens.into(),
24        Err(err) => err.to_compile_error().into(),
25    }
26}
27
28#[proc_macro_derive(Relation, attributes(relation))]
29pub fn derive_relation(input: TokenStream) -> TokenStream {
30    match derive_relation_impl(parse_macro_input!(input as DeriveInput)) {
31        Ok(tokens) => tokens.into(),
32        Err(err) => err.to_compile_error().into(),
33    }
34}
35
36#[proc_macro_derive(Bridge)]
37pub fn derive_bridge(input: TokenStream) -> TokenStream {
38    match derive_bridge_impl(parse_macro_input!(input as DeriveInput)) {
39        Ok(tokens) => tokens.into(),
40        Err(err) => err.to_compile_error().into(),
41    }
42}
43
44fn derive_store_impl(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
45    let struct_ident = input.ident;
46    let vis = input.vis.clone();
47    let table_alias = table_alias_target(&input.attrs)?;
48
49    let named_fields = match input.data {
50        Data::Struct(data) => match data.fields {
51            Fields::Named(fields) => fields.named,
52            _ => {
53                return Err(Error::new_spanned(
54                    struct_ident,
55                    "Store can only be derived for structs with named fields",
56                ));
57            }
58        },
59        _ => {
60            return Err(Error::new_spanned(
61                struct_ident,
62                "Store can only be derived for structs",
63            ));
64        }
65    };
66
67    let id_fields = named_fields
68        .iter()
69        .filter(|field| is_id_type(&field.ty))
70        .map(|field| field.ident.clone().expect("named field"))
71        .collect::<Vec<_>>();
72
73    let secure_fields = named_fields
74        .iter()
75        .filter(|field| has_secure_attr(&field.attrs))
76        .map(|field| field.ident.clone().expect("named field"))
77        .collect::<Vec<_>>();
78
79    let unique_fields = named_fields
80        .iter()
81        .filter(|field| has_unique_attr(&field.attrs))
82        .map(|field| field.ident.clone().expect("named field"))
83        .collect::<Vec<_>>();
84
85    let pagin_fields = named_fields
86        .iter()
87        .filter_map(|field| match field_pagin_attr(field) {
88            Ok(Some(attr)) => Some(parse_pagin_field(field, attr)),
89            Ok(None) => None,
90            Err(err) => Some(Err(err)),
91        })
92        .collect::<syn::Result<Vec<_>>>()?;
93    if pagin_fields.len() > 1 {
94        return Err(Error::new_spanned(
95            &pagin_fields[1].ident,
96            "Store supports at most one #[pagin] field",
97        ));
98    }
99    let pagin_field = pagin_fields.first().cloned();
100
101    if id_fields.len() > 1 {
102        return Err(Error::new_spanned(
103            struct_ident,
104            "Store supports at most one `Id` field for automatic HasId generation",
105        ));
106    }
107
108    if let Some(invalid_field) = named_fields
109        .iter()
110        .find(|field| has_secure_attr(&field.attrs) && has_unique_attr(&field.attrs))
111    {
112        let ident = invalid_field.ident.as_ref().expect("named field");
113        return Err(Error::new_spanned(
114            ident,
115            "#[secure] fields cannot be used as #[unique] lookup keys",
116        ));
117    }
118
119    let foreign_fields = named_fields
120        .iter()
121        .filter_map(|field| match field_foreign_attr(field) {
122            Ok(Some(attr)) => Some(parse_foreign_field(field, attr)),
123            Ok(None) => None,
124            Err(err) => Some(Err(err)),
125        })
126        .collect::<syn::Result<Vec<_>>>()?;
127
128    let relate_fields = named_fields
129        .iter()
130        .filter_map(|field| match field_relation_attr(field) {
131            Ok(Some(attr)) => Some(parse_relate_field(field, attr)),
132            Ok(None) => None,
133            Err(err) => Some(Err(err)),
134        })
135        .collect::<syn::Result<Vec<_>>>()?;
136
137    if let Some(non_store_child) = foreign_fields
138        .iter()
139        .find_map(|field| invalid_foreign_leaf_type(&field.kind.original_ty))
140    {
141        return Err(Error::new_spanned(
142            non_store_child,
143            BINDREF_BRIDGE_STORE_ONLY,
144        ));
145    }
146
147    if let Some(invalid_field) = named_fields.iter().find_map(|field| {
148        field_relation_attr(field)
149            .ok()
150            .flatten()
151            .filter(|_| has_unique_attr(&field.attrs))
152            .map(|attr| (field, attr))
153    }) {
154        let ident = invalid_field.0.ident.as_ref().expect("named field");
155        return Err(Error::new_spanned(
156            ident,
157            format!(
158                "{} fields cannot be used as #[unique] lookup keys",
159                relation_attr_label(invalid_field.1)
160            ),
161        ));
162    }
163
164    if let Some(invalid_field) = named_fields.iter().find_map(|field| {
165        field_relation_attr(field)
166            .ok()
167            .flatten()
168            .filter(|_| has_secure_attr(&field.attrs))
169            .map(|attr| (field, attr))
170    }) {
171        let ident = invalid_field.0.ident.as_ref().expect("named field");
172        return Err(Error::new_spanned(
173            ident,
174            format!(
175                "{} fields cannot be marked #[secure]",
176                relation_attr_label(invalid_field.1)
177            ),
178        ));
179    }
180
181    if let Some(invalid_field) = named_fields.iter().find_map(|field| {
182        field_relation_attr(field)
183            .ok()
184            .flatten()
185            .filter(|_| field_foreign_attr(field).ok().flatten().is_some())
186            .map(|attr| (field, attr))
187    }) {
188        let ident = invalid_field.0.ident.as_ref().expect("named field");
189        return Err(Error::new_spanned(
190            ident,
191            format!(
192                "{} cannot be combined with #[foreign]",
193                relation_attr_label(invalid_field.1)
194            ),
195        ));
196    }
197
198    if let Some(invalid_field) = named_fields.iter().find(|field| {
199        field_pagin_attr(field).ok().flatten().is_some() && has_secure_attr(&field.attrs)
200    }) {
201        let ident = invalid_field.ident.as_ref().expect("named field");
202        return Err(Error::new_spanned(
203            ident,
204            "#[pagin] fields cannot be marked #[secure]",
205        ));
206    }
207
208    if let Some(invalid_field) = named_fields.iter().find(|field| {
209        field_pagin_attr(field).ok().flatten().is_some()
210            && field_foreign_attr(field).ok().flatten().is_some()
211    }) {
212        let ident = invalid_field.ident.as_ref().expect("named field");
213        return Err(Error::new_spanned(
214            ident,
215            "#[pagin] cannot be combined with #[foreign]",
216        ));
217    }
218
219    if let Some(invalid_field) = named_fields.iter().find_map(|field| {
220        field_pagin_attr(field)
221            .ok()
222            .flatten()
223            .and_then(|_| field_relation_attr(field).ok().flatten())
224            .map(|attr| (field, attr))
225    }) {
226        let ident = invalid_field.0.ident.as_ref().expect("named field");
227        return Err(Error::new_spanned(
228            ident,
229            format!(
230                "#[pagin] cannot be combined with {}",
231                relation_attr_label(invalid_field.1)
232            ),
233        ));
234    }
235
236    let mut seen_relation_names = HashSet::new();
237    for field in &relate_fields {
238        if !seen_relation_names.insert(field.relation_name.clone()) {
239            return Err(Error::new_spanned(
240                &field.ident,
241                format!(
242                    "duplicate {} relation name is not supported within one Store model",
243                    field.direction.attr_label()
244                ),
245            ));
246        }
247    }
248
249    let auto_has_id_impl = id_fields.first().map(|field| {
250        quote! {
251            impl ::appdb::model::meta::HasId for #struct_ident {
252                fn id(&self) -> ::surrealdb::types::RecordId {
253                    ::surrealdb::types::RecordId::new(
254                        <Self as ::appdb::model::meta::ModelMeta>::storage_table(),
255                        self.#field.clone(),
256                    )
257                }
258            }
259        }
260    });
261
262    let resolve_record_id_impl = if let Some(field) = id_fields.first() {
263        quote! {
264            #[::async_trait::async_trait]
265            impl ::appdb::model::meta::ResolveRecordId for #struct_ident {
266                async fn resolve_record_id(&self) -> ::anyhow::Result<::surrealdb::types::RecordId> {
267                    Ok(::surrealdb::types::RecordId::new(
268                        <Self as ::appdb::model::meta::ModelMeta>::storage_table(),
269                        self.#field.clone(),
270                    ))
271                }
272            }
273        }
274    } else {
275        quote! {
276            #[::async_trait::async_trait]
277            impl ::appdb::model::meta::ResolveRecordId for #struct_ident {
278                async fn resolve_record_id(&self) -> ::anyhow::Result<::surrealdb::types::RecordId> {
279                    ::appdb::repository::Repo::<Self>::find_unique_id_for(self).await
280                }
281            }
282        }
283    };
284
285    let resolved_table_name_expr = if let Some(target_ty) = &table_alias {
286        quote! { <#target_ty as ::appdb::model::meta::ModelMeta>::table_name() }
287    } else {
288        quote! {
289            {
290                let table = ::appdb::model::meta::default_table_name(stringify!(#struct_ident));
291                ::appdb::model::meta::register_table(stringify!(#struct_ident), table)
292            }
293        }
294    };
295
296    let unique_schema_impls = unique_fields.iter().map(|field| {
297        let field_name = field.to_string();
298        let index_name = format!(
299            "{}_{}_unique",
300            resolved_schema_table_name(&struct_ident, table_alias.as_ref()),
301            field_name
302        );
303        let ddl = format!(
304            "DEFINE INDEX IF NOT EXISTS {index_name} ON {} FIELDS {field_name} UNIQUE;",
305            resolved_schema_table_name(&struct_ident, table_alias.as_ref())
306        );
307
308        quote! {
309            ::inventory::submit! {
310                ::appdb::model::schema::SchemaItem {
311                    ddl: #ddl,
312                }
313            }
314        }
315    });
316
317    let pagin_schema_impl = pagin_field.iter().map(|field| {
318        let field_name = field.ident.to_string();
319        let index_name = format!(
320            "{}_{}_id_pagin",
321            resolved_schema_table_name(&struct_ident, table_alias.as_ref()),
322            field_name
323        );
324        let ddl = if field_name == "id" {
325            format!(
326                "DEFINE INDEX IF NOT EXISTS {index_name} ON {} FIELDS id;",
327                resolved_schema_table_name(&struct_ident, table_alias.as_ref())
328            )
329        } else {
330            format!(
331                "DEFINE INDEX IF NOT EXISTS {index_name} ON {} FIELDS {field_name},id;",
332                resolved_schema_table_name(&struct_ident, table_alias.as_ref())
333            )
334        };
335
336        quote! {
337            ::inventory::submit! {
338                ::appdb::model::schema::SchemaItem {
339                    ddl: #ddl,
340                }
341            }
342        }
343    });
344
345    let lookup_fields = if unique_fields.is_empty() {
346        named_fields
347            .iter()
348            .filter_map(|field| {
349                let ident = field.ident.as_ref()?;
350                if ident == "id"
351                    || secure_fields.iter().any(|secure| secure == ident)
352                    || relate_fields.iter().any(|relate| relate.ident == *ident)
353                {
354                    None
355                } else {
356                    Some(ident.to_string())
357                }
358            })
359            .collect::<Vec<_>>()
360    } else {
361        unique_fields
362            .iter()
363            .map(|field| field.to_string())
364            .collect::<Vec<_>>()
365    };
366
367    let foreign_field_literals = foreign_fields
368        .iter()
369        .map(|field| field.ident.to_string())
370        .map(|field| quote! { #field })
371        .collect::<Vec<_>>();
372    let relate_field_literals = relate_fields
373        .iter()
374        .map(|field| field.ident.to_string())
375        .map(|field| quote! { #field })
376        .collect::<Vec<_>>();
377    if id_fields.is_empty() && lookup_fields.is_empty() {
378        return Err(Error::new_spanned(
379            struct_ident,
380            "Store requires an `Id` field or at least one non-secure lookup field for automatic record resolution",
381        ));
382    }
383    let lookup_field_literals = lookup_fields.iter().map(|field| quote! { #field });
384    let resolve_lookup_field_value_arms = foreign_fields.iter().map(|field| {
385        let ident = &field.ident;
386        let field_name = ident.to_string();
387        let original_ty = &field.kind.original_ty;
388        quote! {
389            #field_name => Ok(::std::option::Option::Some(
390                ::surrealdb::types::SurrealValue::into_value(
391                    <#original_ty as ::appdb::ForeignLookupShape>::resolve_foreign_lookup_shape(&self.#ident).await?
392                )
393            )),
394        }
395    });
396
397    let pagination_meta_impl = if let Some(field) = &pagin_field {
398        let field_name = field.ident.to_string();
399        quote! {
400            impl ::appdb::model::meta::PaginationMeta for #struct_ident {
401                fn pagination_field() -> ::std::option::Option<&'static str> {
402                    ::std::option::Option::Some(#field_name)
403                }
404            }
405        }
406    } else {
407        quote! {
408            impl ::appdb::model::meta::PaginationMeta for #struct_ident {}
409        }
410    };
411
412    let pagination_methods_impl = if pagin_field.is_some() {
413        quote! {
414            pub async fn pagin_desc(
415                count: i64,
416                cursor: ::std::option::Option<::appdb::PageCursor>,
417            ) -> ::anyhow::Result<::appdb::Page<Self>> {
418                ::appdb::repository::Repo::<Self>::pagin_desc(count, cursor).await
419            }
420
421            pub async fn pagin_asc(
422                count: i64,
423                cursor: ::std::option::Option<::appdb::PageCursor>,
424            ) -> ::anyhow::Result<::appdb::Page<Self>> {
425                ::appdb::repository::Repo::<Self>::pagin_asc(count, cursor).await
426            }
427        }
428    } else {
429        quote! {}
430    };
431
432    let stored_model_impl = if !foreign_fields.is_empty() {
433        quote! {}
434    } else if secure_field_count(&named_fields) > 0 {
435        quote! {
436            impl ::appdb::StoredModel for #struct_ident {
437                type Stored = <Self as ::appdb::Sensitive>::Encrypted;
438
439                fn into_stored(self) -> ::anyhow::Result<Self::Stored> {
440                    <Self as ::appdb::Sensitive>::encrypt_with_runtime_resolver(&self)
441                        .map_err(::anyhow::Error::from)
442                }
443
444                fn from_stored(stored: Self::Stored) -> ::anyhow::Result<Self> {
445                    <Self as ::appdb::Sensitive>::decrypt_with_runtime_resolver(&stored)
446                        .map_err(::anyhow::Error::from)
447                }
448
449                fn supports_create_return_id() -> bool {
450                    false
451                }
452            }
453        }
454    } else {
455        quote! {
456            impl ::appdb::StoredModel for #struct_ident {
457                type Stored = Self;
458
459                fn into_stored(self) -> ::anyhow::Result<Self::Stored> {
460                    ::std::result::Result::Ok(self)
461                }
462
463                fn from_stored(stored: Self::Stored) -> ::anyhow::Result<Self> {
464                    ::std::result::Result::Ok(stored)
465                }
466            }
467        }
468    };
469
470    let stored_fields = named_fields.iter().map(|field| {
471        let ident = field.ident.clone().expect("named field");
472        let ty = stored_field_type(field, &foreign_fields);
473        if is_record_id_type(&ty) {
474            quote! {
475                #[serde(deserialize_with = "::appdb::serde_utils::id::deserialize_record_id_or_compat_string")]
476                #ident: #ty
477            }
478        } else {
479            quote! { #ident: #ty }
480        }
481    });
482
483    let into_stored_assignments = named_fields.iter().map(|field| {
484        let ident = field.ident.clone().expect("named field");
485        match foreign_field_kind(&ident, &foreign_fields) {
486            Some(ForeignFieldKind { original_ty, .. }) => quote! {
487                #ident: <#original_ty as ::appdb::ForeignShape>::persist_foreign_shape(value.#ident).await?
488            },
489            None => quote! { #ident: value.#ident },
490        }
491    });
492
493    let from_stored_assignments = named_fields.iter().map(|field| {
494        let ident = field.ident.clone().expect("named field");
495        match foreign_field_kind(&ident, &foreign_fields) {
496            Some(ForeignFieldKind { original_ty, .. }) => quote! {
497                #ident: <#original_ty as ::appdb::ForeignShape>::hydrate_foreign_shape(stored.#ident).await?
498            },
499            None => quote! { #ident: stored.#ident },
500        }
501    });
502
503    let decode_foreign_fields = foreign_fields.iter().map(|field| {
504        let ident = field.ident.to_string();
505        quote! {
506            if let ::std::option::Option::Some(value) = map.get_mut(#ident) {
507                ::appdb::decode_stored_record_links(value);
508            }
509        }
510    });
511
512    let relation_methods_impl = if relate_fields.is_empty() {
513        quote! {}
514    } else {
515        let strip_relation_fields = relate_fields.iter().map(|field| {
516            let ident = field.ident.to_string();
517            quote! {
518                map.remove(#ident);
519            }
520        });
521
522        let inject_relation_values_from_model = relate_fields.iter().map(|field| {
523            let ident = &field.ident;
524            let name = ident.to_string();
525            quote! {
526                map.insert(#name.to_owned(), ::serde_json::to_value(&self.#ident)?);
527            }
528        });
529
530        let prepare_relation_writes = relate_fields.iter().map(|field| {
531            let ident = &field.ident;
532            let relation_name = &field.relation_name;
533            let field_ty = &field.field_ty;
534            let write_direction = field.direction.write_direction_tokens();
535            let write_edges = field.direction.write_edges_tokens();
536            quote! {
537                {
538                    let ids = <#field_ty as ::appdb::RelateShape>::persist_relate_shape(self.#ident.clone()).await?;
539                    writes.push(::appdb::RelationWrite {
540                        relation: #relation_name,
541                        record: record.clone(),
542                        direction: #write_direction,
543                        edges: #write_edges,
544                    });
545                }
546            }
547        });
548
549        let inject_relation_values_from_db = relate_fields.iter().map(|field| {
550            let relation_name = &field.relation_name;
551            let field_ty = &field.field_ty;
552            let ident = field.ident.to_string();
553            let load_relation_ids = field.direction.load_edges_tokens(relation_name);
554            quote! {
555                {
556                    let value = <#field_ty as ::appdb::RelateShape>::hydrate_relate_shape(#load_relation_ids).await?;
557                    map.insert(#ident.to_owned(), ::serde_json::to_value(value)?);
558                }
559            }
560        });
561
562        quote! {
563            fn has_relation_fields() -> bool {
564                true
565            }
566
567            fn relation_field_names() -> &'static [&'static str] {
568                &[ #( #relate_field_literals ),* ]
569            }
570
571            fn strip_relation_fields(row: &mut ::serde_json::Value) {
572                if let ::serde_json::Value::Object(map) = row {
573                    #( #strip_relation_fields )*
574                }
575            }
576
577            fn inject_relation_values_from_model(
578                &self,
579                row: &mut ::serde_json::Value,
580            ) -> ::anyhow::Result<()> {
581                if let ::serde_json::Value::Object(map) = row {
582                    #( #inject_relation_values_from_model )*
583                }
584                Ok(())
585            }
586
587            fn prepare_relation_writes(
588                &self,
589                record: ::surrealdb::types::RecordId,
590            ) -> impl ::std::future::Future<Output = ::anyhow::Result<::std::vec::Vec<::appdb::RelationWrite>>> + Send {
591                async move {
592                    let mut writes = ::std::vec::Vec::new();
593                    #( #prepare_relation_writes )*
594                    Ok(writes)
595                }
596            }
597
598            fn inject_relation_values_from_db(
599                record: ::surrealdb::types::RecordId,
600                row: &mut ::serde_json::Value,
601            ) -> impl ::std::future::Future<Output = ::anyhow::Result<()>> + Send {
602                async move {
603                    if let ::serde_json::Value::Object(map) = row {
604                        #( #inject_relation_values_from_db )*
605                    }
606                    Ok(())
607                }
608            }
609        }
610    };
611
612    let foreign_model_impl = if foreign_fields.is_empty() {
613        quote! {
614            impl ::appdb::ForeignModel for #struct_ident {
615                async fn persist_foreign(value: Self) -> ::anyhow::Result<Self::Stored> {
616                    <Self as ::appdb::StoredModel>::into_stored(value)
617                }
618
619                async fn hydrate_foreign(stored: Self::Stored) -> ::anyhow::Result<Self> {
620                    <Self as ::appdb::StoredModel>::from_stored(stored)
621                }
622
623                fn decode_stored_row(
624                    row: ::surrealdb::types::Value,
625                ) -> ::anyhow::Result<Self::Stored>
626                where
627                    Self::Stored: ::serde::de::DeserializeOwned,
628                {
629                    Ok(::serde_json::from_value(row.into_json_value())?)
630                }
631
632                #relation_methods_impl
633            }
634        }
635    } else {
636        let stored_struct_ident = format_ident!("AppdbStored{}", struct_ident);
637        quote! {
638            #[derive(
639                Debug,
640                Clone,
641                ::serde::Serialize,
642                ::serde::Deserialize,
643                ::surrealdb::types::SurrealValue,
644            )]
645            #vis struct #stored_struct_ident {
646                #( #stored_fields, )*
647            }
648
649            impl ::appdb::StoredModel for #struct_ident {
650                type Stored = #stored_struct_ident;
651
652                fn into_stored(self) -> ::anyhow::Result<Self::Stored> {
653                    unreachable!("foreign fields require async persist_foreign")
654                }
655
656                fn from_stored(_stored: Self::Stored) -> ::anyhow::Result<Self> {
657                    unreachable!("foreign fields require async hydrate_foreign")
658                }
659            }
660
661            impl ::appdb::ForeignModel for #struct_ident {
662                async fn persist_foreign(value: Self) -> ::anyhow::Result<Self::Stored> {
663                    let value = value;
664                    Ok(#stored_struct_ident {
665                        #( #into_stored_assignments, )*
666                    })
667                }
668
669                async fn hydrate_foreign(stored: Self::Stored) -> ::anyhow::Result<Self> {
670                    Ok(Self {
671                        #( #from_stored_assignments, )*
672                    })
673                }
674
675                fn has_foreign_fields() -> bool {
676                    true
677                }
678
679                fn foreign_field_names() -> &'static [&'static str] {
680                    &[ #( #foreign_field_literals ),* ]
681                }
682
683                fn decode_stored_row(
684                    row: ::surrealdb::types::Value,
685                ) -> ::anyhow::Result<Self::Stored>
686                where
687                    Self::Stored: ::serde::de::DeserializeOwned,
688                {
689                    let mut row = row.into_json_value();
690                    if let ::serde_json::Value::Object(map) = &mut row {
691                        #( #decode_foreign_fields )*
692                    }
693                    Ok(::serde_json::from_value(row)?)
694                }
695
696                #relation_methods_impl
697            }
698        }
699    };
700
701    let store_marker_ident = format_ident!("AppdbStoreMarker{}", struct_ident);
702
703    Ok(quote! {
704        #[doc(hidden)]
705        #vis struct #store_marker_ident;
706
707        impl ::appdb::model::meta::ModelMeta for #struct_ident {
708            fn storage_table() -> &'static str {
709                #resolved_table_name_expr
710            }
711
712            fn table_name() -> &'static str {
713                static TABLE_NAME: ::std::sync::OnceLock<&'static str> = ::std::sync::OnceLock::new();
714                TABLE_NAME.get_or_init(|| {
715                    let table = #resolved_table_name_expr;
716                    ::appdb::model::meta::register_table(stringify!(#struct_ident), table)
717                })
718            }
719        }
720
721        impl ::appdb::model::meta::StoreModelMarker for #struct_ident {}
722        impl ::appdb::model::meta::StoreModelMarker for #store_marker_ident {}
723        #pagination_meta_impl
724
725        impl ::appdb::model::meta::UniqueLookupMeta for #struct_ident {
726            fn lookup_fields() -> &'static [&'static str] {
727                &[ #( #lookup_field_literals ),* ]
728            }
729
730            fn foreign_fields() -> &'static [&'static str] {
731                &[ #( #foreign_field_literals ),* ]
732            }
733
734            fn resolve_lookup_field_value(
735                &self,
736                field: &str,
737            ) -> impl ::std::future::Future<
738                Output = ::anyhow::Result<::std::option::Option<::surrealdb::types::Value>>
739            > {
740                async move {
741                    match field {
742                        #( #resolve_lookup_field_value_arms )*
743                        _ => Ok(::std::option::Option::None),
744                    }
745                }
746            }
747        }
748        #stored_model_impl
749        #foreign_model_impl
750
751        #auto_has_id_impl
752        #resolve_record_id_impl
753
754        #( #unique_schema_impls )*
755        #( #pagin_schema_impl )*
756
757        impl ::appdb::repository::Crud for #struct_ident {}
758
759        impl #struct_ident {
760            /// Saves one value through the recommended Store CRUD surface.
761            ///
762            /// Prefer these generated model methods in application code. Lower-level
763            /// `appdb::repository::Repo` helpers exist for library internals and
764            /// advanced integration seams, not as the primary public path.
765            pub async fn save(self) -> ::anyhow::Result<Self> {
766                <Self as ::appdb::repository::Crud>::save(self).await
767            }
768
769            /// Saves many values through the recommended Store CRUD surface.
770            pub async fn save_many(data: ::std::vec::Vec<Self>) -> ::anyhow::Result<::std::vec::Vec<Self>> {
771                <Self as ::appdb::repository::Crud>::save_many(data).await
772            }
773
774            pub async fn get<T>(id: T) -> ::anyhow::Result<Self>
775            where
776                ::surrealdb::types::RecordIdKey: From<T>,
777                T: Send,
778            {
779                ::appdb::repository::Repo::<Self>::get(id).await
780            }
781
782            pub async fn list() -> ::anyhow::Result<::std::vec::Vec<Self>> {
783                ::appdb::repository::Repo::<Self>::list().await
784            }
785
786            pub async fn list_limit(count: i64) -> ::anyhow::Result<::std::vec::Vec<Self>> {
787                ::appdb::repository::Repo::<Self>::list_limit(count).await
788            }
789            #pagination_methods_impl
790
791            pub async fn relate_by_name<Target>(&self, target: &Target, relation: &str) -> ::anyhow::Result<()>
792            where
793                Target: ::appdb::model::meta::ResolveRecordId + Send + Sync,
794            {
795                <Self as ::appdb::graph::GraphCrud>::relate_by_name(self, target, relation).await
796            }
797
798            pub async fn back_relate_by_name<Target>(&self, target: &Target, relation: &str) -> ::anyhow::Result<()>
799            where
800                Target: ::appdb::model::meta::ResolveRecordId + Send + Sync,
801            {
802                <Self as ::appdb::graph::GraphCrud>::back_relate_by_name(self, target, relation).await
803            }
804
805            pub async fn unrelate_by_name<Target>(&self, target: &Target, relation: &str) -> ::anyhow::Result<()>
806            where
807                Target: ::appdb::model::meta::ResolveRecordId + Send + Sync,
808            {
809                <Self as ::appdb::graph::GraphCrud>::unrelate_by_name(self, target, relation).await
810            }
811
812            pub async fn outgoing_ids(&self, relation: &str) -> ::anyhow::Result<::std::vec::Vec<::surrealdb::types::RecordId>> {
813                <Self as ::appdb::repository::Crud>::outgoing_ids(self, relation).await
814            }
815
816            pub async fn outgoing<Target>(&self, relation: &str) -> ::anyhow::Result<::std::vec::Vec<Target>>
817            where
818                Target: ::appdb::model::meta::ModelMeta + ::appdb::StoredModel + ::appdb::ForeignModel,
819                <Target as ::appdb::StoredModel>::Stored: ::serde::de::DeserializeOwned,
820            {
821                <Self as ::appdb::repository::Crud>::outgoing::<Target>(self, relation).await
822            }
823
824            pub async fn outgoing_count(&self, relation: &str) -> ::anyhow::Result<i64> {
825                <Self as ::appdb::repository::Crud>::outgoing_count(self, relation).await
826            }
827
828            pub async fn outgoing_count_as<Target>(&self, relation: &str) -> ::anyhow::Result<i64>
829            where
830                Target: ::appdb::model::meta::ModelMeta + ::appdb::StoredModel + ::appdb::ForeignModel,
831            {
832                <Self as ::appdb::repository::Crud>::outgoing_count_as::<Target>(self, relation).await
833            }
834
835            pub async fn incoming_ids(&self, relation: &str) -> ::anyhow::Result<::std::vec::Vec<::surrealdb::types::RecordId>> {
836                <Self as ::appdb::repository::Crud>::incoming_ids(self, relation).await
837            }
838
839            pub async fn incoming<Target>(&self, relation: &str) -> ::anyhow::Result<::std::vec::Vec<Target>>
840            where
841                Target: ::appdb::model::meta::ModelMeta + ::appdb::StoredModel + ::appdb::ForeignModel,
842                <Target as ::appdb::StoredModel>::Stored: ::serde::de::DeserializeOwned,
843            {
844                <Self as ::appdb::repository::Crud>::incoming::<Target>(self, relation).await
845            }
846
847            pub async fn incoming_count(&self, relation: &str) -> ::anyhow::Result<i64> {
848                <Self as ::appdb::repository::Crud>::incoming_count(self, relation).await
849            }
850
851            pub async fn incoming_count_as<Target>(&self, relation: &str) -> ::anyhow::Result<i64>
852            where
853                Target: ::appdb::model::meta::ModelMeta + ::appdb::StoredModel + ::appdb::ForeignModel,
854            {
855                <Self as ::appdb::repository::Crud>::incoming_count_as::<Target>(self, relation).await
856            }
857
858            pub async fn delete_all() -> ::anyhow::Result<()> {
859                ::appdb::repository::Repo::<Self>::delete_all().await
860            }
861
862            pub async fn find_one_id(
863                k: &str,
864                v: &str,
865            ) -> ::anyhow::Result<::surrealdb::types::RecordId> {
866                ::appdb::repository::Repo::<Self>::find_one_id(k, v).await
867            }
868
869            pub async fn list_record_ids() -> ::anyhow::Result<::std::vec::Vec<::surrealdb::types::RecordId>> {
870                ::appdb::repository::Repo::<Self>::list_record_ids().await
871            }
872
873            pub async fn create_at(
874                id: ::surrealdb::types::RecordId,
875                data: Self,
876            ) -> ::anyhow::Result<Self> {
877                ::appdb::repository::Repo::<Self>::create_at(id, data).await
878            }
879
880            pub async fn upsert_at(
881                id: ::surrealdb::types::RecordId,
882                data: Self,
883            ) -> ::anyhow::Result<Self> {
884                ::appdb::repository::Repo::<Self>::upsert_at(id, data).await
885            }
886
887            pub async fn update_at(
888                self,
889                id: ::surrealdb::types::RecordId,
890            ) -> ::anyhow::Result<Self> {
891                ::appdb::repository::Repo::<Self>::update_at(id, self).await
892            }
893
894
895            pub async fn delete<T>(id: T) -> ::anyhow::Result<()>
896            where
897                ::surrealdb::types::RecordIdKey: From<T>,
898                T: Send,
899            {
900                ::appdb::repository::Repo::<Self>::delete(id).await
901            }
902        }
903    })
904}
905
906fn derive_bridge_impl(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
907    let enum_ident = input.ident;
908
909    let variants = match input.data {
910        Data::Enum(data) => data.variants,
911        _ => {
912            return Err(Error::new_spanned(
913                enum_ident,
914                "Bridge can only be derived for enums",
915            ));
916        }
917    };
918
919    let payloads = variants
920        .iter()
921        .map(parse_bridge_variant)
922        .collect::<syn::Result<Vec<_>>>()?;
923
924    let from_impls = payloads.iter().map(|variant| {
925        let variant_ident = &variant.variant_ident;
926        let payload_ty = &variant.payload_ty;
927
928        quote! {
929            impl ::std::convert::From<#payload_ty> for #enum_ident {
930                fn from(value: #payload_ty) -> Self {
931                    Self::#variant_ident(value)
932                }
933            }
934        }
935    });
936
937    let persist_match_arms = payloads.iter().map(|variant| {
938        let variant_ident = &variant.variant_ident;
939
940        quote! {
941            Self::#variant_ident(value) => <_ as ::appdb::Bridge>::persist_foreign(value).await,
942        }
943    });
944
945    let hydrate_match_arms = payloads.iter().map(|variant| {
946        let variant_ident = &variant.variant_ident;
947        let payload_ty = &variant.payload_ty;
948
949        quote! {
950            table if table == <#payload_ty as ::appdb::model::meta::ModelMeta>::storage_table() => {
951                ::std::result::Result::Ok(Self::#variant_ident(
952                    <#payload_ty as ::appdb::Bridge>::hydrate_foreign(id).await?,
953                ))
954            }
955        }
956    });
957
958    let lookup_match_arms = payloads.iter().map(|variant| {
959        let variant_ident = &variant.variant_ident;
960        let payload_ty = &variant.payload_ty;
961
962        quote! {
963            Self::#variant_ident(value) => {
964                <#payload_ty as ::appdb::ForeignLookupShape>::resolve_foreign_lookup_shape(value).await
965            }
966        }
967    });
968
969    Ok(quote! {
970        #( #from_impls )*
971
972        #[::async_trait::async_trait]
973        impl ::appdb::Bridge for #enum_ident {
974            async fn persist_foreign(self) -> ::anyhow::Result<::surrealdb::types::RecordId> {
975                match self {
976                    #( #persist_match_arms )*
977                }
978            }
979
980            async fn hydrate_foreign(
981                id: ::surrealdb::types::RecordId,
982            ) -> ::anyhow::Result<Self> {
983                match id.table.to_string().as_str() {
984                    #( #hydrate_match_arms, )*
985                    table => ::anyhow::bail!(
986                        "unsupported foreign table `{table}` for enum dispatcher `{}`",
987                        ::std::stringify!(#enum_ident)
988                    ),
989                }
990            }
991        }
992
993        impl ::appdb::ForeignLookupShape for #enum_ident {
994            type LookupStored = ::surrealdb::types::RecordId;
995
996            fn resolve_foreign_lookup_shape(
997                &self,
998            ) -> impl ::std::future::Future<Output = ::anyhow::Result<Self::LookupStored>> {
999                async move {
1000                    match self {
1001                        #( #lookup_match_arms, )*
1002                    }
1003                }
1004            }
1005        }
1006    })
1007}
1008
1009#[derive(Clone)]
1010struct BridgeVariant {
1011    variant_ident: syn::Ident,
1012    payload_ty: Type,
1013}
1014
1015fn parse_bridge_variant(variant: &syn::Variant) -> syn::Result<BridgeVariant> {
1016    let payload_ty = match &variant.fields {
1017        Fields::Unnamed(fields) if fields.unnamed.len() == 1 => {
1018            fields.unnamed.first().expect("single field").ty.clone()
1019        }
1020        Fields::Unnamed(_) => {
1021            return Err(Error::new_spanned(
1022                &variant.ident,
1023                "Bridge variants must be single-field tuple variants",
1024            ));
1025        }
1026        Fields::Unit => {
1027            return Err(Error::new_spanned(
1028                &variant.ident,
1029                "Bridge does not support unit variants",
1030            ));
1031        }
1032        Fields::Named(_) => {
1033            return Err(Error::new_spanned(
1034                &variant.ident,
1035                "Bridge does not support struct variants",
1036            ));
1037        }
1038    };
1039
1040    let payload_path = match &payload_ty {
1041        Type::Path(path) => path,
1042        _ => {
1043            return Err(Error::new_spanned(
1044                &payload_ty,
1045                "Bridge payload must implement appdb::Bridge",
1046            ));
1047        }
1048    };
1049
1050    let segment = payload_path.path.segments.last().ok_or_else(|| {
1051        Error::new_spanned(&payload_ty, "Bridge payload must implement appdb::Bridge")
1052    })?;
1053
1054    if !matches!(segment.arguments, PathArguments::None) {
1055        return Err(Error::new_spanned(
1056            &payload_ty,
1057            "Bridge payload must implement appdb::Bridge",
1058        ));
1059    }
1060
1061    Ok(BridgeVariant {
1062        variant_ident: variant.ident.clone(),
1063        payload_ty,
1064    })
1065}
1066
1067fn derive_relation_impl(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
1068    let struct_ident = input.ident;
1069    let relation_name = relation_name_override(&input.attrs)?
1070        .unwrap_or_else(|| to_snake_case(&struct_ident.to_string()));
1071
1072    match input.data {
1073        Data::Struct(data) => match data.fields {
1074            Fields::Unit | Fields::Named(_) => {}
1075            _ => {
1076                return Err(Error::new_spanned(
1077                    struct_ident,
1078                    "Relation can only be derived for unit structs or structs with named fields",
1079                ));
1080            }
1081        },
1082        _ => {
1083            return Err(Error::new_spanned(
1084                struct_ident,
1085                "Relation can only be derived for structs",
1086            ));
1087        }
1088    }
1089
1090    Ok(quote! {
1091        impl ::appdb::model::relation::RelationMeta for #struct_ident {
1092            fn relation_name() -> &'static str {
1093                static REL_NAME: ::std::sync::OnceLock<&'static str> = ::std::sync::OnceLock::new();
1094                REL_NAME.get_or_init(|| ::appdb::model::relation::register_relation(#relation_name))
1095            }
1096        }
1097
1098        impl #struct_ident {
1099            pub async fn relate<A, B>(a: &A, b: &B) -> ::anyhow::Result<()>
1100            where
1101                A: ::appdb::model::meta::ResolveRecordId + Send + Sync,
1102                B: ::appdb::model::meta::ResolveRecordId + Send + Sync,
1103            {
1104                ::appdb::graph::relate_at(a.resolve_record_id().await?, b.resolve_record_id().await?, <Self as ::appdb::model::relation::RelationMeta>::relation_name()).await
1105            }
1106
1107            pub async fn back_relate<A, B>(a: &A, b: &B) -> ::anyhow::Result<()>
1108            where
1109                A: ::appdb::model::meta::ResolveRecordId + Send + Sync,
1110                B: ::appdb::model::meta::ResolveRecordId + Send + Sync,
1111            {
1112                ::appdb::graph::back_relate_at(a.resolve_record_id().await?, b.resolve_record_id().await?, <Self as ::appdb::model::relation::RelationMeta>::relation_name()).await
1113            }
1114
1115            pub async fn unrelate<A, B>(a: &A, b: &B) -> ::anyhow::Result<()>
1116            where
1117                A: ::appdb::model::meta::ResolveRecordId + Send + Sync,
1118                B: ::appdb::model::meta::ResolveRecordId + Send + Sync,
1119            {
1120                ::appdb::graph::unrelate_at(a.resolve_record_id().await?, b.resolve_record_id().await?, <Self as ::appdb::model::relation::RelationMeta>::relation_name()).await
1121            }
1122
1123            pub async fn out_ids<A>(a: &A, out_table: &str) -> ::anyhow::Result<::std::vec::Vec<::surrealdb::types::RecordId>>
1124            where
1125                A: ::appdb::model::meta::ResolveRecordId + Send + Sync,
1126            {
1127                ::appdb::graph::out_ids(a.resolve_record_id().await?, <Self as ::appdb::model::relation::RelationMeta>::relation_name(), out_table).await
1128            }
1129
1130            pub async fn in_ids<B>(b: &B, in_table: &str) -> ::anyhow::Result<::std::vec::Vec<::surrealdb::types::RecordId>>
1131            where
1132                B: ::appdb::model::meta::ResolveRecordId + Send + Sync,
1133            {
1134                ::appdb::graph::in_ids(b.resolve_record_id().await?, <Self as ::appdb::model::relation::RelationMeta>::relation_name(), in_table).await
1135            }
1136        }
1137    })
1138}
1139
1140fn derive_sensitive_impl(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
1141    let struct_ident = input.ident;
1142    let encrypted_ident = format_ident!("Encrypted{}", struct_ident);
1143    let vis = input.vis;
1144    let type_crypto_config = type_crypto_config(&input.attrs)?;
1145    let named_fields = match input.data {
1146        Data::Struct(data) => match data.fields {
1147            Fields::Named(fields) => fields.named,
1148            _ => {
1149                return Err(Error::new_spanned(
1150                    struct_ident,
1151                    "Sensitive can only be derived for structs with named fields",
1152                ));
1153            }
1154        },
1155        _ => {
1156            return Err(Error::new_spanned(
1157                struct_ident,
1158                "Sensitive can only be derived for structs",
1159            ));
1160        }
1161    };
1162
1163    let mut secure_field_count = 0usize;
1164    let mut encrypted_fields = Vec::new();
1165    let mut encrypt_assignments = Vec::new();
1166    let mut decrypt_assignments = Vec::new();
1167    let mut runtime_encrypt_assignments = Vec::new();
1168    let mut runtime_decrypt_assignments = Vec::new();
1169    let mut field_tag_structs = Vec::new();
1170    let mut secure_field_meta_entries = Vec::new();
1171
1172    for field in named_fields.iter() {
1173        let ident = field.ident.clone().expect("named field");
1174        let field_vis = field.vis.clone();
1175        let secure = has_secure_attr(&field.attrs);
1176        let field_crypto_config = field_crypto_config(&field.attrs)?;
1177
1178        if !secure && field_crypto_config.is_present() {
1179            return Err(Error::new_spanned(
1180                ident,
1181                "#[crypto(...)] on a field requires #[secure] on the same field",
1182            ));
1183        }
1184
1185        if secure {
1186            secure_field_count += 1;
1187            let secure_kind = secure_kind(field)?;
1188            let encrypted_ty = secure_kind.encrypted_type();
1189            let field_tag_ident = format_ident!(
1190                "AppdbSensitiveFieldTag{}{}",
1191                struct_ident,
1192                to_pascal_case(&ident.to_string())
1193            );
1194            let field_tag_literal = ident.to_string();
1195            let effective_account = field_crypto_config
1196                .field_account
1197                .clone()
1198                .or_else(|| type_crypto_config.account.clone());
1199            let service_override = type_crypto_config.service.clone();
1200            let account_literal = effective_account
1201                .as_ref()
1202                .map(|value| quote! { ::std::option::Option::Some(#value) })
1203                .unwrap_or_else(|| quote! { ::std::option::Option::None });
1204            let service_literal = service_override
1205                .as_ref()
1206                .map(|value| quote! { ::std::option::Option::Some(#value) })
1207                .unwrap_or_else(|| quote! { ::std::option::Option::None });
1208            let encrypt_expr = secure_kind.encrypt_with_context_expr(&ident);
1209            let decrypt_expr = secure_kind.decrypt_with_context_expr(&ident);
1210            let runtime_encrypt_expr =
1211                secure_kind.encrypt_with_runtime_expr(&ident, &field_tag_ident);
1212            let runtime_decrypt_expr =
1213                secure_kind.decrypt_with_runtime_expr(&ident, &field_tag_ident);
1214            encrypted_fields.push(quote! { #field_vis #ident: #encrypted_ty });
1215            encrypt_assignments.push(quote! { #ident: #encrypt_expr });
1216            decrypt_assignments.push(quote! { #ident: #decrypt_expr });
1217            runtime_encrypt_assignments.push(quote! { #ident: #runtime_encrypt_expr });
1218            runtime_decrypt_assignments.push(quote! { #ident: #runtime_decrypt_expr });
1219            secure_field_meta_entries.push(quote! {
1220                ::appdb::crypto::SensitiveFieldMetadata {
1221                    model_tag: ::std::concat!(::std::module_path!(), "::", ::std::stringify!(#struct_ident)),
1222                    field_tag: #field_tag_literal,
1223                    service: #service_literal,
1224                    account: #account_literal,
1225                    secure_fields: &[],
1226                }
1227            });
1228            field_tag_structs.push(quote! {
1229                #[doc(hidden)]
1230                #vis struct #field_tag_ident;
1231
1232                impl ::appdb::crypto::SensitiveFieldTag for #field_tag_ident {
1233                    fn model_tag() -> &'static str {
1234                        <#struct_ident as ::appdb::crypto::SensitiveModelTag>::model_tag()
1235                    }
1236
1237                    fn field_tag() -> &'static str {
1238                        #field_tag_literal
1239                    }
1240
1241                    fn crypto_metadata() -> &'static ::appdb::crypto::SensitiveFieldMetadata {
1242                        static FIELD_META: ::std::sync::OnceLock<::appdb::crypto::SensitiveFieldMetadata> = ::std::sync::OnceLock::new();
1243                        FIELD_META.get_or_init(|| ::appdb::crypto::SensitiveFieldMetadata {
1244                            model_tag: <#struct_ident as ::appdb::crypto::SensitiveModelTag>::model_tag(),
1245                            field_tag: #field_tag_literal,
1246                            service: #service_literal,
1247                            account: #account_literal,
1248                            secure_fields: &#struct_ident::SECURE_FIELDS,
1249                        })
1250                    }
1251                }
1252            });
1253        } else {
1254            let ty = field.ty.clone();
1255            encrypted_fields.push(quote! { #field_vis #ident: #ty });
1256            encrypt_assignments.push(quote! { #ident: self.#ident.clone() });
1257            decrypt_assignments.push(quote! { #ident: encrypted.#ident.clone() });
1258            runtime_encrypt_assignments.push(quote! { #ident: self.#ident.clone() });
1259            runtime_decrypt_assignments.push(quote! { #ident: encrypted.#ident.clone() });
1260        }
1261    }
1262
1263    if secure_field_count == 0 {
1264        return Err(Error::new_spanned(
1265            struct_ident,
1266            "Sensitive requires at least one #[secure] field",
1267        ));
1268    }
1269
1270    Ok(quote! {
1271        #[derive(
1272            Debug,
1273            Clone,
1274            ::serde::Serialize,
1275            ::serde::Deserialize,
1276            ::surrealdb::types::SurrealValue,
1277        )]
1278        #vis struct #encrypted_ident {
1279            #( #encrypted_fields, )*
1280        }
1281
1282        impl ::appdb::crypto::SensitiveModelTag for #struct_ident {
1283            fn model_tag() -> &'static str {
1284                ::std::concat!(::std::module_path!(), "::", ::std::stringify!(#struct_ident))
1285            }
1286        }
1287
1288        #( #field_tag_structs )*
1289
1290        impl ::appdb::Sensitive for #struct_ident {
1291            type Encrypted = #encrypted_ident;
1292
1293            fn encrypt(
1294                &self,
1295                context: &::appdb::crypto::CryptoContext,
1296            ) -> ::std::result::Result<Self::Encrypted, ::appdb::crypto::CryptoError> {
1297                ::std::result::Result::Ok(#encrypted_ident {
1298                    #( #encrypt_assignments, )*
1299                })
1300            }
1301
1302            fn decrypt(
1303                encrypted: &Self::Encrypted,
1304                context: &::appdb::crypto::CryptoContext,
1305            ) -> ::std::result::Result<Self, ::appdb::crypto::CryptoError> {
1306                ::std::result::Result::Ok(Self {
1307                    #( #decrypt_assignments, )*
1308                })
1309            }
1310
1311            fn encrypt_with_runtime_resolver(
1312                &self,
1313            ) -> ::std::result::Result<Self::Encrypted, ::appdb::crypto::CryptoError> {
1314                ::std::result::Result::Ok(#encrypted_ident {
1315                    #( #runtime_encrypt_assignments, )*
1316                })
1317            }
1318
1319            fn decrypt_with_runtime_resolver(
1320                encrypted: &Self::Encrypted,
1321            ) -> ::std::result::Result<Self, ::appdb::crypto::CryptoError> {
1322                ::std::result::Result::Ok(Self {
1323                    #( #runtime_decrypt_assignments, )*
1324                })
1325            }
1326
1327            fn secure_fields() -> &'static [::appdb::crypto::SensitiveFieldMetadata] {
1328                &Self::SECURE_FIELDS
1329            }
1330        }
1331
1332        impl #struct_ident {
1333            pub const SECURE_FIELDS: [::appdb::crypto::SensitiveFieldMetadata; #secure_field_count] = [
1334                #( #secure_field_meta_entries, )*
1335            ];
1336
1337            pub fn encrypt(
1338                &self,
1339                context: &::appdb::crypto::CryptoContext,
1340            ) -> ::std::result::Result<#encrypted_ident, ::appdb::crypto::CryptoError> {
1341                <Self as ::appdb::Sensitive>::encrypt(self, context)
1342            }
1343        }
1344
1345        impl #encrypted_ident {
1346            pub fn decrypt(
1347                &self,
1348                context: &::appdb::crypto::CryptoContext,
1349            ) -> ::std::result::Result<#struct_ident, ::appdb::crypto::CryptoError> {
1350                <#struct_ident as ::appdb::Sensitive>::decrypt(self, context)
1351            }
1352        }
1353    })
1354}
1355
1356fn has_secure_attr(attrs: &[Attribute]) -> bool {
1357    attrs.iter().any(|attr| attr.path().is_ident("secure"))
1358}
1359
1360fn has_unique_attr(attrs: &[Attribute]) -> bool {
1361    attrs.iter().any(|attr| attr.path().is_ident("unique"))
1362}
1363
1364#[derive(Default, Clone)]
1365struct TypeCryptoConfig {
1366    service: Option<String>,
1367    account: Option<String>,
1368}
1369
1370#[derive(Default, Clone)]
1371struct FieldCryptoConfig {
1372    field_account: Option<String>,
1373}
1374
1375impl FieldCryptoConfig {
1376    fn is_present(&self) -> bool {
1377        self.field_account.is_some()
1378    }
1379}
1380
1381fn type_crypto_config(attrs: &[Attribute]) -> syn::Result<TypeCryptoConfig> {
1382    let mut config = TypeCryptoConfig::default();
1383    let mut seen = HashSet::new();
1384
1385    for attr in attrs {
1386        if !attr.path().is_ident("crypto") {
1387            continue;
1388        }
1389
1390        attr.parse_nested_meta(|meta| {
1391            let key = meta
1392                .path
1393                .get_ident()
1394                .cloned()
1395                .ok_or_else(|| meta.error("unsupported crypto attribute"))?;
1396
1397            if !seen.insert(key.to_string()) {
1398                return Err(meta.error("duplicate crypto attribute key"));
1399            }
1400
1401            let value = meta.value()?;
1402            let literal: syn::LitStr = value.parse()?;
1403            match key.to_string().as_str() {
1404                "service" => config.service = Some(literal.value()),
1405                "account" => config.account = Some(literal.value()),
1406                _ => {
1407                    return Err(
1408                        meta.error("unsupported crypto attribute; expected `service` or `account`")
1409                    );
1410                }
1411            }
1412            Ok(())
1413        })?;
1414    }
1415
1416    Ok(config)
1417}
1418
1419fn field_crypto_config(attrs: &[Attribute]) -> syn::Result<FieldCryptoConfig> {
1420    let mut config = FieldCryptoConfig::default();
1421    let mut seen = HashSet::new();
1422
1423    for attr in attrs {
1424        if attr.path().is_ident("crypto") {
1425            attr.parse_nested_meta(|meta| {
1426                let key = meta
1427                    .path
1428                    .get_ident()
1429                    .cloned()
1430                    .ok_or_else(|| meta.error("unsupported crypto attribute"))?;
1431
1432                if !seen.insert(key.to_string()) {
1433                    return Err(meta.error("duplicate crypto attribute key"));
1434                }
1435
1436                let value = meta.value()?;
1437                let literal: syn::LitStr = value.parse()?;
1438                match key.to_string().as_str() {
1439                    "field_account" => config.field_account = Some(literal.value()),
1440                    _ => {
1441                        return Err(meta.error(
1442                            "unsupported field crypto attribute; expected `field_account`",
1443                        ));
1444                    }
1445                }
1446                Ok(())
1447            })?;
1448        } else if attr.path().is_ident("secure") && matches!(attr.meta, Meta::List(_)) {
1449            return Err(Error::new_spanned(
1450                attr,
1451                "#[secure] does not accept arguments; use #[crypto(field_account = \"...\")] on the field",
1452            ));
1453        }
1454    }
1455
1456    Ok(config)
1457}
1458
1459fn table_alias_target(attrs: &[Attribute]) -> syn::Result<Option<Type>> {
1460    let mut target = None;
1461
1462    for attr in attrs {
1463        if !attr.path().is_ident("table_as") {
1464            continue;
1465        }
1466
1467        if target.is_some() {
1468            return Err(Error::new_spanned(
1469                attr,
1470                "duplicate #[table_as(...)] attribute is not supported",
1471            ));
1472        }
1473
1474        let parsed: Type = attr.parse_args().map_err(|_| {
1475            Error::new_spanned(attr, "#[table_as(...)] requires exactly one target type")
1476        })?;
1477
1478        match parsed {
1479            Type::Path(TypePath { ref path, .. }) if !path.segments.is_empty() => {
1480                target = Some(parsed);
1481            }
1482            _ => {
1483                return Err(Error::new_spanned(
1484                    parsed,
1485                    "#[table_as(...)] target must be a type path",
1486                ));
1487            }
1488        }
1489    }
1490
1491    Ok(target)
1492}
1493
1494fn resolved_schema_table_name(struct_ident: &syn::Ident, table_alias: Option<&Type>) -> String {
1495    match table_alias {
1496        Some(Type::Path(type_path)) => type_path
1497            .path
1498            .segments
1499            .last()
1500            .map(|segment| to_snake_case(&segment.ident.to_string()))
1501            .unwrap_or_else(|| to_snake_case(&struct_ident.to_string())),
1502        Some(_) => to_snake_case(&struct_ident.to_string()),
1503        None => to_snake_case(&struct_ident.to_string()),
1504    }
1505}
1506
1507fn field_foreign_attr(field: &Field) -> syn::Result<Option<&Attribute>> {
1508    let mut foreign_attr = None;
1509
1510    for attr in &field.attrs {
1511        if !attr.path().is_ident("foreign") {
1512            continue;
1513        }
1514
1515        if foreign_attr.is_some() {
1516            return Err(Error::new_spanned(
1517                attr,
1518                "duplicate nested-ref attribute is not supported",
1519            ));
1520        }
1521
1522        foreign_attr = Some(attr);
1523    }
1524
1525    Ok(foreign_attr)
1526}
1527
1528fn field_relation_attr(field: &Field) -> syn::Result<Option<&Attribute>> {
1529    let mut relate_attr = None;
1530
1531    for attr in &field.attrs {
1532        if !attr.path().is_ident("relate") && !attr.path().is_ident("back_relate") {
1533            continue;
1534        }
1535
1536        if let Some(previous) = relate_attr {
1537            return Err(Error::new_spanned(
1538                attr,
1539                relation_attr_conflict_message(previous, attr),
1540            ));
1541        }
1542
1543        relate_attr = Some(attr);
1544    }
1545
1546    Ok(relate_attr)
1547}
1548
1549fn field_pagin_attr(field: &Field) -> syn::Result<Option<&Attribute>> {
1550    let mut pagin_attr = None;
1551
1552    for attr in &field.attrs {
1553        if !attr.path().is_ident("pagin") {
1554            continue;
1555        }
1556
1557        if pagin_attr.is_some() {
1558            return Err(Error::new_spanned(
1559                attr,
1560                "duplicate #[pagin] attribute is not supported",
1561            ));
1562        }
1563
1564        pagin_attr = Some(attr);
1565    }
1566
1567    Ok(pagin_attr)
1568}
1569
1570fn validate_foreign_field(field: &Field, attr: &Attribute) -> syn::Result<Type> {
1571    if attr.path().is_ident("foreign") {
1572        return foreign_leaf_type(&field.ty)
1573            .ok_or_else(|| Error::new_spanned(&field.ty, BINDREF_ACCEPTED_SHAPES));
1574    }
1575
1576    Err(Error::new_spanned(attr, "unsupported foreign attribute"))
1577}
1578
1579const BINDREF_ACCEPTED_SHAPES: &str = "#[foreign] supports recursive Option<_> / Vec<_> shapes whose leaf type implements appdb::Bridge";
1580
1581const BINDREF_BRIDGE_STORE_ONLY: &str =
1582    "#[foreign] leaf types must derive Store or #[derive(Bridge)] dispatcher enums";
1583
1584const RELATE_ACCEPTED_SHAPES: &str = "relation-backed fields support Child / Option<Child> / Vec<Child> / Option<Vec<Child>> shapes whose leaf type implements appdb::Bridge";
1585
1586const PAGIN_ACCEPTED_SHAPES: &str = "#[pagin] supports direct scalar fields plus Id/RecordId; Option<_> and Vec<_> wrappers are not supported";
1587
1588#[derive(Clone, Copy)]
1589enum RelationFieldDirection {
1590    Outgoing,
1591    Incoming,
1592}
1593
1594impl RelationFieldDirection {
1595    fn attr_label(self) -> &'static str {
1596        match self {
1597            Self::Outgoing => "#[relate(...)]",
1598            Self::Incoming => "#[back_relate(...)]",
1599        }
1600    }
1601
1602    fn write_direction_tokens(self) -> proc_macro2::TokenStream {
1603        match self {
1604            Self::Outgoing => quote!(::appdb::RelationWriteDirection::Outgoing),
1605            Self::Incoming => quote!(::appdb::RelationWriteDirection::Incoming),
1606        }
1607    }
1608
1609    fn write_edges_tokens(self) -> proc_macro2::TokenStream {
1610        match self {
1611            Self::Outgoing => quote! {
1612                ids
1613                    .into_iter()
1614                    .enumerate()
1615                    .map(|(position, out)| ::appdb::graph::OrderedRelationEdge {
1616                        _in: ::std::option::Option::Some(record.clone()),
1617                        out,
1618                        position: position as i64,
1619                    })
1620                    .collect()
1621            },
1622            Self::Incoming => quote! {
1623                ids
1624                    .into_iter()
1625                    .enumerate()
1626                    .map(|(position, source)| ::appdb::graph::OrderedRelationEdge {
1627                        _in: ::std::option::Option::Some(source),
1628                        out: record.clone(),
1629                        position: position as i64,
1630                    })
1631                    .collect()
1632            },
1633        }
1634    }
1635
1636    fn load_edges_tokens(self, relation_name: &str) -> proc_macro2::TokenStream {
1637        match self {
1638            Self::Outgoing => quote! {
1639                ::appdb::graph::GraphRepo::out_edges(record.clone(), #relation_name)
1640                    .await?
1641                    .into_iter()
1642                    .map(|edge| edge.out)
1643                    .collect()
1644            },
1645            Self::Incoming => quote! {
1646                {
1647                    let mut ids = ::std::vec::Vec::new();
1648                    for edge in ::appdb::graph::GraphRepo::in_edges(record.clone(), #relation_name).await? {
1649                        let incoming = edge._in.ok_or_else(|| {
1650                            ::anyhow::anyhow!("back relate field received relation edge without `in` record id")
1651                        })?;
1652                        ids.push(incoming);
1653                    }
1654                    ids
1655                }
1656            },
1657        }
1658    }
1659}
1660
1661#[derive(Clone)]
1662struct ForeignField {
1663    ident: syn::Ident,
1664    kind: ForeignFieldKind,
1665}
1666
1667#[derive(Clone)]
1668struct ForeignFieldKind {
1669    original_ty: Type,
1670    stored_ty: Type,
1671}
1672
1673#[derive(Clone)]
1674struct RelateField {
1675    ident: syn::Ident,
1676    relation_name: String,
1677    field_ty: Type,
1678    direction: RelationFieldDirection,
1679}
1680
1681#[derive(Clone)]
1682struct PaginField {
1683    ident: syn::Ident,
1684}
1685
1686fn parse_foreign_field(field: &Field, attr: &Attribute) -> syn::Result<ForeignField> {
1687    validate_foreign_field(field, attr)?;
1688    let ident = field.ident.clone().expect("named field");
1689
1690    let kind = ForeignFieldKind {
1691        original_ty: field.ty.clone(),
1692        stored_ty: foreign_stored_type(&field.ty)
1693            .ok_or_else(|| Error::new_spanned(&field.ty, BINDREF_ACCEPTED_SHAPES))?,
1694    };
1695
1696    Ok(ForeignField { ident, kind })
1697}
1698
1699fn parse_relate_field(field: &Field, attr: &Attribute) -> syn::Result<RelateField> {
1700    let direction = parse_relation_direction(attr)?;
1701    let relation_name = attr
1702        .parse_args::<syn::LitStr>()
1703        .map_err(|_| {
1704            Error::new_spanned(
1705                attr,
1706                format!(
1707                    "{} requires exactly one string literal",
1708                    direction.attr_label()
1709                ),
1710            )
1711        })?
1712        .value();
1713    if relation_name.is_empty() {
1714        return Err(Error::new_spanned(
1715            attr,
1716            format!("{} relation name must not be empty", direction.attr_label()),
1717        ));
1718    }
1719
1720    validate_relate_field(field, attr)?;
1721
1722    Ok(RelateField {
1723        ident: field.ident.clone().expect("named field"),
1724        relation_name,
1725        field_ty: field.ty.clone(),
1726        direction,
1727    })
1728}
1729
1730fn parse_pagin_field(field: &Field, attr: &Attribute) -> syn::Result<PaginField> {
1731    validate_pagin_field(field, attr)?;
1732    Ok(PaginField {
1733        ident: field.ident.clone().expect("named field"),
1734    })
1735}
1736
1737fn validate_relate_field(field: &Field, attr: &Attribute) -> syn::Result<Type> {
1738    if !attr.path().is_ident("relate") && !attr.path().is_ident("back_relate") {
1739        return Err(Error::new_spanned(attr, "unsupported relate attribute"));
1740    }
1741
1742    let accepted = relate_leaf_type(&field.ty).cloned().map(Type::Path);
1743
1744    accepted.ok_or_else(|| Error::new_spanned(&field.ty, RELATE_ACCEPTED_SHAPES))
1745}
1746
1747fn validate_pagin_field(field: &Field, attr: &Attribute) -> syn::Result<Type> {
1748    if !attr.path().is_ident("pagin") {
1749        return Err(Error::new_spanned(attr, "unsupported pagination attribute"));
1750    }
1751
1752    if pagination_leaf_type(&field.ty).is_none() {
1753        return Err(Error::new_spanned(&field.ty, PAGIN_ACCEPTED_SHAPES));
1754    }
1755
1756    Ok(field.ty.clone())
1757}
1758
1759fn relate_leaf_type(ty: &Type) -> Option<&TypePath> {
1760    if let Some(leaf) = direct_store_child_type(ty) {
1761        return Some(leaf);
1762    }
1763
1764    if let Some(inner) = option_inner_type(ty) {
1765        if let Some(leaf) = direct_store_child_type(inner) {
1766            return Some(leaf);
1767        }
1768
1769        if let Some(vec_inner) = vec_inner_type(inner) {
1770            return direct_store_child_type(vec_inner);
1771        }
1772
1773        return None;
1774    }
1775
1776    if let Some(inner) = vec_inner_type(ty) {
1777        return direct_store_child_type(inner);
1778    }
1779
1780    None
1781}
1782
1783fn pagination_leaf_type(ty: &Type) -> Option<Type> {
1784    if option_inner_type(ty).is_some() || vec_inner_type(ty).is_some() {
1785        return None;
1786    }
1787
1788    if is_id_type(ty)
1789        || is_record_id_type(ty)
1790        || is_string_type(ty)
1791        || is_common_non_store_leaf_type(ty)
1792    {
1793        return Some(ty.clone());
1794    }
1795
1796    None
1797}
1798
1799fn parse_relation_direction(attr: &Attribute) -> syn::Result<RelationFieldDirection> {
1800    if attr.path().is_ident("relate") {
1801        return Ok(RelationFieldDirection::Outgoing);
1802    }
1803    if attr.path().is_ident("back_relate") {
1804        return Ok(RelationFieldDirection::Incoming);
1805    }
1806
1807    Err(Error::new_spanned(attr, "unsupported relate attribute"))
1808}
1809
1810fn relation_attr_label(attr: &Attribute) -> &'static str {
1811    if attr.path().is_ident("back_relate") {
1812        "#[back_relate(...)]"
1813    } else {
1814        "#[relate(...)]"
1815    }
1816}
1817
1818fn relation_attr_conflict_message(previous: &Attribute, current: &Attribute) -> String {
1819    match (
1820        previous.path().is_ident("relate"),
1821        previous.path().is_ident("back_relate"),
1822        current.path().is_ident("relate"),
1823        current.path().is_ident("back_relate"),
1824    ) {
1825        (true, false, true, false) => {
1826            "duplicate #[relate(...)] attribute is not supported".to_owned()
1827        }
1828        (false, true, false, true) => {
1829            "duplicate #[back_relate(...)] attribute is not supported".to_owned()
1830        }
1831        _ => "#[relate(...)] cannot be combined with #[back_relate(...)]".to_owned(),
1832    }
1833}
1834
1835fn foreign_field_kind<'a>(
1836    ident: &syn::Ident,
1837    fields: &'a [ForeignField],
1838) -> Option<&'a ForeignFieldKind> {
1839    fields
1840        .iter()
1841        .find(|field| field.ident == *ident)
1842        .map(|field| &field.kind)
1843}
1844
1845fn stored_field_type(field: &Field, foreign_fields: &[ForeignField]) -> Type {
1846    let ident = field.ident.as_ref().expect("named field");
1847    match foreign_field_kind(ident, foreign_fields) {
1848        Some(ForeignFieldKind { stored_ty, .. }) => stored_ty.clone(),
1849        None => field.ty.clone(),
1850    }
1851}
1852
1853fn foreign_stored_type(ty: &Type) -> Option<Type> {
1854    if let Some(inner) = option_inner_type(ty) {
1855        let inner = foreign_stored_type(inner)?;
1856        return Some(syn::parse_quote!(::std::option::Option<#inner>));
1857    }
1858
1859    if let Some(inner) = vec_inner_type(ty) {
1860        let inner = foreign_stored_type(inner)?;
1861        return Some(syn::parse_quote!(::std::vec::Vec<#inner>));
1862    }
1863
1864    direct_store_child_type(ty)
1865        .cloned()
1866        .map(|_| syn::parse_quote!(::surrealdb::types::RecordId))
1867}
1868
1869fn foreign_leaf_type(ty: &Type) -> Option<Type> {
1870    if let Some(inner) = option_inner_type(ty) {
1871        return foreign_leaf_type(inner);
1872    }
1873
1874    if let Some(inner) = vec_inner_type(ty) {
1875        return foreign_leaf_type(inner);
1876    }
1877
1878    direct_store_child_type(ty).cloned().map(Type::Path)
1879}
1880
1881fn invalid_foreign_leaf_type(ty: &Type) -> Option<Type> {
1882    let leaf = foreign_leaf_type(ty)?;
1883    match &leaf {
1884        Type::Path(type_path) => {
1885            let segment = type_path.path.segments.last()?;
1886            if matches!(segment.arguments, PathArguments::None) {
1887                None
1888            } else {
1889                Some(leaf)
1890            }
1891        }
1892        _ => Some(leaf),
1893    }
1894}
1895
1896fn direct_store_child_type(ty: &Type) -> Option<&TypePath> {
1897    let Type::Path(type_path) = ty else {
1898        return None;
1899    };
1900
1901    let segment = type_path.path.segments.last()?;
1902    if !matches!(segment.arguments, PathArguments::None) {
1903        return None;
1904    }
1905
1906    if is_id_type(ty) || is_string_type(ty) || is_common_non_store_leaf_type(ty) {
1907        return None;
1908    }
1909
1910    Some(type_path)
1911}
1912
1913fn is_common_non_store_leaf_type(ty: &Type) -> bool {
1914    matches!(
1915        ty,
1916        Type::Path(TypePath { path, .. })
1917            if path.is_ident("bool")
1918                || path.is_ident("u8")
1919                || path.is_ident("u16")
1920                || path.is_ident("u32")
1921                || path.is_ident("u64")
1922                || path.is_ident("u128")
1923                || path.is_ident("usize")
1924                || path.is_ident("i8")
1925                || path.is_ident("i16")
1926                || path.is_ident("i32")
1927                || path.is_ident("i64")
1928                || path.is_ident("i128")
1929                || path.is_ident("isize")
1930                || path.is_ident("f32")
1931                || path.is_ident("f64")
1932                || path.is_ident("char")
1933    )
1934}
1935
1936fn secure_field_count(fields: &syn::punctuated::Punctuated<Field, syn::token::Comma>) -> usize {
1937    fields
1938        .iter()
1939        .filter(|field| has_secure_attr(&field.attrs))
1940        .count()
1941}
1942
1943fn relation_name_override(attrs: &[Attribute]) -> syn::Result<Option<String>> {
1944    for attr in attrs {
1945        if !attr.path().is_ident("relation") {
1946            continue;
1947        }
1948
1949        let mut name = None;
1950        attr.parse_nested_meta(|meta| {
1951            if meta.path.is_ident("name") {
1952                let value = meta.value()?;
1953                let literal: syn::LitStr = value.parse()?;
1954                name = Some(literal.value());
1955                Ok(())
1956            } else {
1957                Err(meta.error("unsupported relation attribute"))
1958            }
1959        })?;
1960        return Ok(name);
1961    }
1962
1963    Ok(None)
1964}
1965
1966enum SecureKind {
1967    Shape(Type),
1968}
1969
1970impl SecureKind {
1971    fn encrypted_type(&self) -> proc_macro2::TokenStream {
1972        match self {
1973            SecureKind::Shape(ty) => quote! { <#ty as ::appdb::SensitiveShape>::Encrypted },
1974        }
1975    }
1976
1977    fn encrypt_with_context_expr(&self, ident: &syn::Ident) -> proc_macro2::TokenStream {
1978        match self {
1979            SecureKind::Shape(ty) => {
1980                quote! { <#ty as ::appdb::SensitiveShape>::encrypt_with_context(&self.#ident, context)? }
1981            }
1982        }
1983    }
1984
1985    fn decrypt_with_context_expr(&self, ident: &syn::Ident) -> proc_macro2::TokenStream {
1986        match self {
1987            SecureKind::Shape(ty) => {
1988                quote! { <#ty as ::appdb::SensitiveShape>::decrypt_with_context(&encrypted.#ident, context)? }
1989            }
1990        }
1991    }
1992
1993    fn encrypt_with_runtime_expr(
1994        &self,
1995        ident: &syn::Ident,
1996        field_tag_ident: &syn::Ident,
1997    ) -> proc_macro2::TokenStream {
1998        match self {
1999            SecureKind::Shape(ty) => {
2000                quote! {{
2001                    let context = ::appdb::crypto::resolve_crypto_context_for::<#field_tag_ident>()?;
2002                    <#ty as ::appdb::SensitiveShape>::encrypt_with_context(&self.#ident, context.as_ref())?
2003                }}
2004            }
2005        }
2006    }
2007
2008    fn decrypt_with_runtime_expr(
2009        &self,
2010        ident: &syn::Ident,
2011        field_tag_ident: &syn::Ident,
2012    ) -> proc_macro2::TokenStream {
2013        match self {
2014            SecureKind::Shape(ty) => {
2015                quote! {{
2016                    let context = ::appdb::crypto::resolve_crypto_context_for::<#field_tag_ident>()?;
2017                    <#ty as ::appdb::SensitiveShape>::decrypt_with_context(&encrypted.#ident, context.as_ref())?
2018                }}
2019            }
2020        }
2021    }
2022}
2023
2024fn secure_kind(field: &Field) -> syn::Result<SecureKind> {
2025    if secure_shape_supported(&field.ty) {
2026        return Ok(SecureKind::Shape(field.ty.clone()));
2027    }
2028
2029    Err(Error::new_spanned(
2030        &field.ty,
2031        secure_shape_error_message(&field.ty),
2032    ))
2033}
2034
2035fn secure_shape_supported(ty: &Type) -> bool {
2036    if is_string_type(ty) {
2037        return true;
2038    }
2039
2040    if sensitive_value_wrapper_inner_type(ty).is_some() {
2041        return true;
2042    }
2043
2044    if let Some(inner) = option_inner_type(ty) {
2045        return secure_shape_supported(inner);
2046    }
2047
2048    if let Some(inner) = vec_inner_type(ty) {
2049        return secure_shape_supported(inner);
2050    }
2051
2052    direct_sensitive_child_type(ty).is_some()
2053}
2054
2055fn secure_shape_error_message(ty: &Type) -> &'static str {
2056    if invalid_secure_leaf_type(ty).is_some() {
2057        "#[secure] child shapes require a direct named Sensitive type leaf with only Option<_> and Vec<_> wrappers"
2058    } else {
2059        "#[secure] supports String, appdb::SensitiveValueOf<T>, and recursive Child / Option<Child> / Vec<Child> shapes where Child implements appdb::Sensitive"
2060    }
2061}
2062
2063fn direct_sensitive_child_type(ty: &Type) -> Option<&TypePath> {
2064    let Type::Path(type_path) = ty else {
2065        return None;
2066    };
2067
2068    let segment = type_path.path.segments.last()?;
2069    if !matches!(segment.arguments, PathArguments::None) {
2070        return None;
2071    }
2072
2073    if is_id_type(ty) || is_string_type(ty) || is_common_non_store_leaf_type(ty) {
2074        return None;
2075    }
2076
2077    Some(type_path)
2078}
2079
2080fn invalid_secure_leaf_type(ty: &Type) -> Option<Type> {
2081    if let Some(inner) = option_inner_type(ty) {
2082        return invalid_secure_leaf_type(inner);
2083    }
2084
2085    if let Some(inner) = vec_inner_type(ty) {
2086        return invalid_secure_leaf_type(inner);
2087    }
2088
2089    let leaf = direct_sensitive_child_type(ty)?.clone();
2090    let segment = leaf.path.segments.last()?;
2091    if matches!(segment.arguments, PathArguments::None) {
2092        None
2093    } else {
2094        Some(Type::Path(leaf))
2095    }
2096}
2097
2098fn is_string_type(ty: &Type) -> bool {
2099    match ty {
2100        Type::Path(TypePath { path, .. }) => path.is_ident("String"),
2101        _ => false,
2102    }
2103}
2104
2105fn is_id_type(ty: &Type) -> bool {
2106    match ty {
2107        Type::Path(TypePath { path, .. }) => path.segments.last().is_some_and(|segment| {
2108            let ident = segment.ident.to_string();
2109            ident == "Id"
2110        }),
2111        _ => false,
2112    }
2113}
2114
2115fn is_record_id_type(ty: &Type) -> bool {
2116    match ty {
2117        Type::Path(TypePath { path, .. }) => path.segments.last().is_some_and(|segment| {
2118            let ident = segment.ident.to_string();
2119            ident == "RecordId"
2120        }),
2121        _ => false,
2122    }
2123}
2124
2125fn option_inner_type(ty: &Type) -> Option<&Type> {
2126    let Type::Path(TypePath { path, .. }) = ty else {
2127        return None;
2128    };
2129    let segment = path.segments.last()?;
2130    if segment.ident != "Option" {
2131        return None;
2132    }
2133    let PathArguments::AngleBracketed(args) = &segment.arguments else {
2134        return None;
2135    };
2136    let GenericArgument::Type(inner) = args.args.first()? else {
2137        return None;
2138    };
2139    Some(inner)
2140}
2141
2142fn vec_inner_type(ty: &Type) -> Option<&Type> {
2143    let Type::Path(TypePath { path, .. }) = ty else {
2144        return None;
2145    };
2146    let segment = path.segments.last()?;
2147    if segment.ident != "Vec" {
2148        return None;
2149    }
2150    let PathArguments::AngleBracketed(args) = &segment.arguments else {
2151        return None;
2152    };
2153    let GenericArgument::Type(inner) = args.args.first()? else {
2154        return None;
2155    };
2156    Some(inner)
2157}
2158
2159fn sensitive_value_wrapper_inner_type(ty: &Type) -> Option<&Type> {
2160    let Type::Path(TypePath { path, .. }) = ty else {
2161        return None;
2162    };
2163    let segment = path.segments.last()?;
2164    if segment.ident != "SensitiveValueOf" {
2165        return None;
2166    }
2167    let PathArguments::AngleBracketed(args) = &segment.arguments else {
2168        return None;
2169    };
2170    let GenericArgument::Type(inner) = args.args.first()? else {
2171        return None;
2172    };
2173    Some(inner)
2174}
2175
2176fn to_snake_case(input: &str) -> String {
2177    let mut out = String::with_capacity(input.len() + 4);
2178    let mut prev_is_lower_or_digit = false;
2179
2180    for ch in input.chars() {
2181        if ch.is_ascii_uppercase() {
2182            if prev_is_lower_or_digit {
2183                out.push('_');
2184            }
2185            out.push(ch.to_ascii_lowercase());
2186            prev_is_lower_or_digit = false;
2187        } else {
2188            out.push(ch);
2189            prev_is_lower_or_digit = ch.is_ascii_lowercase() || ch.is_ascii_digit();
2190        }
2191    }
2192
2193    out
2194}
2195
2196fn to_pascal_case(input: &str) -> String {
2197    let mut out = String::with_capacity(input.len());
2198    let mut uppercase_next = true;
2199
2200    for ch in input.chars() {
2201        if ch == '_' || ch == '-' {
2202            uppercase_next = true;
2203            continue;
2204        }
2205
2206        if uppercase_next {
2207            out.push(ch.to_ascii_uppercase());
2208            uppercase_next = false;
2209        } else {
2210            out.push(ch);
2211        }
2212    }
2213
2214    out
2215}