mant-ir 0.11.0

Normalized, source-neutral document intermediate representation for ManT
Documentation
//! Actual decoding guards, independent of generated schema snapshots.
use crate::{Block, DefinitionItem, EntryFacts, EntryKind, ListItem, SemanticEntry};
use serde::{Serialize, de::DeserializeOwned};
use serde_json::{Value, json};

fn cases() -> Value {
    serde_json::from_str(include_str!("convergence-wire.json")).unwrap()
}

fn definition() -> Value {
    cases()["definition"]["new"].clone()
}

fn reject_fields<T: DeserializeOwned>(value: &Value, fields: &Value) {
    for field in fields.as_array().unwrap() {
        let mut invalid = value.clone();
        invalid[field.as_str().unwrap()] = Value::Null;
        assert!(
            serde_json::from_value::<T>(invalid.clone()).is_err(),
            "{invalid}"
        );
    }
}

fn roundtrip<T: DeserializeOwned + Serialize>(value: Value) -> Value {
    let decoded: T = serde_json::from_value(value).unwrap();
    let canonical = serde_json::to_value(decoded).unwrap();
    let _: T = serde_json::from_value(canonical.clone()).unwrap();
    canonical
}

#[test]
fn owners_reject_old_and_unknown_fields_including_mixed_shapes() {
    let definition = definition();
    assert!(
        serde_json::from_value::<DefinitionItem>(cases()["definition"]["old"].clone()).is_err()
    );
    reject_fields::<DefinitionItem>(&definition, &cases()["definition"]["rejectFields"]);
    reject_fields::<ListItem>(
        &cases()["listItem"]["new"],
        &cases()["listItem"]["rejectFields"],
    );
    for entry in [None, Some(Value::Null), Some(definition["entry"].clone())] {
        let mut list = json!({"blocks": [], "source": null});
        if let Some(entry) = entry {
            list["entry"] = entry;
        }
        let canonical = roundtrip::<ListItem>(list);
        assert!(!canonical.as_object().unwrap().contains_key("source"));
        assert!(canonical.get("entry").is_none_or(Value::is_object));
    }
    let mut block = json!({"type": "definition-list", "items": [definition]});
    let _ = roundtrip::<Block>(block.clone());
    block["items"][0]["identity"] = Value::Null;
    assert!(serde_json::from_value::<Block>(block).is_err());
    assert!(
        serde_json::from_str::<ListItem>(r#"{"blocks":[],"entry":null,"entry":null}"#).is_err()
    );
    assert!(
        serde_json::from_str::<DefinitionItem>(
            r#"{"terms":[],"description":[],"source":null,"source":null}"#
        )
        .is_err()
    );
}

#[test]
fn facts_are_closed_but_content_references_are_semantically_validated() {
    let facts = definition()["entry"].clone();
    reject_fields::<EntryFacts>(&facts, &cases()["facts"]["rejectFields"]);
    for invalid in [Value::Null, json!({}), json!("implicit")] {
        let mut value = facts.clone();
        value["forms"] = invalid;
        assert!(serde_json::from_value::<EntryFacts>(value).is_err());
    }
    for forms in [None, Some(json!([]))] {
        let mut value = facts.clone();
        value.as_object_mut().unwrap().remove("forms");
        if let Some(forms) = forms {
            value["forms"] = forms;
        }
        assert!(
            serde_json::from_value::<EntryFacts>(value)
                .unwrap()
                .forms
                .is_empty()
        );
    }
    let mut bad_reference = facts.clone();
    bad_reference["forms"][0]["parts"][0]["root"]["index"] = json!(999);
    let mut owner: DefinitionItem = serde_json::from_value(definition()).unwrap();
    owner.entry = Some(serde_json::from_value(bad_reference).unwrap());
    assert!(crate::EntryOwner::Definition(&owner).forms().is_none());
    for kind in [
        json!("option"),
        json!({"kind":"command","unknown":null}),
        json!({"kind":"parameter","parameterKind":"wrong"}),
    ] {
        assert!(serde_json::from_value::<EntryKind>(kind).is_err());
    }
    let raw = serde_json::to_string(&facts).unwrap();
    let duplicate = raw.replacen('{', "{\"case\":\"sensitive\",", 1);
    assert!(serde_json::from_str::<EntryFacts>(&duplicate).is_err());
}

#[test]
fn derived_names_are_not_a_second_alias_vocabulary() {
    let entry = json!({"id":"example", "kind":{"kind":"command"},
        "case":"sensitive", "names":["example"], "forms":["example"],
        "aliasGroups":[["example"]], "aliasOf":"other"});
    reject_fields::<SemanticEntry>(&entry, &cases()["semanticEntry"]["rejectFields"]);
    let canonical = roundtrip::<SemanticEntry>(entry);
    assert_eq!(canonical["names"], json!(["example"]));
    assert_eq!(canonical["aliasOf"], "other");
    assert!(serde_json::from_str::<SemanticEntry>(r#"{"id":"x","kind":{"kind":"term"},"case":"sensitive","forms":[],"names":[],"names":[]}"#).is_err());
}

#[test]
fn definition_layout_keeps_inherited_and_explicit_zero_spacing_distinct() {
    use crate::DefinitionLayout;
    for value in cases()["layout"]["accept"].as_array().unwrap() {
        let layout: DefinitionLayout = serde_json::from_value(value.clone()).unwrap();
        let canonical = serde_json::to_value(layout).unwrap();
        assert_eq!(
            canonical.get("spacingBeforeLines"),
            value.get("spacingBeforeLines").filter(|v| !v.is_null())
        );
        let mut item = definition();
        item["layout"] = value.clone();
        let output = roundtrip::<DefinitionItem>(item);
        assert_eq!(output.get("layout").is_none(), layout.is_empty());
    }
    for value in cases()["layout"]["reject"].as_array().unwrap() {
        assert!(serde_json::from_value::<DefinitionLayout>(value.clone()).is_err());
        let mut item = definition();
        item["layout"] = value.clone();
        assert!(serde_json::from_value::<DefinitionItem>(item).is_err());
    }
    for invalid in [
        r#"{"inlineTerm":true,"inlineTerm":false}"#,
        r#"{"spacingBeforeLines":null,"spacingBeforeLines":0}"#,
        r#"{"spacingBeforeLines":-1}"#,
        r#"{"inlineTerm":null}"#,
    ] {
        assert!(
            serde_json::from_str::<DefinitionLayout>(invalid).is_err(),
            "{invalid}"
        );
    }
    let inherited: DefinitionItem =
        serde_json::from_value(json!({"terms":[],"description":[]})).unwrap();
    assert_eq!(inherited.layout, DefinitionLayout::default());
}

#[test]
fn only_ordered_lists_accept_start_in_actual_decoders() {
    use crate::ListKind;
    for value in cases()["listKind"]["accept"].as_array().unwrap() {
        let canonical = roundtrip::<ListKind>(value.clone());
        assert!(canonical.get("start").is_none_or(|start| !start.is_null()));
        let block = json!({"type":"list", "kind":value, "items":[]});
        let _ = roundtrip::<Block>(block.clone());
        let mut legacy = block;
        legacy["start"] = Value::Null;
        assert!(serde_json::from_value::<Block>(legacy).is_err());
    }
    for invalid in cases()["listKind"]["reject"].as_array().unwrap() {
        assert!(
            serde_json::from_value::<ListKind>(invalid.clone()).is_err(),
            "{invalid}"
        );
        assert!(
            serde_json::from_value::<Block>(json!({"type":"list","kind":invalid,"items":[]}))
                .is_err()
        );
    }
    for raw in cases()["listKind"]["rejectRaw"].as_array().unwrap() {
        assert!(
            serde_json::from_str::<ListKind>(raw.as_str().unwrap()).is_err(),
            "{raw}"
        );
    }
    for start in [None, Some(0), Some(7), Some(u64::MAX)] {
        let kind = ListKind::Ordered { start };
        for offset in [0, 1, 3, usize::MAX] {
            let expected = start
                .unwrap_or(1)
                .saturating_add(u64::try_from(offset).unwrap_or(u64::MAX));
            assert_eq!(kind.ordinal(offset), Some(expected));
            assert_eq!(
                kind.for_excerpt(offset),
                ListKind::Ordered {
                    start: Some(expected)
                }
            );
            assert_eq!(kind, ListKind::Ordered { start });
        }
    }
    for kind in [ListKind::Bullet, ListKind::Plain] {
        assert_eq!(kind.ordinal(0), None);
        assert_eq!(kind.for_excerpt(8), kind);
    }
}