1use crate::ast::*;
2use crate::typescript_instructions::{
3 dedupe_errors_by_code, normalize_seed_arg_type, split_generic,
4};
5use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
6
7#[derive(Debug, Clone)]
8pub struct RustOutput {
9 pub cargo_toml: String,
10 pub lib_rs: String,
11 pub types_rs: String,
12 pub entity_rs: String,
13 pub programs_rs: Option<String>,
16}
17
18#[derive(Debug, Clone)]
25pub struct RustProgramOutput {
26 pub cargo_toml: String,
27 pub lib_rs: String,
28 pub types_rs: String,
29 pub programs_rs: String,
30}
31
32impl RustOutput {
33 pub fn full_lib(&self) -> String {
34 let mut output = format!(
35 "{}\n\n// types.rs\n{}\n\n// entity.rs\n{}",
36 self.lib_rs, self.types_rs, self.entity_rs
37 );
38 if let Some(programs) = &self.programs_rs {
39 output.push_str("\n\n// programs.rs\n");
40 output.push_str(programs);
41 }
42 output
43 }
44
45 pub fn mod_rs(&self) -> String {
46 self.lib_rs.clone()
47 }
48}
49
50#[derive(Debug, Clone)]
51pub struct RustConfig {
52 pub crate_name: String,
53 pub sdk_version: String,
54 pub module_mode: bool,
55 pub url: Option<String>,
57}
58
59impl Default for RustConfig {
60 fn default() -> Self {
61 Self {
62 crate_name: "generated-stack".to_string(),
63 sdk_version: "0.4".to_string(),
64 module_mode: false,
65 url: None,
66 }
67 }
68}
69
70pub fn compile_serializable_spec(
71 spec: SerializableStreamSpec,
72 entity_name: String,
73 config: Option<RustConfig>,
74) -> Result<RustOutput, String> {
75 let config = config.unwrap_or_default();
76 let compiler = RustCompiler::new(spec, entity_name, config);
77 Ok(compiler.compile())
78}
79
80pub fn write_rust_crate(
81 output: &RustOutput,
82 crate_dir: &std::path::Path,
83) -> Result<(), std::io::Error> {
84 std::fs::create_dir_all(crate_dir.join("src"))?;
85 std::fs::write(crate_dir.join("Cargo.toml"), &output.cargo_toml)?;
86 std::fs::write(crate_dir.join("src/lib.rs"), &output.lib_rs)?;
87 std::fs::write(crate_dir.join("src/types.rs"), &output.types_rs)?;
88 std::fs::write(crate_dir.join("src/entity.rs"), &output.entity_rs)?;
89 if let Some(programs) = &output.programs_rs {
90 std::fs::write(crate_dir.join("src/programs.rs"), programs)?;
91 }
92 Ok(())
93}
94
95pub fn write_rust_module(
96 output: &RustOutput,
97 module_dir: &std::path::Path,
98) -> Result<(), std::io::Error> {
99 std::fs::create_dir_all(module_dir)?;
100 std::fs::write(module_dir.join("mod.rs"), output.mod_rs())?;
101 std::fs::write(module_dir.join("types.rs"), &output.types_rs)?;
102 std::fs::write(module_dir.join("entity.rs"), &output.entity_rs)?;
103 if let Some(programs) = &output.programs_rs {
104 std::fs::write(module_dir.join("programs.rs"), programs)?;
105 }
106 Ok(())
107}
108
109pub fn write_rust_program_crate(
110 output: &RustProgramOutput,
111 crate_dir: &std::path::Path,
112) -> Result<(), std::io::Error> {
113 std::fs::create_dir_all(crate_dir.join("src"))?;
114 std::fs::write(crate_dir.join("Cargo.toml"), &output.cargo_toml)?;
115 std::fs::write(crate_dir.join("src/lib.rs"), &output.lib_rs)?;
116 std::fs::write(crate_dir.join("src/types.rs"), &output.types_rs)?;
117 std::fs::write(crate_dir.join("src/programs.rs"), &output.programs_rs)?;
118 Ok(())
119}
120
121pub fn write_rust_program_module(
122 output: &RustProgramOutput,
123 module_dir: &std::path::Path,
124) -> Result<(), std::io::Error> {
125 std::fs::create_dir_all(module_dir)?;
126 std::fs::write(module_dir.join("mod.rs"), &output.lib_rs)?;
127 std::fs::write(module_dir.join("types.rs"), &output.types_rs)?;
128 std::fs::write(module_dir.join("programs.rs"), &output.programs_rs)?;
129 Ok(())
130}
131
132#[derive(Debug, Clone, Copy, PartialEq, Eq)]
136pub(crate) enum WrapperKind {
137 None,
138 Capture,
139 Event,
140}
141
142pub(crate) fn capture_field_targets(spec: &SerializableStreamSpec) -> HashSet<String> {
148 let mut targets = HashSet::new();
149 for handler in &spec.handlers {
150 for mapping in &handler.mappings {
151 if matches!(&mapping.source, MappingSource::AsCapture { .. }) {
152 targets.insert(mapping.target_path.clone());
153 }
154 }
155 }
156 targets
157}
158
159pub(crate) fn wrapper_kind_for(
164 field: &FieldTypeInfo,
165 resolved: &ResolvedStructType,
166 capture_fields: &HashSet<String>,
167) -> WrapperKind {
168 if resolved.is_event || (resolved.is_instruction && field.is_array) {
169 return WrapperKind::Event;
170 }
171 if resolved.is_account
172 && (capture_fields.contains(&field.field_name)
173 || capture_fields.contains(field.raw_field_name()))
174 {
175 return WrapperKind::Capture;
176 }
177 WrapperKind::None
178}
179
180const WRAPPER_TYPES: &str = r#"/// Wrapper for event data that includes context metadata.
187/// Events are automatically wrapped in this structure at runtime.
188#[derive(Debug, Clone, Serialize, Deserialize)]
189pub struct EventWrapper<T> {
190 /// Unix timestamp when the event was processed.
191 #[serde(default, deserialize_with = "serde_utils::deserialize_i64")]
192 pub timestamp: i64,
193 /// The event-specific data.
194 pub data: T,
195 /// Optional blockchain slot number.
196 #[serde(default, deserialize_with = "serde_utils::deserialize_option_u64")]
197 pub slot: Option<u64>,
198 /// Optional transaction signature.
199 #[serde(default)]
200 pub signature: Option<String>,
201}
202
203impl<T: Default> Default for EventWrapper<T> {
204 fn default() -> Self {
205 Self {
206 timestamp: 0,
207 data: T::default(),
208 slot: None,
209 signature: None,
210 }
211 }
212}
213
214/// Wrapper for account data captured with `#[capture]`, including context
215/// metadata. Captured accounts are automatically wrapped in this structure at
216/// runtime.
217#[derive(Debug, Clone, Serialize, Deserialize)]
218pub struct CaptureWrapper<T> {
219 /// Unix timestamp when the account was captured.
220 #[serde(default, deserialize_with = "serde_utils::deserialize_i64")]
221 pub timestamp: i64,
222 /// The account address (base58 encoded public key).
223 #[serde(default)]
224 pub account_address: String,
225 /// The captured account data.
226 pub data: T,
227 /// Optional blockchain slot number.
228 #[serde(default, deserialize_with = "serde_utils::deserialize_option_u64")]
229 pub slot: Option<u64>,
230 /// Optional transaction signature.
231 #[serde(default)]
232 pub signature: Option<String>,
233}
234
235impl<T: Default> Default for CaptureWrapper<T> {
236 fn default() -> Self {
237 Self {
238 timestamp: 0,
239 account_address: String::new(),
240 data: T::default(),
241 slot: None,
242 signature: None,
243 }
244 }
245}
246"#;
247
248const BUILTIN_RESOLVER_STRUCTS: &[(&str, &str)] = &[
263 (
264 "SlotHashBytes",
265 r#"/// Slot hash resolved by the builtin `SlotHash` resolver.
266#[derive(Debug, Clone, Serialize, Deserialize, Default)]
267pub struct SlotHashBytes {
268 /// 32-byte slot hash.
269 #[serde(default)]
270 pub bytes: Vec<u8>,
271}"#,
272 ),
273 (
274 "TokenMetadata",
275 r#"/// Token metadata resolved by the builtin `TokenMetadata` resolver.
276#[derive(Debug, Clone, Serialize, Deserialize, Default)]
277pub struct TokenMetadata {
278 #[serde(default)]
279 pub mint: String,
280 #[serde(default)]
281 pub name: Option<String>,
282 #[serde(default)]
283 pub symbol: Option<String>,
284 #[serde(default)]
285 pub decimals: Option<u8>,
286 #[serde(default)]
287 pub logo_uri: Option<String>,
288}"#,
289 ),
290];
291
292pub(crate) fn builtin_resolver_struct(inner_type: Option<&str>) -> Option<&'static str> {
297 let inner = inner_type?;
298 if !crate::resolvers::is_resolver_output_type(inner) {
299 return None;
300 }
301 BUILTIN_RESOLVER_STRUCTS
302 .iter()
303 .find(|(name, _)| *name == inner)
304 .map(|(name, _)| *name)
305}
306
307fn render_builtin_resolver_structs(used: &BTreeSet<&'static str>) -> String {
310 let mut output = String::new();
311 for (name, definition) in BUILTIN_RESOLVER_STRUCTS {
312 if used.contains(name) {
313 output.push_str(definition);
314 output.push_str("\n\n");
315 }
316 }
317 output
318}
319
320fn rust_scalar_array_element(inner_type: &str) -> Option<&'static str> {
326 let trimmed = inner_type.trim();
327 let element = trimmed
328 .strip_prefix("Vec <")
329 .and_then(|rest| rest.strip_suffix('>'))
330 .or_else(|| {
331 trimmed
332 .strip_prefix("Vec<")
333 .and_then(|rest| rest.strip_suffix('>'))
334 })
335 .map(str::trim)
336 .unwrap_or(trimmed);
337 match element {
338 "f32" | "f64" => Some("f64"),
339 "bool" => Some("bool"),
340 "String" | "&str" | "str" => Some("String"),
341 _ => None,
342 }
343}
344
345struct RustScalarShape {
350 rust_type: String,
352 integer_kind: Option<&'static str>,
355 is_vec: bool,
357}
358
359fn rust_scalar_field_shape(
369 base_type: &BaseType,
370 integer_kind: Option<IntegerKind>,
371 is_array: bool,
372 inner_type: Option<&str>,
373 rust_type_name: &str,
374) -> RustScalarShape {
375 if is_array && matches!(base_type, BaseType::Array) {
376 if let Some(kind) = integer_kind {
377 let kind = normalized_integer_kind_of(kind);
378 return RustScalarShape {
379 rust_type: format!("Vec<{kind}>"),
380 integer_kind: Some(kind),
381 is_vec: true,
382 };
383 }
384 if let Some(element) = inner_type.and_then(rust_scalar_array_element) {
385 return RustScalarShape {
386 rust_type: format!("Vec<{element}>"),
387 integer_kind: None,
388 is_vec: false,
389 };
390 }
391 }
392
393 let kind = match base_type {
395 BaseType::Integer => Some(normalized_integer_kind(rust_type_name)),
396 BaseType::Timestamp => Some("i64"),
397 _ => None,
398 };
399 let is_vec = is_array && !matches!(base_type, BaseType::Array);
400 let base = base_type_to_rust(base_type, rust_type_name);
401 RustScalarShape {
402 rust_type: if is_vec { format!("Vec<{base}>") } else { base },
403 integer_kind: kind,
404 is_vec,
405 }
406}
407
408fn deserialize_with_for_shape(shape: &RustScalarShape, is_optional: bool) -> Option<String> {
411 let kind = shape.integer_kind?;
412 Some(match (is_optional, shape.is_vec) {
413 (false, false) => format!("serde_utils::deserialize_option_{kind}"),
414 (true, false) => format!("serde_utils::deserialize_option_option_{kind}"),
415 (false, true) => format!("serde_utils::deserialize_option_vec_{kind}"),
416 (true, true) => format!("serde_utils::deserialize_option_option_vec_{kind}"),
417 })
418}
419
420fn base_type_to_rust(base_type: &BaseType, rust_type_name: &str) -> String {
421 match base_type {
422 BaseType::Integer => normalized_integer_kind(rust_type_name).to_string(),
423 BaseType::Float => "f64".to_string(),
424 BaseType::String => "String".to_string(),
425 BaseType::Boolean => "bool".to_string(),
426 BaseType::Timestamp => "i64".to_string(),
427 BaseType::Binary => "Vec<u8>".to_string(),
428 BaseType::Pubkey => "String".to_string(),
429 BaseType::Array => "Vec<serde_json::Value>".to_string(),
430 BaseType::Object => "serde_json::Value".to_string(),
431 BaseType::Any => "serde_json::Value".to_string(),
432 }
433}
434
435pub(crate) struct RustCompiler {
436 spec: SerializableStreamSpec,
437 entity_name: String,
438 config: RustConfig,
439 capture_fields: HashSet<String>,
441}
442
443impl RustCompiler {
444 pub(crate) fn new(
445 spec: SerializableStreamSpec,
446 entity_name: String,
447 config: RustConfig,
448 ) -> Self {
449 let capture_fields = capture_field_targets(&spec);
450 Self {
451 spec,
452 entity_name,
453 config,
454 capture_fields,
455 }
456 }
457
458 fn compile(&self) -> RustOutput {
459 RustOutput {
460 cargo_toml: self.generate_cargo_toml(),
461 lib_rs: self.generate_lib_rs(),
462 types_rs: self.generate_types_rs(),
463 entity_rs: self.generate_entity_rs(),
464 programs_rs: None,
465 }
466 }
467
468 fn generate_cargo_toml(&self) -> String {
469 format!(
470 r#"[package]
471name = "{}"
472version = "0.1.0"
473edition = "2021"
474
475[dependencies]
476arete-sdk = {{ package = "arete-a4-sdk", version = "{}" }}
477serde = {{ version = "1", features = ["derive"] }}
478serde_json = "1"
479"#,
480 self.config.crate_name, self.config.sdk_version
481 )
482 }
483
484 fn generate_lib_rs(&self) -> String {
485 let stack_name = self.derive_stack_name();
486 let entity_name = &self.entity_name;
487
488 format!(
489 r#"mod entity;
490mod types;
491
492pub use entity::{{{stack_name}Stack, {stack_name}StackViews, {entity_name}EntityViews}};
493pub use types::*;
494
495pub use arete_sdk::{{ConnectionState, Arete, Stack, Update, Views}};
496"#,
497 stack_name = stack_name,
498 entity_name = entity_name
499 )
500 }
501
502 fn generate_types_rs(&self) -> String {
503 let mut output = String::new();
504 output.push_str("use serde::{Deserialize, Serialize};\n");
505 output.push_str("use arete_sdk::serde_utils;\n\n");
506
507 let resolved_name_map = self.build_resolved_type_name_map();
508 let mut generated = HashSet::new();
509
510 for section in &self.spec.sections {
511 if !Self::is_root_section(§ion.name)
512 && section.fields.iter().any(|field| field.emit)
513 && generated.insert(section.name.clone())
514 {
515 output.push_str(&self.generate_struct_for_section(section, &resolved_name_map));
516 output.push_str("\n\n");
517 }
518 }
519
520 output.push_str(&self.generate_main_entity_struct(&resolved_name_map));
521 output.push_str(&self.generate_resolved_types(&resolved_name_map, &mut generated, None));
522
523 let builtins = render_builtin_resolver_structs(&self.used_builtin_resolver_types());
524 if !builtins.is_empty() {
525 output.push_str("\n\n");
526 output.push_str(builtins.trim_end());
527 }
528
529 output.push_str(&self.generate_wrapper_types());
530
531 output
532 }
533
534 pub(crate) fn generate_struct_for_section(
535 &self,
536 section: &EntitySection,
537 resolved_name_map: &HashMap<String, String>,
538 ) -> String {
539 let struct_name = format!("{}{}", self.entity_name, to_pascal_case(§ion.name));
540 let mut fields = Vec::new();
541
542 for field in §ion.fields {
543 if !field.emit {
544 continue;
545 }
546 let field_name = to_snake_case(&field.field_name);
547 let rust_type = self.field_type_to_rust(field, §ion.name, resolved_name_map);
548 let serde_attr = self.serde_attr_for_field(field, §ion.name);
549
550 fields.push(format!(
551 " {}\n pub {}: {},",
552 serde_attr, field_name, rust_type
553 ));
554 }
555
556 format!(
557 "#[derive(Debug, Clone, Serialize, Deserialize, Default)]\npub struct {} {{\n{}\n}}",
558 struct_name,
559 fields.join("\n")
560 )
561 }
562
563 pub(crate) fn is_root_section(name: &str) -> bool {
564 name.eq_ignore_ascii_case("root")
565 }
566
567 pub(crate) fn generate_main_entity_struct(
568 &self,
569 resolved_name_map: &HashMap<String, String>,
570 ) -> String {
571 let mut fields = Vec::new();
572
573 for section in &self.spec.sections {
574 if !Self::is_root_section(§ion.name)
575 && section.fields.iter().any(|field| field.emit)
576 {
577 let field_name = to_snake_case(§ion.name);
578 let type_name = format!("{}{}", self.entity_name, to_pascal_case(§ion.name));
579 fields.push(format!(
580 " #[serde(default)]\n pub {}: {},",
581 field_name, type_name
582 ));
583 }
584 }
585
586 for section in &self.spec.sections {
587 if Self::is_root_section(§ion.name) {
588 for field in §ion.fields {
589 if !field.emit {
590 continue;
591 }
592 let field_name = to_snake_case(&field.field_name);
593 let rust_type =
594 self.field_type_to_rust(field, §ion.name, resolved_name_map);
595 let serde_attr = self.serde_attr_for_field(field, §ion.name);
596 fields.push(format!(
597 " {}\n pub {}: {},",
598 serde_attr, field_name, rust_type
599 ));
600 }
601 }
602 }
603
604 format!(
605 "#[derive(Debug, Clone, Serialize, Deserialize, Default)]\npub struct {} {{\n{}\n}}",
606 self.entity_name,
607 fields.join("\n")
608 )
609 }
610
611 pub(crate) fn generate_resolved_types(
612 &self,
613 resolved_name_map: &HashMap<String, String>,
614 generated: &mut HashSet<String>,
615 mut account_structs: Option<&mut BTreeMap<String, String>>,
616 ) -> String {
617 let mut output = String::new();
618
619 for section in &self.spec.sections {
620 for field in §ion.fields {
621 if !field.emit {
622 continue;
623 }
624 if let Some(resolved) = &field.resolved_type {
625 let emitted_name = self.resolved_type_to_rust_name(resolved, resolved_name_map);
626 if generated.insert(emitted_name.clone()) {
627 if resolved.is_account && !resolved.is_enum {
628 if let Some(map) = account_structs.as_deref_mut() {
629 map.entry(resolved.type_name.clone())
630 .or_insert_with(|| emitted_name.clone());
631 }
632 }
633 output.push_str("\n\n");
634 output.push_str(&self.generate_resolved_struct(resolved, &emitted_name));
635 }
636 }
637 }
638 }
639
640 output
641 }
642
643 fn generate_resolved_struct(
644 &self,
645 resolved: &ResolvedStructType,
646 emitted_name: &str,
647 ) -> String {
648 if resolved.is_enum {
649 let variants: Vec<String> = resolved
650 .enum_variants
651 .iter()
652 .map(|v| format!(" {},", to_pascal_case(v)))
653 .collect();
654
655 format!(
656 "#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]\npub enum {} {{\n{}\n}}",
657 emitted_name,
658 variants.join("\n")
659 )
660 } else {
661 let fields: Vec<String> = resolved
662 .fields
663 .iter()
664 .map(|f| {
665 let rust_type = self.resolved_field_to_rust(f);
666 let serde_attr = self.serde_attr_for_resolved_field(f);
667 format!(
668 " {}\n pub {}: {},",
669 serde_attr,
670 to_snake_case(&f.field_name),
671 rust_type
672 )
673 })
674 .collect();
675
676 format!(
677 "#[derive(Debug, Clone, Serialize, Deserialize, Default)]\npub struct {} {{\n{}\n}}",
678 emitted_name,
679 fields.join("\n")
680 )
681 }
682 }
683
684 fn generate_wrapper_types(&self) -> String {
685 format!("\n\n{WRAPPER_TYPES}")
686 }
687
688 fn generate_entity_rs(&self) -> String {
689 let entity_name = &self.entity_name;
690 let stack_name = self.derive_stack_name();
691 let stack_name_kebab = to_kebab_case(entity_name);
692 let entity_snake = to_snake_case(entity_name);
693
694 let types_import = if self.config.module_mode {
695 "super::types"
696 } else {
697 "crate::types"
698 };
699
700 let url_impl = match &self.config.url {
702 Some(url) => format!(
703 r#"fn url() -> &'static str {{
704 "{}"
705 }}"#,
706 url
707 ),
708 None => r#"fn url() -> &'static str {
709 "" // TODO: Set URL after first deployment in arete.toml
710 }"#
711 .to_string(),
712 };
713
714 let entity_views = self.generate_entity_views_struct();
715
716 format!(
717 r#"use {types_import}::{entity_name};
718use arete_sdk::{{Stack, StateView, ViewBuilder, ViewHandle, Views}};
719
720pub struct {stack_name}Stack;
721
722impl Stack for {stack_name}Stack {{
723 type Views = {stack_name}StackViews;
724 type Programs = ();
725
726 fn name() -> &'static str {{
727 "{stack_name_kebab}"
728 }}
729
730 {url_impl}
731}}
732
733pub struct {stack_name}StackViews {{
734 pub {entity_snake}: {entity_name}EntityViews,
735}}
736
737impl Views for {stack_name}StackViews {{
738 fn from_builder(builder: ViewBuilder) -> Self {{
739 Self {{
740 {entity_snake}: {entity_name}EntityViews {{ builder }},
741 }}
742 }}
743}}
744{entity_views}"#,
745 types_import = types_import,
746 entity_name = entity_name,
747 stack_name = stack_name,
748 stack_name_kebab = stack_name_kebab,
749 entity_snake = entity_snake,
750 url_impl = url_impl,
751 entity_views = entity_views
752 )
753 }
754
755 fn generate_entity_views_struct(&self) -> String {
756 let entity_name = &self.entity_name;
757
758 let derived: Vec<_> = self
759 .spec
760 .views
761 .iter()
762 .filter(|v| {
763 !v.id.ends_with("/state")
764 && !v.id.ends_with("/list")
765 && v.id.starts_with(entity_name)
766 })
767 .collect();
768
769 let mut derived_methods = String::new();
770 for view in &derived {
771 let view_name = view.id.split('/').nth(1).unwrap_or("unknown");
772 let method_name = to_snake_case(view_name);
773
774 derived_methods.push_str(&format!(
775 r#"
776 pub fn {method_name}(&self) -> ViewHandle<{entity_name}> {{
777 self.builder.view("{view_id}")
778 }}
779"#,
780 method_name = method_name,
781 entity_name = entity_name,
782 view_id = view.id
783 ));
784 }
785
786 format!(
787 r#"
788pub struct {entity_name}EntityViews {{
789 builder: ViewBuilder,
790}}
791
792impl {entity_name}EntityViews {{
793 pub fn state(&self) -> StateView<{entity_name}> {{
794 StateView::new(
795 self.builder.connection().clone(),
796 self.builder.store().clone(),
797 "{entity_name}/state".to_string(),
798 self.builder.initial_data_timeout(),
799 )
800 }}
801
802 pub fn list(&self) -> ViewHandle<{entity_name}> {{
803 self.builder.view("{entity_name}/list")
804 }}
805{derived_methods}}}"#,
806 entity_name = entity_name,
807 derived_methods = derived_methods
808 )
809 }
810
811 fn derive_stack_name(&self) -> String {
814 let entity_name = &self.entity_name;
815
816 let suffixes = ["Round", "Token", "Game", "State", "Entity", "Data"];
818
819 for suffix in suffixes {
820 if entity_name.ends_with(suffix) && entity_name.len() > suffix.len() {
821 return entity_name[..entity_name.len() - suffix.len()].to_string();
822 }
823 }
824
825 entity_name.clone()
827 }
828
829 fn field_type_to_rust(
843 &self,
844 field: &FieldTypeInfo,
845 section_name: &str,
846 resolved_name_map: &HashMap<String, String>,
847 ) -> String {
848 let typed = if let Some(resolved) = &field.resolved_type {
852 let name = self.resolved_type_to_rust_name(resolved, resolved_name_map);
853 let element = match wrapper_kind_for(field, resolved, &self.capture_fields) {
854 WrapperKind::None => name,
855 WrapperKind::Capture => format!("CaptureWrapper<{}>", name),
856 WrapperKind::Event => format!("EventWrapper<{}>", name),
857 };
858 if field.is_array {
859 format!("Vec<{}>", element)
860 } else {
861 element
862 }
863 } else if let Some(builtin) = self.builtin_type_for_field(section_name, field) {
864 if field.is_array {
867 format!("Vec<{}>", builtin)
868 } else {
869 builtin.to_string()
870 }
871 } else {
872 self.scalar_shape_for_field(field).rust_type
873 };
874
875 if field.is_optional {
878 format!("Option<Option<{}>>", typed)
879 } else {
880 format!("Option<{}>", typed)
881 }
882 }
883
884 fn builtin_type_for_field(
892 &self,
893 section_name: &str,
894 field: &FieldTypeInfo,
895 ) -> Option<&'static str> {
896 if let Some(name) = builtin_resolver_struct(field.inner_type.as_deref()) {
897 return Some(name);
898 }
899 let field_path = format!("{}.{}", section_name, field.field_name);
900 self.spec
901 .field_mappings
902 .get(&field_path)
903 .and_then(|mapping| builtin_resolver_struct(mapping.inner_type.as_deref()))
904 }
905
906 pub(crate) fn used_builtin_resolver_types(&self) -> BTreeSet<&'static str> {
908 let mut used = BTreeSet::new();
909 for section in &self.spec.sections {
910 for field in §ion.fields {
911 if !field.emit || field.resolved_type.is_some() {
912 continue;
913 }
914 if let Some(name) = self.builtin_type_for_field(§ion.name, field) {
915 used.insert(name);
916 }
917 }
918 }
919 used
920 }
921
922 fn scalar_shape_for_field(&self, field: &FieldTypeInfo) -> RustScalarShape {
923 rust_scalar_field_shape(
924 &field.base_type,
925 field.effective_integer_kind(),
926 field.is_array,
927 field
928 .inner_type
929 .as_deref()
930 .or(Some(field.rust_type_name.as_str())),
931 &field.rust_type_name,
932 )
933 }
934
935 fn scalar_shape_for_resolved_field(&self, field: &ResolvedField) -> RustScalarShape {
936 rust_scalar_field_shape(
937 &field.base_type,
938 field.effective_integer_kind(),
939 field.is_array,
940 Some(field.field_type.as_str()),
941 &field.field_type,
942 )
943 }
944
945 fn serde_attr_for_field(&self, field: &FieldTypeInfo, section_name: &str) -> String {
949 if field.resolved_type.is_some()
950 || self.builtin_type_for_field(section_name, field).is_some()
951 {
952 return "#[serde(default)]".to_string();
953 }
954 let shape = self.scalar_shape_for_field(field);
955 match deserialize_with_for_shape(&shape, field.is_optional) {
956 Some(deser_fn) => format!("#[serde(default, deserialize_with = \"{}\")]", deser_fn),
957 None => "#[serde(default)]".to_string(),
958 }
959 }
960
961 fn serde_attr_for_resolved_field(&self, field: &ResolvedField) -> String {
963 let shape = self.scalar_shape_for_resolved_field(field);
964 match deserialize_with_for_shape(&shape, field.is_optional) {
965 Some(deser_fn) => format!("#[serde(default, deserialize_with = \"{}\")]", deser_fn),
966 None => "#[serde(default)]".to_string(),
967 }
968 }
969
970 fn resolved_field_to_rust(&self, field: &ResolvedField) -> String {
971 let typed = self.scalar_shape_for_resolved_field(field).rust_type;
972
973 if field.is_optional {
974 format!("Option<Option<{}>>", typed)
975 } else {
976 format!("Option<{}>", typed)
977 }
978 }
979
980 fn build_resolved_type_name_map(&self) -> HashMap<String, String> {
981 let mut reserved_names = HashSet::from([
982 self.entity_name.clone(),
983 "EventWrapper".to_string(),
984 "CaptureWrapper".to_string(),
985 ]);
986
987 for (name, _) in BUILTIN_RESOLVER_STRUCTS {
991 reserved_names.insert((*name).to_string());
992 }
993
994 for section in &self.spec.sections {
995 if !Self::is_root_section(§ion.name)
996 && section.fields.iter().any(|field| field.emit)
997 {
998 reserved_names.insert(format!(
999 "{}{}",
1000 self.entity_name,
1001 to_pascal_case(§ion.name)
1002 ));
1003 }
1004 }
1005
1006 let mut resolved_name_map = HashMap::new();
1007
1008 for section in &self.spec.sections {
1009 for field in §ion.fields {
1010 if !field.emit {
1011 continue;
1012 }
1013
1014 let Some(resolved) = &field.resolved_type else {
1015 continue;
1016 };
1017
1018 if resolved_name_map.contains_key(&resolved.type_name) {
1019 continue;
1020 }
1021
1022 let emitted_name = unique_resolved_type_name(resolved, &mut reserved_names);
1023 resolved_name_map.insert(resolved.type_name.clone(), emitted_name);
1024 }
1025 }
1026
1027 resolved_name_map
1028 }
1029
1030 fn resolved_type_to_rust_name(
1031 &self,
1032 resolved: &ResolvedStructType,
1033 resolved_name_map: &HashMap<String, String>,
1034 ) -> String {
1035 resolved_name_map
1036 .get(&resolved.type_name)
1037 .cloned()
1038 .unwrap_or_else(|| to_pascal_case(&resolved.type_name))
1039 }
1040}
1041
1042fn unique_resolved_type_name(
1043 resolved: &ResolvedStructType,
1044 reserved_names: &mut HashSet<String>,
1045) -> String {
1046 let base_name = to_pascal_case(&resolved.type_name);
1047 if reserved_names.insert(base_name.clone()) {
1048 return base_name;
1049 }
1050
1051 let suffix = if resolved.is_account {
1052 "Account"
1053 } else if resolved.is_event {
1054 "Event"
1055 } else if resolved.is_instruction {
1056 "Instruction"
1057 } else {
1058 "Type"
1059 };
1060
1061 let preferred = format!("{}{}", base_name, suffix);
1062 if reserved_names.insert(preferred.clone()) {
1063 return preferred;
1064 }
1065
1066 let mut index = 2;
1067 loop {
1068 let candidate = format!("{}{}{}", base_name, suffix, index);
1069 if reserved_names.insert(candidate.clone()) {
1070 return candidate;
1071 }
1072 index += 1;
1073 }
1074}
1075
1076fn normalized_integer_kind_of(kind: IntegerKind) -> &'static str {
1081 match kind {
1082 IntegerKind::U64 => "u64",
1083 IntegerKind::U32 => "u32",
1084 IntegerKind::I32 => "i32",
1085 IntegerKind::U8 | IntegerKind::U16 | IntegerKind::Usize => "u64",
1086 _ => "i64",
1088 }
1089}
1090
1091fn normalized_integer_kind(rust_type_name: &str) -> &'static str {
1092 if rust_type_name.contains("u64") {
1093 "u64"
1094 } else if rust_type_name.contains("i64") {
1095 "i64"
1096 } else if rust_type_name.contains("u32") {
1097 "u32"
1098 } else if rust_type_name.contains("i32") {
1099 "i32"
1100 } else if rust_type_name.contains("u16")
1101 || rust_type_name.contains("u8")
1102 || rust_type_name.contains("usize")
1103 {
1104 "u64"
1105 } else {
1106 "i64"
1108 }
1109}
1110
1111#[cfg(test)]
1112mod tests {
1113 use super::*;
1114 use std::collections::BTreeMap;
1115 use std::process::Command;
1116
1117 fn identity_spec() -> IdentitySpec {
1118 IdentitySpec {
1119 primary_keys: vec!["id.address".to_string()],
1120 lookup_indexes: vec![],
1121 }
1122 }
1123
1124 #[test]
1125 fn rust_generator_renames_account_types_on_collision() {
1126 let plan_field = FieldTypeInfo {
1127 field_name: "plan".to_string(),
1128 raw_name: Some("plan".to_string()),
1129 canonical_name: Some("plan".to_string()),
1130 rust_type_name: "Option<serde_json::Value>".to_string(),
1131 base_type: BaseType::Object,
1132 integer_kind: None,
1133 is_optional: false,
1134 is_array: false,
1135 inner_type: Some("Value".to_string()),
1136 source_path: None,
1137 resolved_type: Some(ResolvedStructType {
1138 type_name: "plan".to_string(),
1139 fields: vec![],
1140 is_instruction: false,
1141 is_account: true,
1142 is_event: false,
1143 is_enum: false,
1144 enum_variants: vec![],
1145 }),
1146 emit: true,
1147 };
1148
1149 let spec = SerializableStreamSpec {
1150 ast_version: CURRENT_AST_VERSION.to_string(),
1151 state_name: "Plan".to_string(),
1152 program_id: None,
1153 idl: None,
1154 identity: identity_spec(),
1155 handlers: vec![],
1156 sections: vec![
1157 EntitySection {
1158 name: "id".to_string(),
1159 fields: vec![FieldTypeInfo::new(
1160 "address".to_string(),
1161 "String".to_string(),
1162 )],
1163 is_nested_struct: false,
1164 parent_field: None,
1165 },
1166 EntitySection {
1167 name: "plan".to_string(),
1168 fields: vec![plan_field],
1169 is_nested_struct: false,
1170 parent_field: None,
1171 },
1172 ],
1173 field_mappings: BTreeMap::new(),
1174 resolver_hooks: vec![],
1175 instruction_hooks: vec![],
1176 resolver_specs: vec![],
1177 computed_fields: vec![],
1178 computed_field_specs: vec![],
1179 content_hash: None,
1180 views: vec![],
1181 };
1182
1183 let output = compile_serializable_spec(spec, "Plan".to_string(), None)
1184 .expect("rust sdk generation should succeed");
1185
1186 assert!(output.types_rs.contains("pub struct PlanAccount"));
1191 assert!(output.types_rs.contains("pub plan: Option<PlanAccount>"));
1192 assert!(!output
1193 .types_rs
1194 .contains("pub plan: Option<serde_json::Value>"));
1195 assert!(
1196 !output.types_rs.contains("pub struct Plan {\n #[serde(default, deserialize_with = \"serde_utils::deserialize_option_u64\")]\n pub discriminator")
1197 );
1198 }
1199
1200 #[test]
1201 fn rust_generator_keeps_unsigned_numeric_fields_unsigned() {
1202 let spec = SerializableStreamSpec {
1203 ast_version: CURRENT_AST_VERSION.to_string(),
1204 state_name: "Plan".to_string(),
1205 program_id: None,
1206 idl: None,
1207 identity: identity_spec(),
1208 handlers: vec![],
1209 sections: vec![
1210 EntitySection {
1211 name: "id".to_string(),
1212 fields: vec![FieldTypeInfo::new(
1213 "address".to_string(),
1214 "String".to_string(),
1215 )],
1216 is_nested_struct: false,
1217 parent_field: None,
1218 },
1219 EntitySection {
1220 name: "state".to_string(),
1221 fields: vec![FieldTypeInfo::new(
1222 "status".to_string(),
1223 "Option<u8>".to_string(),
1224 )],
1225 is_nested_struct: false,
1226 parent_field: None,
1227 },
1228 ],
1229 field_mappings: BTreeMap::new(),
1230 resolver_hooks: vec![],
1231 instruction_hooks: vec![],
1232 resolver_specs: vec![],
1233 computed_fields: vec![],
1234 computed_field_specs: vec![],
1235 content_hash: None,
1236 views: vec![],
1237 };
1238
1239 let output = compile_serializable_spec(spec, "Plan".to_string(), None)
1240 .expect("rust sdk generation should succeed");
1241
1242 assert!(
1243 output.types_rs.contains("pub status: Option<Option<u64>>"),
1244 "expected unsigned optional field, got:\n{}",
1245 output.types_rs
1246 );
1247 }
1248
1249 #[test]
1256 fn rust_generator_types_scalar_arrays() {
1257 let mut entity = minimal_entity("OreRound");
1258 entity.sections.push(EntitySection {
1259 name: "state".to_string(),
1260 fields: vec![
1261 FieldTypeInfo::new(
1262 "deployed_per_square".to_string(),
1263 "Option<Vec<u64>>".to_string(),
1264 ),
1265 FieldTypeInfo::new(
1266 "deployed_per_square_ui".to_string(),
1267 "Option<Vec<f64>>".to_string(),
1268 ),
1269 FieldTypeInfo::new("flags".to_string(), "Option<Vec<bool>>".to_string()),
1270 FieldTypeInfo::new("labels".to_string(), "Option<Vec<String>>".to_string()),
1271 FieldTypeInfo::new("resolved_seed".to_string(), "Option<Vec<u8>>".to_string()),
1272 FieldTypeInfo::new("payload".to_string(), "Option<Vec<u8>>".to_string()),
1276 ],
1277 is_nested_struct: false,
1278 parent_field: None,
1279 });
1280 for field in &mut entity.sections[1].fields {
1282 match field.field_name.as_str() {
1283 "deployed_per_square" => {
1284 field.base_type = BaseType::Array;
1285 field.integer_kind = Some(IntegerKind::U64);
1286 field.is_array = true;
1287 field.inner_type = Some("Vec < u64 >".to_string());
1288 }
1289 "resolved_seed" => {
1290 field.base_type = BaseType::Array;
1291 field.integer_kind = Some(IntegerKind::U8);
1292 field.is_array = true;
1293 field.inner_type = Some("Vec < u8 >".to_string());
1294 }
1295 "deployed_per_square_ui" => {
1296 field.base_type = BaseType::Array;
1297 field.is_array = true;
1298 field.inner_type = Some("Vec < f64 >".to_string());
1299 }
1300 "flags" => {
1301 field.base_type = BaseType::Array;
1302 field.is_array = true;
1303 field.inner_type = Some("Vec < bool >".to_string());
1304 }
1305 "labels" => {
1306 field.base_type = BaseType::Array;
1307 field.is_array = true;
1308 field.inner_type = Some("Vec < String >".to_string());
1309 }
1310 "payload" => {
1311 field.base_type = BaseType::Binary;
1312 field.integer_kind = Some(IntegerKind::U8);
1313 field.is_array = false;
1314 field.inner_type = Some("Vec < u8 >".to_string());
1315 }
1316 _ => {}
1317 }
1318 }
1319
1320 let output = compile_stack_spec(stack_of("OreRound", entity), None)
1321 .expect("rust stack generation should succeed");
1322 let types = &output.types_rs;
1323
1324 assert!(
1325 !types.contains("Vec<serde_json::Value>"),
1326 "scalar arrays should not fall back to untyped values:\n{types}"
1327 );
1328
1329 assert!(
1332 types.contains(
1333 "#[serde(default, deserialize_with = \"serde_utils::deserialize_option_option_vec_u64\")]\n pub deployed_per_square: Option<Option<Vec<u64>>>,"
1334 ),
1335 "expected a typed u64 vector with its deserializer:\n{types}"
1336 );
1337 assert!(
1340 types.contains(
1341 "#[serde(default, deserialize_with = \"serde_utils::deserialize_option_option_vec_u64\")]\n pub resolved_seed: Option<Option<Vec<u64>>>,"
1342 ),
1343 "expected u8 arrays to widen to Vec<u64>:\n{types}"
1344 );
1345
1346 assert!(types.contains(
1349 "#[serde(default)]\n pub deployed_per_square_ui: Option<Option<Vec<f64>>>,"
1350 ));
1351 assert!(types.contains("#[serde(default)]\n pub flags: Option<Option<Vec<bool>>>,"));
1352 assert!(types.contains("#[serde(default)]\n pub labels: Option<Option<Vec<String>>>,"));
1353
1354 assert!(
1356 types.contains("#[serde(default)]\n pub payload: Option<Option<Vec<u8>>>,"),
1357 "binary fields must keep Vec<u8>:\n{types}"
1358 );
1359 }
1360
1361 #[test]
1368 fn rust_generator_types_builtin_resolver_fields() {
1369 let mut entity = minimal_entity("OreRound");
1370 let mut ore_metadata = FieldTypeInfo::new(
1371 "ore_metadata".to_string(),
1372 "Option<TokenMetadata>".to_string(),
1373 );
1374 ore_metadata.base_type = BaseType::Object;
1375 ore_metadata.is_optional = true;
1376 ore_metadata.inner_type = Some("TokenMetadata".to_string());
1377
1378 let mut expires_at_slot_hash = FieldTypeInfo::new(
1379 "expires_at_slot_hash".to_string(),
1380 "Option<ResolvedSlotHash>".to_string(),
1381 );
1382 expires_at_slot_hash.base_type = BaseType::Object;
1383 expires_at_slot_hash.is_optional = true;
1384 expires_at_slot_hash.inner_type = Some("ResolvedSlotHash".to_string());
1385
1386 let mut rng = FieldTypeInfo::new("rng".to_string(), "Option<u64>".to_string());
1389 rng.is_optional = true;
1390 rng.inner_type = Some("KeccakRngValue".to_string());
1391 rng.integer_kind = Some(IntegerKind::U64);
1392
1393 entity.sections.push(EntitySection {
1394 name: "results".to_string(),
1395 fields: vec![expires_at_slot_hash.clone(), rng],
1396 is_nested_struct: false,
1397 parent_field: None,
1398 });
1399 entity.sections.push(EntitySection {
1400 name: "root".to_string(),
1401 fields: vec![ore_metadata],
1402 is_nested_struct: false,
1403 parent_field: None,
1404 });
1405
1406 let mut slot_hash_mapping = expires_at_slot_hash;
1407 slot_hash_mapping.base_type = BaseType::Any;
1408 slot_hash_mapping.inner_type = Some("SlotHashBytes".to_string());
1409 entity.field_mappings.insert(
1410 "results.expires_at_slot_hash".to_string(),
1411 slot_hash_mapping,
1412 );
1413
1414 let output = compile_stack_spec(stack_of("OreRound", entity), None)
1415 .expect("rust stack generation should succeed");
1416 let types = &output.types_rs;
1417
1418 assert!(
1419 types.contains("pub ore_metadata: Option<Option<TokenMetadata>>,"),
1420 "expected a typed TokenMetadata field:\n{types}"
1421 );
1422 assert!(
1423 types.contains("pub expires_at_slot_hash: Option<Option<SlotHashBytes>>,"),
1424 "expected the field_mappings override to type the slot hash:\n{types}"
1425 );
1426 assert!(!types.contains("pub ore_metadata: Option<Option<serde_json::Value>>,"));
1427 assert!(!types.contains("pub expires_at_slot_hash: Option<Option<serde_json::Value>>,"));
1428
1429 assert_eq!(types.matches("pub struct TokenMetadata {").count(), 1);
1431 assert_eq!(types.matches("pub struct SlotHashBytes {").count(), 1);
1432 assert!(types.contains(" pub logo_uri: Option<String>,"));
1433 assert!(types.contains(" pub bytes: Vec<u8>,"));
1434
1435 assert!(
1437 types.contains(
1438 "#[serde(default, deserialize_with = \"serde_utils::deserialize_option_option_u64\")]\n pub rng: Option<Option<u64>>,"
1439 ),
1440 "KeccakRngValue fields must stay integers:\n{types}"
1441 );
1442 assert!(!types.contains("pub struct KeccakRngValue"));
1443 }
1444
1445 #[test]
1447 fn rust_generator_omits_unused_builtin_resolver_structs() {
1448 let output = compile_stack_spec(stack_of("OreTreasury", capture_entity()), None)
1449 .expect("rust stack generation should succeed");
1450
1451 assert!(!output.types_rs.contains("pub struct TokenMetadata"));
1452 assert!(!output.types_rs.contains("pub struct SlotHashBytes"));
1453 }
1454
1455 #[test]
1456 fn generated_manifest_uses_published_arete_sdk_package() {
1457 let manifest = generate_stack_cargo_toml(&RustStackConfig::default());
1458
1459 assert!(manifest.contains("arete-sdk = { package = \"arete-a4-sdk\", version = \"0.4\" }"));
1460 }
1461
1462 fn resolved_field_of(name: &str, field_type: &str, base_type: BaseType) -> ResolvedField {
1463 ResolvedField {
1464 field_name: name.to_string(),
1465 raw_name: Some(name.to_string()),
1466 canonical_name: None,
1467 field_type: field_type.to_string(),
1468 base_type,
1469 integer_kind: IntegerKind::from_rust_type(field_type),
1470 is_optional: false,
1471 is_array: false,
1472 }
1473 }
1474
1475 fn snapshot_field(
1478 field_name: &str,
1479 type_name: &str,
1480 is_account: bool,
1481 is_event: bool,
1482 ) -> FieldTypeInfo {
1483 FieldTypeInfo {
1484 field_name: field_name.to_string(),
1485 raw_name: Some(field_name.to_string()),
1486 canonical_name: None,
1487 rust_type_name: "Option<serde_json::Value>".to_string(),
1488 base_type: BaseType::Object,
1489 integer_kind: None,
1490 is_optional: true,
1491 is_array: false,
1492 inner_type: Some("Value".to_string()),
1493 source_path: None,
1494 resolved_type: Some(ResolvedStructType {
1495 type_name: type_name.to_string(),
1496 fields: vec![
1497 resolved_field_of("motherlode", "u64", BaseType::Integer),
1498 resolved_field_of("owner", "publicKey", BaseType::Pubkey),
1499 ],
1500 is_instruction: false,
1501 is_account,
1502 is_event,
1503 is_enum: false,
1504 enum_variants: vec![],
1505 }),
1506 emit: true,
1507 }
1508 }
1509
1510 fn capture_handler(target_path: &str) -> SerializableHandlerSpec {
1512 SerializableHandlerSpec {
1513 source: SourceSpec::Source {
1514 program_id: None,
1515 discriminator: None,
1516 type_name: "Treasury".to_string(),
1517 serialization: None,
1518 is_account: true,
1519 },
1520 key_resolution: KeyResolutionStrategy::Embedded {
1521 primary_field: FieldPath::new(&["id", "address"]),
1522 },
1523 mappings: vec![SerializableFieldMapping {
1524 target_path: target_path.to_string(),
1525 source: MappingSource::AsCapture {
1526 field_transforms: BTreeMap::new(),
1527 },
1528 transform: None,
1529 population: PopulationStrategy::LastWrite,
1530 condition: None,
1531 when: None,
1532 stop: None,
1533 emit: true,
1534 }],
1535 conditions: vec![],
1536 emit: true,
1537 }
1538 }
1539
1540 fn stack_of(name: &str, entity: SerializableStreamSpec) -> SerializableStackSpec {
1541 SerializableStackSpec {
1542 ast_version: CURRENT_AST_VERSION.to_string(),
1543 stack_name: name.to_string(),
1544 program_ids: vec![],
1545 idls: vec![],
1546 program_specs: vec![],
1547 entities: vec![entity],
1548 pdas: BTreeMap::new(),
1549 instructions: vec![],
1550 content_hash: None,
1551 }
1552 }
1553
1554 fn capture_entity() -> SerializableStreamSpec {
1555 let mut entity = minimal_entity("OreTreasury");
1556 entity.handlers.push(capture_handler("treasury_snapshot"));
1557 entity.sections.push(EntitySection {
1558 name: "root".to_string(),
1559 fields: vec![
1560 snapshot_field("treasury_snapshot", "Treasury", true, false),
1561 snapshot_field("plain_account", "Vault", true, false),
1563 snapshot_field("deposit_event", "DepositEvent", false, true),
1564 ],
1565 is_nested_struct: false,
1566 parent_field: None,
1567 });
1568 entity
1569 }
1570
1571 #[test]
1577 fn rust_generator_wraps_capture_and_event_fields() {
1578 let output = compile_stack_spec(stack_of("OreTreasury", capture_entity()), None)
1579 .expect("rust stack generation should succeed");
1580 let types = &output.types_rs;
1581
1582 assert!(types.contains("pub struct EventWrapper<T> {"));
1584 assert!(types.contains("pub struct CaptureWrapper<T> {"));
1585 assert!(types.contains(" pub account_address: String,"));
1586 assert!(types.contains(" pub data: T,"));
1587 assert!(types.contains(" pub slot: Option<u64>,"));
1588 assert!(types.contains(" pub signature: Option<String>,"));
1589 assert_eq!(types.matches("pub struct CaptureWrapper<T>").count(), 1);
1590
1591 assert!(
1593 types.contains("pub treasury_snapshot: Option<Option<CaptureWrapper<Treasury>>>,"),
1594 "expected a typed capture envelope, got:\n{types}"
1595 );
1596 assert!(!types.contains("pub treasury_snapshot: Option<Option<serde_json::Value>>,"));
1597
1598 assert!(types.contains("pub deposit_event: Option<Option<EventWrapper<DepositEvent>>>,"));
1600
1601 assert!(types.contains("pub plain_account: Option<Option<Vault>>,"));
1603 assert!(!types.contains("CaptureWrapper<Vault>"));
1604
1605 assert!(types.contains("pub struct Treasury {"));
1607 assert!(types.contains("pub struct Vault {"));
1608 assert!(types.contains("pub struct DepositEvent {"));
1609 }
1610
1611 #[test]
1614 fn rust_generator_wraps_capture_fields_in_single_entity_mode() {
1615 let output = compile_serializable_spec(capture_entity(), "OreTreasury".to_string(), None)
1616 .expect("rust sdk generation should succeed");
1617 let types = &output.types_rs;
1618
1619 assert!(types.contains("pub struct CaptureWrapper<T> {"));
1620 assert!(types.contains("pub struct EventWrapper<T> {"));
1621 assert!(types.contains("pub treasury_snapshot: Option<Option<CaptureWrapper<Treasury>>>,"));
1622 assert!(types.contains("pub deposit_event: Option<Option<EventWrapper<DepositEvent>>>,"));
1623 assert!(types.contains("pub plain_account: Option<Option<Vault>>,"));
1624 }
1625
1626 #[test]
1630 fn rust_generator_reserves_wrapper_type_names() {
1631 let mut entity = minimal_entity("OreTreasury");
1632 entity.sections.push(EntitySection {
1633 name: "root".to_string(),
1634 fields: vec![
1635 snapshot_field("wrapped", "CaptureWrapper", true, false),
1636 snapshot_field("evented", "EventWrapper", true, false),
1637 ],
1638 is_nested_struct: false,
1639 parent_field: None,
1640 });
1641
1642 let output = compile_stack_spec(stack_of("OreTreasury", entity), None)
1643 .expect("rust stack generation should succeed");
1644 let types = &output.types_rs;
1645
1646 assert!(types.contains("pub struct CaptureWrapperAccount {"));
1647 assert!(types.contains("pub struct EventWrapperAccount {"));
1648 assert!(types.contains("pub wrapped: Option<Option<CaptureWrapperAccount>>,"));
1649 assert!(types.contains("pub evented: Option<Option<EventWrapperAccount>>,"));
1650 assert_eq!(types.matches("pub struct CaptureWrapper<T>").count(), 1);
1651 }
1652
1653 const TEST_PROGRAM_ID: &str = "Prog111111111111111111111111111111111111111";
1654
1655 fn minimal_entity(name: &str) -> SerializableStreamSpec {
1656 SerializableStreamSpec {
1657 ast_version: CURRENT_AST_VERSION.to_string(),
1658 state_name: name.to_string(),
1659 program_id: None,
1660 idl: None,
1661 identity: identity_spec(),
1662 handlers: vec![],
1663 sections: vec![EntitySection {
1664 name: "id".to_string(),
1665 fields: vec![FieldTypeInfo::new(
1666 "address".to_string(),
1667 "String".to_string(),
1668 )],
1669 is_nested_struct: false,
1670 parent_field: None,
1671 }],
1672 field_mappings: BTreeMap::new(),
1673 resolver_hooks: vec![],
1674 instruction_hooks: vec![],
1675 resolver_specs: vec![],
1676 computed_fields: vec![],
1677 computed_field_specs: vec![],
1678 content_hash: None,
1679 views: vec![],
1680 }
1681 }
1682
1683 fn test_idl() -> IdlSnapshot {
1684 IdlSnapshot {
1685 name: "demo".to_string(),
1686 program_id: Some(TEST_PROGRAM_ID.to_string()),
1687 version: "0.1.0".to_string(),
1688 accounts: vec![],
1689 instructions: vec![],
1690 types: vec![],
1691 events: vec![],
1692 errors: vec![IdlErrorSnapshot {
1693 code: 6000,
1694 name: "SlippageExceeded".to_string(),
1695 msg: Some("Slippage exceeded".to_string()),
1696 }],
1697 discriminant_size: 8,
1698 }
1699 }
1700
1701 fn instruction_account(name: &str, resolution: AccountResolution) -> InstructionAccountDef {
1702 InstructionAccountDef {
1703 name: name.to_string(),
1704 is_signer: matches!(resolution, AccountResolution::Signer),
1705 is_writable: true,
1706 resolution,
1707 is_optional: false,
1708 docs: vec![],
1709 }
1710 }
1711
1712 fn instruction_arg(name: &str, arg_type: &str) -> InstructionArgDef {
1713 InstructionArgDef {
1714 name: name.to_string(),
1715 arg_type: arg_type.to_string(),
1716 docs: vec![],
1717 amount_hint: None,
1718 }
1719 }
1720
1721 fn programs_stack_spec() -> SerializableStackSpec {
1722 let mut demo_pdas = BTreeMap::new();
1723 demo_pdas.insert(
1724 "counter".to_string(),
1725 PdaDefinition {
1726 name: "counter".to_string(),
1727 seeds: vec![
1728 PdaSeedDef::Literal {
1729 value: "counter".to_string(),
1730 },
1731 PdaSeedDef::AccountRef {
1732 account_name: "authority".to_string(),
1733 },
1734 ],
1735 program_id: None,
1736 program: None,
1737 },
1738 );
1739 let mut pdas = BTreeMap::new();
1740 pdas.insert("demo".to_string(), demo_pdas);
1741
1742 SerializableStackSpec {
1743 ast_version: CURRENT_AST_VERSION.to_string(),
1744 stack_name: "Demo".to_string(),
1745 program_ids: vec![TEST_PROGRAM_ID.to_string()],
1746 idls: vec![test_idl()],
1747 program_specs: vec![],
1748 entities: vec![minimal_entity("DemoThing")],
1749 pdas,
1750 instructions: vec![InstructionDef {
1751 name: "doThing".to_string(),
1752 discriminator: vec![12, 34],
1753 discriminator_size: 2,
1754 accounts: vec![
1755 instruction_account("signer", AccountResolution::Signer),
1756 instruction_account("authority", AccountResolution::UserProvided),
1757 instruction_account(
1758 "counter",
1759 AccountResolution::PdaRef {
1760 pda_name: "counter".to_string(),
1761 },
1762 ),
1763 instruction_account(
1764 "systemProgram",
1765 AccountResolution::Known {
1766 address: "11111111111111111111111111111111".to_string(),
1767 },
1768 ),
1769 ],
1770 args: vec![
1771 instruction_arg("roundId", "u64"),
1772 instruction_arg("admin", "solana_pubkey::Pubkey"),
1773 instruction_arg("tip", "Option<u64>"),
1774 ],
1775 errors: vec![],
1776 program_id: Some(TEST_PROGRAM_ID.to_string()),
1777 docs: vec!["Does the thing.".to_string()],
1778 }],
1779 content_hash: None,
1780 }
1781 }
1782
1783 #[test]
1784 fn rust_generator_emits_program_sdk_module() {
1785 let output = compile_stack_spec(programs_stack_spec(), None)
1786 .expect("rust stack generation should succeed");
1787 let programs = output
1788 .programs_rs
1789 .expect("programs.rs should be generated for stacks with instructions");
1790
1791 assert!(programs.contains("pub mod demo {"));
1792 assert!(programs.contains(&format!(
1793 "pub const PROGRAM_ID: &str = \"{}\";",
1794 TEST_PROGRAM_ID
1795 )));
1796
1797 assert!(programs.contains("pub struct DoThingParams {"));
1799 assert!(programs.contains("#[serde(rename = \"roundId\")]"));
1800 assert!(programs.contains("pub round_id: u64,"));
1801 assert!(programs.contains("pub admin: String,"));
1802 assert!(programs.contains("pub tip: Option<u64>,"));
1803 assert!(programs.contains("pub signer: Option<String>,"));
1804 assert!(programs.contains("pub authority: String,"));
1805 assert!(programs.contains("#[serde(skip_serializing_if = \"Option::is_none\")]"));
1806
1807 assert!(programs.contains("discriminator: vec![12, 34]"));
1809 assert!(programs.contains("resolution: AccountResolution::Signer,"));
1810 assert!(programs.contains(
1811 "AccountResolution::Known(\"11111111111111111111111111111111\".to_string())"
1812 ));
1813 assert!(programs.contains(
1814 "AccountResolution::Pda(PdaConfig { program_id: None, seeds: vec![PdaSeed::Literal(\"counter\".to_string()), PdaSeed::AccountRef(\"authority\".to_string())] })"
1815 ));
1816 assert!(programs.contains("ArgSchema { name: \"roundId\".to_string(), ty: ArgType::U64 }"));
1817 assert!(programs.contains("ty: ArgType::Option(Box::new(ArgType::U64))"));
1818 assert!(programs.contains("ty: ArgType::Pubkey"));
1819 assert!(programs.contains(
1820 "ErrorMetadata { code: 6000, name: \"SlippageExceeded\".to_string(), msg: \"Slippage exceeded\".to_string() }"
1821 ));
1822
1823 assert!(programs
1825 .contains("pub fn counter(authority: &str) -> Result<(Pubkey, u8), InstructionError>"));
1826
1827 assert!(programs.contains("pub struct DemoProgram {"));
1829 assert!(programs.contains("builder: arete_sdk::ProgramBuilder,"));
1830 assert!(
1831 programs.contains("pub fn from_builder(builder: arete_sdk::ProgramBuilder) -> Self")
1832 );
1833 assert!(programs.contains(
1834 "pub fn do_thing(params: DoThingParams) -> Result<BuiltInstruction, InstructionError>"
1835 ));
1836 assert!(programs.contains("pub fn do_thing_handler() -> InstructionHandler"));
1837
1838 assert!(programs.contains(
1840 "/// Program read layer omitted: no program specification was recorded for this program."
1841 ));
1842 assert!(!programs.contains("pub const PROGRAM_SPEC_HASH"));
1843 assert!(!programs.contains("pub fn read_descriptor"));
1844
1845 assert!(output
1847 .entity_rs
1848 .contains("type Programs = DemoStackPrograms;"));
1849 assert!(output
1850 .entity_rs
1851 .contains("pub demo: crate::programs::demo::DemoProgram,"));
1852 assert!(output
1853 .entity_rs
1854 .contains("demo: crate::programs::demo::DemoProgram::from_builder(builder),"));
1855 assert!(output
1856 .entity_rs
1857 .contains("impl arete_sdk::Programs for DemoStackPrograms"));
1858 assert!(output.lib_rs.contains("pub mod programs;"));
1859 assert!(output.lib_rs.contains("DemoStackPrograms"));
1860 }
1861
1862 #[test]
1863 fn rust_program_compiler_emits_no_view_or_stack_shell() {
1864 let output = compile_program_modules(
1865 programs_stack_spec(),
1866 Some(RustStackConfig {
1867 crate_name: "demo-program".to_string(),
1868 sdk_version: env!("CARGO_PKG_VERSION").to_string(),
1869 program_reads: vec![RustProgramReadConfig {
1870 program_id: TEST_PROGRAM_ID.to_string(),
1871 program_spec_hash: "arete:h1:program-spec:sha256:test".to_string(),
1872 program_release_hash: "arete:h1:program-release:sha256:test".to_string(),
1873 descriptor: Some(serde_json::json!({
1874 "release": {
1875 "programReleaseHash": "arete:h1:program-release:sha256:test",
1876 "programSpecHash": "arete:h1:program-spec:sha256:test"
1877 },
1878 "transport": {
1879 "kind": "hosted-binding",
1880 "binding": {
1881 "endpoint": "https://reads.example.test",
1882 "programReadBindingId": "prb_00000000000000000000000000000001",
1883 "auth": {
1884 "sessionEndpoint": "https://auth.example.test/session",
1885 "targetKind": "program-read-binding",
1886 "targetId": "prb_00000000000000000000000000000001"
1887 }
1888 }
1889 }
1890 })),
1891 }],
1892 ..Default::default()
1893 }),
1894 )
1895 .expect("standalone program generation should succeed");
1896
1897 assert!(!output.lib_rs.contains("mod entity"));
1898 assert!(!output.lib_rs.contains("StackViews"));
1899 assert!(!output.programs_rs.contains("EntityViews"));
1900 assert!(output.lib_rs.contains("pub use programs::DemoPrograms;"));
1901 assert!(output
1902 .programs_rs
1903 .contains("impl arete_sdk::ProgramSdk for DemoPrograms"));
1904 assert!(output
1905 .programs_rs
1906 .contains("impl arete_sdk::Programs for DemoPrograms"));
1907
1908 let base = std::env::temp_dir().join(format!(
1909 "arete-rust-program-codegen-{}-{:?}",
1910 std::process::id(),
1911 std::thread::current().id()
1912 ));
1913 let _ = std::fs::remove_dir_all(&base);
1914 write_rust_program_crate(&output, &base).expect("program crate should write");
1915 assert!(base.join("src/programs.rs").is_file());
1916 assert!(base.join("src/types.rs").is_file());
1917 assert!(!base.join("src/entity.rs").exists());
1918
1919 let repo_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
1923 .parent()
1924 .expect("interpreter crate lives in the repo root");
1925 let runtime_path = repo_root.join("rust/arete-a4-sdk");
1926 let manifest_path = base.join("Cargo.toml");
1927 let mut manifest = std::fs::read_to_string(&manifest_path).expect("generated Cargo.toml");
1928 let generated_dependency = format!(
1929 "arete-sdk = {{ package = \"arete-a4-sdk\", version = {:?} }}",
1930 env!("CARGO_PKG_VERSION")
1931 );
1932 assert!(
1933 manifest.contains(&generated_dependency),
1934 "generated manifest must expose the expected runtime dependency"
1935 );
1936 manifest.push_str(&format!(
1937 "\n[patch.crates-io]\narete-a4-sdk = {{ path = {:?} }}\n",
1938 runtime_path
1939 ));
1940 std::fs::write(&manifest_path, manifest).expect("localize generated dependency");
1941
1942 let cargo = std::env::var_os("CARGO").unwrap_or_else(|| "cargo".into());
1943 let checked = Command::new(cargo)
1944 .args(["check", "--quiet", "--offline", "--manifest-path"])
1945 .arg(&manifest_path)
1946 .env("CARGO_TARGET_DIR", base.join("target"))
1947 .output()
1948 .expect("cargo must be available for generated consumer smoke tests");
1949 assert!(
1950 checked.status.success(),
1951 "generated standalone Rust crate failed cargo check:\nstdout:\n{}\nstderr:\n{}",
1952 String::from_utf8_lossy(&checked.stdout),
1953 String::from_utf8_lossy(&checked.stderr),
1954 );
1955 let _ = std::fs::remove_dir_all(&base);
1956 }
1957
1958 #[test]
1959 fn rust_program_compiler_keeps_idl_only_programs() {
1960 let mut spec = programs_stack_spec();
1961 spec.instructions.clear();
1962 let output = compile_program_modules(spec, None)
1963 .expect("an IDL-only standalone program should still be emitted");
1964 assert!(output.programs_rs.contains("pub mod demo"));
1965 assert!(output.programs_rs.contains("pub struct DemoProgram"));
1966 assert!(output.programs_rs.contains("pub struct DemoPrograms"));
1967 }
1968
1969 #[test]
1970 fn rust_generator_without_instructions_binds_unit_programs() {
1971 let mut spec = programs_stack_spec();
1972 spec.instructions.clear();
1973
1974 let output = compile_stack_spec(spec, None).expect("rust stack generation should succeed");
1975
1976 assert!(output.programs_rs.is_none());
1977 assert!(output.entity_rs.contains("type Programs = ();"));
1978 assert!(!output.lib_rs.contains("pub mod programs;"));
1979 assert!(!output.entity_rs.contains("StackPrograms"));
1980 }
1981
1982 #[test]
1983 fn rust_generator_notes_skipped_instructions() {
1984 let mut spec = programs_stack_spec();
1985 spec.instructions.push(InstructionDef {
1986 name: "badThing".to_string(),
1987 discriminator: vec![9],
1988 discriminator_size: 1,
1989 accounts: vec![],
1990 args: vec![instruction_arg("payload", "MysteryType")],
1991 errors: vec![],
1992 program_id: Some(TEST_PROGRAM_ID.to_string()),
1993 docs: vec![],
1994 });
1995
1996 let output = compile_stack_spec(spec, None).expect("rust stack generation should succeed");
1997 let programs = output.programs_rs.expect("programs.rs should be generated");
1998
1999 assert!(programs.contains("/// Skipped instructions (unsupported by instruction codegen):"));
2000 assert!(
2001 programs.contains("/// - `badThing`: arg 'payload' has unsupported type 'MysteryType'")
2002 );
2003 assert!(!programs.contains("BadThingParams"));
2004 assert!(programs.contains("pub struct DoThingParams {"));
2006 }
2007
2008 #[test]
2009 fn rust_generator_emits_program_read_layer() {
2010 let idl_json = format!(
2013 r#"{{
2014 "address": "{TEST_PROGRAM_ID}",
2015 "version": "0.1.0",
2016 "name": "demo",
2017 "instructions": [
2018 {{
2019 "name": "doThing",
2020 "accounts": [{{ "name": "payer", "isMut": true, "isSigner": true }}],
2021 "args": [{{ "name": "amount", "type": "u64" }}],
2022 "discriminant": {{ "type": "u8", "value": 1 }}
2023 }}
2024 ],
2025 "accounts": [
2026 {{
2027 "name": "Counter",
2028 "type": {{
2029 "kind": "struct",
2030 "fields": [{{ "name": "count", "type": "u64" }}]
2031 }}
2032 }}
2033 ],
2034 "types": [],
2035 "events": [],
2036 "errors": []
2037 }}"#
2038 );
2039 let mut spec = crate::program_sdk::build_program_only_stack_spec_from_idl_bytes(
2040 idl_json.as_bytes(),
2041 None,
2042 "Demo",
2043 )
2044 .expect("program-only stack spec should build");
2045 let expected_spec_hash = spec.program_specs[0].hash().unwrap().to_string();
2046 let expected_release_hash = spec.program_specs[0]
2047 .oss_release_hash()
2048 .unwrap()
2049 .to_string();
2050
2051 let mut entity = minimal_entity("DemoThing");
2053 entity.sections.push(EntitySection {
2054 name: "state".to_string(),
2055 fields: vec![FieldTypeInfo {
2056 field_name: "counter".to_string(),
2057 raw_name: Some("counter".to_string()),
2058 canonical_name: Some("counter".to_string()),
2059 rust_type_name: "Option<serde_json::Value>".to_string(),
2060 base_type: BaseType::Object,
2061 integer_kind: None,
2062 is_optional: false,
2063 is_array: false,
2064 inner_type: Some("Value".to_string()),
2065 source_path: None,
2066 resolved_type: Some(ResolvedStructType {
2067 type_name: "Counter".to_string(),
2068 fields: vec![],
2069 is_instruction: false,
2070 is_account: true,
2071 is_event: false,
2072 is_enum: false,
2073 enum_variants: vec![],
2074 }),
2075 emit: true,
2076 }],
2077 is_nested_struct: false,
2078 parent_field: None,
2079 });
2080 spec.entities.push(entity);
2081
2082 let output = compile_stack_spec(spec, None).expect("rust stack generation should succeed");
2083 let programs = output.programs_rs.expect("programs.rs should be generated");
2084
2085 assert!(programs.contains(&format!(
2087 "pub const PROGRAM_SPEC_HASH: &str = \"{expected_spec_hash}\";"
2088 )));
2089 assert!(programs.contains(&format!(
2090 "pub const PROGRAM_RELEASE_HASH: &str = \"{expected_release_hash}\";"
2091 )));
2092 assert!(programs.contains("pub fn read_descriptor() -> arete_sdk::ProgramReadDescriptor"));
2093 assert!(programs.contains("arete_sdk::ProgramReadDescriptor::LocalHttp"));
2094 assert!(!programs.contains("Program read layer omitted"));
2095
2096 assert!(output.types_rs.contains("pub struct Counter"));
2098 assert!(programs.contains(
2099 "pub fn counter_accounts(&self) -> Result<arete_sdk::AccountReader<crate::types::Counter>, arete_sdk::AreteError>"
2100 ));
2101 assert!(programs.contains("self.builder.account_transport(\"demo\", &read_descriptor())?"));
2102 assert!(programs.contains("arete_sdk::AccountReader::new(\n \"Counter\","));
2103 }
2104
2105 #[test]
2106 fn rust_generator_emits_platform_release_override() {
2107 let idl_json = format!(
2108 r#"{{
2109 "address": "{TEST_PROGRAM_ID}",
2110 "version": "0.1.0",
2111 "name": "demo",
2112 "instructions": [
2113 {{
2114 "name": "doThing",
2115 "accounts": [{{ "name": "payer", "isMut": true, "isSigner": true }}],
2116 "args": [{{ "name": "amount", "type": "u64" }}],
2117 "discriminant": {{ "type": "u8", "value": 1 }}
2118 }}
2119 ],
2120 "accounts": [],
2121 "types": [],
2122 "events": [],
2123 "errors": []
2124 }}"#
2125 );
2126 let spec = crate::program_sdk::build_program_only_stack_spec_from_idl_bytes(
2127 idl_json.as_bytes(),
2128 None,
2129 "Demo",
2130 )
2131 .expect("program-only stack spec should build");
2132
2133 let platform_spec = "arete:h1:program-spec:sha256:platformspec".to_string();
2135 let platform_release = "arete:h1:program-release:sha256:platformrelease".to_string();
2136 let config = RustStackConfig {
2137 program_reads: vec![RustProgramReadConfig {
2138 program_id: TEST_PROGRAM_ID.to_string(),
2139 program_spec_hash: platform_spec.clone(),
2140 program_release_hash: platform_release.clone(),
2141 descriptor: Some(serde_json::json!({
2142 "release": {
2143 "programReleaseHash": platform_release.clone(),
2144 "programSpecHash": platform_spec.clone(),
2145 },
2146 "transport": {"kind": "hosted-binding", "binding": {
2147 "endpoint": "https://reads.example.test",
2148 "programReadBindingId": "prb_00000000000000000000000000000001",
2149 "auth": {
2150 "sessionEndpoint": "https://auth.example.test/session",
2151 "targetKind": "program-read-binding",
2152 "targetId": "prb_00000000000000000000000000000001"
2153 }
2154 }}
2155 })),
2156 }],
2157 ..Default::default()
2158 };
2159
2160 let output =
2161 compile_stack_spec(spec, Some(config)).expect("rust stack generation should succeed");
2162 let programs = output.programs_rs.expect("programs.rs should be generated");
2163
2164 assert!(programs.contains(&format!(
2165 "pub const PROGRAM_SPEC_HASH: &str = \"{platform_spec}\";"
2166 )));
2167 assert!(programs.contains(&format!(
2168 "pub const PROGRAM_RELEASE_HASH: &str = \"{platform_release}\";"
2169 )));
2170 assert!(programs.contains("pub fn read_descriptor() -> arete_sdk::ProgramReadDescriptor"));
2171 assert!(programs.contains("\\\"kind\\\":\\\"hosted-binding\\\""));
2172 }
2173
2174 #[test]
2175 fn rust_generator_emits_stack_http_url_override() {
2176 let output = compile_stack_spec(programs_stack_spec(), None)
2177 .expect("rust stack generation should succeed");
2178 assert!(!output.entity_rs.contains("fn http_url"));
2179
2180 let config = RustStackConfig {
2181 http_url: Some("https://demo.stack.example".to_string()),
2182 ..Default::default()
2183 };
2184 let output = compile_stack_spec(programs_stack_spec(), Some(config))
2185 .expect("rust stack generation should succeed");
2186 assert!(output.entity_rs.contains(
2187 "fn http_url() -> &'static str {\n \"https://demo.stack.example\"\n }"
2188 ));
2189 }
2190
2191 #[test]
2192 fn rust_generator_wires_extension_modules_after_generated_decls() {
2193 let config = RustStackConfig {
2194 module_mode: true,
2195 extension_modules: vec!["devex".to_string(), "extensions".to_string()],
2196 extension_entry: Some("extensions".to_string()),
2197 ..Default::default()
2198 };
2199 let output = compile_stack_spec(programs_stack_spec(), Some(config))
2200 .expect("rust stack generation should succeed");
2201 let mod_rs = output.mod_rs();
2202
2203 assert!(mod_rs.contains(
2204 "// Hand-authored devex extensions (staged from extensions.json; not generated)."
2205 ));
2206 let sdk_reexport = mod_rs.find("pub use arete_sdk::").expect("sdk re-export");
2207 let devex = mod_rs.find("pub mod devex;").expect("devex module decl");
2208 let entry = mod_rs
2209 .find("pub mod extensions;")
2210 .expect("entry module decl");
2211 let entry_reexport = mod_rs
2212 .find("pub use extensions::*;")
2213 .expect("entry glob re-export");
2214 assert!(sdk_reexport < devex);
2215 assert!(devex < entry);
2216 assert!(entry < entry_reexport);
2217 assert!(!mod_rs.contains("pub use devex::*;"));
2218 }
2219
2220 #[test]
2221 fn rust_generator_omits_extension_wiring_without_entry() {
2222 let output = compile_stack_spec(programs_stack_spec(), None)
2223 .expect("rust stack generation should succeed");
2224
2225 assert!(!output.mod_rs().contains("Hand-authored devex extensions"));
2226 assert!(!output.mod_rs().contains("pub mod extensions;"));
2227 }
2228
2229 #[test]
2230 fn rust_generator_rejects_extension_module_collisions() {
2231 for reserved in ["entity", "types", "programs"] {
2232 let config = RustStackConfig {
2233 extension_modules: vec![reserved.to_string(), "extensions".to_string()],
2234 extension_entry: Some("extensions".to_string()),
2235 ..Default::default()
2236 };
2237 let error = compile_stack_spec(programs_stack_spec(), Some(config))
2238 .expect_err("collision with a generated module must fail");
2239 assert!(
2240 error.contains(&format!("'{reserved}.rs'")),
2241 "collision error should name the file: {error}"
2242 );
2243 }
2244
2245 let duplicate = RustStackConfig {
2246 extension_modules: vec![
2247 "devex".to_string(),
2248 "devex".to_string(),
2249 "extensions".to_string(),
2250 ],
2251 extension_entry: Some("extensions".to_string()),
2252 ..Default::default()
2253 };
2254 assert!(compile_stack_spec(programs_stack_spec(), Some(duplicate)).is_err());
2255
2256 let entry_not_last = RustStackConfig {
2257 extension_modules: vec!["extensions".to_string(), "devex".to_string()],
2258 extension_entry: Some("extensions".to_string()),
2259 ..Default::default()
2260 };
2261 assert!(compile_stack_spec(programs_stack_spec(), Some(entry_not_last)).is_err());
2262 }
2263
2264 #[test]
2277 #[ignore = "writes into examples/ore-rust; run explicitly to regenerate"]
2278 fn regenerate_ore_example() {
2279 let repo_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
2280 .parent()
2281 .expect("interpreter crate lives in the repo root")
2282 .to_path_buf();
2283 let spec_json =
2284 std::fs::read_to_string(repo_root.join("stacks/ore/.arete/OreStream.stack.json"))
2285 .expect("ore stack spec should exist");
2286 let spec = crate::versioned::load_stack_spec(&spec_json)
2287 .expect("ore stack spec should deserialize");
2288
2289 let out_dir = repo_root.join("examples/ore-rust/src/generated/ore");
2290 let (extension_modules, extension_entry) =
2291 match std::fs::read_to_string(out_dir.join("extensions.json")) {
2292 Ok(manifest_json) => {
2293 let manifest: serde_json::Value = serde_json::from_str(&manifest_json)
2294 .expect("staged extensions.json should parse");
2295 let language = manifest["language"].as_str();
2296 assert!(
2297 language.is_none() || language == Some("rust"),
2298 "staged ore extensions must be a Rust bundle"
2299 );
2300 let entry_stem = rust_module_name(
2301 manifest["entry"]
2302 .as_str()
2303 .and_then(|entry| entry.strip_suffix(".rs"))
2304 .expect("extensions entry should be a .rs file"),
2305 );
2306 let mut stems: Vec<String> = manifest["files"]
2307 .as_array()
2308 .expect("extensions files should be an array")
2309 .iter()
2310 .map(|file| {
2311 rust_module_name(
2312 file.as_str()
2313 .and_then(|file| file.strip_suffix(".rs"))
2314 .expect("extension files should be .rs files"),
2315 )
2316 })
2317 .filter(|stem| stem != &entry_stem)
2318 .collect();
2319 stems.sort();
2320 stems.dedup();
2321 stems.push(entry_stem.clone());
2322 (stems, Some(entry_stem))
2323 }
2324 Err(_) => (Vec::new(), None),
2325 };
2326
2327 let config = RustStackConfig {
2328 crate_name: "ore-stack".to_string(),
2329 sdk_version: "0.4".to_string(),
2330 module_mode: true,
2331 url: Some("wss://ore.stack.arete.run".to_string()),
2332 http_url: Some("https://ore.stack.arete.run".to_string()),
2333 extension_modules,
2334 extension_entry,
2335 program_reads: Vec::new(),
2336 };
2337 let output =
2338 compile_stack_spec(spec, Some(config)).expect("ore stack should compile to Rust");
2339
2340 std::fs::write(out_dir.join("mod.rs"), output.mod_rs()).unwrap();
2341 std::fs::write(out_dir.join("types.rs"), &output.types_rs).unwrap();
2342 std::fs::write(out_dir.join("entity.rs"), &output.entity_rs).unwrap();
2343 std::fs::write(
2344 out_dir.join("programs.rs"),
2345 output.programs_rs.as_deref().expect("ore has instructions"),
2346 )
2347 .unwrap();
2348 }
2349}
2350
2351#[derive(Debug, Clone)]
2356pub struct RustStackConfig {
2357 pub crate_name: String,
2358 pub sdk_version: String,
2359 pub module_mode: bool,
2360 pub url: Option<String>,
2361 pub http_url: Option<String>,
2366 pub extension_modules: Vec<String>,
2370 pub extension_entry: Option<String>,
2375 pub program_reads: Vec<RustProgramReadConfig>,
2383}
2384
2385#[derive(Debug, Clone)]
2386pub struct RustProgramReadConfig {
2387 pub program_id: String,
2388 pub program_spec_hash: String,
2389 pub program_release_hash: String,
2390 pub descriptor: Option<serde_json::Value>,
2393}
2394
2395#[derive(Debug, Clone, Default)]
2396pub struct RustCompositionConfig {
2397 pub stack: RustStackConfig,
2398 pub live_urls: BTreeMap<String, String>,
2399}
2400
2401#[derive(Debug, Clone)]
2402pub struct RustAliasedStackOutput {
2403 pub alias: String,
2404 pub module_name: String,
2405 pub output: RustOutput,
2406}
2407
2408#[derive(Debug, Clone)]
2409pub struct RustCompositionOutput {
2410 pub name: String,
2411 pub cargo_toml: String,
2412 pub lib_rs: String,
2413 pub live_stacks: Vec<RustAliasedStackOutput>,
2414}
2415
2416impl Default for RustStackConfig {
2417 fn default() -> Self {
2418 Self {
2419 crate_name: "generated-stack".to_string(),
2420 sdk_version: "0.4".to_string(),
2421 module_mode: false,
2422 url: None,
2423 http_url: None,
2424 extension_modules: Vec::new(),
2425 extension_entry: None,
2426 program_reads: Vec::new(),
2427 }
2428 }
2429}
2430
2431pub fn compile_stack_spec(
2436 stack_spec: SerializableStackSpec,
2437 config: Option<RustStackConfig>,
2438) -> Result<RustOutput, String> {
2439 compile_stack_spec_with_view_selection(stack_spec, config, false)
2440}
2441
2442fn compile_stack_spec_with_view_selection(
2443 stack_spec: SerializableStackSpec,
2444 config: Option<RustStackConfig>,
2445 exact_views: bool,
2446) -> Result<RustOutput, String> {
2447 let config = config.unwrap_or_default();
2448 let stack_name = &stack_spec.stack_name;
2449 let stack_kebab = to_kebab_case(stack_name);
2450
2451 let mut entity_names: Vec<String> = Vec::new();
2452 let mut entity_specs: Vec<SerializableStreamSpec> = Vec::new();
2453
2454 for mut spec in stack_spec.entities {
2455 if spec.idl.is_none() {
2456 spec.idl = stack_spec.idls.first().cloned();
2457 }
2458 entity_names.push(spec.state_name.clone());
2459 entity_specs.push(spec);
2460 }
2461
2462 let view_entity_names = entity_specs
2463 .iter()
2464 .zip(&entity_names)
2465 .filter(|(spec, _)| !exact_views || !spec.views.is_empty())
2466 .map(|(_, name)| name.clone())
2467 .collect::<Vec<_>>();
2468
2469 let (types_rs, account_structs) = generate_stack_types_rs(&entity_specs, &entity_names);
2470
2471 let programs = generate_stack_programs_rs(
2472 stack_name,
2473 &stack_spec.instructions,
2474 &stack_spec.idls,
2475 &stack_spec.pdas,
2476 &stack_spec.program_ids,
2477 &stack_spec.program_specs,
2478 &account_structs,
2479 config.module_mode,
2480 &config.program_reads,
2481 false,
2482 );
2483 let entity_rs = generate_stack_entity_rs(
2484 stack_name,
2485 &stack_kebab,
2486 &entity_specs,
2487 &entity_names,
2488 &config,
2489 exact_views,
2490 programs.as_ref(),
2491 );
2492 validate_extension_modules(&config, programs.is_some())?;
2493 let lib_rs = generate_stack_lib_rs(
2494 stack_name,
2495 &view_entity_names,
2496 config.module_mode,
2497 programs.is_some(),
2498 &config.extension_modules,
2499 config.extension_entry.as_deref(),
2500 );
2501 let cargo_toml = generate_stack_cargo_toml(&config);
2502
2503 Ok(RustOutput {
2504 cargo_toml,
2505 lib_rs,
2506 types_rs,
2507 entity_rs,
2508 programs_rs: programs.map(|codegen| codegen.code),
2509 })
2510}
2511
2512pub fn compile_stack_spec_with_exact_views(
2515 stack_spec: SerializableStackSpec,
2516 config: Option<RustStackConfig>,
2517) -> Result<RustOutput, String> {
2518 compile_stack_spec_with_view_selection(stack_spec, config, true)
2519}
2520
2521pub fn compile_program_modules(
2525 stack_spec: SerializableStackSpec,
2526 config: Option<RustStackConfig>,
2527) -> Result<RustProgramOutput, String> {
2528 let config = config.unwrap_or_default();
2529 if stack_spec.idls.is_empty() {
2530 return Err(format!(
2531 "Stack '{}' carries no IDLs; a program-only SDK has nothing to emit",
2532 stack_spec.stack_name
2533 ));
2534 }
2535
2536 let entity_names = stack_spec
2537 .entities
2538 .iter()
2539 .map(|entity| entity.state_name.clone())
2540 .collect::<Vec<_>>();
2541 let (types_rs, account_structs) = generate_stack_types_rs(&stack_spec.entities, &entity_names);
2542 let mut programs = generate_stack_programs_rs(
2543 &stack_spec.stack_name,
2544 &stack_spec.instructions,
2545 &stack_spec.idls,
2546 &stack_spec.pdas,
2547 &stack_spec.program_ids,
2548 &stack_spec.program_specs,
2549 &account_structs,
2550 config.module_mode,
2551 &config.program_reads,
2552 true,
2553 )
2554 .ok_or_else(|| {
2555 format!(
2556 "Stack '{}' contains no programs to emit",
2557 stack_spec.stack_name
2558 )
2559 })?;
2560
2561 validate_extension_modules(&config, true)?;
2562 let aggregate_name = format!("{}Programs", to_pascal_case(&stack_spec.stack_name));
2563 programs.code.push('\n');
2564 programs.code.push_str(&generate_programs_accessor_struct(
2565 &aggregate_name,
2566 &programs,
2567 "self",
2568 ));
2569 programs.code.push_str(&format!(
2570 "\n\nimpl arete_sdk::ProgramSdk for {aggregate_name} {{\n fn name() -> &'static str {{\n {}\n }}\n}}\n",
2571 rust_string_literal(&to_kebab_case(&stack_spec.stack_name)),
2572 ));
2573
2574 let mut lib_rs = format!(
2575 "mod types;\npub mod programs;\n\npub use programs::{aggregate_name};\npub use types::*;\n\npub use arete_sdk::{{ProgramSdk, Programs}};\n"
2576 );
2577 append_rust_extension_exports(
2578 &mut lib_rs,
2579 &config.extension_modules,
2580 config.extension_entry.as_deref(),
2581 );
2582
2583 Ok(RustProgramOutput {
2584 cargo_toml: generate_stack_cargo_toml(&config),
2585 lib_rs,
2586 types_rs,
2587 programs_rs: programs.code,
2588 })
2589}
2590
2591pub fn compile_public_artifacts(
2593 programs: &[arete_artifacts::ProgramSpecArtifact],
2594 live_spec: &arete_artifacts::LiveSpecArtifact,
2595 manifest: &arete_artifacts::StackManifestArtifact,
2596 config: Option<RustStackConfig>,
2597) -> Result<RustOutput, String> {
2598 let stack_spec =
2599 crate::public_artifacts::stack_spec_from_artifacts(programs, live_spec, manifest)?;
2600 compile_stack_spec(stack_spec, config)
2601}
2602
2603pub fn compile_public_artifacts_v2(
2605 programs: &[arete_artifacts::ProgramSpecArtifact],
2606 live_spec: &arete_artifacts::LiveSpecArtifactV2,
2607 manifest: &arete_artifacts::StackManifestArtifactV2,
2608 config: Option<RustStackConfig>,
2609) -> Result<RustOutput, String> {
2610 let stack_spec =
2611 crate::public_artifacts::stack_spec_from_artifacts_v2(programs, live_spec, manifest)?;
2612 compile_stack_spec_with_view_selection(stack_spec, config, true)
2613}
2614
2615pub fn compile_composed_public_artifacts_v2(
2618 programs: &[arete_artifacts::ProgramSpecArtifact],
2619 live_specs: &[(String, arete_artifacts::LiveSpecArtifactV2)],
2620 manifest: &arete_artifacts::StackManifestArtifactV2,
2621 config: Option<RustCompositionConfig>,
2622) -> Result<RustCompositionOutput, String> {
2623 let composed =
2624 crate::public_artifacts::stack_specs_from_artifacts_v2(programs, live_specs, manifest)?;
2625 if composed.live_specs.is_empty() {
2626 return Err(
2627 "Rust composition generation requires at least one aliased LiveSpec".to_string(),
2628 );
2629 }
2630 let config = config.unwrap_or_default();
2631 if !config.stack.extension_modules.is_empty() || config.stack.extension_entry.is_some() {
2632 return Err(
2633 "Rust composition SDKs do not support stack extensions; extensions attach to a single-live stack module".to_string(),
2634 );
2635 }
2636 let mut live_stacks = Vec::with_capacity(composed.live_specs.len());
2637 for live in composed.live_specs {
2638 let module_name = rust_module_name(&live.alias);
2639 let mut live_config = config.stack.clone();
2640 live_config.module_mode = true;
2641 live_config.url = config.live_urls.get(&live.alias).cloned();
2642 let output =
2643 compile_stack_spec_with_view_selection(live.stack_spec, Some(live_config), true)?;
2644 live_stacks.push(RustAliasedStackOutput {
2645 alias: live.alias,
2646 module_name,
2647 output,
2648 });
2649 }
2650 let lib_rs = live_stacks
2651 .iter()
2652 .map(|live| format!("pub mod {};", live.module_name))
2653 .collect::<Vec<_>>()
2654 .join("\n");
2655 Ok(RustCompositionOutput {
2656 name: composed.name,
2657 cargo_toml: generate_stack_cargo_toml(&config.stack),
2658 lib_rs: format!("{lib_rs}\n"),
2659 live_stacks,
2660 })
2661}
2662
2663pub fn write_rust_composition_crate(
2664 output: &RustCompositionOutput,
2665 crate_dir: &std::path::Path,
2666) -> Result<(), std::io::Error> {
2667 let source = crate_dir.join("src");
2668 std::fs::create_dir_all(&source)?;
2669 std::fs::write(crate_dir.join("Cargo.toml"), &output.cargo_toml)?;
2670 std::fs::write(source.join("lib.rs"), &output.lib_rs)?;
2671 for live in &output.live_stacks {
2672 write_rust_module(&live.output, &source.join(&live.module_name))?;
2673 }
2674 Ok(())
2675}
2676
2677pub fn write_rust_composition_module(
2678 output: &RustCompositionOutput,
2679 module_dir: &std::path::Path,
2680) -> Result<(), std::io::Error> {
2681 std::fs::create_dir_all(module_dir)?;
2682 std::fs::write(module_dir.join("mod.rs"), &output.lib_rs)?;
2683 for live in &output.live_stacks {
2684 write_rust_module(&live.output, &module_dir.join(&live.module_name))?;
2685 }
2686 Ok(())
2687}
2688
2689fn generate_stack_cargo_toml(config: &RustStackConfig) -> String {
2690 format!(
2691 r#"[package]
2692name = "{}"
2693version = "0.1.0"
2694edition = "2021"
2695
2696[dependencies]
2697arete-sdk = {{ package = "arete-a4-sdk", version = "{}" }}
2698serde = {{ version = "1", features = ["derive"] }}
2699serde_json = "1"
2700"#,
2701 config.crate_name, config.sdk_version
2702 )
2703}
2704
2705fn validate_extension_modules(config: &RustStackConfig, has_programs: bool) -> Result<(), String> {
2709 if config.extension_modules.is_empty() && config.extension_entry.is_none() {
2710 return Ok(());
2711 }
2712 if config.extension_entry.is_none() {
2713 return Err("extension modules were configured without an extension entry".to_string());
2714 }
2715 let mut seen = HashSet::new();
2716 for stem in &config.extension_modules {
2717 let reserved = matches!(stem.as_str(), "entity" | "types" | "mod" | "lib")
2718 || (stem == "programs" && has_programs);
2719 if reserved {
2720 return Err(format!(
2721 "extension file '{stem}.rs' collides with the generated '{stem}' module; rename the extension file"
2722 ));
2723 }
2724 if !seen.insert(stem.as_str()) {
2725 return Err(format!(
2726 "extension file '{stem}.rs' resolves to the same module name as another staged extension file"
2727 ));
2728 }
2729 }
2730 match &config.extension_entry {
2731 Some(entry) if config.extension_modules.last() == Some(entry) => Ok(()),
2732 Some(entry) => Err(format!(
2733 "extension entry module '{entry}' must be the last configured extension module"
2734 )),
2735 None => unreachable!("checked above"),
2736 }
2737}
2738
2739fn generate_stack_lib_rs(
2740 stack_name: &str,
2741 entity_names: &[String],
2742 _module_mode: bool,
2743 has_programs: bool,
2744 extension_modules: &[String],
2745 extension_entry: Option<&str>,
2746) -> String {
2747 let entity_views_exports: Vec<String> = entity_names
2748 .iter()
2749 .map(|name| format!("{}EntityViews", name))
2750 .collect();
2751
2752 let mut all_exports = format!(
2753 "{}Stack, {}StackViews, {}",
2754 stack_name,
2755 stack_name,
2756 entity_views_exports.join(", ")
2757 );
2758 if has_programs {
2759 all_exports.push_str(&format!(", {}StackPrograms", stack_name));
2760 }
2761
2762 let programs_mod = if has_programs {
2763 "\npub mod programs;"
2764 } else {
2765 ""
2766 };
2767
2768 let mut output = format!(
2769 r#"mod entity;
2770mod types;{programs_mod}
2771
2772pub use entity::{{{all_exports}}};
2773pub use types::*;
2774
2775pub use arete_sdk::{{ConnectionState, Arete, Stack, Update, Views}};
2776"#,
2777 programs_mod = programs_mod,
2778 all_exports = all_exports
2779 );
2780
2781 append_rust_extension_exports(&mut output, extension_modules, extension_entry);
2782
2783 output
2784}
2785
2786fn append_rust_extension_exports(
2787 output: &mut String,
2788 extension_modules: &[String],
2789 extension_entry: Option<&str>,
2790) {
2791 if let Some(entry) = extension_entry {
2792 output.push_str(
2793 "\n// Hand-authored devex extensions (staged from extensions.json; not generated).\n",
2794 );
2795 for stem in extension_modules {
2796 output.push_str(&format!("pub mod {stem};\n"));
2797 }
2798 output.push_str(&format!("pub use {entry}::*;\n"));
2799 }
2800}
2801
2802fn generate_stack_types_rs(
2808 entity_specs: &[SerializableStreamSpec],
2809 entity_names: &[String],
2810) -> (String, BTreeMap<String, String>) {
2811 let mut output = String::new();
2812 output.push_str("use serde::{Deserialize, Serialize};\n");
2813 output.push_str("use arete_sdk::serde_utils;\n\n");
2814
2815 let mut generated = HashSet::new();
2816 let mut account_structs: BTreeMap<String, String> = BTreeMap::new();
2817 let mut used_builtins: BTreeSet<&'static str> = BTreeSet::new();
2818
2819 for (i, spec) in entity_specs.iter().enumerate() {
2820 let entity_name = &entity_names[i];
2821 let compiler = RustCompiler::new(spec.clone(), entity_name.clone(), RustConfig::default());
2822 let resolved_name_map = compiler.build_resolved_type_name_map();
2823 used_builtins.extend(compiler.used_builtin_resolver_types());
2824
2825 for section in &spec.sections {
2827 if !RustCompiler::is_root_section(§ion.name) {
2828 let struct_name = format!("{}{}", entity_name, to_pascal_case(§ion.name));
2829 if generated.insert(struct_name) {
2830 output.push_str(
2831 &compiler.generate_struct_for_section(section, &resolved_name_map),
2832 );
2833 output.push_str("\n\n");
2834 }
2835 }
2836 }
2837
2838 output.push_str(&compiler.generate_main_entity_struct(&resolved_name_map));
2840 output.push_str("\n\n");
2841
2842 let resolved = compiler.generate_resolved_types(
2843 &resolved_name_map,
2844 &mut generated,
2845 Some(&mut account_structs),
2846 );
2847 output.push_str(&resolved);
2848 while !output.ends_with("\n\n") {
2849 output.push('\n');
2850 }
2851 }
2852
2853 output.push_str(&render_builtin_resolver_structs(&used_builtins));
2856
2857 output.push('\n');
2859 output.push_str(WRAPPER_TYPES);
2860
2861 (output, account_structs)
2862}
2863
2864fn generate_stack_entity_rs(
2866 stack_name: &str,
2867 stack_kebab: &str,
2868 entity_specs: &[SerializableStreamSpec],
2869 entity_names: &[String],
2870 config: &RustStackConfig,
2871 exact_views: bool,
2872 programs: Option<&ProgramsCodegen>,
2873) -> String {
2874 let types_import = if config.module_mode {
2875 "super::types"
2876 } else {
2877 "crate::types"
2878 };
2879
2880 let selected_entities = entity_specs
2881 .iter()
2882 .zip(entity_names)
2883 .filter(|(spec, _)| !exact_views || !spec.views.is_empty())
2884 .collect::<Vec<_>>();
2885 let entity_type_imports = selected_entities
2886 .iter()
2887 .map(|(_, name)| (*name).to_string())
2888 .collect::<Vec<_>>();
2889
2890 let url_impl = match &config.url {
2891 Some(url) => format!(
2892 r#"fn url() -> &'static str {{
2893 "{}"
2894 }}"#,
2895 url
2896 ),
2897 None => r#"fn url() -> &'static str {
2898 "" // TODO: Set URL after first deployment in arete.toml
2899 }"#
2900 .to_string(),
2901 };
2902
2903 let http_url_impl = match config.http_url.as_deref() {
2905 Some(http_url) if !http_url.is_empty() => format!(
2906 r#"
2907
2908 fn http_url() -> &'static str {{
2909 "{}"
2910 }}"#,
2911 http_url
2912 ),
2913 _ => String::new(),
2914 };
2915
2916 let views_fields: Vec<String> = selected_entities
2918 .iter()
2919 .map(|(_, name)| {
2920 let snake = to_snake_case(name);
2921 format!(" pub {}: {}EntityViews,", snake, name)
2922 })
2923 .collect();
2924
2925 let views_builder_fields: Vec<String> = selected_entities
2927 .iter()
2928 .enumerate()
2929 .map(|(i, (_, name))| {
2930 let snake = to_snake_case(name);
2931 if i < selected_entities.len() - 1 {
2932 format!(
2933 " {}: {}EntityViews {{ builder: builder.clone() }},",
2934 snake, name
2935 )
2936 } else {
2937 format!(" {}: {}EntityViews {{ builder }},", snake, name)
2938 }
2939 })
2940 .collect();
2941
2942 let mut entity_views_structs = Vec::new();
2944 for (i, entity_name) in entity_names.iter().enumerate() {
2945 let spec = &entity_specs[i];
2946 if exact_views && spec.views.is_empty() {
2947 continue;
2948 }
2949
2950 let derived: Vec<_> = spec
2951 .views
2952 .iter()
2953 .filter(|v| {
2954 !v.id.ends_with("/state")
2955 && !v.id.ends_with("/list")
2956 && v.id.starts_with(entity_name.as_str())
2957 })
2958 .collect();
2959
2960 let mut methods = Vec::new();
2961
2962 if !exact_views
2963 || spec
2964 .views
2965 .iter()
2966 .any(|view| view.id == format!("{entity_name}/state"))
2967 {
2968 methods.push(format!(
2969 r#" pub fn state(&self) -> StateView<{entity}> {{
2970 StateView::new(
2971 self.builder.connection().clone(),
2972 self.builder.store().clone(),
2973 "{entity}/state".to_string(),
2974 self.builder.initial_data_timeout(),
2975 )
2976 }}"#,
2977 entity = entity_name
2978 ));
2979 }
2980
2981 if !exact_views
2982 || spec
2983 .views
2984 .iter()
2985 .any(|view| view.id == format!("{entity_name}/list"))
2986 {
2987 methods.push(format!(
2988 r#"
2989 pub fn list(&self) -> ViewHandle<{entity}> {{
2990 self.builder.view("{entity}/list")
2991 }}"#,
2992 entity = entity_name
2993 ));
2994 }
2995
2996 for view in &derived {
2998 let view_name = view.id.split('/').nth(1).unwrap_or("unknown");
2999 let method_name = to_snake_case(view_name);
3000 methods.push(format!(
3001 r#"
3002 pub fn {method}(&self) -> ViewHandle<{entity}> {{
3003 self.builder.view("{view_id}")
3004 }}"#,
3005 method = method_name,
3006 entity = entity_name,
3007 view_id = view.id
3008 ));
3009 }
3010
3011 entity_views_structs.push(format!(
3012 r#"
3013pub struct {entity}EntityViews {{
3014 builder: ViewBuilder,
3015}}
3016
3017impl {entity}EntityViews {{
3018{methods}
3019}}"#,
3020 entity = entity_name,
3021 methods = methods.join("\n")
3022 ));
3023 }
3024
3025 let types_use = if entity_type_imports.is_empty() {
3026 String::new()
3027 } else {
3028 format!(
3029 "use {types_import}::{{{}}};\n",
3030 entity_type_imports.join(", ")
3031 )
3032 };
3033 let empty_builder = if selected_entities.is_empty() {
3034 " let _ = builder;\n"
3035 } else {
3036 ""
3037 };
3038
3039 let (programs_assoc, programs_struct) = match programs {
3042 Some(codegen) => {
3043 let programs_root = if config.module_mode { "super" } else { "crate" };
3044 let aggregate_name = format!("{}StackPrograms", stack_name);
3045 (
3046 format!("type Programs = {aggregate_name};"),
3047 format!(
3048 "\n{}",
3049 generate_programs_accessor_struct(
3050 &aggregate_name,
3051 codegen,
3052 &format!("{programs_root}::programs"),
3053 )
3054 ),
3055 )
3056 }
3057 None => ("type Programs = ();".to_string(), String::new()),
3058 };
3059
3060 format!(
3061 r#"{types_use}use arete_sdk::{{Stack, StateView, ViewBuilder, ViewHandle, Views}};
3062
3063pub struct {stack}Stack;
3064
3065impl Stack for {stack}Stack {{
3066 type Views = {stack}StackViews;
3067 {programs_assoc}
3068
3069 fn name() -> &'static str {{
3070 "{stack_kebab}"
3071 }}
3072
3073 {url_impl}{http_url_impl}
3074}}
3075
3076pub struct {stack}StackViews {{
3077{views_fields}
3078}}
3079
3080impl Views for {stack}StackViews {{
3081 fn from_builder(builder: ViewBuilder) -> Self {{
3082{empty_builder} Self {{
3083{views_builder}
3084 }}
3085 }}
3086}}
3087{entity_views}{programs_struct}"#,
3088 types_use = types_use,
3089 stack = stack_name,
3090 stack_kebab = stack_kebab,
3091 programs_assoc = programs_assoc,
3092 url_impl = url_impl,
3093 http_url_impl = http_url_impl,
3094 views_fields = views_fields.join("\n"),
3095 views_builder = views_builder_fields.join("\n"),
3096 entity_views = entity_views_structs.join("\n"),
3097 empty_builder = empty_builder,
3098 programs_struct = programs_struct,
3099 )
3100}
3101
3102fn generate_programs_accessor_struct(
3103 aggregate_name: &str,
3104 codegen: &ProgramsCodegen,
3105 programs_root: &str,
3106) -> String {
3107 let fields = codegen
3108 .modules
3109 .iter()
3110 .map(|module| {
3111 format!(
3112 " pub {}: {programs_root}::{}::{},",
3113 module.module_name, module.module_name, module.struct_name,
3114 )
3115 })
3116 .collect::<Vec<_>>();
3117 let inits = codegen
3118 .modules
3119 .iter()
3120 .enumerate()
3121 .map(|(index, module)| {
3122 let builder_expr = if index < codegen.modules.len() - 1 {
3123 "builder.clone()"
3124 } else {
3125 "builder"
3126 };
3127 format!(
3128 " {}: {programs_root}::{}::{}::from_builder({builder_expr}),",
3129 module.module_name, module.module_name, module.struct_name,
3130 )
3131 })
3132 .collect::<Vec<_>>();
3133
3134 format!(
3135 r#"pub struct {aggregate_name} {{
3136{fields}
3137}}
3138
3139impl arete_sdk::Programs for {aggregate_name} {{
3140 fn from_builder(builder: arete_sdk::ProgramBuilder) -> Self {{
3141 Self {{
3142{inits}
3143 }}
3144 }}
3145}}"#,
3146 fields = fields.join("\n"),
3147 inits = inits.join("\n"),
3148 )
3149}
3150
3151#[derive(Debug, Clone)]
3157pub(crate) struct ProgramModule {
3158 module_name: String,
3159 struct_name: String,
3160}
3161
3162#[derive(Debug, Clone)]
3164pub(crate) struct ProgramsCodegen {
3165 code: String,
3166 modules: Vec<ProgramModule>,
3167}
3168
3169#[derive(Debug, Default)]
3171struct ProgramImports {
3172 account_meta: bool,
3173 arg_schema: bool,
3174 pda: bool,
3175 error_metadata: bool,
3176}
3177
3178#[derive(Debug, Clone)]
3180struct RustParsedArg {
3181 schema: String,
3183 param_type: String,
3185 supported: bool,
3187}
3188
3189fn rust_unsupported() -> RustParsedArg {
3190 RustParsedArg {
3191 schema: "ArgType::U8".to_string(),
3192 param_type: "()".to_string(),
3193 supported: false,
3194 }
3195}
3196
3197fn rust_prim(schema: &str, param_type: &str) -> RustParsedArg {
3198 RustParsedArg {
3199 schema: schema.to_string(),
3200 param_type: param_type.to_string(),
3201 supported: true,
3202 }
3203}
3204
3205fn rust_string_literal(value: &str) -> String {
3207 format!("{:?}", value)
3208}
3209
3210struct RustDefinedTypes<'a> {
3215 defs: BTreeMap<String, &'a IdlTypeDefSnapshot>,
3217 lower: BTreeMap<String, String>,
3219 resolved: BTreeMap<String, Option<RustParsedArg>>,
3221 visiting: HashSet<String>,
3223}
3224
3225impl<'a> RustDefinedTypes<'a> {
3226 fn new(idls: &'a [IdlSnapshot]) -> Self {
3227 let mut defs: BTreeMap<String, &'a IdlTypeDefSnapshot> = BTreeMap::new();
3228 let mut lower: BTreeMap<String, String> = BTreeMap::new();
3229 for idl in idls {
3230 for def in &idl.types {
3231 if !defs.contains_key(def.name.as_str()) {
3232 defs.insert(def.name.clone(), def);
3233 lower.insert(def.name.to_lowercase(), def.name.clone());
3234 }
3235 }
3236 }
3237 RustDefinedTypes {
3238 defs,
3239 lower,
3240 resolved: BTreeMap::new(),
3241 visiting: HashSet::new(),
3242 }
3243 }
3244
3245 fn parse_arg_type(&mut self, raw: &str) -> RustParsedArg {
3248 let t = raw.trim().trim_start_matches('&').trim();
3249
3250 if let Some((name, inner)) = split_generic(t) {
3252 match name {
3253 "Option" => {
3254 let inner = self.parse_arg_type(inner);
3255 return RustParsedArg {
3256 schema: format!("ArgType::Option(Box::new({}))", inner.schema),
3257 param_type: format!("Option<{}>", inner.param_type),
3258 supported: inner.supported,
3259 };
3260 }
3261 "Vec" => {
3262 let inner = self.parse_arg_type(inner);
3263 return RustParsedArg {
3264 schema: format!("ArgType::Vec(Box::new({}))", inner.schema),
3265 param_type: format!("Vec<{}>", inner.param_type),
3266 supported: inner.supported,
3267 };
3268 }
3269 _ => return rust_unsupported(),
3270 }
3271 }
3272
3273 if let Some(stripped) = t.strip_prefix('[').and_then(|s| s.strip_suffix(']')) {
3275 if let Some((ty, n)) = stripped.rsplit_once(';') {
3276 let inner = self.parse_arg_type(ty.trim());
3277 let n = n.trim();
3278 if n.parse::<usize>().is_ok() {
3279 return RustParsedArg {
3280 schema: format!("ArgType::Array(Box::new({}), {})", inner.schema, n),
3281 param_type: format!("Vec<{}>", inner.param_type),
3282 supported: inner.supported,
3283 };
3284 }
3285 }
3286 }
3287
3288 let last = t.rsplit("::").next().unwrap_or(t);
3290 match last {
3291 "u8" => rust_prim("ArgType::U8", "u8"),
3292 "u16" => rust_prim("ArgType::U16", "u16"),
3293 "u32" => rust_prim("ArgType::U32", "u32"),
3294 "u64" => rust_prim("ArgType::U64", "u64"),
3295 "u128" => rust_prim("ArgType::U128", "String"),
3298 "i8" => rust_prim("ArgType::I8", "i8"),
3299 "i16" => rust_prim("ArgType::I16", "i16"),
3300 "i32" => rust_prim("ArgType::I32", "i32"),
3301 "i64" => rust_prim("ArgType::I64", "i64"),
3302 "i128" => rust_prim("ArgType::I128", "String"),
3303 "f32" => rust_prim("ArgType::F32", "f32"),
3304 "f64" => rust_prim("ArgType::F64", "f64"),
3305 "bool" => rust_prim("ArgType::Bool", "bool"),
3306 "String" | "string" | "str" => rust_prim("ArgType::String", "String"),
3307 "Pubkey" | "pubkey" | "PublicKey" | "publicKey" => {
3308 rust_prim("ArgType::Pubkey", "String")
3309 }
3310 "bytes" => rust_prim("ArgType::Bytes", "Vec<u8>"),
3311 _ => self.resolve_defined(last).unwrap_or_else(rust_unsupported),
3312 }
3313 }
3314
3315 fn parse_snapshot_type(&mut self, t: &IdlTypeSnapshot) -> RustParsedArg {
3317 match t {
3318 IdlTypeSnapshot::Simple(s) => self.parse_arg_type(s),
3319 IdlTypeSnapshot::Option(o) => {
3320 let inner = self.parse_snapshot_type(&o.option);
3321 RustParsedArg {
3322 schema: format!("ArgType::Option(Box::new({}))", inner.schema),
3323 param_type: format!("Option<{}>", inner.param_type),
3324 supported: inner.supported,
3325 }
3326 }
3327 IdlTypeSnapshot::Vec(v) => {
3328 let inner = self.parse_snapshot_type(&v.vec);
3329 RustParsedArg {
3330 schema: format!("ArgType::Vec(Box::new({}))", inner.schema),
3331 param_type: format!("Vec<{}>", inner.param_type),
3332 supported: inner.supported,
3333 }
3334 }
3335 IdlTypeSnapshot::Array(arr) => {
3336 let mut element: Option<RustParsedArg> = None;
3337 let mut size: Option<u32> = None;
3338 for part in &arr.array {
3339 match part {
3340 IdlArrayElementSnapshot::Type(inner) => {
3341 element = Some(self.parse_snapshot_type(inner))
3342 }
3343 IdlArrayElementSnapshot::TypeName(name) => {
3344 element = Some(self.parse_arg_type(name))
3345 }
3346 IdlArrayElementSnapshot::Size(n) => size = Some(*n),
3347 }
3348 }
3349 match (element, size) {
3350 (Some(inner), Some(n)) => RustParsedArg {
3351 schema: format!("ArgType::Array(Box::new({}), {})", inner.schema, n),
3352 param_type: format!("Vec<{}>", inner.param_type),
3353 supported: inner.supported,
3354 },
3355 _ => rust_unsupported(),
3356 }
3357 }
3358 IdlTypeSnapshot::HashMap(map) => {
3359 let key = self.parse_snapshot_type(&map.hash_map.0);
3360 let value = self.parse_snapshot_type(&map.hash_map.1);
3361 if !key.supported || key.schema != "ArgType::String" || !value.supported {
3362 rust_unsupported()
3363 } else {
3364 RustParsedArg {
3365 schema: format!(
3366 "ArgType::HashMap(Box::new({}), Box::new({}))",
3367 key.schema, value.schema
3368 ),
3369 param_type: "serde_json::Value".to_string(),
3370 supported: true,
3371 }
3372 }
3373 }
3374 IdlTypeSnapshot::Defined(d) => {
3375 let name = match &d.defined {
3376 IdlDefinedInnerSnapshot::Named { name } => name.as_str(),
3377 IdlDefinedInnerSnapshot::Simple(s) => s.as_str(),
3378 };
3379 self.resolve_defined(name).unwrap_or_else(rust_unsupported)
3380 }
3381 }
3382 }
3383
3384 fn resolve_defined(&mut self, name: &str) -> Option<RustParsedArg> {
3387 if let Some(cached) = self.resolved.get(name) {
3388 return cached.clone();
3389 }
3390 if self.visiting.contains(name) {
3391 return None;
3393 }
3394
3395 let key = if self.defs.contains_key(name) {
3396 name.to_string()
3397 } else {
3398 match self.lower.get(&name.to_lowercase()) {
3399 Some(canonical) => canonical.clone(),
3400 None => {
3401 self.resolved.insert(name.to_string(), None);
3402 return None;
3403 }
3404 }
3405 };
3406
3407 self.visiting.insert(key.clone());
3408 let def = self.defs[&key];
3409 let result = match &def.type_def {
3410 IdlTypeDefKindSnapshot::Struct { fields, .. } => {
3411 let fields = fields.clone();
3412 self.resolve_struct(&fields)
3413 }
3414 IdlTypeDefKindSnapshot::TupleStruct { .. } => None,
3415 IdlTypeDefKindSnapshot::Enum { variants, .. } => {
3416 let variants = variants.clone();
3417 self.resolve_enum(&variants)
3418 }
3419 };
3420 self.visiting.remove(&key);
3421 self.resolved.insert(name.to_string(), result.clone());
3422 if name != key {
3423 self.resolved.insert(key, result.clone());
3424 }
3425 result
3426 }
3427
3428 fn resolve_struct(&mut self, fields: &[IdlFieldSnapshot]) -> Option<RustParsedArg> {
3429 let mut field_exprs: Vec<String> = Vec::new();
3430 for field in fields {
3431 let parsed = self.parse_snapshot_type(&field.type_);
3432 if !parsed.supported {
3433 return None;
3434 }
3435 field_exprs.push(format!(
3436 "ArgField {{ name: {}.to_string(), ty: {} }}",
3437 rust_string_literal(&field.name),
3438 parsed.schema
3439 ));
3440 }
3441 Some(RustParsedArg {
3442 schema: format!("ArgType::Struct(vec![{}])", field_exprs.join(", ")),
3443 param_type: "serde_json::Value".to_string(),
3444 supported: true,
3445 })
3446 }
3447
3448 fn resolve_enum(&mut self, variants: &[IdlEnumVariantSnapshot]) -> Option<RustParsedArg> {
3449 let mut variant_exprs: Vec<String> = Vec::new();
3450 for variant in variants {
3451 let name_literal = rust_string_literal(&variant.name);
3452 if variant.fields.is_empty() {
3453 variant_exprs.push(format!(
3454 "EnumVariantDef {{ name: {}.to_string(), kind: EnumVariantKind::Unit }}",
3455 name_literal
3456 ));
3457 continue;
3458 }
3459
3460 let named: Vec<_> = variant
3461 .fields
3462 .iter()
3463 .filter_map(|field| match field {
3464 IdlEnumVariantFieldSnapshot::Named(field) => Some(field),
3465 IdlEnumVariantFieldSnapshot::Tuple(_) => None,
3466 })
3467 .collect();
3468
3469 if named.len() == variant.fields.len() {
3470 let mut field_exprs: Vec<String> = Vec::new();
3471 for field in named {
3472 let parsed = self.parse_snapshot_type(&field.type_);
3473 if !parsed.supported {
3474 return None;
3475 }
3476 field_exprs.push(format!(
3477 "ArgField {{ name: {}.to_string(), ty: {} }}",
3478 rust_string_literal(&field.name),
3479 parsed.schema
3480 ));
3481 }
3482 variant_exprs.push(format!(
3483 "EnumVariantDef {{ name: {}.to_string(), kind: EnumVariantKind::Struct(vec![{}]) }}",
3484 name_literal,
3485 field_exprs.join(", ")
3486 ));
3487 } else if named.is_empty() {
3488 let mut element_exprs: Vec<String> = Vec::new();
3489 for field in &variant.fields {
3490 let IdlEnumVariantFieldSnapshot::Tuple(ty) = field else {
3491 unreachable!("named.is_empty() guarantees tuple fields");
3492 };
3493 let parsed = self.parse_snapshot_type(ty);
3494 if !parsed.supported {
3495 return None;
3496 }
3497 element_exprs.push(parsed.schema);
3498 }
3499 variant_exprs.push(format!(
3500 "EnumVariantDef {{ name: {}.to_string(), kind: EnumVariantKind::Tuple(vec![{}]) }}",
3501 name_literal,
3502 element_exprs.join(", ")
3503 ));
3504 } else {
3505 return None;
3507 }
3508 }
3509 Some(RustParsedArg {
3510 schema: format!("ArgType::Enum(vec![{}])", variant_exprs.join(", ")),
3511 param_type: "serde_json::Value".to_string(),
3512 supported: true,
3513 })
3514 }
3515}
3516
3517fn schema_uses_defined_types(schema: &str) -> bool {
3520 schema.contains("ArgField") || schema.contains("EnumVariantDef")
3521}
3522
3523#[derive(Debug, Clone, Copy, PartialEq)]
3525enum RustAccountFieldKind {
3526 Signer,
3528 Required,
3530 Optional,
3532}
3533
3534struct MappedRustAccount {
3536 literal: String,
3538 field: Option<(String, RustAccountFieldKind)>,
3540 notes: Vec<String>,
3542 uses_pda: bool,
3544}
3545
3546fn rust_account_meta_literal(
3547 acc: &InstructionAccountDef,
3548 resolution: &str,
3549 comment: Option<&str>,
3550) -> String {
3551 let mut out = String::new();
3552 if let Some(comment) = comment {
3553 out.push_str(&format!(" // [arete codegen] {}\n", comment));
3554 }
3555 out.push_str(&format!(
3556 " AccountMeta {{\n name: {name}.to_string(),\n is_signer: {is_signer},\n is_writable: {is_writable},\n resolution: {resolution},\n is_optional: {is_optional},\n }},",
3557 name = rust_string_literal(&acc.name),
3558 is_signer = acc.is_signer,
3559 is_writable = acc.is_writable,
3560 resolution = resolution,
3561 is_optional = acc.is_optional,
3562 ));
3563 out
3564}
3565
3566fn map_rust_account(
3567 acc: &InstructionAccountDef,
3568 pda_lookup: &BTreeMap<&str, &PdaDefinition>,
3569 account_names: &HashSet<&str>,
3570 arg_types: &BTreeMap<&str, &str>,
3571) -> MappedRustAccount {
3572 let user_field_kind = if acc.is_optional {
3573 RustAccountFieldKind::Optional
3574 } else {
3575 RustAccountFieldKind::Required
3576 };
3577 let degraded = |reason: String| -> MappedRustAccount {
3578 let note = format!(
3579 "account `{}` degraded to user-provided ({})",
3580 acc.name, reason
3581 );
3582 MappedRustAccount {
3583 literal: rust_account_meta_literal(acc, "AccountResolution::UserProvided", Some(¬e)),
3584 field: Some((acc.name.clone(), user_field_kind)),
3585 notes: vec![note],
3586 uses_pda: false,
3587 }
3588 };
3589
3590 match &acc.resolution {
3591 AccountResolution::Signer => MappedRustAccount {
3592 literal: rust_account_meta_literal(acc, "AccountResolution::Signer", None),
3593 field: Some((acc.name.clone(), RustAccountFieldKind::Signer)),
3594 notes: Vec::new(),
3595 uses_pda: false,
3596 },
3597 AccountResolution::Known { address } => MappedRustAccount {
3598 literal: rust_account_meta_literal(
3599 acc,
3600 &format!(
3601 "AccountResolution::Known({}.to_string())",
3602 rust_string_literal(address)
3603 ),
3604 None,
3605 ),
3606 field: None,
3607 notes: Vec::new(),
3608 uses_pda: false,
3609 },
3610 AccountResolution::UserProvided => MappedRustAccount {
3611 literal: rust_account_meta_literal(acc, "AccountResolution::UserProvided", None),
3612 field: Some((acc.name.clone(), user_field_kind)),
3613 notes: Vec::new(),
3614 uses_pda: false,
3615 },
3616 AccountResolution::PdaInline {
3617 seeds,
3618 program_id,
3619 program,
3620 } => {
3621 if program.is_some() {
3622 return degraded(
3623 "uses a dynamic PDA program selector not supported by the Rust low-level resolver"
3624 .to_string(),
3625 );
3626 }
3627 match build_rust_pda_config(seeds, program_id.as_deref(), account_names, arg_types) {
3628 Ok((resolution, notes)) => MappedRustAccount {
3629 literal: rust_account_meta_literal(acc, &resolution, None),
3630 field: None,
3631 notes,
3632 uses_pda: true,
3633 },
3634 Err(reason) => degraded(reason),
3635 }
3636 }
3637 AccountResolution::PdaRef { pda_name } => match pda_lookup.get(pda_name.as_str()) {
3638 Some(def) => {
3639 if def.program.is_some() {
3640 return degraded(format!(
3641 "PDA '{}' uses a dynamic program selector not supported by the Rust low-level resolver",
3642 pda_name
3643 ));
3644 }
3645 match build_rust_pda_config(
3646 &def.seeds,
3647 def.program_id.as_deref(),
3648 account_names,
3649 arg_types,
3650 ) {
3651 Ok((resolution, notes)) => MappedRustAccount {
3652 literal: rust_account_meta_literal(acc, &resolution, None),
3653 field: None,
3654 notes,
3655 uses_pda: true,
3656 },
3657 Err(reason) => degraded(format!("PDA '{}': {}", pda_name, reason)),
3658 }
3659 }
3660 None => degraded(format!("references unknown PDA '{}'", pda_name)),
3661 },
3662 }
3663}
3664
3665fn build_rust_pda_config(
3669 seeds: &[PdaSeedDef],
3670 program_id: Option<&str>,
3671 account_names: &HashSet<&str>,
3672 arg_types: &BTreeMap<&str, &str>,
3673) -> Result<(String, Vec<String>), String> {
3674 let mut seed_exprs: Vec<String> = Vec::new();
3675 let mut notes: Vec<String> = Vec::new();
3676 for seed in seeds {
3677 match seed {
3678 PdaSeedDef::Literal { value } => {
3679 seed_exprs.push(format!(
3680 "PdaSeed::Literal({}.to_string())",
3681 rust_string_literal(value)
3682 ));
3683 }
3684 PdaSeedDef::Bytes { value } => {
3685 let bytes: Vec<String> = value.iter().map(|b| b.to_string()).collect();
3686 seed_exprs.push(format!("PdaSeed::Bytes(vec![{}])", bytes.join(", ")));
3687 }
3688 PdaSeedDef::AccountRef { account_name } => {
3689 if account_name.contains('.') {
3690 return Err(format!(
3691 "seed references account field '{}' which is not supported for auto-resolution",
3692 account_name
3693 ));
3694 }
3695 if !account_names.contains(account_name.as_str()) {
3696 return Err(format!(
3697 "seed references account '{}' not present in this instruction",
3698 account_name
3699 ));
3700 }
3701 seed_exprs.push(format!(
3702 "PdaSeed::AccountRef({}.to_string())",
3703 rust_string_literal(account_name)
3704 ));
3705 }
3706 PdaSeedDef::ArgRef { arg_name, arg_type } => {
3707 let arg_root = arg_name.split('.').next().unwrap_or(arg_name.as_str());
3708 let present =
3709 arg_types.contains_key(arg_name.as_str()) || arg_types.contains_key(arg_root);
3710 let raw_type = arg_type
3713 .as_deref()
3714 .or_else(|| arg_types.get(arg_name.as_str()).copied())
3715 .or_else(|| arg_types.get(arg_root).copied());
3716 let canonical = raw_type.and_then(normalize_seed_arg_type);
3717 if !present {
3718 if canonical.is_none() {
3719 return Err(format!(
3720 "seed helper arg '{}' is not present in this instruction and has no primitive type information",
3721 arg_name
3722 ));
3723 }
3724 notes.push(format!(
3725 "seed input `{}` must be supplied via the `resolve` key when building through the raw handler",
3726 arg_name
3727 ));
3728 }
3729 match canonical {
3730 Some(canonical) => seed_exprs.push(format!(
3731 "PdaSeed::ArgRef {{ arg: {}.to_string(), arg_type: Some({}.to_string()) }}",
3732 rust_string_literal(arg_name),
3733 rust_string_literal(&canonical)
3734 )),
3735 None => {
3736 notes.push(format!(
3737 "seed arg `{}` has non-primitive type '{}'; the runtime will use heuristic encoding",
3738 arg_name,
3739 raw_type.unwrap_or("<unknown>")
3740 ));
3741 seed_exprs.push(format!(
3742 "PdaSeed::ArgRef {{ arg: {}.to_string(), arg_type: None }}",
3743 rust_string_literal(arg_name)
3744 ));
3745 }
3746 }
3747 }
3748 }
3749 }
3750
3751 let program_expr = match program_id {
3752 Some(pid) => format!("Some({}.to_string())", rust_string_literal(pid)),
3753 None => "None".to_string(),
3754 };
3755 Ok((
3756 format!(
3757 "AccountResolution::Pda(PdaConfig {{ program_id: {}, seeds: vec![{}] }})",
3758 program_expr,
3759 seed_exprs.join(", ")
3760 ),
3761 notes,
3762 ))
3763}
3764
3765struct RustInstructionBlock {
3767 code: String,
3768 method: String,
3769 uses_defined_types: bool,
3770}
3771
3772fn generate_rust_instruction_block(
3773 instr: &InstructionDef,
3774 errors: &[IdlErrorSnapshot],
3775 pda_lookup: &BTreeMap<&str, &PdaDefinition>,
3776 parser: &mut RustDefinedTypes<'_>,
3777 needs: &mut ProgramImports,
3778) -> Result<RustInstructionBlock, String> {
3779 let mut parsed_args: Vec<(&InstructionArgDef, RustParsedArg)> = Vec::new();
3781 for arg in &instr.args {
3782 let parsed = parser.parse_arg_type(&arg.arg_type);
3783 if !parsed.supported {
3784 return Err(format!(
3785 "arg '{}' has unsupported type '{}'",
3786 arg.name, arg.arg_type
3787 ));
3788 }
3789 parsed_args.push((arg, parsed));
3790 }
3791
3792 let account_names: HashSet<&str> = instr.accounts.iter().map(|a| a.name.as_str()).collect();
3794 let arg_types: BTreeMap<&str, &str> = instr
3795 .args
3796 .iter()
3797 .map(|a| (a.name.as_str(), a.arg_type.as_str()))
3798 .collect();
3799
3800 let mut account_literals: Vec<String> = Vec::new();
3801 let mut account_fields: Vec<(String, RustAccountFieldKind)> = Vec::new();
3802 let mut notes: Vec<String> = Vec::new();
3803 for acc in &instr.accounts {
3804 let mapped = map_rust_account(acc, pda_lookup, &account_names, &arg_types);
3805 account_literals.push(mapped.literal);
3806 if let Some(field) = mapped.field {
3807 account_fields.push(field);
3808 }
3809 notes.extend(mapped.notes);
3810 if mapped.uses_pda {
3811 needs.pda = true;
3812 }
3813 }
3814 if !instr.accounts.is_empty() {
3815 needs.account_meta = true;
3816 }
3817 if !instr.args.is_empty() {
3818 needs.arg_schema = true;
3819 }
3820 if !errors.is_empty() {
3821 needs.error_metadata = true;
3822 }
3823
3824 let fn_name = to_snake_case(&instr.name);
3825 let pascal = to_pascal_case(&instr.name);
3826 let params_name = format!("{}Params", pascal);
3827
3828 let arg_name_set: HashSet<&str> = instr.args.iter().map(|a| a.name.as_str()).collect();
3832 let mut used_field_names: HashSet<String> = HashSet::new();
3833 let mut param_fields: Vec<String> = Vec::new();
3834 let mut uses_defined_types = false;
3835 for (arg, parsed) in &parsed_args {
3836 let field_name = to_snake_case(&arg.name);
3837 used_field_names.insert(field_name.clone());
3838 uses_defined_types |= schema_uses_defined_types(&parsed.schema);
3839 let mut lines = Vec::new();
3840 if field_name != arg.name {
3841 lines.push(format!(
3842 " #[serde(rename = {})]",
3843 rust_string_literal(&arg.name)
3844 ));
3845 }
3846 lines.push(format!(
3847 " pub {}: {},",
3848 field_name, parsed.param_type
3849 ));
3850 param_fields.push(lines.join("\n"));
3851 }
3852 for (name, kind) in &account_fields {
3853 if arg_name_set.contains(name.as_str()) {
3854 notes.push(format!(
3855 "account `{}` shares its name with an instruction arg and has no typed override field",
3856 name
3857 ));
3858 continue;
3859 }
3860 let field_name = to_snake_case(name);
3861 if !used_field_names.insert(field_name.clone()) {
3862 notes.push(format!(
3863 "account `{}` collides with another params field and has no typed override field",
3864 name
3865 ));
3866 continue;
3867 }
3868 let mut lines = Vec::new();
3869 match kind {
3870 RustAccountFieldKind::Signer => lines.push(format!(
3871 " /// Optional address override for the `{}` signer (defaults to the payer).",
3872 name
3873 )),
3874 RustAccountFieldKind::Required => {
3875 lines.push(format!(" /// Address of the `{}` account.", name))
3876 }
3877 RustAccountFieldKind::Optional => lines.push(format!(
3878 " /// Optional address of the `{}` account.",
3879 name
3880 )),
3881 }
3882 if field_name != *name {
3883 lines.push(format!(
3884 " #[serde(rename = {})]",
3885 rust_string_literal(name)
3886 ));
3887 }
3888 match kind {
3889 RustAccountFieldKind::Required => {
3890 lines.push(format!(" pub {}: String,", field_name))
3891 }
3892 _ => {
3893 lines.push(
3894 " #[serde(skip_serializing_if = \"Option::is_none\")]".to_string(),
3895 );
3896 lines.push(format!(" pub {}: Option<String>,", field_name));
3897 }
3898 }
3899 param_fields.push(lines.join("\n"));
3900 }
3901
3902 let params_struct = if param_fields.is_empty() {
3903 format!(
3904 " /// Typed params for `{name}` (no args or caller-supplied accounts).\n #[derive(Debug, Clone, Serialize, Default)]\n pub struct {params_name} {{}}",
3905 name = instr.name,
3906 params_name = params_name
3907 )
3908 } else {
3909 format!(
3910 " /// Typed params for `{name}`: instruction args plus overridable accounts.\n #[derive(Debug, Clone, Serialize, Default)]\n pub struct {params_name} {{\n{fields}\n }}",
3911 name = instr.name,
3912 params_name = params_name,
3913 fields = param_fields.join("\n")
3914 )
3915 };
3916
3917 let mut doc_lines: Vec<String> = instr
3919 .docs
3920 .iter()
3921 .map(|line| line.trim().to_string())
3922 .collect();
3923 if doc_lines.is_empty() {
3924 doc_lines.push(format!("Builds the `{}` instruction.", instr.name));
3925 }
3926 if !notes.is_empty() {
3927 doc_lines.push(String::new());
3928 doc_lines.push("Codegen notes:".to_string());
3929 for note in ¬es {
3930 doc_lines.push(format!("- {}", note));
3931 }
3932 }
3933 let docs = doc_lines
3934 .iter()
3935 .map(|line| {
3936 if line.is_empty() {
3937 " ///".to_string()
3938 } else {
3939 format!(" /// {}", line)
3940 }
3941 })
3942 .collect::<Vec<_>>()
3943 .join("\n");
3944
3945 let typed_fn = format!(
3946 "{docs}\n pub fn {fn_name}(params: {params_name}) -> Result<BuiltInstruction, InstructionError> {{\n let params = serde_json::to_value(params).map_err(|error| InstructionError::InvalidValue {{\n context: \"params\".to_string(),\n message: error.to_string(),\n }})?;\n {fn_name}_handler().build(params)\n }}",
3947 docs = docs,
3948 fn_name = fn_name,
3949 params_name = params_name
3950 );
3951
3952 let discriminator = instr
3954 .discriminator
3955 .iter()
3956 .map(|b| b.to_string())
3957 .collect::<Vec<_>>()
3958 .join(", ");
3959 let accounts_literal = if account_literals.is_empty() {
3960 "vec![]".to_string()
3961 } else {
3962 format!("vec![\n{}\n ]", account_literals.join("\n"))
3963 };
3964 let args_literal = if parsed_args.is_empty() {
3965 "vec![]".to_string()
3966 } else {
3967 let entries: Vec<String> = parsed_args
3968 .iter()
3969 .map(|(arg, parsed)| {
3970 format!(
3971 " ArgSchema {{ name: {}.to_string(), ty: {} }},",
3972 rust_string_literal(&arg.name),
3973 parsed.schema
3974 )
3975 })
3976 .collect();
3977 format!("vec![\n{}\n ]", entries.join("\n"))
3978 };
3979 let errors_literal = if errors.is_empty() {
3980 "vec![]".to_string()
3981 } else {
3982 let entries: Vec<String> = errors
3983 .iter()
3984 .map(|error| {
3985 format!(
3986 " ErrorMetadata {{ code: {}, name: {}.to_string(), msg: {}.to_string() }},",
3987 error.code,
3988 rust_string_literal(&error.name),
3989 rust_string_literal(error.msg.as_deref().unwrap_or(""))
3990 )
3991 })
3992 .collect();
3993 format!("vec![\n{}\n ]", entries.join("\n"))
3994 };
3995
3996 let handler_fn = format!(
3997 " /// Raw instruction handler for `{name}`.\n pub fn {fn_name}_handler() -> InstructionHandler {{\n InstructionHandler {{\n program_id: PROGRAM_ID.to_string(),\n discriminator: vec![{discriminator}],\n accounts: {accounts},\n args: {args},\n errors: {errors},\n }}\n }}",
3998 name = instr.name,
3999 fn_name = fn_name,
4000 discriminator = discriminator,
4001 accounts = accounts_literal,
4002 args = args_literal,
4003 errors = errors_literal
4004 );
4005
4006 let method = format!(
4007 " pub fn {fn_name}(&self, params: {params_name}) -> Result<BuiltInstruction, InstructionError> {{\n {fn_name}(params)\n }}",
4008 fn_name = fn_name,
4009 params_name = params_name
4010 );
4011
4012 Ok(RustInstructionBlock {
4013 code: format!("{}\n\n{}\n\n{}", params_struct, typed_fn, handler_fn),
4014 method,
4015 uses_defined_types,
4016 })
4017}
4018
4019fn generate_rust_pdas_module(pdas: &BTreeMap<String, PdaDefinition>) -> Option<String> {
4022 if pdas.is_empty() {
4023 return None;
4024 }
4025
4026 let mut fns: Vec<String> = Vec::new();
4027 let mut needs_serialize = false;
4028 let mut needs_program_id = false;
4029 for def in pdas.values() {
4030 let fn_name = to_snake_case(&def.name);
4031 let mut params: Vec<(String, String)> = Vec::new();
4032 let mut seed_exprs: Vec<String> = Vec::new();
4033 for seed in &def.seeds {
4034 match seed {
4035 PdaSeedDef::Literal { value } => {
4036 seed_exprs.push(format!(
4037 "{}.as_bytes().to_vec()",
4038 rust_string_literal(value)
4039 ));
4040 }
4041 PdaSeedDef::Bytes { value } => {
4042 let bytes: Vec<String> = value.iter().map(|b| b.to_string()).collect();
4043 seed_exprs.push(format!("vec![{}]", bytes.join(", ")));
4044 }
4045 PdaSeedDef::AccountRef { account_name } => {
4046 let param = to_snake_case(account_name);
4047 if !params.iter().any(|(name, _)| *name == param) {
4048 params.push((param.clone(), "&str".to_string()));
4049 }
4050 needs_serialize = true;
4051 seed_exprs.push(format!(
4052 "serialize_seed_value(&serde_json::json!({}), Some(\"pubkey\"))?",
4053 param
4054 ));
4055 }
4056 PdaSeedDef::ArgRef { arg_name, arg_type } => {
4057 let param = to_snake_case(arg_name);
4058 let canonical = arg_type.as_deref().and_then(normalize_seed_arg_type);
4059 let (param_type, hint) = match canonical.as_deref() {
4060 Some("pubkey") => ("&str", "Some(\"pubkey\")".to_string()),
4061 Some("string") => ("&str", "Some(\"string\")".to_string()),
4062 Some(int) if int.starts_with('i') => {
4063 ("i64", format!("Some({})", rust_string_literal(int)))
4064 }
4065 Some(int) => ("u64", format!("Some({})", rust_string_literal(int))),
4066 None => ("&str", "None".to_string()),
4067 };
4068 if !params.iter().any(|(name, _)| *name == param) {
4069 params.push((param.clone(), param_type.to_string()));
4070 }
4071 needs_serialize = true;
4072 seed_exprs.push(format!(
4073 "serialize_seed_value(&serde_json::json!({}), {})?",
4074 param, hint
4075 ));
4076 }
4077 }
4078 }
4079
4080 let program_expr = match (&def.program_id, &def.program) {
4081 (Some(pid), _) => rust_string_literal(pid),
4082 (None, Some(PdaProgramDef::AccountRef { account_name })) => {
4083 let param = to_snake_case(account_name);
4084 if !params.iter().any(|(name, _)| *name == param) {
4085 params.push((param.clone(), "&str".to_string()));
4086 }
4087 param
4088 }
4089 (None, Some(PdaProgramDef::ArgRef { arg_name })) => {
4090 let param = to_snake_case(arg_name);
4091 if !params.iter().any(|(name, _)| *name == param) {
4092 params.push((param.clone(), "&str".to_string()));
4093 }
4094 param
4095 }
4096 (None, None) => {
4097 needs_program_id = true;
4098 "PROGRAM_ID".to_string()
4099 }
4100 };
4101 let param_list = params
4102 .iter()
4103 .map(|(name, ty)| format!("{}: {}", name, ty))
4104 .collect::<Vec<_>>()
4105 .join(", ");
4106 let seeds_body = if seed_exprs.is_empty() {
4107 " let seeds: Vec<Vec<u8>> = vec![];".to_string()
4108 } else {
4109 format!(
4110 " let seeds: Vec<Vec<u8>> = vec![\n{}\n ];",
4111 seed_exprs
4112 .iter()
4113 .map(|expr| format!(" {},", expr))
4114 .collect::<Vec<_>>()
4115 .join("\n")
4116 )
4117 };
4118 fns.push(format!(
4119 " /// Derive the `{name}` PDA (returns the address and bump).\n pub fn {fn_name}({params}) -> Result<(Pubkey, u8), InstructionError> {{\n{seeds}\n derive_program_address(&seeds, {program})\n }}",
4120 name = def.name,
4121 fn_name = fn_name,
4122 params = param_list,
4123 seeds = seeds_body,
4124 program = program_expr
4125 ));
4126 }
4127
4128 let mut imports = vec!["derive_program_address"];
4129 if needs_serialize {
4130 imports.push("serialize_seed_value");
4131 }
4132 imports.extend(["InstructionError", "Pubkey"]);
4133 imports.sort_unstable();
4134 let mut use_lines = format!(
4135 " use arete_sdk::instruction::{{{}}};",
4136 imports.join(", ")
4137 );
4138 if needs_program_id {
4139 use_lines.push_str("\n\n use super::PROGRAM_ID;");
4140 }
4141
4142 Some(format!(
4143 " /// PDA derivation helpers for this program.\n pub mod pdas {{\n{use_lines}\n\n{fns}\n }}",
4144 use_lines = use_lines,
4145 fns = fns.join("\n\n")
4146 ))
4147}
4148
4149type ProgramReadLayer = Result<(String, String, Option<serde_json::Value>), String>;
4152
4153fn resolve_program_read_layer(
4156 program_specs: &[arete_hash::ProgramSpecV1],
4157 program_id: &str,
4158) -> ProgramReadLayer {
4159 let Some(spec) = program_specs
4160 .iter()
4161 .find(|spec| spec.program_id == program_id)
4162 else {
4163 return Err("no program specification was recorded for this program".to_string());
4164 };
4165 let spec_hash = spec
4166 .hash()
4167 .map_err(|error| format!("failed to compute the program spec hash ({error})"))?;
4168 let release_hash = spec
4169 .oss_release_hash()
4170 .map_err(|error| format!("failed to compute the release hash ({error})"))?;
4171 Ok((spec_hash.to_string(), release_hash.to_string(), None))
4172}
4173
4174#[allow(clippy::too_many_arguments)]
4179fn generate_stack_programs_rs(
4180 stack_name: &str,
4181 instructions: &[InstructionDef],
4182 idls: &[IdlSnapshot],
4183 pdas: &BTreeMap<String, BTreeMap<String, PdaDefinition>>,
4184 program_ids: &[String],
4185 program_specs: &[arete_hash::ProgramSpecV1],
4186 account_structs: &BTreeMap<String, String>,
4187 module_mode: bool,
4188 reads: &[RustProgramReadConfig],
4189 include_idl_only_programs: bool,
4190) -> Option<ProgramsCodegen> {
4191 if instructions.is_empty() && !include_idl_only_programs {
4192 return None;
4193 }
4194
4195 let types_path = if module_mode {
4198 "super::super::types"
4199 } else {
4200 "crate::types"
4201 };
4202
4203 let default_program_id = program_ids.first().cloned().unwrap_or_default();
4204
4205 let mut groups: Vec<(String, Vec<&InstructionDef>)> = Vec::new();
4207 if include_idl_only_programs {
4208 for (index, idl) in idls.iter().enumerate() {
4209 let program_id = idl
4210 .program_id
4211 .clone()
4212 .or_else(|| program_ids.get(index).cloned())
4213 .unwrap_or_default();
4214 if !groups.iter().any(|(existing, _)| *existing == program_id) {
4215 groups.push((program_id, Vec::new()));
4216 }
4217 }
4218 }
4219 for instr in instructions {
4220 let pid = instr
4221 .program_id
4222 .clone()
4223 .unwrap_or_else(|| default_program_id.clone());
4224 match groups.iter_mut().find(|(existing, _)| *existing == pid) {
4225 Some((_, list)) => list.push(instr),
4226 None => groups.push((pid, vec![instr])),
4227 }
4228 }
4229
4230 let mut parser = RustDefinedTypes::new(idls);
4231 let mut used_module_names: HashSet<String> = HashSet::new();
4232 let mut module_blocks: Vec<String> = Vec::new();
4233 let mut modules: Vec<ProgramModule> = Vec::new();
4234
4235 for (index, (program_id, group)) in groups.iter().enumerate() {
4236 let idl = idls
4237 .iter()
4238 .find(|idl| idl.program_id.as_deref() == Some(program_id.as_str()));
4239 let raw_name = match idl {
4240 Some(idl) => idl.name.clone(),
4241 None if index == 0 => stack_name.to_string(),
4242 None => format!("program{}", index),
4243 };
4244 let mut module_name = rust_module_name(&raw_name);
4245 if module_name.is_empty() {
4246 module_name = format!("program{}", index);
4247 }
4248 while !used_module_names.insert(module_name.clone()) {
4249 module_name.push('_');
4250 }
4251 let struct_name = format!("{}Program", to_pascal_case(&module_name));
4252
4253 let own_pdas = idl.and_then(|idl| pdas.get(idl.name.as_str()));
4255 let mut pda_lookup: BTreeMap<&str, &PdaDefinition> = BTreeMap::new();
4256 if let Some(own) = own_pdas {
4257 for (name, def) in own {
4258 pda_lookup.insert(name.as_str(), def);
4259 }
4260 }
4261 for group_pdas in pdas.values() {
4262 for (name, def) in group_pdas {
4263 pda_lookup.entry(name.as_str()).or_insert(def);
4264 }
4265 }
4266
4267 let program_errors = idl
4268 .map(|idl| dedupe_errors_by_code(&idl.errors))
4269 .unwrap_or_default();
4270
4271 let mut needs = ProgramImports::default();
4272 let mut blocks: Vec<String> = Vec::new();
4273 let mut methods: Vec<String> = Vec::new();
4274 let mut skipped: Vec<(String, String)> = Vec::new();
4275 let mut uses_defined_types = false;
4276 for instr in group {
4277 let errors = if instr.errors.is_empty() {
4278 program_errors.clone()
4279 } else {
4280 dedupe_errors_by_code(&instr.errors)
4281 };
4282 match generate_rust_instruction_block(
4283 instr,
4284 &errors,
4285 &pda_lookup,
4286 &mut parser,
4287 &mut needs,
4288 ) {
4289 Ok(block) => {
4290 blocks.push(block.code);
4291 methods.push(block.method);
4292 uses_defined_types |= block.uses_defined_types;
4293 }
4294 Err(reason) => skipped.push((instr.name.clone(), reason)),
4295 }
4296 }
4297
4298 let read_layer = match reads.iter().find(|r| r.program_id == *program_id) {
4300 Some(r) => Ok((
4301 r.program_spec_hash.clone(),
4302 r.program_release_hash.clone(),
4303 r.descriptor.clone(),
4304 )),
4305 None => resolve_program_read_layer(program_specs, program_id),
4306 };
4307 let mut reader_methods: Vec<String> = Vec::new();
4308 let mut reader_notes: Vec<String> = Vec::new();
4309 if read_layer.is_ok() {
4310 let mut used_method_names: HashSet<String> = group
4311 .iter()
4312 .map(|instr| to_snake_case(&instr.name))
4313 .collect();
4314 used_method_names.insert("from_builder".to_string());
4315 let accounts = idl.map(|idl| idl.accounts.as_slice()).unwrap_or_default();
4316 for account in accounts {
4317 let Some(struct_name) = account_structs.get(&account.name).or_else(|| {
4318 account_structs
4319 .iter()
4320 .find(|(name, _)| name.eq_ignore_ascii_case(&account.name))
4321 .map(|(_, emitted)| emitted)
4322 }) else {
4323 continue;
4325 };
4326 let method_name = format!("{}_accounts", to_snake_case(&account.name));
4327 if !used_method_names.insert(method_name.clone()) {
4328 reader_notes.push(format!(
4329 "account reader for `{}` skipped: method name `{}` collides with an instruction builder",
4330 account.name, method_name
4331 ));
4332 continue;
4333 }
4334 reader_methods.push(format!(
4335 " /// Typed reader for `{account}` accounts (release-addressed HTTP reads).\n pub fn {method_name}(&self) -> Result<arete_sdk::AccountReader<{types_path}::{struct_name}>, arete_sdk::AreteError> {{\n Ok(arete_sdk::AccountReader::new(\n {account_literal},\n std::sync::Arc::new(self.builder.account_transport({program_literal}, &read_descriptor())?),\n ))\n }}",
4336 account = account.name,
4337 method_name = method_name,
4338 types_path = types_path,
4339 struct_name = struct_name,
4340 account_literal = rust_string_literal(&account.name),
4341 program_literal = rust_string_literal(&raw_name),
4342 ));
4343 }
4344 }
4345
4346 let mut sections: Vec<String> = Vec::new();
4347 if !blocks.is_empty() {
4348 let mut imports = vec!["BuiltInstruction", "InstructionError", "InstructionHandler"];
4349 if needs.account_meta {
4350 imports.extend(["AccountMeta", "AccountResolution"]);
4351 }
4352 if needs.arg_schema {
4353 imports.extend(["ArgSchema", "ArgType"]);
4354 }
4355 if uses_defined_types {
4356 imports.extend(["ArgField", "EnumVariantDef", "EnumVariantKind"]);
4357 }
4358 if needs.error_metadata {
4359 imports.push("ErrorMetadata");
4360 }
4361 if needs.pda {
4362 imports.extend(["PdaConfig", "PdaSeed"]);
4363 }
4364 imports.sort_unstable();
4365 sections.push(format!(
4366 " use arete_sdk::instruction::{{{}}};\n use serde::Serialize;",
4367 imports.join(", ")
4368 ));
4369 }
4370 sections.push(format!(
4371 " pub const PROGRAM_ID: &str = {};",
4372 rust_string_literal(program_id)
4373 ));
4374 if let Ok((spec_hash, release_hash, descriptor)) = &read_layer {
4375 let descriptor_body = match descriptor {
4376 Some(descriptor) => {
4377 let json = serde_json::to_string(descriptor)
4378 .expect("program read descriptor must serialize");
4379 format!(
4380 " serde_json::from_str({}).expect(\"generated hosted program read descriptor must be valid\")",
4381 rust_string_literal(&json),
4382 )
4383 }
4384 None => " arete_sdk::ProgramReadDescriptor::LocalHttp {\n release: arete_sdk::ProgramReleaseReference {\n program_release_hash: PROGRAM_RELEASE_HASH.to_string(),\n program_spec_hash: PROGRAM_SPEC_HASH.to_string(),\n },\n }".to_string(),
4385 };
4386 sections.push(format!(
4387 " /// Content hash of the exact program specification captured at generation time.\n pub const PROGRAM_SPEC_HASH: &str = {spec};\n\n /// Release identity addressing hosted account reads for this program.\n pub const PROGRAM_RELEASE_HASH: &str = {release};\n\n /// Exact release-addressed read descriptor for this program.\n pub fn read_descriptor() -> arete_sdk::ProgramReadDescriptor {{\n{descriptor_body}\n }}",
4388 spec = rust_string_literal(spec_hash),
4389 release = rust_string_literal(release_hash),
4390 ));
4391 }
4392 sections.extend(blocks);
4393 if let Some(pdas_module) = own_pdas.and_then(generate_rust_pdas_module) {
4394 sections.push(pdas_module);
4395 }
4396
4397 let builder_field = if reader_methods.is_empty() {
4401 " #[allow(dead_code)]\n builder: arete_sdk::ProgramBuilder,"
4403 } else {
4404 " builder: arete_sdk::ProgramBuilder,"
4405 };
4406 let mut impl_methods: Vec<String> = vec![
4407 " /// Construct from the connected client's program runtime.\n pub fn from_builder(builder: arete_sdk::ProgramBuilder) -> Self {\n Self { builder }\n }"
4408 .to_string(),
4409 ];
4410 impl_methods.extend(methods);
4411 impl_methods.extend(reader_methods);
4412 let program_struct = format!(
4413 " /// Program accessor exposed on the stack client's `programs` namespace.\n #[derive(Clone)]\n pub struct {struct_name} {{\n{builder_field}\n }}\n\n impl {struct_name} {{\n{impl_methods}\n }}",
4414 struct_name = struct_name,
4415 builder_field = builder_field,
4416 impl_methods = impl_methods.join("\n\n")
4417 );
4418 sections.push(program_struct);
4419
4420 let mut doc = format!(
4421 "/// Program SDK for `{}` (program ID `{}`).\n",
4422 raw_name, program_id
4423 );
4424 if let Err(reason) = &read_layer {
4425 doc.push_str(&format!(
4426 "///\n/// Program read layer omitted: {}.\n",
4427 reason
4428 ));
4429 }
4430 if !reader_notes.is_empty() {
4431 doc.push_str("///\n");
4432 for note in &reader_notes {
4433 doc.push_str(&format!("/// {}\n", note));
4434 }
4435 }
4436 if !skipped.is_empty() {
4437 doc.push_str("///\n/// Skipped instructions (unsupported by instruction codegen):\n");
4438 for (name, reason) in &skipped {
4439 doc.push_str(&format!("/// - `{}`: {}\n", name, reason));
4440 }
4441 }
4442 module_blocks.push(format!(
4443 "{doc}pub mod {module_name} {{\n{body}\n}}",
4444 doc = doc,
4445 module_name = module_name,
4446 body = sections.join("\n\n")
4447 ));
4448 modules.push(ProgramModule {
4449 module_name,
4450 struct_name,
4451 });
4452 }
4453
4454 let code = format!(
4455 "//! Generated program SDK: typed instruction builders grouped per program.\n//!\n//! Instruction building is pure (no network access). Each program module\n//! exposes `PROGRAM_ID`, typed `*Params` structs, `fn <instruction>(params)`\n//! builders returning `BuiltInstruction`, raw `*_handler()` accessors, and a\n//! `pdas` module with PDA derivation helpers. Programs with a recorded\n//! program spec additionally expose `PROGRAM_SPEC_HASH` /\n//! `PROGRAM_RELEASE_HASH`, a `read_descriptor()` for release-addressed HTTP\n//! reads, and typed `*_accounts()` readers on the program accessor. Standalone\n//! output also exports a `ProgramSdk` aggregate for direct/session composition.\n\n{}\n",
4456 module_blocks.join("\n\n")
4457 );
4458
4459 Some(ProgramsCodegen { code, modules })
4460}
4461
4462fn to_kebab_case(s: &str) -> String {
4463 let mut result = String::new();
4464 for (i, c) in s.chars().enumerate() {
4465 if c.is_uppercase() {
4466 if i > 0 {
4467 result.push('-');
4468 }
4469 result.push(c.to_lowercase().next().unwrap());
4470 } else {
4471 result.push(c);
4472 }
4473 }
4474 result
4475}
4476
4477fn to_pascal_case(s: &str) -> String {
4478 s.split(['_', '-', '.'])
4479 .map(|word| {
4480 let mut chars = word.chars();
4481 match chars.next() {
4482 None => String::new(),
4483 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
4484 }
4485 })
4486 .collect()
4487}
4488
4489fn to_snake_case(s: &str) -> String {
4490 let mut result = String::new();
4491 let mut separator = false;
4492 for ch in s.chars() {
4493 if ch.is_ascii_alphanumeric() {
4494 if separator && !result.is_empty() {
4495 result.push('_');
4496 }
4497 separator = false;
4498 if ch.is_ascii_uppercase() {
4499 if !result.is_empty() && !result.ends_with('_') {
4500 result.push('_');
4501 }
4502 result.push(ch.to_ascii_lowercase());
4503 } else {
4504 result.push(ch.to_ascii_lowercase());
4505 }
4506 } else {
4507 separator = true;
4508 }
4509 }
4510 if result
4511 .chars()
4512 .next()
4513 .is_some_and(|character| character.is_ascii_digit())
4514 {
4515 result.insert_str(0, "value_");
4516 }
4517 if is_rust_keyword(&result) {
4518 result.push('_');
4519 }
4520 result
4521}
4522
4523pub fn rust_module_name(value: &str) -> String {
4528 let mut output = String::new();
4529 let mut separator = false;
4530 for character in value.chars() {
4531 if character.is_ascii_alphanumeric() {
4532 if separator && !output.is_empty() {
4533 output.push('_');
4534 }
4535 separator = false;
4536 output.push(character.to_ascii_lowercase());
4537 } else {
4538 separator = true;
4539 }
4540 }
4541 if output
4542 .chars()
4543 .next()
4544 .is_some_and(|character| character.is_ascii_digit())
4545 {
4546 output.insert_str(0, "live_");
4547 }
4548 if is_rust_keyword(&output) {
4549 output.push_str("_live");
4550 }
4551 output
4552}
4553
4554fn is_rust_keyword(value: &str) -> bool {
4555 matches!(
4556 value,
4557 "as" | "async"
4558 | "await"
4559 | "break"
4560 | "const"
4561 | "continue"
4562 | "crate"
4563 | "dyn"
4564 | "else"
4565 | "enum"
4566 | "extern"
4567 | "false"
4568 | "fn"
4569 | "for"
4570 | "if"
4571 | "impl"
4572 | "in"
4573 | "let"
4574 | "loop"
4575 | "match"
4576 | "mod"
4577 | "move"
4578 | "mut"
4579 | "pub"
4580 | "ref"
4581 | "return"
4582 | "self"
4583 | "Self"
4584 | "static"
4585 | "struct"
4586 | "super"
4587 | "trait"
4588 | "true"
4589 | "type"
4590 | "union"
4591 | "unsafe"
4592 | "use"
4593 | "where"
4594 | "while"
4595 | "abstract"
4596 | "become"
4597 | "box"
4598 | "do"
4599 | "final"
4600 | "macro"
4601 | "override"
4602 | "priv"
4603 | "typeof"
4604 | "unsized"
4605 | "virtual"
4606 | "yield"
4607 | "try"
4608 )
4609}