|
1 | 1 | #!/usr/bin/env python |
2 | 2 | """ |
3 | | -Validate build/*.json against schemas/question.schema.json |
| 3 | +Validate build/*.json (or files passed on the CLI) against schemas/question.schema.json |
4 | 4 | """ |
5 | 5 |
|
6 | | -import json, pathlib, sys |
7 | | -from jsonschema import Draft7Validator |
| 6 | +import json |
| 7 | +import pathlib |
| 8 | +import sys |
| 9 | +from json.decoder import JSONDecodeError |
| 10 | +from jsonschema import Draft7Validator, exceptions as js_exceptions |
8 | 11 |
|
9 | | -SCHEMA = json.load(open("schemas/question.schema.json")) |
| 12 | +HERE = pathlib.Path(__file__).resolve().parent |
| 13 | +SCHEMA_PATH = (HERE / ".." / "schemas" / "question.schema.json").resolve() |
10 | 14 |
|
11 | | -def validate_file(fp: pathlib.Path): |
12 | | - data = json.load(fp.open()) |
13 | | - errors = list(Draft7Validator(SCHEMA).iter_errors(data)) |
| 15 | +def load_json_strict(path: pathlib.Path): |
| 16 | + try: |
| 17 | + with path.open("r", encoding="utf-8") as f: |
| 18 | + return json.load(f) |
| 19 | + except JSONDecodeError as e: |
| 20 | + # Helpful context around the error location |
| 21 | + text = path.read_text(encoding="utf-8", errors="replace") |
| 22 | + start = max(e.pos - 60, 0) |
| 23 | + end = min(e.pos + 60, len(text)) |
| 24 | + snippet = text[start:end] |
| 25 | + pointer = " " * (e.pos - start) + "^" |
| 26 | + print(f"\n❌ JSON parse error in {path} @ line {e.lineno}, col {e.colno}: {e.msg}") |
| 27 | + print(" Context:") |
| 28 | + print(snippet) |
| 29 | + print(pointer) |
| 30 | + raise |
| 31 | + |
| 32 | +def load_schema(): |
| 33 | + if not SCHEMA_PATH.exists(): |
| 34 | + print(f"❌ Schema not found at {SCHEMA_PATH}") |
| 35 | + sys.exit(1) |
| 36 | + schema = load_json_strict(SCHEMA_PATH) |
| 37 | + try: |
| 38 | + Draft7Validator.check_schema(schema) |
| 39 | + except js_exceptions.SchemaError as se: |
| 40 | + print("❌ The schema file is not a valid Draft-07 JSON Schema.") |
| 41 | + print(" ", se.message) |
| 42 | + sys.exit(1) |
| 43 | + return schema |
| 44 | + |
| 45 | +def validate_file(validator: Draft7Validator, fp: pathlib.Path) -> bool: |
| 46 | + try: |
| 47 | + data = load_json_strict(fp) |
| 48 | + except JSONDecodeError: |
| 49 | + print(f" (Could not parse {fp.name} as JSON.)") |
| 50 | + return False |
| 51 | + |
| 52 | + errors = sorted(validator.iter_errors(data), key=lambda e: e.path) |
14 | 53 | if errors: |
15 | 54 | print(f"❌ {fp.name}") |
16 | 55 | for e in errors: |
17 | | - path = "/".join(map(str, e.path)) |
18 | | - print(" •", path, e.message) |
| 56 | + path = "/".join(map(str, e.path)) or "(root)" |
| 57 | + print(" •", path, "-", e.message) |
19 | 58 | return False |
20 | | - print(f"✓ {fp.name}") |
21 | | - return True |
| 59 | + else: |
| 60 | + print(f"✓ {fp.name}") |
| 61 | + return True |
22 | 62 |
|
23 | 63 | def main(): |
24 | | - build_dir = pathlib.Path("build") |
25 | | - ok = all(validate_file(f) for f in build_dir.glob("*.json")) |
| 64 | + schema = load_schema() |
| 65 | + validator = Draft7Validator(schema) |
| 66 | + |
| 67 | + # Files passed on CLI? Use those. Otherwise default to build/*.json |
| 68 | + args = [pathlib.Path(a) for a in sys.argv[1:]] |
| 69 | + if args: |
| 70 | + files = [] |
| 71 | + for a in args: |
| 72 | + if a.is_dir(): |
| 73 | + files.extend(sorted(a.glob("*.json"))) |
| 74 | + else: |
| 75 | + files.append(a) |
| 76 | + else: |
| 77 | + files = sorted((HERE / ".." / "build").resolve().glob("*.json")) |
| 78 | + |
| 79 | + if not files: |
| 80 | + print("No JSON files found to validate.") |
| 81 | + sys.exit(0) |
| 82 | + |
| 83 | + ok = True |
| 84 | + for fp in files: |
| 85 | + ok = validate_file(validator, fp) and ok |
| 86 | + |
26 | 87 | sys.exit(0 if ok else 1) |
27 | 88 |
|
28 | 89 | if __name__ == "__main__": |
|
0 commit comments