From da2023fd6c1654b385d5222f7babb791648aead0 Mon Sep 17 00:00:00 2001 From: Doug Date: Sun, 5 Jul 2026 19:25:54 -0400 Subject: [PATCH 1/7] feat(validator): scaffold helios-fhir-validator with cooperative validation engine Phase 1 of the FHIR Schema based validator (see fhir-schema sibling repo): - FhirSchema serde model (differential, order-preserving, Arc-shared elements) - SchemaResolver trait + SchemaRegistry/CompositeResolver (SearchParameter registry pattern) - Cooperative schema-set walk: base layering, complex-type resolution, unknown-element, array/scalar shape, required/excluded, choices/choiceOf, fixed/pattern, numeric min/max, dynamic Resource resolution (Bundle.entry.resource, contained) - Deferred-effects collection for constraints/bindings (executed in a later phase) - Vendored upstream FHIR Schema conformance suite (MIT, provenance in UPSTREAM.md) with exact ordered error matching; fixtures 1,2,3,4,7 green, 5/6 gated until slicing lands Co-Authored-By: Claude Opus 4.8 (1M context) --- Cargo.lock | 9 + Cargo.toml | 2 + crates/fhir-validator/Cargo.toml | 34 ++ crates/fhir-validator/src/effects.rs | 37 ++ crates/fhir-validator/src/engine/errors.rs | 303 ++++++++++ crates/fhir-validator/src/engine/mod.rs | 82 +++ crates/fhir-validator/src/engine/path.rs | 127 +++++ crates/fhir-validator/src/engine/walk.rs | 527 ++++++++++++++++++ crates/fhir-validator/src/lib.rs | 55 ++ crates/fhir-validator/src/resolver.rs | 157 ++++++ crates/fhir-validator/src/schema.rs | 313 +++++++++++ crates/fhir-validator/tests/conformance.rs | 137 +++++ .../tests/fixtures/upstream/1_elements.json | 281 ++++++++++ .../tests/fixtures/upstream/2_base.json | 96 ++++ .../tests/fixtures/upstream/3_choices.json | 98 ++++ .../tests/fixtures/upstream/4_required.json | 165 ++++++ .../tests/fixtures/upstream/5_slices.json | 164 ++++++ .../tests/fixtures/upstream/6_extensions.json | 115 ++++ .../tests/fixtures/upstream/7_bundles.json | 96 ++++ .../tests/fixtures/upstream/UPSTREAM.md | 27 + 20 files changed, 2825 insertions(+) create mode 100644 crates/fhir-validator/Cargo.toml create mode 100644 crates/fhir-validator/src/effects.rs create mode 100644 crates/fhir-validator/src/engine/errors.rs create mode 100644 crates/fhir-validator/src/engine/mod.rs create mode 100644 crates/fhir-validator/src/engine/path.rs create mode 100644 crates/fhir-validator/src/engine/walk.rs create mode 100644 crates/fhir-validator/src/lib.rs create mode 100644 crates/fhir-validator/src/resolver.rs create mode 100644 crates/fhir-validator/src/schema.rs create mode 100644 crates/fhir-validator/tests/conformance.rs create mode 100644 crates/fhir-validator/tests/fixtures/upstream/1_elements.json create mode 100644 crates/fhir-validator/tests/fixtures/upstream/2_base.json create mode 100644 crates/fhir-validator/tests/fixtures/upstream/3_choices.json create mode 100644 crates/fhir-validator/tests/fixtures/upstream/4_required.json create mode 100644 crates/fhir-validator/tests/fixtures/upstream/5_slices.json create mode 100644 crates/fhir-validator/tests/fixtures/upstream/6_extensions.json create mode 100644 crates/fhir-validator/tests/fixtures/upstream/7_bundles.json create mode 100644 crates/fhir-validator/tests/fixtures/upstream/UPSTREAM.md diff --git a/Cargo.lock b/Cargo.lock index 499fac454..b78a453cc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3145,6 +3145,15 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "helios-fhir-validator" +version = "0.2.1" +dependencies = [ + "indexmap 2.14.0", + "serde", + "serde_json", +] + [[package]] name = "helios-fhirpath" version = "0.2.1" diff --git a/Cargo.toml b/Cargo.toml index 3c520a7f8..72ea2b9b9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ default-members = [ "crates/fhir", "crates/fhir-gen", "crates/fhir-macro", + "crates/fhir-validator", "crates/fhirpath", "crates/fhirpath-support", "crates/serde", @@ -38,4 +39,5 @@ rust-version = "1.90" serde = { version = "=1.0.224", features = ["derive"] } serde_json = "=1.0.144" helios-fhir = { path = "crates/fhir", version = "0.2.1", default-features = false } +helios-fhir-validator = { path = "crates/fhir-validator", version = "0.2.1", default-features = false } chrono = { version = "0.4", features = ["serde"] } diff --git a/crates/fhir-validator/Cargo.toml b/crates/fhir-validator/Cargo.toml new file mode 100644 index 000000000..b6d27099e --- /dev/null +++ b/crates/fhir-validator/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "helios-fhir-validator" +version.workspace = true +edition.workspace = true +license.workspace = true +authors.workspace = true +repository.workspace = true +rust-version.workspace = true +description = "FHIR resource validator for HFS — FHIR Schema based structural and profile validation" +homepage = "https://github.com/HeliosSoftware/hfs/tree/main/crates/fhir-validator" +keywords = ["helios-software", "hl7", "fhir", "validation", "fhir-schema"] + +[features] +default = ["R4"] +# Version features are placeholders until the converter/pack phases wire +# helios-fhir (and helios-fhirpath) cascades in. Kept so downstream crates can +# already forward their version features to this crate. +R4 = [] +R4B = [] +R5 = [] +R6 = [] + +[dependencies] +# `rc` enables Deserialize/Serialize for Arc — element schemas are shared +# by reference across cooperative schema sets. +serde = { workspace = true, features = ["rc"] } +# `preserve_order` is load-bearing: the conformance contract requires errors +# to be emitted in document key order. +serde_json = { workspace = true, features = ["preserve_order"] } +indexmap = { version = "2", features = ["serde"] } + +[dev-dependencies] +serde = { workspace = true } +serde_json = { workspace = true } diff --git a/crates/fhir-validator/src/effects.rs b/crates/fhir-validator/src/effects.rs new file mode 100644 index 000000000..ae94c35a2 --- /dev/null +++ b/crates/fhir-validator/src/effects.rs @@ -0,0 +1,37 @@ +//! Deferred effects: validations collected during the pure synchronous walk +//! and executed afterwards. +//! +//! The structural walk ([`crate::engine`]) never performs I/O and never +//! evaluates FHIRPath. When it encounters a `constraints` or `binding` +//! keyword it records a [`Deferred`] entry; the async half of validation +//! (Phase 4) executes them via pluggable handlers. + +use crate::schema::Binding; +use serde_json::Value; + +/// A validation obligation collected during the sync walk. +#[derive(Debug, Clone, PartialEq)] +pub enum Deferred { + /// A FHIRPath invariant to evaluate with the node at `path` as focus. + Constraint { + /// Dotted conformance path of the node the constraint attaches to. + path: String, + /// Constraint id (e.g. `pat-1`). + id: String, + /// FHIRPath expression. + expression: String, + /// Human-readable description (used in the emitted message). + human: Option, + /// `error` | `warning` | `guideline`. + severity: Option, + }, + /// A required-strength terminology binding to check. + Binding { + /// Dotted conformance path of the coded node. + path: String, + /// The binding (ValueSet canonical + strength). + binding: Binding, + /// The coded value as found in the data. + value: Value, + }, +} diff --git a/crates/fhir-validator/src/engine/errors.rs b/crates/fhir-validator/src/engine/errors.rs new file mode 100644 index 000000000..cb9b903d6 --- /dev/null +++ b/crates/fhir-validator/src/engine/errors.rs @@ -0,0 +1,303 @@ +//! The validation error model and — critically — every message template. +//! +//! The upstream FHIR Schema conformance suite compares error lists with exact +//! ordered deep-equality, message strings included. All message text lives in +//! this one file so the contract is pinned in a single place; the unit tests +//! at the bottom hold the literal strings copied from the fixtures and the +//! reference validator (`fhirschema-js/src/index.js`). + +use serde::Serialize; +use serde_json::Value; + +/// Stable error type strings — the cross-implementation contract. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] +#[serde(rename_all = "kebab-case")] +pub enum ErrorKind { + /// A data key matched no element in any schema of the cooperative set. + UnknownElement, + /// A singular value was given where some layer declares `array: true`. + NotArray, + /// An array was given where every layer says singular. + NotSingular, + /// Wrong JSON container type (e.g. `required` applied to a non-object). + Type, + /// A key listed in `required` is absent. + Required, + /// A key listed in `excluded` is present. + Excluded, + /// Array shorter than `min`. + Min, + /// Array longer than `max`. + Max, + /// A slice's matched-item count violates its `min`/`max`. + SliceCardinality, + /// More than one branch of a choice group present at once. + Choice, + /// A present choice branch is not in an allowed `choices` list. + ChoiceExcluded, + /// Value not deeply equal to `fixed`. + FixedValue, + /// Value does not partially match `pattern`. + PatternValue, + /// A `severity: error` FHIRPath constraint evaluated to false. + FhirpathConstraint, + /// A coded value failed its required-strength ValueSet binding. + TerminologyBinding, + // ------------------------------------------------------------------ + // Helios extensions — not pinned by (and never triggered in) the + // upstream conformance fixtures. + // ------------------------------------------------------------------ + /// A primitive value failed its type-class or regex check. + PrimitiveValue, + /// A referenced schema (root, `base`, or complex `type`) could not be + /// resolved. The reference validator throws here; we report and continue. + UnknownSchema, + /// A profile named in `meta.profile` or the caller's profile list could + /// not be resolved. + UnknownProfile, +} + +/// Issue severity. Internal only — deliberately not serialized, so the +/// conformance error shape stays exactly `{type, path, message, ...extra}`. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum Severity { + #[default] + Error, + Warning, +} + +/// One validation issue, shaped for the conformance contract. +#[derive(Debug, Clone, PartialEq, Serialize)] +pub struct ValidationError { + #[serde(rename = "type")] + pub kind: ErrorKind, + /// Dot-joined location rooted at the resource type, numeric array + /// indices as bare numbers: `Bundle.entry.0.resource.extra`. + pub path: String, + pub message: String, + /// Structured extras carried by some kinds (`constraint` id, + /// `binding` object). + #[serde(flatten)] + pub extra: serde_json::Map, + /// Internal severity; REST maps it onto OperationOutcome. + #[serde(skip)] + pub severity: Severity, +} + +impl ValidationError { + pub fn new(kind: ErrorKind, path: String, message: String) -> Self { + Self { kind, path, message, extra: serde_json::Map::new(), severity: Severity::Error } + } + + pub fn with_severity(mut self, severity: Severity) -> Self { + self.severity = severity; + self + } + + pub fn with_extra(mut self, key: &str, value: Value) -> Self { + self.extra.insert(key.to_string(), value); + self + } +} + +// --------------------------------------------------------------------- +// Message templates — the single source of truth. +// +// Templates marked [pinned] are exercised verbatim by the upstream fixtures +// and must not change. Templates marked [reference] reproduce the reference +// validator's wording for keywords no fixture exercises yet. Templates +// marked [helios] are our own. +// --------------------------------------------------------------------- + +/// [pinned] `{key} is unknown` +pub(crate) fn msg_unknown_element(key: &str) -> String { + format!("{key} is unknown") +} + +/// [pinned] `{key} is not array` +pub(crate) fn msg_not_array(key: &str) -> String { + format!("{key} is not array") +} + +/// [pinned] `{key} is not singular` +pub(crate) fn msg_not_singular(key: &str) -> String { + format!("{key} is not singular") +} + +/// [pinned] `{key} is required` +pub(crate) fn msg_required(key: &str) -> String { + format!("{key} is required") +} + +/// [reference] `excluded property {key} is present` +pub(crate) fn msg_excluded(key: &str) -> String { + format!("excluded property {key} is present") +} + +/// [reference] `expected object got {json_type}` — the `required`/`excluded` +/// wording (no comma). +pub(crate) fn msg_expected_object_no_comma(json_type: &str) -> String { + format!("expected object got {json_type}") +} + +/// [reference] `expected object, got {json_type}` — the `elements` wording +/// (with comma). Yes, the two differ only by a comma; both are contract. +pub(crate) fn msg_expected_object_comma(json_type: &str) -> String { + format!("expected object, got {json_type}") +} + +/// [reference] `expected {min} > {actual_len}` +pub(crate) fn msg_min(min: u64, actual_len: usize) -> String { + format!("expected {min} > {actual_len}") +} + +/// [reference] `expected {max} < {actual_len}` +pub(crate) fn msg_max(max: u64, actual_len: usize) -> String { + format!("expected {max} < {actual_len}") +} + +/// [pinned] `only one choice for {group} allowed, but multiple found: {a, b}` +pub(crate) fn msg_choice(group: &str, branches: &[String]) -> String { + format!( + "only one choice for {group} allowed, but multiple found: {}", + branches.join(", ") + ) +} + +/// [pinned] `{key} is excluded choice` +pub(crate) fn msg_choice_excluded(key: &str) -> String { + format!("{key} is excluded choice") +} + +/// [pinned] `Slice defines the following min cardinality: '{min}', actual cardinality: '{n}'` +pub(crate) fn msg_slice_min(min: u64, actual: u64) -> String { + format!("Slice defines the following min cardinality: '{min}', actual cardinality: '{actual}'") +} + +/// [pinned] `Slice defines the following max cardinality: '{max}', actual cardinality: '{n}'` +pub(crate) fn msg_slice_max(max: u64, actual: u64) -> String { + format!("Slice defines the following max cardinality: '{max}', actual cardinality: '{actual}'") +} + +/// [helios] fixed-value mismatch. The reference validator's wording coerces +/// JS objects to `[object Object]`; no fixture pins it, so we render JSON. +pub(crate) fn msg_fixed_value(expected: &Value, actual: &Value) -> String { + format!( + "Expected value to be exactly equal to 'fixed' pattern '{expected}', but got: '{actual}'" + ) +} + +/// [helios] pattern-value mismatch (same JSON-rendering rationale as fixed). +pub(crate) fn msg_pattern_value(expected: &Value, actual: &Value) -> String { + format!("Expected value to match 'pattern' '{expected}', but got: '{actual}'") +} + +/// [helios] a schema reference could not be resolved (reference validator +/// throws `could not resolve ["…"]`; we report and continue). +pub(crate) fn msg_unknown_schema(reference: &str) -> String { + format!("could not resolve schema '{reference}'") +} + +/// [helios] a profile reference could not be resolved. +pub(crate) fn msg_unknown_profile(reference: &str) -> String { + format!("could not resolve profile '{reference}'") +} + +/// The reference validator's JSON type vocabulary (`getType`). +pub(crate) fn json_type_name(v: &Value) -> &'static str { + match v { + Value::Array(_) => "array", + Value::Object(_) => "object", + Value::String(_) => "string", + Value::Number(_) => "number", + Value::Bool(_) => "boolean", + Value::Null => "null", + } +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + /// Literal strings copied from the upstream fixtures — the [pinned] set. + #[test] + fn pinned_messages_match_fixture_literals() { + assert_eq!(msg_unknown_element("unknown"), "unknown is unknown"); + assert_eq!(msg_not_array("multielement"), "multielement is not array"); + assert_eq!(msg_not_singular("status"), "status is not singular"); + assert_eq!(msg_required("anotherRequiredElement"), "anotherRequiredElement is required"); + assert_eq!( + msg_choice("choice", &["choiceString".into(), "choiceInteger".into()]), + "only one choice for choice allowed, but multiple found: choiceString, choiceInteger" + ); + assert_eq!(msg_choice_excluded("choiceInteger"), "choiceInteger is excluded choice"); + assert_eq!( + msg_slice_min(1, 0), + "Slice defines the following min cardinality: '1', actual cardinality: '0'" + ); + assert_eq!( + msg_slice_max(1, 2), + "Slice defines the following max cardinality: '1', actual cardinality: '2'" + ); + } + + /// Literal strings copied from fhirschema-js/src/index.js — the + /// [reference] set (no fixture exercises them yet). + #[test] + fn reference_messages_match_source_literals() { + assert_eq!(msg_expected_object_no_comma("string"), "expected object got string"); + assert_eq!(msg_expected_object_comma("string"), "expected object, got string"); + assert_eq!(msg_excluded("gender"), "excluded property gender is present"); + assert_eq!(msg_min(1, 0), "expected 1 > 0"); + assert_eq!(msg_max(2, 3), "expected 2 < 3"); + } + + #[test] + fn serializes_to_conformance_shape() { + let err = ValidationError::new( + ErrorKind::UnknownElement, + "Patient.unknown".into(), + msg_unknown_element("unknown"), + ); + assert_eq!( + serde_json::to_value(&err).unwrap(), + json!({ + "type": "unknown-element", + "path": "Patient.unknown", + "message": "unknown is unknown" + }) + ); + } + + #[test] + fn kebab_case_kind_strings() { + for (kind, s) in [ + (ErrorKind::UnknownElement, "unknown-element"), + (ErrorKind::NotArray, "not-array"), + (ErrorKind::NotSingular, "not-singular"), + (ErrorKind::Type, "type"), + (ErrorKind::SliceCardinality, "slice-cardinality"), + (ErrorKind::ChoiceExcluded, "choice-excluded"), + (ErrorKind::FixedValue, "fixed-value"), + (ErrorKind::PatternValue, "pattern-value"), + (ErrorKind::FhirpathConstraint, "fhirpath-constraint"), + (ErrorKind::TerminologyBinding, "terminology-binding"), + ] { + assert_eq!(serde_json::to_value(kind).unwrap(), json!(s)); + } + } + + #[test] + fn extra_fields_flatten() { + let err = ValidationError::new( + ErrorKind::FhirpathConstraint, + "Patient".into(), + "FHIRPath constraint pat-1 error: must have a name".into(), + ) + .with_extra("constraint", json!("pat-1")); + let v = serde_json::to_value(&err).unwrap(); + assert_eq!(v["constraint"], json!("pat-1")); + assert!(v.get("severity").is_none(), "severity must not serialize"); + } +} diff --git a/crates/fhir-validator/src/engine/mod.rs b/crates/fhir-validator/src/engine/mod.rs new file mode 100644 index 000000000..a414cd15d --- /dev/null +++ b/crates/fhir-validator/src/engine/mod.rs @@ -0,0 +1,82 @@ +//! The cooperative validation engine. +//! +//! Validation never flattens schemas: it builds a *set* of schemas (the +//! *schemata*) for every data node — root schema from `resourceType`, +//! profiles, plus every transitively referenced `base` and complex `type` — +//! and every member of the set judges the same node together. See +//! `docs/guide/mental-model.md` in the FHIR Schema repo for the model, and +//! `walk.rs` for the deterministic error-emission order. + +mod errors; +mod path; +mod walk; + +pub use errors::{ErrorKind, Severity, ValidationError}; +pub use path::dotted_to_fhirpath; + +use crate::effects::Deferred; +use crate::resolver::SchemaResolver; +use serde_json::Value; +use std::sync::Arc; + +/// Policy for profile references (`meta.profile` / caller-supplied) that the +/// resolver cannot resolve. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum UnknownProfilePolicy { + /// Report as a warning-severity issue (default). + #[default] + Warn, + /// Report as an error-severity issue. + Error, + /// Skip silently. + Ignore, +} + +/// Options for one validation run. +#[derive(Debug, Clone)] +pub struct ValidationOptions { + /// Extra profile canonicals/names to layer on, in addition to the root + /// schema and (optionally) `meta.profile`. + pub profiles: Vec, + /// Layer on the profiles the resource claims in `meta.profile`. + pub use_meta_profiles: bool, + /// What to do when a profile reference cannot be resolved. + pub unknown_profile: UnknownProfilePolicy, +} + +impl Default for ValidationOptions { + fn default() -> Self { + Self { + profiles: Vec::new(), + use_meta_profiles: true, + unknown_profile: UnknownProfilePolicy::default(), + } + } +} + +/// Result of the pure synchronous walk. +#[derive(Debug, Default)] +pub struct SyncOutcome { + /// Structural issues, in deterministic emission order. + pub errors: Vec, + /// Constraint/binding obligations for the async effects pass (Phase 4). + pub deferred: Vec, +} + +/// The validator: a resolver plus the walk. +pub struct Validator { + resolver: Arc, +} + +impl Validator { + pub fn new(resolver: Arc) -> Self { + Self { resolver } + } + + /// Pure, synchronous structural validation. No I/O, no FHIRPath — + /// constraint/binding obligations are returned as [`Deferred`] entries + /// instead of being executed. + pub fn validate_sync(&self, resource: &Value, opts: &ValidationOptions) -> SyncOutcome { + walk::validate(self.resolver.as_ref(), resource, opts) + } +} diff --git a/crates/fhir-validator/src/engine/path.rs b/crates/fhir-validator/src/engine/path.rs new file mode 100644 index 000000000..36b0db617 --- /dev/null +++ b/crates/fhir-validator/src/engine/path.rs @@ -0,0 +1,127 @@ +//! Path tracking during the validation walk. +//! +//! The conformance contract renders paths dot-joined, rooted at the +//! resource type, with array indices as bare numbers: +//! `Bundle.entry.0.resource.extra`. The REST layer additionally needs +//! bracket-indexed FHIRPath (`Bundle.entry[0].resource.extra`) for +//! `OperationOutcome.issue.expression`. + +#[derive(Debug, Clone, PartialEq)] +pub(crate) enum PathSeg { + Key(String), + Index(usize), +} + +#[derive(Debug, Clone, Default)] +pub struct PathTracker { + segs: Vec, +} + +impl PathTracker { + /// Root the path at the resource type (empty string when the data has no + /// `resourceType`, mirroring the reference validator). + pub(crate) fn new(root: impl Into) -> Self { + Self { segs: vec![PathSeg::Key(root.into())] } + } + + pub(crate) fn push_key(&mut self, key: &str) { + self.segs.push(PathSeg::Key(key.to_string())); + } + + pub(crate) fn push_index(&mut self, index: usize) { + self.segs.push(PathSeg::Index(index)); + } + + pub(crate) fn pop(&mut self) { + self.segs.pop(); + } + + /// The last `Key` segment — the element name currently being validated. + /// Used by the `choices` keyword, which reports on the branch key. + pub(crate) fn last_key(&self) -> Option<&str> { + self.segs.iter().rev().find_map(|s| match s { + PathSeg::Key(k) => Some(k.as_str()), + PathSeg::Index(_) => None, + }) + } + + /// Dot-joined conformance rendering: `Patient.name.0.given`. + pub(crate) fn render_dotted(&self) -> String { + let mut out = String::new(); + for (i, seg) in self.segs.iter().enumerate() { + if i > 0 { + out.push('.'); + } + match seg { + PathSeg::Key(k) => out.push_str(k), + PathSeg::Index(n) => out.push_str(&n.to_string()), + } + } + out + } +} + +/// Render a dotted conformance path as bracket-indexed FHIRPath: +/// `Patient.name.0.given` → `Patient.name[0].given`. +/// +/// Used by the REST layer for `OperationOutcome.issue.expression`. +pub fn dotted_to_fhirpath(dotted: &str) -> String { + let mut out = String::new(); + for seg in dotted.split('.') { + if !out.is_empty() && seg.bytes().all(|b| b.is_ascii_digit()) && !seg.is_empty() { + out.push('['); + out.push_str(seg); + out.push(']'); + } else { + if !out.is_empty() { + out.push('.'); + } + out.push_str(seg); + } + } + out +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn renders_dotted_with_numeric_indices() { + let mut p = PathTracker::new("Bundle"); + p.push_key("entry"); + p.push_index(0); + p.push_key("resource"); + p.push_key("extra"); + assert_eq!(p.render_dotted(), "Bundle.entry.0.resource.extra"); + p.pop(); + assert_eq!(p.render_dotted(), "Bundle.entry.0.resource"); + } + + #[test] + fn empty_root_renders_like_reference() { + // No resourceType → root joins to the empty string. + let mut p = PathTracker::new(""); + assert_eq!(p.render_dotted(), ""); + p.push_key("x"); + assert_eq!(p.render_dotted(), ".x"); + } + + #[test] + fn last_key_skips_indices() { + let mut p = PathTracker::new("Patient"); + p.push_key("name"); + p.push_index(2); + assert_eq!(p.last_key(), Some("name")); + } + + #[test] + fn fhirpath_rendering_brackets_indices() { + assert_eq!( + dotted_to_fhirpath("Bundle.entry.0.resource.extra"), + "Bundle.entry[0].resource.extra" + ); + assert_eq!(dotted_to_fhirpath("Patient"), "Patient"); + assert_eq!(dotted_to_fhirpath("Patient.name.10"), "Patient.name[10]"); + } +} diff --git a/crates/fhir-validator/src/engine/walk.rs b/crates/fhir-validator/src/engine/walk.rs new file mode 100644 index 000000000..0d01159bb --- /dev/null +++ b/crates/fhir-validator/src/engine/walk.rs @@ -0,0 +1,527 @@ +//! The cooperative validation walk. +//! +//! Port of the FHIR Schema validation algorithm (`docs/algorythm.md` and the +//! reference validator `fhirschema-js/src/index.js`), with the divergences +//! called out inline. The behavioral contract is the vendored conformance +//! suite; where the reference validator and this port differ, no fixture +//! distinguishes them and the difference is documented. +//! +//! # Deterministic error-emission order +//! +//! 1. Schema sets iterate in insertion order, ancestors first: a schema's +//! `base` chain is added before the schema itself (pinned by fixture +//! `4_required.json`: the base's `required` errors precede the profile's). +//! 2. Within one schema, keyword validators run in the canonical order +//! `fixed, pattern, constraints, required, excluded, binding, elements, +//! choices` (the reference validator follows JSON key declaration order; +//! no fixture distinguishes). +//! 3. Data keys are walked in document order (`serde_json`'s +//! `preserve_order`), array items in index order. +//! 4. Node-level keyword errors precede per-key errors; choice-conflict +//! errors (`postValidate`) come last for each node. +//! +//! # Known divergences from the reference validator +//! +//! - Unresolvable schema references produce an `unknown-schema` error and +//! validation continues; the reference validator throws. +//! - `excluded` is enforced (the reference implementation crashes on it). +//! - Dynamic `Resource` schemas are resolved per array item; the reference +//! validator leaks them into the shared element set across siblings. +//! - A choice branch declared by several layers is counted once per node in +//! the multiple-choices-present check (the reference validator counts it +//! once per layer, which would false-positive on real profiles). +//! - The choice-group *declarer* schema joins a branch element's set under +//! the key `{layer}.{group}` (the reference validator uses `{layer}.{key}`, +//! which silently overwrites the branch schema and loses its constraints). + +use super::errors::{self, ErrorKind, Severity, ValidationError}; +use super::path::PathTracker; +use super::{SyncOutcome, UnknownProfilePolicy, ValidationOptions}; +use crate::effects::Deferred; +use crate::resolver::SchemaResolver; +use crate::schema::FhirSchema; +use indexmap::IndexMap; +use serde_json::Value; +use std::collections::HashSet; +use std::sync::Arc; + +/// The special element type resolved dynamically from `data.resourceType`. +const RESOURCE_TYPE_NAME: &str = "Resource"; + +/// A cooperative schema set (the *schemata*): insertion-ordered, ancestors +/// first, deduplicated both by resolution key and by schema identity. +#[derive(Clone, Default)] +struct SchemaSet { + map: IndexMap>, + /// Identity dedup (`Arc` pointer): the same schema reachable under + /// several aliases (name and canonical URL) is added once. + seen: HashSet, +} + +impl SchemaSet { + fn new() -> Self { + Self::default() + } + + fn is_empty(&self) -> bool { + self.map.is_empty() + } + + fn contains_key(&self, key: &str) -> bool { + self.map.contains_key(key) + } + + fn iter(&self) -> impl Iterator)> { + self.map.iter().map(|(k, v)| (k.as_str(), v)) + } + + fn schemas(&self) -> impl Iterator> { + self.map.values() + } +} + +struct WalkCtx<'a> { + resolver: &'a dyn SchemaResolver, + errors: Vec, + deferred: Vec, + path: PathTracker, +} + +impl WalkCtx<'_> { + fn error(&mut self, kind: ErrorKind, message: String) { + self.errors.push(ValidationError::new(kind, self.path.render_dotted(), message)); + } + + fn error_with_severity(&mut self, kind: ErrorKind, message: String, severity: Severity) { + self.errors.push( + ValidationError::new(kind, self.path.render_dotted(), message) + .with_severity(severity), + ); + } +} + +pub(super) fn validate( + resolver: &dyn SchemaResolver, + resource: &Value, + opts: &ValidationOptions, +) -> SyncOutcome { + let resource_type = resource + .get("resourceType") + .and_then(Value::as_str) + .unwrap_or_default(); + + let mut ctx = WalkCtx { + resolver, + errors: Vec::new(), + deferred: Vec::new(), + path: PathTracker::new(resource_type), + }; + + // Root schema-set: resourceType, then meta.profile claims, then + // caller-supplied profiles (the draft-algorithm order). + let mut set = SchemaSet::new(); + if !resource_type.is_empty() { + match resolver.resolve(resource_type) { + Some(schema) => add_schemas_to_set(&mut ctx, &mut set, schema, resource_type), + None => ctx.error( + ErrorKind::UnknownSchema, + errors::msg_unknown_schema(resource_type), + ), + } + } + if opts.use_meta_profiles { + for profile in meta_profiles(resource) { + add_profile(&mut ctx, &mut set, &profile, opts.unknown_profile); + } + } + for profile in &opts.profiles { + add_profile(&mut ctx, &mut set, profile, opts.unknown_profile); + } + + validate_node(&mut ctx, &set, resource); + + SyncOutcome { errors: ctx.errors, deferred: ctx.deferred } +} + +/// `meta.profile` entries, in document order. +fn meta_profiles(resource: &Value) -> Vec { + resource + .get("meta") + .and_then(|m| m.get("profile")) + .and_then(Value::as_array) + .map(|arr| { + arr.iter() + .filter_map(Value::as_str) + .map(str::to_string) + .collect() + }) + .unwrap_or_default() +} + +fn add_profile( + ctx: &mut WalkCtx<'_>, + set: &mut SchemaSet, + profile: &str, + policy: UnknownProfilePolicy, +) { + match ctx.resolver.resolve(profile) { + Some(schema) => add_schemas_to_set(ctx, set, schema, profile), + None => match policy { + UnknownProfilePolicy::Warn => ctx.error_with_severity( + ErrorKind::UnknownProfile, + errors::msg_unknown_profile(profile), + Severity::Warning, + ), + UnknownProfilePolicy::Error => { + ctx.error(ErrorKind::UnknownProfile, errors::msg_unknown_profile(profile)) + } + UnknownProfilePolicy::Ignore => {} + }, + } +} + +/// Pull `schema` into `set` together with everything it references: +/// its `base` chain first (ancestors-first ordering), then its complex +/// `type` (skipped for primitive leaves), then the schema itself. +fn add_schemas_to_set( + ctx: &mut WalkCtx<'_>, + set: &mut SchemaSet, + schema: Arc, + key: &str, +) { + let identity = Arc::as_ptr(&schema) as usize; + if !set.seen.insert(identity) { + return; + } + + if let Some(base) = schema.base.clone() + && !set.contains_key(&base) { + match ctx.resolver.resolve(&base) { + Some(base_schema) => add_schemas_to_set(ctx, set, base_schema, &base), + None => ctx.error(ErrorKind::UnknownSchema, errors::msg_unknown_schema(&base)), + } + } + + if let Some(type_ref) = schema.type_.clone() + && !schema.is_primitive() && !set.contains_key(&type_ref) { + match ctx.resolver.resolve(&type_ref) { + Some(type_schema) => add_schemas_to_set(ctx, set, type_schema, &type_ref), + None => { + ctx.error(ErrorKind::UnknownSchema, errors::msg_unknown_schema(&type_ref)) + } + } + } + + set.map.insert(key.to_string(), schema); +} + +/// Validate one data node against a cooperative schema set. +fn validate_node(ctx: &mut WalkCtx<'_>, set: &SchemaSet, data: &Value) { + // Dynamic `Resource` resolution: an element typed `Resource` takes its + // concrete schema from the nested `resourceType` at validation time + // (Bundle.entry.resource, contained, Parameters.parameter.resource). + let mut extended; + let set = if data.is_object() + && set + .schemas() + .any(|s| s.type_.as_deref() == Some(RESOURCE_TYPE_NAME)) + { + extended = set.clone(); + if let Some(rt) = data.get("resourceType").and_then(Value::as_str) { + match ctx.resolver.resolve(rt) { + Some(schema) => add_schemas_to_set(ctx, &mut extended, schema, rt), + None => ctx.error(ErrorKind::UnknownSchema, errors::msg_unknown_schema(rt)), + } + } + // No resourceType on a Resource-typed node: nothing to resolve; the + // static layers (e.g. the base `Resource` schema) still apply. + &extended + } else { + set + }; + + eval_validators(ctx, set, data); + + let Some(obj) = data.as_object() else { + return; + }; + + // Choice branches seen on this node, keyed by choice group, in first- + // encounter order. + let mut multi_choice: IndexMap> = IndexMap::new(); + + for (key, value) in obj { + ctx.path.push_key(key); + eval_element(ctx, set, &mut multi_choice, key, value); + ctx.path.pop(); + } + + // postValidate: more than one branch of the same choice group present. + for (group, branches) in &multi_choice { + if branches.len() > 1 { + ctx.path.push_key(group); + ctx.error(ErrorKind::Choice, errors::msg_choice(group, branches)); + ctx.path.pop(); + } + } +} + +/// Run every keyword validator of every schema in the set against the node. +fn eval_validators(ctx: &mut WalkCtx<'_>, set: &SchemaSet, data: &Value) { + // Clone the Arcs up front: keyword validators need `&mut ctx` while the + // set is borrowed, and Arc clones are cheap. + let schemas: Vec> = set.schemas().cloned().collect(); + for schema in &schemas { + if let Some(fixed) = &schema.fixed + && data != fixed { + ctx.error(ErrorKind::FixedValue, errors::msg_fixed_value(fixed, data)); + } + if let Some(pattern) = &schema.pattern + && !is_partial_match(data, pattern) { + ctx.error(ErrorKind::PatternValue, errors::msg_pattern_value(pattern, data)); + } + if let Some(constraints) = &schema.constraints { + let path = ctx.path.render_dotted(); + for (id, c) in constraints { + ctx.deferred.push(Deferred::Constraint { + path: path.clone(), + id: id.clone(), + expression: c.expression.clone(), + human: c.human.clone(), + severity: c.severity.clone(), + }); + } + } + if let Some(required) = &schema.required { + validate_required(ctx, set, required, data); + } + if let Some(excluded) = &schema.excluded { + validate_excluded(ctx, excluded, data); + } + if let Some(binding) = &schema.binding { + ctx.deferred.push(Deferred::Binding { + path: ctx.path.render_dotted(), + binding: binding.clone(), + value: data.clone(), + }); + } + if schema.elements.is_some() && !data.is_object() { + ctx.error( + ErrorKind::Type, + errors::msg_expected_object_comma(errors::json_type_name(data)), + ); + } + if let Some(choices) = &schema.choices { + // Runs when a choice-group declarer joins a branch element's + // set; the branch key is the last named path segment. + if let Some(branch) = ctx.path.last_key() + && !choices.iter().any(|c| c == branch) { + let message = errors::msg_choice_excluded(branch); + ctx.error(ErrorKind::ChoiceExcluded, message); + } + } + } +} + +fn validate_required(ctx: &mut WalkCtx<'_>, set: &SchemaSet, required: &[String], data: &Value) { + let Some(obj) = data.as_object() else { + ctx.error( + ErrorKind::Type, + errors::msg_expected_object_no_comma(errors::json_type_name(data)), + ); + return; + }; + for key in required { + ctx.path.push_key(key); + if !obj.contains_key(key) && !choice_branch_satisfies(set, key, obj) { + ctx.error(ErrorKind::Required, errors::msg_required(key)); + } + ctx.path.pop(); + } +} + +/// Helios extension: a required key that is a choice group (`foo` for +/// `foo[x]`) is satisfied by any declared branch (`fooBoolean`, ...) being +/// present. The converter maps `min >= 1` on `foo[x]` to `required: ["foo"]`, +/// and the literal key never appears in data. No upstream fixture combines +/// `required` with choices, so conformance is unaffected. +fn choice_branch_satisfies( + set: &SchemaSet, + key: &str, + obj: &serde_json::Map, +) -> bool { + set.schemas().any(|schema| { + schema + .elements + .as_ref() + .and_then(|els| els.get(key)) + .and_then(|el| el.choices.as_ref()) + .is_some_and(|branches| branches.iter().any(|b| obj.contains_key(b))) + }) +} + +fn validate_excluded(ctx: &mut WalkCtx<'_>, excluded: &[String], data: &Value) { + let Some(obj) = data.as_object() else { + ctx.error( + ErrorKind::Type, + errors::msg_expected_object_no_comma(errors::json_type_name(data)), + ); + return; + }; + for key in excluded { + ctx.path.push_key(key); + if obj.contains_key(key) { + ctx.error(ErrorKind::Excluded, errors::msg_excluded(key)); + } + ctx.path.pop(); + } +} + +/// Validate one data key against the element schemas collected from every +/// layer of the set. +fn eval_element( + ctx: &mut WalkCtx<'_>, + set: &SchemaSet, + multi_choice: &mut IndexMap>, + key: &str, + value: &Value, +) { + let mut elset = SchemaSet::new(); + collect_schemas_for_element(ctx, set, &mut elset, multi_choice, key); + + if elset.is_empty() { + ctx.error(ErrorKind::UnknownElement, errors::msg_unknown_element(key)); + return; + } + + // Array-ness is cooperative: an element is an array if ANY layer says so. + let expect_array = elset.schemas().any(|s| s.array == Some(true)); + match (expect_array, value.is_array()) { + (true, false) => ctx.error(ErrorKind::NotArray, errors::msg_not_array(key)), + (false, true) => ctx.error(ErrorKind::NotSingular, errors::msg_not_singular(key)), + _ => {} + } + // The walk continues after a shape mismatch, mirroring the reference + // validator (the mismatched value still validates against the set). + + if let Some(items) = value.as_array() { + // Whole-collection keywords, per schema: min then max. + let schemas: Vec> = elset.schemas().cloned().collect(); + for schema in &schemas { + if let Some(min) = schema.min + && (items.len() as u64) < min { + ctx.error(ErrorKind::Min, errors::msg_min(min, items.len())); + } + if let Some(max) = schema.max + && (items.len() as u64) > max { + ctx.error(ErrorKind::Max, errors::msg_max(max, items.len())); + } + } + + // Phase 2: slicing (mark/sweep) runs here, before per-item recursion. + + for (index, item) in items.iter().enumerate() { + ctx.path.push_index(index); + validate_node(ctx, &elset, item); + ctx.path.pop(); + } + } else { + validate_node(ctx, &elset, value); + } +} + +/// Gather the element sub-set for `key` across all layers: each layer's +/// `elements[key]` (plus its transitive `base`/`type`), and — when the key +/// is a choice branch — every layer's declarer schema for the choice group. +fn collect_schemas_for_element( + ctx: &mut WalkCtx<'_>, + set: &SchemaSet, + elset: &mut SchemaSet, + multi_choice: &mut IndexMap>, + key: &str, +) { + // Phase 2: `extensions` maps compile into slicing here when + // key == "extension". + + let layers: Vec<(String, Arc)> = set + .iter() + .map(|(name, schema)| (name.to_string(), Arc::clone(schema))) + .collect(); + + let mut choice_group: Option = None; + for (layer_name, layer) in &layers { + let Some(subschema) = layer.elements.as_ref().and_then(|els| els.get(key)) else { + continue; + }; + // Choice-group declarers are not element schemas for their own key: + // a bare group key in data (e.g. `"choice": ...`) is unknown. + if subschema.choices.is_none() { + let subset_key = format!("{layer_name}.{key}"); + add_schemas_to_set(ctx, elset, Arc::clone(subschema), &subset_key); + } + if let Some(group) = &subschema.choice_of { + choice_group = Some(group.clone()); + let entry = multi_choice.entry(group.clone()).or_default(); + // Count each branch once per node, even when several layers + // declare it. + if !entry.iter().any(|k| k == key) { + entry.push(key.to_string()); + } + } + } + + // The key is a choice branch: every layer's declarer for the group joins + // the branch's set, so `choices` narrowing applies cooperatively. + if let Some(group) = choice_group { + for (layer_name, layer) in &layers { + if let Some(declarer) = layer.elements.as_ref().and_then(|els| els.get(&group)) { + let subset_key = format!("{layer_name}.{group}"); + add_schemas_to_set(ctx, elset, Arc::clone(declarer), &subset_key); + } + } + } +} + +/// Lodash-style `_.isMatch`: partial deep match. Every key present in +/// `pattern` must exist in `data` and match recursively; extra data keys are +/// permitted. Arrays match index-wise as a prefix; scalars by equality. +pub(crate) fn is_partial_match(data: &Value, pattern: &Value) -> bool { + match pattern { + Value::Object(pattern_map) => { + let Some(data_map) = data.as_object() else { + return false; + }; + pattern_map.iter().all(|(k, pv)| { + data_map.get(k).is_some_and(|dv| is_partial_match(dv, pv)) + }) + } + Value::Array(pattern_items) => { + let Some(data_items) = data.as_array() else { + return false; + }; + pattern_items.len() <= data_items.len() + && pattern_items + .iter() + .zip(data_items.iter()) + .all(|(pv, dv)| is_partial_match(dv, pv)) + } + scalar => data == scalar, + } +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + #[test] + fn partial_match_semantics() { + assert!(is_partial_match(&json!({"use": "home", "city": "X"}), &json!({"use": "home"}))); + assert!(!is_partial_match(&json!({"use": "work"}), &json!({"use": "home"}))); + assert!(!is_partial_match(&json!("home"), &json!({"use": "home"}))); + assert!(is_partial_match(&json!({"a": {"b": 1, "c": 2}}), &json!({"a": {"b": 1}}))); + assert!(is_partial_match(&json!([1, 2, 3]), &json!([1, 2]))); + assert!(!is_partial_match(&json!([2, 1]), &json!([1]))); + assert!(is_partial_match(&json!(5), &json!(5))); + } +} diff --git a/crates/fhir-validator/src/lib.rs b/crates/fhir-validator/src/lib.rs new file mode 100644 index 000000000..83d6e082d --- /dev/null +++ b/crates/fhir-validator/src/lib.rs @@ -0,0 +1,55 @@ +//! # helios-fhir-validator +//! +//! FHIR resource validation for the Helios FHIR server, built on the +//! [FHIR Schema](https://fhir-schema.github.io/fhir-schema/) approach: a +//! JSON-Schema-like, differential-by-design compiled form of FHIR +//! StructureDefinitions, validated via **cooperative schema sets** rather +//! than snapshot flattening. +//! +//! ## Quick start +//! +//! ``` +//! use helios_fhir_validator::{FhirSchema, SchemaRegistry, ValidationOptions, Validator}; +//! use std::sync::Arc; +//! +//! let mut registry = SchemaRegistry::new(); +//! registry.insert_named( +//! "string", +//! serde_json::from_str::(r#"{ "kind": "primitive-type" }"#).unwrap(), +//! ); +//! registry.insert_named( +//! "Patient", +//! serde_json::from_str::( +//! r#"{ "elements": { "resourceType": {"type": "string"}, "status": {"type": "string"} } }"#, +//! ) +//! .unwrap(), +//! ); +//! +//! let validator = Validator::new(Arc::new(registry)); +//! let resource = serde_json::json!({ "resourceType": "Patient", "status": "active" }); +//! let outcome = validator.validate_sync(&resource, &ValidationOptions::default()); +//! assert!(outcome.errors.is_empty()); +//! ``` +//! +//! The engine walks raw `serde_json::Value` — deliberately, since the typed +//! `helios-fhir` models deserialize leniently and cannot surface unknown +//! elements. Structural validation is pure and synchronous; FHIRPath +//! constraints and terminology bindings are collected as [`Deferred`] +//! obligations for an async effects pass. +//! +//! The behavioral contract is the vendored FHIR Schema conformance suite in +//! `tests/fixtures/upstream/` (exact ordered error matching), plus Helios +//! extended fixtures in `tests/fixtures/extended/`. + +pub mod effects; +pub mod engine; +pub mod resolver; +pub mod schema; + +pub use effects::Deferred; +pub use engine::{ + dotted_to_fhirpath, ErrorKind, Severity, SyncOutcome, UnknownProfilePolicy, ValidationError, + ValidationOptions, Validator, +}; +pub use resolver::{CompositeResolver, SchemaRegistry, SchemaResolver}; +pub use schema::{Binding, Constraint, FhirSchema, Match, Slice, Slicing}; diff --git a/crates/fhir-validator/src/resolver.rs b/crates/fhir-validator/src/resolver.rs new file mode 100644 index 000000000..04fc0bcbb --- /dev/null +++ b/crates/fhir-validator/src/resolver.rs @@ -0,0 +1,157 @@ +//! Schema resolution: how the engine turns a name or canonical URL into a +//! [`FhirSchema`]. +//! +//! Mirrors the SearchParameter registry pattern in `crates/fhir/src/search/`: +//! an `Arc`-backed in-memory registry, composable so tenant-uploaded profiles +//! can overlay the embedded core packs. + +use crate::schema::FhirSchema; +use std::collections::HashMap; +use std::sync::Arc; + +/// Resolves a schema reference — a bare name (`"Patient"`) or a canonical URL +/// (`"http://hl7.org/fhir/StructureDefinition/Patient"`) — to a schema. +pub trait SchemaResolver: Send + Sync { + /// Returns the schema, or `None` if the reference is unknown. + fn resolve(&self, reference: &str) -> Option>; +} + +/// In-memory schema registry. +/// +/// Schemas are indexed under every identity they carry: the explicit key they +/// were inserted with, their `url`, and their `name`. All aliases map to the +/// same `Arc`, so identity-based deduplication in the engine treats them as +/// one schema. +#[derive(Debug, Default)] +pub struct SchemaRegistry { + map: HashMap>, +} + +impl SchemaRegistry { + pub fn new() -> Self { + Self::default() + } + + /// Number of distinct keys (aliases included). + pub fn len(&self) -> usize { + self.map.len() + } + + pub fn is_empty(&self) -> bool { + self.map.is_empty() + } + + /// Insert a schema under an explicit key, plus its `url`/`name` aliases. + /// + /// The explicit key wins on collision (later inserts overwrite earlier + /// ones for the same key — last write wins, like the JS fixture maps). + pub fn insert_named(&mut self, key: impl Into, schema: FhirSchema) { + let schema = Arc::new(schema); + let key = key.into(); + if let Some(url) = schema.url.clone() + && url != key { + self.map.insert(url, Arc::clone(&schema)); + } + if let Some(name) = schema.name.clone() + && name != key { + self.map.insert(name, Arc::clone(&schema)); + } + self.map.insert(key, schema); + } + + /// Insert a schema under its own `url` and `name`. + /// + /// Returns `false` (and stores nothing) if the schema carries neither. + pub fn insert(&mut self, schema: FhirSchema) -> bool { + match (schema.url.clone(), schema.name.clone()) { + (Some(url), _) => { + self.insert_named(url, schema); + true + } + (None, Some(name)) => { + self.insert_named(name, schema); + true + } + (None, None) => false, + } + } + + /// Build a registry from `(key, schema)` pairs. + pub fn from_named(iter: impl IntoIterator) -> Self { + let mut reg = Self::new(); + for (k, s) in iter { + reg.insert_named(k, s); + } + reg + } +} + +impl SchemaResolver for SchemaRegistry { + fn resolve(&self, reference: &str) -> Option> { + self.map.get(reference).cloned() + } +} + +/// Layered resolver: earlier layers win. Used to overlay tenant-uploaded +/// profiles on top of an embedded core pack. +#[derive(Clone, Default)] +pub struct CompositeResolver { + layers: Vec>, +} + +impl CompositeResolver { + pub fn new(layers: Vec>) -> Self { + Self { layers } + } + + /// Add a layer with lower precedence than all existing layers. + pub fn push(&mut self, layer: Arc) { + self.layers.push(layer); + } +} + +impl SchemaResolver for CompositeResolver { + fn resolve(&self, reference: &str) -> Option> { + self.layers.iter().find_map(|l| l.resolve(reference)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn named(name: &str) -> FhirSchema { + FhirSchema { + name: Some(name.to_string()), + url: Some(format!("http://example.org/{name}")), + ..Default::default() + } + } + + #[test] + fn resolves_by_key_url_and_name() { + let mut reg = SchemaRegistry::new(); + reg.insert_named("PatKey", named("Patient")); + let by_key = reg.resolve("PatKey").unwrap(); + let by_url = reg.resolve("http://example.org/Patient").unwrap(); + let by_name = reg.resolve("Patient").unwrap(); + assert!(Arc::ptr_eq(&by_key, &by_url)); + assert!(Arc::ptr_eq(&by_key, &by_name)); + assert!(reg.resolve("nope").is_none()); + } + + #[test] + fn composite_earlier_layer_wins() { + let mut base = SchemaRegistry::new(); + base.insert_named("Patient", FhirSchema { kind: Some("resource".into()), ..Default::default() }); + let mut overlay = SchemaRegistry::new(); + overlay.insert_named("Patient", FhirSchema { kind: Some("constraint".into()), ..Default::default() }); + + let composite = + CompositeResolver::new(vec![Arc::new(overlay), Arc::new(base)]); + assert_eq!( + composite.resolve("Patient").unwrap().kind.as_deref(), + Some("constraint") + ); + } +} diff --git a/crates/fhir-validator/src/schema.rs b/crates/fhir-validator/src/schema.rs new file mode 100644 index 000000000..eba382648 --- /dev/null +++ b/crates/fhir-validator/src/schema.rs @@ -0,0 +1,313 @@ +//! The FHIR Schema data model. +//! +//! FHIR Schema is a JSON-Schema-like, differential-by-design representation of +//! FHIR StructureDefinitions (see ). +//! One struct serves both the schema level and the element level — the two are +//! the same shape in the format (confirmed by the upstream conformance suite). +//! +//! Everything is optional: a schema on disk is a differential fragment, and +//! the "complete picture" only exists transiently as a cooperative schema set +//! during validation (see [`crate::engine`]). +//! +//! Deserialization is tolerant: unknown keys are ignored (mirroring the +//! reference validator, which logs and skips unrecognized keywords). + +use indexmap::IndexMap; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use std::sync::Arc; + +/// Schema `kind` values with meaning to the validation engine. +/// +/// Kept as plain string constants (rather than an enum) because fixtures and +/// converted packs carry open-ended kinds (`resource`, `complex-type`, +/// `logical`, `extension`, ...) and only `primitive-type` alters behavior: +/// it stops complex-`type` expansion during schema-set assembly. +pub mod kind { + pub const PRIMITIVE_TYPE: &str = "primitive-type"; + pub const RESOURCE: &str = "resource"; + pub const COMPLEX_TYPE: &str = "complex-type"; + pub const EXTENSION: &str = "extension"; +} + +/// A FHIR Schema — used both as a top-level (named) schema and as an element +/// schema nested under [`FhirSchema::elements`]. +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct FhirSchema { + // ------------------------------------------------------------------ + // Identity (top-level schemas) + // ------------------------------------------------------------------ + /// Canonical URL this schema is published under. + #[serde(skip_serializing_if = "Option::is_none")] + pub url: Option, + /// Machine-readable name (also usable as a resolution alias). + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, + /// Schema this one layers onto; resolved into the same cooperative set. + #[serde(skip_serializing_if = "Option::is_none")] + pub base: Option, + /// Schema kind — see [`kind`]. Only `primitive-type` alters assembly. + #[serde(skip_serializing_if = "Option::is_none")] + pub kind: Option, + /// `specialization` (custom resources) or `constraint` (profiles). + #[serde(skip_serializing_if = "Option::is_none")] + pub derivation: Option, + /// The type this schema (or element) constrains. On an element, naming a + /// complex type pulls that type's schema into the set; the special value + /// `Resource` resolves dynamically from the data's `resourceType`. + #[serde(rename = "type", skip_serializing_if = "Option::is_none")] + pub type_: Option, + + // ------------------------------------------------------------------ + // Shape + // ------------------------------------------------------------------ + /// Element is a collection: only JSON arrays are accepted. Array-ness is + /// decided cooperatively — true if *any* layer says `array: true`. + #[serde(skip_serializing_if = "Option::is_none")] + pub array: Option, + /// Element is scalar: JSON arrays are rejected. Mutually exclusive with + /// `array` within one schema (layers may still disagree; array wins). + #[serde(skip_serializing_if = "Option::is_none")] + pub scalar: Option, + /// Minimum number of items (arrays only). + #[serde(skip_serializing_if = "Option::is_none")] + pub min: Option, + /// Maximum number of items (arrays only). + #[serde(skip_serializing_if = "Option::is_none")] + pub max: Option, + + // ------------------------------------------------------------------ + // Structure + // ------------------------------------------------------------------ + /// Nested element schemas, keyed by element name. Order is preserved — + /// deterministic error emission is part of the conformance contract. + /// Values are `Arc` so cooperative sets can share them without cloning. + #[serde(skip_serializing_if = "Option::is_none")] + pub elements: Option>>, + /// Child keys that must be present on the node. + #[serde(skip_serializing_if = "Option::is_none")] + pub required: Option>, + /// Child keys that must be absent from the node. + #[serde(skip_serializing_if = "Option::is_none")] + pub excluded: Option>, + /// Reference to another element's schema by path, for recursive + /// structures (e.g. `Questionnaire.item`): + /// `["Questionnaire", "elements", "item"]`. + #[serde(skip_serializing_if = "Option::is_none")] + pub element_reference: Option>, + + // ------------------------------------------------------------------ + // Choice types (value[x]) + // ------------------------------------------------------------------ + /// On a choice-group declarer: the allowed concrete branch names. + #[serde(skip_serializing_if = "Option::is_none")] + pub choices: Option>, + /// On a concrete branch: the name of the choice group it belongs to. + #[serde(skip_serializing_if = "Option::is_none")] + pub choice_of: Option, + + // ------------------------------------------------------------------ + // Value constraints + // ------------------------------------------------------------------ + /// Value must be deeply equal to this constant. + #[serde(skip_serializing_if = "Option::is_none")] + pub fixed: Option, + /// Value must partially match this constant (pattern keys present and + /// equal; extra data keys permitted). + #[serde(skip_serializing_if = "Option::is_none")] + pub pattern: Option, + /// Terminology binding. Only `required`-strength bindings are enforced. + #[serde(skip_serializing_if = "Option::is_none")] + pub binding: Option, + /// FHIRPath invariants, keyed by constraint id. + #[serde(skip_serializing_if = "Option::is_none")] + pub constraints: Option>, + /// Allowed reference target types (for Reference/canonical elements). + #[serde(skip_serializing_if = "Option::is_none")] + pub refers: Option>, + + // ------------------------------------------------------------------ + // Slicing + // ------------------------------------------------------------------ + /// Array slicing definition. + #[serde(skip_serializing_if = "Option::is_none")] + pub slicing: Option, + /// Extension sugar: named extensions keyed by slice name, compiled into + /// `slicing` on the `extension` element (matched by `{url}`) at + /// validation time. Entry values reuse [`FhirSchema`] (they carry `url`, + /// `min`, `max`, plus any extra element constraints). + #[serde(skip_serializing_if = "Option::is_none")] + pub extensions: Option>>, + + // ------------------------------------------------------------------ + // Informational (carried, never enforced) + // ------------------------------------------------------------------ + /// Mirrors `ElementDefinition.isModifier`. + #[serde(skip_serializing_if = "Option::is_none")] + pub modifier: Option, + /// Mirrors `ElementDefinition.mustSupport`. + #[serde(skip_serializing_if = "Option::is_none")] + pub must_support: Option, + /// Mirrors `ElementDefinition.isSummary`. + #[serde(skip_serializing_if = "Option::is_none")] + pub summary: Option, + + // ------------------------------------------------------------------ + // Helios extension + // ------------------------------------------------------------------ + /// Primitive value regex, carried from the FHIR spec's primitive type + /// StructureDefinitions by our converter. Not part of upstream FHIR + /// Schema; absent from the upstream conformance fixtures. + #[serde(skip_serializing_if = "Option::is_none")] + pub regex: Option, +} + +impl FhirSchema { + /// Whether this schema is a primitive-type leaf: registered in the set + /// but never expanded as a complex type. + pub fn is_primitive(&self) -> bool { + self.kind.as_deref() == Some(kind::PRIMITIVE_TYPE) + } +} + +/// Terminology binding: the ValueSet a coded value must belong to. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Binding { + /// Canonical URL of the bound ValueSet. + pub value_set: String, + /// FHIR binding strength (`required`, `extensible`, `preferred`, + /// `example`). Only `required` is validated. + #[serde(skip_serializing_if = "Option::is_none")] + pub strength: Option, +} + +/// A FHIRPath invariant. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct Constraint { + /// FHIRPath expression that must evaluate truthy on the node. + pub expression: String, + /// `error` | `warning` | `guideline`. Only `error` fails validation. + #[serde(skip_serializing_if = "Option::is_none")] + pub severity: Option, + /// Human-readable description, used in the emitted message. + #[serde(skip_serializing_if = "Option::is_none")] + pub human: Option, +} + +/// Array slicing: partitions a repeating element into named slices. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct Slicing { + /// Named slices. Order preserved for deterministic sweep/error order. + pub slices: IndexMap, + /// `open` (default) | `closed` | `openAtEnd`. + #[serde(skip_serializing_if = "Option::is_none")] + pub rules: Option, + /// When true, matched items must appear in slice `order`. + #[serde(skip_serializing_if = "Option::is_none")] + pub ordered: Option, +} + +/// One slice of a sliced array. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Slice { + /// How items are matched into this slice. Optional: constraining slices + /// (`sliceIsConstraining`) reference a parent slice instead. + #[serde(rename = "match", skip_serializing_if = "Option::is_none")] + pub match_: Option, + /// Minimum number of matched items across the whole array. + #[serde(skip_serializing_if = "Option::is_none")] + pub min: Option, + /// Maximum number of matched items across the whole array. + /// `Some(0)` means the slice is prohibited. + #[serde(skip_serializing_if = "Option::is_none")] + pub max: Option, + /// Position for `ordered` slicing. + #[serde(skip_serializing_if = "Option::is_none")] + pub order: Option, + /// Name of the parent slice being re-sliced. + #[serde(skip_serializing_if = "Option::is_none")] + pub reslice: Option, + /// Further constrains a slice of the same name from a parent schema. + #[serde(skip_serializing_if = "Option::is_none")] + pub slice_is_constraining: Option, + /// Element schema applied cooperatively to items matched into the slice. + #[serde(skip_serializing_if = "Option::is_none")] + pub schema: Option>, +} + +/// A slice match rule. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Match { + /// `pattern` | `binding` | `profile` | `type` (the reference validator + /// only implements `pattern`; we start there too). + #[serde(rename = "type", skip_serializing_if = "Option::is_none")] + pub type_: Option, + /// The pattern / binding / profile / type payload. + #[serde(skip_serializing_if = "Option::is_none")] + pub value: Option, + /// Match against the resolved reference target instead of the reference + /// element itself. + #[serde(rename = "resolve-ref", skip_serializing_if = "Option::is_none")] + pub resolve_ref: Option, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parses_minimal_primitive_schema() { + let s: FhirSchema = serde_json::from_str(r#"{ "kind": "primitive-type" }"#).unwrap(); + assert!(s.is_primitive()); + assert!(s.elements.is_none()); + } + + #[test] + fn parses_elements_preserving_order() { + let s: FhirSchema = serde_json::from_str( + r#"{ "elements": { "z": {"type": "string"}, "a": {"type": "string"} } }"#, + ) + .unwrap(); + let keys: Vec<&str> = s.elements.as_ref().unwrap().keys().map(|k| k.as_str()).collect(); + assert_eq!(keys, vec!["z", "a"], "element declaration order must be preserved"); + } + + #[test] + fn tolerates_unknown_keys() { + let s: FhirSchema = + serde_json::from_str(r#"{ "type": "string", "somethingNew": 42 }"#).unwrap(); + assert_eq!(s.type_.as_deref(), Some("string")); + } + + #[test] + fn parses_slicing_shape() { + let s: FhirSchema = serde_json::from_str( + r#"{ + "slicing": { + "slices": { + "home": { + "match": { "type": "pattern", "value": { "use": "home" } }, + "min": 1, "max": 1, + "schema": { "required": ["city"] } + } + } + } + }"#, + ) + .unwrap(); + let slicing = s.slicing.unwrap(); + let home = &slicing.slices["home"]; + assert_eq!(home.min, Some(1)); + let m = home.match_.as_ref().unwrap(); + assert_eq!(m.type_.as_deref(), Some("pattern")); + assert_eq!(m.value.as_ref().unwrap()["use"], "home"); + assert_eq!( + home.schema.as_ref().unwrap().required.as_ref().unwrap(), + &vec!["city".to_string()] + ); + } +} diff --git a/crates/fhir-validator/tests/conformance.rs b/crates/fhir-validator/tests/conformance.rs new file mode 100644 index 000000000..ceeaea5c6 --- /dev/null +++ b/crates/fhir-validator/tests/conformance.rs @@ -0,0 +1,137 @@ +//! Runner for the vendored upstream FHIR Schema conformance suite +//! (`tests/fixtures/upstream/*.json` — see UPSTREAM.md there for provenance). +//! +//! Contract (mirrors `fhirschema-js/tests/tests.test.js`): +//! - the fixture's `schemas` map becomes the resolver, keyed by name/url +//! - each case validates `data` with the root schema auto-resolved from +//! `data.resourceType`, plus the case's extra `schemas` as profiles +//! - the emitted error list must equal the expected `errors` (or `[]` when +//! the key is absent) under **exact ordered deep-equality** — `type`, +//! `path`, and `message` strings included +//! - `skip: true` cases are skipped; `focus: true` is ignored (all cases run) + +use helios_fhir_validator::{ + FhirSchema, SchemaRegistry, UnknownProfilePolicy, ValidationOptions, Validator, +}; +use serde::Deserialize; +use serde_json::Value; +use std::fs; +use std::path::PathBuf; +use std::sync::Arc; + +#[derive(Deserialize)] +struct FixtureFile { + #[allow(dead_code)] + desc: Option, + schemas: serde_json::Map, + tests: Vec, +} + +#[derive(Deserialize)] +struct FixtureCase { + desc: Option, + /// Extra profile schema names to layer on top of the auto-resolved root. + #[serde(default)] + schemas: Vec, + data: Value, + /// Absent ⇒ the case must validate clean. + errors: Option, + #[serde(default)] + skip: bool, +} + +fn fixture_dir(sub: &str) -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures").join(sub) +} + +/// Runs every case in one fixture file; panics with a per-case diff report on +/// any mismatch. +fn run_fixture_file(sub: &str, name: &str) { + let path = fixture_dir(sub).join(name); + let raw = fs::read_to_string(&path) + .unwrap_or_else(|e| panic!("cannot read fixture {}: {e}", path.display())); + let file: FixtureFile = serde_json::from_str(&raw) + .unwrap_or_else(|e| panic!("cannot parse fixture {name}: {e}")); + + let mut registry = SchemaRegistry::new(); + for (key, def) in &file.schemas { + let schema: FhirSchema = serde_json::from_value(def.clone()) + .unwrap_or_else(|e| panic!("fixture {name}: schema '{key}' failed to parse: {e}")); + registry.insert_named(key.clone(), schema); + } + let validator = Validator::new(Arc::new(registry)); + + let mut failures: Vec = Vec::new(); + let mut ran = 0usize; + for (i, case) in file.tests.iter().enumerate() { + if case.skip { + continue; + } + ran += 1; + let desc = case.desc.clone().unwrap_or_else(|| format!("case #{i}")); + let opts = ValidationOptions { + profiles: case.schemas.clone(), + // Fixtures never carry meta.profile, and unresolvable references + // in fixtures are fixture bugs — surface them as errors. + use_meta_profiles: true, + unknown_profile: UnknownProfilePolicy::Error, + }; + let outcome = validator.validate_sync(&case.data, &opts); + let actual = serde_json::to_value(&outcome.errors).expect("errors serialize"); + let expected = case.errors.clone().unwrap_or_else(|| Value::Array(vec![])); + if actual != expected { + failures.push(format!( + " ✗ {desc}\n expected: {expected}\n actual: {actual}" + )); + } + } + + assert!( + failures.is_empty(), + "{name}: {}/{} cases failed:\n{}", + failures.len(), + ran, + failures.join("\n") + ); +} + +fn run_upstream(name: &str) { + run_fixture_file("upstream", name) +} + +#[test] +fn upstream_1_elements() { + run_upstream("1_elements.json"); +} + +#[test] +fn upstream_2_base() { + run_upstream("2_base.json"); +} + +#[test] +fn upstream_3_choices() { + run_upstream("3_choices.json"); +} + +#[test] +fn upstream_4_required() { + run_upstream("4_required.json"); +} + +#[test] +#[ignore = "Phase 2: slicing (engine/slicing.rs) not implemented yet"] +fn upstream_5_slices() { + run_upstream("5_slices.json"); +} + +#[test] +#[ignore = "Phase 2: extensions→slicing compilation not implemented yet"] +fn upstream_6_extensions() { + run_upstream("6_extensions.json"); +} + +#[test] +fn upstream_7_bundles() { + run_upstream("7_bundles.json"); +} diff --git a/crates/fhir-validator/tests/fixtures/upstream/1_elements.json b/crates/fhir-validator/tests/fixtures/upstream/1_elements.json new file mode 100644 index 000000000..9bd5aec9f --- /dev/null +++ b/crates/fhir-validator/tests/fixtures/upstream/1_elements.json @@ -0,0 +1,281 @@ +{ + "desc": "test elements and arrays", + "schemas": { + "string": { + "kind": "primitive-type" + }, + "HumanName": { + "elements": { + "given": { + "type": "string", + "array": true + }, + "family": {"type": "string"}, + "nested": { + "elements": { + "element": {"type": "string"} + } + } + } + }, + "Patient": { + "elements": { + "resourceType": {"type": "string"}, + "status": {"type": "string"}, + "multielement": { + "type": "string", + "array": true + }, + "multielementObject": { + "array": true, + "elements": { + "element": { + "type": "string" + } + } + }, + "name": { + "type": "HumanName" + }, + "nested": { + "elements": { + "element": { + "type": "string" + }, + "nestedTwice": { + "elements": { + "element": { + "type": "string" + } + } + } + } + } + } + } + }, + "tests": [ + { + "desc": "basic element", + "data": { + "resourceType": "Patient", + "status": "active" + } + }, + { + "desc": "unknown element", + "data": { + "resourceType": "Patient", + "unknown": "value" + }, + "errors": [ + { + "message": "unknown is unknown", + "type": "unknown-element", + "path": "Patient.unknown" + } + ] + }, + { + "desc": "nested element", + "data": { + "resourceType": "Patient", + "nested": { + "element": "value" + } + } + }, + { + "desc": "nested unknown element", + "data": { + "resourceType": "Patient", + "nested": { + "unknown": "not-ok" + } + }, + "errors": [ + { + "message": "unknown is unknown", + "path": "Patient.nested.unknown", + "type": "unknown-element" + } + ] + }, + { + "desc": "nested twice element", + "data": { + "resourceType": "Patient", + "nested": { + "nestedTwice": { + "element": "ok" + } + } + } + }, + { + "desc": "nested twice unknown element", + "data": { + "resourceType": "Patient", + "nested": { + "nestedTwice": { + "unknown": "not-ok" + } + } + }, + "errors": [ + { + "message": "unknown is unknown", + "path": "Patient.nested.nestedTwice.unknown", + "type": "unknown-element" + } + ] + }, + { + "desc": "complex type", + "data": { + "resourceType": "Patient", + "name": { + "family": "ok" + } + } + }, + { + "desc": "complex type unknown", + "data": { + "resourceType": "Patient", + "name": { + "unknown": "not-ok" + } + }, + "errors": [ + { + "message": "unknown is unknown", + "path": "Patient.name.unknown", + "type": "unknown-element" + } + ] + }, + { + "desc": "complex type nested element", + "data": { + "resourceType": "Patient", + "name": { + "nested": { + "element": "ok" + } + } + } + }, + { + "desc": "complex type nested unknown", + "data": { + "resourceType": "Patient", + "name": { + "nested": { + "unknown": "not-ok" + } + } + }, + "errors": [ + { + "message": "unknown is unknown", + "path": "Patient.name.nested.unknown", + "type": "unknown-element" + } + ] + }, + { + "desc": "array", + "data": { + "resourceType": "Patient", + "multielement": [ + "a", + "b" + ] + } + }, + { + "desc": "not array", + "data": { + "resourceType": "Patient", + "multielement": "a" + }, + "errors": [ + { + "message": "multielement is not array", + "path": "Patient.multielement", + "type": "not-array" + } + ] + }, + { + "desc": "singular is array", + "data": { + "resourceType": "Patient", + "status": [ + "active" + ] + }, + "errors": [ + { + "message": "status is not singular", + "path": "Patient.status", + "type": "not-singular" + } + ] + }, + { + "desc": "array object", + "data": { + "resourceType": "Patient", + "multielementObject": [ + { + "element": "a" + }, + { + "element": "b" + } + ] + } + }, + { + "desc": "not array object", + "data": { + "resourceType": "Patient", + "multielementObject": {"element": "a"} + }, + "errors": [ + { + "message": "multielementObject is not array", + "path": "Patient.multielementObject", + "type": "not-array" + } + ] + }, + { + "desc": "complex type wrong not-array", + "data": { + "resourceType": "Patient", + "name": {"given": "ok"} + }, + "errors": [ + { + "message": "given is not array", + "path": "Patient.name.given", + "type": "not-array" + } + ] + }, + {"desc": "complex type wrong array", + "data": { + "resourceType": "Patient", + "name": {"family": ["ok"]} + }, + "errors": [{ + "message": "family is not singular", + "path": "Patient.name.family", + "type": "not-singular" + }] + } + ] +} diff --git a/crates/fhir-validator/tests/fixtures/upstream/2_base.json b/crates/fhir-validator/tests/fixtures/upstream/2_base.json new file mode 100644 index 000000000..95109ba5f --- /dev/null +++ b/crates/fhir-validator/tests/fixtures/upstream/2_base.json @@ -0,0 +1,96 @@ +{ + "desc": "base keyword - inheritance", + "schemas": { + "string": { "kind": "primitive-type" }, + "Complex": { + "elements": { + "value": {"type": "string"} + } + }, + "ComplexChild": { + "base": "Complex", + "elements": { + "quantity": {"type": "string"} + } + }, + "Resource": { + "elements": { + "resourceType": {"type": "string"}, + "id": {"type": "string"} + } + }, + "DomainResource": { + "base": "Resource", + "elements": { + "narrative": {"type": "string"} + } + }, + "Patient": { + "base": "DomainResource", + "elements": { + "status": {"type": "string"}, + "complex": {"type": "ComplexChild"} + } + } + }, + "tests": [ + { + "desc": "own property", + "data": { + "resourceType": "Patient", + "status": "active" + } + }, + { + "desc": "father property", + "data": { + "resourceType": "Patient", + "narrative": "ok" + } + }, + { + "desc": "grandfather property", + "data": { + "resourceType": "Patient", + "id": "pt-1" + } + }, + { + "desc": "complex type property", + "data": { + "resourceType": "Patient", + "complex": {"quantity": "ok"} + } + }, + { + "desc": "complex type inherited property", + "data": { + "resourceType": "Patient", + "complex": {"value": "ok"} + } + }, + { + "desc": "complex type property and inherited property", + "data": { + "resourceType": "Patient", + "complex": {"value": "ok", "quantity": "ok"} + } + }, + { + "desc": "complex type unknonw", + "data": { + "resourceType": "Patient", + "complex": { + "unknown": "not-ok", + "value": "ok", + "quantity": "ok" + } + }, + "errors": [{ + "message": "unknown is unknown", + "path": "Patient.complex.unknown", + "type": "unknown-element" + }] + } + ] +} diff --git a/crates/fhir-validator/tests/fixtures/upstream/3_choices.json b/crates/fhir-validator/tests/fixtures/upstream/3_choices.json new file mode 100644 index 000000000..bc359658e --- /dev/null +++ b/crates/fhir-validator/tests/fixtures/upstream/3_choices.json @@ -0,0 +1,98 @@ +{ + "desc": "choices keyword", + "schemas": { + "string": { "kind": "primitive-type" }, + "integer": { "kind": "primitive-type" }, + "Resource": { + "elements": { + "resourceType": {"type": "string"}, + "choice": { + "choices": ["choiceString", "choiceInteger"] + }, + "choiceString": {"choiceOf": "choice", "type": "string"}, + "choiceInteger": {"choiceOf": "choice", "type": "integer"} + } + }, + "Profile": { + "base": "Resource", + "elements": { + "choice": { + "choices": ["choiceString"] + } + } + } + }, + "tests": [ + { + "desc": "choice string", + "data": { + "resourceType": "Resource", + "choiceString": "ok" + } + }, + { + "desc": "choice integer", + "data": { + "resourceType": "Resource", + "choiceInteger": 1 + } + }, + { + "desc": "wrong choice", + "data": { + "resourceType": "Resource", + "choice": "ups" + }, + "errors": [{ + "type": "unknown-element", + "path": "Resource.choice", + "message": "choice is unknown" + }] + }, + { + "desc": "choices conflict - two at the same time", + "data": { + "resourceType": "Resource", + "choiceString": "ok", + "choiceInteger": 1 + }, + "errors": [{ + "type": "choice", + "path": "Resource.choice", + "message": "only one choice for choice allowed, but multiple found: choiceString, choiceInteger" + }] + }, + { + "desc": "unknown element", + "data": { + "resourceType": "Resource", + "unknown": "ups" + }, + "errors": [{ + "type": "unknown-element", + "path": "Resource.unknown", + "message": "unknown is unknown" + }] + }, + { + "desc": "constraint choice ok", + "data": { + "resourceType": "Profile", + "choiceString": "ok" + } + }, + { + "desc": "constraint choice not ok", + "focus": true, + "data": { + "resourceType": "Profile", + "choiceInteger": 1 + }, + "errors": [{ + "message": "choiceInteger is excluded choice", + "path": "Profile.choiceInteger", + "type": "choice-excluded" + }] + } + ] +} diff --git a/crates/fhir-validator/tests/fixtures/upstream/4_required.json b/crates/fhir-validator/tests/fixtures/upstream/4_required.json new file mode 100644 index 000000000..8c894d0d6 --- /dev/null +++ b/crates/fhir-validator/tests/fixtures/upstream/4_required.json @@ -0,0 +1,165 @@ +{ + "desc": "required keyword", + "schemas": { + "string": { "kind": "primitive-type" }, + "Resource": { + "required": ["requiredElement", "anotherRequiredElement"], + "elements": { + "resourceType": {"type": "string"}, + "requiredElement": {"type": "string"}, + "anotherRequiredElement": {"type": "string"}, + "optionalElement": {"type": "string"}, + "elementForProfile": {"type": "string"} + } + }, + "Profile": { + "base": "Resource", + "required": ["elementForProfile", "localElement"], + "elements": { + "localElement": {"type": "string"} + } + } + }, + "tests": [ + { + "desc": "required satisfied", + "data": { + "resourceType": "Resource", + "requiredElement": "ok", + "anotherRequiredElement": "ok" + } + }, + { + "desc": "one required missed element", + "data": { + "resourceType": "Resource", + "requiredElement": "ok" + }, + "errors": [ + { + "message": "anotherRequiredElement is required", + "type": "required", + "path": "Resource.anotherRequiredElement" + } + ] + }, + { + "desc": "another required missed element", + "data": { + "resourceType": "Resource", + "anotherRequiredElement": "ok" + }, + "errors": [ + { + "message": "requiredElement is required", + "type": "required", + "path": "Resource.requiredElement" + } + ] + }, + { + "desc": "both required missed element", + "data": { + "resourceType": "Resource" + }, + "errors": [ + { + "message": "requiredElement is required", + "type": "required", + "path": "Resource.requiredElement" + }, + { + "message": "anotherRequiredElement is required", + "type": "required", + "path": "Resource.anotherRequiredElement" + } + ] + }, + { + "desc": "required ok in profile", + "data": { + "resourceType": "Profile", + "localElement": "ok", + "elementForProfile": "ok", + "requiredElement": "ok", + "anotherRequiredElement": "ok" + } + }, + { + "desc": "one parent required missed in profile", + "data": { + "resourceType": "Profile", + "localElement": "ok", + "elementForProfile": "ok", + "anotherRequiredElement": "ok" + }, + "errors": [ + { + "message": "requiredElement is required", + "type": "required", + "path": "Profile.requiredElement" + } + ] + }, + { + "desc": "both parent required missed in profile", + "data": { + "resourceType": "Profile", + "localElement": "ok", + "elementForProfile": "ok" + }, + "errors": [ + { + "message": "requiredElement is required", + "type": "required", + "path": "Profile.requiredElement" + }, + { + "message": "anotherRequiredElement is required", + "type": "required", + "path": "Profile.anotherRequiredElement" + } + ] + }, + { + "desc": "elementForProfile missed in profile", + "data": { + "resourceType": "Profile", + "requiredElement": "ok", + "anotherRequiredElement": "ok", + "localElement": "ok" + }, + "errors": [ + { + "message": "elementForProfile is required", + "type": "required", + "path": "Profile.elementForProfile" + } + ] + }, + { + "desc": "all required missed in profile", + "data": { + "resourceType": "Profile", + "localElement": "ok" + }, + "errors": [ + { + "message": "requiredElement is required", + "type": "required", + "path": "Profile.requiredElement" + }, + { + "message": "anotherRequiredElement is required", + "type": "required", + "path": "Profile.anotherRequiredElement" + }, + { + "message": "elementForProfile is required", + "path": "Profile.elementForProfile", + "type": "required" + } + ] + } + ] +} diff --git a/crates/fhir-validator/tests/fixtures/upstream/5_slices.json b/crates/fhir-validator/tests/fixtures/upstream/5_slices.json new file mode 100644 index 000000000..453af4163 --- /dev/null +++ b/crates/fhir-validator/tests/fixtures/upstream/5_slices.json @@ -0,0 +1,164 @@ +{ + "desc": "slices keyword", + "schemas": { + "string": { "kind": "primitive-type" }, + "Resource": { + "elements": { + "resourceType": {"type": "string"}, + "address": { + "array": true, + "elements": { + "use": {"type": "string"}, + "city": {"type": "string"}, + "postalCode": {"type": "string"}, + "line": {"type": "string", "array": true} + } + } + } + }, + "Profile": { + "base": "Resource", + "elements": { + "address": { + "slicing": { + "slices": { + "home": { + "match": {"type": "pattern", "value": {"use": "home"}}, + "min": 1, + "max": 1, + "schema": { "required": ["city"] } + }, + "work": { + "min": 0, + "max": 2, + "match": {"type": "pattern", "value": {"use": "work"}}, + "schema": {"required": ["city", "postalCode"]} + } + } + } + } + } + } + }, + "tests": [ + { + "desc": "valid slices", + "data": { + "resourceType": "Profile", + "address": [ + {"use": "home", "city": "CITY"}, + {"use": "work", "city": "CITY", "postalCode": "CODE"} + ] + } + }, + { + "desc": "invalid slice work", + "data": { + "resourceType": "Profile", + "address": [ + {"use": "home", "city": "CITY"}, + {"use": "work", "postalCode": "CODE"} + ] + }, + "errors": [ + { + "message": "city is required", + "path": "Profile.address.1.city", + "type": "required" + } + ] + }, + { + "desc": "invalid slice home", + "data": { + "resourceType": "Profile", + "address": [ + {"use": "home" }, + {"use": "work", "city": "CITY", "postalCode": "CODE"} + ] + }, + "errors": [ + { + "message": "city is required", + "path": "Profile.address.0.city", + "type": "required" + } + ] + }, + { + "desc": "invalid both slices", + "data": { + "resourceType": "Profile", + "address": [ + {"use": "home" }, + {"use": "work", "postalCode": "CODE"} + ] + }, + "errors": [ + { + "message": "city is required", + "path": "Profile.address.0.city", + "type": "required" + }, + { + "message": "city is required", + "path": "Profile.address.1.city", + "type": "required" + } + ] + }, + { + "desc": "missed home slice", + "data": { + "resourceType": "Profile", + "address": [ + {"use": "work", "city": "CITY", "postalCode": "CODE"} + ] + }, + "errors": [ + { + "message": "Slice defines the following min cardinality: '1', actual cardinality: '0'", + "path": "Profile.address", + "type": "slice-cardinality" + } + ] + }, + { + "desc": "extra home slice", + "data": { + "resourceType": "Profile", + "address": [ + {"use": "home", "city": "CITY", "postalCode": "CODE"}, + {"use": "home", "city": "CITY2", "postalCode": "CODE2"} + ] + }, + "errors": [ + { + "message": "Slice defines the following max cardinality: '1', actual cardinality: '2'", + "path": "Profile.address", + "type": "slice-cardinality" + } + ] + }, + { + "desc": "extra work slice", + "data": { + "resourceType": "Profile", + "address": [ + {"use": "home", "city": "CITY", "postalCode": "CODE"}, + {"use": "work", "city": "CITY1", "postalCode": "CODE1"}, + {"use": "work", "city": "CITY2", "postalCode": "CODE3"}, + {"use": "work", "city": "CITY2", "postalCode": "CODE3"} + ] + }, + "errors": [ + { + "message": "Slice defines the following max cardinality: '2', actual cardinality: '3'", + "path": "Profile.address", + "type": "slice-cardinality" + } + ] + } + + ] +} diff --git a/crates/fhir-validator/tests/fixtures/upstream/6_extensions.json b/crates/fhir-validator/tests/fixtures/upstream/6_extensions.json new file mode 100644 index 000000000..97f115052 --- /dev/null +++ b/crates/fhir-validator/tests/fixtures/upstream/6_extensions.json @@ -0,0 +1,115 @@ +{ + "desc": "extensions", + "schemas": { + "string": { "kind": "primitive-type" }, + "http://hl7.org/us-core-race": { + "kind": "extension", + "elements": { + "value": { + "choices": ["valueString"] + } + } + }, + "Extension": { + "elements": { + "url": {"type": "string"}, + "value": {"choices": ["valueString", "valueCode"]}, + "valueString": {"choiceOf": "value", "type": "string"}, + "valueCode": {"choiceOf": "value", "type": "string"} + } + }, + "Resource": { + "extensions": { + "us-core-race": { + "url": "http://hl7.org/us-core-race", + "max": 1, + "min": 1 + } + }, + "elements": { + "resourceType": { "type": "string" }, + "extension": { "type": "Extension", "array": true }, + "element": { "type": "string" } + } + } + }, + "tests": [ + { + "desc": "unknown extension - just check the shape", + "data": { + "resourceType": "Resource", + "extension": [ + { + "url": "unknown", + "valueString": "string" + }, + { + "url": "unknown", + "valueCode": "code" + }, + { + "url": "http://hl7.org/us-core-race", + "valueString": "ok" + } + ] + } + }, + { + "desc": "missed extension (no extension element)", + "skip": true, + "comment": "Shall we support it or it should be combination of required and slices like in JSON schema?", + "data": { + "resourceType": "Resource" + }, + "errors": [ + { + "message": "Slice defines the following min cardinality: '1', actual cardinality: '0'", + "path": "Resource.extension", + "type": "slice-cardinality" + } + ] + }, + { + "desc": "missed extension (with extension element)", + "data": { + "resourceType": "Resource", + "extension": [ + { + "url": "unknown", + "valueString": "string" + }, + { + "url": "unknown", + "valueCode": "code" + } + ] + }, + "errors": [ + { + "message": "Slice defines the following min cardinality: '1', actual cardinality: '0'", + "path": "Resource.extension", + "type": "slice-cardinality" + } + ] + }, + { + "desc": "broken extension schema", + "data": { + "resourceType": "Resource", + "extension": [ + { + "url": "http://hl7.org/us-core-race", + "valueCode": "wrong" + } + ] + }, + "errors": [ + { + "message": "valueCode is excluded choice", + "path": "Resource.extension.0.valueCode", + "type": "choice-excluded" + } + ] + } + ] +} diff --git a/crates/fhir-validator/tests/fixtures/upstream/7_bundles.json b/crates/fhir-validator/tests/fixtures/upstream/7_bundles.json new file mode 100644 index 000000000..11dd13059 --- /dev/null +++ b/crates/fhir-validator/tests/fixtures/upstream/7_bundles.json @@ -0,0 +1,96 @@ +{ + "desc": "bundles", + "schemas": { + "string": { "kind": "primitive-type" }, + "Resource": { + "elements": { + "resourceType": { "type": "string" }, + "contained": { + "array": true, + "type": "Resource" + } + } + }, + "Patient": { + "base": "Resource", + "elements": {"name": { "type": "string" }} + }, + "Encounter": { + "base": "Resource", + "elements": {"type": { "type": "string" }} + }, + "Bundle": { + "base": "Resource", + "elements": { + "entry": { + "array": true, + "elements": { + "resource": { + "type": "Resource" + } + } + } + } + } + }, + "tests": [ + { + "desc": "ok bundle", + "data": { + "resourceType": "Bundle", + "entry": [ + {"resource": {"resourceType": "Patient", "name": "John"}}, + {"resource": {"resourceType": "Encounter", "type": "in-patient"}} + ] + } + }, + { + "desc": "pt contained", + "data": { + "resourceType": "Patient", + "contained": [ + {"resourceType": "Encounter", "type": "in-patient"} + ] + } + }, + { + "desc": "enc contained", + "data": { + "resourceType": "Encounter", + "contained": [ + {"resourceType": "Patient", "name": "John"} + ] + } + }, + { + "desc": "broken bundle", + "data": { + "resourceType": "Bundle", + "entry": [ + {"resource": {"resourceType": "Patient", "extra": "ups"}} + ] + }, + "errors": + [{ + "message": "extra is unknown", + "path": "Bundle.entry.0.resource.extra", + "type": "unknown-element" + }] + }, + { + "desc": "broken bundle", + "data": { + "resourceType": "Bundle", + "entry": [ + {"resource": {"resourceType": "Encounter", "extra": "ups"}} + ] + }, + "errors": + [{ + "message": "extra is unknown", + "path": "Bundle.entry.0.resource.extra", + "type": "unknown-element" + }] + } + ] +} diff --git a/crates/fhir-validator/tests/fixtures/upstream/UPSTREAM.md b/crates/fhir-validator/tests/fixtures/upstream/UPSTREAM.md new file mode 100644 index 000000000..7da0f48e8 --- /dev/null +++ b/crates/fhir-validator/tests/fixtures/upstream/UPSTREAM.md @@ -0,0 +1,27 @@ +# Upstream FHIR Schema conformance fixtures + +The `*.json` files in this directory are vendored, unmodified, from the +**FHIR Schema** project's language-agnostic conformance suite: + +- Source repository: https://github.com/dougc95/fhir-schema (fork of + https://github.com/fhir-schema/fhir-schema) +- Vendored from local clone at commit: `dff20652992ee2f27d46122598acdf4356298c92` + (2026-07-03) +- Upstream path: `tests/*.json` +- License: MIT (Copyright (c) 2018-2023 Nikolai Ryzhikov, Ewout Kramer, + FHIR Community, Health Samurai, Firely) — see the upstream `LICENSE.md`. + +These fixtures are the behavioral contract for the validation engine in this +crate: each file bundles inline schemas, resource instances, and the exact +error objects (`{type, path, message}`) a conforming validator must emit, in +order. The harness in `../../conformance.rs` runs them with **exact ordered +deep-equality** — message strings included. + +Do not edit these files. Helios-specific behavior beyond the upstream contract +(primitive value checks, `excluded`, numeric cardinality when absent, slicing +rules, etc.) is pinned by our own fixtures in `../extended/` instead. + +Fixture semantics honored by the harness: +- a test without an `errors` key must validate clean (`[]`) +- `skip: true` tests are skipped (known-unimplemented upstream behavior) +- `focus: true` is ignored — every non-skipped test always runs From b1d75c67c7830a44183eacd4634a02ec76882955 Mon Sep 17 00:00:00 2001 From: Doug Date: Sun, 5 Jul 2026 19:38:17 -0400 Subject: [PATCH 2/7] feat(validator): slicing, extensions, sidecars, and elementReference MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 2 — the full upstream conformance suite is now green (all 7 fixtures): - engine/slicing.rs: mark/sweep slicing with verbatim cardinality messages; closed/openAtEnd/ordered rules, @default absorption, and prohibited max:0 slices (all beyond the reference validator, pinned by extended fixtures) - extensions maps compile into synthetic {url}-matched slicing on the extension element at validation time - _field primitive-extension sidecars: base-element pairing, shape mirroring, Element-part validation with null gaps - elementReference resolution for recursive structures (Questionnaire.item) - extended fixture suite pinning Helios-specific semantics (excluded, numeric min/max, fixed/pattern wording, slicing rules, sidecars, elementReference, required-satisfied-by-choice-branch) Co-Authored-By: Claude Opus 4.8 (1M context) --- crates/fhir-validator/src/engine/errors.rs | 73 +++++- crates/fhir-validator/src/engine/mod.rs | 1 + crates/fhir-validator/src/engine/slicing.rs | 214 +++++++++++++++++ crates/fhir-validator/src/engine/walk.rs | 221 ++++++++++++++++- crates/fhir-validator/tests/common/mod.rs | 94 ++++++++ crates/fhir-validator/tests/conformance.rs | 98 +------- crates/fhir-validator/tests/extended.rs | 50 ++++ .../tests/fixtures/extended/cardinality.json | 40 +++ .../fixtures/extended/choice_required.json | 33 +++ .../fixtures/extended/element_reference.json | 84 +++++++ .../tests/fixtures/extended/excluded.json | 54 +++++ .../fixtures/extended/fixed_pattern.json | 54 +++++ .../tests/fixtures/extended/sidecars.json | 120 +++++++++ .../fixtures/extended/slicing_rules.json | 227 ++++++++++++++++++ 14 files changed, 1249 insertions(+), 114 deletions(-) create mode 100644 crates/fhir-validator/src/engine/slicing.rs create mode 100644 crates/fhir-validator/tests/common/mod.rs create mode 100644 crates/fhir-validator/tests/extended.rs create mode 100644 crates/fhir-validator/tests/fixtures/extended/cardinality.json create mode 100644 crates/fhir-validator/tests/fixtures/extended/choice_required.json create mode 100644 crates/fhir-validator/tests/fixtures/extended/element_reference.json create mode 100644 crates/fhir-validator/tests/fixtures/extended/excluded.json create mode 100644 crates/fhir-validator/tests/fixtures/extended/fixed_pattern.json create mode 100644 crates/fhir-validator/tests/fixtures/extended/sidecars.json create mode 100644 crates/fhir-validator/tests/fixtures/extended/slicing_rules.json diff --git a/crates/fhir-validator/src/engine/errors.rs b/crates/fhir-validator/src/engine/errors.rs index cb9b903d6..69012b08c 100644 --- a/crates/fhir-validator/src/engine/errors.rs +++ b/crates/fhir-validator/src/engine/errors.rs @@ -49,6 +49,11 @@ pub enum ErrorKind { // ------------------------------------------------------------------ /// A primitive value failed its type-class or regex check. PrimitiveValue, + /// An array item matched no slice under `rules: closed`, or an unmatched + /// item preceded matched items under `rules: openAtEnd`. + SliceUnmatched, + /// Matched items violate the declared slice order (`ordered: true`). + SliceOrder, /// A referenced schema (root, `base`, or complex `type`) could not be /// resolved. The reference validator throws here; we report and continue. UnknownSchema, @@ -179,17 +184,48 @@ pub(crate) fn msg_slice_max(max: u64, actual: u64) -> String { format!("Slice defines the following max cardinality: '{max}', actual cardinality: '{actual}'") } -/// [helios] fixed-value mismatch. The reference validator's wording coerces -/// JS objects to `[object Object]`; no fixture pins it, so we render JSON. +/// Render a value for message text: bare strings (like the reference +/// validator's template interpolation), compact JSON for everything else. +pub(crate) fn render_value(v: &Value) -> String { + match v { + Value::String(s) => s.clone(), + other => other.to_string(), + } +} + +/// [helios] fixed-value mismatch. Matches the reference wording for scalars; +/// composites render as compact JSON (the reference coerces JS objects to +/// `[object Object]`, which no fixture pins). pub(crate) fn msg_fixed_value(expected: &Value, actual: &Value) -> String { format!( - "Expected value to be exactly equal to 'fixed' pattern '{expected}', but got: '{actual}'" + "Expected value to be exactly equal to 'fixed' pattern '{}', but got: '{}'", + render_value(expected), + render_value(actual) ) } -/// [helios] pattern-value mismatch (same JSON-rendering rationale as fixed). +/// [helios] pattern-value mismatch (same rendering rationale as fixed). pub(crate) fn msg_pattern_value(expected: &Value, actual: &Value) -> String { - format!("Expected value to match 'pattern' '{expected}', but got: '{actual}'") + format!( + "Expected value to match 'pattern' '{}', but got: '{}'", + render_value(expected), + render_value(actual) + ) +} + +/// [helios] `rules: closed` violation, at the unmatched item's path. +pub(crate) fn msg_slice_closed_unmatched() -> String { + "item does not match any slice in closed slicing".to_string() +} + +/// [helios] `rules: openAtEnd` violation, at the unmatched item's path. +pub(crate) fn msg_slice_open_at_end() -> String { + "unmatched item must be at the end of the array (openAtEnd slicing)".to_string() +} + +/// [helios] `ordered: true` violation, at the out-of-order item's path. +pub(crate) fn msg_slice_order(slice: &str) -> String { + format!("slice '{slice}' is out of order (ordered slicing)") } /// [helios] a schema reference could not be resolved (reference validator @@ -253,6 +289,33 @@ mod tests { assert_eq!(msg_max(2, 3), "expected 2 < 3"); } + /// Helios-owned wording, pinned here and by the extended fixtures. + #[test] + fn helios_messages_match_extended_fixture_literals() { + assert_eq!( + msg_fixed_value(&json!("male"), &json!("female")), + "Expected value to be exactly equal to 'fixed' pattern 'male', but got: 'female'" + ); + assert_eq!( + msg_pattern_value(&json!({"system": "http://x"}), &json!({"text": "M"})), + "Expected value to match 'pattern' '{\"system\":\"http://x\"}', but got: '{\"text\":\"M\"}'" + ); + assert_eq!( + msg_slice_closed_unmatched(), + "item does not match any slice in closed slicing" + ); + assert_eq!( + msg_slice_open_at_end(), + "unmatched item must be at the end of the array (openAtEnd slicing)" + ); + assert_eq!(msg_slice_order("home"), "slice 'home' is out of order (ordered slicing)"); + assert_eq!( + serde_json::to_value(ErrorKind::SliceUnmatched).unwrap(), + json!("slice-unmatched") + ); + assert_eq!(serde_json::to_value(ErrorKind::SliceOrder).unwrap(), json!("slice-order")); + } + #[test] fn serializes_to_conformance_shape() { let err = ValidationError::new( diff --git a/crates/fhir-validator/src/engine/mod.rs b/crates/fhir-validator/src/engine/mod.rs index a414cd15d..00a166f47 100644 --- a/crates/fhir-validator/src/engine/mod.rs +++ b/crates/fhir-validator/src/engine/mod.rs @@ -9,6 +9,7 @@ mod errors; mod path; +mod slicing; mod walk; pub use errors::{ErrorKind, Severity, ValidationError}; diff --git a/crates/fhir-validator/src/engine/slicing.rs b/crates/fhir-validator/src/engine/slicing.rs new file mode 100644 index 000000000..838bc5282 --- /dev/null +++ b/crates/fhir-validator/src/engine/slicing.rs @@ -0,0 +1,214 @@ +//! Array slicing: the mark/sweep pass. +//! +//! For every slicing-bearing schema in the element's set: +//! - **mark**: match each array item against each slice's pattern +//! (`_.isMatch`-style partial deep equality), counting matches per slice; +//! - items matched by at least one slice **with a schema** are validated +//! once, cooperatively, against the element set extended with *all* their +//! matched slice schemas (the reference validator validates once per +//! matched slice against `elset ∪ that slice`, and then re-validates every +//! item in the plain pass — duplicating base-set errors; we consume +//! matched items instead, which is indistinguishable on the conformance +//! suite and saner on real resources); +//! - **sweep**: `rules: closed` (with `@default` absorption) and +//! `rules: openAtEnd` violations, `ordered` violations, then per-slice +//! `min`/`max` cardinality in slice declaration order. +//! +//! Divergences from the reference validator (which implements only pattern +//! matching + cardinality): `closed`/`openAtEnd`/`ordered`/`@default` are +//! enforced (pinned by `tests/fixtures/extended/slicing_rules.json`), and a +//! `max: 0` prohibited slice is enforced (the reference skips falsy bounds). +//! +//! Match types other than `pattern` (`type`, `profile`, `binding`, +//! `resolve-ref`) are deliberately not evaluated yet — they arrive in a later +//! phase alongside the converter's discriminator translation. A slice whose +//! matcher we cannot evaluate matches nothing, and a slice with no `match` +//! at all (a constraining slice) also matches nothing. + +use super::errors::{self, ErrorKind}; +use super::walk::{add_schemas_to_set, is_partial_match, validate_node, SchemaSet, WalkCtx}; +use crate::schema::{Slice, Slicing}; +use indexmap::IndexMap; +use serde_json::Value; +use std::collections::HashSet; +use std::sync::Arc; + +/// The reserved slice name that absorbs unmatched items in closed slicing. +const DEFAULT_SLICE: &str = "@default"; + +struct LayerMark { + slicing: Slicing, + /// Matched-item count per slice, in declaration order. + counters: IndexMap, + /// For each array item, the slice names it matched (declaration order). + item_matches: Vec>, +} + +/// Run the slicing pass over an array element. Returns the indices of items +/// that were already fully validated (consumed) here. +pub(super) fn validate_slices( + ctx: &mut WalkCtx<'_>, + elset: &SchemaSet, + items: &[Value], +) -> HashSet { + let slicings: Vec = elset + .schemas() + .filter_map(|s| s.slicing.clone()) + .collect(); + if slicings.is_empty() { + return HashSet::new(); + } + + // ---- mark ---- + let mut marks: Vec = Vec::with_capacity(slicings.len()); + for slicing in slicings { + let mut counters: IndexMap = + slicing.slices.keys().map(|k| (k.clone(), 0)).collect(); + let mut item_matches: Vec> = vec![Vec::new(); items.len()]; + for (index, item) in items.iter().enumerate() { + for (name, slice) in &slicing.slices { + if name == DEFAULT_SLICE { + continue; + } + if slice_matches(slice, item) { + *counters.get_mut(name).expect("counter exists") += 1; + item_matches[index].push(name.clone()); + } + } + } + // `@default` (closed slicing only) absorbs items no other slice + // matched; count them so its own min/max can apply. + if slicing.slices.contains_key(DEFAULT_SLICE) + && slicing.rules.as_deref() == Some("closed") + { + let absorbed = item_matches.iter().filter(|m| m.is_empty()).count() as u64; + *counters.get_mut(DEFAULT_SLICE).expect("counter exists") = absorbed; + } + marks.push(LayerMark { slicing, counters, item_matches }); + } + + // ---- validate matched items (item order), consuming them ---- + let mut consumed: HashSet = HashSet::new(); + for (index, item) in items.iter().enumerate() { + let mut slice_schemas: Vec<(String, Arc)> = Vec::new(); + for mark in &marks { + for name in &mark.item_matches[index] { + if let Some(schema) = &mark.slicing.slices[name].schema { + slice_schemas.push((name.clone(), Arc::clone(schema))); + } + } + } + if slice_schemas.is_empty() { + continue; + } + let mut extended = elset.clone(); + for (name, schema) in &slice_schemas { + add_schemas_to_set(ctx, &mut extended, Arc::clone(schema), name); + } + ctx.path.push_index(index); + validate_node(ctx, &extended, item); + ctx.path.pop(); + consumed.insert(index); + } + + // ---- sweep: rules, order, cardinality (per layer, elset order) ---- + for mark in &marks { + let rules = mark.slicing.rules.as_deref().unwrap_or("open"); + + if rules == "closed" { + let default_slice = mark.slicing.slices.get(DEFAULT_SLICE); + for (index, item) in items.iter().enumerate() { + if !mark.item_matches[index].is_empty() { + continue; + } + match default_slice { + Some(Slice { schema: Some(schema), .. }) => { + let mut extended = elset.clone(); + add_schemas_to_set(ctx, &mut extended, Arc::clone(schema), DEFAULT_SLICE); + ctx.path.push_index(index); + validate_node(ctx, &extended, item); + ctx.path.pop(); + consumed.insert(index); + } + Some(_) => { + // @default without a schema: absorbed, nothing extra + // to check; the plain pass still validates the item. + } + None => { + ctx.path.push_index(index); + ctx.error( + ErrorKind::SliceUnmatched, + errors::msg_slice_closed_unmatched(), + ); + ctx.path.pop(); + } + } + } + } else if rules == "openAtEnd" + && let Some(last_matched) = + mark.item_matches.iter().rposition(|m| !m.is_empty()) + { + for index in 0..last_matched { + if mark.item_matches[index].is_empty() { + ctx.path.push_index(index); + ctx.error(ErrorKind::SliceUnmatched, errors::msg_slice_open_at_end()); + ctx.path.pop(); + } + } + } + + if mark.slicing.ordered == Some(true) { + let mut max_order_seen: Option = None; + for (index, matches) in mark.item_matches.iter().enumerate() { + let Some(first) = matches.first() else { + continue; + }; + let Some(order) = mark.slicing.slices[first].order else { + continue; + }; + if let Some(seen) = max_order_seen + && order < seen + { + ctx.path.push_index(index); + ctx.error(ErrorKind::SliceOrder, errors::msg_slice_order(first)); + ctx.path.pop(); + } + max_order_seen = Some(max_order_seen.unwrap_or(0).max(order)); + } + } + + for (name, slice) in &mark.slicing.slices { + let found = mark.counters[name]; + if let Some(min) = slice.min + && min > 0 + && found < min + { + ctx.error(ErrorKind::SliceCardinality, errors::msg_slice_min(min, found)); + } + // Unlike the reference validator (which skips falsy bounds), + // `max: 0` — a prohibited slice, a real FHIR pattern — is + // enforced. + if let Some(max) = slice.max + && found > max + { + ctx.error(ErrorKind::SliceCardinality, errors::msg_slice_max(max, found)); + } + } + } + + consumed +} + +/// Does an item belong to a slice? Only `pattern` matching is evaluated +/// today; a missing `match` (constraining slice) matches nothing, and a +/// `match` with no `value` matches everything (lodash `_.isMatch` semantics +/// for an empty source). +fn slice_matches(slice: &Slice, item: &Value) -> bool { + let Some(match_) = &slice.match_ else { + return false; + }; + match match_.value.as_ref() { + Some(pattern) => is_partial_match(item, pattern), + None => true, + } +} diff --git a/crates/fhir-validator/src/engine/walk.rs b/crates/fhir-validator/src/engine/walk.rs index 0d01159bb..4db52405b 100644 --- a/crates/fhir-validator/src/engine/walk.rs +++ b/crates/fhir-validator/src/engine/walk.rs @@ -51,7 +51,7 @@ const RESOURCE_TYPE_NAME: &str = "Resource"; /// A cooperative schema set (the *schemata*): insertion-ordered, ancestors /// first, deduplicated both by resolution key and by schema identity. #[derive(Clone, Default)] -struct SchemaSet { +pub(super) struct SchemaSet { map: IndexMap>, /// Identity dedup (`Arc` pointer): the same schema reachable under /// several aliases (name and canonical URL) is added once. @@ -75,20 +75,20 @@ impl SchemaSet { self.map.iter().map(|(k, v)| (k.as_str(), v)) } - fn schemas(&self) -> impl Iterator> { + pub(super) fn schemas(&self) -> impl Iterator> { self.map.values() } } -struct WalkCtx<'a> { +pub(super) struct WalkCtx<'a> { resolver: &'a dyn SchemaResolver, errors: Vec, deferred: Vec, - path: PathTracker, + pub(super) path: PathTracker, } impl WalkCtx<'_> { - fn error(&mut self, kind: ErrorKind, message: String) { + pub(super) fn error(&mut self, kind: ErrorKind, message: String) { self.errors.push(ValidationError::new(kind, self.path.render_dotted(), message)); } @@ -182,8 +182,9 @@ fn add_profile( /// Pull `schema` into `set` together with everything it references: /// its `base` chain first (ancestors-first ordering), then its complex -/// `type` (skipped for primitive leaves), then the schema itself. -fn add_schemas_to_set( +/// `type` (skipped for primitive leaves), then any `elementReference` +/// target, then the schema itself. +pub(super) fn add_schemas_to_set( ctx: &mut WalkCtx<'_>, set: &mut SchemaSet, schema: Arc, @@ -212,11 +213,43 @@ fn add_schemas_to_set( } } + // `elementReference` pulls another element's schema into the set — + // the mechanism behind recursive structures like Questionnaire.item. + // Cycles terminate via the identity dedup above. + if let Some(segments) = schema.element_reference.clone() { + let ref_key = segments.join("."); + match resolve_element_reference(ctx.resolver, &segments) { + Some(target) => add_schemas_to_set(ctx, set, target, &ref_key), + None => ctx.error(ErrorKind::UnknownSchema, errors::msg_unknown_schema(&ref_key)), + } + } + set.map.insert(key.to_string(), schema); } +/// Navigate an element reference: `["Questionnaire", "elements", "item"]` — +/// a root schema name followed by alternating `"elements"` / element-name +/// segments. +fn resolve_element_reference( + resolver: &dyn SchemaResolver, + segments: &[String], +) -> Option> { + let (root, rest) = segments.split_first()?; + let mut current = resolver.resolve(root)?; + let mut iter = rest.iter(); + while let Some(marker) = iter.next() { + if marker != "elements" { + return None; + } + let name = iter.next()?; + let next = current.elements.as_ref()?.get(name)?.clone(); + current = next; + } + Some(current) +} + /// Validate one data node against a cooperative schema set. -fn validate_node(ctx: &mut WalkCtx<'_>, set: &SchemaSet, data: &Value) { +pub(super) fn validate_node(ctx: &mut WalkCtx<'_>, set: &SchemaSet, data: &Value) { // Dynamic `Resource` resolution: an element typed `Resource` takes its // concrete schema from the nested `resourceType` at validation time // (Bundle.entry.resource, contained, Parameters.parameter.resource). @@ -252,7 +285,16 @@ fn validate_node(ctx: &mut WalkCtx<'_>, set: &SchemaSet, data: &Value) { for (key, value) in obj { ctx.path.push_key(key); - eval_element(ctx, set, &mut multi_choice, key, value); + // `_foo` sidecars carry the Element part (id/extension) of a + // primitive `foo` — they pair with the base element rather than + // resolving as elements themselves. + if let Some(base_key) = key.strip_prefix('_') + && !base_key.is_empty() + { + handle_sidecar(ctx, set, key, base_key, value); + } else { + eval_element(ctx, set, &mut multi_choice, key, value); + } ctx.path.pop(); } @@ -418,9 +460,15 @@ fn eval_element( } } - // Phase 2: slicing (mark/sweep) runs here, before per-item recursion. + // Slicing (mark/sweep). Items validated cooperatively with their + // matched slice schemas are consumed — the plain per-item pass below + // skips them so base-set errors are not emitted twice. + let consumed = super::slicing::validate_slices(ctx, &elset, items); for (index, item) in items.iter().enumerate() { + if consumed.contains(&index) { + continue; + } ctx.path.push_index(index); validate_node(ctx, &elset, item); ctx.path.pop(); @@ -430,6 +478,63 @@ fn eval_element( } } +/// Validate a `_foo` primitive-extension sidecar. +/// +/// The sidecar pairs with the base element `foo`: it is only legal when some +/// layer declares `foo` with a primitive type, it mirrors `foo`'s +/// array-vs-singular shape, and its content (per item for arrays, with +/// `null` gaps allowed) validates against the resolver's `Element` schema. +fn handle_sidecar( + ctx: &mut WalkCtx<'_>, + set: &SchemaSet, + key: &str, + base_key: &str, + value: &Value, +) { + // Collect the base element's sub-set; choice bookkeeping is discarded + // (a sidecar is not a branch occurrence). + let mut scratch: IndexMap> = IndexMap::new(); + let mut base_elset = SchemaSet::new(); + collect_schemas_for_element(ctx, set, &mut base_elset, &mut scratch, base_key); + + // Stray sidecar, or sidecar on a non-primitive element: unknown. + if base_elset.is_empty() || !base_elset.schemas().any(|s| s.is_primitive()) { + ctx.error(ErrorKind::UnknownElement, errors::msg_unknown_element(key)); + return; + } + + // The sidecar mirrors the base element's shape. + let expect_array = base_elset.schemas().any(|s| s.array == Some(true)); + match (expect_array, value.is_array()) { + (true, false) => ctx.error(ErrorKind::NotArray, errors::msg_not_array(key)), + (false, true) => ctx.error(ErrorKind::NotSingular, errors::msg_not_singular(key)), + _ => {} + } + + // Validate the Element part(s). Without a resolvable `Element` schema + // (core packs always carry one) the content is left unchecked. + let Some(element_schema) = ctx.resolver.resolve("Element") else { + return; + }; + let mut element_set = SchemaSet::new(); + add_schemas_to_set(ctx, &mut element_set, element_schema, "Element"); + + match value { + Value::Array(items) => { + for (index, item) in items.iter().enumerate() { + if item.is_null() { + continue; + } + ctx.path.push_index(index); + validate_node(ctx, &element_set, item); + ctx.path.pop(); + } + } + Value::Null => {} + other => validate_node(ctx, &element_set, other), + } +} + /// Gather the element sub-set for `key` across all layers: each layer's /// `elements[key]` (plus its transitive `base`/`type`), and — when the key /// is a choice branch — every layer's declarer schema for the choice group. @@ -440,14 +545,18 @@ fn collect_schemas_for_element( multi_choice: &mut IndexMap>, key: &str, ) { - // Phase 2: `extensions` maps compile into slicing here when - // key == "extension". - let layers: Vec<(String, Arc)> = set .iter() .map(|(name, schema)| (name.to_string(), Arc::clone(schema))) .collect(); + // Every layer's `extensions` map compiles into a synthetic slicing on + // the `extension` element, matched by `{url}` — authoring sugar rewritten + // into the core slicing primitive at validation time. + if key == "extension" { + compile_extensions(ctx, &layers, elset); + } + let mut choice_group: Option = None; for (layer_name, layer) in &layers { let Some(subschema) = layer.elements.as_ref().and_then(|els| els.get(key)) else { @@ -482,6 +591,92 @@ fn collect_schemas_for_element( } } +/// Compile `extensions` maps from every layer into one synthetic +/// slicing-bearing schema on the `extension` element. Each entry becomes a +/// slice matched by `{url: }` carrying the entry's `min`/`max`; its +/// slice schema is the entry (minus url/min/max) overlaid by the schema the +/// extension's url resolves to. +/// +/// An unresolvable extension url is reported as `unknown-schema` and the +/// entry-only schema is used (the reference validator throws instead). +fn compile_extensions( + ctx: &mut WalkCtx<'_>, + layers: &[(String, Arc)], + elset: &mut SchemaSet, +) { + use crate::schema::{Match, Slice, Slicing}; + + let mut slices: IndexMap = IndexMap::new(); + for (_, layer) in layers { + let Some(extensions) = &layer.extensions else { + continue; + }; + for (ext_name, entry) in extensions { + let Some(url) = entry.url.clone() else { + continue; + }; + let resolved = ctx.resolver.resolve(&url); + if resolved.is_none() { + ctx.error(ErrorKind::UnknownSchema, errors::msg_unknown_schema(&url)); + } + let merged = merge_extension_schema(entry, resolved.as_deref()); + slices.insert( + ext_name.clone(), + Slice { + match_: Some(Match { + type_: Some("pattern".to_string()), + value: Some(serde_json::json!({ "url": url })), + resolve_ref: None, + }), + min: entry.min, + max: entry.max, + order: None, + reslice: None, + slice_is_constraining: None, + schema: Some(Arc::new(merged)), + }, + ); + } + } + + if !slices.is_empty() { + let synthetic = FhirSchema { + slicing: Some(Slicing { slices, rules: None, ordered: None }), + ..Default::default() + }; + add_schemas_to_set(ctx, elset, Arc::new(synthetic), "extension"); + } +} + +/// Shallow overlay merge for extension slice schemas, mirroring the +/// reference validator's `Object.assign(entry-minus-url/min/max, resolved)`: +/// fields present on the resolved extension schema win; entry fields fill +/// the gaps. +fn merge_extension_schema(entry: &FhirSchema, resolved: Option<&FhirSchema>) -> FhirSchema { + let mut out = entry.clone(); + out.url = None; + out.min = None; + out.max = None; + + let Some(r) = resolved else { + return out; + }; + + macro_rules! overlay { + ($($field:ident),* $(,)?) => { + $(if r.$field.is_some() { + out.$field = r.$field.clone(); + })* + }; + } + overlay!( + url, name, base, kind, derivation, type_, array, scalar, min, max, elements, required, + excluded, element_reference, choices, choice_of, fixed, pattern, binding, constraints, + refers, slicing, extensions, modifier, must_support, summary, regex, + ); + out +} + /// Lodash-style `_.isMatch`: partial deep match. Every key present in /// `pattern` must exist in `data` and match recursively; extra data keys are /// permitted. Arrays match index-wise as a prefix; scalars by equality. diff --git a/crates/fhir-validator/tests/common/mod.rs b/crates/fhir-validator/tests/common/mod.rs new file mode 100644 index 000000000..0416bb4da --- /dev/null +++ b/crates/fhir-validator/tests/common/mod.rs @@ -0,0 +1,94 @@ +//! Shared fixture runner for the conformance-style suites. +//! +//! Fixture shape (upstream FHIR Schema contract, reused verbatim for the +//! Helios extended suite): +//! - the file's `schemas` map becomes the resolver, keyed by name/url +//! - each case validates `data` with the root schema auto-resolved from +//! `data.resourceType`, plus the case's extra `schemas` as profiles +//! - the emitted error list must equal the expected `errors` (or `[]` when +//! the key is absent) under exact ordered deep-equality +//! - `skip: true` cases are skipped; `focus: true` is ignored + +use helios_fhir_validator::{ + FhirSchema, SchemaRegistry, UnknownProfilePolicy, ValidationOptions, Validator, +}; +use serde::Deserialize; +use serde_json::Value; +use std::fs; +use std::path::PathBuf; +use std::sync::Arc; + +#[derive(Deserialize)] +struct FixtureFile { + #[allow(dead_code)] + desc: Option, + schemas: serde_json::Map, + tests: Vec, +} + +#[derive(Deserialize)] +struct FixtureCase { + desc: Option, + /// Extra profile schema names to layer on top of the auto-resolved root. + #[serde(default)] + schemas: Vec, + data: Value, + /// Absent ⇒ the case must validate clean. + errors: Option, + #[serde(default)] + skip: bool, +} + +/// Runs every case in one fixture file; panics with a per-case diff report on +/// any mismatch. +pub fn run_fixture_file(sub: &str, name: &str) { + let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("tests/fixtures") + .join(sub) + .join(name); + let raw = fs::read_to_string(&path) + .unwrap_or_else(|e| panic!("cannot read fixture {}: {e}", path.display())); + let file: FixtureFile = serde_json::from_str(&raw) + .unwrap_or_else(|e| panic!("cannot parse fixture {name}: {e}")); + + let mut registry = SchemaRegistry::new(); + for (key, def) in &file.schemas { + let schema: FhirSchema = serde_json::from_value(def.clone()) + .unwrap_or_else(|e| panic!("fixture {name}: schema '{key}' failed to parse: {e}")); + registry.insert_named(key.clone(), schema); + } + let validator = Validator::new(Arc::new(registry)); + + let mut failures: Vec = Vec::new(); + let mut ran = 0usize; + for (i, case) in file.tests.iter().enumerate() { + if case.skip { + continue; + } + ran += 1; + let desc = case.desc.clone().unwrap_or_else(|| format!("case #{i}")); + let opts = ValidationOptions { + profiles: case.schemas.clone(), + // Fixtures never carry meta.profile, and unresolvable references + // in fixtures are fixture bugs — surface them as errors. + use_meta_profiles: true, + unknown_profile: UnknownProfilePolicy::Error, + }; + let outcome = validator.validate_sync(&case.data, &opts); + let actual = serde_json::to_value(&outcome.errors).expect("errors serialize"); + let expected = case.errors.clone().unwrap_or_else(|| Value::Array(vec![])); + if actual != expected { + failures.push(format!( + " ✗ {desc}\n expected: {expected}\n actual: {actual}" + )); + } + } + + assert!( + failures.is_empty(), + "{name}: {}/{} cases failed:\n{}", + failures.len(), + ran, + failures.join("\n") + ); +} diff --git a/crates/fhir-validator/tests/conformance.rs b/crates/fhir-validator/tests/conformance.rs index ceeaea5c6..921ad8097 100644 --- a/crates/fhir-validator/tests/conformance.rs +++ b/crates/fhir-validator/tests/conformance.rs @@ -1,102 +1,10 @@ //! Runner for the vendored upstream FHIR Schema conformance suite //! (`tests/fixtures/upstream/*.json` — see UPSTREAM.md there for provenance). -//! -//! Contract (mirrors `fhirschema-js/tests/tests.test.js`): -//! - the fixture's `schemas` map becomes the resolver, keyed by name/url -//! - each case validates `data` with the root schema auto-resolved from -//! `data.resourceType`, plus the case's extra `schemas` as profiles -//! - the emitted error list must equal the expected `errors` (or `[]` when -//! the key is absent) under **exact ordered deep-equality** — `type`, -//! `path`, and `message` strings included -//! - `skip: true` cases are skipped; `focus: true` is ignored (all cases run) -use helios_fhir_validator::{ - FhirSchema, SchemaRegistry, UnknownProfilePolicy, ValidationOptions, Validator, -}; -use serde::Deserialize; -use serde_json::Value; -use std::fs; -use std::path::PathBuf; -use std::sync::Arc; - -#[derive(Deserialize)] -struct FixtureFile { - #[allow(dead_code)] - desc: Option, - schemas: serde_json::Map, - tests: Vec, -} - -#[derive(Deserialize)] -struct FixtureCase { - desc: Option, - /// Extra profile schema names to layer on top of the auto-resolved root. - #[serde(default)] - schemas: Vec, - data: Value, - /// Absent ⇒ the case must validate clean. - errors: Option, - #[serde(default)] - skip: bool, -} - -fn fixture_dir(sub: &str) -> PathBuf { - PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures").join(sub) -} - -/// Runs every case in one fixture file; panics with a per-case diff report on -/// any mismatch. -fn run_fixture_file(sub: &str, name: &str) { - let path = fixture_dir(sub).join(name); - let raw = fs::read_to_string(&path) - .unwrap_or_else(|e| panic!("cannot read fixture {}: {e}", path.display())); - let file: FixtureFile = serde_json::from_str(&raw) - .unwrap_or_else(|e| panic!("cannot parse fixture {name}: {e}")); - - let mut registry = SchemaRegistry::new(); - for (key, def) in &file.schemas { - let schema: FhirSchema = serde_json::from_value(def.clone()) - .unwrap_or_else(|e| panic!("fixture {name}: schema '{key}' failed to parse: {e}")); - registry.insert_named(key.clone(), schema); - } - let validator = Validator::new(Arc::new(registry)); - - let mut failures: Vec = Vec::new(); - let mut ran = 0usize; - for (i, case) in file.tests.iter().enumerate() { - if case.skip { - continue; - } - ran += 1; - let desc = case.desc.clone().unwrap_or_else(|| format!("case #{i}")); - let opts = ValidationOptions { - profiles: case.schemas.clone(), - // Fixtures never carry meta.profile, and unresolvable references - // in fixtures are fixture bugs — surface them as errors. - use_meta_profiles: true, - unknown_profile: UnknownProfilePolicy::Error, - }; - let outcome = validator.validate_sync(&case.data, &opts); - let actual = serde_json::to_value(&outcome.errors).expect("errors serialize"); - let expected = case.errors.clone().unwrap_or_else(|| Value::Array(vec![])); - if actual != expected { - failures.push(format!( - " ✗ {desc}\n expected: {expected}\n actual: {actual}" - )); - } - } - - assert!( - failures.is_empty(), - "{name}: {}/{} cases failed:\n{}", - failures.len(), - ran, - failures.join("\n") - ); -} +mod common; fn run_upstream(name: &str) { - run_fixture_file("upstream", name) + common::run_fixture_file("upstream", name) } #[test] @@ -120,13 +28,11 @@ fn upstream_4_required() { } #[test] -#[ignore = "Phase 2: slicing (engine/slicing.rs) not implemented yet"] fn upstream_5_slices() { run_upstream("5_slices.json"); } #[test] -#[ignore = "Phase 2: extensions→slicing compilation not implemented yet"] fn upstream_6_extensions() { run_upstream("6_extensions.json"); } diff --git a/crates/fhir-validator/tests/extended.rs b/crates/fhir-validator/tests/extended.rs new file mode 100644 index 000000000..db95d6595 --- /dev/null +++ b/crates/fhir-validator/tests/extended.rs @@ -0,0 +1,50 @@ +//! Runner for the Helios extended fixture suite +//! (`tests/fixtures/extended/*.json`). +//! +//! These fixtures pin behavior the upstream conformance suite leaves +//! unspecified: `excluded`, numeric array cardinality messages, fixed/pattern +//! messages, slicing rules (closed/openAtEnd/ordered/@default, prohibited +//! `max: 0` slices), primitive-extension sidecars (`_field`), +//! `elementReference` recursion, and required-satisfied-by-choice-branch. +//! Same fixture format and exact-match contract as the upstream suite. + +mod common; + +fn run_extended(name: &str) { + common::run_fixture_file("extended", name) +} + +#[test] +fn extended_excluded() { + run_extended("excluded.json"); +} + +#[test] +fn extended_cardinality() { + run_extended("cardinality.json"); +} + +#[test] +fn extended_fixed_pattern() { + run_extended("fixed_pattern.json"); +} + +#[test] +fn extended_choice_required() { + run_extended("choice_required.json"); +} + +#[test] +fn extended_slicing_rules() { + run_extended("slicing_rules.json"); +} + +#[test] +fn extended_sidecars() { + run_extended("sidecars.json"); +} + +#[test] +fn extended_element_reference() { + run_extended("element_reference.json"); +} diff --git a/crates/fhir-validator/tests/fixtures/extended/cardinality.json b/crates/fhir-validator/tests/fixtures/extended/cardinality.json new file mode 100644 index 000000000..3f74ccad4 --- /dev/null +++ b/crates/fhir-validator/tests/fixtures/extended/cardinality.json @@ -0,0 +1,40 @@ +{ + "desc": "numeric array cardinality — Helios extended", + "schemas": { + "string": { "kind": "primitive-type" }, + "Patient": { + "elements": { + "resourceType": { "type": "string" }, + "name": { + "array": true, + "min": 2, + "max": 3, + "elements": { "text": { "type": "string" } } + } + } + } + }, + "tests": [ + { + "desc": "within bounds", + "data": { "resourceType": "Patient", "name": [{ "text": "a" }, { "text": "b" }] } + }, + { + "desc": "below min", + "data": { "resourceType": "Patient", "name": [{ "text": "a" }] }, + "errors": [ + { "type": "min", "path": "Patient.name", "message": "expected 2 > 1" } + ] + }, + { + "desc": "above max", + "data": { + "resourceType": "Patient", + "name": [{ "text": "a" }, { "text": "b" }, { "text": "c" }, { "text": "d" }] + }, + "errors": [ + { "type": "max", "path": "Patient.name", "message": "expected 3 < 4" } + ] + } + ] +} diff --git a/crates/fhir-validator/tests/fixtures/extended/choice_required.json b/crates/fhir-validator/tests/fixtures/extended/choice_required.json new file mode 100644 index 000000000..7656d9d8d --- /dev/null +++ b/crates/fhir-validator/tests/fixtures/extended/choice_required.json @@ -0,0 +1,33 @@ +{ + "desc": "required satisfied by any declared choice branch — Helios extended (converter maps min>=1 on foo[x] to required:[foo])", + "schemas": { + "string": { "kind": "primitive-type" }, + "boolean": { "kind": "primitive-type" }, + "Observation": { + "required": ["value"], + "elements": { + "resourceType": { "type": "string" }, + "value": { "choices": ["valueString", "valueBoolean"] }, + "valueString": { "choiceOf": "value", "type": "string" }, + "valueBoolean": { "choiceOf": "value", "type": "boolean" } + } + } + }, + "tests": [ + { + "desc": "branch presence satisfies the group requirement", + "data": { "resourceType": "Observation", "valueString": "ok" } + }, + { + "desc": "other branch also satisfies", + "data": { "resourceType": "Observation", "valueBoolean": true } + }, + { + "desc": "missing entirely", + "data": { "resourceType": "Observation" }, + "errors": [ + { "type": "required", "path": "Observation.value", "message": "value is required" } + ] + } + ] +} diff --git a/crates/fhir-validator/tests/fixtures/extended/element_reference.json b/crates/fhir-validator/tests/fixtures/extended/element_reference.json new file mode 100644 index 000000000..722fd005c --- /dev/null +++ b/crates/fhir-validator/tests/fixtures/extended/element_reference.json @@ -0,0 +1,84 @@ +{ + "desc": "elementReference recursion — Helios extended (Questionnaire.item pattern; absent from the reference validator)", + "schemas": { + "string": { "kind": "primitive-type" }, + "Questionnaire": { + "elements": { + "resourceType": { "type": "string" }, + "item": { + "array": true, + "elements": { + "linkId": { "type": "string" }, + "type": { "type": "string" }, + "item": { + "array": true, + "elementReference": ["Questionnaire", "elements", "item"] + } + } + } + } + } + }, + "tests": [ + { + "desc": "nested items validate through the reference", + "data": { + "resourceType": "Questionnaire", + "item": [ + { + "linkId": "q1", + "type": "group", + "item": [{ "linkId": "q2", "type": "display" }] + } + ] + } + }, + { + "desc": "three levels deep", + "data": { + "resourceType": "Questionnaire", + "item": [ + { + "linkId": "q1", + "item": [ + { "linkId": "q2", "item": [{ "linkId": "q3", "type": "display" }] } + ] + } + ] + } + }, + { + "desc": "unknown key at depth is caught", + "data": { + "resourceType": "Questionnaire", + "item": [ + { + "linkId": "q1", + "item": [{ "item": [{ "bogus": true }] }] + } + ] + }, + "errors": [ + { + "type": "unknown-element", + "path": "Questionnaire.item.0.item.0.item.0.bogus", + "message": "bogus is unknown" + } + ] + }, + { + "desc": "nested item must still be an array", + "data": { + "resourceType": "Questionnaire", + "item": [{ "linkId": "q1", "item": { "linkId": "q2" } }] + }, + "errors": [ + { + "type": "not-array", + "path": "Questionnaire.item.0.item", + "message": "item is not array" + } + ] + } + ] +} diff --git a/crates/fhir-validator/tests/fixtures/extended/excluded.json b/crates/fhir-validator/tests/fixtures/extended/excluded.json new file mode 100644 index 000000000..f9f93cdd8 --- /dev/null +++ b/crates/fhir-validator/tests/fixtures/extended/excluded.json @@ -0,0 +1,54 @@ +{ + "desc": "excluded keyword — Helios extended (broken in the reference validator)", + "schemas": { + "string": { "kind": "primitive-type" }, + "Patient": { + "elements": { + "resourceType": { "type": "string" }, + "gender": { "type": "string" }, + "birthDate": { "type": "string" } + } + }, + "NoGender": { + "base": "Patient", + "excluded": ["gender"], + "required": ["birthDate"] + } + }, + "tests": [ + { + "desc": "clean when excluded key absent", + "schemas": ["NoGender"], + "data": { "resourceType": "Patient", "birthDate": "2000-01-01" } + }, + { + "desc": "excluded key present", + "schemas": ["NoGender"], + "data": { "resourceType": "Patient", "gender": "male", "birthDate": "2000-01-01" }, + "errors": [ + { + "type": "excluded", + "path": "Patient.gender", + "message": "excluded property gender is present" + } + ] + }, + { + "desc": "excluded and required combine across layers", + "schemas": ["NoGender"], + "data": { "resourceType": "Patient", "gender": "other" }, + "errors": [ + { + "type": "required", + "path": "Patient.birthDate", + "message": "birthDate is required" + }, + { + "type": "excluded", + "path": "Patient.gender", + "message": "excluded property gender is present" + } + ] + } + ] +} diff --git a/crates/fhir-validator/tests/fixtures/extended/fixed_pattern.json b/crates/fhir-validator/tests/fixtures/extended/fixed_pattern.json new file mode 100644 index 000000000..73d8a8eb9 --- /dev/null +++ b/crates/fhir-validator/tests/fixtures/extended/fixed_pattern.json @@ -0,0 +1,54 @@ +{ + "desc": "fixed and pattern keywords — Helios extended (message wording is ours: scalars render bare, composites render as compact JSON)", + "schemas": { + "string": { "kind": "primitive-type" }, + "Patient": { + "elements": { + "resourceType": { "type": "string" }, + "gender": { "type": "string", "fixed": "male" }, + "maritalStatus": { + "pattern": { "system": "http://x" }, + "elements": { + "text": { "type": "string" }, + "system": { "type": "string" } + } + } + } + } + }, + "tests": [ + { + "desc": "fixed satisfied", + "data": { "resourceType": "Patient", "gender": "male" } + }, + { + "desc": "fixed violated", + "data": { "resourceType": "Patient", "gender": "female" }, + "errors": [ + { + "type": "fixed-value", + "path": "Patient.gender", + "message": "Expected value to be exactly equal to 'fixed' pattern 'male', but got: 'female'" + } + ] + }, + { + "desc": "pattern satisfied with extra keys", + "data": { + "resourceType": "Patient", + "maritalStatus": { "system": "http://x", "text": "M" } + } + }, + { + "desc": "pattern violated", + "data": { "resourceType": "Patient", "maritalStatus": { "text": "M" } }, + "errors": [ + { + "type": "pattern-value", + "path": "Patient.maritalStatus", + "message": "Expected value to match 'pattern' '{\"system\":\"http://x\"}', but got: '{\"text\":\"M\"}'" + } + ] + } + ] +} diff --git a/crates/fhir-validator/tests/fixtures/extended/sidecars.json b/crates/fhir-validator/tests/fixtures/extended/sidecars.json new file mode 100644 index 000000000..645a2533d --- /dev/null +++ b/crates/fhir-validator/tests/fixtures/extended/sidecars.json @@ -0,0 +1,120 @@ +{ + "desc": "primitive extension sidecars (_field) — Helios extended", + "schemas": { + "string": { "kind": "primitive-type" }, + "Extension": { + "elements": { + "url": { "type": "string" }, + "valueString": { "type": "string" } + } + }, + "Element": { + "elements": { + "id": { "type": "string" }, + "extension": { "array": true, "type": "Extension" } + } + }, + "Patient": { + "elements": { + "resourceType": { "type": "string" }, + "status": { "type": "string" }, + "given": { "type": "string", "array": true }, + "name": { + "elements": { "family": { "type": "string" } } + } + } + } + }, + "tests": [ + { + "desc": "scalar sidecar with id and extension", + "data": { + "resourceType": "Patient", + "status": "active", + "_status": { + "id": "s1", + "extension": [{ "url": "http://x", "valueString": "v" }] + } + } + }, + { + "desc": "orphan sidecar (value absent) is legal for a declared primitive", + "data": { "resourceType": "Patient", "_status": { "id": "s1" } } + }, + { + "desc": "unknown key inside the sidecar Element part", + "data": { "resourceType": "Patient", "_status": { "bogus": 1 } }, + "errors": [ + { + "type": "unknown-element", + "path": "Patient._status.bogus", + "message": "bogus is unknown" + } + ] + }, + { + "desc": "stray sidecar for an undeclared element", + "data": { "resourceType": "Patient", "_bogus": { "id": "x" } }, + "errors": [ + { + "type": "unknown-element", + "path": "Patient._bogus", + "message": "_bogus is unknown" + } + ] + }, + { + "desc": "sidecar on a complex element rejected", + "data": { + "resourceType": "Patient", + "name": { "family": "F" }, + "_name": { "id": "x" } + }, + "errors": [ + { + "type": "unknown-element", + "path": "Patient._name", + "message": "_name is unknown" + } + ] + }, + { + "desc": "array sidecar aligned with nulls on either side", + "data": { + "resourceType": "Patient", + "given": ["A", null], + "_given": [null, { "id": "g2" }] + } + }, + { + "desc": "array element demands array sidecar", + "data": { + "resourceType": "Patient", + "given": ["A"], + "_given": { "id": "g" } + }, + "errors": [ + { + "type": "not-array", + "path": "Patient._given", + "message": "_given is not array" + } + ] + }, + { + "desc": "scalar element demands scalar sidecar", + "data": { + "resourceType": "Patient", + "status": "active", + "_status": [{ "id": "s" }] + }, + "errors": [ + { + "type": "not-singular", + "path": "Patient._status", + "message": "_status is not singular" + } + ] + } + ] +} diff --git a/crates/fhir-validator/tests/fixtures/extended/slicing_rules.json b/crates/fhir-validator/tests/fixtures/extended/slicing_rules.json new file mode 100644 index 000000000..ba0ee07c5 --- /dev/null +++ b/crates/fhir-validator/tests/fixtures/extended/slicing_rules.json @@ -0,0 +1,227 @@ +{ + "desc": "slicing rules — Helios extended (closed/openAtEnd/ordered/@default and prohibited max:0 slices; the reference validator implements none of these)", + "schemas": { + "string": { "kind": "primitive-type" }, + "Resource": { + "elements": { + "resourceType": { "type": "string" }, + "address": { + "array": true, + "elements": { + "use": { "type": "string" }, + "city": { "type": "string" }, + "type": { "type": "string" } + } + } + } + }, + "ClosedProfile": { + "base": "Resource", + "elements": { + "address": { + "slicing": { + "rules": "closed", + "slices": { + "home": { + "match": { "type": "pattern", "value": { "use": "home" } }, + "min": 1 + } + } + } + } + } + }, + "DefaultProfile": { + "base": "Resource", + "elements": { + "address": { + "slicing": { + "rules": "closed", + "slices": { + "home": { + "match": { "type": "pattern", "value": { "use": "home" } }, + "min": 1 + }, + "@default": { + "schema": { "required": ["type"] } + } + } + } + } + } + }, + "OpenAtEndProfile": { + "base": "Resource", + "elements": { + "address": { + "slicing": { + "rules": "openAtEnd", + "ordered": true, + "slices": { + "home": { + "order": 0, + "match": { "type": "pattern", "value": { "use": "home" } } + }, + "work": { + "order": 1, + "match": { "type": "pattern", "value": { "use": "work" } } + } + } + } + } + } + }, + "OrderedProfile": { + "base": "Resource", + "elements": { + "address": { + "slicing": { + "ordered": true, + "slices": { + "home": { + "order": 0, + "match": { "type": "pattern", "value": { "use": "home" } } + }, + "work": { + "order": 1, + "match": { "type": "pattern", "value": { "use": "work" } } + } + } + } + } + } + }, + "ProhibitProfile": { + "base": "Resource", + "elements": { + "address": { + "slicing": { + "slices": { + "temp": { + "max": 0, + "match": { "type": "pattern", "value": { "use": "temp" } } + } + } + } + } + } + } + }, + "tests": [ + { + "desc": "closed: all items match", + "schemas": ["ClosedProfile"], + "data": { + "resourceType": "Resource", + "address": [{ "use": "home" }, { "use": "home", "city": "X" }] + } + }, + { + "desc": "closed: unmatched item rejected", + "schemas": ["ClosedProfile"], + "data": { + "resourceType": "Resource", + "address": [{ "use": "home" }, { "use": "work" }] + }, + "errors": [ + { + "type": "slice-unmatched", + "path": "Resource.address.1", + "message": "item does not match any slice in closed slicing" + } + ] + }, + { + "desc": "closed with @default: unmatched items absorbed and validated", + "schemas": ["DefaultProfile"], + "data": { + "resourceType": "Resource", + "address": [{ "use": "home" }, { "use": "billing", "type": "postal" }] + } + }, + { + "desc": "closed with @default: absorbed item violates the default schema", + "schemas": ["DefaultProfile"], + "data": { + "resourceType": "Resource", + "address": [{ "use": "home" }, { "use": "billing" }] + }, + "errors": [ + { + "type": "required", + "path": "Resource.address.1.type", + "message": "type is required" + } + ] + }, + { + "desc": "openAtEnd: unmatched items allowed at the end", + "schemas": ["OpenAtEndProfile"], + "data": { + "resourceType": "Resource", + "address": [{ "use": "home" }, { "use": "work" }, { "use": "temp" }] + } + }, + { + "desc": "openAtEnd: unmatched item before matched items rejected", + "schemas": ["OpenAtEndProfile"], + "data": { + "resourceType": "Resource", + "address": [{ "use": "temp" }, { "use": "home" }, { "use": "work" }] + }, + "errors": [ + { + "type": "slice-unmatched", + "path": "Resource.address.0", + "message": "unmatched item must be at the end of the array (openAtEnd slicing)" + } + ] + }, + { + "desc": "ordered: items follow slice order", + "schemas": ["OrderedProfile"], + "data": { + "resourceType": "Resource", + "address": [{ "use": "home" }, { "use": "home" }, { "use": "work" }] + } + }, + { + "desc": "ordered: out-of-order item rejected", + "schemas": ["OrderedProfile"], + "data": { + "resourceType": "Resource", + "address": [{ "use": "work" }, { "use": "home" }] + }, + "errors": [ + { + "type": "slice-order", + "path": "Resource.address.1", + "message": "slice 'home' is out of order (ordered slicing)" + } + ] + }, + { + "desc": "prohibited slice (max 0) absent", + "schemas": ["ProhibitProfile"], + "data": { + "resourceType": "Resource", + "address": [{ "use": "home" }] + } + }, + { + "desc": "prohibited slice (max 0) present — enforced despite the reference validator's falsy-max bug", + "schemas": ["ProhibitProfile"], + "data": { + "resourceType": "Resource", + "address": [{ "use": "temp" }] + }, + "errors": [ + { + "type": "slice-cardinality", + "path": "Resource.address", + "message": "Slice defines the following max cardinality: '0', actual cardinality: '1'" + } + ] + } + ] +} From 7e2f31a760346c70ea4af131bd3da17f1ce8ed33 Mon Sep 17 00:00:00 2001 From: Doug Date: Sun, 5 Jul 2026 20:20:37 -0400 Subject: [PATCH 3/7] feat(validator): SD-to-FhirSchema converter, embedded core packs, validator-cli MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 3: - converter/: StructureDefinition (raw JSON, snapshot OR differential) to FhirSchema — ED-id tree reconstruction, choice [x] expansion, discriminator-to-pattern-match translation, extension-slicing to extensions sugar, contentReference to elementReference, targetProfile to refers, min/max to required/excluded/cardinality, ele-1/ext-1 dedup, primitive value-regex extraction. Golden tests pin every mapping. - generate-schema-packs bin (gen-packs feature): converts crates/fhir-gen/resources/{V}/profiles-{types,resources,others}.json. All four versions convert with ZERO failures: R4 257, R4B 247, R5 296, R6 228 schemas; packs total ~437 KB gzipped, committed under packs/. - packs.rs: include_bytes + per-version OnceLock core registries (profiles-first insertion so core names win aliases). - pack_smoke (#[ignore]): real-world Patient with choice types, sidecars, contained resources and extensions validates clean against the converted R4 core; structural breakage and Bundle dynamic resolution verified. - validator-cli bin (cli feature): file/stdin, --fhir-version, --profile, --profile-file (converts SDs on the fly), text/json output, exit 0/1/2. Co-Authored-By: Claude Opus 4.8 (1M context) --- Cargo.lock | 3 + crates/fhir-validator/Cargo.toml | 30 +- .../packs/fhir_schemas_r4.json.gz | Bin 0 -> 92607 bytes .../packs/fhir_schemas_r4b.json.gz | Bin 0 -> 98288 bytes .../packs/fhir_schemas_r5.json.gz | Bin 0 -> 142465 bytes .../packs/fhir_schemas_r6.json.gz | Bin 0 -> 103929 bytes .../src/bin/generate_schema_packs.rs | 141 ++++++ .../fhir-validator/src/bin/validator_cli.rs | 177 ++++++++ crates/fhir-validator/src/converter/mod.rs | 301 +++++++++++++ .../src/converter/primitives.rs | 45 ++ .../fhir-validator/src/converter/slicing.rs | 128 ++++++ crates/fhir-validator/src/converter/tree.rs | 426 ++++++++++++++++++ crates/fhir-validator/src/lib.rs | 2 + crates/fhir-validator/src/packs.rs | 81 ++++ .../fhir-validator/tests/converter_tests.rs | 191 ++++++++ .../structuredefinitions/mini-patient.json | 123 +++++ .../structuredefinitions/mini-profile.json | 60 +++ .../mini-questionnaire.json | 47 ++ .../primitive-string.json | 46 ++ crates/fhir-validator/tests/pack_smoke.rs | 113 +++++ 20 files changed, 1907 insertions(+), 7 deletions(-) create mode 100644 crates/fhir-validator/packs/fhir_schemas_r4.json.gz create mode 100644 crates/fhir-validator/packs/fhir_schemas_r4b.json.gz create mode 100644 crates/fhir-validator/packs/fhir_schemas_r5.json.gz create mode 100644 crates/fhir-validator/packs/fhir_schemas_r6.json.gz create mode 100644 crates/fhir-validator/src/bin/generate_schema_packs.rs create mode 100644 crates/fhir-validator/src/bin/validator_cli.rs create mode 100644 crates/fhir-validator/src/converter/mod.rs create mode 100644 crates/fhir-validator/src/converter/primitives.rs create mode 100644 crates/fhir-validator/src/converter/slicing.rs create mode 100644 crates/fhir-validator/src/converter/tree.rs create mode 100644 crates/fhir-validator/src/packs.rs create mode 100644 crates/fhir-validator/tests/converter_tests.rs create mode 100644 crates/fhir-validator/tests/fixtures/structuredefinitions/mini-patient.json create mode 100644 crates/fhir-validator/tests/fixtures/structuredefinitions/mini-profile.json create mode 100644 crates/fhir-validator/tests/fixtures/structuredefinitions/mini-questionnaire.json create mode 100644 crates/fhir-validator/tests/fixtures/structuredefinitions/primitive-string.json create mode 100644 crates/fhir-validator/tests/pack_smoke.rs diff --git a/Cargo.lock b/Cargo.lock index b78a453cc..41022dd17 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3149,6 +3149,9 @@ dependencies = [ name = "helios-fhir-validator" version = "0.2.1" dependencies = [ + "clap", + "flate2", + "helios-fhir", "indexmap 2.14.0", "serde", "serde_json", diff --git a/crates/fhir-validator/Cargo.toml b/crates/fhir-validator/Cargo.toml index b6d27099e..bedb0c6f8 100644 --- a/crates/fhir-validator/Cargo.toml +++ b/crates/fhir-validator/Cargo.toml @@ -12,15 +12,18 @@ keywords = ["helios-software", "hl7", "fhir", "validation", "fhir-schema"] [features] default = ["R4"] -# Version features are placeholders until the converter/pack phases wire -# helios-fhir (and helios-fhirpath) cascades in. Kept so downstream crates can -# already forward their version features to this crate. -R4 = [] -R4B = [] -R5 = [] -R6 = [] +R4 = ["helios-fhir/R4"] +R4B = ["helios-fhir/R4B"] +R5 = ["helios-fhir/R5"] +R6 = ["helios-fhir/R6"] +# Enables the generate-schema-packs binary (dev-time; reads the FHIR spec +# bundles from crates/fhir-gen/resources and writes packs/). +gen-packs = [] +# Enables the validator-cli binary. +cli = ["dep:clap"] [dependencies] +helios-fhir = { path = "../fhir", version = "0.2.1", default-features = false } # `rc` enables Deserialize/Serialize for Arc — element schemas are shared # by reference across cooperative schema sets. serde = { workspace = true, features = ["rc"] } @@ -28,7 +31,20 @@ serde = { workspace = true, features = ["rc"] } # to be emitted in document key order. serde_json = { workspace = true, features = ["preserve_order"] } indexmap = { version = "2", features = ["serde"] } +# Schema pack (de)compression. +flate2 = "1" +clap = { version = "4", features = ["derive"], optional = true } [dev-dependencies] serde = { workspace = true } serde_json = { workspace = true } + +[[bin]] +name = "generate-schema-packs" +path = "src/bin/generate_schema_packs.rs" +required-features = ["gen-packs"] + +[[bin]] +name = "validator-cli" +path = "src/bin/validator_cli.rs" +required-features = ["cli"] diff --git a/crates/fhir-validator/packs/fhir_schemas_r4.json.gz b/crates/fhir-validator/packs/fhir_schemas_r4.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..f538901d524971579f972c3d174100657d4a5a01 GIT binary patch literal 92607 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 literal 0 HcmV?d00001 diff --git a/crates/fhir-validator/packs/fhir_schemas_r4b.json.gz b/crates/fhir-validator/packs/fhir_schemas_r4b.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..8c9df8c6e1e9e0d65e3bc24bca2987ae1b9c795a GIT binary patch literal 98288 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 Dk6u<4 literal 0 HcmV?d00001 diff --git a/crates/fhir-validator/packs/fhir_schemas_r5.json.gz b/crates/fhir-validator/packs/fhir_schemas_r5.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..6f7684df0154ad75e8907234972418a853904ac1 GIT binary patch literal 142465 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

8qGK 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�yn8Qp*+j_jD!vh4!SPe+QKD(tIet&V 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 = std::env::args().skip(1).collect(); + let versions: Vec<&str> = if args.is_empty() { + vec!["R4", "R4B", "R5", "R6"] + } else { + args.iter().map(String::as_str).collect() + }; + + let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + let resources_root = manifest_dir.join("../fhir-gen/resources"); + let packs_dir = manifest_dir.join("packs"); + fs::create_dir_all(&packs_dir).expect("create packs dir"); + + let mut failed = false; + for version in versions { + match generate_pack(&resources_root.join(version), &packs_dir, version) { + Ok(report) => println!("{report}"), + Err(e) => { + eprintln!("{version}: {e}"); + failed = true; + } + } + } + if failed { + std::process::exit(1); + } +} + +fn generate_pack(source_dir: &Path, packs_dir: &Path, version: &str) -> Result { + if !source_dir.exists() { + return Ok(format!( + "{version}: SKIPPED — spec directory {} not present (run a fhir-gen R6 build to download)", + source_dir.display() + )); + } + + let mut schemas: Vec = Vec::new(); + let mut sd_count = 0usize; + let mut warning_count = 0usize; + let mut failures: Vec = Vec::new(); + + for file in SOURCE_FILES { + let path = source_dir.join(file); + if !path.exists() { + eprintln!("{version}: {file} missing; continuing without it"); + continue; + } + let raw = fs::read_to_string(&path).map_err(|e| format!("read {file}: {e}"))?; + let bundle: Value = serde_json::from_str(&raw).map_err(|e| format!("parse {file}: {e}"))?; + let entries = bundle + .get("entry") + .and_then(Value::as_array) + .ok_or_else(|| format!("{file}: no Bundle.entry"))?; + + for entry in entries { + let Some(resource) = entry.get("resource") else { continue }; + if resource.get("resourceType").and_then(Value::as_str) != Some("StructureDefinition") + { + continue; + } + sd_count += 1; + let sd_url = resource.get("url").and_then(Value::as_str).unwrap_or(""); + match convert(resource) { + Ok(conversion) => { + warning_count += conversion.warnings.len(); + for w in &conversion.warnings { + eprintln!("{version}: [warn] {sd_url}: {w}"); + } + schemas.push( + serde_json::to_value(&conversion.schema).expect("schema serializes"), + ); + } + Err(e) => failures.push(format!("{sd_url}: {e}")), + } + } + } + + if !failures.is_empty() { + return Err(format!( + "{} of {} StructureDefinitions failed to convert:\n {}", + failures.len(), + sd_count, + failures.join("\n ") + )); + } + + // Diff-stable output: sort by url. + schemas.sort_by(|a, b| { + a.get("url") + .and_then(Value::as_str) + .unwrap_or_default() + .cmp(b.get("url").and_then(Value::as_str).unwrap_or_default()) + }); + + let json = serde_json::to_vec(&schemas).expect("pack serializes"); + let out_path = packs_dir.join(format!("fhir_schemas_{}.json.gz", version.to_lowercase())); + let mut encoder = GzEncoder::new(Vec::new(), Compression::best()); + encoder.write_all(&json).expect("gzip write"); + let compressed = encoder.finish().expect("gzip finish"); + fs::write(&out_path, &compressed).map_err(|e| format!("write pack: {e}"))?; + + Ok(format!( + "{version}: {} schemas from {} StructureDefinitions ({} converter warnings) → {} ({} KB raw, {} KB gz)", + schemas.len(), + sd_count, + warning_count, + out_path.display(), + json.len() / 1024, + compressed.len() / 1024 + )) +} diff --git a/crates/fhir-validator/src/bin/validator_cli.rs b/crates/fhir-validator/src/bin/validator_cli.rs new file mode 100644 index 000000000..46c6a7884 --- /dev/null +++ b/crates/fhir-validator/src/bin/validator_cli.rs @@ -0,0 +1,177 @@ +//! validator-cli — validate a FHIR resource from a file or stdin against the +//! embedded core schemas (and optional profiles), following the +//! fhirpath-cli / sof-cli pattern. +//! +//! ```text +//! validator-cli patient.json +//! cat patient.json | validator-cli --fhir-version R4 --output json +//! validator-cli patient.json --profile http://example.org/StructureDefinition/my-patient \ +//! --profile-file my-profile-sd.json +//! ``` +//! +//! Exit codes: 0 = valid (no error-severity issues), 1 = validation errors, +//! 2 = usage/input failure. + +use clap::Parser; +use helios_fhir::FhirVersion; +use helios_fhir_validator::converter::convert; +use helios_fhir_validator::packs::core_registry; +use helios_fhir_validator::{ + CompositeResolver, SchemaRegistry, Severity, UnknownProfilePolicy, ValidationOptions, + Validator, +}; +use std::fs; +use std::io::Read; +use std::process::ExitCode; +use std::sync::Arc; + +#[derive(Parser)] +#[command( + name = "validator-cli", + about = "Validate a FHIR resource against the core specification and optional profiles", + version +)] +struct Args { + /// Resource file to validate (JSON). Reads stdin when omitted. + file: Option, + + /// FHIR version to validate against. + #[arg(long, value_enum, default_value_t = FhirVersion::R4)] + fhir_version: FhirVersion, + + /// Additional profile canonical(s) to validate against (repeatable). + /// The profile must resolve — from --profile-file or the core pack. + #[arg(long = "profile")] + profiles: Vec, + + /// StructureDefinition file(s) to convert and make resolvable + /// (repeatable). Use together with --profile, or let meta.profile pick + /// them up. + #[arg(long = "profile-file")] + profile_files: Vec, + + /// Ignore the profiles the resource claims in meta.profile. + #[arg(long)] + no_meta_profiles: bool, + + /// Output format. + #[arg(long, value_enum, default_value_t = Output::Text)] + output: Output, +} + +#[derive(Clone, Copy, PartialEq, clap::ValueEnum)] +enum Output { + Text, + Json, +} + +fn main() -> ExitCode { + let args = Args::parse(); + + let raw = match &args.file { + Some(path) => match fs::read_to_string(path) { + Ok(s) => s, + Err(e) => { + eprintln!("error: cannot read {path}: {e}"); + return ExitCode::from(2); + } + }, + None => { + let mut buf = String::new(); + if let Err(e) = std::io::stdin().read_to_string(&mut buf) { + eprintln!("error: cannot read stdin: {e}"); + return ExitCode::from(2); + } + buf + } + }; + let resource: serde_json::Value = match serde_json::from_str(&raw) { + Ok(v) => v, + Err(e) => { + eprintln!("error: input is not valid JSON: {e}"); + return ExitCode::from(2); + } + }; + + // Core pack, optionally overlaid with converted profile files. + let core = core_registry(args.fhir_version); + let resolver: Arc = if args.profile_files.is_empty() + { + core + } else { + let mut overlay = SchemaRegistry::new(); + for path in &args.profile_files { + let sd: serde_json::Value = match fs::read_to_string(path) + .map_err(|e| e.to_string()) + .and_then(|s| serde_json::from_str(&s).map_err(|e| e.to_string())) + { + Ok(v) => v, + Err(e) => { + eprintln!("error: cannot load profile {path}: {e}"); + return ExitCode::from(2); + } + }; + match convert(&sd) { + Ok(conversion) => { + for w in &conversion.warnings { + eprintln!("warning: {path}: {w}"); + } + if !overlay.insert(conversion.schema) { + eprintln!("error: profile {path} has neither url nor name"); + return ExitCode::from(2); + } + } + Err(e) => { + eprintln!("error: profile {path} failed to convert: {e}"); + return ExitCode::from(2); + } + } + } + Arc::new(CompositeResolver::new(vec![Arc::new(overlay), core])) + }; + + let validator = Validator::new(resolver); + let opts = ValidationOptions { + profiles: args.profiles.clone(), + use_meta_profiles: !args.no_meta_profiles, + unknown_profile: UnknownProfilePolicy::Warn, + }; + let outcome = validator.validate_sync(&resource, &opts); + + let error_count = + outcome.errors.iter().filter(|e| e.severity == Severity::Error).count(); + + match args.output { + Output::Json => { + println!( + "{}", + serde_json::to_string_pretty(&serde_json::json!({ + "valid": error_count == 0, + "errors": outcome.errors, + })) + .expect("report serializes") + ); + } + Output::Text => { + if outcome.errors.is_empty() { + println!("valid: no issues"); + } else { + for e in &outcome.errors { + let sev = if e.severity == Severity::Error { "error" } else { "warning" }; + let kind = serde_json::to_value(e.kind) + .ok() + .and_then(|v| v.as_str().map(str::to_string)) + .unwrap_or_default(); + println!("{sev} [{kind}] {}: {}", e.path, e.message); + } + println!( + "{} issue(s), {} error(s)", + outcome.errors.len(), + error_count + ); + } + } + } + + if error_count > 0 { ExitCode::from(1) } else { ExitCode::SUCCESS } +} diff --git a/crates/fhir-validator/src/converter/mod.rs b/crates/fhir-validator/src/converter/mod.rs new file mode 100644 index 000000000..74fc9c477 --- /dev/null +++ b/crates/fhir-validator/src/converter/mod.rs @@ -0,0 +1,301 @@ +//! StructureDefinition → FhirSchema conversion (fhir2schema). +//! +//! Consumes StructureDefinitions as **raw JSON** — deliberately not the +//! fhir-gen bootstrap model (whose build.rs performs the R6 download) and not +//! the typed models (uploaded profiles arrive as `Value` from the REST +//! extractor). Works from `snapshot.element` when present, otherwise +//! `differential.element`: because FHIR Schema is differential-by-design and +//! validation layers schemas cooperatively, **no snapshot generation is ever +//! needed** — sparse differential paths simply produce empty intermediate +//! nodes that the base layers fill in at validation time. +//! +//! Mapping summary (pinned by `tests/converter_tests.rs` goldens): +//! +//! | ElementDefinition | FhirSchema | +//! |---|---| +//! | `max == "0"` | parent `excluded` (element omitted) | +//! | `min >= 1` | parent `required` (choice base name for `foo[x]`) | +//! | `base.max` (else `max`) `*`/`>1` | `array: true` + numeric `min`/`max` | +//! | multi-type / `foo[x]` | `choices` declarer + one `choiceOf` branch per type | +//! | `contentReference: "#A.b"` | `elementReference: ["A", "elements", "b"]` | +//! | `slicing.discriminator` | `slicing.slices[].match` (pattern) — see `slicing` | +//! | extension slice + type profile | parent `extensions` sugar | +//! | `binding` | `{valueSet, strength}` carried for all strengths | +//! | `constraint[]` | `constraints` map (`ele-1`/`ext-1` dropped off non-root elements — they are enforced once via the `Element`/`Extension` type schemas) | +//! | `fixed[x]` / `pattern[x]` | `fixed` / `pattern` | +//! | `type[].targetProfile` | `refers` | +//! | primitive `.value` regex extension | schema `regex` | +//! +//! Additionally, `resourceType: {type: "code"}` is injected into resource +//! schemas with `derivation != constraint` (StructureDefinitions never +//! declare `resourceType`; profiles inherit the injected element through +//! their base chain). + +mod primitives; +mod slicing; +mod tree; + +use crate::schema::FhirSchema; +use serde::Deserialize; +use serde_json::Value; +use std::fmt; + +/// A successful conversion: the schema plus non-fatal warnings (unmappable +/// discriminators, dropped slice minimums, missing primitive regexes, ...). +#[derive(Debug)] +pub struct Conversion { + pub schema: FhirSchema, + pub warnings: Vec, +} + +/// A conversion failure. +#[derive(Debug)] +pub enum ConvertError { + /// The input is not a StructureDefinition. + NotAStructureDefinition, + /// The StructureDefinition is missing a required part. + Missing(&'static str), + /// The element list is malformed. + Malformed(String), +} + +impl fmt::Display for ConvertError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ConvertError::NotAStructureDefinition => { + write!(f, "input is not a StructureDefinition") + } + ConvertError::Missing(what) => write!(f, "StructureDefinition is missing {what}"), + ConvertError::Malformed(detail) => write!(f, "malformed StructureDefinition: {detail}"), + } + } +} + +impl std::error::Error for ConvertError {} + +// --------------------------------------------------------------------- +// Lightweight ElementDefinition model (tolerant serde over raw JSON). +// --------------------------------------------------------------------- + +#[derive(Debug, Deserialize)] +pub(crate) struct Ed { + pub id: Option, + pub path: String, + #[serde(rename = "sliceName")] + pub slice_name: Option, + pub min: Option, + pub max: Option, + pub base: Option, + #[serde(rename = "type", default)] + pub types: Vec, + #[serde(rename = "contentReference")] + pub content_reference: Option, + pub slicing: Option, + pub binding: Option, + #[serde(default)] + pub constraint: Vec, + /// Everything else — scanned for `fixed[x]` / `pattern[x]`. + #[serde(flatten)] + pub rest: serde_json::Map, +} + +#[derive(Debug, Deserialize)] +pub(crate) struct EdBase { + pub max: Option, +} + +#[derive(Debug, Deserialize)] +pub(crate) struct EdType { + pub code: String, + #[serde(default)] + pub profile: Vec, + #[serde(rename = "targetProfile", default)] + pub target_profile: Vec, + #[serde(default)] + pub extension: Vec, +} + +#[derive(Debug, Deserialize)] +pub(crate) struct EdTypeExtension { + pub url: String, + #[serde(rename = "valueString")] + pub value_string: Option, + #[serde(rename = "valueUrl")] + pub value_url: Option, + #[serde(rename = "valueUri")] + pub value_uri: Option, +} + +#[derive(Debug, Deserialize)] +pub(crate) struct EdSlicing { + #[serde(default)] + pub discriminator: Vec, + pub rules: Option, + pub ordered: Option, +} + +#[derive(Debug, Clone, Deserialize)] +pub(crate) struct EdDiscriminator { + #[serde(rename = "type")] + pub type_: String, + pub path: String, +} + +#[derive(Debug, Deserialize)] +pub(crate) struct EdBinding { + pub strength: Option, + #[serde(rename = "valueSet")] + pub value_set: Option, +} + +#[derive(Debug, Deserialize)] +pub(crate) struct EdConstraint { + pub key: String, + pub severity: Option, + pub human: Option, + pub expression: Option, +} + +/// The FHIR type-code extensions carried on `type[].extension`. +const REGEX_EXTENSION: &str = "http://hl7.org/fhir/StructureDefinition/regex"; +const FHIR_TYPE_EXTENSION: &str = + "http://hl7.org/fhir/StructureDefinition/structuredefinition-fhir-type"; + +impl EdType { + /// The effective type code: FHIRPath system types + /// (`http://hl7.org/fhirpath/System.String`, ...) resolve through the + /// `structuredefinition-fhir-type` extension, falling back to the + /// lower-cased suffix. + pub(crate) fn effective_code(&self) -> String { + if let Some(suffix) = self.code.strip_prefix("http://hl7.org/fhirpath/System.") { + for ext in &self.extension { + if ext.url == FHIR_TYPE_EXTENSION { + if let Some(v) = ext + .value_url + .as_deref() + .or(ext.value_uri.as_deref()) + .or(ext.value_string.as_deref()) + { + return v.to_string(); + } + } + } + let mut chars = suffix.chars(); + return match chars.next() { + Some(first) => first.to_lowercase().collect::() + chars.as_str(), + None => suffix.to_string(), + }; + } + self.code.clone() + } + + /// The regex constraint carried on primitive value elements. + pub(crate) fn regex(&self) -> Option<&str> { + self.extension + .iter() + .find(|e| e.url == REGEX_EXTENSION) + .and_then(|e| e.value_string.as_deref()) + } +} + +/// Convert a StructureDefinition (raw JSON, snapshot or differential) to a +/// FHIR Schema. +pub fn convert(sd: &Value) -> Result { + if sd.get("resourceType").and_then(Value::as_str) != Some("StructureDefinition") { + return Err(ConvertError::NotAStructureDefinition); + } + + let get_str = |key: &str| sd.get(key).and_then(Value::as_str).map(str::to_string); + let url = get_str("url").ok_or(ConvertError::Missing("url"))?; + let name = get_str("name"); + let sd_type = get_str("type").ok_or(ConvertError::Missing("type"))?; + let kind_raw = get_str("kind").ok_or(ConvertError::Missing("kind"))?; + let derivation = get_str("derivation"); + let base = get_str("baseDefinition"); + + let element_values = sd + .get("snapshot") + .and_then(|s| s.get("element")) + .or_else(|| sd.get("differential").and_then(|d| d.get("element"))) + .and_then(Value::as_array) + .ok_or(ConvertError::Missing("snapshot.element or differential.element"))?; + + let elements: Vec = element_values + .iter() + .map(|v| { + serde_json::from_value(v.clone()) + .map_err(|e| ConvertError::Malformed(format!("element: {e}"))) + }) + .collect::>()?; + + let mut warnings: Vec = Vec::new(); + + // Primitive types produce a leaf schema with a value regex. + if kind_raw == "primitive-type" { + let schema = primitives::convert_primitive( + url, name, &sd_type, derivation, &elements, &mut warnings, + ); + return Ok(Conversion { schema, warnings }); + } + + // `Extension`-constraining SDs get the dedicated kind. + let kind = if sd_type == "Extension" && derivation.as_deref() == Some("constraint") { + "extension".to_string() + } else { + kind_raw.clone() + }; + + // Build the nested element tree by navigating each ED's id segments. + let mut root = tree::Node::new(sd_type.clone()); + for ed in &elements { + // Navigation is id-based; hand-authored differentials may omit ids, + // in which case the slice position is reconstructed from sliceName. + let synthesized; + let id = match (&ed.id, &ed.slice_name) { + (Some(id), _) => id.as_str(), + (None, Some(slice)) => { + synthesized = format!("{}:{slice}", ed.path); + &synthesized + } + (None, None) => &ed.path, + }; + let segments = tree::parse_segments(id) + .map_err(|e| ConvertError::Malformed(format!("element id '{id}': {e}")))?; + if segments.is_empty() { + // Root element: constraints attach at schema level (all kept — + // dom-* live here). + tree::apply_constraints(&mut root.schema, &ed.constraint, true); + continue; + } + tree::apply(&mut root, &segments, ed, &mut warnings); + } + + let mut schema = tree::finalize(root, &mut warnings).unwrap_or_default(); + + // Header fields. + schema.url = Some(url); + schema.name = name; + schema.base = base; + schema.kind = Some(kind); + schema.derivation = derivation.clone(); + schema.type_ = Some(sd_type); + + // StructureDefinitions never declare `resourceType`; inject it for + // resource specializations (profiles inherit it via their base chain). + if kind_raw == "resource" && derivation.as_deref() != Some("constraint") { + let mut elements = indexmap::IndexMap::new(); + elements.insert( + "resourceType".to_string(), + std::sync::Arc::new(FhirSchema { + type_: Some("code".to_string()), + ..Default::default() + }), + ); + if let Some(existing) = schema.elements.take() { + elements.extend(existing); + } + schema.elements = Some(elements); + } + + Ok(Conversion { schema, warnings }) +} diff --git a/crates/fhir-validator/src/converter/primitives.rs b/crates/fhir-validator/src/converter/primitives.rs new file mode 100644 index 000000000..9fd888663 --- /dev/null +++ b/crates/fhir-validator/src/converter/primitives.rs @@ -0,0 +1,45 @@ +//! Primitive-type StructureDefinitions → leaf schemas. +//! +//! A primitive SD becomes `{kind: "primitive-type"}` plus the value regex +//! extracted from its `.value` element +//! (`type[0].extension[url=.../regex].valueString`). No `base` is emitted: +//! pulling `Element` into every primitive's cooperative set would be wasted +//! work — the `_field` sidecar machinery owns the Element part instead. +//! +//! There is deliberately no hardcoded fallback regex table: the spec files +//! carry the regexes, and a wrong guess is worse than the JSON type-class +//! check the engine applies from the primitive's name anyway. A missing +//! regex just produces a warning. + +use super::Ed; +use crate::schema::FhirSchema; + +pub(super) fn convert_primitive( + url: String, + name: Option, + sd_type: &str, + derivation: Option, + elements: &[Ed], + warnings: &mut Vec, +) -> FhirSchema { + let value_path = format!("{sd_type}.value"); + let regex = elements + .iter() + .find(|ed| ed.path == value_path) + .and_then(|ed| ed.types.first()) + .and_then(|t| t.regex()) + .map(str::to_string); + if regex.is_none() { + warnings.push(format!("primitive '{sd_type}': no value regex found")); + } + + FhirSchema { + url: Some(url), + name, + kind: Some(crate::schema::kind::PRIMITIVE_TYPE.to_string()), + derivation, + type_: Some(sd_type.to_string()), + regex, + ..Default::default() + } +} diff --git a/crates/fhir-validator/src/converter/slicing.rs b/crates/fhir-validator/src/converter/slicing.rs new file mode 100644 index 000000000..d549c3a8f --- /dev/null +++ b/crates/fhir-validator/src/converter/slicing.rs @@ -0,0 +1,128 @@ +//! Discriminator → slice-match translation. +//! +//! A `value`/`pattern` discriminator at path `P` becomes a partial-match +//! pattern built from the `fixed[x]`/`pattern[x]` constant found at `P` +//! inside the slice's subtree (`$this` meaning the item root). Discriminator +//! types we cannot evaluate yet (`type`, `profile`, `exists`, and any path +//! containing `resolve()` or `extension(...)`) produce a slice with **no +//! match and no minimum**: it can never produce false cardinality errors, +//! its constraints simply stay dormant until the matcher lands (Phase 7), +//! and the generator surfaces a warning. + +use super::tree::{finalize, SliceNode}; +use super::EdDiscriminator; +use crate::schema::{Match, Slice, Slicing}; +use indexmap::IndexMap; +use serde_json::{Map, Value}; +use std::sync::Arc; + +pub(super) fn build_slicing( + slices: IndexMap, + discriminators: &[EdDiscriminator], + rules: Option, + ordered: Option, + warnings: &mut Vec, +) -> Option { + let mut out: IndexMap = IndexMap::new(); + for (name, slice_node) in slices { + let SliceNode { node, min, max, extension_profile: _ } = slice_node; + + let match_ = build_match(&node, discriminators); + if match_.is_none() { + warnings.push(format!( + "slice '{name}': discriminator(s) {:?} not translatable to a match; \ + slice kept without match or min", + discriminators.iter().map(|d| format!("{}@{}", d.type_, d.path)).collect::>() + )); + } + + let schema = finalize(node, warnings); + out.insert( + name, + Slice { + // Without a matcher the slice must not enforce a minimum — + // nothing can ever match it. + min: if match_.is_some() { min } else { None }, + max, + match_, + order: None, + reslice: None, + slice_is_constraining: None, + schema: schema.map(Arc::new), + }, + ); + } + + if out.is_empty() { + return None; + } + Some(Slicing { slices: out, rules, ordered }) +} + +/// Build a pattern match from `value`/`pattern` discriminators, reading the +/// constant at each discriminator path out of the slice subtree. +fn build_match(node: &super::tree::Node, discriminators: &[EdDiscriminator]) -> Option { + if discriminators.is_empty() { + return None; + } + + let mut this_constant: Option = None; + let mut pattern = Map::new(); + + for disc in discriminators { + if !matches!(disc.type_.as_str(), "value" | "pattern") { + return None; + } + if disc.path.contains("resolve()") || disc.path.contains("extension(") { + return None; + } + let constant = constant_at(node, &disc.path)?; + if disc.path == "$this" { + if discriminators.len() > 1 { + return None; // $this plus siblings — ambiguous + } + this_constant = Some(constant); + } else { + insert_nested(&mut pattern, &disc.path, constant); + } + } + + let value = match this_constant { + Some(v) => v, + None if !pattern.is_empty() => Value::Object(pattern), + None => return None, + }; + Some(Match { type_: Some("pattern".to_string()), value: Some(value), resolve_ref: None }) +} + +/// The fixed/pattern constant at `path` within the slice subtree +/// (`$this` → the subtree root itself). +fn constant_at(node: &super::tree::Node, path: &str) -> Option { + let target = if path == "$this" { + node + } else { + let mut current = node; + for segment in path.split('.') { + current = current.children.get(segment)?; + } + current + }; + target.schema.fixed.clone().or_else(|| target.schema.pattern.clone()) +} + +/// `insert_nested(map, "a.b", v)` → `{a: {b: v}}` (merging siblings). +fn insert_nested(map: &mut Map, path: &str, value: Value) { + let mut segments = path.split('.').peekable(); + let mut current = map; + while let Some(segment) = segments.next() { + if segments.peek().is_none() { + current.insert(segment.to_string(), value); + return; + } + current = current + .entry(segment.to_string()) + .or_insert_with(|| Value::Object(Map::new())) + .as_object_mut() + .expect("nested pattern segment is an object"); + } +} diff --git a/crates/fhir-validator/src/converter/tree.rs b/crates/fhir-validator/src/converter/tree.rs new file mode 100644 index 000000000..47d400d1d --- /dev/null +++ b/crates/fhir-validator/src/converter/tree.rs @@ -0,0 +1,426 @@ +//! ElementDefinition-id → nested tree reconstruction. +//! +//! Each ElementDefinition is applied by navigating its `id` absolutely from +//! the root, segment by segment (`name` or `name:sliceName`), creating empty +//! intermediate nodes as needed — which is what makes sparse differentials +//! work without snapshot generation. Choice (`foo[x]`) expansion also lives +//! here: the `[x]` element becomes a `choices` declarer plus one `choiceOf` +//! branch element per type. + +use super::{Ed, EdConstraint, EdDiscriminator}; +use crate::schema::{Binding, Constraint, FhirSchema}; +use indexmap::IndexMap; +use std::sync::Arc; + +/// One `id` step: an element name plus an optional slice name. +#[derive(Debug, Clone, PartialEq)] +pub(super) struct Segment { + pub name: String, + pub slice: Option, +} + +/// Parse an ElementDefinition id into segments, dropping the root segment. +/// `"Patient.identifier:mrn.system"` → `[identifier:mrn, system]`. +pub(super) fn parse_segments(id: &str) -> Result, String> { + let mut parts = id.split('.'); + let root = parts.next().ok_or("empty id")?; + if root.is_empty() { + return Err("empty root segment".to_string()); + } + let mut segments = Vec::new(); + for part in parts { + if part.is_empty() { + return Err("empty segment".to_string()); + } + match part.split_once(':') { + Some((name, slice)) if !name.is_empty() && !slice.is_empty() => { + segments.push(Segment { name: name.to_string(), slice: Some(slice.to_string()) }); + } + Some(_) => return Err(format!("malformed slice segment '{part}'")), + None => segments.push(Segment { name: part.to_string(), slice: None }), + } + } + Ok(segments) +} + +/// A node of the element tree under construction. +#[derive(Debug, Default)] +pub(super) struct Node { + /// The element name this node sits under (used for extension-sugar + /// detection on `extension`/`modifierExtension`). + pub element_name: String, + /// Accumulated element-level keywords. + pub schema: FhirSchema, + pub children: IndexMap, + pub slices: IndexMap, + /// Slicing declaration from the sliced element's own ED. + pub discriminators: Vec, + pub slicing_rules: Option, + pub slicing_ordered: Option, + /// `max: "0"` — the element is prohibited; recorded on the parent's + /// `excluded` and the subtree is dropped. + pub dead: bool, +} + +impl Node { + pub(super) fn new(element_name: String) -> Self { + Self { element_name, ..Default::default() } + } +} + +#[derive(Debug, Default)] +pub(super) struct SliceNode { + pub node: Node, + pub min: Option, + pub max: Option, + /// `type[0] == Extension` with a profile: compiles to the parent-level + /// `extensions` sugar instead of raw slicing. + pub extension_profile: Option, +} + +/// Apply one ElementDefinition at its navigated position. +pub(super) fn apply(root: &mut Node, segments: &[Segment], ed: &Ed, warnings: &mut Vec) { + // Navigate to the parent of the final segment. + let mut node: &mut Node = root; + for seg in &segments[..segments.len() - 1] { + if node.dead { + return; + } + node = descend(node, seg); + } + if node.dead { + return; + } + let last = &segments[segments.len() - 1]; + + // Explicit choice-branch constraint: `value[x]:valueQuantity`. + if let (true, Some(slice)) = (last.name.ends_with("[x]"), &last.slice) { + let branch = node + .children + .entry(slice.clone()) + .or_insert_with(|| Node::new(slice.clone())); + apply_element_content(branch, ed, warnings); + return; + } + + // Slice definition: `identifier:mrn`. + if let Some(slice_name) = &last.slice { + let element = node + .children + .entry(last.name.clone()) + .or_insert_with(|| Node::new(last.name.clone())); + let slice = element.slices.entry(slice_name.clone()).or_default(); + slice.min = ed.min; + slice.max = parse_numeric_max(ed.max.as_deref()); + if matches!(element.element_name.as_str(), "extension" | "modifierExtension") + && let Some(first) = ed.types.first() + && first.code == "Extension" + && let Some(profile) = first.profile.first() + { + slice.extension_profile = Some(profile.clone()); + } + // Value-level keywords on the slice ED itself apply to the slice + // schema (shape/cardinality were consumed above as slice bounds). + apply_value_keywords(&mut slice.node.schema, ed, warnings); + return; + } + + // Choice element: `deceased[x]` — declarer + one branch per type. + if let Some(base_name) = last.name.strip_suffix("[x]") { + apply_choice(node, base_name, ed, warnings); + return; + } + + // Plain element. + let name = last.name.clone(); + if ed.max.as_deref() == Some("0") { + push_unique(&mut node.schema.excluded, &name); + let element = + node.children.entry(name.clone()).or_insert_with(|| Node::new(name.clone())); + element.dead = true; + return; + } + if ed.min.unwrap_or(0) >= 1 { + push_unique(&mut node.schema.required, &name); + } + let element = node.children.entry(name.clone()).or_insert_with(|| Node::new(name)); + if let Some(slicing) = &ed.slicing { + element.discriminators = slicing.discriminator.clone(); + element.slicing_rules = slicing.rules.clone(); + element.slicing_ordered = slicing.ordered; + } + apply_shape(element, ed); + apply_element_content(element, ed, warnings); +} + +fn descend<'a>(node: &'a mut Node, seg: &Segment) -> &'a mut Node { + let child = node + .children + .entry(seg.name.clone()) + .or_insert_with(|| Node::new(seg.name.clone())); + match &seg.slice { + Some(slice) => &mut child.slices.entry(slice.clone()).or_default().node, + None => child, + } +} + +/// Array-ness comes from `base.max` when present (profiles constrain the +/// count, not the shape), else the ED's own `max`/`min`. +fn apply_shape(element: &mut Node, ed: &Ed) { + let shape_max = ed.base.as_ref().and_then(|b| b.max.as_deref()).or(ed.max.as_deref()); + let is_array = match shape_max { + Some("*") => true, + Some(n) => n.parse::().map(|n| n > 1).unwrap_or(false), + None => ed.min.unwrap_or(0) > 1, + }; + if is_array { + element.schema.array = Some(true); + if let Some(min) = ed.min + && min > 0 + { + element.schema.min = Some(min); + } + if let Some(max) = parse_numeric_max(ed.max.as_deref()) { + element.schema.max = Some(max); + } + } +} + +/// Type reference, contentReference, and the value-level keywords. +fn apply_element_content(element: &mut Node, ed: &Ed, warnings: &mut Vec) { + match ed.types.len() { + 0 => { + if let Some(reference) = &ed.content_reference { + match parse_content_reference(reference) { + Some(segments) => element.schema.element_reference = Some(segments), + None => warnings + .push(format!("{}: unparseable contentReference '{reference}'", ed.path)), + } + } + } + 1 => { + let t = &ed.types[0]; + element.schema.type_ = Some(t.effective_code()); + if !t.target_profile.is_empty() { + element.schema.refers = Some(t.target_profile.clone()); + } + } + _ => { + // Multiple types without `[x]` should not occur; take the first. + warnings.push(format!("{}: multiple types without [x]; using the first", ed.path)); + element.schema.type_ = Some(ed.types[0].effective_code()); + } + } + apply_value_keywords(&mut element.schema, ed, warnings); +} + +/// Binding, constraints, fixed/pattern — the keywords that apply to a value +/// wherever the ED lands (plain element, choice branch, or slice schema). +fn apply_value_keywords(schema: &mut FhirSchema, ed: &Ed, _warnings: &mut Vec) { + if let Some(binding) = &ed.binding + && let Some(value_set) = &binding.value_set + { + schema.binding = Some(Binding { + value_set: value_set.clone(), + strength: binding.strength.clone(), + }); + } + apply_constraints(schema, &ed.constraint, false); + for (key, value) in &ed.rest { + if let Some(rest) = key.strip_prefix("fixed") + && !rest.is_empty() + && !key.starts_with('_') + { + schema.fixed = Some(value.clone()); + } else if let Some(rest) = key.strip_prefix("pattern") + && !rest.is_empty() + && !key.starts_with('_') + { + schema.pattern = Some(value.clone()); + } + } +} + +/// Carry FHIRPath constraints. On non-root elements the ubiquitous inherited +/// invariants (`ele-1` on every element, `ext-1` on every extension) are +/// dropped — they are enforced once via the `Element`/`Extension` type +/// schemas that join every relevant cooperative set, and carrying them +/// per-element would bloat packs and the deferred-effects list enormously. +pub(super) fn apply_constraints(schema: &mut FhirSchema, constraints: &[EdConstraint], root: bool) { + for c in constraints { + if !root && matches!(c.key.as_str(), "ele-1" | "ext-1") { + continue; + } + let Some(expression) = &c.expression else { + continue; // xpath-only constraint + }; + schema + .constraints + .get_or_insert_with(IndexMap::new) + .entry(c.key.clone()) + .or_insert_with(|| Constraint { + expression: expression.clone(), + severity: c.severity.clone(), + human: c.human.clone(), + }); + } +} + +/// Expand `foo[x]`: parent gets a `choices` declarer plus one branch element +/// per type, each carrying the value-level keywords of the `[x]` ED. +fn apply_choice(parent: &mut Node, base_name: &str, ed: &Ed, warnings: &mut Vec) { + if ed.max.as_deref() == Some("0") { + push_unique(&mut parent.schema.excluded, base_name); + return; + } + if ed.min.unwrap_or(0) >= 1 { + push_unique(&mut parent.schema.required, base_name); + } + + let mut branch_names = Vec::new(); + for t in &ed.types { + let code = t.effective_code(); + let branch_name = format!("{base_name}{}", capitalize(&code)); + branch_names.push(branch_name.clone()); + + let branch = parent + .children + .entry(branch_name.clone()) + .or_insert_with(|| Node::new(branch_name)); + branch.schema.type_ = Some(code); + branch.schema.choice_of = Some(base_name.to_string()); + if !t.target_profile.is_empty() { + branch.schema.refers = Some(t.target_profile.clone()); + } + apply_value_keywords(&mut branch.schema, ed, warnings); + } + + let declarer = parent + .children + .entry(base_name.to_string()) + .or_insert_with(|| Node::new(base_name.to_string())); + match &mut declarer.schema.choices { + Some(existing) => { + for name in branch_names { + if !existing.contains(&name) { + existing.push(name); + } + } + } + None => declarer.schema.choices = Some(branch_names), + } +} + +/// Recursively turn a tree node into a schema, pruning empty nodes. +pub(super) fn finalize(node: Node, warnings: &mut Vec) -> Option { + let Node { + element_name: _, + mut schema, + children, + slices, + discriminators, + slicing_rules, + slicing_ordered, + dead, + } = node; + + if dead { + return None; + } + + // Slices on this node (non-sugar) become a slicing definition. + if !slices.is_empty() { + let sugar: Vec<&String> = + slices.iter().filter(|(_, s)| s.extension_profile.is_some()).map(|(n, _)| n).collect(); + debug_assert!(sugar.is_empty(), "extension-sugar slices are lifted by the parent"); + let slicing = + super::slicing::build_slicing(slices, &discriminators, slicing_rules, slicing_ordered, warnings); + if let Some(slicing) = slicing { + schema.slicing = Some(slicing); + } + } + + // Children: lift extension-sugar slices into our `extensions` map, then + // finalize the rest into `elements`. + let mut elements: IndexMap> = IndexMap::new(); + for (name, mut child) in children { + // Extension sugar: slices on an extension child with a type profile + // become entries in *this* schema's `extensions` map. + if matches!(child.element_name.as_str(), "extension" | "modifierExtension") { + let mut remaining: IndexMap = IndexMap::new(); + for (slice_name, slice) in std::mem::take(&mut child.slices) { + match &slice.extension_profile { + Some(profile) => { + let mut entry = finalize(slice.node, warnings).unwrap_or_default(); + entry.url = Some(profile.clone()); + entry.min = slice.min; + entry.max = slice.max; + schema + .extensions + .get_or_insert_with(IndexMap::new) + .insert(slice_name, Arc::new(entry)); + } + None => { + remaining.insert(slice_name, slice); + } + } + } + child.slices = remaining; + } + if let Some(child_schema) = finalize(child, warnings) { + elements.insert(name, Arc::new(child_schema)); + } + } + if !elements.is_empty() { + schema.elements = Some(elements); + } + + if schema == FhirSchema::default() { + None + } else { + Some(schema) + } +} + +fn parse_numeric_max(max: Option<&str>) -> Option { + match max { + Some("*") | None => None, + Some(n) => n.parse().ok(), + } +} + +fn parse_content_reference(reference: &str) -> Option> { + let path = reference.strip_prefix('#')?; + let mut parts = path.split('.'); + let root = parts.next()?; + if root.is_empty() { + return None; + } + let mut segments = vec![root.to_string()]; + for part in parts { + if part.is_empty() { + return None; + } + segments.push("elements".to_string()); + segments.push(part.to_string()); + } + if segments.len() == 1 { + return None; // reference to a whole root — not an element reference + } + Some(segments) +} + +fn push_unique(list: &mut Option>, value: &str) { + let list = list.get_or_insert_with(Vec::new); + if !list.iter().any(|v| v == value) { + list.push(value.to_string()); + } +} + +fn capitalize(s: &str) -> String { + let mut chars = s.chars(); + match chars.next() { + Some(first) => first.to_uppercase().collect::() + chars.as_str(), + None => String::new(), + } +} diff --git a/crates/fhir-validator/src/lib.rs b/crates/fhir-validator/src/lib.rs index 83d6e082d..e7f65b4c2 100644 --- a/crates/fhir-validator/src/lib.rs +++ b/crates/fhir-validator/src/lib.rs @@ -41,8 +41,10 @@ //! `tests/fixtures/upstream/` (exact ordered error matching), plus Helios //! extended fixtures in `tests/fixtures/extended/`. +pub mod converter; pub mod effects; pub mod engine; +pub mod packs; pub mod resolver; pub mod schema; diff --git a/crates/fhir-validator/src/packs.rs b/crates/fhir-validator/src/packs.rs new file mode 100644 index 000000000..a1b8b9288 --- /dev/null +++ b/crates/fhir-validator/src/packs.rs @@ -0,0 +1,81 @@ +//! Embedded core schema packs. +//! +//! One gzipped JSON array of FHIR Schemas per enabled FHIR version, +//! generated by the `generate-schema-packs` binary from the spec bundles in +//! `crates/fhir-gen/resources/` and committed under `packs/` (the same +//! committed-generated-artifact workflow as the fhir-gen models). +//! +//! Registries are built lazily, once per process per version. + +use crate::resolver::SchemaRegistry; +use crate::schema::FhirSchema; +use flate2::read::GzDecoder; +use helios_fhir::FhirVersion; +use std::io::Read; +use std::sync::{Arc, OnceLock}; + +#[cfg(feature = "R4")] +static R4_PACK: &[u8] = include_bytes!("../packs/fhir_schemas_r4.json.gz"); +#[cfg(feature = "R4B")] +static R4B_PACK: &[u8] = include_bytes!("../packs/fhir_schemas_r4b.json.gz"); +#[cfg(feature = "R5")] +static R5_PACK: &[u8] = include_bytes!("../packs/fhir_schemas_r5.json.gz"); +#[cfg(feature = "R6")] +static R6_PACK: &[u8] = include_bytes!("../packs/fhir_schemas_r6.json.gz"); + +/// The embedded core registry for a FHIR version. Parsed on first use; +/// subsequent calls return the shared instance. +pub fn core_registry(version: FhirVersion) -> Arc { + match version { + #[cfg(feature = "R4")] + FhirVersion::R4 => { + static REG: OnceLock> = OnceLock::new(); + Arc::clone(REG.get_or_init(|| Arc::new(load_pack(R4_PACK)))) + } + #[cfg(feature = "R4B")] + FhirVersion::R4B => { + static REG: OnceLock> = OnceLock::new(); + Arc::clone(REG.get_or_init(|| Arc::new(load_pack(R4B_PACK)))) + } + #[cfg(feature = "R5")] + FhirVersion::R5 => { + static REG: OnceLock> = OnceLock::new(); + Arc::clone(REG.get_or_init(|| Arc::new(load_pack(R5_PACK)))) + } + #[cfg(feature = "R6")] + FhirVersion::R6 => { + static REG: OnceLock> = OnceLock::new(); + Arc::clone(REG.get_or_init(|| Arc::new(load_pack(R6_PACK)))) + } + // Workspace feature unification can add FhirVersion variants beyond + // the packs this crate was built with. + #[allow(unreachable_patterns)] + other => panic!( + "no schema pack embedded for FHIR version {other:?} — enable the matching \ + helios-fhir-validator feature" + ), + } +} + +/// Decompress and index a pack. +/// +/// Insertion is two-pass — profiles (`derivation: constraint`) first, core +/// types and resources second — so that when a profile's `name` collides +/// with a core type name, the core schema wins the name alias. Canonical +/// URLs are unique and unaffected. +fn load_pack(bytes: &[u8]) -> SchemaRegistry { + let mut decoder = GzDecoder::new(bytes); + let mut json = Vec::new(); + decoder.read_to_end(&mut json).expect("embedded pack decompresses"); + let schemas: Vec = + serde_json::from_slice(&json).expect("embedded pack parses"); + + let mut registry = SchemaRegistry::new(); + let (profiles, core): (Vec<_>, Vec<_>) = schemas + .into_iter() + .partition(|s| s.derivation.as_deref() == Some("constraint")); + for schema in profiles.into_iter().chain(core) { + registry.insert(schema); + } + registry +} diff --git a/crates/fhir-validator/tests/converter_tests.rs b/crates/fhir-validator/tests/converter_tests.rs new file mode 100644 index 000000000..ae7384afd --- /dev/null +++ b/crates/fhir-validator/tests/converter_tests.rs @@ -0,0 +1,191 @@ +//! Golden tests for the StructureDefinition → FhirSchema converter. +//! +//! Each fixture in `tests/fixtures/structuredefinitions/` is a trimmed but +//! shape-faithful StructureDefinition; the expected FhirSchema is asserted +//! with exact deep equality on the serialized form, so every mapping rule is +//! pinned: shape (base.max → array), min → parent required (choice base name +//! for `foo[x]`), max 0 → parent excluded, choice expansion, discriminator → +//! pattern match, extension slicing → extensions sugar, contentReference → +//! elementReference, targetProfile → refers, binding/constraint carrying +//! (ele-1/ext-1 dropped on non-root elements), and primitive regex +//! extraction. + +use helios_fhir_validator::converter::convert; +use serde_json::{json, Value}; +use std::fs; +use std::path::PathBuf; + +fn load_sd(name: &str) -> Value { + let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("tests/fixtures/structuredefinitions") + .join(name); + serde_json::from_str(&fs::read_to_string(&path).unwrap()).unwrap() +} + +fn convert_to_value(name: &str) -> Value { + let sd = load_sd(name); + let conversion = convert(&sd).unwrap_or_else(|e| panic!("{name}: conversion failed: {e}")); + serde_json::to_value(&conversion.schema).unwrap() +} + +#[test] +fn converts_snapshot_resource() { + let actual = convert_to_value("mini-patient.json"); + let expected = json!({ + "url": "http://hl7.org/fhir/StructureDefinition/Patient", + "name": "Patient", + "base": "http://hl7.org/fhir/StructureDefinition/DomainResource", + "kind": "resource", + "derivation": "specialization", + "type": "Patient", + "constraints": { + "dom-2": { + "expression": "contained.contained.empty()", + "severity": "error", + "human": "If the resource is contained in another resource, it SHALL NOT contain nested Resources" + }, + "dom-6": { + "expression": "text.`div`.exists()", + "severity": "warning", + "human": "A resource should have narrative for robust management" + } + }, + "elements": { + "resourceType": { "type": "code" }, + "gender": { + "type": "code", + "binding": { + "valueSet": "http://hl7.org/fhir/ValueSet/administrative-gender|4.0.1", + "strength": "required" + } + }, + "name": { "type": "HumanName", "array": true }, + "deceased": { "choices": ["deceasedBoolean", "deceasedDateTime"] }, + "deceasedBoolean": { "type": "boolean", "choiceOf": "deceased" }, + "deceasedDateTime": { "type": "dateTime", "choiceOf": "deceased" }, + "link": { + "type": "BackboneElement", + "array": true, + "required": ["other", "type"], + "constraints": { + "pat-1": { + "expression": "other.exists()", + "severity": "error", + "human": "Contact must have details" + } + }, + "elements": { + "other": { + "type": "Reference", + "refers": [ + "http://hl7.org/fhir/StructureDefinition/Patient", + "http://hl7.org/fhir/StructureDefinition/RelatedPerson" + ] + }, + "type": { + "type": "code", + "binding": { + "valueSet": "http://hl7.org/fhir/ValueSet/link-type|4.0.1", + "strength": "required" + } + } + } + } + } + }); + assert_eq!(actual, expected); +} + +#[test] +fn converts_differential_profile() { + let actual = convert_to_value("mini-profile.json"); + let expected = json!({ + "url": "http://example.org/StructureDefinition/mini-patient-profile", + "name": "MiniPatientProfile", + "base": "http://hl7.org/fhir/StructureDefinition/Patient", + "kind": "resource", + "derivation": "constraint", + "type": "Patient", + "required": ["birthDate", "identifier"], + "excluded": ["gender"], + "extensions": { + "race": { + "url": "http://example.org/StructureDefinition/race", + "min": 1, + "max": 1 + } + }, + "elements": { + "identifier": { + "array": true, + "min": 1, + "slicing": { + "slices": { + "mrn": { + "match": { + "type": "pattern", + "value": { "system": "http://example.org/mrn" } + }, + "min": 1, + "max": 1, + "schema": { + "required": ["system"], + "elements": { + "system": { "fixed": "http://example.org/mrn" } + } + } + } + }, + "rules": "open" + } + }, + "maritalStatus": { + "pattern": { "coding": [{ "system": "http://x" }] } + } + } + }); + assert_eq!(actual, expected); +} + +#[test] +fn converts_content_reference() { + let actual = convert_to_value("mini-questionnaire.json"); + let expected = json!({ + "url": "http://hl7.org/fhir/StructureDefinition/Questionnaire", + "name": "Questionnaire", + "base": "http://hl7.org/fhir/StructureDefinition/DomainResource", + "kind": "resource", + "derivation": "specialization", + "type": "Questionnaire", + "elements": { + "resourceType": { "type": "code" }, + "item": { + "type": "BackboneElement", + "array": true, + "required": ["linkId"], + "elements": { + "linkId": { "type": "string" }, + "item": { + "array": true, + "elementReference": ["Questionnaire", "elements", "item"] + } + } + } + } + }); + assert_eq!(actual, expected); +} + +#[test] +fn converts_primitive_with_regex() { + let actual = convert_to_value("primitive-string.json"); + let expected = json!({ + "url": "http://hl7.org/fhir/StructureDefinition/string", + "name": "string", + "kind": "primitive-type", + "derivation": "specialization", + "type": "string", + "regex": "[ \\r\\n\\t\\S]+" + }); + assert_eq!(actual, expected); +} diff --git a/crates/fhir-validator/tests/fixtures/structuredefinitions/mini-patient.json b/crates/fhir-validator/tests/fixtures/structuredefinitions/mini-patient.json new file mode 100644 index 000000000..e655516e7 --- /dev/null +++ b/crates/fhir-validator/tests/fixtures/structuredefinitions/mini-patient.json @@ -0,0 +1,123 @@ +{ + "resourceType": "StructureDefinition", + "id": "Patient", + "url": "http://hl7.org/fhir/StructureDefinition/Patient", + "name": "Patient", + "status": "active", + "kind": "resource", + "abstract": false, + "type": "Patient", + "baseDefinition": "http://hl7.org/fhir/StructureDefinition/DomainResource", + "derivation": "specialization", + "snapshot": { + "element": [ + { + "id": "Patient", + "path": "Patient", + "min": 0, + "max": "*", + "base": { "path": "Patient", "min": 0, "max": "*" }, + "constraint": [ + { + "key": "dom-2", + "severity": "error", + "human": "If the resource is contained in another resource, it SHALL NOT contain nested Resources", + "expression": "contained.contained.empty()" + }, + { + "key": "dom-6", + "severity": "warning", + "human": "A resource should have narrative for robust management", + "expression": "text.`div`.exists()" + } + ] + }, + { + "id": "Patient.gender", + "path": "Patient.gender", + "min": 0, + "max": "1", + "base": { "path": "Patient.gender", "min": 0, "max": "1" }, + "type": [{ "code": "code" }], + "binding": { + "strength": "required", + "valueSet": "http://hl7.org/fhir/ValueSet/administrative-gender|4.0.1" + }, + "constraint": [ + { + "key": "ele-1", + "severity": "error", + "human": "All FHIR elements must have a @value or children", + "expression": "hasValue() or (children().count() > id.count())" + } + ] + }, + { + "id": "Patient.name", + "path": "Patient.name", + "min": 0, + "max": "*", + "base": { "path": "Patient.name", "min": 0, "max": "*" }, + "type": [{ "code": "HumanName" }] + }, + { + "id": "Patient.deceased[x]", + "path": "Patient.deceased[x]", + "min": 0, + "max": "1", + "base": { "path": "Patient.deceased[x]", "min": 0, "max": "1" }, + "type": [{ "code": "boolean" }, { "code": "dateTime" }] + }, + { + "id": "Patient.link", + "path": "Patient.link", + "min": 0, + "max": "*", + "base": { "path": "Patient.link", "min": 0, "max": "*" }, + "type": [{ "code": "BackboneElement" }], + "constraint": [ + { + "key": "pat-1", + "severity": "error", + "human": "Contact must have details", + "expression": "other.exists()" + }, + { + "key": "ele-1", + "severity": "error", + "human": "All FHIR elements must have a @value or children", + "expression": "hasValue() or (children().count() > id.count())" + } + ] + }, + { + "id": "Patient.link.other", + "path": "Patient.link.other", + "min": 1, + "max": "1", + "base": { "path": "Patient.link.other", "min": 1, "max": "1" }, + "type": [ + { + "code": "Reference", + "targetProfile": [ + "http://hl7.org/fhir/StructureDefinition/Patient", + "http://hl7.org/fhir/StructureDefinition/RelatedPerson" + ] + } + ] + }, + { + "id": "Patient.link.type", + "path": "Patient.link.type", + "min": 1, + "max": "1", + "base": { "path": "Patient.link.type", "min": 1, "max": "1" }, + "type": [{ "code": "code" }], + "binding": { + "strength": "required", + "valueSet": "http://hl7.org/fhir/ValueSet/link-type|4.0.1" + } + } + ] + } +} diff --git a/crates/fhir-validator/tests/fixtures/structuredefinitions/mini-profile.json b/crates/fhir-validator/tests/fixtures/structuredefinitions/mini-profile.json new file mode 100644 index 000000000..7ddbd9c4c --- /dev/null +++ b/crates/fhir-validator/tests/fixtures/structuredefinitions/mini-profile.json @@ -0,0 +1,60 @@ +{ + "resourceType": "StructureDefinition", + "id": "mini-patient-profile", + "url": "http://example.org/StructureDefinition/mini-patient-profile", + "name": "MiniPatientProfile", + "status": "active", + "kind": "resource", + "abstract": false, + "type": "Patient", + "baseDefinition": "http://hl7.org/fhir/StructureDefinition/Patient", + "derivation": "constraint", + "differential": { + "element": [ + { "id": "Patient", "path": "Patient" }, + { "id": "Patient.birthDate", "path": "Patient.birthDate", "min": 1 }, + { "id": "Patient.gender", "path": "Patient.gender", "max": "0" }, + { + "id": "Patient.identifier", + "path": "Patient.identifier", + "min": 1, + "max": "*", + "slicing": { + "discriminator": [{ "type": "value", "path": "system" }], + "rules": "open" + } + }, + { + "id": "Patient.identifier:mrn", + "path": "Patient.identifier", + "sliceName": "mrn", + "min": 1, + "max": "1" + }, + { + "id": "Patient.identifier:mrn.system", + "path": "Patient.identifier.system", + "min": 1, + "fixedUri": "http://example.org/mrn" + }, + { + "id": "Patient.maritalStatus", + "path": "Patient.maritalStatus", + "patternCodeableConcept": { "coding": [{ "system": "http://x" }] } + }, + { + "id": "Patient.extension:race", + "path": "Patient.extension", + "sliceName": "race", + "min": 1, + "max": "1", + "type": [ + { + "code": "Extension", + "profile": ["http://example.org/StructureDefinition/race"] + } + ] + } + ] + } +} diff --git a/crates/fhir-validator/tests/fixtures/structuredefinitions/mini-questionnaire.json b/crates/fhir-validator/tests/fixtures/structuredefinitions/mini-questionnaire.json new file mode 100644 index 000000000..07b8c117c --- /dev/null +++ b/crates/fhir-validator/tests/fixtures/structuredefinitions/mini-questionnaire.json @@ -0,0 +1,47 @@ +{ + "resourceType": "StructureDefinition", + "id": "Questionnaire", + "url": "http://hl7.org/fhir/StructureDefinition/Questionnaire", + "name": "Questionnaire", + "status": "active", + "kind": "resource", + "abstract": false, + "type": "Questionnaire", + "baseDefinition": "http://hl7.org/fhir/StructureDefinition/DomainResource", + "derivation": "specialization", + "snapshot": { + "element": [ + { + "id": "Questionnaire", + "path": "Questionnaire", + "min": 0, + "max": "*", + "base": { "path": "Questionnaire", "min": 0, "max": "*" } + }, + { + "id": "Questionnaire.item", + "path": "Questionnaire.item", + "min": 0, + "max": "*", + "base": { "path": "Questionnaire.item", "min": 0, "max": "*" }, + "type": [{ "code": "BackboneElement" }] + }, + { + "id": "Questionnaire.item.linkId", + "path": "Questionnaire.item.linkId", + "min": 1, + "max": "1", + "base": { "path": "Questionnaire.item.linkId", "min": 1, "max": "1" }, + "type": [{ "code": "string" }] + }, + { + "id": "Questionnaire.item.item", + "path": "Questionnaire.item.item", + "min": 0, + "max": "*", + "base": { "path": "Questionnaire.item.item", "min": 0, "max": "*" }, + "contentReference": "#Questionnaire.item" + } + ] + } +} diff --git a/crates/fhir-validator/tests/fixtures/structuredefinitions/primitive-string.json b/crates/fhir-validator/tests/fixtures/structuredefinitions/primitive-string.json new file mode 100644 index 000000000..4214b5960 --- /dev/null +++ b/crates/fhir-validator/tests/fixtures/structuredefinitions/primitive-string.json @@ -0,0 +1,46 @@ +{ + "resourceType": "StructureDefinition", + "id": "string", + "url": "http://hl7.org/fhir/StructureDefinition/string", + "name": "string", + "status": "active", + "kind": "primitive-type", + "abstract": false, + "type": "string", + "baseDefinition": "http://hl7.org/fhir/StructureDefinition/Element", + "derivation": "specialization", + "snapshot": { + "element": [ + { + "id": "string", + "path": "string", + "min": 0, + "max": "*", + "base": { "path": "string", "min": 0, "max": "*" } + }, + { + "id": "string.value", + "path": "string.value", + "representation": ["xmlAttr"], + "min": 0, + "max": "1", + "base": { "path": "string.value", "min": 0, "max": "1" }, + "type": [ + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/structuredefinition-fhir-type", + "valueUrl": "string" + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/regex", + "valueString": "[ \\r\\n\\t\\S]+" + } + ], + "code": "http://hl7.org/fhirpath/System.String" + } + ] + } + ] + } +} diff --git a/crates/fhir-validator/tests/pack_smoke.rs b/crates/fhir-validator/tests/pack_smoke.rs new file mode 100644 index 000000000..46da2314f --- /dev/null +++ b/crates/fhir-validator/tests/pack_smoke.rs @@ -0,0 +1,113 @@ +//! Whole-spec smoke tests over the embedded R4 pack. `#[ignore]`d in normal +//! runs (they parse the full pack); run explicitly with +//! `cargo test -p helios-fhir-validator -- --ignored`. + +#![cfg(feature = "R4")] + +use helios_fhir::FhirVersion; +use helios_fhir_validator::packs::core_registry; +use helios_fhir_validator::{SchemaResolver, ValidationOptions, Validator}; +use serde_json::json; + +#[test] +#[ignore = "whole-pack parse; run with -- --ignored"] +fn r4_pack_loads_and_resolves_core_schemas() { + let registry = core_registry(FhirVersion::R4); + for name in ["Patient", "Observation", "Bundle", "Resource", "DomainResource", "Element", + "Extension", "HumanName", "string", "boolean", "dateTime", "Questionnaire"] + { + assert!(registry.resolve(name).is_some(), "core schema '{name}' must resolve"); + } + // Canonical URLs resolve to the same schemas. + let by_name = registry.resolve("Patient").unwrap(); + let by_url = registry.resolve("http://hl7.org/fhir/StructureDefinition/Patient").unwrap(); + assert!(std::sync::Arc::ptr_eq(&by_name, &by_url)); + // Primitives carry their value regexes. + assert!(registry.resolve("string").unwrap().regex.is_some()); + // Questionnaire.item recursion converted to an elementReference. + let q = registry.resolve("Questionnaire").unwrap(); + let item = &q.elements.as_ref().unwrap()["item"]; + let nested = &item.elements.as_ref().unwrap()["item"]; + assert_eq!( + nested.element_reference.as_deref(), + Some(&["Questionnaire".to_string(), "elements".to_string(), "item".to_string()][..]) + ); +} + +#[test] +#[ignore = "whole-pack parse; run with -- --ignored"] +fn r4_pack_validates_known_good_and_bad_resources() { + let registry = core_registry(FhirVersion::R4); + let validator = Validator::new(registry); + let opts = ValidationOptions::default(); + + // A well-formed Patient with common shapes: choice type, arrays, + // primitive sidecar, contained resource, extension. + let good = json!({ + "resourceType": "Patient", + "id": "example", + "meta": { "versionId": "1" }, + "extension": [{ + "url": "http://example.org/unknown-extension", + "valueString": "free-form" + }], + "identifier": [{ "system": "http://example.org/mrn", "value": "12345" }], + "active": true, + "name": [{ "use": "official", "family": "Chalmers", "given": ["Peter", "James"] }], + "gender": "male", + "birthDate": "1974-12-25", + "_birthDate": { + "extension": [{ + "url": "http://hl7.org/fhir/StructureDefinition/patient-birthTime", + "valueDateTime": "1974-12-25T14:35:45-05:00" + }] + }, + "deceasedBoolean": false, + "contained": [{ + "resourceType": "Organization", + "id": "org1", + "name": "ACME Healthcare" + }], + "managingOrganization": { "reference": "#org1" } + }); + let outcome = validator.validate_sync(&good, &opts); + assert_eq!( + outcome.errors, + vec![], + "known-good Patient must validate clean, got: {}", + serde_json::to_string_pretty(&outcome.errors).unwrap() + ); + + // Structural breakage must surface. + let bad = json!({ + "resourceType": "Patient", + "bogusElement": true, + "gender": ["male"], + "name": { "family": "NotAnArray" }, + "deceasedBoolean": false, + "deceasedDateTime": "2020-01-01" + }); + let outcome = validator.validate_sync(&bad, &opts); + let kinds: Vec = outcome + .errors + .iter() + .map(|e| serde_json::to_value(e.kind).unwrap().as_str().unwrap().to_string()) + .collect(); + assert!(kinds.contains(&"unknown-element".to_string()), "kinds: {kinds:?}"); + assert!(kinds.contains(&"not-singular".to_string()), "kinds: {kinds:?}"); + assert!(kinds.contains(&"not-array".to_string()), "kinds: {kinds:?}"); + assert!(kinds.contains(&"choice".to_string()), "kinds: {kinds:?}"); + + // A Bundle whose entry resource is dynamically resolved. + let bundle = json!({ + "resourceType": "Bundle", + "type": "collection", + "entry": [{ "resource": { "resourceType": "Patient", "wrong": 1 } }] + }); + let outcome = validator.validate_sync(&bundle, &opts); + assert!( + outcome.errors.iter().any(|e| e.path == "Bundle.entry.0.resource.wrong"), + "dynamic resolution must reach the nested Patient, got: {}", + serde_json::to_string_pretty(&outcome.errors).unwrap() + ); +} From 2c7477c457a83a23568c8289e19daa2daf7314b3 Mon Sep 17 00:00:00 2001 From: Doug Date: Sun, 5 Jul 2026 21:09:51 -0400 Subject: [PATCH 4/7] =?UTF-8?q?feat(validator):=20deferred=20effects=20?= =?UTF-8?q?=E2=80=94=20primitive=20values,=20FHIRPath=20constraints,=20ter?= =?UTF-8?q?minology=20bindings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 4: - engine/primitives.rs: JSON type-class checks from the primitive's declared type + anchored spec-regex checks (process-wide compiled-regex cache); kind 'primitive-value'. Upstream fixtures declare bare primitives, so the exact-match conformance contract is untouched (pinned by extended/primitives.json). - effects.rs: execute() pipeline — constraints deduped by (path,id) across cooperative layers, severity mapping (error/warning; guideline skipped), suppress list, not-evaluable surfaced as warnings; bindings filtered to required strength, coded shape from declared type with value-shape fallback, fail-open/fail-closed terminology outages. Async TerminologyProvider (async-trait), sync ConstraintEvaluator with batch evaluate_all. - Validator::validate(): sync walk + effects execution; validate_sync remains pure. - fhirpath_effects.rs (feature 'fhirpath'): typed-parse-once per resource, focus via 'path.all(expr)' wrapping, FHIR invariant semantics (empty → pass; the JS reference fails empty — known bug there), Arc-cached expression ASTs. - 6 stub-driven effects tests + real-invariant test (pat-1 from the R4 pack; binary carries an MSVC 14.44 LNK1318 caveat, scenarios also covered via the rest integration suite). Co-Authored-By: Claude Opus 4.8 (1M context) --- Cargo.lock | 5 + crates/fhir-validator/Cargo.toml | 16 +- crates/fhir-validator/src/effects.rs | 275 +++++++++++++++- crates/fhir-validator/src/engine/errors.rs | 38 +++ crates/fhir-validator/src/engine/mod.rs | 31 +- .../fhir-validator/src/engine/primitives.rs | 120 +++++++ crates/fhir-validator/src/engine/walk.rs | 4 + crates/fhir-validator/src/fhirpath_effects.rs | 198 ++++++++++++ crates/fhir-validator/src/lib.rs | 8 +- crates/fhir-validator/tests/effects_tests.rs | 296 ++++++++++++++++++ crates/fhir-validator/tests/extended.rs | 5 + .../tests/fhirpath_constraint_tests.rs | 106 +++++++ .../tests/fixtures/extended/primitives.json | 89 ++++++ 13 files changed, 1182 insertions(+), 9 deletions(-) create mode 100644 crates/fhir-validator/src/engine/primitives.rs create mode 100644 crates/fhir-validator/src/fhirpath_effects.rs create mode 100644 crates/fhir-validator/tests/effects_tests.rs create mode 100644 crates/fhir-validator/tests/fhirpath_constraint_tests.rs create mode 100644 crates/fhir-validator/tests/fixtures/extended/primitives.json diff --git a/Cargo.lock b/Cargo.lock index 41022dd17..a89ce1e0c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3149,12 +3149,16 @@ dependencies = [ name = "helios-fhir-validator" version = "0.2.1" dependencies = [ + "async-trait", "clap", "flate2", "helios-fhir", + "helios-fhirpath", "indexmap 2.14.0", + "regex", "serde", "serde_json", + "tokio", ] [[package]] @@ -3340,6 +3344,7 @@ dependencies = [ "helios-audit", "helios-auth", "helios-fhir", + "helios-fhir-validator", "helios-persistence", "helios-serde", "helios-sof", diff --git a/crates/fhir-validator/Cargo.toml b/crates/fhir-validator/Cargo.toml index bedb0c6f8..8ea9cdb0d 100644 --- a/crates/fhir-validator/Cargo.toml +++ b/crates/fhir-validator/Cargo.toml @@ -12,10 +12,12 @@ keywords = ["helios-software", "hl7", "fhir", "validation", "fhir-schema"] [features] default = ["R4"] -R4 = ["helios-fhir/R4"] -R4B = ["helios-fhir/R4B"] -R5 = ["helios-fhir/R5"] -R6 = ["helios-fhir/R6"] +R4 = ["helios-fhir/R4", "helios-fhirpath?/R4"] +R4B = ["helios-fhir/R4B", "helios-fhirpath?/R4B"] +R5 = ["helios-fhir/R5", "helios-fhirpath?/R5"] +R6 = ["helios-fhir/R6", "helios-fhirpath?/R6"] +# FHIRPath-backed ConstraintEvaluator (pulls in helios-fhirpath). +fhirpath = ["dep:helios-fhirpath"] # Enables the generate-schema-packs binary (dev-time; reads the FHIR spec # bundles from crates/fhir-gen/resources and writes packs/). gen-packs = [] @@ -33,11 +35,17 @@ serde_json = { workspace = true, features = ["preserve_order"] } indexmap = { version = "2", features = ["serde"] } # Schema pack (de)compression. flate2 = "1" +# Primitive value regexes (FHIR spec patterns carried in the packs). +regex = "1" +# Dyn-compatible async TerminologyProvider. +async-trait = "0.1" clap = { version = "4", features = ["derive"], optional = true } +helios-fhirpath = { path = "../fhirpath", version = "0.2.1", optional = true, default-features = false } [dev-dependencies] serde = { workspace = true } serde_json = { workspace = true } +tokio = { version = "1", features = ["rt", "macros"] } [[bin]] name = "generate-schema-packs" diff --git a/crates/fhir-validator/src/effects.rs b/crates/fhir-validator/src/effects.rs index ae94c35a2..bc0721615 100644 --- a/crates/fhir-validator/src/effects.rs +++ b/crates/fhir-validator/src/effects.rs @@ -3,11 +3,39 @@ //! //! The structural walk ([`crate::engine`]) never performs I/O and never //! evaluates FHIRPath. When it encounters a `constraints` or `binding` -//! keyword it records a [`Deferred`] entry; the async half of validation -//! (Phase 4) executes them via pluggable handlers. +//! keyword it records a [`Deferred`] entry; [`execute`] runs them through +//! pluggable handlers — a synchronous [`ConstraintEvaluator`] (the +//! FHIRPath-backed implementation lives behind the `fhirpath` feature) and +//! an async [`TerminologyProvider`] (HTTP `$validate-code`, in-process hts, +//! or a test stub). +//! +//! Execution semantics: +//! - constraints: deduplicated by `(path, id)` across cooperative layers; +//! `severity: error` failures emit error issues, `warning` failures emit +//! warning issues, `guideline` entries are skipped, ids on the suppress +//! list are skipped, and evaluation problems surface as warning issues +//! (never silent). An **empty** FHIRPath result counts as a pass, per +//! FHIR invariant semantics (the reference validator treats empty as a +//! failure — a known bug there). +//! - bindings: only `strength: required` is enforced. The coded shape is +//! the element's declared type when known, else inferred from the value +//! (`code` string / Coding / CodeableConcept, mirroring the reference). +//! Provider errors surface as warning issues by default (fail-open) or +//! error issues when `terminology_fail_closed` is set. +//! +//! Issue order: all constraint issues (deferred order), then all binding +//! issues (deferred order) — always after the structural errors. +use crate::engine::{ErrorKind, Severity, ValidationError}; use crate::schema::Binding; +use async_trait::async_trait; +use helios_fhir::FhirVersion; use serde_json::Value; +use std::collections::HashSet; + +// Message templates live with the rest of the contract in engine::errors; +// re-exported crate-internally via these thin wrappers. +use crate::engine::messages; /// A validation obligation collected during the sync walk. #[derive(Debug, Clone, PartialEq)] @@ -25,7 +53,8 @@ pub enum Deferred { /// `error` | `warning` | `guideline`. severity: Option, }, - /// A required-strength terminology binding to check. + /// A terminology binding to check (filtered to `required` strength at + /// execution time). Binding { /// Dotted conformance path of the coded node. path: String, @@ -33,5 +62,245 @@ pub enum Deferred { binding: Binding, /// The coded value as found in the data. value: Value, + /// The element's declared type, when the schema stated one. + type_hint: Option, }, } + +/// A coded value in one of FHIR's three coded shapes. +#[derive(Debug, Clone, PartialEq)] +pub enum CodedValue { + /// A bare `code` string (system to be inferred from the ValueSet). + Code(String), + /// A Coding object. + Coding(Value), + /// A CodeableConcept object. + CodeableConcept(Value), +} + +/// Checks coded-value membership in a ValueSet (`$validate-code`). +#[async_trait] +pub trait TerminologyProvider: Send + Sync { + /// `Ok(true)` = the coded value is in the ValueSet. + async fn validate_code( + &self, + value_set: &str, + coded: &CodedValue, + ) -> Result; +} + +/// A terminology-service failure (network, unknown ValueSet, ...). +#[derive(Debug, Clone)] +pub struct TerminologyError(pub String); + +impl std::fmt::Display for TerminologyError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.0) + } +} +impl std::error::Error for TerminologyError {} + +/// The outcome of evaluating one constraint. +#[derive(Debug, Clone, PartialEq)] +pub enum ConstraintOutcome { + Passed, + Failed, + /// The expression could not be evaluated (parse error, non-boolean + /// result, typed-model mismatch); surfaced as a warning issue. + NotEvaluable(String), +} + +/// Evaluates FHIRPath invariants against a resource. Implementations parse +/// the resource once per call and evaluate every constraint against it. +pub trait ConstraintEvaluator: Send + Sync { + /// Outcomes aligned index-for-index with `constraints`. + fn evaluate_all( + &self, + resource: &Value, + version: FhirVersion, + constraints: &[DeferredConstraint<'_>], + ) -> Vec; +} + +/// A borrowed view of one deferred constraint, handed to evaluators. +#[derive(Debug, Clone, Copy)] +pub struct DeferredConstraint<'a> { + /// Dotted conformance path of the node the constraint attaches to + /// (rooted at the resource type, numeric indices). + pub path: &'a str, + pub id: &'a str, + pub expression: &'a str, +} + +/// The handlers for one validation run. +#[derive(Default)] +pub struct EffectHandlers<'a> { + pub terminology: Option<&'a dyn TerminologyProvider>, + pub constraints: Option<&'a dyn ConstraintEvaluator>, + /// Constraint ids to skip entirely (e.g. `dom-6`). + pub suppress_constraints: &'a [String], + /// Treat terminology-service failures as errors instead of warnings. + pub terminology_fail_closed: bool, +} + +/// Execute the deferred obligations, appending issues to `errors`. +pub async fn execute( + resource: &Value, + version: FhirVersion, + deferred: &[Deferred], + handlers: &EffectHandlers<'_>, + errors: &mut Vec, +) { + execute_constraints(resource, version, deferred, handlers, errors); + execute_bindings(deferred, handlers, errors).await; +} + +fn execute_constraints( + resource: &Value, + version: FhirVersion, + deferred: &[Deferred], + handlers: &EffectHandlers<'_>, + errors: &mut Vec, +) { + let Some(evaluator) = handlers.constraints else { + return; + }; + + // Select, filter, and dedup — preserving deferred order. + let mut seen: HashSet<(String, String)> = HashSet::new(); + let mut selected: Vec<(&str, &str, &str, Option<&str>, Severity)> = Vec::new(); + for d in deferred { + let Deferred::Constraint { path, id, expression, human, severity } = d else { + continue; + }; + let issue_severity = match severity.as_deref() { + None | Some("error") => Severity::Error, + Some("warning") => Severity::Warning, + _ => continue, // guideline and unknown severities: not evaluated + }; + if handlers.suppress_constraints.iter().any(|s| s == id) { + continue; + } + if !seen.insert((path.clone(), id.clone())) { + continue; // same constraint from several cooperative layers + } + selected.push((path, id, expression, human.as_deref(), issue_severity)); + } + if selected.is_empty() { + return; + } + + let refs: Vec> = selected + .iter() + .map(|(path, id, expression, _, _)| DeferredConstraint { path, id, expression }) + .collect(); + let outcomes = evaluator.evaluate_all(resource, version, &refs); + debug_assert_eq!(outcomes.len(), refs.len(), "evaluator must align outcomes"); + + for ((path, id, _, human, severity), outcome) in selected.iter().zip(outcomes) { + match outcome { + ConstraintOutcome::Passed => {} + ConstraintOutcome::Failed => { + errors.push( + ValidationError::new( + ErrorKind::FhirpathConstraint, + path.to_string(), + messages::fhirpath_constraint(id, *human), + ) + .with_severity(*severity) + .with_extra("constraint", Value::String(id.to_string())), + ); + } + ConstraintOutcome::NotEvaluable(detail) => { + errors.push( + ValidationError::new( + ErrorKind::FhirpathConstraint, + path.to_string(), + messages::constraint_not_evaluable(id, &detail), + ) + .with_severity(Severity::Warning) + .with_extra("constraint", Value::String(id.to_string())), + ); + } + } + } +} + +async fn execute_bindings( + deferred: &[Deferred], + handlers: &EffectHandlers<'_>, + errors: &mut Vec, +) { + let Some(provider) = handlers.terminology else { + return; + }; + + for d in deferred { + let Deferred::Binding { path, binding, value, type_hint } = d else { + continue; + }; + if binding.strength.as_deref() != Some("required") { + continue; + } + let Some(coded) = coded_value(value, type_hint.as_deref()) else { + continue; // shape not coded (e.g. null gap) — structural checks own it + }; + match provider.validate_code(&binding.value_set, &coded).await { + Ok(true) => {} + Ok(false) => { + errors.push( + ValidationError::new( + ErrorKind::TerminologyBinding, + path.clone(), + messages::terminology_binding(value, &binding.value_set), + ) + .with_extra( + "binding", + serde_json::to_value(binding).expect("binding serializes"), + ), + ); + } + Err(e) => { + let severity = if handlers.terminology_fail_closed { + Severity::Error + } else { + Severity::Warning + }; + errors.push( + ValidationError::new( + ErrorKind::TerminologyBinding, + path.clone(), + messages::terminology_unavailable(&binding.value_set, &e.0), + ) + .with_severity(severity), + ); + } + } + } +} + +/// Determine the coded shape: the element's declared type wins, inference +/// from the value is the fallback (mirroring the reference validator). +fn coded_value(value: &Value, type_hint: Option<&str>) -> Option { + match type_hint { + Some("code") | Some("string") | Some("uri") | Some("canonical") => { + value.as_str().map(|s| CodedValue::Code(s.to_string())) + } + Some("Coding") => value.is_object().then(|| CodedValue::Coding(value.clone())), + Some("CodeableConcept") => { + value.is_object().then(|| CodedValue::CodeableConcept(value.clone())) + } + Some("Quantity") => { + // Quantity binds via its system+code pair — treated as a Coding. + value.is_object().then(|| CodedValue::Coding(value.clone())) + } + _ => match value { + Value::String(s) => Some(CodedValue::Code(s.clone())), + Value::Object(o) if o.contains_key("coding") || o.contains_key("text") => { + Some(CodedValue::CodeableConcept(value.clone())) + } + Value::Object(_) => Some(CodedValue::Coding(value.clone())), + _ => None, + }, + } +} diff --git a/crates/fhir-validator/src/engine/errors.rs b/crates/fhir-validator/src/engine/errors.rs index 69012b08c..66f08f885 100644 --- a/crates/fhir-validator/src/engine/errors.rs +++ b/crates/fhir-validator/src/engine/errors.rs @@ -228,6 +228,44 @@ pub(crate) fn msg_slice_order(slice: &str) -> String { format!("slice '{slice}' is out of order (ordered slicing)") } +/// [helios] a primitive value has the wrong JSON type class. +pub(crate) fn msg_primitive_type(type_name: &str, json_type: &str) -> String { + format!("expected {type_name}, got {json_type}") +} + +/// [helios] a primitive string value failed the type's regex. +pub(crate) fn msg_primitive_regex(type_name: &str, value: &str) -> String { + format!("value '{value}' is not a valid {type_name}") +} + +/// [reference] `FHIRPath constraint {id} error: {human}` — the wording of +/// the reference validator, including its fallback when `human` is absent. +pub(crate) fn msg_fhirpath_constraint(id: &str, human: Option<&str>) -> String { + format!( + "FHIRPath constraint {id} error: {}", + human.unwrap_or("property \"human\" is not provided") + ) +} + +/// [reference] terminology-binding failure wording. +pub(crate) fn msg_terminology_binding(value: &Value, value_set: &str) -> String { + format!( + "Provided coded value '{}' does not pass validation against the following valueset: '{value_set}'", + render_value(value) + ) +} + +/// [helios] the terminology service could not be reached / errored. +pub(crate) fn msg_terminology_unavailable(value_set: &str, detail: &str) -> String { + format!("terminology validation against '{value_set}' could not be performed: {detail}") +} + +/// [helios] constraint evaluation itself failed (parse error, non-boolean +/// result, typed-model mismatch). +pub(crate) fn msg_constraint_not_evaluable(id: &str, detail: &str) -> String { + format!("FHIRPath constraint {id} could not be evaluated: {detail}") +} + /// [helios] a schema reference could not be resolved (reference validator /// throws `could not resolve ["…"]`; we report and continue). pub(crate) fn msg_unknown_schema(reference: &str) -> String { diff --git a/crates/fhir-validator/src/engine/mod.rs b/crates/fhir-validator/src/engine/mod.rs index 00a166f47..86852b3a6 100644 --- a/crates/fhir-validator/src/engine/mod.rs +++ b/crates/fhir-validator/src/engine/mod.rs @@ -9,14 +9,27 @@ mod errors; mod path; +mod primitives; mod slicing; mod walk; pub use errors::{ErrorKind, Severity, ValidationError}; pub use path::dotted_to_fhirpath; -use crate::effects::Deferred; +/// Crate-internal access to the effect-issue message templates (the full +/// template catalog lives in `errors.rs`, the single source of truth). +pub(crate) mod messages { + pub(crate) use super::errors::{ + msg_constraint_not_evaluable as constraint_not_evaluable, + msg_fhirpath_constraint as fhirpath_constraint, + msg_terminology_binding as terminology_binding, + msg_terminology_unavailable as terminology_unavailable, + }; +} + +use crate::effects::{Deferred, EffectHandlers}; use crate::resolver::SchemaResolver; +use helios_fhir::FhirVersion; use serde_json::Value; use std::sync::Arc; @@ -80,4 +93,20 @@ impl Validator { pub fn validate_sync(&self, resource: &Value, opts: &ValidationOptions) -> SyncOutcome { walk::validate(self.resolver.as_ref(), resource, opts) } + + /// Full validation: the structural walk plus execution of the deferred + /// FHIRPath constraints and terminology bindings through `effects`. + /// Structural issues come first, then constraint issues, then binding + /// issues (each in walk order). + pub async fn validate( + &self, + resource: &Value, + version: FhirVersion, + opts: &ValidationOptions, + effects: &EffectHandlers<'_>, + ) -> Vec { + let SyncOutcome { mut errors, deferred } = self.validate_sync(resource, opts); + crate::effects::execute(resource, version, &deferred, effects, &mut errors).await; + errors + } } diff --git a/crates/fhir-validator/src/engine/primitives.rs b/crates/fhir-validator/src/engine/primitives.rs new file mode 100644 index 000000000..887f7f2fa --- /dev/null +++ b/crates/fhir-validator/src/engine/primitives.rs @@ -0,0 +1,120 @@ +//! Primitive value validation: JSON type-class and regex checks. +//! +//! Runs when a primitive-type schema is a member of a node's cooperative +//! set. The type class comes from the primitive's declared `type` (falling +//! back to `name`); the regex is the FHIR spec pattern carried into the +//! packs by the converter. Upstream conformance fixtures declare primitives +//! as bare `{kind: "primitive-type"}` — no type, no regex — so neither +//! check can fire there and the exact-match contract is unaffected. +//! +//! FHIR spec regexes are implicitly anchored: they must match the whole +//! value, so patterns are compiled as `^(?:...)$`. + +use super::errors::{self, ErrorKind}; +use super::walk::WalkCtx; +use crate::schema::FhirSchema; +use regex::Regex; +use serde_json::Value; +use std::collections::HashMap; +use std::sync::{Arc, OnceLock, RwLock}; + +/// Validate a data value against a primitive-type schema. `Null` is skipped: +/// legal as a sidecar-array gap, and shape errors are reported elsewhere. +pub(super) fn validate_primitive(ctx: &mut WalkCtx<'_>, schema: &FhirSchema, data: &Value) { + if data.is_null() || data.is_array() { + return; + } + let Some(type_name) = schema.type_.as_deref().or(schema.name.as_deref()) else { + return; // bare fixture primitive — nothing to check against + }; + + if let Some(expected) = expected_json_class(type_name) + && !expected.matches(data) + { + ctx.error( + ErrorKind::PrimitiveValue, + errors::msg_primitive_type(type_name, errors::json_type_name(data)), + ); + return; + } + + if let Some(pattern) = &schema.regex + && let Some(s) = data.as_str() + && let Some(re) = compiled(pattern) + && !re.is_match(s) + { + ctx.error(ErrorKind::PrimitiveValue, errors::msg_primitive_regex(type_name, s)); + } +} + +/// The JSON type class a FHIR primitive requires. +#[derive(Clone, Copy)] +enum JsonClass { + Boolean, + Integer, + Number, + String, +} + +impl JsonClass { + fn matches(self, v: &Value) -> bool { + match self { + JsonClass::Boolean => v.is_boolean(), + JsonClass::Integer => v.as_number().is_some_and(|n| n.is_i64() || n.is_u64()), + JsonClass::Number => v.is_number(), + JsonClass::String => v.is_string(), + } + } +} + +fn expected_json_class(type_name: &str) -> Option { + Some(match type_name { + "boolean" => JsonClass::Boolean, + "integer" | "positiveInt" | "unsignedInt" | "integer64" => JsonClass::Integer, + "decimal" => JsonClass::Number, + // Everything else — string, uri, code, dateTime, base64Binary, ... — + // is a JSON string. Unknown primitive names get no class check. + "string" | "uri" | "url" | "canonical" | "oid" | "uuid" | "id" | "code" | "markdown" + | "base64Binary" | "instant" | "date" | "dateTime" | "time" | "xhtml" => { + JsonClass::String + } + _ => return None, + }) +} + +/// Process-wide compiled-regex cache (patterns repeat across every node of +/// every resource). Invalid patterns are cached as misses so they are only +/// reported... never: they are simply skipped — the converter emits spec +/// patterns, and a bad pattern must not fail validation. +fn compiled(pattern: &str) -> Option> { + static CACHE: OnceLock>>>> = OnceLock::new(); + let cache = CACHE.get_or_init(|| RwLock::new(HashMap::new())); + + if let Some(hit) = cache.read().expect("regex cache lock").get(pattern) { + return hit.clone(); + } + let compiled = Regex::new(&format!("^(?:{pattern})$")).ok().map(Arc::new); + cache + .write() + .expect("regex cache lock") + .insert(pattern.to_string(), compiled.clone()); + compiled +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn anchoring_is_whole_value() { + let re = compiled("[0-9]+").unwrap(); + assert!(re.is_match("123")); + assert!(!re.is_match("a123")); + assert!(!re.is_match("123b")); + } + + #[test] + fn invalid_pattern_is_skipped() { + assert!(compiled("([unclosed").is_none()); + } +} diff --git a/crates/fhir-validator/src/engine/walk.rs b/crates/fhir-validator/src/engine/walk.rs index 4db52405b..c2a53cd23 100644 --- a/crates/fhir-validator/src/engine/walk.rs +++ b/crates/fhir-validator/src/engine/walk.rs @@ -314,6 +314,9 @@ fn eval_validators(ctx: &mut WalkCtx<'_>, set: &SchemaSet, data: &Value) { // set is borrowed, and Arc clones are cheap. let schemas: Vec> = set.schemas().cloned().collect(); for schema in &schemas { + if schema.is_primitive() { + super::primitives::validate_primitive(ctx, schema, data); + } if let Some(fixed) = &schema.fixed && data != fixed { ctx.error(ErrorKind::FixedValue, errors::msg_fixed_value(fixed, data)); @@ -345,6 +348,7 @@ fn eval_validators(ctx: &mut WalkCtx<'_>, set: &SchemaSet, data: &Value) { path: ctx.path.render_dotted(), binding: binding.clone(), value: data.clone(), + type_hint: schema.type_.clone(), }); } if schema.elements.is_some() && !data.is_object() { diff --git a/crates/fhir-validator/src/fhirpath_effects.rs b/crates/fhir-validator/src/fhirpath_effects.rs new file mode 100644 index 000000000..87b974325 --- /dev/null +++ b/crates/fhir-validator/src/fhirpath_effects.rs @@ -0,0 +1,198 @@ +//! FHIRPath-backed [`ConstraintEvaluator`] (feature `fhirpath`). +//! +//! Per resource: the raw JSON is parsed **once** into the typed +//! `helios_fhir` model (the `crates/sof` pattern) — helios-fhirpath +//! evaluates against typed resources, not raw JSON. Each constraint is then +//! evaluated as one combined expression: for a constraint attached below +//! the root, the validator path is rendered as a relative FHIRPath and the +//! invariant is wrapped in `.all(...)`: +//! +//! ```text +//! path Patient.contact.0, expr "name.exists() or telecom.exists()" +//! → contact[0].all(name.exists() or telecom.exists()) +//! ``` +//! +//! `all()` gives the correct per-node focus *and* the FHIR invariant +//! semantics for absent nodes (empty collection → pass). Root-attached +//! constraints evaluate as-is. +//! +//! Result coercion (FHIRPath 5.1.1 / FHIR invariant semantics): empty → +//! pass; a single boolean → itself; anything else → not evaluable +//! (surfaced as a warning issue by the effects layer). Note the JS +//! reference validator fails on empty — a known bug there, upstream +//! fixtures don't pin it. +//! +//! Parsed expression ASTs are memoized per evaluator instance — core +//! invariants repeat across every resource of a type. +//! +//! Known limitation (documented in the plan): helios-fhirpath resolves +//! `%resource`/`%rootResource` to the root resource, so constraints that +//! depend on nested-resource `%resource` semantics may misfire; and the +//! `.all()` wrapping makes `%context` the root rather than the node. + +use crate::effects::{ConstraintEvaluator, ConstraintOutcome, DeferredConstraint}; +use helios_fhir::{FhirResource, FhirVersion}; +use helios_fhirpath::{EvaluationContext, EvaluationResult}; +use serde_json::Value; +use std::collections::HashMap; +use std::sync::Mutex; + +/// Evaluates invariants with helios-fhirpath. Cheap to construct; hold one +/// per service for the lifetime of the process to benefit from the AST +/// cache. +#[derive(Default)] +pub struct FhirPathConstraintEvaluator { + /// Combined-expression parse cache. + asts: Mutex>>, +} + +impl FhirPathConstraintEvaluator { + pub fn new() -> Self { + Self::default() + } +} + +impl ConstraintEvaluator for FhirPathConstraintEvaluator { + fn evaluate_all( + &self, + resource: &Value, + version: FhirVersion, + constraints: &[DeferredConstraint<'_>], + ) -> Vec { + let typed = match parse_typed(resource, version) { + Ok(t) => t, + Err(e) => { + // Structural errors are already on the report when the + // typed parse fails; constraints simply cannot run. + let detail = format!("resource does not parse into the typed model: {e}"); + return constraints + .iter() + .map(|_| ConstraintOutcome::NotEvaluable(detail.clone())) + .collect(); + } + }; + let context = EvaluationContext::new(vec![typed]); + + constraints + .iter() + .map(|c| { + let combined = combine(c.path, c.expression); + self.evaluate_one(&combined, &context) + }) + .collect() + } +} + +impl FhirPathConstraintEvaluator { + fn evaluate_one(&self, expression: &str, context: &EvaluationContext) -> ConstraintOutcome { + // Parse (or reuse) the AST, then evaluate. + let parsed = { + let mut cache = self.asts.lock().expect("ast cache lock"); + match cache.get(expression) { + Some(ast) => std::sync::Arc::clone(ast), + None => match helios_fhirpath::parse_expression(expression) { + Ok(ast) => { + let ast = std::sync::Arc::new(ast); + cache.insert(expression.to_string(), std::sync::Arc::clone(&ast)); + ast + } + Err(e) => return ConstraintOutcome::NotEvaluable(format!("parse error: {e}")), + }, + } + }; + + match helios_fhirpath::evaluator::evaluate(&parsed, context, None) { + Ok(result) => coerce(result), + Err(e) => ConstraintOutcome::NotEvaluable(format!("evaluation error: {e}")), + } + } +} + +/// Build the combined focus+invariant expression from a dotted validator +/// path (`Patient.contact.0` → `contact[0].all()`). +fn combine(path: &str, expression: &str) -> String { + let mut relative = String::new(); + for segment in path.split('.').skip(1) { + if segment.chars().all(|c| c.is_ascii_digit()) && !segment.is_empty() { + relative.push('['); + relative.push_str(segment); + relative.push(']'); + } else { + if !relative.is_empty() { + relative.push('.'); + } + relative.push_str(segment); + } + } + if relative.is_empty() { + expression.to_string() + } else { + format!("{relative}.all({expression})") + } +} + +/// FHIRPath 5.1.1 boolean coercion with FHIR invariant semantics. +fn coerce(result: EvaluationResult) -> ConstraintOutcome { + match result { + EvaluationResult::Empty => ConstraintOutcome::Passed, + EvaluationResult::Boolean(b, ..) => { + if b { + ConstraintOutcome::Passed + } else { + ConstraintOutcome::Failed + } + } + EvaluationResult::Collection { items, .. } => match items.as_slice() { + [] => ConstraintOutcome::Passed, + [EvaluationResult::Boolean(b, ..)] => { + if *b { + ConstraintOutcome::Passed + } else { + ConstraintOutcome::Failed + } + } + [_] => ConstraintOutcome::NotEvaluable("single non-boolean result".to_string()), + _ => ConstraintOutcome::NotEvaluable("multiple results".to_string()), + }, + _ => ConstraintOutcome::NotEvaluable("single non-boolean result".to_string()), + } +} + +/// Parse raw JSON into the version's typed model (`crates/sof` pattern). +fn parse_typed(resource: &Value, version: FhirVersion) -> Result { + match version { + #[cfg(feature = "R4")] + FhirVersion::R4 => serde_json::from_value::(resource.clone()) + .map(|r| FhirResource::R4(Box::new(r))) + .map_err(|e| e.to_string()), + #[cfg(feature = "R4B")] + FhirVersion::R4B => serde_json::from_value::(resource.clone()) + .map(|r| FhirResource::R4B(Box::new(r))) + .map_err(|e| e.to_string()), + #[cfg(feature = "R5")] + FhirVersion::R5 => serde_json::from_value::(resource.clone()) + .map(|r| FhirResource::R5(Box::new(r))) + .map_err(|e| e.to_string()), + #[cfg(feature = "R6")] + FhirVersion::R6 => serde_json::from_value::(resource.clone()) + .map(|r| FhirResource::R6(Box::new(r))) + .map_err(|e| e.to_string()), + #[allow(unreachable_patterns)] + other => Err(format!("FHIR version {other:?} not enabled in helios-fhir-validator")), + } +} + +#[cfg(test)] +mod tests { + use super::combine; + + #[test] + fn combines_paths_into_focused_expressions() { + assert_eq!(combine("Patient", "name.exists()"), "name.exists()"); + assert_eq!( + combine("Patient.contact.0", "name.exists() or telecom.exists()"), + "contact[0].all(name.exists() or telecom.exists())" + ); + assert_eq!(combine("Patient.name.2.given", "length() < 10"), "name[2].given.all(length() < 10)"); + } +} diff --git a/crates/fhir-validator/src/lib.rs b/crates/fhir-validator/src/lib.rs index e7f65b4c2..ca98054ae 100644 --- a/crates/fhir-validator/src/lib.rs +++ b/crates/fhir-validator/src/lib.rs @@ -48,7 +48,13 @@ pub mod packs; pub mod resolver; pub mod schema; -pub use effects::Deferred; +#[cfg(feature = "fhirpath")] +pub mod fhirpath_effects; + +pub use effects::{ + CodedValue, ConstraintEvaluator, ConstraintOutcome, Deferred, DeferredConstraint, + EffectHandlers, TerminologyError, TerminologyProvider, +}; pub use engine::{ dotted_to_fhirpath, ErrorKind, Severity, SyncOutcome, UnknownProfilePolicy, ValidationError, ValidationOptions, Validator, diff --git a/crates/fhir-validator/tests/effects_tests.rs b/crates/fhir-validator/tests/effects_tests.rs new file mode 100644 index 000000000..986034563 --- /dev/null +++ b/crates/fhir-validator/tests/effects_tests.rs @@ -0,0 +1,296 @@ +//! Deferred-effects execution: stub-driven tests pinning constraint and +//! binding issue shapes, severity handling, suppression, dedup, and +//! fail-open/fail-closed terminology behavior. + +use async_trait::async_trait; +use helios_fhir::FhirVersion; +use helios_fhir_validator::{ + CodedValue, ConstraintEvaluator, ConstraintOutcome, DeferredConstraint, EffectHandlers, + FhirSchema, SchemaRegistry, Severity, TerminologyError, TerminologyProvider, + ValidationOptions, Validator, +}; +use serde_json::{json, Value}; +use std::sync::{Arc, Mutex}; + +fn registry() -> SchemaRegistry { + let mut reg = SchemaRegistry::new(); + reg.insert_named( + "code", + serde_json::from_value::(json!({ "kind": "primitive-type", "type": "code" })) + .unwrap(), + ); + reg.insert_named( + "string", + serde_json::from_value::(json!({ "kind": "primitive-type", "type": "string" })) + .unwrap(), + ); + reg.insert_named( + "Patient", + serde_json::from_value::(json!({ + "constraints": { + "pat-x": { + "expression": "name.exists()", + "severity": "error", + "human": "must have a name" + }, + "pat-w": { + "expression": "telecom.exists()", + "severity": "warning", + "human": "should have telecom" + }, + "pat-g": { + "expression": "language.exists()", + "severity": "guideline" + } + }, + "elements": { + "resourceType": { "type": "string" }, + "name": { "type": "string" }, + "telecom": { "type": "string" }, + "gender": { + "type": "code", + "binding": { + "strength": "required", + "valueSet": "http://hl7.org/fhir/ValueSet/administrative-gender" + } + }, + "maritalStatus": { + "type": "CodeableConcept", + "binding": { + "strength": "extensible", + "valueSet": "http://hl7.org/fhir/ValueSet/marital-status" + } + } + } + })) + .unwrap(), + ); + reg.insert_named( + "CodeableConcept", + serde_json::from_value::(json!({ + "elements": { "text": { "type": "string" } } + })) + .unwrap(), + ); + reg +} + +/// Forces every selected constraint to the same outcome, recording calls. +struct ScriptedConstraints { + outcome: fn(&DeferredConstraint<'_>) -> ConstraintOutcome, + seen: Mutex>, +} + +impl ConstraintEvaluator for ScriptedConstraints { + fn evaluate_all( + &self, + _resource: &Value, + _version: FhirVersion, + constraints: &[DeferredConstraint<'_>], + ) -> Vec { + let mut seen = self.seen.lock().unwrap(); + constraints + .iter() + .map(|c| { + seen.push(c.id.to_string()); + (self.outcome)(c) + }) + .collect() + } +} + +/// Allow-list terminology stub. +struct AllowList(&'static [&'static str]); + +#[async_trait] +impl TerminologyProvider for AllowList { + async fn validate_code( + &self, + _value_set: &str, + coded: &CodedValue, + ) -> Result { + let code = match coded { + CodedValue::Code(c) => c.clone(), + CodedValue::Coding(v) => v["code"].as_str().unwrap_or_default().to_string(), + CodedValue::CodeableConcept(v) => { + v["coding"][0]["code"].as_str().unwrap_or_default().to_string() + } + }; + Ok(self.0.contains(&code.as_str())) + } +} + +/// Always-erroring terminology stub. +struct Down; + +#[async_trait] +impl TerminologyProvider for Down { + async fn validate_code( + &self, + _value_set: &str, + _coded: &CodedValue, + ) -> Result { + Err(TerminologyError("connection refused".to_string())) + } +} + +fn validator() -> Validator { + Validator::new(Arc::new(registry())) +} + +#[tokio::test] +async fn constraint_failure_shapes_and_severities() { + let evaluator = ScriptedConstraints { + outcome: |_| ConstraintOutcome::Failed, + seen: Mutex::new(Vec::new()), + }; + let handlers = + EffectHandlers { constraints: Some(&evaluator), ..Default::default() }; + let resource = json!({ "resourceType": "Patient" }); + let errors = validator() + .validate(&resource, FhirVersion::R4, &ValidationOptions::default(), &handlers) + .await; + + // guideline (pat-g) is never evaluated. + assert_eq!(evaluator.seen.lock().unwrap().as_slice(), &["pat-x", "pat-w"]); + + let as_json = serde_json::to_value(&errors).unwrap(); + assert_eq!( + as_json, + json!([ + { + "type": "fhirpath-constraint", + "path": "Patient", + "message": "FHIRPath constraint pat-x error: must have a name", + "constraint": "pat-x" + }, + { + "type": "fhirpath-constraint", + "path": "Patient", + "message": "FHIRPath constraint pat-w error: should have telecom", + "constraint": "pat-w" + } + ]) + ); + assert_eq!(errors[0].severity, Severity::Error); + assert_eq!(errors[1].severity, Severity::Warning); +} + +#[tokio::test] +async fn constraints_suppressed_and_not_evaluable() { + let evaluator = ScriptedConstraints { + outcome: |_| ConstraintOutcome::NotEvaluable("parse error: boom".to_string()), + seen: Mutex::new(Vec::new()), + }; + let suppress = vec!["pat-w".to_string()]; + let handlers = EffectHandlers { + constraints: Some(&evaluator), + suppress_constraints: &suppress, + ..Default::default() + }; + let resource = json!({ "resourceType": "Patient" }); + let errors = validator() + .validate(&resource, FhirVersion::R4, &ValidationOptions::default(), &handlers) + .await; + + assert_eq!(evaluator.seen.lock().unwrap().as_slice(), &["pat-x"]); + assert_eq!(errors.len(), 1); + assert_eq!(errors[0].severity, Severity::Warning, "not-evaluable is a warning"); + assert_eq!( + errors[0].message, + "FHIRPath constraint pat-x could not be evaluated: parse error: boom" + ); +} + +#[tokio::test] +async fn required_binding_pass_and_fail() { + let allow = AllowList(&["male", "female"]); + let handlers = EffectHandlers { terminology: Some(&allow), ..Default::default() }; + + let ok = json!({ "resourceType": "Patient", "gender": "male" }); + let errors = validator() + .validate(&ok, FhirVersion::R4, &ValidationOptions::default(), &handlers) + .await; + assert_eq!(errors, vec![]); + + let bad = json!({ "resourceType": "Patient", "gender": "zzz" }); + let errors = validator() + .validate(&bad, FhirVersion::R4, &ValidationOptions::default(), &handlers) + .await; + let as_json = serde_json::to_value(&errors).unwrap(); + assert_eq!( + as_json, + json!([{ + "type": "terminology-binding", + "path": "Patient.gender", + "message": "Provided coded value 'zzz' does not pass validation against the following valueset: 'http://hl7.org/fhir/ValueSet/administrative-gender'", + "binding": { + "valueSet": "http://hl7.org/fhir/ValueSet/administrative-gender", + "strength": "required" + } + }]) + ); +} + +#[tokio::test] +async fn non_required_bindings_are_not_checked() { + let allow = AllowList(&[]); // everything would fail if checked + let handlers = EffectHandlers { terminology: Some(&allow), ..Default::default() }; + let resource = json!({ + "resourceType": "Patient", + "maritalStatus": { "text": "married" } + }); + let errors = validator() + .validate(&resource, FhirVersion::R4, &ValidationOptions::default(), &handlers) + .await; + assert_eq!(errors, vec![], "extensible bindings must not be enforced"); +} + +#[tokio::test] +async fn terminology_outage_fail_open_and_closed() { + let down = Down; + let resource = json!({ "resourceType": "Patient", "gender": "male" }); + + let open = EffectHandlers { terminology: Some(&down), ..Default::default() }; + let errors = validator() + .validate(&resource, FhirVersion::R4, &ValidationOptions::default(), &open) + .await; + assert_eq!(errors.len(), 1); + assert_eq!(errors[0].severity, Severity::Warning, "fail-open surfaces a warning"); + assert!(errors[0].message.contains("could not be performed")); + + let closed = EffectHandlers { + terminology: Some(&down), + terminology_fail_closed: true, + ..Default::default() + }; + let errors = validator() + .validate(&resource, FhirVersion::R4, &ValidationOptions::default(), &closed) + .await; + assert_eq!(errors.len(), 1); + assert_eq!(errors[0].severity, Severity::Error, "fail-closed escalates to an error"); +} + +#[tokio::test] +async fn no_handlers_means_structural_only() { + let resource = json!({ "resourceType": "Patient", "gender": "zzz", "bogus": 1 }); + let errors = validator() + .validate( + &resource, + FhirVersion::R4, + &ValidationOptions::default(), + &EffectHandlers::default(), + ) + .await; + // Only the structural issue; deferred constraint/binding work is inert + // without handlers. + let as_json = serde_json::to_value(&errors).unwrap(); + assert_eq!( + as_json, + json!([{ + "type": "unknown-element", + "path": "Patient.bogus", + "message": "bogus is unknown" + }]) + ); +} diff --git a/crates/fhir-validator/tests/extended.rs b/crates/fhir-validator/tests/extended.rs index db95d6595..390091a54 100644 --- a/crates/fhir-validator/tests/extended.rs +++ b/crates/fhir-validator/tests/extended.rs @@ -48,3 +48,8 @@ fn extended_sidecars() { fn extended_element_reference() { run_extended("element_reference.json"); } + +#[test] +fn extended_primitives() { + run_extended("primitives.json"); +} diff --git a/crates/fhir-validator/tests/fhirpath_constraint_tests.rs b/crates/fhir-validator/tests/fhirpath_constraint_tests.rs new file mode 100644 index 000000000..5edaf4695 --- /dev/null +++ b/crates/fhir-validator/tests/fhirpath_constraint_tests.rs @@ -0,0 +1,106 @@ +//! End-to-end FHIRPath invariant evaluation (feature `fhirpath`): real +//! spec constraints from the embedded R4 pack, evaluated by helios-fhirpath +//! through the deferred-effects pipeline. +//! +//! Run with: `cargo test -p helios-fhir-validator --features fhirpath` +//! +//! ## MSVC 14.44 linker caveat +//! +//! This binary links helios-fhirpath's full dependency tree and can trip the +//! MSVC 14.44 (`link.exe` 14.44.35207) `LNK1318: Unexpected PDB error; +//! LIMIT (12)` defect on Windows. If that happens, the same scenarios are +//! covered end-to-end by `crates/rest/tests/validate_operation_tests.rs` +//! (`validate_evaluates_real_fhirpath_invariants`), whose test binary links +//! fine; alternatively add `-C link-arg=/PDBPAGESIZE:8192` to the MSVC +//! target rustflags or use a non-affected MSVC toolchain. + +#![cfg(all(feature = "fhirpath", feature = "R4"))] + +use helios_fhir::FhirVersion; +use helios_fhir_validator::fhirpath_effects::FhirPathConstraintEvaluator; +use helios_fhir_validator::packs::core_registry; +use helios_fhir_validator::{EffectHandlers, ErrorKind, ValidationOptions, Validator}; +use serde_json::json; + +fn handlers(evaluator: &FhirPathConstraintEvaluator) -> EffectHandlers<'_> { + EffectHandlers { constraints: Some(evaluator), ..Default::default() } +} + +/// dom-6 (narrative present) is a warning-severity invariant that fires on +/// almost every test resource; suppress it like the server default does. +fn suppress() -> Vec { + vec!["dom-6".to_string()] +} + +#[tokio::test] +async fn real_pat1_invariant_fires_on_detail_less_contact() { + let validator = Validator::new(core_registry(FhirVersion::R4)); + let evaluator = FhirPathConstraintEvaluator::new(); + let suppress = suppress(); + let mut h = handlers(&evaluator); + h.suppress_constraints = &suppress; + + // pat-1: "SHALL at least contain a contact's details or a reference to + // an organization" — a contact with only a gender violates it. + let violating = json!({ + "resourceType": "Patient", + "contact": [{ "gender": "male" }] + }); + let errors = validator + .validate(&violating, FhirVersion::R4, &ValidationOptions::default(), &h) + .await; + let pat1: Vec<_> = errors + .iter() + .filter(|e| e.kind == ErrorKind::FhirpathConstraint && e.path == "Patient.contact.0") + .collect(); + assert_eq!( + pat1.len(), + 1, + "pat-1 must fire exactly once at the contact node; all issues: {}", + serde_json::to_string_pretty(&errors).unwrap() + ); + assert_eq!( + pat1[0].extra.get("constraint"), + Some(&json!("pat-1")), + "constraint id carried in extras" + ); + + let satisfied = json!({ + "resourceType": "Patient", + "contact": [{ "gender": "male", "name": { "family": "Smith" } }] + }); + let errors = validator + .validate(&satisfied, FhirVersion::R4, &ValidationOptions::default(), &h) + .await; + assert!( + !errors.iter().any(|e| e.kind == ErrorKind::FhirpathConstraint + && e.extra.get("constraint") == Some(&json!("pat-1"))), + "pat-1 must pass when the contact has a name; got: {}", + serde_json::to_string_pretty(&errors).unwrap() + ); +} + +#[tokio::test] +async fn absent_nodes_pass_invariants() { + // FHIR invariant semantics: a constraint over an absent element is + // vacuously true (empty → pass). + let validator = Validator::new(core_registry(FhirVersion::R4)); + let evaluator = FhirPathConstraintEvaluator::new(); + let suppress = suppress(); + let mut h = handlers(&evaluator); + h.suppress_constraints = &suppress; + + let minimal = json!({ "resourceType": "Patient" }); + let errors = validator + .validate(&minimal, FhirVersion::R4, &ValidationOptions::default(), &h) + .await; + let hard_failures: Vec<_> = errors + .iter() + .filter(|e| e.severity == helios_fhir_validator::Severity::Error) + .collect(); + assert!( + hard_failures.is_empty(), + "a minimal Patient must have no error-severity issues, got: {}", + serde_json::to_string_pretty(&errors).unwrap() + ); +} diff --git a/crates/fhir-validator/tests/fixtures/extended/primitives.json b/crates/fhir-validator/tests/fixtures/extended/primitives.json new file mode 100644 index 000000000..79ffa6f7f --- /dev/null +++ b/crates/fhir-validator/tests/fixtures/extended/primitives.json @@ -0,0 +1,89 @@ +{ + "desc": "primitive value validation — Helios extended (JSON type-class from the primitive's declared type, regex from the pack's `regex` extension; dead code upstream)", + "schemas": { + "string": { "kind": "primitive-type", "type": "string", "regex": "[ \\r\\n\\t\\S]+" }, + "boolean": { "kind": "primitive-type", "type": "boolean" }, + "integer": { "kind": "primitive-type", "type": "integer" }, + "decimal": { "kind": "primitive-type", "type": "decimal" }, + "date": { + "kind": "primitive-type", + "type": "date", + "regex": "([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)(-(0[1-9]|1[0-2])(-(0[1-9]|[1-2][0-9]|3[0-1]))?)?" + }, + "Patient": { + "elements": { + "resourceType": { "type": "string" }, + "active": { "type": "boolean" }, + "rank": { "type": "integer" }, + "score": { "type": "decimal" }, + "birthDate": { "type": "date" }, + "note": { "type": "string" } + } + } + }, + "tests": [ + { + "desc": "all primitive values well-typed", + "data": { + "resourceType": "Patient", + "active": true, + "rank": 3, + "score": 1.5, + "birthDate": "1974-12-25", + "note": "hello" + } + }, + { + "desc": "boolean given a string", + "data": { "resourceType": "Patient", "active": "true" }, + "errors": [ + { + "type": "primitive-value", + "path": "Patient.active", + "message": "expected boolean, got string" + } + ] + }, + { + "desc": "integer given a decimal number", + "data": { "resourceType": "Patient", "rank": 2.5 }, + "errors": [ + { + "type": "primitive-value", + "path": "Patient.rank", + "message": "expected integer, got number" + } + ] + }, + { + "desc": "string given a number", + "data": { "resourceType": "Patient", "note": 5 }, + "errors": [ + { + "type": "primitive-value", + "path": "Patient.note", + "message": "expected string, got number" + } + ] + }, + { + "desc": "date failing its regex", + "data": { "resourceType": "Patient", "birthDate": "25/12/1974" }, + "errors": [ + { + "type": "primitive-value", + "path": "Patient.birthDate", + "message": "value '25/12/1974' is not a valid date" + } + ] + }, + { + "desc": "partial date passes the anchored regex", + "data": { "resourceType": "Patient", "birthDate": "1974-12" } + }, + { + "desc": "decimal accepts integers too", + "data": { "resourceType": "Patient", "score": 2 } + } + ] +} From 29d170738df46fb27c9b2b58f51a6f5680c19978 Mon Sep 17 00:00:00 2001 From: Doug Date: Sun, 5 Jul 2026 21:29:26 -0400 Subject: [PATCH 5/7] feat(rest): $validate operation backed by helios-fhir-validator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 5: - routes: POST /{type}/$validate and GET|POST /{type}/{id}/$validate (before the catch-all; GET validates the stored resource) — the CapabilityStatement's long-advertised validate operation is now real. - handlers/validate.rs: raw-resource or Parameters bodies (resource/mode/ profile parts, query fallbacks), mode=create|update|delete|profile semantics, always-200-OperationOutcome contract (4xx only for malformed requests). - validation.rs: ValidationService (embedded core packs per version, FHIRPath constraint evaluator with dom-6 suppressed by default, meta.profile honored, pluggable TerminologyProvider) + ErrorKind → IssueType mapping with bracket-indexed FHIRPath expressions. - OperationOutcome: IssueType gains Invariant and CodeInvalid. - AppState carries the service (always present; write-path gating comes with HFS_VALIDATION_* in the next phase). - 12 integration tests incl. real spec invariant (pat-1) evaluation through the full HTTP stack. Co-Authored-By: Claude Opus 4.8 (1M context) --- crates/rest/Cargo.toml | 11 +- crates/rest/src/handlers/mod.rs | 4 + crates/rest/src/handlers/validate.rs | 258 +++++++++++++++ crates/rest/src/lib.rs | 1 + .../rest/src/responses/operation_outcome.rs | 6 + crates/rest/src/routing/fhir_routes.rs | 11 + crates/rest/src/state.rs | 22 ++ crates/rest/src/validation.rs | 155 +++++++++ crates/rest/tests/validate_operation_tests.rs | 302 ++++++++++++++++++ 9 files changed, 765 insertions(+), 5 deletions(-) create mode 100644 crates/rest/src/handlers/validate.rs create mode 100644 crates/rest/src/validation.rs create mode 100644 crates/rest/tests/validate_operation_tests.rs diff --git a/crates/rest/Cargo.toml b/crates/rest/Cargo.toml index 8669c0d1c..694649b24 100644 --- a/crates/rest/Cargo.toml +++ b/crates/rest/Cargo.toml @@ -14,11 +14,11 @@ categories = ["web-programming", "database"] [features] default = ["R4", "sqlite"] -# FHIR version features (pass through to helios-fhir, helios-persistence, helios-audit, helios-serde, helios-sof, helios-subscriptions) -R4 = ["helios-fhir/R4", "helios-persistence/R4", "helios-audit/R4", "helios-serde?/R4", "helios-sof/R4", "helios-subscriptions?/R4"] -R4B = ["helios-fhir/R4B", "helios-persistence/R4B", "helios-audit/R4B", "helios-serde?/R4B", "helios-sof/R4B", "helios-subscriptions?/R4B"] -R5 = ["helios-fhir/R5", "helios-persistence/R5", "helios-audit/R5", "helios-serde?/R5", "helios-sof/R5", "helios-subscriptions?/R5"] -R6 = ["helios-fhir/R6", "helios-persistence/R6", "helios-audit/R6", "helios-serde?/R6", "helios-sof/R6", "helios-subscriptions?/R6"] +# FHIR version features (pass through to helios-fhir, helios-persistence, helios-audit, helios-serde, helios-sof, helios-subscriptions, helios-fhir-validator) +R4 = ["helios-fhir/R4", "helios-persistence/R4", "helios-audit/R4", "helios-serde?/R4", "helios-sof/R4", "helios-subscriptions?/R4", "helios-fhir-validator/R4"] +R4B = ["helios-fhir/R4B", "helios-persistence/R4B", "helios-audit/R4B", "helios-serde?/R4B", "helios-sof/R4B", "helios-subscriptions?/R4B", "helios-fhir-validator/R4B"] +R5 = ["helios-fhir/R5", "helios-persistence/R5", "helios-audit/R5", "helios-serde?/R5", "helios-sof/R5", "helios-subscriptions?/R5", "helios-fhir-validator/R5"] +R6 = ["helios-fhir/R6", "helios-persistence/R6", "helios-audit/R6", "helios-serde?/R6", "helios-sof/R6", "helios-subscriptions?/R6", "helios-fhir-validator/R6"] # Serialization format features xml = ["helios-fhir/xml", "dep:helios-serde", "helios-serde?/xml"] @@ -50,6 +50,7 @@ helios-sof = { path = "../sof", version = "0.2.1", default-features = false } helios-auth = { path = "../auth", version = "0.2.1" } helios-audit = { path = "../audit", version = "0.2.1", default-features = false } helios-subscriptions = { path = "../subscriptions", version = "0.2.1", optional = true, default-features = false } +helios-fhir-validator = { path = "../fhir-validator", version = "0.2.1", default-features = false, features = ["fhirpath"] } # Async runtime tokio = { version = "1", features = ["full"] } diff --git a/crates/rest/src/handlers/mod.rs b/crates/rest/src/handlers/mod.rs index 7808d1c8c..feb2bdb68 100644 --- a/crates/rest/src/handlers/mod.rs +++ b/crates/rest/src/handlers/mod.rs @@ -35,6 +35,7 @@ pub mod subscription_event; #[cfg(feature = "subscriptions")] pub mod subscriptions; pub mod update; +pub mod validate; pub mod versions; pub mod vread; #[cfg(feature = "subscriptions")] @@ -77,5 +78,8 @@ pub use patch::patch_handler; pub use read::{head_read_handler, read_handler}; pub use search::{search_get_handler, search_post_handler}; pub use update::{conditional_update_handler, update_handler}; +pub use validate::{ + validate_instance_get_handler, validate_instance_post_handler, validate_type_handler, +}; pub use versions::versions_handler; pub use vread::vread_handler; diff --git a/crates/rest/src/handlers/validate.rs b/crates/rest/src/handlers/validate.rs new file mode 100644 index 000000000..3f769dd7f --- /dev/null +++ b/crates/rest/src/handlers/validate.rs @@ -0,0 +1,258 @@ +//! `$validate` operation handlers. +//! +//! Implements the FHIR [Resource-validate operation](https://hl7.org/fhir/OperationDefinition/Resource-validate): +//! +//! - `POST [base]/[type]/$validate` — validate the body (a raw resource or +//! a `Parameters` wrapper carrying `resource`/`mode`/`profile` parts) +//! - `GET [base]/[type]/[id]/$validate` — validate the **stored** resource +//! - `POST [base]/[type]/[id]/$validate` — validate the body in the context +//! of the addressed instance (update semantics) +//! +//! Per the operation definition, validation **always returns `200 OK`** +//! with an `OperationOutcome` — an invalid resource is a successful +//! validation with error issues. Non-200 responses are reserved for +//! malformed requests (unknown type, missing body, bad `Parameters`, +//! `mode=profile` without a profile). +//! +//! `mode=delete` performs no content validation (no referential-integrity +//! checking yet) and reports the all-clear outcome. + +use axum::extract::{Path, Query, State}; +use axum::http::{HeaderMap, StatusCode}; +use axum::response::Response; +use helios_persistence::core::ResourceStorage; +use serde::Deserialize; +use serde_json::Value; +use tracing::debug; + +use crate::error::{RestError, RestResult}; +use crate::extractors::{FhirResource, FhirVersionExtractor, TenantExtractor}; +use crate::middleware::content_type::negotiate_format; +use crate::responses::format_resource_response; +use crate::responses::operation_outcome::{IssueType, OperationOutcomeBuilder}; +use crate::state::AppState; +use crate::validation::validation_outcome; + +/// Query-string fallbacks for the operation inputs (GET, or POST without a +/// `Parameters` wrapper). +#[derive(Debug, Default, Deserialize)] +pub struct ValidateQuery { + mode: Option, + profile: Option, +} + +/// The operation inputs after unwrapping body/query. +struct ValidateInputs { + resource: Option, + mode: ValidateMode, + profiles: Vec, +} + +#[derive(Debug, Clone, Copy, PartialEq)] +enum ValidateMode { + General, + Create, + Update, + Delete, + Profile, +} + +impl ValidateMode { + fn parse(raw: Option<&str>) -> Result { + match raw { + None => Ok(ValidateMode::General), + Some("create") => Ok(ValidateMode::Create), + Some("update") => Ok(ValidateMode::Update), + Some("delete") => Ok(ValidateMode::Delete), + Some("profile") => Ok(ValidateMode::Profile), + Some(other) => Err(RestError::BadRequest { + message: format!( + "Invalid $validate mode '{other}' (expected create | update | delete | profile)" + ), + }), + } + } +} + +/// Unwrap the request body: either a raw resource, or a `Parameters` +/// carrying `resource` / `mode` / `profile` parts. Query parameters fill in +/// whatever the body did not provide. +fn unwrap_inputs(body: Option, query: &ValidateQuery) -> Result { + let mut resource = None; + let mut mode_raw: Option = None; + let mut profiles: Vec = Vec::new(); + + if let Some(body) = body { + if body.get("resourceType").and_then(Value::as_str) == Some("Parameters") { + for param in body + .get("parameter") + .and_then(Value::as_array) + .map(|a| a.as_slice()) + .unwrap_or_default() + { + match param.get("name").and_then(Value::as_str) { + Some("resource") => { + resource = param.get("resource").cloned(); + } + Some("mode") => { + mode_raw = param + .get("valueCode") + .or_else(|| param.get("valueString")) + .and_then(Value::as_str) + .map(str::to_string); + } + Some("profile") => { + if let Some(p) = param + .get("valueUri") + .or_else(|| param.get("valueCanonical")) + .or_else(|| param.get("valueString")) + .and_then(Value::as_str) + { + profiles.push(p.to_string()); + } + } + _ => {} + } + } + } else { + resource = Some(body); + } + } + + if mode_raw.is_none() { + mode_raw = query.mode.clone(); + } + if let Some(p) = &query.profile { + if !profiles.iter().any(|existing| existing == p) { + profiles.push(p.clone()); + } + } + + let mode = ValidateMode::parse(mode_raw.as_deref())?; + if mode == ValidateMode::Profile && profiles.is_empty() { + return Err(RestError::BadRequest { + message: "$validate mode=profile requires a profile parameter".to_string(), + }); + } + + Ok(ValidateInputs { resource, mode, profiles }) +} + +/// Shared tail: run validation and shape the 200 OperationOutcome response. +async fn respond( + state: &AppState, + fhir_version: helios_fhir::FhirVersion, + inputs: ValidateInputs, + resource_type: &str, + req_headers: &HeaderMap, +) -> RestResult +where + S: ResourceStorage + Send + Sync, +{ + let negotiated = negotiate_format(req_headers, None); + + // Delete validation: no content checks yet (no referential integrity). + if inputs.mode == ValidateMode::Delete { + let outcome = OperationOutcomeBuilder::new() + .information(IssueType::Informational, "Delete validation not performed: no checks configured") + .build(); + return format_resource_response(StatusCode::OK, HeaderMap::new(), &outcome, negotiated.format) + .map_err(|_| RestError::InternalError { + message: "Failed to serialize response".to_string(), + }); + } + + let Some(resource) = inputs.resource else { + return Err(RestError::BadRequest { + message: "$validate requires a resource (in the body or as the 'resource' parameter)" + .to_string(), + }); + }; + + // The resource must be of the addressed type. + if let Some(body_type) = resource.get("resourceType").and_then(Value::as_str) { + if body_type != resource_type { + return Err(RestError::BadRequest { + message: format!( + "Resource type in body ({body_type}) does not match URL ({resource_type})" + ), + }); + } + } + + let issues = state + .validation() + .validate_resource(fhir_version, &resource, inputs.profiles) + .await; + + debug!( + resource_type = %resource_type, + issue_count = issues.len(), + "$validate completed" + ); + + let outcome = validation_outcome(&issues); + format_resource_response(StatusCode::OK, HeaderMap::new(), &outcome, negotiated.format) + .map_err(|_| RestError::InternalError { message: "Failed to serialize response".to_string() }) +} + +/// `POST [base]/[type]/$validate` +pub async fn validate_type_handler( + State(state): State>, + Path(resource_type): Path, + _tenant: TenantExtractor, + version: FhirVersionExtractor, + Query(query): Query, + req_headers: HeaderMap, + FhirResource(body): FhirResource, +) -> RestResult +where + S: ResourceStorage + Send + Sync, +{ + let inputs = unwrap_inputs(Some(body), &query)?; + respond(&state, version.storage_version(), inputs, &resource_type, &req_headers).await +} + +/// `GET [base]/[type]/[id]/$validate` — validate the stored resource. +pub async fn validate_instance_get_handler( + State(state): State>, + Path((resource_type, id)): Path<(String, String)>, + tenant: TenantExtractor, + version: FhirVersionExtractor, + Query(query): Query, + req_headers: HeaderMap, +) -> RestResult +where + S: ResourceStorage + Send + Sync, +{ + let stored = state + .storage() + .read(tenant.context(), &resource_type, &id) + .await? + .ok_or_else(|| RestError::NotFound { + resource_type: resource_type.clone(), + id: id.clone(), + })?; + + let mut inputs = unwrap_inputs(None, &query)?; + inputs.resource = Some(stored.content().clone()); + respond(&state, version.storage_version(), inputs, &resource_type, &req_headers).await +} + +/// `POST [base]/[type]/[id]/$validate` — validate the body in the context +/// of the addressed instance (update semantics). +pub async fn validate_instance_post_handler( + State(state): State>, + Path((resource_type, _id)): Path<(String, String)>, + _tenant: TenantExtractor, + version: FhirVersionExtractor, + Query(query): Query, + req_headers: HeaderMap, + FhirResource(body): FhirResource, +) -> RestResult +where + S: ResourceStorage + Send + Sync, +{ + let inputs = unwrap_inputs(Some(body), &query)?; + respond(&state, version.storage_version(), inputs, &resource_type, &req_headers).await +} diff --git a/crates/rest/src/lib.rs b/crates/rest/src/lib.rs index 77d958436..276936ef5 100644 --- a/crates/rest/src/lib.rs +++ b/crates/rest/src/lib.rs @@ -161,6 +161,7 @@ pub mod routing; pub mod state; pub mod tenant; pub mod terminology; +pub mod validation; // Re-export commonly used types pub use config::{MultitenancyConfig, ServerConfig, StorageBackendMode, TenantRoutingMode}; diff --git a/crates/rest/src/responses/operation_outcome.rs b/crates/rest/src/responses/operation_outcome.rs index dda1f09d5..e521ae992 100644 --- a/crates/rest/src/responses/operation_outcome.rs +++ b/crates/rest/src/responses/operation_outcome.rs @@ -40,6 +40,10 @@ pub enum IssueType { Required, /// Value out of range. Value, + /// FHIRPath invariant violation. + Invariant, + /// Invalid code / failed terminology binding. + CodeInvalid, /// Resource not found. NotFound, /// Resource was deleted. @@ -78,6 +82,8 @@ impl IssueType { IssueType::Structure => "structure", IssueType::Required => "required", IssueType::Value => "value", + IssueType::Invariant => "invariant", + IssueType::CodeInvalid => "code-invalid", IssueType::NotFound => "not-found", IssueType::Deleted => "deleted", IssueType::MultipleMatches => "multiple-matches", diff --git a/crates/rest/src/routing/fhir_routes.rs b/crates/rest/src/routing/fhir_routes.rs index 5e7295262..09f5625f7 100644 --- a/crates/rest/src/routing/fhir_routes.rs +++ b/crates/rest/src/routing/fhir_routes.rs @@ -276,6 +276,17 @@ where "/bulk-submit-file/{poll_token}/{part}", get(handlers::bulk_submit_file_handler::), ) + // Resource validation ($validate) — operation routes precede the + // catch-all (matchit gives static segments priority over {id}). + .route( + "/{resource_type}/$validate", + post(handlers::validate_type_handler::), + ) + .route( + "/{resource_type}/{id}/$validate", + get(handlers::validate_instance_get_handler::) + .post(handlers::validate_instance_post_handler::), + ) // Type-level routes .route("/{resource_type}", get(handlers::search_get_handler::)) .route("/{resource_type}", post(handlers::create_handler::)) diff --git a/crates/rest/src/state.rs b/crates/rest/src/state.rs index f4fee7956..5bb3b561b 100644 --- a/crates/rest/src/state.rs +++ b/crates/rest/src/state.rs @@ -93,6 +93,10 @@ pub struct AppState { /// Bulk submit configuration. bulk_submit_config: Arc, + + /// Resource validation service ($validate + optional write-path + /// enforcement). Always present; write-path behavior is config-gated. + validation: Arc, } // Manually implement Clone since S is wrapped in Arc and doesn't need to be Clone @@ -118,6 +122,7 @@ impl Clone for AppState { bulk_submit_output: self.bulk_submit_output.clone(), bulk_submit_file_auth: self.bulk_submit_file_auth.clone(), bulk_submit_config: Arc::clone(&self.bulk_submit_config), + validation: Arc::clone(&self.validation), } } } @@ -152,6 +157,7 @@ impl AppState { bulk_submit_output: None, bulk_submit_file_auth: None, bulk_submit_config, + validation: Arc::new(crate::validation::ValidationService::new()), } } @@ -196,9 +202,25 @@ impl AppState { bulk_submit_output: None, bulk_submit_file_auth: None, bulk_submit_config, + validation: Arc::new(crate::validation::ValidationService::new()), } } + /// Replaces the validation service (e.g. one configured with a + /// terminology provider or non-default constraint suppression). + pub fn with_validation( + mut self, + validation: Arc, + ) -> Self { + self.validation = validation; + self + } + + /// Returns the resource validation service. + pub fn validation(&self) -> &crate::validation::ValidationService { + &self.validation + } + /// Sets the SQL-on-FHIR runner for this application state. /// /// Typically called at startup after creating the state, once the runner has been diff --git a/crates/rest/src/validation.rs b/crates/rest/src/validation.rs new file mode 100644 index 000000000..942d64bb0 --- /dev/null +++ b/crates/rest/src/validation.rs @@ -0,0 +1,155 @@ +//! Resource validation service: the REST layer's bridge to +//! `helios-fhir-validator`. +//! +//! Owns the per-version core validators (embedded schema packs), the +//! FHIRPath constraint evaluator, the optional terminology provider, and +//! the mapping from validator issues onto FHIR `OperationOutcome` issues. +//! Used by the `$validate` operation (always available) and — when +//! `HFS_VALIDATION_MODE` enables it — by the write-path enforcement hooks. + +use crate::responses::operation_outcome::{ + Issue, IssueSeverity, IssueType, OperationOutcomeBuilder, +}; +use helios_fhir::FhirVersion; +use helios_fhir_validator::fhirpath_effects::FhirPathConstraintEvaluator; +use helios_fhir_validator::{ + dotted_to_fhirpath, EffectHandlers, ErrorKind, Severity, TerminologyProvider, + UnknownProfilePolicy, ValidationError, ValidationOptions, Validator, +}; +use serde_json::Value; +use std::sync::Arc; + +/// The validation service. Cheap to construct (core registries load lazily, +/// once per process); hold one in `AppState` for the AST cache to pay off. +pub struct ValidationService { + constraint_evaluator: FhirPathConstraintEvaluator, + terminology: Option>, + /// Constraint ids never evaluated (default: `dom-6`, the narrative + /// warning that fires on almost every machine-generated resource). + suppress_constraints: Vec, + /// Escalate terminology-service failures to errors. + terminology_fail_closed: bool, + /// Validate against `meta.profile` claims. + use_meta_profiles: bool, + unknown_profile: UnknownProfilePolicy, +} + +impl Default for ValidationService { + fn default() -> Self { + Self { + constraint_evaluator: FhirPathConstraintEvaluator::new(), + terminology: None, + suppress_constraints: vec!["dom-6".to_string()], + terminology_fail_closed: false, + use_meta_profiles: true, + unknown_profile: UnknownProfilePolicy::Warn, + } + } +} + +impl ValidationService { + /// A service with the default posture: constraints on (dom-6 + /// suppressed), meta.profile honored, unresolvable profiles warned, + /// no terminology provider. + pub fn new() -> Self { + Self::default() + } + + /// Replace the terminology provider (bindings stay unchecked without one). + pub fn with_terminology(mut self, provider: Arc) -> Self { + self.terminology = Some(provider); + self + } + + /// Escalate terminology-service failures from warnings to errors. + pub fn set_terminology_fail_closed(&mut self, closed: bool) { + self.terminology_fail_closed = closed; + } + + /// Replace the set of constraint ids that are never evaluated. + pub fn set_suppress_constraints(&mut self, ids: Vec) { + self.suppress_constraints = ids; + } + + /// Toggle validation against `meta.profile` claims. + pub fn set_use_meta_profiles(&mut self, on: bool) { + self.use_meta_profiles = on; + } + + /// Validate a resource against the core pack for `version` plus any + /// extra profile canonicals. Structural issues first, then constraint + /// issues, then binding issues. + pub async fn validate_resource( + &self, + version: FhirVersion, + resource: &Value, + profiles: Vec, + ) -> Vec { + let validator = Validator::new(helios_fhir_validator::packs::core_registry(version)); + let opts = ValidationOptions { + profiles, + use_meta_profiles: self.use_meta_profiles, + unknown_profile: self.unknown_profile, + }; + let handlers = EffectHandlers { + constraints: Some(&self.constraint_evaluator), + terminology: self.terminology.as_deref(), + suppress_constraints: &self.suppress_constraints, + terminology_fail_closed: self.terminology_fail_closed, + }; + validator.validate(resource, version, &opts, &handlers).await + } +} + +/// Map one validator issue onto an OperationOutcome issue. +pub fn to_outcome_issue(error: &ValidationError) -> Issue { + let code = match error.kind { + ErrorKind::Required => IssueType::Required, + ErrorKind::FixedValue | ErrorKind::PatternValue | ErrorKind::PrimitiveValue => { + IssueType::Value + } + ErrorKind::FhirpathConstraint => IssueType::Invariant, + ErrorKind::TerminologyBinding => IssueType::CodeInvalid, + ErrorKind::UnknownSchema | ErrorKind::UnknownProfile => IssueType::NotSupported, + // Everything structural: unknown-element, shape, cardinality, + // slicing, choices, wrong container type. + ErrorKind::UnknownElement + | ErrorKind::NotArray + | ErrorKind::NotSingular + | ErrorKind::Type + | ErrorKind::Excluded + | ErrorKind::Min + | ErrorKind::Max + | ErrorKind::SliceCardinality + | ErrorKind::SliceUnmatched + | ErrorKind::SliceOrder + | ErrorKind::Choice + | ErrorKind::ChoiceExcluded => IssueType::Structure, + }; + let severity = match error.severity { + Severity::Error => IssueSeverity::Error, + Severity::Warning => IssueSeverity::Warning, + }; + Issue::new(severity, code, error.message.clone()) + .with_expression(dotted_to_fhirpath(&error.path)) +} + +/// Build the `$validate` OperationOutcome: the mapped issues, or the +/// canonical all-clear information issue when there are none. +pub fn validation_outcome(errors: &[ValidationError]) -> Value { + let mut builder = OperationOutcomeBuilder::new(); + if errors.is_empty() { + builder = builder.information(IssueType::Informational, "Validation successful"); + } else { + for error in errors { + builder = builder.add_issue(to_outcome_issue(error)); + } + } + builder.build() +} + +/// Whether any issue is error severity (drives `$validate` reporting and +/// enforce-mode rejection). +pub fn has_errors(errors: &[ValidationError]) -> bool { + errors.iter().any(|e| e.severity == Severity::Error) +} diff --git a/crates/rest/tests/validate_operation_tests.rs b/crates/rest/tests/validate_operation_tests.rs new file mode 100644 index 000000000..6f0f923ca --- /dev/null +++ b/crates/rest/tests/validate_operation_tests.rs @@ -0,0 +1,302 @@ +//! Integration tests for the `$validate` operation. +//! +//! `POST [base]/[type]/$validate` (raw resource or Parameters wrapper), +//! `GET/POST [base]/[type]/[id]/$validate`, mode/profile handling, and the +//! always-200-OperationOutcome contract. + +use std::path::PathBuf; +use std::sync::Arc; + +use axum_test::TestServer; +use helios_fhir::FhirVersion; +use helios_persistence::backends::sqlite::{SqliteBackend, SqliteBackendConfig}; +use helios_persistence::core::ResourceStorage; +use helios_persistence::tenant::{TenantContext, TenantId, TenantPermissions}; +use helios_rest::{MultitenancyConfig, ServerConfig, TenantRoutingMode}; +use serde_json::{json, Value}; + +async fn create_test_server() -> (TestServer, Arc) { + let data_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .parent() + .and_then(|p| p.parent()) + .map(|p| p.join("data")) + .unwrap_or_else(|| PathBuf::from("data")); + + let backend_config = SqliteBackendConfig { data_dir: Some(data_dir), ..Default::default() }; + let backend = SqliteBackend::with_config(":memory:", backend_config) + .expect("Failed to create SQLite backend"); + backend.init_schema().expect("Failed to init schema"); + let backend = Arc::new(backend); + + let config = ServerConfig { + multitenancy: MultitenancyConfig { + routing_mode: TenantRoutingMode::HeaderOnly, + ..Default::default() + }, + base_url: "http://localhost:8080".to_string(), + default_tenant: "test-tenant".to_string(), + ..ServerConfig::for_testing() + }; + + let state = helios_rest::AppState::new(Arc::clone(&backend), config); + let app = helios_rest::routing::fhir_routes::create_routes(state); + let server = TestServer::new(app).expect("Failed to create test server"); + (server, backend) +} + +fn test_tenant() -> TenantContext { + TenantContext::new(TenantId::new("test-tenant"), TenantPermissions::full_access()) +} + +fn issue_codes(outcome: &Value) -> Vec<(String, String)> { + outcome["issue"] + .as_array() + .map(|issues| { + issues + .iter() + .map(|i| { + ( + i["severity"].as_str().unwrap_or_default().to_string(), + i["code"].as_str().unwrap_or_default().to_string(), + ) + }) + .collect() + }) + .unwrap_or_default() +} + +#[tokio::test] +async fn validate_valid_patient_returns_all_clear() { + let (server, _backend) = create_test_server().await; + let response = server + .post("/Patient/$validate") + .json(&json!({ + "resourceType": "Patient", + "name": [{ "family": "Smith", "given": ["Jan"] }], + "gender": "female", + "birthDate": "1980-02-29" + })) + .await; + + response.assert_status_ok(); + let outcome: Value = response.json(); + assert_eq!(outcome["resourceType"], "OperationOutcome"); + assert_eq!( + issue_codes(&outcome), + vec![("information".to_string(), "informational".to_string())], + "clean validation reports the all-clear issue: {outcome:#}" + ); +} + +#[tokio::test] +async fn validate_reports_structural_issues_with_expressions() { + let (server, _backend) = create_test_server().await; + let response = server + .post("/Patient/$validate") + .json(&json!({ + "resourceType": "Patient", + "bogusElement": true, + "gender": ["male"], + "name": { "family": "NotAnArray" } + })) + .await; + + // Invalid resource is still a SUCCESSFUL validation: 200 + issues. + response.assert_status_ok(); + let outcome: Value = response.json(); + let issues = outcome["issue"].as_array().expect("issues"); + assert!( + issues.iter().any(|i| i["code"] == "structure" + && i["expression"][0] == "Patient.bogusElement"), + "unknown element issue with FHIRPath expression expected: {outcome:#}" + ); + assert!( + issues.iter().any(|i| i["expression"][0] == "Patient.gender"), + "not-singular issue on gender expected: {outcome:#}" + ); + assert!( + issues.iter().any(|i| i["expression"][0] == "Patient.name"), + "not-array issue on name expected: {outcome:#}" + ); +} + +#[tokio::test] +async fn validate_accepts_parameters_wrapper() { + let (server, _backend) = create_test_server().await; + let response = server + .post("/Patient/$validate") + .json(&json!({ + "resourceType": "Parameters", + "parameter": [ + { "name": "mode", "valueCode": "create" }, + { "name": "resource", "resource": { "resourceType": "Patient", "active": true } } + ] + })) + .await; + + response.assert_status_ok(); + let outcome: Value = response.json(); + assert_eq!( + issue_codes(&outcome), + vec![("information".to_string(), "informational".to_string())], + "{outcome:#}" + ); +} + +#[tokio::test] +async fn validate_mode_delete_skips_content_validation() { + let (server, _backend) = create_test_server().await; + // Delete validation needs no resource at all. + let response = server + .post("/Patient/$validate?mode=delete") + .json(&json!({ + "resourceType": "Parameters", + "parameter": [] + })) + .await; + response.assert_status_ok(); + let outcome: Value = response.json(); + assert_eq!(outcome["issue"][0]["severity"], "information", "{outcome:#}"); +} + +#[tokio::test] +async fn validate_mode_profile_requires_profile() { + let (server, _backend) = create_test_server().await; + let response = server + .post("/Patient/$validate?mode=profile") + .json(&json!({ "resourceType": "Patient" })) + .await; + response.assert_status_bad_request(); +} + +#[tokio::test] +async fn validate_rejects_unknown_mode() { + let (server, _backend) = create_test_server().await; + let response = server + .post("/Patient/$validate?mode=bogus") + .json(&json!({ "resourceType": "Patient" })) + .await; + response.assert_status_bad_request(); +} + +#[tokio::test] +async fn validate_rejects_type_mismatch() { + let (server, _backend) = create_test_server().await; + let response = server + .post("/Patient/$validate") + .json(&json!({ "resourceType": "Observation", "status": "final" })) + .await; + response.assert_status_bad_request(); +} + +#[tokio::test] +async fn validate_unknown_profile_reports_warning() { + let (server, _backend) = create_test_server().await; + let response = server + .post("/Patient/$validate?profile=http://example.org/StructureDefinition/nope") + .json(&json!({ "resourceType": "Patient" })) + .await; + response.assert_status_ok(); + let outcome: Value = response.json(); + assert!( + issue_codes(&outcome) + .iter() + .any(|(sev, code)| sev == "warning" && code == "not-supported"), + "unresolvable profile surfaces as a warning: {outcome:#}" + ); +} + +#[tokio::test] +async fn validate_instance_get_validates_stored_resource() { + let (server, backend) = create_test_server().await; + let tenant = test_tenant(); + backend + .create( + &tenant, + "Patient", + json!({ "resourceType": "Patient", "id": "p1", "active": true }), + FhirVersion::R4, + ) + .await + .expect("seed patient"); + + let response = server.get("/Patient/p1/$validate").await; + response.assert_status_ok(); + let outcome: Value = response.json(); + assert_eq!(outcome["resourceType"], "OperationOutcome", "{outcome:#}"); + assert!( + !issue_codes(&outcome).iter().any(|(sev, _)| sev == "error"), + "stored minimal patient must validate without errors: {outcome:#}" + ); +} + +#[tokio::test] +async fn validate_instance_get_unknown_id_is_404() { + let (server, _backend) = create_test_server().await; + let response = server.get("/Patient/does-not-exist/$validate").await; + response.assert_status_not_found(); +} + +#[tokio::test] +async fn validate_evaluates_real_fhirpath_invariants() { + // End-to-end: the default ValidationService carries the FHIRPath + // constraint evaluator, so the real spec invariant pat-1 ("contact + // SHALL contain details or an organization reference") fires against + // the embedded R4 pack. + let (server, _backend) = create_test_server().await; + let response = server + .post("/Patient/$validate") + .json(&json!({ + "resourceType": "Patient", + "contact": [{ "gender": "male" }] + })) + .await; + response.assert_status_ok(); + let outcome: Value = response.json(); + let issues = outcome["issue"].as_array().expect("issues"); + assert!( + issues.iter().any(|i| i["code"] == "invariant" + && i["severity"] == "error" + && i["expression"][0] == "Patient.contact[0]" + && i["details"]["text"].as_str().unwrap_or_default().contains("pat-1")), + "pat-1 invariant issue expected: {outcome:#}" + ); + + // Satisfying the invariant clears it. + let response = server + .post("/Patient/$validate") + .json(&json!({ + "resourceType": "Patient", + "contact": [{ "gender": "male", "name": { "family": "Smith" } }] + })) + .await; + response.assert_status_ok(); + let outcome: Value = response.json(); + assert!( + !outcome["issue"] + .as_array() + .unwrap() + .iter() + .any(|i| i["code"] == "invariant" && i["severity"] == "error"), + "satisfied contact must not fire pat-1: {outcome:#}" + ); +} + +#[tokio::test] +async fn validate_instance_post_validates_body() { + let (server, _backend) = create_test_server().await; + let response = server + .post("/Patient/p1/$validate?mode=update") + .json(&json!({ "resourceType": "Patient", "id": "p1", "oops": 1 })) + .await; + response.assert_status_ok(); + let outcome: Value = response.json(); + assert!( + outcome["issue"] + .as_array() + .unwrap() + .iter() + .any(|i| i["expression"][0] == "Patient.oops"), + "{outcome:#}" + ); +} From 43c2bd452cfd30d40e494fa25537df74130f91d2 Mon Sep 17 00:00:00 2001 From: Doug Date: Sun, 5 Jul 2026 21:50:09 -0400 Subject: [PATCH 6/7] feat(rest): write-path validation enforcement, HFS_VALIDATION_* config, tenant profiles Phase 6: - ValidationConfig (HFS_VALIDATION_MODE off|log|enforce, META_PROFILES, UNKNOWN_PROFILE, CONSTRAINTS, SUPPRESS_CONSTRAINTS, TERMINOLOGY off|remote, TERMINOLOGY_TIMEOUT_MS, TERMINOLOGY_FAIL open|closed, STORED_PROFILES) following the BulkExportConfig pattern; remote terminology requires HFS_TERMINOLOGY_SERVER (validated). - ValidationService::from_config + check_write: enforce mode rejects invalid writes with 422 carrying the full multi-issue OperationOutcome (new RestError::ValidationFailed); log mode traces and proceeds. - Hooks on every write path: create, update, conditional create/update, batch (per-entry 422 results), and transaction (atomic pre-execution rejection). - Per-tenant profile registries: stored StructureDefinitions convert and register on write (all write paths), overlay the core packs via CompositeResolver for $validate, meta.profile, and enforcement. In-memory since process start (startup warm-load is future work). - RemoteTerminologyProvider: ValueSet/$validate-code against HFS_TERMINOLOGY_SERVER with a 300s verdict cache. - Docs: config env table, run-hfs-server skill section, CLAUDE.md workspace table (validator + web crates). - 6 enforcement integration tests (enforce/log/off matrix, batch per-entry, stored-profile $validate round-trip, meta.profile enforcement); rest and batch conformance suites still green (39 + 57). Co-Authored-By: Claude Opus 4.8 (1M context) --- .claude/skills/run-hfs-server/SKILL.md | 22 ++ CLAUDE.md | 5 +- crates/rest/src/config.rs | 158 ++++++++ crates/rest/src/error.rs | 14 + crates/rest/src/handlers/batch.rs | 112 +++++- crates/rest/src/handlers/create.rs | 22 ++ crates/rest/src/handlers/update.rs | 19 + crates/rest/src/handlers/validate.rs | 61 +++- crates/rest/src/state.rs | 12 +- crates/rest/src/validation.rs | 344 ++++++++++++++++-- .../tests/validation_enforcement_tests.rs | 249 +++++++++++++ 11 files changed, 966 insertions(+), 52 deletions(-) create mode 100644 crates/rest/tests/validation_enforcement_tests.rs diff --git a/.claude/skills/run-hfs-server/SKILL.md b/.claude/skills/run-hfs-server/SKILL.md index 963012d53..e6911c7f0 100644 --- a/.claude/skills/run-hfs-server/SKILL.md +++ b/.claude/skills/run-hfs-server/SKILL.md @@ -128,6 +128,28 @@ curl http://localhost:8080/clinic-a/Patient | `HFS_ENABLE_VERSIONING` | `true` | Enable ETag versioning | | `HFS_REQUIRE_IF_MATCH` | `false` | Require If-Match header for updates | +## Resource Validation + +FHIR Schema based validation (helios-fhir-validator). `$validate` is always +available (`POST /[type]/$validate`, `GET|POST /[type]/[id]/$validate`); +these settings gate the write path and tune the shared service. Stored +StructureDefinitions (per tenant) become validatable profiles on write. + +| Variable | Default | Description | +|---|---|---| +| `HFS_VALIDATION_MODE` | `off` | Write-path validation: `off`, `log`, or `enforce` (422 on invalid) | +| `HFS_VALIDATION_META_PROFILES` | `true` | Validate against `meta.profile` claims | +| `HFS_VALIDATION_UNKNOWN_PROFILE` | `warn` | Unresolvable profiles: `warn`, `error`, or `ignore` | +| `HFS_VALIDATION_CONSTRAINTS` | `true` | Evaluate FHIRPath invariants | +| `HFS_VALIDATION_SUPPRESS_CONSTRAINTS` | `dom-6` | Comma-separated constraint ids to skip | +| `HFS_VALIDATION_TERMINOLOGY` | `off` | Required-binding checks: `off` or `remote` (`ValueSet/$validate-code` against `HFS_TERMINOLOGY_SERVER`) | +| `HFS_VALIDATION_TERMINOLOGY_TIMEOUT_MS` | `3000` | Per-check terminology timeout | +| `HFS_VALIDATION_TERMINOLOGY_FAIL` | `open` | Terminology outage posture: `open` (warn) or `closed` (error) | +| `HFS_VALIDATION_STORED_PROFILES` | `true` | Maintain per-tenant profile registries from stored StructureDefinitions | + +Note: tenant profile registries are in-memory and populated by +StructureDefinition writes since process start (no startup warm-load yet). + ## API Endpoints | Interaction | Method | URL | diff --git a/CLAUDE.md b/CLAUDE.md index 79885f091..78531c3df 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,7 +6,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ### Workspace Structure -The project is a Rust workspace with 17 crates (16 default-members; `pysof` excluded from the default build): +The project is a Rust workspace with 19 crates (18 default-members; `pysof` excluded from the default build): | Crate | Description | |-------|-------------| @@ -15,6 +15,7 @@ The project is a Rust workspace with 17 crates (16 default-members; `pysof` excl | **`helios-fhir-macro`** | Procedural macros for FHIR functionality. | | **`helios-fhirpath`** | FHIRPath expression language — parser (chumsky), evaluator, CLI tool, and HTTP server. | | **`helios-fhirpath-support`** | Shared support utilities for FHIRPath. | +| **`helios-fhir-validator`** | FHIR resource validation — FHIR Schema based structural/profile engine, SD→schema converter, embedded core packs (R4–R6), deferred FHIRPath-constraint and terminology-binding effects. Configured via `HFS_VALIDATION_*`. | | **`helios-serde`** | JSON and XML serialization for FHIR resources (`xml` feature flag). | | **`helios-serde-support`** | Shared serde helpers. | | **`helios-rest`** | FHIR RESTful API layer (Axum) — handlers, middleware, extractors, multi-tenancy routing. | @@ -26,6 +27,7 @@ The project is a Rust workspace with 17 crates (16 default-members; `pysof` excl | **`helios-audit`** | Audit logging — FHIR AuditEvent with IHE BALP profiles; pluggable sinks (database, file, CloudWatch, S3). Configured via `HFS_AUDIT_*`. | | **`helios-subscriptions`** | FHIR topic-based Subscriptions engine — rest-hook, websocket, email, and messaging channels. Configured via `HFS_SUBSCRIPTION(S)_*`. | | **`helios-cds-hooks`** | CDS Hooks protocol types and async service trait (HL7 CDS Hooks v3.0.0-ballot). Standalone library. | +| **`helios-web`** | HTMX-first web UI foundation for HFS. | | **`pysof`** | Python bindings (PyO3/maturin) for SQL-on-FHIR. Excluded from default workspace build. | ### Binaries @@ -39,6 +41,7 @@ The project is a Rust workspace with 17 crates (16 default-members; `pysof` excl | `sof-server` | helios-sof | SQL-on-FHIR HTTP server | | `config-advisor` | helios-persistence | Storage configuration advisor | | `hts` | helios-hts | FHIR Terminology Server (HTS) | +| `validator-cli` | helios-fhir-validator | FHIR resource validator CLI (`cli` feature) | ### Key Design Patterns diff --git a/crates/rest/src/config.rs b/crates/rest/src/config.rs index 2c06a3130..330e3027e 100644 --- a/crates/rest/src/config.rs +++ b/crates/rest/src/config.rs @@ -23,6 +23,15 @@ //! | `HFS_TENANT_STRICT_VALIDATION` | false | Error if URL and header tenant disagree | //! | `HFS_JWT_TENANT_CLAIM` | tenant_id | JWT claim name for tenant (future use) | //! | `HFS_TERMINOLOGY_SERVER` | (none) | HTS base URL for `:in`/`:not-in` search and FHIRPath terminology functions | +//! | `HFS_VALIDATION_MODE` | off | Write-path validation: off, log, or enforce (422 on invalid) | +//! | `HFS_VALIDATION_META_PROFILES` | true | Validate against `meta.profile` claims | +//! | `HFS_VALIDATION_UNKNOWN_PROFILE` | warn | Unresolvable profiles: warn, error, or ignore | +//! | `HFS_VALIDATION_CONSTRAINTS` | true | Evaluate FHIRPath invariants | +//! | `HFS_VALIDATION_SUPPRESS_CONSTRAINTS` | dom-6 | Comma-separated constraint ids to skip | +//! | `HFS_VALIDATION_TERMINOLOGY` | off | Required-binding checks: off or remote (`$validate-code` against `HFS_TERMINOLOGY_SERVER`) | +//! | `HFS_VALIDATION_TERMINOLOGY_TIMEOUT_MS` | 3000 | Per-check terminology timeout | +//! | `HFS_VALIDATION_TERMINOLOGY_FAIL` | open | Terminology outage posture: open (warn) or closed (error) | +//! | `HFS_VALIDATION_STORED_PROFILES` | true | Maintain per-tenant profile registries from stored StructureDefinitions | //! //! # Example //! @@ -426,6 +435,136 @@ impl BulkExportConfig { } } +/// Resource validation configuration, loaded from `HFS_VALIDATION_*` +/// environment variables. +/// +/// The `$validate` operation is always available; these settings gate the +/// **write-path** behavior (create/update/batch) and tune the shared +/// validation service. +#[derive(Debug, Clone)] +pub struct ValidationConfig { + /// Write-path behavior: `off` (skip), `log` (validate, log issues, + /// proceed), or `enforce` (reject invalid resources with `422`). + pub mode: String, + /// Validate against the profiles a resource claims in `meta.profile`. + pub meta_profiles: bool, + /// Unresolvable profile references: `warn`, `error`, or `ignore`. + pub unknown_profile: String, + /// Evaluate FHIRPath invariant constraints. + pub constraints: bool, + /// Constraint ids never evaluated (comma-separated in the env var). + pub suppress_constraints: Vec, + /// Terminology binding checking: `off` or `remote` + /// (`remote` uses `HFS_TERMINOLOGY_SERVER`'s `ValueSet/$validate-code`). + pub terminology: String, + /// Per-check terminology timeout, in milliseconds. + pub terminology_timeout_ms: u64, + /// Terminology outages: `open` (warn and proceed) or `closed` + /// (treat as validation errors). + pub terminology_fail: String, + /// Maintain per-tenant profile registries from stored + /// StructureDefinitions (updated on StructureDefinition writes). + pub stored_profiles: bool, +} + +impl Default for ValidationConfig { + fn default() -> Self { + Self { + mode: "off".to_string(), + meta_profiles: true, + unknown_profile: "warn".to_string(), + constraints: true, + suppress_constraints: vec!["dom-6".to_string()], + terminology: "off".to_string(), + terminology_timeout_ms: 3000, + terminology_fail: "open".to_string(), + stored_profiles: true, + } + } +} + +impl ValidationConfig { + /// Loads validation configuration from `HFS_VALIDATION_*` env vars. + pub fn from_env() -> Self { + fn env_bool(key: &str, default: bool) -> bool { + std::env::var(key) + .map(|s| { + let s = s.to_lowercase(); + s == "true" || s == "1" + }) + .unwrap_or(default) + } + fn env_u64(key: &str, default: u64) -> u64 { + std::env::var(key) + .ok() + .and_then(|s| s.parse().ok()) + .unwrap_or(default) + } + let d = Self::default(); + Self { + mode: std::env::var("HFS_VALIDATION_MODE").unwrap_or(d.mode), + meta_profiles: env_bool("HFS_VALIDATION_META_PROFILES", d.meta_profiles), + unknown_profile: std::env::var("HFS_VALIDATION_UNKNOWN_PROFILE") + .unwrap_or(d.unknown_profile), + constraints: env_bool("HFS_VALIDATION_CONSTRAINTS", d.constraints), + suppress_constraints: std::env::var("HFS_VALIDATION_SUPPRESS_CONSTRAINTS") + .map(|s| { + s.split(',') + .map(str::trim) + .filter(|s| !s.is_empty()) + .map(str::to_string) + .collect() + }) + .unwrap_or(d.suppress_constraints), + terminology: std::env::var("HFS_VALIDATION_TERMINOLOGY").unwrap_or(d.terminology), + terminology_timeout_ms: env_u64( + "HFS_VALIDATION_TERMINOLOGY_TIMEOUT_MS", + d.terminology_timeout_ms, + ), + terminology_fail: std::env::var("HFS_VALIDATION_TERMINOLOGY_FAIL") + .unwrap_or(d.terminology_fail), + stored_profiles: env_bool("HFS_VALIDATION_STORED_PROFILES", d.stored_profiles), + } + } + + /// Validates the validation configuration. + pub fn validate(&self) -> Result<(), Vec> { + let mut errors = Vec::new(); + if !matches!(self.mode.as_str(), "off" | "log" | "enforce") { + errors.push(format!( + "HFS_VALIDATION_MODE '{}' invalid (expected off|log|enforce)", + self.mode + )); + } + if !matches!(self.unknown_profile.as_str(), "warn" | "error" | "ignore") { + errors.push(format!( + "HFS_VALIDATION_UNKNOWN_PROFILE '{}' invalid (expected warn|error|ignore)", + self.unknown_profile + )); + } + if !matches!(self.terminology.as_str(), "off" | "remote") { + errors.push(format!( + "HFS_VALIDATION_TERMINOLOGY '{}' invalid (expected off|remote)", + self.terminology + )); + } + if !matches!(self.terminology_fail.as_str(), "open" | "closed") { + errors.push(format!( + "HFS_VALIDATION_TERMINOLOGY_FAIL '{}' invalid (expected open|closed)", + self.terminology_fail + )); + } + if self.terminology_timeout_ms == 0 { + errors.push("HFS_VALIDATION_TERMINOLOGY_TIMEOUT_MS must be > 0".to_string()); + } + if errors.is_empty() { + Ok(()) + } else { + Err(errors) + } + } +} + /// Bulk Data **Submit** (`$bulk-submit`) configuration, loaded from /// `HFS_BULK_SUBMIT_*` environment variables. /// @@ -849,6 +988,10 @@ pub struct ServerConfig { /// Bulk data submit configuration (loaded from environment variables). #[arg(skip)] pub bulk_submit: BulkSubmitConfig, + + /// Resource validation configuration (loaded from environment variables). + #[arg(skip)] + pub validation: ValidationConfig, } impl ServerConfig { @@ -903,6 +1046,7 @@ impl Default for ServerConfig { multitenancy: MultitenancyConfig::default(), bulk_export: BulkExportConfig::default(), bulk_submit: BulkSubmitConfig::default(), + validation: ValidationConfig::default(), } } } @@ -921,6 +1065,8 @@ impl ServerConfig { config.bulk_export = BulkExportConfig::from_env(); // Load bulk submit config from environment config.bulk_submit = BulkSubmitConfig::from_env(); + // Load validation config from environment + config.validation = ValidationConfig::from_env(); config } @@ -966,6 +1112,17 @@ impl ServerConfig { errors.append(&mut submit_errors); } + if let Err(mut validation_errors) = self.validation.validate() { + errors.append(&mut validation_errors); + } + + // Remote terminology checking needs a terminology server to call. + if self.validation.terminology == "remote" && self.terminology_server.is_none() { + errors.push( + "HFS_VALIDATION_TERMINOLOGY=remote requires HFS_TERMINOLOGY_SERVER".to_string(), + ); + } + if errors.is_empty() { Ok(()) } else { @@ -1021,6 +1178,7 @@ impl ServerConfig { multitenancy: MultitenancyConfig::default(), bulk_export: BulkExportConfig::default(), bulk_submit: BulkSubmitConfig::default(), + validation: ValidationConfig::default(), } } diff --git a/crates/rest/src/error.rs b/crates/rest/src/error.rs index 54d8bcaf3..6b6df3a66 100644 --- a/crates/rest/src/error.rs +++ b/crates/rest/src/error.rs @@ -116,6 +116,13 @@ pub enum RestError { message: String, }, + /// Resource validation failed in enforce mode (HTTP 422). Carries the + /// full multi-issue OperationOutcome from the validator. + ValidationFailed { + /// The OperationOutcome to return as the response body. + outcome: serde_json::Value, + }, + /// Unauthorized — missing or invalid authentication (HTTP 401). Unauthorized { /// Error message. @@ -231,6 +238,9 @@ impl fmt::Display for RestError { RestError::UnprocessableEntity { message } => { write!(f, "Unprocessable entity: {}", message) } + RestError::ValidationFailed { .. } => { + write!(f, "Resource validation failed") + } RestError::Unauthorized { message } => { write!(f, "Unauthorized: {}", message) } @@ -325,6 +335,10 @@ impl IntoResponse for RestError { "processing", message.clone(), ), + RestError::ValidationFailed { outcome } => { + return (StatusCode::UNPROCESSABLE_ENTITY, Json(outcome.clone())) + .into_response(); + } RestError::Unauthorized { message } => { let operation_outcome = create_operation_outcome("error", "login", message); return ( diff --git a/crates/rest/src/handlers/batch.rs b/crates/rest/src/handlers/batch.rs index 5292eb2b4..35eded0dc 100644 --- a/crates/rest/src/handlers/batch.rs +++ b/crates/rest/src/handlers/batch.rs @@ -227,6 +227,28 @@ where } } + // Write-path validation: transactions are atomic, so any invalid write + // entry rejects the whole bundle before anything executes. + for (index, entry, _) in &indexed_entries { + if !matches!(entry.method, BundleMethod::Post | BundleMethod::Put) { + continue; + } + let Some(resource) = &entry.resource else { + continue; + }; + let (resource_type, _) = parse_request_url(&entry.url) + .map_err(|e| RestError::BadRequest { message: format!("Entry {}: {}", index, e) })?; + state + .validation() + .check_write( + tenant.tenant_id(), + FhirVersion::default_enabled(), + &resource_type, + resource, + ) + .await?; + } + // Sort by processing order: DELETE -> POST -> PUT/PATCH -> GET indexed_entries.sort_by_key(|(_, entry, _)| method_processing_order(&entry.method)); @@ -248,6 +270,23 @@ where match result { Ok(bundle_result) => { + // Stored StructureDefinitions feed the tenant's profile + // registry. The request content is what was stored (modulo + // server-assigned id/meta, which the converter does not read). + for (_, entry, _) in &indexed_entries { + if matches!(entry.method, BundleMethod::Post | BundleMethod::Put) + && let Some(resource) = &entry.resource + && resource.get("resourceType").and_then(Value::as_str) + == Some("StructureDefinition") + { + state.validation().upsert_stored_profile( + tenant.tenant_id(), + FhirVersion::default_enabled(), + resource, + ); + } + } + // Reorder results back to original entry order let mut ordered_results: Vec<(usize, &BundleEntry, &BundleEntryResult)> = indexed_entries @@ -382,6 +421,20 @@ where } }; + // Write-path validation (per-entry outcome in batch semantics). + if let Err(e) = state + .validation() + .check_write( + tenant.tenant_id(), + FhirVersion::default_enabled(), + &resource_type, + &resource, + ) + .await + { + return create_error_result(422, &validation_failure_message(&e)); + } + // Use default FHIR version for batch operations match state .storage() @@ -393,7 +446,16 @@ where ) .await { - Ok(stored) => BundleEntryResult::created(stored), + Ok(stored) => { + if resource_type == "StructureDefinition" { + state.validation().upsert_stored_profile( + tenant.tenant_id(), + FhirVersion::default_enabled(), + stored.content(), + ); + } + BundleEntryResult::created(stored) + } Err(e) => create_error_result(400, &e.to_string()), } } @@ -406,6 +468,20 @@ where } }; + // Write-path validation (per-entry outcome in batch semantics). + if let Err(e) = state + .validation() + .check_write( + tenant.tenant_id(), + FhirVersion::default_enabled(), + &resource_type, + &resource, + ) + .await + { + return create_error_result(422, &validation_failure_message(&e)); + } + // Use default FHIR version for batch operations match state .storage() @@ -419,6 +495,13 @@ where .await { Ok((stored, created)) => { + if resource_type == "StructureDefinition" { + state.validation().upsert_stored_profile( + tenant.tenant_id(), + FhirVersion::default_enabled(), + stored.content(), + ); + } if created { BundleEntryResult::created(stored) } else { @@ -665,6 +748,33 @@ fn parse_request_url(url: &str) -> Result<(String, String), String> { } /// Creates an error BundleEntryResult. +/// Flatten an enforce-mode validation failure into a per-entry message +/// (batch entry outcomes are message-based). +fn validation_failure_message(error: &RestError) -> String { + if let RestError::ValidationFailed { outcome } = error { + let details: Vec = outcome + .get("issue") + .and_then(|i| i.as_array()) + .map(|issues| { + issues + .iter() + .filter_map(|issue| { + issue + .get("details") + .and_then(|d| d.get("text")) + .and_then(|t| t.as_str()) + .map(str::to_string) + }) + .collect() + }) + .unwrap_or_default(); + if !details.is_empty() { + return format!("Validation failed: {}", details.join("; ")); + } + } + format!("Validation failed: {error}") +} + fn create_error_result(status: u16, message: &str) -> BundleEntryResult { let outcome = serde_json::json!({ "resourceType": "OperationOutcome", diff --git a/crates/rest/src/handlers/create.rs b/crates/rest/src/handlers/create.rs index 088c30643..20f24138b 100644 --- a/crates/rest/src/handlers/create.rs +++ b/crates/rest/src/handlers/create.rs @@ -104,6 +104,12 @@ where }); } + // Write-path validation (HFS_VALIDATION_MODE: off | log | enforce). + state + .validation() + .check_write(tenant.tenant_id(), fhir_version, &resource_type, &resource) + .await?; + // Check for conditional create if let Some(search_params) = conditional.if_none_exist() { debug!(search_params = %search_params, "Processing conditional create"); @@ -122,6 +128,15 @@ where use helios_persistence::core::ConditionalCreateResult; return match result { ConditionalCreateResult::Created(stored) => { + // Stored StructureDefinitions feed the tenant's profile + // registry. + if resource_type == "StructureDefinition" { + state.validation().upsert_stored_profile( + tenant.tenant_id(), + fhir_version, + stored.content(), + ); + } let headers = ResourceHeaders::from_stored(&stored, &state); let location = format!("{}/{}/{}", state.base_url(), resource_type, stored.id()); @@ -192,6 +207,13 @@ where .create(tenant.context(), &resource_type, resource, fhir_version) .await?; + // Stored StructureDefinitions feed the tenant's profile registry. + if resource_type == "StructureDefinition" { + state + .validation() + .upsert_stored_profile(tenant.tenant_id(), fhir_version, stored.content()); + } + let headers = ResourceHeaders::from_stored(&stored, &state); let location = format!("{}/{}/{}", state.base_url(), resource_type, stored.id()); diff --git a/crates/rest/src/handlers/update.rs b/crates/rest/src/handlers/update.rs index bae778ec1..18b00527c 100644 --- a/crates/rest/src/handlers/update.rs +++ b/crates/rest/src/handlers/update.rs @@ -125,6 +125,12 @@ where }); } + // Write-path validation (HFS_VALIDATION_MODE: off | log | enforce). + state + .validation() + .check_write(tenant.tenant_id(), fhir_version, &resource_type, &resource) + .await?; + // Try to read existing resource for version check let existing = state .storage() @@ -169,6 +175,13 @@ where ) .await?; + // Stored StructureDefinitions feed the tenant's profile registry. + if resource_type == "StructureDefinition" { + state + .validation() + .upsert_stored_profile(tenant.tenant_id(), fhir_version, stored.content()); + } + let headers = ResourceHeaders::from_stored(&stored, &state); let status = if created { StatusCode::CREATED @@ -276,6 +289,12 @@ where } } + // Write-path validation (HFS_VALIDATION_MODE: off | log | enforce). + state + .validation() + .check_write(tenant.tenant_id(), fhir_version, &resource_type, &resource) + .await?; + let result = state .storage() .conditional_update( diff --git a/crates/rest/src/handlers/validate.rs b/crates/rest/src/handlers/validate.rs index 3f769dd7f..65147225c 100644 --- a/crates/rest/src/handlers/validate.rs +++ b/crates/rest/src/handlers/validate.rs @@ -122,10 +122,10 @@ fn unwrap_inputs(body: Option, query: &ValidateQuery) -> Result( fhir_version: helios_fhir::FhirVersion, inputs: ValidateInputs, resource_type: &str, + tenant_id: &str, req_headers: &HeaderMap, ) -> RestResult where @@ -170,19 +171,19 @@ where }; // The resource must be of the addressed type. - if let Some(body_type) = resource.get("resourceType").and_then(Value::as_str) { - if body_type != resource_type { - return Err(RestError::BadRequest { - message: format!( - "Resource type in body ({body_type}) does not match URL ({resource_type})" - ), - }); - } + if let Some(body_type) = resource.get("resourceType").and_then(Value::as_str) + && body_type != resource_type + { + return Err(RestError::BadRequest { + message: format!( + "Resource type in body ({body_type}) does not match URL ({resource_type})" + ), + }); } let issues = state .validation() - .validate_resource(fhir_version, &resource, inputs.profiles) + .validate_resource(fhir_version, &resource, inputs.profiles, Some(tenant_id)) .await; debug!( @@ -200,7 +201,7 @@ where pub async fn validate_type_handler( State(state): State>, Path(resource_type): Path, - _tenant: TenantExtractor, + tenant: TenantExtractor, version: FhirVersionExtractor, Query(query): Query, req_headers: HeaderMap, @@ -210,7 +211,15 @@ where S: ResourceStorage + Send + Sync, { let inputs = unwrap_inputs(Some(body), &query)?; - respond(&state, version.storage_version(), inputs, &resource_type, &req_headers).await + respond( + &state, + version.storage_version(), + inputs, + &resource_type, + tenant.tenant_id(), + &req_headers, + ) + .await } /// `GET [base]/[type]/[id]/$validate` — validate the stored resource. @@ -236,7 +245,15 @@ where let mut inputs = unwrap_inputs(None, &query)?; inputs.resource = Some(stored.content().clone()); - respond(&state, version.storage_version(), inputs, &resource_type, &req_headers).await + respond( + &state, + version.storage_version(), + inputs, + &resource_type, + tenant.tenant_id(), + &req_headers, + ) + .await } /// `POST [base]/[type]/[id]/$validate` — validate the body in the context @@ -244,7 +261,7 @@ where pub async fn validate_instance_post_handler( State(state): State>, Path((resource_type, _id)): Path<(String, String)>, - _tenant: TenantExtractor, + tenant: TenantExtractor, version: FhirVersionExtractor, Query(query): Query, req_headers: HeaderMap, @@ -254,5 +271,13 @@ where S: ResourceStorage + Send + Sync, { let inputs = unwrap_inputs(Some(body), &query)?; - respond(&state, version.storage_version(), inputs, &resource_type, &req_headers).await + respond( + &state, + version.storage_version(), + inputs, + &resource_type, + tenant.tenant_id(), + &req_headers, + ) + .await } diff --git a/crates/rest/src/state.rs b/crates/rest/src/state.rs index 5bb3b561b..079d65756 100644 --- a/crates/rest/src/state.rs +++ b/crates/rest/src/state.rs @@ -137,6 +137,10 @@ impl AppState { pub fn new(storage: Arc, config: ServerConfig) -> Self { let bulk_export_config = Arc::new(config.bulk_export.clone()); let bulk_submit_config = Arc::new(config.bulk_submit.clone()); + let validation = Arc::new(crate::validation::ValidationService::from_config( + &config.validation, + config.terminology_server.as_deref(), + )); Self { storage, config: Arc::new(config), @@ -157,7 +161,7 @@ impl AppState { bulk_submit_output: None, bulk_submit_file_auth: None, bulk_submit_config, - validation: Arc::new(crate::validation::ValidationService::new()), + validation, } } @@ -182,6 +186,10 @@ impl AppState { ) -> Self { let bulk_export_config = Arc::new(config.bulk_export.clone()); let bulk_submit_config = Arc::new(config.bulk_submit.clone()); + let validation = Arc::new(crate::validation::ValidationService::from_config( + &config.validation, + config.terminology_server.as_deref(), + )); Self { storage, config: Arc::new(config), @@ -202,7 +210,7 @@ impl AppState { bulk_submit_output: None, bulk_submit_file_auth: None, bulk_submit_config, - validation: Arc::new(crate::validation::ValidationService::new()), + validation, } } diff --git a/crates/rest/src/validation.rs b/crates/rest/src/validation.rs index 942d64bb0..066b00ee8 100644 --- a/crates/rest/src/validation.rs +++ b/crates/rest/src/validation.rs @@ -2,27 +2,55 @@ //! `helios-fhir-validator`. //! //! Owns the per-version core validators (embedded schema packs), the -//! FHIRPath constraint evaluator, the optional terminology provider, and +//! FHIRPath constraint evaluator, the optional terminology provider, +//! per-tenant profile registries fed from stored StructureDefinitions, and //! the mapping from validator issues onto FHIR `OperationOutcome` issues. -//! Used by the `$validate` operation (always available) and — when -//! `HFS_VALIDATION_MODE` enables it — by the write-path enforcement hooks. +//! +//! The `$validate` operation uses [`ValidationService::validate_resource`] +//! unconditionally; the write path (create/update/batch) goes through +//! [`ValidationService::check_write`], which is gated by +//! `HFS_VALIDATION_MODE` (`off` | `log` | `enforce`). +use crate::config::ValidationConfig; +use crate::error::RestError; use crate::responses::operation_outcome::{ Issue, IssueSeverity, IssueType, OperationOutcomeBuilder, }; +use async_trait::async_trait; +use dashmap::DashMap; use helios_fhir::FhirVersion; use helios_fhir_validator::fhirpath_effects::FhirPathConstraintEvaluator; use helios_fhir_validator::{ - dotted_to_fhirpath, EffectHandlers, ErrorKind, Severity, TerminologyProvider, - UnknownProfilePolicy, ValidationError, ValidationOptions, Validator, + dotted_to_fhirpath, CodedValue, CompositeResolver, EffectHandlers, ErrorKind, SchemaRegistry, + SchemaResolver, Severity, TerminologyError, TerminologyProvider, UnknownProfilePolicy, + ValidationError, ValidationOptions, Validator, }; -use serde_json::Value; -use std::sync::Arc; +use serde_json::{json, Value}; +use std::sync::{Arc, RwLock}; +use std::time::{Duration, Instant}; +use tracing::{debug, warn}; + +/// Per-tenant, per-version profile registries fed from stored +/// StructureDefinitions. +type TenantProfileMap = DashMap<(String, FhirVersion), Arc>>; + +/// Write-path behavior. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum ValidationMode { + /// Skip write-path validation entirely. + #[default] + Off, + /// Validate, log issues, and proceed. + Log, + /// Reject invalid resources with `422 Unprocessable Entity`. + Enforce, +} /// The validation service. Cheap to construct (core registries load lazily, /// once per process); hold one in `AppState` for the AST cache to pay off. pub struct ValidationService { - constraint_evaluator: FhirPathConstraintEvaluator, + mode: ValidationMode, + constraint_evaluator: Option, terminology: Option>, /// Constraint ids never evaluated (default: `dom-6`, the narrative /// warning that fires on almost every machine-generated resource). @@ -32,75 +60,331 @@ pub struct ValidationService { /// Validate against `meta.profile` claims. use_meta_profiles: bool, unknown_profile: UnknownProfilePolicy, + /// Per-tenant profile overlays, fed from stored StructureDefinition + /// writes. `None` disables the feature. + tenant_profiles: Option, } impl Default for ValidationService { fn default() -> Self { Self { - constraint_evaluator: FhirPathConstraintEvaluator::new(), + mode: ValidationMode::Off, + constraint_evaluator: Some(FhirPathConstraintEvaluator::new()), terminology: None, suppress_constraints: vec!["dom-6".to_string()], terminology_fail_closed: false, use_meta_profiles: true, unknown_profile: UnknownProfilePolicy::Warn, + tenant_profiles: Some(DashMap::new()), } } } impl ValidationService { - /// A service with the default posture: constraints on (dom-6 - /// suppressed), meta.profile honored, unresolvable profiles warned, - /// no terminology provider. + /// A service with the default posture: write path off, constraints on + /// (dom-6 suppressed), meta.profile honored, unresolvable profiles + /// warned, stored profiles on, no terminology provider. pub fn new() -> Self { Self::default() } + /// Build a service from `HFS_VALIDATION_*` configuration. + /// `terminology_server` is `HFS_TERMINOLOGY_SERVER` (required for + /// `terminology = remote`; the config validator enforces the pairing). + pub fn from_config(config: &ValidationConfig, terminology_server: Option<&str>) -> Self { + let mode = match config.mode.as_str() { + "log" => ValidationMode::Log, + "enforce" => ValidationMode::Enforce, + _ => ValidationMode::Off, + }; + let unknown_profile = match config.unknown_profile.as_str() { + "error" => UnknownProfilePolicy::Error, + "ignore" => UnknownProfilePolicy::Ignore, + _ => UnknownProfilePolicy::Warn, + }; + let terminology: Option> = + match (config.terminology.as_str(), terminology_server) { + ("remote", Some(base)) => Some(Arc::new(RemoteTerminologyProvider::new( + base.to_string(), + Duration::from_millis(config.terminology_timeout_ms), + ))), + _ => None, + }; + Self { + mode, + constraint_evaluator: config.constraints.then(FhirPathConstraintEvaluator::new), + terminology, + suppress_constraints: config.suppress_constraints.clone(), + terminology_fail_closed: config.terminology_fail == "closed", + use_meta_profiles: config.meta_profiles, + unknown_profile, + tenant_profiles: config.stored_profiles.then(DashMap::new), + } + } + /// Replace the terminology provider (bindings stay unchecked without one). pub fn with_terminology(mut self, provider: Arc) -> Self { self.terminology = Some(provider); self } - /// Escalate terminology-service failures from warnings to errors. - pub fn set_terminology_fail_closed(&mut self, closed: bool) { - self.terminology_fail_closed = closed; - } - - /// Replace the set of constraint ids that are never evaluated. - pub fn set_suppress_constraints(&mut self, ids: Vec) { - self.suppress_constraints = ids; - } - - /// Toggle validation against `meta.profile` claims. - pub fn set_use_meta_profiles(&mut self, on: bool) { - self.use_meta_profiles = on; + /// The configured write-path mode. + pub fn mode(&self) -> ValidationMode { + self.mode } - /// Validate a resource against the core pack for `version` plus any - /// extra profile canonicals. Structural issues first, then constraint - /// issues, then binding issues. + /// Validate a resource against the core pack for `version` (overlaid + /// with the tenant's stored profiles) plus any extra profile canonicals. + /// Structural issues first, then constraint issues, then binding issues. pub async fn validate_resource( &self, version: FhirVersion, resource: &Value, profiles: Vec, + tenant: Option<&str>, ) -> Vec { - let validator = Validator::new(helios_fhir_validator::packs::core_registry(version)); + let core = helios_fhir_validator::packs::core_registry(version); + let resolver: Arc = match self.tenant_overlay(tenant, version) { + Some(overlay) => Arc::new(CompositeResolver::new(vec![overlay, core])), + None => core, + }; + let validator = Validator::new(resolver); let opts = ValidationOptions { profiles, use_meta_profiles: self.use_meta_profiles, unknown_profile: self.unknown_profile, }; let handlers = EffectHandlers { - constraints: Some(&self.constraint_evaluator), + constraints: self + .constraint_evaluator + .as_ref() + .map(|e| e as &dyn helios_fhir_validator::ConstraintEvaluator), terminology: self.terminology.as_deref(), suppress_constraints: &self.suppress_constraints, terminology_fail_closed: self.terminology_fail_closed, }; validator.validate(resource, version, &opts, &handlers).await } + + /// Write-path gate. `Ok(())` = proceed with the write; `Err` = reject + /// (enforce mode with error-severity issues → `422` carrying the full + /// OperationOutcome). + pub async fn check_write( + &self, + tenant: &str, + version: FhirVersion, + resource_type: &str, + resource: &Value, + ) -> Result<(), RestError> { + if self.mode == ValidationMode::Off { + return Ok(()); + } + let issues = self + .validate_resource(version, resource, Vec::new(), Some(tenant)) + .await; + if issues.is_empty() { + return Ok(()); + } + match self.mode { + ValidationMode::Off => Ok(()), + ValidationMode::Log => { + for issue in &issues { + warn!( + tenant = %tenant, + resource_type = %resource_type, + path = %issue.path, + kind = ?issue.kind, + "validation (log mode): {}", + issue.message + ); + } + Ok(()) + } + ValidationMode::Enforce => { + if has_errors(&issues) { + debug!( + tenant = %tenant, + resource_type = %resource_type, + issues = issues.len(), + "rejecting write: validation failed" + ); + Err(RestError::ValidationFailed { outcome: validation_outcome(&issues) }) + } else { + // Warnings only: proceed, but leave a trace. + for issue in &issues { + warn!( + tenant = %tenant, + resource_type = %resource_type, + path = %issue.path, + "validation warning on write: {}", + issue.message + ); + } + Ok(()) + } + } + } + } + + /// Fold a stored StructureDefinition into the tenant's profile registry + /// (called after successful StructureDefinition writes). Conversion + /// failures are logged, never fatal — the write itself already + /// succeeded. + pub fn upsert_stored_profile(&self, tenant: &str, version: FhirVersion, sd: &Value) { + let Some(registries) = &self.tenant_profiles else { + return; + }; + match helios_fhir_validator::converter::convert(sd) { + Ok(conversion) => { + for w in &conversion.warnings { + warn!(tenant = %tenant, "profile conversion warning: {w}"); + } + let registry = registries + .entry((tenant.to_string(), version)) + .or_insert_with(|| Arc::new(RwLock::new(SchemaRegistry::new()))) + .clone(); + let inserted = registry + .write() + .expect("tenant profile registry lock") + .insert(conversion.schema); + if inserted { + debug!(tenant = %tenant, url = ?sd.get("url"), "tenant profile registered"); + } else { + warn!(tenant = %tenant, "stored StructureDefinition has neither url nor name; not registered"); + } + } + Err(e) => { + warn!(tenant = %tenant, "stored StructureDefinition failed to convert: {e}"); + } + } + } + + fn tenant_overlay( + &self, + tenant: Option<&str>, + version: FhirVersion, + ) -> Option> { + let registries = self.tenant_profiles.as_ref()?; + let tenant = tenant?; + let registry = registries.get(&(tenant.to_string(), version))?.clone(); + Some(Arc::new(LockedRegistryResolver(registry))) + } +} + +/// Resolver adapter over a shared, mutable registry. +struct LockedRegistryResolver(Arc>); + +impl SchemaResolver for LockedRegistryResolver { + fn resolve(&self, reference: &str) -> Option> { + self.0.read().expect("tenant profile registry lock").resolve(reference) + } +} + +// --------------------------------------------------------------------- +// Remote terminology provider +// --------------------------------------------------------------------- + +/// `TerminologyProvider` backed by a FHIR terminology server's +/// `ValueSet/$validate-code`, with a small in-memory TTL cache (neither the +/// fhirpath nor the search terminology clients cache). +pub struct RemoteTerminologyProvider { + base_url: String, + client: reqwest::Client, + /// `(valueSet, coded-token)` → validity, cached for [`CACHE_TTL`]. + cache: DashMap, +} + +/// How long `$validate-code` verdicts are cached. +const CACHE_TTL: Duration = Duration::from_secs(300); + +impl RemoteTerminologyProvider { + /// `base_url` is the terminology server root (e.g. `http://hts:8090`). + pub fn new(base_url: String, timeout: Duration) -> Self { + Self { + base_url: base_url.trim_end_matches('/').to_string(), + client: reqwest::Client::builder() + .timeout(timeout) + .build() + .expect("reqwest client builds"), + cache: DashMap::new(), + } + } + + fn payload(value_set: &str, coded: &CodedValue) -> Value { + let mut parameter = vec![json!({ "name": "url", "valueUri": value_set })]; + match coded { + CodedValue::Code(code) => { + parameter.push(json!({ "name": "inferSystem", "valueBoolean": true })); + parameter.push(json!({ "name": "code", "valueCode": code })); + } + CodedValue::Coding(coding) => { + parameter.push(json!({ "name": "coding", "valueCoding": coding })); + } + CodedValue::CodeableConcept(concept) => { + parameter + .push(json!({ "name": "codeableConcept", "valueCodeableConcept": concept })); + } + } + json!({ "resourceType": "Parameters", "parameter": parameter }) + } +} + +#[async_trait] +impl TerminologyProvider for RemoteTerminologyProvider { + async fn validate_code( + &self, + value_set: &str, + coded: &CodedValue, + ) -> Result { + let key = format!("{value_set}|{coded:?}"); + if let Some(entry) = self.cache.get(&key) { + let (verdict, at) = *entry; + if at.elapsed() < CACHE_TTL { + return Ok(verdict); + } + } + + let url = format!("{}/ValueSet/$validate-code", self.base_url); + let response = self + .client + .post(&url) + .json(&Self::payload(value_set, coded)) + .send() + .await + .map_err(|e| TerminologyError(format!("request to {url} failed: {e}")))?; + if !response.status().is_success() { + return Err(TerminologyError(format!( + "{url} returned {}", + response.status() + ))); + } + let body: Value = response + .json() + .await + .map_err(|e| TerminologyError(format!("invalid $validate-code response: {e}")))?; + let verdict = body + .get("parameter") + .and_then(Value::as_array) + .and_then(|params| { + params + .iter() + .find(|p| p.get("name").and_then(Value::as_str) == Some("result")) + }) + .and_then(|p| p.get("valueBoolean")) + .and_then(Value::as_bool) + .ok_or_else(|| { + TerminologyError("no boolean 'result' parameter in response".to_string()) + })?; + + self.cache.insert(key, (verdict, Instant::now())); + Ok(verdict) + } } +// --------------------------------------------------------------------- +// OperationOutcome mapping +// --------------------------------------------------------------------- + /// Map one validator issue onto an OperationOutcome issue. pub fn to_outcome_issue(error: &ValidationError) -> Issue { let code = match error.kind { diff --git a/crates/rest/tests/validation_enforcement_tests.rs b/crates/rest/tests/validation_enforcement_tests.rs new file mode 100644 index 000000000..1d74acb90 --- /dev/null +++ b/crates/rest/tests/validation_enforcement_tests.rs @@ -0,0 +1,249 @@ +//! Write-path validation enforcement (`HFS_VALIDATION_MODE`) and +//! stored-profile (tenant StructureDefinition registry) integration tests. + +use std::path::PathBuf; +use std::sync::Arc; + +use axum_test::TestServer; +use helios_persistence::backends::sqlite::{SqliteBackend, SqliteBackendConfig}; +use helios_rest::config::ValidationConfig; +use helios_rest::{MultitenancyConfig, ServerConfig, TenantRoutingMode}; +use serde_json::{json, Value}; + +async fn create_test_server(mode: &str) -> TestServer { + let data_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .parent() + .and_then(|p| p.parent()) + .map(|p| p.join("data")) + .unwrap_or_else(|| PathBuf::from("data")); + + let backend_config = SqliteBackendConfig { data_dir: Some(data_dir), ..Default::default() }; + let backend = SqliteBackend::with_config(":memory:", backend_config) + .expect("Failed to create SQLite backend"); + backend.init_schema().expect("Failed to init schema"); + let backend = Arc::new(backend); + + let config = ServerConfig { + multitenancy: MultitenancyConfig { + routing_mode: TenantRoutingMode::HeaderOnly, + ..Default::default() + }, + base_url: "http://localhost:8080".to_string(), + default_tenant: "test-tenant".to_string(), + validation: ValidationConfig { mode: mode.to_string(), ..Default::default() }, + ..ServerConfig::for_testing() + }; + + let state = helios_rest::AppState::new(backend, config); + let app = helios_rest::routing::fhir_routes::create_routes(state); + TestServer::new(app).expect("Failed to create test server") +} + +fn invalid_patient() -> Value { + json!({ "resourceType": "Patient", "bogusElement": true }) +} + +fn valid_patient() -> Value { + json!({ "resourceType": "Patient", "active": true }) +} + +#[tokio::test] +async fn enforce_mode_rejects_invalid_writes_with_outcome() { + let server = create_test_server("enforce").await; + + let response = server.post("/Patient").json(&invalid_patient()).await; + response.assert_status_unprocessable_entity(); + let outcome: Value = response.json(); + assert_eq!(outcome["resourceType"], "OperationOutcome", "{outcome:#}"); + assert!( + outcome["issue"] + .as_array() + .unwrap() + .iter() + .any(|i| i["code"] == "structure" && i["expression"][0] == "Patient.bogusElement"), + "expected the structural issue in the 422 body: {outcome:#}" + ); + + // Updates are enforced too. + let response = server + .put("/Patient/p1") + .json(&json!({ "resourceType": "Patient", "id": "p1", "bogusElement": true })) + .await; + response.assert_status_unprocessable_entity(); +} + +#[tokio::test] +async fn enforce_mode_allows_valid_writes() { + let server = create_test_server("enforce").await; + let response = server.post("/Patient").json(&valid_patient()).await; + assert_eq!(response.status_code(), 201, "{}", response.text()); +} + +#[tokio::test] +async fn log_and_off_modes_do_not_reject() { + for mode in ["log", "off"] { + let server = create_test_server(mode).await; + let response = server.post("/Patient").json(&invalid_patient()).await; + assert_eq!( + response.status_code(), + 201, + "mode={mode} must not reject: {}", + response.text() + ); + } +} + +#[tokio::test] +async fn enforce_mode_rejects_invalid_batch_entries_individually() { + let server = create_test_server("enforce").await; + let response = server + .post("/") + .json(&json!({ + "resourceType": "Bundle", + "type": "batch", + "entry": [ + { + "request": { "method": "POST", "url": "Patient" }, + "resource": invalid_patient() + }, + { + "request": { "method": "POST", "url": "Patient" }, + "resource": valid_patient() + } + ] + })) + .await; + response.assert_status_ok(); + let bundle: Value = response.json(); + let entries = bundle["entry"].as_array().expect("entries"); + assert_eq!(entries.len(), 2); + assert!( + entries[0]["response"]["status"] + .as_str() + .unwrap_or_default() + .starts_with("422"), + "first entry must fail validation: {bundle:#}" + ); + assert!( + entries[1]["response"]["status"] + .as_str() + .unwrap_or_default() + .starts_with("201"), + "second entry must succeed: {bundle:#}" + ); +} + +#[tokio::test] +async fn stored_profile_registers_and_validates() { + let server = create_test_server("off").await; + + // Upload a differential profile constraining Patient: birthDate 1..1, + // gender prohibited. + let profile = json!({ + "resourceType": "StructureDefinition", + "id": "strict-patient", + "url": "http://example.org/StructureDefinition/strict-patient", + "name": "StrictPatient", + "status": "active", + "kind": "resource", + "abstract": false, + "type": "Patient", + "baseDefinition": "http://hl7.org/fhir/StructureDefinition/Patient", + "derivation": "constraint", + "differential": { + "element": [ + { "id": "Patient", "path": "Patient" }, + { "id": "Patient.birthDate", "path": "Patient.birthDate", "min": 1 }, + { "id": "Patient.gender", "path": "Patient.gender", "max": "0" } + ] + } + }); + let response = server.put("/StructureDefinition/strict-patient").json(&profile).await; + assert!( + response.status_code() == 201 || response.status_code() == 200, + "profile upload failed: {}", + response.text() + ); + + // $validate against the stored profile: violations reported. + let response = server + .post("/Patient/$validate?profile=http://example.org/StructureDefinition/strict-patient") + .json(&json!({ "resourceType": "Patient", "gender": "male" })) + .await; + response.assert_status_ok(); + let outcome: Value = response.json(); + let issues = outcome["issue"].as_array().expect("issues"); + assert!( + issues.iter().any(|i| i["code"] == "required" + && i["expression"][0] == "Patient.birthDate"), + "profile-required birthDate must be reported: {outcome:#}" + ); + assert!( + issues.iter().any(|i| i["code"] == "structure" + && i["expression"][0] == "Patient.gender"), + "profile-excluded gender must be reported: {outcome:#}" + ); + + // A conforming resource passes. + let response = server + .post("/Patient/$validate?profile=http://example.org/StructureDefinition/strict-patient") + .json(&json!({ "resourceType": "Patient", "birthDate": "1980-01-01" })) + .await; + response.assert_status_ok(); + let outcome: Value = response.json(); + assert_eq!( + outcome["issue"][0]["severity"], "information", + "conforming resource must validate clean: {outcome:#}" + ); +} + +#[tokio::test] +async fn enforce_mode_honors_meta_profile_claims() { + let server = create_test_server("enforce").await; + + let profile = json!({ + "resourceType": "StructureDefinition", + "id": "must-have-birthdate", + "url": "http://example.org/StructureDefinition/must-have-birthdate", + "name": "MustHaveBirthDate", + "status": "active", + "kind": "resource", + "abstract": false, + "type": "Patient", + "baseDefinition": "http://hl7.org/fhir/StructureDefinition/Patient", + "derivation": "constraint", + "differential": { + "element": [ + { "id": "Patient", "path": "Patient" }, + { "id": "Patient.birthDate", "path": "Patient.birthDate", "min": 1 } + ] + } + }); + let response = server.put("/StructureDefinition/must-have-birthdate").json(&profile).await; + assert!( + response.status_code().is_success(), + "profile upload failed: {}", + response.text() + ); + + // A write claiming the profile but violating it is rejected. + let response = server + .post("/Patient") + .json(&json!({ + "resourceType": "Patient", + "meta": { "profile": ["http://example.org/StructureDefinition/must-have-birthdate"] } + })) + .await; + response.assert_status_unprocessable_entity(); + + // Satisfying the claimed profile passes. + let response = server + .post("/Patient") + .json(&json!({ + "resourceType": "Patient", + "meta": { "profile": ["http://example.org/StructureDefinition/must-have-birthdate"] }, + "birthDate": "1980-01-01" + })) + .await; + assert_eq!(response.status_code(), 201, "{}", response.text()); +} From bb09ace11420fc702007ec83ac9749158ab0e1b6 Mon Sep 17 00:00:00 2001 From: Doug Date: Sun, 5 Jul 2026 22:08:56 -0400 Subject: [PATCH 7/7] test(validator): fuzz-lite robustness sweep, all-version pack tests, latency smoke MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 7 (quick wins; remaining hardening documented in lib.rs): - fuzz_lite: 2500 deterministic mutations of a rich Patient plus pure-junk roots — the walk never panics on arbitrary JSON. - pack_smoke: every enabled version's embedded pack (R4/R4B/R5/R6) loads, resolves core schemas, validates a Patient, and catches unknown elements. - Latency smoke: ~300µs per typical Patient structural validation in DEBUG builds — the 5ms release-plan target is beaten by an order of magnitude. - lib.rs 'Current limitations' section pins the hardening backlog: type/profile/binding/resolve-ref slice matchers, refers enforcement, extensible-binding warnings, schema-set memoization, hts in-process terminology adapter, core extension-definitions vendoring. Co-Authored-By: Claude Opus 4.8 (1M context) --- crates/fhir-validator/src/converter/mod.rs | 9 +- crates/fhir-validator/src/lib.rs | 20 +++ crates/fhir-validator/tests/fuzz_lite.rs | 139 +++++++++++++++++++++ crates/fhir-validator/tests/pack_smoke.rs | 79 +++++++++++- 4 files changed, 239 insertions(+), 8 deletions(-) create mode 100644 crates/fhir-validator/tests/fuzz_lite.rs diff --git a/crates/fhir-validator/src/converter/mod.rs b/crates/fhir-validator/src/converter/mod.rs index 74fc9c477..7190afcb6 100644 --- a/crates/fhir-validator/src/converter/mod.rs +++ b/crates/fhir-validator/src/converter/mod.rs @@ -169,15 +169,14 @@ impl EdType { pub(crate) fn effective_code(&self) -> String { if let Some(suffix) = self.code.strip_prefix("http://hl7.org/fhirpath/System.") { for ext in &self.extension { - if ext.url == FHIR_TYPE_EXTENSION { - if let Some(v) = ext + if ext.url == FHIR_TYPE_EXTENSION + && let Some(v) = ext .value_url .as_deref() .or(ext.value_uri.as_deref()) .or(ext.value_string.as_deref()) - { - return v.to_string(); - } + { + return v.to_string(); } } let mut chars = suffix.chars(); diff --git a/crates/fhir-validator/src/lib.rs b/crates/fhir-validator/src/lib.rs index ca98054ae..d37d4d206 100644 --- a/crates/fhir-validator/src/lib.rs +++ b/crates/fhir-validator/src/lib.rs @@ -40,6 +40,26 @@ //! The behavioral contract is the vendored FHIR Schema conformance suite in //! `tests/fixtures/upstream/` (exact ordered error matching), plus Helios //! extended fixtures in `tests/fixtures/extended/`. +//! +//! ## Current limitations (hardening backlog) +//! +//! - Slice matchers: only `pattern` matching is evaluated. `type`, +//! `profile`, `binding`, and `resolve-ref` matchers are parsed but inert +//! (such slices match nothing and never enforce a minimum; the converter +//! emits a warning when it cannot build a pattern match). +//! - `refers` (reference target types) is carried but not enforced. +//! - `extensible`-strength bindings are never checked (only `required`, +//! per the FHIR Schema spec); a warning mode may come later. +//! - Constraint evaluation resolves `%resource`/`%rootResource` to the root +//! resource and evaluates via `path.all(expr)`, so invariants relying on +//! nested-resource `%resource` semantics can misfire (helios-fhirpath +//! limitation; see `fhirpath_effects`). +//! - Schema sets are assembled per node without cross-resource memoization; +//! structural validation of a typical Patient measures ~300µs in debug +//! builds (see `tests/pack_smoke.rs`), so this has not been worth it yet. +//! - Core extension definitions (`extension-definitions.json`) are not in +//! the vendored spec bundles, so pack profiles whose `extensions` sugar +//! references core extension URLs report `unknown-schema` when exercised. pub mod converter; pub mod effects; diff --git a/crates/fhir-validator/tests/fuzz_lite.rs b/crates/fhir-validator/tests/fuzz_lite.rs new file mode 100644 index 000000000..596af8379 --- /dev/null +++ b/crates/fhir-validator/tests/fuzz_lite.rs @@ -0,0 +1,139 @@ +//! Fuzz-lite robustness sweep: the validator must never panic, whatever +//! JSON it is fed. Deterministic (seeded xorshift, no clock/rand deps) so +//! failures reproduce. + +#![cfg(feature = "R4")] + +use helios_fhir::FhirVersion; +use helios_fhir_validator::packs::core_registry; +use helios_fhir_validator::{ValidationOptions, Validator}; +use serde_json::{json, Value}; + +/// Tiny deterministic PRNG (xorshift64*). +struct Rng(u64); + +impl Rng { + fn next(&mut self) -> u64 { + let mut x = self.0; + x ^= x >> 12; + x ^= x << 25; + x ^= x >> 27; + self.0 = x; + x.wrapping_mul(0x2545F4914F6CDD1D) + } + + fn below(&mut self, n: usize) -> usize { + (self.next() % n as u64) as usize + } +} + +/// A pool of pathological values to splice into resources. +fn junk(rng: &mut Rng) -> Value { + match rng.below(12) { + 0 => Value::Null, + 1 => json!(true), + 2 => json!(-1), + 3 => json!(1e308), + 4 => json!(""), + 5 => json!("\u{0}\u{ffff}"), + 6 => json!([]), + 7 => json!({}), + 8 => json!([null, [null, [null]]]), + 9 => json!({"resourceType": null}), + 10 => json!({"resourceType": "Patient", "resourceType2": {"a": [{}]}}), + _ => json!({"_": {"__": {"___": []}}}), + } +} + +/// Mutate a node in place: replace, delete a key, insert junk, or recurse. +fn mutate(value: &mut Value, rng: &mut Rng, depth: usize) { + if depth > 6 { + return; + } + match rng.below(5) { + 0 => *value = junk(rng), + 1 => { + if let Some(obj) = value.as_object_mut() { + if let Some(key) = obj.keys().nth(rng.below(obj.len().max(1))).cloned() { + obj.remove(&key); + } + } else { + *value = junk(rng); + } + } + 2 => { + if let Some(obj) = value.as_object_mut() { + let j = junk(rng); + let names = ["x", "_x", "extension", "value", "resourceType", "contained", "9"]; + obj.insert(names[rng.below(names.len())].to_string(), j); + } else if let Some(arr) = value.as_array_mut() { + let j = junk(rng); + arr.push(j); + } + } + _ => { + let target = match value { + Value::Object(obj) if !obj.is_empty() => { + let idx = rng.below(obj.len()); + obj.values_mut().nth(idx) + } + Value::Array(arr) if !arr.is_empty() => { + let idx = rng.below(arr.len()); + arr.get_mut(idx) + } + _ => None, + }; + match target { + Some(inner) => mutate(inner, rng, depth + 1), + None => *value = junk(rng), + } + } + } +} + +fn seed_patient() -> Value { + json!({ + "resourceType": "Patient", + "id": "fuzz", + "meta": { "profile": ["http://example.org/nope"] }, + "extension": [{ "url": "http://x", "valueString": "v" }], + "identifier": [{ "system": "http://example.org", "value": "1" }], + "active": true, + "name": [{ "family": "F", "given": ["G", null] }], + "_birthDate": { "id": "b" }, + "birthDate": "1980-01-01", + "deceasedBoolean": false, + "contained": [{ "resourceType": "Organization", "id": "o", "name": "N" }], + "link": [{ "other": { "reference": "#o" }, "type": "seealso" }] + }) +} + +#[test] +fn never_panics_on_mutated_resources() { + let validator = Validator::new(core_registry(FhirVersion::R4)); + let opts = ValidationOptions::default(); + let mut rng = Rng(0x5EED_CAFE_F00D_D00D); + + for round in 0..2000 { + let mut resource = seed_patient(); + // Escalate mutation aggressiveness with the round number. + for _ in 0..(1 + round % 7) { + mutate(&mut resource, &mut rng, 0); + } + // Must return (errors are fine, panics are not). + let _ = validator.validate_sync(&resource, &opts); + } +} + +#[test] +fn never_panics_on_pure_junk_roots() { + let validator = Validator::new(core_registry(FhirVersion::R4)); + let opts = ValidationOptions::default(); + let mut rng = Rng(0xBAD_5EED); + + for _ in 0..500 { + let mut root = junk(&mut rng); + mutate(&mut root, &mut rng, 0); + let _ = validator.validate_sync(&root, &opts); + } +} diff --git a/crates/fhir-validator/tests/pack_smoke.rs b/crates/fhir-validator/tests/pack_smoke.rs index 46da2314f..7dbb786f7 100644 --- a/crates/fhir-validator/tests/pack_smoke.rs +++ b/crates/fhir-validator/tests/pack_smoke.rs @@ -1,6 +1,7 @@ -//! Whole-spec smoke tests over the embedded R4 pack. `#[ignore]`d in normal -//! runs (they parse the full pack); run explicitly with -//! `cargo test -p helios-fhir-validator -- --ignored`. +//! Whole-spec smoke tests over the embedded packs. `#[ignore]`d in normal +//! runs (they parse full packs); run explicitly with +//! `cargo test -p helios-fhir-validator -- --ignored` +//! (add `--features R4B,R5,R6` for the other-version sweeps). #![cfg(feature = "R4")] @@ -9,6 +10,78 @@ use helios_fhir_validator::packs::core_registry; use helios_fhir_validator::{SchemaResolver, ValidationOptions, Validator}; use serde_json::json; +/// Every enabled version's pack loads and validates a minimal Patient. +#[test] +#[ignore = "whole-pack parse; run with -- --ignored"] +fn all_enabled_packs_load_and_validate() { + let versions = [ + FhirVersion::R4, + #[cfg(feature = "R4B")] + FhirVersion::R4B, + #[cfg(feature = "R5")] + FhirVersion::R5, + #[cfg(feature = "R6")] + FhirVersion::R6, + ]; + for version in versions { + let registry = core_registry(version); + for name in ["Patient", "Observation", "Bundle", "Resource", "Element", "string"] { + assert!( + registry.resolve(name).is_some(), + "{version:?}: core schema '{name}' must resolve" + ); + } + let validator = Validator::new(registry); + let outcome = validator.validate_sync( + &json!({ "resourceType": "Patient", "active": true }), + &ValidationOptions::default(), + ); + assert_eq!(outcome.errors, vec![], "{version:?}: minimal Patient must be clean"); + let outcome = validator.validate_sync( + &json!({ "resourceType": "Patient", "bogus": 1 }), + &ValidationOptions::default(), + ); + assert!(!outcome.errors.is_empty(), "{version:?}: unknown element must be caught"); + } +} + +/// Rough structural-validation latency check (debug builds are far slower +/// than release; the plan's <5ms target refers to release). +#[test] +#[ignore = "timing; run with -- --ignored"] +fn structural_validation_latency_smoke() { + let validator = Validator::new(core_registry(FhirVersion::R4)); + let opts = ValidationOptions::default(); + let patient = json!({ + "resourceType": "Patient", + "id": "perf", + "extension": [{ "url": "http://x", "valueString": "v" }], + "identifier": [{ "system": "http://example.org/mrn", "value": "12345" }], + "active": true, + "name": [{ "use": "official", "family": "Chalmers", "given": ["Peter", "James"] }], + "gender": "male", + "birthDate": "1974-12-25", + "deceasedBoolean": false, + "contained": [{ "resourceType": "Organization", "id": "org1", "name": "ACME" }], + "managingOrganization": { "reference": "#org1" } + }); + + // Warm the lazy pack parse before timing. + let _ = validator.validate_sync(&patient, &opts); + + let iterations = 200u32; + let start = std::time::Instant::now(); + for _ in 0..iterations { + let outcome = validator.validate_sync(&patient, &opts); + assert!(outcome.errors.is_empty()); + } + let per_run = start.elapsed() / iterations; + println!("structural validation: {per_run:?} per typical Patient"); + // Generous ceiling so debug builds pass; release comfortably beats the + // 5ms plan target (verified manually). + assert!(per_run.as_millis() < 50, "structural validation too slow: {per_run:?}"); +} + #[test] #[ignore = "whole-pack parse; run with -- --ignored"] fn r4_pack_loads_and_resolves_core_schemas() {