1use crate::ast::*;
2use std::collections::HashSet;
3
4#[derive(Debug, Clone)]
5pub struct RustOutput {
6 pub cargo_toml: String,
7 pub lib_rs: String,
8 pub types_rs: String,
9 pub entity_rs: String,
10}
11
12impl RustOutput {
13 pub fn full_lib(&self) -> String {
14 format!(
15 "{}\n\n// types.rs\n{}\n\n// entity.rs\n{}",
16 self.lib_rs, self.types_rs, self.entity_rs
17 )
18 }
19
20 pub fn mod_rs(&self) -> String {
21 self.lib_rs.clone()
22 }
23}
24
25#[derive(Debug, Clone)]
26pub struct RustConfig {
27 pub crate_name: String,
28 pub sdk_version: String,
29 pub module_mode: bool,
30 pub url: Option<String>,
32}
33
34impl Default for RustConfig {
35 fn default() -> Self {
36 Self {
37 crate_name: "generated-stack".to_string(),
38 sdk_version: "0.2".to_string(),
39 module_mode: false,
40 url: None,
41 }
42 }
43}
44
45pub fn compile_serializable_spec(
46 spec: SerializableStreamSpec,
47 entity_name: String,
48 config: Option<RustConfig>,
49) -> Result<RustOutput, String> {
50 let config = config.unwrap_or_default();
51 let compiler = RustCompiler::new(spec, entity_name, config);
52 Ok(compiler.compile())
53}
54
55pub fn write_rust_crate(
56 output: &RustOutput,
57 crate_dir: &std::path::Path,
58) -> Result<(), std::io::Error> {
59 std::fs::create_dir_all(crate_dir.join("src"))?;
60 std::fs::write(crate_dir.join("Cargo.toml"), &output.cargo_toml)?;
61 std::fs::write(crate_dir.join("src/lib.rs"), &output.lib_rs)?;
62 std::fs::write(crate_dir.join("src/types.rs"), &output.types_rs)?;
63 std::fs::write(crate_dir.join("src/entity.rs"), &output.entity_rs)?;
64 Ok(())
65}
66
67pub fn write_rust_module(
68 output: &RustOutput,
69 module_dir: &std::path::Path,
70) -> Result<(), std::io::Error> {
71 std::fs::create_dir_all(module_dir)?;
72 std::fs::write(module_dir.join("mod.rs"), output.mod_rs())?;
73 std::fs::write(module_dir.join("types.rs"), &output.types_rs)?;
74 std::fs::write(module_dir.join("entity.rs"), &output.entity_rs)?;
75 Ok(())
76}
77
78pub(crate) struct RustCompiler {
79 spec: SerializableStreamSpec,
80 entity_name: String,
81 config: RustConfig,
82}
83
84impl RustCompiler {
85 pub(crate) fn new(
86 spec: SerializableStreamSpec,
87 entity_name: String,
88 config: RustConfig,
89 ) -> Self {
90 Self {
91 spec,
92 entity_name,
93 config,
94 }
95 }
96
97 fn compile(&self) -> RustOutput {
98 RustOutput {
99 cargo_toml: self.generate_cargo_toml(),
100 lib_rs: self.generate_lib_rs(),
101 types_rs: self.generate_types_rs(),
102 entity_rs: self.generate_entity_rs(),
103 }
104 }
105
106 fn generate_cargo_toml(&self) -> String {
107 format!(
108 r#"[package]
109name = "{}"
110version = "0.1.0"
111edition = "2021"
112
113[dependencies]
114hyperstack-sdk = "{}"
115serde = {{ version = "1", features = ["derive"] }}
116serde_json = "1"
117"#,
118 self.config.crate_name, self.config.sdk_version
119 )
120 }
121
122 fn generate_lib_rs(&self) -> String {
123 let stack_name = self.derive_stack_name();
124 let entity_name = &self.entity_name;
125
126 format!(
127 r#"mod entity;
128mod types;
129
130pub use entity::{{{stack_name}Stack, {stack_name}StackViews, {entity_name}EntityViews}};
131pub use types::*;
132
133pub use hyperstack_sdk::{{ConnectionState, HyperStack, Stack, Update, Views}};
134"#,
135 stack_name = stack_name,
136 entity_name = entity_name
137 )
138 }
139
140 fn generate_types_rs(&self) -> String {
141 let mut output = String::new();
142 output.push_str("use serde::{Deserialize, Serialize};\n");
143 output.push_str("use hyperstack_sdk::serde_utils;\n\n");
144
145 let mut generated = HashSet::new();
146
147 for section in &self.spec.sections {
148 if !Self::is_root_section(§ion.name)
149 && section.fields.iter().any(|field| field.emit)
150 && generated.insert(section.name.clone())
151 {
152 output.push_str(&self.generate_struct_for_section(section));
153 output.push_str("\n\n");
154 }
155 }
156
157 output.push_str(&self.generate_main_entity_struct());
158 output.push_str(&self.generate_resolved_types(&mut generated));
159 output.push_str(&self.generate_event_wrapper());
160
161 output
162 }
163
164 pub(crate) fn generate_struct_for_section(&self, section: &EntitySection) -> String {
165 let struct_name = format!("{}{}", self.entity_name, to_pascal_case(§ion.name));
166 let mut fields = Vec::new();
167
168 for field in §ion.fields {
169 if !field.emit {
170 continue;
171 }
172 let field_name = to_snake_case(&field.field_name);
173 let rust_type = self.field_type_to_rust(field);
174 let serde_attr = self.serde_attr_for_field(field);
175
176 fields.push(format!(
177 " {}\n pub {}: {},",
178 serde_attr, field_name, rust_type
179 ));
180 }
181
182 format!(
183 "#[derive(Debug, Clone, Serialize, Deserialize, Default)]\npub struct {} {{\n{}\n}}",
184 struct_name,
185 fields.join("\n")
186 )
187 }
188
189 pub(crate) fn is_root_section(name: &str) -> bool {
190 name.eq_ignore_ascii_case("root")
191 }
192
193 pub(crate) fn generate_main_entity_struct(&self) -> String {
194 let mut fields = Vec::new();
195
196 for section in &self.spec.sections {
197 if !Self::is_root_section(§ion.name)
198 && section.fields.iter().any(|field| field.emit)
199 {
200 let field_name = to_snake_case(§ion.name);
201 let type_name = format!("{}{}", self.entity_name, to_pascal_case(§ion.name));
202 fields.push(format!(
203 " #[serde(default)]\n pub {}: {},",
204 field_name, type_name
205 ));
206 }
207 }
208
209 for section in &self.spec.sections {
210 if Self::is_root_section(§ion.name) {
211 for field in §ion.fields {
212 if !field.emit {
213 continue;
214 }
215 let field_name = to_snake_case(&field.field_name);
216 let rust_type = self.field_type_to_rust(field);
217 let serde_attr = self.serde_attr_for_field(field);
218 fields.push(format!(
219 " {}\n pub {}: {},",
220 serde_attr, field_name, rust_type
221 ));
222 }
223 }
224 }
225
226 format!(
227 "#[derive(Debug, Clone, Serialize, Deserialize, Default)]\npub struct {} {{\n{}\n}}",
228 self.entity_name,
229 fields.join("\n")
230 )
231 }
232
233 pub(crate) fn generate_resolved_types(&self, generated: &mut HashSet<String>) -> String {
234 let mut output = String::new();
235
236 for section in &self.spec.sections {
237 for field in §ion.fields {
238 if !field.emit {
239 continue;
240 }
241 if let Some(resolved) = &field.resolved_type {
242 if generated.insert(resolved.type_name.clone()) {
243 output.push_str("\n\n");
244 output.push_str(&self.generate_resolved_struct(resolved));
245 }
246 }
247 }
248 }
249
250 output
251 }
252
253 fn generate_resolved_struct(&self, resolved: &ResolvedStructType) -> String {
254 if resolved.is_enum {
255 let variants: Vec<String> = resolved
256 .enum_variants
257 .iter()
258 .map(|v| format!(" {},", to_pascal_case(v)))
259 .collect();
260
261 format!(
262 "#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]\npub enum {} {{\n{}\n}}",
263 to_pascal_case(&resolved.type_name),
264 variants.join("\n")
265 )
266 } else {
267 let fields: Vec<String> = resolved
268 .fields
269 .iter()
270 .map(|f| {
271 let rust_type = self.resolved_field_to_rust(f);
272 let serde_attr = self.serde_attr_for_resolved_field(f);
273 format!(
274 " {}\n pub {}: {},",
275 serde_attr,
276 to_snake_case(&f.field_name),
277 rust_type
278 )
279 })
280 .collect();
281
282 format!(
283 "#[derive(Debug, Clone, Serialize, Deserialize, Default)]\npub struct {} {{\n{}\n}}",
284 to_pascal_case(&resolved.type_name),
285 fields.join("\n")
286 )
287 }
288 }
289
290 fn generate_event_wrapper(&self) -> String {
291 r#"
292
293#[derive(Debug, Clone, Serialize, Deserialize)]
294pub struct EventWrapper<T> {
295 #[serde(default, deserialize_with = "serde_utils::deserialize_i64")]
296 pub timestamp: i64,
297 pub data: T,
298 #[serde(default)]
299 pub slot: Option<f64>,
300 #[serde(default)]
301 pub signature: Option<String>,
302}
303
304impl<T: Default> Default for EventWrapper<T> {
305 fn default() -> Self {
306 Self {
307 timestamp: 0,
308 data: T::default(),
309 slot: None,
310 signature: None,
311 }
312 }
313}
314"#
315 .to_string()
316 }
317
318 fn generate_entity_rs(&self) -> String {
319 let entity_name = &self.entity_name;
320 let stack_name = self.derive_stack_name();
321 let stack_name_kebab = to_kebab_case(entity_name);
322 let entity_snake = to_snake_case(entity_name);
323
324 let types_import = if self.config.module_mode {
325 "super::types"
326 } else {
327 "crate::types"
328 };
329
330 let url_impl = match &self.config.url {
332 Some(url) => format!(
333 r#"fn url() -> &'static str {{
334 "{}"
335 }}"#,
336 url
337 ),
338 None => r#"fn url() -> &'static str {
339 "" // TODO: Set URL after first deployment in hyperstack.toml
340 }"#
341 .to_string(),
342 };
343
344 let entity_views = self.generate_entity_views_struct();
345
346 format!(
347 r#"use {types_import}::{entity_name};
348use hyperstack_sdk::{{Stack, StateView, ViewBuilder, ViewHandle, Views}};
349
350pub struct {stack_name}Stack;
351
352impl Stack for {stack_name}Stack {{
353 type Views = {stack_name}StackViews;
354
355 fn name() -> &'static str {{
356 "{stack_name_kebab}"
357 }}
358
359 {url_impl}
360}}
361
362pub struct {stack_name}StackViews {{
363 pub {entity_snake}: {entity_name}EntityViews,
364}}
365
366impl Views for {stack_name}StackViews {{
367 fn from_builder(builder: ViewBuilder) -> Self {{
368 Self {{
369 {entity_snake}: {entity_name}EntityViews {{ builder }},
370 }}
371 }}
372}}
373{entity_views}"#,
374 types_import = types_import,
375 entity_name = entity_name,
376 stack_name = stack_name,
377 stack_name_kebab = stack_name_kebab,
378 entity_snake = entity_snake,
379 url_impl = url_impl,
380 entity_views = entity_views
381 )
382 }
383
384 fn generate_entity_views_struct(&self) -> String {
385 let entity_name = &self.entity_name;
386
387 let derived: Vec<_> = self
388 .spec
389 .views
390 .iter()
391 .filter(|v| {
392 !v.id.ends_with("/state")
393 && !v.id.ends_with("/list")
394 && v.id.starts_with(entity_name)
395 })
396 .collect();
397
398 let mut derived_methods = String::new();
399 for view in &derived {
400 let view_name = view.id.split('/').nth(1).unwrap_or("unknown");
401 let method_name = to_snake_case(view_name);
402
403 derived_methods.push_str(&format!(
404 r#"
405 pub fn {method_name}(&self) -> ViewHandle<{entity_name}> {{
406 self.builder.view("{view_id}")
407 }}
408"#,
409 method_name = method_name,
410 entity_name = entity_name,
411 view_id = view.id
412 ));
413 }
414
415 format!(
416 r#"
417pub struct {entity_name}EntityViews {{
418 builder: ViewBuilder,
419}}
420
421impl {entity_name}EntityViews {{
422 pub fn state(&self) -> StateView<{entity_name}> {{
423 StateView::new(
424 self.builder.connection().clone(),
425 self.builder.store().clone(),
426 "{entity_name}/state".to_string(),
427 self.builder.initial_data_timeout(),
428 )
429 }}
430
431 pub fn list(&self) -> ViewHandle<{entity_name}> {{
432 self.builder.view("{entity_name}/list")
433 }}
434{derived_methods}}}"#,
435 entity_name = entity_name,
436 derived_methods = derived_methods
437 )
438 }
439
440 fn derive_stack_name(&self) -> String {
443 let entity_name = &self.entity_name;
444
445 let suffixes = ["Round", "Token", "Game", "State", "Entity", "Data"];
447
448 for suffix in suffixes {
449 if entity_name.ends_with(suffix) && entity_name.len() > suffix.len() {
450 return entity_name[..entity_name.len() - suffix.len()].to_string();
451 }
452 }
453
454 entity_name.clone()
456 }
457
458 fn field_type_to_rust(&self, field: &FieldTypeInfo) -> String {
472 let base = self.base_type_to_rust(&field.base_type, &field.rust_type_name);
473
474 let typed = if field.is_array && !matches!(field.base_type, BaseType::Array) {
475 format!("Vec<{}>", base)
476 } else {
477 base
478 };
479
480 if field.is_optional {
483 format!("Option<Option<{}>>", typed)
484 } else {
485 format!("Option<{}>", typed)
486 }
487 }
488
489 fn base_type_to_rust(&self, base_type: &BaseType, rust_type_name: &str) -> String {
490 match base_type {
491 BaseType::Integer => {
492 if rust_type_name.contains("u64") {
493 "u64".to_string()
494 } else if rust_type_name.contains("i64") {
495 "i64".to_string()
496 } else if rust_type_name.contains("u32") {
497 "u32".to_string()
498 } else if rust_type_name.contains("i32") {
499 "i32".to_string()
500 } else {
501 "i64".to_string()
502 }
503 }
504 BaseType::Float => "f64".to_string(),
505 BaseType::String => "String".to_string(),
506 BaseType::Boolean => "bool".to_string(),
507 BaseType::Timestamp => "i64".to_string(),
508 BaseType::Binary => "Vec<u8>".to_string(),
509 BaseType::Pubkey => "String".to_string(),
510 BaseType::Array => "Vec<serde_json::Value>".to_string(),
511 BaseType::Object => "serde_json::Value".to_string(),
512 BaseType::Any => "serde_json::Value".to_string(),
513 }
514 }
515
516 fn serde_attr_for_field(&self, field: &FieldTypeInfo) -> String {
520 if let Some(deser_fn) = self.deserialize_with_for_type(
521 &field.base_type,
522 field.is_optional,
523 field.is_array && !matches!(field.base_type, BaseType::Array),
524 &field.rust_type_name,
525 ) {
526 format!("#[serde(default, deserialize_with = \"{}\")]", deser_fn)
527 } else {
528 "#[serde(default)]".to_string()
529 }
530 }
531
532 fn serde_attr_for_resolved_field(&self, field: &ResolvedField) -> String {
534 if let Some(deser_fn) = self.deserialize_with_for_type(
535 &field.base_type,
536 field.is_optional,
537 field.is_array,
538 &field.field_type,
539 ) {
540 format!("#[serde(default, deserialize_with = \"{}\")]", deser_fn)
541 } else {
542 "#[serde(default)]".to_string()
543 }
544 }
545
546 fn deserialize_with_for_type(
549 &self,
550 base_type: &BaseType,
551 is_optional: bool,
552 is_array: bool,
553 rust_type_name: &str,
554 ) -> Option<String> {
555 let int_kind = match base_type {
557 BaseType::Integer => {
558 if rust_type_name.contains("i64") {
559 "i64"
560 } else if rust_type_name.contains("i32") {
561 "i32"
562 } else if rust_type_name.contains("u32") {
563 "u32"
564 } else {
565 "u64"
566 }
567 }
568 BaseType::Timestamp => "i64",
569 _ => return None,
570 };
571
572 let fn_name = match (is_optional, is_array) {
573 (false, false) => format!("serde_utils::deserialize_option_{}", int_kind),
574 (true, false) => format!("serde_utils::deserialize_option_option_{}", int_kind),
575 (false, true) => format!("serde_utils::deserialize_option_vec_{}", int_kind),
576 (true, true) => format!("serde_utils::deserialize_option_option_vec_{}", int_kind),
577 };
578
579 Some(fn_name)
580 }
581
582 fn resolved_field_to_rust(&self, field: &ResolvedField) -> String {
583 let base = self.base_type_to_rust(&field.base_type, &field.field_type);
584
585 let typed = if field.is_array {
586 format!("Vec<{}>", base)
587 } else {
588 base
589 };
590
591 if field.is_optional {
592 format!("Option<Option<{}>>", typed)
593 } else {
594 format!("Option<{}>", typed)
595 }
596 }
597}
598
599#[derive(Debug, Clone)]
604pub struct RustStackConfig {
605 pub crate_name: String,
606 pub sdk_version: String,
607 pub module_mode: bool,
608 pub url: Option<String>,
609}
610
611impl Default for RustStackConfig {
612 fn default() -> Self {
613 Self {
614 crate_name: "generated-stack".to_string(),
615 sdk_version: "0.2".to_string(),
616 module_mode: false,
617 url: None,
618 }
619 }
620}
621
622pub fn compile_stack_spec(
627 stack_spec: SerializableStackSpec,
628 config: Option<RustStackConfig>,
629) -> Result<RustOutput, String> {
630 let config = config.unwrap_or_default();
631 let stack_name = &stack_spec.stack_name;
632 let stack_kebab = to_kebab_case(stack_name);
633
634 let mut entity_names: Vec<String> = Vec::new();
635 let mut entity_specs: Vec<SerializableStreamSpec> = Vec::new();
636
637 for mut spec in stack_spec.entities {
638 if spec.idl.is_none() {
639 spec.idl = stack_spec.idls.first().cloned();
640 }
641 entity_names.push(spec.state_name.clone());
642 entity_specs.push(spec);
643 }
644
645 let types_rs = generate_stack_types_rs(&entity_specs, &entity_names);
646 let entity_rs = generate_stack_entity_rs(
647 stack_name,
648 &stack_kebab,
649 &entity_specs,
650 &entity_names,
651 &config,
652 );
653 let lib_rs = generate_stack_lib_rs(stack_name, &entity_names, config.module_mode);
654 let cargo_toml = generate_stack_cargo_toml(&config);
655
656 Ok(RustOutput {
657 cargo_toml,
658 lib_rs,
659 types_rs,
660 entity_rs,
661 })
662}
663
664fn generate_stack_cargo_toml(config: &RustStackConfig) -> String {
665 format!(
666 r#"[package]
667name = "{}"
668version = "0.1.0"
669edition = "2021"
670
671[dependencies]
672hyperstack-sdk = "{}"
673serde = {{ version = "1", features = ["derive"] }}
674serde_json = "1"
675"#,
676 config.crate_name, config.sdk_version
677 )
678}
679
680fn generate_stack_lib_rs(stack_name: &str, entity_names: &[String], _module_mode: bool) -> String {
681 let entity_views_exports: Vec<String> = entity_names
682 .iter()
683 .map(|name| format!("{}EntityViews", name))
684 .collect();
685
686 let all_exports = format!(
687 "{}Stack, {}StackViews, {}",
688 stack_name,
689 stack_name,
690 entity_views_exports.join(", ")
691 );
692
693 format!(
694 r#"mod entity;
695mod types;
696
697pub use entity::{{{all_exports}}};
698pub use types::*;
699
700pub use hyperstack_sdk::{{ConnectionState, HyperStack, Stack, Update, Views}};
701"#,
702 all_exports = all_exports
703 )
704}
705
706fn generate_stack_types_rs(
708 entity_specs: &[SerializableStreamSpec],
709 entity_names: &[String],
710) -> String {
711 let mut output = String::new();
712 output.push_str("use serde::{Deserialize, Serialize};\n");
713 output.push_str("use hyperstack_sdk::serde_utils;\n\n");
714
715 let mut generated = HashSet::new();
716
717 for (i, spec) in entity_specs.iter().enumerate() {
718 let entity_name = &entity_names[i];
719 let compiler = RustCompiler::new(spec.clone(), entity_name.clone(), RustConfig::default());
720
721 for section in &spec.sections {
723 if !RustCompiler::is_root_section(§ion.name) {
724 let struct_name = format!("{}{}", entity_name, to_pascal_case(§ion.name));
725 if generated.insert(struct_name) {
726 output.push_str(&compiler.generate_struct_for_section(section));
727 output.push_str("\n\n");
728 }
729 }
730 }
731
732 output.push_str(&compiler.generate_main_entity_struct());
734 output.push_str("\n\n");
735
736 let resolved = compiler.generate_resolved_types(&mut generated);
737 output.push_str(&resolved);
738 while !output.ends_with("\n\n") {
739 output.push('\n');
740 }
741 }
742
743 output.push_str(
745 r#"
746#[derive(Debug, Clone, Serialize, Deserialize)]
747pub struct EventWrapper<T> {
748 #[serde(default, deserialize_with = "serde_utils::deserialize_i64")]
749 pub timestamp: i64,
750 pub data: T,
751 #[serde(default)]
752 pub slot: Option<f64>,
753 #[serde(default)]
754 pub signature: Option<String>,
755}
756
757impl<T: Default> Default for EventWrapper<T> {
758 fn default() -> Self {
759 Self {
760 timestamp: 0,
761 data: T::default(),
762 slot: None,
763 signature: None,
764 }
765 }
766}
767"#,
768 );
769
770 output
771}
772
773fn generate_stack_entity_rs(
775 stack_name: &str,
776 stack_kebab: &str,
777 entity_specs: &[SerializableStreamSpec],
778 entity_names: &[String],
779 config: &RustStackConfig,
780) -> String {
781 let types_import = if config.module_mode {
782 "super::types"
783 } else {
784 "crate::types"
785 };
786
787 let entity_type_imports: Vec<String> =
788 entity_names.iter().map(|name| name.to_string()).collect();
789
790 let url_impl = match &config.url {
791 Some(url) => format!(
792 r#"fn url() -> &'static str {{
793 "{}"
794 }}"#,
795 url
796 ),
797 None => r#"fn url() -> &'static str {
798 "" // TODO: Set URL after first deployment in hyperstack.toml
799 }"#
800 .to_string(),
801 };
802
803 let views_fields: Vec<String> = entity_names
805 .iter()
806 .map(|name| {
807 let snake = to_snake_case(name);
808 format!(" pub {}: {}EntityViews,", snake, name)
809 })
810 .collect();
811
812 let views_builder_fields: Vec<String> = entity_names
814 .iter()
815 .enumerate()
816 .map(|(i, name)| {
817 let snake = to_snake_case(name);
818 if i < entity_names.len() - 1 {
819 format!(
820 " {}: {}EntityViews {{ builder: builder.clone() }},",
821 snake, name
822 )
823 } else {
824 format!(" {}: {}EntityViews {{ builder }},", snake, name)
825 }
826 })
827 .collect();
828
829 let mut entity_views_structs = Vec::new();
831 for (i, entity_name) in entity_names.iter().enumerate() {
832 let spec = &entity_specs[i];
833
834 let derived: Vec<_> = spec
835 .views
836 .iter()
837 .filter(|v| {
838 !v.id.ends_with("/state")
839 && !v.id.ends_with("/list")
840 && v.id.starts_with(entity_name.as_str())
841 })
842 .collect();
843
844 let mut methods = Vec::new();
845
846 methods.push(format!(
848 r#" pub fn state(&self) -> StateView<{entity}> {{
849 StateView::new(
850 self.builder.connection().clone(),
851 self.builder.store().clone(),
852 "{entity}/state".to_string(),
853 self.builder.initial_data_timeout(),
854 )
855 }}"#,
856 entity = entity_name
857 ));
858
859 methods.push(format!(
861 r#"
862 pub fn list(&self) -> ViewHandle<{entity}> {{
863 self.builder.view("{entity}/list")
864 }}"#,
865 entity = entity_name
866 ));
867
868 for view in &derived {
870 let view_name = view.id.split('/').nth(1).unwrap_or("unknown");
871 let method_name = to_snake_case(view_name);
872 methods.push(format!(
873 r#"
874 pub fn {method}(&self) -> ViewHandle<{entity}> {{
875 self.builder.view("{view_id}")
876 }}"#,
877 method = method_name,
878 entity = entity_name,
879 view_id = view.id
880 ));
881 }
882
883 entity_views_structs.push(format!(
884 r#"
885pub struct {entity}EntityViews {{
886 builder: ViewBuilder,
887}}
888
889impl {entity}EntityViews {{
890{methods}
891}}"#,
892 entity = entity_name,
893 methods = methods.join("\n")
894 ));
895 }
896
897 format!(
898 r#"use {types_import}::{{{entity_imports}}};
899use hyperstack_sdk::{{Stack, StateView, ViewBuilder, ViewHandle, Views}};
900
901pub struct {stack}Stack;
902
903impl Stack for {stack}Stack {{
904 type Views = {stack}StackViews;
905
906 fn name() -> &'static str {{
907 "{stack_kebab}"
908 }}
909
910 {url_impl}
911}}
912
913pub struct {stack}StackViews {{
914{views_fields}
915}}
916
917impl Views for {stack}StackViews {{
918 fn from_builder(builder: ViewBuilder) -> Self {{
919 Self {{
920{views_builder}
921 }}
922 }}
923}}
924{entity_views}"#,
925 types_import = types_import,
926 entity_imports = entity_type_imports.join(", "),
927 stack = stack_name,
928 stack_kebab = stack_kebab,
929 url_impl = url_impl,
930 views_fields = views_fields.join("\n"),
931 views_builder = views_builder_fields.join("\n"),
932 entity_views = entity_views_structs.join("\n"),
933 )
934}
935
936fn to_kebab_case(s: &str) -> String {
937 let mut result = String::new();
938 for (i, c) in s.chars().enumerate() {
939 if c.is_uppercase() {
940 if i > 0 {
941 result.push('-');
942 }
943 result.push(c.to_lowercase().next().unwrap());
944 } else {
945 result.push(c);
946 }
947 }
948 result
949}
950
951fn to_pascal_case(s: &str) -> String {
952 s.split(['_', '-', '.'])
953 .map(|word| {
954 let mut chars = word.chars();
955 match chars.next() {
956 None => String::new(),
957 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
958 }
959 })
960 .collect()
961}
962
963fn to_snake_case(s: &str) -> String {
964 let mut result = String::new();
965 for (i, ch) in s.chars().enumerate() {
966 if ch.is_uppercase() {
967 if i > 0 {
968 result.push('_');
969 }
970 result.push(ch.to_lowercase().next().unwrap());
971 } else {
972 result.push(ch);
973 }
974 }
975 result
976}