diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..9dc281b --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,22 @@ +name: CI +on: + pull_request: + types: + - opened + - edited + - reopened + - synchronize + +env: + CARGO_TERM_COLOR: always + RUST_TARGET: x86_64-unknown-linux-musl + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Build and test + uses: gmiam/rust-musl-action@master + with: + args: make build && make ut diff --git a/Cargo.lock b/Cargo.lock index 77638a6..fa77d0b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -231,6 +231,15 @@ dependencies = [ "synstructure", ] +[[package]] +name = "fastrand" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499" +dependencies = [ + "instant", +] + [[package]] name = "gimli" version = "0.26.2" @@ -295,6 +304,15 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + [[package]] name = "itertools" version = "0.10.5" @@ -464,6 +482,24 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags", +] + +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi", +] + [[package]] name = "rustc-demangle" version = "0.1.21" @@ -558,6 +594,20 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "tempfile" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" +dependencies = [ + "cfg-if", + "fastrand", + "libc", + "redox_syscall", + "remove_dir_all", + "winapi", +] + [[package]] name = "termcolor" version = "1.1.3" @@ -597,6 +647,7 @@ dependencies = [ "serde", "serde_json", "structopt", + "tempfile", "toml_edit", ] diff --git a/Cargo.toml b/Cargo.toml index 5ca8ecc..2f232cf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,3 +25,6 @@ serde_json = "1.0" structopt = "0.3" toml_edit = "0.15" chrono = "0.4" + +[dev-dependencies] +tempfile = "3.3.0" diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..3aaba14 --- /dev/null +++ b/Makefile @@ -0,0 +1,24 @@ +default: build + +CARGO ?= $(shell which cargo) +RUST_TARGET ?= x86_64-unknown-linux-musl + +.format: + ${CARGO} fmt -- --check + +build: .format + ${CARGO} build --target ${RUST_TARGET} --release + # Cargo will skip checking if it is already checked + ${CARGO} clippy --bins --tests -- -Dwarnings + +clean: + ${CARGO} clean + +ut: + RUST_BACKTRACE=1 ${CARGO} test --workspace -- --skip integration --nocapture + +integration: + # run tests under `test` directory + RUST_BACKTRACE=1 ${CARGO} test --workspace -- integration --nocapture + +test: ut integration diff --git a/src/main.rs b/src/main.rs index 4b37af6..94149a3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -52,14 +52,14 @@ enum Args { // TODO: append/add (name TBD) } -#[derive(StructOpt)] +#[derive(Clone, Copy, Default, StructOpt)] struct GetOpts { /// Print as a TOML fragment (default: print as JSON) #[structopt(long)] output_toml: bool, } -#[derive(StructOpt)] +#[derive(Clone, Copy, Default, StructOpt)] struct SetOpts { /// Overwrite the TOML file (default: print to stdout) #[structopt(long)] @@ -137,20 +137,31 @@ fn check(path: PathBuf, query: &str) { } fn get(path: PathBuf, query: &str, opts: GetOpts) -> Result<(), Error> { + let value = get_value(path, query, opts)?; + if opts.output_toml { + print!("{}", value); + } else { + println!("{}", value); + } + Ok(()) +} + +fn get_value(path: PathBuf, query: &str, opts: GetOpts) -> Result { let tpath = parse_query_cli(query)?.0; let doc = read_parse(path)?; - if opts.output_toml { - print_toml_fragment(&doc, &tpath); + let value = if opts.output_toml { + format_toml_fragment(&doc, &tpath) } else { let item = walk_tpath(doc.as_item(), &tpath); // TODO: support shell-friendly output like `jq -r` - println!("{}", serde_json::to_string(&JsonItem(item))?); - } - Ok(()) + serde_json::to_string(&JsonItem(item))? + }; + + Ok(value) } -fn print_toml_fragment(doc: &Document, tpath: &[TpathSegment]) { +fn format_toml_fragment(doc: &Document, tpath: &[TpathSegment]) -> String { let mut item = doc.as_item(); let mut breadcrumbs = vec![]; for seg in tpath { @@ -190,10 +201,23 @@ fn print_toml_fragment(doc: &Document, tpath: &[TpathSegment]) { } } let doc = Document::from(item.into_table().unwrap()); - print!("{}", doc); + format!("{}", doc) } fn set(path: PathBuf, query: &str, value_str: &str, opts: SetOpts) -> Result<(), Error> { + let result = set_value(path, query, value_str, opts)?; + if let Some(doc) = result { + print!("{}", doc); + } + Ok(()) +} + +fn set_value( + path: PathBuf, + query: &str, + value_str: &str, + opts: SetOpts, +) -> Result, Error> { let tpath = parse_query_cli(query)?.0; let mut doc = read_parse(path.clone())?; @@ -238,7 +262,7 @@ fn set(path: PathBuf, query: &str, value_str: &str, opts: SetOpts) -> Result<(), } *item = detect_value(value_str); - if opts.overwrite { + let result = if opts.overwrite { // write content to path if opts.backup { let now: DateTime = Utc::now(); @@ -248,10 +272,12 @@ fn set(path: PathBuf, query: &str, value_str: &str, opts: SetOpts) -> Result<(), } let mut output = OpenOptions::new().write(true).truncate(true).open(path)?; write!(output, "{}", doc)?; + None } else { - print!("{}", doc); - } - Ok(()) + Some(format!("{}", doc)) + }; + + Ok(result) } fn detect_value(value_str: &str) -> Item { @@ -353,3 +379,137 @@ impl Serialize for JsonValue<'_> { } } } + +#[cfg(test)] +mod tests { + use std::fs; + + // functions to test + use super::check_exists; + use super::detect_value; + use super::{get_value, GetOpts}; + use super::{set_value, SetOpts}; + + #[test] + fn test_detect_value() { + let i = detect_value("abc"); + assert_eq!("string", i.type_name()); + assert!(i.is_str()); + assert_eq!(Some("abc"), i.as_str()); + + let i = detect_value("123"); + assert_eq!("integer", i.type_name()); + assert!(i.is_integer()); + assert_eq!(Some(123), i.as_integer()); + + let i = detect_value("true"); + assert_eq!("boolean", i.type_name()); + assert!(i.is_bool()); + assert_eq!(Some(true), i.as_bool()); + } + + #[test] + fn test_check_exists() { + let body = r#"[a] +b = "c" +[x] +y = "z""#; + let dir = tempfile::tempdir().expect("failed to create tempdir"); + let toml_file = dir.path().join("test.toml"); + fs::write(&toml_file, body).expect("failed to create tempfile"); + + // x.y exists + let result = check_exists(toml_file.clone(), "x.y"); + assert!(result.is_ok()); + assert!(result.unwrap()); + + // x.z does not exists + let result = check_exists(toml_file, "x.z"); + assert!(result.is_ok()); + assert!(!result.unwrap()); + } + + #[test] + fn test_get_value() { + let body = r#"[a] +b = "c" +[x] +y = "z""#; + let dir = tempfile::tempdir().expect("failed to create tempdir"); + let toml_file = dir.path().join("test.toml"); + fs::write(&toml_file, body).expect("failed to write tempfile"); + + let opts = GetOpts::default(); + // x.y exists + let result = get_value(toml_file.clone(), "x.y", opts); + assert!(result.is_ok()); + assert_eq!("\"z\"", result.unwrap()); + + // x.z does not exists + // FIXME: get_value now will panic, it's not a well-desined API. + let result = std::panic::catch_unwind(|| { + let _ = get_value(toml_file.clone(), "x.z", opts); + }); + assert!(result.is_err()); + } + + #[test] + fn test_set_value() { + // fn set(path: PathBuf, query: &str, value_str: &str, opts: SetOpts) -> Result<(), Error> { + let body = r#"[a] +b = "c" +[x] +y = "z""#; + let dir = tempfile::tempdir().expect("failed to create tempdir"); + let toml_file = dir.path().join("test.toml"); + fs::write(&toml_file, body).expect("failed to write tempfile"); + + let mut opts = SetOpts::default(); + // x.y exists + let result = set_value(toml_file.clone(), "x.y", "new", opts); + assert!(result.is_ok()); + let excepted = r#"[a] +b = "c" +[x] +y = "new" +"#; + assert_eq!(excepted, result.unwrap().unwrap()); + + let result = set_value(toml_file.clone(), "x.z", "123", opts); + assert!(result.is_ok()); + let excepted = r#"[a] +b = "c" +[x] +y = "z" +z = 123 +"#; + assert_eq!(excepted, result.unwrap().unwrap()); + + let result = set_value(toml_file.clone(), "x.z", "false", opts); + assert!(result.is_ok()); + let excepted = r#"[a] +b = "c" +[x] +y = "z" +z = false +"#; + assert_eq!(excepted, result.unwrap().unwrap()); + + // test overwrite the original file + opts.overwrite = true; + let result = set_value(toml_file.clone(), "x.z", "false", opts); + assert!(result.is_ok()); + println!("{:?}", result); + // --overwrite will not generate any output. + assert_eq!(None, result.unwrap()); + + let excepted = r#"[a] +b = "c" +[x] +y = "z" +z = false +"#; + let new_body = fs::read_to_string(toml_file).expect("failed to read TOML file"); + assert_eq!(excepted, new_body); + } +} diff --git a/test/test.rs b/test/test.rs index f17eb54..d11b7b6 100644 --- a/test/test.rs +++ b/test/test.rs @@ -5,7 +5,7 @@ use std::process; use std::str; #[test] -fn help_if_no_args() { +fn integration_test_help_if_no_args() { // Probably want to factor out much of this when adding more tests. let proc = process::Command::new(get_exec_path()).output().unwrap(); assert!(!proc.status.success());