tlq_fhir_validator/
validator.rs

1use crate::{ValidationPlan, ConfigError};
2use tlq_fhir_context::FhirContext;
3use tlq_fhirpath::Engine as FhirPathEngine;
4use tlq_fhir_snapshot::ExpandedFhirContext;
5use serde_json::Value;
6use std::sync::Arc;
7
8/// Reusable validator - owns plan, context, and FHIRPath engine
9pub struct Validator<C: FhirContext> {
10    plan: ValidationPlan,
11    context: Arc<C>,
12    fhirpath_engine: Arc<FhirPathEngine>,
13}
14
15impl<C: FhirContext + 'static> Validator<C> {
16    pub fn new(plan: ValidationPlan, context: C) -> Self {
17        let context = Arc::new(context);
18
19        // Create FHIRPath engine sharing the same context for discriminator evaluation
20        let fhirpath_engine = Arc::new(FhirPathEngine::new(
21            context.clone() as Arc<dyn FhirContext>,
22            None,
23        ));
24
25        Self {
26            plan,
27            context,
28            fhirpath_engine,
29        }
30    }
31
32    pub fn from_config(
33        config: &crate::ValidatorConfig,
34        context: C,
35    ) -> Result<Self, ConfigError> {
36        let plan = config.compile()?;
37        Ok(Self::new(plan, context))
38    }
39
40    /// Wrap the current context with an [`ExpandedFhirContext`], which:
41    /// - materializes snapshots from differentials (via `baseDefinition`)
42    /// - deep-expands snapshots for nested type validation
43    /// - caches expanded StructureDefinitions across validation runs
44    pub fn with_expanded_snapshots(self) -> Validator<ExpandedFhirContext<C>>
45    where
46        C: Clone,
47    {
48        // Extract inner context from Arc
49        let inner_context = Arc::try_unwrap(self.context)
50            .unwrap_or_else(|arc| (*arc).clone());
51        let expanded_context = ExpandedFhirContext::new(inner_context);
52        let expanded_arc = Arc::new(expanded_context);
53
54        // Create new engine for the expanded context
55        let fhirpath_engine = Arc::new(FhirPathEngine::new(
56            expanded_arc.clone() as Arc<dyn FhirContext>,
57            None,
58        ));
59
60        Validator {
61            plan: self.plan,
62            context: expanded_arc,
63            fhirpath_engine,
64        }
65    }
66
67    pub fn validate(&self, resource: &Value) -> ValidationOutcome {
68        ValidationRun::new(&self.plan, &self.context, &self.fhirpath_engine, resource).execute()
69    }
70
71    pub fn validate_batch(&self, resources: &[Value]) -> Vec<ValidationOutcome> {
72        resources.iter().map(|r| self.validate(r)).collect()
73    }
74
75    pub fn plan(&self) -> &ValidationPlan {
76        &self.plan
77    }
78
79    pub fn context(&self) -> &Arc<C> {
80        &self.context
81    }
82}
83
84/// Short-lived validation execution
85struct ValidationRun<'a, C: FhirContext> {
86    plan: &'a ValidationPlan,
87    context: &'a Arc<C>,
88    fhirpath_engine: &'a Arc<FhirPathEngine>,
89    resource: &'a Value,
90    issues: Vec<ValidationIssue>,
91}
92
93impl<'a, C: FhirContext> ValidationRun<'a, C> {
94    fn new(plan: &'a ValidationPlan, context: &'a Arc<C>, fhirpath_engine: &'a Arc<FhirPathEngine>, resource: &'a Value) -> Self {
95        Self {
96            plan,
97            context,
98            fhirpath_engine,
99            resource,
100            issues: Vec::new(),
101        }
102    }
103
104    fn execute(mut self) -> ValidationOutcome {
105        for step in &self.plan.steps {
106            if self.plan.fail_fast && self.has_errors() {
107                break;
108            }
109
110            if self.issues.len() >= self.plan.max_issues {
111                break;
112            }
113
114            self.execute_step(step);
115        }
116
117        ValidationOutcome {
118            resource_type: self.get_resource_type(),
119            valid: !self.has_errors(),
120            issues: self.issues,
121        }
122    }
123
124    fn execute_step(&mut self, step: &crate::Step) {
125        use crate::Step;
126
127        match step {
128            Step::Schema(plan) => self.validate_schema(plan),
129            Step::Profiles(plan) => self.validate_profiles(plan),
130            Step::Constraints(plan) => self.validate_constraints(plan),
131            Step::Terminology(plan) => self.validate_terminology(plan),
132            Step::References(plan) => self.validate_references(plan),
133            Step::Bundles(plan) => self.validate_bundles(plan),
134        }
135    }
136
137    fn validate_schema(&mut self, plan: &crate::SchemaPlan) {
138        crate::steps::schema::validate_schema(self.resource, plan, self.context.as_ref(), &mut self.issues);
139    }
140
141    fn validate_profiles(&mut self, plan: &crate::ProfilesPlan) {
142        crate::steps::profiles::validate_profiles(
143            self.resource,
144            plan,
145            self.context.as_ref(),
146            self.fhirpath_engine,
147            &mut self.issues
148        );
149    }
150
151    fn validate_constraints(&mut self, plan: &crate::ConstraintsPlan) {
152        crate::steps::constraints::validate_constraints(
153            self.resource,
154            plan,
155            self.context.as_ref(),
156            self.fhirpath_engine,
157            &mut self.issues
158        );
159    }
160
161    fn validate_terminology(&mut self, _plan: &crate::TerminologyPlan) {
162        // TODO: Implement terminology validation
163    }
164
165    fn validate_references(&mut self, _plan: &crate::ReferencesPlan) {
166        // TODO: Implement reference validation
167    }
168
169    fn validate_bundles(&mut self, _plan: &crate::BundlePlan) {
170        // TODO: Implement bundle validation
171    }
172
173    fn has_errors(&self) -> bool {
174        self.issues.iter().any(|i| i.severity == IssueSeverity::Error || i.severity == IssueSeverity::Fatal)
175    }
176
177    fn get_resource_type(&self) -> Option<String> {
178        self.resource
179            .get("resourceType")
180            .and_then(|v| v.as_str())
181            .map(|s| s.to_string())
182    }
183}
184
185/// Validation result for a single resource
186#[derive(Debug, Clone)]
187pub struct ValidationOutcome {
188    pub resource_type: Option<String>,
189    pub valid: bool,
190    pub issues: Vec<ValidationIssue>,
191}
192
193impl ValidationOutcome {
194    pub fn success(resource_type: Option<String>) -> Self {
195        Self {
196            resource_type,
197            valid: true,
198            issues: Vec::new(),
199        }
200    }
201
202    pub fn has_errors(&self) -> bool {
203        !self.valid
204    }
205
206    pub fn error_count(&self) -> usize {
207        self.issues.iter().filter(|i| i.severity == IssueSeverity::Error || i.severity == IssueSeverity::Fatal).count()
208    }
209
210    pub fn warning_count(&self) -> usize {
211        self.issues.iter().filter(|i| i.severity == IssueSeverity::Warning).count()
212    }
213
214    pub fn to_operation_outcome(&self) -> Value {
215        serde_json::json!({
216            "resourceType": "OperationOutcome",
217            "issue": self.issues.iter().map(|i| i.to_json()).collect::<Vec<_>>()
218        })
219    }
220}
221
222/// Individual validation issue
223#[derive(Debug, Clone, PartialEq)]
224pub struct ValidationIssue {
225    pub severity: IssueSeverity,
226    pub code: IssueCode,
227    pub diagnostics: String,
228    pub location: Option<String>,
229    pub expression: Option<Vec<String>>,
230}
231
232impl ValidationIssue {
233    pub fn error(code: IssueCode, diagnostics: String) -> Self {
234        Self {
235            severity: IssueSeverity::Error,
236            code,
237            diagnostics,
238            location: None,
239            expression: None,
240        }
241    }
242
243    pub fn warning(code: IssueCode, diagnostics: String) -> Self {
244        Self {
245            severity: IssueSeverity::Warning,
246            code,
247            diagnostics,
248            location: None,
249            expression: None,
250        }
251    }
252
253    pub fn information(code: IssueCode, diagnostics: String) -> Self {
254        Self {
255            severity: IssueSeverity::Information,
256            code,
257            diagnostics,
258            location: None,
259            expression: None,
260        }
261    }
262
263    pub fn with_location(mut self, location: String) -> Self {
264        self.location = Some(location);
265        self
266    }
267
268    pub fn with_expression(mut self, expression: Vec<String>) -> Self {
269        self.expression = Some(expression);
270        self
271    }
272
273    fn to_json(&self) -> Value {
274        let mut issue = serde_json::json!({
275            "severity": self.severity.to_string().to_lowercase(),
276            "code": self.code.to_string(),
277            "diagnostics": self.diagnostics,
278        });
279
280        if let Some(ref loc) = self.location {
281            issue["location"] = serde_json::json!([loc]);
282        }
283
284        if let Some(ref expr) = self.expression {
285            issue["expression"] = serde_json::json!(expr);
286        }
287
288        issue
289    }
290}
291
292#[derive(Debug, Clone, Copy, PartialEq, Eq)]
293pub enum IssueSeverity {
294    Fatal,
295    Error,
296    Warning,
297    Information,
298}
299
300impl std::fmt::Display for IssueSeverity {
301    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
302        match self {
303            Self::Fatal => write!(f, "Fatal"),
304            Self::Error => write!(f, "Error"),
305            Self::Warning => write!(f, "Warning"),
306            Self::Information => write!(f, "Information"),
307        }
308    }
309}
310
311#[derive(Debug, Clone, Copy, PartialEq, Eq)]
312pub enum IssueCode {
313    Invalid,
314    Structure,
315    Required,
316    Value,
317    Invariant,
318    Security,
319    Login,
320    Unknown,
321    Expired,
322    Forbidden,
323    Suppressed,
324    Processing,
325    NotSupported,
326    Duplicate,
327    MultipleMatches,
328    NotFound,
329    Deleted,
330    TooLong,
331    CodeInvalid,
332    Extension,
333    TooCostly,
334    BusinessRule,
335    Conflict,
336    Transient,
337    LockError,
338    NoStore,
339    Exception,
340    Timeout,
341    Incomplete,
342    Throttled,
343    Informational,
344}
345
346impl std::fmt::Display for IssueCode {
347    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
348        let s = match self {
349            Self::Invalid => "invalid",
350            Self::Structure => "structure",
351            Self::Required => "required",
352            Self::Value => "value",
353            Self::Invariant => "invariant",
354            Self::Security => "security",
355            Self::Login => "login",
356            Self::Unknown => "unknown",
357            Self::Expired => "expired",
358            Self::Forbidden => "forbidden",
359            Self::Suppressed => "suppressed",
360            Self::Processing => "processing",
361            Self::NotSupported => "not-supported",
362            Self::Duplicate => "duplicate",
363            Self::MultipleMatches => "multiple-matches",
364            Self::NotFound => "not-found",
365            Self::Deleted => "deleted",
366            Self::TooLong => "too-long",
367            Self::CodeInvalid => "code-invalid",
368            Self::Extension => "extension",
369            Self::TooCostly => "too-costly",
370            Self::BusinessRule => "business-rule",
371            Self::Conflict => "conflict",
372            Self::Transient => "transient",
373            Self::LockError => "lock-error",
374            Self::NoStore => "no-store",
375            Self::Exception => "exception",
376            Self::Timeout => "timeout",
377            Self::Incomplete => "incomplete",
378            Self::Throttled => "throttled",
379            Self::Informational => "informational",
380        };
381        write!(f, "{}", s)
382    }
383}
384
385#[cfg(test)]
386mod tests {
387    use super::*;
388
389    #[test]
390    fn test_validation_outcome_operations() {
391        let outcome = ValidationOutcome {
392            resource_type: Some("Patient".to_string()),
393            valid: false,
394            issues: vec![
395                ValidationIssue::error(IssueCode::Required, "Missing required field".to_string()),
396                ValidationIssue::warning(IssueCode::Value, "Deprecated code".to_string()),
397            ],
398        };
399
400        assert!(!outcome.valid);
401        assert!(outcome.has_errors());
402        assert_eq!(outcome.error_count(), 1);
403        assert_eq!(outcome.warning_count(), 1);
404    }
405
406    #[test]
407    fn test_operation_outcome_conversion() {
408        let outcome = ValidationOutcome {
409            resource_type: Some("Patient".to_string()),
410            valid: false,
411            issues: vec![
412                ValidationIssue::error(IssueCode::Required, "name is required".to_string())
413                    .with_location("Patient.name".to_string())
414                    .with_expression(vec!["Patient.name".to_string()]),
415            ],
416        };
417
418        let op_outcome = outcome.to_operation_outcome();
419        assert_eq!(op_outcome["resourceType"], "OperationOutcome");
420        assert_eq!(op_outcome["issue"][0]["severity"], "error");
421        assert_eq!(op_outcome["issue"][0]["code"], "required");
422    }
423}