stratosphere_core/
token.rs

1use crate::resource_specification::*;
2use quote::quote;
3
4pub struct Target<'a> {
5    tag_definition: TagDefinition<'a>,
6    vendor_map: VendorMap<'a>,
7}
8
9impl<'a> Target<'a> {
10    pub fn for_services(
11        specification: &'a ResourceSpecification<'a>,
12        selected_services: std::collections::BTreeSet<ServiceIdentifier<'a>>,
13    ) -> Self {
14        fn mk_service_definition<'a>(
15            resource_type_name: &'a ResourceTypeName,
16            resource_type: &'a ResourceType,
17        ) -> ServiceDefinition<'a> {
18            ServiceDefinition {
19                resource_type_map: [(resource_type_name, resource_type)].into(),
20                resource_property_type_map: [].into(),
21            }
22        }
23        let mut vendor_map = VendorMap::new();
24
25        for (resource_type_name, resource_type) in &specification.resource_types {
26            let target_service = &resource_type_name.service;
27
28            if !selected_services.contains(target_service) {
29                continue;
30            }
31
32            let vendor_name = &target_service.vendor_name;
33            let service_name = &target_service.service_name;
34
35            vendor_map
36                .entry(vendor_name)
37                .and_modify(|service_map: &mut ServiceMap| {
38                    service_map
39                        .entry(service_name)
40                        .and_modify(|service_definition| {
41                            service_definition
42                                .resource_type_map
43                                .insert(resource_type_name, resource_type);
44                        })
45                        .or_insert_with(|| {
46                            mk_service_definition(resource_type_name, resource_type)
47                        });
48                })
49                .or_insert_with(|| {
50                    [(
51                        service_name,
52                        mk_service_definition(resource_type_name, resource_type),
53                    )]
54                    .into()
55                });
56        }
57
58        let mut tag_definition: Option<TagDefinition> = None;
59
60        for (property_type_name, property_type) in &specification.property_types {
61            match property_type_name {
62                PropertyTypeName::Tag => {
63                    tag_definition = Some(TagDefinition(property_type));
64                }
65                PropertyTypeName::PropertyTypeName(resource_property_type_name) => {
66                    let vendor_name = &resource_property_type_name.vendor_name;
67                    let service_name = &resource_property_type_name.service_name;
68
69                    let service_identifier = ServiceIdentifier {
70                        vendor_name: vendor_name.clone(),
71                        service_name: service_name.clone(),
72                    };
73
74                    if !selected_services.contains(&service_identifier) {
75                        continue;
76                    }
77
78                    vendor_map
79                        .entry(&resource_property_type_name.vendor_name)
80                        .and_modify(|service_map: &mut ServiceMap| {
81                            service_map
82                                .entry(service_name)
83                                .and_modify(|service_definition| {
84                                    service_definition.add_resource_property_type(
85                                        resource_property_type_name,
86                                        property_type,
87                                    )
88                                });
89                        })
90                        .or_insert_with(|| panic!("Property without resource"));
91                }
92            }
93        }
94
95        Self {
96            tag_definition: tag_definition.expect("Tag property missing"),
97            vendor_map,
98        }
99    }
100}
101
102pub struct ServiceDefinition<'a> {
103    pub resource_type_map: ResourceTypeMap<'a>,
104    pub resource_property_type_map: ResourcePropertyTypeMap<'a>,
105}
106
107impl<'a> ServiceDefinition<'a> {
108    pub fn add_resource_property_type(
109        &mut self,
110        resource_property_type_name: &'a ResourcePropertyTypeName<'a>,
111        property_type: &'a PropertyType<'a>,
112    ) {
113        self.resource_property_type_map
114            .entry(&resource_property_type_name.resource_name)
115            .and_modify(|property_type_map| {
116                property_type_map.insert(&resource_property_type_name.property_name, property_type);
117            })
118            .or_insert([(&resource_property_type_name.property_name, property_type)].into());
119    }
120}
121
122pub struct TagDefinition<'a>(pub &'a PropertyType<'a>);
123
124impl quote::ToTokens for TagDefinition<'_> {
125    fn to_tokens(&self, stream: &mut proc_macro2::TokenStream) {
126        let property_name = PropertyName("Tag");
127
128        stream.extend(property_type_token_stream(
129            &PropertyTypeName::Tag,
130            &property_name,
131            self.0,
132        ))
133    }
134}
135
136pub type ServiceMap<'a> = std::collections::BTreeMap<&'a ServiceName<'a>, ServiceDefinition<'a>>;
137
138type ResourceTypeMap<'a> =
139    std::collections::BTreeMap<&'a ResourceTypeName<'a>, &'a ResourceType<'a>>;
140
141type ResourcePropertyTypeMap<'a> =
142    std::collections::BTreeMap<&'a ResourceName<'a>, PropertyTypeMap<'a>>;
143
144type PropertyTypeMap<'a> = std::collections::BTreeMap<&'a PropertyName<'a>, &'a PropertyType<'a>>;
145
146pub type VendorMap<'a> = std::collections::BTreeMap<&'a VendorName<'a>, ServiceMap<'a>>;
147
148pub fn token_stream(target: Target) -> proc_macro2::TokenStream {
149    let mut stream = proc_macro2::TokenStream::new();
150
151    for (vendor_name, service_map) in target.vendor_map {
152        let vendor_module_name = VendorModuleName::new(vendor_name);
153
154        let services = service_map_token_stream(vendor_name, service_map);
155
156        let module_stream: proc_macro2::TokenStream = quote! {
157            pub mod #vendor_module_name {
158                #services
159            }
160        };
161
162        stream.extend(module_stream);
163    }
164
165    let tag_definition = target.tag_definition;
166
167    quote! {
168        pub mod cloudformation {
169            #tag_definition
170            #stream
171        }
172    }
173}
174
175fn service_map_token_stream(
176    vendor_name: &VendorName<'_>,
177    service_map: ServiceMap,
178) -> proc_macro2::TokenStream {
179    let mut stream = proc_macro2::TokenStream::new();
180
181    for (service_name, service_definition) in service_map {
182        let service_module_name = ServiceModuleName::new(service_name);
183
184        let properties = resource_property_type_map_token_stream(
185            &ServiceIdentifier {
186                vendor_name: vendor_name.clone(),
187                service_name: service_name.clone(),
188            },
189            service_definition.resource_property_type_map,
190        );
191        let resources = resource_type_map_token_stream(
192            &service_module_name,
193            service_definition.resource_type_map,
194        );
195
196        let service_stream: proc_macro2::TokenStream = quote! {
197            pub mod #service_module_name {
198                #properties
199                #resources
200            }
201        };
202
203        stream.extend(service_stream);
204    }
205    stream
206}
207
208fn resource_property_type_map_token_stream(
209    service: &ServiceIdentifier<'_>,
210    resource_property_type_map: ResourcePropertyTypeMap,
211) -> proc_macro2::TokenStream {
212    let mut stream = proc_macro2::TokenStream::new();
213
214    for (resource_name, property_type_map) in resource_property_type_map {
215        stream.extend(property_type_map_token_stream(
216            service,
217            resource_name,
218            property_type_map,
219        ))
220    }
221
222    stream
223}
224
225fn property_type_map_token_stream(
226    service: &ServiceIdentifier<'_>,
227    resource_name: &ResourceName<'_>,
228    property_type_map: PropertyTypeMap<'_>,
229) -> proc_macro2::TokenStream {
230    let resource_module_name = ResourceModuleName::new(resource_name);
231
232    let property_types: Vec<_> = property_type_map
233        .into_iter()
234        .map(|(property_name, property_type)| {
235            property_type_token_stream(
236                &PropertyTypeName::PropertyTypeName(ResourcePropertyTypeName {
237                    vendor_name: service.vendor_name.clone(),
238                    service_name: service.service_name.clone(),
239                    resource_name: resource_name.clone(),
240                    property_name: property_name.clone(),
241                }),
242                property_name,
243                property_type,
244            )
245        })
246        .collect();
247
248    quote! {
249        pub mod #resource_module_name {
250            #(#property_types)*
251        }
252    }
253}
254
255fn property_type_token_stream(
256    property_type_name: &PropertyTypeName<'_>,
257    property_name: &PropertyName<'_>,
258    property_type: &PropertyType<'_>,
259) -> proc_macro2::TokenStream {
260    let struct_name = property_name_struct_name(property_name);
261    let macro_name = quote::format_ident!("{property_name}");
262
263    let properties = match &property_type.properties {
264        Some(properties) => properties,
265        None => &PropertyTypePropertiesMap::new(),
266    };
267
268    let documentation = property_type.documentation.as_str();
269
270    let fields: Vec<_> = properties
271        .iter()
272        .map(|(property_name, property_type_property)| {
273            let field_name = mk_field_name(property_name.as_str());
274
275            let field_type = property_type_property_token_stream(property_type_property);
276
277            quote! {
278                pub #field_name : #field_type
279            }
280        })
281        .collect();
282
283    let to_values: Vec<_> = properties
284        .iter()
285        .map(|(property_name, property_type)| mk_to_value(property_name, property_type))
286        .collect();
287
288    let property_type_name_str: &str = &property_type_name.to_string();
289
290    quote! {
291        #[doc = #documentation]
292        pub struct #struct_name {
293            #(#fields),*
294        }
295
296        #[allow(unused_macros)]
297        macro_rules! #macro_name {
298            ($($field:ident : $value:expr),* $(,)?) => {
299               stratosphere::generator::construct_property_type!(#property_type_name_str $($field $value)*)
300            }
301        }
302
303        pub(crate) use #macro_name;
304
305        impl stratosphere::value::ToValue for #struct_name {
306            fn to_value(&self) -> serde_json::Value {
307                let mut properties = serde_json::Map::new();
308
309                #(#to_values)*
310
311                properties.into()
312            }
313        }
314    }
315}
316
317fn property_type_property_token_stream(
318    property_type_property: &PropertyTypeProperty,
319) -> proc_macro2::TokenStream {
320    let field_type = match property_type_property {
321        PropertyTypeProperty {
322            documentation: _,
323            duplicates_allowed: None,
324            item_type: None,
325            primitive_item_type: None,
326            primitive_type: Some(primitive_type),
327            r#type: None,
328            required: _,
329            update_type: _,
330        } => mk_primitive_type(primitive_type),
331        PropertyTypeProperty {
332            documentation: _,
333            duplicates_allowed: _,
334            item_type: Some(TypeReference::Tag),
335            primitive_item_type: None,
336            primitive_type: None,
337            r#type: Some(TypeReference::List),
338            required: _,
339            update_type: _,
340        } => quote! { Vec<super::super::super::Tag_> },
341        PropertyTypeProperty {
342            documentation: _,
343            duplicates_allowed: _,
344            item_type: Some(TypeReference::Subproperty(property_name)),
345            primitive_item_type: None,
346            primitive_type: None,
347            r#type: Some(TypeReference::List),
348            required: _,
349            update_type: _,
350        } => {
351            let struct_name = property_name_struct_name(property_name);
352            quote! { Vec<#struct_name> }
353        }
354        PropertyTypeProperty {
355            documentation: _,
356            duplicates_allowed: _,
357            item_type: None,
358            primitive_item_type: Some(primitive_item_type),
359            primitive_type: None,
360            r#type: Some(TypeReference::List),
361            required: _,
362            update_type: _,
363        } => mk_primitive_list_type(primitive_item_type),
364        PropertyTypeProperty {
365            documentation: _,
366            duplicates_allowed: None,
367            item_type: None,
368            primitive_item_type: None,
369            primitive_type: None,
370            r#type: Some(TypeReference::Subproperty(property_name)),
371            required: _,
372            update_type: _,
373        } => {
374            let struct_name = property_name_struct_name(property_name);
375            quote! { #struct_name }
376        }
377        PropertyTypeProperty {
378            documentation: _,
379            duplicates_allowed: None,
380            item_type: None,
381            primitive_item_type: Some(item_type),
382            primitive_type: None,
383            r#type: Some(TypeReference::Map),
384            required: _,
385            update_type: _,
386        } => mk_primitive_map_type(item_type),
387        other => panic!("Unsupported property type property: {other:#?}"),
388    };
389
390    mk_option(property_type_property.required, field_type)
391}
392
393fn mk_option(required: bool, stream: proc_macro2::TokenStream) -> proc_macro2::TokenStream {
394    if !required {
395        quote! { Option<#stream> }
396    } else {
397        stream
398    }
399}
400
401fn resource_type_map_token_stream(
402    service_module_name: &ServiceModuleName,
403    resource_type_map: ResourceTypeMap,
404) -> proc_macro2::TokenStream {
405    let mut stream = proc_macro2::TokenStream::new();
406
407    for (resource_type_name, resource_type) in resource_type_map {
408        stream.extend(resource_type_token_stream(
409            service_module_name,
410            resource_type_name,
411            resource_type,
412        ))
413    }
414
415    stream
416}
417
418trait IsRequired {
419    fn required(&self) -> bool;
420}
421
422impl IsRequired for &ResourceTypeProperty<'_> {
423    fn required(&self) -> bool {
424        self.required
425    }
426}
427
428impl IsRequired for &PropertyTypeProperty<'_> {
429    fn required(&self) -> bool {
430        self.required
431    }
432}
433
434fn mk_to_value(
435    property_name: impl AsRef<str>,
436    property_type: impl IsRequired,
437) -> proc_macro2::TokenStream {
438    let field_name = mk_field_name(property_name.as_ref());
439    let key = property_name.as_ref();
440
441    if property_type.required() {
442        quote! {
443            properties
444                .insert(
445                    #key.to_string(),
446                    stratosphere::value::ToValue::to_value(&self.#field_name)
447                );
448        }
449    } else {
450        quote! {
451          if let Some(ref value) = self.#field_name {
452              properties.insert(
453                  #key.to_string(),
454                  stratosphere::value::ToValue::to_value(value)
455              );
456          }
457        }
458    }
459}
460
461fn resource_type_token_stream(
462    service_module_name: &ServiceModuleName,
463    resource_type_name: &ResourceTypeName<'_>,
464    resource_type: &ResourceType<'_>,
465) -> proc_macro2::TokenStream {
466    let resource_name_ref = resource_type_name.resource_name.as_ref();
467
468    let struct_name = quote::format_ident!("{resource_name_ref}_");
469
470    let macro_name = quote::format_ident!("{resource_name_ref}");
471
472    let resource_module_name = ResourceModuleName::new(&resource_type_name.resource_name);
473
474    let documentation = resource_type.documentation.as_str();
475
476    let fields: Vec<_> = resource_type
477        .properties
478        .iter()
479        .map(|(property_name, property_type)| {
480            resource_property_type_token_stream(
481                service_module_name,
482                &resource_module_name,
483                property_name,
484                property_type,
485            )
486        })
487        .collect();
488
489    let to_values: Vec<_> = resource_type
490        .properties
491        .iter()
492        .map(|(property_name, property_type)| mk_to_value(property_name, property_type))
493        .collect();
494
495    let resource_type_name_str: &str = &resource_type_name.to_string();
496
497    let stream = quote! {
498        #[doc = #documentation]
499        pub struct #struct_name {
500            #(#fields),*
501        }
502
503        #[allow(unused_macros)]
504        macro_rules! #macro_name {
505            ($($field:ident : $value:expr),* $(,)?) => {
506               stratosphere::generator::construct_resource_type!(#resource_type_name_str $($field $value)*)
507            }
508        }
509
510        pub(crate) use #macro_name;
511
512        impl stratosphere::template::ToResource for #struct_name {
513            const RESOURCE_TYPE_NAME: stratosphere::resource_specification::ResourceTypeName<'static> =
514                #resource_type_name;
515
516            fn to_resource_properties(&self) -> stratosphere::template::ResourceProperties {
517                let mut properties = stratosphere::template::ResourceProperties::new();
518
519                #(#to_values)*
520
521                properties
522            }
523        }
524    };
525
526    stream
527}
528
529fn resource_property_type_token_stream(
530    service_module_name: &ServiceModuleName,
531    resource_module_name: &ResourceModuleName,
532    resource_type_property_name: &ResourceTypePropertyName<'_>,
533    resource_type_property: &ResourceTypeProperty<'_>,
534) -> proc_macro2::TokenStream {
535    let field_name = mk_field_name(resource_type_property_name);
536
537    let property_type = resource_type_property_token_stream(
538        service_module_name,
539        resource_module_name,
540        resource_type_property,
541    );
542
543    let field_type = mk_option(resource_type_property.required, property_type);
544
545    quote! {
546        pub #field_name: #field_type
547    }
548}
549
550fn resource_type_property_token_stream(
551    service_module_name: &ServiceModuleName,
552    resource_module_name: &ResourceModuleName,
553    resource_type_property: &ResourceTypeProperty<'_>,
554) -> proc_macro2::TokenStream {
555    match resource_type_property {
556        ResourceTypeProperty {
557            primitive_type: Some(primitive_type),
558            item_type: None,
559            duplicates_allowed: None,
560            primitive_item_type: None,
561            documentation: _,
562            update_type: _,
563            required: _,
564            r#type: None,
565        } => mk_primitive_type(primitive_type),
566        ResourceTypeProperty {
567            primitive_type: None,
568            item_type: Some(item_type),
569            duplicates_allowed: _,
570            primitive_item_type,
571            documentation: _,
572            update_type: _,
573            required: _,
574            r#type: Some(TypeReference::List),
575        } => mk_list_type(
576            service_module_name,
577            resource_module_name,
578            item_type,
579            primitive_item_type.as_ref(),
580        ),
581        ResourceTypeProperty {
582            primitive_type: None,
583            item_type: None,
584            duplicates_allowed: _,
585            primitive_item_type: Some(primitive_item_type),
586            documentation: _,
587            update_type: _,
588            required: _,
589            r#type: Some(TypeReference::List),
590        } => mk_primitive_list_type(primitive_item_type),
591        ResourceTypeProperty {
592            primitive_type: None,
593            item_type: None,
594            duplicates_allowed: None,
595            primitive_item_type: None,
596            documentation: _,
597            update_type: _,
598            required: _,
599            r#type: Some(TypeReference::Subproperty(subproperty_name)),
600        } => mk_subproperty(service_module_name, resource_module_name, subproperty_name),
601        other => panic!("Unsupported property type: {other:#?}"),
602    }
603}
604
605fn mk_list_type(
606    service_module_name: &ServiceModuleName,
607    resource_module_name: &ResourceModuleName,
608    item_type: &TypeReference,
609    primitive_item_type: Option<&PrimitiveItemType>,
610) -> proc_macro2::TokenStream {
611    let item_type = mk_type_reference_type(
612        service_module_name,
613        resource_module_name,
614        item_type,
615        primitive_item_type,
616    );
617
618    quote! { Vec<#item_type> }
619}
620
621fn mk_primitive_list_type(primitive_item_type: &PrimitiveItemType) -> proc_macro2::TokenStream {
622    let item_type = match primitive_item_type {
623        PrimitiveItemType::Json => quote! { serde_json::Value },
624        PrimitiveItemType::Double => quote! { f64 },
625        PrimitiveItemType::Integer => quote! { i64 },
626        PrimitiveItemType::String => quote! { stratosphere::value::ExpString },
627    };
628
629    quote! { Vec<#item_type> }
630}
631
632fn mk_primitive_map_type(primitive_item_type: &PrimitiveItemType) -> proc_macro2::TokenStream {
633    let item_type = match primitive_item_type {
634        PrimitiveItemType::Json => quote! { serde_json::Value },
635        PrimitiveItemType::Double => quote! { f64 },
636        PrimitiveItemType::Integer => quote! { i64 },
637        PrimitiveItemType::String => quote! { stratosphere::value::ExpString },
638    };
639
640    quote! { std::collections::BTreeMap<String, #item_type> }
641}
642
643fn mk_type_reference_type(
644    service_module_name: &ServiceModuleName,
645    resource_module_name: &ResourceModuleName,
646    item_type: &TypeReference,
647    primitive_item_type: Option<&PrimitiveItemType>,
648) -> proc_macro2::TokenStream {
649    match item_type {
650        TypeReference::Subproperty(name) => {
651            mk_subproperty(service_module_name, resource_module_name, name)
652        }
653        TypeReference::Tag => quote! { super::super::Tag_ },
654        TypeReference::List => match primitive_item_type {
655            Some(PrimitiveItemType::Json) => quote! { Vec<serde_json::Value> },
656            Some(PrimitiveItemType::Double) => quote! { Vec<f64> },
657            Some(PrimitiveItemType::Integer) => quote! { Vec<i64> },
658            Some(PrimitiveItemType::String) => quote! { Vec<stratosphere::value::ExpString> },
659            None => panic!("TypeReference::List requires primitive_item_type to be specified"),
660        },
661        TypeReference::Map => match primitive_item_type {
662            Some(PrimitiveItemType::Json) => {
663                quote! { std::collections::BTreeMap<String, serde_json::Value> }
664            }
665            Some(PrimitiveItemType::Double) => {
666                quote! { std::collections::BTreeMap<String, f64> }
667            }
668            Some(PrimitiveItemType::Integer) => {
669                quote! { std::collections::BTreeMap<String, i64> }
670            }
671            Some(PrimitiveItemType::String) => {
672                quote! { std::collections::BTreeMap<String, stratosphere::value::ExpString> }
673            }
674            None => panic!("TypeReference::Map requires primitive_item_type to be specified"),
675        },
676    }
677}
678
679pub struct VendorModuleName(proc_macro2::Ident);
680
681impl VendorModuleName {
682    pub fn new(vendor_name: &VendorName<'_>) -> Self {
683        Self(quote::format_ident!(
684            "{}",
685            vendor_name.as_ref().to_lowercase()
686        ))
687    }
688}
689
690impl quote::ToTokens for VendorModuleName {
691    fn to_tokens(&self, stream: &mut proc_macro2::TokenStream) {
692        let ident = &self.0;
693        stream.extend(quote! { #ident })
694    }
695}
696
697pub struct ServiceModuleName(proc_macro2::Ident);
698
699impl ServiceModuleName {
700    pub fn new(service_name: &ServiceName<'_>) -> Self {
701        Self(quote::format_ident!(
702            "{}",
703            service_name.as_ref().to_lowercase()
704        ))
705    }
706}
707
708impl quote::ToTokens for ServiceModuleName {
709    fn to_tokens(&self, stream: &mut proc_macro2::TokenStream) {
710        let ident = &self.0;
711        stream.extend(quote! { #ident })
712    }
713}
714
715pub struct ResourceModuleName(proc_macro2::Ident);
716
717impl ResourceModuleName {
718    pub fn new(resource_name: &ResourceName<'_>) -> Self {
719        Self(quote::format_ident!(
720            "{}",
721            resource_name.as_ref().to_lowercase()
722        ))
723    }
724}
725
726impl quote::ToTokens for ResourceModuleName {
727    fn to_tokens(&self, stream: &mut proc_macro2::TokenStream) {
728        let ident = &self.0;
729        stream.extend(quote! { #ident })
730    }
731}
732
733fn mk_subproperty(
734    service_module_name: &ServiceModuleName,
735    resource_module_name: &ResourceModuleName,
736    property_name: &PropertyName,
737) -> proc_macro2::TokenStream {
738    let property_name = property_name_struct_name(property_name);
739
740    quote! {
741        super::#service_module_name::#resource_module_name::#property_name
742    }
743}
744
745fn mk_primitive_type(primitive_type: &PrimitiveType) -> proc_macro2::TokenStream {
746    match primitive_type {
747        PrimitiveType::Boolean => quote! { stratosphere::value::ExpBool },
748        PrimitiveType::Double => quote! { f64 },
749        PrimitiveType::Integer => quote! { i64 },
750        PrimitiveType::Long => quote! { i64 },
751        PrimitiveType::String => quote! { stratosphere::value::ExpString },
752        PrimitiveType::Timestamp => quote! { chrono::DateTime<chrono::Utc> },
753        PrimitiveType::Json => quote! { serde_json::Value },
754    }
755}
756
757/// Converts a string from CamelCase or PascalCase to snake_case
758pub fn to_snake_case(input: &str) -> String {
759    if input.is_empty() {
760        return String::new();
761    }
762
763    let mut result = String::new();
764    let mut it = input.chars().peekable();
765
766    let mut prev_is_lower = false;
767
768    let first = it.next().unwrap();
769    result.push(if first.is_uppercase() {
770        first.to_lowercase().next().unwrap()
771    } else {
772        prev_is_lower = true;
773        first
774    });
775
776    while let Some(ch) = it.next() {
777        let is_upper = ch.is_uppercase();
778        let next_is_lower = it.peek().is_some_and(|&next_ch| next_ch.is_lowercase());
779
780        if (next_is_lower || prev_is_lower) && is_upper {
781            result.push('_');
782        }
783
784        result.push(if is_upper {
785            ch.to_lowercase().next().unwrap()
786        } else {
787            ch
788        });
789
790        prev_is_lower = !is_upper;
791    }
792
793    result
794}
795
796pub fn mk_field_name(name: impl AsRef<str>) -> syn::Ident {
797    let name = name.as_ref();
798    let snake_case_name = to_snake_case(name);
799
800    if snake_case_name == "self" {
801        return quote::format_ident!("self_value");
802    }
803
804    if syn::parse_str::<syn::Ident>(&snake_case_name).is_err() {
805        quote::format_ident!("r#{}", snake_case_name)
806    } else {
807        quote::format_ident!("{}", snake_case_name)
808    }
809}
810
811pub fn resource_type_struct_name(resource_type_name: &ResourceTypeName<'_>) -> syn::Ident {
812    quote::format_ident!("{}_", resource_type_name.resource_name.as_str())
813}
814
815pub fn property_name_struct_name(property_name: &PropertyName<'_>) -> syn::Ident {
816    quote::format_ident!("{property_name}_")
817}
818
819#[cfg(test)]
820mod tests {
821    use super::*;
822
823    fn assert_snake_case(original: &str, expected: &str) {
824        assert_eq!(
825            to_snake_case(original),
826            expected,
827            "Original: {original}, expected: {expected}"
828        );
829    }
830
831    #[test]
832    fn test_pascal_case() {
833        assert_snake_case("HelloWorld", "hello_world");
834    }
835
836    #[test]
837    fn test_camel_case() {
838        assert_snake_case("helloWorld", "hello_world");
839    }
840
841    #[test]
842    fn test_all_upper() {
843        assert_snake_case("XMLHTTPRequest", "xmlhttp_request");
844    }
845
846    #[test]
847    fn test_single_word() {
848        assert_snake_case("Hello", "hello");
849    }
850
851    #[test]
852    fn test_lowercase() {
853        assert_snake_case("hello", "hello");
854    }
855
856    #[test]
857    fn test_keyword_detection_and_escaping() {
858        assert_eq!(mk_field_name("Type").to_string(), "r#type");
859        assert_eq!(mk_field_name("Match").to_string(), "r#match");
860        assert_eq!(mk_field_name("Async").to_string(), "r#async");
861        assert_eq!(mk_field_name("Await").to_string(), "r#await");
862        assert_eq!(mk_field_name("Const").to_string(), "r#const");
863        assert_eq!(mk_field_name("Loop").to_string(), "r#loop");
864        assert_eq!(mk_field_name("Return").to_string(), "r#return");
865        assert_eq!(mk_field_name("Impl").to_string(), "r#impl");
866        assert_eq!(mk_field_name("Mod").to_string(), "r#mod");
867        assert_eq!(mk_field_name("Pub").to_string(), "r#pub");
868        assert_eq!(mk_field_name("Use").to_string(), "r#use");
869        assert_eq!(mk_field_name("Fn").to_string(), "r#fn");
870        assert_eq!(mk_field_name("Static").to_string(), "r#static");
871        assert_eq!(mk_field_name("Mut").to_string(), "r#mut");
872        assert_eq!(mk_field_name("Ref").to_string(), "r#ref");
873        assert_eq!(mk_field_name("Dyn").to_string(), "r#dyn");
874        assert_eq!(mk_field_name("Self").to_string(), "self_value");
875        assert_eq!(mk_field_name("Abstract").to_string(), "r#abstract");
876        assert_eq!(mk_field_name("Final").to_string(), "r#final");
877        assert_eq!(mk_field_name("Override").to_string(), "r#override");
878        assert_eq!(mk_field_name("Yield").to_string(), "r#yield");
879        assert_eq!(mk_field_name("Try").to_string(), "r#try");
880        assert_eq!(mk_field_name("Gen").to_string(), "gen");
881        assert_eq!(mk_field_name("Union").to_string(), "union");
882        assert_eq!(mk_field_name("FooBar").to_string(), "foo_bar");
883        assert_eq!(mk_field_name("TestValue").to_string(), "test_value");
884        assert_eq!(mk_field_name("MyField").to_string(), "my_field");
885        assert_eq!(
886            mk_field_name("EnableDnsSupport").to_string(),
887            "enable_dns_support"
888        );
889        assert_eq!(mk_field_name("VpcId").to_string(), "vpc_id");
890    }
891}