type-history-codegen 0.3.3

Shared type history parsing, authority, and Rust generation
Documentation
use serde_json::{json, Value};
use type_history_core::resolved::{
    FieldPresence, Membership, SchemaField, SchemaShape, SchemaVariant, SchemaVariantShape,
    StorageProfile,
};

use super::{compare_shapes, readable_path, DifferenceCode, PathSegment};

fn field(name: &str, schema: SchemaShape) -> SchemaField {
    SchemaField {
        name: name.into(),
        presence: FieldPresence::Required,
        schema,
    }
}
fn record(fields: Vec<SchemaField>) -> SchemaShape {
    SchemaShape::Record { fields }
}
fn enumeration(name: &str, shape: SchemaVariantShape) -> SchemaShape {
    SchemaShape::Enum {
        variants: vec![SchemaVariant {
            name: name.into(),
            shape,
        }],
    }
}

#[test]
fn every_primitive_kind_change_reports_complete_subtrees() {
    let shapes = [
        SchemaShape::Bool,
        SchemaShape::I8,
        SchemaShape::I16,
        SchemaShape::I32,
        SchemaShape::I64,
        SchemaShape::I128,
        SchemaShape::U8,
        SchemaShape::U16,
        SchemaShape::U32,
        SchemaShape::U64,
        SchemaShape::U128,
        SchemaShape::String,
        SchemaShape::Bytes,
    ];
    for expected in &shapes {
        for actual in &shapes {
            let differences = compare_shapes(expected, actual);
            if expected == actual {
                assert!(differences.is_empty());
                continue;
            }
            assert_eq!(differences.len(), 1);
            let difference = &differences[0];
            assert_eq!(difference.code, DifferenceCode::ShapeKindChanged);
            assert!(difference.path.is_empty());
            assert_eq!(difference.expected, serde_json::to_value(expected).unwrap());
            assert_eq!(difference.actual, serde_json::to_value(actual).unwrap());
            assert!(!difference.hint.is_empty());
        }
    }
}

#[test]
fn nested_changes_keep_full_typed_paths_and_independent_siblings() {
    let nested = |scalar| SchemaShape::Option {
        value: Box::new(SchemaShape::Sequence {
            value: Box::new(SchemaShape::Array {
                length: 3,
                value: Box::new(enumeration(
                    "V",
                    SchemaVariantShape::Tuple {
                        items: vec![scalar],
                    },
                )),
            }),
        }),
    };
    let expected = record(vec![
        field("b", SchemaShape::U32),
        field("a", nested(SchemaShape::Bool)),
    ]);
    let actual = record(vec![
        field("a", nested(SchemaShape::String)),
        field("b", SchemaShape::String),
    ]);
    let differences = compare_shapes(&expected, &actual);
    assert_eq!(differences.len(), 2);
    assert_eq!(
        serde_json::to_value(&differences[0].path).unwrap(),
        json!([
            {"kind":"field","name":"a"},{"kind":"option_value"},{"kind":"sequence_item"},
            {"kind":"array_item"},{"kind":"variant","name":"V"},{"kind":"tuple_item","index":0}
        ])
    );
    assert_eq!(
        readable_path(&differences[0].path),
        "$[\"a\"].option_value.sequence_item.array_item.variant[\"V\"][0]"
    );
    assert_eq!(differences, compare_shapes(&expected.normalized(), &actual));
}

#[test]
fn missing_added_presence_and_nested_field_changes_are_aggregated() {
    let expected = record(vec![
        field("missing", record(vec![field("inner", SchemaShape::Bool)])),
        field("value", SchemaShape::U32),
    ]);
    let actual = record(vec![
        field("added", SchemaShape::Bytes),
        SchemaField {
            name: "value".into(),
            presence: FieldPresence::Optional,
            schema: SchemaShape::Option {
                value: Box::new(SchemaShape::U32),
            },
        },
    ]);
    let differences = compare_shapes(&expected, &actual);
    assert_eq!(
        differences
            .iter()
            .map(|value| value.code)
            .collect::<Vec<_>>(),
        vec![
            DifferenceCode::FieldAdded,
            DifferenceCode::FieldMissing,
            DifferenceCode::FieldPresenceChanged,
            DifferenceCode::ShapeKindChanged
        ]
    );
    assert_eq!(differences[0].expected, Value::Null);
    assert_eq!(differences[1].actual, Value::Null);
    assert_eq!(
        differences[1].expected["schema"]["fields"][0]["name"],
        "inner"
    );
    assert_eq!(differences[2].expected, "required");
    assert_eq!(differences[2].actual, "optional");
}

#[test]
fn array_and_tuple_lengths_do_not_hide_child_changes_or_missing_subtrees() {
    let expected = enumeration(
        "V",
        SchemaVariantShape::Tuple {
            items: vec![
                SchemaShape::Array {
                    length: 2,
                    value: Box::new(SchemaShape::U8),
                },
                SchemaShape::Bool,
            ],
        },
    );
    let actual = enumeration(
        "V",
        SchemaVariantShape::Tuple {
            items: vec![SchemaShape::Array {
                length: 3,
                value: Box::new(SchemaShape::U32),
            }],
        },
    );
    let differences = compare_shapes(&expected, &actual);
    assert_eq!(differences.len(), 4);
    assert_eq!(differences[0].code, DifferenceCode::TupleLengthChanged);
    assert_eq!(differences[0].expected, 2);
    assert_eq!(differences[0].actual, 1);
    assert_eq!(differences[1].code, DifferenceCode::ArrayLengthChanged);
    assert_eq!(differences[2].path.last(), Some(&PathSegment::ArrayItem));
    assert_eq!(differences[3].expected, json!({"kind":"bool"}));
    assert_eq!(differences[3].actual, Value::Null);
    let reversed = compare_shapes(&actual, &expected);
    assert_eq!(reversed[3].expected, Value::Null);
    assert_eq!(reversed[3].actual, json!({"kind":"bool"}));
}

#[test]
fn all_variant_payloads_compare_and_kind_changes_stop_at_the_variant() {
    let payloads = [
        SchemaVariantShape::Unit,
        SchemaVariantShape::Newtype {
            schema: Box::new(SchemaShape::Bool),
        },
        SchemaVariantShape::Tuple {
            items: vec![SchemaShape::Bool],
        },
        SchemaVariantShape::Record {
            fields: vec![field("a", SchemaShape::Bool)],
        },
    ];
    for (a, expected) in payloads.iter().enumerate() {
        for (b, actual) in payloads.iter().enumerate() {
            let differences = compare_shapes(
                &enumeration("V", expected.clone()),
                &enumeration("V", actual.clone()),
            );
            if a == b {
                assert!(differences.is_empty());
            } else {
                assert_eq!(differences.len(), 1);
                assert_eq!(differences[0].code, DifferenceCode::VariantKindChanged);
            }
        }
    }
    for (expected, actual, last) in [
        (
            SchemaVariantShape::Newtype {
                schema: Box::new(SchemaShape::Bool),
            },
            SchemaVariantShape::Newtype {
                schema: Box::new(SchemaShape::String),
            },
            PathSegment::VariantValue,
        ),
        (
            SchemaVariantShape::Record {
                fields: vec![field("a", SchemaShape::Bool)],
            },
            SchemaVariantShape::Record {
                fields: vec![field("a", SchemaShape::String)],
            },
            PathSegment::Field { name: "a".into() },
        ),
    ] {
        let differences = compare_shapes(&enumeration("V", expected), &enumeration("V", actual));
        assert_eq!(differences.len(), 1);
        assert_eq!(differences[0].path.last(), Some(&last));
    }
    let differences = compare_shapes(
        &enumeration("Old", payloads[2].clone()),
        &enumeration("New", payloads[3].clone()),
    );
    assert_eq!(differences[0].code, DifferenceCode::VariantAdded);
    assert_eq!(differences[0].actual["fields"][0]["name"], "a");
    assert_eq!(differences[1].code, DifferenceCode::VariantMissing);
    assert_eq!(differences[1].expected["items"][0], json!({"kind":"bool"}));
}

#[test]
fn incompatible_containers_report_complete_subtrees() {
    let shapes = [
        SchemaShape::Bool,
        record(vec![field("a", SchemaShape::U32)]),
        enumeration("V", SchemaVariantShape::Unit),
        SchemaShape::Option {
            value: Box::new(SchemaShape::U32),
        },
        SchemaShape::Sequence {
            value: Box::new(SchemaShape::U32),
        },
        SchemaShape::Array {
            value: Box::new(SchemaShape::U32),
            length: 2,
        },
        SchemaShape::Map {
            value: Box::new(SchemaShape::U32),
        },
        SchemaShape::Set {
            value: Box::new(SchemaShape::U32),
            membership: membership("example:exact:v1"),
        },
        SchemaShape::Tuple {
            items: vec![SchemaShape::U32],
        },
        SchemaShape::Profile {
            profile: StorageProfile::Finite32,
        },
    ];
    for (a, expected) in shapes.iter().enumerate() {
        for (b, actual) in shapes.iter().enumerate() {
            let differences = compare_shapes(expected, actual);
            if a == b {
                assert!(differences.is_empty());
            } else {
                assert_eq!(differences.len(), 1);
                assert_eq!(differences[0].code, DifferenceCode::ShapeKindChanged);
                assert_eq!(
                    differences[0].expected,
                    serde_json::to_value(expected).unwrap()
                );
            }
        }
    }
}

fn membership(id: &str) -> Membership {
    Membership {
        id: id.to_owned(),
        parameters: vec![],
    }
}

#[test]
fn collection_profile_and_membership_changes_have_precise_paths() {
    let expected = SchemaShape::Map {
        value: Box::new(SchemaShape::Set {
            value: Box::new(SchemaShape::Tuple {
                items: vec![SchemaShape::Profile {
                    profile: StorageProfile::Finite32,
                }],
            }),
            membership: membership("example:exact:v1"),
        }),
    };
    let actual = SchemaShape::Map {
        value: Box::new(SchemaShape::Set {
            value: Box::new(SchemaShape::Tuple {
                items: vec![SchemaShape::Profile {
                    profile: StorageProfile::Finite64,
                }],
            }),
            membership: membership("example:folded:v1"),
        }),
    };
    let differences = compare_shapes(&expected, &actual);
    assert_eq!(differences.len(), 2);
    assert_eq!(differences[0].code, DifferenceCode::MembershipChanged);
    assert_eq!(differences[0].path, vec![PathSegment::MapValue]);
    assert_eq!(differences[0].expected["id"], "example:exact:v1");
    assert_eq!(differences[1].code, DifferenceCode::ProfileChanged);
    assert_eq!(
        differences[1].path,
        vec![
            PathSegment::MapValue,
            PathSegment::SetItem,
            PathSegment::TupleItem { index: 0 },
        ]
    );
    assert_eq!(differences[1].expected, "type-history:finite32:v1");
    assert_eq!(
        readable_path(&differences[1].path),
        "$.map_value.set_item[0]"
    );
}

#[test]
fn readable_names_and_codes_have_stable_unambiguous_spellings() {
    let path = [PathSegment::Field {
        name: "a.b\"\\🦀".into(),
    }];
    assert_eq!(readable_path(&path), "$[\"a.b\\\"\\\\🦀\"]");
    for code in [
        DifferenceCode::FieldMissing,
        DifferenceCode::FieldAdded,
        DifferenceCode::FieldPresenceChanged,
        DifferenceCode::ShapeKindChanged,
        DifferenceCode::ArrayLengthChanged,
        DifferenceCode::MembershipChanged,
        DifferenceCode::ProfileChanged,
        DifferenceCode::VariantMissing,
        DifferenceCode::VariantAdded,
        DifferenceCode::VariantKindChanged,
        DifferenceCode::TupleLengthChanged,
    ] {
        assert_eq!(serde_json::to_value(code).unwrap(), code.as_str());
    }
}