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