microsandbox-types-macros 0.6.17

Derive macros for microsandbox shared contract types.
Documentation
use std::collections::BTreeMap;

use microsandbox_types_macros::ConfigPatch;

#[derive(Debug, Clone, Default, PartialEq, ConfigPatch)]
struct Inner {
    scalar: u8,
    nullable: Option<String>,
}

#[derive(Debug, Clone, Default, PartialEq, ConfigPatch)]
struct Outer {
    #[config_patch(nested)]
    inner: Inner,
    values: Vec<u8>,
    optional: Option<u32>,
    future_field: bool,
}

#[derive(Debug, Clone, Default, PartialEq, ConfigPatch)]
struct OptionalOuter {
    #[config_patch(nested)]
    inner: Option<Inner>,
}

#[derive(Debug, Clone, Default, PartialEq, ConfigPatch)]
#[config_patch(name = RenamedPatch)]
struct Renamed {
    value: u8,
}

#[derive(Debug, Clone, Default, PartialEq, ConfigPatch)]
struct Collections {
    #[config_patch(merge)]
    values: Vec<u8>,
    #[config_patch(merge)]
    optional_values: Option<Vec<u8>>,
    #[config_patch(merge)]
    labels: BTreeMap<String, String>,
    #[config_patch(merge_with = merge_unique)]
    unique_values: Vec<u8>,
}

fn merge_unique(base: &mut Vec<u8>, higher: Vec<u8>) {
    for value in higher {
        if !base.contains(&value) {
            base.push(value);
        }
    }
}

#[test]
fn generated_patch_preserves_presence_and_recurses() {
    let mut target = Outer {
        inner: Inner {
            scalar: 1,
            nullable: Some("inherited".into()),
        },
        values: vec![1, 2],
        optional: Some(7),
        future_field: false,
    };
    let source = Outer {
        inner: Inner {
            scalar: 3,
            nullable: None,
        },
        values: vec![9],
        optional: None,
        future_field: true,
    };

    OuterPatch::from_present_fields(source).apply_to(&mut target);

    assert_eq!(target.inner.scalar, 3);
    assert_eq!(target.inner.nullable.as_deref(), Some("inherited"));
    assert_eq!(target.values, vec![9]);
    assert_eq!(target.optional, Some(7));
    assert!(target.future_field);
}

#[test]
fn clear_removes_nullable_changes_from_the_patch() {
    let mut target = Outer {
        inner: Inner {
            scalar: 3,
            nullable: Some("inherited".into()),
        },
        optional: Some(7),
        ..Default::default()
    };

    OuterPatch::new()
        .inner(
            InnerPatch::new()
                .scalar(9)
                .clear_scalar()
                .nullable("higher".into())
                .clear_nullable(),
        )
        .optional(9)
        .clear_optional()
        .future_field(true)
        .clear_future_field()
        .apply_to(&mut target);

    assert_eq!(target.inner.scalar, 3);
    assert_eq!(target.inner.nullable.as_deref(), Some("inherited"));
    assert_eq!(target.optional, Some(7));
    assert!(!target.future_field);
}

#[test]
fn optional_nested_patches_modify_and_clear_pending_changes() {
    let mut target = OptionalOuter {
        inner: Some(Inner {
            scalar: 1,
            nullable: Some("inherited".into()),
        }),
    };

    OptionalOuterPatch::new()
        .modify_inner(|inner| inner.scalar(2))
        .apply_to(&mut target);
    assert_eq!(
        target.inner,
        Some(Inner {
            scalar: 2,
            nullable: Some("inherited".into()),
        })
    );

    OptionalOuterPatch::new()
        .modify_inner(|inner| inner.scalar(9))
        .clear_inner()
        .apply_to(&mut target);
    assert_eq!(target.inner.as_ref().unwrap().scalar, 2);

    target.inner = Some(Inner {
        scalar: 7,
        nullable: Some("must survive".into()),
    });
    OptionalOuterPatch::new()
        .clear_inner()
        .overlay(OptionalOuterPatch::new().modify_inner(|inner| inner.scalar(4)))
        .apply_to(&mut target);
    assert_eq!(
        target.inner,
        Some(Inner {
            scalar: 4,
            nullable: Some("must survive".into()),
        })
    );

    OptionalOuterPatch::from_present_fields(OptionalOuter::default()).apply_to(&mut target);
    assert!(target.inner.is_some());
}

#[test]
fn generated_patch_name_can_be_overridden() {
    let mut target = Renamed::default();
    RenamedPatch::new().value(7).apply_to(&mut target);
    assert_eq!(target.value, 7);
}

#[test]
fn collection_fields_merge_replace_and_clear() {
    let mut target = Collections {
        values: vec![1],
        optional_values: Some(vec![1]),
        labels: BTreeMap::from([("shared".into(), "lower".into())]),
        unique_values: vec![1],
    };

    CollectionsPatch::new()
        .values(vec![2])
        .values(vec![3])
        .optional_values(vec![2])
        .labels(BTreeMap::from([
            ("shared".into(), "higher".into()),
            ("new".into(), "value".into()),
        ]))
        .unique_values(vec![1, 2])
        .apply_to(&mut target);

    assert_eq!(target.values, [1, 2, 3]);
    assert_eq!(target.optional_values.as_deref(), Some([1, 2].as_slice()));
    assert_eq!(target.labels["shared"], "higher");
    assert_eq!(target.labels["new"], "value");
    assert_eq!(target.unique_values, [1, 2]);

    CollectionsPatch::new()
        .replace_values(vec![9])
        .optional_values(vec![9])
        .clear_optional_values()
        .labels(BTreeMap::from([("ignored".into(), "value".into())]))
        .clear_labels()
        .apply_to(&mut target);

    assert_eq!(target.values, [9]);
    assert_eq!(target.optional_values.as_deref(), Some([1, 2].as_slice()));
    assert_eq!(target.labels["shared"], "higher");
    assert_eq!(target.labels["new"], "value");
}

#[test]
fn collection_patch_overlay_preserves_replace_and_merge_order() {
    let mut target = Collections {
        values: vec![0],
        ..Default::default()
    };

    CollectionsPatch::new()
        .replace_values(vec![1])
        .overlay(CollectionsPatch::new().values(vec![2]))
        .apply_to(&mut target);
    assert_eq!(target.values, [1, 2]);

    CollectionsPatch::new()
        .values(vec![3])
        .overlay(CollectionsPatch::new().replace_values(vec![4]))
        .apply_to(&mut target);
    assert_eq!(target.values, [4]);
}