type-history-codegen 0.3.2

Shared type history parsing, authority, and Rust generation
Documentation
use super::{
    HistoryLedger, HistoryReadiness, LedgerAuthority, LedgerError, SchemaIdentity, Snapshot,
};
use crate::history::HistoryPlan;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::fs;
use std::path::Path;

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
enum Role {
    Creation,
    Update,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct Metadata {
    role: Role,
}

const RELATIVE_PATH: &str = "type-history/schemas.json";
fn parse(bytes: &[u8]) -> Result<HistoryLedger<Metadata>, String> {
    HistoryLedger::parse(bytes, SchemaIdentity::new("urn:typehistory:schema:"))
}
fn from_entries(
    entries: BTreeMap<String, BTreeMap<u32, Snapshot<Metadata>>>,
) -> Result<HistoryLedger<Metadata>, String> {
    HistoryLedger::from_entries(entries, SchemaIdentity::new("urn:typehistory:schema:"))
}
fn read(root: &Path) -> Result<HistoryLedger<Metadata>, LedgerError> {
    HistoryLedger::read_file(
        &root.join(RELATIVE_PATH),
        SchemaIdentity::new("urn:typehistory:schema:"),
    )
}

#[test]
fn nested_duplicate_schema_properties_are_rejected() {
    let bytes = ledger("a.events.registered", &[1]).replace(
        "\"type\":\"string\"",
        "\"type\":\"string\",\"type\":\"string\"",
    );
    let error = parse(bytes.as_bytes()).unwrap_err();
    assert!(error.contains("duplicate JSON key `type`"), "{error}");
}

#[test]
fn ledger_stable_name_and_source_role_remain_authoritative() {
    let stable_name = "a.events.registered";
    let invalid = ledger(stable_name, &[1]).replace(
        "urn:typehistory:schema:a.events.registered",
        "urn:typehistory:schema:a.events.different",
    );
    assert!(parse(invalid.as_bytes()).unwrap_err().contains("$id"));
    let baseline = parse(ledger(stable_name, &[1]).as_bytes()).unwrap();
    assert!(baseline
        .authorize_history(
            stable_name,
            &Metadata { role: Role::Update },
            HistoryPlan { head: 1 }
        )
        .unwrap_err()
        .contains("metadata"));
}

#[test]
fn resets_keep_the_latest_reservation_and_block_successors() {
    let stable_name = "a.events.registered";
    let baseline = parse(ledger(stable_name, &[1, 2]).as_bytes()).unwrap();
    let mut entries = baseline.entries().clone();
    entries
        .get_mut(stable_name)
        .unwrap()
        .get_mut(&1)
        .unwrap()
        .reset_draft = true;
    assert!(from_entries(entries).unwrap_err().contains("latest"));
    let mut entries = baseline.entries().clone();
    entries
        .get_mut(stable_name)
        .unwrap()
        .get_mut(&2)
        .unwrap()
        .reset_draft = true;
    let reset = from_entries(entries).unwrap();
    assert_eq!(
        reset
            .authorize_history(
                stable_name,
                &Metadata {
                    role: Role::Creation
                },
                HistoryPlan { head: 2 }
            )
            .unwrap(),
        HistoryReadiness::ResetDraft
    );
    assert!(reset
        .authorize_history(
            stable_name,
            &Metadata {
                role: Role::Creation
            },
            HistoryPlan { head: 3 }
        )
        .unwrap_err()
        .contains("cannot have a successor"));
    assert_eq!(
        reset.snapshot(stable_name, 2).unwrap().schema,
        baseline.snapshot(stable_name, 2).unwrap().schema
    );
}

fn document(stable_name: &str, property: &str) -> String {
    format!(
        r#"{{"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"urn:typehistory:schema:{stable_name}","type":"object","additionalProperties":false,"required":["{property}"],"properties":{{"{property}":{{"type":"string"}}}}}}"#
    )
}

fn ledger(stable_name: &str, versions: &[u32]) -> String {
    let entries = versions
        .iter()
        .map(|version| {
            format!(
                r#""{version}":{{"metadata":{{"role":"creation"}},"schema":{}}}"#,
                document(stable_name, "owner_name")
            )
        })
        .collect::<Vec<_>>()
        .join(",");
    format!(r#"{{"{stable_name}":{{{entries}}}}}"#)
}

#[test]
fn an_absent_stable_name_has_no_committed_history() {
    let baseline = parse(ledger("a.events.registered", &[1]).as_bytes()).expect("valid authority");
    assert_eq!(
        baseline.authority("a.events.other"),
        LedgerAuthority::NewHistory
    );
}

#[test]
fn a_committed_stable_name_reports_its_exact_range() {
    let baseline =
        parse(ledger("a.events.registered", &[1, 2, 3, 4]).as_bytes()).expect("valid authority");
    assert_eq!(
        baseline.authority("a.events.registered"),
        LedgerAuthority::Frozen { head: 4 }
    );
}

#[test]
fn every_history_starts_at_version_one() {
    for versions in [&[2][..], &[3, 4][..]] {
        let error =
            parse(ledger("a.events.registered", versions).as_bytes()).expect_err("V1 is required");
        assert!(error.contains("expected V1"), "{error}");
    }
}

#[test]
fn a_gap_in_the_retained_range_is_rejected() {
    let error =
        parse(ledger("a.events.registered", &[1, 3]).as_bytes()).expect_err("a gap is invalid");
    assert!(error.contains("non-contiguous"), "{error}");
}

#[test]
fn a_duplicate_version_key_is_rejected() {
    let stable_name = "a.events.registered";
    let schema = document(stable_name, "owner_name");
    let bytes = format!(
        r#"{{"{stable_name}":{{"1":{{"metadata":{{"role":"creation"}},"schema":{schema}}},"1":{{"metadata":{{"role":"creation"}},"schema":{schema}}}}}}}"#
    );
    let error = parse(bytes.as_bytes()).expect_err("a duplicate key is invalid");
    assert!(error.contains("duplicate JSON key"), "{error}");
}

#[test]
fn a_duplicate_stable_name_key_is_rejected() {
    let stable_name = "a.events.registered";
    let schema = document(stable_name, "owner_name");
    let entry = format!(
        r#""{stable_name}":{{"1":{{"metadata":{{"role":"creation"}},"schema":{schema}}}}}"#
    );
    let bytes = format!("{{{entry},{entry}}}");
    let error = parse(bytes.as_bytes()).expect_err("a duplicate key is invalid");
    assert!(error.contains("duplicate JSON key"), "{error}");
}

#[test]
fn role_disagreement_within_one_stable_name_is_rejected() {
    let stable_name = "a.events.registered";
    let schema = document(stable_name, "owner_name");
    let bytes = format!(
        r#"{{"{stable_name}":{{"1":{{"metadata":{{"role":"creation"}},"schema":{schema}}},"2":{{"metadata":{{"role":"update"}},"schema":{schema}}}}}}}"#
    );
    let error = parse(bytes.as_bytes()).expect_err("role drift is invalid");
    assert!(error.contains("disagrees about its metadata"), "{error}");
}

#[test]
fn an_absent_file_is_not_empty_authority() {
    let directory = tempfile::tempdir().expect("a temporary directory");
    let error = read(directory.path()).expect_err("restore or explicitly initialize authority");
    assert!(matches!(error, LedgerError::Unreadable { .. }));
}

#[test]
fn a_present_but_corrupt_file_is_never_read_as_empty_history() {
    let directory = tempfile::tempdir().expect("a temporary directory");
    fs::create_dir_all(directory.path().join("type-history")).expect("authority directory");
    fs::write(directory.path().join(RELATIVE_PATH), b"{ this is not json")
        .expect("corrupt authority");
    let error = read(directory.path()).expect_err("corruption is an error");
    assert!(matches!(error, LedgerError::Invalid { .. }));
}

#[test]
fn a_new_history_permits_only_version_one() {
    assert!(LedgerAuthority::NewHistory.permits(1).is_ok());
    let error = LedgerAuthority::NewHistory
        .permits(2)
        .expect_err("a new stable_name cannot begin above V1");
    assert!(error.contains("begins with only V1"), "{error}");
}

#[test]
fn committed_authority_permits_its_head_and_one_successor() {
    let authority = LedgerAuthority::Frozen { head: 4 };
    assert!(authority.permits(4).is_ok());
    assert!(authority.permits(5).is_ok());
}

#[test]
fn a_second_successor_draft_is_rejected() {
    let authority = LedgerAuthority::Frozen { head: 4 };
    let error = authority
        .permits(6)
        .expect_err("only one draft is permitted");
    assert!(error.contains("one permitted successor"), "{error}");
}

#[test]
fn an_enormous_boundary_is_rejected_before_expansion() {
    let authority = LedgerAuthority::Frozen { head: 1 };
    let error = authority
        .permits(4_000_000_000)
        .expect_err("an enormous head is invalid");
    assert!(error.contains("one permitted successor"), "{error}");
}

#[test]
fn a_head_below_committed_authority_is_rejected() {
    let authority = LedgerAuthority::Frozen { head: 3 };
    let error = authority
        .permits(2)
        .expect_err("deleted history is invalid");
    assert!(error.contains("below committed head"), "{error}");
}

#[test]
fn a_head_at_the_representable_ceiling_is_permitted_without_arithmetic() {
    let authority = LedgerAuthority::Frozen { head: u32::MAX };
    assert!(authority.permits(u32::MAX).is_ok());
}

#[test]
fn noncanonical_and_invalid_positive_version_keys_are_rejected() {
    for key in ["0", "01", "+1", " 1", "4294967296"] {
        let bytes = ledger("a.events.registered", &[1]).replace("\"1\":", &format!("\"{key}\":"));
        assert!(parse(bytes.as_bytes()).is_err(), "version key {key}");
    }
}

#[test]
fn strict_metadata_and_snapshot_keys_are_rejected() {
    let original = ledger("a.events.registered", &[1]);
    for bytes in [
        original.replace(
            "\"role\":\"creation\"",
            "\"role\":\"creation\",\"unknown\":true",
        ),
        original.replace("\"schema\":", "\"unknown\":true,\"schema\":"),
    ] {
        assert!(parse(bytes.as_bytes()).is_err());
    }
}