Skip to main content

appdb_macros/
lib.rs

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