Coverage for src/lumen/fhir/validator.py: 100%
51 statements
« prev ^ index » next coverage.py v7.16.0, created at 2026-08-31 18:46 +0000
« prev ^ index » next coverage.py v7.16.0, created at 2026-08-31 18:46 +0000
1"""
2This module performs validation on incoming resources using the official FHIR JSON Schema.
3It uses jsonschema-rs to compile the schema once and validate writes at high performance.
4"""
6from __future__ import annotations
8import json
9import re
10from functools import lru_cache
11from pathlib import Path
12from typing import Any
14import jsonschema_rs # type: ignore[import-untyped] # no stubs available
16_SCHEMA_PATH = Path(__file__).parent / "data" / "r4b" / "fhir.schema.json"
19@lru_cache(maxsize=1)
20def _get_base_schema() -> dict[str, Any]:
21 """Load and cache the base FHIR schema JSON."""
22 return json.loads(_SCHEMA_PATH.read_text())
25@lru_cache(maxsize=1)
26def _base_validator() -> Any:
27 """Compiled base validator fallback."""
28 return jsonschema_rs.validator_for(_get_base_schema())
31@lru_cache(maxsize=200)
32def _resource_validator(resource_type: str) -> Any:
33 """Compile and cache a validator targeted at a specific FHIR resource type."""
34 schema = _get_base_schema()
35 if not resource_type or resource_type not in schema.get("definitions", {}):
36 return _base_validator()
38 target_schema = {
39 "$ref": f"#/definitions/{resource_type}",
40 "definitions": schema["definitions"],
41 }
42 return jsonschema_rs.validator_for(target_schema)
45def clean_validation_error(
46 err_str: str, resource_type: str | None = None, instance_path: list[str] | None = None
47) -> str:
48 """Strip raw JSON serialization and format jsonschema-rs errors cleanly with paths."""
49 # Take only the first line of the error message to discard verbose stack trace info
50 first_line = err_str.splitlines()[0] if err_str else ""
52 path_prefix = ""
53 if instance_path:
54 path_prefix = f"At '{'.'.join(str(p) for p in instance_path)}': "
56 # Clean up pattern mismatch errors (hide massive regex strings)
57 pattern_match = re.search(r'does not match\s+"[^"]+"', first_line)
58 if pattern_match:
59 first_line = re.sub(
60 r'does not match\s+"[^"]+"',
61 "does not match the required format pattern",
62 first_line,
63 )
65 for marker in ["} is ", "] is ", "} was ", "] was "]:
66 idx = first_line.find(marker)
67 if idx != -1:
68 suffix = first_line[idx + 1 :]
69 res_name = f"Resource of type '{resource_type}'" if resource_type else "Resource"
70 msg = f"{res_name}{suffix}"
71 if "not valid under any of the schemas listed in the 'oneOf' keyword" in msg:
72 return f"{path_prefix}{res_name} does not conform to the FHIR schema specification"
73 return f"{path_prefix}{msg}"
75 # Fallback regex matching JSON at start
76 match = re.match(r"^[\{\[].*?[\}\]]\s+(.*)$", first_line, re.DOTALL)
77 if match:
78 suffix = match.group(1)
79 res_name = f"Resource of type '{resource_type}'" if resource_type else "Resource"
80 msg = f"{res_name} {suffix}"
81 if "not valid under any of the schemas listed in the 'oneOf' keyword" in msg:
82 return f"{path_prefix}{res_name} does not conform to the FHIR schema specification"
83 return f"{path_prefix}{msg}"
85 return f"{path_prefix}{first_line}"
88def validate_resource(resource: dict[str, Any]) -> list[str]:
89 """Return a list of validation error messages, empty if valid."""
90 resource_type = resource.get("resourceType") or ""
91 validator = _resource_validator(resource_type)
92 return [
93 clean_validation_error(str(err), resource_type, err.instance_path)
94 for err in validator.iter_errors(resource)
95 ]