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(
18 Store,
19 attributes(
20 unique,
21 secure,
22 foreign,
23 table_as,
24 crypto,
25 relate,
26 back_relate,
27 pagin,
28 fill
29 )
30)]
31pub fn derive_store(input: TokenStream) -> TokenStream {
32 match derive_store_impl(parse_macro_input!(input as DeriveInput)) {
33 Ok(tokens) => tokens.into(),
34 Err(err) => err.to_compile_error().into(),
35 }
36}
37
38#[proc_macro_derive(Relation, attributes(relation))]
39pub fn derive_relation(input: TokenStream) -> TokenStream {
40 match derive_relation_impl(parse_macro_input!(input as DeriveInput)) {
41 Ok(tokens) => tokens.into(),
42 Err(err) => err.to_compile_error().into(),
43 }
44}
45
46#[proc_macro_derive(Bridge)]
47pub fn derive_bridge(input: TokenStream) -> TokenStream {
48 match derive_bridge_impl(parse_macro_input!(input as DeriveInput)) {
49 Ok(tokens) => tokens.into(),
50 Err(err) => err.to_compile_error().into(),
51 }
52}
53
54fn derive_store_impl(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
55 let struct_ident = input.ident;
56 let vis = input.vis.clone();
57 let table_alias = table_alias_target(&input.attrs)?;
58
59 let named_fields = match input.data {
60 Data::Struct(data) => match data.fields {
61 Fields::Named(fields) => fields.named,
62 _ => {
63 return Err(Error::new_spanned(
64 struct_ident,
65 "Store can only be derived for structs with named fields",
66 ));
67 }
68 },
69 _ => {
70 return Err(Error::new_spanned(
71 struct_ident,
72 "Store can only be derived for structs",
73 ));
74 }
75 };
76
77 let id_fields = named_fields
78 .iter()
79 .filter(|field| is_id_type(&field.ty))
80 .map(|field| field.ident.clone().expect("named field"))
81 .collect::<Vec<_>>();
82
83 let secure_fields = named_fields
84 .iter()
85 .filter(|field| has_secure_attr(&field.attrs))
86 .map(|field| field.ident.clone().expect("named field"))
87 .collect::<Vec<_>>();
88
89 let unique_fields = named_fields
90 .iter()
91 .filter(|field| has_unique_attr(&field.attrs))
92 .map(|field| field.ident.clone().expect("named field"))
93 .collect::<Vec<_>>();
94
95 let pagin_fields = named_fields
96 .iter()
97 .filter_map(|field| match field_pagin_attr(field) {
98 Ok(Some(attr)) => Some(parse_pagin_field(field, attr)),
99 Ok(None) => None,
100 Err(err) => Some(Err(err)),
101 })
102 .collect::<syn::Result<Vec<_>>>()?;
103 if pagin_fields.len() > 1 {
104 return Err(Error::new_spanned(
105 &pagin_fields[1].ident,
106 "Store supports at most one #[pagin] field",
107 ));
108 }
109 let pagin_field = pagin_fields.first().cloned();
110
111 let fill_fields = named_fields
112 .iter()
113 .filter_map(|field| match field_fill_attr(field) {
114 Ok(Some(attr)) => Some(parse_fill_field(field, attr)),
115 Ok(None) => None,
116 Err(err) => Some(Err(err)),
117 })
118 .collect::<syn::Result<Vec<_>>>()?;
119
120 if id_fields.len() > 1 {
121 return Err(Error::new_spanned(
122 struct_ident,
123 "Store supports at most one `Id` field for automatic HasId generation",
124 ));
125 }
126
127 if let Some(invalid_field) = named_fields
128 .iter()
129 .find(|field| has_secure_attr(&field.attrs) && 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 "#[secure] fields cannot be used as #[unique] lookup keys",
135 ));
136 }
137
138 let foreign_fields = named_fields
139 .iter()
140 .filter_map(|field| match field_foreign_attr(field) {
141 Ok(Some(attr)) => Some(parse_foreign_field(field, attr)),
142 Ok(None) => None,
143 Err(err) => Some(Err(err)),
144 })
145 .collect::<syn::Result<Vec<_>>>()?;
146
147 let relate_fields = named_fields
148 .iter()
149 .filter_map(|field| match field_relation_attr(field) {
150 Ok(Some(attr)) => Some(parse_relate_field(field, attr)),
151 Ok(None) => None,
152 Err(err) => Some(Err(err)),
153 })
154 .collect::<syn::Result<Vec<_>>>()?;
155
156 if let Some(non_store_child) = foreign_fields
157 .iter()
158 .find_map(|field| invalid_foreign_leaf_type(&field.kind.original_ty))
159 {
160 return Err(Error::new_spanned(
161 non_store_child,
162 BINDREF_BRIDGE_STORE_ONLY,
163 ));
164 }
165
166 if let Some(invalid_field) = named_fields.iter().find_map(|field| {
167 field_relation_attr(field)
168 .ok()
169 .flatten()
170 .filter(|_| has_unique_attr(&field.attrs))
171 .map(|attr| (field, attr))
172 }) {
173 let ident = invalid_field.0.ident.as_ref().expect("named field");
174 return Err(Error::new_spanned(
175 ident,
176 format!(
177 "{} fields cannot be used as #[unique] lookup keys",
178 relation_attr_label(invalid_field.1)
179 ),
180 ));
181 }
182
183 if let Some(invalid_field) = named_fields.iter().find_map(|field| {
184 field_relation_attr(field)
185 .ok()
186 .flatten()
187 .filter(|_| has_secure_attr(&field.attrs))
188 .map(|attr| (field, attr))
189 }) {
190 let ident = invalid_field.0.ident.as_ref().expect("named field");
191 return Err(Error::new_spanned(
192 ident,
193 format!(
194 "{} fields cannot be marked #[secure]",
195 relation_attr_label(invalid_field.1)
196 ),
197 ));
198 }
199
200 if let Some(invalid_field) = named_fields.iter().find_map(|field| {
201 field_relation_attr(field)
202 .ok()
203 .flatten()
204 .filter(|_| field_foreign_attr(field).ok().flatten().is_some())
205 .map(|attr| (field, attr))
206 }) {
207 let ident = invalid_field.0.ident.as_ref().expect("named field");
208 return Err(Error::new_spanned(
209 ident,
210 format!(
211 "{} cannot be combined with #[foreign]",
212 relation_attr_label(invalid_field.1)
213 ),
214 ));
215 }
216
217 if let Some(invalid_field) = named_fields.iter().find(|field| {
218 field_pagin_attr(field).ok().flatten().is_some() && has_secure_attr(&field.attrs)
219 }) {
220 let ident = invalid_field.ident.as_ref().expect("named field");
221 return Err(Error::new_spanned(
222 ident,
223 "#[pagin] fields cannot be marked #[secure]",
224 ));
225 }
226
227 if let Some(invalid_field) = named_fields.iter().find(|field| {
228 field_pagin_attr(field).ok().flatten().is_some()
229 && field_foreign_attr(field).ok().flatten().is_some()
230 }) {
231 let ident = invalid_field.ident.as_ref().expect("named field");
232 return Err(Error::new_spanned(
233 ident,
234 "#[pagin] cannot be combined with #[foreign]",
235 ));
236 }
237
238 if let Some(invalid_field) = named_fields.iter().find_map(|field| {
239 field_pagin_attr(field)
240 .ok()
241 .flatten()
242 .and_then(|_| field_relation_attr(field).ok().flatten())
243 .map(|attr| (field, attr))
244 }) {
245 let ident = invalid_field.0.ident.as_ref().expect("named field");
246 return Err(Error::new_spanned(
247 ident,
248 format!(
249 "#[pagin] cannot be combined with {}",
250 relation_attr_label(invalid_field.1)
251 ),
252 ));
253 }
254
255 if let Some(invalid_field) = named_fields.iter().find_map(|field| {
256 field_fill_attr(field)
257 .ok()
258 .flatten()
259 .filter(|_| has_secure_attr(&field.attrs))
260 .map(|attr| (field, attr))
261 }) {
262 let ident = invalid_field.0.ident.as_ref().expect("named field");
263 return Err(Error::new_spanned(
264 ident,
265 "#[fill(...)] fields cannot be marked #[secure]",
266 ));
267 }
268
269 if let Some(invalid_field) = named_fields.iter().find_map(|field| {
270 field_fill_attr(field)
271 .ok()
272 .flatten()
273 .filter(|_| field_foreign_attr(field).ok().flatten().is_some())
274 .map(|attr| (field, attr))
275 }) {
276 let ident = invalid_field.0.ident.as_ref().expect("named field");
277 return Err(Error::new_spanned(
278 ident,
279 "#[fill(...)] cannot be combined with #[foreign]",
280 ));
281 }
282
283 if let Some(invalid_field) = named_fields.iter().find_map(|field| {
284 field_fill_attr(field)
285 .ok()
286 .flatten()
287 .and_then(|_| field_relation_attr(field).ok().flatten())
288 .map(|attr| (field, attr))
289 }) {
290 let ident = invalid_field.0.ident.as_ref().expect("named field");
291 return Err(Error::new_spanned(
292 ident,
293 format!(
294 "#[fill(...)] cannot be combined with {}",
295 relation_attr_label(invalid_field.1)
296 ),
297 ));
298 }
299
300 if let Some(invalid_field) = named_fields.iter().find(|field| {
301 is_autofill_type(&field.ty) && field_fill_attr(field).ok().flatten().is_none()
302 }) {
303 let ident = invalid_field.ident.as_ref().expect("named field");
304 return Err(Error::new_spanned(
305 ident,
306 "appdb::AutoFill fields require #[fill(...)]",
307 ));
308 }
309
310 let mut seen_relation_names = HashSet::new();
311 for field in &relate_fields {
312 if !seen_relation_names.insert(field.relation_name.clone()) {
313 return Err(Error::new_spanned(
314 &field.ident,
315 format!(
316 "duplicate {} relation name is not supported within one Store model",
317 field.direction.attr_label()
318 ),
319 ));
320 }
321 }
322
323 let auto_has_id_impl = id_fields.first().map(|field| {
324 quote! {
325 impl ::appdb::model::meta::HasId for #struct_ident {
326 fn id(&self) -> ::surrealdb::types::RecordId {
327 ::surrealdb::types::RecordId::new(
328 <Self as ::appdb::model::meta::ModelMeta>::storage_table(),
329 self.#field.clone(),
330 )
331 }
332 }
333 }
334 });
335
336 let resolve_record_id_impl = if let Some(field) = id_fields.first() {
337 quote! {
338 #[::async_trait::async_trait]
339 impl ::appdb::model::meta::ResolveRecordId for #struct_ident {
340 async fn resolve_record_id(&self) -> ::anyhow::Result<::surrealdb::types::RecordId> {
341 Ok(::surrealdb::types::RecordId::new(
342 <Self as ::appdb::model::meta::ModelMeta>::storage_table(),
343 self.#field.clone(),
344 ))
345 }
346 }
347 }
348 } else {
349 quote! {
350 #[::async_trait::async_trait]
351 impl ::appdb::model::meta::ResolveRecordId for #struct_ident {
352 async fn resolve_record_id(&self) -> ::anyhow::Result<::surrealdb::types::RecordId> {
353 ::appdb::repository::Repo::<Self>::find_unique_id_for(self).await
354 }
355 }
356 }
357 };
358
359 let resolved_table_name_expr = if let Some(target_ty) = &table_alias {
360 quote! { <#target_ty as ::appdb::model::meta::ModelMeta>::table_name() }
361 } else {
362 quote! {
363 {
364 let table = ::appdb::model::meta::default_table_name(stringify!(#struct_ident));
365 ::appdb::model::meta::register_table(stringify!(#struct_ident), table)
366 }
367 }
368 };
369
370 let unique_schema_impls = unique_fields.iter().map(|field| {
371 let field_name = field.to_string();
372 let index_name = format!(
373 "{}_{}_unique",
374 resolved_schema_table_name(&struct_ident, table_alias.as_ref()),
375 field_name
376 );
377 let ddl = format!(
378 "DEFINE INDEX IF NOT EXISTS {index_name} ON {} FIELDS {field_name} UNIQUE;",
379 resolved_schema_table_name(&struct_ident, table_alias.as_ref())
380 );
381
382 quote! {
383 ::inventory::submit! {
384 ::appdb::model::schema::SchemaItem {
385 ddl: #ddl,
386 }
387 }
388 }
389 });
390
391 let pagin_schema_impl = pagin_field.iter().map(|field| {
392 let field_name = field.ident.to_string();
393 let index_name = format!(
394 "{}_{}_id_pagin",
395 resolved_schema_table_name(&struct_ident, table_alias.as_ref()),
396 field_name
397 );
398 let ddl = if field_name == "id" {
399 format!(
400 "DEFINE INDEX IF NOT EXISTS {index_name} ON {} FIELDS id;",
401 resolved_schema_table_name(&struct_ident, table_alias.as_ref())
402 )
403 } else {
404 format!(
405 "DEFINE INDEX IF NOT EXISTS {index_name} ON {} FIELDS {field_name},id;",
406 resolved_schema_table_name(&struct_ident, table_alias.as_ref())
407 )
408 };
409
410 quote! {
411 ::inventory::submit! {
412 ::appdb::model::schema::SchemaItem {
413 ddl: #ddl,
414 }
415 }
416 }
417 });
418
419 let lookup_fields = if unique_fields.is_empty() {
420 named_fields
421 .iter()
422 .filter_map(|field| {
423 let ident = field.ident.as_ref()?;
424 if ident == "id"
425 || secure_fields.iter().any(|secure| secure == ident)
426 || relate_fields.iter().any(|relate| relate.ident == *ident)
427 {
428 None
429 } else {
430 Some(ident.to_string())
431 }
432 })
433 .collect::<Vec<_>>()
434 } else {
435 unique_fields
436 .iter()
437 .map(|field| field.to_string())
438 .collect::<Vec<_>>()
439 };
440
441 let foreign_field_literals = foreign_fields
442 .iter()
443 .map(|field| field.ident.to_string())
444 .map(|field| quote! { #field })
445 .collect::<Vec<_>>();
446 let relate_field_literals = relate_fields
447 .iter()
448 .map(|field| field.ident.to_string())
449 .map(|field| quote! { #field })
450 .collect::<Vec<_>>();
451 if id_fields.is_empty() && lookup_fields.is_empty() {
452 return Err(Error::new_spanned(
453 struct_ident,
454 "Store requires an `Id` field or at least one non-secure lookup field for automatic record resolution",
455 ));
456 }
457 let lookup_field_literals = lookup_fields.iter().map(|field| quote! { #field });
458 let resolve_lookup_field_value_arms = foreign_fields.iter().map(|field| {
459 let ident = &field.ident;
460 let field_name = ident.to_string();
461 let original_ty = &field.kind.original_ty;
462 quote! {
463 #field_name => Ok(::std::option::Option::Some(
464 ::surrealdb::types::SurrealValue::into_value(
465 <#original_ty as ::appdb::ForeignLookupShape>::resolve_foreign_lookup_shape(&self.#ident).await?
466 )
467 )),
468 }
469 });
470
471 let pagination_meta_impl = if let Some(field) = &pagin_field {
472 let field_name = field.ident.to_string();
473 quote! {
474 impl ::appdb::model::meta::PaginationMeta for #struct_ident {
475 fn pagination_field() -> ::std::option::Option<&'static str> {
476 ::std::option::Option::Some(#field_name)
477 }
478 }
479 }
480 } else {
481 quote! {
482 impl ::appdb::model::meta::PaginationMeta for #struct_ident {}
483 }
484 };
485
486 let pagination_methods_impl = if pagin_field.is_some() {
487 quote! {
488 pub async fn pagin_desc(
489 count: i64,
490 cursor: ::std::option::Option<::appdb::PageCursor>,
491 ) -> ::anyhow::Result<::appdb::Page<Self>> {
492 ::appdb::repository::Repo::<Self>::pagin_desc(count, cursor).await
493 }
494
495 pub async fn pagin_asc(
496 count: i64,
497 cursor: ::std::option::Option<::appdb::PageCursor>,
498 ) -> ::anyhow::Result<::appdb::Page<Self>> {
499 ::appdb::repository::Repo::<Self>::pagin_asc(count, cursor).await
500 }
501 }
502 } else {
503 quote! {}
504 };
505
506 let stored_model_impl = if !foreign_fields.is_empty() {
507 quote! {}
508 } else if secure_field_count(&named_fields) > 0 {
509 quote! {
510 impl ::appdb::StoredModel for #struct_ident {
511 type Stored = <Self as ::appdb::Sensitive>::Encrypted;
512
513 fn into_stored(self) -> ::anyhow::Result<Self::Stored> {
514 <Self as ::appdb::Sensitive>::encrypt_with_runtime_resolver(&self)
515 .map_err(::anyhow::Error::from)
516 }
517
518 fn from_stored(stored: Self::Stored) -> ::anyhow::Result<Self> {
519 <Self as ::appdb::Sensitive>::decrypt_with_runtime_resolver(&stored)
520 .map_err(::anyhow::Error::from)
521 }
522
523 fn supports_create_return_id() -> bool {
524 false
525 }
526 }
527 }
528 } else {
529 quote! {
530 impl ::appdb::StoredModel for #struct_ident {
531 type Stored = Self;
532
533 fn into_stored(self) -> ::anyhow::Result<Self::Stored> {
534 ::std::result::Result::Ok(self)
535 }
536
537 fn from_stored(stored: Self::Stored) -> ::anyhow::Result<Self> {
538 ::std::result::Result::Ok(stored)
539 }
540 }
541 }
542 };
543
544 let stored_fields = named_fields.iter().map(|field| {
545 let ident = field.ident.clone().expect("named field");
546 let ty = stored_field_type(field, &foreign_fields);
547 if is_record_id_type(&ty) {
548 quote! {
549 #[serde(deserialize_with = "::appdb::serde_utils::id::deserialize_record_id_or_compat_string")]
550 #ident: #ty
551 }
552 } else {
553 quote! { #ident: #ty }
554 }
555 });
556
557 let into_stored_assignments = named_fields.iter().map(|field| {
558 let ident = field.ident.clone().expect("named field");
559 match foreign_field_kind(&ident, &foreign_fields) {
560 Some(ForeignFieldKind { original_ty, .. }) => quote! {
561 #ident: <#original_ty as ::appdb::ForeignShape>::persist_foreign_shape(value.#ident).await?
562 },
563 None => quote! { #ident: value.#ident },
564 }
565 });
566
567 let from_stored_assignments = named_fields.iter().map(|field| {
568 let ident = field.ident.clone().expect("named field");
569 match foreign_field_kind(&ident, &foreign_fields) {
570 Some(ForeignFieldKind { original_ty, .. }) => quote! {
571 #ident: <#original_ty as ::appdb::ForeignShape>::hydrate_foreign_shape(stored.#ident).await?
572 },
573 None => quote! { #ident: stored.#ident },
574 }
575 });
576
577 let fill_assignments = fill_fields.iter().map(|field| {
578 let ident = &field.ident;
579 match field.provider {
580 FillProvider::Now => quote! {
581 value.#ident.fill_now_if_pending();
582 },
583 }
584 });
585
586 let decode_foreign_fields = foreign_fields.iter().map(|field| {
587 let ident = field.ident.to_string();
588 quote! {
589 if let ::std::option::Option::Some(value) = map.get_mut(#ident) {
590 ::appdb::decode_stored_record_links(value);
591 }
592 }
593 });
594
595 let relation_methods_impl = if relate_fields.is_empty() {
596 quote! {}
597 } else {
598 let strip_relation_fields = relate_fields.iter().map(|field| {
599 let ident = field.ident.to_string();
600 quote! {
601 map.remove(#ident);
602 }
603 });
604
605 let inject_relation_values_from_model = relate_fields.iter().map(|field| {
606 let ident = &field.ident;
607 let name = ident.to_string();
608 quote! {
609 map.insert(#name.to_owned(), ::serde_json::to_value(&self.#ident)?);
610 }
611 });
612
613 let prepare_relation_writes = relate_fields.iter().map(|field| {
614 let ident = &field.ident;
615 let relation_name = &field.relation_name;
616 let field_ty = &field.field_ty;
617 let write_direction = field.direction.write_direction_tokens();
618 let write_edges = field.direction.write_edges_tokens();
619 quote! {
620 {
621 let ids = <#field_ty as ::appdb::RelateShape>::persist_relate_shape(self.#ident.clone()).await?;
622 writes.push(::appdb::RelationWrite {
623 relation: #relation_name,
624 record: record.clone(),
625 direction: #write_direction,
626 edges: #write_edges,
627 });
628 }
629 }
630 });
631
632 let inject_relation_values_from_db = relate_fields.iter().map(|field| {
633 let relation_name = &field.relation_name;
634 let field_ty = &field.field_ty;
635 let ident = field.ident.to_string();
636 let load_relation_ids = field.direction.load_edges_tokens(relation_name);
637 quote! {
638 {
639 let value = <#field_ty as ::appdb::RelateShape>::hydrate_relate_shape(#load_relation_ids).await?;
640 map.insert(#ident.to_owned(), ::serde_json::to_value(value)?);
641 }
642 }
643 });
644
645 quote! {
646 fn has_relation_fields() -> bool {
647 true
648 }
649
650 fn relation_field_names() -> &'static [&'static str] {
651 &[ #( #relate_field_literals ),* ]
652 }
653
654 fn strip_relation_fields(row: &mut ::serde_json::Value) {
655 if let ::serde_json::Value::Object(map) = row {
656 #( #strip_relation_fields )*
657 }
658 }
659
660 fn inject_relation_values_from_model(
661 &self,
662 row: &mut ::serde_json::Value,
663 ) -> ::anyhow::Result<()> {
664 if let ::serde_json::Value::Object(map) = row {
665 #( #inject_relation_values_from_model )*
666 }
667 Ok(())
668 }
669
670 fn prepare_relation_writes(
671 &self,
672 record: ::surrealdb::types::RecordId,
673 ) -> impl ::std::future::Future<Output = ::anyhow::Result<::std::vec::Vec<::appdb::RelationWrite>>> + Send {
674 async move {
675 let mut writes = ::std::vec::Vec::new();
676 #( #prepare_relation_writes )*
677 Ok(writes)
678 }
679 }
680
681 fn inject_relation_values_from_db(
682 record: ::surrealdb::types::RecordId,
683 row: &mut ::serde_json::Value,
684 ) -> impl ::std::future::Future<Output = ::anyhow::Result<()>> + Send {
685 async move {
686 if let ::serde_json::Value::Object(map) = row {
687 #( #inject_relation_values_from_db )*
688 }
689 Ok(())
690 }
691 }
692 }
693 };
694
695 let foreign_model_impl = if foreign_fields.is_empty() {
696 quote! {
697 impl ::appdb::ForeignModel for #struct_ident {
698 async fn persist_foreign(value: Self) -> ::anyhow::Result<Self::Stored> {
699 let mut value = value;
700 #( #fill_assignments )*
701 <Self as ::appdb::StoredModel>::into_stored(value)
702 }
703
704 async fn hydrate_foreign(stored: Self::Stored) -> ::anyhow::Result<Self> {
705 <Self as ::appdb::StoredModel>::from_stored(stored)
706 }
707
708 fn decode_stored_row(
709 row: ::surrealdb::types::Value,
710 ) -> ::anyhow::Result<Self::Stored>
711 where
712 Self::Stored: ::serde::de::DeserializeOwned,
713 {
714 Ok(::serde_json::from_value(row.into_json_value())?)
715 }
716
717 #relation_methods_impl
718 }
719 }
720 } else {
721 let stored_struct_ident = format_ident!("AppdbStored{}", struct_ident);
722 quote! {
723 #[derive(
724 Debug,
725 Clone,
726 ::serde::Serialize,
727 ::serde::Deserialize,
728 ::surrealdb::types::SurrealValue,
729 )]
730 #vis struct #stored_struct_ident {
731 #( #stored_fields, )*
732 }
733
734 impl ::appdb::StoredModel for #struct_ident {
735 type Stored = #stored_struct_ident;
736
737 fn into_stored(self) -> ::anyhow::Result<Self::Stored> {
738 unreachable!("foreign fields require async persist_foreign")
739 }
740
741 fn from_stored(_stored: Self::Stored) -> ::anyhow::Result<Self> {
742 unreachable!("foreign fields require async hydrate_foreign")
743 }
744 }
745
746 impl ::appdb::ForeignModel for #struct_ident {
747 async fn persist_foreign(value: Self) -> ::anyhow::Result<Self::Stored> {
748 let mut value = value;
749 #( #fill_assignments )*
750 Ok(#stored_struct_ident {
751 #( #into_stored_assignments, )*
752 })
753 }
754
755 async fn hydrate_foreign(stored: Self::Stored) -> ::anyhow::Result<Self> {
756 Ok(Self {
757 #( #from_stored_assignments, )*
758 })
759 }
760
761 fn has_foreign_fields() -> bool {
762 true
763 }
764
765 fn foreign_field_names() -> &'static [&'static str] {
766 &[ #( #foreign_field_literals ),* ]
767 }
768
769 fn decode_stored_row(
770 row: ::surrealdb::types::Value,
771 ) -> ::anyhow::Result<Self::Stored>
772 where
773 Self::Stored: ::serde::de::DeserializeOwned,
774 {
775 let mut row = row.into_json_value();
776 if let ::serde_json::Value::Object(map) = &mut row {
777 #( #decode_foreign_fields )*
778 }
779 Ok(::serde_json::from_value(row)?)
780 }
781
782 #relation_methods_impl
783 }
784 }
785 };
786
787 let store_marker_ident = format_ident!("AppdbStoreMarker{}", struct_ident);
788
789 Ok(quote! {
790 #[doc(hidden)]
791 #vis struct #store_marker_ident;
792
793 impl ::appdb::model::meta::ModelMeta for #struct_ident {
794 fn storage_table() -> &'static str {
795 #resolved_table_name_expr
796 }
797
798 fn table_name() -> &'static str {
799 static TABLE_NAME: ::std::sync::OnceLock<&'static str> = ::std::sync::OnceLock::new();
800 TABLE_NAME.get_or_init(|| {
801 let table = #resolved_table_name_expr;
802 ::appdb::model::meta::register_table(stringify!(#struct_ident), table)
803 })
804 }
805 }
806
807 impl ::appdb::model::meta::StoreModelMarker for #struct_ident {}
808 impl ::appdb::model::meta::StoreModelMarker for #store_marker_ident {}
809 #pagination_meta_impl
810
811 impl ::appdb::model::meta::UniqueLookupMeta for #struct_ident {
812 fn lookup_fields() -> &'static [&'static str] {
813 &[ #( #lookup_field_literals ),* ]
814 }
815
816 fn foreign_fields() -> &'static [&'static str] {
817 &[ #( #foreign_field_literals ),* ]
818 }
819
820 fn resolve_lookup_field_value(
821 &self,
822 field: &str,
823 ) -> impl ::std::future::Future<
824 Output = ::anyhow::Result<::std::option::Option<::surrealdb::types::Value>>
825 > {
826 async move {
827 match field {
828 #( #resolve_lookup_field_value_arms )*
829 _ => Ok(::std::option::Option::None),
830 }
831 }
832 }
833 }
834 #stored_model_impl
835 #foreign_model_impl
836
837 #auto_has_id_impl
838 #resolve_record_id_impl
839
840 #( #unique_schema_impls )*
841 #( #pagin_schema_impl )*
842
843 impl ::appdb::repository::Crud for #struct_ident {}
844
845 impl #struct_ident {
846 pub async fn save(self) -> ::anyhow::Result<Self> {
852 <Self as ::appdb::repository::Crud>::save(self).await
853 }
854
855 pub async fn save_many(data: ::std::vec::Vec<Self>) -> ::anyhow::Result<::std::vec::Vec<Self>> {
857 <Self as ::appdb::repository::Crud>::save_many(data).await
858 }
859
860 pub async fn get<T>(id: T) -> ::anyhow::Result<Self>
861 where
862 ::surrealdb::types::RecordIdKey: From<T>,
863 T: Send,
864 {
865 ::appdb::repository::Repo::<Self>::get(id).await
866 }
867
868 pub fn list() -> ::appdb::repository::ListQuery<Self> {
869 ::appdb::repository::Repo::<Self>::list()
870 }
871
872 pub async fn list_limit(count: i64) -> ::anyhow::Result<::std::vec::Vec<Self>> {
873 ::appdb::repository::Repo::<Self>::list_limit(count).await
874 }
875 #pagination_methods_impl
876
877 pub async fn relate_by_name<Target>(&self, target: &Target, relation: &str) -> ::anyhow::Result<()>
878 where
879 Target: ::appdb::model::meta::ResolveRecordId + Send + Sync,
880 {
881 <Self as ::appdb::graph::GraphCrud>::relate_by_name(self, target, relation).await
882 }
883
884 pub async fn back_relate_by_name<Target>(&self, target: &Target, relation: &str) -> ::anyhow::Result<()>
885 where
886 Target: ::appdb::model::meta::ResolveRecordId + Send + Sync,
887 {
888 <Self as ::appdb::graph::GraphCrud>::back_relate_by_name(self, target, relation).await
889 }
890
891 pub async fn unrelate_by_name<Target>(&self, target: &Target, relation: &str) -> ::anyhow::Result<()>
892 where
893 Target: ::appdb::model::meta::ResolveRecordId + Send + Sync,
894 {
895 <Self as ::appdb::graph::GraphCrud>::unrelate_by_name(self, target, relation).await
896 }
897
898 pub async fn outgoing_ids(&self, relation: &str) -> ::anyhow::Result<::std::vec::Vec<::surrealdb::types::RecordId>> {
899 <Self as ::appdb::repository::Crud>::outgoing_ids(self, relation).await
900 }
901
902 pub async fn outgoing<Target>(&self, relation: &str) -> ::anyhow::Result<::std::vec::Vec<Target>>
903 where
904 Target: ::appdb::model::meta::ModelMeta + ::appdb::StoredModel + ::appdb::ForeignModel,
905 <Target as ::appdb::StoredModel>::Stored: ::serde::de::DeserializeOwned,
906 {
907 <Self as ::appdb::repository::Crud>::outgoing::<Target>(self, relation).await
908 }
909
910 pub async fn outgoing_count(&self, relation: &str) -> ::anyhow::Result<i64> {
911 <Self as ::appdb::repository::Crud>::outgoing_count(self, relation).await
912 }
913
914 pub async fn outgoing_count_as<Target>(&self, relation: &str) -> ::anyhow::Result<i64>
915 where
916 Target: ::appdb::model::meta::ModelMeta + ::appdb::StoredModel + ::appdb::ForeignModel,
917 {
918 <Self as ::appdb::repository::Crud>::outgoing_count_as::<Target>(self, relation).await
919 }
920
921 pub async fn incoming_ids(&self, relation: &str) -> ::anyhow::Result<::std::vec::Vec<::surrealdb::types::RecordId>> {
922 <Self as ::appdb::repository::Crud>::incoming_ids(self, relation).await
923 }
924
925 pub async fn incoming<Target>(&self, relation: &str) -> ::anyhow::Result<::std::vec::Vec<Target>>
926 where
927 Target: ::appdb::model::meta::ModelMeta + ::appdb::StoredModel + ::appdb::ForeignModel,
928 <Target as ::appdb::StoredModel>::Stored: ::serde::de::DeserializeOwned,
929 {
930 <Self as ::appdb::repository::Crud>::incoming::<Target>(self, relation).await
931 }
932
933 pub async fn incoming_count(&self, relation: &str) -> ::anyhow::Result<i64> {
934 <Self as ::appdb::repository::Crud>::incoming_count(self, relation).await
935 }
936
937 pub async fn incoming_count_as<Target>(&self, relation: &str) -> ::anyhow::Result<i64>
938 where
939 Target: ::appdb::model::meta::ModelMeta + ::appdb::StoredModel + ::appdb::ForeignModel,
940 {
941 <Self as ::appdb::repository::Crud>::incoming_count_as::<Target>(self, relation).await
942 }
943
944 pub async fn delete_all() -> ::anyhow::Result<()> {
945 ::appdb::repository::Repo::<Self>::delete_all().await
946 }
947
948 pub async fn find_one_id(
949 k: &str,
950 v: &str,
951 ) -> ::anyhow::Result<::surrealdb::types::RecordId> {
952 ::appdb::repository::Repo::<Self>::find_one_id(k, v).await
953 }
954
955 pub async fn list_record_ids() -> ::anyhow::Result<::std::vec::Vec<::surrealdb::types::RecordId>> {
956 ::appdb::repository::Repo::<Self>::list_record_ids().await
957 }
958
959 pub async fn create_at(
960 id: ::surrealdb::types::RecordId,
961 data: Self,
962 ) -> ::anyhow::Result<Self> {
963 ::appdb::repository::Repo::<Self>::create_at(id, data).await
964 }
965
966 pub async fn upsert_at(
967 id: ::surrealdb::types::RecordId,
968 data: Self,
969 ) -> ::anyhow::Result<Self> {
970 ::appdb::repository::Repo::<Self>::upsert_at(id, data).await
971 }
972
973 pub async fn update_at(
974 self,
975 id: ::surrealdb::types::RecordId,
976 ) -> ::anyhow::Result<Self> {
977 ::appdb::repository::Repo::<Self>::update_at(id, self).await
978 }
979
980
981 pub async fn delete<T>(id: T) -> ::anyhow::Result<()>
982 where
983 ::surrealdb::types::RecordIdKey: From<T>,
984 T: Send,
985 {
986 ::appdb::repository::Repo::<Self>::delete(id).await
987 }
988 }
989 })
990}
991
992fn derive_bridge_impl(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
993 let enum_ident = input.ident;
994
995 let variants = match input.data {
996 Data::Enum(data) => data.variants,
997 _ => {
998 return Err(Error::new_spanned(
999 enum_ident,
1000 "Bridge can only be derived for enums",
1001 ));
1002 }
1003 };
1004
1005 let payloads = variants
1006 .iter()
1007 .map(parse_bridge_variant)
1008 .collect::<syn::Result<Vec<_>>>()?;
1009
1010 let from_impls = payloads.iter().map(|variant| {
1011 let variant_ident = &variant.variant_ident;
1012 let payload_ty = &variant.payload_ty;
1013
1014 quote! {
1015 impl ::std::convert::From<#payload_ty> for #enum_ident {
1016 fn from(value: #payload_ty) -> Self {
1017 Self::#variant_ident(value)
1018 }
1019 }
1020 }
1021 });
1022
1023 let persist_match_arms = payloads.iter().map(|variant| {
1024 let variant_ident = &variant.variant_ident;
1025
1026 quote! {
1027 Self::#variant_ident(value) => <_ as ::appdb::Bridge>::persist_foreign(value).await,
1028 }
1029 });
1030
1031 let hydrate_match_arms = payloads.iter().map(|variant| {
1032 let variant_ident = &variant.variant_ident;
1033 let payload_ty = &variant.payload_ty;
1034
1035 quote! {
1036 table if table == <#payload_ty as ::appdb::model::meta::ModelMeta>::storage_table() => {
1037 ::std::result::Result::Ok(Self::#variant_ident(
1038 <#payload_ty as ::appdb::Bridge>::hydrate_foreign(id).await?,
1039 ))
1040 }
1041 }
1042 });
1043
1044 let lookup_match_arms = payloads.iter().map(|variant| {
1045 let variant_ident = &variant.variant_ident;
1046 let payload_ty = &variant.payload_ty;
1047
1048 quote! {
1049 Self::#variant_ident(value) => {
1050 <#payload_ty as ::appdb::ForeignLookupShape>::resolve_foreign_lookup_shape(value).await
1051 }
1052 }
1053 });
1054
1055 Ok(quote! {
1056 #( #from_impls )*
1057
1058 #[::async_trait::async_trait]
1059 impl ::appdb::Bridge for #enum_ident {
1060 async fn persist_foreign(self) -> ::anyhow::Result<::surrealdb::types::RecordId> {
1061 match self {
1062 #( #persist_match_arms )*
1063 }
1064 }
1065
1066 async fn hydrate_foreign(
1067 id: ::surrealdb::types::RecordId,
1068 ) -> ::anyhow::Result<Self> {
1069 match id.table.to_string().as_str() {
1070 #( #hydrate_match_arms, )*
1071 table => ::anyhow::bail!(
1072 "unsupported foreign table `{table}` for enum dispatcher `{}`",
1073 ::std::stringify!(#enum_ident)
1074 ),
1075 }
1076 }
1077 }
1078
1079 impl ::appdb::ForeignLookupShape for #enum_ident {
1080 type LookupStored = ::surrealdb::types::RecordId;
1081
1082 fn resolve_foreign_lookup_shape(
1083 &self,
1084 ) -> impl ::std::future::Future<Output = ::anyhow::Result<Self::LookupStored>> {
1085 async move {
1086 match self {
1087 #( #lookup_match_arms, )*
1088 }
1089 }
1090 }
1091 }
1092 })
1093}
1094
1095#[derive(Clone)]
1096struct BridgeVariant {
1097 variant_ident: syn::Ident,
1098 payload_ty: Type,
1099}
1100
1101fn parse_bridge_variant(variant: &syn::Variant) -> syn::Result<BridgeVariant> {
1102 let payload_ty = match &variant.fields {
1103 Fields::Unnamed(fields) if fields.unnamed.len() == 1 => {
1104 fields.unnamed.first().expect("single field").ty.clone()
1105 }
1106 Fields::Unnamed(_) => {
1107 return Err(Error::new_spanned(
1108 &variant.ident,
1109 "Bridge variants must be single-field tuple variants",
1110 ));
1111 }
1112 Fields::Unit => {
1113 return Err(Error::new_spanned(
1114 &variant.ident,
1115 "Bridge does not support unit variants",
1116 ));
1117 }
1118 Fields::Named(_) => {
1119 return Err(Error::new_spanned(
1120 &variant.ident,
1121 "Bridge does not support struct variants",
1122 ));
1123 }
1124 };
1125
1126 let payload_path = match &payload_ty {
1127 Type::Path(path) => path,
1128 _ => {
1129 return Err(Error::new_spanned(
1130 &payload_ty,
1131 "Bridge payload must implement appdb::Bridge",
1132 ));
1133 }
1134 };
1135
1136 let segment = payload_path.path.segments.last().ok_or_else(|| {
1137 Error::new_spanned(&payload_ty, "Bridge payload must implement appdb::Bridge")
1138 })?;
1139
1140 if !matches!(segment.arguments, PathArguments::None) {
1141 return Err(Error::new_spanned(
1142 &payload_ty,
1143 "Bridge payload must implement appdb::Bridge",
1144 ));
1145 }
1146
1147 Ok(BridgeVariant {
1148 variant_ident: variant.ident.clone(),
1149 payload_ty,
1150 })
1151}
1152
1153fn derive_relation_impl(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
1154 let struct_ident = input.ident;
1155 let relation_name = relation_name_override(&input.attrs)?
1156 .unwrap_or_else(|| to_snake_case(&struct_ident.to_string()));
1157
1158 match input.data {
1159 Data::Struct(data) => match data.fields {
1160 Fields::Unit | Fields::Named(_) => {}
1161 _ => {
1162 return Err(Error::new_spanned(
1163 struct_ident,
1164 "Relation can only be derived for unit structs or structs with named fields",
1165 ));
1166 }
1167 },
1168 _ => {
1169 return Err(Error::new_spanned(
1170 struct_ident,
1171 "Relation can only be derived for structs",
1172 ));
1173 }
1174 }
1175
1176 Ok(quote! {
1177 impl ::appdb::model::relation::RelationMeta for #struct_ident {
1178 fn relation_name() -> &'static str {
1179 static REL_NAME: ::std::sync::OnceLock<&'static str> = ::std::sync::OnceLock::new();
1180 REL_NAME.get_or_init(|| ::appdb::model::relation::register_relation(#relation_name))
1181 }
1182 }
1183
1184 impl #struct_ident {
1185 pub async fn relate<A, B>(a: &A, b: &B) -> ::anyhow::Result<()>
1186 where
1187 A: ::appdb::model::meta::ResolveRecordId + Send + Sync,
1188 B: ::appdb::model::meta::ResolveRecordId + Send + Sync,
1189 {
1190 ::appdb::graph::relate_at(a.resolve_record_id().await?, b.resolve_record_id().await?, <Self as ::appdb::model::relation::RelationMeta>::relation_name()).await
1191 }
1192
1193 pub async fn back_relate<A, B>(a: &A, b: &B) -> ::anyhow::Result<()>
1194 where
1195 A: ::appdb::model::meta::ResolveRecordId + Send + Sync,
1196 B: ::appdb::model::meta::ResolveRecordId + Send + Sync,
1197 {
1198 ::appdb::graph::back_relate_at(a.resolve_record_id().await?, b.resolve_record_id().await?, <Self as ::appdb::model::relation::RelationMeta>::relation_name()).await
1199 }
1200
1201 pub async fn unrelate<A, B>(a: &A, b: &B) -> ::anyhow::Result<()>
1202 where
1203 A: ::appdb::model::meta::ResolveRecordId + Send + Sync,
1204 B: ::appdb::model::meta::ResolveRecordId + Send + Sync,
1205 {
1206 ::appdb::graph::unrelate_at(a.resolve_record_id().await?, b.resolve_record_id().await?, <Self as ::appdb::model::relation::RelationMeta>::relation_name()).await
1207 }
1208
1209 pub async fn out_ids<A>(a: &A, out_table: &str) -> ::anyhow::Result<::std::vec::Vec<::surrealdb::types::RecordId>>
1210 where
1211 A: ::appdb::model::meta::ResolveRecordId + Send + Sync,
1212 {
1213 ::appdb::graph::out_ids(a.resolve_record_id().await?, <Self as ::appdb::model::relation::RelationMeta>::relation_name(), out_table).await
1214 }
1215
1216 pub async fn in_ids<B>(b: &B, in_table: &str) -> ::anyhow::Result<::std::vec::Vec<::surrealdb::types::RecordId>>
1217 where
1218 B: ::appdb::model::meta::ResolveRecordId + Send + Sync,
1219 {
1220 ::appdb::graph::in_ids(b.resolve_record_id().await?, <Self as ::appdb::model::relation::RelationMeta>::relation_name(), in_table).await
1221 }
1222 }
1223 })
1224}
1225
1226fn derive_sensitive_impl(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
1227 let struct_ident = input.ident;
1228 let encrypted_ident = format_ident!("Encrypted{}", struct_ident);
1229 let vis = input.vis;
1230 let type_crypto_config = type_crypto_config(&input.attrs)?;
1231 let named_fields = match input.data {
1232 Data::Struct(data) => match data.fields {
1233 Fields::Named(fields) => fields.named,
1234 _ => {
1235 return Err(Error::new_spanned(
1236 struct_ident,
1237 "Sensitive can only be derived for structs with named fields",
1238 ));
1239 }
1240 },
1241 _ => {
1242 return Err(Error::new_spanned(
1243 struct_ident,
1244 "Sensitive can only be derived for structs",
1245 ));
1246 }
1247 };
1248
1249 let mut secure_field_count = 0usize;
1250 let mut encrypted_fields = Vec::new();
1251 let mut encrypt_assignments = Vec::new();
1252 let mut decrypt_assignments = Vec::new();
1253 let mut runtime_encrypt_assignments = Vec::new();
1254 let mut runtime_decrypt_assignments = Vec::new();
1255 let mut field_tag_structs = Vec::new();
1256 let mut secure_field_meta_entries = Vec::new();
1257
1258 for field in named_fields.iter() {
1259 let ident = field.ident.clone().expect("named field");
1260 let field_vis = field.vis.clone();
1261 let secure = has_secure_attr(&field.attrs);
1262 let field_crypto_config = field_crypto_config(&field.attrs)?;
1263
1264 if !secure && field_crypto_config.is_present() {
1265 return Err(Error::new_spanned(
1266 ident,
1267 "#[crypto(...)] on a field requires #[secure] on the same field",
1268 ));
1269 }
1270
1271 if secure {
1272 secure_field_count += 1;
1273 let secure_kind = secure_kind(field)?;
1274 let encrypted_ty = secure_kind.encrypted_type();
1275 let field_tag_ident = format_ident!(
1276 "AppdbSensitiveFieldTag{}{}",
1277 struct_ident,
1278 to_pascal_case(&ident.to_string())
1279 );
1280 let field_tag_literal = ident.to_string();
1281 let effective_account = field_crypto_config
1282 .field_account
1283 .clone()
1284 .or_else(|| type_crypto_config.account.clone());
1285 let service_override = type_crypto_config.service.clone();
1286 let account_literal = effective_account
1287 .as_ref()
1288 .map(|value| quote! { ::std::option::Option::Some(#value) })
1289 .unwrap_or_else(|| quote! { ::std::option::Option::None });
1290 let service_literal = service_override
1291 .as_ref()
1292 .map(|value| quote! { ::std::option::Option::Some(#value) })
1293 .unwrap_or_else(|| quote! { ::std::option::Option::None });
1294 let encrypt_expr = secure_kind.encrypt_with_context_expr(&ident);
1295 let decrypt_expr = secure_kind.decrypt_with_context_expr(&ident);
1296 let runtime_encrypt_expr =
1297 secure_kind.encrypt_with_runtime_expr(&ident, &field_tag_ident);
1298 let runtime_decrypt_expr =
1299 secure_kind.decrypt_with_runtime_expr(&ident, &field_tag_ident);
1300 encrypted_fields.push(quote! { #field_vis #ident: #encrypted_ty });
1301 encrypt_assignments.push(quote! { #ident: #encrypt_expr });
1302 decrypt_assignments.push(quote! { #ident: #decrypt_expr });
1303 runtime_encrypt_assignments.push(quote! { #ident: #runtime_encrypt_expr });
1304 runtime_decrypt_assignments.push(quote! { #ident: #runtime_decrypt_expr });
1305 secure_field_meta_entries.push(quote! {
1306 ::appdb::crypto::SensitiveFieldMetadata {
1307 model_tag: ::std::concat!(::std::module_path!(), "::", ::std::stringify!(#struct_ident)),
1308 field_tag: #field_tag_literal,
1309 service: #service_literal,
1310 account: #account_literal,
1311 secure_fields: &[],
1312 }
1313 });
1314 field_tag_structs.push(quote! {
1315 #[doc(hidden)]
1316 #vis struct #field_tag_ident;
1317
1318 impl ::appdb::crypto::SensitiveFieldTag for #field_tag_ident {
1319 fn model_tag() -> &'static str {
1320 <#struct_ident as ::appdb::crypto::SensitiveModelTag>::model_tag()
1321 }
1322
1323 fn field_tag() -> &'static str {
1324 #field_tag_literal
1325 }
1326
1327 fn crypto_metadata() -> &'static ::appdb::crypto::SensitiveFieldMetadata {
1328 static FIELD_META: ::std::sync::OnceLock<::appdb::crypto::SensitiveFieldMetadata> = ::std::sync::OnceLock::new();
1329 FIELD_META.get_or_init(|| ::appdb::crypto::SensitiveFieldMetadata {
1330 model_tag: <#struct_ident as ::appdb::crypto::SensitiveModelTag>::model_tag(),
1331 field_tag: #field_tag_literal,
1332 service: #service_literal,
1333 account: #account_literal,
1334 secure_fields: &#struct_ident::SECURE_FIELDS,
1335 })
1336 }
1337 }
1338 });
1339 } else {
1340 let ty = field.ty.clone();
1341 encrypted_fields.push(quote! { #field_vis #ident: #ty });
1342 encrypt_assignments.push(quote! { #ident: self.#ident.clone() });
1343 decrypt_assignments.push(quote! { #ident: encrypted.#ident.clone() });
1344 runtime_encrypt_assignments.push(quote! { #ident: self.#ident.clone() });
1345 runtime_decrypt_assignments.push(quote! { #ident: encrypted.#ident.clone() });
1346 }
1347 }
1348
1349 if secure_field_count == 0 {
1350 return Err(Error::new_spanned(
1351 struct_ident,
1352 "Sensitive requires at least one #[secure] field",
1353 ));
1354 }
1355
1356 Ok(quote! {
1357 #[derive(
1358 Debug,
1359 Clone,
1360 ::serde::Serialize,
1361 ::serde::Deserialize,
1362 ::surrealdb::types::SurrealValue,
1363 )]
1364 #vis struct #encrypted_ident {
1365 #( #encrypted_fields, )*
1366 }
1367
1368 impl ::appdb::crypto::SensitiveModelTag for #struct_ident {
1369 fn model_tag() -> &'static str {
1370 ::std::concat!(::std::module_path!(), "::", ::std::stringify!(#struct_ident))
1371 }
1372 }
1373
1374 #( #field_tag_structs )*
1375
1376 impl ::appdb::Sensitive for #struct_ident {
1377 type Encrypted = #encrypted_ident;
1378
1379 fn encrypt(
1380 &self,
1381 context: &::appdb::crypto::CryptoContext,
1382 ) -> ::std::result::Result<Self::Encrypted, ::appdb::crypto::CryptoError> {
1383 ::std::result::Result::Ok(#encrypted_ident {
1384 #( #encrypt_assignments, )*
1385 })
1386 }
1387
1388 fn decrypt(
1389 encrypted: &Self::Encrypted,
1390 context: &::appdb::crypto::CryptoContext,
1391 ) -> ::std::result::Result<Self, ::appdb::crypto::CryptoError> {
1392 ::std::result::Result::Ok(Self {
1393 #( #decrypt_assignments, )*
1394 })
1395 }
1396
1397 fn encrypt_with_runtime_resolver(
1398 &self,
1399 ) -> ::std::result::Result<Self::Encrypted, ::appdb::crypto::CryptoError> {
1400 ::std::result::Result::Ok(#encrypted_ident {
1401 #( #runtime_encrypt_assignments, )*
1402 })
1403 }
1404
1405 fn decrypt_with_runtime_resolver(
1406 encrypted: &Self::Encrypted,
1407 ) -> ::std::result::Result<Self, ::appdb::crypto::CryptoError> {
1408 ::std::result::Result::Ok(Self {
1409 #( #runtime_decrypt_assignments, )*
1410 })
1411 }
1412
1413 fn secure_fields() -> &'static [::appdb::crypto::SensitiveFieldMetadata] {
1414 &Self::SECURE_FIELDS
1415 }
1416 }
1417
1418 impl #struct_ident {
1419 pub const SECURE_FIELDS: [::appdb::crypto::SensitiveFieldMetadata; #secure_field_count] = [
1420 #( #secure_field_meta_entries, )*
1421 ];
1422
1423 pub fn encrypt(
1424 &self,
1425 context: &::appdb::crypto::CryptoContext,
1426 ) -> ::std::result::Result<#encrypted_ident, ::appdb::crypto::CryptoError> {
1427 <Self as ::appdb::Sensitive>::encrypt(self, context)
1428 }
1429 }
1430
1431 impl #encrypted_ident {
1432 pub fn decrypt(
1433 &self,
1434 context: &::appdb::crypto::CryptoContext,
1435 ) -> ::std::result::Result<#struct_ident, ::appdb::crypto::CryptoError> {
1436 <#struct_ident as ::appdb::Sensitive>::decrypt(self, context)
1437 }
1438 }
1439 })
1440}
1441
1442fn has_secure_attr(attrs: &[Attribute]) -> bool {
1443 attrs.iter().any(|attr| attr.path().is_ident("secure"))
1444}
1445
1446fn has_unique_attr(attrs: &[Attribute]) -> bool {
1447 attrs.iter().any(|attr| attr.path().is_ident("unique"))
1448}
1449
1450#[derive(Default, Clone)]
1451struct TypeCryptoConfig {
1452 service: Option<String>,
1453 account: Option<String>,
1454}
1455
1456#[derive(Default, Clone)]
1457struct FieldCryptoConfig {
1458 field_account: Option<String>,
1459}
1460
1461impl FieldCryptoConfig {
1462 fn is_present(&self) -> bool {
1463 self.field_account.is_some()
1464 }
1465}
1466
1467fn type_crypto_config(attrs: &[Attribute]) -> syn::Result<TypeCryptoConfig> {
1468 let mut config = TypeCryptoConfig::default();
1469 let mut seen = HashSet::new();
1470
1471 for attr in attrs {
1472 if !attr.path().is_ident("crypto") {
1473 continue;
1474 }
1475
1476 attr.parse_nested_meta(|meta| {
1477 let key = meta
1478 .path
1479 .get_ident()
1480 .cloned()
1481 .ok_or_else(|| meta.error("unsupported crypto attribute"))?;
1482
1483 if !seen.insert(key.to_string()) {
1484 return Err(meta.error("duplicate crypto attribute key"));
1485 }
1486
1487 let value = meta.value()?;
1488 let literal: syn::LitStr = value.parse()?;
1489 match key.to_string().as_str() {
1490 "service" => config.service = Some(literal.value()),
1491 "account" => config.account = Some(literal.value()),
1492 _ => {
1493 return Err(
1494 meta.error("unsupported crypto attribute; expected `service` or `account`")
1495 );
1496 }
1497 }
1498 Ok(())
1499 })?;
1500 }
1501
1502 Ok(config)
1503}
1504
1505fn field_crypto_config(attrs: &[Attribute]) -> syn::Result<FieldCryptoConfig> {
1506 let mut config = FieldCryptoConfig::default();
1507 let mut seen = HashSet::new();
1508
1509 for attr in attrs {
1510 if attr.path().is_ident("crypto") {
1511 attr.parse_nested_meta(|meta| {
1512 let key = meta
1513 .path
1514 .get_ident()
1515 .cloned()
1516 .ok_or_else(|| meta.error("unsupported crypto attribute"))?;
1517
1518 if !seen.insert(key.to_string()) {
1519 return Err(meta.error("duplicate crypto attribute key"));
1520 }
1521
1522 let value = meta.value()?;
1523 let literal: syn::LitStr = value.parse()?;
1524 match key.to_string().as_str() {
1525 "field_account" => config.field_account = Some(literal.value()),
1526 _ => {
1527 return Err(meta.error(
1528 "unsupported field crypto attribute; expected `field_account`",
1529 ));
1530 }
1531 }
1532 Ok(())
1533 })?;
1534 } else if attr.path().is_ident("secure") && matches!(attr.meta, Meta::List(_)) {
1535 return Err(Error::new_spanned(
1536 attr,
1537 "#[secure] does not accept arguments; use #[crypto(field_account = \"...\")] on the field",
1538 ));
1539 }
1540 }
1541
1542 Ok(config)
1543}
1544
1545fn table_alias_target(attrs: &[Attribute]) -> syn::Result<Option<Type>> {
1546 let mut target = None;
1547
1548 for attr in attrs {
1549 if !attr.path().is_ident("table_as") {
1550 continue;
1551 }
1552
1553 if target.is_some() {
1554 return Err(Error::new_spanned(
1555 attr,
1556 "duplicate #[table_as(...)] attribute is not supported",
1557 ));
1558 }
1559
1560 let parsed: Type = attr.parse_args().map_err(|_| {
1561 Error::new_spanned(attr, "#[table_as(...)] requires exactly one target type")
1562 })?;
1563
1564 match parsed {
1565 Type::Path(TypePath { ref path, .. }) if !path.segments.is_empty() => {
1566 target = Some(parsed);
1567 }
1568 _ => {
1569 return Err(Error::new_spanned(
1570 parsed,
1571 "#[table_as(...)] target must be a type path",
1572 ));
1573 }
1574 }
1575 }
1576
1577 Ok(target)
1578}
1579
1580fn resolved_schema_table_name(struct_ident: &syn::Ident, table_alias: Option<&Type>) -> String {
1581 match table_alias {
1582 Some(Type::Path(type_path)) => type_path
1583 .path
1584 .segments
1585 .last()
1586 .map(|segment| to_snake_case(&segment.ident.to_string()))
1587 .unwrap_or_else(|| to_snake_case(&struct_ident.to_string())),
1588 Some(_) => to_snake_case(&struct_ident.to_string()),
1589 None => to_snake_case(&struct_ident.to_string()),
1590 }
1591}
1592
1593fn field_foreign_attr(field: &Field) -> syn::Result<Option<&Attribute>> {
1594 let mut foreign_attr = None;
1595
1596 for attr in &field.attrs {
1597 if !attr.path().is_ident("foreign") {
1598 continue;
1599 }
1600
1601 if foreign_attr.is_some() {
1602 return Err(Error::new_spanned(
1603 attr,
1604 "duplicate nested-ref attribute is not supported",
1605 ));
1606 }
1607
1608 foreign_attr = Some(attr);
1609 }
1610
1611 Ok(foreign_attr)
1612}
1613
1614fn field_relation_attr(field: &Field) -> syn::Result<Option<&Attribute>> {
1615 let mut relate_attr = None;
1616
1617 for attr in &field.attrs {
1618 if !attr.path().is_ident("relate") && !attr.path().is_ident("back_relate") {
1619 continue;
1620 }
1621
1622 if let Some(previous) = relate_attr {
1623 return Err(Error::new_spanned(
1624 attr,
1625 relation_attr_conflict_message(previous, attr),
1626 ));
1627 }
1628
1629 relate_attr = Some(attr);
1630 }
1631
1632 Ok(relate_attr)
1633}
1634
1635fn field_pagin_attr(field: &Field) -> syn::Result<Option<&Attribute>> {
1636 let mut pagin_attr = None;
1637
1638 for attr in &field.attrs {
1639 if !attr.path().is_ident("pagin") {
1640 continue;
1641 }
1642
1643 if pagin_attr.is_some() {
1644 return Err(Error::new_spanned(
1645 attr,
1646 "duplicate #[pagin] attribute is not supported",
1647 ));
1648 }
1649
1650 pagin_attr = Some(attr);
1651 }
1652
1653 Ok(pagin_attr)
1654}
1655
1656fn field_fill_attr(field: &Field) -> syn::Result<Option<&Attribute>> {
1657 let mut fill_attr = None;
1658
1659 for attr in &field.attrs {
1660 if !attr.path().is_ident("fill") {
1661 continue;
1662 }
1663
1664 if fill_attr.is_some() {
1665 return Err(Error::new_spanned(
1666 attr,
1667 "duplicate #[fill(...)] attribute is not supported",
1668 ));
1669 }
1670
1671 fill_attr = Some(attr);
1672 }
1673
1674 Ok(fill_attr)
1675}
1676
1677fn validate_foreign_field(field: &Field, attr: &Attribute) -> syn::Result<Type> {
1678 if attr.path().is_ident("foreign") {
1679 return foreign_leaf_type(&field.ty)
1680 .ok_or_else(|| Error::new_spanned(&field.ty, BINDREF_ACCEPTED_SHAPES));
1681 }
1682
1683 Err(Error::new_spanned(attr, "unsupported foreign attribute"))
1684}
1685
1686const BINDREF_ACCEPTED_SHAPES: &str = "#[foreign] supports recursive Option<_> / Vec<_> shapes whose leaf type implements appdb::Bridge";
1687
1688const BINDREF_BRIDGE_STORE_ONLY: &str =
1689 "#[foreign] leaf types must derive Store or #[derive(Bridge)] dispatcher enums";
1690
1691const RELATE_ACCEPTED_SHAPES: &str = "relation-backed fields support Child / Option<Child> / Vec<Child> / Option<Vec<Child>> shapes whose leaf type implements appdb::Bridge";
1692
1693const PAGIN_ACCEPTED_SHAPES: &str = "#[pagin] supports direct scalar fields plus Id/RecordId; Option<_> and Vec<_> wrappers are not supported";
1694
1695const FILL_ACCEPTED_SHAPES: &str = "#[fill(...)] fields must use appdb::AutoFill";
1696
1697#[derive(Clone, Copy)]
1698enum RelationFieldDirection {
1699 Outgoing,
1700 Incoming,
1701}
1702
1703#[derive(Clone, Copy)]
1704enum FillProvider {
1705 Now,
1706}
1707
1708impl RelationFieldDirection {
1709 fn attr_label(self) -> &'static str {
1710 match self {
1711 Self::Outgoing => "#[relate(...)]",
1712 Self::Incoming => "#[back_relate(...)]",
1713 }
1714 }
1715
1716 fn write_direction_tokens(self) -> proc_macro2::TokenStream {
1717 match self {
1718 Self::Outgoing => quote!(::appdb::RelationWriteDirection::Outgoing),
1719 Self::Incoming => quote!(::appdb::RelationWriteDirection::Incoming),
1720 }
1721 }
1722
1723 fn write_edges_tokens(self) -> proc_macro2::TokenStream {
1724 match self {
1725 Self::Outgoing => quote! {
1726 ids
1727 .into_iter()
1728 .enumerate()
1729 .map(|(position, out)| ::appdb::graph::OrderedRelationEdge {
1730 _in: ::std::option::Option::Some(record.clone()),
1731 out,
1732 position: position as i64,
1733 })
1734 .collect()
1735 },
1736 Self::Incoming => quote! {
1737 ids
1738 .into_iter()
1739 .enumerate()
1740 .map(|(position, source)| ::appdb::graph::OrderedRelationEdge {
1741 _in: ::std::option::Option::Some(source),
1742 out: record.clone(),
1743 position: position as i64,
1744 })
1745 .collect()
1746 },
1747 }
1748 }
1749
1750 fn load_edges_tokens(self, relation_name: &str) -> proc_macro2::TokenStream {
1751 match self {
1752 Self::Outgoing => quote! {
1753 ::appdb::graph::GraphRepo::out_edges(record.clone(), #relation_name)
1754 .await?
1755 .into_iter()
1756 .map(|edge| edge.out)
1757 .collect()
1758 },
1759 Self::Incoming => quote! {
1760 {
1761 let mut ids = ::std::vec::Vec::new();
1762 for edge in ::appdb::graph::GraphRepo::in_edges(record.clone(), #relation_name).await? {
1763 let incoming = edge._in.ok_or_else(|| {
1764 ::anyhow::anyhow!("back relate field received relation edge without `in` record id")
1765 })?;
1766 ids.push(incoming);
1767 }
1768 ids
1769 }
1770 },
1771 }
1772 }
1773}
1774
1775#[derive(Clone)]
1776struct ForeignField {
1777 ident: syn::Ident,
1778 kind: ForeignFieldKind,
1779}
1780
1781#[derive(Clone)]
1782struct ForeignFieldKind {
1783 original_ty: Type,
1784 stored_ty: Type,
1785}
1786
1787#[derive(Clone)]
1788struct RelateField {
1789 ident: syn::Ident,
1790 relation_name: String,
1791 field_ty: Type,
1792 direction: RelationFieldDirection,
1793}
1794
1795#[derive(Clone)]
1796struct PaginField {
1797 ident: syn::Ident,
1798}
1799
1800#[derive(Clone)]
1801struct FillField {
1802 ident: syn::Ident,
1803 provider: FillProvider,
1804}
1805
1806fn parse_foreign_field(field: &Field, attr: &Attribute) -> syn::Result<ForeignField> {
1807 validate_foreign_field(field, attr)?;
1808 let ident = field.ident.clone().expect("named field");
1809
1810 let kind = ForeignFieldKind {
1811 original_ty: field.ty.clone(),
1812 stored_ty: foreign_stored_type(&field.ty)
1813 .ok_or_else(|| Error::new_spanned(&field.ty, BINDREF_ACCEPTED_SHAPES))?,
1814 };
1815
1816 Ok(ForeignField { ident, kind })
1817}
1818
1819fn parse_relate_field(field: &Field, attr: &Attribute) -> syn::Result<RelateField> {
1820 let direction = parse_relation_direction(attr)?;
1821 let relation_name = attr
1822 .parse_args::<syn::LitStr>()
1823 .map_err(|_| {
1824 Error::new_spanned(
1825 attr,
1826 format!(
1827 "{} requires exactly one string literal",
1828 direction.attr_label()
1829 ),
1830 )
1831 })?
1832 .value();
1833 if relation_name.is_empty() {
1834 return Err(Error::new_spanned(
1835 attr,
1836 format!("{} relation name must not be empty", direction.attr_label()),
1837 ));
1838 }
1839
1840 validate_relate_field(field, attr)?;
1841
1842 Ok(RelateField {
1843 ident: field.ident.clone().expect("named field"),
1844 relation_name,
1845 field_ty: field.ty.clone(),
1846 direction,
1847 })
1848}
1849
1850fn parse_pagin_field(field: &Field, attr: &Attribute) -> syn::Result<PaginField> {
1851 validate_pagin_field(field, attr)?;
1852 Ok(PaginField {
1853 ident: field.ident.clone().expect("named field"),
1854 })
1855}
1856
1857fn parse_fill_field(field: &Field, attr: &Attribute) -> syn::Result<FillField> {
1858 let provider = validate_fill_field(field, attr)?;
1859 Ok(FillField {
1860 ident: field.ident.clone().expect("named field"),
1861 provider,
1862 })
1863}
1864
1865fn validate_relate_field(field: &Field, attr: &Attribute) -> syn::Result<Type> {
1866 if !attr.path().is_ident("relate") && !attr.path().is_ident("back_relate") {
1867 return Err(Error::new_spanned(attr, "unsupported relate attribute"));
1868 }
1869
1870 let accepted = relate_leaf_type(&field.ty).cloned().map(Type::Path);
1871
1872 accepted.ok_or_else(|| Error::new_spanned(&field.ty, RELATE_ACCEPTED_SHAPES))
1873}
1874
1875fn validate_pagin_field(field: &Field, attr: &Attribute) -> syn::Result<Type> {
1876 if !attr.path().is_ident("pagin") {
1877 return Err(Error::new_spanned(attr, "unsupported pagination attribute"));
1878 }
1879
1880 if pagination_leaf_type(&field.ty).is_none() {
1881 return Err(Error::new_spanned(&field.ty, PAGIN_ACCEPTED_SHAPES));
1882 }
1883
1884 Ok(field.ty.clone())
1885}
1886
1887fn validate_fill_field(field: &Field, attr: &Attribute) -> syn::Result<FillProvider> {
1888 if !attr.path().is_ident("fill") {
1889 return Err(Error::new_spanned(attr, "unsupported fill attribute"));
1890 }
1891
1892 if !is_autofill_type(&field.ty) {
1893 return Err(Error::new_spanned(&field.ty, FILL_ACCEPTED_SHAPES));
1894 }
1895
1896 parse_fill_provider(attr)
1897}
1898
1899fn parse_fill_provider(attr: &Attribute) -> syn::Result<FillProvider> {
1900 let provider = attr.parse_args::<syn::Path>().map_err(|_| {
1901 Error::new_spanned(
1902 attr,
1903 "#[fill(...)] requires exactly one provider like #[fill(now)]",
1904 )
1905 })?;
1906
1907 if provider.is_ident("now") {
1908 Ok(FillProvider::Now)
1909 } else {
1910 Err(Error::new_spanned(
1911 attr,
1912 "unsupported fill provider; expected #[fill(now)]",
1913 ))
1914 }
1915}
1916
1917fn relate_leaf_type(ty: &Type) -> Option<&TypePath> {
1918 if let Some(leaf) = direct_store_child_type(ty) {
1919 return Some(leaf);
1920 }
1921
1922 if let Some(inner) = option_inner_type(ty) {
1923 if let Some(leaf) = direct_store_child_type(inner) {
1924 return Some(leaf);
1925 }
1926
1927 if let Some(vec_inner) = vec_inner_type(inner) {
1928 return direct_store_child_type(vec_inner);
1929 }
1930
1931 return None;
1932 }
1933
1934 if let Some(inner) = vec_inner_type(ty) {
1935 return direct_store_child_type(inner);
1936 }
1937
1938 None
1939}
1940
1941fn pagination_leaf_type(ty: &Type) -> Option<Type> {
1942 if option_inner_type(ty).is_some() || vec_inner_type(ty).is_some() {
1943 return None;
1944 }
1945
1946 if is_id_type(ty)
1947 || is_record_id_type(ty)
1948 || is_autofill_type(ty)
1949 || is_string_type(ty)
1950 || is_common_non_store_leaf_type(ty)
1951 {
1952 return Some(ty.clone());
1953 }
1954
1955 None
1956}
1957
1958fn parse_relation_direction(attr: &Attribute) -> syn::Result<RelationFieldDirection> {
1959 if attr.path().is_ident("relate") {
1960 return Ok(RelationFieldDirection::Outgoing);
1961 }
1962 if attr.path().is_ident("back_relate") {
1963 return Ok(RelationFieldDirection::Incoming);
1964 }
1965
1966 Err(Error::new_spanned(attr, "unsupported relate attribute"))
1967}
1968
1969fn relation_attr_label(attr: &Attribute) -> &'static str {
1970 if attr.path().is_ident("back_relate") {
1971 "#[back_relate(...)]"
1972 } else {
1973 "#[relate(...)]"
1974 }
1975}
1976
1977fn relation_attr_conflict_message(previous: &Attribute, current: &Attribute) -> String {
1978 match (
1979 previous.path().is_ident("relate"),
1980 previous.path().is_ident("back_relate"),
1981 current.path().is_ident("relate"),
1982 current.path().is_ident("back_relate"),
1983 ) {
1984 (true, false, true, false) => {
1985 "duplicate #[relate(...)] attribute is not supported".to_owned()
1986 }
1987 (false, true, false, true) => {
1988 "duplicate #[back_relate(...)] attribute is not supported".to_owned()
1989 }
1990 _ => "#[relate(...)] cannot be combined with #[back_relate(...)]".to_owned(),
1991 }
1992}
1993
1994fn foreign_field_kind<'a>(
1995 ident: &syn::Ident,
1996 fields: &'a [ForeignField],
1997) -> Option<&'a ForeignFieldKind> {
1998 fields
1999 .iter()
2000 .find(|field| field.ident == *ident)
2001 .map(|field| &field.kind)
2002}
2003
2004fn stored_field_type(field: &Field, foreign_fields: &[ForeignField]) -> Type {
2005 let ident = field.ident.as_ref().expect("named field");
2006 match foreign_field_kind(ident, foreign_fields) {
2007 Some(ForeignFieldKind { stored_ty, .. }) => stored_ty.clone(),
2008 None => field.ty.clone(),
2009 }
2010}
2011
2012fn foreign_stored_type(ty: &Type) -> Option<Type> {
2013 if let Some(inner) = option_inner_type(ty) {
2014 let inner = foreign_stored_type(inner)?;
2015 return Some(syn::parse_quote!(::std::option::Option<#inner>));
2016 }
2017
2018 if let Some(inner) = vec_inner_type(ty) {
2019 let inner = foreign_stored_type(inner)?;
2020 return Some(syn::parse_quote!(::std::vec::Vec<#inner>));
2021 }
2022
2023 direct_store_child_type(ty)
2024 .cloned()
2025 .map(|_| syn::parse_quote!(::surrealdb::types::RecordId))
2026}
2027
2028fn foreign_leaf_type(ty: &Type) -> Option<Type> {
2029 if let Some(inner) = option_inner_type(ty) {
2030 return foreign_leaf_type(inner);
2031 }
2032
2033 if let Some(inner) = vec_inner_type(ty) {
2034 return foreign_leaf_type(inner);
2035 }
2036
2037 direct_store_child_type(ty).cloned().map(Type::Path)
2038}
2039
2040fn invalid_foreign_leaf_type(ty: &Type) -> Option<Type> {
2041 let leaf = foreign_leaf_type(ty)?;
2042 match &leaf {
2043 Type::Path(type_path) => {
2044 let segment = type_path.path.segments.last()?;
2045 if matches!(segment.arguments, PathArguments::None) {
2046 None
2047 } else {
2048 Some(leaf)
2049 }
2050 }
2051 _ => Some(leaf),
2052 }
2053}
2054
2055fn direct_store_child_type(ty: &Type) -> Option<&TypePath> {
2056 let Type::Path(type_path) = ty else {
2057 return None;
2058 };
2059
2060 let segment = type_path.path.segments.last()?;
2061 if !matches!(segment.arguments, PathArguments::None) {
2062 return None;
2063 }
2064
2065 if is_id_type(ty)
2066 || is_autofill_type(ty)
2067 || is_string_type(ty)
2068 || is_common_non_store_leaf_type(ty)
2069 {
2070 return None;
2071 }
2072
2073 Some(type_path)
2074}
2075
2076fn is_common_non_store_leaf_type(ty: &Type) -> bool {
2077 matches!(
2078 ty,
2079 Type::Path(TypePath { path, .. })
2080 if path.is_ident("bool")
2081 || path.is_ident("u8")
2082 || path.is_ident("u16")
2083 || path.is_ident("u32")
2084 || path.is_ident("u64")
2085 || path.is_ident("u128")
2086 || path.is_ident("usize")
2087 || path.is_ident("i8")
2088 || path.is_ident("i16")
2089 || path.is_ident("i32")
2090 || path.is_ident("i64")
2091 || path.is_ident("i128")
2092 || path.is_ident("isize")
2093 || path.is_ident("f32")
2094 || path.is_ident("f64")
2095 || path.is_ident("char")
2096 )
2097}
2098
2099fn secure_field_count(fields: &syn::punctuated::Punctuated<Field, syn::token::Comma>) -> usize {
2100 fields
2101 .iter()
2102 .filter(|field| has_secure_attr(&field.attrs))
2103 .count()
2104}
2105
2106fn relation_name_override(attrs: &[Attribute]) -> syn::Result<Option<String>> {
2107 for attr in attrs {
2108 if !attr.path().is_ident("relation") {
2109 continue;
2110 }
2111
2112 let mut name = None;
2113 attr.parse_nested_meta(|meta| {
2114 if meta.path.is_ident("name") {
2115 let value = meta.value()?;
2116 let literal: syn::LitStr = value.parse()?;
2117 name = Some(literal.value());
2118 Ok(())
2119 } else {
2120 Err(meta.error("unsupported relation attribute"))
2121 }
2122 })?;
2123 return Ok(name);
2124 }
2125
2126 Ok(None)
2127}
2128
2129enum SecureKind {
2130 Shape(Type),
2131}
2132
2133impl SecureKind {
2134 fn encrypted_type(&self) -> proc_macro2::TokenStream {
2135 match self {
2136 SecureKind::Shape(ty) => quote! { <#ty as ::appdb::SensitiveShape>::Encrypted },
2137 }
2138 }
2139
2140 fn encrypt_with_context_expr(&self, ident: &syn::Ident) -> proc_macro2::TokenStream {
2141 match self {
2142 SecureKind::Shape(ty) => {
2143 quote! { <#ty as ::appdb::SensitiveShape>::encrypt_with_context(&self.#ident, context)? }
2144 }
2145 }
2146 }
2147
2148 fn decrypt_with_context_expr(&self, ident: &syn::Ident) -> proc_macro2::TokenStream {
2149 match self {
2150 SecureKind::Shape(ty) => {
2151 quote! { <#ty as ::appdb::SensitiveShape>::decrypt_with_context(&encrypted.#ident, context)? }
2152 }
2153 }
2154 }
2155
2156 fn encrypt_with_runtime_expr(
2157 &self,
2158 ident: &syn::Ident,
2159 field_tag_ident: &syn::Ident,
2160 ) -> proc_macro2::TokenStream {
2161 match self {
2162 SecureKind::Shape(ty) => {
2163 quote! {{
2164 let context = ::appdb::crypto::resolve_crypto_context_for::<#field_tag_ident>()?;
2165 <#ty as ::appdb::SensitiveShape>::encrypt_with_context(&self.#ident, context.as_ref())?
2166 }}
2167 }
2168 }
2169 }
2170
2171 fn decrypt_with_runtime_expr(
2172 &self,
2173 ident: &syn::Ident,
2174 field_tag_ident: &syn::Ident,
2175 ) -> proc_macro2::TokenStream {
2176 match self {
2177 SecureKind::Shape(ty) => {
2178 quote! {{
2179 let context = ::appdb::crypto::resolve_crypto_context_for::<#field_tag_ident>()?;
2180 <#ty as ::appdb::SensitiveShape>::decrypt_with_context(&encrypted.#ident, context.as_ref())?
2181 }}
2182 }
2183 }
2184 }
2185}
2186
2187fn secure_kind(field: &Field) -> syn::Result<SecureKind> {
2188 if secure_shape_supported(&field.ty) {
2189 return Ok(SecureKind::Shape(field.ty.clone()));
2190 }
2191
2192 Err(Error::new_spanned(
2193 &field.ty,
2194 secure_shape_error_message(&field.ty),
2195 ))
2196}
2197
2198fn secure_shape_supported(ty: &Type) -> bool {
2199 if is_string_type(ty) {
2200 return true;
2201 }
2202
2203 if sensitive_value_wrapper_inner_type(ty).is_some() {
2204 return true;
2205 }
2206
2207 if let Some(inner) = option_inner_type(ty) {
2208 return secure_shape_supported(inner);
2209 }
2210
2211 if let Some(inner) = vec_inner_type(ty) {
2212 return secure_shape_supported(inner);
2213 }
2214
2215 direct_sensitive_child_type(ty).is_some()
2216}
2217
2218fn secure_shape_error_message(ty: &Type) -> &'static str {
2219 if invalid_secure_leaf_type(ty).is_some() {
2220 "#[secure] child shapes require a direct named Sensitive type leaf with only Option<_> and Vec<_> wrappers"
2221 } else {
2222 "#[secure] supports String, appdb::SensitiveValueOf<T>, and recursive Child / Option<Child> / Vec<Child> shapes where Child implements appdb::Sensitive"
2223 }
2224}
2225
2226fn direct_sensitive_child_type(ty: &Type) -> Option<&TypePath> {
2227 let Type::Path(type_path) = ty else {
2228 return None;
2229 };
2230
2231 let segment = type_path.path.segments.last()?;
2232 if !matches!(segment.arguments, PathArguments::None) {
2233 return None;
2234 }
2235
2236 if is_id_type(ty) || is_string_type(ty) || is_common_non_store_leaf_type(ty) {
2237 return None;
2238 }
2239
2240 Some(type_path)
2241}
2242
2243fn invalid_secure_leaf_type(ty: &Type) -> Option<Type> {
2244 if let Some(inner) = option_inner_type(ty) {
2245 return invalid_secure_leaf_type(inner);
2246 }
2247
2248 if let Some(inner) = vec_inner_type(ty) {
2249 return invalid_secure_leaf_type(inner);
2250 }
2251
2252 let leaf = direct_sensitive_child_type(ty)?.clone();
2253 let segment = leaf.path.segments.last()?;
2254 if matches!(segment.arguments, PathArguments::None) {
2255 None
2256 } else {
2257 Some(Type::Path(leaf))
2258 }
2259}
2260
2261fn is_string_type(ty: &Type) -> bool {
2262 match ty {
2263 Type::Path(TypePath { path, .. }) => path.is_ident("String"),
2264 _ => false,
2265 }
2266}
2267
2268fn is_id_type(ty: &Type) -> bool {
2269 match ty {
2270 Type::Path(TypePath { path, .. }) => path.segments.last().is_some_and(|segment| {
2271 let ident = segment.ident.to_string();
2272 ident == "Id"
2273 }),
2274 _ => false,
2275 }
2276}
2277
2278fn is_autofill_type(ty: &Type) -> bool {
2279 match ty {
2280 Type::Path(TypePath { path, .. }) => path
2281 .segments
2282 .last()
2283 .is_some_and(|segment| segment.ident == "AutoFill"),
2284 _ => false,
2285 }
2286}
2287
2288fn is_record_id_type(ty: &Type) -> bool {
2289 match ty {
2290 Type::Path(TypePath { path, .. }) => path.segments.last().is_some_and(|segment| {
2291 let ident = segment.ident.to_string();
2292 ident == "RecordId"
2293 }),
2294 _ => false,
2295 }
2296}
2297
2298fn option_inner_type(ty: &Type) -> Option<&Type> {
2299 let Type::Path(TypePath { path, .. }) = ty else {
2300 return None;
2301 };
2302 let segment = path.segments.last()?;
2303 if segment.ident != "Option" {
2304 return None;
2305 }
2306 let PathArguments::AngleBracketed(args) = &segment.arguments else {
2307 return None;
2308 };
2309 let GenericArgument::Type(inner) = args.args.first()? else {
2310 return None;
2311 };
2312 Some(inner)
2313}
2314
2315fn vec_inner_type(ty: &Type) -> Option<&Type> {
2316 let Type::Path(TypePath { path, .. }) = ty else {
2317 return None;
2318 };
2319 let segment = path.segments.last()?;
2320 if segment.ident != "Vec" {
2321 return None;
2322 }
2323 let PathArguments::AngleBracketed(args) = &segment.arguments else {
2324 return None;
2325 };
2326 let GenericArgument::Type(inner) = args.args.first()? else {
2327 return None;
2328 };
2329 Some(inner)
2330}
2331
2332fn sensitive_value_wrapper_inner_type(ty: &Type) -> Option<&Type> {
2333 let Type::Path(TypePath { path, .. }) = ty else {
2334 return None;
2335 };
2336 let segment = path.segments.last()?;
2337 if segment.ident != "SensitiveValueOf" {
2338 return None;
2339 }
2340 let PathArguments::AngleBracketed(args) = &segment.arguments else {
2341 return None;
2342 };
2343 let GenericArgument::Type(inner) = args.args.first()? else {
2344 return None;
2345 };
2346 Some(inner)
2347}
2348
2349fn to_snake_case(input: &str) -> String {
2350 let mut out = String::with_capacity(input.len() + 4);
2351 let mut prev_is_lower_or_digit = false;
2352
2353 for ch in input.chars() {
2354 if ch.is_ascii_uppercase() {
2355 if prev_is_lower_or_digit {
2356 out.push('_');
2357 }
2358 out.push(ch.to_ascii_lowercase());
2359 prev_is_lower_or_digit = false;
2360 } else {
2361 out.push(ch);
2362 prev_is_lower_or_digit = ch.is_ascii_lowercase() || ch.is_ascii_digit();
2363 }
2364 }
2365
2366 out
2367}
2368
2369fn to_pascal_case(input: &str) -> String {
2370 let mut out = String::with_capacity(input.len());
2371 let mut uppercase_next = true;
2372
2373 for ch in input.chars() {
2374 if ch == '_' || ch == '-' {
2375 uppercase_next = true;
2376 continue;
2377 }
2378
2379 if uppercase_next {
2380 out.push(ch.to_ascii_uppercase());
2381 uppercase_next = false;
2382 } else {
2383 out.push(ch);
2384 }
2385 }
2386
2387 out
2388}