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