Skip to main content

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 = format!("<{}>", 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 = format!("<{}>", 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! { i32 },
634        PrimitiveItemType::Long => quote! { i64 },
635        PrimitiveItemType::String => quote! { crate::value::ExpString },
636    };
637
638    quote! { Vec<#item_type> }
639}
640
641fn mk_map_type(
642    service_module_name: &ServiceModuleName,
643    resource_module_name: &ResourceModuleName,
644    item_type: &TypeReference,
645    primitive_item_type: Option<&PrimitiveItemType>,
646) -> proc_macro2::TokenStream {
647    let value_type = mk_type_reference_type(
648        service_module_name,
649        resource_module_name,
650        item_type,
651        primitive_item_type,
652    );
653
654    quote! { std::collections::BTreeMap<String, #value_type> }
655}
656
657fn mk_primitive_map_type(primitive_item_type: &PrimitiveItemType) -> proc_macro2::TokenStream {
658    let item_type = match primitive_item_type {
659        PrimitiveItemType::Json => quote! { serde_json::Value },
660        PrimitiveItemType::Double => quote! { f64 },
661        PrimitiveItemType::Integer => quote! { i32 },
662        PrimitiveItemType::Long => quote! { i64 },
663        PrimitiveItemType::String => quote! { crate::value::ExpString },
664    };
665
666    quote! { std::collections::BTreeMap<String, #item_type> }
667}
668
669fn mk_type_reference_type(
670    service_module_name: &ServiceModuleName,
671    resource_module_name: &ResourceModuleName,
672    item_type: &TypeReference,
673    primitive_item_type: Option<&PrimitiveItemType>,
674) -> proc_macro2::TokenStream {
675    match item_type {
676        TypeReference::Subproperty(name) => {
677            mk_subproperty(service_module_name, resource_module_name, name)
678        }
679        TypeReference::Tag => quote! { crate::Tag_ },
680        TypeReference::List => match primitive_item_type {
681            Some(PrimitiveItemType::Json) => quote! { Vec<serde_json::Value> },
682            Some(PrimitiveItemType::Double) => quote! { Vec<f64> },
683            Some(PrimitiveItemType::Integer) => quote! { Vec<i32> },
684            Some(PrimitiveItemType::Long) => quote! { Vec<i64> },
685            Some(PrimitiveItemType::String) => quote! { Vec<crate::value::ExpString> },
686            None => panic!("TypeReference::List requires primitive_item_type to be specified"),
687        },
688        TypeReference::Map => match primitive_item_type {
689            Some(PrimitiveItemType::Json) => {
690                quote! { std::collections::BTreeMap<String, serde_json::Value> }
691            }
692            Some(PrimitiveItemType::Double) => {
693                quote! { std::collections::BTreeMap<String, f64> }
694            }
695            Some(PrimitiveItemType::Integer) => {
696                quote! { std::collections::BTreeMap<String, i32> }
697            }
698            Some(PrimitiveItemType::Long) => {
699                quote! { std::collections::BTreeMap<String, i64> }
700            }
701            Some(PrimitiveItemType::String) => {
702                quote! { std::collections::BTreeMap<String, crate::value::ExpString> }
703            }
704            None => panic!("TypeReference::Map requires primitive_item_type to be specified"),
705        },
706    }
707}
708
709pub struct VendorModuleName(proc_macro2::Ident);
710
711impl VendorModuleName {
712    #[must_use]
713    pub fn new(vendor_name: &VendorName<'_>) -> Self {
714        Self(quote::format_ident!(
715            "{}",
716            vendor_name.as_ref().to_lowercase()
717        ))
718    }
719}
720
721impl quote::ToTokens for VendorModuleName {
722    fn to_tokens(&self, stream: &mut proc_macro2::TokenStream) {
723        let ident = &self.0;
724        stream.extend(quote! { #ident })
725    }
726}
727
728pub struct ServiceModuleName(proc_macro2::Ident);
729
730impl ServiceModuleName {
731    #[must_use]
732    pub fn new(service_name: &ServiceName<'_>) -> Self {
733        Self(quote::format_ident!(
734            "{}",
735            service_name.as_ref().to_lowercase()
736        ))
737    }
738}
739
740impl quote::ToTokens for ServiceModuleName {
741    fn to_tokens(&self, stream: &mut proc_macro2::TokenStream) {
742        let ident = &self.0;
743        stream.extend(quote! { #ident })
744    }
745}
746
747pub struct ResourceModuleName(proc_macro2::Ident);
748
749impl ResourceModuleName {
750    #[must_use]
751    pub fn new(resource_name: &ResourceName<'_>) -> Self {
752        Self(resource_name.to_module_ident())
753    }
754}
755
756impl quote::ToTokens for ResourceModuleName {
757    fn to_tokens(&self, stream: &mut proc_macro2::TokenStream) {
758        let ident = &self.0;
759        stream.extend(quote! { #ident })
760    }
761}
762
763fn mk_subproperty(
764    service_module_name: &ServiceModuleName,
765    resource_module_name: &ResourceModuleName,
766    property_name: &PropertyName,
767) -> proc_macro2::TokenStream {
768    let property_name = property_name_struct_name(property_name);
769
770    quote! {
771        super::#service_module_name::#resource_module_name::#property_name
772    }
773}
774
775fn mk_primitive_type(primitive_type: &PrimitiveType) -> proc_macro2::TokenStream {
776    match primitive_type {
777        PrimitiveType::Boolean => quote! { crate::value::ExpBool },
778        PrimitiveType::Double => quote! { f64 },
779        PrimitiveType::Integer => quote! { i32 },
780        PrimitiveType::Long => quote! { i64 },
781        PrimitiveType::String => quote! { crate::value::ExpString },
782        PrimitiveType::Timestamp => quote! { chrono::DateTime<chrono::Utc> },
783        PrimitiveType::Json => quote! { serde_json::Value },
784    }
785}
786
787/// Converts a string from CamelCase or PascalCase to snake_case
788#[must_use]
789pub fn to_snake_case(input: &str) -> String {
790    if input.is_empty() {
791        return String::new();
792    }
793
794    let mut result = String::new();
795    let mut it = input.chars().peekable();
796
797    let mut prev_is_lower = false;
798
799    let first = it.next().unwrap();
800    result.push(if first.is_uppercase() {
801        first.to_lowercase().next().unwrap()
802    } else {
803        prev_is_lower = true;
804        first
805    });
806
807    while let Some(ch) = it.next() {
808        let is_upper = ch.is_uppercase();
809        let next_is_lower = it.peek().is_some_and(|&next_ch| next_ch.is_lowercase());
810
811        if (next_is_lower || prev_is_lower) && is_upper {
812            result.push('_');
813        }
814
815        result.push(if is_upper {
816            ch.to_lowercase().next().unwrap()
817        } else {
818            ch
819        });
820
821        prev_is_lower = !is_upper;
822    }
823
824    result
825}
826
827/// Creates a safe Rust identifier, escaping with `r#` if the name is a keyword
828#[must_use]
829pub fn mk_safe_ident(name: impl AsRef<str>) -> syn::Ident {
830    let name = name.as_ref();
831    if syn::parse_str::<syn::Ident>(name).is_err() {
832        quote::format_ident!("r#{}", name)
833    } else {
834        quote::format_ident!("{}", name)
835    }
836}
837
838pub fn mk_field_name(name: impl AsRef<str>) -> syn::Ident {
839    let name = name.as_ref();
840    let snake_case_name = to_snake_case(name);
841
842    if snake_case_name == "self" {
843        return quote::format_ident!("self_value");
844    }
845
846    mk_safe_ident(snake_case_name)
847}
848
849#[must_use]
850pub fn resource_type_struct_name(resource_type_name: &ResourceTypeName<'_>) -> syn::Ident {
851    quote::format_ident!("{}_", resource_type_name.resource_name.as_str())
852}
853
854#[must_use]
855pub fn property_name_struct_name(property_name: &PropertyName<'_>) -> syn::Ident {
856    quote::format_ident!("{property_name}_")
857}
858
859#[cfg(test)]
860mod tests {
861    use super::*;
862
863    fn assert_snake_case(original: &str, expected: &str) {
864        assert_eq!(
865            to_snake_case(original),
866            expected,
867            "Original: {original}, expected: {expected}"
868        );
869    }
870
871    #[test]
872    fn test_pascal_case() {
873        assert_snake_case("HelloWorld", "hello_world");
874    }
875
876    #[test]
877    fn test_camel_case() {
878        assert_snake_case("helloWorld", "hello_world");
879    }
880
881    #[test]
882    fn test_all_upper() {
883        assert_snake_case("XMLHTTPRequest", "xmlhttp_request");
884    }
885
886    #[test]
887    fn test_single_word() {
888        assert_snake_case("Hello", "hello");
889    }
890
891    #[test]
892    fn test_lowercase() {
893        assert_snake_case("hello", "hello");
894    }
895
896    #[test]
897    fn test_keyword_detection_and_escaping() {
898        assert_eq!(mk_field_name("Type").to_string(), "r#type");
899        assert_eq!(mk_field_name("Match").to_string(), "r#match");
900        assert_eq!(mk_field_name("Async").to_string(), "r#async");
901        assert_eq!(mk_field_name("Await").to_string(), "r#await");
902        assert_eq!(mk_field_name("Const").to_string(), "r#const");
903        assert_eq!(mk_field_name("Loop").to_string(), "r#loop");
904        assert_eq!(mk_field_name("Return").to_string(), "r#return");
905        assert_eq!(mk_field_name("Impl").to_string(), "r#impl");
906        assert_eq!(mk_field_name("Mod").to_string(), "r#mod");
907        assert_eq!(mk_field_name("Pub").to_string(), "r#pub");
908        assert_eq!(mk_field_name("Use").to_string(), "r#use");
909        assert_eq!(mk_field_name("Fn").to_string(), "r#fn");
910        assert_eq!(mk_field_name("Static").to_string(), "r#static");
911        assert_eq!(mk_field_name("Mut").to_string(), "r#mut");
912        assert_eq!(mk_field_name("Ref").to_string(), "r#ref");
913        assert_eq!(mk_field_name("Dyn").to_string(), "r#dyn");
914        assert_eq!(mk_field_name("Self").to_string(), "self_value");
915        assert_eq!(mk_field_name("Abstract").to_string(), "r#abstract");
916        assert_eq!(mk_field_name("Final").to_string(), "r#final");
917        assert_eq!(mk_field_name("Override").to_string(), "r#override");
918        assert_eq!(mk_field_name("Yield").to_string(), "r#yield");
919        assert_eq!(mk_field_name("Try").to_string(), "r#try");
920        assert_eq!(mk_field_name("Gen").to_string(), "gen");
921        assert_eq!(mk_field_name("Union").to_string(), "union");
922        assert_eq!(mk_field_name("FooBar").to_string(), "foo_bar");
923        assert_eq!(mk_field_name("TestValue").to_string(), "test_value");
924        assert_eq!(mk_field_name("MyField").to_string(), "my_field");
925        assert_eq!(
926            mk_field_name("EnableDnsSupport").to_string(),
927            "enable_dns_support"
928        );
929        assert_eq!(mk_field_name("VpcId").to_string(), "vpc_id");
930    }
931}