Контракт: скрипт validate_contracts.py валидирует примеры схемами
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
38c50c2101
commit
af0f25fbd8
116
scripts/validate_contracts.py
Normal file
116
scripts/validate_contracts.py
Normal file
@ -0,0 +1,116 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Валидация примеров контракта распознавания схемами JSON Schema 2020-12.
|
||||
|
||||
Запуск из любого cwd: python scripts/validate_contracts.py
|
||||
Зависимости: стандартная библиотека + jsonschema.
|
||||
Код возврата: 0 — всё валидно, 1 — любая ошибка.
|
||||
"""
|
||||
import datetime
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from jsonschema import Draft202012Validator, FormatChecker
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
CONTRACTS = ROOT / "contracts"
|
||||
EXAMPLES = CONTRACTS / "examples"
|
||||
|
||||
SCHEMAS = {
|
||||
"request": CONTRACTS / "recognition-request.schema.json",
|
||||
"message": CONTRACTS / "recognition-result-message.schema.json",
|
||||
"result": CONTRACTS / "recognition-result.schema.json",
|
||||
}
|
||||
|
||||
# Порядок важен: result-message-* проверяется раньше result-*.
|
||||
PATTERNS = [
|
||||
("request-", "request"),
|
||||
("result-message-", "message"),
|
||||
("result-", "result"),
|
||||
]
|
||||
|
||||
_DATE_TIME_RE = re.compile(r"^\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:\d{2}(\.\d+)?([Zz]|[+-]\d{2}:\d{2})$")
|
||||
|
||||
|
||||
def _check_date_time(value):
|
||||
# Собственная проверка вместо rfc3339-validator: нужна и дата, и время, и часовой пояс.
|
||||
if not isinstance(value, str):
|
||||
return True
|
||||
if not _DATE_TIME_RE.match(value):
|
||||
return False
|
||||
datetime.datetime.fromisoformat(value.replace("z", "Z")) # ValueError -> формат не прошёл
|
||||
return True
|
||||
|
||||
|
||||
def make_format_checker():
|
||||
checker = FormatChecker()
|
||||
checker.checks("date-time", raises=ValueError)(_check_date_time)
|
||||
return checker
|
||||
|
||||
|
||||
def short_path(error):
|
||||
path = "/".join(str(p) for p in error.absolute_path)
|
||||
return f"{path or '<root>'}: {error.message[:120]}"
|
||||
|
||||
|
||||
def main():
|
||||
failed = False
|
||||
validators = {}
|
||||
format_checker = make_format_checker()
|
||||
|
||||
for kind, path in SCHEMAS.items():
|
||||
if not path.is_file():
|
||||
print(f"FAIL схема {path.name}: файл не найден")
|
||||
failed = True
|
||||
continue
|
||||
try:
|
||||
schema = json.loads(path.read_text(encoding="utf-8"))
|
||||
Draft202012Validator.check_schema(schema)
|
||||
except Exception as exc: # битый JSON или нарушение метасхемы
|
||||
print(f"FAIL схема {path.name}: {str(exc).splitlines()[0][:150]}")
|
||||
failed = True
|
||||
continue
|
||||
validators[kind] = Draft202012Validator(schema, format_checker=format_checker)
|
||||
print(f"OK схема {path.name}")
|
||||
|
||||
counts = {kind: 0 for kind in SCHEMAS}
|
||||
for example in sorted(EXAMPLES.glob("*.json")):
|
||||
kind = next((k for prefix, k in PATTERNS if example.name.startswith(prefix)), None)
|
||||
if kind is None:
|
||||
print(f"FAIL {example.name}: имя не подходит ни под один шаблон (request-*, result-message-*, result-*)")
|
||||
failed = True
|
||||
continue
|
||||
counts[kind] += 1
|
||||
validator = validators.get(kind)
|
||||
if validator is None:
|
||||
print(f"FAIL {example.name} [{SCHEMAS[kind].name}]: схема недоступна")
|
||||
failed = True
|
||||
continue
|
||||
try:
|
||||
instance = json.loads(example.read_text(encoding="utf-8"))
|
||||
except ValueError as exc:
|
||||
print(f"FAIL {example.name} [{SCHEMAS[kind].name}]: не JSON: {exc}")
|
||||
failed = True
|
||||
continue
|
||||
errors = sorted(validator.iter_errors(instance), key=lambda e: list(e.absolute_path))
|
||||
if errors:
|
||||
failed = True
|
||||
print(f"FAIL {example.name} [{SCHEMAS[kind].name}]")
|
||||
for error in errors:
|
||||
print(f" {short_path(error)}")
|
||||
else:
|
||||
print(f"OK {example.name} [{SCHEMAS[kind].name}]")
|
||||
|
||||
for kind, count in counts.items():
|
||||
if count == 0:
|
||||
print(f"FAIL нет ни одного примера для схемы {SCHEMAS[kind].name}")
|
||||
failed = True
|
||||
|
||||
total = sum(counts.values())
|
||||
print(f"Итого: примеров {total}, {'есть ошибки' if failed else 'всё валидно'}")
|
||||
return 1 if failed else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Loading…
x
Reference in New Issue
Block a user