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#[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#[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 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#[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#[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}