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}