Skip to content

Commit

Permalink
feat(CLI): Each generated contract method adds <contract-arg>-file-pa…
Browse files Browse the repository at this point in the history
…th (stellar#833)

* feat(CLI): Each generated contract method adds <contract-arg>-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 <paul@stellar.org>
  • Loading branch information
willemneal and Paul Bellamy committed Aug 9, 2023
1 parent 517f1a6 commit ba855c4
Show file tree
Hide file tree
Showing 4 changed files with 104 additions and 4 deletions.
13 changes: 13 additions & 0 deletions cmd/crates/soroban-test/tests/it/custom_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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}));
Expand Down
41 changes: 41 additions & 0 deletions cmd/crates/soroban-test/tests/it/invoke_sandbox.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 <world-file-path>' cannot be used with '--world <Symbol>'"))
.failure();
}

#[test]
fn invoke_hello_world_with_lib() {
TestEnv::with_default(|e| {
Expand Down
52 changes: 49 additions & 3 deletions cmd/soroban-cli/src/commands/contract/invoke.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<OsString>,

Expand Down Expand Up @@ -153,6 +153,8 @@ pub enum Error {
StrKey(#[from] stellar_strkey::DecodeError),
#[error(transparent)]
ContractSpec(#[from] contract_spec::Error),
#[error("")]
MissingFileArg(PathBuf),
}

impl From<Infallible> for Error {
Expand Down Expand Up @@ -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::<PathBuf>(&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))
}
Expand Down Expand Up @@ -521,17 +543,27 @@ fn build_custom_cmd(name: &str, spec: &Spec) -> Result<clap::Command, Error> {
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())
.num_args(1)
.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);
Expand All @@ -553,6 +585,20 @@ fn build_custom_cmd(name: &str, spec: &Spec) -> Result<clap::Command, Error> {
};

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 --<arg_name>-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"#
)
}
2 changes: 1 addition & 1 deletion docs/soroban-cli-full-docs.md
Original file line number Diff line number Diff line change
Expand Up @@ -331,7 +331,7 @@ soroban contract invoke ... -- --help

###### **Arguments:**

* `<CONTRACT_FN_AND_ARGS>`
* `<CONTRACT_FN_AND_ARGS>` — Function name as subcommand, then arguments for that function as `--arg-name value`

###### **Options:**

Expand Down

0 comments on commit ba855c4

Please sign in to comment.