diff --git a/src/main.rs b/src/main.rs index 0f85a13..89a5937 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,7 @@ mod query_parser; use std::path::PathBuf; -use std::str; +use std::str::{self, FromStr}; use std::{fs, process::exit}; use anyhow::Error; @@ -51,8 +51,15 @@ enum Args { /// Query within the TOML data (e.g. `dependencies.serde`, `foo[0].bar`) query: String, - /// String value to place at the given spot (bool, array, etc. are TODO) - value_str: String, // TODO more forms + /// Value to place at the given spot. Treated as a string if it cannot + /// be parsed as a non-string TOML type. Specify `--type` to enforce a + /// specific type. + value_str: String, + + /// Require the value to parse as a specific type. See + /// `Value::type_name()` for values. + #[structopt(name = "type", long)] + required_type: Option, }, // // TODO: append/add (name TBD) @@ -78,6 +85,8 @@ enum CliError { NotArray(), #[error("array index out of bounds")] ArrayIndexOob(), + #[error("value type does not match required type")] + TypeMismatch(), } /// An error that should cause a failure exit, but no message on stderr. @@ -95,7 +104,8 @@ fn main() { path, query, value_str, - } => set(&path, &query, &value_str), + required_type, + } => set(&path, &query, &value_str, &required_type), }; result.unwrap_or_else(|err| { match err.downcast::() { @@ -183,7 +193,14 @@ fn print_toml_fragment(doc: &Document, tpath: &[TpathSegment]) { print!("{}", doc); } -fn set(path: &PathBuf, query: &str, value_str: &str) -> Result<(), Error> { +fn set( + path: &PathBuf, + query: &str, + value_str: &str, + required_type: &Option, +) -> Result<(), Error> { + let valval = parse_value(value_str, required_type)?; + let tpath = parse_query_cli(query)?.0; let mut doc = read_parse(path)?; @@ -227,7 +244,7 @@ fn set(path: &PathBuf, query: &str, value_str: &str) -> Result<(), Error> { } } } - *item = value(value_str); + *item = valval; // TODO actually write back print!("{}", doc); @@ -254,6 +271,26 @@ fn walk_tpath<'a>( Some(item) } +fn parse_value(value_str: &str, required_type: &Option) -> Result { + Ok(match required_type.as_deref() { + None => match Value::from_str(value_str) { + Ok(Value::String(_)) => value(value_str), + Ok(v) => value(v), + Err(_) => value(value_str), + }, + Some("string") => value(value_str), + Some(type_name) => Value::from_str(value_str) + .map_err(|_| CliError::TypeMismatch()) + .and_then(|v| { + if v.type_name() == type_name { + Ok(value(v)) + } else { + Err(CliError::TypeMismatch()) + } + })?, + }) +} + // TODO Can we do newtypes more cleanly than this? struct JsonItem<'a>(&'a toml_edit::Item); diff --git a/test/test.rs b/test/test.rs index 2d705a1..f42c63e 100644 --- a/test/test.rs +++ b/test/test.rs @@ -104,42 +104,112 @@ tomltest_get_err_empty!(get_missing, ["nosuchkey"]); tomltest_get_err_empty!(get_missing_num, ["key[1]"]); macro_rules! tomltest_set { - ($name:ident, $args:expr, $expected:expr) => { + ($name:ident, $args:expr, $expected:literal) => { tomltest!($name, |mut t: TestCaseState| { - t.write_file(INITIAL); + t.write_file(&("\n".to_owned() + INITIAL + "\n")); t.cmd.args(["set", &t.filename()]).args($args); - check_eq(&$expected, &t.expect_success()); + check_eq(&format!($expected), &t.expect_success()); }); }; } -const INITIAL: &str = r#" -[x] -y = 1 -"#; +const INITIAL: &str = "[x]\ny = 1"; -#[rustfmt::skip] -tomltest_set!(set_string_existing, ["x.y", "new"], r#" +tomltest_set!( + set_auto_string_replace_existing, + ["x.y", "update"], + r#" [x] -y = "new" -"#); +y = "update" +"# +); -#[rustfmt::skip] -tomltest_set!(set_string_existing_table, ["x.z", "123"], format!( -r#"{INITIAL}z = "123" -"#)); +tomltest_set!( + set_auto_string_extend_existing, + ["x.z", "new"], + r#" +{INITIAL} +z = "new" +"# +); + +tomltest_set!( + set_auto_string_new_table, + ["foo.bar", "baz"], + r#" +{INITIAL} -#[rustfmt::skip] -tomltest_set!(set_string_new_table, ["foo.bar", "baz"], format!( -r#"{INITIAL} [foo] bar = "baz" -"#)); +"# +); -#[rustfmt::skip] -tomltest_set!(set_string_toplevel, ["foo", "bar"], format!( -r#"foo = "bar" -{INITIAL}"#)); +tomltest_set!( + set_auto_string_new_toplevel, + ["foo", "bar"], + r#"foo = "bar" + +{INITIAL} +"# +); + +tomltest_set!( + set_auto_bool_replace_existing, + ["x.y", "true"], + r#" +[x] +y = true +"# +); + +tomltest_set!( + set_auto_string_array_replace_existing, + ["x.y", r#"["a", "b"]"#], + r#" +[x] +y = ["a", "b"] +"# +); + +tomltest_set!( + set_require_string_for_intlike_value, + ["x.y", "2", "--type", "string"], + r#" +[x] +y = "2" +"# +); + +tomltest_set!( + set_auto_string_with_quoted_string, + ["x.y", r#""update""#], + r#" +[x] +y = "\"update\"" +"# +); + +macro_rules! tomltest_set_err { + ($name:ident, $args:expr, $pattern:expr) => { + tomltest!($name, |mut t: TestCaseState| { + t.write_file(&("\n".to_owned() + INITIAL + "\n")); + t.cmd.args(["set", &t.filename()]).args($args); + check_contains($pattern, &t.expect_error()); + }); + }; +} + +tomltest_set_err!( + set_require_int_got_string, + ["x.y", "foo", "--type", "int"], + "toml: value type does not match required type" +); + +tomltest_set_err!( + set_require_bool_got_int, + ["x.y", "2", "--type", "boolean"], + "toml: value type does not match required type" +); // TODO test `set` on string with newlines and other fun characters // TODO test `set` when existing value is an array, table, or array of tables