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