From 30221d4de4097a91f33c7a1c23290db78f392726 Mon Sep 17 00:00:00 2001 From: Matt Faltyn Date: Sun, 26 Jul 2026 20:34:42 +0200 Subject: [PATCH] fix(validation): report schema errors for invalid YAML Signed-off-by: Matt Faltyn --- validation/test_validate.py | 61 +++++++++++++++++++++++++++++++++++++ validation/validate.py | 9 +++--- 2 files changed, 65 insertions(+), 5 deletions(-) create mode 100644 validation/test_validate.py diff --git a/validation/test_validate.py b/validation/test_validate.py new file mode 100644 index 00000000..0a646037 --- /dev/null +++ b/validation/test_validate.py @@ -0,0 +1,61 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import io +import sys +import tempfile +import unittest +from contextlib import redirect_stdout +from pathlib import Path +from unittest.mock import patch + +from validation import validate + + +class ValidateCliTest(unittest.TestCase): + def test_schema_invalid_documents_report_errors(self) -> None: + schema_path = Path(__file__).parents[1] / "core-spec" / "osi-schema.json" + + cases = ( + ("", "(root)"), + ("[]\n", "(root)"), + ("scalar\n", "(root)"), + ("semantic_model: invalid\n", "semantic_model"), + ) + for content, error_path in cases: + with self.subTest(content=content), tempfile.TemporaryDirectory() as tmpdir: + yaml_path = Path(tmpdir) / "model.yaml" + yaml_path.write_text(content, encoding="utf-8") + stdout = io.StringIO() + + with ( + patch.object( + sys, + "argv", + ["validate.py", str(yaml_path), "--schema", str(schema_path)], + ), + redirect_stdout(stdout), + self.assertRaises(SystemExit) as raised, + ): + validate.main() + + self.assertEqual(raised.exception.code, 1) + self.assertIn(f"[Schema] {error_path}:", stdout.getvalue()) + + +if __name__ == "__main__": + unittest.main() diff --git a/validation/validate.py b/validation/validate.py index 258d34f1..bd73e5cb 100644 --- a/validation/validate.py +++ b/validation/validate.py @@ -75,7 +75,7 @@ SKIP_SQL_VALIDATION = {"MDX", "TABLEAU", "MAQL"} -def validate_schema(data: dict, schema: dict) -> list[str]: +def validate_schema(data: object, schema: dict) -> list[str]: """Validate against JSON Schema.""" validator = Draft202012Validator(schema) errors = [] @@ -255,11 +255,10 @@ def main(): sys.exit(1) # Run validations - errors = [] - errors.extend(validate_schema(data, schema)) + errors = validate_schema(data, schema) - # Run semantic-model-specific checks only for semantic model payloads. - if data.get("semantic_model"): + # Semantic checks assume the schema has established the nested data shapes. + if not errors and isinstance(data, dict) and data.get("semantic_model"): errors.extend(validate_unique_names(data)) errors.extend(validate_references(data)) errors.extend(validate_sql(data))