summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/main.rs182
1 files changed, 182 insertions, 0 deletions
diff --git a/src/main.rs b/src/main.rs
new file mode 100644
index 0000000..f40604e
--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,182 @@
+use anyhow::{anyhow, Context};
+use serde_json::Map;
+use toml_edit::{DocumentMut, Table};
+
+/// Apply updates from a JSON file to a TOML file while preserving comments in the latter.
+#[derive(argh::FromArgs)]
+struct Args {
+ #[argh(positional)]
+ toml_file: String,
+
+ #[argh(positional)]
+ json_file: String,
+
+ /// overwrites the given TOML file rather than printing to stdout
+ #[argh(switch)]
+ write: bool,
+}
+
+fn main() -> anyhow::Result<()> {
+ let args: Args = argh::from_env();
+
+ let toml = std::fs::read_to_string(&args.toml_file)?;
+ let json = std::fs::read_to_string(&args.json_file)?;
+
+ let mut doc: DocumentMut = toml.parse()?;
+ let patch: Map<String, serde_json::Value> =
+ serde_json::from_str(&json).context("failed to parse JSON")?;
+ apply(&patch, doc.as_table_mut())?;
+ if args.write {
+ std::fs::write(args.toml_file, doc.to_string())?;
+ } else {
+ print!("{doc}");
+ }
+ Ok(())
+}
+
+fn apply(patch: &Map<String, serde_json::Value>, table: &mut Table) -> anyhow::Result<()> {
+ for (key, val) in patch {
+ match val {
+ serde_json::Value::Object(map) => {
+ if !table.contains_key(key) || !matches!(table[key], toml_edit::Item::Table(_)) {
+ table.insert(key, toml_edit::table());
+ }
+ apply(map, table[key].as_table_mut().unwrap())?;
+ }
+ other => match json_value_to_toml_value(other)? {
+ None => {
+ table.remove(key);
+ }
+ Some(new_value) => {
+ if let Some((_, value)) = table.get_key_value_mut(key) {
+ *value = new_value.into();
+ } else {
+ table.insert(key, new_value.into());
+ }
+ }
+ },
+ }
+ }
+ Ok(())
+}
+
+fn json_value_to_toml_value(value: &serde_json::Value) -> anyhow::Result<Option<toml_edit::Value>> {
+ Ok(match value {
+ serde_json::Value::Null => None,
+ serde_json::Value::Bool(bool) => Some((*bool).into()),
+ serde_json::Value::Number(number) => {
+ if number.is_u64() {
+ Some(i64::try_from(number.as_u64().unwrap()).unwrap().into())
+ } else if number.is_i64() {
+ Some(number.as_i64().unwrap().into())
+ } else if number.is_f64() {
+ Some(number.as_f64().unwrap().into())
+ } else {
+ unreachable!();
+ }
+ }
+ serde_json::Value::String(str) => Some(str.into()),
+ serde_json::Value::Array(vec) => {
+ let mut new = toml_edit::Array::new();
+ for value in vec {
+ match json_value_to_toml_value(value)? {
+ None => return Err(anyhow!("arrays must not contain null")),
+ Some(value) => new.push(value),
+ }
+ }
+ Some(new.into())
+ }
+ serde_json::Value::Object(_) => unreachable!(),
+ })
+}
+
+#[cfg(test)]
+mod tests {
+ use indoc::indoc;
+ use serde_json::json;
+ use toml_edit::DocumentMut;
+
+ fn apply(new_value: serde_json::Value, toml: &str) -> anyhow::Result<String> {
+ let mut doc = toml.parse::<DocumentMut>().unwrap();
+ super::apply(new_value.as_object().unwrap(), doc.as_table_mut())?;
+ Ok(doc.to_string())
+ }
+
+ #[test]
+ fn patch_object() {
+ assert_eq!(
+ apply(
+ json!({"x": {"y": {"foo": true}}}),
+ indoc! {r#"
+ [x.y]
+ # This is x.y.z.
+ z = 3
+ # End of x.y section.
+ "#}
+ )
+ .unwrap(),
+ indoc! {r#"
+ [x.y]
+ # This is x.y.z.
+ z = 3
+ foo = true
+ # End of x.y section.
+ "#}
+ );
+ }
+
+ #[test]
+ fn null() {
+ assert_eq!(apply(json!({"x": null}), "x = 0").unwrap(), "");
+ }
+
+ #[test]
+ fn object() {
+ assert_eq!(
+ apply(json!({"x": {"y": {"z": true}}}), "x = 0").unwrap(),
+ indoc! {r#"
+ [x]
+
+ [x.y]
+ z = true
+ "#}
+ );
+ }
+
+ #[test]
+ fn array_may_not_contain_null() {
+ assert_eq!(
+ apply(json!({"x": [null]}), "x = 0")
+ .unwrap_err()
+ .to_string(),
+ "arrays must not contain null"
+ );
+ }
+
+ macro_rules! test_apply {
+ ($name:ident, $json_value:expr, $toml_value:expr) => {
+ #[test]
+ fn $name() {
+ assert_eq!(apply(json!({"x": $json_value}), "x = 0").unwrap(), format!("x = {}\n", $toml_value));
+
+ // FUTURE: move these assertions to separate functions once https://github.com/rust-lang/rust/issues/124225 is stable
+ assert_eq!(
+ apply(json!({"x": $json_value}), "# before\nx = 0").unwrap(),
+ format!("# before\nx = {}\n", $toml_value)
+ );
+
+ assert_eq!(
+ apply(json!({"x": $json_value}), "x = 0\n# after").unwrap(),
+ format!("x = {}\n# after", $toml_value)
+ );
+ }
+ };
+ }
+
+ test_apply!(bool, true, "true");
+ test_apply!(u64, 1, "1");
+ test_apply!(i64, -1, "-1");
+ test_apply!(f64, 1.0, "1.0");
+ test_apply!(str, "foo", "\"foo\"");
+ test_apply!(array, ["foo", "bar"], "[\"foo\", \"bar\"]");
+}