Skip to main content

appdb_macros/
lib.rs

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}