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>::table_name(),
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>::table_name(),
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        quote! { #ident: #ty }
272    });
273
274    let into_stored_assignments = named_fields.iter().map(|field| {
275        let ident = field.ident.clone().expect("named field");
276        match foreign_field_kind(&ident, &foreign_fields) {
277            Some(ForeignFieldKind { original_ty, .. }) => quote! {
278                #ident: <#original_ty as ::appdb::ForeignShape>::persist_foreign_shape(value.#ident).await?
279            },
280            None => quote! { #ident: value.#ident },
281        }
282    });
283
284    let from_stored_assignments = named_fields.iter().map(|field| {
285        let ident = field.ident.clone().expect("named field");
286        match foreign_field_kind(&ident, &foreign_fields) {
287            Some(ForeignFieldKind { original_ty, .. }) => quote! {
288                #ident: <#original_ty as ::appdb::ForeignShape>::hydrate_foreign_shape(stored.#ident).await?
289            },
290            None => quote! { #ident: stored.#ident },
291        }
292    });
293
294    let decode_foreign_fields = foreign_fields.iter().map(|field| {
295        let ident = field.ident.to_string();
296        quote! {
297            if let ::std::option::Option::Some(value) = map.get_mut(#ident) {
298                ::appdb::rewrite_foreign_json_value(value);
299            }
300        }
301    });
302
303    let foreign_model_impl = if foreign_fields.is_empty() {
304        quote! {
305            impl ::appdb::ForeignModel for #struct_ident {
306                async fn persist_foreign(value: Self) -> ::anyhow::Result<Self::Stored> {
307                    <Self as ::appdb::StoredModel>::into_stored(value)
308                }
309
310                async fn hydrate_foreign(stored: Self::Stored) -> ::anyhow::Result<Self> {
311                    <Self as ::appdb::StoredModel>::from_stored(stored)
312                }
313
314                fn decode_stored_row(
315                    row: ::surrealdb::types::Value,
316                ) -> ::anyhow::Result<Self::Stored>
317                where
318                    Self::Stored: ::serde::de::DeserializeOwned,
319                {
320                    Ok(::serde_json::from_value(row.into_json_value())?)
321                }
322            }
323        }
324    } else {
325        let stored_struct_ident = format_ident!("AppdbStored{}", struct_ident);
326        quote! {
327            #[derive(
328                Debug,
329                Clone,
330                ::serde::Serialize,
331                ::serde::Deserialize,
332                ::surrealdb::types::SurrealValue,
333            )]
334            #vis struct #stored_struct_ident {
335                #( #stored_fields, )*
336            }
337
338            impl ::appdb::StoredModel for #struct_ident {
339                type Stored = #stored_struct_ident;
340
341                fn into_stored(self) -> ::anyhow::Result<Self::Stored> {
342                    unreachable!("foreign fields require async persist_foreign")
343                }
344
345                fn from_stored(_stored: Self::Stored) -> ::anyhow::Result<Self> {
346                    unreachable!("foreign fields require async hydrate_foreign")
347                }
348            }
349
350            impl ::appdb::ForeignModel for #struct_ident {
351                async fn persist_foreign(value: Self) -> ::anyhow::Result<Self::Stored> {
352                    let value = value;
353                    Ok(#stored_struct_ident {
354                        #( #into_stored_assignments, )*
355                    })
356                }
357
358                async fn hydrate_foreign(stored: Self::Stored) -> ::anyhow::Result<Self> {
359                    Ok(Self {
360                        #( #from_stored_assignments, )*
361                    })
362                }
363
364                fn has_foreign_fields() -> bool {
365                    true
366                }
367
368                fn decode_stored_row(
369                    row: ::surrealdb::types::Value,
370                ) -> ::anyhow::Result<Self::Stored>
371                where
372                    Self::Stored: ::serde::de::DeserializeOwned,
373                {
374                    let mut row = row.into_json_value();
375                    if let ::serde_json::Value::Object(map) = &mut row {
376                        #( #decode_foreign_fields )*
377                    }
378                    Ok(::serde_json::from_value(row)?)
379                }
380            }
381        }
382    };
383
384    let store_marker_ident = format_ident!("AppdbStoreMarker{}", struct_ident);
385
386    Ok(quote! {
387        #[doc(hidden)]
388        #vis struct #store_marker_ident;
389
390        impl ::appdb::model::meta::ModelMeta for #struct_ident {
391            fn table_name() -> &'static str {
392                static TABLE_NAME: ::std::sync::OnceLock<&'static str> = ::std::sync::OnceLock::new();
393                TABLE_NAME.get_or_init(|| {
394                    let table = #resolved_table_name_expr;
395                    ::appdb::model::meta::register_table(stringify!(#struct_ident), table)
396                })
397            }
398        }
399
400        impl ::appdb::model::meta::StoreModelMarker for #struct_ident {}
401        impl ::appdb::model::meta::StoreModelMarker for #store_marker_ident {}
402
403        impl ::appdb::model::meta::UniqueLookupMeta for #struct_ident {
404            fn lookup_fields() -> &'static [&'static str] {
405                &[ #( #lookup_field_literals ),* ]
406            }
407
408            fn foreign_fields() -> &'static [&'static str] {
409                &[ #( #foreign_field_literals ),* ]
410            }
411        }
412        #stored_model_impl
413        #foreign_model_impl
414
415        #auto_has_id_impl
416        #resolve_record_id_impl
417
418        #( #unique_schema_impls )*
419
420        impl ::appdb::repository::Crud for #struct_ident {}
421
422        impl #struct_ident {
423            pub async fn get<T>(id: T) -> ::anyhow::Result<Self>
424            where
425                ::surrealdb::types::RecordIdKey: From<T>,
426                T: Send,
427            {
428                ::appdb::repository::Repo::<Self>::get(id).await
429            }
430
431            pub async fn list() -> ::anyhow::Result<::std::vec::Vec<Self>> {
432                ::appdb::repository::Repo::<Self>::list().await
433            }
434
435            pub async fn list_limit(count: i64) -> ::anyhow::Result<::std::vec::Vec<Self>> {
436                ::appdb::repository::Repo::<Self>::list_limit(count).await
437            }
438
439            pub async fn delete_all() -> ::anyhow::Result<()> {
440                ::appdb::repository::Repo::<Self>::delete_all().await
441            }
442
443            pub async fn find_one_id(
444                k: &str,
445                v: &str,
446            ) -> ::anyhow::Result<::surrealdb::types::RecordId> {
447                ::appdb::repository::Repo::<Self>::find_one_id(k, v).await
448            }
449
450            pub async fn list_record_ids() -> ::anyhow::Result<::std::vec::Vec<::surrealdb::types::RecordId>> {
451                ::appdb::repository::Repo::<Self>::list_record_ids().await
452            }
453
454            pub async fn create_at(
455                id: ::surrealdb::types::RecordId,
456                data: Self,
457            ) -> ::anyhow::Result<Self> {
458                ::appdb::repository::Repo::<Self>::create_at(id, data).await
459            }
460
461            pub async fn upsert_at(
462                id: ::surrealdb::types::RecordId,
463                data: Self,
464            ) -> ::anyhow::Result<Self> {
465                ::appdb::repository::Repo::<Self>::upsert_at(id, data).await
466            }
467
468            pub async fn update_at(
469                self,
470                id: ::surrealdb::types::RecordId,
471            ) -> ::anyhow::Result<Self> {
472                ::appdb::repository::Repo::<Self>::update_at(id, self).await
473            }
474
475            pub async fn delete<T>(id: T) -> ::anyhow::Result<()>
476            where
477                ::surrealdb::types::RecordIdKey: From<T>,
478                T: Send,
479            {
480                ::appdb::repository::Repo::<Self>::delete(id).await
481            }
482        }
483    })
484}
485
486fn derive_bridge_impl(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
487    let enum_ident = input.ident;
488
489    let variants = match input.data {
490        Data::Enum(data) => data.variants,
491        _ => {
492            return Err(Error::new_spanned(
493                enum_ident,
494                "Bridge can only be derived for enums",
495            ))
496        }
497    };
498
499    let payloads = variants
500        .iter()
501        .map(parse_bridge_variant)
502        .collect::<syn::Result<Vec<_>>>()?;
503
504    let from_impls = payloads.iter().map(|variant| {
505        let variant_ident = &variant.variant_ident;
506        let payload_ty = &variant.payload_ty;
507
508        quote! {
509            impl ::std::convert::From<#payload_ty> for #enum_ident {
510                fn from(value: #payload_ty) -> Self {
511                    Self::#variant_ident(value)
512                }
513            }
514        }
515    });
516
517    let persist_match_arms = payloads.iter().map(|variant| {
518        let variant_ident = &variant.variant_ident;
519
520        quote! {
521            Self::#variant_ident(value) => <_ as ::appdb::Bridge>::persist_foreign(value).await,
522        }
523    });
524
525    let hydrate_match_arms = payloads.iter().map(|variant| {
526        let variant_ident = &variant.variant_ident;
527        let payload_ty = &variant.payload_ty;
528
529        quote! {
530            table if table == <#payload_ty as ::appdb::model::meta::ModelMeta>::table_name() => {
531                ::std::result::Result::Ok(Self::#variant_ident(
532                    <#payload_ty as ::appdb::Bridge>::hydrate_foreign(id).await?,
533                ))
534            }
535        }
536    });
537
538    Ok(quote! {
539        #( #from_impls )*
540
541        #[::async_trait::async_trait]
542        impl ::appdb::Bridge for #enum_ident {
543            async fn persist_foreign(self) -> ::anyhow::Result<::surrealdb::types::RecordId> {
544                match self {
545                    #( #persist_match_arms )*
546                }
547            }
548
549            async fn hydrate_foreign(
550                id: ::surrealdb::types::RecordId,
551            ) -> ::anyhow::Result<Self> {
552                match id.table.to_string().as_str() {
553                    #( #hydrate_match_arms, )*
554                    table => ::anyhow::bail!(
555                        "unsupported foreign table `{table}` for enum dispatcher `{}`",
556                        ::std::stringify!(#enum_ident)
557                    ),
558                }
559            }
560        }
561    })
562}
563
564#[derive(Clone)]
565struct BridgeVariant {
566    variant_ident: syn::Ident,
567    payload_ty: Type,
568}
569
570fn parse_bridge_variant(variant: &syn::Variant) -> syn::Result<BridgeVariant> {
571    let payload_ty = match &variant.fields {
572        Fields::Unnamed(fields) if fields.unnamed.len() == 1 => {
573            fields.unnamed.first().expect("single field").ty.clone()
574        }
575        Fields::Unnamed(_) => {
576            return Err(Error::new_spanned(
577                &variant.ident,
578                "Bridge variants must be single-field tuple variants",
579            ))
580        }
581        Fields::Unit => {
582            return Err(Error::new_spanned(
583                &variant.ident,
584                "Bridge does not support unit variants",
585            ))
586        }
587        Fields::Named(_) => {
588            return Err(Error::new_spanned(
589                &variant.ident,
590                "Bridge does not support struct variants",
591            ))
592        }
593    };
594
595    let payload_path = match &payload_ty {
596        Type::Path(path) => path,
597        _ => {
598            return Err(Error::new_spanned(
599                &payload_ty,
600                "Bridge payload must implement appdb::Bridge",
601            ))
602        }
603    };
604
605    let segment = payload_path.path.segments.last().ok_or_else(|| {
606        Error::new_spanned(&payload_ty, "Bridge payload must implement appdb::Bridge")
607    })?;
608
609    if !matches!(segment.arguments, PathArguments::None) {
610        return Err(Error::new_spanned(
611            &payload_ty,
612            "Bridge payload must implement appdb::Bridge",
613        ));
614    }
615
616    Ok(BridgeVariant {
617        variant_ident: variant.ident.clone(),
618        payload_ty,
619    })
620}
621
622fn derive_relation_impl(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
623    let struct_ident = input.ident;
624    let relation_name = relation_name_override(&input.attrs)?
625        .unwrap_or_else(|| to_snake_case(&struct_ident.to_string()));
626
627    match input.data {
628        Data::Struct(data) => {
629            match data.fields {
630                Fields::Unit | Fields::Named(_) => {}
631                _ => return Err(Error::new_spanned(
632                    struct_ident,
633                    "Relation can only be derived for unit structs or structs with named fields",
634                )),
635            }
636        }
637        _ => {
638            return Err(Error::new_spanned(
639                struct_ident,
640                "Relation can only be derived for structs",
641            ))
642        }
643    }
644
645    Ok(quote! {
646        impl ::appdb::model::relation::RelationMeta for #struct_ident {
647            fn relation_name() -> &'static str {
648                static REL_NAME: ::std::sync::OnceLock<&'static str> = ::std::sync::OnceLock::new();
649                REL_NAME.get_or_init(|| ::appdb::model::relation::register_relation(#relation_name))
650            }
651        }
652
653        impl #struct_ident {
654            pub async fn relate<A, B>(a: &A, b: &B) -> ::anyhow::Result<()>
655            where
656                A: ::appdb::model::meta::ResolveRecordId + Send + Sync,
657                B: ::appdb::model::meta::ResolveRecordId + Send + Sync,
658            {
659                ::appdb::graph::relate_at(a.resolve_record_id().await?, b.resolve_record_id().await?, <Self as ::appdb::model::relation::RelationMeta>::relation_name()).await
660            }
661
662            pub async fn unrelate<A, B>(a: &A, b: &B) -> ::anyhow::Result<()>
663            where
664                A: ::appdb::model::meta::ResolveRecordId + Send + Sync,
665                B: ::appdb::model::meta::ResolveRecordId + Send + Sync,
666            {
667                ::appdb::graph::unrelate_at(a.resolve_record_id().await?, b.resolve_record_id().await?, <Self as ::appdb::model::relation::RelationMeta>::relation_name()).await
668            }
669
670            pub async fn out_ids<A>(a: &A, out_table: &str) -> ::anyhow::Result<::std::vec::Vec<::surrealdb::types::RecordId>>
671            where
672                A: ::appdb::model::meta::ResolveRecordId + Send + Sync,
673            {
674                ::appdb::graph::out_ids(a.resolve_record_id().await?, <Self as ::appdb::model::relation::RelationMeta>::relation_name(), out_table).await
675            }
676
677            pub async fn in_ids<B>(b: &B, in_table: &str) -> ::anyhow::Result<::std::vec::Vec<::surrealdb::types::RecordId>>
678            where
679                B: ::appdb::model::meta::ResolveRecordId + Send + Sync,
680            {
681                ::appdb::graph::in_ids(b.resolve_record_id().await?, <Self as ::appdb::model::relation::RelationMeta>::relation_name(), in_table).await
682            }
683        }
684    })
685}
686
687fn derive_sensitive_impl(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
688    let struct_ident = input.ident;
689    let encrypted_ident = format_ident!("Encrypted{}", struct_ident);
690    let vis = input.vis;
691    let named_fields = match input.data {
692        Data::Struct(data) => match data.fields {
693            Fields::Named(fields) => fields.named,
694            _ => {
695                return Err(Error::new_spanned(
696                    struct_ident,
697                    "Sensitive can only be derived for structs with named fields",
698                ))
699            }
700        },
701        _ => {
702            return Err(Error::new_spanned(
703                struct_ident,
704                "Sensitive can only be derived for structs",
705            ))
706        }
707    };
708
709    let mut secure_field_count = 0usize;
710    let mut encrypted_fields = Vec::new();
711    let mut encrypt_assignments = Vec::new();
712    let mut decrypt_assignments = Vec::new();
713    let mut runtime_encrypt_assignments = Vec::new();
714    let mut runtime_decrypt_assignments = Vec::new();
715    let mut field_tag_structs = Vec::new();
716
717    for field in named_fields.iter() {
718        let ident = field.ident.clone().expect("named field");
719        let field_vis = field.vis.clone();
720        let secure = has_secure_attr(&field.attrs);
721
722        if secure {
723            secure_field_count += 1;
724            let secure_kind = secure_kind(field)?;
725            let encrypted_ty = secure_kind.encrypted_type();
726            let field_tag_ident = format_ident!(
727                "AppdbSensitiveFieldTag{}{}",
728                struct_ident,
729                to_pascal_case(&ident.to_string())
730            );
731            let field_tag_literal = ident.to_string();
732            let encrypt_expr = secure_kind.encrypt_with_context_expr(&ident);
733            let decrypt_expr = secure_kind.decrypt_with_context_expr(&ident);
734            let runtime_encrypt_expr =
735                secure_kind.encrypt_with_runtime_expr(&ident, &field_tag_ident);
736            let runtime_decrypt_expr =
737                secure_kind.decrypt_with_runtime_expr(&ident, &field_tag_ident);
738            encrypted_fields.push(quote! { #field_vis #ident: #encrypted_ty });
739            encrypt_assignments.push(quote! { #ident: #encrypt_expr });
740            decrypt_assignments.push(quote! { #ident: #decrypt_expr });
741            runtime_encrypt_assignments.push(quote! { #ident: #runtime_encrypt_expr });
742            runtime_decrypt_assignments.push(quote! { #ident: #runtime_decrypt_expr });
743            field_tag_structs.push(quote! {
744                #[doc(hidden)]
745                #vis struct #field_tag_ident;
746
747                impl ::appdb::crypto::SensitiveFieldTag for #field_tag_ident {
748                    fn model_tag() -> &'static str {
749                        <#struct_ident as ::appdb::crypto::SensitiveModelTag>::model_tag()
750                    }
751
752                    fn field_tag() -> &'static str {
753                        #field_tag_literal
754                    }
755                }
756            });
757        } else {
758            let ty = field.ty.clone();
759            encrypted_fields.push(quote! { #field_vis #ident: #ty });
760            encrypt_assignments.push(quote! { #ident: self.#ident.clone() });
761            decrypt_assignments.push(quote! { #ident: encrypted.#ident.clone() });
762            runtime_encrypt_assignments.push(quote! { #ident: self.#ident.clone() });
763            runtime_decrypt_assignments.push(quote! { #ident: encrypted.#ident.clone() });
764        }
765    }
766
767    if secure_field_count == 0 {
768        return Err(Error::new_spanned(
769            struct_ident,
770            "Sensitive requires at least one #[secure] field",
771        ));
772    }
773
774    Ok(quote! {
775        #[derive(
776            Debug,
777            Clone,
778            ::serde::Serialize,
779            ::serde::Deserialize,
780            ::surrealdb::types::SurrealValue,
781        )]
782        #vis struct #encrypted_ident {
783            #( #encrypted_fields, )*
784        }
785
786        impl ::appdb::crypto::SensitiveModelTag for #struct_ident {
787            fn model_tag() -> &'static str {
788                ::std::concat!(::std::module_path!(), "::", ::std::stringify!(#struct_ident))
789            }
790        }
791
792        #( #field_tag_structs )*
793
794        impl ::appdb::Sensitive for #struct_ident {
795            type Encrypted = #encrypted_ident;
796
797            fn encrypt(
798                &self,
799                context: &::appdb::crypto::CryptoContext,
800            ) -> ::std::result::Result<Self::Encrypted, ::appdb::crypto::CryptoError> {
801                ::std::result::Result::Ok(#encrypted_ident {
802                    #( #encrypt_assignments, )*
803                })
804            }
805
806            fn decrypt(
807                encrypted: &Self::Encrypted,
808                context: &::appdb::crypto::CryptoContext,
809            ) -> ::std::result::Result<Self, ::appdb::crypto::CryptoError> {
810                ::std::result::Result::Ok(Self {
811                    #( #decrypt_assignments, )*
812                })
813            }
814
815            fn encrypt_with_runtime_resolver(
816                &self,
817            ) -> ::std::result::Result<Self::Encrypted, ::appdb::crypto::CryptoError> {
818                ::std::result::Result::Ok(#encrypted_ident {
819                    #( #runtime_encrypt_assignments, )*
820                })
821            }
822
823            fn decrypt_with_runtime_resolver(
824                encrypted: &Self::Encrypted,
825            ) -> ::std::result::Result<Self, ::appdb::crypto::CryptoError> {
826                ::std::result::Result::Ok(Self {
827                    #( #runtime_decrypt_assignments, )*
828                })
829            }
830        }
831
832        impl #struct_ident {
833            pub fn encrypt(
834                &self,
835                context: &::appdb::crypto::CryptoContext,
836            ) -> ::std::result::Result<#encrypted_ident, ::appdb::crypto::CryptoError> {
837                <Self as ::appdb::Sensitive>::encrypt(self, context)
838            }
839        }
840
841        impl #encrypted_ident {
842            pub fn decrypt(
843                &self,
844                context: &::appdb::crypto::CryptoContext,
845            ) -> ::std::result::Result<#struct_ident, ::appdb::crypto::CryptoError> {
846                <#struct_ident as ::appdb::Sensitive>::decrypt(self, context)
847            }
848        }
849    })
850}
851
852fn has_secure_attr(attrs: &[Attribute]) -> bool {
853    attrs.iter().any(|attr| attr.path().is_ident("secure"))
854}
855
856fn has_unique_attr(attrs: &[Attribute]) -> bool {
857    attrs.iter().any(|attr| attr.path().is_ident("unique"))
858}
859
860fn table_alias_target(attrs: &[Attribute]) -> syn::Result<Option<Type>> {
861    let mut target = None;
862
863    for attr in attrs {
864        if !attr.path().is_ident("table_as") {
865            continue;
866        }
867
868        if target.is_some() {
869            return Err(Error::new_spanned(
870                attr,
871                "duplicate #[table_as(...)] attribute is not supported",
872            ));
873        }
874
875        let parsed: Type = attr.parse_args().map_err(|_| {
876            Error::new_spanned(attr, "#[table_as(...)] requires exactly one target type")
877        })?;
878
879        match parsed {
880            Type::Path(TypePath { ref path, .. }) if !path.segments.is_empty() => {
881                target = Some(parsed);
882            }
883            _ => {
884                return Err(Error::new_spanned(
885                    parsed,
886                    "#[table_as(...)] target must be a type path",
887                ))
888            }
889        }
890    }
891
892    Ok(target)
893}
894
895fn resolved_schema_table_name(struct_ident: &syn::Ident, table_alias: Option<&Type>) -> String {
896    match table_alias {
897        Some(Type::Path(type_path)) => type_path
898            .path
899            .segments
900            .last()
901            .map(|segment| to_snake_case(&segment.ident.to_string()))
902            .unwrap_or_else(|| to_snake_case(&struct_ident.to_string())),
903        Some(_) => to_snake_case(&struct_ident.to_string()),
904        None => to_snake_case(&struct_ident.to_string()),
905    }
906}
907
908fn field_foreign_attr(field: &Field) -> syn::Result<Option<&Attribute>> {
909    let mut foreign_attr = None;
910
911    for attr in &field.attrs {
912        if !attr.path().is_ident("foreign") {
913            continue;
914        }
915
916        if foreign_attr.is_some() {
917            return Err(Error::new_spanned(
918                attr,
919                "duplicate nested-ref attribute is not supported",
920            ));
921        }
922
923        foreign_attr = Some(attr);
924    }
925
926    Ok(foreign_attr)
927}
928
929fn validate_foreign_field(field: &Field, attr: &Attribute) -> syn::Result<Type> {
930    if attr.path().is_ident("foreign") {
931        return foreign_leaf_type(&field.ty)
932            .ok_or_else(|| Error::new_spanned(&field.ty, BINDREF_ACCEPTED_SHAPES));
933    }
934
935    Err(Error::new_spanned(attr, "unsupported foreign attribute"))
936}
937
938const BINDREF_ACCEPTED_SHAPES: &str =
939    "#[foreign] supports recursive Option<_> / Vec<_> shapes whose leaf type implements appdb::Bridge";
940
941const BINDREF_BRIDGE_STORE_ONLY: &str =
942    "#[foreign] leaf types must derive Store or #[derive(Bridge)] dispatcher enums";
943
944#[derive(Clone)]
945struct ForeignField {
946    ident: syn::Ident,
947    kind: ForeignFieldKind,
948}
949
950#[derive(Clone)]
951struct ForeignFieldKind {
952    original_ty: Type,
953    stored_ty: Type,
954}
955
956fn parse_foreign_field(field: &Field, attr: &Attribute) -> syn::Result<ForeignField> {
957    validate_foreign_field(field, attr)?;
958    let ident = field.ident.clone().expect("named field");
959
960    let kind = ForeignFieldKind {
961        original_ty: field.ty.clone(),
962        stored_ty: foreign_stored_type(&field.ty)
963            .ok_or_else(|| Error::new_spanned(&field.ty, BINDREF_ACCEPTED_SHAPES))?,
964    };
965
966    Ok(ForeignField { ident, kind })
967}
968
969fn foreign_field_kind<'a>(
970    ident: &syn::Ident,
971    fields: &'a [ForeignField],
972) -> Option<&'a ForeignFieldKind> {
973    fields
974        .iter()
975        .find(|field| field.ident == *ident)
976        .map(|field| &field.kind)
977}
978
979fn stored_field_type(field: &Field, foreign_fields: &[ForeignField]) -> Type {
980    let ident = field.ident.as_ref().expect("named field");
981    match foreign_field_kind(ident, foreign_fields) {
982        Some(ForeignFieldKind { stored_ty, .. }) => stored_ty.clone(),
983        None => field.ty.clone(),
984    }
985}
986
987fn foreign_stored_type(ty: &Type) -> Option<Type> {
988    if let Some(inner) = option_inner_type(ty) {
989        let inner = foreign_stored_type(inner)?;
990        return Some(syn::parse_quote!(::std::option::Option<#inner>));
991    }
992
993    if let Some(inner) = vec_inner_type(ty) {
994        let inner = foreign_stored_type(inner)?;
995        return Some(syn::parse_quote!(::std::vec::Vec<#inner>));
996    }
997
998    direct_store_child_type(ty)
999        .cloned()
1000        .map(|_| syn::parse_quote!(::surrealdb::types::RecordId))
1001}
1002
1003fn foreign_leaf_type(ty: &Type) -> Option<Type> {
1004    if let Some(inner) = option_inner_type(ty) {
1005        return foreign_leaf_type(inner);
1006    }
1007
1008    if let Some(inner) = vec_inner_type(ty) {
1009        return foreign_leaf_type(inner);
1010    }
1011
1012    direct_store_child_type(ty).cloned().map(Type::Path)
1013}
1014
1015fn invalid_foreign_leaf_type(ty: &Type) -> Option<Type> {
1016    let leaf = foreign_leaf_type(ty)?;
1017    match &leaf {
1018        Type::Path(type_path) => {
1019            let segment = type_path.path.segments.last()?;
1020            if matches!(segment.arguments, PathArguments::None) {
1021                None
1022            } else {
1023                Some(leaf)
1024            }
1025        }
1026        _ => Some(leaf),
1027    }
1028}
1029
1030fn direct_store_child_type(ty: &Type) -> Option<&TypePath> {
1031    let Type::Path(type_path) = ty else {
1032        return None;
1033    };
1034
1035    let segment = type_path.path.segments.last()?;
1036    if !matches!(segment.arguments, PathArguments::None) {
1037        return None;
1038    }
1039
1040    if is_id_type(ty) || is_string_type(ty) || is_common_non_store_leaf_type(ty) {
1041        return None;
1042    }
1043
1044    Some(type_path)
1045}
1046
1047fn is_common_non_store_leaf_type(ty: &Type) -> bool {
1048    matches!(
1049        ty,
1050        Type::Path(TypePath { path, .. })
1051            if path.is_ident("bool")
1052                || path.is_ident("u8")
1053                || path.is_ident("u16")
1054                || path.is_ident("u32")
1055                || path.is_ident("u64")
1056                || path.is_ident("u128")
1057                || path.is_ident("usize")
1058                || path.is_ident("i8")
1059                || path.is_ident("i16")
1060                || path.is_ident("i32")
1061                || path.is_ident("i64")
1062                || path.is_ident("i128")
1063                || path.is_ident("isize")
1064                || path.is_ident("f32")
1065                || path.is_ident("f64")
1066                || path.is_ident("char")
1067    )
1068}
1069
1070fn secure_field_count(fields: &syn::punctuated::Punctuated<Field, syn::token::Comma>) -> usize {
1071    fields
1072        .iter()
1073        .filter(|field| has_secure_attr(&field.attrs))
1074        .count()
1075}
1076
1077fn relation_name_override(attrs: &[Attribute]) -> syn::Result<Option<String>> {
1078    for attr in attrs {
1079        if !attr.path().is_ident("relation") {
1080            continue;
1081        }
1082
1083        let mut name = None;
1084        attr.parse_nested_meta(|meta| {
1085            if meta.path.is_ident("name") {
1086                let value = meta.value()?;
1087                let literal: syn::LitStr = value.parse()?;
1088                name = Some(literal.value());
1089                Ok(())
1090            } else {
1091                Err(meta.error("unsupported relation attribute"))
1092            }
1093        })?;
1094        return Ok(name);
1095    }
1096
1097    Ok(None)
1098}
1099
1100enum SecureKind {
1101    String,
1102    OptionString,
1103}
1104
1105impl SecureKind {
1106    fn encrypted_type(&self) -> proc_macro2::TokenStream {
1107        match self {
1108            SecureKind::String => quote! { ::std::vec::Vec<u8> },
1109            SecureKind::OptionString => quote! { ::std::option::Option<::std::vec::Vec<u8>> },
1110        }
1111    }
1112
1113    fn encrypt_with_context_expr(&self, ident: &syn::Ident) -> proc_macro2::TokenStream {
1114        match self {
1115            SecureKind::String => {
1116                quote! { ::appdb::crypto::encrypt_string(&self.#ident, context)? }
1117            }
1118            SecureKind::OptionString => {
1119                quote! { ::appdb::crypto::encrypt_optional_string(&self.#ident, context)? }
1120            }
1121        }
1122    }
1123
1124    fn decrypt_with_context_expr(&self, ident: &syn::Ident) -> proc_macro2::TokenStream {
1125        match self {
1126            SecureKind::String => {
1127                quote! { ::appdb::crypto::decrypt_string(&encrypted.#ident, context)? }
1128            }
1129            SecureKind::OptionString => {
1130                quote! { ::appdb::crypto::decrypt_optional_string(&encrypted.#ident, context)? }
1131            }
1132        }
1133    }
1134
1135    fn encrypt_with_runtime_expr(
1136        &self,
1137        ident: &syn::Ident,
1138        field_tag_ident: &syn::Ident,
1139    ) -> proc_macro2::TokenStream {
1140        match self {
1141            SecureKind::String => {
1142                quote! {{
1143                    let context = ::appdb::crypto::resolve_crypto_context_for::<#field_tag_ident>()?;
1144                    ::appdb::crypto::encrypt_string(&self.#ident, context.as_ref())?
1145                }}
1146            }
1147            SecureKind::OptionString => {
1148                quote! {{
1149                    let context = ::appdb::crypto::resolve_crypto_context_for::<#field_tag_ident>()?;
1150                    ::appdb::crypto::encrypt_optional_string(&self.#ident, context.as_ref())?
1151                }}
1152            }
1153        }
1154    }
1155
1156    fn decrypt_with_runtime_expr(
1157        &self,
1158        ident: &syn::Ident,
1159        field_tag_ident: &syn::Ident,
1160    ) -> proc_macro2::TokenStream {
1161        match self {
1162            SecureKind::String => {
1163                quote! {{
1164                    let context = ::appdb::crypto::resolve_crypto_context_for::<#field_tag_ident>()?;
1165                    ::appdb::crypto::decrypt_string(&encrypted.#ident, context.as_ref())?
1166                }}
1167            }
1168            SecureKind::OptionString => {
1169                quote! {{
1170                    let context = ::appdb::crypto::resolve_crypto_context_for::<#field_tag_ident>()?;
1171                    ::appdb::crypto::decrypt_optional_string(&encrypted.#ident, context.as_ref())?
1172                }}
1173            }
1174        }
1175    }
1176}
1177
1178fn secure_kind(field: &Field) -> syn::Result<SecureKind> {
1179    if is_string_type(&field.ty) {
1180        return Ok(SecureKind::String);
1181    }
1182
1183    if let Some(inner) = option_inner_type(&field.ty) {
1184        if is_string_type(inner) {
1185            return Ok(SecureKind::OptionString);
1186        }
1187    }
1188
1189    Err(Error::new_spanned(
1190        &field.ty,
1191        "#[secure] currently supports only String and Option<String>",
1192    ))
1193}
1194
1195fn is_string_type(ty: &Type) -> bool {
1196    match ty {
1197        Type::Path(TypePath { path, .. }) => path.is_ident("String"),
1198        _ => false,
1199    }
1200}
1201
1202fn is_id_type(ty: &Type) -> bool {
1203    match ty {
1204        Type::Path(TypePath { path, .. }) => path.segments.last().is_some_and(|segment| {
1205            let ident = segment.ident.to_string();
1206            ident == "Id"
1207        }),
1208        _ => false,
1209    }
1210}
1211
1212fn option_inner_type(ty: &Type) -> Option<&Type> {
1213    let Type::Path(TypePath { path, .. }) = ty else {
1214        return None;
1215    };
1216    let segment = path.segments.last()?;
1217    if segment.ident != "Option" {
1218        return None;
1219    }
1220    let PathArguments::AngleBracketed(args) = &segment.arguments else {
1221        return None;
1222    };
1223    let GenericArgument::Type(inner) = args.args.first()? else {
1224        return None;
1225    };
1226    Some(inner)
1227}
1228
1229fn vec_inner_type(ty: &Type) -> Option<&Type> {
1230    let Type::Path(TypePath { path, .. }) = ty else {
1231        return None;
1232    };
1233    let segment = path.segments.last()?;
1234    if segment.ident != "Vec" {
1235        return None;
1236    }
1237    let PathArguments::AngleBracketed(args) = &segment.arguments else {
1238        return None;
1239    };
1240    let GenericArgument::Type(inner) = args.args.first()? else {
1241        return None;
1242    };
1243    Some(inner)
1244}
1245
1246fn to_snake_case(input: &str) -> String {
1247    let mut out = String::with_capacity(input.len() + 4);
1248    let mut prev_is_lower_or_digit = false;
1249
1250    for ch in input.chars() {
1251        if ch.is_ascii_uppercase() {
1252            if prev_is_lower_or_digit {
1253                out.push('_');
1254            }
1255            out.push(ch.to_ascii_lowercase());
1256            prev_is_lower_or_digit = false;
1257        } else {
1258            out.push(ch);
1259            prev_is_lower_or_digit = ch.is_ascii_lowercase() || ch.is_ascii_digit();
1260        }
1261    }
1262
1263    out
1264}
1265
1266fn to_pascal_case(input: &str) -> String {
1267    let mut out = String::with_capacity(input.len());
1268    let mut uppercase_next = true;
1269
1270    for ch in input.chars() {
1271        if ch == '_' || ch == '-' {
1272            uppercase_next = true;
1273            continue;
1274        }
1275
1276        if uppercase_next {
1277            out.push(ch.to_ascii_uppercase());
1278            uppercase_next = false;
1279        } else {
1280            out.push(ch);
1281        }
1282    }
1283
1284    out
1285}