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        let supports_raw_partial_update_impl =
727            if secure_field_count(&named_fields) == 0 && relate_fields.is_empty() {
728                quote! {
729                    fn supports_raw_partial_update() -> bool {
730                        true
731                    }
732                }
733            } else {
734                quote! {}
735            };
736
737        quote! {
738            impl ::appdb::ForeignModel for #struct_ident {
739                async fn persist_foreign(value: Self) -> ::anyhow::Result<Self::Stored> {
740                    let mut value = value;
741                    #( #fill_assignments )*
742                    <Self as ::appdb::StoredModel>::into_stored(value)
743                }
744
745                async fn persist_foreign_with_plan(
746                    value: Self,
747                    foreign_plan: &::appdb::ForeignWritePlan,
748                ) -> ::anyhow::Result<Self::Stored> {
749                    foreign_plan.ensure_known_fields(Self::foreign_field_names())?;
750                    <Self as ::appdb::ForeignModel>::persist_foreign(value).await
751                }
752
753                async fn hydrate_foreign(stored: Self::Stored) -> ::anyhow::Result<Self> {
754                    <Self as ::appdb::StoredModel>::from_stored(stored)
755                }
756
757                fn decode_stored_row(
758                    row: ::surrealdb::types::Value,
759                ) -> ::anyhow::Result<Self::Stored>
760                where
761                    Self::Stored: ::serde::de::DeserializeOwned,
762                {
763                    Ok(::serde_json::from_value(row.into_json_value())?)
764                }
765
766                #relation_methods_impl
767                #supports_raw_partial_update_impl
768            }
769        }
770    } else {
771        let stored_struct_ident = format_ident!("AppdbStored{}", struct_ident);
772        quote! {
773            #[derive(
774                Debug,
775                Clone,
776                ::serde::Serialize,
777                ::serde::Deserialize,
778                ::surrealdb::types::SurrealValue,
779            )]
780            #vis struct #stored_struct_ident {
781                #( #stored_fields, )*
782            }
783
784            impl ::appdb::StoredModel for #struct_ident {
785                type Stored = #stored_struct_ident;
786
787                fn into_stored(self) -> ::anyhow::Result<Self::Stored> {
788                    unreachable!("foreign fields require async persist_foreign")
789                }
790
791                fn from_stored(_stored: Self::Stored) -> ::anyhow::Result<Self> {
792                    unreachable!("foreign fields require async hydrate_foreign")
793                }
794            }
795
796            impl ::appdb::ForeignModel for #struct_ident {
797                async fn persist_foreign(value: Self) -> ::anyhow::Result<Self::Stored> {
798                    let mut value = value;
799                    #( #fill_assignments )*
800                    Ok(#stored_struct_ident {
801                        #( #into_stored_assignments, )*
802                    })
803                }
804
805                async fn persist_foreign_with_plan(
806                    value: Self,
807                    foreign_plan: &::appdb::ForeignWritePlan,
808                ) -> ::anyhow::Result<Self::Stored> {
809                    foreign_plan.ensure_known_fields(Self::foreign_field_names())?;
810                    let mut value = value;
811                    #( #fill_assignments )*
812                    Ok(#stored_struct_ident {
813                        #( #into_stored_with_plan_assignments, )*
814                    })
815                }
816
817                async fn hydrate_foreign(stored: Self::Stored) -> ::anyhow::Result<Self> {
818                    Ok(Self {
819                        #( #from_stored_assignments, )*
820                    })
821                }
822
823                fn has_foreign_fields() -> bool {
824                    true
825                }
826
827                fn foreign_field_names() -> &'static [&'static str] {
828                    &[ #( #foreign_field_literals ),* ]
829                }
830
831                fn decode_stored_row(
832                    row: ::surrealdb::types::Value,
833                ) -> ::anyhow::Result<Self::Stored>
834                where
835                    Self::Stored: ::serde::de::DeserializeOwned,
836                {
837                    let mut row = row.into_json_value();
838                    if let ::serde_json::Value::Object(map) = &mut row {
839                        #( #decode_foreign_fields )*
840                    }
841                    Ok(::serde_json::from_value(row)?)
842                }
843
844                #relation_methods_impl
845            }
846        }
847    };
848
849    let foreign_write_api_impl = if foreign_fields.is_empty() {
850        quote! {}
851    } else {
852        let foreign_write_ident = format_ident!("AppdbForeignWrite{}", struct_ident);
853        let foreign_write_field_methods = foreign_fields.iter().map(|field| {
854            let ident = &field.ident;
855            let field_name = ident.to_string();
856            let stored_ty = &field.kind.stored_ty;
857            quote! {
858                pub fn #ident(mut self, value: #stored_ty) -> ::anyhow::Result<Self> {
859                    self.query = self.query.set_field_shape(#field_name, value)?;
860                    Ok(self)
861                }
862            }
863        });
864
865        quote! {
866            #vis struct #foreign_write_ident {
867                query: ::appdb::repository::ForeignWriteQuery<#struct_ident>,
868            }
869
870            impl #foreign_write_ident {
871                #( #foreign_write_field_methods )*
872
873                pub async fn create_at(
874                    self,
875                    id: ::surrealdb::types::RecordId,
876                ) -> ::anyhow::Result<#struct_ident> {
877                    self.query.create_at(id).await
878                }
879
880                pub async fn upsert_at(
881                    self,
882                    id: ::surrealdb::types::RecordId,
883                ) -> ::anyhow::Result<#struct_ident> {
884                    self.query.upsert_at(id).await
885                }
886
887                pub async fn update_at(
888                    self,
889                    id: ::surrealdb::types::RecordId,
890                ) -> ::anyhow::Result<#struct_ident> {
891                    self.query.update_at(id).await
892                }
893
894                pub async fn create_at_returning<View>(
895                    self,
896                    id: ::surrealdb::types::RecordId,
897                ) -> ::anyhow::Result<View>
898                where
899                    View: ::appdb::repository::WriteReturnView<#struct_ident>,
900                {
901                    self.query.create_at_returning::<View>(id).await
902                }
903
904                pub async fn upsert_at_returning<View>(
905                    self,
906                    id: ::surrealdb::types::RecordId,
907                ) -> ::anyhow::Result<View>
908                where
909                    View: ::appdb::repository::WriteReturnView<#struct_ident>,
910                {
911                    self.query.upsert_at_returning::<View>(id).await
912                }
913
914                pub async fn update_at_returning<View>(
915                    self,
916                    id: ::surrealdb::types::RecordId,
917                ) -> ::anyhow::Result<View>
918                where
919                    View: ::appdb::repository::WriteReturnView<#struct_ident>,
920                {
921                    self.query.update_at_returning::<View>(id).await
922                }
923            }
924        }
925    };
926
927    let foreign_write_constructor_impl = if foreign_fields.is_empty() {
928        quote! {}
929    } else {
930        let foreign_write_ident = format_ident!("AppdbForeignWrite{}", struct_ident);
931        quote! {
932            pub fn foreign(self) -> #foreign_write_ident {
933                #foreign_write_ident {
934                    query: ::appdb::repository::ForeignWriteQuery::new(self),
935                }
936            }
937        }
938    };
939
940    let store_marker_ident = format_ident!("AppdbStoreMarker{}", struct_ident);
941
942    Ok(quote! {
943        #[doc(hidden)]
944        #vis struct #store_marker_ident;
945
946        impl ::appdb::model::meta::ModelMeta for #struct_ident {
947            fn storage_table() -> &'static str {
948                #resolved_table_name_expr
949            }
950
951            fn table_name() -> &'static str {
952                static TABLE_NAME: ::std::sync::OnceLock<&'static str> = ::std::sync::OnceLock::new();
953                TABLE_NAME.get_or_init(|| {
954                    let table = #resolved_table_name_expr;
955                    ::appdb::model::meta::register_table(stringify!(#struct_ident), table)
956                })
957            }
958        }
959
960        impl ::appdb::model::meta::StoreModelMarker for #struct_ident {}
961        impl ::appdb::model::meta::StoreModelMarker for #store_marker_ident {}
962        #pagination_meta_impl
963
964        impl ::appdb::model::meta::UniqueLookupMeta for #struct_ident {
965            fn lookup_fields() -> &'static [&'static str] {
966                &[ #( #lookup_field_literals ),* ]
967            }
968
969            fn foreign_fields() -> &'static [&'static str] {
970                &[ #( #foreign_field_literals ),* ]
971            }
972
973            fn resolve_lookup_field_value(
974                &self,
975                field: &str,
976            ) -> impl ::std::future::Future<
977                Output = ::anyhow::Result<::std::option::Option<::surrealdb::types::Value>>
978            > {
979                async move {
980                    match field {
981                        #( #resolve_lookup_field_value_arms )*
982                        _ => Ok(::std::option::Option::None),
983                    }
984                }
985            }
986        }
987        #stored_model_impl
988        #foreign_model_impl
989        #foreign_write_api_impl
990
991        #auto_has_id_impl
992        #resolve_record_id_impl
993
994        #( #unique_schema_impls )*
995        #( #pagin_schema_impl )*
996
997        impl ::appdb::repository::Crud for #struct_ident {}
998
999        impl #struct_ident {
1000            /// Saves one value through the recommended Store CRUD surface.
1001            ///
1002            /// Prefer these generated model methods in application code. Lower-level
1003            /// `appdb::repository::Repo` helpers exist for library internals and
1004            /// advanced integration seams, not as the primary public path.
1005            pub async fn save(self) -> ::anyhow::Result<Self> {
1006                <Self as ::appdb::repository::Crud>::save(self).await
1007            }
1008
1009            /// Saves many values through the recommended Store CRUD surface.
1010            pub async fn save_many(data: ::std::vec::Vec<Self>) -> ::anyhow::Result<::std::vec::Vec<Self>> {
1011                <Self as ::appdb::repository::Crud>::save_many(data).await
1012            }
1013
1014            pub async fn get<T>(id: T) -> ::anyhow::Result<Self>
1015            where
1016                ::surrealdb::types::RecordIdKey: From<T>,
1017                T: Send,
1018            {
1019                ::appdb::repository::Repo::<Self>::get(id).await
1020            }
1021
1022            pub fn list() -> ::appdb::repository::ListQuery<Self> {
1023                ::appdb::repository::Repo::<Self>::list()
1024            }
1025
1026            pub async fn list_limit(count: i64) -> ::anyhow::Result<::std::vec::Vec<Self>> {
1027                ::appdb::repository::Repo::<Self>::list_limit(count).await
1028            }
1029            #pagination_methods_impl
1030
1031            pub async fn relate_by_name<Target>(&self, target: &Target, relation: &str) -> ::anyhow::Result<()>
1032            where
1033                Target: ::appdb::model::meta::ResolveRecordId + Send + Sync,
1034            {
1035                <Self as ::appdb::graph::GraphCrud>::relate_by_name(self, target, relation).await
1036            }
1037
1038            pub async fn back_relate_by_name<Target>(&self, target: &Target, relation: &str) -> ::anyhow::Result<()>
1039            where
1040                Target: ::appdb::model::meta::ResolveRecordId + Send + Sync,
1041            {
1042                <Self as ::appdb::graph::GraphCrud>::back_relate_by_name(self, target, relation).await
1043            }
1044
1045            pub async fn unrelate_by_name<Target>(&self, target: &Target, relation: &str) -> ::anyhow::Result<()>
1046            where
1047                Target: ::appdb::model::meta::ResolveRecordId + Send + Sync,
1048            {
1049                <Self as ::appdb::graph::GraphCrud>::unrelate_by_name(self, target, relation).await
1050            }
1051
1052            pub async fn outgoing_ids(&self, relation: &str) -> ::anyhow::Result<::std::vec::Vec<::surrealdb::types::RecordId>> {
1053                <Self as ::appdb::repository::Crud>::outgoing_ids(self, relation).await
1054            }
1055
1056            pub async fn outgoing<Target>(&self, relation: &str) -> ::anyhow::Result<::std::vec::Vec<Target>>
1057            where
1058                Target: ::appdb::model::meta::ModelMeta + ::appdb::StoredModel + ::appdb::ForeignModel,
1059                <Target as ::appdb::StoredModel>::Stored: ::serde::de::DeserializeOwned,
1060            {
1061                <Self as ::appdb::repository::Crud>::outgoing::<Target>(self, relation).await
1062            }
1063
1064            pub async fn outgoing_count(&self, relation: &str) -> ::anyhow::Result<i64> {
1065                <Self as ::appdb::repository::Crud>::outgoing_count(self, relation).await
1066            }
1067
1068            pub async fn outgoing_count_as<Target>(&self, relation: &str) -> ::anyhow::Result<i64>
1069            where
1070                Target: ::appdb::model::meta::ModelMeta + ::appdb::StoredModel + ::appdb::ForeignModel,
1071            {
1072                <Self as ::appdb::repository::Crud>::outgoing_count_as::<Target>(self, relation).await
1073            }
1074
1075            pub async fn incoming_ids(&self, relation: &str) -> ::anyhow::Result<::std::vec::Vec<::surrealdb::types::RecordId>> {
1076                <Self as ::appdb::repository::Crud>::incoming_ids(self, relation).await
1077            }
1078
1079            pub async fn incoming<Target>(&self, relation: &str) -> ::anyhow::Result<::std::vec::Vec<Target>>
1080            where
1081                Target: ::appdb::model::meta::ModelMeta + ::appdb::StoredModel + ::appdb::ForeignModel,
1082                <Target as ::appdb::StoredModel>::Stored: ::serde::de::DeserializeOwned,
1083            {
1084                <Self as ::appdb::repository::Crud>::incoming::<Target>(self, relation).await
1085            }
1086
1087            pub async fn incoming_count(&self, relation: &str) -> ::anyhow::Result<i64> {
1088                <Self as ::appdb::repository::Crud>::incoming_count(self, relation).await
1089            }
1090
1091            pub async fn incoming_count_as<Target>(&self, relation: &str) -> ::anyhow::Result<i64>
1092            where
1093                Target: ::appdb::model::meta::ModelMeta + ::appdb::StoredModel + ::appdb::ForeignModel,
1094            {
1095                <Self as ::appdb::repository::Crud>::incoming_count_as::<Target>(self, relation).await
1096            }
1097
1098            pub async fn delete_all() -> ::anyhow::Result<()> {
1099                ::appdb::repository::Repo::<Self>::delete_all().await
1100            }
1101
1102            pub async fn find_one_id(
1103                k: &str,
1104                v: &str,
1105            ) -> ::anyhow::Result<::surrealdb::types::RecordId> {
1106                ::appdb::repository::Repo::<Self>::find_one_id(k, v).await
1107            }
1108
1109            pub async fn list_record_ids() -> ::anyhow::Result<::std::vec::Vec<::surrealdb::types::RecordId>> {
1110                ::appdb::repository::Repo::<Self>::list_record_ids().await
1111            }
1112
1113            pub async fn create_at(
1114                id: ::surrealdb::types::RecordId,
1115                data: Self,
1116            ) -> ::anyhow::Result<Self> {
1117                ::appdb::repository::Repo::<Self>::create_at(id, data).await
1118            }
1119
1120            pub async fn upsert_at(
1121                id: ::surrealdb::types::RecordId,
1122                data: Self,
1123            ) -> ::anyhow::Result<Self> {
1124                ::appdb::repository::Repo::<Self>::upsert_at(id, data).await
1125            }
1126
1127            pub async fn update_at(
1128                self,
1129                id: ::surrealdb::types::RecordId,
1130            ) -> ::anyhow::Result<Self> {
1131                ::appdb::repository::Repo::<Self>::update_at(id, self).await
1132            }
1133
1134            #foreign_write_constructor_impl
1135
1136            pub async fn delete<T>(id: T) -> ::anyhow::Result<()>
1137            where
1138                ::surrealdb::types::RecordIdKey: From<T>,
1139                T: Send,
1140            {
1141                ::appdb::repository::Repo::<Self>::delete(id).await
1142            }
1143        }
1144    })
1145}
1146
1147fn derive_view_impl(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
1148    let struct_ident = input.ident;
1149    let vis = input.vis.clone();
1150    let view_source = view_source_config(&input.attrs)?;
1151
1152    let named_fields = match input.data {
1153        Data::Struct(data) => match data.fields {
1154            Fields::Named(fields) => fields.named,
1155            _ => {
1156                return Err(Error::new_spanned(
1157                    struct_ident,
1158                    "View can only be derived for structs with named fields",
1159                ));
1160            }
1161        },
1162        _ => {
1163            return Err(Error::new_spanned(
1164                struct_ident,
1165                "View can only be derived for structs",
1166            ));
1167        }
1168    };
1169
1170    let stored_struct_ident = format_ident!("AppdbStoredView{}", struct_ident);
1171    let view_fields = named_fields
1172        .iter()
1173        .map(|field| field.ident.as_ref().expect("named field").to_string())
1174        .collect::<Vec<_>>();
1175    let view_field_literals = view_fields.iter().map(|field| quote! { #field });
1176    let nested_view_fields = named_fields
1177        .iter()
1178        .filter_map(|field| match field_view_nested_attr(field) {
1179            Ok(true) => Some(Ok(field.ident.as_ref().expect("named field").to_string())),
1180            Ok(false) => None,
1181            Err(err) => Some(Err(err)),
1182        })
1183        .collect::<syn::Result<Vec<_>>>()?;
1184    let nested_view_field_literals = nested_view_fields.iter().map(|field| quote! { #field });
1185
1186    let stored_fields = named_fields
1187        .iter()
1188        .map(|field| {
1189            let ident = field.ident.clone().expect("named field");
1190            let ty = view_stored_type(field)?;
1191            if is_record_id_type(&ty) {
1192                Ok(quote! {
1193                    #[serde(deserialize_with = "::appdb::serde_utils::id::deserialize_record_id_or_compat_string")]
1194                    #ident: #ty
1195                })
1196            } else {
1197                Ok(quote! { #ident: #ty })
1198            }
1199        })
1200        .collect::<syn::Result<Vec<_>>>()?;
1201
1202    let hydrate_assignments = named_fields.iter().map(|field| {
1203        let ident = field.ident.clone().expect("named field");
1204        let ty = field.ty.clone();
1205        if nested_view_fields.contains(&ident.to_string()) {
1206            quote! {
1207                #ident: <#ty as ::appdb::ViewShape>::hydrate_view_shape(stored.#ident).await?
1208            }
1209        } else {
1210            quote! {
1211                #ident: stored.#ident
1212            }
1213        }
1214    });
1215
1216    let source_impl = view_source.impl_tokens();
1217    let source_methods_impl = view_source.methods_tokens(&struct_ident);
1218
1219    Ok(quote! {
1220        #[derive(
1221            Debug,
1222            Clone,
1223            ::serde::Serialize,
1224            ::serde::Deserialize,
1225            ::surrealdb::types::SurrealValue,
1226        )]
1227        #vis struct #stored_struct_ident {
1228            #( #stored_fields, )*
1229        }
1230
1231        #[::async_trait::async_trait]
1232        impl ::appdb::model::meta::ViewMeta for #struct_ident {
1233            #source_impl
1234            type Stored = #stored_struct_ident;
1235
1236            fn view_fields() -> &'static [&'static str] {
1237                &[ #( #view_field_literals ),* ]
1238            }
1239
1240            fn nested_view_fields() -> &'static [&'static str] {
1241                &[ #( #nested_view_field_literals ),* ]
1242            }
1243
1244            fn decode_stored_view_row(
1245                row: ::serde_json::Value,
1246            ) -> ::anyhow::Result<Self::Stored> {
1247                Ok(::serde_json::from_value(row)?)
1248            }
1249
1250            async fn hydrate_view(stored: Self::Stored) -> ::anyhow::Result<Self> {
1251                Ok(Self {
1252                    #( #hydrate_assignments, )*
1253                })
1254            }
1255        }
1256
1257        #[::async_trait::async_trait]
1258        impl ::appdb::ViewShape for #struct_ident {
1259            type Stored = ::surrealdb::types::RecordId;
1260
1261            async fn hydrate_view_shape(stored: Self::Stored) -> ::anyhow::Result<Self> {
1262                ::appdb::repository::ViewRepo::<Self>::get_record(stored).await
1263            }
1264        }
1265
1266        impl #struct_ident {
1267            pub fn list() -> ::appdb::repository::ViewListQuery<Self> {
1268                ::appdb::repository::ViewRepo::<Self>::list()
1269            }
1270
1271            #source_methods_impl
1272
1273            pub async fn get<T>(id: T) -> ::anyhow::Result<Self>
1274            where
1275                ::surrealdb::types::RecordIdKey: ::std::convert::From<T>,
1276                T: Send,
1277            {
1278                ::appdb::repository::ViewRepo::<Self>::get(id).await
1279            }
1280
1281            pub async fn get_record(
1282                id: ::surrealdb::types::RecordId,
1283            ) -> ::anyhow::Result<Self> {
1284                ::appdb::repository::ViewRepo::<Self>::get_record(id).await
1285            }
1286
1287            pub async fn list_records(
1288            ) -> ::anyhow::Result<::std::vec::Vec<::appdb::repository::ViewRecord<Self>>> {
1289                ::appdb::repository::ViewRepo::<Self>::list_records().await
1290            }
1291
1292            pub async fn outgoing_records(
1293                id: ::surrealdb::types::RecordId,
1294                relation: &str,
1295            ) -> ::anyhow::Result<::std::vec::Vec<::appdb::repository::ViewRecord<Self>>> {
1296                ::appdb::repository::ViewRepo::<Self>::outgoing_records(id, relation).await
1297            }
1298
1299            pub async fn outgoing_records_by_owners(
1300                ids: ::std::vec::Vec<::surrealdb::types::RecordId>,
1301                relation: &str,
1302            ) -> ::anyhow::Result<::std::vec::Vec<::appdb::repository::ViewRelatedRecord<Self>>> {
1303                ::appdb::repository::ViewRepo::<Self>::outgoing_records_by_owners(ids, relation).await
1304            }
1305
1306            pub async fn incoming_records(
1307                id: ::surrealdb::types::RecordId,
1308                relation: &str,
1309            ) -> ::anyhow::Result<::std::vec::Vec<::appdb::repository::ViewRecord<Self>>> {
1310                ::appdb::repository::ViewRepo::<Self>::incoming_records(id, relation).await
1311            }
1312
1313            pub async fn incoming_records_by_owners(
1314                ids: ::std::vec::Vec<::surrealdb::types::RecordId>,
1315                relation: &str,
1316            ) -> ::anyhow::Result<::std::vec::Vec<::appdb::repository::ViewRelatedRecord<Self>>> {
1317                ::appdb::repository::ViewRepo::<Self>::incoming_records_by_owners(ids, relation).await
1318            }
1319
1320            pub async fn find_one(
1321                k: &str,
1322                v: &str,
1323            ) -> ::anyhow::Result<Self> {
1324                ::appdb::repository::ViewRepo::<Self>::find_one(k, v).await
1325            }
1326
1327            pub async fn find_one_id(
1328                k: &str,
1329                v: &str,
1330            ) -> ::anyhow::Result<::surrealdb::types::RecordId> {
1331                ::appdb::repository::ViewRepo::<Self>::find_one_id(k, v).await
1332            }
1333        }
1334    })
1335}
1336
1337fn derive_bridge_impl(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
1338    let enum_ident = input.ident;
1339
1340    let variants = match input.data {
1341        Data::Enum(data) => data.variants,
1342        _ => {
1343            return Err(Error::new_spanned(
1344                enum_ident,
1345                "Bridge can only be derived for enums",
1346            ));
1347        }
1348    };
1349
1350    let payloads = variants
1351        .iter()
1352        .map(parse_bridge_variant)
1353        .collect::<syn::Result<Vec<_>>>()?;
1354
1355    let from_impls = payloads.iter().map(|variant| {
1356        let variant_ident = &variant.variant_ident;
1357        let payload_ty = &variant.payload_ty;
1358
1359        quote! {
1360            impl ::std::convert::From<#payload_ty> for #enum_ident {
1361                fn from(value: #payload_ty) -> Self {
1362                    Self::#variant_ident(value)
1363                }
1364            }
1365        }
1366    });
1367
1368    let persist_match_arms = payloads.iter().map(|variant| {
1369        let variant_ident = &variant.variant_ident;
1370
1371        quote! {
1372            Self::#variant_ident(value) => <_ as ::appdb::Bridge>::persist_foreign(value).await,
1373        }
1374    });
1375
1376    let hydrate_match_arms = payloads.iter().map(|variant| {
1377        let variant_ident = &variant.variant_ident;
1378        let payload_ty = &variant.payload_ty;
1379
1380        quote! {
1381            table if table == <#payload_ty as ::appdb::model::meta::ModelMeta>::storage_table() => {
1382                ::std::result::Result::Ok(Self::#variant_ident(
1383                    <#payload_ty as ::appdb::Bridge>::hydrate_foreign(id).await?,
1384                ))
1385            }
1386        }
1387    });
1388
1389    let lookup_match_arms = payloads.iter().map(|variant| {
1390        let variant_ident = &variant.variant_ident;
1391        let payload_ty = &variant.payload_ty;
1392
1393        quote! {
1394            Self::#variant_ident(value) => {
1395                <#payload_ty as ::appdb::ForeignLookupShape>::resolve_foreign_lookup_shape(value).await
1396            }
1397        }
1398    });
1399
1400    Ok(quote! {
1401        #( #from_impls )*
1402
1403        #[::async_trait::async_trait]
1404        impl ::appdb::Bridge for #enum_ident {
1405            async fn persist_foreign(self) -> ::anyhow::Result<::surrealdb::types::RecordId> {
1406                match self {
1407                    #( #persist_match_arms )*
1408                }
1409            }
1410
1411            async fn hydrate_foreign(
1412                id: ::surrealdb::types::RecordId,
1413            ) -> ::anyhow::Result<Self> {
1414                match id.table.to_string().as_str() {
1415                    #( #hydrate_match_arms, )*
1416                    table => ::anyhow::bail!(
1417                        "unsupported foreign table `{table}` for enum dispatcher `{}`",
1418                        ::std::stringify!(#enum_ident)
1419                    ),
1420                }
1421            }
1422        }
1423
1424        impl ::appdb::ForeignLookupShape for #enum_ident {
1425            type LookupStored = ::surrealdb::types::RecordId;
1426
1427            fn resolve_foreign_lookup_shape(
1428                &self,
1429            ) -> impl ::std::future::Future<Output = ::anyhow::Result<Self::LookupStored>> {
1430                async move {
1431                    match self {
1432                        #( #lookup_match_arms, )*
1433                    }
1434                }
1435            }
1436        }
1437    })
1438}
1439
1440#[derive(Clone)]
1441struct BridgeVariant {
1442    variant_ident: syn::Ident,
1443    payload_ty: Type,
1444}
1445
1446fn parse_bridge_variant(variant: &syn::Variant) -> syn::Result<BridgeVariant> {
1447    let payload_ty = match &variant.fields {
1448        Fields::Unnamed(fields) if fields.unnamed.len() == 1 => {
1449            fields.unnamed.first().expect("single field").ty.clone()
1450        }
1451        Fields::Unnamed(_) => {
1452            return Err(Error::new_spanned(
1453                &variant.ident,
1454                "Bridge variants must be single-field tuple variants",
1455            ));
1456        }
1457        Fields::Unit => {
1458            return Err(Error::new_spanned(
1459                &variant.ident,
1460                "Bridge does not support unit variants",
1461            ));
1462        }
1463        Fields::Named(_) => {
1464            return Err(Error::new_spanned(
1465                &variant.ident,
1466                "Bridge does not support struct variants",
1467            ));
1468        }
1469    };
1470
1471    let payload_path = match &payload_ty {
1472        Type::Path(path) => path,
1473        _ => {
1474            return Err(Error::new_spanned(
1475                &payload_ty,
1476                "Bridge payload must implement appdb::Bridge",
1477            ));
1478        }
1479    };
1480
1481    let segment = payload_path.path.segments.last().ok_or_else(|| {
1482        Error::new_spanned(&payload_ty, "Bridge payload must implement appdb::Bridge")
1483    })?;
1484
1485    if !matches!(segment.arguments, PathArguments::None) {
1486        return Err(Error::new_spanned(
1487            &payload_ty,
1488            "Bridge payload must implement appdb::Bridge",
1489        ));
1490    }
1491
1492    Ok(BridgeVariant {
1493        variant_ident: variant.ident.clone(),
1494        payload_ty,
1495    })
1496}
1497
1498fn derive_relation_impl(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
1499    let struct_ident = input.ident;
1500    let relation_name = relation_name_override(&input.attrs)?
1501        .unwrap_or_else(|| to_snake_case(&struct_ident.to_string()));
1502    validate_relation_name_literal(&relation_name, &struct_ident, "#[derive(Relation)]")?;
1503
1504    match input.data {
1505        Data::Struct(data) => match data.fields {
1506            Fields::Unit | Fields::Named(_) => {}
1507            _ => {
1508                return Err(Error::new_spanned(
1509                    struct_ident,
1510                    "Relation can only be derived for unit structs or structs with named fields",
1511                ));
1512            }
1513        },
1514        _ => {
1515            return Err(Error::new_spanned(
1516                struct_ident,
1517                "Relation can only be derived for structs",
1518            ));
1519        }
1520    }
1521
1522    Ok(quote! {
1523        impl ::appdb::model::relation::RelationMeta for #struct_ident {
1524            fn relation_name() -> &'static str {
1525                static REL_NAME: ::std::sync::OnceLock<&'static str> = ::std::sync::OnceLock::new();
1526                REL_NAME.get_or_init(|| ::appdb::model::relation::register_relation(#relation_name))
1527            }
1528        }
1529
1530        impl #struct_ident {
1531            pub async fn relate<A, B>(a: &A, b: &B) -> ::anyhow::Result<()>
1532            where
1533                A: ::appdb::model::meta::ResolveRecordId + Send + Sync,
1534                B: ::appdb::model::meta::ResolveRecordId + Send + Sync,
1535            {
1536                ::appdb::graph::relate_at(a.resolve_record_id().await?, b.resolve_record_id().await?, <Self as ::appdb::model::relation::RelationMeta>::relation_name()).await
1537            }
1538
1539            pub async fn back_relate<A, B>(a: &A, b: &B) -> ::anyhow::Result<()>
1540            where
1541                A: ::appdb::model::meta::ResolveRecordId + Send + Sync,
1542                B: ::appdb::model::meta::ResolveRecordId + Send + Sync,
1543            {
1544                ::appdb::graph::back_relate_at(a.resolve_record_id().await?, b.resolve_record_id().await?, <Self as ::appdb::model::relation::RelationMeta>::relation_name()).await
1545            }
1546
1547            pub async fn unrelate<A, B>(a: &A, b: &B) -> ::anyhow::Result<()>
1548            where
1549                A: ::appdb::model::meta::ResolveRecordId + Send + Sync,
1550                B: ::appdb::model::meta::ResolveRecordId + Send + Sync,
1551            {
1552                ::appdb::graph::unrelate_at(a.resolve_record_id().await?, b.resolve_record_id().await?, <Self as ::appdb::model::relation::RelationMeta>::relation_name()).await
1553            }
1554
1555            pub async fn out_ids<A>(a: &A, out_table: &str) -> ::anyhow::Result<::std::vec::Vec<::surrealdb::types::RecordId>>
1556            where
1557                A: ::appdb::model::meta::ResolveRecordId + Send + Sync,
1558            {
1559                ::appdb::graph::out_ids(a.resolve_record_id().await?, <Self as ::appdb::model::relation::RelationMeta>::relation_name(), out_table).await
1560            }
1561
1562            pub async fn in_ids<B>(b: &B, in_table: &str) -> ::anyhow::Result<::std::vec::Vec<::surrealdb::types::RecordId>>
1563            where
1564                B: ::appdb::model::meta::ResolveRecordId + Send + Sync,
1565            {
1566                ::appdb::graph::in_ids(b.resolve_record_id().await?, <Self as ::appdb::model::relation::RelationMeta>::relation_name(), in_table).await
1567            }
1568        }
1569    })
1570}
1571
1572fn derive_sensitive_impl(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
1573    let struct_ident = input.ident;
1574    let encrypted_ident = format_ident!("Encrypted{}", struct_ident);
1575    let vis = input.vis;
1576    let type_crypto_config = type_crypto_config(&input.attrs)?;
1577    let named_fields = match input.data {
1578        Data::Struct(data) => match data.fields {
1579            Fields::Named(fields) => fields.named,
1580            _ => {
1581                return Err(Error::new_spanned(
1582                    struct_ident,
1583                    "Sensitive can only be derived for structs with named fields",
1584                ));
1585            }
1586        },
1587        _ => {
1588            return Err(Error::new_spanned(
1589                struct_ident,
1590                "Sensitive can only be derived for structs",
1591            ));
1592        }
1593    };
1594
1595    let mut secure_field_count = 0usize;
1596    let mut encrypted_fields = Vec::new();
1597    let mut encrypt_assignments = Vec::new();
1598    let mut decrypt_assignments = Vec::new();
1599    let mut runtime_encrypt_assignments = Vec::new();
1600    let mut runtime_decrypt_assignments = Vec::new();
1601    let mut field_tag_structs = Vec::new();
1602    let mut secure_field_meta_entries = Vec::new();
1603
1604    for field in named_fields.iter() {
1605        let ident = field.ident.clone().expect("named field");
1606        let field_vis = field.vis.clone();
1607        let secure = has_secure_attr(&field.attrs);
1608        let field_crypto_config = field_crypto_config(&field.attrs)?;
1609
1610        if !secure && field_crypto_config.is_present() {
1611            return Err(Error::new_spanned(
1612                ident,
1613                "#[crypto(...)] on a field requires #[secure] on the same field",
1614            ));
1615        }
1616
1617        if secure {
1618            secure_field_count += 1;
1619            let secure_kind = secure_kind(field)?;
1620            let encrypted_ty = secure_kind.encrypted_type();
1621            let field_tag_ident = format_ident!(
1622                "AppdbSensitiveFieldTag{}{}",
1623                struct_ident,
1624                to_pascal_case(&ident.to_string())
1625            );
1626            let field_tag_literal = ident.to_string();
1627            let effective_account = field_crypto_config
1628                .field_account
1629                .clone()
1630                .or_else(|| type_crypto_config.account.clone());
1631            let service_override = type_crypto_config.service.clone();
1632            let account_literal = effective_account
1633                .as_ref()
1634                .map(|value| quote! { ::std::option::Option::Some(#value) })
1635                .unwrap_or_else(|| quote! { ::std::option::Option::None });
1636            let service_literal = service_override
1637                .as_ref()
1638                .map(|value| quote! { ::std::option::Option::Some(#value) })
1639                .unwrap_or_else(|| quote! { ::std::option::Option::None });
1640            let encrypt_expr = secure_kind.encrypt_with_context_expr(&ident);
1641            let decrypt_expr = secure_kind.decrypt_with_context_expr(&ident);
1642            let runtime_encrypt_expr =
1643                secure_kind.encrypt_with_runtime_expr(&ident, &field_tag_ident);
1644            let runtime_decrypt_expr =
1645                secure_kind.decrypt_with_runtime_expr(&ident, &field_tag_ident);
1646            encrypted_fields.push(quote! { #field_vis #ident: #encrypted_ty });
1647            encrypt_assignments.push(quote! { #ident: #encrypt_expr });
1648            decrypt_assignments.push(quote! { #ident: #decrypt_expr });
1649            runtime_encrypt_assignments.push(quote! { #ident: #runtime_encrypt_expr });
1650            runtime_decrypt_assignments.push(quote! { #ident: #runtime_decrypt_expr });
1651            secure_field_meta_entries.push(quote! {
1652                ::appdb::crypto::SensitiveFieldMetadata {
1653                    model_tag: ::std::concat!(::std::module_path!(), "::", ::std::stringify!(#struct_ident)),
1654                    field_tag: #field_tag_literal,
1655                    service: #service_literal,
1656                    account: #account_literal,
1657                    secure_fields: &[],
1658                }
1659            });
1660            field_tag_structs.push(quote! {
1661                #[doc(hidden)]
1662                #vis struct #field_tag_ident;
1663
1664                impl ::appdb::crypto::SensitiveFieldTag for #field_tag_ident {
1665                    fn model_tag() -> &'static str {
1666                        <#struct_ident as ::appdb::crypto::SensitiveModelTag>::model_tag()
1667                    }
1668
1669                    fn field_tag() -> &'static str {
1670                        #field_tag_literal
1671                    }
1672
1673                    fn crypto_metadata() -> &'static ::appdb::crypto::SensitiveFieldMetadata {
1674                        static FIELD_META: ::std::sync::OnceLock<::appdb::crypto::SensitiveFieldMetadata> = ::std::sync::OnceLock::new();
1675                        FIELD_META.get_or_init(|| ::appdb::crypto::SensitiveFieldMetadata {
1676                            model_tag: <#struct_ident as ::appdb::crypto::SensitiveModelTag>::model_tag(),
1677                            field_tag: #field_tag_literal,
1678                            service: #service_literal,
1679                            account: #account_literal,
1680                            secure_fields: &#struct_ident::SECURE_FIELDS,
1681                        })
1682                    }
1683                }
1684            });
1685        } else {
1686            let ty = field.ty.clone();
1687            encrypted_fields.push(quote! { #field_vis #ident: #ty });
1688            encrypt_assignments.push(quote! { #ident: self.#ident.clone() });
1689            decrypt_assignments.push(quote! { #ident: encrypted.#ident.clone() });
1690            runtime_encrypt_assignments.push(quote! { #ident: self.#ident.clone() });
1691            runtime_decrypt_assignments.push(quote! { #ident: encrypted.#ident.clone() });
1692        }
1693    }
1694
1695    if secure_field_count == 0 {
1696        return Err(Error::new_spanned(
1697            struct_ident,
1698            "Sensitive requires at least one #[secure] field",
1699        ));
1700    }
1701
1702    Ok(quote! {
1703        #[derive(
1704            Debug,
1705            Clone,
1706            ::serde::Serialize,
1707            ::serde::Deserialize,
1708            ::surrealdb::types::SurrealValue,
1709        )]
1710        #vis struct #encrypted_ident {
1711            #( #encrypted_fields, )*
1712        }
1713
1714        impl ::appdb::crypto::SensitiveModelTag for #struct_ident {
1715            fn model_tag() -> &'static str {
1716                ::std::concat!(::std::module_path!(), "::", ::std::stringify!(#struct_ident))
1717            }
1718        }
1719
1720        #( #field_tag_structs )*
1721
1722        impl ::appdb::Sensitive for #struct_ident {
1723            type Encrypted = #encrypted_ident;
1724
1725            fn encrypt(
1726                &self,
1727                context: &::appdb::crypto::CryptoContext,
1728            ) -> ::std::result::Result<Self::Encrypted, ::appdb::crypto::CryptoError> {
1729                ::std::result::Result::Ok(#encrypted_ident {
1730                    #( #encrypt_assignments, )*
1731                })
1732            }
1733
1734            fn decrypt(
1735                encrypted: &Self::Encrypted,
1736                context: &::appdb::crypto::CryptoContext,
1737            ) -> ::std::result::Result<Self, ::appdb::crypto::CryptoError> {
1738                ::std::result::Result::Ok(Self {
1739                    #( #decrypt_assignments, )*
1740                })
1741            }
1742
1743            fn encrypt_with_runtime_resolver(
1744                &self,
1745            ) -> ::std::result::Result<Self::Encrypted, ::appdb::crypto::CryptoError> {
1746                ::std::result::Result::Ok(#encrypted_ident {
1747                    #( #runtime_encrypt_assignments, )*
1748                })
1749            }
1750
1751            fn decrypt_with_runtime_resolver(
1752                encrypted: &Self::Encrypted,
1753            ) -> ::std::result::Result<Self, ::appdb::crypto::CryptoError> {
1754                ::std::result::Result::Ok(Self {
1755                    #( #runtime_decrypt_assignments, )*
1756                })
1757            }
1758
1759            fn secure_fields() -> &'static [::appdb::crypto::SensitiveFieldMetadata] {
1760                &Self::SECURE_FIELDS
1761            }
1762        }
1763
1764        impl #struct_ident {
1765            pub const SECURE_FIELDS: [::appdb::crypto::SensitiveFieldMetadata; #secure_field_count] = [
1766                #( #secure_field_meta_entries, )*
1767            ];
1768
1769            pub fn encrypt(
1770                &self,
1771                context: &::appdb::crypto::CryptoContext,
1772            ) -> ::std::result::Result<#encrypted_ident, ::appdb::crypto::CryptoError> {
1773                <Self as ::appdb::Sensitive>::encrypt(self, context)
1774            }
1775        }
1776
1777        impl #encrypted_ident {
1778            pub fn decrypt(
1779                &self,
1780                context: &::appdb::crypto::CryptoContext,
1781            ) -> ::std::result::Result<#struct_ident, ::appdb::crypto::CryptoError> {
1782                <#struct_ident as ::appdb::Sensitive>::decrypt(self, context)
1783            }
1784        }
1785    })
1786}
1787
1788fn has_secure_attr(attrs: &[Attribute]) -> bool {
1789    attrs.iter().any(|attr| attr.path().is_ident("secure"))
1790}
1791
1792fn has_unique_attr(attrs: &[Attribute]) -> bool {
1793    attrs.iter().any(|attr| attr.path().is_ident("unique"))
1794}
1795
1796#[derive(Default, Clone)]
1797struct TypeCryptoConfig {
1798    service: Option<String>,
1799    account: Option<String>,
1800}
1801
1802#[derive(Default, Clone)]
1803struct FieldCryptoConfig {
1804    field_account: Option<String>,
1805}
1806
1807impl FieldCryptoConfig {
1808    fn is_present(&self) -> bool {
1809        self.field_account.is_some()
1810    }
1811}
1812
1813fn type_crypto_config(attrs: &[Attribute]) -> syn::Result<TypeCryptoConfig> {
1814    let mut config = TypeCryptoConfig::default();
1815    let mut seen = HashSet::new();
1816
1817    for attr in attrs {
1818        if !attr.path().is_ident("crypto") {
1819            continue;
1820        }
1821
1822        attr.parse_nested_meta(|meta| {
1823            let key = meta
1824                .path
1825                .get_ident()
1826                .cloned()
1827                .ok_or_else(|| meta.error("unsupported crypto attribute"))?;
1828
1829            if !seen.insert(key.to_string()) {
1830                return Err(meta.error("duplicate crypto attribute key"));
1831            }
1832
1833            let value = meta.value()?;
1834            let literal: syn::LitStr = value.parse()?;
1835            match key.to_string().as_str() {
1836                "service" => config.service = Some(literal.value()),
1837                "account" => config.account = Some(literal.value()),
1838                _ => {
1839                    return Err(
1840                        meta.error("unsupported crypto attribute; expected `service` or `account`")
1841                    );
1842                }
1843            }
1844            Ok(())
1845        })?;
1846    }
1847
1848    Ok(config)
1849}
1850
1851fn field_crypto_config(attrs: &[Attribute]) -> syn::Result<FieldCryptoConfig> {
1852    let mut config = FieldCryptoConfig::default();
1853    let mut seen = HashSet::new();
1854
1855    for attr in attrs {
1856        if attr.path().is_ident("crypto") {
1857            attr.parse_nested_meta(|meta| {
1858                let key = meta
1859                    .path
1860                    .get_ident()
1861                    .cloned()
1862                    .ok_or_else(|| meta.error("unsupported crypto attribute"))?;
1863
1864                if !seen.insert(key.to_string()) {
1865                    return Err(meta.error("duplicate crypto attribute key"));
1866                }
1867
1868                let value = meta.value()?;
1869                let literal: syn::LitStr = value.parse()?;
1870                match key.to_string().as_str() {
1871                    "field_account" => config.field_account = Some(literal.value()),
1872                    _ => {
1873                        return Err(meta.error(
1874                            "unsupported field crypto attribute; expected `field_account`",
1875                        ));
1876                    }
1877                }
1878                Ok(())
1879            })?;
1880        } else if attr.path().is_ident("secure") && matches!(attr.meta, Meta::List(_)) {
1881            return Err(Error::new_spanned(
1882                attr,
1883                "#[secure] does not accept arguments; use #[crypto(field_account = \"...\")] on the field",
1884            ));
1885        }
1886    }
1887
1888    Ok(config)
1889}
1890
1891fn table_alias_target(attrs: &[Attribute]) -> syn::Result<Option<Type>> {
1892    let mut target = None;
1893
1894    for attr in attrs {
1895        if !attr.path().is_ident("table_as") {
1896            continue;
1897        }
1898
1899        if target.is_some() {
1900            return Err(Error::new_spanned(
1901                attr,
1902                "duplicate #[table_as(...)] attribute is not supported",
1903            ));
1904        }
1905
1906        let parsed: Type = attr.parse_args().map_err(|_| {
1907            Error::new_spanned(attr, "#[table_as(...)] requires exactly one target type")
1908        })?;
1909
1910        match parsed {
1911            Type::Path(TypePath { ref path, .. }) if !path.segments.is_empty() => {
1912                target = Some(parsed);
1913            }
1914            _ => {
1915                return Err(Error::new_spanned(
1916                    parsed,
1917                    "#[table_as(...)] target must be a type path",
1918                ));
1919            }
1920        }
1921    }
1922
1923    Ok(target)
1924}
1925
1926enum ViewSourceConfig {
1927    Table {
1928        source_ty: Type,
1929    },
1930    Sql {
1931        params_ty: Type,
1932        sql: LitStr,
1933        result_idx: usize,
1934    },
1935}
1936
1937impl ViewSourceConfig {
1938    fn impl_tokens(&self) -> proc_macro2::TokenStream {
1939        match self {
1940            Self::Table { source_ty } => quote! {
1941                type Source = #source_ty;
1942                type Params = ();
1943            },
1944            Self::Sql {
1945                params_ty,
1946                sql,
1947                result_idx,
1948            } => quote! {
1949                type Source = ::appdb::model::meta::NoViewSource;
1950                type Params = #params_ty;
1951
1952                fn source_kind() -> ::appdb::model::meta::ViewSource {
1953                    ::appdb::model::meta::ViewSource::Sql
1954                }
1955
1956                fn sql() -> ::std::option::Option<&'static str> {
1957                    ::std::option::Option::Some(#sql)
1958                }
1959
1960                fn sql_result_index() -> usize {
1961                    #result_idx
1962                }
1963            },
1964        }
1965    }
1966
1967    fn methods_tokens(&self, struct_ident: &syn::Ident) -> proc_macro2::TokenStream {
1968        match self {
1969            Self::Table { .. } => quote! {},
1970            Self::Sql { params_ty, .. } => quote! {
1971                pub async fn query(params: #params_ty) -> ::anyhow::Result<::std::vec::Vec<#struct_ident>> {
1972                    ::appdb::repository::ViewRepo::<Self>::query(params).await
1973                }
1974            },
1975        }
1976    }
1977}
1978
1979fn view_source_config(attrs: &[Attribute]) -> syn::Result<ViewSourceConfig> {
1980    let mut source = None;
1981    let mut sql = None;
1982    let mut params = None;
1983    let mut result = None;
1984
1985    for attr in attrs {
1986        if !attr.path().is_ident("view") {
1987            continue;
1988        }
1989
1990        attr.parse_nested_meta(|meta| {
1991            if meta.path.is_ident("source") {
1992                if source.is_some() {
1993                    return Err(meta.error("duplicate #[view(source = ...)] attribute is not supported"));
1994                }
1995                let value = meta.value()?;
1996                let parsed: Type = value.parse()?;
1997                match parsed {
1998                    Type::Path(TypePath { ref path, .. }) if !path.segments.is_empty() => {
1999                        source = Some(parsed);
2000                        Ok(())
2001                    }
2002                    _ => Err(Error::new_spanned(
2003                        parsed,
2004                        "#[view(source = ...)] source must be a type path",
2005                    )),
2006                }
2007            } else if meta.path.is_ident("sql") {
2008                if sql.is_some() {
2009                    return Err(meta.error("duplicate #[view(sql = ...)] attribute is not supported"));
2010                }
2011                let value = meta.value()?;
2012                sql = Some(value.parse()?);
2013                Ok(())
2014            } else if meta.path.is_ident("params") {
2015                if params.is_some() {
2016                    return Err(meta.error("duplicate #[view(params = ...)] attribute is not supported"));
2017                }
2018                let value = meta.value()?;
2019                let parsed: Type = value.parse()?;
2020                match parsed {
2021                    Type::Path(TypePath { ref path, .. }) if !path.segments.is_empty() => {
2022                        params = Some(parsed);
2023                        Ok(())
2024                    }
2025                    _ => Err(Error::new_spanned(
2026                        parsed,
2027                        "#[view(params = ...)] params must be a type path",
2028                    )),
2029                }
2030            } else if meta.path.is_ident("result") {
2031                if result.is_some() {
2032                    return Err(meta.error("duplicate #[view(result = ...)] attribute is not supported"));
2033                }
2034                let value = meta.value()?;
2035                let parsed: LitInt = value.parse()?;
2036                result = Some(parsed.base10_parse::<usize>()?);
2037                Ok(())
2038            } else {
2039                Err(meta.error("unsupported view attribute; expected `source = Type`, `sql = \"...\"`, `params = Type`, or `result = N`"))
2040            }
2041        })?;
2042    }
2043
2044    match (sql, params, result) {
2045        (None, None, None) => {
2046            let source_ty = source.ok_or_else(|| {
2047                Error::new(
2048                    proc_macro2::Span::call_site(),
2049                    "View requires #[view(source = SourceStoreType)] or #[view(sql = \"...\", params = ParamsType)]",
2050                )
2051            })?;
2052            Ok(ViewSourceConfig::Table { source_ty })
2053        }
2054        (Some(sql), Some(params_ty), result_idx) => {
2055            if source.is_some() {
2056                return Err(Error::new(
2057                    proc_macro2::Span::call_site(),
2058                    "SQL View cannot combine #[view(source = ...)] with #[view(sql = ...)]",
2059                ));
2060            }
2061            Ok(ViewSourceConfig::Sql {
2062                params_ty,
2063                sql,
2064                result_idx: result_idx.unwrap_or(0),
2065            })
2066        }
2067        (Some(_), None, _) => Err(Error::new(
2068            proc_macro2::Span::call_site(),
2069            "SQL View requires #[view(params = ParamsType)]",
2070        )),
2071        (None, Some(_), _) | (None, _, Some(_)) => Err(Error::new(
2072            proc_macro2::Span::call_site(),
2073            "#[view(params = ...)] and #[view(result = ...)] require #[view(sql = \"...\")]",
2074        )),
2075    }
2076}
2077
2078fn field_view_nested_attr(field: &Field) -> syn::Result<bool> {
2079    let mut is_nested = false;
2080
2081    for attr in &field.attrs {
2082        if !attr.path().is_ident("view") {
2083            continue;
2084        }
2085
2086        attr.parse_nested_meta(|meta| {
2087            if meta.path.is_ident("nested") {
2088                if is_nested {
2089                    return Err(meta.error("duplicate #[view(nested)] marker is not supported"));
2090                }
2091                is_nested = true;
2092                Ok(())
2093            } else {
2094                Err(meta.error("unsupported view field attribute; expected #[view(nested)]"))
2095            }
2096        })?;
2097    }
2098
2099    Ok(is_nested)
2100}
2101
2102fn resolved_schema_table_name(struct_ident: &syn::Ident, table_alias: Option<&Type>) -> String {
2103    match table_alias {
2104        Some(Type::Path(type_path)) => type_path
2105            .path
2106            .segments
2107            .last()
2108            .map(|segment| to_snake_case(&segment.ident.to_string()))
2109            .unwrap_or_else(|| to_snake_case(&struct_ident.to_string())),
2110        Some(_) => to_snake_case(&struct_ident.to_string()),
2111        None => to_snake_case(&struct_ident.to_string()),
2112    }
2113}
2114
2115fn field_foreign_attr(field: &Field) -> syn::Result<Option<&Attribute>> {
2116    let mut foreign_attr = None;
2117
2118    for attr in &field.attrs {
2119        if !attr.path().is_ident("foreign") {
2120            continue;
2121        }
2122
2123        if foreign_attr.is_some() {
2124            return Err(Error::new_spanned(
2125                attr,
2126                "duplicate nested-ref attribute is not supported",
2127            ));
2128        }
2129
2130        foreign_attr = Some(attr);
2131    }
2132
2133    Ok(foreign_attr)
2134}
2135
2136fn field_relation_attr(field: &Field) -> syn::Result<Option<&Attribute>> {
2137    let mut relate_attr = None;
2138
2139    for attr in &field.attrs {
2140        if !attr.path().is_ident("relate") && !attr.path().is_ident("back_relate") {
2141            continue;
2142        }
2143
2144        if let Some(previous) = relate_attr {
2145            return Err(Error::new_spanned(
2146                attr,
2147                relation_attr_conflict_message(previous, attr),
2148            ));
2149        }
2150
2151        relate_attr = Some(attr);
2152    }
2153
2154    Ok(relate_attr)
2155}
2156
2157fn field_pagin_attr(field: &Field) -> syn::Result<Option<&Attribute>> {
2158    let mut pagin_attr = None;
2159
2160    for attr in &field.attrs {
2161        if !attr.path().is_ident("pagin") {
2162            continue;
2163        }
2164
2165        if pagin_attr.is_some() {
2166            return Err(Error::new_spanned(
2167                attr,
2168                "duplicate #[pagin] attribute is not supported",
2169            ));
2170        }
2171
2172        pagin_attr = Some(attr);
2173    }
2174
2175    Ok(pagin_attr)
2176}
2177
2178fn field_fill_attr(field: &Field) -> syn::Result<Option<&Attribute>> {
2179    let mut fill_attr = None;
2180
2181    for attr in &field.attrs {
2182        if !attr.path().is_ident("fill") {
2183            continue;
2184        }
2185
2186        if fill_attr.is_some() {
2187            return Err(Error::new_spanned(
2188                attr,
2189                "duplicate #[fill(...)] attribute is not supported",
2190            ));
2191        }
2192
2193        fill_attr = Some(attr);
2194    }
2195
2196    Ok(fill_attr)
2197}
2198
2199fn validate_foreign_field(field: &Field, attr: &Attribute) -> syn::Result<Type> {
2200    if attr.path().is_ident("foreign") {
2201        return foreign_leaf_type(&field.ty)
2202            .ok_or_else(|| Error::new_spanned(&field.ty, BINDREF_ACCEPTED_SHAPES));
2203    }
2204
2205    Err(Error::new_spanned(attr, "unsupported foreign attribute"))
2206}
2207
2208const BINDREF_ACCEPTED_SHAPES: &str = "#[foreign] supports recursive Option<_> / Vec<_> shapes whose leaf type implements appdb::Bridge";
2209
2210const BINDREF_BRIDGE_STORE_ONLY: &str =
2211    "#[foreign] leaf types must derive Store or #[derive(Bridge)] dispatcher enums";
2212
2213const RELATE_ACCEPTED_SHAPES: &str = "relation-backed fields support Child / Option<Child> / Vec<Child> / Option<Vec<Child>> shapes whose leaf type implements appdb::Bridge";
2214
2215const PAGIN_ACCEPTED_SHAPES: &str = "#[pagin] supports direct scalar fields plus Id/RecordId; Option<_> and Vec<_> wrappers are not supported";
2216
2217const FILL_ACCEPTED_SHAPES: &str = "#[fill(...)] fields must use appdb::AutoFill";
2218
2219#[derive(Clone, Copy)]
2220enum RelationFieldDirection {
2221    Outgoing,
2222    Incoming,
2223}
2224
2225#[derive(Clone, Copy)]
2226enum FillProvider {
2227    Now,
2228}
2229
2230impl RelationFieldDirection {
2231    fn attr_label(self) -> &'static str {
2232        match self {
2233            Self::Outgoing => "#[relate(...)]",
2234            Self::Incoming => "#[back_relate(...)]",
2235        }
2236    }
2237
2238    fn write_direction_tokens(self) -> proc_macro2::TokenStream {
2239        match self {
2240            Self::Outgoing => quote!(::appdb::RelationWriteDirection::Outgoing),
2241            Self::Incoming => quote!(::appdb::RelationWriteDirection::Incoming),
2242        }
2243    }
2244
2245    fn write_edges_tokens(self) -> proc_macro2::TokenStream {
2246        match self {
2247            Self::Outgoing => quote! {
2248                ids
2249                    .into_iter()
2250                    .enumerate()
2251                    .map(|(position, out)| ::appdb::graph::OrderedRelationEdge {
2252                        _in: ::std::option::Option::Some(record.clone()),
2253                        out,
2254                        position: position as i64,
2255                    })
2256                    .collect()
2257            },
2258            Self::Incoming => quote! {
2259                ids
2260                    .into_iter()
2261                    .enumerate()
2262                    .map(|(position, source)| ::appdb::graph::OrderedRelationEdge {
2263                        _in: ::std::option::Option::Some(source),
2264                        out: record.clone(),
2265                        position: position as i64,
2266                    })
2267                    .collect()
2268            },
2269        }
2270    }
2271
2272    fn load_edges_tokens(self, relation_name: &str) -> proc_macro2::TokenStream {
2273        match self {
2274            Self::Outgoing => quote! {
2275                ::appdb::graph::GraphRepo::out_edges(record.clone(), #relation_name)
2276                    .await?
2277                    .into_iter()
2278                    .map(|edge| edge.out)
2279                    .collect()
2280            },
2281            Self::Incoming => quote! {
2282                {
2283                    let mut ids = ::std::vec::Vec::new();
2284                    for edge in ::appdb::graph::GraphRepo::in_edges(record.clone(), #relation_name).await? {
2285                        let incoming = edge._in.ok_or_else(|| {
2286                            ::anyhow::anyhow!("back relate field received relation edge without `in` record id")
2287                        })?;
2288                        ids.push(incoming);
2289                    }
2290                    ids
2291                }
2292            },
2293        }
2294    }
2295}
2296
2297#[derive(Clone)]
2298struct ForeignField {
2299    ident: syn::Ident,
2300    kind: ForeignFieldKind,
2301}
2302
2303#[derive(Clone)]
2304struct ForeignFieldKind {
2305    original_ty: Type,
2306    stored_ty: Type,
2307}
2308
2309#[derive(Clone)]
2310struct RelateField {
2311    ident: syn::Ident,
2312    relation_name: String,
2313    field_ty: Type,
2314    direction: RelationFieldDirection,
2315}
2316
2317#[derive(Clone)]
2318struct PaginField {
2319    ident: syn::Ident,
2320}
2321
2322#[derive(Clone)]
2323struct FillField {
2324    ident: syn::Ident,
2325    provider: FillProvider,
2326}
2327
2328fn parse_foreign_field(field: &Field, attr: &Attribute) -> syn::Result<ForeignField> {
2329    validate_foreign_field(field, attr)?;
2330    let ident = field.ident.clone().expect("named field");
2331
2332    let kind = ForeignFieldKind {
2333        original_ty: field.ty.clone(),
2334        stored_ty: foreign_stored_type(&field.ty)
2335            .ok_or_else(|| Error::new_spanned(&field.ty, BINDREF_ACCEPTED_SHAPES))?,
2336    };
2337
2338    Ok(ForeignField { ident, kind })
2339}
2340
2341fn parse_relate_field(field: &Field, attr: &Attribute) -> syn::Result<RelateField> {
2342    let direction = parse_relation_direction(attr)?;
2343    let relation_name = attr
2344        .parse_args::<syn::LitStr>()
2345        .map_err(|_| {
2346            Error::new_spanned(
2347                attr,
2348                format!(
2349                    "{} requires exactly one string literal",
2350                    direction.attr_label()
2351                ),
2352            )
2353        })?
2354        .value();
2355    validate_relation_name_literal(&relation_name, attr, direction.attr_label())?;
2356
2357    validate_relate_field(field, attr)?;
2358
2359    Ok(RelateField {
2360        ident: field.ident.clone().expect("named field"),
2361        relation_name,
2362        field_ty: field.ty.clone(),
2363        direction,
2364    })
2365}
2366
2367fn parse_pagin_field(field: &Field, attr: &Attribute) -> syn::Result<PaginField> {
2368    validate_pagin_field(field, attr)?;
2369    Ok(PaginField {
2370        ident: field.ident.clone().expect("named field"),
2371    })
2372}
2373
2374fn parse_fill_field(field: &Field, attr: &Attribute) -> syn::Result<FillField> {
2375    let provider = validate_fill_field(field, attr)?;
2376    Ok(FillField {
2377        ident: field.ident.clone().expect("named field"),
2378        provider,
2379    })
2380}
2381
2382fn validate_relate_field(field: &Field, attr: &Attribute) -> syn::Result<Type> {
2383    if !attr.path().is_ident("relate") && !attr.path().is_ident("back_relate") {
2384        return Err(Error::new_spanned(attr, "unsupported relate attribute"));
2385    }
2386
2387    let accepted = relate_leaf_type(&field.ty).cloned().map(Type::Path);
2388
2389    accepted.ok_or_else(|| Error::new_spanned(&field.ty, RELATE_ACCEPTED_SHAPES))
2390}
2391
2392fn validate_pagin_field(field: &Field, attr: &Attribute) -> syn::Result<Type> {
2393    if !attr.path().is_ident("pagin") {
2394        return Err(Error::new_spanned(attr, "unsupported pagination attribute"));
2395    }
2396
2397    if pagination_leaf_type(&field.ty).is_none() {
2398        return Err(Error::new_spanned(&field.ty, PAGIN_ACCEPTED_SHAPES));
2399    }
2400
2401    Ok(field.ty.clone())
2402}
2403
2404fn validate_fill_field(field: &Field, attr: &Attribute) -> syn::Result<FillProvider> {
2405    if !attr.path().is_ident("fill") {
2406        return Err(Error::new_spanned(attr, "unsupported fill attribute"));
2407    }
2408
2409    if !is_autofill_type(&field.ty) {
2410        return Err(Error::new_spanned(&field.ty, FILL_ACCEPTED_SHAPES));
2411    }
2412
2413    parse_fill_provider(attr)
2414}
2415
2416fn parse_fill_provider(attr: &Attribute) -> syn::Result<FillProvider> {
2417    let provider = attr.parse_args::<syn::Path>().map_err(|_| {
2418        Error::new_spanned(
2419            attr,
2420            "#[fill(...)] requires exactly one provider like #[fill(now)]",
2421        )
2422    })?;
2423
2424    if provider.is_ident("now") {
2425        Ok(FillProvider::Now)
2426    } else {
2427        Err(Error::new_spanned(
2428            attr,
2429            "unsupported fill provider; expected #[fill(now)]",
2430        ))
2431    }
2432}
2433
2434fn relate_leaf_type(ty: &Type) -> Option<&TypePath> {
2435    if let Some(leaf) = direct_store_child_type(ty) {
2436        return Some(leaf);
2437    }
2438
2439    if let Some(inner) = option_inner_type(ty) {
2440        if let Some(leaf) = direct_store_child_type(inner) {
2441            return Some(leaf);
2442        }
2443
2444        if let Some(vec_inner) = vec_inner_type(inner) {
2445            return direct_store_child_type(vec_inner);
2446        }
2447
2448        return None;
2449    }
2450
2451    if let Some(inner) = vec_inner_type(ty) {
2452        return direct_store_child_type(inner);
2453    }
2454
2455    None
2456}
2457
2458fn pagination_leaf_type(ty: &Type) -> Option<Type> {
2459    if option_inner_type(ty).is_some() || vec_inner_type(ty).is_some() {
2460        return None;
2461    }
2462
2463    if is_id_type(ty)
2464        || is_record_id_type(ty)
2465        || is_autofill_type(ty)
2466        || is_string_type(ty)
2467        || is_common_non_store_leaf_type(ty)
2468    {
2469        return Some(ty.clone());
2470    }
2471
2472    None
2473}
2474
2475fn parse_relation_direction(attr: &Attribute) -> syn::Result<RelationFieldDirection> {
2476    if attr.path().is_ident("relate") {
2477        return Ok(RelationFieldDirection::Outgoing);
2478    }
2479    if attr.path().is_ident("back_relate") {
2480        return Ok(RelationFieldDirection::Incoming);
2481    }
2482
2483    Err(Error::new_spanned(attr, "unsupported relate attribute"))
2484}
2485
2486fn relation_attr_label(attr: &Attribute) -> &'static str {
2487    if attr.path().is_ident("back_relate") {
2488        "#[back_relate(...)]"
2489    } else {
2490        "#[relate(...)]"
2491    }
2492}
2493
2494fn relation_attr_conflict_message(previous: &Attribute, current: &Attribute) -> String {
2495    match (
2496        previous.path().is_ident("relate"),
2497        previous.path().is_ident("back_relate"),
2498        current.path().is_ident("relate"),
2499        current.path().is_ident("back_relate"),
2500    ) {
2501        (true, false, true, false) => {
2502            "duplicate #[relate(...)] attribute is not supported".to_owned()
2503        }
2504        (false, true, false, true) => {
2505            "duplicate #[back_relate(...)] attribute is not supported".to_owned()
2506        }
2507        _ => "#[relate(...)] cannot be combined with #[back_relate(...)]".to_owned(),
2508    }
2509}
2510
2511fn foreign_field_kind<'a>(
2512    ident: &syn::Ident,
2513    fields: &'a [ForeignField],
2514) -> Option<&'a ForeignFieldKind> {
2515    fields
2516        .iter()
2517        .find(|field| field.ident == *ident)
2518        .map(|field| &field.kind)
2519}
2520
2521fn stored_field_type(field: &Field, foreign_fields: &[ForeignField]) -> Type {
2522    let ident = field.ident.as_ref().expect("named field");
2523    match foreign_field_kind(ident, foreign_fields) {
2524        Some(ForeignFieldKind { stored_ty, .. }) => stored_ty.clone(),
2525        None => field.ty.clone(),
2526    }
2527}
2528
2529fn view_stored_type(field: &Field) -> syn::Result<Type> {
2530    if field_view_nested_attr(field)? {
2531        view_record_shape_type(&field.ty).ok_or_else(|| {
2532            Error::new_spanned(
2533                &field.ty,
2534                "#[view(nested)] fields must be a View type, Option<View>, Vec<View>, or nested Option/Vec wrappers",
2535            )
2536        })
2537    } else {
2538        Ok(field.ty.clone())
2539    }
2540}
2541
2542fn view_record_shape_type(ty: &Type) -> Option<Type> {
2543    if let Some(inner) = option_inner_type(ty) {
2544        let inner = view_record_shape_type(inner)?;
2545        return Some(syn::parse_quote!(::std::option::Option<#inner>));
2546    }
2547
2548    if let Some(inner) = vec_inner_type(ty) {
2549        let inner = view_record_shape_type(inner)?;
2550        return Some(syn::parse_quote!(::std::vec::Vec<#inner>));
2551    }
2552
2553    view_leaf_type(ty).map(|_| syn::parse_quote!(::surrealdb::types::RecordId))
2554}
2555
2556fn view_leaf_type(ty: &Type) -> Option<&TypePath> {
2557    let Type::Path(type_path) = ty else {
2558        return None;
2559    };
2560
2561    let segment = type_path.path.segments.last()?;
2562    if !matches!(segment.arguments, PathArguments::None) {
2563        return None;
2564    }
2565
2566    if is_id_type(ty)
2567        || is_record_id_type(ty)
2568        || is_autofill_type(ty)
2569        || is_string_type(ty)
2570        || is_common_non_store_leaf_type(ty)
2571    {
2572        return None;
2573    }
2574
2575    Some(type_path)
2576}
2577
2578fn foreign_stored_type(ty: &Type) -> Option<Type> {
2579    if let Some(inner) = option_inner_type(ty) {
2580        let inner = foreign_stored_type(inner)?;
2581        return Some(syn::parse_quote!(::std::option::Option<#inner>));
2582    }
2583
2584    if let Some(inner) = vec_inner_type(ty) {
2585        let inner = foreign_stored_type(inner)?;
2586        return Some(syn::parse_quote!(::std::vec::Vec<#inner>));
2587    }
2588
2589    direct_store_child_type(ty)
2590        .cloned()
2591        .map(|_| syn::parse_quote!(::surrealdb::types::RecordId))
2592}
2593
2594fn foreign_leaf_type(ty: &Type) -> Option<Type> {
2595    if let Some(inner) = option_inner_type(ty) {
2596        return foreign_leaf_type(inner);
2597    }
2598
2599    if let Some(inner) = vec_inner_type(ty) {
2600        return foreign_leaf_type(inner);
2601    }
2602
2603    direct_store_child_type(ty).cloned().map(Type::Path)
2604}
2605
2606fn invalid_foreign_leaf_type(ty: &Type) -> Option<Type> {
2607    let leaf = foreign_leaf_type(ty)?;
2608    match &leaf {
2609        Type::Path(type_path) => {
2610            let segment = type_path.path.segments.last()?;
2611            if matches!(segment.arguments, PathArguments::None) {
2612                None
2613            } else {
2614                Some(leaf)
2615            }
2616        }
2617        _ => Some(leaf),
2618    }
2619}
2620
2621fn direct_store_child_type(ty: &Type) -> Option<&TypePath> {
2622    let Type::Path(type_path) = ty else {
2623        return None;
2624    };
2625
2626    let segment = type_path.path.segments.last()?;
2627    if !matches!(segment.arguments, PathArguments::None) {
2628        return None;
2629    }
2630
2631    if is_id_type(ty)
2632        || is_autofill_type(ty)
2633        || is_string_type(ty)
2634        || is_common_non_store_leaf_type(ty)
2635    {
2636        return None;
2637    }
2638
2639    Some(type_path)
2640}
2641
2642fn is_common_non_store_leaf_type(ty: &Type) -> bool {
2643    matches!(
2644        ty,
2645        Type::Path(TypePath { path, .. })
2646            if path.is_ident("bool")
2647                || path.is_ident("u8")
2648                || path.is_ident("u16")
2649                || path.is_ident("u32")
2650                || path.is_ident("u64")
2651                || path.is_ident("u128")
2652                || path.is_ident("usize")
2653                || path.is_ident("i8")
2654                || path.is_ident("i16")
2655                || path.is_ident("i32")
2656                || path.is_ident("i64")
2657                || path.is_ident("i128")
2658                || path.is_ident("isize")
2659                || path.is_ident("f32")
2660                || path.is_ident("f64")
2661                || path.is_ident("char")
2662    )
2663}
2664
2665fn secure_field_count(fields: &syn::punctuated::Punctuated<Field, syn::token::Comma>) -> usize {
2666    fields
2667        .iter()
2668        .filter(|field| has_secure_attr(&field.attrs))
2669        .count()
2670}
2671
2672fn relation_name_override(attrs: &[Attribute]) -> syn::Result<Option<String>> {
2673    for attr in attrs {
2674        if !attr.path().is_ident("relation") {
2675            continue;
2676        }
2677
2678        let mut name = None;
2679        attr.parse_nested_meta(|meta| {
2680            if meta.path.is_ident("name") {
2681                let value = meta.value()?;
2682                let literal: syn::LitStr = value.parse()?;
2683                let relation_name = literal.value();
2684                validate_relation_name_literal(
2685                    &relation_name,
2686                    &literal,
2687                    "#[relation(name = ...)]",
2688                )?;
2689                name = Some(relation_name);
2690                Ok(())
2691            } else {
2692                Err(meta.error("unsupported relation attribute"))
2693            }
2694        })?;
2695        return Ok(name);
2696    }
2697
2698    Ok(None)
2699}
2700
2701fn validate_relation_name_literal<T: quote::ToTokens>(
2702    name: &str,
2703    tokens: T,
2704    label: &str,
2705) -> syn::Result<()> {
2706    if is_relation_identifier(name) {
2707        return Ok(());
2708    }
2709
2710    Err(Error::new_spanned(
2711        tokens,
2712        format!("{label} relation name must be a plain SurrealQL identifier"),
2713    ))
2714}
2715
2716fn is_relation_identifier(name: &str) -> bool {
2717    let mut bytes = name.bytes();
2718    let Some(first) = bytes.next() else {
2719        return false;
2720    };
2721
2722    matches!(first, b'A'..=b'Z' | b'a'..=b'z' | b'_')
2723        && bytes.all(|byte| matches!(byte, b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'_'))
2724}
2725
2726enum SecureKind {
2727    Shape(Type),
2728}
2729
2730impl SecureKind {
2731    fn encrypted_type(&self) -> proc_macro2::TokenStream {
2732        match self {
2733            SecureKind::Shape(ty) => quote! { <#ty as ::appdb::SensitiveShape>::Encrypted },
2734        }
2735    }
2736
2737    fn encrypt_with_context_expr(&self, ident: &syn::Ident) -> proc_macro2::TokenStream {
2738        match self {
2739            SecureKind::Shape(ty) => {
2740                quote! { <#ty as ::appdb::SensitiveShape>::encrypt_with_context(&self.#ident, context)? }
2741            }
2742        }
2743    }
2744
2745    fn decrypt_with_context_expr(&self, ident: &syn::Ident) -> proc_macro2::TokenStream {
2746        match self {
2747            SecureKind::Shape(ty) => {
2748                quote! { <#ty as ::appdb::SensitiveShape>::decrypt_with_context(&encrypted.#ident, context)? }
2749            }
2750        }
2751    }
2752
2753    fn encrypt_with_runtime_expr(
2754        &self,
2755        ident: &syn::Ident,
2756        field_tag_ident: &syn::Ident,
2757    ) -> proc_macro2::TokenStream {
2758        match self {
2759            SecureKind::Shape(ty) => {
2760                quote! {{
2761                    let context = ::appdb::crypto::resolve_crypto_context_for::<#field_tag_ident>()?;
2762                    <#ty as ::appdb::SensitiveShape>::encrypt_with_context(&self.#ident, context.as_ref())?
2763                }}
2764            }
2765        }
2766    }
2767
2768    fn decrypt_with_runtime_expr(
2769        &self,
2770        ident: &syn::Ident,
2771        field_tag_ident: &syn::Ident,
2772    ) -> proc_macro2::TokenStream {
2773        match self {
2774            SecureKind::Shape(ty) => {
2775                quote! {{
2776                    let context = ::appdb::crypto::resolve_crypto_context_for::<#field_tag_ident>()?;
2777                    <#ty as ::appdb::SensitiveShape>::decrypt_with_context(&encrypted.#ident, context.as_ref())?
2778                }}
2779            }
2780        }
2781    }
2782}
2783
2784fn secure_kind(field: &Field) -> syn::Result<SecureKind> {
2785    if secure_shape_supported(&field.ty) {
2786        return Ok(SecureKind::Shape(field.ty.clone()));
2787    }
2788
2789    Err(Error::new_spanned(
2790        &field.ty,
2791        secure_shape_error_message(&field.ty),
2792    ))
2793}
2794
2795fn secure_shape_supported(ty: &Type) -> bool {
2796    if is_string_type(ty) {
2797        return true;
2798    }
2799
2800    if sensitive_value_wrapper_inner_type(ty).is_some() {
2801        return true;
2802    }
2803
2804    if let Some(inner) = option_inner_type(ty) {
2805        return secure_shape_supported(inner);
2806    }
2807
2808    if let Some(inner) = vec_inner_type(ty) {
2809        return secure_shape_supported(inner);
2810    }
2811
2812    direct_sensitive_child_type(ty).is_some()
2813}
2814
2815fn secure_shape_error_message(ty: &Type) -> &'static str {
2816    if invalid_secure_leaf_type(ty).is_some() {
2817        "#[secure] child shapes require a direct named Sensitive type leaf with only Option<_> and Vec<_> wrappers"
2818    } else {
2819        "#[secure] supports String, appdb::SensitiveValueOf<T>, and recursive Child / Option<Child> / Vec<Child> shapes where Child implements appdb::Sensitive"
2820    }
2821}
2822
2823fn direct_sensitive_child_type(ty: &Type) -> Option<&TypePath> {
2824    let Type::Path(type_path) = ty else {
2825        return None;
2826    };
2827
2828    let segment = type_path.path.segments.last()?;
2829    if !matches!(segment.arguments, PathArguments::None) {
2830        return None;
2831    }
2832
2833    if is_id_type(ty) || is_string_type(ty) || is_common_non_store_leaf_type(ty) {
2834        return None;
2835    }
2836
2837    Some(type_path)
2838}
2839
2840fn invalid_secure_leaf_type(ty: &Type) -> Option<Type> {
2841    if let Some(inner) = option_inner_type(ty) {
2842        return invalid_secure_leaf_type(inner);
2843    }
2844
2845    if let Some(inner) = vec_inner_type(ty) {
2846        return invalid_secure_leaf_type(inner);
2847    }
2848
2849    let leaf = direct_sensitive_child_type(ty)?.clone();
2850    let segment = leaf.path.segments.last()?;
2851    if matches!(segment.arguments, PathArguments::None) {
2852        None
2853    } else {
2854        Some(Type::Path(leaf))
2855    }
2856}
2857
2858fn is_string_type(ty: &Type) -> bool {
2859    match ty {
2860        Type::Path(TypePath { path, .. }) => path.is_ident("String"),
2861        _ => false,
2862    }
2863}
2864
2865fn is_id_type(ty: &Type) -> bool {
2866    match ty {
2867        Type::Path(TypePath { path, .. }) => path.segments.last().is_some_and(|segment| {
2868            let ident = segment.ident.to_string();
2869            ident == "Id"
2870        }),
2871        _ => false,
2872    }
2873}
2874
2875fn is_autofill_type(ty: &Type) -> bool {
2876    match ty {
2877        Type::Path(TypePath { path, .. }) => path
2878            .segments
2879            .last()
2880            .is_some_and(|segment| segment.ident == "AutoFill"),
2881        _ => false,
2882    }
2883}
2884
2885fn is_record_id_type(ty: &Type) -> bool {
2886    match ty {
2887        Type::Path(TypePath { path, .. }) => path.segments.last().is_some_and(|segment| {
2888            let ident = segment.ident.to_string();
2889            ident == "RecordId"
2890        }),
2891        _ => false,
2892    }
2893}
2894
2895fn option_inner_type(ty: &Type) -> Option<&Type> {
2896    let Type::Path(TypePath { path, .. }) = ty else {
2897        return None;
2898    };
2899    let segment = path.segments.last()?;
2900    if segment.ident != "Option" {
2901        return None;
2902    }
2903    let PathArguments::AngleBracketed(args) = &segment.arguments else {
2904        return None;
2905    };
2906    let GenericArgument::Type(inner) = args.args.first()? else {
2907        return None;
2908    };
2909    Some(inner)
2910}
2911
2912fn vec_inner_type(ty: &Type) -> Option<&Type> {
2913    let Type::Path(TypePath { path, .. }) = ty else {
2914        return None;
2915    };
2916    let segment = path.segments.last()?;
2917    if segment.ident != "Vec" {
2918        return None;
2919    }
2920    let PathArguments::AngleBracketed(args) = &segment.arguments else {
2921        return None;
2922    };
2923    let GenericArgument::Type(inner) = args.args.first()? else {
2924        return None;
2925    };
2926    Some(inner)
2927}
2928
2929fn sensitive_value_wrapper_inner_type(ty: &Type) -> Option<&Type> {
2930    let Type::Path(TypePath { path, .. }) = ty else {
2931        return None;
2932    };
2933    let segment = path.segments.last()?;
2934    if segment.ident != "SensitiveValueOf" {
2935        return None;
2936    }
2937    let PathArguments::AngleBracketed(args) = &segment.arguments else {
2938        return None;
2939    };
2940    let GenericArgument::Type(inner) = args.args.first()? else {
2941        return None;
2942    };
2943    Some(inner)
2944}
2945
2946fn to_snake_case(input: &str) -> String {
2947    let mut out = String::with_capacity(input.len() + 4);
2948    let mut prev_is_lower_or_digit = false;
2949
2950    for ch in input.chars() {
2951        if ch.is_ascii_uppercase() {
2952            if prev_is_lower_or_digit {
2953                out.push('_');
2954            }
2955            out.push(ch.to_ascii_lowercase());
2956            prev_is_lower_or_digit = false;
2957        } else {
2958            out.push(ch);
2959            prev_is_lower_or_digit = ch.is_ascii_lowercase() || ch.is_ascii_digit();
2960        }
2961    }
2962
2963    out
2964}
2965
2966fn to_pascal_case(input: &str) -> String {
2967    let mut out = String::with_capacity(input.len());
2968    let mut uppercase_next = true;
2969
2970    for ch in input.chars() {
2971        if ch == '_' || ch == '-' {
2972            uppercase_next = true;
2973            continue;
2974        }
2975
2976        if uppercase_next {
2977            out.push(ch.to_ascii_uppercase());
2978            uppercase_next = false;
2979        } else {
2980            out.push(ch);
2981        }
2982    }
2983
2984    out
2985}