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