From ba855c45650b7105974cca9443beb20948c7aad2 Mon Sep 17 00:00:00 2001 From: Willem Wyndham Date: Fri, 4 Aug 2023 11:17:41 -0400 Subject: [PATCH] feat(CLI): Each generated contract method adds -file-path (#833) * feat(CLI): Each generated contract method adds -file-path This allows passing file paths in place of arguments. Files are read as strings for all types except Bytes and BytesN where the file is read as raw bytes. The info about the new args is in the help doc header for each subcommand, otherwise the new args are hidden to not clog up the help documentation. * fix: docs * feat: add conflicts_with to arg-file for normal arg --------- Co-authored-by: Paul Bellamy --- .../soroban-test/tests/it/custom_types.rs | 13 +++++ .../soroban-test/tests/it/invoke_sandbox.rs | 41 +++++++++++++++ .../src/commands/contract/invoke.rs | 52 +++++++++++++++++-- docs/soroban-cli-full-docs.md | 2 +- 4 files changed, 104 insertions(+), 4 deletions(-) diff --git a/cmd/crates/soroban-test/tests/it/custom_types.rs b/cmd/crates/soroban-test/tests/it/custom_types.rs index a76c4ab6d..9fb1008b0 100644 --- a/cmd/crates/soroban-test/tests/it/custom_types.rs +++ b/cmd/crates/soroban-test/tests/it/custom_types.rs @@ -59,6 +59,19 @@ fn multi_arg_success() { .stdout("42\n"); } +#[test] +fn bytes_as_file() { + let env = &TestEnv::default(); + let path = env.temp_dir.join("bytes.txt"); + std::fs::write(&path, 0x0073_7465_6c6c_6172u128.to_be_bytes()).unwrap(); + invoke(env, "bytes") + .arg("--bytes-file-path") + .arg(path) + .assert() + .success() + .stdout("\"0000000000000000007374656c6c6172\"\n"); +} + #[test] fn map() { invoke_with_roundtrip("map", json!({"0": true, "1": false})); diff --git a/cmd/crates/soroban-test/tests/it/invoke_sandbox.rs b/cmd/crates/soroban-test/tests/it/invoke_sandbox.rs index 9ac0ae0e4..cc372df9b 100644 --- a/cmd/crates/soroban-test/tests/it/invoke_sandbox.rs +++ b/cmd/crates/soroban-test/tests/it/invoke_sandbox.rs @@ -89,6 +89,47 @@ fn invoke_hello_world() { .success(); } +#[test] +fn invoke_hello_world_from_file() { + let sandbox = TestEnv::default(); + let tmp_file = sandbox.temp_dir.join("world.txt"); + std::fs::write(&tmp_file, "world").unwrap(); + sandbox + .new_assert_cmd("contract") + .arg("invoke") + .arg("--id=1") + .arg("--wasm") + .arg(HELLO_WORLD.path()) + .arg("--") + .arg("hello") + .arg("--world-file-path") + .arg(&tmp_file) + .assert() + .stdout("[\"Hello\",\"world\"]\n") + .success(); +} + +#[test] +fn invoke_hello_world_from_file_fail() { + let sandbox = TestEnv::default(); + let tmp_file = sandbox.temp_dir.join("world.txt"); + std::fs::write(&tmp_file, "world").unwrap(); + sandbox + .new_assert_cmd("contract") + .arg("invoke") + .arg("--id=1") + .arg("--wasm") + .arg(HELLO_WORLD.path()) + .arg("--") + .arg("hello") + .arg("--world-file-path") + .arg(&tmp_file) + .arg("--world=hello") + .assert() + .stderr(predicates::str::contains("error: the argument '--world-file-path ' cannot be used with '--world '")) + .failure(); +} + #[test] fn invoke_hello_world_with_lib() { TestEnv::with_default(|e| { diff --git a/cmd/soroban-cli/src/commands/contract/invoke.rs b/cmd/soroban-cli/src/commands/contract/invoke.rs index 3bfc8653c..b401d9bda 100644 --- a/cmd/soroban-cli/src/commands/contract/invoke.rs +++ b/cmd/soroban-cli/src/commands/contract/invoke.rs @@ -6,7 +6,7 @@ use std::path::{Path, PathBuf}; use std::str::FromStr; use std::{fmt::Debug, fs, io, rc::Rc}; -use clap::{arg, command, Parser}; +use clap::{arg, command, value_parser, Parser}; use heck::ToKebabCase; use soroban_env_host::{ budget::Budget, @@ -58,7 +58,7 @@ pub struct Cmd { help_heading = HEADING_SANDBOX)] pub unlimited_budget: bool, - // Function name as subcommand, then arguments for that function as `--arg-name value` + /// Function name as subcommand, then arguments for that function as `--arg-name value` #[arg(last = true, id = "CONTRACT_FN_AND_ARGS")] pub slop: Vec, @@ -153,6 +153,8 @@ pub enum Error { StrKey(#[from] stellar_strkey::DecodeError), #[error(transparent)] ContractSpec(#[from] contract_spec::Error), + #[error("")] + MissingFileArg(PathBuf), } impl From for Error { @@ -203,6 +205,26 @@ impl Cmd { .map_err(|error| Error::CannotParseArg { arg: name, error }) } else if matches!(i.type_, ScSpecTypeDef::Option(_)) { Ok(ScVal::Void) + } else if let Some(arg_path) = + matches_.get_one::(&fmt_arg_file_name(&name)) + { + if matches!(i.type_, ScSpecTypeDef::Bytes | ScSpecTypeDef::BytesN(_)) { + Ok(ScVal::try_from( + &std::fs::read(arg_path) + .map_err(|_| Error::MissingFileArg(arg_path.clone()))?, + ) + .unwrap()) + } else { + let file_contents = std::fs::read_to_string(arg_path) + .map_err(|_| Error::MissingFileArg(arg_path.clone()))?; + tracing::debug!( + "file {arg_path:?}, has contents:\n{file_contents}\nAnd type {:#?}\n{}", + i.type_, + file_contents.len() + ); + spec.from_string(&file_contents, &i.type_) + .map_err(|error| Error::CannotParseArg { arg: name, error }) + } } else { Err(Error::MissingArgument(name)) } @@ -521,10 +543,12 @@ fn build_custom_cmd(name: &str, spec: &Spec) -> Result { cmd = cmd.alias(kebab_name); } let func = spec.find_function(name).unwrap(); - let doc: &'static str = Box::leak(func.doc.to_string_lossy().into_boxed_str()); + let doc: &'static str = Box::leak(arg_file_help(&func.doc.to_string_lossy()).into_boxed_str()); cmd = cmd.about(Some(doc)); for (name, type_) in inputs_map.iter() { let mut arg = clap::Arg::new(name); + let file_arg_name = fmt_arg_file_name(name); + let mut file_arg = clap::Arg::new(&file_arg_name); arg = arg .long(name) .alias(name.to_kebab_case()) @@ -532,6 +556,14 @@ fn build_custom_cmd(name: &str, spec: &Spec) -> Result { .value_parser(clap::builder::NonEmptyStringValueParser::new()) .long_help(spec.doc(name, type_).unwrap()); + file_arg = file_arg + .long(&file_arg_name) + .alias(file_arg_name.to_kebab_case()) + .num_args(1) + .hide(true) + .value_parser(value_parser!(PathBuf)) + .conflicts_with(name); + if let Some(value_name) = spec.arg_value_name(type_, 0) { let value_name: &'static str = Box::leak(value_name.into_boxed_str()); arg = arg.value_name(value_name); @@ -553,6 +585,20 @@ fn build_custom_cmd(name: &str, spec: &Spec) -> Result { }; cmd = cmd.arg(arg); + cmd = cmd.arg(file_arg); } Ok(cmd) } + +fn fmt_arg_file_name(name: &str) -> String { + format!("{name}-file-path") +} + +fn arg_file_help(docs: &str) -> String { + format!( + r#"{docs} +Usage Notes: +Each arg has a corresponding ---file-path which is a path to a file containing the corresponding JSON argument. +Note: The only types which aren't JSON are Bytes and Bytes which are raw bytes"# + ) +} diff --git a/docs/soroban-cli-full-docs.md b/docs/soroban-cli-full-docs.md index 7a5e29879..0e1170470 100644 --- a/docs/soroban-cli-full-docs.md +++ b/docs/soroban-cli-full-docs.md @@ -331,7 +331,7 @@ soroban contract invoke ... -- --help ###### **Arguments:** -* `` +* `` — Function name as subcommand, then arguments for that function as `--arg-name value` ###### **Options:**