shine-core 2.0.0

Reusable lifecycle runtime and domain core for Shine applications
Documentation
/// Recursively syncs `doc` (a formatted TOML document) to match `target`:
/// removes keys absent from `target`, inserts/updates keys that differ, recurses
/// into nested tables, and leaves unchanged values (and their formatting/comments) untouched.
pub fn sync_table(doc: &mut toml_edit::Table, target: &toml::Table) {
    let to_remove = doc
        .iter()
        .map(|(k, _)| k.to_string())
        .filter(|k| !target.contains_key(k))
        .collect::<Vec<_>>();

    for k in &to_remove {
        doc.remove(k);
    }
    for (key, target_value) in target {
        match target_value {
            toml::Value::Table(sub_target) => {
                let entry = doc
                    .entry(key)
                    .or_insert(toml_edit::Item::Table(Default::default()));
                if let Some(sub_doc) = entry.as_table_mut() {
                    sync_table(sub_doc, sub_target);
                }
            }
            _ => {
                if let (Some(inline), toml::Value::String(value)) = (
                    doc.get_mut(key).and_then(|item| item.as_inline_table_mut()),
                    target_value,
                ) && inline.contains_key("value")
                {
                    let unchanged = inline
                        .get("value")
                        .and_then(toml_edit::Value::as_str)
                        .is_some_and(|existing| existing == value);
                    if !unchanged {
                        inline.insert("value", toml_edit::Value::from(value.as_str()));
                    }
                    continue;
                }
                if let Some(existing) = doc.get(key).and_then(|t| t.as_value())
                    && values_equal(existing, target_value)
                {
                    continue;
                }
                doc.insert(
                    key,
                    toml_edit::value(convert_to_edit_toml_value(target_value)),
                );
            }
        }
    }
}

fn values_equal(edit: &toml_edit::Value, target: &toml::Value) -> bool {
    match (edit, target) {
        (toml_edit::Value::String(a), toml::Value::String(b)) => a.value() == b,
        (toml_edit::Value::Integer(a), toml::Value::Integer(b)) => a.value() == b,
        (toml_edit::Value::Float(a), toml::Value::Float(b)) => a.value() == b,
        (toml_edit::Value::Boolean(a), toml::Value::Boolean(b)) => a.value() == b,
        (toml_edit::Value::Array(a), toml::Value::Array(b)) => {
            a.len() == b.len() && a.iter().zip(b).all(|(a, b)| values_equal(a, b))
        }
        _ => false,
    }
}

fn convert_to_edit_toml_value(v: &toml::Value) -> toml_edit::Value {
    match v {
        toml::Value::String(s) => toml_edit::Value::from(s.as_str()),
        toml::Value::Integer(i) => toml_edit::Value::from(*i),
        toml::Value::Float(f) => toml_edit::Value::from(*f),
        toml::Value::Boolean(b) => toml_edit::Value::from(*b),
        toml::Value::Datetime(d) => toml_edit::Value::from(*d),
        toml::Value::Array(arr) => {
            toml_edit::Value::Array(arr.iter().map(convert_to_edit_toml_value).collect())
        }
        toml::Value::Table(table) => {
            let mut inline = toml_edit::InlineTable::new();
            for (k, v) in table {
                inline.insert(k, convert_to_edit_toml_value(v));
            }
            toml_edit::Value::InlineTable(inline)
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_convert_to_edit_toml_value_string() {
        let v = toml::Value::String("hello".to_string());
        let result = convert_to_edit_toml_value(&v);
        assert!(matches!(result, toml_edit::Value::String(_)));
        if let toml_edit::Value::String(s) = result {
            assert_eq!(s.value(), "hello");
        }
    }

    #[test]
    fn test_convert_to_edit_toml_value_integer() {
        let v = toml::Value::Integer(42);
        let result = convert_to_edit_toml_value(&v);
        assert!(matches!(result, toml_edit::Value::Integer(_)));
        if let toml_edit::Value::Integer(i) = result {
            assert_eq!(*i.value(), 42);
        }
    }

    #[test]
    fn test_convert_to_edit_toml_value_float() {
        let v = toml::Value::Float(2.5);
        let result = convert_to_edit_toml_value(&v);
        assert!(matches!(result, toml_edit::Value::Float(_)));
        if let toml_edit::Value::Float(f) = result {
            assert_eq!(*f.value(), 2.5);
        }
    }

    #[test]
    fn test_convert_to_edit_toml_value_boolean() {
        for b in [true, false] {
            let v = toml::Value::Boolean(b);
            let result = convert_to_edit_toml_value(&v);
            assert!(matches!(result, toml_edit::Value::Boolean(_)));
            if let toml_edit::Value::Boolean(val) = result {
                assert_eq!(*val.value(), b);
            }
        }
    }

    #[test]
    fn test_convert_to_edit_toml_value_datetime() {
        let dt: toml::value::Datetime = "1979-05-27T07:32:00Z".parse().unwrap();
        let v = toml::Value::Datetime(dt);
        let result = convert_to_edit_toml_value(&v);
        assert!(matches!(result, toml_edit::Value::Datetime(_)));
        if let toml_edit::Value::Datetime(d) = result {
            assert_eq!(d.value().to_string(), dt.to_string());
        }
    }

    #[test]
    fn test_convert_to_edit_toml_value_array() {
        let v = toml::Value::Array(vec![
            toml::Value::String("a".to_string()),
            toml::Value::Integer(1),
            toml::Value::Boolean(false),
        ]);
        let result = convert_to_edit_toml_value(&v);
        assert!(matches!(result, toml_edit::Value::Array(_)));
        if let toml_edit::Value::Array(arr) = result {
            assert_eq!(arr.len(), 3);
            assert!(matches!(
                arr.iter().next().unwrap(),
                toml_edit::Value::String(_)
            ));
        }
    }

    #[test]
    fn test_convert_to_edit_toml_value_nested_array() {
        let v = toml::Value::Array(vec![
            toml::Value::Array(vec![toml::Value::Integer(1), toml::Value::Integer(2)]),
            toml::Value::Array(vec![toml::Value::Integer(3)]),
        ]);
        let result = convert_to_edit_toml_value(&v);
        assert!(matches!(result, toml_edit::Value::Array(_)));
        if let toml_edit::Value::Array(outer) = result {
            assert_eq!(outer.len(), 2);
            assert!(matches!(
                outer.iter().next().unwrap(),
                toml_edit::Value::Array(_)
            ));
        }
    }

    #[test]
    fn test_convert_to_edit_toml_value_table() {
        let mut table = toml::value::Table::new();
        table.insert("key1".to_string(), toml::Value::String("val".to_string()));
        table.insert("key2".to_string(), toml::Value::Integer(99));
        let v = toml::Value::Table(table);
        let result = convert_to_edit_toml_value(&v);
        assert!(matches!(result, toml_edit::Value::InlineTable(_)));
        if let toml_edit::Value::InlineTable(inline) = result {
            assert_eq!(inline.len(), 2);
            let v1 = inline.get("key1").unwrap();
            assert!(matches!(v1, toml_edit::Value::String(_)));
            if let toml_edit::Value::String(s) = v1 {
                assert_eq!(s.value(), "val");
            }
            let v2 = inline.get("key2").unwrap();
            assert!(matches!(v2, toml_edit::Value::Integer(_)));
            if let toml_edit::Value::Integer(i) = v2 {
                assert_eq!(*i.value(), 99);
            }
        }
    }

    #[test]
    fn test_convert_to_edit_toml_value_nested_table() {
        let mut inner = toml::value::Table::new();
        inner.insert("x".to_string(), toml::Value::Boolean(true));
        let mut outer = toml::value::Table::new();
        outer.insert("inner".to_string(), toml::Value::Table(inner));
        let v = toml::Value::Table(outer);
        let result = convert_to_edit_toml_value(&v);
        assert!(matches!(result, toml_edit::Value::InlineTable(_)));
        if let toml_edit::Value::InlineTable(inline) = result {
            let nested = inline.get("inner").unwrap();
            assert!(matches!(nested, toml_edit::Value::InlineTable(_)));
            if let toml_edit::Value::InlineTable(inner_inline) = nested {
                assert_eq!(inner_inline.len(), 1);
                let xv = inner_inline.get("x").unwrap();
                assert!(matches!(xv, toml_edit::Value::Boolean(_)));
                if let toml_edit::Value::Boolean(b) = xv {
                    assert!(*b.value());
                }
            }
        }
    }

    fn make_doc(toml: &str) -> toml_edit::DocumentMut {
        toml.parse().unwrap()
    }

    fn make_target(toml: &str) -> toml::Table {
        toml::from_str(toml).unwrap()
    }

    #[test]
    fn sync_table_adds_new_keys() {
        let mut doc = make_doc("");
        let target = make_target("name = \"shine\"\nversion = 1\n");
        sync_table(doc.as_table_mut(), &target);
        assert_eq!(doc["name"].as_str(), Some("shine"));
        assert_eq!(doc["version"].as_integer(), Some(1));
    }

    #[test]
    fn sync_table_removes_stale_keys() {
        let mut doc = make_doc("old = \"gone\"\nkeep = true\n");
        let target = make_target("keep = true");
        sync_table(doc.as_table_mut(), &target);
        assert!(doc.get("old").is_none());
        assert!(doc.get("keep").is_some());
    }

    #[test]
    fn sync_table_updates_changed_value() {
        let mut doc = make_doc("version = 1");
        let target = make_target("version = 2");
        sync_table(doc.as_table_mut(), &target);
        assert_eq!(doc["version"].as_integer(), Some(2));
    }

    #[test]
    fn sync_table_preserves_unchanged_value() {
        let mut doc = make_doc(r#"name = "shine" # important comment"#);
        let target = make_target(r#"name = "shine""#);
        let before = doc.to_string();
        sync_table(doc.as_table_mut(), &target);
        assert_eq!(doc.to_string(), before);
    }

    #[test]
    fn sync_table_updates_inline_env_value_and_preserves_description() {
        let mut doc = make_doc(
            r#"[env]
TOKEN = { value = "old", description = "API token" }
"#,
        );
        let target = make_target("[env]\nTOKEN = \"new\"\n");

        sync_table(doc.as_table_mut(), &target);

        let inline = doc["env"]["TOKEN"].as_inline_table().unwrap();
        assert_eq!(inline["value"].as_str(), Some("new"));
        assert_eq!(inline["description"].as_str(), Some("API token"));
    }

    #[test]
    fn sync_table_recurses_into_nested_tables() {
        let mut doc = make_doc("[db]\nhost = \"old\"\nport = 5432\n");
        let target = make_target("[db]\nhost = \"new\"\nport = 5432\n");
        sync_table(doc.as_table_mut(), &target);
        assert_eq!(doc["db"]["host"].as_str(), Some("new"));
        assert_eq!(doc["db"]["port"].as_integer(), Some(5432));
    }

    #[test]
    fn sync_table_removes_nested_stale_keys() {
        let mut doc = make_doc("[section]\nkeep = 1\nstale = 2\n");
        let target = make_target("[section]\nkeep = 1\n");
        sync_table(doc.as_table_mut(), &target);
        assert!(doc["section"].get("stale").is_none());
        assert_eq!(doc["section"]["keep"].as_integer(), Some(1));
    }

    #[test]
    fn sync_table_empty_target_clears_doc() {
        let mut doc = make_doc("a = 1\nb = 2\n");
        let target = make_target("");
        sync_table(doc.as_table_mut(), &target);
        assert!(doc.get("a").is_none());
        assert!(doc.get("b").is_none());
    }

    #[test]
    fn sync_table_empty_doc_fills_from_target() {
        let mut doc = make_doc("");
        let target = make_target("x = true\ny = 3.14\n");
        sync_table(doc.as_table_mut(), &target);
        assert_eq!(doc["x"].as_bool(), Some(true));
    }

    #[test]
    fn test_values_equal() {
        use toml::Value as TargetValue;
        use toml_edit::Value as EditValue;

        assert!(values_equal(
            &EditValue::from("hello"),
            &TargetValue::from("hello")
        ));
        assert!(values_equal(
            &EditValue::from(42i64),
            &TargetValue::from(42i64)
        ));
        assert!(values_equal(
            &EditValue::from(2.5f64),
            &TargetValue::from(2.5f64)
        ));
        assert!(values_equal(
            &EditValue::from(true),
            &TargetValue::from(true)
        ));

        let edit_array = EditValue::Array(
            vec![
                EditValue::from("a"),
                EditValue::from(1i64),
                EditValue::from(false),
            ]
            .into_iter()
            .collect(),
        );
        let target_array = TargetValue::Array(vec![
            TargetValue::from("a"),
            TargetValue::from(1i64),
            TargetValue::from(false),
        ]);

        assert!(values_equal(&edit_array, &target_array));

        let different_string = TargetValue::from("world");
        assert!(!values_equal(&EditValue::from("hello"), &different_string));

        let different_int = TargetValue::from(7i64);
        assert!(!values_equal(&EditValue::from(42i64), &different_int));

        let different_type = TargetValue::from(42i64);
        assert!(!values_equal(&EditValue::from("42"), &different_type));

        let shorter_array =
            TargetValue::Array(vec![TargetValue::from("a"), TargetValue::from(1i64)]);
        assert!(!values_equal(&edit_array, &shorter_array));

        let nested_edit_array = EditValue::Array(
            vec![
                EditValue::Array(vec![EditValue::from("x")].into_iter().collect()),
                EditValue::from(false),
            ]
            .into_iter()
            .collect(),
        );
        let nested_target_array = TargetValue::Array(vec![
            TargetValue::Array(vec![TargetValue::from("x")]),
            TargetValue::from(false),
        ]);
        assert!(values_equal(&nested_edit_array, &nested_target_array));
    }
}