1use proc_macro::TokenStream;
2use quote::{format_ident, quote};
3use syn::{
4 parse_macro_input, Attribute, Data, DeriveInput, Error, Field, Fields, GenericArgument,
5 PathArguments, Type, TypePath,
6};
7
8#[proc_macro_derive(Sensitive, attributes(secure))]
9pub fn derive_sensitive(input: TokenStream) -> TokenStream {
10 match derive_sensitive_impl(parse_macro_input!(input as DeriveInput)) {
11 Ok(tokens) => tokens.into(),
12 Err(err) => err.to_compile_error().into(),
13 }
14}
15
16#[proc_macro_derive(Store, attributes(unique, secure))]
17pub fn derive_store(input: TokenStream) -> TokenStream {
18 match derive_store_impl(parse_macro_input!(input as DeriveInput)) {
19 Ok(tokens) => tokens.into(),
20 Err(err) => err.to_compile_error().into(),
21 }
22}
23
24#[proc_macro_derive(Relation, attributes(relation))]
25pub fn derive_relation(input: TokenStream) -> TokenStream {
26 match derive_relation_impl(parse_macro_input!(input as DeriveInput)) {
27 Ok(tokens) => tokens.into(),
28 Err(err) => err.to_compile_error().into(),
29 }
30}
31
32fn derive_store_impl(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
33 let struct_ident = input.ident;
34
35 let named_fields = match input.data {
36 Data::Struct(data) => match data.fields {
37 Fields::Named(fields) => fields.named,
38 _ => {
39 return Err(Error::new_spanned(
40 struct_ident,
41 "Store can only be derived for structs with named fields",
42 ))
43 }
44 },
45 _ => {
46 return Err(Error::new_spanned(
47 struct_ident,
48 "Store can only be derived for structs",
49 ))
50 }
51 };
52
53 let id_fields = named_fields
54 .iter()
55 .filter(|field| is_id_type(&field.ty))
56 .map(|field| field.ident.clone().expect("named field"))
57 .collect::<Vec<_>>();
58
59 let secure_fields = named_fields
60 .iter()
61 .filter(|field| has_secure_attr(&field.attrs))
62 .map(|field| field.ident.clone().expect("named field"))
63 .collect::<Vec<_>>();
64
65 let unique_fields = named_fields
66 .iter()
67 .filter(|field| has_unique_attr(&field.attrs))
68 .map(|field| field.ident.clone().expect("named field"))
69 .collect::<Vec<_>>();
70
71 if id_fields.len() > 1 {
72 return Err(Error::new_spanned(
73 struct_ident,
74 "Store supports at most one `Id` field for automatic HasId generation",
75 ));
76 }
77
78 if let Some(invalid_field) = named_fields
79 .iter()
80 .find(|field| has_secure_attr(&field.attrs) && has_unique_attr(&field.attrs))
81 {
82 let ident = invalid_field.ident.as_ref().expect("named field");
83 return Err(Error::new_spanned(
84 ident,
85 "#[secure] fields cannot be used as #[unique] lookup keys",
86 ));
87 }
88
89 let auto_has_id_impl = id_fields.first().map(|field| {
90 quote! {
91 impl ::appdb::model::meta::HasId for #struct_ident {
92 fn id(&self) -> ::surrealdb::types::RecordId {
93 ::surrealdb::types::RecordId::new(
94 <Self as ::appdb::model::meta::ModelMeta>::table_name(),
95 self.#field.clone(),
96 )
97 }
98 }
99 }
100 });
101
102 let resolve_record_id_impl = if let Some(field) = id_fields.first() {
103 quote! {
104 #[::async_trait::async_trait]
105 impl ::appdb::model::meta::ResolveRecordId for #struct_ident {
106 async fn resolve_record_id(&self) -> ::anyhow::Result<::surrealdb::types::RecordId> {
107 Ok(::surrealdb::types::RecordId::new(
108 <Self as ::appdb::model::meta::ModelMeta>::table_name(),
109 self.#field.clone(),
110 ))
111 }
112 }
113 }
114 } else {
115 quote! {
116 #[::async_trait::async_trait]
117 impl ::appdb::model::meta::ResolveRecordId for #struct_ident {
118 async fn resolve_record_id(&self) -> ::anyhow::Result<::surrealdb::types::RecordId> {
119 ::appdb::repository::Repo::<Self>::find_unique_id_for(self).await
120 }
121 }
122 }
123 };
124
125 let unique_schema_impls = unique_fields.iter().map(|field| {
126 let field_name = field.to_string();
127 let index_name = format!(
128 "{}_{}_unique",
129 to_snake_case(&struct_ident.to_string()),
130 field_name
131 );
132 let ddl = format!(
133 "DEFINE INDEX IF NOT EXISTS {index_name} ON {} FIELDS {field_name} UNIQUE;",
134 to_snake_case(&struct_ident.to_string())
135 );
136
137 quote! {
138 ::inventory::submit! {
139 ::appdb::model::schema::SchemaItem {
140 ddl: #ddl,
141 }
142 }
143 }
144 });
145
146 let lookup_fields = if unique_fields.is_empty() {
147 named_fields
148 .iter()
149 .filter_map(|field| {
150 let ident = field.ident.as_ref()?;
151 if ident == "id" || secure_fields.iter().any(|secure| secure == ident) {
152 None
153 } else {
154 Some(ident.to_string())
155 }
156 })
157 .collect::<Vec<_>>()
158 } else {
159 unique_fields
160 .iter()
161 .map(|field| field.to_string())
162 .collect::<Vec<_>>()
163 };
164
165 if id_fields.is_empty() && lookup_fields.is_empty() {
166 return Err(Error::new_spanned(
167 struct_ident,
168 "Store requires an `Id` field or at least one non-secure lookup field for automatic record resolution",
169 ));
170 }
171 let lookup_field_literals = lookup_fields.iter().map(|field| quote! { #field });
172
173 let stored_model_impl = if secure_field_count(&named_fields) > 0 {
174 quote! {
175 impl ::appdb::StoredModel for #struct_ident {
176 type Stored = <Self as ::appdb::Sensitive>::Encrypted;
177
178 fn into_stored(self) -> ::anyhow::Result<Self::Stored> {
179 <Self as ::appdb::Sensitive>::encrypt_with_runtime_resolver(&self)
180 .map_err(::anyhow::Error::from)
181 }
182
183 fn from_stored(stored: Self::Stored) -> ::anyhow::Result<Self> {
184 <Self as ::appdb::Sensitive>::decrypt_with_runtime_resolver(&stored)
185 .map_err(::anyhow::Error::from)
186 }
187
188 fn supports_create_return_id() -> bool {
189 false
190 }
191 }
192 }
193 } else {
194 quote! {
195 impl ::appdb::StoredModel for #struct_ident {
196 type Stored = Self;
197
198 fn into_stored(self) -> ::anyhow::Result<Self::Stored> {
199 ::std::result::Result::Ok(self)
200 }
201
202 fn from_stored(stored: Self::Stored) -> ::anyhow::Result<Self> {
203 ::std::result::Result::Ok(stored)
204 }
205 }
206 }
207 };
208
209 Ok(quote! {
210 impl ::appdb::model::meta::ModelMeta for #struct_ident {
211 fn table_name() -> &'static str {
212 static TABLE_NAME: ::std::sync::OnceLock<&'static str> = ::std::sync::OnceLock::new();
213 TABLE_NAME.get_or_init(|| {
214 let table = ::appdb::model::meta::default_table_name(stringify!(#struct_ident));
215 ::appdb::model::meta::register_table(stringify!(#struct_ident), table)
216 })
217 }
218 }
219
220 impl ::appdb::model::meta::UniqueLookupMeta for #struct_ident {
221 fn lookup_fields() -> &'static [&'static str] {
222 &[ #( #lookup_field_literals ),* ]
223 }
224 }
225
226 #stored_model_impl
227
228 #auto_has_id_impl
229 #resolve_record_id_impl
230
231 #( #unique_schema_impls )*
232
233 impl ::appdb::repository::Crud for #struct_ident {}
234
235 impl #struct_ident {
236 pub async fn get<T>(id: T) -> ::anyhow::Result<Self>
237 where
238 ::surrealdb::types::RecordIdKey: From<T>,
239 T: Send,
240 {
241 ::appdb::repository::Repo::<Self>::get(id).await
242 }
243
244 pub async fn list() -> ::anyhow::Result<::std::vec::Vec<Self>> {
245 ::appdb::repository::Repo::<Self>::list().await
246 }
247
248 pub async fn list_limit(count: i64) -> ::anyhow::Result<::std::vec::Vec<Self>> {
249 ::appdb::repository::Repo::<Self>::list_limit(count).await
250 }
251
252 pub async fn delete_all() -> ::anyhow::Result<()> {
253 ::appdb::repository::Repo::<Self>::delete_all().await
254 }
255
256 pub async fn find_one_id(
257 k: &str,
258 v: &str,
259 ) -> ::anyhow::Result<::surrealdb::types::RecordId> {
260 ::appdb::repository::Repo::<Self>::find_one_id(k, v).await
261 }
262
263 pub async fn list_record_ids() -> ::anyhow::Result<::std::vec::Vec<::surrealdb::types::RecordId>> {
264 ::appdb::repository::Repo::<Self>::list_record_ids().await
265 }
266
267 pub async fn create_at(
268 id: ::surrealdb::types::RecordId,
269 data: Self,
270 ) -> ::anyhow::Result<Self> {
271 ::appdb::repository::Repo::<Self>::create_at(id, data).await
272 }
273
274 pub async fn upsert_at(
275 id: ::surrealdb::types::RecordId,
276 data: Self,
277 ) -> ::anyhow::Result<Self> {
278 ::appdb::repository::Repo::<Self>::upsert_at(id, data).await
279 }
280
281 pub async fn update_at(
282 self,
283 id: ::surrealdb::types::RecordId,
284 ) -> ::anyhow::Result<Self> {
285 ::appdb::repository::Repo::<Self>::update_at(id, self).await
286 }
287
288 pub async fn delete<T>(id: T) -> ::anyhow::Result<()>
289 where
290 ::surrealdb::types::RecordIdKey: From<T>,
291 T: Send,
292 {
293 ::appdb::repository::Repo::<Self>::delete(id).await
294 }
295 }
296 })
297}
298
299fn derive_relation_impl(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
300 let struct_ident = input.ident;
301 let relation_name = relation_name_override(&input.attrs)?
302 .unwrap_or_else(|| to_snake_case(&struct_ident.to_string()));
303
304 match input.data {
305 Data::Struct(data) => {
306 match data.fields {
307 Fields::Unit | Fields::Named(_) => {}
308 _ => return Err(Error::new_spanned(
309 struct_ident,
310 "Relation can only be derived for unit structs or structs with named fields",
311 )),
312 }
313 }
314 _ => {
315 return Err(Error::new_spanned(
316 struct_ident,
317 "Relation can only be derived for structs",
318 ))
319 }
320 }
321
322 Ok(quote! {
323 impl ::appdb::model::relation::RelationMeta for #struct_ident {
324 fn relation_name() -> &'static str {
325 static REL_NAME: ::std::sync::OnceLock<&'static str> = ::std::sync::OnceLock::new();
326 REL_NAME.get_or_init(|| ::appdb::model::relation::register_relation(#relation_name))
327 }
328 }
329
330 impl #struct_ident {
331 pub async fn relate<A, B>(a: &A, b: &B) -> ::anyhow::Result<()>
332 where
333 A: ::appdb::model::meta::ResolveRecordId + Send + Sync,
334 B: ::appdb::model::meta::ResolveRecordId + Send + Sync,
335 {
336 ::appdb::graph::relate_at(a.resolve_record_id().await?, b.resolve_record_id().await?, <Self as ::appdb::model::relation::RelationMeta>::relation_name()).await
337 }
338
339 pub async fn unrelate<A, B>(a: &A, b: &B) -> ::anyhow::Result<()>
340 where
341 A: ::appdb::model::meta::ResolveRecordId + Send + Sync,
342 B: ::appdb::model::meta::ResolveRecordId + Send + Sync,
343 {
344 ::appdb::graph::unrelate_at(a.resolve_record_id().await?, b.resolve_record_id().await?, <Self as ::appdb::model::relation::RelationMeta>::relation_name()).await
345 }
346
347 pub async fn out_ids<A>(a: &A, out_table: &str) -> ::anyhow::Result<::std::vec::Vec<::surrealdb::types::RecordId>>
348 where
349 A: ::appdb::model::meta::ResolveRecordId + Send + Sync,
350 {
351 ::appdb::graph::out_ids(a.resolve_record_id().await?, <Self as ::appdb::model::relation::RelationMeta>::relation_name(), out_table).await
352 }
353
354 pub async fn in_ids<B>(b: &B, in_table: &str) -> ::anyhow::Result<::std::vec::Vec<::surrealdb::types::RecordId>>
355 where
356 B: ::appdb::model::meta::ResolveRecordId + Send + Sync,
357 {
358 ::appdb::graph::in_ids(b.resolve_record_id().await?, <Self as ::appdb::model::relation::RelationMeta>::relation_name(), in_table).await
359 }
360 }
361 })
362}
363
364fn derive_sensitive_impl(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
365 let struct_ident = input.ident;
366 let encrypted_ident = format_ident!("Encrypted{}", struct_ident);
367 let vis = input.vis;
368 let named_fields = match input.data {
369 Data::Struct(data) => match data.fields {
370 Fields::Named(fields) => fields.named,
371 _ => {
372 return Err(Error::new_spanned(
373 struct_ident,
374 "Sensitive can only be derived for structs with named fields",
375 ))
376 }
377 },
378 _ => {
379 return Err(Error::new_spanned(
380 struct_ident,
381 "Sensitive can only be derived for structs",
382 ))
383 }
384 };
385
386 let mut secure_field_count = 0usize;
387 let mut encrypted_fields = Vec::new();
388 let mut encrypt_assignments = Vec::new();
389 let mut decrypt_assignments = Vec::new();
390 let mut runtime_encrypt_assignments = Vec::new();
391 let mut runtime_decrypt_assignments = Vec::new();
392 let mut field_tag_structs = Vec::new();
393
394 for field in named_fields.iter() {
395 let ident = field.ident.clone().expect("named field");
396 let field_vis = field.vis.clone();
397 let secure = has_secure_attr(&field.attrs);
398
399 if secure {
400 secure_field_count += 1;
401 let secure_kind = secure_kind(field)?;
402 let encrypted_ty = secure_kind.encrypted_type();
403 let field_tag_ident = format_ident!(
404 "AppdbSensitiveFieldTag{}{}",
405 struct_ident,
406 to_pascal_case(&ident.to_string())
407 );
408 let field_tag_literal = ident.to_string();
409 let encrypt_expr = secure_kind.encrypt_with_context_expr(&ident);
410 let decrypt_expr = secure_kind.decrypt_with_context_expr(&ident);
411 let runtime_encrypt_expr =
412 secure_kind.encrypt_with_runtime_expr(&ident, &field_tag_ident);
413 let runtime_decrypt_expr =
414 secure_kind.decrypt_with_runtime_expr(&ident, &field_tag_ident);
415 encrypted_fields.push(quote! { #field_vis #ident: #encrypted_ty });
416 encrypt_assignments.push(quote! { #ident: #encrypt_expr });
417 decrypt_assignments.push(quote! { #ident: #decrypt_expr });
418 runtime_encrypt_assignments.push(quote! { #ident: #runtime_encrypt_expr });
419 runtime_decrypt_assignments.push(quote! { #ident: #runtime_decrypt_expr });
420 field_tag_structs.push(quote! {
421 #[doc(hidden)]
422 #vis struct #field_tag_ident;
423
424 impl ::appdb::crypto::SensitiveFieldTag for #field_tag_ident {
425 fn model_tag() -> &'static str {
426 <#struct_ident as ::appdb::crypto::SensitiveModelTag>::model_tag()
427 }
428
429 fn field_tag() -> &'static str {
430 #field_tag_literal
431 }
432 }
433 });
434 } else {
435 let ty = field.ty.clone();
436 encrypted_fields.push(quote! { #field_vis #ident: #ty });
437 encrypt_assignments.push(quote! { #ident: self.#ident.clone() });
438 decrypt_assignments.push(quote! { #ident: encrypted.#ident.clone() });
439 runtime_encrypt_assignments.push(quote! { #ident: self.#ident.clone() });
440 runtime_decrypt_assignments.push(quote! { #ident: encrypted.#ident.clone() });
441 }
442 }
443
444 if secure_field_count == 0 {
445 return Err(Error::new_spanned(
446 struct_ident,
447 "Sensitive requires at least one #[secure] field",
448 ));
449 }
450
451 Ok(quote! {
452 #[derive(
453 Debug,
454 Clone,
455 ::serde::Serialize,
456 ::serde::Deserialize,
457 ::surrealdb::types::SurrealValue,
458 )]
459 #vis struct #encrypted_ident {
460 #( #encrypted_fields, )*
461 }
462
463 impl ::appdb::crypto::SensitiveModelTag for #struct_ident {
464 fn model_tag() -> &'static str {
465 ::std::concat!(::std::module_path!(), "::", ::std::stringify!(#struct_ident))
466 }
467 }
468
469 #( #field_tag_structs )*
470
471 impl ::appdb::Sensitive for #struct_ident {
472 type Encrypted = #encrypted_ident;
473
474 fn encrypt(
475 &self,
476 context: &::appdb::crypto::CryptoContext,
477 ) -> ::std::result::Result<Self::Encrypted, ::appdb::crypto::CryptoError> {
478 ::std::result::Result::Ok(#encrypted_ident {
479 #( #encrypt_assignments, )*
480 })
481 }
482
483 fn decrypt(
484 encrypted: &Self::Encrypted,
485 context: &::appdb::crypto::CryptoContext,
486 ) -> ::std::result::Result<Self, ::appdb::crypto::CryptoError> {
487 ::std::result::Result::Ok(Self {
488 #( #decrypt_assignments, )*
489 })
490 }
491
492 fn encrypt_with_runtime_resolver(
493 &self,
494 ) -> ::std::result::Result<Self::Encrypted, ::appdb::crypto::CryptoError> {
495 ::std::result::Result::Ok(#encrypted_ident {
496 #( #runtime_encrypt_assignments, )*
497 })
498 }
499
500 fn decrypt_with_runtime_resolver(
501 encrypted: &Self::Encrypted,
502 ) -> ::std::result::Result<Self, ::appdb::crypto::CryptoError> {
503 ::std::result::Result::Ok(Self {
504 #( #runtime_decrypt_assignments, )*
505 })
506 }
507 }
508
509 impl #struct_ident {
510 pub fn encrypt(
511 &self,
512 context: &::appdb::crypto::CryptoContext,
513 ) -> ::std::result::Result<#encrypted_ident, ::appdb::crypto::CryptoError> {
514 <Self as ::appdb::Sensitive>::encrypt(self, context)
515 }
516 }
517
518 impl #encrypted_ident {
519 pub fn decrypt(
520 &self,
521 context: &::appdb::crypto::CryptoContext,
522 ) -> ::std::result::Result<#struct_ident, ::appdb::crypto::CryptoError> {
523 <#struct_ident as ::appdb::Sensitive>::decrypt(self, context)
524 }
525 }
526 })
527}
528
529fn has_secure_attr(attrs: &[Attribute]) -> bool {
530 attrs.iter().any(|attr| attr.path().is_ident("secure"))
531}
532
533fn has_unique_attr(attrs: &[Attribute]) -> bool {
534 attrs.iter().any(|attr| attr.path().is_ident("unique"))
535}
536
537fn secure_field_count(fields: &syn::punctuated::Punctuated<Field, syn::token::Comma>) -> usize {
538 fields
539 .iter()
540 .filter(|field| has_secure_attr(&field.attrs))
541 .count()
542}
543
544fn relation_name_override(attrs: &[Attribute]) -> syn::Result<Option<String>> {
545 for attr in attrs {
546 if !attr.path().is_ident("relation") {
547 continue;
548 }
549
550 let mut name = None;
551 attr.parse_nested_meta(|meta| {
552 if meta.path.is_ident("name") {
553 let value = meta.value()?;
554 let literal: syn::LitStr = value.parse()?;
555 name = Some(literal.value());
556 Ok(())
557 } else {
558 Err(meta.error("unsupported relation attribute"))
559 }
560 })?;
561 return Ok(name);
562 }
563
564 Ok(None)
565}
566
567enum SecureKind {
568 String,
569 OptionString,
570}
571
572impl SecureKind {
573 fn encrypted_type(&self) -> proc_macro2::TokenStream {
574 match self {
575 SecureKind::String => quote! { ::std::vec::Vec<u8> },
576 SecureKind::OptionString => quote! { ::std::option::Option<::std::vec::Vec<u8>> },
577 }
578 }
579
580 fn encrypt_with_context_expr(&self, ident: &syn::Ident) -> proc_macro2::TokenStream {
581 match self {
582 SecureKind::String => {
583 quote! { ::appdb::crypto::encrypt_string(&self.#ident, context)? }
584 }
585 SecureKind::OptionString => {
586 quote! { ::appdb::crypto::encrypt_optional_string(&self.#ident, context)? }
587 }
588 }
589 }
590
591 fn decrypt_with_context_expr(&self, ident: &syn::Ident) -> proc_macro2::TokenStream {
592 match self {
593 SecureKind::String => {
594 quote! { ::appdb::crypto::decrypt_string(&encrypted.#ident, context)? }
595 }
596 SecureKind::OptionString => {
597 quote! { ::appdb::crypto::decrypt_optional_string(&encrypted.#ident, context)? }
598 }
599 }
600 }
601
602 fn encrypt_with_runtime_expr(
603 &self,
604 ident: &syn::Ident,
605 field_tag_ident: &syn::Ident,
606 ) -> proc_macro2::TokenStream {
607 match self {
608 SecureKind::String => {
609 quote! {{
610 let context = ::appdb::crypto::resolve_crypto_context_for::<#field_tag_ident>()?;
611 ::appdb::crypto::encrypt_string(&self.#ident, context.as_ref())?
612 }}
613 }
614 SecureKind::OptionString => {
615 quote! {{
616 let context = ::appdb::crypto::resolve_crypto_context_for::<#field_tag_ident>()?;
617 ::appdb::crypto::encrypt_optional_string(&self.#ident, context.as_ref())?
618 }}
619 }
620 }
621 }
622
623 fn decrypt_with_runtime_expr(
624 &self,
625 ident: &syn::Ident,
626 field_tag_ident: &syn::Ident,
627 ) -> proc_macro2::TokenStream {
628 match self {
629 SecureKind::String => {
630 quote! {{
631 let context = ::appdb::crypto::resolve_crypto_context_for::<#field_tag_ident>()?;
632 ::appdb::crypto::decrypt_string(&encrypted.#ident, context.as_ref())?
633 }}
634 }
635 SecureKind::OptionString => {
636 quote! {{
637 let context = ::appdb::crypto::resolve_crypto_context_for::<#field_tag_ident>()?;
638 ::appdb::crypto::decrypt_optional_string(&encrypted.#ident, context.as_ref())?
639 }}
640 }
641 }
642 }
643}
644
645fn secure_kind(field: &Field) -> syn::Result<SecureKind> {
646 if is_string_type(&field.ty) {
647 return Ok(SecureKind::String);
648 }
649
650 if let Some(inner) = option_inner_type(&field.ty) {
651 if is_string_type(inner) {
652 return Ok(SecureKind::OptionString);
653 }
654 }
655
656 Err(Error::new_spanned(
657 &field.ty,
658 "#[secure] currently supports only String and Option<String>",
659 ))
660}
661
662fn is_string_type(ty: &Type) -> bool {
663 match ty {
664 Type::Path(TypePath { path, .. }) => path.is_ident("String"),
665 _ => false,
666 }
667}
668
669fn is_id_type(ty: &Type) -> bool {
670 match ty {
671 Type::Path(TypePath { path, .. }) => path.segments.last().is_some_and(|segment| {
672 let ident = segment.ident.to_string();
673 ident == "Id"
674 }),
675 _ => false,
676 }
677}
678
679fn option_inner_type(ty: &Type) -> Option<&Type> {
680 let Type::Path(TypePath { path, .. }) = ty else {
681 return None;
682 };
683 let segment = path.segments.last()?;
684 if segment.ident != "Option" {
685 return None;
686 }
687 let PathArguments::AngleBracketed(args) = &segment.arguments else {
688 return None;
689 };
690 let GenericArgument::Type(inner) = args.args.first()? else {
691 return None;
692 };
693 Some(inner)
694}
695
696fn to_snake_case(input: &str) -> String {
697 let mut out = String::with_capacity(input.len() + 4);
698 let mut prev_is_lower_or_digit = false;
699
700 for ch in input.chars() {
701 if ch.is_ascii_uppercase() {
702 if prev_is_lower_or_digit {
703 out.push('_');
704 }
705 out.push(ch.to_ascii_lowercase());
706 prev_is_lower_or_digit = false;
707 } else {
708 out.push(ch);
709 prev_is_lower_or_digit = ch.is_ascii_lowercase() || ch.is_ascii_digit();
710 }
711 }
712
713 out
714}
715
716fn to_pascal_case(input: &str) -> String {
717 let mut out = String::with_capacity(input.len());
718 let mut uppercase_next = true;
719
720 for ch in input.chars() {
721 if ch == '_' || ch == '-' {
722 uppercase_next = true;
723 continue;
724 }
725
726 if uppercase_next {
727 out.push(ch.to_ascii_uppercase());
728 uppercase_next = false;
729 } else {
730 out.push(ch);
731 }
732 }
733
734 out
735}