odcs 0.9.1

Reference implementation of the Open Data Contract Standard (ODCS)
Documentation
//! Integration tests for the ODCS reference implementation.

mod common;

use std::fs;
use std::path::PathBuf;

use odcs::model::DataContract;
use odcs::{
    codes, parse, parse_file, validate, DocumentFormat, ParseResult, UPSTREAM_SPEC_VERSION,
};

fn fixture(name: &str) -> PathBuf {
    PathBuf::from(env!("CARGO_MANIFEST_DIR"))
        .join("tests/fixtures")
        .join(name)
}

fn parse_fixture(name: &str) -> ParseResult {
    let content = fs::read(fixture(name)).expect("read fixture");
    let format = if name.ends_with(".json") {
        DocumentFormat::Json
    } else {
        DocumentFormat::Yaml
    };
    parse(&content, format)
}

fn parse_fixture_contract(name: &str) -> DataContract {
    parse_fixture(name).into_contract().expect("parse fixture")
}

#[test]
fn upstream_spec_version_is_set() {
    assert_eq!(UPSTREAM_SPEC_VERSION, "3.1.0");
}

#[test]
fn parses_minimal_yaml_fixture() {
    let contract = parse_fixture_contract("minimal.odcs.yaml");
    assert_eq!(contract.name.as_deref(), Some("customer_data_contract"));
    assert_eq!(contract.version, UPSTREAM_SPEC_VERSION);
    assert_eq!(contract.api_version, "v3.1.0");
    assert_eq!(contract.id, "customer-data-contract");
    assert_eq!(contract.schema.len(), 1);
    assert_eq!(contract.quality_rules().len(), 1);
}

#[test]
fn parses_minimal_json_fixture() {
    let contract = parse_fixture_contract("minimal.odcs.json");
    assert_eq!(contract.name.as_deref(), Some("customer_data_contract"));
    assert_eq!(contract.kind, "DataContract");
}

#[test]
fn validates_minimal_fixture() {
    let result = parse_file("tests/fixtures/minimal.odcs.yaml").expect("read fixture");
    let report = result.validate();
    assert!(report.is_valid(), "{:?}", report.diagnostics);
}

#[test]
fn parses_example_yaml() {
    let result = parse_file("examples/minimal.odcs.yaml").expect("read example");
    let contract = result.into_contract().expect("parse example");
    assert_eq!(contract.kind, "DataContract");
}

#[test]
fn parses_example_json() {
    let result = parse_file("examples/minimal.odcs.json").expect("read example");
    let contract = result.into_contract().expect("parse example");
    assert_eq!(contract.name.as_deref(), Some("customer_data_contract"));
}

#[test]
fn rejects_malformed_yaml() {
    let result = parse_fixture("malformed.yaml");
    assert!(result.contract.is_none());
    assert!(result
        .report
        .diagnostics
        .iter()
        .any(|d| d.id == codes::PARSE_YAML));
}

#[test]
fn rejects_malformed_json() {
    let result = parse_fixture("malformed.json");
    assert!(result.contract.is_none());
    assert!(result
        .report
        .diagnostics
        .iter()
        .any(|d| d.id == codes::PARSE_JSON));
}

#[test]
fn rejects_empty_id() {
    let report = parse_fixture("invalid-empty-id.yaml").validate();
    assert!(!report.is_valid());
    assert!(report
        .diagnostics
        .iter()
        .any(|d| d.id == codes::MISSING_REQUIRED_FIELD && d.object_ref.as_deref() == Some("id")));
    assert!(
        !report
            .diagnostics
            .iter()
            .any(|d| d.id == codes::INVALID_EXTENSION && d.object_ref.as_deref() == Some("id")),
        "empty id should not also emit invalid-extension: {:?}",
        report.diagnostics
    );
}

#[test]
fn rejects_invalid_kind() {
    let report = parse_fixture("invalid-kind.yaml").validate();
    assert!(!report.is_valid());
    assert!(report
        .diagnostics
        .iter()
        .any(|d| d.id == codes::INVALID_KIND));
}

#[test]
fn rejects_unsupported_version() {
    let report = parse_fixture("unsupported-version.yaml").validate();
    assert!(!report.is_valid());
    assert!(report
        .diagnostics
        .iter()
        .any(|d| d.id == codes::UNSUPPORTED_VERSION));
}

#[test]
fn preserves_custom_properties() {
    let contract = parse_fixture_contract("with-extensions.yaml");
    let root = contract
        .custom_properties
        .as_ref()
        .expect("root custom properties");
    assert!(root.iter().any(|p| p.property == "customDomain"));
    assert!(root.iter().any(|p| p.property == "metadata"));

    let nested = contract.schema[0].properties[0]
        .element
        .custom_properties
        .as_ref()
        .expect("nested custom properties");
    assert!(nested.iter().any(|p| p.property == "sourceSystem"));
}

#[test]
fn rejects_unknown_root_field() {
    let result = parse_fixture("unknown-field.yaml");
    assert!(result.contract.is_none());
    assert!(result
        .report
        .diagnostics
        .iter()
        .any(|d| d.id == codes::UNKNOWN_FIELD));
}

#[test]
fn rejects_nested_unknown_field() {
    let result = parse_fixture("nested-unknown-field.yaml");
    assert!(result.contract.is_none());
    assert!(result
        .report
        .diagnostics
        .iter()
        .any(|d| d.id == codes::UNKNOWN_FIELD));
}

#[test]
fn rejects_lone_team_member_object() {
    let result = parse_fixture("invalid-lone-team-member.yaml");
    assert!(result.contract.is_none());
    assert!(!result.report.is_valid());
    assert!(
        result
            .report
            .diagnostics
            .iter()
            .any(|d| { d.id == codes::PARSE_YAML && d.message.contains("TeamDeclaration") }),
        "expected team parse failure: {:?}",
        result.report.diagnostics
    );
}

#[test]
fn diagnostics_are_deterministic_for_invalid_kind() {
    let first = parse_fixture("invalid-kind.yaml").validate();
    let second = parse_fixture("invalid-kind.yaml").validate();
    assert_eq!(first.diagnostics.len(), second.diagnostics.len());
    assert_eq!(first.diagnostics[0].id, second.diagnostics[0].id);
    assert_eq!(first.diagnostics[0].message, second.diagnostics[0].message);
}

#[test]
fn into_contract_requires_valid_parse() {
    let result = parse_fixture("malformed.yaml");
    assert!(result.into_contract().is_err());
}

#[test]
fn into_contract_rejects_validation_invalid_contract() {
    let result = parse_fixture("invalid-kind.yaml");
    assert!(result.into_contract().is_err());
}

#[test]
fn parses_all_section_fixtures() {
    for name in common::SECTION_FIXTURES {
        let contract = parse_fixture_contract(name);
        assert!(!contract.id.is_empty(), "fixture {name} missing id");
        assert_eq!(contract.api_version, "v3.1.0");
        let report = validate(&contract);
        assert!(
            report.is_valid(),
            "fixture {name}: {:?}",
            report.diagnostics
        );
    }
}

#[test]
fn parses_relationship_type_field() {
    let contract = parse_fixture_contract("with-relationships.yaml");
    let relationship = &contract.schema[0].relationships[0];
    assert_eq!(
        relationship.base.relationship_type.as_deref(),
        Some("foreignKey")
    );
}

#[test]
fn parses_array_items_property() {
    let contract = parse_fixture_contract("with-schema-array-items.yaml");
    let items = contract.schema[0].properties[0]
        .items
        .as_ref()
        .expect("array items");
    assert_eq!(items.element.name.as_deref(), Some("tag"));
}

#[test]
fn parses_custom_quality_object_implementation() {
    let contract = parse_fixture_contract("with-custom-quality-object.yaml");
    let implementation = contract.schema[0].quality.as_ref().expect("quality rules")[0]
        .implementation
        .as_ref()
        .expect("implementation");
    assert!(implementation.is_object());
}

#[test]
fn team_object_and_legacy_array_forms_parse() {
    let object_form = parse_fixture_contract("with-team.yaml");
    let legacy_form = parse_fixture_contract("with-team-legacy-array.yaml");
    assert_eq!(object_form.team.as_ref().unwrap().members().len(), 1);
    assert_eq!(legacy_form.team.as_ref().unwrap().members().len(), 1);
}

#[test]
fn yaml_round_trip_through_json() {
    let original = parse_fixture_contract("minimal.odcs.yaml");
    let json = serde_json::to_string(&original).expect("serialize json");
    let reparsed = parse(json.as_bytes(), DocumentFormat::Json)
        .into_contract()
        .expect("parse json round-trip");
    assert_eq!(original, reparsed);
}

#[test]
fn json_round_trip_through_yaml() {
    let original = parse_fixture_contract("minimal.odcs.json");
    let yaml = serde_yaml::to_string(&original).expect("serialize yaml");
    let reparsed = parse(yaml.as_bytes(), DocumentFormat::Yaml)
        .into_contract()
        .expect("parse yaml round-trip");
    assert_eq!(original, reparsed);
}

#[test]
fn custom_properties_survive_round_trip() {
    let original = parse_fixture_contract("with-custom-properties.yaml");
    let json = serde_json::to_string(&original).expect("serialize");
    let reparsed = parse(json.as_bytes(), DocumentFormat::Json)
        .into_contract()
        .expect("parse");
    assert_eq!(original, reparsed);
}

#[test]
fn relationship_type_round_trips() {
    let original = parse_fixture_contract("with-relationships.yaml");
    let json = serde_json::to_string(&original).expect("serialize");
    assert!(json.contains("\"type\":\"foreignKey\""));
    let reparsed = parse(json.as_bytes(), DocumentFormat::Json)
        .into_contract()
        .expect("parse");
    assert_eq!(original, reparsed);
}