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