Skip to main content

arete_interpreter/
rust.rs

1use crate::ast::*;
2use std::collections::{HashMap, 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]
114arete-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 arete_sdk::{{ConnectionState, Arete, 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 arete_sdk::serde_utils;\n\n");
144
145        let resolved_name_map = self.build_resolved_type_name_map();
146        let mut generated = HashSet::new();
147
148        for section in &self.spec.sections {
149            if !Self::is_root_section(&section.name)
150                && section.fields.iter().any(|field| field.emit)
151                && generated.insert(section.name.clone())
152            {
153                output.push_str(&self.generate_struct_for_section(section, &resolved_name_map));
154                output.push_str("\n\n");
155            }
156        }
157
158        output.push_str(&self.generate_main_entity_struct(&resolved_name_map));
159        output.push_str(&self.generate_resolved_types(&resolved_name_map, &mut generated));
160        output.push_str(&self.generate_event_wrapper());
161
162        output
163    }
164
165    pub(crate) fn generate_struct_for_section(
166        &self,
167        section: &EntitySection,
168        resolved_name_map: &HashMap<String, String>,
169    ) -> String {
170        let struct_name = format!("{}{}", self.entity_name, to_pascal_case(&section.name));
171        let mut fields = Vec::new();
172
173        for field in &section.fields {
174            if !field.emit {
175                continue;
176            }
177            let field_name = to_snake_case(&field.field_name);
178            let rust_type = self.field_type_to_rust(field, resolved_name_map);
179            let serde_attr = self.serde_attr_for_field(field);
180
181            fields.push(format!(
182                "    {}\n    pub {}: {},",
183                serde_attr, field_name, rust_type
184            ));
185        }
186
187        format!(
188            "#[derive(Debug, Clone, Serialize, Deserialize, Default)]\npub struct {} {{\n{}\n}}",
189            struct_name,
190            fields.join("\n")
191        )
192    }
193
194    pub(crate) fn is_root_section(name: &str) -> bool {
195        name.eq_ignore_ascii_case("root")
196    }
197
198    pub(crate) fn generate_main_entity_struct(
199        &self,
200        resolved_name_map: &HashMap<String, String>,
201    ) -> String {
202        let mut fields = Vec::new();
203
204        for section in &self.spec.sections {
205            if !Self::is_root_section(&section.name)
206                && section.fields.iter().any(|field| field.emit)
207            {
208                let field_name = to_snake_case(&section.name);
209                let type_name = format!("{}{}", self.entity_name, to_pascal_case(&section.name));
210                fields.push(format!(
211                    "    #[serde(default)]\n    pub {}: {},",
212                    field_name, type_name
213                ));
214            }
215        }
216
217        for section in &self.spec.sections {
218            if Self::is_root_section(&section.name) {
219                for field in &section.fields {
220                    if !field.emit {
221                        continue;
222                    }
223                    let field_name = to_snake_case(&field.field_name);
224                    let rust_type = self.field_type_to_rust(field, resolved_name_map);
225                    let serde_attr = self.serde_attr_for_field(field);
226                    fields.push(format!(
227                        "    {}\n    pub {}: {},",
228                        serde_attr, field_name, rust_type
229                    ));
230                }
231            }
232        }
233
234        format!(
235            "#[derive(Debug, Clone, Serialize, Deserialize, Default)]\npub struct {} {{\n{}\n}}",
236            self.entity_name,
237            fields.join("\n")
238        )
239    }
240
241    pub(crate) fn generate_resolved_types(
242        &self,
243        resolved_name_map: &HashMap<String, String>,
244        generated: &mut HashSet<String>,
245    ) -> String {
246        let mut output = String::new();
247
248        for section in &self.spec.sections {
249            for field in &section.fields {
250                if !field.emit {
251                    continue;
252                }
253                if let Some(resolved) = &field.resolved_type {
254                    let emitted_name = self.resolved_type_to_rust_name(resolved, resolved_name_map);
255                    if generated.insert(emitted_name.clone()) {
256                        output.push_str("\n\n");
257                        output.push_str(&self.generate_resolved_struct(resolved, &emitted_name));
258                    }
259                }
260            }
261        }
262
263        output
264    }
265
266    fn generate_resolved_struct(
267        &self,
268        resolved: &ResolvedStructType,
269        emitted_name: &str,
270    ) -> String {
271        if resolved.is_enum {
272            let variants: Vec<String> = resolved
273                .enum_variants
274                .iter()
275                .map(|v| format!("    {},", to_pascal_case(v)))
276                .collect();
277
278            format!(
279                "#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]\npub enum {} {{\n{}\n}}",
280                emitted_name,
281                variants.join("\n")
282            )
283        } else {
284            let fields: Vec<String> = resolved
285                .fields
286                .iter()
287                .map(|f| {
288                    let rust_type = self.resolved_field_to_rust(f);
289                    let serde_attr = self.serde_attr_for_resolved_field(f);
290                    format!(
291                        "    {}\n    pub {}: {},",
292                        serde_attr,
293                        to_snake_case(&f.field_name),
294                        rust_type
295                    )
296                })
297                .collect();
298
299            format!(
300                "#[derive(Debug, Clone, Serialize, Deserialize, Default)]\npub struct {} {{\n{}\n}}",
301                emitted_name,
302                fields.join("\n")
303            )
304        }
305    }
306
307    fn generate_event_wrapper(&self) -> String {
308        r#"
309
310#[derive(Debug, Clone, Serialize, Deserialize)]
311pub struct EventWrapper<T> {
312    #[serde(default, deserialize_with = "serde_utils::deserialize_i64")]
313    pub timestamp: i64,
314    pub data: T,
315    #[serde(default)]
316    pub slot: Option<f64>,
317    #[serde(default)]
318    pub signature: Option<String>,
319}
320
321impl<T: Default> Default for EventWrapper<T> {
322    fn default() -> Self {
323        Self {
324            timestamp: 0,
325            data: T::default(),
326            slot: None,
327            signature: None,
328        }
329    }
330}
331"#
332        .to_string()
333    }
334
335    fn generate_entity_rs(&self) -> String {
336        let entity_name = &self.entity_name;
337        let stack_name = self.derive_stack_name();
338        let stack_name_kebab = to_kebab_case(entity_name);
339        let entity_snake = to_snake_case(entity_name);
340
341        let types_import = if self.config.module_mode {
342            "super::types"
343        } else {
344            "crate::types"
345        };
346
347        // Generate URL line - either actual URL or placeholder comment
348        let url_impl = match &self.config.url {
349            Some(url) => format!(
350                r#"fn url() -> &'static str {{
351        "{}"
352    }}"#,
353                url
354            ),
355            None => r#"fn url() -> &'static str {
356        "" // TODO: Set URL after first deployment in arete.toml
357    }"#
358            .to_string(),
359        };
360
361        let entity_views = self.generate_entity_views_struct();
362
363        format!(
364            r#"use {types_import}::{entity_name};
365use arete_sdk::{{Stack, StateView, ViewBuilder, ViewHandle, Views}};
366
367pub struct {stack_name}Stack;
368
369impl Stack for {stack_name}Stack {{
370    type Views = {stack_name}StackViews;
371
372    fn name() -> &'static str {{
373        "{stack_name_kebab}"
374    }}
375
376    {url_impl}
377}}
378
379pub struct {stack_name}StackViews {{
380    pub {entity_snake}: {entity_name}EntityViews,
381}}
382
383impl Views for {stack_name}StackViews {{
384    fn from_builder(builder: ViewBuilder) -> Self {{
385        Self {{
386            {entity_snake}: {entity_name}EntityViews {{ builder }},
387        }}
388    }}
389}}
390{entity_views}"#,
391            types_import = types_import,
392            entity_name = entity_name,
393            stack_name = stack_name,
394            stack_name_kebab = stack_name_kebab,
395            entity_snake = entity_snake,
396            url_impl = url_impl,
397            entity_views = entity_views
398        )
399    }
400
401    fn generate_entity_views_struct(&self) -> String {
402        let entity_name = &self.entity_name;
403
404        let derived: Vec<_> = self
405            .spec
406            .views
407            .iter()
408            .filter(|v| {
409                !v.id.ends_with("/state")
410                    && !v.id.ends_with("/list")
411                    && v.id.starts_with(entity_name)
412            })
413            .collect();
414
415        let mut derived_methods = String::new();
416        for view in &derived {
417            let view_name = view.id.split('/').nth(1).unwrap_or("unknown");
418            let method_name = to_snake_case(view_name);
419
420            derived_methods.push_str(&format!(
421                r#"
422    pub fn {method_name}(&self) -> ViewHandle<{entity_name}> {{
423        self.builder.view("{view_id}")
424    }}
425"#,
426                method_name = method_name,
427                entity_name = entity_name,
428                view_id = view.id
429            ));
430        }
431
432        format!(
433            r#"
434pub struct {entity_name}EntityViews {{
435    builder: ViewBuilder,
436}}
437
438impl {entity_name}EntityViews {{
439    pub fn state(&self) -> StateView<{entity_name}> {{
440        StateView::new(
441            self.builder.connection().clone(),
442            self.builder.store().clone(),
443            "{entity_name}/state".to_string(),
444            self.builder.initial_data_timeout(),
445        )
446    }}
447
448    pub fn list(&self) -> ViewHandle<{entity_name}> {{
449        self.builder.view("{entity_name}/list")
450    }}
451{derived_methods}}}"#,
452            entity_name = entity_name,
453            derived_methods = derived_methods
454        )
455    }
456
457    /// Derive stack name from entity name.
458    /// E.g., "OreRound" -> "Ore", "PumpfunToken" -> "Pumpfun"
459    fn derive_stack_name(&self) -> String {
460        let entity_name = &self.entity_name;
461
462        // Common suffixes to strip
463        let suffixes = ["Round", "Token", "Game", "State", "Entity", "Data"];
464
465        for suffix in suffixes {
466            if entity_name.ends_with(suffix) && entity_name.len() > suffix.len() {
467                return entity_name[..entity_name.len() - suffix.len()].to_string();
468            }
469        }
470
471        // If no suffix matched, use the full entity name
472        entity_name.clone()
473    }
474
475    /// Generate Rust type for a field.
476    ///
477    /// All fields are wrapped in Option<T> because we receive partial patches,
478    /// so any field may not yet be present.
479    ///
480    /// - Non-optional spec fields become `Option<T>`:
481    ///   - `None` = not yet received in any patch
482    ///   - `Some(value)` = has value
483    ///
484    /// - Optional spec fields become `Option<Option<T>>`:
485    ///   - `None` = not yet received in any patch
486    ///   - `Some(None)` = explicitly set to null
487    ///   - `Some(Some(value))` = has value
488    fn field_type_to_rust(
489        &self,
490        field: &FieldTypeInfo,
491        _resolved_name_map: &HashMap<String, String>,
492    ) -> String {
493        let base = self.base_type_to_rust(&field.base_type, &field.rust_type_name);
494
495        let typed = if field.is_array && !matches!(field.base_type, BaseType::Array) {
496            format!("Vec<{}>", base)
497        } else {
498            base
499        };
500
501        // All fields wrapped in Option since we receive patches
502        // Optional spec fields get Option<Option<T>> to distinguish "not received" from "explicitly null"
503        if field.is_optional {
504            format!("Option<Option<{}>>", typed)
505        } else {
506            format!("Option<{}>", typed)
507        }
508    }
509
510    fn base_type_to_rust(&self, base_type: &BaseType, rust_type_name: &str) -> String {
511        match base_type {
512            BaseType::Integer => normalized_integer_kind(rust_type_name).to_string(),
513            BaseType::Float => "f64".to_string(),
514            BaseType::String => "String".to_string(),
515            BaseType::Boolean => "bool".to_string(),
516            BaseType::Timestamp => "i64".to_string(),
517            BaseType::Binary => "Vec<u8>".to_string(),
518            BaseType::Pubkey => "String".to_string(),
519            BaseType::Array => "Vec<serde_json::Value>".to_string(),
520            BaseType::Object => "serde_json::Value".to_string(),
521            BaseType::Any => "serde_json::Value".to_string(),
522        }
523    }
524
525    /// Return the `#[serde(...)]` attribute for a field.
526    /// Integer fields get a `deserialize_with` pointing to the appropriate
527    /// `serde_utils` function so that string-encoded big integers are handled.
528    fn serde_attr_for_field(&self, field: &FieldTypeInfo) -> String {
529        if let Some(deser_fn) = self.deserialize_with_for_type(
530            &field.base_type,
531            field.is_optional,
532            field.is_array && !matches!(field.base_type, BaseType::Array),
533            &field.rust_type_name,
534        ) {
535            format!("#[serde(default, deserialize_with = \"{}\")]", deser_fn)
536        } else {
537            "#[serde(default)]".to_string()
538        }
539    }
540
541    /// Same as `serde_attr_for_field` but for resolved struct fields.
542    fn serde_attr_for_resolved_field(&self, field: &ResolvedField) -> String {
543        if let Some(deser_fn) = self.deserialize_with_for_type(
544            &field.base_type,
545            field.is_optional,
546            field.is_array,
547            &field.field_type,
548        ) {
549            format!("#[serde(default, deserialize_with = \"{}\")]", deser_fn)
550        } else {
551            "#[serde(default)]".to_string()
552        }
553    }
554
555    /// Determine the appropriate `serde_utils::deserialize_*` function for a
556    /// given type combination, or `None` if no custom deserializer is needed.
557    fn deserialize_with_for_type(
558        &self,
559        base_type: &BaseType,
560        is_optional: bool,
561        is_array: bool,
562        rust_type_name: &str,
563    ) -> Option<String> {
564        // Only integer and timestamp types need the string-or-number treatment
565        let int_kind = match base_type {
566            BaseType::Integer => normalized_integer_kind(rust_type_name),
567            BaseType::Timestamp => "i64",
568            _ => return None,
569        };
570
571        let fn_name = match (is_optional, is_array) {
572            (false, false) => format!("serde_utils::deserialize_option_{}", int_kind),
573            (true, false) => format!("serde_utils::deserialize_option_option_{}", int_kind),
574            (false, true) => format!("serde_utils::deserialize_option_vec_{}", int_kind),
575            (true, true) => format!("serde_utils::deserialize_option_option_vec_{}", int_kind),
576        };
577
578        Some(fn_name)
579    }
580
581    fn resolved_field_to_rust(&self, field: &ResolvedField) -> String {
582        let base = self.base_type_to_rust(&field.base_type, &field.field_type);
583
584        let typed = if field.is_array {
585            format!("Vec<{}>", base)
586        } else {
587            base
588        };
589
590        if field.is_optional {
591            format!("Option<Option<{}>>", typed)
592        } else {
593            format!("Option<{}>", typed)
594        }
595    }
596
597    fn build_resolved_type_name_map(&self) -> HashMap<String, String> {
598        let mut reserved_names =
599            HashSet::from([self.entity_name.clone(), "EventWrapper".to_string()]);
600
601        for section in &self.spec.sections {
602            if !Self::is_root_section(&section.name)
603                && section.fields.iter().any(|field| field.emit)
604            {
605                reserved_names.insert(format!(
606                    "{}{}",
607                    self.entity_name,
608                    to_pascal_case(&section.name)
609                ));
610            }
611        }
612
613        let mut resolved_name_map = HashMap::new();
614
615        for section in &self.spec.sections {
616            for field in &section.fields {
617                if !field.emit {
618                    continue;
619                }
620
621                let Some(resolved) = &field.resolved_type else {
622                    continue;
623                };
624
625                if resolved_name_map.contains_key(&resolved.type_name) {
626                    continue;
627                }
628
629                let emitted_name = unique_resolved_type_name(resolved, &mut reserved_names);
630                resolved_name_map.insert(resolved.type_name.clone(), emitted_name);
631            }
632        }
633
634        resolved_name_map
635    }
636
637    fn resolved_type_to_rust_name(
638        &self,
639        resolved: &ResolvedStructType,
640        resolved_name_map: &HashMap<String, String>,
641    ) -> String {
642        resolved_name_map
643            .get(&resolved.type_name)
644            .cloned()
645            .unwrap_or_else(|| to_pascal_case(&resolved.type_name))
646    }
647}
648
649fn unique_resolved_type_name(
650    resolved: &ResolvedStructType,
651    reserved_names: &mut HashSet<String>,
652) -> String {
653    let base_name = to_pascal_case(&resolved.type_name);
654    if reserved_names.insert(base_name.clone()) {
655        return base_name;
656    }
657
658    let suffix = if resolved.is_account {
659        "Account"
660    } else if resolved.is_event {
661        "Event"
662    } else if resolved.is_instruction {
663        "Instruction"
664    } else {
665        "Type"
666    };
667
668    let preferred = format!("{}{}", base_name, suffix);
669    if reserved_names.insert(preferred.clone()) {
670        return preferred;
671    }
672
673    let mut index = 2;
674    loop {
675        let candidate = format!("{}{}{}", base_name, suffix, index);
676        if reserved_names.insert(candidate.clone()) {
677            return candidate;
678        }
679        index += 1;
680    }
681}
682
683fn normalized_integer_kind(rust_type_name: &str) -> &'static str {
684    if rust_type_name.contains("u64") {
685        "u64"
686    } else if rust_type_name.contains("i64") {
687        "i64"
688    } else if rust_type_name.contains("u32") {
689        "u32"
690    } else if rust_type_name.contains("i32") {
691        "i32"
692    } else if rust_type_name.contains("u16")
693        || rust_type_name.contains("u8")
694        || rust_type_name.contains("usize")
695    {
696        "u64"
697    } else {
698        // Signed small ints (i16/i8/isize) and anything unknown widen to i64.
699        "i64"
700    }
701}
702
703#[cfg(test)]
704mod tests {
705    use super::*;
706    use std::collections::BTreeMap;
707
708    fn identity_spec() -> IdentitySpec {
709        IdentitySpec {
710            primary_keys: vec!["id.address".to_string()],
711            lookup_indexes: vec![],
712        }
713    }
714
715    #[test]
716    fn rust_generator_renames_account_types_on_collision() {
717        let plan_field = FieldTypeInfo {
718            field_name: "plan".to_string(),
719            raw_name: Some("plan".to_string()),
720            canonical_name: Some("plan".to_string()),
721            rust_type_name: "Option<serde_json::Value>".to_string(),
722            base_type: BaseType::Object,
723            integer_kind: None,
724            is_optional: false,
725            is_array: false,
726            inner_type: Some("Value".to_string()),
727            source_path: None,
728            resolved_type: Some(ResolvedStructType {
729                type_name: "plan".to_string(),
730                fields: vec![],
731                is_instruction: false,
732                is_account: true,
733                is_event: false,
734                is_enum: false,
735                enum_variants: vec![],
736            }),
737            emit: true,
738        };
739
740        let spec = SerializableStreamSpec {
741            ast_version: CURRENT_AST_VERSION.to_string(),
742            state_name: "Plan".to_string(),
743            program_id: None,
744            idl: None,
745            identity: identity_spec(),
746            handlers: vec![],
747            sections: vec![
748                EntitySection {
749                    name: "id".to_string(),
750                    fields: vec![FieldTypeInfo::new(
751                        "address".to_string(),
752                        "String".to_string(),
753                    )],
754                    is_nested_struct: false,
755                    parent_field: None,
756                },
757                EntitySection {
758                    name: "plan".to_string(),
759                    fields: vec![plan_field],
760                    is_nested_struct: false,
761                    parent_field: None,
762                },
763            ],
764            field_mappings: BTreeMap::new(),
765            resolver_hooks: vec![],
766            instruction_hooks: vec![],
767            resolver_specs: vec![],
768            computed_fields: vec![],
769            computed_field_specs: vec![],
770            content_hash: None,
771            views: vec![],
772        };
773
774        let output = compile_serializable_spec(spec, "Plan".to_string(), None)
775            .expect("rust sdk generation should succeed");
776
777        assert!(output
778            .types_rs
779            .contains("pub plan: Option<serde_json::Value>"));
780        assert!(output.types_rs.contains("pub struct PlanAccount"));
781        assert!(!output.types_rs.contains("pub plan: Option<PlanAccount>"));
782        assert!(
783            !output.types_rs.contains("pub struct Plan {\n    #[serde(default, deserialize_with = \"serde_utils::deserialize_option_u64\")]\n    pub discriminator")
784        );
785    }
786
787    #[test]
788    fn rust_generator_keeps_unsigned_numeric_fields_unsigned() {
789        let spec = SerializableStreamSpec {
790            ast_version: CURRENT_AST_VERSION.to_string(),
791            state_name: "Plan".to_string(),
792            program_id: None,
793            idl: None,
794            identity: identity_spec(),
795            handlers: vec![],
796            sections: vec![
797                EntitySection {
798                    name: "id".to_string(),
799                    fields: vec![FieldTypeInfo::new(
800                        "address".to_string(),
801                        "String".to_string(),
802                    )],
803                    is_nested_struct: false,
804                    parent_field: None,
805                },
806                EntitySection {
807                    name: "state".to_string(),
808                    fields: vec![FieldTypeInfo::new(
809                        "status".to_string(),
810                        "Option<u8>".to_string(),
811                    )],
812                    is_nested_struct: false,
813                    parent_field: None,
814                },
815            ],
816            field_mappings: BTreeMap::new(),
817            resolver_hooks: vec![],
818            instruction_hooks: vec![],
819            resolver_specs: vec![],
820            computed_fields: vec![],
821            computed_field_specs: vec![],
822            content_hash: None,
823            views: vec![],
824        };
825
826        let output = compile_serializable_spec(spec, "Plan".to_string(), None)
827            .expect("rust sdk generation should succeed");
828
829        assert!(
830            output.types_rs.contains("pub status: Option<Option<u64>>"),
831            "expected unsigned optional field, got:\n{}",
832            output.types_rs
833        );
834    }
835}
836
837// ============================================================================
838// Stack-level compilation (multi-entity)
839// ============================================================================
840
841#[derive(Debug, Clone)]
842pub struct RustStackConfig {
843    pub crate_name: String,
844    pub sdk_version: String,
845    pub module_mode: bool,
846    pub url: Option<String>,
847}
848
849impl Default for RustStackConfig {
850    fn default() -> Self {
851        Self {
852            crate_name: "generated-stack".to_string(),
853            sdk_version: "0.2".to_string(),
854            module_mode: false,
855            url: None,
856        }
857    }
858}
859
860/// Compile a full SerializableStackSpec (multi-entity) into unified Rust output.
861///
862/// Generates types.rs with ALL entity structs, entity.rs with a single Stack impl
863/// and per-entity EntityViews, and mod.rs/lib.rs re-exporting everything.
864pub fn compile_stack_spec(
865    stack_spec: SerializableStackSpec,
866    config: Option<RustStackConfig>,
867) -> Result<RustOutput, String> {
868    let config = config.unwrap_or_default();
869    let stack_name = &stack_spec.stack_name;
870    let stack_kebab = to_kebab_case(stack_name);
871
872    let mut entity_names: Vec<String> = Vec::new();
873    let mut entity_specs: Vec<SerializableStreamSpec> = Vec::new();
874
875    for mut spec in stack_spec.entities {
876        if spec.idl.is_none() {
877            spec.idl = stack_spec.idls.first().cloned();
878        }
879        entity_names.push(spec.state_name.clone());
880        entity_specs.push(spec);
881    }
882
883    let types_rs = generate_stack_types_rs(&entity_specs, &entity_names);
884    let entity_rs = generate_stack_entity_rs(
885        stack_name,
886        &stack_kebab,
887        &entity_specs,
888        &entity_names,
889        &config,
890    );
891    let lib_rs = generate_stack_lib_rs(stack_name, &entity_names, config.module_mode);
892    let cargo_toml = generate_stack_cargo_toml(&config);
893
894    Ok(RustOutput {
895        cargo_toml,
896        lib_rs,
897        types_rs,
898        entity_rs,
899    })
900}
901
902fn generate_stack_cargo_toml(config: &RustStackConfig) -> String {
903    format!(
904        r#"[package]
905name = "{}"
906version = "0.1.0"
907edition = "2021"
908
909[dependencies]
910arete-sdk = "{}"
911serde = {{ version = "1", features = ["derive"] }}
912serde_json = "1"
913"#,
914        config.crate_name, config.sdk_version
915    )
916}
917
918fn generate_stack_lib_rs(stack_name: &str, entity_names: &[String], _module_mode: bool) -> String {
919    let entity_views_exports: Vec<String> = entity_names
920        .iter()
921        .map(|name| format!("{}EntityViews", name))
922        .collect();
923
924    let all_exports = format!(
925        "{}Stack, {}StackViews, {}",
926        stack_name,
927        stack_name,
928        entity_views_exports.join(", ")
929    );
930
931    format!(
932        r#"mod entity;
933mod types;
934
935pub use entity::{{{all_exports}}};
936pub use types::*;
937
938pub use arete_sdk::{{ConnectionState, Arete, Stack, Update, Views}};
939"#,
940        all_exports = all_exports
941    )
942}
943
944/// Generate types.rs containing structs for ALL entities in the stack.
945fn generate_stack_types_rs(
946    entity_specs: &[SerializableStreamSpec],
947    entity_names: &[String],
948) -> String {
949    let mut output = String::new();
950    output.push_str("use serde::{Deserialize, Serialize};\n");
951    output.push_str("use arete_sdk::serde_utils;\n\n");
952
953    let mut generated = HashSet::new();
954
955    for (i, spec) in entity_specs.iter().enumerate() {
956        let entity_name = &entity_names[i];
957        let compiler = RustCompiler::new(spec.clone(), entity_name.clone(), RustConfig::default());
958        let resolved_name_map = compiler.build_resolved_type_name_map();
959
960        // Generate section structs (e.g., OreRoundId, OreRoundState)
961        for section in &spec.sections {
962            if !RustCompiler::is_root_section(&section.name) {
963                let struct_name = format!("{}{}", entity_name, to_pascal_case(&section.name));
964                if generated.insert(struct_name) {
965                    output.push_str(
966                        &compiler.generate_struct_for_section(section, &resolved_name_map),
967                    );
968                    output.push_str("\n\n");
969                }
970            }
971        }
972
973        // Generate main entity struct (e.g., OreRound, OreTreasury)
974        output.push_str(&compiler.generate_main_entity_struct(&resolved_name_map));
975        output.push_str("\n\n");
976
977        let resolved = compiler.generate_resolved_types(&resolved_name_map, &mut generated);
978        output.push_str(&resolved);
979        while !output.ends_with("\n\n") {
980            output.push('\n');
981        }
982    }
983
984    // Generate EventWrapper once
985    output.push_str(
986        r#"
987#[derive(Debug, Clone, Serialize, Deserialize)]
988pub struct EventWrapper<T> {
989    #[serde(default, deserialize_with = "serde_utils::deserialize_i64")]
990    pub timestamp: i64,
991    pub data: T,
992    #[serde(default)]
993    pub slot: Option<f64>,
994    #[serde(default)]
995    pub signature: Option<String>,
996}
997
998impl<T: Default> Default for EventWrapper<T> {
999    fn default() -> Self {
1000        Self {
1001            timestamp: 0,
1002            data: T::default(),
1003            slot: None,
1004            signature: None,
1005        }
1006    }
1007}
1008"#,
1009    );
1010
1011    output
1012}
1013
1014/// Generate entity.rs with a single Stack impl and per-entity EntityViews.
1015fn generate_stack_entity_rs(
1016    stack_name: &str,
1017    stack_kebab: &str,
1018    entity_specs: &[SerializableStreamSpec],
1019    entity_names: &[String],
1020    config: &RustStackConfig,
1021) -> String {
1022    let types_import = if config.module_mode {
1023        "super::types"
1024    } else {
1025        "crate::types"
1026    };
1027
1028    let entity_type_imports: Vec<String> =
1029        entity_names.iter().map(|name| name.to_string()).collect();
1030
1031    let url_impl = match &config.url {
1032        Some(url) => format!(
1033            r#"fn url() -> &'static str {{
1034        "{}"
1035    }}"#,
1036            url
1037        ),
1038        None => r#"fn url() -> &'static str {
1039        "" // TODO: Set URL after first deployment in arete.toml
1040    }"#
1041        .to_string(),
1042    };
1043
1044    // StackViews struct fields
1045    let views_fields: Vec<String> = entity_names
1046        .iter()
1047        .map(|name| {
1048            let snake = to_snake_case(name);
1049            format!("    pub {}: {}EntityViews,", snake, name)
1050        })
1051        .collect();
1052
1053    // Views::from_builder body — clone builder for all but last entity
1054    let views_builder_fields: Vec<String> = entity_names
1055        .iter()
1056        .enumerate()
1057        .map(|(i, name)| {
1058            let snake = to_snake_case(name);
1059            if i < entity_names.len() - 1 {
1060                format!(
1061                    "            {}: {}EntityViews {{ builder: builder.clone() }},",
1062                    snake, name
1063                )
1064            } else {
1065                format!("            {}: {}EntityViews {{ builder }},", snake, name)
1066            }
1067        })
1068        .collect();
1069
1070    // Per-entity EntityViews structs
1071    let mut entity_views_structs = Vec::new();
1072    for (i, entity_name) in entity_names.iter().enumerate() {
1073        let spec = &entity_specs[i];
1074
1075        let derived: Vec<_> = spec
1076            .views
1077            .iter()
1078            .filter(|v| {
1079                !v.id.ends_with("/state")
1080                    && !v.id.ends_with("/list")
1081                    && v.id.starts_with(entity_name.as_str())
1082            })
1083            .collect();
1084
1085        let mut methods = Vec::new();
1086
1087        // state() method — always present
1088        methods.push(format!(
1089            r#"    pub fn state(&self) -> StateView<{entity}> {{
1090        StateView::new(
1091            self.builder.connection().clone(),
1092            self.builder.store().clone(),
1093            "{entity}/state".to_string(),
1094            self.builder.initial_data_timeout(),
1095        )
1096    }}"#,
1097            entity = entity_name
1098        ));
1099
1100        // Always include list view (built-in view, like state)
1101        methods.push(format!(
1102            r#"
1103    pub fn list(&self) -> ViewHandle<{entity}> {{
1104        self.builder.view("{entity}/list")
1105    }}"#,
1106            entity = entity_name
1107        ));
1108
1109        // Derived view methods
1110        for view in &derived {
1111            let view_name = view.id.split('/').nth(1).unwrap_or("unknown");
1112            let method_name = to_snake_case(view_name);
1113            methods.push(format!(
1114                r#"
1115    pub fn {method}(&self) -> ViewHandle<{entity}> {{
1116        self.builder.view("{view_id}")
1117    }}"#,
1118                method = method_name,
1119                entity = entity_name,
1120                view_id = view.id
1121            ));
1122        }
1123
1124        entity_views_structs.push(format!(
1125            r#"
1126pub struct {entity}EntityViews {{
1127    builder: ViewBuilder,
1128}}
1129
1130impl {entity}EntityViews {{
1131{methods}
1132}}"#,
1133            entity = entity_name,
1134            methods = methods.join("\n")
1135        ));
1136    }
1137
1138    format!(
1139        r#"use {types_import}::{{{entity_imports}}};
1140use arete_sdk::{{Stack, StateView, ViewBuilder, ViewHandle, Views}};
1141
1142pub struct {stack}Stack;
1143
1144impl Stack for {stack}Stack {{
1145    type Views = {stack}StackViews;
1146
1147    fn name() -> &'static str {{
1148        "{stack_kebab}"
1149    }}
1150
1151    {url_impl}
1152}}
1153
1154pub struct {stack}StackViews {{
1155{views_fields}
1156}}
1157
1158impl Views for {stack}StackViews {{
1159    fn from_builder(builder: ViewBuilder) -> Self {{
1160        Self {{
1161{views_builder}
1162        }}
1163    }}
1164}}
1165{entity_views}"#,
1166        types_import = types_import,
1167        entity_imports = entity_type_imports.join(", "),
1168        stack = stack_name,
1169        stack_kebab = stack_kebab,
1170        url_impl = url_impl,
1171        views_fields = views_fields.join("\n"),
1172        views_builder = views_builder_fields.join("\n"),
1173        entity_views = entity_views_structs.join("\n"),
1174    )
1175}
1176
1177fn to_kebab_case(s: &str) -> String {
1178    let mut result = String::new();
1179    for (i, c) in s.chars().enumerate() {
1180        if c.is_uppercase() {
1181            if i > 0 {
1182                result.push('-');
1183            }
1184            result.push(c.to_lowercase().next().unwrap());
1185        } else {
1186            result.push(c);
1187        }
1188    }
1189    result
1190}
1191
1192fn to_pascal_case(s: &str) -> String {
1193    s.split(['_', '-', '.'])
1194        .map(|word| {
1195            let mut chars = word.chars();
1196            match chars.next() {
1197                None => String::new(),
1198                Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
1199            }
1200        })
1201        .collect()
1202}
1203
1204fn to_snake_case(s: &str) -> String {
1205    let mut result = String::new();
1206    for (i, ch) in s.chars().enumerate() {
1207        if ch.is_uppercase() {
1208            if i > 0 {
1209                result.push('_');
1210            }
1211            result.push(ch.to_lowercase().next().unwrap());
1212        } else {
1213            result.push(ch);
1214        }
1215    }
1216    result
1217}