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/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..46ec6c6 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,118 @@ +name: release + +on: + push: + tags: + - v[0-9]+.[0-9]+.[0-9]+* + workflow_dispatch: + +env: + CARGO_TERM_COLOR: always + RUST_TARGET: x86_64-unknown-linux-musl + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + build-linux: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Build toml cli + # uses: juankaram/rust-musl-action@master + uses: gmiam/rust-musl-action@master + with: + args: cargo build --target $RUST_TARGET --release + - name: store-artifacts + uses: actions/upload-artifact@v2 + with: + if-no-files-found: error + name: toml-artifacts-linux + path: | + target/${{ env.RUST_TARGET }}/release/toml + + prepare-tarball-linux: + runs-on: ubuntu-latest + needs: [build-linux] + steps: + - name: download artifacts + uses: actions/download-artifact@v2 + with: + name: toml-artifacts-linux + path: toml-cli + - name: prepare release tarball + run: | + tag=$(echo $GITHUB_REF | cut -d/ -f3-) + tarball="toml-cli-$tag-linux-amd64.tgz" + chmod +x toml-cli/* + tar cf - toml-cli | gzip > ${tarball} + echo "tarball=${tarball}" >> $GITHUB_ENV + + shasum="$tarball.sha256sum" + sha256sum $tarball > $shasum + echo "tarball_shasum=${shasum}" >> $GITHUB_ENV + - name: store-artifacts + uses: actions/upload-artifact@v2 + with: + name: release-tarball + path: | + ${{ env.tarball }} + ${{ env.tarball_shasum }} + + create-release: + runs-on: ubuntu-latest + needs: [prepare-tarball-linux] + steps: + - name: download artifacts + uses: actions/download-artifact@v2 + with: + name: release-tarball + path: tarballs + - name: prepare release env + run: | + echo "tarballs<> $GITHUB_ENV + for I in $(ls tarballs);do echo "tarballs/${I}" >> $GITHUB_ENV; done + echo "EOF" >> $GITHUB_ENV + tag=$(echo $GITHUB_REF | cut -d/ -f3-) + echo "tag=${tag}" >> $GITHUB_ENV + cat $GITHUB_ENV + - name: push release + if: github.event_name == 'push' + uses: softprops/action-gh-release@v1 + with: + name: "Toml cli ${{ env.tag }}" + body: | + "Toml cli release ${{ env.tag }}" + generate_release_notes: true + files: | + ${{ env.tarballs }} + + publish-image: + runs-on: ubuntu-latest + needs: [build-linux] + steps: + - name: Checkout repository + uses: actions/checkout@v2 + - name: Log in to the container registry + uses: docker/login-action@v2 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: download artifacts + uses: actions/download-artifact@v2 + with: + name: toml-artifacts-linux + path: misc + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@v4 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + - name: build and push toml cli image + uses: docker/build-push-action@v3 + with: + context: misc + file: misc/Dockerfile + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} diff --git a/.gitignore b/.gitignore index 53eaa21..346d12b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /target **/*.rs.bk +toml diff --git a/Cargo.lock b/Cargo.lock index 78f01d3..fa77d0b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,15 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "ansi_term" version = "0.12.1" @@ -70,6 +79,12 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bumpalo" +version = "3.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba" + [[package]] name = "bytes" version = "1.3.0" @@ -88,6 +103,21 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-integer", + "num-traits", + "time", + "wasm-bindgen", + "winapi", +] + [[package]] name = "clap" version = "2.34.0" @@ -103,6 +133,16 @@ dependencies = [ "vec_map", ] +[[package]] +name = "codespan-reporting" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" +dependencies = [ + "termcolor", + "unicode-width", +] + [[package]] name = "combine" version = "4.6.6" @@ -113,6 +153,56 @@ dependencies = [ "memchr", ] +[[package]] +name = "core-foundation-sys" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" + +[[package]] +name = "cxx" +version = "1.0.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4a41a86530d0fe7f5d9ea779916b7cadd2d4f9add748b99c2c029cbbdfaf453" +dependencies = [ + "cc", + "cxxbridge-flags", + "cxxbridge-macro", + "link-cplusplus", +] + +[[package]] +name = "cxx-build" +version = "1.0.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06416d667ff3e3ad2df1cd8cd8afae5da26cf9cec4d0825040f88b5ca659a2f0" +dependencies = [ + "cc", + "codespan-reporting", + "once_cell", + "proc-macro2", + "quote", + "scratch", + "syn", +] + +[[package]] +name = "cxxbridge-flags" +version = "1.0.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "820a9a2af1669deeef27cb271f476ffd196a2c4b6731336011e0ba63e2c7cf71" + +[[package]] +name = "cxxbridge-macro" +version = "1.0.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a08a6e2fcc370a089ad3b4aaf54db3b1b4cee38ddabce5896b33eb693275f470" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "either" version = "1.8.0" @@ -141,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" @@ -171,6 +270,30 @@ dependencies = [ "libc", ] +[[package]] +name = "iana-time-zone" +version = "0.1.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64c122667b287044802d6ce17ee2ddf13207ed924c712de9a66a5814d5b64765" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "winapi", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca" +dependencies = [ + "cxx", + "cxx-build", +] + [[package]] name = "indexmap" version = "1.9.2" @@ -181,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" @@ -196,6 +328,15 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4217ad341ebadf8d8e724e264f13e593e0648f5b3e94b3896a5df283be015ecc" +[[package]] +name = "js-sys" +version = "0.3.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47" +dependencies = [ + "wasm-bindgen", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -221,6 +362,24 @@ version = "0.2.137" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89" +[[package]] +name = "link-cplusplus" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9272ab7b96c9046fbc5bc56c06c117cb639fe2d509df0c421cad82d2915cf369" +dependencies = [ + "cc", +] + +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", +] + [[package]] name = "memchr" version = "2.5.0" @@ -247,6 +406,25 @@ dependencies = [ "version_check", ] +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + [[package]] name = "object" version = "0.29.0" @@ -256,6 +434,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "once_cell" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860" + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -298,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" @@ -310,6 +512,12 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" +[[package]] +name = "scratch" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8132065adcfd6e02db789d9285a0deb2f3fcb04002865ab67d5fb103533898" + [[package]] name = "serde" version = "1.0.148" @@ -386,6 +594,29 @@ 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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" +dependencies = [ + "winapi-util", +] + [[package]] name = "textwrap" version = "0.11.0" @@ -395,15 +626,28 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "time" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" +dependencies = [ + "libc", + "wasi", + "winapi", +] + [[package]] name = "toml-cli" version = "0.2.0" dependencies = [ + "chrono", "failure", "nom", "serde", "serde_json", "structopt", + "tempfile", "toml_edit", ] @@ -461,6 +705,66 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + +[[package]] +name = "wasm-bindgen" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f" + [[package]] name = "winapi" version = "0.3.9" @@ -477,6 +781,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" diff --git a/Cargo.toml b/Cargo.toml index cbd9d32..2f232cf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,3 +24,7 @@ serde = "1.0" 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/README.md b/README.md index 7f7d25d..e07bc18 100644 --- a/README.md +++ b/README.md @@ -100,9 +100,43 @@ FLAGS: -V, --version Prints version information SUBCOMMANDS: - get Print some data from the file - help Prints this message or the help of the given subcommand(s) - set Edit the file to set some data (currently, just print modified version) + check Check if a key exists + get Print some data from the file + help Prints this message or the help of the given subcommand(s) + set Edit the file to set some data +``` + +### `toml check` + +``` +$ toml check --help +toml-check 0.2.0 +Check if a key exists + +USAGE: + toml check + +FLAGS: + -h, --help Prints help information + -V, --version Prints version information + +ARGS: + Path to the TOML file to read + Query within the TOML data (e.g. `dependencies.serde`, `foo[0].bar`) +``` + +Check whether a key exists. It will print `true` to stdout in case exists, and set exit code to `0`, +otherwise it will print `false` to stderr and set exit code to `1`. + +```sh +$ toml check test.toml plugins.name2 +false +$ echo $? +1 +$ toml check test.toml plugins.name +true +$ echo $? +0 ``` ### `toml get` @@ -130,14 +164,16 @@ ARGS: ``` $ toml set --help toml-set 0.2.0 -Edit the file to set some data (currently, just print modified version) +Edit the file to set some data USAGE: toml set FLAGS: - -h, --help Prints help information - -V, --version Prints version information + --backup Create a backup file when `overwrite` is set(default: doesn't create a backup file) + -h, --help Prints help information + --overwrite Overwrite the TOML file (default: print to stdout) + -V, --version Prints version information ARGS: Path to the TOML file to read diff --git a/misc/Dockerfile b/misc/Dockerfile new file mode 100644 index 0000000..11df8c4 --- /dev/null +++ b/misc/Dockerfile @@ -0,0 +1,4 @@ +FROM alpine:3.17 + +ADD toml /bin/toml +RUN chmod +x /bin/toml diff --git a/src/main.rs b/src/main.rs index 0d1c10f..94149a3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,9 @@ mod query_parser; +use chrono::{DateTime, Utc}; use std::fs; +use std::fs::OpenOptions; +use std::io::Write; use std::path::PathBuf; use std::str; @@ -10,11 +13,20 @@ use structopt::StructOpt; use toml_edit::{value, Document, Item, Table, Value}; use query_parser::{parse_query, Query, TpathSegment}; +use TpathSegment::{Name, Num}; // TODO: Get more of the description in the README into the CLI help. #[derive(StructOpt)] #[structopt(about)] enum Args { + /// Check if a key exists + Check { + /// Path to the TOML file to read + #[structopt(parse(from_os_str))] + path: PathBuf, + /// Query within the TOML data (e.g. `dependencies.serde`, `foo[0].bar`) + query: String, + }, /// Print some data from the file Get { /// Path to the TOML file to read @@ -25,7 +37,7 @@ enum Args { #[structopt(flatten)] opts: GetOpts, }, - /// Edit the file to set some data (currently, just print modified version) + /// Edit the file to set some data Set { /// Path to the TOML file to read #[structopt(parse(from_os_str))] @@ -34,17 +46,29 @@ enum Args { query: String, /// String value to place at the given spot (bool, array, etc. are TODO) value_str: String, // TODO more forms + #[structopt(flatten)] + opts: SetOpts, }, // 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(Clone, Copy, Default, StructOpt)] +struct SetOpts { + /// Overwrite the TOML file (default: print to stdout) + #[structopt(long)] + overwrite: bool, + /// Create a backup file when `overwrite` is set(default: doesn't create a backup file) + #[structopt(long)] + backup: bool, +} + #[derive(Debug, Fail)] enum CliError { #[fail(display = "bad query")] @@ -58,12 +82,14 @@ enum CliError { fn main() -> Result<(), Error> { let args = Args::from_args(); match args { + Args::Check { path, query } => check(path, &query), Args::Get { path, query, opts } => get(path, &query, opts)?, Args::Set { path, query, value_str, - } => set(path, &query, &value_str)?, + opts, + } => set(path, &query, &value_str, opts)?, } Ok(()) } @@ -75,23 +101,67 @@ fn read_parse(path: PathBuf) -> Result { Ok(data.parse::()?) } -fn get(path: PathBuf, query: &str, opts: GetOpts) -> Result<(), Error> { +fn check_exists(path: PathBuf, query: &str) -> Result { let tpath = parse_query_cli(query)?.0; let doc = read_parse(path)?; + let mut item = doc.as_item(); + for seg in tpath { + match seg { + Name(n) => { + let i = item.get(n); + if i.is_none() { + return Ok(false); + } + item = i.unwrap(); + } + Num(n) => item = &item[n], + } + } + + Ok(true) +} + +/// Check whether a key exists. +/// It will print 'true' to stdout in case exists, and set exit code to '0' +/// otherwise it will print 'false' to stderr and set exit code to '1' +fn check(path: PathBuf, query: &str) { + if let Ok(r) = check_exists(path, query) { + if r { + println!("true"); + std::process::exit(0); + } + } + eprintln!("false"); + std::process::exit(1); +} + +fn get(path: PathBuf, query: &str, opts: GetOpts) -> Result<(), Error> { + let value = get_value(path, query, opts)?; if opts.output_toml { - print_toml_fragment(&doc, &tpath); + print!("{}", value); } else { - let item = walk_tpath(doc.as_item(), &tpath); - // TODO: support shell-friendly output like `jq -r` - println!("{}", serde_json::to_string(&JsonItem(item))?); + println!("{}", value); } Ok(()) } -fn print_toml_fragment(doc: &Document, tpath: &[TpathSegment]) { - use TpathSegment::{Name, Num}; +fn get_value(path: PathBuf, query: &str, opts: GetOpts) -> Result { + let tpath = parse_query_cli(query)?.0; + let doc = read_parse(path)?; + + 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` + serde_json::to_string(&JsonItem(item))? + }; + + Ok(value) +} +fn format_toml_fragment(doc: &Document, tpath: &[TpathSegment]) -> String { let mut item = doc.as_item(); let mut breadcrumbs = vec![]; for seg in tpath { @@ -131,17 +201,29 @@ 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(path: PathBuf, query: &str, value_str: &str) -> Result<(), Error> { +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)?; + let mut doc = read_parse(path.clone())?; let mut item = doc.as_item_mut(); let mut already_inline = false; let mut tpath = &tpath[..]; - use TpathSegment::{Name, Num}; while let Some(seg) = tpath.first() { tpath = &tpath[1..]; // TODO simplify to `for`, unless end up needing a tail match seg { @@ -178,11 +260,34 @@ fn set(path: PathBuf, query: &str, value_str: &str) -> Result<(), Error> { } } } - *item = value(value_str); + *item = detect_value(value_str); - // TODO actually write back - print!("{}", doc); - Ok(()) + let result = if opts.overwrite { + // write content to path + if opts.backup { + let now: DateTime = Utc::now(); + let ext = now.format("%Y%m%d-%H%M%S-%f"); + let backup_file = format!("{}.{}", path.display(), ext); + fs::copy(path.clone(), backup_file)?; + } + let mut output = OpenOptions::new().write(true).truncate(true).open(path)?; + write!(output, "{}", doc)?; + None + } else { + Some(format!("{}", doc)) + }; + + Ok(result) +} + +fn detect_value(value_str: &str) -> Item { + if let Ok(i) = value_str.parse::() { + value(i) + } else if let Ok(b) = value_str.parse::() { + value(b) + } else { + value(value_str) + } } fn parse_query_cli(query: &str) -> Result { @@ -192,7 +297,6 @@ fn parse_query_cli(query: &str) -> Result { } fn walk_tpath<'a>(mut item: &'a toml_edit::Item, tpath: &[TpathSegment]) -> &'a toml_edit::Item { - use TpathSegment::{Name, Num}; for seg in tpath { match seg { Name(n) => item = &item[n], @@ -275,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());