1use {
13 crate::AbsolutePath,
14 anyhow::{anyhow, bail, Context, Result},
15 clap::{Parser, ValueEnum},
16 serde_json::{json, Map, Value as JsonValue},
17 std::{
18 collections::{BTreeSet, HashMap},
19 fs,
20 path::{Path, PathBuf},
21 process::{Command, Stdio},
22 },
23};
24
25const CODAMA_VERSION: &str = "1.6.0";
30
31#[derive(Debug, Parser, AbsolutePath)]
32pub enum CodamaCommand {
33 Convert {
36 path: String,
38 #[clap(short, long)]
40 out: Option<String>,
41 },
42 Generate {
50 #[clap(
53 short = 'l',
54 long = "language",
55 value_delimiter = ',',
56 value_enum,
57 required = true
58 )]
59 language: Vec<Language>,
60 #[clap(short = 'p', long = "path", default_value = "clients")]
63 path: String,
64 idl: String,
66 },
67}
68
69#[derive(Debug, Clone, Copy, ValueEnum, Eq, Ord, PartialEq, PartialOrd, AbsolutePath)]
71#[clap(rename_all = "kebab-case")]
72pub enum Language {
73 Js,
74 JsUmi,
75 Rust,
76 Go,
77}
78
79impl Language {
80 pub fn id(self) -> &'static str {
83 match self {
84 Language::Js => "js",
85 Language::JsUmi => "js-umi",
86 Language::Rust => "rust",
87 Language::Go => "go",
88 }
89 }
90
91 pub fn from_id(id: &str) -> Option<Self> {
94 match id {
95 "js" => Some(Language::Js),
96 "js-umi" => Some(Language::JsUmi),
97 "rust" => Some(Language::Rust),
98 "go" => Some(Language::Go),
99 _ => None,
100 }
101 }
102
103 pub fn renderer_package(self) -> &'static str {
105 match self {
106 Language::Js => "@codama/renderers-js",
107 Language::JsUmi => "@codama/renderers-js-umi",
108 Language::Rust => "@codama/renderers-rust",
109 Language::Go => "@codama/renderers-go",
110 }
111 }
112}
113
114pub fn entry(cmd: CodamaCommand) -> Result<()> {
115 match cmd {
116 CodamaCommand::Convert { path, out } => convert(path, out),
117 CodamaCommand::Generate {
118 language,
119 path,
120 idl,
121 } => generate(idl, path, language),
122 }
123}
124
125pub fn convert(path: String, out: Option<String>) -> Result<()> {
126 let bytes = fs::read(&path).with_context(|| format!("Failed to read IDL file `{path}`"))?;
127 let idl: JsonValue = serde_json::from_slice(&bytes)
128 .with_context(|| format!("Failed to parse IDL JSON at `{path}`"))?;
129 let root = root_node_from_anchor(&idl)?;
130 let json = serde_json::to_string_pretty(&root)?;
131 match out {
132 Some(out) => fs::write(out, json)?,
133 None => println!("{json}"),
134 }
135 Ok(())
136}
137
138pub fn generate(idl_path: String, base_path: String, languages: Vec<Language>) -> Result<()> {
152 if languages.is_empty() {
153 bail!("`anchor codama generate` requires at least one --language");
154 }
155 let unique: BTreeSet<Language> = languages.into_iter().collect();
158 let base = PathBuf::from(&base_path);
159 let targets: Vec<(Language, PathBuf)> =
160 unique.iter().map(|l| (*l, base.join(l.id()))).collect();
161 let stage_dir = base.join(".codama");
162 render_targets(Path::new(&idl_path), &stage_dir, &targets)
163}
164
165pub fn auto_generate_for_workspace(
176 clients_cfg: &crate::config::ClientsConfig,
177 workspace_dir: &Path,
178 idl_paths: &[PathBuf],
179) -> Result<()> {
180 if !clients_cfg.auto {
181 return Ok(());
182 }
183 let base = workspace_dir.join("clients");
184 let entries = clients_cfg.enabled(&base);
185 if entries.is_empty() {
186 eprintln!(
187 "warning: `[clients] auto = true` but no language is enabled — nothing to generate.",
188 );
189 return Ok(());
190 }
191 if idl_paths.is_empty() {
192 eprintln!(
193 "warning: `[clients] auto = true` but no IDL files were produced by the build — \
194 nothing to generate.",
195 );
196 return Ok(());
197 }
198
199 let multi_program = idl_paths.len() > 1;
200 let codama_stage_root = workspace_dir.join("target").join("codama");
201 for idl_path in idl_paths {
202 let stem = idl_path
203 .file_stem()
204 .and_then(|s| s.to_str())
205 .ok_or_else(|| anyhow!("Invalid IDL filename: {}", idl_path.display()))?;
206 let targets: Vec<(Language, PathBuf)> = entries
207 .iter()
208 .filter_map(|(id, path)| {
209 let lang = Language::from_id(id)?;
210 let out = if multi_program {
211 path.join(stem)
212 } else {
213 path.clone()
214 };
215 Some((lang, out))
216 })
217 .collect();
218 if targets.is_empty() {
219 continue;
220 }
221 let stage_dir = codama_stage_root.join(stem);
222 render_targets(idl_path, &stage_dir, &targets)?;
223 }
224 Ok(())
225}
226
227fn render_targets(
245 idl_path: &Path,
246 stage_dir: &Path,
247 targets: &[(Language, PathBuf)],
248) -> Result<()> {
249 if targets.is_empty() {
250 return Ok(());
251 }
252
253 let bytes = fs::read(idl_path)
254 .with_context(|| format!("Failed to read IDL file `{}`", idl_path.display()))?;
255 let idl: JsonValue = serde_json::from_slice(&bytes)
256 .with_context(|| format!("Failed to parse IDL JSON at `{}`", idl_path.display()))?;
257 let root = root_node_from_anchor(&idl)?;
258
259 fs::create_dir_all(stage_dir).with_context(|| {
260 format!(
261 "Failed to create staging directory `{}`",
262 stage_dir.display()
263 )
264 })?;
265 let staged_idl = stage_dir.join("idl.json");
266 fs::write(&staged_idl, serde_json::to_string_pretty(&root)?)
267 .with_context(|| format!("Failed to write `{}`", staged_idl.display()))?;
268
269 for (_lang, out) in targets {
273 fs::create_dir_all(out)
274 .with_context(|| format!("Failed to create output directory `{}`", out.display()))?;
275 }
276
277 let abs_idl = staged_idl
278 .canonicalize()
279 .with_context(|| format!("Failed to resolve `{}`", staged_idl.display()))?;
280 let mut scripts = Map::new();
281 for (lang, out) in targets {
282 let abs_out = out
283 .canonicalize()
284 .with_context(|| format!("Failed to resolve `{}`", out.display()))?;
285 scripts.insert(
286 lang.id().to_string(),
287 json!({
288 "from": lang.renderer_package(),
289 "args": [abs_out.to_string_lossy()],
290 }),
291 );
292 }
293 let config = json!({
294 "idl": abs_idl.to_string_lossy(),
295 "scripts": scripts,
296 });
297 let config_path = stage_dir.join("codama.json");
298 fs::write(&config_path, serde_json::to_string_pretty(&config)?)
299 .with_context(|| format!("Failed to write `{}`", config_path.display()))?;
300
301 let labels: Vec<&str> = targets.iter().map(|(l, _)| l.id()).collect();
302 eprintln!(
303 "Generating Codama clients [{}] for `{}` ...",
304 labels.join(", "),
305 idl_path.display(),
306 );
307 run_codama(&config_path)?;
308 Ok(())
309}
310
311fn run_codama(config_path: &Path) -> Result<()> {
312 let (program, leading_args) = match std::env::var("ANCHOR_CODAMA_CMD") {
313 Ok(s) if !s.trim().is_empty() => {
314 let mut parts = s.split_whitespace().map(str::to_owned);
316 let program = parts.next().expect("non-empty after trim");
317 (program, parts.collect::<Vec<_>>())
318 }
319 _ => (
320 "npx".to_string(),
321 vec!["--yes".to_string(), "codama".to_string()],
322 ),
323 };
324
325 let mut cmd = Command::new(&program);
326 for arg in &leading_args {
327 cmd.arg(arg);
328 }
329 cmd.arg("run")
330 .arg("--config")
331 .arg(config_path.as_os_str())
332 .arg("--all")
333 .stdin(Stdio::inherit())
334 .stdout(Stdio::inherit())
335 .stderr(Stdio::inherit());
336
337 let status = cmd.status().map_err(|e| {
338 anyhow!(
339 "Failed to spawn `{}`: {e}. Install Node.js + npm, or set ANCHOR_CODAMA_CMD to point \
340 at your Codama binary.",
341 display_command(&program, &leading_args),
342 )
343 })?;
344 if !status.success() {
345 bail!(
346 "`{} run --config {} --all` failed with {status}",
347 display_command(&program, &leading_args),
348 config_path.display(),
349 );
350 }
351 Ok(())
352}
353
354fn display_command(program: &str, args: &[String]) -> String {
355 if args.is_empty() {
356 program.to_string()
357 } else {
358 format!("{program} {}", args.join(" "))
359 }
360}
361
362fn root_node_from_anchor(idl: &JsonValue) -> Result<JsonValue> {
367 let program = program_node_from_anchor(idl)?;
368 Ok(json!({
369 "kind": "rootNode",
370 "standard": "codama",
371 "version": CODAMA_VERSION,
372 "program": program,
373 "additionalPrograms": [],
374 }))
375}
376
377fn program_node_from_anchor(idl: &JsonValue) -> Result<JsonValue> {
378 let metadata = idl
379 .get("metadata")
380 .and_then(JsonValue::as_object)
381 .ok_or_else(|| anyhow!("IDL is missing `metadata`"))?;
382 let name = metadata
383 .get("name")
384 .and_then(JsonValue::as_str)
385 .ok_or_else(|| anyhow!("IDL is missing `metadata.name`"))?;
386 let version = metadata
387 .get("version")
388 .and_then(JsonValue::as_str)
389 .unwrap_or("0.0.0");
390 let public_key = idl
391 .get("address")
392 .and_then(JsonValue::as_str)
393 .ok_or_else(|| anyhow!("IDL is missing `address`"))?;
394
395 let types = idl
396 .get("types")
397 .and_then(JsonValue::as_array)
398 .cloned()
399 .unwrap_or_default();
400 let accounts = idl
401 .get("accounts")
402 .and_then(JsonValue::as_array)
403 .cloned()
404 .unwrap_or_default();
405 let events = idl
406 .get("events")
407 .and_then(JsonValue::as_array)
408 .cloned()
409 .unwrap_or_default();
410 let instructions = idl
411 .get("instructions")
412 .and_then(JsonValue::as_array)
413 .cloned()
414 .unwrap_or_default();
415 let errors = idl
416 .get("errors")
417 .and_then(JsonValue::as_array)
418 .cloned()
419 .unwrap_or_default();
420
421 let (non_generic_types, generics) = extract_generics(&types);
422
423 let account_names: Vec<&str> = accounts.iter().filter_map(named).collect();
427 let event_names: Vec<&str> = events.iter().filter_map(named).collect();
428 let mut defined_types = Vec::new();
429 for ty in &non_generic_types {
430 let n = match named(ty) {
431 Some(n) => n,
432 None => continue,
433 };
434 if account_names.contains(&n) || event_names.contains(&n) {
435 continue;
436 }
437 defined_types.push(defined_type_node_from_anchor(ty, &generics)?);
438 }
439
440 let account_nodes: Vec<JsonValue> = accounts
441 .iter()
442 .map(|a| account_node_from_anchor(a, &types, &generics))
443 .collect::<Result<_>>()?;
444 let event_nodes: Vec<JsonValue> = events
445 .iter()
446 .map(|e| event_node_from_anchor(e, &types, &generics))
447 .collect::<Result<_>>()?;
448 let instruction_nodes: Vec<JsonValue> = instructions
449 .iter()
450 .map(|i| instruction_node_from_anchor(i, &generics))
451 .collect::<Result<_>>()?;
452 let error_nodes: Vec<JsonValue> = errors.iter().map(error_node_from_anchor).collect();
453
454 Ok(json!({
455 "kind": "programNode",
456 "name": camel_case(name),
457 "publicKey": public_key,
458 "version": version,
459 "origin": "anchor",
460 "docs": [],
461 "accounts": account_nodes,
462 "instructions": instruction_nodes,
463 "definedTypes": defined_types,
464 "pdas": [],
465 "events": event_nodes,
466 "errors": error_nodes,
467 }))
468}
469
470fn named(v: &JsonValue) -> Option<&str> {
471 v.get("name").and_then(JsonValue::as_str)
472}
473
474#[derive(Debug, Clone, Default)]
480struct Generics {
481 types: HashMap<String, JsonValue>,
483 type_args: HashMap<String, JsonValue>,
486 const_args: HashMap<String, String>,
489}
490
491fn extract_generics(types: &[JsonValue]) -> (Vec<JsonValue>, Generics) {
492 let mut non_generic = Vec::new();
493 let mut generic_types = HashMap::new();
494 for t in types {
495 let has_generics = t
496 .get("generics")
497 .and_then(JsonValue::as_array)
498 .is_some_and(|a| !a.is_empty());
499 if has_generics {
500 if let Some(n) = named(t) {
501 generic_types.insert(n.to_string(), t.clone());
502 }
503 } else {
504 non_generic.push(t.clone());
505 }
506 }
507 (
508 non_generic,
509 Generics {
510 types: generic_types,
511 type_args: HashMap::new(),
512 const_args: HashMap::new(),
513 },
514 )
515}
516
517fn unwrap_generic_type(defined: &JsonValue, generics: &Generics) -> Result<JsonValue> {
518 let inner = defined
519 .get("defined")
520 .and_then(JsonValue::as_object)
521 .ok_or_else(|| anyhow!("Expected `defined` object"))?;
522 let name = inner
523 .get("name")
524 .and_then(JsonValue::as_str)
525 .ok_or_else(|| anyhow!("`defined` missing `name`"))?;
526 let generic_type = generics
527 .types
528 .get(name)
529 .ok_or_else(|| anyhow!("Generic type `{name}` not found"))?
530 .clone();
531 let generic_definitions = generic_type
532 .get("generics")
533 .and_then(JsonValue::as_array)
534 .cloned()
535 .unwrap_or_default();
536 let generic_args = inner
537 .get("generics")
538 .and_then(JsonValue::as_array)
539 .cloned()
540 .unwrap_or_default();
541
542 let mut type_args: HashMap<String, JsonValue> = HashMap::new();
549 let mut const_args: HashMap<String, String> = HashMap::new();
550 for (i, def) in generic_definitions.iter().enumerate() {
551 let def_name = def
552 .get("name")
553 .and_then(JsonValue::as_str)
554 .ok_or_else(|| anyhow!("Generic definition missing `name`"))?
555 .to_string();
556 let def_kind = def
557 .get("kind")
558 .and_then(JsonValue::as_str)
559 .unwrap_or("type");
560 let arg = generic_args
561 .get(i)
562 .ok_or_else(|| anyhow!("Missing generic argument for `{def_name}`"))?;
563 if def_kind == "const" {
564 if let Some(v) = arg.get("value").and_then(JsonValue::as_str) {
566 const_args.insert(def_name, v.to_string());
567 } else {
568 let outer_name = arg
574 .get("type")
575 .and_then(|t| t.get("generic"))
576 .and_then(JsonValue::as_str)
577 .ok_or_else(|| anyhow!("Const generic arg `{def_name}` missing `value`"))?;
578 let v = generics.const_args.get(outer_name).ok_or_else(|| {
579 anyhow!(
580 "Const generic arg `{def_name}` forwards unknown outer const \
581 `{outer_name}`"
582 )
583 })?;
584 const_args.insert(def_name, v.clone());
585 }
586 } else {
587 let arg_type = arg
588 .get("type")
589 .ok_or_else(|| anyhow!("Type generic arg `{def_name}` missing `type`"))?;
590 let resolved = type_node_from_anchor(arg_type, generics)?;
591 type_args.insert(def_name, resolved);
592 }
593 }
594
595 let scoped = Generics {
596 types: generics.types.clone(),
597 type_args,
598 const_args,
599 };
600 let inner_ty = generic_type
601 .get("type")
602 .ok_or_else(|| anyhow!("Generic typedef `{name}` missing `type`"))?;
603 type_node_from_anchor(inner_ty, &scoped)
604}
605
606const NUMBER_LEAVES: &[&str] = &[
611 "u8", "u16", "u32", "u64", "u128", "i8", "i16", "i32", "i64", "i128", "f32", "f64", "shortU16",
612];
613
614fn type_node_from_anchor(ty: &JsonValue, generics: &Generics) -> Result<JsonValue> {
615 if let Some(leaf) = ty.as_str() {
617 return Ok(match leaf {
618 "bool" => json!({ "kind": "booleanTypeNode", "size": number_node("u8") }),
619 "pubkey" => json!({ "kind": "publicKeyTypeNode" }),
620 "string" => size_prefix_node(string_node("utf8"), number_node("u32")),
621 "bytes" => size_prefix_node(bytes_node(), number_node("u32")),
622 n if NUMBER_LEAVES.contains(&n) => number_node(n),
623 other => bail!("Unrecognized Anchor IDL leaf type: `{other}`"),
624 });
625 }
626 let obj = ty
627 .as_object()
628 .ok_or_else(|| anyhow!("Unrecognized Anchor IDL type: {ty}"))?;
629
630 if obj.contains_key("array") {
631 let arr = obj["array"]
632 .as_array()
633 .ok_or_else(|| anyhow!("`array` must be a 2-tuple"))?;
634 if arr.len() != 2 {
635 bail!("`array` must be a 2-tuple, got {} elements", arr.len());
636 }
637 let item = type_node_from_anchor(&arr[0], generics)?;
638 let size = match &arr[1] {
639 JsonValue::Number(n) => n
640 .as_u64()
641 .ok_or_else(|| anyhow!("Array length must be a non-negative integer"))?,
642 JsonValue::Object(o) if o.contains_key("generic") => {
643 let gname = o["generic"]
644 .as_str()
645 .ok_or_else(|| anyhow!("`generic` must be a string"))?;
646 let v = generics
647 .const_args
648 .get(gname)
649 .ok_or_else(|| anyhow!("Const generic `{gname}` not found"))?;
650 v.parse::<u64>()
651 .with_context(|| format!("Const generic `{gname}` value `{v}` is not u64"))?
652 }
653 other => bail!("Unrecognized array length: {other}"),
654 };
655 return Ok(json!({
656 "kind": "arrayTypeNode",
657 "item": item,
658 "count": { "kind": "fixedCountNode", "value": size },
659 }));
660 }
661
662 if let Some(inner) = obj.get("vec") {
663 let item = type_node_from_anchor(inner, generics)?;
664 return Ok(json!({
665 "kind": "arrayTypeNode",
666 "item": item,
667 "count": { "kind": "prefixedCountNode", "prefix": number_node("u32") },
668 }));
669 }
670
671 if let Some(defined) = obj.get("defined") {
672 let def_obj = defined
676 .as_object()
677 .ok_or_else(|| anyhow!("`defined` must be an object"))?;
678 let has_generics = def_obj
679 .get("generics")
680 .and_then(JsonValue::as_array)
681 .is_some_and(|a| !a.is_empty());
682 if has_generics {
683 return unwrap_generic_type(ty, generics);
684 }
685 let name = def_obj
686 .get("name")
687 .and_then(JsonValue::as_str)
688 .ok_or_else(|| anyhow!("`defined` missing `name`"))?;
689 return Ok(json!({
690 "kind": "definedTypeLinkNode",
691 "name": camel_case(name),
692 }));
693 }
694
695 if let Some(generic) = obj.get("generic").and_then(JsonValue::as_str) {
696 let resolved = generics
698 .type_args
699 .get(generic)
700 .ok_or_else(|| anyhow!("Type generic `{generic}` not found"))?;
701 return Ok(resolved.clone());
702 }
703
704 if let Some(inner) = obj.get("option") {
705 let item = type_node_from_anchor(inner, generics)?;
706 return Ok(json!({
707 "kind": "optionTypeNode",
708 "fixed": false,
709 "item": item,
710 "prefix": number_node("u8"),
711 }));
712 }
713
714 if let Some(inner) = obj.get("coption") {
715 let item = type_node_from_anchor(inner, generics)?;
716 return Ok(json!({
717 "kind": "optionTypeNode",
718 "fixed": true,
719 "item": item,
720 "prefix": number_node("u32"),
721 }));
722 }
723
724 let kind = obj.get("kind").and_then(JsonValue::as_str);
725 if matches!(kind, Some("enum")) {
726 let variants = obj
727 .get("variants")
728 .and_then(JsonValue::as_array)
729 .cloned()
730 .unwrap_or_default();
731 let variant_nodes: Vec<JsonValue> = variants
732 .iter()
733 .map(|v| enum_variant_from_anchor(v, generics))
734 .collect::<Result<_>>()?;
735 return Ok(json!({
736 "kind": "enumTypeNode",
737 "variants": variant_nodes,
738 "size": number_node("u8"),
739 }));
740 }
741
742 if matches!(kind, Some("type")) {
746 if let Some(alias) = obj.get("alias") {
747 return type_node_from_anchor(alias, generics);
748 }
749 }
750 if matches!(kind, Some("alias")) {
751 if let Some(value) = obj.get("value") {
752 return type_node_from_anchor(value, generics);
753 }
754 }
755
756 if matches!(kind, Some("struct")) {
757 let fields = obj
758 .get("fields")
759 .and_then(JsonValue::as_array)
760 .cloned()
761 .unwrap_or_default();
762 return struct_or_tuple_from_fields(&fields, generics);
763 }
764
765 bail!("Unrecognized Anchor IDL type: {ty}")
766}
767
768fn struct_or_tuple_from_fields(fields: &[JsonValue], generics: &Generics) -> Result<JsonValue> {
769 if fields.is_empty() || is_struct_field_array(fields) {
770 let nodes: Vec<JsonValue> = fields
771 .iter()
772 .map(|f| struct_field_from_anchor(f, generics))
773 .collect::<Result<_>>()?;
774 return Ok(json!({ "kind": "structTypeNode", "fields": nodes }));
775 }
776 if is_tuple_field_array(fields) {
777 let items: Vec<JsonValue> = fields
778 .iter()
779 .map(|f| type_node_from_anchor(f, generics))
780 .collect::<Result<_>>()?;
781 return Ok(json!({ "kind": "tupleTypeNode", "items": items }));
782 }
783 bail!("Mixed named/positional fields in struct: {:?}", fields)
784}
785
786fn is_struct_field(field: &JsonValue) -> bool {
787 field
788 .as_object()
789 .is_some_and(|o| o.contains_key("name") && o.contains_key("type"))
790}
791
792fn is_struct_field_array(fields: &[JsonValue]) -> bool {
793 fields.iter().all(is_struct_field)
794}
795
796fn is_tuple_field_array(fields: &[JsonValue]) -> bool {
797 fields.iter().all(|f| !is_struct_field(f))
798}
799
800fn struct_field_from_anchor(field: &JsonValue, generics: &Generics) -> Result<JsonValue> {
801 let obj = field
802 .as_object()
803 .ok_or_else(|| anyhow!("Struct field must be an object: {field}"))?;
804 let name = obj
805 .get("name")
806 .and_then(JsonValue::as_str)
807 .ok_or_else(|| anyhow!("Struct field missing `name`"))?;
808 let ty = obj
809 .get("type")
810 .ok_or_else(|| anyhow!("Struct field `{name}` missing `type`"))?;
811 Ok(json!({
812 "kind": "structFieldTypeNode",
813 "name": camel_case(name),
814 "docs": docs(obj.get("docs")),
815 "type": type_node_from_anchor(ty, generics)?,
816 }))
817}
818
819fn enum_variant_from_anchor(variant: &JsonValue, generics: &Generics) -> Result<JsonValue> {
820 let obj = variant
821 .as_object()
822 .ok_or_else(|| anyhow!("Enum variant must be an object: {variant}"))?;
823 let name = obj.get("name").and_then(JsonValue::as_str).unwrap_or("");
824 let fields = obj.get("fields").and_then(JsonValue::as_array);
825 match fields {
826 None => Ok(json!({
827 "kind": "enumEmptyVariantTypeNode",
828 "name": camel_case(name),
829 })),
830 Some(fs) if fs.is_empty() => Ok(json!({
831 "kind": "enumEmptyVariantTypeNode",
832 "name": camel_case(name),
833 })),
834 Some(fs) if is_struct_field_array(fs) => {
835 let nodes: Vec<JsonValue> = fs
836 .iter()
837 .map(|f| struct_field_from_anchor(f, generics))
838 .collect::<Result<_>>()?;
839 Ok(json!({
840 "kind": "enumStructVariantTypeNode",
841 "name": camel_case(name),
842 "struct": { "kind": "structTypeNode", "fields": nodes },
843 }))
844 }
845 Some(fs) => {
846 let items: Vec<JsonValue> = fs
847 .iter()
848 .map(|f| type_node_from_anchor(f, generics))
849 .collect::<Result<_>>()?;
850 Ok(json!({
851 "kind": "enumTupleVariantTypeNode",
852 "name": camel_case(name),
853 "tuple": { "kind": "tupleTypeNode", "items": items },
854 }))
855 }
856 }
857}
858
859fn defined_type_node_from_anchor(ty: &JsonValue, generics: &Generics) -> Result<JsonValue> {
864 let name = named(ty).unwrap_or("");
865 let inner = ty
866 .get("type")
867 .cloned()
868 .unwrap_or_else(|| json!({ "kind": "struct", "fields": [] }));
869 let node = type_node_from_anchor(&inner, generics)?;
870 Ok(json!({
871 "kind": "definedTypeNode",
872 "name": camel_case(name),
873 "docs": docs(ty.get("docs")),
874 "type": node,
875 }))
876}
877
878fn account_node_from_anchor(
879 acc: &JsonValue,
880 types: &[JsonValue],
881 generics: &Generics,
882) -> Result<JsonValue> {
883 let name = named(acc).ok_or_else(|| anyhow!("Account missing `name`"))?;
884 let ty_def = types
885 .iter()
886 .find(|t| named(t) == Some(name))
887 .ok_or_else(|| anyhow!("Account type `{name}` not found in `types`"))?;
888 let inner = ty_def
889 .get("type")
890 .ok_or_else(|| anyhow!("Account type `{name}` missing `type`"))?;
891 let data = type_node_from_anchor(inner, generics)?;
892 let data_obj = data
893 .as_object()
894 .filter(|o| o.get("kind").and_then(JsonValue::as_str) == Some("structTypeNode"))
895 .ok_or_else(|| anyhow!("Account `{name}` data must be a struct"))?;
896 let mut fields = data_obj
897 .get("fields")
898 .and_then(JsonValue::as_array)
899 .cloned()
900 .unwrap_or_default();
901 let disc = discriminator_bytes(acc)?;
902 let discriminator_field = json!({
903 "kind": "structFieldTypeNode",
904 "name": "discriminator",
905 "docs": [],
906 "type": {
907 "kind": "fixedSizeTypeNode",
908 "size": disc.len(),
909 "type": bytes_node(),
910 },
911 "defaultValue": discriminator_value(&disc),
912 "defaultValueStrategy": "omitted",
913 });
914 fields.insert(0, discriminator_field);
915 Ok(json!({
916 "kind": "accountNode",
917 "name": camel_case(name),
918 "docs": [],
919 "data": { "kind": "structTypeNode", "fields": fields },
920 "discriminators": [{ "kind": "fieldDiscriminatorNode", "name": "discriminator", "offset": 0 }],
921 }))
922}
923
924fn event_node_from_anchor(
925 ev: &JsonValue,
926 types: &[JsonValue],
927 generics: &Generics,
928) -> Result<JsonValue> {
929 let name = named(ev).ok_or_else(|| anyhow!("Event missing `name`"))?;
930 let ty_def = types
931 .iter()
932 .find(|t| named(t) == Some(name))
933 .ok_or_else(|| anyhow!("Event type `{name}` not found in `types`"))?;
934 let inner = ty_def
935 .get("type")
936 .ok_or_else(|| anyhow!("Event type `{name}` missing `type`"))?;
937 let data = type_node_from_anchor(inner, generics)?;
938 let disc = discriminator_bytes(ev)?;
939 let constant = json!({
940 "kind": "constantValueNode",
941 "type": { "kind": "fixedSizeTypeNode", "size": disc.len(), "type": bytes_node() },
942 "value": discriminator_value(&disc),
943 });
944 Ok(json!({
945 "kind": "eventNode",
946 "name": camel_case(name),
947 "docs": [],
948 "data": {
949 "kind": "hiddenPrefixTypeNode",
950 "type": data,
951 "prefix": [constant.clone()],
952 },
953 "discriminators": [{
954 "kind": "constantDiscriminatorNode",
955 "offset": 0,
956 "constant": constant,
957 }],
958 }))
959}
960
961fn error_node_from_anchor(err: &JsonValue) -> JsonValue {
962 let name = named(err).unwrap_or("");
963 let msg = err
964 .get("msg")
965 .and_then(JsonValue::as_str)
966 .unwrap_or("")
967 .to_string();
968 let code = err.get("code").and_then(JsonValue::as_i64).unwrap_or(-1);
969 json!({
970 "kind": "errorNode",
971 "name": camel_case(name),
972 "code": code,
973 "message": msg,
974 "docs": [format!("{name}: {msg}")],
975 })
976}
977
978fn instruction_node_from_anchor(ix: &JsonValue, generics: &Generics) -> Result<JsonValue> {
983 let name = named(ix).ok_or_else(|| anyhow!("Instruction missing `name`"))?;
984 let args = ix
985 .get("args")
986 .and_then(JsonValue::as_array)
987 .cloned()
988 .unwrap_or_default();
989 let mut data_arguments: Vec<JsonValue> = args
990 .iter()
991 .map(|a| instruction_argument_from_anchor(a, generics))
992 .collect::<Result<_>>()?;
993 let disc = discriminator_bytes(ix)?;
994 let discriminator_arg = json!({
995 "kind": "instructionArgumentNode",
996 "name": "discriminator",
997 "docs": [],
998 "type": {
999 "kind": "fixedSizeTypeNode",
1000 "size": disc.len(),
1001 "type": bytes_node(),
1002 },
1003 "defaultValue": discriminator_value(&disc),
1004 "defaultValueStrategy": "omitted",
1005 });
1006 data_arguments.insert(0, discriminator_arg);
1007
1008 let raw_accounts = ix
1009 .get("accounts")
1010 .and_then(JsonValue::as_array)
1011 .cloned()
1012 .unwrap_or_default();
1013 let accounts =
1014 instruction_account_nodes_from_anchor(&raw_accounts, &data_arguments, None, false)?;
1015
1016 Ok(json!({
1017 "kind": "instructionNode",
1018 "name": camel_case(name),
1019 "docs": ix.get("docs").cloned().unwrap_or_else(|| json!([])),
1020 "optionalAccountStrategy": "programId",
1021 "accounts": accounts,
1022 "arguments": data_arguments,
1023 "discriminators": [{ "kind": "fieldDiscriminatorNode", "name": "discriminator", "offset": 0 }],
1024 }))
1025}
1026
1027fn instruction_argument_from_anchor(arg: &JsonValue, generics: &Generics) -> Result<JsonValue> {
1028 let obj = arg
1029 .as_object()
1030 .ok_or_else(|| anyhow!("Instruction argument must be an object: {arg}"))?;
1031 let name = obj
1032 .get("name")
1033 .and_then(JsonValue::as_str)
1034 .ok_or_else(|| anyhow!("Instruction argument missing `name`"))?;
1035 let ty = obj
1036 .get("type")
1037 .ok_or_else(|| anyhow!("Instruction argument `{name}` missing `type`"))?;
1038 Ok(json!({
1039 "kind": "instructionArgumentNode",
1040 "name": camel_case(name),
1041 "docs": docs(obj.get("docs")),
1042 "type": type_node_from_anchor(ty, generics)?,
1043 }))
1044}
1045
1046fn collect_camel_names(items: &[JsonValue], out: &mut Vec<String>) {
1049 for item in items {
1050 let Some(obj) = item.as_object() else {
1051 continue;
1052 };
1053 if let Some(nested) = obj.get("accounts").and_then(JsonValue::as_array) {
1054 collect_camel_names(nested, out);
1055 } else if let Some(n) = obj.get("name").and_then(JsonValue::as_str) {
1056 out.push(camel_case(n));
1057 }
1058 }
1059}
1060
1061fn has_duplicate_account_names(items: &[JsonValue]) -> bool {
1062 let mut names = Vec::new();
1063 collect_camel_names(items, &mut names);
1064 let mut seen = std::collections::HashSet::new();
1065 !names.into_iter().all(|n| seen.insert(n))
1066}
1067
1068fn instruction_account_nodes_from_anchor(
1069 items: &[JsonValue],
1070 instruction_arguments: &[JsonValue],
1071 prefix: Option<&str>,
1072 forced: bool,
1074) -> Result<Vec<JsonValue>> {
1075 let should_prefix = forced || prefix.is_some() || has_duplicate_account_names(items);
1076 let mut out = Vec::new();
1077 for item in items {
1078 let obj = match item.as_object() {
1079 Some(o) => o,
1080 None => continue,
1081 };
1082 if let Some(nested) = obj.get("accounts").and_then(JsonValue::as_array) {
1083 let group_name = obj.get("name").and_then(JsonValue::as_str).unwrap_or("");
1084 let new_prefix = if should_prefix {
1085 Some(match prefix {
1086 Some(p) => format!("{p}_{group_name}"),
1087 None => group_name.to_string(),
1088 })
1089 } else {
1090 None
1091 };
1092 let nested_nodes = instruction_account_nodes_from_anchor(
1096 nested,
1097 instruction_arguments,
1098 new_prefix.as_deref(),
1099 should_prefix,
1100 )?;
1101 out.extend(nested_nodes);
1102 } else {
1103 out.push(instruction_account_node_from_anchor(
1104 item,
1105 instruction_arguments,
1106 if should_prefix { prefix } else { None },
1107 )?);
1108 }
1109 }
1110 Ok(out)
1111}
1112
1113fn instruction_account_node_from_anchor(
1114 item: &JsonValue,
1115 instruction_arguments: &[JsonValue],
1116 prefix: Option<&str>,
1117) -> Result<JsonValue> {
1118 let obj = item
1119 .as_object()
1120 .ok_or_else(|| anyhow!("Account item must be an object: {item}"))?;
1121 let raw_name = obj.get("name").and_then(JsonValue::as_str).unwrap_or("");
1122 let name = match prefix {
1123 Some(p) => format!("{p}_{raw_name}"),
1124 None => raw_name.to_string(),
1125 };
1126 let camel_name = camel_case(&name);
1127 let is_writable = obj
1128 .get("writable")
1129 .and_then(JsonValue::as_bool)
1130 .unwrap_or(false);
1131 let is_signer = obj
1132 .get("signer")
1133 .and_then(JsonValue::as_bool)
1134 .unwrap_or(false);
1135 let is_optional = obj
1136 .get("optional")
1137 .and_then(JsonValue::as_bool)
1138 .unwrap_or(false);
1139 let docs_v = docs(obj.get("docs"));
1140
1141 let mut node = Map::new();
1142 node.insert("kind".into(), json!("instructionAccountNode"));
1143 node.insert("name".into(), json!(camel_name.clone()));
1144 node.insert("isWritable".into(), json!(is_writable));
1145 node.insert("isSigner".into(), json!(is_signer));
1146 node.insert("isOptional".into(), json!(is_optional));
1147 node.insert("docs".into(), docs_v);
1148
1149 if let Some(addr) = obj.get("address").and_then(JsonValue::as_str) {
1150 node.insert(
1151 "defaultValue".into(),
1152 json!({
1153 "kind": "publicKeyValueNode",
1154 "publicKey": addr,
1155 "identifier": camel_name,
1156 }),
1157 );
1158 } else if let Some(pda) = obj.get("pda").and_then(JsonValue::as_object) {
1159 let seeds = pda
1160 .get("seeds")
1161 .and_then(JsonValue::as_array)
1162 .cloned()
1163 .unwrap_or_default();
1164 let nested_path = seeds.iter().any(|s| {
1168 s.get("path")
1169 .and_then(JsonValue::as_str)
1170 .is_some_and(|p| p.contains('.'))
1171 });
1172 if !nested_path {
1173 let mut definitions = Vec::new();
1174 let mut values = Vec::new();
1175 for seed in &seeds {
1176 let (def, val) = pda_seed_node_from_anchor(seed, instruction_arguments, prefix)?;
1177 definitions.push(def);
1178 if let Some(v) = val {
1179 values.push(v);
1180 }
1181 }
1182 let mut program_id: Option<String> = None;
1186 let mut program_id_value: Option<JsonValue> = None;
1187 if let Some(prog) = pda.get("program") {
1188 let (def, val) = pda_seed_node_from_anchor(prog, instruction_arguments, prefix)?;
1189 if let Some(def_obj) = def.as_object() {
1190 if def_obj.get("kind").and_then(JsonValue::as_str)
1191 == Some("constantPdaSeedNode")
1192 {
1193 if let Some(value) = def_obj.get("value").and_then(JsonValue::as_object) {
1194 if value.get("kind").and_then(JsonValue::as_str)
1195 == Some("bytesValueNode")
1196 && value.get("encoding").and_then(JsonValue::as_str)
1197 == Some("base58")
1198 {
1199 program_id = value
1200 .get("data")
1201 .and_then(JsonValue::as_str)
1202 .map(str::to_string);
1203 }
1204 }
1205 }
1206 }
1207 if program_id.is_none() {
1208 if let Some(v) = val {
1209 if let Some(inner_value) = v.get("value").cloned() {
1210 if let Some(k) = inner_value.get("kind").and_then(JsonValue::as_str) {
1211 if k == "accountValueNode" || k == "argumentValueNode" {
1212 program_id_value = Some(inner_value);
1213 }
1214 }
1215 }
1216 }
1217 }
1218 }
1219
1220 let mut pda_link = Map::new();
1221 pda_link.insert("kind".into(), json!("pdaNode"));
1222 pda_link.insert("name".into(), json!(camel_name.clone()));
1223 pda_link.insert("docs".into(), json!([]));
1224 if let Some(pid) = program_id {
1225 pda_link.insert("programId".into(), json!(pid));
1226 }
1227 pda_link.insert("seeds".into(), json!(definitions));
1228
1229 let mut pda_value = Map::new();
1230 pda_value.insert("kind".into(), json!("pdaValueNode"));
1231 pda_value.insert("pda".into(), JsonValue::Object(pda_link));
1232 pda_value.insert("seeds".into(), json!(values));
1233 if let Some(pidv) = program_id_value {
1234 pda_value.insert("programId".into(), pidv);
1235 }
1236 node.insert("defaultValue".into(), JsonValue::Object(pda_value));
1237 }
1238 }
1239
1240 Ok(JsonValue::Object(node))
1241}
1242
1243fn pda_seed_node_from_anchor(
1244 seed: &JsonValue,
1245 instruction_arguments: &[JsonValue],
1246 prefix: Option<&str>,
1247) -> Result<(JsonValue, Option<JsonValue>)> {
1248 let obj = seed
1249 .as_object()
1250 .ok_or_else(|| anyhow!("PDA seed must be an object: {seed}"))?;
1251 let kind = obj
1252 .get("kind")
1253 .and_then(JsonValue::as_str)
1254 .ok_or_else(|| anyhow!("PDA seed missing `kind`"))?;
1255 match kind {
1256 "const" => {
1257 let bytes = obj
1258 .get("value")
1259 .and_then(JsonValue::as_array)
1260 .ok_or_else(|| anyhow!("Const seed missing `value` array"))?;
1261 let raw: Vec<u8> = bytes
1262 .iter()
1263 .map(|b| {
1264 b.as_u64()
1265 .and_then(|n| u8::try_from(n).ok())
1266 .ok_or_else(|| anyhow!("Const seed byte must be 0..=255"))
1267 })
1268 .collect::<Result<_>>()?;
1269 let data = bs58::encode(raw).into_string();
1270 Ok((
1271 json!({
1272 "kind": "constantPdaSeedNode",
1273 "type": bytes_node(),
1274 "value": {
1275 "kind": "bytesValueNode",
1276 "encoding": "base58",
1277 "data": data,
1278 },
1279 }),
1280 None,
1281 ))
1282 }
1283 "account" => {
1284 let path = obj
1285 .get("path")
1286 .and_then(JsonValue::as_str)
1287 .ok_or_else(|| anyhow!("Account seed missing `path`"))?;
1288 let head = path.split('.').next().unwrap_or("");
1289 let prefixed = match prefix {
1290 Some(p) => format!("{p}_{head}"),
1291 None => head.to_string(),
1292 };
1293 let camel_name = camel_case(&prefixed);
1294 Ok((
1295 json!({
1296 "kind": "variablePdaSeedNode",
1297 "name": camel_name.clone(),
1298 "docs": [],
1299 "type": { "kind": "publicKeyTypeNode" },
1300 }),
1301 Some(json!({
1302 "kind": "pdaSeedValueNode",
1303 "name": camel_name.clone(),
1304 "value": { "kind": "accountValueNode", "name": camel_name },
1305 })),
1306 ))
1307 }
1308 "arg" => {
1309 let path = obj
1310 .get("path")
1311 .and_then(JsonValue::as_str)
1312 .ok_or_else(|| anyhow!("Arg seed missing `path`"))?;
1313 let head = path.split('.').next().unwrap_or("");
1314 let arg_name = camel_case(head);
1315 let arg_node = instruction_arguments
1316 .iter()
1317 .find(|a| a.get("name").and_then(JsonValue::as_str) == Some(arg_name.as_str()))
1318 .ok_or_else(|| anyhow!("Arg seed `{path}` not found in instruction arguments"))?;
1319 let arg_type = arg_node.get("type").cloned().unwrap_or_else(|| json!("u8"));
1323 let unwrapped = if is_borsh_string(&arg_type) {
1324 json!({ "kind": "stringTypeNode", "encoding": "utf8" })
1325 } else {
1326 arg_type
1327 };
1328 Ok((
1329 json!({
1330 "kind": "variablePdaSeedNode",
1331 "name": arg_name.clone(),
1332 "docs": [],
1333 "type": unwrapped,
1334 }),
1335 Some(json!({
1336 "kind": "pdaSeedValueNode",
1337 "name": arg_name.clone(),
1338 "value": { "kind": "argumentValueNode", "name": arg_name },
1339 })),
1340 ))
1341 }
1342 other => bail!("Unimplemented PDA seed kind: `{other}`"),
1343 }
1344}
1345
1346fn is_borsh_string(ty: &JsonValue) -> bool {
1347 let Some(obj) = ty.as_object() else {
1348 return false;
1349 };
1350 if obj.get("kind").and_then(JsonValue::as_str) != Some("sizePrefixTypeNode") {
1351 return false;
1352 }
1353 let inner = obj.get("type").and_then(JsonValue::as_object);
1354 let prefix = obj.get("prefix").and_then(JsonValue::as_object);
1355 let inner_ok = inner.is_some_and(|o| {
1356 o.get("kind").and_then(JsonValue::as_str) == Some("stringTypeNode")
1357 && o.get("encoding").and_then(JsonValue::as_str) == Some("utf8")
1358 });
1359 let prefix_ok = prefix.is_some_and(|o| {
1360 o.get("kind").and_then(JsonValue::as_str) == Some("numberTypeNode")
1361 && o.get("format").and_then(JsonValue::as_str) == Some("u32")
1362 });
1363 inner_ok && prefix_ok
1364}
1365
1366fn discriminator_bytes(node: &JsonValue) -> Result<Vec<u8>> {
1371 let arr = node
1372 .get("discriminator")
1373 .and_then(JsonValue::as_array)
1374 .ok_or_else(|| anyhow!("Missing `discriminator`"))?;
1375 arr.iter()
1376 .map(|b| {
1377 b.as_u64()
1378 .and_then(|n| u8::try_from(n).ok())
1379 .ok_or_else(|| anyhow!("Discriminator byte must be 0..=255"))
1380 })
1381 .collect()
1382}
1383
1384fn discriminator_value(bytes: &[u8]) -> JsonValue {
1385 let hex: String = bytes.iter().map(|b| format!("{b:02x}")).collect();
1386 json!({
1387 "kind": "bytesValueNode",
1388 "encoding": "base16",
1389 "data": hex,
1390 })
1391}
1392
1393fn number_node(format: &str) -> JsonValue {
1394 json!({ "kind": "numberTypeNode", "format": format, "endian": "le" })
1395}
1396
1397fn bytes_node() -> JsonValue {
1398 json!({ "kind": "bytesTypeNode" })
1399}
1400
1401fn string_node(encoding: &str) -> JsonValue {
1402 json!({ "kind": "stringTypeNode", "encoding": encoding })
1403}
1404
1405fn size_prefix_node(ty: JsonValue, prefix: JsonValue) -> JsonValue {
1406 json!({ "kind": "sizePrefixTypeNode", "type": ty, "prefix": prefix })
1407}
1408
1409fn docs(value: Option<&JsonValue>) -> JsonValue {
1410 match value {
1411 Some(JsonValue::Array(_)) => value.unwrap().clone(),
1412 Some(JsonValue::String(s)) => json!([s]),
1413 _ => json!([]),
1414 }
1415}
1416
1417fn camel_case(s: &str) -> String {
1422 if s.is_empty() {
1423 return String::new();
1424 }
1425 let mut spaced = String::with_capacity(s.len() + 4);
1428 for c in s.chars() {
1429 if c.is_ascii_uppercase() {
1430 spaced.push(' ');
1431 }
1432 spaced.push(c);
1433 }
1434 let words: Vec<String> = spaced
1436 .split(|c: char| !c.is_ascii_alphanumeric())
1437 .filter(|w| !w.is_empty())
1438 .map(capitalize_word)
1439 .collect();
1440 let pascal: String = words.join("");
1441 let mut chars = pascal.chars();
1442 match chars.next() {
1443 None => String::new(),
1444 Some(c) => c.to_ascii_lowercase().to_string() + chars.as_str(),
1445 }
1446}
1447
1448fn capitalize_word(w: &str) -> String {
1449 let mut iter = w.chars();
1450 match iter.next() {
1451 None => String::new(),
1452 Some(c) => {
1453 let mut out = String::with_capacity(w.len());
1454 out.push(c.to_ascii_uppercase());
1455 for r in iter {
1456 out.push(r.to_ascii_lowercase());
1457 }
1458 out
1459 }
1460 }
1461}
1462
1463#[cfg(test)]
1464mod tests {
1465 use super::*;
1466
1467 fn convert_str(input: &str) -> JsonValue {
1468 let idl: JsonValue = serde_json::from_str(input).unwrap();
1469 root_node_from_anchor(&idl).unwrap()
1470 }
1471
1472 #[test]
1473 fn camel_case_basic() {
1474 assert_eq!(camel_case("snake_case_name"), "snakeCaseName");
1475 assert_eq!(camel_case("kebab-case-name"), "kebabCaseName");
1476 assert_eq!(camel_case("PascalCaseName"), "pascalCaseName");
1477 assert_eq!(camel_case("alreadyCamel"), "alreadyCamel");
1478 assert_eq!(camel_case("u8"), "u8");
1479 assert_eq!(camel_case(""), "");
1480 assert_eq!(camel_case("MyABCThing"), "myABCThing");
1483 }
1484
1485 #[test]
1486 fn empty_idl_yields_root() {
1487 let idl = json!({
1488 "address": "11111111111111111111111111111111",
1489 "metadata": { "name": "demo", "version": "0.1.0", "spec": "0.1.0" },
1490 "instructions": [],
1491 });
1492 let root = root_node_from_anchor(&idl).unwrap();
1493 assert_eq!(root["kind"], "rootNode");
1494 assert_eq!(root["standard"], "codama");
1495 let prog = &root["program"];
1496 assert_eq!(prog["kind"], "programNode");
1497 assert_eq!(prog["name"], "demo");
1498 assert_eq!(prog["origin"], "anchor");
1499 assert_eq!(prog["publicKey"], "11111111111111111111111111111111");
1500 assert_eq!(prog["instructions"].as_array().unwrap().len(), 0);
1501 }
1502
1503 #[test]
1504 fn instruction_with_primitives_and_discriminator() {
1505 let idl = json!({
1506 "address": "11111111111111111111111111111111",
1507 "metadata": { "name": "demo", "version": "0.1.0", "spec": "0.1.0" },
1508 "instructions": [{
1509 "name": "do_thing",
1510 "discriminator": [1,2,3,4,5,6,7,8],
1511 "accounts": [
1512 { "name": "payer", "writable": true, "signer": true }
1513 ],
1514 "args": [
1515 { "name": "amount", "type": "u64" },
1516 { "name": "label", "type": "string" },
1517 { "name": "data", "type": "bytes" }
1518 ]
1519 }],
1520 });
1521 let root = root_node_from_anchor(&idl).unwrap();
1522 let ix = &root["program"]["instructions"][0];
1523 assert_eq!(ix["name"], "doThing");
1524 let args = ix["arguments"].as_array().unwrap();
1525 assert_eq!(args.len(), 4);
1527 assert_eq!(args[0]["name"], "discriminator");
1528 assert_eq!(args[0]["defaultValue"]["data"], "0102030405060708");
1529 assert_eq!(args[1]["name"], "amount");
1530 assert_eq!(args[1]["type"]["format"], "u64");
1531 assert_eq!(args[2]["type"]["kind"], "sizePrefixTypeNode");
1533 assert_eq!(args[2]["type"]["type"]["kind"], "stringTypeNode");
1534 assert_eq!(args[2]["type"]["prefix"]["format"], "u32");
1535 assert_eq!(args[3]["type"]["type"]["kind"], "bytesTypeNode");
1537
1538 let accounts = ix["accounts"].as_array().unwrap();
1539 assert_eq!(accounts[0]["name"], "payer");
1540 assert_eq!(accounts[0]["isWritable"], true);
1541 assert_eq!(accounts[0]["isSigner"], true);
1542 assert_eq!(accounts[0]["isOptional"], false);
1543 }
1544
1545 #[test]
1546 fn account_has_discriminator_field_prepended() {
1547 let idl = json!({
1548 "address": "11111111111111111111111111111111",
1549 "metadata": { "name": "demo", "version": "0.1.0", "spec": "0.1.0" },
1550 "instructions": [],
1551 "accounts": [
1552 { "name": "Counter", "discriminator": [9,8,7,6,5,4,3,2] }
1553 ],
1554 "types": [
1555 {
1556 "name": "Counter",
1557 "type": {
1558 "kind": "struct",
1559 "fields": [{ "name": "count", "type": "u64" }]
1560 }
1561 }
1562 ]
1563 });
1564 let root = convert_str(&serde_json::to_string(&idl).unwrap());
1565 let acc = &root["program"]["accounts"][0];
1566 assert_eq!(acc["kind"], "accountNode");
1567 assert_eq!(acc["name"], "counter");
1568 let fields = acc["data"]["fields"].as_array().unwrap();
1569 assert_eq!(fields.len(), 2);
1570 assert_eq!(fields[0]["name"], "discriminator");
1571 assert_eq!(fields[0]["defaultValueStrategy"], "omitted");
1572 assert_eq!(fields[1]["name"], "count");
1573 assert_eq!(root["program"]["definedTypes"].as_array().unwrap().len(), 0);
1575 }
1576
1577 #[test]
1578 fn event_uses_hidden_prefix_and_constant_discriminator() {
1579 let idl = json!({
1580 "address": "11111111111111111111111111111111",
1581 "metadata": { "name": "demo", "version": "0.1.0", "spec": "0.1.0" },
1582 "instructions": [],
1583 "events": [{ "name": "Tick", "discriminator": [1,1,1,1,1,1,1,1] }],
1584 "types": [{
1585 "name": "Tick",
1586 "type": { "kind": "struct", "fields": [{ "name": "n", "type": "u32" }] }
1587 }]
1588 });
1589 let root = convert_str(&serde_json::to_string(&idl).unwrap());
1590 let ev = &root["program"]["events"][0];
1591 assert_eq!(ev["data"]["kind"], "hiddenPrefixTypeNode");
1592 assert_eq!(ev["discriminators"][0]["kind"], "constantDiscriminatorNode");
1593 }
1594
1595 #[test]
1596 fn errors_format_docs_as_name_colon_msg() {
1597 let idl = json!({
1598 "address": "11111111111111111111111111111111",
1599 "metadata": { "name": "demo", "version": "0.1.0", "spec": "0.1.0" },
1600 "instructions": [],
1601 "errors": [{ "code": 6000, "name": "Boom", "msg": "Kaboom!" }]
1602 });
1603 let root = convert_str(&serde_json::to_string(&idl).unwrap());
1604 let e = &root["program"]["errors"][0];
1605 assert_eq!(e["code"], 6000);
1606 assert_eq!(e["name"], "boom");
1607 assert_eq!(e["docs"][0], "Boom: Kaboom!");
1608 }
1609
1610 #[test]
1611 fn enum_variants_struct_tuple_unit() {
1612 let idl = json!({
1613 "address": "11111111111111111111111111111111",
1614 "metadata": { "name": "demo", "version": "0.1.0", "spec": "0.1.0" },
1615 "instructions": [],
1616 "types": [{
1617 "name": "E",
1618 "type": {
1619 "kind": "enum",
1620 "variants": [
1621 { "name": "Empty" },
1622 { "name": "Tup", "fields": ["u8", "u16"] },
1623 { "name": "Stru", "fields": [{ "name": "x", "type": "bool" }] }
1624 ]
1625 }
1626 }]
1627 });
1628 let root = convert_str(&serde_json::to_string(&idl).unwrap());
1629 let variants = root["program"]["definedTypes"][0]["type"]["variants"]
1630 .as_array()
1631 .unwrap();
1632 assert_eq!(variants[0]["kind"], "enumEmptyVariantTypeNode");
1633 assert_eq!(variants[1]["kind"], "enumTupleVariantTypeNode");
1634 assert_eq!(variants[1]["tuple"]["items"][0]["format"], "u8");
1635 assert_eq!(variants[2]["kind"], "enumStructVariantTypeNode");
1636 }
1637
1638 #[test]
1639 fn vec_array_option_coption() {
1640 let idl = json!({
1641 "address": "11111111111111111111111111111111",
1642 "metadata": { "name": "demo", "version": "0.1.0", "spec": "0.1.0" },
1643 "instructions": [{
1644 "name": "f",
1645 "discriminator": [0,0,0,0,0,0,0,0],
1646 "accounts": [],
1647 "args": [
1648 { "name": "v", "type": { "vec": "u8" } },
1649 { "name": "a", "type": { "array": ["u8", 4] } },
1650 { "name": "o", "type": { "option": "u64" } },
1651 { "name": "co", "type": { "coption": "u64" } }
1652 ]
1653 }]
1654 });
1655 let root = convert_str(&serde_json::to_string(&idl).unwrap());
1656 let args = root["program"]["instructions"][0]["arguments"]
1657 .as_array()
1658 .unwrap();
1659 assert_eq!(args[1]["type"]["count"]["kind"], "prefixedCountNode");
1661 assert_eq!(args[2]["type"]["count"]["kind"], "fixedCountNode");
1662 assert_eq!(args[2]["type"]["count"]["value"], 4);
1663 assert_eq!(args[3]["type"]["kind"], "optionTypeNode");
1664 assert_eq!(args[3]["type"]["fixed"], false);
1665 assert_eq!(args[3]["type"]["prefix"]["format"], "u8");
1666 assert_eq!(args[4]["type"]["fixed"], true);
1667 assert_eq!(args[4]["type"]["prefix"]["format"], "u32");
1668 }
1669
1670 #[test]
1671 fn generics_unwrap_value_and_const() {
1672 let idl = json!({
1673 "address": "11111111111111111111111111111111",
1674 "metadata": { "name": "demo", "version": "0.1.0", "spec": "0.1.0" },
1675 "instructions": [{
1676 "name": "f",
1677 "discriminator": [0,0,0,0,0,0,0,0],
1678 "accounts": [],
1679 "args": [
1680 { "name": "x", "type": {
1681 "defined": {
1682 "name": "Wrap",
1683 "generics": [
1684 { "kind": "type", "type": "u64" },
1685 { "kind": "const", "value": "3" }
1686 ]
1687 }
1688 }}
1689 ]
1690 }],
1691 "types": [{
1692 "name": "Wrap",
1693 "generics": [
1694 { "kind": "type", "name": "T" },
1695 { "kind": "const", "name": "N", "type": "usize" }
1696 ],
1697 "type": {
1698 "kind": "struct",
1699 "fields": [
1700 { "name": "items", "type": { "array": [{ "generic": "T" }, { "generic": "N" }] } }
1701 ]
1702 }
1703 }]
1704 });
1705 let root = convert_str(&serde_json::to_string(&idl).unwrap());
1706 let arg = &root["program"]["instructions"][0]["arguments"][1];
1707 assert_eq!(arg["type"]["kind"], "structTypeNode");
1709 let items_field = &arg["type"]["fields"][0];
1710 assert_eq!(items_field["name"], "items");
1711 assert_eq!(items_field["type"]["item"]["format"], "u64");
1712 assert_eq!(items_field["type"]["count"]["value"], 3);
1713 }
1714
1715 #[test]
1716 fn pda_seeds_const_account_arg() {
1717 let idl = json!({
1718 "address": "11111111111111111111111111111111",
1719 "metadata": { "name": "demo", "version": "0.1.0", "spec": "0.1.0" },
1720 "instructions": [{
1721 "name": "f",
1722 "discriminator": [0,0,0,0,0,0,0,0],
1723 "accounts": [
1724 {
1725 "name": "vault",
1726 "pda": {
1727 "seeds": [
1728 { "kind": "const", "value": [118, 97, 117, 108, 116] },
1729 { "kind": "account", "path": "owner" },
1730 { "kind": "arg", "path": "id" }
1731 ]
1732 }
1733 },
1734 { "name": "owner", "signer": true }
1735 ],
1736 "args": [
1737 { "name": "id", "type": "u64" }
1738 ]
1739 }]
1740 });
1741 let root = convert_str(&serde_json::to_string(&idl).unwrap());
1742 let acc = &root["program"]["instructions"][0]["accounts"][0];
1743 assert_eq!(acc["name"], "vault");
1744 let dv = &acc["defaultValue"];
1745 assert_eq!(dv["kind"], "pdaValueNode");
1746 let seeds = dv["pda"]["seeds"].as_array().unwrap();
1747 assert_eq!(seeds[0]["kind"], "constantPdaSeedNode");
1748 assert_eq!(seeds[0]["value"]["data"], "EMeDBmd");
1750 assert_eq!(seeds[1]["kind"], "variablePdaSeedNode");
1751 assert_eq!(seeds[1]["name"], "owner");
1752 assert_eq!(seeds[2]["name"], "id");
1753 assert_eq!(seeds[2]["type"]["format"], "u64");
1754 let values = dv["seeds"].as_array().unwrap();
1755 assert_eq!(values.len(), 2); assert_eq!(values[0]["value"]["kind"], "accountValueNode");
1757 assert_eq!(values[1]["value"]["kind"], "argumentValueNode");
1758 }
1759
1760 #[test]
1761 fn pda_arg_string_seed_unwraps_borsh_prefix() {
1762 let idl = json!({
1763 "address": "11111111111111111111111111111111",
1764 "metadata": { "name": "demo", "version": "0.1.0", "spec": "0.1.0" },
1765 "instructions": [{
1766 "name": "f",
1767 "discriminator": [0,0,0,0,0,0,0,0],
1768 "accounts": [{
1769 "name": "vault",
1770 "pda": { "seeds": [{ "kind": "arg", "path": "label" }] }
1771 }],
1772 "args": [{ "name": "label", "type": "string" }]
1773 }]
1774 });
1775 let root = convert_str(&serde_json::to_string(&idl).unwrap());
1776 let acc = &root["program"]["instructions"][0]["accounts"][0];
1777 let seed = &acc["defaultValue"]["pda"]["seeds"][0];
1778 assert_eq!(seed["type"]["kind"], "stringTypeNode");
1779 assert_eq!(seed["type"]["encoding"], "utf8");
1780 }
1781
1782 #[test]
1783 fn composite_accounts_get_prefixed_on_collision() {
1784 let idl = json!({
1785 "address": "11111111111111111111111111111111",
1786 "metadata": { "name": "demo", "version": "0.1.0", "spec": "0.1.0" },
1787 "instructions": [{
1788 "name": "f",
1789 "discriminator": [0,0,0,0,0,0,0,0],
1790 "args": [],
1791 "accounts": [
1792 { "name": "a", "accounts": [
1793 { "name": "user", "writable": true }
1794 ]},
1795 { "name": "b", "accounts": [
1796 { "name": "user", "writable": false }
1797 ]}
1798 ]
1799 }]
1800 });
1801 let root = convert_str(&serde_json::to_string(&idl).unwrap());
1802 let accs = root["program"]["instructions"][0]["accounts"]
1803 .as_array()
1804 .unwrap();
1805 let names: Vec<&str> = accs.iter().map(|a| a["name"].as_str().unwrap()).collect();
1806 assert_eq!(names, vec!["aUser", "bUser"]);
1807 }
1808
1809 #[test]
1810 fn pda_with_nested_path_drops_default_value() {
1811 let idl = json!({
1812 "address": "11111111111111111111111111111111",
1813 "metadata": { "name": "demo", "version": "0.1.0", "spec": "0.1.0" },
1814 "instructions": [{
1815 "name": "f",
1816 "discriminator": [0,0,0,0,0,0,0,0],
1817 "accounts": [{
1818 "name": "child",
1819 "pda": { "seeds": [{ "kind": "account", "path": "parent.field" }] }
1820 }],
1821 "args": []
1822 }]
1823 });
1824 let root = convert_str(&serde_json::to_string(&idl).unwrap());
1825 let acc = &root["program"]["instructions"][0]["accounts"][0];
1826 assert!(acc.get("defaultValue").is_none());
1827 }
1828
1829 #[test]
1830 fn language_id_and_renderer_package_are_stable() {
1831 assert_eq!(Language::Js.id(), "js");
1834 assert_eq!(Language::JsUmi.id(), "js-umi");
1835 assert_eq!(Language::Rust.id(), "rust");
1836 assert_eq!(Language::Go.id(), "go");
1837 assert_eq!(Language::Js.renderer_package(), "@codama/renderers-js");
1838 assert_eq!(
1839 Language::JsUmi.renderer_package(),
1840 "@codama/renderers-js-umi"
1841 );
1842 assert_eq!(Language::Rust.renderer_package(), "@codama/renderers-rust");
1843 assert_eq!(Language::Go.renderer_package(), "@codama/renderers-go");
1844 }
1845
1846 #[test]
1847 fn generate_cli_parses_repeated_and_comma_separated_languages() {
1848 use clap::Parser;
1849 let parsed = CodamaCommand::try_parse_from([
1852 "codama", "generate", "-l", "go,js", "-l", "rust", "-p", "out", "idl.json",
1853 ])
1854 .expect("flags parse");
1855 match parsed {
1856 CodamaCommand::Generate {
1857 language,
1858 path,
1859 idl,
1860 } => {
1861 assert_eq!(language, vec![Language::Go, Language::Js, Language::Rust]);
1862 assert_eq!(path, "out");
1863 assert_eq!(idl, "idl.json");
1864 }
1865 other => panic!("expected Generate, got {other:?}"),
1866 }
1867 }
1868
1869 #[test]
1870 fn language_from_id_is_inverse_of_id() {
1871 for lang in [Language::Js, Language::JsUmi, Language::Rust, Language::Go] {
1872 assert_eq!(Language::from_id(lang.id()), Some(lang));
1873 }
1874 assert_eq!(Language::from_id("python"), None);
1875 }
1876
1877 #[test]
1878 fn auto_generate_noops_when_auto_disabled() {
1879 use crate::config::{ClientLanguageConfig, ClientsConfig};
1884 let cfg = ClientsConfig {
1885 auto: false,
1886 rust: Some(ClientLanguageConfig::Enabled(true)),
1887 ..Default::default()
1888 };
1889 let tmp = std::env::temp_dir().join("anchor_codama_auto_disabled");
1890 let _ = fs::remove_dir_all(&tmp);
1891 fs::create_dir_all(&tmp).unwrap();
1892 let idl = tmp.join("p.json");
1893 fs::write(&idl, "{}").unwrap();
1894 auto_generate_for_workspace(&cfg, &tmp, &[idl]).unwrap();
1897 fs::remove_dir_all(&tmp).ok();
1898 }
1899
1900 #[test]
1901 fn auto_generate_warns_when_no_languages_enabled() {
1902 use crate::config::ClientsConfig;
1903 let cfg = ClientsConfig {
1904 auto: true,
1905 ..Default::default()
1906 };
1907 let tmp = std::env::temp_dir().join("anchor_codama_no_langs");
1908 let _ = fs::remove_dir_all(&tmp);
1909 fs::create_dir_all(&tmp).unwrap();
1910 auto_generate_for_workspace(&cfg, &tmp, &[]).unwrap();
1913 fs::remove_dir_all(&tmp).ok();
1914 }
1915
1916 #[test]
1917 fn defined_link_is_emitted_for_non_generic_reference() {
1918 let idl = json!({
1919 "address": "11111111111111111111111111111111",
1920 "metadata": { "name": "demo", "version": "0.1.0", "spec": "0.1.0" },
1921 "instructions": [{
1922 "name": "f",
1923 "discriminator": [0,0,0,0,0,0,0,0],
1924 "accounts": [],
1925 "args": [
1926 { "name": "s", "type": { "defined": { "name": "MyStruct" } } }
1927 ]
1928 }],
1929 "types": [{
1930 "name": "MyStruct",
1931 "type": { "kind": "struct", "fields": [] }
1932 }]
1933 });
1934 let root = convert_str(&serde_json::to_string(&idl).unwrap());
1935 let arg = &root["program"]["instructions"][0]["arguments"][1];
1936 assert_eq!(arg["type"]["kind"], "definedTypeLinkNode");
1937 assert_eq!(arg["type"]["name"], "myStruct");
1938 }
1939}