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
8pub 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 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 pub fn with_expanded_snapshots(self) -> Validator<ExpandedFhirContext<C>>
45 where
46 C: Clone,
47 {
48 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 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
84struct 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 }
164
165 fn validate_references(&mut self, _plan: &crate::ReferencesPlan) {
166 }
168
169 fn validate_bundles(&mut self, _plan: &crate::BundlePlan) {
170 }
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#[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#[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}