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");
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(&section.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(&section.name));
166        let mut fields = Vec::new();
167
168        for field in &section.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(&section.name)
198                && section.fields.iter().any(|field| field.emit)
199            {
200                let field_name = to_snake_case(&section.name);
201                let type_name = format!("{}{}", self.entity_name, to_pascal_case(&section.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(&section.name) {
211                for field in &section.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 &section.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        // Generate URL line - either actual URL or placeholder comment
331        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    /// Derive stack name from entity name.
441    /// E.g., "OreRound" -> "Ore", "PumpfunToken" -> "Pumpfun"
442    fn derive_stack_name(&self) -> String {
443        let entity_name = &self.entity_name;
444
445        // Common suffixes to strip
446        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        // If no suffix matched, use the full entity name
455        entity_name.clone()
456    }
457
458    /// Generate Rust type for a field.
459    ///
460    /// All fields are wrapped in Option<T> because we receive partial patches,
461    /// so any field may not yet be present.
462    ///
463    /// - Non-optional spec fields become `Option<T>`:
464    ///   - `None` = not yet received in any patch
465    ///   - `Some(value)` = has value
466    ///
467    /// - Optional spec fields become `Option<Option<T>>`:
468    ///   - `None` = not yet received in any patch
469    ///   - `Some(None)` = explicitly set to null
470    ///   - `Some(Some(value))` = has value
471    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        // All fields wrapped in Option since we receive patches
481        // Optional spec fields get Option<Option<T>> to distinguish "not received" from "explicitly null"
482        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    /// Return the `#[serde(...)]` attribute for a field.
517    /// Integer fields get a `deserialize_with` pointing to the appropriate
518    /// `serde_utils` function so that string-encoded big integers are handled.
519    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    /// Same as `serde_attr_for_field` but for resolved struct fields.
533    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    /// Determine the appropriate `serde_utils::deserialize_*` function for a
547    /// given type combination, or `None` if no custom deserializer is needed.
548    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        // Only integer and timestamp types need the string-or-number treatment
556        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// ============================================================================
600// Stack-level compilation (multi-entity)
601// ============================================================================
602
603#[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
622/// Compile a full SerializableStackSpec (multi-entity) into unified Rust output.
623///
624/// Generates types.rs with ALL entity structs, entity.rs with a single Stack impl
625/// and per-entity EntityViews, and mod.rs/lib.rs re-exporting everything.
626pub 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
706/// Generate types.rs containing structs for ALL entities in the stack.
707fn 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        // Generate section structs (e.g., OreRoundId, OreRoundState)
722        for section in &spec.sections {
723            if !RustCompiler::is_root_section(&section.name) {
724                let struct_name = format!("{}{}", entity_name, to_pascal_case(&section.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        // Generate main entity struct (e.g., OreRound, OreTreasury)
733        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    // Generate EventWrapper once
744    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
773/// Generate entity.rs with a single Stack impl and per-entity EntityViews.
774fn 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    // StackViews struct fields
804    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    // Views::from_builder body — clone builder for all but last entity
813    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    // Per-entity EntityViews structs
830    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        // state() method — always present
847        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        // Always include list view (built-in view, like state)
860        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        // Derived view methods
869        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}