From b0405bac765f9d366396ccf86dfc575c3755c902 Mon Sep 17 00:00:00 2001 From: Eric Swanson <64809312+ericswanson-dfinity@users.noreply.github.com> Date: Tue, 19 Mar 2024 15:56:52 -0700 Subject: [PATCH] feat: canister call accepts --output json (#3663) this is the same as dfx canister call ... | idl2json See: https://github.com/dfinity/idl2json Fixes: https://dfinity.atlassian.net/browse/SDK-1370 --- CHANGELOG.md | 6 +++ Cargo.lock | 41 ++++++++++----- docs/cli-reference/dfx-canister.mdx | 35 ++++++++++++- e2e/assets/method_signatures/dfx.json | 8 +++ e2e/assets/method_signatures/main.mo | 62 ++++++++++++++++++++++ e2e/tests-dfx/call.bash | 74 +++++++++++++++++++++++++++ src/dfx/Cargo.toml | 1 + src/dfx/src/commands/canister/call.rs | 2 +- src/dfx/src/util/mod.rs | 23 ++++++++- 9 files changed, 234 insertions(+), 18 deletions(-) create mode 100644 e2e/assets/method_signatures/dfx.json create mode 100644 e2e/assets/method_signatures/main.mo diff --git a/CHANGELOG.md b/CHANGELOG.md index e56a07d515..9dd9043c0c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ # UNRELEASED +### feat: dfx canister call ... --output json + +This is the same as `dfx canister call ... | idl2json`, for convenience. + +See also: https://github.com/dfinity/idl2json + ### fix: Output of dfx ping is now valid JSON Added commas in between fields, and newlines to improve formatting. diff --git a/Cargo.lock b/Cargo.lock index 8026c6e000..f8ac0d8927 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -874,9 +874,9 @@ checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" [[package]] name = "chrono" -version = "0.4.34" +version = "0.4.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bc015644b92d5890fab7489e49d21f879d5c990186827d42ec511919404f38b" +checksum = "8eaf5903dcbc0a39312feb77df2ff4c76387d591b9fc7b04a238dcf8bb62639a" dependencies = [ "android-tzdata", "iana-time-zone", @@ -917,9 +917,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.1" +version = "4.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c918d541ef2913577a0f9566e9ce27cb35b6df072075769e0b26cb5a554520da" +checksum = "b230ab84b0ffdf890d5a10abdbc8b83ae1c4918275daea1ab8801f71536b2651" dependencies = [ "clap_builder", "clap_derive", @@ -927,9 +927,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.1" +version = "4.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f3e7391dad68afb0c2ede1bf619f579a3dc9c2ec67f089baa397123a2f3d1eb" +checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" dependencies = [ "anstream", "anstyle", @@ -1264,9 +1264,9 @@ dependencies = [ [[package]] name = "cxx" -version = "1.0.118" +version = "1.0.119" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2673ca5ae28334544ec2a6b18ebe666c42a2650abfb48abbd532ed409a44be2b" +checksum = "635179be18797d7e10edb9cd06c859580237750c7351f39ed9b298bfc17544ad" dependencies = [ "cc", "cxxbridge-flags", @@ -1276,9 +1276,9 @@ dependencies = [ [[package]] name = "cxx-build" -version = "1.0.118" +version = "1.0.119" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9df46fe0eb43066a332586114174c449a62c25689f85a08f28fdcc8e12c380b9" +checksum = "9324397d262f63ef77eb795d900c0d682a34a43ac0932bec049ed73055d52f63" dependencies = [ "cc", "codespan-reporting", @@ -1291,15 +1291,15 @@ dependencies = [ [[package]] name = "cxxbridge-flags" -version = "1.0.118" +version = "1.0.119" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "886acf875df67811c11cd015506b3392b9e1820b1627af1a6f4e93ccdfc74d11" +checksum = "a87ff7342ffaa54b7c61618e0ce2bbcf827eba6d55b923b83d82551acbbecfe5" [[package]] name = "cxxbridge-macro" -version = "1.0.118" +version = "1.0.119" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d151cc139c3080e07f448f93a1284577ab2283d2a44acd902c6fba9ec20b6de" +checksum = "70b5b86cf65fa0626d85720619d80b288013477a91a0389fa8bc716bf4903ad1" dependencies = [ "proc-macro2", "quote", @@ -1489,6 +1489,7 @@ dependencies = [ "ic-utils 0.34.0", "ic-wasm", "icrc-ledger-types", + "idl2json", "indicatif", "itertools 0.10.5", "json-patch", @@ -3240,6 +3241,18 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" +[[package]] +name = "idl2json" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c98794e1d7ff2814db503a7fb3aed059b8c816cbaaaa7cf7420ab39edbca41" +dependencies = [ + "candid", + "candid_parser", + "serde_json", + "sha2 0.10.8", +] + [[package]] name = "idna" version = "0.5.0" diff --git a/docs/cli-reference/dfx-canister.mdx b/docs/cli-reference/dfx-canister.mdx index c90c0d0ad3..36fa464857 100644 --- a/docs/cli-reference/dfx-canister.mdx +++ b/docs/cli-reference/dfx-canister.mdx @@ -120,7 +120,7 @@ You can use the following options with the `dfx canister call` command. | `--argument-file ` | Specifies the file from which to read the argument to pass to the method. Stdin may be referred to as `-`. | | `--async` | Specifies not to wait for the result of the call to be returned by polling the replica. Instead return a response ID. | | `--candid ` | Provide the .did file with which to decode the response. Overrides value from dfx.json for project canisters. | -| `--output ` | Specifies the output format to use when displaying a method’s return result. The valid values are `idl`, `pp` and `raw`. The `pp` option is equivalent to `idl`, but is pretty-printed. | +| `--output ` | Specifies the output format to use when displaying a method’s return result. The valid values are `idl`, 'json', `pp` and `raw`. The `pp` option is equivalent to `idl`, but is pretty-printed. | | `--query` | Sends a query request instead of an update request. For information about the difference between query and update calls, see [Canisters include both program and state](/docs/current/concepts/canisters-code#canister-state). | | `--random ` | Specifies the config for generating random arguments. | | `--type ` | Specifies the data format for the argument when making the call using an argument. The valid values are `idl` and `raw`. | @@ -193,6 +193,39 @@ dfx canister call hello greet --type raw '4449444c00017103e29883' This example uses the raw data type to pass a hexadecimal to the `greet` function of the `hello` canister. +### JSON output + +The `--output json` option formats the output as JSON. + +Candid types don't map 1:1 to JSON types. + +Notably, the following Candid types map to strings rather than numbers: nat, nat64, int, int64. + +These are the mappings: + +| Candid type | JSON type | +|-------------|-----------| +| `null` | `null` | +| `bool` | `boolean` | +| `nat` | `string` | +| `nat8` | `number` | +| `nat16` | `number` | +| `nat32` | `number` | +| `nat64` | `string` | +| `int` | `string` | +| `int8` | `number` | +| `int16` | `number` | +| `int32` | `number` | +| `int64` | `string` | +| `float32` | float or "NaN" | +| `float64` | float or "NaN" | +| `text` | `string` | +| `opt` | array with 0 or 1 elements | +| `vec` | array | +| `record` | object | +| `variant` | object | +| `blob` | array of numbers | + ## dfx canister create Use the `dfx canister create` command to register one or more canister identifiers without compiled code. The new diff --git a/e2e/assets/method_signatures/dfx.json b/e2e/assets/method_signatures/dfx.json new file mode 100644 index 0000000000..6cd4d4338a --- /dev/null +++ b/e2e/assets/method_signatures/dfx.json @@ -0,0 +1,8 @@ +{ + "canisters": { + "hello_backend": { + "type": "motoko", + "main": "main.mo" + } + } +} diff --git a/e2e/assets/method_signatures/main.mo b/e2e/assets/method_signatures/main.mo new file mode 100644 index 0000000000..70198585d0 --- /dev/null +++ b/e2e/assets/method_signatures/main.mo @@ -0,0 +1,62 @@ +import Text "mo:base/Text"; + +actor { + public query func returns_string(name: Text) : async Text { + return "Hello, " # name # "!"; + }; + + public query func returns_opt_string(name: ?Text) : async ?Text { + return switch (name) { + case null null; + case (?x) ?("Hello, " # x # "!"); + }; + }; + + public query func returns_int(v: Int) : async Int { + return v; + }; + + public query func returns_int32(v: Int32) : async Int32 { + return v; + }; + + public query func returns_principal(p: Principal) : async Principal { + return p; + }; + + public query func returns_strings() : async [Text] { + return ["Hello, world!", "Hello, Mars!"]; + }; + + type ObjectReturnType = { + foo: Text; + bar: Int; + }; + + public query func returns_object() : async ObjectReturnType { + return {foo = "baz"; bar = 42}; + }; + + type VariantType = { #foo; #bar : Text; #baz : { a : Int32 }; }; + public query func returns_variant(i: Nat) : async VariantType { + if (i == 0) { + return #foo; + } else if (i == 1) { + return #bar("a bar"); + } else { + return #baz({a = 51}); + } + }; + + public query func returns_blob(s: Text): async Blob { + return Text.encodeUtf8(s); + }; + + public query func returns_tuple(): async (Text, Nat32, Text) { + return ("the first element", 42, "the third element"); + }; + + public query func returns_single_elem_tuple(): async (Text) { + return ("the only element"); + }; +} diff --git a/e2e/tests-dfx/call.bash b/e2e/tests-dfx/call.bash index 3b433f03cc..ed48ebd40d 100644 --- a/e2e/tests-dfx/call.bash +++ b/e2e/tests-dfx/call.bash @@ -14,6 +14,80 @@ teardown() { standard_teardown } +@test "call --output json" { + install_asset method_signatures + + dfx_start + dfx deploy + + assert_command dfx canister call hello_backend returns_string '("you")' --output json + assert_eq '"Hello, you!"' + + assert_command dfx canister call hello_backend returns_opt_string '(null)' --output json + assert_eq '[]' + assert_command dfx canister call hello_backend returns_opt_string '(opt "world")' --output json + assert_eq '[ + "Hello, world!" +]' + + + # int is unbounded, so formatted as a string + assert_command dfx canister call hello_backend returns_int '(67)' --output json + assert_eq '"67"' + assert_command dfx canister call hello_backend returns_int '(111222333444555666777888999 : int)' --output json + assert_eq '"111_222_333_444_555_666_777_888_999"' + + assert_command dfx canister call hello_backend returns_int32 '(67)' --output json + assert_eq '67' + + assert_command dfx canister call hello_backend returns_principal '(principal "fg7gi-vyaaa-aaaal-qadca-cai")' --output json + assert_eq '"fg7gi-vyaaa-aaaal-qadca-cai"' + + # variant + assert_command dfx canister call hello_backend returns_variant '(0)' --output json + assert_eq '{ + "foo": null +}' + assert_command dfx canister call hello_backend returns_variant '(1)' --output json + assert_eq '{ + "bar": "a bar" +}' + assert_command dfx canister call hello_backend returns_variant '(2)' --output json + assert_eq '{ + "baz": { + "a": 51 + } +}' + + assert_command dfx canister call hello_backend returns_strings '()' --output json + assert_eq '[ + "Hello, world!", + "Hello, Mars!" +]' + + assert_command dfx canister call hello_backend returns_object '()' --output json + assert_eq '{ + "bar": "42", + "foo": "baz" +}' + + assert_command dfx canister call hello_backend returns_blob '("abd")' --output json + assert_eq '[ + 97, + 98, + 100 +]' + + assert_command dfx canister call hello_backend returns_tuple '()' --output json + assert_eq '"the first element" +42 +"the third element"' + + + assert_command dfx canister call hello_backend returns_single_elem_tuple '()' --output json + assert_eq '"the only element"' +} + @test "call --candid " { install_asset call diff --git a/src/dfx/Cargo.toml b/src/dfx/Cargo.toml index 5fe9c6d15d..61d4b409da 100644 --- a/src/dfx/Cargo.toml +++ b/src/dfx/Cargo.toml @@ -69,6 +69,7 @@ ic-identity-hsm = { workspace = true } ic-utils = { workspace = true } ic-wasm = "0.7.0" icrc-ledger-types = "0.1.5" +idl2json = "0.10.1" indicatif = "0.16.0" itertools.workspace = true json-patch = "1.0.0" diff --git a/src/dfx/src/commands/canister/call.rs b/src/dfx/src/commands/canister/call.rs index c2ef2eb9f0..400291a8e6 100644 --- a/src/dfx/src/commands/canister/call.rs +++ b/src/dfx/src/commands/canister/call.rs @@ -56,7 +56,7 @@ pub struct CanisterCallOpts { /// Specifies the format for displaying the method's return result. #[arg(long, conflicts_with("async"), - value_parser = ["idl", "raw", "pp"])] + value_parser = ["idl", "raw", "pp", "json"])] output: Option, /// Specifies the amount of cycles to send on the call. diff --git a/src/dfx/src/util/mod.rs b/src/dfx/src/util/mod.rs index 969f179df2..6657144f4a 100644 --- a/src/dfx/src/util/mod.rs +++ b/src/dfx/src/util/mod.rs @@ -1,7 +1,7 @@ use crate::lib::environment::Environment; use crate::lib::error::DfxResult; use crate::{error_invalid_argument, error_invalid_data, error_unknown}; -use anyhow::{bail, Context}; +use anyhow::{anyhow, bail, Context}; use backoff::backoff::Backoff; use backoff::ExponentialBackoff; use bytes::Bytes; @@ -11,6 +11,7 @@ use candid_parser::error::pretty_wrap; use candid_parser::utils::CandidSource; use dfx_core::fs::create_dir_all; use fn_error_context::context; +use idl2json::{idl2json, Idl2JsonOptions}; use num_traits::FromPrimitive; use reqwest::{Client, StatusCode, Url}; use rust_decimal::Decimal; @@ -76,7 +77,7 @@ pub fn print_idl_blob( let hex_string = hex::encode(blob); println!("{}", hex_string); } - "idl" | "pp" => { + "idl" | "pp" | "json" => { let result = match method_type { None => candid::IDLArgs::from_bytes(blob), Some((env, func)) => candid::IDLArgs::from_bytes_with_types(blob, env, &func.rets), @@ -87,6 +88,9 @@ pub fn print_idl_blob( } if output_type == "idl" { println!("{:?}", result?); + } else if output_type == "json" { + let json = convert_all(&result?)?; + println!("{}", json); } else { println!("{}", result?); } @@ -96,6 +100,21 @@ pub fn print_idl_blob( Ok(()) } +/// Candid typically comes as a tuple of values. This converts a single value in such a tuple. +fn convert_one(idl_value: &IDLValue) -> DfxResult { + let json_value = idl2json(idl_value, &Idl2JsonOptions::default()); + serde_json::to_string_pretty(&json_value).with_context(|| anyhow!("Cannot pretty-print json")) +} + +fn convert_all(idl_args: &IDLArgs) -> DfxResult { + let json_structures = idl_args + .args + .iter() + .map(convert_one) + .collect::, _>>()?; + Ok(json_structures.join("\n")) +} + pub async fn read_module_metadata( agent: &ic_agent::Agent, canister_id: candid::Principal,