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