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 pub async fn save(self) -> ::anyhow::Result<Self> {
440 <Self as ::appdb::repository::Crud>::save(self).await
441 }
442
443 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}