Skip to main content

appdb_macros/
lib.rs

1use proc_macro::TokenStream;
2use quote::{format_ident, quote};
3use syn::{
4    parse_macro_input, Attribute, Data, DeriveInput, Error, Field, Fields, GenericArgument,
5    PathArguments, Type, TypePath,
6};
7
8#[proc_macro_derive(Sensitive, attributes(secure))]
9pub fn derive_sensitive(input: TokenStream) -> TokenStream {
10    match derive_sensitive_impl(parse_macro_input!(input as DeriveInput)) {
11        Ok(tokens) => tokens.into(),
12        Err(err) => err.to_compile_error().into(),
13    }
14}
15
16#[proc_macro_derive(Store, attributes(unique, secure, foreign, table_as))]
17pub fn derive_store(input: TokenStream) -> TokenStream {
18    match derive_store_impl(parse_macro_input!(input as DeriveInput)) {
19        Ok(tokens) => tokens.into(),
20        Err(err) => err.to_compile_error().into(),
21    }
22}
23
24#[proc_macro_derive(Relation, attributes(relation))]
25pub fn derive_relation(input: TokenStream) -> TokenStream {
26    match derive_relation_impl(parse_macro_input!(input as DeriveInput)) {
27        Ok(tokens) => tokens.into(),
28        Err(err) => err.to_compile_error().into(),
29    }
30}
31
32#[proc_macro_derive(Bridge)]
33pub fn derive_bridge(input: TokenStream) -> TokenStream {
34    match derive_bridge_impl(parse_macro_input!(input as DeriveInput)) {
35        Ok(tokens) => tokens.into(),
36        Err(err) => err.to_compile_error().into(),
37    }
38}
39
40fn derive_store_impl(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
41    let struct_ident = input.ident;
42    let vis = input.vis.clone();
43    let table_alias = table_alias_target(&input.attrs)?;
44
45    let named_fields = match input.data {
46        Data::Struct(data) => match data.fields {
47            Fields::Named(fields) => fields.named,
48            _ => {
49                return Err(Error::new_spanned(
50                    struct_ident,
51                    "Store can only be derived for structs with named fields",
52                ))
53            }
54        },
55        _ => {
56            return Err(Error::new_spanned(
57                struct_ident,
58                "Store can only be derived for structs",
59            ))
60        }
61    };
62
63    let id_fields = named_fields
64        .iter()
65        .filter(|field| is_id_type(&field.ty))
66        .map(|field| field.ident.clone().expect("named field"))
67        .collect::<Vec<_>>();
68
69    let secure_fields = named_fields
70        .iter()
71        .filter(|field| has_secure_attr(&field.attrs))
72        .map(|field| field.ident.clone().expect("named field"))
73        .collect::<Vec<_>>();
74
75    let unique_fields = named_fields
76        .iter()
77        .filter(|field| has_unique_attr(&field.attrs))
78        .map(|field| field.ident.clone().expect("named field"))
79        .collect::<Vec<_>>();
80
81    if id_fields.len() > 1 {
82        return Err(Error::new_spanned(
83            struct_ident,
84            "Store supports at most one `Id` field for automatic HasId generation",
85        ));
86    }
87
88    if let Some(invalid_field) = named_fields
89        .iter()
90        .find(|field| has_secure_attr(&field.attrs) && has_unique_attr(&field.attrs))
91    {
92        let ident = invalid_field.ident.as_ref().expect("named field");
93        return Err(Error::new_spanned(
94            ident,
95            "#[secure] fields cannot be used as #[unique] lookup keys",
96        ));
97    }
98
99    let foreign_fields = named_fields
100        .iter()
101        .filter_map(|field| match field_foreign_attr(field) {
102            Ok(Some(attr)) => Some(parse_foreign_field(field, attr)),
103            Ok(None) => None,
104            Err(err) => Some(Err(err)),
105        })
106        .collect::<syn::Result<Vec<_>>>()?;
107
108    if let Some(non_store_child) = foreign_fields
109        .iter()
110        .find_map(|field| invalid_foreign_leaf_type(&field.kind.original_ty))
111    {
112        return Err(Error::new_spanned(
113            non_store_child,
114            BINDREF_BRIDGE_STORE_ONLY,
115        ));
116    }
117
118    if let Some(invalid_field) = named_fields.iter().find(|field| {
119        field_foreign_attr(field).ok().flatten().is_some() && has_unique_attr(&field.attrs)
120    }) {
121        let ident = invalid_field.ident.as_ref().expect("named field");
122        return Err(Error::new_spanned(
123            ident,
124            "#[foreign] fields cannot be used as #[unique] lookup keys",
125        ));
126    }
127
128    let auto_has_id_impl = id_fields.first().map(|field| {
129        quote! {
130            impl ::appdb::model::meta::HasId for #struct_ident {
131                fn id(&self) -> ::surrealdb::types::RecordId {
132                    ::surrealdb::types::RecordId::new(
133                        <Self as ::appdb::model::meta::ModelMeta>::storage_table(),
134                        self.#field.clone(),
135                    )
136                }
137            }
138        }
139    });
140
141    let resolve_record_id_impl = if let Some(field) = id_fields.first() {
142        quote! {
143            #[::async_trait::async_trait]
144            impl ::appdb::model::meta::ResolveRecordId for #struct_ident {
145                async fn resolve_record_id(&self) -> ::anyhow::Result<::surrealdb::types::RecordId> {
146                    Ok(::surrealdb::types::RecordId::new(
147                        <Self as ::appdb::model::meta::ModelMeta>::storage_table(),
148                        self.#field.clone(),
149                    ))
150                }
151            }
152        }
153    } else {
154        quote! {
155            #[::async_trait::async_trait]
156            impl ::appdb::model::meta::ResolveRecordId for #struct_ident {
157                async fn resolve_record_id(&self) -> ::anyhow::Result<::surrealdb::types::RecordId> {
158                    ::appdb::repository::Repo::<Self>::find_unique_id_for(self).await
159                }
160            }
161        }
162    };
163
164    let resolved_table_name_expr = if let Some(target_ty) = &table_alias {
165        quote! { <#target_ty as ::appdb::model::meta::ModelMeta>::table_name() }
166    } else {
167        quote! {
168            {
169                let table = ::appdb::model::meta::default_table_name(stringify!(#struct_ident));
170                ::appdb::model::meta::register_table(stringify!(#struct_ident), table)
171            }
172        }
173    };
174
175    let unique_schema_impls = unique_fields.iter().map(|field| {
176        let field_name = field.to_string();
177        let index_name = format!(
178            "{}_{}_unique",
179            resolved_schema_table_name(&struct_ident, table_alias.as_ref()),
180            field_name
181        );
182        let ddl = format!(
183            "DEFINE INDEX IF NOT EXISTS {index_name} ON {} FIELDS {field_name} UNIQUE;",
184            resolved_schema_table_name(&struct_ident, table_alias.as_ref())
185        );
186
187        quote! {
188            ::inventory::submit! {
189                ::appdb::model::schema::SchemaItem {
190                    ddl: #ddl,
191                }
192            }
193        }
194    });
195
196    let lookup_fields = if unique_fields.is_empty() {
197        named_fields
198            .iter()
199            .filter_map(|field| {
200                let ident = field.ident.as_ref()?;
201                if ident == "id"
202                    || secure_fields.iter().any(|secure| secure == ident)
203                    || foreign_fields.iter().any(|foreign| foreign.ident == *ident)
204                {
205                    None
206                } else {
207                    Some(ident.to_string())
208                }
209            })
210            .collect::<Vec<_>>()
211    } else {
212        unique_fields
213            .iter()
214            .map(|field| field.to_string())
215            .collect::<Vec<_>>()
216    };
217
218    let foreign_field_literals = foreign_fields
219        .iter()
220        .map(|field| field.ident.to_string())
221        .map(|field| quote! { #field });
222    if id_fields.is_empty() && lookup_fields.is_empty() {
223        return Err(Error::new_spanned(
224            struct_ident,
225            "Store requires an `Id` field or at least one non-secure lookup field for automatic record resolution",
226        ));
227    }
228    let lookup_field_literals = lookup_fields.iter().map(|field| quote! { #field });
229
230    let stored_model_impl = if !foreign_fields.is_empty() {
231        quote! {}
232    } else if secure_field_count(&named_fields) > 0 {
233        quote! {
234            impl ::appdb::StoredModel for #struct_ident {
235                type Stored = <Self as ::appdb::Sensitive>::Encrypted;
236
237                fn into_stored(self) -> ::anyhow::Result<Self::Stored> {
238                    <Self as ::appdb::Sensitive>::encrypt_with_runtime_resolver(&self)
239                        .map_err(::anyhow::Error::from)
240                }
241
242                fn from_stored(stored: Self::Stored) -> ::anyhow::Result<Self> {
243                    <Self as ::appdb::Sensitive>::decrypt_with_runtime_resolver(&stored)
244                        .map_err(::anyhow::Error::from)
245                }
246
247                fn supports_create_return_id() -> bool {
248                    false
249                }
250            }
251        }
252    } else {
253        quote! {
254            impl ::appdb::StoredModel for #struct_ident {
255                type Stored = Self;
256
257                fn into_stored(self) -> ::anyhow::Result<Self::Stored> {
258                    ::std::result::Result::Ok(self)
259                }
260
261                fn from_stored(stored: Self::Stored) -> ::anyhow::Result<Self> {
262                    ::std::result::Result::Ok(stored)
263                }
264            }
265        }
266    };
267
268    let stored_fields = named_fields.iter().map(|field| {
269        let ident = field.ident.clone().expect("named field");
270        let ty = stored_field_type(field, &foreign_fields);
271        if is_record_id_type(&ty) {
272            quote! {
273                #[serde(deserialize_with = "::appdb::serde_utils::id::deserialize_record_id_or_compat_string")]
274                #ident: #ty
275            }
276        } else {
277            quote! { #ident: #ty }
278        }
279    });
280
281    let into_stored_assignments = named_fields.iter().map(|field| {
282        let ident = field.ident.clone().expect("named field");
283        match foreign_field_kind(&ident, &foreign_fields) {
284            Some(ForeignFieldKind { original_ty, .. }) => quote! {
285                #ident: <#original_ty as ::appdb::ForeignShape>::persist_foreign_shape(value.#ident).await?
286            },
287            None => quote! { #ident: value.#ident },
288        }
289    });
290
291    let from_stored_assignments = named_fields.iter().map(|field| {
292        let ident = field.ident.clone().expect("named field");
293        match foreign_field_kind(&ident, &foreign_fields) {
294            Some(ForeignFieldKind { original_ty, .. }) => quote! {
295                #ident: <#original_ty as ::appdb::ForeignShape>::hydrate_foreign_shape(stored.#ident).await?
296            },
297            None => quote! { #ident: stored.#ident },
298        }
299    });
300
301    let decode_foreign_fields = foreign_fields.iter().map(|field| {
302        let ident = field.ident.to_string();
303        quote! {
304            if let ::std::option::Option::Some(value) = map.get_mut(#ident) {
305                ::appdb::decode_stored_record_links(value);
306            }
307        }
308    });
309
310    let foreign_model_impl = if foreign_fields.is_empty() {
311        quote! {
312            impl ::appdb::ForeignModel for #struct_ident {
313                async fn persist_foreign(value: Self) -> ::anyhow::Result<Self::Stored> {
314                    <Self as ::appdb::StoredModel>::into_stored(value)
315                }
316
317                async fn hydrate_foreign(stored: Self::Stored) -> ::anyhow::Result<Self> {
318                    <Self as ::appdb::StoredModel>::from_stored(stored)
319                }
320
321                fn decode_stored_row(
322                    row: ::surrealdb::types::Value,
323                ) -> ::anyhow::Result<Self::Stored>
324                where
325                    Self::Stored: ::serde::de::DeserializeOwned,
326                {
327                    Ok(::serde_json::from_value(row.into_json_value())?)
328                }
329            }
330        }
331    } else {
332        let stored_struct_ident = format_ident!("AppdbStored{}", struct_ident);
333        quote! {
334            #[derive(
335                Debug,
336                Clone,
337                ::serde::Serialize,
338                ::serde::Deserialize,
339                ::surrealdb::types::SurrealValue,
340            )]
341            #vis struct #stored_struct_ident {
342                #( #stored_fields, )*
343            }
344
345            impl ::appdb::StoredModel for #struct_ident {
346                type Stored = #stored_struct_ident;
347
348                fn into_stored(self) -> ::anyhow::Result<Self::Stored> {
349                    unreachable!("foreign fields require async persist_foreign")
350                }
351
352                fn from_stored(_stored: Self::Stored) -> ::anyhow::Result<Self> {
353                    unreachable!("foreign fields require async hydrate_foreign")
354                }
355            }
356
357            impl ::appdb::ForeignModel for #struct_ident {
358                async fn persist_foreign(value: Self) -> ::anyhow::Result<Self::Stored> {
359                    let value = value;
360                    Ok(#stored_struct_ident {
361                        #( #into_stored_assignments, )*
362                    })
363                }
364
365                async fn hydrate_foreign(stored: Self::Stored) -> ::anyhow::Result<Self> {
366                    Ok(Self {
367                        #( #from_stored_assignments, )*
368                    })
369                }
370
371                fn has_foreign_fields() -> bool {
372                    true
373                }
374
375                fn decode_stored_row(
376                    row: ::surrealdb::types::Value,
377                ) -> ::anyhow::Result<Self::Stored>
378                where
379                    Self::Stored: ::serde::de::DeserializeOwned,
380                {
381                    let mut row = row.into_json_value();
382                    if let ::serde_json::Value::Object(map) = &mut row {
383                        #( #decode_foreign_fields )*
384                    }
385                    Ok(::serde_json::from_value(row)?)
386                }
387            }
388        }
389    };
390
391    let store_marker_ident = format_ident!("AppdbStoreMarker{}", struct_ident);
392
393    Ok(quote! {
394        #[doc(hidden)]
395        #vis struct #store_marker_ident;
396
397        impl ::appdb::model::meta::ModelMeta for #struct_ident {
398            fn storage_table() -> &'static str {
399                #resolved_table_name_expr
400            }
401
402            fn table_name() -> &'static str {
403                static TABLE_NAME: ::std::sync::OnceLock<&'static str> = ::std::sync::OnceLock::new();
404                TABLE_NAME.get_or_init(|| {
405                    let table = #resolved_table_name_expr;
406                    ::appdb::model::meta::register_table(stringify!(#struct_ident), table)
407                })
408            }
409        }
410
411        impl ::appdb::model::meta::StoreModelMarker for #struct_ident {}
412        impl ::appdb::model::meta::StoreModelMarker for #store_marker_ident {}
413
414        impl ::appdb::model::meta::UniqueLookupMeta for #struct_ident {
415            fn lookup_fields() -> &'static [&'static str] {
416                &[ #( #lookup_field_literals ),* ]
417            }
418
419            fn foreign_fields() -> &'static [&'static str] {
420                &[ #( #foreign_field_literals ),* ]
421            }
422        }
423        #stored_model_impl
424        #foreign_model_impl
425
426        #auto_has_id_impl
427        #resolve_record_id_impl
428
429        #( #unique_schema_impls )*
430
431        impl ::appdb::repository::Crud for #struct_ident {}
432
433        impl #struct_ident {
434            /// Saves one value through the recommended Store CRUD surface.
435            ///
436            /// Prefer these generated model methods in application code. Lower-level
437            /// `appdb::repository::Repo` helpers exist for library internals and
438            /// advanced integration seams, not as the primary public path.
439            pub async fn save(self) -> ::anyhow::Result<Self> {
440                <Self as ::appdb::repository::Crud>::save(self).await
441            }
442
443            /// Saves many values through the recommended Store CRUD surface.
444            pub async fn save_many(data: ::std::vec::Vec<Self>) -> ::anyhow::Result<::std::vec::Vec<Self>> {
445                <Self as ::appdb::repository::Crud>::save_many(data).await
446            }
447
448            pub async fn get<T>(id: T) -> ::anyhow::Result<Self>
449            where
450                ::surrealdb::types::RecordIdKey: From<T>,
451                T: Send,
452            {
453                ::appdb::repository::Repo::<Self>::get(id).await
454            }
455
456            pub async fn list() -> ::anyhow::Result<::std::vec::Vec<Self>> {
457                ::appdb::repository::Repo::<Self>::list().await
458            }
459
460            pub async fn list_limit(count: i64) -> ::anyhow::Result<::std::vec::Vec<Self>> {
461                ::appdb::repository::Repo::<Self>::list_limit(count).await
462            }
463
464            pub async fn delete_all() -> ::anyhow::Result<()> {
465                ::appdb::repository::Repo::<Self>::delete_all().await
466            }
467
468            pub async fn find_one_id(
469                k: &str,
470                v: &str,
471            ) -> ::anyhow::Result<::surrealdb::types::RecordId> {
472                ::appdb::repository::Repo::<Self>::find_one_id(k, v).await
473            }
474
475            pub async fn list_record_ids() -> ::anyhow::Result<::std::vec::Vec<::surrealdb::types::RecordId>> {
476                ::appdb::repository::Repo::<Self>::list_record_ids().await
477            }
478
479            pub async fn create_at(
480                id: ::surrealdb::types::RecordId,
481                data: Self,
482            ) -> ::anyhow::Result<Self> {
483                ::appdb::repository::Repo::<Self>::create_at(id, data).await
484            }
485
486            pub async fn upsert_at(
487                id: ::surrealdb::types::RecordId,
488                data: Self,
489            ) -> ::anyhow::Result<Self> {
490                ::appdb::repository::Repo::<Self>::upsert_at(id, data).await
491            }
492
493            pub async fn update_at(
494                self,
495                id: ::surrealdb::types::RecordId,
496            ) -> ::anyhow::Result<Self> {
497                ::appdb::repository::Repo::<Self>::update_at(id, self).await
498            }
499
500
501            pub async fn delete<T>(id: T) -> ::anyhow::Result<()>
502            where
503                ::surrealdb::types::RecordIdKey: From<T>,
504                T: Send,
505            {
506                ::appdb::repository::Repo::<Self>::delete(id).await
507            }
508        }
509    })
510}
511
512fn derive_bridge_impl(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
513    let enum_ident = input.ident;
514
515    let variants = match input.data {
516        Data::Enum(data) => data.variants,
517        _ => {
518            return Err(Error::new_spanned(
519                enum_ident,
520                "Bridge can only be derived for enums",
521            ))
522        }
523    };
524
525    let payloads = variants
526        .iter()
527        .map(parse_bridge_variant)
528        .collect::<syn::Result<Vec<_>>>()?;
529
530    let from_impls = payloads.iter().map(|variant| {
531        let variant_ident = &variant.variant_ident;
532        let payload_ty = &variant.payload_ty;
533
534        quote! {
535            impl ::std::convert::From<#payload_ty> for #enum_ident {
536                fn from(value: #payload_ty) -> Self {
537                    Self::#variant_ident(value)
538                }
539            }
540        }
541    });
542
543    let persist_match_arms = payloads.iter().map(|variant| {
544        let variant_ident = &variant.variant_ident;
545
546        quote! {
547            Self::#variant_ident(value) => <_ as ::appdb::Bridge>::persist_foreign(value).await,
548        }
549    });
550
551    let hydrate_match_arms = payloads.iter().map(|variant| {
552        let variant_ident = &variant.variant_ident;
553        let payload_ty = &variant.payload_ty;
554
555        quote! {
556            table if table == <#payload_ty as ::appdb::model::meta::ModelMeta>::storage_table() => {
557                ::std::result::Result::Ok(Self::#variant_ident(
558                    <#payload_ty as ::appdb::Bridge>::hydrate_foreign(id).await?,
559                ))
560            }
561        }
562    });
563
564    Ok(quote! {
565        #( #from_impls )*
566
567        #[::async_trait::async_trait]
568        impl ::appdb::Bridge for #enum_ident {
569            async fn persist_foreign(self) -> ::anyhow::Result<::surrealdb::types::RecordId> {
570                match self {
571                    #( #persist_match_arms )*
572                }
573            }
574
575            async fn hydrate_foreign(
576                id: ::surrealdb::types::RecordId,
577            ) -> ::anyhow::Result<Self> {
578                match id.table.to_string().as_str() {
579                    #( #hydrate_match_arms, )*
580                    table => ::anyhow::bail!(
581                        "unsupported foreign table `{table}` for enum dispatcher `{}`",
582                        ::std::stringify!(#enum_ident)
583                    ),
584                }
585            }
586        }
587    })
588}
589
590#[derive(Clone)]
591struct BridgeVariant {
592    variant_ident: syn::Ident,
593    payload_ty: Type,
594}
595
596fn parse_bridge_variant(variant: &syn::Variant) -> syn::Result<BridgeVariant> {
597    let payload_ty = match &variant.fields {
598        Fields::Unnamed(fields) if fields.unnamed.len() == 1 => {
599            fields.unnamed.first().expect("single field").ty.clone()
600        }
601        Fields::Unnamed(_) => {
602            return Err(Error::new_spanned(
603                &variant.ident,
604                "Bridge variants must be single-field tuple variants",
605            ))
606        }
607        Fields::Unit => {
608            return Err(Error::new_spanned(
609                &variant.ident,
610                "Bridge does not support unit variants",
611            ))
612        }
613        Fields::Named(_) => {
614            return Err(Error::new_spanned(
615                &variant.ident,
616                "Bridge does not support struct variants",
617            ))
618        }
619    };
620
621    let payload_path = match &payload_ty {
622        Type::Path(path) => path,
623        _ => {
624            return Err(Error::new_spanned(
625                &payload_ty,
626                "Bridge payload must implement appdb::Bridge",
627            ))
628        }
629    };
630
631    let segment = payload_path.path.segments.last().ok_or_else(|| {
632        Error::new_spanned(&payload_ty, "Bridge payload must implement appdb::Bridge")
633    })?;
634
635    if !matches!(segment.arguments, PathArguments::None) {
636        return Err(Error::new_spanned(
637            &payload_ty,
638            "Bridge payload must implement appdb::Bridge",
639        ));
640    }
641
642    Ok(BridgeVariant {
643        variant_ident: variant.ident.clone(),
644        payload_ty,
645    })
646}
647
648fn derive_relation_impl(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
649    let struct_ident = input.ident;
650    let relation_name = relation_name_override(&input.attrs)?
651        .unwrap_or_else(|| to_snake_case(&struct_ident.to_string()));
652
653    match input.data {
654        Data::Struct(data) => {
655            match data.fields {
656                Fields::Unit | Fields::Named(_) => {}
657                _ => return Err(Error::new_spanned(
658                    struct_ident,
659                    "Relation can only be derived for unit structs or structs with named fields",
660                )),
661            }
662        }
663        _ => {
664            return Err(Error::new_spanned(
665                struct_ident,
666                "Relation can only be derived for structs",
667            ))
668        }
669    }
670
671    Ok(quote! {
672        impl ::appdb::model::relation::RelationMeta for #struct_ident {
673            fn relation_name() -> &'static str {
674                static REL_NAME: ::std::sync::OnceLock<&'static str> = ::std::sync::OnceLock::new();
675                REL_NAME.get_or_init(|| ::appdb::model::relation::register_relation(#relation_name))
676            }
677        }
678
679        impl #struct_ident {
680            pub async fn relate<A, B>(a: &A, b: &B) -> ::anyhow::Result<()>
681            where
682                A: ::appdb::model::meta::ResolveRecordId + Send + Sync,
683                B: ::appdb::model::meta::ResolveRecordId + Send + Sync,
684            {
685                ::appdb::graph::relate_at(a.resolve_record_id().await?, b.resolve_record_id().await?, <Self as ::appdb::model::relation::RelationMeta>::relation_name()).await
686            }
687
688            pub async fn unrelate<A, B>(a: &A, b: &B) -> ::anyhow::Result<()>
689            where
690                A: ::appdb::model::meta::ResolveRecordId + Send + Sync,
691                B: ::appdb::model::meta::ResolveRecordId + Send + Sync,
692            {
693                ::appdb::graph::unrelate_at(a.resolve_record_id().await?, b.resolve_record_id().await?, <Self as ::appdb::model::relation::RelationMeta>::relation_name()).await
694            }
695
696            pub async fn out_ids<A>(a: &A, out_table: &str) -> ::anyhow::Result<::std::vec::Vec<::surrealdb::types::RecordId>>
697            where
698                A: ::appdb::model::meta::ResolveRecordId + Send + Sync,
699            {
700                ::appdb::graph::out_ids(a.resolve_record_id().await?, <Self as ::appdb::model::relation::RelationMeta>::relation_name(), out_table).await
701            }
702
703            pub async fn in_ids<B>(b: &B, in_table: &str) -> ::anyhow::Result<::std::vec::Vec<::surrealdb::types::RecordId>>
704            where
705                B: ::appdb::model::meta::ResolveRecordId + Send + Sync,
706            {
707                ::appdb::graph::in_ids(b.resolve_record_id().await?, <Self as ::appdb::model::relation::RelationMeta>::relation_name(), in_table).await
708            }
709        }
710    })
711}
712
713fn derive_sensitive_impl(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
714    let struct_ident = input.ident;
715    let encrypted_ident = format_ident!("Encrypted{}", struct_ident);
716    let vis = input.vis;
717    let named_fields = match input.data {
718        Data::Struct(data) => match data.fields {
719            Fields::Named(fields) => fields.named,
720            _ => {
721                return Err(Error::new_spanned(
722                    struct_ident,
723                    "Sensitive can only be derived for structs with named fields",
724                ))
725            }
726        },
727        _ => {
728            return Err(Error::new_spanned(
729                struct_ident,
730                "Sensitive can only be derived for structs",
731            ))
732        }
733    };
734
735    let mut secure_field_count = 0usize;
736    let mut encrypted_fields = Vec::new();
737    let mut encrypt_assignments = Vec::new();
738    let mut decrypt_assignments = Vec::new();
739    let mut runtime_encrypt_assignments = Vec::new();
740    let mut runtime_decrypt_assignments = Vec::new();
741    let mut field_tag_structs = Vec::new();
742
743    for field in named_fields.iter() {
744        let ident = field.ident.clone().expect("named field");
745        let field_vis = field.vis.clone();
746        let secure = has_secure_attr(&field.attrs);
747
748        if secure {
749            secure_field_count += 1;
750            let secure_kind = secure_kind(field)?;
751            let encrypted_ty = secure_kind.encrypted_type();
752            let field_tag_ident = format_ident!(
753                "AppdbSensitiveFieldTag{}{}",
754                struct_ident,
755                to_pascal_case(&ident.to_string())
756            );
757            let field_tag_literal = ident.to_string();
758            let encrypt_expr = secure_kind.encrypt_with_context_expr(&ident);
759            let decrypt_expr = secure_kind.decrypt_with_context_expr(&ident);
760            let runtime_encrypt_expr =
761                secure_kind.encrypt_with_runtime_expr(&ident, &field_tag_ident);
762            let runtime_decrypt_expr =
763                secure_kind.decrypt_with_runtime_expr(&ident, &field_tag_ident);
764            encrypted_fields.push(quote! { #field_vis #ident: #encrypted_ty });
765            encrypt_assignments.push(quote! { #ident: #encrypt_expr });
766            decrypt_assignments.push(quote! { #ident: #decrypt_expr });
767            runtime_encrypt_assignments.push(quote! { #ident: #runtime_encrypt_expr });
768            runtime_decrypt_assignments.push(quote! { #ident: #runtime_decrypt_expr });
769            field_tag_structs.push(quote! {
770                #[doc(hidden)]
771                #vis struct #field_tag_ident;
772
773                impl ::appdb::crypto::SensitiveFieldTag for #field_tag_ident {
774                    fn model_tag() -> &'static str {
775                        <#struct_ident as ::appdb::crypto::SensitiveModelTag>::model_tag()
776                    }
777
778                    fn field_tag() -> &'static str {
779                        #field_tag_literal
780                    }
781                }
782            });
783        } else {
784            let ty = field.ty.clone();
785            encrypted_fields.push(quote! { #field_vis #ident: #ty });
786            encrypt_assignments.push(quote! { #ident: self.#ident.clone() });
787            decrypt_assignments.push(quote! { #ident: encrypted.#ident.clone() });
788            runtime_encrypt_assignments.push(quote! { #ident: self.#ident.clone() });
789            runtime_decrypt_assignments.push(quote! { #ident: encrypted.#ident.clone() });
790        }
791    }
792
793    if secure_field_count == 0 {
794        return Err(Error::new_spanned(
795            struct_ident,
796            "Sensitive requires at least one #[secure] field",
797        ));
798    }
799
800    Ok(quote! {
801        #[derive(
802            Debug,
803            Clone,
804            ::serde::Serialize,
805            ::serde::Deserialize,
806            ::surrealdb::types::SurrealValue,
807        )]
808        #vis struct #encrypted_ident {
809            #( #encrypted_fields, )*
810        }
811
812        impl ::appdb::crypto::SensitiveModelTag for #struct_ident {
813            fn model_tag() -> &'static str {
814                ::std::concat!(::std::module_path!(), "::", ::std::stringify!(#struct_ident))
815            }
816        }
817
818        #( #field_tag_structs )*
819
820        impl ::appdb::Sensitive for #struct_ident {
821            type Encrypted = #encrypted_ident;
822
823            fn encrypt(
824                &self,
825                context: &::appdb::crypto::CryptoContext,
826            ) -> ::std::result::Result<Self::Encrypted, ::appdb::crypto::CryptoError> {
827                ::std::result::Result::Ok(#encrypted_ident {
828                    #( #encrypt_assignments, )*
829                })
830            }
831
832            fn decrypt(
833                encrypted: &Self::Encrypted,
834                context: &::appdb::crypto::CryptoContext,
835            ) -> ::std::result::Result<Self, ::appdb::crypto::CryptoError> {
836                ::std::result::Result::Ok(Self {
837                    #( #decrypt_assignments, )*
838                })
839            }
840
841            fn encrypt_with_runtime_resolver(
842                &self,
843            ) -> ::std::result::Result<Self::Encrypted, ::appdb::crypto::CryptoError> {
844                ::std::result::Result::Ok(#encrypted_ident {
845                    #( #runtime_encrypt_assignments, )*
846                })
847            }
848
849            fn decrypt_with_runtime_resolver(
850                encrypted: &Self::Encrypted,
851            ) -> ::std::result::Result<Self, ::appdb::crypto::CryptoError> {
852                ::std::result::Result::Ok(Self {
853                    #( #runtime_decrypt_assignments, )*
854                })
855            }
856        }
857
858        impl #struct_ident {
859            pub fn encrypt(
860                &self,
861                context: &::appdb::crypto::CryptoContext,
862            ) -> ::std::result::Result<#encrypted_ident, ::appdb::crypto::CryptoError> {
863                <Self as ::appdb::Sensitive>::encrypt(self, context)
864            }
865        }
866
867        impl #encrypted_ident {
868            pub fn decrypt(
869                &self,
870                context: &::appdb::crypto::CryptoContext,
871            ) -> ::std::result::Result<#struct_ident, ::appdb::crypto::CryptoError> {
872                <#struct_ident as ::appdb::Sensitive>::decrypt(self, context)
873            }
874        }
875    })
876}
877
878fn has_secure_attr(attrs: &[Attribute]) -> bool {
879    attrs.iter().any(|attr| attr.path().is_ident("secure"))
880}
881
882fn has_unique_attr(attrs: &[Attribute]) -> bool {
883    attrs.iter().any(|attr| attr.path().is_ident("unique"))
884}
885
886fn table_alias_target(attrs: &[Attribute]) -> syn::Result<Option<Type>> {
887    let mut target = None;
888
889    for attr in attrs {
890        if !attr.path().is_ident("table_as") {
891            continue;
892        }
893
894        if target.is_some() {
895            return Err(Error::new_spanned(
896                attr,
897                "duplicate #[table_as(...)] attribute is not supported",
898            ));
899        }
900
901        let parsed: Type = attr.parse_args().map_err(|_| {
902            Error::new_spanned(attr, "#[table_as(...)] requires exactly one target type")
903        })?;
904
905        match parsed {
906            Type::Path(TypePath { ref path, .. }) if !path.segments.is_empty() => {
907                target = Some(parsed);
908            }
909            _ => {
910                return Err(Error::new_spanned(
911                    parsed,
912                    "#[table_as(...)] target must be a type path",
913                ))
914            }
915        }
916    }
917
918    Ok(target)
919}
920
921fn resolved_schema_table_name(struct_ident: &syn::Ident, table_alias: Option<&Type>) -> String {
922    match table_alias {
923        Some(Type::Path(type_path)) => type_path
924            .path
925            .segments
926            .last()
927            .map(|segment| to_snake_case(&segment.ident.to_string()))
928            .unwrap_or_else(|| to_snake_case(&struct_ident.to_string())),
929        Some(_) => to_snake_case(&struct_ident.to_string()),
930        None => to_snake_case(&struct_ident.to_string()),
931    }
932}
933
934fn field_foreign_attr(field: &Field) -> syn::Result<Option<&Attribute>> {
935    let mut foreign_attr = None;
936
937    for attr in &field.attrs {
938        if !attr.path().is_ident("foreign") {
939            continue;
940        }
941
942        if foreign_attr.is_some() {
943            return Err(Error::new_spanned(
944                attr,
945                "duplicate nested-ref attribute is not supported",
946            ));
947        }
948
949        foreign_attr = Some(attr);
950    }
951
952    Ok(foreign_attr)
953}
954
955fn validate_foreign_field(field: &Field, attr: &Attribute) -> syn::Result<Type> {
956    if attr.path().is_ident("foreign") {
957        return foreign_leaf_type(&field.ty)
958            .ok_or_else(|| Error::new_spanned(&field.ty, BINDREF_ACCEPTED_SHAPES));
959    }
960
961    Err(Error::new_spanned(attr, "unsupported foreign attribute"))
962}
963
964const BINDREF_ACCEPTED_SHAPES: &str =
965    "#[foreign] supports recursive Option<_> / Vec<_> shapes whose leaf type implements appdb::Bridge";
966
967const BINDREF_BRIDGE_STORE_ONLY: &str =
968    "#[foreign] leaf types must derive Store or #[derive(Bridge)] dispatcher enums";
969
970#[derive(Clone)]
971struct ForeignField {
972    ident: syn::Ident,
973    kind: ForeignFieldKind,
974}
975
976#[derive(Clone)]
977struct ForeignFieldKind {
978    original_ty: Type,
979    stored_ty: Type,
980}
981
982fn parse_foreign_field(field: &Field, attr: &Attribute) -> syn::Result<ForeignField> {
983    validate_foreign_field(field, attr)?;
984    let ident = field.ident.clone().expect("named field");
985
986    let kind = ForeignFieldKind {
987        original_ty: field.ty.clone(),
988        stored_ty: foreign_stored_type(&field.ty)
989            .ok_or_else(|| Error::new_spanned(&field.ty, BINDREF_ACCEPTED_SHAPES))?,
990    };
991
992    Ok(ForeignField { ident, kind })
993}
994
995fn foreign_field_kind<'a>(
996    ident: &syn::Ident,
997    fields: &'a [ForeignField],
998) -> Option<&'a ForeignFieldKind> {
999    fields
1000        .iter()
1001        .find(|field| field.ident == *ident)
1002        .map(|field| &field.kind)
1003}
1004
1005fn stored_field_type(field: &Field, foreign_fields: &[ForeignField]) -> Type {
1006    let ident = field.ident.as_ref().expect("named field");
1007    match foreign_field_kind(ident, foreign_fields) {
1008        Some(ForeignFieldKind { stored_ty, .. }) => stored_ty.clone(),
1009        None => field.ty.clone(),
1010    }
1011}
1012
1013fn foreign_stored_type(ty: &Type) -> Option<Type> {
1014    if let Some(inner) = option_inner_type(ty) {
1015        let inner = foreign_stored_type(inner)?;
1016        return Some(syn::parse_quote!(::std::option::Option<#inner>));
1017    }
1018
1019    if let Some(inner) = vec_inner_type(ty) {
1020        let inner = foreign_stored_type(inner)?;
1021        return Some(syn::parse_quote!(::std::vec::Vec<#inner>));
1022    }
1023
1024    direct_store_child_type(ty)
1025        .cloned()
1026        .map(|_| syn::parse_quote!(::surrealdb::types::RecordId))
1027}
1028
1029fn foreign_leaf_type(ty: &Type) -> Option<Type> {
1030    if let Some(inner) = option_inner_type(ty) {
1031        return foreign_leaf_type(inner);
1032    }
1033
1034    if let Some(inner) = vec_inner_type(ty) {
1035        return foreign_leaf_type(inner);
1036    }
1037
1038    direct_store_child_type(ty).cloned().map(Type::Path)
1039}
1040
1041fn invalid_foreign_leaf_type(ty: &Type) -> Option<Type> {
1042    let leaf = foreign_leaf_type(ty)?;
1043    match &leaf {
1044        Type::Path(type_path) => {
1045            let segment = type_path.path.segments.last()?;
1046            if matches!(segment.arguments, PathArguments::None) {
1047                None
1048            } else {
1049                Some(leaf)
1050            }
1051        }
1052        _ => Some(leaf),
1053    }
1054}
1055
1056fn direct_store_child_type(ty: &Type) -> Option<&TypePath> {
1057    let Type::Path(type_path) = ty else {
1058        return None;
1059    };
1060
1061    let segment = type_path.path.segments.last()?;
1062    if !matches!(segment.arguments, PathArguments::None) {
1063        return None;
1064    }
1065
1066    if is_id_type(ty) || is_string_type(ty) || is_common_non_store_leaf_type(ty) {
1067        return None;
1068    }
1069
1070    Some(type_path)
1071}
1072
1073fn is_common_non_store_leaf_type(ty: &Type) -> bool {
1074    matches!(
1075        ty,
1076        Type::Path(TypePath { path, .. })
1077            if path.is_ident("bool")
1078                || path.is_ident("u8")
1079                || path.is_ident("u16")
1080                || path.is_ident("u32")
1081                || path.is_ident("u64")
1082                || path.is_ident("u128")
1083                || path.is_ident("usize")
1084                || path.is_ident("i8")
1085                || path.is_ident("i16")
1086                || path.is_ident("i32")
1087                || path.is_ident("i64")
1088                || path.is_ident("i128")
1089                || path.is_ident("isize")
1090                || path.is_ident("f32")
1091                || path.is_ident("f64")
1092                || path.is_ident("char")
1093    )
1094}
1095
1096fn secure_field_count(fields: &syn::punctuated::Punctuated<Field, syn::token::Comma>) -> usize {
1097    fields
1098        .iter()
1099        .filter(|field| has_secure_attr(&field.attrs))
1100        .count()
1101}
1102
1103fn relation_name_override(attrs: &[Attribute]) -> syn::Result<Option<String>> {
1104    for attr in attrs {
1105        if !attr.path().is_ident("relation") {
1106            continue;
1107        }
1108
1109        let mut name = None;
1110        attr.parse_nested_meta(|meta| {
1111            if meta.path.is_ident("name") {
1112                let value = meta.value()?;
1113                let literal: syn::LitStr = value.parse()?;
1114                name = Some(literal.value());
1115                Ok(())
1116            } else {
1117                Err(meta.error("unsupported relation attribute"))
1118            }
1119        })?;
1120        return Ok(name);
1121    }
1122
1123    Ok(None)
1124}
1125
1126enum SecureKind {
1127    String,
1128    OptionString,
1129}
1130
1131impl SecureKind {
1132    fn encrypted_type(&self) -> proc_macro2::TokenStream {
1133        match self {
1134            SecureKind::String => quote! { ::std::vec::Vec<u8> },
1135            SecureKind::OptionString => quote! { ::std::option::Option<::std::vec::Vec<u8>> },
1136        }
1137    }
1138
1139    fn encrypt_with_context_expr(&self, ident: &syn::Ident) -> proc_macro2::TokenStream {
1140        match self {
1141            SecureKind::String => {
1142                quote! { ::appdb::crypto::encrypt_string(&self.#ident, context)? }
1143            }
1144            SecureKind::OptionString => {
1145                quote! { ::appdb::crypto::encrypt_optional_string(&self.#ident, context)? }
1146            }
1147        }
1148    }
1149
1150    fn decrypt_with_context_expr(&self, ident: &syn::Ident) -> proc_macro2::TokenStream {
1151        match self {
1152            SecureKind::String => {
1153                quote! { ::appdb::crypto::decrypt_string(&encrypted.#ident, context)? }
1154            }
1155            SecureKind::OptionString => {
1156                quote! { ::appdb::crypto::decrypt_optional_string(&encrypted.#ident, context)? }
1157            }
1158        }
1159    }
1160
1161    fn encrypt_with_runtime_expr(
1162        &self,
1163        ident: &syn::Ident,
1164        field_tag_ident: &syn::Ident,
1165    ) -> proc_macro2::TokenStream {
1166        match self {
1167            SecureKind::String => {
1168                quote! {{
1169                    let context = ::appdb::crypto::resolve_crypto_context_for::<#field_tag_ident>()?;
1170                    ::appdb::crypto::encrypt_string(&self.#ident, context.as_ref())?
1171                }}
1172            }
1173            SecureKind::OptionString => {
1174                quote! {{
1175                    let context = ::appdb::crypto::resolve_crypto_context_for::<#field_tag_ident>()?;
1176                    ::appdb::crypto::encrypt_optional_string(&self.#ident, context.as_ref())?
1177                }}
1178            }
1179        }
1180    }
1181
1182    fn decrypt_with_runtime_expr(
1183        &self,
1184        ident: &syn::Ident,
1185        field_tag_ident: &syn::Ident,
1186    ) -> proc_macro2::TokenStream {
1187        match self {
1188            SecureKind::String => {
1189                quote! {{
1190                    let context = ::appdb::crypto::resolve_crypto_context_for::<#field_tag_ident>()?;
1191                    ::appdb::crypto::decrypt_string(&encrypted.#ident, context.as_ref())?
1192                }}
1193            }
1194            SecureKind::OptionString => {
1195                quote! {{
1196                    let context = ::appdb::crypto::resolve_crypto_context_for::<#field_tag_ident>()?;
1197                    ::appdb::crypto::decrypt_optional_string(&encrypted.#ident, context.as_ref())?
1198                }}
1199            }
1200        }
1201    }
1202}
1203
1204fn secure_kind(field: &Field) -> syn::Result<SecureKind> {
1205    if is_string_type(&field.ty) {
1206        return Ok(SecureKind::String);
1207    }
1208
1209    if let Some(inner) = option_inner_type(&field.ty) {
1210        if is_string_type(inner) {
1211            return Ok(SecureKind::OptionString);
1212        }
1213    }
1214
1215    Err(Error::new_spanned(
1216        &field.ty,
1217        "#[secure] currently supports only String and Option<String>",
1218    ))
1219}
1220
1221fn is_string_type(ty: &Type) -> bool {
1222    match ty {
1223        Type::Path(TypePath { path, .. }) => path.is_ident("String"),
1224        _ => false,
1225    }
1226}
1227
1228fn is_id_type(ty: &Type) -> bool {
1229    match ty {
1230        Type::Path(TypePath { path, .. }) => path.segments.last().is_some_and(|segment| {
1231            let ident = segment.ident.to_string();
1232            ident == "Id"
1233        }),
1234        _ => false,
1235    }
1236}
1237
1238fn is_record_id_type(ty: &Type) -> bool {
1239    match ty {
1240        Type::Path(TypePath { path, .. }) => path.segments.last().is_some_and(|segment| {
1241            let ident = segment.ident.to_string();
1242            ident == "RecordId"
1243        }),
1244        _ => false,
1245    }
1246}
1247
1248fn option_inner_type(ty: &Type) -> Option<&Type> {
1249    let Type::Path(TypePath { path, .. }) = ty else {
1250        return None;
1251    };
1252    let segment = path.segments.last()?;
1253    if segment.ident != "Option" {
1254        return None;
1255    }
1256    let PathArguments::AngleBracketed(args) = &segment.arguments else {
1257        return None;
1258    };
1259    let GenericArgument::Type(inner) = args.args.first()? else {
1260        return None;
1261    };
1262    Some(inner)
1263}
1264
1265fn vec_inner_type(ty: &Type) -> Option<&Type> {
1266    let Type::Path(TypePath { path, .. }) = ty else {
1267        return None;
1268    };
1269    let segment = path.segments.last()?;
1270    if segment.ident != "Vec" {
1271        return None;
1272    }
1273    let PathArguments::AngleBracketed(args) = &segment.arguments else {
1274        return None;
1275    };
1276    let GenericArgument::Type(inner) = args.args.first()? else {
1277        return None;
1278    };
1279    Some(inner)
1280}
1281
1282fn to_snake_case(input: &str) -> String {
1283    let mut out = String::with_capacity(input.len() + 4);
1284    let mut prev_is_lower_or_digit = false;
1285
1286    for ch in input.chars() {
1287        if ch.is_ascii_uppercase() {
1288            if prev_is_lower_or_digit {
1289                out.push('_');
1290            }
1291            out.push(ch.to_ascii_lowercase());
1292            prev_is_lower_or_digit = false;
1293        } else {
1294            out.push(ch);
1295            prev_is_lower_or_digit = ch.is_ascii_lowercase() || ch.is_ascii_digit();
1296        }
1297    }
1298
1299    out
1300}
1301
1302fn to_pascal_case(input: &str) -> String {
1303    let mut out = String::with_capacity(input.len());
1304    let mut uppercase_next = true;
1305
1306    for ch in input.chars() {
1307        if ch == '_' || ch == '-' {
1308            uppercase_next = true;
1309            continue;
1310        }
1311
1312        if uppercase_next {
1313            out.push(ch.to_ascii_uppercase());
1314            uppercase_next = false;
1315        } else {
1316            out.push(ch);
1317        }
1318    }
1319
1320    out
1321}