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