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_stack_http_url_override() {
1961 let output = compile_stack_spec(programs_stack_spec(), None)
1962 .expect("rust stack generation should succeed");
1963 assert!(!output.entity_rs.contains("fn http_url"));
1964
1965 let config = RustStackConfig {
1966 http_url: Some("https://demo.stack.example".to_string()),
1967 ..Default::default()
1968 };
1969 let output = compile_stack_spec(programs_stack_spec(), Some(config))
1970 .expect("rust stack generation should succeed");
1971 assert!(output.entity_rs.contains(
1972 "fn http_url() -> &'static str {\n \"https://demo.stack.example\"\n }"
1973 ));
1974 }
1975
1976 #[test]
1977 fn rust_generator_wires_extension_modules_after_generated_decls() {
1978 let config = RustStackConfig {
1979 module_mode: true,
1980 extension_modules: vec!["devex".to_string(), "extensions".to_string()],
1981 extension_entry: Some("extensions".to_string()),
1982 ..Default::default()
1983 };
1984 let output = compile_stack_spec(programs_stack_spec(), Some(config))
1985 .expect("rust stack generation should succeed");
1986 let mod_rs = output.mod_rs();
1987
1988 assert!(mod_rs.contains(
1989 "// Hand-authored devex extensions (staged from extensions.json; not generated)."
1990 ));
1991 let sdk_reexport = mod_rs.find("pub use arete_sdk::").expect("sdk re-export");
1992 let devex = mod_rs.find("pub mod devex;").expect("devex module decl");
1993 let entry = mod_rs
1994 .find("pub mod extensions;")
1995 .expect("entry module decl");
1996 let entry_reexport = mod_rs
1997 .find("pub use extensions::*;")
1998 .expect("entry glob re-export");
1999 assert!(sdk_reexport < devex);
2000 assert!(devex < entry);
2001 assert!(entry < entry_reexport);
2002 assert!(!mod_rs.contains("pub use devex::*;"));
2003 }
2004
2005 #[test]
2006 fn rust_generator_omits_extension_wiring_without_entry() {
2007 let output = compile_stack_spec(programs_stack_spec(), None)
2008 .expect("rust stack generation should succeed");
2009
2010 assert!(!output.mod_rs().contains("Hand-authored devex extensions"));
2011 assert!(!output.mod_rs().contains("pub mod extensions;"));
2012 }
2013
2014 #[test]
2015 fn rust_generator_rejects_extension_module_collisions() {
2016 for reserved in ["entity", "types", "programs"] {
2017 let config = RustStackConfig {
2018 extension_modules: vec![reserved.to_string(), "extensions".to_string()],
2019 extension_entry: Some("extensions".to_string()),
2020 ..Default::default()
2021 };
2022 let error = compile_stack_spec(programs_stack_spec(), Some(config))
2023 .expect_err("collision with a generated module must fail");
2024 assert!(
2025 error.contains(&format!("'{reserved}.rs'")),
2026 "collision error should name the file: {error}"
2027 );
2028 }
2029
2030 let duplicate = RustStackConfig {
2031 extension_modules: vec![
2032 "devex".to_string(),
2033 "devex".to_string(),
2034 "extensions".to_string(),
2035 ],
2036 extension_entry: Some("extensions".to_string()),
2037 ..Default::default()
2038 };
2039 assert!(compile_stack_spec(programs_stack_spec(), Some(duplicate)).is_err());
2040
2041 let entry_not_last = RustStackConfig {
2042 extension_modules: vec!["extensions".to_string(), "devex".to_string()],
2043 extension_entry: Some("extensions".to_string()),
2044 ..Default::default()
2045 };
2046 assert!(compile_stack_spec(programs_stack_spec(), Some(entry_not_last)).is_err());
2047 }
2048
2049 #[test]
2062 #[ignore = "writes into examples/ore-rust; run explicitly to regenerate"]
2063 fn regenerate_ore_example() {
2064 let repo_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
2065 .parent()
2066 .expect("interpreter crate lives in the repo root")
2067 .to_path_buf();
2068 let spec_json =
2069 std::fs::read_to_string(repo_root.join("stacks/ore/.arete/OreStream.stack.json"))
2070 .expect("ore stack spec should exist");
2071 let spec = crate::versioned::load_stack_spec(&spec_json)
2072 .expect("ore stack spec should deserialize");
2073
2074 let out_dir = repo_root.join("examples/ore-rust/src/generated/ore");
2075 let (extension_modules, extension_entry) =
2076 match std::fs::read_to_string(out_dir.join("extensions.json")) {
2077 Ok(manifest_json) => {
2078 let manifest: serde_json::Value = serde_json::from_str(&manifest_json)
2079 .expect("staged extensions.json should parse");
2080 let language = manifest["language"].as_str();
2081 assert!(
2082 language.is_none() || language == Some("rust"),
2083 "staged ore extensions must be a Rust bundle"
2084 );
2085 let entry_stem = rust_module_name(
2086 manifest["entry"]
2087 .as_str()
2088 .and_then(|entry| entry.strip_suffix(".rs"))
2089 .expect("extensions entry should be a .rs file"),
2090 );
2091 let mut stems: Vec<String> = manifest["files"]
2092 .as_array()
2093 .expect("extensions files should be an array")
2094 .iter()
2095 .map(|file| {
2096 rust_module_name(
2097 file.as_str()
2098 .and_then(|file| file.strip_suffix(".rs"))
2099 .expect("extension files should be .rs files"),
2100 )
2101 })
2102 .filter(|stem| stem != &entry_stem)
2103 .collect();
2104 stems.sort();
2105 stems.dedup();
2106 stems.push(entry_stem.clone());
2107 (stems, Some(entry_stem))
2108 }
2109 Err(_) => (Vec::new(), None),
2110 };
2111
2112 let config = RustStackConfig {
2113 crate_name: "ore-stack".to_string(),
2114 sdk_version: "0.4".to_string(),
2115 module_mode: true,
2116 url: Some("wss://ore.stack.arete.run".to_string()),
2117 http_url: Some("https://ore.stack.arete.run".to_string()),
2118 extension_modules,
2119 extension_entry,
2120 };
2121 let output =
2122 compile_stack_spec(spec, Some(config)).expect("ore stack should compile to Rust");
2123
2124 std::fs::write(out_dir.join("mod.rs"), output.mod_rs()).unwrap();
2125 std::fs::write(out_dir.join("types.rs"), &output.types_rs).unwrap();
2126 std::fs::write(out_dir.join("entity.rs"), &output.entity_rs).unwrap();
2127 std::fs::write(
2128 out_dir.join("programs.rs"),
2129 output.programs_rs.as_deref().expect("ore has instructions"),
2130 )
2131 .unwrap();
2132 }
2133}
2134
2135#[derive(Debug, Clone)]
2140pub struct RustStackConfig {
2141 pub crate_name: String,
2142 pub sdk_version: String,
2143 pub module_mode: bool,
2144 pub url: Option<String>,
2145 pub http_url: Option<String>,
2150 pub extension_modules: Vec<String>,
2154 pub extension_entry: Option<String>,
2159}
2160
2161#[derive(Debug, Clone, Default)]
2162pub struct RustCompositionConfig {
2163 pub stack: RustStackConfig,
2164 pub live_urls: BTreeMap<String, String>,
2165}
2166
2167#[derive(Debug, Clone)]
2168pub struct RustAliasedStackOutput {
2169 pub alias: String,
2170 pub module_name: String,
2171 pub output: RustOutput,
2172}
2173
2174#[derive(Debug, Clone)]
2175pub struct RustCompositionOutput {
2176 pub name: String,
2177 pub cargo_toml: String,
2178 pub lib_rs: String,
2179 pub live_stacks: Vec<RustAliasedStackOutput>,
2180}
2181
2182impl Default for RustStackConfig {
2183 fn default() -> Self {
2184 Self {
2185 crate_name: "generated-stack".to_string(),
2186 sdk_version: "0.4".to_string(),
2187 module_mode: false,
2188 url: None,
2189 http_url: None,
2190 extension_modules: Vec::new(),
2191 extension_entry: None,
2192 }
2193 }
2194}
2195
2196pub fn compile_stack_spec(
2201 stack_spec: SerializableStackSpec,
2202 config: Option<RustStackConfig>,
2203) -> Result<RustOutput, String> {
2204 compile_stack_spec_with_view_selection(stack_spec, config, false)
2205}
2206
2207fn compile_stack_spec_with_view_selection(
2208 stack_spec: SerializableStackSpec,
2209 config: Option<RustStackConfig>,
2210 exact_views: bool,
2211) -> Result<RustOutput, String> {
2212 let config = config.unwrap_or_default();
2213 let stack_name = &stack_spec.stack_name;
2214 let stack_kebab = to_kebab_case(stack_name);
2215
2216 let mut entity_names: Vec<String> = Vec::new();
2217 let mut entity_specs: Vec<SerializableStreamSpec> = Vec::new();
2218
2219 for mut spec in stack_spec.entities {
2220 if spec.idl.is_none() {
2221 spec.idl = stack_spec.idls.first().cloned();
2222 }
2223 entity_names.push(spec.state_name.clone());
2224 entity_specs.push(spec);
2225 }
2226
2227 let view_entity_names = entity_specs
2228 .iter()
2229 .zip(&entity_names)
2230 .filter(|(spec, _)| !exact_views || !spec.views.is_empty())
2231 .map(|(_, name)| name.clone())
2232 .collect::<Vec<_>>();
2233
2234 let (types_rs, account_structs) = generate_stack_types_rs(&entity_specs, &entity_names);
2235
2236 let programs = generate_stack_programs_rs(
2237 stack_name,
2238 &stack_spec.instructions,
2239 &stack_spec.idls,
2240 &stack_spec.pdas,
2241 &stack_spec.program_ids,
2242 &stack_spec.program_specs,
2243 &account_structs,
2244 config.module_mode,
2245 );
2246 let entity_rs = generate_stack_entity_rs(
2247 stack_name,
2248 &stack_kebab,
2249 &entity_specs,
2250 &entity_names,
2251 &config,
2252 exact_views,
2253 programs.as_ref(),
2254 );
2255 validate_extension_modules(&config, programs.is_some())?;
2256 let lib_rs = generate_stack_lib_rs(
2257 stack_name,
2258 &view_entity_names,
2259 config.module_mode,
2260 programs.is_some(),
2261 &config.extension_modules,
2262 config.extension_entry.as_deref(),
2263 );
2264 let cargo_toml = generate_stack_cargo_toml(&config);
2265
2266 Ok(RustOutput {
2267 cargo_toml,
2268 lib_rs,
2269 types_rs,
2270 entity_rs,
2271 programs_rs: programs.map(|codegen| codegen.code),
2272 })
2273}
2274
2275pub fn compile_stack_spec_with_exact_views(
2278 stack_spec: SerializableStackSpec,
2279 config: Option<RustStackConfig>,
2280) -> Result<RustOutput, String> {
2281 compile_stack_spec_with_view_selection(stack_spec, config, true)
2282}
2283
2284pub fn compile_public_artifacts(
2286 programs: &[arete_artifacts::ProgramSpecArtifact],
2287 live_spec: &arete_artifacts::LiveSpecArtifact,
2288 manifest: &arete_artifacts::StackManifestArtifact,
2289 config: Option<RustStackConfig>,
2290) -> Result<RustOutput, String> {
2291 let stack_spec =
2292 crate::public_artifacts::stack_spec_from_artifacts(programs, live_spec, manifest)?;
2293 compile_stack_spec(stack_spec, config)
2294}
2295
2296pub fn compile_public_artifacts_v2(
2298 programs: &[arete_artifacts::ProgramSpecArtifact],
2299 live_spec: &arete_artifacts::LiveSpecArtifactV2,
2300 manifest: &arete_artifacts::StackManifestArtifactV2,
2301 config: Option<RustStackConfig>,
2302) -> Result<RustOutput, String> {
2303 let stack_spec =
2304 crate::public_artifacts::stack_spec_from_artifacts_v2(programs, live_spec, manifest)?;
2305 compile_stack_spec_with_view_selection(stack_spec, config, true)
2306}
2307
2308pub fn compile_composed_public_artifacts_v2(
2311 programs: &[arete_artifacts::ProgramSpecArtifact],
2312 live_specs: &[(String, arete_artifacts::LiveSpecArtifactV2)],
2313 manifest: &arete_artifacts::StackManifestArtifactV2,
2314 config: Option<RustCompositionConfig>,
2315) -> Result<RustCompositionOutput, String> {
2316 let composed =
2317 crate::public_artifacts::stack_specs_from_artifacts_v2(programs, live_specs, manifest)?;
2318 if composed.live_specs.is_empty() {
2319 return Err(
2320 "Rust composition generation requires at least one aliased LiveSpec".to_string(),
2321 );
2322 }
2323 let config = config.unwrap_or_default();
2324 if !config.stack.extension_modules.is_empty() || config.stack.extension_entry.is_some() {
2325 return Err(
2326 "Rust composition SDKs do not support stack extensions; extensions attach to a single-live stack module".to_string(),
2327 );
2328 }
2329 let mut live_stacks = Vec::with_capacity(composed.live_specs.len());
2330 for live in composed.live_specs {
2331 let module_name = rust_module_name(&live.alias);
2332 let mut live_config = config.stack.clone();
2333 live_config.module_mode = true;
2334 live_config.url = config.live_urls.get(&live.alias).cloned();
2335 let output =
2336 compile_stack_spec_with_view_selection(live.stack_spec, Some(live_config), true)?;
2337 live_stacks.push(RustAliasedStackOutput {
2338 alias: live.alias,
2339 module_name,
2340 output,
2341 });
2342 }
2343 let lib_rs = live_stacks
2344 .iter()
2345 .map(|live| format!("pub mod {};", live.module_name))
2346 .collect::<Vec<_>>()
2347 .join("\n");
2348 Ok(RustCompositionOutput {
2349 name: composed.name,
2350 cargo_toml: generate_stack_cargo_toml(&config.stack),
2351 lib_rs: format!("{lib_rs}\n"),
2352 live_stacks,
2353 })
2354}
2355
2356pub fn write_rust_composition_crate(
2357 output: &RustCompositionOutput,
2358 crate_dir: &std::path::Path,
2359) -> Result<(), std::io::Error> {
2360 let source = crate_dir.join("src");
2361 std::fs::create_dir_all(&source)?;
2362 std::fs::write(crate_dir.join("Cargo.toml"), &output.cargo_toml)?;
2363 std::fs::write(source.join("lib.rs"), &output.lib_rs)?;
2364 for live in &output.live_stacks {
2365 write_rust_module(&live.output, &source.join(&live.module_name))?;
2366 }
2367 Ok(())
2368}
2369
2370pub fn write_rust_composition_module(
2371 output: &RustCompositionOutput,
2372 module_dir: &std::path::Path,
2373) -> Result<(), std::io::Error> {
2374 std::fs::create_dir_all(module_dir)?;
2375 std::fs::write(module_dir.join("mod.rs"), &output.lib_rs)?;
2376 for live in &output.live_stacks {
2377 write_rust_module(&live.output, &module_dir.join(&live.module_name))?;
2378 }
2379 Ok(())
2380}
2381
2382fn generate_stack_cargo_toml(config: &RustStackConfig) -> String {
2383 format!(
2384 r#"[package]
2385name = "{}"
2386version = "0.1.0"
2387edition = "2021"
2388
2389[dependencies]
2390arete-sdk = {{ package = "arete-a4-sdk", version = "{}" }}
2391serde = {{ version = "1", features = ["derive"] }}
2392serde_json = "1"
2393"#,
2394 config.crate_name, config.sdk_version
2395 )
2396}
2397
2398fn validate_extension_modules(config: &RustStackConfig, has_programs: bool) -> Result<(), String> {
2402 if config.extension_modules.is_empty() && config.extension_entry.is_none() {
2403 return Ok(());
2404 }
2405 if config.extension_entry.is_none() {
2406 return Err("extension modules were configured without an extension entry".to_string());
2407 }
2408 let mut seen = HashSet::new();
2409 for stem in &config.extension_modules {
2410 let reserved = matches!(stem.as_str(), "entity" | "types" | "mod" | "lib")
2411 || (stem == "programs" && has_programs);
2412 if reserved {
2413 return Err(format!(
2414 "extension file '{stem}.rs' collides with the generated '{stem}' module; rename the extension file"
2415 ));
2416 }
2417 if !seen.insert(stem.as_str()) {
2418 return Err(format!(
2419 "extension file '{stem}.rs' resolves to the same module name as another staged extension file"
2420 ));
2421 }
2422 }
2423 match &config.extension_entry {
2424 Some(entry) if config.extension_modules.last() == Some(entry) => Ok(()),
2425 Some(entry) => Err(format!(
2426 "extension entry module '{entry}' must be the last configured extension module"
2427 )),
2428 None => unreachable!("checked above"),
2429 }
2430}
2431
2432fn generate_stack_lib_rs(
2433 stack_name: &str,
2434 entity_names: &[String],
2435 _module_mode: bool,
2436 has_programs: bool,
2437 extension_modules: &[String],
2438 extension_entry: Option<&str>,
2439) -> String {
2440 let entity_views_exports: Vec<String> = entity_names
2441 .iter()
2442 .map(|name| format!("{}EntityViews", name))
2443 .collect();
2444
2445 let mut all_exports = format!(
2446 "{}Stack, {}StackViews, {}",
2447 stack_name,
2448 stack_name,
2449 entity_views_exports.join(", ")
2450 );
2451 if has_programs {
2452 all_exports.push_str(&format!(", {}StackPrograms", stack_name));
2453 }
2454
2455 let programs_mod = if has_programs {
2456 "\npub mod programs;"
2457 } else {
2458 ""
2459 };
2460
2461 let mut output = format!(
2462 r#"mod entity;
2463mod types;{programs_mod}
2464
2465pub use entity::{{{all_exports}}};
2466pub use types::*;
2467
2468pub use arete_sdk::{{ConnectionState, Arete, Stack, Update, Views}};
2469"#,
2470 programs_mod = programs_mod,
2471 all_exports = all_exports
2472 );
2473
2474 if let Some(entry) = extension_entry {
2475 output.push_str(
2476 "\n// Hand-authored devex extensions (staged from extensions.json; not generated).\n",
2477 );
2478 for stem in extension_modules {
2479 output.push_str(&format!("pub mod {stem};\n"));
2480 }
2481 output.push_str(&format!("pub use {entry}::*;\n"));
2482 }
2483
2484 output
2485}
2486
2487fn generate_stack_types_rs(
2493 entity_specs: &[SerializableStreamSpec],
2494 entity_names: &[String],
2495) -> (String, BTreeMap<String, String>) {
2496 let mut output = String::new();
2497 output.push_str("use serde::{Deserialize, Serialize};\n");
2498 output.push_str("use arete_sdk::serde_utils;\n\n");
2499
2500 let mut generated = HashSet::new();
2501 let mut account_structs: BTreeMap<String, String> = BTreeMap::new();
2502 let mut used_builtins: BTreeSet<&'static str> = BTreeSet::new();
2503
2504 for (i, spec) in entity_specs.iter().enumerate() {
2505 let entity_name = &entity_names[i];
2506 let compiler = RustCompiler::new(spec.clone(), entity_name.clone(), RustConfig::default());
2507 let resolved_name_map = compiler.build_resolved_type_name_map();
2508 used_builtins.extend(compiler.used_builtin_resolver_types());
2509
2510 for section in &spec.sections {
2512 if !RustCompiler::is_root_section(§ion.name) {
2513 let struct_name = format!("{}{}", entity_name, to_pascal_case(§ion.name));
2514 if generated.insert(struct_name) {
2515 output.push_str(
2516 &compiler.generate_struct_for_section(section, &resolved_name_map),
2517 );
2518 output.push_str("\n\n");
2519 }
2520 }
2521 }
2522
2523 output.push_str(&compiler.generate_main_entity_struct(&resolved_name_map));
2525 output.push_str("\n\n");
2526
2527 let resolved = compiler.generate_resolved_types(
2528 &resolved_name_map,
2529 &mut generated,
2530 Some(&mut account_structs),
2531 );
2532 output.push_str(&resolved);
2533 while !output.ends_with("\n\n") {
2534 output.push('\n');
2535 }
2536 }
2537
2538 output.push_str(&render_builtin_resolver_structs(&used_builtins));
2541
2542 output.push('\n');
2544 output.push_str(WRAPPER_TYPES);
2545
2546 (output, account_structs)
2547}
2548
2549fn generate_stack_entity_rs(
2551 stack_name: &str,
2552 stack_kebab: &str,
2553 entity_specs: &[SerializableStreamSpec],
2554 entity_names: &[String],
2555 config: &RustStackConfig,
2556 exact_views: bool,
2557 programs: Option<&ProgramsCodegen>,
2558) -> String {
2559 let types_import = if config.module_mode {
2560 "super::types"
2561 } else {
2562 "crate::types"
2563 };
2564
2565 let selected_entities = entity_specs
2566 .iter()
2567 .zip(entity_names)
2568 .filter(|(spec, _)| !exact_views || !spec.views.is_empty())
2569 .collect::<Vec<_>>();
2570 let entity_type_imports = selected_entities
2571 .iter()
2572 .map(|(_, name)| (*name).to_string())
2573 .collect::<Vec<_>>();
2574
2575 let url_impl = match &config.url {
2576 Some(url) => format!(
2577 r#"fn url() -> &'static str {{
2578 "{}"
2579 }}"#,
2580 url
2581 ),
2582 None => r#"fn url() -> &'static str {
2583 "" // TODO: Set URL after first deployment in arete.toml
2584 }"#
2585 .to_string(),
2586 };
2587
2588 let http_url_impl = match config.http_url.as_deref() {
2590 Some(http_url) if !http_url.is_empty() => format!(
2591 r#"
2592
2593 fn http_url() -> &'static str {{
2594 "{}"
2595 }}"#,
2596 http_url
2597 ),
2598 _ => String::new(),
2599 };
2600
2601 let views_fields: Vec<String> = selected_entities
2603 .iter()
2604 .map(|(_, name)| {
2605 let snake = to_snake_case(name);
2606 format!(" pub {}: {}EntityViews,", snake, name)
2607 })
2608 .collect();
2609
2610 let views_builder_fields: Vec<String> = selected_entities
2612 .iter()
2613 .enumerate()
2614 .map(|(i, (_, name))| {
2615 let snake = to_snake_case(name);
2616 if i < selected_entities.len() - 1 {
2617 format!(
2618 " {}: {}EntityViews {{ builder: builder.clone() }},",
2619 snake, name
2620 )
2621 } else {
2622 format!(" {}: {}EntityViews {{ builder }},", snake, name)
2623 }
2624 })
2625 .collect();
2626
2627 let mut entity_views_structs = Vec::new();
2629 for (i, entity_name) in entity_names.iter().enumerate() {
2630 let spec = &entity_specs[i];
2631 if exact_views && spec.views.is_empty() {
2632 continue;
2633 }
2634
2635 let derived: Vec<_> = spec
2636 .views
2637 .iter()
2638 .filter(|v| {
2639 !v.id.ends_with("/state")
2640 && !v.id.ends_with("/list")
2641 && v.id.starts_with(entity_name.as_str())
2642 })
2643 .collect();
2644
2645 let mut methods = Vec::new();
2646
2647 if !exact_views
2648 || spec
2649 .views
2650 .iter()
2651 .any(|view| view.id == format!("{entity_name}/state"))
2652 {
2653 methods.push(format!(
2654 r#" pub fn state(&self) -> StateView<{entity}> {{
2655 StateView::new(
2656 self.builder.connection().clone(),
2657 self.builder.store().clone(),
2658 "{entity}/state".to_string(),
2659 self.builder.initial_data_timeout(),
2660 )
2661 }}"#,
2662 entity = entity_name
2663 ));
2664 }
2665
2666 if !exact_views
2667 || spec
2668 .views
2669 .iter()
2670 .any(|view| view.id == format!("{entity_name}/list"))
2671 {
2672 methods.push(format!(
2673 r#"
2674 pub fn list(&self) -> ViewHandle<{entity}> {{
2675 self.builder.view("{entity}/list")
2676 }}"#,
2677 entity = entity_name
2678 ));
2679 }
2680
2681 for view in &derived {
2683 let view_name = view.id.split('/').nth(1).unwrap_or("unknown");
2684 let method_name = to_snake_case(view_name);
2685 methods.push(format!(
2686 r#"
2687 pub fn {method}(&self) -> ViewHandle<{entity}> {{
2688 self.builder.view("{view_id}")
2689 }}"#,
2690 method = method_name,
2691 entity = entity_name,
2692 view_id = view.id
2693 ));
2694 }
2695
2696 entity_views_structs.push(format!(
2697 r#"
2698pub struct {entity}EntityViews {{
2699 builder: ViewBuilder,
2700}}
2701
2702impl {entity}EntityViews {{
2703{methods}
2704}}"#,
2705 entity = entity_name,
2706 methods = methods.join("\n")
2707 ));
2708 }
2709
2710 let types_use = if entity_type_imports.is_empty() {
2711 String::new()
2712 } else {
2713 format!(
2714 "use {types_import}::{{{}}};\n",
2715 entity_type_imports.join(", ")
2716 )
2717 };
2718 let empty_builder = if selected_entities.is_empty() {
2719 " let _ = builder;\n"
2720 } else {
2721 ""
2722 };
2723
2724 let programs_root = if config.module_mode { "super" } else { "crate" };
2727 let (programs_assoc, programs_struct) = match programs {
2728 Some(codegen) => {
2729 let fields: Vec<String> = codegen
2730 .modules
2731 .iter()
2732 .map(|module| {
2733 format!(
2734 " pub {}: {root}::programs::{}::{},",
2735 module.module_name,
2736 module.module_name,
2737 module.struct_name,
2738 root = programs_root
2739 )
2740 })
2741 .collect();
2742 let inits: Vec<String> = codegen
2743 .modules
2744 .iter()
2745 .enumerate()
2746 .map(|(index, module)| {
2747 let builder_expr = if index < codegen.modules.len() - 1 {
2748 "builder.clone()"
2749 } else {
2750 "builder"
2751 };
2752 format!(
2753 " {}: {root}::programs::{}::{}::from_builder({builder_expr}),",
2754 module.module_name,
2755 module.module_name,
2756 module.struct_name,
2757 root = programs_root,
2758 builder_expr = builder_expr
2759 )
2760 })
2761 .collect();
2762 (
2763 format!("type Programs = {}StackPrograms;", stack_name),
2764 format!(
2765 r#"
2766
2767pub struct {stack}StackPrograms {{
2768{fields}
2769}}
2770
2771impl arete_sdk::Programs for {stack}StackPrograms {{
2772 fn from_builder(builder: arete_sdk::ProgramBuilder) -> Self {{
2773 Self {{
2774{inits}
2775 }}
2776 }}
2777}}"#,
2778 stack = stack_name,
2779 fields = fields.join("\n"),
2780 inits = inits.join("\n")
2781 ),
2782 )
2783 }
2784 None => ("type Programs = ();".to_string(), String::new()),
2785 };
2786
2787 format!(
2788 r#"{types_use}use arete_sdk::{{Stack, StateView, ViewBuilder, ViewHandle, Views}};
2789
2790pub struct {stack}Stack;
2791
2792impl Stack for {stack}Stack {{
2793 type Views = {stack}StackViews;
2794 {programs_assoc}
2795
2796 fn name() -> &'static str {{
2797 "{stack_kebab}"
2798 }}
2799
2800 {url_impl}{http_url_impl}
2801}}
2802
2803pub struct {stack}StackViews {{
2804{views_fields}
2805}}
2806
2807impl Views for {stack}StackViews {{
2808 fn from_builder(builder: ViewBuilder) -> Self {{
2809{empty_builder} Self {{
2810{views_builder}
2811 }}
2812 }}
2813}}
2814{entity_views}{programs_struct}"#,
2815 types_use = types_use,
2816 stack = stack_name,
2817 stack_kebab = stack_kebab,
2818 programs_assoc = programs_assoc,
2819 url_impl = url_impl,
2820 http_url_impl = http_url_impl,
2821 views_fields = views_fields.join("\n"),
2822 views_builder = views_builder_fields.join("\n"),
2823 entity_views = entity_views_structs.join("\n"),
2824 empty_builder = empty_builder,
2825 programs_struct = programs_struct,
2826 )
2827}
2828
2829#[derive(Debug, Clone)]
2835pub(crate) struct ProgramModule {
2836 module_name: String,
2837 struct_name: String,
2838}
2839
2840#[derive(Debug, Clone)]
2842pub(crate) struct ProgramsCodegen {
2843 code: String,
2844 modules: Vec<ProgramModule>,
2845}
2846
2847#[derive(Debug, Default)]
2849struct ProgramImports {
2850 account_meta: bool,
2851 arg_schema: bool,
2852 pda: bool,
2853 error_metadata: bool,
2854}
2855
2856#[derive(Debug, Clone)]
2858struct RustParsedArg {
2859 schema: String,
2861 param_type: String,
2863 supported: bool,
2865}
2866
2867fn rust_unsupported() -> RustParsedArg {
2868 RustParsedArg {
2869 schema: "ArgType::U8".to_string(),
2870 param_type: "()".to_string(),
2871 supported: false,
2872 }
2873}
2874
2875fn rust_prim(schema: &str, param_type: &str) -> RustParsedArg {
2876 RustParsedArg {
2877 schema: schema.to_string(),
2878 param_type: param_type.to_string(),
2879 supported: true,
2880 }
2881}
2882
2883fn rust_string_literal(value: &str) -> String {
2885 format!("{:?}", value)
2886}
2887
2888struct RustDefinedTypes<'a> {
2893 defs: BTreeMap<String, &'a IdlTypeDefSnapshot>,
2895 lower: BTreeMap<String, String>,
2897 resolved: BTreeMap<String, Option<RustParsedArg>>,
2899 visiting: HashSet<String>,
2901}
2902
2903impl<'a> RustDefinedTypes<'a> {
2904 fn new(idls: &'a [IdlSnapshot]) -> Self {
2905 let mut defs: BTreeMap<String, &'a IdlTypeDefSnapshot> = BTreeMap::new();
2906 let mut lower: BTreeMap<String, String> = BTreeMap::new();
2907 for idl in idls {
2908 for def in &idl.types {
2909 if !defs.contains_key(def.name.as_str()) {
2910 defs.insert(def.name.clone(), def);
2911 lower.insert(def.name.to_lowercase(), def.name.clone());
2912 }
2913 }
2914 }
2915 RustDefinedTypes {
2916 defs,
2917 lower,
2918 resolved: BTreeMap::new(),
2919 visiting: HashSet::new(),
2920 }
2921 }
2922
2923 fn parse_arg_type(&mut self, raw: &str) -> RustParsedArg {
2926 let t = raw.trim().trim_start_matches('&').trim();
2927
2928 if let Some((name, inner)) = split_generic(t) {
2930 match name {
2931 "Option" => {
2932 let inner = self.parse_arg_type(inner);
2933 return RustParsedArg {
2934 schema: format!("ArgType::Option(Box::new({}))", inner.schema),
2935 param_type: format!("Option<{}>", inner.param_type),
2936 supported: inner.supported,
2937 };
2938 }
2939 "Vec" => {
2940 let inner = self.parse_arg_type(inner);
2941 return RustParsedArg {
2942 schema: format!("ArgType::Vec(Box::new({}))", inner.schema),
2943 param_type: format!("Vec<{}>", inner.param_type),
2944 supported: inner.supported,
2945 };
2946 }
2947 _ => return rust_unsupported(),
2948 }
2949 }
2950
2951 if let Some(stripped) = t.strip_prefix('[').and_then(|s| s.strip_suffix(']')) {
2953 if let Some((ty, n)) = stripped.rsplit_once(';') {
2954 let inner = self.parse_arg_type(ty.trim());
2955 let n = n.trim();
2956 if n.parse::<usize>().is_ok() {
2957 return RustParsedArg {
2958 schema: format!("ArgType::Array(Box::new({}), {})", inner.schema, n),
2959 param_type: format!("Vec<{}>", inner.param_type),
2960 supported: inner.supported,
2961 };
2962 }
2963 }
2964 }
2965
2966 let last = t.rsplit("::").next().unwrap_or(t);
2968 match last {
2969 "u8" => rust_prim("ArgType::U8", "u8"),
2970 "u16" => rust_prim("ArgType::U16", "u16"),
2971 "u32" => rust_prim("ArgType::U32", "u32"),
2972 "u64" => rust_prim("ArgType::U64", "u64"),
2973 "u128" => rust_prim("ArgType::U128", "String"),
2976 "i8" => rust_prim("ArgType::I8", "i8"),
2977 "i16" => rust_prim("ArgType::I16", "i16"),
2978 "i32" => rust_prim("ArgType::I32", "i32"),
2979 "i64" => rust_prim("ArgType::I64", "i64"),
2980 "i128" => rust_prim("ArgType::I128", "String"),
2981 "f32" => rust_prim("ArgType::F32", "f32"),
2982 "f64" => rust_prim("ArgType::F64", "f64"),
2983 "bool" => rust_prim("ArgType::Bool", "bool"),
2984 "String" | "string" | "str" => rust_prim("ArgType::String", "String"),
2985 "Pubkey" | "pubkey" | "PublicKey" | "publicKey" => {
2986 rust_prim("ArgType::Pubkey", "String")
2987 }
2988 "bytes" => rust_prim("ArgType::Bytes", "Vec<u8>"),
2989 _ => self.resolve_defined(last).unwrap_or_else(rust_unsupported),
2990 }
2991 }
2992
2993 fn parse_snapshot_type(&mut self, t: &IdlTypeSnapshot) -> RustParsedArg {
2995 match t {
2996 IdlTypeSnapshot::Simple(s) => self.parse_arg_type(s),
2997 IdlTypeSnapshot::Option(o) => {
2998 let inner = self.parse_snapshot_type(&o.option);
2999 RustParsedArg {
3000 schema: format!("ArgType::Option(Box::new({}))", inner.schema),
3001 param_type: format!("Option<{}>", inner.param_type),
3002 supported: inner.supported,
3003 }
3004 }
3005 IdlTypeSnapshot::Vec(v) => {
3006 let inner = self.parse_snapshot_type(&v.vec);
3007 RustParsedArg {
3008 schema: format!("ArgType::Vec(Box::new({}))", inner.schema),
3009 param_type: format!("Vec<{}>", inner.param_type),
3010 supported: inner.supported,
3011 }
3012 }
3013 IdlTypeSnapshot::Array(arr) => {
3014 let mut element: Option<RustParsedArg> = None;
3015 let mut size: Option<u32> = None;
3016 for part in &arr.array {
3017 match part {
3018 IdlArrayElementSnapshot::Type(inner) => {
3019 element = Some(self.parse_snapshot_type(inner))
3020 }
3021 IdlArrayElementSnapshot::TypeName(name) => {
3022 element = Some(self.parse_arg_type(name))
3023 }
3024 IdlArrayElementSnapshot::Size(n) => size = Some(*n),
3025 }
3026 }
3027 match (element, size) {
3028 (Some(inner), Some(n)) => 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 _ => rust_unsupported(),
3034 }
3035 }
3036 IdlTypeSnapshot::HashMap(map) => {
3037 let key = self.parse_snapshot_type(&map.hash_map.0);
3038 let value = self.parse_snapshot_type(&map.hash_map.1);
3039 if !key.supported || key.schema != "ArgType::String" || !value.supported {
3040 rust_unsupported()
3041 } else {
3042 RustParsedArg {
3043 schema: format!(
3044 "ArgType::HashMap(Box::new({}), Box::new({}))",
3045 key.schema, value.schema
3046 ),
3047 param_type: "serde_json::Value".to_string(),
3048 supported: true,
3049 }
3050 }
3051 }
3052 IdlTypeSnapshot::Defined(d) => {
3053 let name = match &d.defined {
3054 IdlDefinedInnerSnapshot::Named { name } => name.as_str(),
3055 IdlDefinedInnerSnapshot::Simple(s) => s.as_str(),
3056 };
3057 self.resolve_defined(name).unwrap_or_else(rust_unsupported)
3058 }
3059 }
3060 }
3061
3062 fn resolve_defined(&mut self, name: &str) -> Option<RustParsedArg> {
3065 if let Some(cached) = self.resolved.get(name) {
3066 return cached.clone();
3067 }
3068 if self.visiting.contains(name) {
3069 return None;
3071 }
3072
3073 let key = if self.defs.contains_key(name) {
3074 name.to_string()
3075 } else {
3076 match self.lower.get(&name.to_lowercase()) {
3077 Some(canonical) => canonical.clone(),
3078 None => {
3079 self.resolved.insert(name.to_string(), None);
3080 return None;
3081 }
3082 }
3083 };
3084
3085 self.visiting.insert(key.clone());
3086 let def = self.defs[&key];
3087 let result = match &def.type_def {
3088 IdlTypeDefKindSnapshot::Struct { fields, .. } => {
3089 let fields = fields.clone();
3090 self.resolve_struct(&fields)
3091 }
3092 IdlTypeDefKindSnapshot::TupleStruct { .. } => None,
3093 IdlTypeDefKindSnapshot::Enum { variants, .. } => {
3094 let variants = variants.clone();
3095 self.resolve_enum(&variants)
3096 }
3097 };
3098 self.visiting.remove(&key);
3099 self.resolved.insert(name.to_string(), result.clone());
3100 if name != key {
3101 self.resolved.insert(key, result.clone());
3102 }
3103 result
3104 }
3105
3106 fn resolve_struct(&mut self, fields: &[IdlFieldSnapshot]) -> Option<RustParsedArg> {
3107 let mut field_exprs: Vec<String> = Vec::new();
3108 for field in fields {
3109 let parsed = self.parse_snapshot_type(&field.type_);
3110 if !parsed.supported {
3111 return None;
3112 }
3113 field_exprs.push(format!(
3114 "ArgField {{ name: {}.to_string(), ty: {} }}",
3115 rust_string_literal(&field.name),
3116 parsed.schema
3117 ));
3118 }
3119 Some(RustParsedArg {
3120 schema: format!("ArgType::Struct(vec![{}])", field_exprs.join(", ")),
3121 param_type: "serde_json::Value".to_string(),
3122 supported: true,
3123 })
3124 }
3125
3126 fn resolve_enum(&mut self, variants: &[IdlEnumVariantSnapshot]) -> Option<RustParsedArg> {
3127 let mut variant_exprs: Vec<String> = Vec::new();
3128 for variant in variants {
3129 let name_literal = rust_string_literal(&variant.name);
3130 if variant.fields.is_empty() {
3131 variant_exprs.push(format!(
3132 "EnumVariantDef {{ name: {}.to_string(), kind: EnumVariantKind::Unit }}",
3133 name_literal
3134 ));
3135 continue;
3136 }
3137
3138 let named: Vec<_> = variant
3139 .fields
3140 .iter()
3141 .filter_map(|field| match field {
3142 IdlEnumVariantFieldSnapshot::Named(field) => Some(field),
3143 IdlEnumVariantFieldSnapshot::Tuple(_) => None,
3144 })
3145 .collect();
3146
3147 if named.len() == variant.fields.len() {
3148 let mut field_exprs: Vec<String> = Vec::new();
3149 for field in named {
3150 let parsed = self.parse_snapshot_type(&field.type_);
3151 if !parsed.supported {
3152 return None;
3153 }
3154 field_exprs.push(format!(
3155 "ArgField {{ name: {}.to_string(), ty: {} }}",
3156 rust_string_literal(&field.name),
3157 parsed.schema
3158 ));
3159 }
3160 variant_exprs.push(format!(
3161 "EnumVariantDef {{ name: {}.to_string(), kind: EnumVariantKind::Struct(vec![{}]) }}",
3162 name_literal,
3163 field_exprs.join(", ")
3164 ));
3165 } else if named.is_empty() {
3166 let mut element_exprs: Vec<String> = Vec::new();
3167 for field in &variant.fields {
3168 let IdlEnumVariantFieldSnapshot::Tuple(ty) = field else {
3169 unreachable!("named.is_empty() guarantees tuple fields");
3170 };
3171 let parsed = self.parse_snapshot_type(ty);
3172 if !parsed.supported {
3173 return None;
3174 }
3175 element_exprs.push(parsed.schema);
3176 }
3177 variant_exprs.push(format!(
3178 "EnumVariantDef {{ name: {}.to_string(), kind: EnumVariantKind::Tuple(vec![{}]) }}",
3179 name_literal,
3180 element_exprs.join(", ")
3181 ));
3182 } else {
3183 return None;
3185 }
3186 }
3187 Some(RustParsedArg {
3188 schema: format!("ArgType::Enum(vec![{}])", variant_exprs.join(", ")),
3189 param_type: "serde_json::Value".to_string(),
3190 supported: true,
3191 })
3192 }
3193}
3194
3195fn schema_uses_defined_types(schema: &str) -> bool {
3198 schema.contains("ArgField") || schema.contains("EnumVariantDef")
3199}
3200
3201#[derive(Debug, Clone, Copy, PartialEq)]
3203enum RustAccountFieldKind {
3204 Signer,
3206 Required,
3208 Optional,
3210}
3211
3212struct MappedRustAccount {
3214 literal: String,
3216 field: Option<(String, RustAccountFieldKind)>,
3218 notes: Vec<String>,
3220 uses_pda: bool,
3222}
3223
3224fn rust_account_meta_literal(
3225 acc: &InstructionAccountDef,
3226 resolution: &str,
3227 comment: Option<&str>,
3228) -> String {
3229 let mut out = String::new();
3230 if let Some(comment) = comment {
3231 out.push_str(&format!(" // [arete codegen] {}\n", comment));
3232 }
3233 out.push_str(&format!(
3234 " 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 }},",
3235 name = rust_string_literal(&acc.name),
3236 is_signer = acc.is_signer,
3237 is_writable = acc.is_writable,
3238 resolution = resolution,
3239 is_optional = acc.is_optional,
3240 ));
3241 out
3242}
3243
3244fn map_rust_account(
3245 acc: &InstructionAccountDef,
3246 pda_lookup: &BTreeMap<&str, &PdaDefinition>,
3247 account_names: &HashSet<&str>,
3248 arg_types: &BTreeMap<&str, &str>,
3249) -> MappedRustAccount {
3250 let user_field_kind = if acc.is_optional {
3251 RustAccountFieldKind::Optional
3252 } else {
3253 RustAccountFieldKind::Required
3254 };
3255 let degraded = |reason: String| -> MappedRustAccount {
3256 let note = format!(
3257 "account `{}` degraded to user-provided ({})",
3258 acc.name, reason
3259 );
3260 MappedRustAccount {
3261 literal: rust_account_meta_literal(acc, "AccountResolution::UserProvided", Some(¬e)),
3262 field: Some((acc.name.clone(), user_field_kind)),
3263 notes: vec![note],
3264 uses_pda: false,
3265 }
3266 };
3267
3268 match &acc.resolution {
3269 AccountResolution::Signer => MappedRustAccount {
3270 literal: rust_account_meta_literal(acc, "AccountResolution::Signer", None),
3271 field: Some((acc.name.clone(), RustAccountFieldKind::Signer)),
3272 notes: Vec::new(),
3273 uses_pda: false,
3274 },
3275 AccountResolution::Known { address } => MappedRustAccount {
3276 literal: rust_account_meta_literal(
3277 acc,
3278 &format!(
3279 "AccountResolution::Known({}.to_string())",
3280 rust_string_literal(address)
3281 ),
3282 None,
3283 ),
3284 field: None,
3285 notes: Vec::new(),
3286 uses_pda: false,
3287 },
3288 AccountResolution::UserProvided => MappedRustAccount {
3289 literal: rust_account_meta_literal(acc, "AccountResolution::UserProvided", None),
3290 field: Some((acc.name.clone(), user_field_kind)),
3291 notes: Vec::new(),
3292 uses_pda: false,
3293 },
3294 AccountResolution::PdaInline { seeds, program_id } => {
3295 match build_rust_pda_config(seeds, program_id.as_deref(), account_names, arg_types) {
3296 Ok((resolution, notes)) => MappedRustAccount {
3297 literal: rust_account_meta_literal(acc, &resolution, None),
3298 field: None,
3299 notes,
3300 uses_pda: true,
3301 },
3302 Err(reason) => degraded(reason),
3303 }
3304 }
3305 AccountResolution::PdaRef { pda_name } => match pda_lookup.get(pda_name.as_str()) {
3306 Some(def) => {
3307 match build_rust_pda_config(
3308 &def.seeds,
3309 def.program_id.as_deref(),
3310 account_names,
3311 arg_types,
3312 ) {
3313 Ok((resolution, notes)) => MappedRustAccount {
3314 literal: rust_account_meta_literal(acc, &resolution, None),
3315 field: None,
3316 notes,
3317 uses_pda: true,
3318 },
3319 Err(reason) => degraded(format!("PDA '{}': {}", pda_name, reason)),
3320 }
3321 }
3322 None => degraded(format!("references unknown PDA '{}'", pda_name)),
3323 },
3324 }
3325}
3326
3327fn build_rust_pda_config(
3331 seeds: &[PdaSeedDef],
3332 program_id: Option<&str>,
3333 account_names: &HashSet<&str>,
3334 arg_types: &BTreeMap<&str, &str>,
3335) -> Result<(String, Vec<String>), String> {
3336 let mut seed_exprs: Vec<String> = Vec::new();
3337 let mut notes: Vec<String> = Vec::new();
3338 for seed in seeds {
3339 match seed {
3340 PdaSeedDef::Literal { value } => {
3341 seed_exprs.push(format!(
3342 "PdaSeed::Literal({}.to_string())",
3343 rust_string_literal(value)
3344 ));
3345 }
3346 PdaSeedDef::Bytes { value } => {
3347 let bytes: Vec<String> = value.iter().map(|b| b.to_string()).collect();
3348 seed_exprs.push(format!("PdaSeed::Bytes(vec![{}])", bytes.join(", ")));
3349 }
3350 PdaSeedDef::AccountRef { account_name } => {
3351 if account_name.contains('.') {
3352 return Err(format!(
3353 "seed references account field '{}' which is not supported for auto-resolution",
3354 account_name
3355 ));
3356 }
3357 if !account_names.contains(account_name.as_str()) {
3358 return Err(format!(
3359 "seed references account '{}' not present in this instruction",
3360 account_name
3361 ));
3362 }
3363 seed_exprs.push(format!(
3364 "PdaSeed::AccountRef({}.to_string())",
3365 rust_string_literal(account_name)
3366 ));
3367 }
3368 PdaSeedDef::ArgRef { arg_name, arg_type } => {
3369 let arg_root = arg_name.split('.').next().unwrap_or(arg_name.as_str());
3370 let present =
3371 arg_types.contains_key(arg_name.as_str()) || arg_types.contains_key(arg_root);
3372 let raw_type = arg_type
3375 .as_deref()
3376 .or_else(|| arg_types.get(arg_name.as_str()).copied())
3377 .or_else(|| arg_types.get(arg_root).copied());
3378 let canonical = raw_type.and_then(normalize_seed_arg_type);
3379 if !present {
3380 if canonical.is_none() {
3381 return Err(format!(
3382 "seed helper arg '{}' is not present in this instruction and has no primitive type information",
3383 arg_name
3384 ));
3385 }
3386 notes.push(format!(
3387 "seed input `{}` must be supplied via the `resolve` key when building through the raw handler",
3388 arg_name
3389 ));
3390 }
3391 match canonical {
3392 Some(canonical) => seed_exprs.push(format!(
3393 "PdaSeed::ArgRef {{ arg: {}.to_string(), arg_type: Some({}.to_string()) }}",
3394 rust_string_literal(arg_name),
3395 rust_string_literal(&canonical)
3396 )),
3397 None => {
3398 notes.push(format!(
3399 "seed arg `{}` has non-primitive type '{}'; the runtime will use heuristic encoding",
3400 arg_name,
3401 raw_type.unwrap_or("<unknown>")
3402 ));
3403 seed_exprs.push(format!(
3404 "PdaSeed::ArgRef {{ arg: {}.to_string(), arg_type: None }}",
3405 rust_string_literal(arg_name)
3406 ));
3407 }
3408 }
3409 }
3410 }
3411 }
3412
3413 let program_expr = match program_id {
3414 Some(pid) => format!("Some({}.to_string())", rust_string_literal(pid)),
3415 None => "None".to_string(),
3416 };
3417 Ok((
3418 format!(
3419 "AccountResolution::Pda(PdaConfig {{ program_id: {}, seeds: vec![{}] }})",
3420 program_expr,
3421 seed_exprs.join(", ")
3422 ),
3423 notes,
3424 ))
3425}
3426
3427struct RustInstructionBlock {
3429 code: String,
3430 method: String,
3431 uses_defined_types: bool,
3432}
3433
3434fn generate_rust_instruction_block(
3435 instr: &InstructionDef,
3436 errors: &[IdlErrorSnapshot],
3437 pda_lookup: &BTreeMap<&str, &PdaDefinition>,
3438 parser: &mut RustDefinedTypes<'_>,
3439 needs: &mut ProgramImports,
3440) -> Result<RustInstructionBlock, String> {
3441 let mut parsed_args: Vec<(&InstructionArgDef, RustParsedArg)> = Vec::new();
3443 for arg in &instr.args {
3444 let parsed = parser.parse_arg_type(&arg.arg_type);
3445 if !parsed.supported {
3446 return Err(format!(
3447 "arg '{}' has unsupported type '{}'",
3448 arg.name, arg.arg_type
3449 ));
3450 }
3451 parsed_args.push((arg, parsed));
3452 }
3453
3454 let account_names: HashSet<&str> = instr.accounts.iter().map(|a| a.name.as_str()).collect();
3456 let arg_types: BTreeMap<&str, &str> = instr
3457 .args
3458 .iter()
3459 .map(|a| (a.name.as_str(), a.arg_type.as_str()))
3460 .collect();
3461
3462 let mut account_literals: Vec<String> = Vec::new();
3463 let mut account_fields: Vec<(String, RustAccountFieldKind)> = Vec::new();
3464 let mut notes: Vec<String> = Vec::new();
3465 for acc in &instr.accounts {
3466 let mapped = map_rust_account(acc, pda_lookup, &account_names, &arg_types);
3467 account_literals.push(mapped.literal);
3468 if let Some(field) = mapped.field {
3469 account_fields.push(field);
3470 }
3471 notes.extend(mapped.notes);
3472 if mapped.uses_pda {
3473 needs.pda = true;
3474 }
3475 }
3476 if !instr.accounts.is_empty() {
3477 needs.account_meta = true;
3478 }
3479 if !instr.args.is_empty() {
3480 needs.arg_schema = true;
3481 }
3482 if !errors.is_empty() {
3483 needs.error_metadata = true;
3484 }
3485
3486 let fn_name = to_snake_case(&instr.name);
3487 let pascal = to_pascal_case(&instr.name);
3488 let params_name = format!("{}Params", pascal);
3489
3490 let arg_name_set: HashSet<&str> = instr.args.iter().map(|a| a.name.as_str()).collect();
3494 let mut used_field_names: HashSet<String> = HashSet::new();
3495 let mut param_fields: Vec<String> = Vec::new();
3496 let mut uses_defined_types = false;
3497 for (arg, parsed) in &parsed_args {
3498 let field_name = to_snake_case(&arg.name);
3499 used_field_names.insert(field_name.clone());
3500 uses_defined_types |= schema_uses_defined_types(&parsed.schema);
3501 let mut lines = Vec::new();
3502 if field_name != arg.name {
3503 lines.push(format!(
3504 " #[serde(rename = {})]",
3505 rust_string_literal(&arg.name)
3506 ));
3507 }
3508 lines.push(format!(
3509 " pub {}: {},",
3510 field_name, parsed.param_type
3511 ));
3512 param_fields.push(lines.join("\n"));
3513 }
3514 for (name, kind) in &account_fields {
3515 if arg_name_set.contains(name.as_str()) {
3516 notes.push(format!(
3517 "account `{}` shares its name with an instruction arg and has no typed override field",
3518 name
3519 ));
3520 continue;
3521 }
3522 let field_name = to_snake_case(name);
3523 if !used_field_names.insert(field_name.clone()) {
3524 notes.push(format!(
3525 "account `{}` collides with another params field and has no typed override field",
3526 name
3527 ));
3528 continue;
3529 }
3530 let mut lines = Vec::new();
3531 match kind {
3532 RustAccountFieldKind::Signer => lines.push(format!(
3533 " /// Optional address override for the `{}` signer (defaults to the payer).",
3534 name
3535 )),
3536 RustAccountFieldKind::Required => {
3537 lines.push(format!(" /// Address of the `{}` account.", name))
3538 }
3539 RustAccountFieldKind::Optional => lines.push(format!(
3540 " /// Optional address of the `{}` account.",
3541 name
3542 )),
3543 }
3544 if field_name != *name {
3545 lines.push(format!(
3546 " #[serde(rename = {})]",
3547 rust_string_literal(name)
3548 ));
3549 }
3550 match kind {
3551 RustAccountFieldKind::Required => {
3552 lines.push(format!(" pub {}: String,", field_name))
3553 }
3554 _ => {
3555 lines.push(
3556 " #[serde(skip_serializing_if = \"Option::is_none\")]".to_string(),
3557 );
3558 lines.push(format!(" pub {}: Option<String>,", field_name));
3559 }
3560 }
3561 param_fields.push(lines.join("\n"));
3562 }
3563
3564 let params_struct = if param_fields.is_empty() {
3565 format!(
3566 " /// Typed params for `{name}` (no args or caller-supplied accounts).\n #[derive(Debug, Clone, Serialize, Default)]\n pub struct {params_name} {{}}",
3567 name = instr.name,
3568 params_name = params_name
3569 )
3570 } else {
3571 format!(
3572 " /// Typed params for `{name}`: instruction args plus overridable accounts.\n #[derive(Debug, Clone, Serialize, Default)]\n pub struct {params_name} {{\n{fields}\n }}",
3573 name = instr.name,
3574 params_name = params_name,
3575 fields = param_fields.join("\n")
3576 )
3577 };
3578
3579 let mut doc_lines: Vec<String> = instr
3581 .docs
3582 .iter()
3583 .map(|line| line.trim().to_string())
3584 .collect();
3585 if doc_lines.is_empty() {
3586 doc_lines.push(format!("Builds the `{}` instruction.", instr.name));
3587 }
3588 if !notes.is_empty() {
3589 doc_lines.push(String::new());
3590 doc_lines.push("Codegen notes:".to_string());
3591 for note in ¬es {
3592 doc_lines.push(format!("- {}", note));
3593 }
3594 }
3595 let docs = doc_lines
3596 .iter()
3597 .map(|line| {
3598 if line.is_empty() {
3599 " ///".to_string()
3600 } else {
3601 format!(" /// {}", line)
3602 }
3603 })
3604 .collect::<Vec<_>>()
3605 .join("\n");
3606
3607 let typed_fn = format!(
3608 "{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 }}",
3609 docs = docs,
3610 fn_name = fn_name,
3611 params_name = params_name
3612 );
3613
3614 let discriminator = instr
3616 .discriminator
3617 .iter()
3618 .map(|b| b.to_string())
3619 .collect::<Vec<_>>()
3620 .join(", ");
3621 let accounts_literal = if account_literals.is_empty() {
3622 "vec![]".to_string()
3623 } else {
3624 format!("vec![\n{}\n ]", account_literals.join("\n"))
3625 };
3626 let args_literal = if parsed_args.is_empty() {
3627 "vec![]".to_string()
3628 } else {
3629 let entries: Vec<String> = parsed_args
3630 .iter()
3631 .map(|(arg, parsed)| {
3632 format!(
3633 " ArgSchema {{ name: {}.to_string(), ty: {} }},",
3634 rust_string_literal(&arg.name),
3635 parsed.schema
3636 )
3637 })
3638 .collect();
3639 format!("vec![\n{}\n ]", entries.join("\n"))
3640 };
3641 let errors_literal = if errors.is_empty() {
3642 "vec![]".to_string()
3643 } else {
3644 let entries: Vec<String> = errors
3645 .iter()
3646 .map(|error| {
3647 format!(
3648 " ErrorMetadata {{ code: {}, name: {}.to_string(), msg: {}.to_string() }},",
3649 error.code,
3650 rust_string_literal(&error.name),
3651 rust_string_literal(error.msg.as_deref().unwrap_or(""))
3652 )
3653 })
3654 .collect();
3655 format!("vec![\n{}\n ]", entries.join("\n"))
3656 };
3657
3658 let handler_fn = format!(
3659 " /// 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 }}",
3660 name = instr.name,
3661 fn_name = fn_name,
3662 discriminator = discriminator,
3663 accounts = accounts_literal,
3664 args = args_literal,
3665 errors = errors_literal
3666 );
3667
3668 let method = format!(
3669 " pub fn {fn_name}(&self, params: {params_name}) -> Result<BuiltInstruction, InstructionError> {{\n {fn_name}(params)\n }}",
3670 fn_name = fn_name,
3671 params_name = params_name
3672 );
3673
3674 Ok(RustInstructionBlock {
3675 code: format!("{}\n\n{}\n\n{}", params_struct, typed_fn, handler_fn),
3676 method,
3677 uses_defined_types,
3678 })
3679}
3680
3681fn generate_rust_pdas_module(pdas: &BTreeMap<String, PdaDefinition>) -> Option<String> {
3684 if pdas.is_empty() {
3685 return None;
3686 }
3687
3688 let mut fns: Vec<String> = Vec::new();
3689 let mut needs_serialize = false;
3690 let mut needs_program_id = false;
3691 for def in pdas.values() {
3692 let fn_name = to_snake_case(&def.name);
3693 let mut params: Vec<(String, String)> = Vec::new();
3694 let mut seed_exprs: Vec<String> = Vec::new();
3695 for seed in &def.seeds {
3696 match seed {
3697 PdaSeedDef::Literal { value } => {
3698 seed_exprs.push(format!(
3699 "{}.as_bytes().to_vec()",
3700 rust_string_literal(value)
3701 ));
3702 }
3703 PdaSeedDef::Bytes { value } => {
3704 let bytes: Vec<String> = value.iter().map(|b| b.to_string()).collect();
3705 seed_exprs.push(format!("vec![{}]", bytes.join(", ")));
3706 }
3707 PdaSeedDef::AccountRef { account_name } => {
3708 let param = to_snake_case(account_name);
3709 if !params.iter().any(|(name, _)| *name == param) {
3710 params.push((param.clone(), "&str".to_string()));
3711 }
3712 needs_serialize = true;
3713 seed_exprs.push(format!(
3714 "serialize_seed_value(&serde_json::json!({}), Some(\"pubkey\"))?",
3715 param
3716 ));
3717 }
3718 PdaSeedDef::ArgRef { arg_name, arg_type } => {
3719 let param = to_snake_case(arg_name);
3720 let canonical = arg_type.as_deref().and_then(normalize_seed_arg_type);
3721 let (param_type, hint) = match canonical.as_deref() {
3722 Some("pubkey") => ("&str", "Some(\"pubkey\")".to_string()),
3723 Some("string") => ("&str", "Some(\"string\")".to_string()),
3724 Some(int) if int.starts_with('i') => {
3725 ("i64", format!("Some({})", rust_string_literal(int)))
3726 }
3727 Some(int) => ("u64", format!("Some({})", rust_string_literal(int))),
3728 None => ("&str", "None".to_string()),
3729 };
3730 if !params.iter().any(|(name, _)| *name == param) {
3731 params.push((param.clone(), param_type.to_string()));
3732 }
3733 needs_serialize = true;
3734 seed_exprs.push(format!(
3735 "serialize_seed_value(&serde_json::json!({}), {})?",
3736 param, hint
3737 ));
3738 }
3739 }
3740 }
3741
3742 let program_expr = match &def.program_id {
3743 Some(pid) => rust_string_literal(pid),
3744 None => {
3745 needs_program_id = true;
3746 "PROGRAM_ID".to_string()
3747 }
3748 };
3749 let param_list = params
3750 .iter()
3751 .map(|(name, ty)| format!("{}: {}", name, ty))
3752 .collect::<Vec<_>>()
3753 .join(", ");
3754 let seeds_body = if seed_exprs.is_empty() {
3755 " let seeds: Vec<Vec<u8>> = vec![];".to_string()
3756 } else {
3757 format!(
3758 " let seeds: Vec<Vec<u8>> = vec![\n{}\n ];",
3759 seed_exprs
3760 .iter()
3761 .map(|expr| format!(" {},", expr))
3762 .collect::<Vec<_>>()
3763 .join("\n")
3764 )
3765 };
3766 fns.push(format!(
3767 " /// 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 }}",
3768 name = def.name,
3769 fn_name = fn_name,
3770 params = param_list,
3771 seeds = seeds_body,
3772 program = program_expr
3773 ));
3774 }
3775
3776 let mut imports = vec!["derive_program_address"];
3777 if needs_serialize {
3778 imports.push("serialize_seed_value");
3779 }
3780 imports.extend(["InstructionError", "Pubkey"]);
3781 imports.sort_unstable();
3782 let mut use_lines = format!(
3783 " use arete_sdk::instruction::{{{}}};",
3784 imports.join(", ")
3785 );
3786 if needs_program_id {
3787 use_lines.push_str("\n\n use super::PROGRAM_ID;");
3788 }
3789
3790 Some(format!(
3791 " /// PDA derivation helpers for this program.\n pub mod pdas {{\n{use_lines}\n\n{fns}\n }}",
3792 use_lines = use_lines,
3793 fns = fns.join("\n\n")
3794 ))
3795}
3796
3797type ProgramReadLayer = Result<(String, String), String>;
3800
3801fn resolve_program_read_layer(
3804 program_specs: &[arete_hash::ProgramSpecV1],
3805 program_id: &str,
3806) -> ProgramReadLayer {
3807 let Some(spec) = program_specs
3808 .iter()
3809 .find(|spec| spec.program_id == program_id)
3810 else {
3811 return Err("no program specification was recorded for this program".to_string());
3812 };
3813 let spec_hash = spec
3814 .hash()
3815 .map_err(|error| format!("failed to compute the program spec hash ({error})"))?;
3816 let release_hash = spec
3817 .oss_release_hash()
3818 .map_err(|error| format!("failed to compute the release hash ({error})"))?;
3819 Ok((spec_hash.to_string(), release_hash.to_string()))
3820}
3821
3822#[allow(clippy::too_many_arguments)]
3827fn generate_stack_programs_rs(
3828 stack_name: &str,
3829 instructions: &[InstructionDef],
3830 idls: &[IdlSnapshot],
3831 pdas: &BTreeMap<String, BTreeMap<String, PdaDefinition>>,
3832 program_ids: &[String],
3833 program_specs: &[arete_hash::ProgramSpecV1],
3834 account_structs: &BTreeMap<String, String>,
3835 module_mode: bool,
3836) -> Option<ProgramsCodegen> {
3837 if instructions.is_empty() {
3838 return None;
3839 }
3840
3841 let types_path = if module_mode {
3844 "super::super::types"
3845 } else {
3846 "crate::types"
3847 };
3848
3849 let default_program_id = program_ids.first().cloned().unwrap_or_default();
3850
3851 let mut groups: Vec<(String, Vec<&InstructionDef>)> = Vec::new();
3853 for instr in instructions {
3854 let pid = instr
3855 .program_id
3856 .clone()
3857 .unwrap_or_else(|| default_program_id.clone());
3858 match groups.iter_mut().find(|(existing, _)| *existing == pid) {
3859 Some((_, list)) => list.push(instr),
3860 None => groups.push((pid, vec![instr])),
3861 }
3862 }
3863
3864 let mut parser = RustDefinedTypes::new(idls);
3865 let mut used_module_names: HashSet<String> = HashSet::new();
3866 let mut module_blocks: Vec<String> = Vec::new();
3867 let mut modules: Vec<ProgramModule> = Vec::new();
3868
3869 for (index, (program_id, group)) in groups.iter().enumerate() {
3870 let idl = idls
3871 .iter()
3872 .find(|idl| idl.program_id.as_deref() == Some(program_id.as_str()));
3873 let raw_name = match idl {
3874 Some(idl) => idl.name.clone(),
3875 None if index == 0 => stack_name.to_string(),
3876 None => format!("program{}", index),
3877 };
3878 let mut module_name = rust_module_name(&raw_name);
3879 if module_name.is_empty() {
3880 module_name = format!("program{}", index);
3881 }
3882 while !used_module_names.insert(module_name.clone()) {
3883 module_name.push('_');
3884 }
3885 let struct_name = format!("{}Program", to_pascal_case(&module_name));
3886
3887 let own_pdas = idl.and_then(|idl| pdas.get(idl.name.as_str()));
3889 let mut pda_lookup: BTreeMap<&str, &PdaDefinition> = BTreeMap::new();
3890 if let Some(own) = own_pdas {
3891 for (name, def) in own {
3892 pda_lookup.insert(name.as_str(), def);
3893 }
3894 }
3895 for group_pdas in pdas.values() {
3896 for (name, def) in group_pdas {
3897 pda_lookup.entry(name.as_str()).or_insert(def);
3898 }
3899 }
3900
3901 let program_errors = idl
3902 .map(|idl| dedupe_errors_by_code(&idl.errors))
3903 .unwrap_or_default();
3904
3905 let mut needs = ProgramImports::default();
3906 let mut blocks: Vec<String> = Vec::new();
3907 let mut methods: Vec<String> = Vec::new();
3908 let mut skipped: Vec<(String, String)> = Vec::new();
3909 let mut uses_defined_types = false;
3910 for instr in group {
3911 let errors = if instr.errors.is_empty() {
3912 program_errors.clone()
3913 } else {
3914 dedupe_errors_by_code(&instr.errors)
3915 };
3916 match generate_rust_instruction_block(
3917 instr,
3918 &errors,
3919 &pda_lookup,
3920 &mut parser,
3921 &mut needs,
3922 ) {
3923 Ok(block) => {
3924 blocks.push(block.code);
3925 methods.push(block.method);
3926 uses_defined_types |= block.uses_defined_types;
3927 }
3928 Err(reason) => skipped.push((instr.name.clone(), reason)),
3929 }
3930 }
3931
3932 let read_layer = resolve_program_read_layer(program_specs, program_id);
3934 let mut reader_methods: Vec<String> = Vec::new();
3935 let mut reader_notes: Vec<String> = Vec::new();
3936 if read_layer.is_ok() {
3937 let mut used_method_names: HashSet<String> = group
3938 .iter()
3939 .map(|instr| to_snake_case(&instr.name))
3940 .collect();
3941 used_method_names.insert("from_builder".to_string());
3942 let accounts = idl.map(|idl| idl.accounts.as_slice()).unwrap_or_default();
3943 for account in accounts {
3944 let Some(struct_name) = account_structs.get(&account.name).or_else(|| {
3945 account_structs
3946 .iter()
3947 .find(|(name, _)| name.eq_ignore_ascii_case(&account.name))
3948 .map(|(_, emitted)| emitted)
3949 }) else {
3950 continue;
3952 };
3953 let method_name = format!("{}_accounts", to_snake_case(&account.name));
3954 if !used_method_names.insert(method_name.clone()) {
3955 reader_notes.push(format!(
3956 "account reader for `{}` skipped: method name `{}` collides with an instruction builder",
3957 account.name, method_name
3958 ));
3959 continue;
3960 }
3961 reader_methods.push(format!(
3962 " /// 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 }}",
3963 account = account.name,
3964 method_name = method_name,
3965 types_path = types_path,
3966 struct_name = struct_name,
3967 account_literal = rust_string_literal(&account.name),
3968 program_literal = rust_string_literal(&raw_name),
3969 ));
3970 }
3971 }
3972
3973 let mut sections: Vec<String> = Vec::new();
3974 if !blocks.is_empty() {
3975 let mut imports = vec!["BuiltInstruction", "InstructionError", "InstructionHandler"];
3976 if needs.account_meta {
3977 imports.extend(["AccountMeta", "AccountResolution"]);
3978 }
3979 if needs.arg_schema {
3980 imports.extend(["ArgSchema", "ArgType"]);
3981 }
3982 if uses_defined_types {
3983 imports.extend(["ArgField", "EnumVariantDef", "EnumVariantKind"]);
3984 }
3985 if needs.error_metadata {
3986 imports.push("ErrorMetadata");
3987 }
3988 if needs.pda {
3989 imports.extend(["PdaConfig", "PdaSeed"]);
3990 }
3991 imports.sort_unstable();
3992 sections.push(format!(
3993 " use arete_sdk::instruction::{{{}}};\n use serde::Serialize;",
3994 imports.join(", ")
3995 ));
3996 }
3997 sections.push(format!(
3998 " pub const PROGRAM_ID: &str = {};",
3999 rust_string_literal(program_id)
4000 ));
4001 if let Ok((spec_hash, release_hash)) = &read_layer {
4002 sections.push(format!(
4003 " /// 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 }}",
4004 spec = rust_string_literal(spec_hash),
4005 release = rust_string_literal(release_hash),
4006 ));
4007 }
4008 sections.extend(blocks);
4009 if let Some(pdas_module) = own_pdas.and_then(generate_rust_pdas_module) {
4010 sections.push(pdas_module);
4011 }
4012
4013 let builder_field = if reader_methods.is_empty() {
4017 " #[allow(dead_code)]\n builder: arete_sdk::ProgramBuilder,"
4019 } else {
4020 " builder: arete_sdk::ProgramBuilder,"
4021 };
4022 let mut impl_methods: Vec<String> = vec![
4023 " /// Construct from the connected client's program runtime.\n pub fn from_builder(builder: arete_sdk::ProgramBuilder) -> Self {\n Self { builder }\n }"
4024 .to_string(),
4025 ];
4026 impl_methods.extend(methods);
4027 impl_methods.extend(reader_methods);
4028 let program_struct = format!(
4029 " /// 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 }}",
4030 struct_name = struct_name,
4031 builder_field = builder_field,
4032 impl_methods = impl_methods.join("\n\n")
4033 );
4034 sections.push(program_struct);
4035
4036 let mut doc = format!(
4037 "/// Program SDK for `{}` (program ID `{}`).\n",
4038 raw_name, program_id
4039 );
4040 if let Err(reason) = &read_layer {
4041 doc.push_str(&format!(
4042 "///\n/// Program read layer omitted: {}.\n",
4043 reason
4044 ));
4045 }
4046 if !reader_notes.is_empty() {
4047 doc.push_str("///\n");
4048 for note in &reader_notes {
4049 doc.push_str(&format!("/// {}\n", note));
4050 }
4051 }
4052 if !skipped.is_empty() {
4053 doc.push_str("///\n/// Skipped instructions (unsupported by instruction codegen):\n");
4054 for (name, reason) in &skipped {
4055 doc.push_str(&format!("/// - `{}`: {}\n", name, reason));
4056 }
4057 }
4058 module_blocks.push(format!(
4059 "{doc}pub mod {module_name} {{\n{body}\n}}",
4060 doc = doc,
4061 module_name = module_name,
4062 body = sections.join("\n\n")
4063 ));
4064 modules.push(ProgramModule {
4065 module_name,
4066 struct_name,
4067 });
4068 }
4069
4070 let code = format!(
4071 "//! 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",
4072 module_blocks.join("\n\n")
4073 );
4074
4075 Some(ProgramsCodegen { code, modules })
4076}
4077
4078fn to_kebab_case(s: &str) -> String {
4079 let mut result = String::new();
4080 for (i, c) in s.chars().enumerate() {
4081 if c.is_uppercase() {
4082 if i > 0 {
4083 result.push('-');
4084 }
4085 result.push(c.to_lowercase().next().unwrap());
4086 } else {
4087 result.push(c);
4088 }
4089 }
4090 result
4091}
4092
4093fn to_pascal_case(s: &str) -> String {
4094 s.split(['_', '-', '.'])
4095 .map(|word| {
4096 let mut chars = word.chars();
4097 match chars.next() {
4098 None => String::new(),
4099 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
4100 }
4101 })
4102 .collect()
4103}
4104
4105fn to_snake_case(s: &str) -> String {
4106 let mut result = String::new();
4107 let mut separator = false;
4108 for ch in s.chars() {
4109 if ch.is_ascii_alphanumeric() {
4110 if separator && !result.is_empty() {
4111 result.push('_');
4112 }
4113 separator = false;
4114 if ch.is_ascii_uppercase() {
4115 if !result.is_empty() && !result.ends_with('_') {
4116 result.push('_');
4117 }
4118 result.push(ch.to_ascii_lowercase());
4119 } else {
4120 result.push(ch.to_ascii_lowercase());
4121 }
4122 } else {
4123 separator = true;
4124 }
4125 }
4126 if result
4127 .chars()
4128 .next()
4129 .is_some_and(|character| character.is_ascii_digit())
4130 {
4131 result.insert_str(0, "value_");
4132 }
4133 if is_rust_keyword(&result) {
4134 result.push('_');
4135 }
4136 result
4137}
4138
4139pub fn rust_module_name(value: &str) -> String {
4144 let mut output = String::new();
4145 let mut separator = false;
4146 for character in value.chars() {
4147 if character.is_ascii_alphanumeric() {
4148 if separator && !output.is_empty() {
4149 output.push('_');
4150 }
4151 separator = false;
4152 output.push(character.to_ascii_lowercase());
4153 } else {
4154 separator = true;
4155 }
4156 }
4157 if output
4158 .chars()
4159 .next()
4160 .is_some_and(|character| character.is_ascii_digit())
4161 {
4162 output.insert_str(0, "live_");
4163 }
4164 if is_rust_keyword(&output) {
4165 output.push_str("_live");
4166 }
4167 output
4168}
4169
4170fn is_rust_keyword(value: &str) -> bool {
4171 matches!(
4172 value,
4173 "as" | "async"
4174 | "await"
4175 | "break"
4176 | "const"
4177 | "continue"
4178 | "crate"
4179 | "dyn"
4180 | "else"
4181 | "enum"
4182 | "extern"
4183 | "false"
4184 | "fn"
4185 | "for"
4186 | "if"
4187 | "impl"
4188 | "in"
4189 | "let"
4190 | "loop"
4191 | "match"
4192 | "mod"
4193 | "move"
4194 | "mut"
4195 | "pub"
4196 | "ref"
4197 | "return"
4198 | "self"
4199 | "Self"
4200 | "static"
4201 | "struct"
4202 | "super"
4203 | "trait"
4204 | "true"
4205 | "type"
4206 | "union"
4207 | "unsafe"
4208 | "use"
4209 | "where"
4210 | "while"
4211 | "abstract"
4212 | "become"
4213 | "box"
4214 | "do"
4215 | "final"
4216 | "macro"
4217 | "override"
4218 | "priv"
4219 | "typeof"
4220 | "unsized"
4221 | "virtual"
4222 | "yield"
4223 | "try"
4224 )
4225}