stratosphere_core/
token.rs

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