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 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(§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
837#[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
860pub 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
944fn 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 for section in &spec.sections {
962 if !RustCompiler::is_root_section(§ion.name) {
963 let struct_name = format!("{}{}", entity_name, to_pascal_case(§ion.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 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 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
1014fn 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 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 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 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 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 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 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}