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