Skip to content

Commit

Permalink
feat: canister call accepts --output json (#3663)
Browse files Browse the repository at this point in the history
this is the same as dfx canister call ... | idl2json

See: https://github.com/dfinity/idl2json

Fixes: https://dfinity.atlassian.net/browse/SDK-1370
  • Loading branch information
ericswanson-dfinity authored Mar 19, 2024
1 parent 731ac56 commit b0405ba
Show file tree
Hide file tree
Showing 9 changed files with 234 additions and 18 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
41 changes: 27 additions & 14 deletions Cargo.lock

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

35 changes: 34 additions & 1 deletion docs/cli-reference/dfx-canister.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ You can use the following options with the `dfx canister call` command.
| `--argument-file <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 <file.did>` | Provide the .did file with which to decode the response. Overrides value from dfx.json for project canisters. |
| `--output <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 <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 <random>` | Specifies the config for generating random arguments. |
| `--type <type>` | Specifies the data format for the argument when making the call using an argument. The valid values are `idl` and `raw`. |
Expand Down Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions e2e/assets/method_signatures/dfx.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"canisters": {
"hello_backend": {
"type": "motoko",
"main": "main.mo"
}
}
}
62 changes: 62 additions & 0 deletions e2e/assets/method_signatures/main.mo
Original file line number Diff line number Diff line change
@@ -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");
};
}
74 changes: 74 additions & 0 deletions e2e/tests-dfx/call.bash
Original file line number Diff line number Diff line change
Expand Up @@ -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 <path to candid file>" {
install_asset call

Expand Down
1 change: 1 addition & 0 deletions src/dfx/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion src/dfx/src/commands/canister/call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>,

/// Specifies the amount of cycles to send on the call.
Expand Down
23 changes: 21 additions & 2 deletions src/dfx/src/util/mod.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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),
Expand All @@ -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?);
}
Expand All @@ -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<String> {
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<String> {
let json_structures = idl_args
.args
.iter()
.map(convert_one)
.collect::<Result<Vec<_>, _>>()?;
Ok(json_structures.join("\n"))
}

pub async fn read_module_metadata(
agent: &ic_agent::Agent,
canister_id: candid::Principal,
Expand Down

0 comments on commit b0405ba

Please sign in to comment.