Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

set: Auto-parse type, add option for explicit type #28

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 43 additions & 6 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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<String>,
},
//
// TODO: append/add (name TBD)
Expand All @@ -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.
Expand All @@ -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::<SilentError>() {
Expand Down Expand Up @@ -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<String>,
) -> Result<(), Error> {
let valval = parse_value(value_str, required_type)?;

let tpath = parse_query_cli(query)?.0;
let mut doc = read_parse(path)?;

Expand Down Expand Up @@ -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);
Expand All @@ -254,6 +271,26 @@ fn walk_tpath<'a>(
Some(item)
}

fn parse_value(value_str: &str, required_type: &Option<String>) -> Result<Item, Error> {
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);

Expand Down
116 changes: 93 additions & 23 deletions test/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down