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