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