Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ handlebars = "6.3.2"
hex = "0.4.3"
httptest = "0.16.3"
ic-agent = { version = "0.45.0" }
idl2json = "0.10.1"
ic-asset = "0.27.0"
ic-ed25519 = "0.5.0"
ic-ledger-types = "0.16.0"
Expand Down
1 change: 1 addition & 0 deletions crates/icp-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ ic-management-canister-types.workspace = true
ic-utils.workspace = true
icp-canister-interfaces.workspace = true
icp.workspace = true
idl2json.workspace = true
icrc-ledger-types.workspace = true
indicatif.workspace = true
itertools.workspace = true
Expand Down
49 changes: 47 additions & 2 deletions crates/icp-cli/src/commands/canister/call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use ic_agent::Agent;
use icp::context::Context;
use icp::prelude::*;
use icp_canister_interfaces::proxy::{ProxyArgs, ProxyResult};
use idl2json::{Idl2JsonOptions, idl2json};
use std::io::{self, Write};
use tracing::warn;

Expand Down Expand Up @@ -53,6 +54,10 @@ pub(crate) struct CallArgs {
#[arg(long, requires = "proxy", value_parser = parse_cycles_amount, default_value = "0")]
pub(crate) cycles: u128,

/// Format output as JSON
#[arg(long = "json")]
pub(crate) json_format: bool,

/// Sends a query request to a canister instead of an update request.
///
/// Query calls are faster but return uncertified responses.
Expand Down Expand Up @@ -180,8 +185,13 @@ pub(crate) async fn exec(ctx: &Context, args: &CallArgs) -> Result<(), anyhow::E
None => IDLArgs::from_bytes(&res[..]).context("failed to decode candid response")?,
};

print_candid_for_term(&mut Term::buffered_stdout(), &ret)
.context("failed to print candid return value")?;
if args.json_format {
let json = idl_args_to_json(&ret).context("failed to convert candid response to JSON")?;
println!("{json}");
} else {
print_candid_for_term(&mut Term::buffered_stdout(), &ret)
.context("failed to print candid return value")?;
}

Ok(())
}
Expand All @@ -206,6 +216,20 @@ pub(crate) fn print_candid_for_term(term: &mut Term, args: &IDLArgs) -> io::Resu
Ok(())
}

/// Converts `IDLArgs` to a pretty-printed JSON string.
fn idl_args_to_json(args: &IDLArgs) -> Result<String, anyhow::Error> {
let options = Idl2JsonOptions::default();
let json_values: Vec<String> = args
.args
.iter()
.map(|v| {
let json = idl2json(v, &options);
serde_json::to_string_pretty(&json).context("failed to serialize JSON")
})
.collect::<Result<_, _>>()?;
Ok(json_values.join("\n"))
}

/// Gets the Candid type of a method on a canister by fetching its Candid interface.
///
/// This is a best effort function: it will succeed if
Expand All @@ -229,6 +253,27 @@ async fn get_candid_type(
mod tests {
use super::*;

#[test]
fn idl_args_to_json_converts_record() {
let args =
candid_parser::parse_idl_args(r#"(record { name = "Alice"; age = 30 })"#).unwrap();
let json = idl_args_to_json(&args).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(parsed["name"], "Alice");
// Candid nat is converted to a JSON string (nats can exceed JS safe integer range)
assert_eq!(parsed["age"], "30");
}

#[test]
fn idl_args_to_json_converts_multiple_values() {
let args = candid_parser::parse_idl_args(r#"(42, "hello")"#).unwrap();
let json = idl_args_to_json(&args).unwrap();
let parts: Vec<&str> = json.split('\n').collect();
// Candid nat becomes a JSON string; text stays a string
assert_eq!(parts[0].trim(), r#""42""#);
assert_eq!(parts[1].trim(), r#""hello""#);
}

#[test]
fn typed_decoding_preserves_record_field_names() {
// Encode a record — field names become hashes in the Candid binary format
Expand Down
17 changes: 17 additions & 0 deletions crates/icp-cli/tests/canister_call_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,23 @@ async fn canister_call_with_arguments() {
.success()
.stdout(eq("(\"Hello, world!\")").trim());

// Test calling with --json output
ctx.icp()
.current_dir(&project_dir)
.args([
"canister",
"call",
"--environment",
"random-environment",
"--json",
"my-canister",
"greet",
"(\"world\")",
])
.assert()
.success()
.stdout(eq("\"Hello, world!\"").trim());

// Test calling with --query flag (greet is a query method in the Candid interface)
ctx.icp()
.current_dir(&project_dir)
Expand Down
1 change: 1 addition & 0 deletions docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ Make a canister call
Only used when --proxy is specified. Defaults to 0.

Default value: `0`
* `--json` — Format output as JSON
* `--query` — Sends a query request to a canister instead of an update request.

Query calls are faster but return uncertified responses. Cannot be used with --proxy (proxy calls are always update calls).
Expand Down
Loading