isoloom-spec 0.11.0

The Isoloom spec: model, parsing, normalization and validation.
Documentation
use serde_json::json;

use super::*;

/// A schema with each shape the walk meets: a map of definitions, a list of mappings, a field
/// that is a scalar or a mapping, a closed set, limits, and a shallow field.
fn schema() -> Value {
    json!({
        "properties": {
            "version": { "type": "integer", "minimum": 1, "maximum": 1, "examples": [1] },
            "name": { "type": "string", "pattern": "^[a-z]+$", "description": "The name,\nwrapped." },
            "port": { "type": "integer", "format": "uint16", "minimum": 0, "maximum": 65535 },
            "small": { "type": "integer", "minimum": 2 },
            "big": { "type": "integer", "maximum": 9 },
            "items": { "type": "array" },
            "free": {},
            "nets": { "type": "object", "additionalProperties": false, "patternProperties": { "^[a-z]+$": { "$ref": "#/$defs/Net" } }, "propertyNames": { "pattern": "^[a-z]+$" }, "default": {} },
            "list": { "type": "array", "items": { "$ref": "#/$defs/Item" }, "default": [] },
            "gpus": { "$ref": "#/$defs/Gpus", "description": "Overrides [`Gpus`]." },
            "arch": { "$ref": "#/$defs/Arch", "default": "amd64" },
            "os": { "type": "string", "enum": ["a", "b"] },
            "vols": { "type": "object", "additionalProperties": { "type": "string", "pattern": "^/" } },
            "common": { "$ref": "#/$defs/Shared" },
            "loose": { "properties": { "x": { "type": "boolean", "default": false } } },
            "groups": { "type": "object", "additionalProperties": { "$ref": "#/$defs/Shared" } }
        },
        "$defs": {
            "Net": { "type": "object", "properties": { "cidr": { "type": "string" } } },
            "Item": { "anyOf": [{ "type": "string" }, { "type": "object", "properties": { "port": { "type": "integer" } } }] },
            "Gpus": { "anyOf": [{ "type": "integer" }, { "$ref": "#/$defs/Full" }], "description": "GPUs." },
            "Full": { "type": "object", "properties": { "count": { "type": "integer" } } },
            "Arch": { "oneOf": [{ "const": "amd64", "description": "x86-64." }, { "const": "arm64", "description": "ARM." }] },
            "Shared": { "type": "object", "properties": { "arch": {}, "dns": {} } }
        }
    })
}

fn field<'a>(all: &'a [Field], path: &str) -> &'a Field {
    all.iter().find(|f| f.path == path).unwrap_or_else(|| panic!("no {path}"))
}

#[test]
fn every_shape_is_walked_with_its_type_in_words() {
    let all = fields(&schema());
    let paths: Vec<&str> = all.iter().map(|f| f.path.as_str()).collect();
    assert_eq!(
        paths,
        [
            "version",
            "name",
            "port",
            "small",
            "big",
            "items",
            "free",
            "nets",
            "nets.*.cidr",
            "list",
            "list[].port",
            "gpus",
            "gpus.count",
            "arch",
            "os",
            "vols",
            "common",
            "loose",
            "loose.x",
            "groups"
        ]
    );
    let kinds: Vec<&str> = all.iter().map(|f| f.kind.as_str()).collect();
    assert_eq!(
        kinds,
        [
            "integer",
            "string",
            "integer",
            "integer",
            "integer",
            "list of values",
            "value",
            "map of name to mapping",
            "string",
            "list of strings or mappings",
            "integer",
            "integer or mapping",
            "integer",
            "string",
            "string",
            "map of name to string",
            "mapping",
            "mapping",
            "boolean",
            "map of name to mapping"
        ]
    );
}

#[test]
fn defaults_values_limits_and_descriptions_read_as_a_spec_writes_them() {
    let all = fields(&schema());
    let version = field(&all, "version");
    assert_eq!(
        (version.limits.clone(), version.examples.clone()),
        (vec!["from 1 to 1".to_owned()], vec!["1".to_owned()])
    );
    let name = field(&all, "name");
    assert_eq!(
        (name.description.as_str(), name.limits[0].as_str()),
        ("The name, wrapped.", "matches `^[a-z]+$`")
    );
    assert!(field(&all, "port").limits.is_empty(), "a range that is only the format's");
    assert_eq!(field(&all, "small").limits, ["at least 2"]);
    assert_eq!(field(&all, "big").limits, ["at most 9"]);
    assert_eq!(field(&all, "nets").limits, ["names match `^[a-z]+$`"]);
    assert_eq!(field(&all, "vols").limits, ["each value matches `^/`"]);
    assert_eq!((field(&all, "nets").default.clone(), field(&all, "list").default.clone()), (None, None));
    assert_eq!(field(&all, "loose.x").default.as_deref(), Some("false"));
}

#[test]
fn closed_sets_shallow_fields_and_rustdoc_links_read_plainly() {
    let all = fields(&schema());
    let arch = field(&all, "arch");
    assert_eq!(arch.default.as_deref(), Some("amd64"));
    assert_eq!(
        arch.allowed,
        [("amd64".to_owned(), "x86-64.".to_owned()), ("arm64".to_owned(), "ARM.".to_owned())]
    );
    assert_eq!(field(&all, "os").allowed, [("a".to_owned(), String::new()), ("b".to_owned(), String::new())]);
    assert_eq!(field(&all, "gpus").description, "Overrides `Gpus`.");
    assert_eq!(field(&all, "common").members, ["arch", "dns"]);
    assert_eq!(field(&all, "groups").members, ["arch", "dns"]);
}

#[test]
fn a_schema_that_refers_to_itself_stops() {
    let looping = json!({
        "properties": { "a": { "$ref": "#/$defs/A" }, "b": { "$ref": "#/$defs/Missing" } },
        "$defs": { "A": { "type": "object", "properties": { "a": { "$ref": "#/$defs/A" } } } }
    });
    let all = fields(&looping);
    assert!(all.len() > MAX_DEPTH / 2 && all.len() <= MAX_DEPTH + 1, "{}", all.len());
    assert_eq!(field(&all, "b").kind, "value");
    let alias = json!({ "properties": { "x": { "$ref": "#/$defs/X" } }, "$defs": { "X": { "$ref": "#/$defs/X" } } });
    assert_eq!(fields(&alias)[0].kind, "value");
    // Alternatives that hold themselves: the walk through them stops too.
    let alternatives = json!({
        "properties": { "x": { "$ref": "#/$defs/X" } },
        "$defs": { "X": { "anyOf": [{ "$ref": "#/$defs/X" }, { "type": "string" }] } }
    });
    assert_eq!(fields(&alternatives).len(), 1);
    // A list of itself: its type in words stops at the depth the walk does.
    let lists = json!({
        "properties": { "x": { "$ref": "#/$defs/X" } },
        "$defs": { "X": { "type": "array", "items": { "$ref": "#/$defs/X" } } }
    });
    assert_eq!(fields(&lists)[0].kind, format!("list of {}values", "lists of ".repeat(MAX_DEPTH - 2)));
}

#[test]
fn a_closed_set_nested_past_the_walks_depth_is_not_listed() {
    let allowed = |wrappers: usize| {
        let nested = (0..wrappers).fold(json!({ "const": "a" }), |inner, _| json!({ "anyOf": [inner] }));
        fields(&json!({ "properties": { "x": nested } }))[0].allowed.clone()
    };
    assert_eq!(allowed(MAX_DEPTH), [("a".to_owned(), String::new())]);
    assert_eq!(allowed(MAX_DEPTH + 1), Vec::<(String, String)>::new());
}

#[test]
fn alternatives_of_alternatives_of_constants_are_one_closed_set() {
    let tiers = json!({ "properties": { "tier": { "anyOf": [{ "anyOf": [{ "const": "a" }, { "const": "b" }] }, { "const": "c" }] } } });
    let all = fields(&tiers);
    assert_eq!(all[0].kind, "string");
    let values: Vec<&str> = all[0].allowed.iter().map(|(v, _)| v.as_str()).collect();
    assert_eq!(values, ["a", "b", "c"]);
}

#[test]
fn a_closed_set_of_fractions_is_numbers() {
    let numbers = json!({ "properties": { "loss": { "enum": [0.5, 1.5] }, "level": { "enum": [1, 2] } } });
    let kinds: Vec<String> = fields(&numbers).into_iter().map(|f| f.kind).collect();
    assert_eq!(kinds, ["number", "integer"]);
}

#[test]
fn a_null_or_empty_text_default_is_not_shown() {
    let defaults = json!({ "properties": { "a": { "default": null }, "b": { "default": "" }, "c": { "default": 0 } } });
    let shown: Vec<Option<String>> = fields(&defaults).into_iter().map(|f| f.default).collect();
    assert_eq!(shown, [None, None, Some("0".to_owned())]);
}

#[test]
fn headings_get_github_anchors() {
    assert_eq!(slug("From the spec's side"), "from-the-specs-side");
    assert_eq!(slug("`machines.*.vm.os`"), "machinesvmos");
    assert_eq!(slug("Top level"), "top-level");
    assert_eq!(slug("`ip_forward` and `rate-limit`"), "ip_forward-and-rate-limit");
}

#[test]
fn a_closed_sets_type_is_its_first_values() {
    let kind = |set: Value| fields(&json!({ "properties": { "x": set } }))[0].kind.clone();
    assert_eq!(kind(json!({ "const": true })), "boolean");
    assert_eq!(kind(json!({ "enum": [1, 2] })), "integer");
    assert_eq!(kind(json!({ "enum": [0.5] })), "number");
    assert_eq!(kind(json!({ "anyOf": [{ "const": "a" }, { "const": 1 }] })), "string");
    assert_eq!(kind(json!({ "const": null })), "value");
}