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 quote! {
727 impl ::appdb::ForeignModel for #struct_ident {
728 async fn persist_foreign(value: Self) -> ::anyhow::Result<Self::Stored> {
729 let mut value = value;
730 #( #fill_assignments )*
731 <Self as ::appdb::StoredModel>::into_stored(value)
732 }
733
734 async fn persist_foreign_with_plan(
735 value: Self,
736 foreign_plan: &::appdb::ForeignWritePlan,
737 ) -> ::anyhow::Result<Self::Stored> {
738 foreign_plan.ensure_known_fields(Self::foreign_field_names())?;
739 <Self as ::appdb::ForeignModel>::persist_foreign(value).await
740 }
741
742 async fn hydrate_foreign(stored: Self::Stored) -> ::anyhow::Result<Self> {
743 <Self as ::appdb::StoredModel>::from_stored(stored)
744 }
745
746 fn decode_stored_row(
747 row: ::surrealdb::types::Value,
748 ) -> ::anyhow::Result<Self::Stored>
749 where
750 Self::Stored: ::serde::de::DeserializeOwned,
751 {
752 Ok(::serde_json::from_value(row.into_json_value())?)
753 }
754
755 #relation_methods_impl
756 }
757 }
758 } else {
759 let stored_struct_ident = format_ident!("AppdbStored{}", struct_ident);
760 quote! {
761 #[derive(
762 Debug,
763 Clone,
764 ::serde::Serialize,
765 ::serde::Deserialize,
766 ::surrealdb::types::SurrealValue,
767 )]
768 #vis struct #stored_struct_ident {
769 #( #stored_fields, )*
770 }
771
772 impl ::appdb::StoredModel for #struct_ident {
773 type Stored = #stored_struct_ident;
774
775 fn into_stored(self) -> ::anyhow::Result<Self::Stored> {
776 unreachable!("foreign fields require async persist_foreign")
777 }
778
779 fn from_stored(_stored: Self::Stored) -> ::anyhow::Result<Self> {
780 unreachable!("foreign fields require async hydrate_foreign")
781 }
782 }
783
784 impl ::appdb::ForeignModel for #struct_ident {
785 async fn persist_foreign(value: Self) -> ::anyhow::Result<Self::Stored> {
786 let mut value = value;
787 #( #fill_assignments )*
788 Ok(#stored_struct_ident {
789 #( #into_stored_assignments, )*
790 })
791 }
792
793 async fn persist_foreign_with_plan(
794 value: Self,
795 foreign_plan: &::appdb::ForeignWritePlan,
796 ) -> ::anyhow::Result<Self::Stored> {
797 foreign_plan.ensure_known_fields(Self::foreign_field_names())?;
798 let mut value = value;
799 #( #fill_assignments )*
800 Ok(#stored_struct_ident {
801 #( #into_stored_with_plan_assignments, )*
802 })
803 }
804
805 async fn hydrate_foreign(stored: Self::Stored) -> ::anyhow::Result<Self> {
806 Ok(Self {
807 #( #from_stored_assignments, )*
808 })
809 }
810
811 fn has_foreign_fields() -> bool {
812 true
813 }
814
815 fn foreign_field_names() -> &'static [&'static str] {
816 &[ #( #foreign_field_literals ),* ]
817 }
818
819 fn decode_stored_row(
820 row: ::surrealdb::types::Value,
821 ) -> ::anyhow::Result<Self::Stored>
822 where
823 Self::Stored: ::serde::de::DeserializeOwned,
824 {
825 let mut row = row.into_json_value();
826 if let ::serde_json::Value::Object(map) = &mut row {
827 #( #decode_foreign_fields )*
828 }
829 Ok(::serde_json::from_value(row)?)
830 }
831
832 #relation_methods_impl
833 }
834 }
835 };
836
837 let foreign_write_api_impl = if foreign_fields.is_empty() {
838 quote! {}
839 } else {
840 let foreign_write_ident = format_ident!("AppdbForeignWrite{}", struct_ident);
841 let foreign_write_field_methods = foreign_fields.iter().map(|field| {
842 let ident = &field.ident;
843 let field_name = ident.to_string();
844 let stored_ty = &field.kind.stored_ty;
845 quote! {
846 pub fn #ident(mut self, value: #stored_ty) -> ::anyhow::Result<Self> {
847 self.query = self.query.set_field_shape(#field_name, value)?;
848 Ok(self)
849 }
850 }
851 });
852
853 quote! {
854 #vis struct #foreign_write_ident {
855 query: ::appdb::repository::ForeignWriteQuery<#struct_ident>,
856 }
857
858 impl #foreign_write_ident {
859 #( #foreign_write_field_methods )*
860
861 pub async fn create_at(
862 self,
863 id: ::surrealdb::types::RecordId,
864 ) -> ::anyhow::Result<#struct_ident> {
865 self.query.create_at(id).await
866 }
867
868 pub async fn upsert_at(
869 self,
870 id: ::surrealdb::types::RecordId,
871 ) -> ::anyhow::Result<#struct_ident> {
872 self.query.upsert_at(id).await
873 }
874
875 pub async fn update_at(
876 self,
877 id: ::surrealdb::types::RecordId,
878 ) -> ::anyhow::Result<#struct_ident> {
879 self.query.update_at(id).await
880 }
881
882 pub async fn create_at_returning<View>(
883 self,
884 id: ::surrealdb::types::RecordId,
885 ) -> ::anyhow::Result<View>
886 where
887 View: ::appdb::repository::WriteReturnView<#struct_ident>,
888 {
889 self.query.create_at_returning::<View>(id).await
890 }
891
892 pub async fn upsert_at_returning<View>(
893 self,
894 id: ::surrealdb::types::RecordId,
895 ) -> ::anyhow::Result<View>
896 where
897 View: ::appdb::repository::WriteReturnView<#struct_ident>,
898 {
899 self.query.upsert_at_returning::<View>(id).await
900 }
901
902 pub async fn update_at_returning<View>(
903 self,
904 id: ::surrealdb::types::RecordId,
905 ) -> ::anyhow::Result<View>
906 where
907 View: ::appdb::repository::WriteReturnView<#struct_ident>,
908 {
909 self.query.update_at_returning::<View>(id).await
910 }
911 }
912 }
913 };
914
915 let foreign_write_constructor_impl = if foreign_fields.is_empty() {
916 quote! {}
917 } else {
918 let foreign_write_ident = format_ident!("AppdbForeignWrite{}", struct_ident);
919 quote! {
920 pub fn foreign(self) -> #foreign_write_ident {
921 #foreign_write_ident {
922 query: ::appdb::repository::ForeignWriteQuery::new(self),
923 }
924 }
925 }
926 };
927
928 let store_marker_ident = format_ident!("AppdbStoreMarker{}", struct_ident);
929
930 Ok(quote! {
931 #[doc(hidden)]
932 #vis struct #store_marker_ident;
933
934 impl ::appdb::model::meta::ModelMeta for #struct_ident {
935 fn storage_table() -> &'static str {
936 #resolved_table_name_expr
937 }
938
939 fn table_name() -> &'static str {
940 static TABLE_NAME: ::std::sync::OnceLock<&'static str> = ::std::sync::OnceLock::new();
941 TABLE_NAME.get_or_init(|| {
942 let table = #resolved_table_name_expr;
943 ::appdb::model::meta::register_table(stringify!(#struct_ident), table)
944 })
945 }
946 }
947
948 impl ::appdb::model::meta::StoreModelMarker for #struct_ident {}
949 impl ::appdb::model::meta::StoreModelMarker for #store_marker_ident {}
950 #pagination_meta_impl
951
952 impl ::appdb::model::meta::UniqueLookupMeta for #struct_ident {
953 fn lookup_fields() -> &'static [&'static str] {
954 &[ #( #lookup_field_literals ),* ]
955 }
956
957 fn foreign_fields() -> &'static [&'static str] {
958 &[ #( #foreign_field_literals ),* ]
959 }
960
961 fn resolve_lookup_field_value(
962 &self,
963 field: &str,
964 ) -> impl ::std::future::Future<
965 Output = ::anyhow::Result<::std::option::Option<::surrealdb::types::Value>>
966 > {
967 async move {
968 match field {
969 #( #resolve_lookup_field_value_arms )*
970 _ => Ok(::std::option::Option::None),
971 }
972 }
973 }
974 }
975 #stored_model_impl
976 #foreign_model_impl
977 #foreign_write_api_impl
978
979 #auto_has_id_impl
980 #resolve_record_id_impl
981
982 #( #unique_schema_impls )*
983 #( #pagin_schema_impl )*
984
985 impl ::appdb::repository::Crud for #struct_ident {}
986
987 impl #struct_ident {
988 pub async fn save(self) -> ::anyhow::Result<Self> {
994 <Self as ::appdb::repository::Crud>::save(self).await
995 }
996
997 pub async fn save_many(data: ::std::vec::Vec<Self>) -> ::anyhow::Result<::std::vec::Vec<Self>> {
999 <Self as ::appdb::repository::Crud>::save_many(data).await
1000 }
1001
1002 pub async fn get<T>(id: T) -> ::anyhow::Result<Self>
1003 where
1004 ::surrealdb::types::RecordIdKey: From<T>,
1005 T: Send,
1006 {
1007 ::appdb::repository::Repo::<Self>::get(id).await
1008 }
1009
1010 pub fn list() -> ::appdb::repository::ListQuery<Self> {
1011 ::appdb::repository::Repo::<Self>::list()
1012 }
1013
1014 pub async fn list_limit(count: i64) -> ::anyhow::Result<::std::vec::Vec<Self>> {
1015 ::appdb::repository::Repo::<Self>::list_limit(count).await
1016 }
1017 #pagination_methods_impl
1018
1019 pub async fn relate_by_name<Target>(&self, target: &Target, relation: &str) -> ::anyhow::Result<()>
1020 where
1021 Target: ::appdb::model::meta::ResolveRecordId + Send + Sync,
1022 {
1023 <Self as ::appdb::graph::GraphCrud>::relate_by_name(self, target, relation).await
1024 }
1025
1026 pub async fn back_relate_by_name<Target>(&self, target: &Target, relation: &str) -> ::anyhow::Result<()>
1027 where
1028 Target: ::appdb::model::meta::ResolveRecordId + Send + Sync,
1029 {
1030 <Self as ::appdb::graph::GraphCrud>::back_relate_by_name(self, target, relation).await
1031 }
1032
1033 pub async fn unrelate_by_name<Target>(&self, target: &Target, relation: &str) -> ::anyhow::Result<()>
1034 where
1035 Target: ::appdb::model::meta::ResolveRecordId + Send + Sync,
1036 {
1037 <Self as ::appdb::graph::GraphCrud>::unrelate_by_name(self, target, relation).await
1038 }
1039
1040 pub async fn outgoing_ids(&self, relation: &str) -> ::anyhow::Result<::std::vec::Vec<::surrealdb::types::RecordId>> {
1041 <Self as ::appdb::repository::Crud>::outgoing_ids(self, relation).await
1042 }
1043
1044 pub async fn outgoing<Target>(&self, relation: &str) -> ::anyhow::Result<::std::vec::Vec<Target>>
1045 where
1046 Target: ::appdb::model::meta::ModelMeta + ::appdb::StoredModel + ::appdb::ForeignModel,
1047 <Target as ::appdb::StoredModel>::Stored: ::serde::de::DeserializeOwned,
1048 {
1049 <Self as ::appdb::repository::Crud>::outgoing::<Target>(self, relation).await
1050 }
1051
1052 pub async fn outgoing_count(&self, relation: &str) -> ::anyhow::Result<i64> {
1053 <Self as ::appdb::repository::Crud>::outgoing_count(self, relation).await
1054 }
1055
1056 pub async fn outgoing_count_as<Target>(&self, relation: &str) -> ::anyhow::Result<i64>
1057 where
1058 Target: ::appdb::model::meta::ModelMeta + ::appdb::StoredModel + ::appdb::ForeignModel,
1059 {
1060 <Self as ::appdb::repository::Crud>::outgoing_count_as::<Target>(self, relation).await
1061 }
1062
1063 pub async fn incoming_ids(&self, relation: &str) -> ::anyhow::Result<::std::vec::Vec<::surrealdb::types::RecordId>> {
1064 <Self as ::appdb::repository::Crud>::incoming_ids(self, relation).await
1065 }
1066
1067 pub async fn incoming<Target>(&self, relation: &str) -> ::anyhow::Result<::std::vec::Vec<Target>>
1068 where
1069 Target: ::appdb::model::meta::ModelMeta + ::appdb::StoredModel + ::appdb::ForeignModel,
1070 <Target as ::appdb::StoredModel>::Stored: ::serde::de::DeserializeOwned,
1071 {
1072 <Self as ::appdb::repository::Crud>::incoming::<Target>(self, relation).await
1073 }
1074
1075 pub async fn incoming_count(&self, relation: &str) -> ::anyhow::Result<i64> {
1076 <Self as ::appdb::repository::Crud>::incoming_count(self, relation).await
1077 }
1078
1079 pub async fn incoming_count_as<Target>(&self, relation: &str) -> ::anyhow::Result<i64>
1080 where
1081 Target: ::appdb::model::meta::ModelMeta + ::appdb::StoredModel + ::appdb::ForeignModel,
1082 {
1083 <Self as ::appdb::repository::Crud>::incoming_count_as::<Target>(self, relation).await
1084 }
1085
1086 pub async fn delete_all() -> ::anyhow::Result<()> {
1087 ::appdb::repository::Repo::<Self>::delete_all().await
1088 }
1089
1090 pub async fn find_one_id(
1091 k: &str,
1092 v: &str,
1093 ) -> ::anyhow::Result<::surrealdb::types::RecordId> {
1094 ::appdb::repository::Repo::<Self>::find_one_id(k, v).await
1095 }
1096
1097 pub async fn list_record_ids() -> ::anyhow::Result<::std::vec::Vec<::surrealdb::types::RecordId>> {
1098 ::appdb::repository::Repo::<Self>::list_record_ids().await
1099 }
1100
1101 pub async fn create_at(
1102 id: ::surrealdb::types::RecordId,
1103 data: Self,
1104 ) -> ::anyhow::Result<Self> {
1105 ::appdb::repository::Repo::<Self>::create_at(id, data).await
1106 }
1107
1108 pub async fn upsert_at(
1109 id: ::surrealdb::types::RecordId,
1110 data: Self,
1111 ) -> ::anyhow::Result<Self> {
1112 ::appdb::repository::Repo::<Self>::upsert_at(id, data).await
1113 }
1114
1115 pub async fn update_at(
1116 self,
1117 id: ::surrealdb::types::RecordId,
1118 ) -> ::anyhow::Result<Self> {
1119 ::appdb::repository::Repo::<Self>::update_at(id, self).await
1120 }
1121
1122 #foreign_write_constructor_impl
1123
1124 pub async fn delete<T>(id: T) -> ::anyhow::Result<()>
1125 where
1126 ::surrealdb::types::RecordIdKey: From<T>,
1127 T: Send,
1128 {
1129 ::appdb::repository::Repo::<Self>::delete(id).await
1130 }
1131 }
1132 })
1133}
1134
1135fn derive_view_impl(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
1136 let struct_ident = input.ident;
1137 let vis = input.vis.clone();
1138 let view_source = view_source_config(&input.attrs)?;
1139
1140 let named_fields = match input.data {
1141 Data::Struct(data) => match data.fields {
1142 Fields::Named(fields) => fields.named,
1143 _ => {
1144 return Err(Error::new_spanned(
1145 struct_ident,
1146 "View can only be derived for structs with named fields",
1147 ));
1148 }
1149 },
1150 _ => {
1151 return Err(Error::new_spanned(
1152 struct_ident,
1153 "View can only be derived for structs",
1154 ));
1155 }
1156 };
1157
1158 let stored_struct_ident = format_ident!("AppdbStoredView{}", struct_ident);
1159 let view_fields = named_fields
1160 .iter()
1161 .map(|field| field.ident.as_ref().expect("named field").to_string())
1162 .collect::<Vec<_>>();
1163 let view_field_literals = view_fields.iter().map(|field| quote! { #field });
1164 let nested_view_fields = named_fields
1165 .iter()
1166 .filter_map(|field| match field_view_nested_attr(field) {
1167 Ok(true) => Some(Ok(field.ident.as_ref().expect("named field").to_string())),
1168 Ok(false) => None,
1169 Err(err) => Some(Err(err)),
1170 })
1171 .collect::<syn::Result<Vec<_>>>()?;
1172 let nested_view_field_literals = nested_view_fields.iter().map(|field| quote! { #field });
1173
1174 let stored_fields = named_fields
1175 .iter()
1176 .map(|field| {
1177 let ident = field.ident.clone().expect("named field");
1178 let ty = view_stored_type(field)?;
1179 if is_record_id_type(&ty) {
1180 Ok(quote! {
1181 #[serde(deserialize_with = "::appdb::serde_utils::id::deserialize_record_id_or_compat_string")]
1182 #ident: #ty
1183 })
1184 } else {
1185 Ok(quote! { #ident: #ty })
1186 }
1187 })
1188 .collect::<syn::Result<Vec<_>>>()?;
1189
1190 let hydrate_assignments = named_fields.iter().map(|field| {
1191 let ident = field.ident.clone().expect("named field");
1192 let ty = field.ty.clone();
1193 if nested_view_fields.contains(&ident.to_string()) {
1194 quote! {
1195 #ident: <#ty as ::appdb::ViewShape>::hydrate_view_shape(stored.#ident).await?
1196 }
1197 } else {
1198 quote! {
1199 #ident: stored.#ident
1200 }
1201 }
1202 });
1203
1204 let source_impl = view_source.impl_tokens();
1205 let source_methods_impl = view_source.methods_tokens(&struct_ident);
1206
1207 Ok(quote! {
1208 #[derive(
1209 Debug,
1210 Clone,
1211 ::serde::Serialize,
1212 ::serde::Deserialize,
1213 ::surrealdb::types::SurrealValue,
1214 )]
1215 #vis struct #stored_struct_ident {
1216 #( #stored_fields, )*
1217 }
1218
1219 #[::async_trait::async_trait]
1220 impl ::appdb::model::meta::ViewMeta for #struct_ident {
1221 #source_impl
1222 type Stored = #stored_struct_ident;
1223
1224 fn view_fields() -> &'static [&'static str] {
1225 &[ #( #view_field_literals ),* ]
1226 }
1227
1228 fn nested_view_fields() -> &'static [&'static str] {
1229 &[ #( #nested_view_field_literals ),* ]
1230 }
1231
1232 fn decode_stored_view_row(
1233 row: ::serde_json::Value,
1234 ) -> ::anyhow::Result<Self::Stored> {
1235 Ok(::serde_json::from_value(row)?)
1236 }
1237
1238 async fn hydrate_view(stored: Self::Stored) -> ::anyhow::Result<Self> {
1239 Ok(Self {
1240 #( #hydrate_assignments, )*
1241 })
1242 }
1243 }
1244
1245 #[::async_trait::async_trait]
1246 impl ::appdb::ViewShape for #struct_ident {
1247 type Stored = ::surrealdb::types::RecordId;
1248
1249 async fn hydrate_view_shape(stored: Self::Stored) -> ::anyhow::Result<Self> {
1250 ::appdb::repository::ViewRepo::<Self>::get_record(stored).await
1251 }
1252 }
1253
1254 impl #struct_ident {
1255 pub fn list() -> ::appdb::repository::ViewListQuery<Self> {
1256 ::appdb::repository::ViewRepo::<Self>::list()
1257 }
1258
1259 #source_methods_impl
1260
1261 pub async fn get<T>(id: T) -> ::anyhow::Result<Self>
1262 where
1263 ::surrealdb::types::RecordIdKey: ::std::convert::From<T>,
1264 T: Send,
1265 {
1266 ::appdb::repository::ViewRepo::<Self>::get(id).await
1267 }
1268
1269 pub async fn get_record(
1270 id: ::surrealdb::types::RecordId,
1271 ) -> ::anyhow::Result<Self> {
1272 ::appdb::repository::ViewRepo::<Self>::get_record(id).await
1273 }
1274
1275 pub async fn list_records(
1276 ) -> ::anyhow::Result<::std::vec::Vec<::appdb::repository::ViewRecord<Self>>> {
1277 ::appdb::repository::ViewRepo::<Self>::list_records().await
1278 }
1279
1280 pub async fn outgoing_records(
1281 id: ::surrealdb::types::RecordId,
1282 relation: &str,
1283 ) -> ::anyhow::Result<::std::vec::Vec<::appdb::repository::ViewRecord<Self>>> {
1284 ::appdb::repository::ViewRepo::<Self>::outgoing_records(id, relation).await
1285 }
1286
1287 pub async fn outgoing_records_by_owners(
1288 ids: ::std::vec::Vec<::surrealdb::types::RecordId>,
1289 relation: &str,
1290 ) -> ::anyhow::Result<::std::vec::Vec<::appdb::repository::ViewRelatedRecord<Self>>> {
1291 ::appdb::repository::ViewRepo::<Self>::outgoing_records_by_owners(ids, relation).await
1292 }
1293
1294 pub async fn incoming_records(
1295 id: ::surrealdb::types::RecordId,
1296 relation: &str,
1297 ) -> ::anyhow::Result<::std::vec::Vec<::appdb::repository::ViewRecord<Self>>> {
1298 ::appdb::repository::ViewRepo::<Self>::incoming_records(id, relation).await
1299 }
1300
1301 pub async fn incoming_records_by_owners(
1302 ids: ::std::vec::Vec<::surrealdb::types::RecordId>,
1303 relation: &str,
1304 ) -> ::anyhow::Result<::std::vec::Vec<::appdb::repository::ViewRelatedRecord<Self>>> {
1305 ::appdb::repository::ViewRepo::<Self>::incoming_records_by_owners(ids, relation).await
1306 }
1307
1308 pub async fn find_one(
1309 k: &str,
1310 v: &str,
1311 ) -> ::anyhow::Result<Self> {
1312 ::appdb::repository::ViewRepo::<Self>::find_one(k, v).await
1313 }
1314
1315 pub async fn find_one_id(
1316 k: &str,
1317 v: &str,
1318 ) -> ::anyhow::Result<::surrealdb::types::RecordId> {
1319 ::appdb::repository::ViewRepo::<Self>::find_one_id(k, v).await
1320 }
1321 }
1322 })
1323}
1324
1325fn derive_bridge_impl(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
1326 let enum_ident = input.ident;
1327
1328 let variants = match input.data {
1329 Data::Enum(data) => data.variants,
1330 _ => {
1331 return Err(Error::new_spanned(
1332 enum_ident,
1333 "Bridge can only be derived for enums",
1334 ));
1335 }
1336 };
1337
1338 let payloads = variants
1339 .iter()
1340 .map(parse_bridge_variant)
1341 .collect::<syn::Result<Vec<_>>>()?;
1342
1343 let from_impls = payloads.iter().map(|variant| {
1344 let variant_ident = &variant.variant_ident;
1345 let payload_ty = &variant.payload_ty;
1346
1347 quote! {
1348 impl ::std::convert::From<#payload_ty> for #enum_ident {
1349 fn from(value: #payload_ty) -> Self {
1350 Self::#variant_ident(value)
1351 }
1352 }
1353 }
1354 });
1355
1356 let persist_match_arms = payloads.iter().map(|variant| {
1357 let variant_ident = &variant.variant_ident;
1358
1359 quote! {
1360 Self::#variant_ident(value) => <_ as ::appdb::Bridge>::persist_foreign(value).await,
1361 }
1362 });
1363
1364 let hydrate_match_arms = payloads.iter().map(|variant| {
1365 let variant_ident = &variant.variant_ident;
1366 let payload_ty = &variant.payload_ty;
1367
1368 quote! {
1369 table if table == <#payload_ty as ::appdb::model::meta::ModelMeta>::storage_table() => {
1370 ::std::result::Result::Ok(Self::#variant_ident(
1371 <#payload_ty as ::appdb::Bridge>::hydrate_foreign(id).await?,
1372 ))
1373 }
1374 }
1375 });
1376
1377 let lookup_match_arms = payloads.iter().map(|variant| {
1378 let variant_ident = &variant.variant_ident;
1379 let payload_ty = &variant.payload_ty;
1380
1381 quote! {
1382 Self::#variant_ident(value) => {
1383 <#payload_ty as ::appdb::ForeignLookupShape>::resolve_foreign_lookup_shape(value).await
1384 }
1385 }
1386 });
1387
1388 Ok(quote! {
1389 #( #from_impls )*
1390
1391 #[::async_trait::async_trait]
1392 impl ::appdb::Bridge for #enum_ident {
1393 async fn persist_foreign(self) -> ::anyhow::Result<::surrealdb::types::RecordId> {
1394 match self {
1395 #( #persist_match_arms )*
1396 }
1397 }
1398
1399 async fn hydrate_foreign(
1400 id: ::surrealdb::types::RecordId,
1401 ) -> ::anyhow::Result<Self> {
1402 match id.table.to_string().as_str() {
1403 #( #hydrate_match_arms, )*
1404 table => ::anyhow::bail!(
1405 "unsupported foreign table `{table}` for enum dispatcher `{}`",
1406 ::std::stringify!(#enum_ident)
1407 ),
1408 }
1409 }
1410 }
1411
1412 impl ::appdb::ForeignLookupShape for #enum_ident {
1413 type LookupStored = ::surrealdb::types::RecordId;
1414
1415 fn resolve_foreign_lookup_shape(
1416 &self,
1417 ) -> impl ::std::future::Future<Output = ::anyhow::Result<Self::LookupStored>> {
1418 async move {
1419 match self {
1420 #( #lookup_match_arms, )*
1421 }
1422 }
1423 }
1424 }
1425 })
1426}
1427
1428#[derive(Clone)]
1429struct BridgeVariant {
1430 variant_ident: syn::Ident,
1431 payload_ty: Type,
1432}
1433
1434fn parse_bridge_variant(variant: &syn::Variant) -> syn::Result<BridgeVariant> {
1435 let payload_ty = match &variant.fields {
1436 Fields::Unnamed(fields) if fields.unnamed.len() == 1 => {
1437 fields.unnamed.first().expect("single field").ty.clone()
1438 }
1439 Fields::Unnamed(_) => {
1440 return Err(Error::new_spanned(
1441 &variant.ident,
1442 "Bridge variants must be single-field tuple variants",
1443 ));
1444 }
1445 Fields::Unit => {
1446 return Err(Error::new_spanned(
1447 &variant.ident,
1448 "Bridge does not support unit variants",
1449 ));
1450 }
1451 Fields::Named(_) => {
1452 return Err(Error::new_spanned(
1453 &variant.ident,
1454 "Bridge does not support struct variants",
1455 ));
1456 }
1457 };
1458
1459 let payload_path = match &payload_ty {
1460 Type::Path(path) => path,
1461 _ => {
1462 return Err(Error::new_spanned(
1463 &payload_ty,
1464 "Bridge payload must implement appdb::Bridge",
1465 ));
1466 }
1467 };
1468
1469 let segment = payload_path.path.segments.last().ok_or_else(|| {
1470 Error::new_spanned(&payload_ty, "Bridge payload must implement appdb::Bridge")
1471 })?;
1472
1473 if !matches!(segment.arguments, PathArguments::None) {
1474 return Err(Error::new_spanned(
1475 &payload_ty,
1476 "Bridge payload must implement appdb::Bridge",
1477 ));
1478 }
1479
1480 Ok(BridgeVariant {
1481 variant_ident: variant.ident.clone(),
1482 payload_ty,
1483 })
1484}
1485
1486fn derive_relation_impl(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
1487 let struct_ident = input.ident;
1488 let relation_name = relation_name_override(&input.attrs)?
1489 .unwrap_or_else(|| to_snake_case(&struct_ident.to_string()));
1490
1491 match input.data {
1492 Data::Struct(data) => match data.fields {
1493 Fields::Unit | Fields::Named(_) => {}
1494 _ => {
1495 return Err(Error::new_spanned(
1496 struct_ident,
1497 "Relation can only be derived for unit structs or structs with named fields",
1498 ));
1499 }
1500 },
1501 _ => {
1502 return Err(Error::new_spanned(
1503 struct_ident,
1504 "Relation can only be derived for structs",
1505 ));
1506 }
1507 }
1508
1509 Ok(quote! {
1510 impl ::appdb::model::relation::RelationMeta for #struct_ident {
1511 fn relation_name() -> &'static str {
1512 static REL_NAME: ::std::sync::OnceLock<&'static str> = ::std::sync::OnceLock::new();
1513 REL_NAME.get_or_init(|| ::appdb::model::relation::register_relation(#relation_name))
1514 }
1515 }
1516
1517 impl #struct_ident {
1518 pub async fn relate<A, B>(a: &A, b: &B) -> ::anyhow::Result<()>
1519 where
1520 A: ::appdb::model::meta::ResolveRecordId + Send + Sync,
1521 B: ::appdb::model::meta::ResolveRecordId + Send + Sync,
1522 {
1523 ::appdb::graph::relate_at(a.resolve_record_id().await?, b.resolve_record_id().await?, <Self as ::appdb::model::relation::RelationMeta>::relation_name()).await
1524 }
1525
1526 pub async fn back_relate<A, B>(a: &A, b: &B) -> ::anyhow::Result<()>
1527 where
1528 A: ::appdb::model::meta::ResolveRecordId + Send + Sync,
1529 B: ::appdb::model::meta::ResolveRecordId + Send + Sync,
1530 {
1531 ::appdb::graph::back_relate_at(a.resolve_record_id().await?, b.resolve_record_id().await?, <Self as ::appdb::model::relation::RelationMeta>::relation_name()).await
1532 }
1533
1534 pub async fn unrelate<A, B>(a: &A, b: &B) -> ::anyhow::Result<()>
1535 where
1536 A: ::appdb::model::meta::ResolveRecordId + Send + Sync,
1537 B: ::appdb::model::meta::ResolveRecordId + Send + Sync,
1538 {
1539 ::appdb::graph::unrelate_at(a.resolve_record_id().await?, b.resolve_record_id().await?, <Self as ::appdb::model::relation::RelationMeta>::relation_name()).await
1540 }
1541
1542 pub async fn out_ids<A>(a: &A, out_table: &str) -> ::anyhow::Result<::std::vec::Vec<::surrealdb::types::RecordId>>
1543 where
1544 A: ::appdb::model::meta::ResolveRecordId + Send + Sync,
1545 {
1546 ::appdb::graph::out_ids(a.resolve_record_id().await?, <Self as ::appdb::model::relation::RelationMeta>::relation_name(), out_table).await
1547 }
1548
1549 pub async fn in_ids<B>(b: &B, in_table: &str) -> ::anyhow::Result<::std::vec::Vec<::surrealdb::types::RecordId>>
1550 where
1551 B: ::appdb::model::meta::ResolveRecordId + Send + Sync,
1552 {
1553 ::appdb::graph::in_ids(b.resolve_record_id().await?, <Self as ::appdb::model::relation::RelationMeta>::relation_name(), in_table).await
1554 }
1555 }
1556 })
1557}
1558
1559fn derive_sensitive_impl(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
1560 let struct_ident = input.ident;
1561 let encrypted_ident = format_ident!("Encrypted{}", struct_ident);
1562 let vis = input.vis;
1563 let type_crypto_config = type_crypto_config(&input.attrs)?;
1564 let named_fields = match input.data {
1565 Data::Struct(data) => match data.fields {
1566 Fields::Named(fields) => fields.named,
1567 _ => {
1568 return Err(Error::new_spanned(
1569 struct_ident,
1570 "Sensitive can only be derived for structs with named fields",
1571 ));
1572 }
1573 },
1574 _ => {
1575 return Err(Error::new_spanned(
1576 struct_ident,
1577 "Sensitive can only be derived for structs",
1578 ));
1579 }
1580 };
1581
1582 let mut secure_field_count = 0usize;
1583 let mut encrypted_fields = Vec::new();
1584 let mut encrypt_assignments = Vec::new();
1585 let mut decrypt_assignments = Vec::new();
1586 let mut runtime_encrypt_assignments = Vec::new();
1587 let mut runtime_decrypt_assignments = Vec::new();
1588 let mut field_tag_structs = Vec::new();
1589 let mut secure_field_meta_entries = Vec::new();
1590
1591 for field in named_fields.iter() {
1592 let ident = field.ident.clone().expect("named field");
1593 let field_vis = field.vis.clone();
1594 let secure = has_secure_attr(&field.attrs);
1595 let field_crypto_config = field_crypto_config(&field.attrs)?;
1596
1597 if !secure && field_crypto_config.is_present() {
1598 return Err(Error::new_spanned(
1599 ident,
1600 "#[crypto(...)] on a field requires #[secure] on the same field",
1601 ));
1602 }
1603
1604 if secure {
1605 secure_field_count += 1;
1606 let secure_kind = secure_kind(field)?;
1607 let encrypted_ty = secure_kind.encrypted_type();
1608 let field_tag_ident = format_ident!(
1609 "AppdbSensitiveFieldTag{}{}",
1610 struct_ident,
1611 to_pascal_case(&ident.to_string())
1612 );
1613 let field_tag_literal = ident.to_string();
1614 let effective_account = field_crypto_config
1615 .field_account
1616 .clone()
1617 .or_else(|| type_crypto_config.account.clone());
1618 let service_override = type_crypto_config.service.clone();
1619 let account_literal = effective_account
1620 .as_ref()
1621 .map(|value| quote! { ::std::option::Option::Some(#value) })
1622 .unwrap_or_else(|| quote! { ::std::option::Option::None });
1623 let service_literal = service_override
1624 .as_ref()
1625 .map(|value| quote! { ::std::option::Option::Some(#value) })
1626 .unwrap_or_else(|| quote! { ::std::option::Option::None });
1627 let encrypt_expr = secure_kind.encrypt_with_context_expr(&ident);
1628 let decrypt_expr = secure_kind.decrypt_with_context_expr(&ident);
1629 let runtime_encrypt_expr =
1630 secure_kind.encrypt_with_runtime_expr(&ident, &field_tag_ident);
1631 let runtime_decrypt_expr =
1632 secure_kind.decrypt_with_runtime_expr(&ident, &field_tag_ident);
1633 encrypted_fields.push(quote! { #field_vis #ident: #encrypted_ty });
1634 encrypt_assignments.push(quote! { #ident: #encrypt_expr });
1635 decrypt_assignments.push(quote! { #ident: #decrypt_expr });
1636 runtime_encrypt_assignments.push(quote! { #ident: #runtime_encrypt_expr });
1637 runtime_decrypt_assignments.push(quote! { #ident: #runtime_decrypt_expr });
1638 secure_field_meta_entries.push(quote! {
1639 ::appdb::crypto::SensitiveFieldMetadata {
1640 model_tag: ::std::concat!(::std::module_path!(), "::", ::std::stringify!(#struct_ident)),
1641 field_tag: #field_tag_literal,
1642 service: #service_literal,
1643 account: #account_literal,
1644 secure_fields: &[],
1645 }
1646 });
1647 field_tag_structs.push(quote! {
1648 #[doc(hidden)]
1649 #vis struct #field_tag_ident;
1650
1651 impl ::appdb::crypto::SensitiveFieldTag for #field_tag_ident {
1652 fn model_tag() -> &'static str {
1653 <#struct_ident as ::appdb::crypto::SensitiveModelTag>::model_tag()
1654 }
1655
1656 fn field_tag() -> &'static str {
1657 #field_tag_literal
1658 }
1659
1660 fn crypto_metadata() -> &'static ::appdb::crypto::SensitiveFieldMetadata {
1661 static FIELD_META: ::std::sync::OnceLock<::appdb::crypto::SensitiveFieldMetadata> = ::std::sync::OnceLock::new();
1662 FIELD_META.get_or_init(|| ::appdb::crypto::SensitiveFieldMetadata {
1663 model_tag: <#struct_ident as ::appdb::crypto::SensitiveModelTag>::model_tag(),
1664 field_tag: #field_tag_literal,
1665 service: #service_literal,
1666 account: #account_literal,
1667 secure_fields: &#struct_ident::SECURE_FIELDS,
1668 })
1669 }
1670 }
1671 });
1672 } else {
1673 let ty = field.ty.clone();
1674 encrypted_fields.push(quote! { #field_vis #ident: #ty });
1675 encrypt_assignments.push(quote! { #ident: self.#ident.clone() });
1676 decrypt_assignments.push(quote! { #ident: encrypted.#ident.clone() });
1677 runtime_encrypt_assignments.push(quote! { #ident: self.#ident.clone() });
1678 runtime_decrypt_assignments.push(quote! { #ident: encrypted.#ident.clone() });
1679 }
1680 }
1681
1682 if secure_field_count == 0 {
1683 return Err(Error::new_spanned(
1684 struct_ident,
1685 "Sensitive requires at least one #[secure] field",
1686 ));
1687 }
1688
1689 Ok(quote! {
1690 #[derive(
1691 Debug,
1692 Clone,
1693 ::serde::Serialize,
1694 ::serde::Deserialize,
1695 ::surrealdb::types::SurrealValue,
1696 )]
1697 #vis struct #encrypted_ident {
1698 #( #encrypted_fields, )*
1699 }
1700
1701 impl ::appdb::crypto::SensitiveModelTag for #struct_ident {
1702 fn model_tag() -> &'static str {
1703 ::std::concat!(::std::module_path!(), "::", ::std::stringify!(#struct_ident))
1704 }
1705 }
1706
1707 #( #field_tag_structs )*
1708
1709 impl ::appdb::Sensitive for #struct_ident {
1710 type Encrypted = #encrypted_ident;
1711
1712 fn encrypt(
1713 &self,
1714 context: &::appdb::crypto::CryptoContext,
1715 ) -> ::std::result::Result<Self::Encrypted, ::appdb::crypto::CryptoError> {
1716 ::std::result::Result::Ok(#encrypted_ident {
1717 #( #encrypt_assignments, )*
1718 })
1719 }
1720
1721 fn decrypt(
1722 encrypted: &Self::Encrypted,
1723 context: &::appdb::crypto::CryptoContext,
1724 ) -> ::std::result::Result<Self, ::appdb::crypto::CryptoError> {
1725 ::std::result::Result::Ok(Self {
1726 #( #decrypt_assignments, )*
1727 })
1728 }
1729
1730 fn encrypt_with_runtime_resolver(
1731 &self,
1732 ) -> ::std::result::Result<Self::Encrypted, ::appdb::crypto::CryptoError> {
1733 ::std::result::Result::Ok(#encrypted_ident {
1734 #( #runtime_encrypt_assignments, )*
1735 })
1736 }
1737
1738 fn decrypt_with_runtime_resolver(
1739 encrypted: &Self::Encrypted,
1740 ) -> ::std::result::Result<Self, ::appdb::crypto::CryptoError> {
1741 ::std::result::Result::Ok(Self {
1742 #( #runtime_decrypt_assignments, )*
1743 })
1744 }
1745
1746 fn secure_fields() -> &'static [::appdb::crypto::SensitiveFieldMetadata] {
1747 &Self::SECURE_FIELDS
1748 }
1749 }
1750
1751 impl #struct_ident {
1752 pub const SECURE_FIELDS: [::appdb::crypto::SensitiveFieldMetadata; #secure_field_count] = [
1753 #( #secure_field_meta_entries, )*
1754 ];
1755
1756 pub fn encrypt(
1757 &self,
1758 context: &::appdb::crypto::CryptoContext,
1759 ) -> ::std::result::Result<#encrypted_ident, ::appdb::crypto::CryptoError> {
1760 <Self as ::appdb::Sensitive>::encrypt(self, context)
1761 }
1762 }
1763
1764 impl #encrypted_ident {
1765 pub fn decrypt(
1766 &self,
1767 context: &::appdb::crypto::CryptoContext,
1768 ) -> ::std::result::Result<#struct_ident, ::appdb::crypto::CryptoError> {
1769 <#struct_ident as ::appdb::Sensitive>::decrypt(self, context)
1770 }
1771 }
1772 })
1773}
1774
1775fn has_secure_attr(attrs: &[Attribute]) -> bool {
1776 attrs.iter().any(|attr| attr.path().is_ident("secure"))
1777}
1778
1779fn has_unique_attr(attrs: &[Attribute]) -> bool {
1780 attrs.iter().any(|attr| attr.path().is_ident("unique"))
1781}
1782
1783#[derive(Default, Clone)]
1784struct TypeCryptoConfig {
1785 service: Option<String>,
1786 account: Option<String>,
1787}
1788
1789#[derive(Default, Clone)]
1790struct FieldCryptoConfig {
1791 field_account: Option<String>,
1792}
1793
1794impl FieldCryptoConfig {
1795 fn is_present(&self) -> bool {
1796 self.field_account.is_some()
1797 }
1798}
1799
1800fn type_crypto_config(attrs: &[Attribute]) -> syn::Result<TypeCryptoConfig> {
1801 let mut config = TypeCryptoConfig::default();
1802 let mut seen = HashSet::new();
1803
1804 for attr in attrs {
1805 if !attr.path().is_ident("crypto") {
1806 continue;
1807 }
1808
1809 attr.parse_nested_meta(|meta| {
1810 let key = meta
1811 .path
1812 .get_ident()
1813 .cloned()
1814 .ok_or_else(|| meta.error("unsupported crypto attribute"))?;
1815
1816 if !seen.insert(key.to_string()) {
1817 return Err(meta.error("duplicate crypto attribute key"));
1818 }
1819
1820 let value = meta.value()?;
1821 let literal: syn::LitStr = value.parse()?;
1822 match key.to_string().as_str() {
1823 "service" => config.service = Some(literal.value()),
1824 "account" => config.account = Some(literal.value()),
1825 _ => {
1826 return Err(
1827 meta.error("unsupported crypto attribute; expected `service` or `account`")
1828 );
1829 }
1830 }
1831 Ok(())
1832 })?;
1833 }
1834
1835 Ok(config)
1836}
1837
1838fn field_crypto_config(attrs: &[Attribute]) -> syn::Result<FieldCryptoConfig> {
1839 let mut config = FieldCryptoConfig::default();
1840 let mut seen = HashSet::new();
1841
1842 for attr in attrs {
1843 if attr.path().is_ident("crypto") {
1844 attr.parse_nested_meta(|meta| {
1845 let key = meta
1846 .path
1847 .get_ident()
1848 .cloned()
1849 .ok_or_else(|| meta.error("unsupported crypto attribute"))?;
1850
1851 if !seen.insert(key.to_string()) {
1852 return Err(meta.error("duplicate crypto attribute key"));
1853 }
1854
1855 let value = meta.value()?;
1856 let literal: syn::LitStr = value.parse()?;
1857 match key.to_string().as_str() {
1858 "field_account" => config.field_account = Some(literal.value()),
1859 _ => {
1860 return Err(meta.error(
1861 "unsupported field crypto attribute; expected `field_account`",
1862 ));
1863 }
1864 }
1865 Ok(())
1866 })?;
1867 } else if attr.path().is_ident("secure") && matches!(attr.meta, Meta::List(_)) {
1868 return Err(Error::new_spanned(
1869 attr,
1870 "#[secure] does not accept arguments; use #[crypto(field_account = \"...\")] on the field",
1871 ));
1872 }
1873 }
1874
1875 Ok(config)
1876}
1877
1878fn table_alias_target(attrs: &[Attribute]) -> syn::Result<Option<Type>> {
1879 let mut target = None;
1880
1881 for attr in attrs {
1882 if !attr.path().is_ident("table_as") {
1883 continue;
1884 }
1885
1886 if target.is_some() {
1887 return Err(Error::new_spanned(
1888 attr,
1889 "duplicate #[table_as(...)] attribute is not supported",
1890 ));
1891 }
1892
1893 let parsed: Type = attr.parse_args().map_err(|_| {
1894 Error::new_spanned(attr, "#[table_as(...)] requires exactly one target type")
1895 })?;
1896
1897 match parsed {
1898 Type::Path(TypePath { ref path, .. }) if !path.segments.is_empty() => {
1899 target = Some(parsed);
1900 }
1901 _ => {
1902 return Err(Error::new_spanned(
1903 parsed,
1904 "#[table_as(...)] target must be a type path",
1905 ));
1906 }
1907 }
1908 }
1909
1910 Ok(target)
1911}
1912
1913enum ViewSourceConfig {
1914 Table {
1915 source_ty: Type,
1916 },
1917 Sql {
1918 params_ty: Type,
1919 sql: LitStr,
1920 result_idx: usize,
1921 },
1922}
1923
1924impl ViewSourceConfig {
1925 fn impl_tokens(&self) -> proc_macro2::TokenStream {
1926 match self {
1927 Self::Table { source_ty } => quote! {
1928 type Source = #source_ty;
1929 type Params = ();
1930 },
1931 Self::Sql {
1932 params_ty,
1933 sql,
1934 result_idx,
1935 } => quote! {
1936 type Source = ::appdb::model::meta::NoViewSource;
1937 type Params = #params_ty;
1938
1939 fn source_kind() -> ::appdb::model::meta::ViewSource {
1940 ::appdb::model::meta::ViewSource::Sql
1941 }
1942
1943 fn sql() -> ::std::option::Option<&'static str> {
1944 ::std::option::Option::Some(#sql)
1945 }
1946
1947 fn sql_result_index() -> usize {
1948 #result_idx
1949 }
1950 },
1951 }
1952 }
1953
1954 fn methods_tokens(&self, struct_ident: &syn::Ident) -> proc_macro2::TokenStream {
1955 match self {
1956 Self::Table { .. } => quote! {},
1957 Self::Sql { params_ty, .. } => quote! {
1958 pub async fn query(params: #params_ty) -> ::anyhow::Result<::std::vec::Vec<#struct_ident>> {
1959 ::appdb::repository::ViewRepo::<Self>::query(params).await
1960 }
1961 },
1962 }
1963 }
1964}
1965
1966fn view_source_config(attrs: &[Attribute]) -> syn::Result<ViewSourceConfig> {
1967 let mut source = None;
1968 let mut sql = None;
1969 let mut params = None;
1970 let mut result = None;
1971
1972 for attr in attrs {
1973 if !attr.path().is_ident("view") {
1974 continue;
1975 }
1976
1977 attr.parse_nested_meta(|meta| {
1978 if meta.path.is_ident("source") {
1979 if source.is_some() {
1980 return Err(meta.error("duplicate #[view(source = ...)] attribute is not supported"));
1981 }
1982 let value = meta.value()?;
1983 let parsed: Type = value.parse()?;
1984 match parsed {
1985 Type::Path(TypePath { ref path, .. }) if !path.segments.is_empty() => {
1986 source = Some(parsed);
1987 Ok(())
1988 }
1989 _ => Err(Error::new_spanned(
1990 parsed,
1991 "#[view(source = ...)] source must be a type path",
1992 )),
1993 }
1994 } else if meta.path.is_ident("sql") {
1995 if sql.is_some() {
1996 return Err(meta.error("duplicate #[view(sql = ...)] attribute is not supported"));
1997 }
1998 let value = meta.value()?;
1999 sql = Some(value.parse()?);
2000 Ok(())
2001 } else if meta.path.is_ident("params") {
2002 if params.is_some() {
2003 return Err(meta.error("duplicate #[view(params = ...)] attribute is not supported"));
2004 }
2005 let value = meta.value()?;
2006 let parsed: Type = value.parse()?;
2007 match parsed {
2008 Type::Path(TypePath { ref path, .. }) if !path.segments.is_empty() => {
2009 params = Some(parsed);
2010 Ok(())
2011 }
2012 _ => Err(Error::new_spanned(
2013 parsed,
2014 "#[view(params = ...)] params must be a type path",
2015 )),
2016 }
2017 } else if meta.path.is_ident("result") {
2018 if result.is_some() {
2019 return Err(meta.error("duplicate #[view(result = ...)] attribute is not supported"));
2020 }
2021 let value = meta.value()?;
2022 let parsed: LitInt = value.parse()?;
2023 result = Some(parsed.base10_parse::<usize>()?);
2024 Ok(())
2025 } else {
2026 Err(meta.error("unsupported view attribute; expected `source = Type`, `sql = \"...\"`, `params = Type`, or `result = N`"))
2027 }
2028 })?;
2029 }
2030
2031 match (sql, params, result) {
2032 (None, None, None) => {
2033 let source_ty = source.ok_or_else(|| {
2034 Error::new(
2035 proc_macro2::Span::call_site(),
2036 "View requires #[view(source = SourceStoreType)] or #[view(sql = \"...\", params = ParamsType)]",
2037 )
2038 })?;
2039 Ok(ViewSourceConfig::Table { source_ty })
2040 }
2041 (Some(sql), Some(params_ty), result_idx) => {
2042 if source.is_some() {
2043 return Err(Error::new(
2044 proc_macro2::Span::call_site(),
2045 "SQL View cannot combine #[view(source = ...)] with #[view(sql = ...)]",
2046 ));
2047 }
2048 Ok(ViewSourceConfig::Sql {
2049 params_ty,
2050 sql,
2051 result_idx: result_idx.unwrap_or(0),
2052 })
2053 }
2054 (Some(_), None, _) => Err(Error::new(
2055 proc_macro2::Span::call_site(),
2056 "SQL View requires #[view(params = ParamsType)]",
2057 )),
2058 (None, Some(_), _) | (None, _, Some(_)) => Err(Error::new(
2059 proc_macro2::Span::call_site(),
2060 "#[view(params = ...)] and #[view(result = ...)] require #[view(sql = \"...\")]",
2061 )),
2062 }
2063}
2064
2065fn field_view_nested_attr(field: &Field) -> syn::Result<bool> {
2066 let mut is_nested = false;
2067
2068 for attr in &field.attrs {
2069 if !attr.path().is_ident("view") {
2070 continue;
2071 }
2072
2073 attr.parse_nested_meta(|meta| {
2074 if meta.path.is_ident("nested") {
2075 if is_nested {
2076 return Err(meta.error("duplicate #[view(nested)] marker is not supported"));
2077 }
2078 is_nested = true;
2079 Ok(())
2080 } else {
2081 Err(meta.error("unsupported view field attribute; expected #[view(nested)]"))
2082 }
2083 })?;
2084 }
2085
2086 Ok(is_nested)
2087}
2088
2089fn resolved_schema_table_name(struct_ident: &syn::Ident, table_alias: Option<&Type>) -> String {
2090 match table_alias {
2091 Some(Type::Path(type_path)) => type_path
2092 .path
2093 .segments
2094 .last()
2095 .map(|segment| to_snake_case(&segment.ident.to_string()))
2096 .unwrap_or_else(|| to_snake_case(&struct_ident.to_string())),
2097 Some(_) => to_snake_case(&struct_ident.to_string()),
2098 None => to_snake_case(&struct_ident.to_string()),
2099 }
2100}
2101
2102fn field_foreign_attr(field: &Field) -> syn::Result<Option<&Attribute>> {
2103 let mut foreign_attr = None;
2104
2105 for attr in &field.attrs {
2106 if !attr.path().is_ident("foreign") {
2107 continue;
2108 }
2109
2110 if foreign_attr.is_some() {
2111 return Err(Error::new_spanned(
2112 attr,
2113 "duplicate nested-ref attribute is not supported",
2114 ));
2115 }
2116
2117 foreign_attr = Some(attr);
2118 }
2119
2120 Ok(foreign_attr)
2121}
2122
2123fn field_relation_attr(field: &Field) -> syn::Result<Option<&Attribute>> {
2124 let mut relate_attr = None;
2125
2126 for attr in &field.attrs {
2127 if !attr.path().is_ident("relate") && !attr.path().is_ident("back_relate") {
2128 continue;
2129 }
2130
2131 if let Some(previous) = relate_attr {
2132 return Err(Error::new_spanned(
2133 attr,
2134 relation_attr_conflict_message(previous, attr),
2135 ));
2136 }
2137
2138 relate_attr = Some(attr);
2139 }
2140
2141 Ok(relate_attr)
2142}
2143
2144fn field_pagin_attr(field: &Field) -> syn::Result<Option<&Attribute>> {
2145 let mut pagin_attr = None;
2146
2147 for attr in &field.attrs {
2148 if !attr.path().is_ident("pagin") {
2149 continue;
2150 }
2151
2152 if pagin_attr.is_some() {
2153 return Err(Error::new_spanned(
2154 attr,
2155 "duplicate #[pagin] attribute is not supported",
2156 ));
2157 }
2158
2159 pagin_attr = Some(attr);
2160 }
2161
2162 Ok(pagin_attr)
2163}
2164
2165fn field_fill_attr(field: &Field) -> syn::Result<Option<&Attribute>> {
2166 let mut fill_attr = None;
2167
2168 for attr in &field.attrs {
2169 if !attr.path().is_ident("fill") {
2170 continue;
2171 }
2172
2173 if fill_attr.is_some() {
2174 return Err(Error::new_spanned(
2175 attr,
2176 "duplicate #[fill(...)] attribute is not supported",
2177 ));
2178 }
2179
2180 fill_attr = Some(attr);
2181 }
2182
2183 Ok(fill_attr)
2184}
2185
2186fn validate_foreign_field(field: &Field, attr: &Attribute) -> syn::Result<Type> {
2187 if attr.path().is_ident("foreign") {
2188 return foreign_leaf_type(&field.ty)
2189 .ok_or_else(|| Error::new_spanned(&field.ty, BINDREF_ACCEPTED_SHAPES));
2190 }
2191
2192 Err(Error::new_spanned(attr, "unsupported foreign attribute"))
2193}
2194
2195const BINDREF_ACCEPTED_SHAPES: &str = "#[foreign] supports recursive Option<_> / Vec<_> shapes whose leaf type implements appdb::Bridge";
2196
2197const BINDREF_BRIDGE_STORE_ONLY: &str =
2198 "#[foreign] leaf types must derive Store or #[derive(Bridge)] dispatcher enums";
2199
2200const RELATE_ACCEPTED_SHAPES: &str = "relation-backed fields support Child / Option<Child> / Vec<Child> / Option<Vec<Child>> shapes whose leaf type implements appdb::Bridge";
2201
2202const PAGIN_ACCEPTED_SHAPES: &str = "#[pagin] supports direct scalar fields plus Id/RecordId; Option<_> and Vec<_> wrappers are not supported";
2203
2204const FILL_ACCEPTED_SHAPES: &str = "#[fill(...)] fields must use appdb::AutoFill";
2205
2206#[derive(Clone, Copy)]
2207enum RelationFieldDirection {
2208 Outgoing,
2209 Incoming,
2210}
2211
2212#[derive(Clone, Copy)]
2213enum FillProvider {
2214 Now,
2215}
2216
2217impl RelationFieldDirection {
2218 fn attr_label(self) -> &'static str {
2219 match self {
2220 Self::Outgoing => "#[relate(...)]",
2221 Self::Incoming => "#[back_relate(...)]",
2222 }
2223 }
2224
2225 fn write_direction_tokens(self) -> proc_macro2::TokenStream {
2226 match self {
2227 Self::Outgoing => quote!(::appdb::RelationWriteDirection::Outgoing),
2228 Self::Incoming => quote!(::appdb::RelationWriteDirection::Incoming),
2229 }
2230 }
2231
2232 fn write_edges_tokens(self) -> proc_macro2::TokenStream {
2233 match self {
2234 Self::Outgoing => quote! {
2235 ids
2236 .into_iter()
2237 .enumerate()
2238 .map(|(position, out)| ::appdb::graph::OrderedRelationEdge {
2239 _in: ::std::option::Option::Some(record.clone()),
2240 out,
2241 position: position as i64,
2242 })
2243 .collect()
2244 },
2245 Self::Incoming => quote! {
2246 ids
2247 .into_iter()
2248 .enumerate()
2249 .map(|(position, source)| ::appdb::graph::OrderedRelationEdge {
2250 _in: ::std::option::Option::Some(source),
2251 out: record.clone(),
2252 position: position as i64,
2253 })
2254 .collect()
2255 },
2256 }
2257 }
2258
2259 fn load_edges_tokens(self, relation_name: &str) -> proc_macro2::TokenStream {
2260 match self {
2261 Self::Outgoing => quote! {
2262 ::appdb::graph::GraphRepo::out_edges(record.clone(), #relation_name)
2263 .await?
2264 .into_iter()
2265 .map(|edge| edge.out)
2266 .collect()
2267 },
2268 Self::Incoming => quote! {
2269 {
2270 let mut ids = ::std::vec::Vec::new();
2271 for edge in ::appdb::graph::GraphRepo::in_edges(record.clone(), #relation_name).await? {
2272 let incoming = edge._in.ok_or_else(|| {
2273 ::anyhow::anyhow!("back relate field received relation edge without `in` record id")
2274 })?;
2275 ids.push(incoming);
2276 }
2277 ids
2278 }
2279 },
2280 }
2281 }
2282}
2283
2284#[derive(Clone)]
2285struct ForeignField {
2286 ident: syn::Ident,
2287 kind: ForeignFieldKind,
2288}
2289
2290#[derive(Clone)]
2291struct ForeignFieldKind {
2292 original_ty: Type,
2293 stored_ty: Type,
2294}
2295
2296#[derive(Clone)]
2297struct RelateField {
2298 ident: syn::Ident,
2299 relation_name: String,
2300 field_ty: Type,
2301 direction: RelationFieldDirection,
2302}
2303
2304#[derive(Clone)]
2305struct PaginField {
2306 ident: syn::Ident,
2307}
2308
2309#[derive(Clone)]
2310struct FillField {
2311 ident: syn::Ident,
2312 provider: FillProvider,
2313}
2314
2315fn parse_foreign_field(field: &Field, attr: &Attribute) -> syn::Result<ForeignField> {
2316 validate_foreign_field(field, attr)?;
2317 let ident = field.ident.clone().expect("named field");
2318
2319 let kind = ForeignFieldKind {
2320 original_ty: field.ty.clone(),
2321 stored_ty: foreign_stored_type(&field.ty)
2322 .ok_or_else(|| Error::new_spanned(&field.ty, BINDREF_ACCEPTED_SHAPES))?,
2323 };
2324
2325 Ok(ForeignField { ident, kind })
2326}
2327
2328fn parse_relate_field(field: &Field, attr: &Attribute) -> syn::Result<RelateField> {
2329 let direction = parse_relation_direction(attr)?;
2330 let relation_name = attr
2331 .parse_args::<syn::LitStr>()
2332 .map_err(|_| {
2333 Error::new_spanned(
2334 attr,
2335 format!(
2336 "{} requires exactly one string literal",
2337 direction.attr_label()
2338 ),
2339 )
2340 })?
2341 .value();
2342 if relation_name.is_empty() {
2343 return Err(Error::new_spanned(
2344 attr,
2345 format!("{} relation name must not be empty", direction.attr_label()),
2346 ));
2347 }
2348
2349 validate_relate_field(field, attr)?;
2350
2351 Ok(RelateField {
2352 ident: field.ident.clone().expect("named field"),
2353 relation_name,
2354 field_ty: field.ty.clone(),
2355 direction,
2356 })
2357}
2358
2359fn parse_pagin_field(field: &Field, attr: &Attribute) -> syn::Result<PaginField> {
2360 validate_pagin_field(field, attr)?;
2361 Ok(PaginField {
2362 ident: field.ident.clone().expect("named field"),
2363 })
2364}
2365
2366fn parse_fill_field(field: &Field, attr: &Attribute) -> syn::Result<FillField> {
2367 let provider = validate_fill_field(field, attr)?;
2368 Ok(FillField {
2369 ident: field.ident.clone().expect("named field"),
2370 provider,
2371 })
2372}
2373
2374fn validate_relate_field(field: &Field, attr: &Attribute) -> syn::Result<Type> {
2375 if !attr.path().is_ident("relate") && !attr.path().is_ident("back_relate") {
2376 return Err(Error::new_spanned(attr, "unsupported relate attribute"));
2377 }
2378
2379 let accepted = relate_leaf_type(&field.ty).cloned().map(Type::Path);
2380
2381 accepted.ok_or_else(|| Error::new_spanned(&field.ty, RELATE_ACCEPTED_SHAPES))
2382}
2383
2384fn validate_pagin_field(field: &Field, attr: &Attribute) -> syn::Result<Type> {
2385 if !attr.path().is_ident("pagin") {
2386 return Err(Error::new_spanned(attr, "unsupported pagination attribute"));
2387 }
2388
2389 if pagination_leaf_type(&field.ty).is_none() {
2390 return Err(Error::new_spanned(&field.ty, PAGIN_ACCEPTED_SHAPES));
2391 }
2392
2393 Ok(field.ty.clone())
2394}
2395
2396fn validate_fill_field(field: &Field, attr: &Attribute) -> syn::Result<FillProvider> {
2397 if !attr.path().is_ident("fill") {
2398 return Err(Error::new_spanned(attr, "unsupported fill attribute"));
2399 }
2400
2401 if !is_autofill_type(&field.ty) {
2402 return Err(Error::new_spanned(&field.ty, FILL_ACCEPTED_SHAPES));
2403 }
2404
2405 parse_fill_provider(attr)
2406}
2407
2408fn parse_fill_provider(attr: &Attribute) -> syn::Result<FillProvider> {
2409 let provider = attr.parse_args::<syn::Path>().map_err(|_| {
2410 Error::new_spanned(
2411 attr,
2412 "#[fill(...)] requires exactly one provider like #[fill(now)]",
2413 )
2414 })?;
2415
2416 if provider.is_ident("now") {
2417 Ok(FillProvider::Now)
2418 } else {
2419 Err(Error::new_spanned(
2420 attr,
2421 "unsupported fill provider; expected #[fill(now)]",
2422 ))
2423 }
2424}
2425
2426fn relate_leaf_type(ty: &Type) -> Option<&TypePath> {
2427 if let Some(leaf) = direct_store_child_type(ty) {
2428 return Some(leaf);
2429 }
2430
2431 if let Some(inner) = option_inner_type(ty) {
2432 if let Some(leaf) = direct_store_child_type(inner) {
2433 return Some(leaf);
2434 }
2435
2436 if let Some(vec_inner) = vec_inner_type(inner) {
2437 return direct_store_child_type(vec_inner);
2438 }
2439
2440 return None;
2441 }
2442
2443 if let Some(inner) = vec_inner_type(ty) {
2444 return direct_store_child_type(inner);
2445 }
2446
2447 None
2448}
2449
2450fn pagination_leaf_type(ty: &Type) -> Option<Type> {
2451 if option_inner_type(ty).is_some() || vec_inner_type(ty).is_some() {
2452 return None;
2453 }
2454
2455 if is_id_type(ty)
2456 || is_record_id_type(ty)
2457 || is_autofill_type(ty)
2458 || is_string_type(ty)
2459 || is_common_non_store_leaf_type(ty)
2460 {
2461 return Some(ty.clone());
2462 }
2463
2464 None
2465}
2466
2467fn parse_relation_direction(attr: &Attribute) -> syn::Result<RelationFieldDirection> {
2468 if attr.path().is_ident("relate") {
2469 return Ok(RelationFieldDirection::Outgoing);
2470 }
2471 if attr.path().is_ident("back_relate") {
2472 return Ok(RelationFieldDirection::Incoming);
2473 }
2474
2475 Err(Error::new_spanned(attr, "unsupported relate attribute"))
2476}
2477
2478fn relation_attr_label(attr: &Attribute) -> &'static str {
2479 if attr.path().is_ident("back_relate") {
2480 "#[back_relate(...)]"
2481 } else {
2482 "#[relate(...)]"
2483 }
2484}
2485
2486fn relation_attr_conflict_message(previous: &Attribute, current: &Attribute) -> String {
2487 match (
2488 previous.path().is_ident("relate"),
2489 previous.path().is_ident("back_relate"),
2490 current.path().is_ident("relate"),
2491 current.path().is_ident("back_relate"),
2492 ) {
2493 (true, false, true, false) => {
2494 "duplicate #[relate(...)] attribute is not supported".to_owned()
2495 }
2496 (false, true, false, true) => {
2497 "duplicate #[back_relate(...)] attribute is not supported".to_owned()
2498 }
2499 _ => "#[relate(...)] cannot be combined with #[back_relate(...)]".to_owned(),
2500 }
2501}
2502
2503fn foreign_field_kind<'a>(
2504 ident: &syn::Ident,
2505 fields: &'a [ForeignField],
2506) -> Option<&'a ForeignFieldKind> {
2507 fields
2508 .iter()
2509 .find(|field| field.ident == *ident)
2510 .map(|field| &field.kind)
2511}
2512
2513fn stored_field_type(field: &Field, foreign_fields: &[ForeignField]) -> Type {
2514 let ident = field.ident.as_ref().expect("named field");
2515 match foreign_field_kind(ident, foreign_fields) {
2516 Some(ForeignFieldKind { stored_ty, .. }) => stored_ty.clone(),
2517 None => field.ty.clone(),
2518 }
2519}
2520
2521fn view_stored_type(field: &Field) -> syn::Result<Type> {
2522 if field_view_nested_attr(field)? {
2523 view_record_shape_type(&field.ty).ok_or_else(|| {
2524 Error::new_spanned(
2525 &field.ty,
2526 "#[view(nested)] fields must be a View type, Option<View>, Vec<View>, or nested Option/Vec wrappers",
2527 )
2528 })
2529 } else {
2530 Ok(field.ty.clone())
2531 }
2532}
2533
2534fn view_record_shape_type(ty: &Type) -> Option<Type> {
2535 if let Some(inner) = option_inner_type(ty) {
2536 let inner = view_record_shape_type(inner)?;
2537 return Some(syn::parse_quote!(::std::option::Option<#inner>));
2538 }
2539
2540 if let Some(inner) = vec_inner_type(ty) {
2541 let inner = view_record_shape_type(inner)?;
2542 return Some(syn::parse_quote!(::std::vec::Vec<#inner>));
2543 }
2544
2545 view_leaf_type(ty).map(|_| syn::parse_quote!(::surrealdb::types::RecordId))
2546}
2547
2548fn view_leaf_type(ty: &Type) -> Option<&TypePath> {
2549 let Type::Path(type_path) = ty else {
2550 return None;
2551 };
2552
2553 let segment = type_path.path.segments.last()?;
2554 if !matches!(segment.arguments, PathArguments::None) {
2555 return None;
2556 }
2557
2558 if is_id_type(ty)
2559 || is_record_id_type(ty)
2560 || is_autofill_type(ty)
2561 || is_string_type(ty)
2562 || is_common_non_store_leaf_type(ty)
2563 {
2564 return None;
2565 }
2566
2567 Some(type_path)
2568}
2569
2570fn foreign_stored_type(ty: &Type) -> Option<Type> {
2571 if let Some(inner) = option_inner_type(ty) {
2572 let inner = foreign_stored_type(inner)?;
2573 return Some(syn::parse_quote!(::std::option::Option<#inner>));
2574 }
2575
2576 if let Some(inner) = vec_inner_type(ty) {
2577 let inner = foreign_stored_type(inner)?;
2578 return Some(syn::parse_quote!(::std::vec::Vec<#inner>));
2579 }
2580
2581 direct_store_child_type(ty)
2582 .cloned()
2583 .map(|_| syn::parse_quote!(::surrealdb::types::RecordId))
2584}
2585
2586fn foreign_leaf_type(ty: &Type) -> Option<Type> {
2587 if let Some(inner) = option_inner_type(ty) {
2588 return foreign_leaf_type(inner);
2589 }
2590
2591 if let Some(inner) = vec_inner_type(ty) {
2592 return foreign_leaf_type(inner);
2593 }
2594
2595 direct_store_child_type(ty).cloned().map(Type::Path)
2596}
2597
2598fn invalid_foreign_leaf_type(ty: &Type) -> Option<Type> {
2599 let leaf = foreign_leaf_type(ty)?;
2600 match &leaf {
2601 Type::Path(type_path) => {
2602 let segment = type_path.path.segments.last()?;
2603 if matches!(segment.arguments, PathArguments::None) {
2604 None
2605 } else {
2606 Some(leaf)
2607 }
2608 }
2609 _ => Some(leaf),
2610 }
2611}
2612
2613fn direct_store_child_type(ty: &Type) -> Option<&TypePath> {
2614 let Type::Path(type_path) = ty else {
2615 return None;
2616 };
2617
2618 let segment = type_path.path.segments.last()?;
2619 if !matches!(segment.arguments, PathArguments::None) {
2620 return None;
2621 }
2622
2623 if is_id_type(ty)
2624 || is_autofill_type(ty)
2625 || is_string_type(ty)
2626 || is_common_non_store_leaf_type(ty)
2627 {
2628 return None;
2629 }
2630
2631 Some(type_path)
2632}
2633
2634fn is_common_non_store_leaf_type(ty: &Type) -> bool {
2635 matches!(
2636 ty,
2637 Type::Path(TypePath { path, .. })
2638 if path.is_ident("bool")
2639 || path.is_ident("u8")
2640 || path.is_ident("u16")
2641 || path.is_ident("u32")
2642 || path.is_ident("u64")
2643 || path.is_ident("u128")
2644 || path.is_ident("usize")
2645 || path.is_ident("i8")
2646 || path.is_ident("i16")
2647 || path.is_ident("i32")
2648 || path.is_ident("i64")
2649 || path.is_ident("i128")
2650 || path.is_ident("isize")
2651 || path.is_ident("f32")
2652 || path.is_ident("f64")
2653 || path.is_ident("char")
2654 )
2655}
2656
2657fn secure_field_count(fields: &syn::punctuated::Punctuated<Field, syn::token::Comma>) -> usize {
2658 fields
2659 .iter()
2660 .filter(|field| has_secure_attr(&field.attrs))
2661 .count()
2662}
2663
2664fn relation_name_override(attrs: &[Attribute]) -> syn::Result<Option<String>> {
2665 for attr in attrs {
2666 if !attr.path().is_ident("relation") {
2667 continue;
2668 }
2669
2670 let mut name = None;
2671 attr.parse_nested_meta(|meta| {
2672 if meta.path.is_ident("name") {
2673 let value = meta.value()?;
2674 let literal: syn::LitStr = value.parse()?;
2675 name = Some(literal.value());
2676 Ok(())
2677 } else {
2678 Err(meta.error("unsupported relation attribute"))
2679 }
2680 })?;
2681 return Ok(name);
2682 }
2683
2684 Ok(None)
2685}
2686
2687enum SecureKind {
2688 Shape(Type),
2689}
2690
2691impl SecureKind {
2692 fn encrypted_type(&self) -> proc_macro2::TokenStream {
2693 match self {
2694 SecureKind::Shape(ty) => quote! { <#ty as ::appdb::SensitiveShape>::Encrypted },
2695 }
2696 }
2697
2698 fn encrypt_with_context_expr(&self, ident: &syn::Ident) -> proc_macro2::TokenStream {
2699 match self {
2700 SecureKind::Shape(ty) => {
2701 quote! { <#ty as ::appdb::SensitiveShape>::encrypt_with_context(&self.#ident, context)? }
2702 }
2703 }
2704 }
2705
2706 fn decrypt_with_context_expr(&self, ident: &syn::Ident) -> proc_macro2::TokenStream {
2707 match self {
2708 SecureKind::Shape(ty) => {
2709 quote! { <#ty as ::appdb::SensitiveShape>::decrypt_with_context(&encrypted.#ident, context)? }
2710 }
2711 }
2712 }
2713
2714 fn encrypt_with_runtime_expr(
2715 &self,
2716 ident: &syn::Ident,
2717 field_tag_ident: &syn::Ident,
2718 ) -> proc_macro2::TokenStream {
2719 match self {
2720 SecureKind::Shape(ty) => {
2721 quote! {{
2722 let context = ::appdb::crypto::resolve_crypto_context_for::<#field_tag_ident>()?;
2723 <#ty as ::appdb::SensitiveShape>::encrypt_with_context(&self.#ident, context.as_ref())?
2724 }}
2725 }
2726 }
2727 }
2728
2729 fn decrypt_with_runtime_expr(
2730 &self,
2731 ident: &syn::Ident,
2732 field_tag_ident: &syn::Ident,
2733 ) -> proc_macro2::TokenStream {
2734 match self {
2735 SecureKind::Shape(ty) => {
2736 quote! {{
2737 let context = ::appdb::crypto::resolve_crypto_context_for::<#field_tag_ident>()?;
2738 <#ty as ::appdb::SensitiveShape>::decrypt_with_context(&encrypted.#ident, context.as_ref())?
2739 }}
2740 }
2741 }
2742 }
2743}
2744
2745fn secure_kind(field: &Field) -> syn::Result<SecureKind> {
2746 if secure_shape_supported(&field.ty) {
2747 return Ok(SecureKind::Shape(field.ty.clone()));
2748 }
2749
2750 Err(Error::new_spanned(
2751 &field.ty,
2752 secure_shape_error_message(&field.ty),
2753 ))
2754}
2755
2756fn secure_shape_supported(ty: &Type) -> bool {
2757 if is_string_type(ty) {
2758 return true;
2759 }
2760
2761 if sensitive_value_wrapper_inner_type(ty).is_some() {
2762 return true;
2763 }
2764
2765 if let Some(inner) = option_inner_type(ty) {
2766 return secure_shape_supported(inner);
2767 }
2768
2769 if let Some(inner) = vec_inner_type(ty) {
2770 return secure_shape_supported(inner);
2771 }
2772
2773 direct_sensitive_child_type(ty).is_some()
2774}
2775
2776fn secure_shape_error_message(ty: &Type) -> &'static str {
2777 if invalid_secure_leaf_type(ty).is_some() {
2778 "#[secure] child shapes require a direct named Sensitive type leaf with only Option<_> and Vec<_> wrappers"
2779 } else {
2780 "#[secure] supports String, appdb::SensitiveValueOf<T>, and recursive Child / Option<Child> / Vec<Child> shapes where Child implements appdb::Sensitive"
2781 }
2782}
2783
2784fn direct_sensitive_child_type(ty: &Type) -> Option<&TypePath> {
2785 let Type::Path(type_path) = ty else {
2786 return None;
2787 };
2788
2789 let segment = type_path.path.segments.last()?;
2790 if !matches!(segment.arguments, PathArguments::None) {
2791 return None;
2792 }
2793
2794 if is_id_type(ty) || is_string_type(ty) || is_common_non_store_leaf_type(ty) {
2795 return None;
2796 }
2797
2798 Some(type_path)
2799}
2800
2801fn invalid_secure_leaf_type(ty: &Type) -> Option<Type> {
2802 if let Some(inner) = option_inner_type(ty) {
2803 return invalid_secure_leaf_type(inner);
2804 }
2805
2806 if let Some(inner) = vec_inner_type(ty) {
2807 return invalid_secure_leaf_type(inner);
2808 }
2809
2810 let leaf = direct_sensitive_child_type(ty)?.clone();
2811 let segment = leaf.path.segments.last()?;
2812 if matches!(segment.arguments, PathArguments::None) {
2813 None
2814 } else {
2815 Some(Type::Path(leaf))
2816 }
2817}
2818
2819fn is_string_type(ty: &Type) -> bool {
2820 match ty {
2821 Type::Path(TypePath { path, .. }) => path.is_ident("String"),
2822 _ => false,
2823 }
2824}
2825
2826fn is_id_type(ty: &Type) -> bool {
2827 match ty {
2828 Type::Path(TypePath { path, .. }) => path.segments.last().is_some_and(|segment| {
2829 let ident = segment.ident.to_string();
2830 ident == "Id"
2831 }),
2832 _ => false,
2833 }
2834}
2835
2836fn is_autofill_type(ty: &Type) -> bool {
2837 match ty {
2838 Type::Path(TypePath { path, .. }) => path
2839 .segments
2840 .last()
2841 .is_some_and(|segment| segment.ident == "AutoFill"),
2842 _ => false,
2843 }
2844}
2845
2846fn is_record_id_type(ty: &Type) -> bool {
2847 match ty {
2848 Type::Path(TypePath { path, .. }) => path.segments.last().is_some_and(|segment| {
2849 let ident = segment.ident.to_string();
2850 ident == "RecordId"
2851 }),
2852 _ => false,
2853 }
2854}
2855
2856fn option_inner_type(ty: &Type) -> Option<&Type> {
2857 let Type::Path(TypePath { path, .. }) = ty else {
2858 return None;
2859 };
2860 let segment = path.segments.last()?;
2861 if segment.ident != "Option" {
2862 return None;
2863 }
2864 let PathArguments::AngleBracketed(args) = &segment.arguments else {
2865 return None;
2866 };
2867 let GenericArgument::Type(inner) = args.args.first()? else {
2868 return None;
2869 };
2870 Some(inner)
2871}
2872
2873fn vec_inner_type(ty: &Type) -> Option<&Type> {
2874 let Type::Path(TypePath { path, .. }) = ty else {
2875 return None;
2876 };
2877 let segment = path.segments.last()?;
2878 if segment.ident != "Vec" {
2879 return None;
2880 }
2881 let PathArguments::AngleBracketed(args) = &segment.arguments else {
2882 return None;
2883 };
2884 let GenericArgument::Type(inner) = args.args.first()? else {
2885 return None;
2886 };
2887 Some(inner)
2888}
2889
2890fn sensitive_value_wrapper_inner_type(ty: &Type) -> Option<&Type> {
2891 let Type::Path(TypePath { path, .. }) = ty else {
2892 return None;
2893 };
2894 let segment = path.segments.last()?;
2895 if segment.ident != "SensitiveValueOf" {
2896 return None;
2897 }
2898 let PathArguments::AngleBracketed(args) = &segment.arguments else {
2899 return None;
2900 };
2901 let GenericArgument::Type(inner) = args.args.first()? else {
2902 return None;
2903 };
2904 Some(inner)
2905}
2906
2907fn to_snake_case(input: &str) -> String {
2908 let mut out = String::with_capacity(input.len() + 4);
2909 let mut prev_is_lower_or_digit = false;
2910
2911 for ch in input.chars() {
2912 if ch.is_ascii_uppercase() {
2913 if prev_is_lower_or_digit {
2914 out.push('_');
2915 }
2916 out.push(ch.to_ascii_lowercase());
2917 prev_is_lower_or_digit = false;
2918 } else {
2919 out.push(ch);
2920 prev_is_lower_or_digit = ch.is_ascii_lowercase() || ch.is_ascii_digit();
2921 }
2922 }
2923
2924 out
2925}
2926
2927fn to_pascal_case(input: &str) -> String {
2928 let mut out = String::with_capacity(input.len());
2929 let mut uppercase_next = true;
2930
2931 for ch in input.chars() {
2932 if ch == '_' || ch == '-' {
2933 uppercase_next = true;
2934 continue;
2935 }
2936
2937 if uppercase_next {
2938 out.push(ch.to_ascii_uppercase());
2939 uppercase_next = false;
2940 } else {
2941 out.push(ch);
2942 }
2943 }
2944
2945 out
2946}