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