diff --git a/src/oold/validation/pipeline.py b/src/oold/validation/pipeline.py index 39a7aa0..023414a 100644 --- a/src/oold/validation/pipeline.py +++ b/src/oold/validation/pipeline.py @@ -409,7 +409,9 @@ def _unmapped_properties(run: _Run, name: str, raw, schema, sample) -> set[str]: Permitted by the specification (`OOLD-SCH-2d05`), so this is not a failure anywhere; it is reported by `context.coverage` and subtracted from what `roundtrip.generated` calls a loss. Returns an empty set when the context cannot be resolved - the checks that report resolution - failures do so on their own, and guessing here would double-report it. + failures do so on their own, and guessing here would double-report it. A context that resolves + to nothing is the opposite case and answers the question: no term exists, so every declared + property is unmapped. """ if name in run._unmapped: return run._unmapped[name] @@ -418,7 +420,11 @@ def _unmapped_properties(run: _Run, name: str, raw, schema, sample) -> set[str]: try: resolved = run.resolver.load(run.directory / name) context = resolve_context(raw, resolved.base_uri, run.resolver) - if not context.errors and not context.is_empty: + if context.errors: + unmapped = set() + elif context.is_empty: + unmapped = set(collect_composed_properties(schema)) + else: id_key, type_key = find_alias_keys(context.terms()) declared = set(collect_composed_properties(schema)) | {id_key, type_key} outcome = check_predicates( @@ -447,6 +453,21 @@ def _check_predicates(run: _Run, name: str, raw, schema, sample) -> None: return if context.is_empty: run.add("context.predicates", name, SKIP, "schema declares no @context") + # Coverage still has an answer here, and it is the strongest one it ever gives: no term + # exists, so nothing reaches RDF. Staying silent would report a schema mapping one + # property of ten while saying nothing about a schema mapping none of them. + declared = sorted(collect_composed_properties(schema)) + if declared and run.options.wants("context.coverage"): + plural = "ies" if declared[1:] else "y" + run.add( + "context.coverage", + name, + FAIL if run.options.strict else WARN, + f"schema declares no @context, so none of its {len(declared)} propert{plural} " + f"will reach RDF: {', '.join(declared)}. Declare @vocab to map them into a " + "default namespace, or add the terms to x-oold-context.", + {"declared": declared}, + ) return _run_rule_checks(run, name, raw, schema, ContextView(terms=context.terms(), entries=list(context.context))) diff --git a/tests/data/oold/broken/no_context.schema.json b/tests/data/oold/broken/no_context.schema.json new file mode 100644 index 0000000..e810e95 --- /dev/null +++ b/tests/data/oold/broken/no_context.schema.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://oo-ld.org/latest/meta/oold-meta-schema.json", + "$id": "no_context.schema.json", + "title": "NoContext", + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "orphan": { + "type": "string" + } + } +} diff --git a/tests/test_validation/test_pipeline.py b/tests/test_validation/test_pipeline.py index e137348..91dd81d 100644 --- a/tests/test_validation/test_pipeline.py +++ b/tests/test_validation/test_pipeline.py @@ -163,6 +163,25 @@ def test_missing_context_term_warns_and_names_the_orphan_property(broken_dir): assert "context.coverage" not in {c.id for c in report.failures()} +def test_a_schema_with_no_context_warns_instead_of_failing_the_round_trip(broken_dir): + """No `@context` means no term, so no property is mapped and none can be lost. + + `roundtrip.generated` subtracts unmapped properties from what it calls a loss, and an + absent context is the case where that set is every declared property. Reporting them as + lost would fail a schema for the one thing `OOLD-SCH-2d05` says must not fail it. + """ + report = validate_schema(broken_dir / "no_context.schema.json", OFFLINE) + checks = {c.id: c for c in report.checks} + + assert checks["roundtrip.generated"].status == OK + assert checks["context.predicates"].status == SKIP + coverage = checks["context.coverage"] + assert coverage.status == WARN + assert coverage.detail["declared"] == ["name", "orphan"] + assert "@vocab" in coverage.message and "x-oold-context" in coverage.message + assert not report.failures() + + def test_a_processor_failure_is_not_downgraded_to_a_coverage_warning(broken_dir, monkeypatch): """A raised processor must not read as a permitted omission.