structupdate 0.4.0

A library for defining complex updatable datastructures
Documentation
use serde::Deserialize;
use serde::Serialize;
use structupdate::Node;
use structupdate::OptRecord;
use structupdate::OptValue;
use structupdate::RecordMap;
use structupdate::Value;
use structupdate::ValueList;
use structupdate::ValueMap;
use structupdate::ValueSet;
use structupdate::structupdate;

fn default_timeout() -> u32 {
    30
}

#[structupdate]
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
struct Inner {
    value: Value<u32>,
    label: Value<String>,
}

#[structupdate]
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
struct Config {
    name: Value<String>,
    count: Value<u32>,
    description: OptValue<String>,
    tags: ValueSet<String>,
    items: ValueList<i32>,
    settings: ValueMap<String, bool>,
    nested: Inner,
    optional: OptRecord<Inner>,
    records: RecordMap<u64, Inner>,
}

/// Config without RecordMap for update serde test (RecordMapUpdates has serde bug)
#[structupdate]
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
struct SimpleConfig {
    name: Value<String>,
    count: Value<u32>,
    description: OptValue<String>,
    tags: ValueSet<String>,
    items: ValueList<i32>,
    settings: ValueMap<String, bool>,
    nested: Inner,
    optional: OptRecord<Inner>,
}

#[test]
fn serialize_roundtrip() {
    let mut config = Config::default();
    let mut update = Config::build_update();
    update
        .name_set("test".into())
        .count_set(42)
        .description_set("a config".into())
        .tags_add("important".into())
        .items_append(1)
        .items_append(2)
        .settings_set("enabled".into(), true)
        .nested_amend(|n| n.value_set(100).label_set("nested".into()))
        .optional_amend(|o| o.value_set(200))
        .records_amend(1, |r| r.value_set(300).label_set("first".into()));
    config.apply_update(update);

    let json = serde_json::to_string_pretty(&config).unwrap();
    assert_eq!(
        json,
        r#"{
  "name": "test",
  "count": 42,
  "description": "a config",
  "tags": [
    "important"
  ],
  "items": [
    1,
    2
  ],
  "settings": {
    "enabled": true
  },
  "nested": {
    "value": 100,
    "label": "nested"
  },
  "optional": {
    "value": 200,
    "label": ""
  },
  "records": {
    "1": {
      "value": 300,
      "label": "first"
    }
  }
}"#
    );

    let parsed: Config = serde_json::from_str(&json).unwrap();
    assert_eq!(config, parsed);
}

#[test]
fn serialize_update() {
    let mut update = SimpleConfig::build_update();
    update
        .name_set("hello".into())
        .count_set(10)
        .description_set("desc".into())
        .tags_add("tag1".into())
        .items_append(42)
        .settings_set("key".into(), false)
        .nested_amend(|n| n.value_set(5))
        .optional_amend(|o| o.label_set("opt".into()));

    let json = serde_json::to_string(&update).unwrap();
    let parsed: SimpleConfigUpdate = serde_json::from_str(&json).unwrap();

    let mut c1 = SimpleConfig::default();
    let mut c2 = SimpleConfig::default();
    c1.apply_update(update);
    c2.apply_update(parsed);
    assert_eq!(c1, c2);
}

#[structupdate]
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
struct WithCustomDefault {
    #[structupdate(init_with = default_timeout)]
    timeout: Value<u32>,
    name: Value<String>,
}

#[test]
fn custom_default_preserved_after_roundtrip() {
    let mut config = WithCustomDefault::default();
    assert_eq!(*config.timeout.value(), 30); // custom default

    let mut update = WithCustomDefault::build_update();
    update.timeout_set(60).name_set("test".into());
    config.apply_update(update);
    assert_eq!(*config.timeout.value(), 60);

    // roundtrip - now serializes cleanly without the default wrapper
    let json = serde_json::to_string(&config).unwrap();
    assert_eq!(json, r#"{"timeout":60,"name":"test"}"#);

    let mut parsed: WithCustomDefault = serde_json::from_str(&json).unwrap();
    assert_eq!(*parsed.timeout.value(), 60);

    // set_to_default should use custom default
    parsed.timeout.set_to_default();
    assert_eq!(*parsed.timeout.value(), 30);
}

// Tests for missing fields during deserialization

#[test]
fn missing_value_with_init_with_uses_custom_default() {
    // Missing "timeout" should use default_timeout() = 30
    let json = r#"{"name":"test"}"#;
    let parsed: WithCustomDefault = serde_json::from_str(json).unwrap();
    assert_eq!(*parsed.timeout.value(), 30);
    assert_eq!(*parsed.name.value(), "test");
}

#[test]
fn missing_value_without_init_with_uses_type_default() {
    // Missing "name" should use String::default() = ""
    let json = r#"{"timeout":99}"#;
    let parsed: WithCustomDefault = serde_json::from_str(json).unwrap();
    assert_eq!(*parsed.timeout.value(), 99);
    assert_eq!(*parsed.name.value(), "");
}

#[test]
fn all_value_fields_missing_uses_defaults() {
    // Empty object - all Value fields should use defaults
    let json = r#"{}"#;
    let parsed: WithCustomDefault = serde_json::from_str(json).unwrap();
    assert_eq!(*parsed.timeout.value(), 30); // custom default
    assert_eq!(*parsed.name.value(), ""); // type default
}

#[test]
fn set_to_default_after_deserialize_with_custom_default() {
    // Both fields present in JSON
    let json = r#"{"timeout":999,"name":"foo"}"#;
    let mut parsed: WithCustomDefault = serde_json::from_str(json).unwrap();

    assert_eq!(*parsed.timeout.value(), 999);
    assert_eq!(*parsed.name.value(), "foo");

    // set_to_default uses custom default for timeout
    parsed.timeout.set_to_default();
    assert_eq!(*parsed.timeout.value(), 30);

    // set_to_default uses type default for name
    parsed.name.set_to_default();
    assert_eq!(*parsed.name.value(), "");
}

#[test]
fn set_to_default_after_deserialize_missing_field_with_init_with() {
    // timeout missing from JSON - should get custom default AND set_to_default should work
    let json = r#"{"name":"bar"}"#;
    let mut parsed: WithCustomDefault = serde_json::from_str(json).unwrap();

    // Initial value should be custom default
    assert_eq!(*parsed.timeout.value(), 30);

    // Modify it
    let mut update = WithCustomDefault::build_update();
    update.timeout_set(100);
    parsed.apply_update(update);
    assert_eq!(*parsed.timeout.value(), 100);

    // set_to_default should still use custom default
    parsed.timeout.set_to_default();
    assert_eq!(*parsed.timeout.value(), 30);
}

#[test]
fn nested_record_missing_value_fields() {
    // Inner's value and label should use their defaults when missing
    let json = r#"{"value":0,"label":""}"#;
    let parsed: Inner = serde_json::from_str(json).unwrap();
    assert_eq!(*parsed.value.value(), 0);
    assert_eq!(*parsed.label.value(), "");
}

#[test]
fn nested_record_all_fields_missing() {
    let json = r#"{}"#;
    let parsed: Inner = serde_json::from_str(json).unwrap();
    assert_eq!(*parsed.value.value(), 0); // u32 default
    assert_eq!(*parsed.label.value(), ""); // String default
}

#[test]
fn missing_optional_fields_deserialize_to_defaults() {
    // OptValue, OptRecord, ValueSet, ValueMap, RecordMap, Record all missing
    let json = r#"{"name":"test","count":1,"items":[]}"#;
    let parsed: Config = serde_json::from_str(json).unwrap();
    assert!(parsed.description.is_empty());
    assert!(parsed.optional.is_empty());
    assert!(parsed.tags.is_empty());
    assert!(parsed.settings.is_empty());
    assert!(parsed.records.is_empty());
    assert_eq!(parsed.nested, Inner::default());
}