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 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(§ion.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(§ion.name));
171 let mut fields = Vec::new();
172
173 for field in §ion.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(§ion.name)
206 && section.fields.iter().any(|field| field.emit)
207 {
208 let field_name = to_snake_case(§ion.name);
209 let type_name = format!("{}{}", self.entity_name, to_pascal_case(§ion.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(§ion.name) {
219 for field in §ion.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 §ion.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 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 fn derive_stack_name(&self) -> String {
460 let entity_name = &self.entity_name;
461
462 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 entity_name.clone()
473 }
474
475 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 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 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 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 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 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(§ion.name)
603 && section.fields.iter().any(|field| field.emit)
604 {
605 reserved_names.insert(format!(
606 "{}{}",
607 self.entity_name,
608 to_pascal_case(§ion.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 §ion.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 "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#[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
888pub 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
946pub 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
955pub 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
967pub 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
979pub 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
1090fn 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 for section in &spec.sections {
1108 if !RustCompiler::is_root_section(§ion.name) {
1109 let struct_name = format!("{}{}", entity_name, to_pascal_case(§ion.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 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 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
1160fn 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 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 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 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 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}