Skip to main content

hyperstack_interpreter/
rust.rs

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    /// WebSocket URL for the stack. If None, generates a placeholder comment.
31    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\n");
143
144        let mut generated = HashSet::new();
145
146        for section in &self.spec.sections {
147            if !Self::is_root_section(&section.name)
148                && section.fields.iter().any(|field| field.emit)
149                && generated.insert(section.name.clone())
150            {
151                output.push_str(&self.generate_struct_for_section(section));
152                output.push_str("\n\n");
153            }
154        }
155
156        output.push_str(&self.generate_main_entity_struct());
157        output.push_str(&self.generate_resolved_types(&mut generated));
158        output.push_str(&self.generate_event_wrapper());
159
160        output
161    }
162
163    pub(crate) fn generate_struct_for_section(&self, section: &EntitySection) -> String {
164        let struct_name = format!("{}{}", self.entity_name, to_pascal_case(&section.name));
165        let mut fields = Vec::new();
166
167        for field in &section.fields {
168            if !field.emit {
169                continue;
170            }
171            let field_name = to_snake_case(&field.field_name);
172            let rust_type = self.field_type_to_rust(field);
173
174            fields.push(format!(
175                "    #[serde(default)]\n    pub {}: {},",
176                field_name, rust_type
177            ));
178        }
179
180        format!(
181            "#[derive(Debug, Clone, Serialize, Deserialize, Default)]\npub struct {} {{\n{}\n}}",
182            struct_name,
183            fields.join("\n")
184        )
185    }
186
187    pub(crate) fn is_root_section(name: &str) -> bool {
188        name.eq_ignore_ascii_case("root")
189    }
190
191    pub(crate) fn generate_main_entity_struct(&self) -> String {
192        let mut fields = Vec::new();
193
194        for section in &self.spec.sections {
195            if !Self::is_root_section(&section.name)
196                && section.fields.iter().any(|field| field.emit)
197            {
198                let field_name = to_snake_case(&section.name);
199                let type_name = format!("{}{}", self.entity_name, to_pascal_case(&section.name));
200                fields.push(format!(
201                    "    #[serde(default)]\n    pub {}: {},",
202                    field_name, type_name
203                ));
204            }
205        }
206
207        for section in &self.spec.sections {
208            if Self::is_root_section(&section.name) {
209                for field in &section.fields {
210                    if !field.emit {
211                        continue;
212                    }
213                    let field_name = to_snake_case(&field.field_name);
214                    let rust_type = self.field_type_to_rust(field);
215                    fields.push(format!(
216                        "    #[serde(default)]\n    pub {}: {},",
217                        field_name, rust_type
218                    ));
219                }
220            }
221        }
222
223        format!(
224            "#[derive(Debug, Clone, Serialize, Deserialize, Default)]\npub struct {} {{\n{}\n}}",
225            self.entity_name,
226            fields.join("\n")
227        )
228    }
229
230    pub(crate) fn generate_resolved_types(&self, generated: &mut HashSet<String>) -> String {
231        let mut output = String::new();
232
233        for section in &self.spec.sections {
234            for field in &section.fields {
235                if !field.emit {
236                    continue;
237                }
238                if let Some(resolved) = &field.resolved_type {
239                    if generated.insert(resolved.type_name.clone()) {
240                        output.push_str("\n\n");
241                        output.push_str(&self.generate_resolved_struct(resolved));
242                    }
243                }
244            }
245        }
246
247        output
248    }
249
250    fn generate_resolved_struct(&self, resolved: &ResolvedStructType) -> String {
251        if resolved.is_enum {
252            let variants: Vec<String> = resolved
253                .enum_variants
254                .iter()
255                .map(|v| format!("    {},", to_pascal_case(v)))
256                .collect();
257
258            format!(
259                "#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]\npub enum {} {{\n{}\n}}",
260                to_pascal_case(&resolved.type_name),
261                variants.join("\n")
262            )
263        } else {
264            let fields: Vec<String> = resolved
265                .fields
266                .iter()
267                .map(|f| {
268                    let rust_type = self.resolved_field_to_rust(f);
269                    format!(
270                        "    #[serde(default)]\n    pub {}: {},",
271                        to_snake_case(&f.field_name),
272                        rust_type
273                    )
274                })
275                .collect();
276
277            format!(
278                "#[derive(Debug, Clone, Serialize, Deserialize, Default)]\npub struct {} {{\n{}\n}}",
279                to_pascal_case(&resolved.type_name),
280                fields.join("\n")
281            )
282        }
283    }
284
285    fn generate_event_wrapper(&self) -> String {
286        r#"
287
288#[derive(Debug, Clone, Serialize, Deserialize)]
289pub struct EventWrapper<T> {
290    #[serde(default)]
291    pub timestamp: i64,
292    pub data: T,
293    #[serde(default)]
294    pub slot: Option<f64>,
295    #[serde(default)]
296    pub signature: Option<String>,
297}
298
299impl<T: Default> Default for EventWrapper<T> {
300    fn default() -> Self {
301        Self {
302            timestamp: 0,
303            data: T::default(),
304            slot: None,
305            signature: None,
306        }
307    }
308}
309"#
310        .to_string()
311    }
312
313    fn generate_entity_rs(&self) -> String {
314        let entity_name = &self.entity_name;
315        let stack_name = self.derive_stack_name();
316        let stack_name_kebab = to_kebab_case(entity_name);
317        let entity_snake = to_snake_case(entity_name);
318
319        let types_import = if self.config.module_mode {
320            "super::types"
321        } else {
322            "crate::types"
323        };
324
325        // Generate URL line - either actual URL or placeholder comment
326        let url_impl = match &self.config.url {
327            Some(url) => format!(
328                r#"fn url() -> &'static str {{
329        "{}"
330    }}"#,
331                url
332            ),
333            None => r#"fn url() -> &'static str {
334        "" // TODO: Set URL after first deployment in hyperstack.toml
335    }"#
336            .to_string(),
337        };
338
339        let entity_views = self.generate_entity_views_struct();
340
341        format!(
342            r#"use {types_import}::{entity_name};
343use hyperstack_sdk::{{Stack, StateView, ViewBuilder, ViewHandle, Views}};
344
345pub struct {stack_name}Stack;
346
347impl Stack for {stack_name}Stack {{
348    type Views = {stack_name}StackViews;
349
350    fn name() -> &'static str {{
351        "{stack_name_kebab}"
352    }}
353
354    {url_impl}
355}}
356
357pub struct {stack_name}StackViews {{
358    pub {entity_snake}: {entity_name}EntityViews,
359}}
360
361impl Views for {stack_name}StackViews {{
362    fn from_builder(builder: ViewBuilder) -> Self {{
363        Self {{
364            {entity_snake}: {entity_name}EntityViews {{ builder }},
365        }}
366    }}
367}}
368{entity_views}"#,
369            types_import = types_import,
370            entity_name = entity_name,
371            stack_name = stack_name,
372            stack_name_kebab = stack_name_kebab,
373            entity_snake = entity_snake,
374            url_impl = url_impl,
375            entity_views = entity_views
376        )
377    }
378
379    fn generate_entity_views_struct(&self) -> String {
380        let entity_name = &self.entity_name;
381
382        let derived: Vec<_> = self
383            .spec
384            .views
385            .iter()
386            .filter(|v| {
387                !v.id.ends_with("/state")
388                    && !v.id.ends_with("/list")
389                    && v.id.starts_with(entity_name)
390            })
391            .collect();
392
393        let mut derived_methods = String::new();
394        for view in &derived {
395            let view_name = view.id.split('/').nth(1).unwrap_or("unknown");
396            let method_name = to_snake_case(view_name);
397
398            derived_methods.push_str(&format!(
399                r#"
400    pub fn {method_name}(&self) -> ViewHandle<{entity_name}> {{
401        self.builder.view("{view_id}")
402    }}
403"#,
404                method_name = method_name,
405                entity_name = entity_name,
406                view_id = view.id
407            ));
408        }
409
410        format!(
411            r#"
412pub struct {entity_name}EntityViews {{
413    builder: ViewBuilder,
414}}
415
416impl {entity_name}EntityViews {{
417    pub fn state(&self) -> StateView<{entity_name}> {{
418        StateView::new(
419            self.builder.connection().clone(),
420            self.builder.store().clone(),
421            "{entity_name}/state".to_string(),
422            self.builder.initial_data_timeout(),
423        )
424    }}
425
426    pub fn list(&self) -> ViewHandle<{entity_name}> {{
427        self.builder.view("{entity_name}/list")
428    }}
429{derived_methods}}}"#,
430            entity_name = entity_name,
431            derived_methods = derived_methods
432        )
433    }
434
435    /// Derive stack name from entity name.
436    /// E.g., "OreRound" -> "Ore", "PumpfunToken" -> "Pumpfun"
437    fn derive_stack_name(&self) -> String {
438        let entity_name = &self.entity_name;
439
440        // Common suffixes to strip
441        let suffixes = ["Round", "Token", "Game", "State", "Entity", "Data"];
442
443        for suffix in suffixes {
444            if entity_name.ends_with(suffix) && entity_name.len() > suffix.len() {
445                return entity_name[..entity_name.len() - suffix.len()].to_string();
446            }
447        }
448
449        // If no suffix matched, use the full entity name
450        entity_name.clone()
451    }
452
453    /// Generate Rust type for a field.
454    ///
455    /// All fields are wrapped in Option<T> because we receive partial patches,
456    /// so any field may not yet be present.
457    ///
458    /// - Non-optional spec fields become `Option<T>`:
459    ///   - `None` = not yet received in any patch
460    ///   - `Some(value)` = has value
461    ///
462    /// - Optional spec fields become `Option<Option<T>>`:
463    ///   - `None` = not yet received in any patch
464    ///   - `Some(None)` = explicitly set to null
465    ///   - `Some(Some(value))` = has value
466    fn field_type_to_rust(&self, field: &FieldTypeInfo) -> String {
467        let base = self.base_type_to_rust(&field.base_type, &field.rust_type_name);
468
469        let typed = if field.is_array && !matches!(field.base_type, BaseType::Array) {
470            format!("Vec<{}>", base)
471        } else {
472            base
473        };
474
475        // All fields wrapped in Option since we receive patches
476        // Optional spec fields get Option<Option<T>> to distinguish "not received" from "explicitly null"
477        if field.is_optional {
478            format!("Option<Option<{}>>", typed)
479        } else {
480            format!("Option<{}>", typed)
481        }
482    }
483
484    fn base_type_to_rust(&self, base_type: &BaseType, rust_type_name: &str) -> String {
485        match base_type {
486            BaseType::Integer => {
487                if rust_type_name.contains("u64") {
488                    "u64".to_string()
489                } else if rust_type_name.contains("i64") {
490                    "i64".to_string()
491                } else if rust_type_name.contains("u32") {
492                    "u32".to_string()
493                } else if rust_type_name.contains("i32") {
494                    "i32".to_string()
495                } else {
496                    "i64".to_string()
497                }
498            }
499            BaseType::Float => "f64".to_string(),
500            BaseType::String => "String".to_string(),
501            BaseType::Boolean => "bool".to_string(),
502            BaseType::Timestamp => "i64".to_string(),
503            BaseType::Binary => "Vec<u8>".to_string(),
504            BaseType::Pubkey => "String".to_string(),
505            BaseType::Array => "Vec<serde_json::Value>".to_string(),
506            BaseType::Object => "serde_json::Value".to_string(),
507            BaseType::Any => "serde_json::Value".to_string(),
508        }
509    }
510
511    fn resolved_field_to_rust(&self, field: &ResolvedField) -> String {
512        let base = self.base_type_to_rust(&field.base_type, &field.field_type);
513
514        let typed = if field.is_array {
515            format!("Vec<{}>", base)
516        } else {
517            base
518        };
519
520        if field.is_optional {
521            format!("Option<Option<{}>>", typed)
522        } else {
523            format!("Option<{}>", typed)
524        }
525    }
526}
527
528// ============================================================================
529// Stack-level compilation (multi-entity)
530// ============================================================================
531
532#[derive(Debug, Clone)]
533pub struct RustStackConfig {
534    pub crate_name: String,
535    pub sdk_version: String,
536    pub module_mode: bool,
537    pub url: Option<String>,
538}
539
540impl Default for RustStackConfig {
541    fn default() -> Self {
542        Self {
543            crate_name: "generated-stack".to_string(),
544            sdk_version: "0.2".to_string(),
545            module_mode: false,
546            url: None,
547        }
548    }
549}
550
551/// Compile a full SerializableStackSpec (multi-entity) into unified Rust output.
552///
553/// Generates types.rs with ALL entity structs, entity.rs with a single Stack impl
554/// and per-entity EntityViews, and mod.rs/lib.rs re-exporting everything.
555pub fn compile_stack_spec(
556    stack_spec: SerializableStackSpec,
557    config: Option<RustStackConfig>,
558) -> Result<RustOutput, String> {
559    let config = config.unwrap_or_default();
560    let stack_name = &stack_spec.stack_name;
561    let stack_kebab = to_kebab_case(stack_name);
562
563    let mut entity_names: Vec<String> = Vec::new();
564    let mut entity_specs: Vec<SerializableStreamSpec> = Vec::new();
565
566    for mut spec in stack_spec.entities {
567        if spec.idl.is_none() {
568            spec.idl = stack_spec.idls.first().cloned();
569        }
570        entity_names.push(spec.state_name.clone());
571        entity_specs.push(spec);
572    }
573
574    let types_rs = generate_stack_types_rs(&entity_specs, &entity_names);
575    let entity_rs = generate_stack_entity_rs(
576        stack_name,
577        &stack_kebab,
578        &entity_specs,
579        &entity_names,
580        &config,
581    );
582    let lib_rs = generate_stack_lib_rs(stack_name, &entity_names, config.module_mode);
583    let cargo_toml = generate_stack_cargo_toml(&config);
584
585    Ok(RustOutput {
586        cargo_toml,
587        lib_rs,
588        types_rs,
589        entity_rs,
590    })
591}
592
593fn generate_stack_cargo_toml(config: &RustStackConfig) -> String {
594    format!(
595        r#"[package]
596name = "{}"
597version = "0.1.0"
598edition = "2021"
599
600[dependencies]
601hyperstack-sdk = "{}"
602serde = {{ version = "1", features = ["derive"] }}
603serde_json = "1"
604"#,
605        config.crate_name, config.sdk_version
606    )
607}
608
609fn generate_stack_lib_rs(stack_name: &str, entity_names: &[String], _module_mode: bool) -> String {
610    let entity_views_exports: Vec<String> = entity_names
611        .iter()
612        .map(|name| format!("{}EntityViews", name))
613        .collect();
614
615    let all_exports = format!(
616        "{}Stack, {}StackViews, {}",
617        stack_name,
618        stack_name,
619        entity_views_exports.join(", ")
620    );
621
622    format!(
623        r#"mod entity;
624mod types;
625
626pub use entity::{{{all_exports}}};
627pub use types::*;
628
629pub use hyperstack_sdk::{{ConnectionState, HyperStack, Stack, Update, Views}};
630"#,
631        all_exports = all_exports
632    )
633}
634
635/// Generate types.rs containing structs for ALL entities in the stack.
636fn generate_stack_types_rs(
637    entity_specs: &[SerializableStreamSpec],
638    entity_names: &[String],
639) -> String {
640    let mut output = String::new();
641    output.push_str("use serde::{Deserialize, Serialize};\n\n");
642
643    let mut generated = HashSet::new();
644
645    for (i, spec) in entity_specs.iter().enumerate() {
646        let entity_name = &entity_names[i];
647        let compiler = RustCompiler::new(spec.clone(), entity_name.clone(), RustConfig::default());
648
649        // Generate section structs (e.g., OreRoundId, OreRoundState)
650        for section in &spec.sections {
651            if !RustCompiler::is_root_section(&section.name) {
652                let struct_name = format!("{}{}", entity_name, to_pascal_case(&section.name));
653                if generated.insert(struct_name) {
654                    output.push_str(&compiler.generate_struct_for_section(section));
655                    output.push_str("\n\n");
656                }
657            }
658        }
659
660        // Generate main entity struct (e.g., OreRound, OreTreasury)
661        output.push_str(&compiler.generate_main_entity_struct());
662        output.push_str("\n\n");
663
664        let resolved = compiler.generate_resolved_types(&mut generated);
665        output.push_str(&resolved);
666        while !output.ends_with("\n\n") {
667            output.push('\n');
668        }
669    }
670
671    // Generate EventWrapper once
672    output.push_str(
673        r#"
674#[derive(Debug, Clone, Serialize, Deserialize)]
675pub struct EventWrapper<T> {
676    #[serde(default)]
677    pub timestamp: i64,
678    pub data: T,
679    #[serde(default)]
680    pub slot: Option<f64>,
681    #[serde(default)]
682    pub signature: Option<String>,
683}
684
685impl<T: Default> Default for EventWrapper<T> {
686    fn default() -> Self {
687        Self {
688            timestamp: 0,
689            data: T::default(),
690            slot: None,
691            signature: None,
692        }
693    }
694}
695"#,
696    );
697
698    output
699}
700
701/// Generate entity.rs with a single Stack impl and per-entity EntityViews.
702fn generate_stack_entity_rs(
703    stack_name: &str,
704    stack_kebab: &str,
705    entity_specs: &[SerializableStreamSpec],
706    entity_names: &[String],
707    config: &RustStackConfig,
708) -> String {
709    let types_import = if config.module_mode {
710        "super::types"
711    } else {
712        "crate::types"
713    };
714
715    let entity_type_imports: Vec<String> =
716        entity_names.iter().map(|name| name.to_string()).collect();
717
718    let url_impl = match &config.url {
719        Some(url) => format!(
720            r#"fn url() -> &'static str {{
721        "{}"
722    }}"#,
723            url
724        ),
725        None => r#"fn url() -> &'static str {
726        "" // TODO: Set URL after first deployment in hyperstack.toml
727    }"#
728        .to_string(),
729    };
730
731    // StackViews struct fields
732    let views_fields: Vec<String> = entity_names
733        .iter()
734        .map(|name| {
735            let snake = to_snake_case(name);
736            format!("    pub {}: {}EntityViews,", snake, name)
737        })
738        .collect();
739
740    // Views::from_builder body — clone builder for all but last entity
741    let views_builder_fields: Vec<String> = entity_names
742        .iter()
743        .enumerate()
744        .map(|(i, name)| {
745            let snake = to_snake_case(name);
746            if i < entity_names.len() - 1 {
747                format!(
748                    "            {}: {}EntityViews {{ builder: builder.clone() }},",
749                    snake, name
750                )
751            } else {
752                format!("            {}: {}EntityViews {{ builder }},", snake, name)
753            }
754        })
755        .collect();
756
757    // Per-entity EntityViews structs
758    let mut entity_views_structs = Vec::new();
759    for (i, entity_name) in entity_names.iter().enumerate() {
760        let spec = &entity_specs[i];
761
762        let derived: Vec<_> = spec
763            .views
764            .iter()
765            .filter(|v| {
766                !v.id.ends_with("/state")
767                    && !v.id.ends_with("/list")
768                    && v.id.starts_with(entity_name.as_str())
769            })
770            .collect();
771
772        let mut methods = Vec::new();
773
774        // state() method — always present
775        methods.push(format!(
776            r#"    pub fn state(&self) -> StateView<{entity}> {{
777        StateView::new(
778            self.builder.connection().clone(),
779            self.builder.store().clone(),
780            "{entity}/state".to_string(),
781            self.builder.initial_data_timeout(),
782        )
783    }}"#,
784            entity = entity_name
785        ));
786
787        // Always include list view (built-in view, like state)
788        methods.push(format!(
789            r#"
790    pub fn list(&self) -> ViewHandle<{entity}> {{
791        self.builder.view("{entity}/list")
792    }}"#,
793            entity = entity_name
794        ));
795
796        // Derived view methods
797        for view in &derived {
798            let view_name = view.id.split('/').nth(1).unwrap_or("unknown");
799            let method_name = to_snake_case(view_name);
800            methods.push(format!(
801                r#"
802    pub fn {method}(&self) -> ViewHandle<{entity}> {{
803        self.builder.view("{view_id}")
804    }}"#,
805                method = method_name,
806                entity = entity_name,
807                view_id = view.id
808            ));
809        }
810
811        entity_views_structs.push(format!(
812            r#"
813pub struct {entity}EntityViews {{
814    builder: ViewBuilder,
815}}
816
817impl {entity}EntityViews {{
818{methods}
819}}"#,
820            entity = entity_name,
821            methods = methods.join("\n")
822        ));
823    }
824
825    format!(
826        r#"use {types_import}::{{{entity_imports}}};
827use hyperstack_sdk::{{Stack, StateView, ViewBuilder, ViewHandle, Views}};
828
829pub struct {stack}Stack;
830
831impl Stack for {stack}Stack {{
832    type Views = {stack}StackViews;
833
834    fn name() -> &'static str {{
835        "{stack_kebab}"
836    }}
837
838    {url_impl}
839}}
840
841pub struct {stack}StackViews {{
842{views_fields}
843}}
844
845impl Views for {stack}StackViews {{
846    fn from_builder(builder: ViewBuilder) -> Self {{
847        Self {{
848{views_builder}
849        }}
850    }}
851}}
852{entity_views}"#,
853        types_import = types_import,
854        entity_imports = entity_type_imports.join(", "),
855        stack = stack_name,
856        stack_kebab = stack_kebab,
857        url_impl = url_impl,
858        views_fields = views_fields.join("\n"),
859        views_builder = views_builder_fields.join("\n"),
860        entity_views = entity_views_structs.join("\n"),
861    )
862}
863
864fn to_kebab_case(s: &str) -> String {
865    let mut result = String::new();
866    for (i, c) in s.chars().enumerate() {
867        if c.is_uppercase() {
868            if i > 0 {
869                result.push('-');
870            }
871            result.push(c.to_lowercase().next().unwrap());
872        } else {
873            result.push(c);
874        }
875    }
876    result
877}
878
879fn to_pascal_case(s: &str) -> String {
880    s.split(['_', '-', '.'])
881        .map(|word| {
882            let mut chars = word.chars();
883            match chars.next() {
884                None => String::new(),
885                Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
886            }
887        })
888        .collect()
889}
890
891fn to_snake_case(s: &str) -> String {
892    let mut result = String::new();
893    for (i, ch) in s.chars().enumerate() {
894        if ch.is_uppercase() {
895            if i > 0 {
896                result.push('_');
897            }
898            result.push(ch.to_lowercase().next().unwrap());
899        } else {
900            result.push(ch);
901        }
902    }
903    result
904}