Skip to main content

arete_interpreter/
rust.rs

1use crate::ast::*;
2use std::collections::{BTreeMap, 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.3".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 = {{ package = "arete-a4-sdk", version = "{}" }}
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    #[test]
837    fn generated_manifest_uses_published_arete_sdk_package() {
838        let manifest = generate_stack_cargo_toml(&RustStackConfig::default());
839
840        assert!(manifest.contains("arete-sdk = { package = \"arete-a4-sdk\", version = \"0.3\" }"));
841    }
842}
843
844// ============================================================================
845// Stack-level compilation (multi-entity)
846// ============================================================================
847
848#[derive(Debug, Clone)]
849pub struct RustStackConfig {
850    pub crate_name: String,
851    pub sdk_version: String,
852    pub module_mode: bool,
853    pub url: Option<String>,
854}
855
856#[derive(Debug, Clone, Default)]
857pub struct RustCompositionConfig {
858    pub stack: RustStackConfig,
859    pub live_urls: BTreeMap<String, String>,
860}
861
862#[derive(Debug, Clone)]
863pub struct RustAliasedStackOutput {
864    pub alias: String,
865    pub module_name: String,
866    pub output: RustOutput,
867}
868
869#[derive(Debug, Clone)]
870pub struct RustCompositionOutput {
871    pub name: String,
872    pub cargo_toml: String,
873    pub lib_rs: String,
874    pub live_stacks: Vec<RustAliasedStackOutput>,
875}
876
877impl Default for RustStackConfig {
878    fn default() -> Self {
879        Self {
880            crate_name: "generated-stack".to_string(),
881            sdk_version: "0.3".to_string(),
882            module_mode: false,
883            url: None,
884        }
885    }
886}
887
888/// Compile a full SerializableStackSpec (multi-entity) into unified Rust output.
889///
890/// Generates types.rs with ALL entity structs, entity.rs with a single Stack impl
891/// and per-entity EntityViews, and mod.rs/lib.rs re-exporting everything.
892pub fn compile_stack_spec(
893    stack_spec: SerializableStackSpec,
894    config: Option<RustStackConfig>,
895) -> Result<RustOutput, String> {
896    compile_stack_spec_with_view_selection(stack_spec, config, false)
897}
898
899fn compile_stack_spec_with_view_selection(
900    stack_spec: SerializableStackSpec,
901    config: Option<RustStackConfig>,
902    exact_views: bool,
903) -> Result<RustOutput, String> {
904    let config = config.unwrap_or_default();
905    let stack_name = &stack_spec.stack_name;
906    let stack_kebab = to_kebab_case(stack_name);
907
908    let mut entity_names: Vec<String> = Vec::new();
909    let mut entity_specs: Vec<SerializableStreamSpec> = Vec::new();
910
911    for mut spec in stack_spec.entities {
912        if spec.idl.is_none() {
913            spec.idl = stack_spec.idls.first().cloned();
914        }
915        entity_names.push(spec.state_name.clone());
916        entity_specs.push(spec);
917    }
918
919    let view_entity_names = entity_specs
920        .iter()
921        .zip(&entity_names)
922        .filter(|(spec, _)| !exact_views || !spec.views.is_empty())
923        .map(|(_, name)| name.clone())
924        .collect::<Vec<_>>();
925
926    let types_rs = generate_stack_types_rs(&entity_specs, &entity_names);
927    let entity_rs = generate_stack_entity_rs(
928        stack_name,
929        &stack_kebab,
930        &entity_specs,
931        &entity_names,
932        &config,
933        exact_views,
934    );
935    let lib_rs = generate_stack_lib_rs(stack_name, &view_entity_names, config.module_mode);
936    let cargo_toml = generate_stack_cargo_toml(&config);
937
938    Ok(RustOutput {
939        cargo_toml,
940        lib_rs,
941        types_rs,
942        entity_rs,
943    })
944}
945
946/// Compile a stack model whose `views` have already been projected by a
947/// StackManifest selected-view allowlist.
948pub fn compile_stack_spec_with_exact_views(
949    stack_spec: SerializableStackSpec,
950    config: Option<RustStackConfig>,
951) -> Result<RustOutput, String> {
952    compile_stack_spec_with_view_selection(stack_spec, config, true)
953}
954
955/// Compile Rust output from an explicit StackManifest and its public dependencies.
956pub fn compile_public_artifacts(
957    programs: &[arete_artifacts::ProgramSpecArtifact],
958    live_spec: &arete_artifacts::LiveSpecArtifact,
959    manifest: &arete_artifacts::StackManifestArtifact,
960    config: Option<RustStackConfig>,
961) -> Result<RustOutput, String> {
962    let stack_spec =
963        crate::public_artifacts::stack_spec_from_artifacts(programs, live_spec, manifest)?;
964    compile_stack_spec(stack_spec, config)
965}
966
967/// Compile typed V2 public artifacts through the current single-live generator.
968pub fn compile_public_artifacts_v2(
969    programs: &[arete_artifacts::ProgramSpecArtifact],
970    live_spec: &arete_artifacts::LiveSpecArtifactV2,
971    manifest: &arete_artifacts::StackManifestArtifactV2,
972    config: Option<RustStackConfig>,
973) -> Result<RustOutput, String> {
974    let stack_spec =
975        crate::public_artifacts::stack_spec_from_artifacts_v2(programs, live_spec, manifest)?;
976    compile_stack_spec_with_view_selection(stack_spec, config, true)
977}
978
979/// Generate one namespaced Rust stack module per live alias plus a manifest
980/// module that preserves alias boundaries instead of flattening views/adapters.
981pub fn compile_composed_public_artifacts_v2(
982    programs: &[arete_artifacts::ProgramSpecArtifact],
983    live_specs: &[(String, arete_artifacts::LiveSpecArtifactV2)],
984    manifest: &arete_artifacts::StackManifestArtifactV2,
985    config: Option<RustCompositionConfig>,
986) -> Result<RustCompositionOutput, String> {
987    let composed =
988        crate::public_artifacts::stack_specs_from_artifacts_v2(programs, live_specs, manifest)?;
989    if composed.live_specs.is_empty() {
990        return Err(
991            "Rust composition generation requires at least one aliased LiveSpec".to_string(),
992        );
993    }
994    let config = config.unwrap_or_default();
995    let mut live_stacks = Vec::with_capacity(composed.live_specs.len());
996    for live in composed.live_specs {
997        let module_name = rust_module_name(&live.alias);
998        let mut live_config = config.stack.clone();
999        live_config.module_mode = true;
1000        live_config.url = config.live_urls.get(&live.alias).cloned();
1001        let output =
1002            compile_stack_spec_with_view_selection(live.stack_spec, Some(live_config), true)?;
1003        live_stacks.push(RustAliasedStackOutput {
1004            alias: live.alias,
1005            module_name,
1006            output,
1007        });
1008    }
1009    let lib_rs = live_stacks
1010        .iter()
1011        .map(|live| format!("pub mod {};", live.module_name))
1012        .collect::<Vec<_>>()
1013        .join("\n");
1014    Ok(RustCompositionOutput {
1015        name: composed.name,
1016        cargo_toml: generate_stack_cargo_toml(&config.stack),
1017        lib_rs: format!("{lib_rs}\n"),
1018        live_stacks,
1019    })
1020}
1021
1022pub fn write_rust_composition_crate(
1023    output: &RustCompositionOutput,
1024    crate_dir: &std::path::Path,
1025) -> Result<(), std::io::Error> {
1026    let source = crate_dir.join("src");
1027    std::fs::create_dir_all(&source)?;
1028    std::fs::write(crate_dir.join("Cargo.toml"), &output.cargo_toml)?;
1029    std::fs::write(source.join("lib.rs"), &output.lib_rs)?;
1030    for live in &output.live_stacks {
1031        write_rust_module(&live.output, &source.join(&live.module_name))?;
1032    }
1033    Ok(())
1034}
1035
1036pub fn write_rust_composition_module(
1037    output: &RustCompositionOutput,
1038    module_dir: &std::path::Path,
1039) -> Result<(), std::io::Error> {
1040    std::fs::create_dir_all(module_dir)?;
1041    std::fs::write(module_dir.join("mod.rs"), &output.lib_rs)?;
1042    for live in &output.live_stacks {
1043        write_rust_module(&live.output, &module_dir.join(&live.module_name))?;
1044    }
1045    Ok(())
1046}
1047
1048fn generate_stack_cargo_toml(config: &RustStackConfig) -> String {
1049    format!(
1050        r#"[package]
1051name = "{}"
1052version = "0.1.0"
1053edition = "2021"
1054
1055[dependencies]
1056arete-sdk = {{ package = "arete-a4-sdk", version = "{}" }}
1057serde = {{ version = "1", features = ["derive"] }}
1058serde_json = "1"
1059"#,
1060        config.crate_name, config.sdk_version
1061    )
1062}
1063
1064fn generate_stack_lib_rs(stack_name: &str, entity_names: &[String], _module_mode: bool) -> String {
1065    let entity_views_exports: Vec<String> = entity_names
1066        .iter()
1067        .map(|name| format!("{}EntityViews", name))
1068        .collect();
1069
1070    let all_exports = format!(
1071        "{}Stack, {}StackViews, {}",
1072        stack_name,
1073        stack_name,
1074        entity_views_exports.join(", ")
1075    );
1076
1077    format!(
1078        r#"mod entity;
1079mod types;
1080
1081pub use entity::{{{all_exports}}};
1082pub use types::*;
1083
1084pub use arete_sdk::{{ConnectionState, Arete, Stack, Update, Views}};
1085"#,
1086        all_exports = all_exports
1087    )
1088}
1089
1090/// Generate types.rs containing structs for ALL entities in the stack.
1091fn generate_stack_types_rs(
1092    entity_specs: &[SerializableStreamSpec],
1093    entity_names: &[String],
1094) -> String {
1095    let mut output = String::new();
1096    output.push_str("use serde::{Deserialize, Serialize};\n");
1097    output.push_str("use arete_sdk::serde_utils;\n\n");
1098
1099    let mut generated = HashSet::new();
1100
1101    for (i, spec) in entity_specs.iter().enumerate() {
1102        let entity_name = &entity_names[i];
1103        let compiler = RustCompiler::new(spec.clone(), entity_name.clone(), RustConfig::default());
1104        let resolved_name_map = compiler.build_resolved_type_name_map();
1105
1106        // Generate section structs (e.g., OreRoundId, OreRoundState)
1107        for section in &spec.sections {
1108            if !RustCompiler::is_root_section(&section.name) {
1109                let struct_name = format!("{}{}", entity_name, to_pascal_case(&section.name));
1110                if generated.insert(struct_name) {
1111                    output.push_str(
1112                        &compiler.generate_struct_for_section(section, &resolved_name_map),
1113                    );
1114                    output.push_str("\n\n");
1115                }
1116            }
1117        }
1118
1119        // Generate main entity struct (e.g., OreRound, OreTreasury)
1120        output.push_str(&compiler.generate_main_entity_struct(&resolved_name_map));
1121        output.push_str("\n\n");
1122
1123        let resolved = compiler.generate_resolved_types(&resolved_name_map, &mut generated);
1124        output.push_str(&resolved);
1125        while !output.ends_with("\n\n") {
1126            output.push('\n');
1127        }
1128    }
1129
1130    // Generate EventWrapper once
1131    output.push_str(
1132        r#"
1133#[derive(Debug, Clone, Serialize, Deserialize)]
1134pub struct EventWrapper<T> {
1135    #[serde(default, deserialize_with = "serde_utils::deserialize_i64")]
1136    pub timestamp: i64,
1137    pub data: T,
1138    #[serde(default)]
1139    pub slot: Option<f64>,
1140    #[serde(default)]
1141    pub signature: Option<String>,
1142}
1143
1144impl<T: Default> Default for EventWrapper<T> {
1145    fn default() -> Self {
1146        Self {
1147            timestamp: 0,
1148            data: T::default(),
1149            slot: None,
1150            signature: None,
1151        }
1152    }
1153}
1154"#,
1155    );
1156
1157    output
1158}
1159
1160/// Generate entity.rs with a single Stack impl and per-entity EntityViews.
1161fn generate_stack_entity_rs(
1162    stack_name: &str,
1163    stack_kebab: &str,
1164    entity_specs: &[SerializableStreamSpec],
1165    entity_names: &[String],
1166    config: &RustStackConfig,
1167    exact_views: bool,
1168) -> String {
1169    let types_import = if config.module_mode {
1170        "super::types"
1171    } else {
1172        "crate::types"
1173    };
1174
1175    let selected_entities = entity_specs
1176        .iter()
1177        .zip(entity_names)
1178        .filter(|(spec, _)| !exact_views || !spec.views.is_empty())
1179        .collect::<Vec<_>>();
1180    let entity_type_imports = selected_entities
1181        .iter()
1182        .map(|(_, name)| (*name).to_string())
1183        .collect::<Vec<_>>();
1184
1185    let url_impl = match &config.url {
1186        Some(url) => format!(
1187            r#"fn url() -> &'static str {{
1188        "{}"
1189    }}"#,
1190            url
1191        ),
1192        None => r#"fn url() -> &'static str {
1193        "" // TODO: Set URL after first deployment in arete.toml
1194    }"#
1195        .to_string(),
1196    };
1197
1198    // StackViews struct fields
1199    let views_fields: Vec<String> = selected_entities
1200        .iter()
1201        .map(|(_, name)| {
1202            let snake = to_snake_case(name);
1203            format!("    pub {}: {}EntityViews,", snake, name)
1204        })
1205        .collect();
1206
1207    // Views::from_builder body — clone builder for all but last entity
1208    let views_builder_fields: Vec<String> = selected_entities
1209        .iter()
1210        .enumerate()
1211        .map(|(i, (_, name))| {
1212            let snake = to_snake_case(name);
1213            if i < selected_entities.len() - 1 {
1214                format!(
1215                    "            {}: {}EntityViews {{ builder: builder.clone() }},",
1216                    snake, name
1217                )
1218            } else {
1219                format!("            {}: {}EntityViews {{ builder }},", snake, name)
1220            }
1221        })
1222        .collect();
1223
1224    // Per-entity EntityViews structs
1225    let mut entity_views_structs = Vec::new();
1226    for (i, entity_name) in entity_names.iter().enumerate() {
1227        let spec = &entity_specs[i];
1228        if exact_views && spec.views.is_empty() {
1229            continue;
1230        }
1231
1232        let derived: Vec<_> = spec
1233            .views
1234            .iter()
1235            .filter(|v| {
1236                !v.id.ends_with("/state")
1237                    && !v.id.ends_with("/list")
1238                    && v.id.starts_with(entity_name.as_str())
1239            })
1240            .collect();
1241
1242        let mut methods = Vec::new();
1243
1244        if !exact_views
1245            || spec
1246                .views
1247                .iter()
1248                .any(|view| view.id == format!("{entity_name}/state"))
1249        {
1250            methods.push(format!(
1251                r#"    pub fn state(&self) -> StateView<{entity}> {{
1252        StateView::new(
1253            self.builder.connection().clone(),
1254            self.builder.store().clone(),
1255            "{entity}/state".to_string(),
1256            self.builder.initial_data_timeout(),
1257        )
1258    }}"#,
1259                entity = entity_name
1260            ));
1261        }
1262
1263        if !exact_views
1264            || spec
1265                .views
1266                .iter()
1267                .any(|view| view.id == format!("{entity_name}/list"))
1268        {
1269            methods.push(format!(
1270                r#"
1271    pub fn list(&self) -> ViewHandle<{entity}> {{
1272        self.builder.view("{entity}/list")
1273    }}"#,
1274                entity = entity_name
1275            ));
1276        }
1277
1278        // Derived view methods
1279        for view in &derived {
1280            let view_name = view.id.split('/').nth(1).unwrap_or("unknown");
1281            let method_name = to_snake_case(view_name);
1282            methods.push(format!(
1283                r#"
1284    pub fn {method}(&self) -> ViewHandle<{entity}> {{
1285        self.builder.view("{view_id}")
1286    }}"#,
1287                method = method_name,
1288                entity = entity_name,
1289                view_id = view.id
1290            ));
1291        }
1292
1293        entity_views_structs.push(format!(
1294            r#"
1295pub struct {entity}EntityViews {{
1296    builder: ViewBuilder,
1297}}
1298
1299impl {entity}EntityViews {{
1300{methods}
1301}}"#,
1302            entity = entity_name,
1303            methods = methods.join("\n")
1304        ));
1305    }
1306
1307    let types_use = if entity_type_imports.is_empty() {
1308        String::new()
1309    } else {
1310        format!(
1311            "use {types_import}::{{{}}};\n",
1312            entity_type_imports.join(", ")
1313        )
1314    };
1315    let empty_builder = if selected_entities.is_empty() {
1316        "        let _ = builder;\n"
1317    } else {
1318        ""
1319    };
1320
1321    format!(
1322        r#"{types_use}use arete_sdk::{{Stack, StateView, ViewBuilder, ViewHandle, Views}};
1323
1324pub struct {stack}Stack;
1325
1326impl Stack for {stack}Stack {{
1327    type Views = {stack}StackViews;
1328
1329    fn name() -> &'static str {{
1330        "{stack_kebab}"
1331    }}
1332
1333    {url_impl}
1334}}
1335
1336pub struct {stack}StackViews {{
1337{views_fields}
1338}}
1339
1340impl Views for {stack}StackViews {{
1341    fn from_builder(builder: ViewBuilder) -> Self {{
1342{empty_builder}        Self {{
1343{views_builder}
1344        }}
1345    }}
1346}}
1347{entity_views}"#,
1348        types_use = types_use,
1349        stack = stack_name,
1350        stack_kebab = stack_kebab,
1351        url_impl = url_impl,
1352        views_fields = views_fields.join("\n"),
1353        views_builder = views_builder_fields.join("\n"),
1354        entity_views = entity_views_structs.join("\n"),
1355        empty_builder = empty_builder,
1356    )
1357}
1358
1359fn to_kebab_case(s: &str) -> String {
1360    let mut result = String::new();
1361    for (i, c) in s.chars().enumerate() {
1362        if c.is_uppercase() {
1363            if i > 0 {
1364                result.push('-');
1365            }
1366            result.push(c.to_lowercase().next().unwrap());
1367        } else {
1368            result.push(c);
1369        }
1370    }
1371    result
1372}
1373
1374fn to_pascal_case(s: &str) -> String {
1375    s.split(['_', '-', '.'])
1376        .map(|word| {
1377            let mut chars = word.chars();
1378            match chars.next() {
1379                None => String::new(),
1380                Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
1381            }
1382        })
1383        .collect()
1384}
1385
1386fn to_snake_case(s: &str) -> String {
1387    let mut result = String::new();
1388    let mut separator = false;
1389    for ch in s.chars() {
1390        if ch.is_ascii_alphanumeric() {
1391            if separator && !result.is_empty() {
1392                result.push('_');
1393            }
1394            separator = false;
1395            if ch.is_ascii_uppercase() {
1396                if !result.is_empty() && !result.ends_with('_') {
1397                    result.push('_');
1398                }
1399                result.push(ch.to_ascii_lowercase());
1400            } else {
1401                result.push(ch.to_ascii_lowercase());
1402            }
1403        } else {
1404            separator = true;
1405        }
1406    }
1407    if result
1408        .chars()
1409        .next()
1410        .is_some_and(|character| character.is_ascii_digit())
1411    {
1412        result.insert_str(0, "value_");
1413    }
1414    if is_rust_keyword(&result) {
1415        result.push('_');
1416    }
1417    result
1418}
1419
1420fn rust_module_name(value: &str) -> String {
1421    let mut output = String::new();
1422    let mut separator = false;
1423    for character in value.chars() {
1424        if character.is_ascii_alphanumeric() {
1425            if separator && !output.is_empty() {
1426                output.push('_');
1427            }
1428            separator = false;
1429            output.push(character.to_ascii_lowercase());
1430        } else {
1431            separator = true;
1432        }
1433    }
1434    if output
1435        .chars()
1436        .next()
1437        .is_some_and(|character| character.is_ascii_digit())
1438    {
1439        output.insert_str(0, "live_");
1440    }
1441    if is_rust_keyword(&output) {
1442        output.push_str("_live");
1443    }
1444    output
1445}
1446
1447fn is_rust_keyword(value: &str) -> bool {
1448    matches!(
1449        value,
1450        "as" | "async"
1451            | "await"
1452            | "break"
1453            | "const"
1454            | "continue"
1455            | "crate"
1456            | "dyn"
1457            | "else"
1458            | "enum"
1459            | "extern"
1460            | "false"
1461            | "fn"
1462            | "for"
1463            | "if"
1464            | "impl"
1465            | "in"
1466            | "let"
1467            | "loop"
1468            | "match"
1469            | "mod"
1470            | "move"
1471            | "mut"
1472            | "pub"
1473            | "ref"
1474            | "return"
1475            | "self"
1476            | "Self"
1477            | "static"
1478            | "struct"
1479            | "super"
1480            | "trait"
1481            | "true"
1482            | "type"
1483            | "union"
1484            | "unsafe"
1485            | "use"
1486            | "where"
1487            | "while"
1488            | "abstract"
1489            | "become"
1490            | "box"
1491            | "do"
1492            | "final"
1493            | "macro"
1494            | "override"
1495            | "priv"
1496            | "typeof"
1497            | "unsized"
1498            | "virtual"
1499            | "yield"
1500            | "try"
1501    )
1502}