1use std::collections::BTreeMap;
9use std::fmt;
10
11use fhir_types::r4::codeable_concept::CodeableConcept;
12use fhir_types::r4::coding::Coding;
13use fhir_types::r4::operation_outcome::OperationOutcome;
14use fhir_types::r4::operation_outcome::OperationOutcomeIssue;
15use fhir_types::r4::primitives;
16
17use crate::issue::Issue;
18use crate::issue::IssueType;
19use crate::issue::MessageId;
20use crate::issue::Rule;
21use crate::issue::Severity;
22
23pub const MESSAGE_SYSTEM: &str = "https://ferrobridge.eu/fhir/CodeSystem/validator-message";
28
29#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
32pub enum Constraint {
33 Slicing,
36 Discriminator {
39 kind: String,
42 path: String,
44 },
45 DiscriminatorValueSet {
48 path: String,
50 value_set: String,
52 },
53 Invariant {
55 key: String,
57 severity: String,
59 },
60 Binding {
62 strength: String,
64 value_set: Option<String>,
66 },
67 TypeProfile {
69 profiles: Vec<String>,
71 },
72 TargetProfile {
75 profiles: Vec<String>,
77 },
78 ClaimedProfile {
80 url: String,
82 },
83 ExtensionContext,
86}
87
88#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
90pub enum Reason {
91 DiscriminatorPath,
94 DiscriminatorType,
96 NoDiscriminatorValue,
99 ValueSetNotEnumerated,
102 UnresolvedReference,
105 ReferenceOutOfScope,
108 ProfileNotFound,
110 DefaultSliceNotClosed,
113 Invariants,
115 Bindings,
117 Profiles,
120 ExtensionContext,
122}
123
124impl fmt::Display for Reason {
125 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
126 f.write_str(match self {
127 Self::DiscriminatorPath => {
128 "the discriminator path is outside the FHIRPath subset discriminators use"
129 }
130 Self::DiscriminatorType => "the discriminator type is none R4 defines",
131 Self::NoDiscriminatorValue => {
132 "the slice sets nothing at the discriminator path to tell it apart by"
133 }
134 Self::ValueSetNotEnumerated => {
135 "the value set cannot be enumerated from the package set"
136 }
137 Self::UnresolvedReference => "a reference the discriminator resolves does not resolve",
138 Self::ReferenceOutOfScope => {
139 "a reference the discriminator resolves points outside the resources searched"
140 }
141 Self::ProfileNotFound => "the profile is not in the package set",
142 Self::DefaultSliceNotClosed => {
143 "the @default slice is not used, because the slicing rules are not closed"
144 }
145 Self::Invariants => "invariants are not evaluated",
146 Self::Bindings => "terminology bindings are not checked",
147 Self::Profiles => "target profiles and claimed profiles are not validated",
148 Self::ExtensionContext => "extension contexts are not checked",
149 })
150 }
151}
152
153#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
155pub struct Unevaluated {
156 pub constraint: Constraint,
158 pub structure: String,
160 pub element: String,
162 pub location: String,
164 pub reason: Reason,
166}
167
168#[derive(Debug, Clone, Default, PartialEq, Eq)]
170pub struct Report {
171 issues: Vec<Issue>,
172 unevaluated: Vec<Unevaluated>,
173}
174
175impl Report {
176 #[must_use]
178 pub fn issues(&self) -> &[Issue] {
179 &self.issues
180 }
181
182 #[must_use]
185 pub fn unevaluated(&self) -> &[Unevaluated] {
186 &self.unevaluated
187 }
188
189 #[must_use]
194 pub fn is_valid(&self) -> bool {
195 !self
196 .issues
197 .iter()
198 .any(|issue| matches!(issue.severity, Severity::Fatal | Severity::Error))
199 }
200
201 pub fn with_message(&self, id: MessageId) -> impl Iterator<Item = &Issue> {
203 self.issues
204 .iter()
205 .filter(move |issue| issue.rule == Rule::Message(id))
206 }
207}
208
209#[derive(Debug, Default)]
211pub(crate) struct Collector {
212 issues: Vec<Issue>,
213 unevaluated: BTreeMap<(String, String, Constraint), (String, Reason)>,
214}
215
216impl Collector {
217 pub(crate) fn issue(
219 &mut self,
220 severity: Severity,
221 code: IssueType,
222 id: MessageId,
223 expression: &str,
224 message: String,
225 ) {
226 self.issues.push(Issue {
227 severity,
228 code,
229 expression: expression.to_owned(),
230 rule: Rule::Message(id),
231 message,
232 });
233 }
234
235 pub(crate) fn unevaluated(
237 &mut self,
238 structure: &str,
239 element: &str,
240 constraint: Constraint,
241 location: &str,
242 reason: Reason,
243 ) {
244 self.unevaluated
245 .entry((structure.to_owned(), element.to_owned(), constraint))
246 .or_insert_with(|| (location.to_owned(), reason));
247 }
248
249 pub(crate) fn is_valid(&self) -> bool {
251 self.first_error().is_none()
252 }
253
254 pub(crate) fn first_error(&self) -> Option<&Issue> {
256 self.issues
257 .iter()
258 .find(|issue| matches!(issue.severity, Severity::Fatal | Severity::Error))
259 }
260
261 pub(crate) fn absorb(&mut self, other: Self) {
263 self.issues.extend(other.issues);
264 for (key, value) in other.unevaluated {
265 self.unevaluated.entry(key).or_insert(value);
266 }
267 }
268
269 pub(crate) fn finish(self) -> Report {
271 Report {
272 issues: self.issues,
273 unevaluated: self
274 .unevaluated
275 .into_iter()
276 .map(
277 |((structure, element, constraint), (location, reason))| Unevaluated {
278 constraint,
279 structure,
280 element,
281 location,
282 reason,
283 },
284 )
285 .collect(),
286 }
287 }
288}
289
290impl From<&Report> for OperationOutcome {
291 fn from(report: &Report) -> Self {
302 let mut issues: Vec<OperationOutcomeIssue> =
303 report.issues.iter().map(outcome_issue).collect();
304 issues.extend(report.unevaluated.iter().map(|unevaluated| {
305 outcome_issue(&Issue {
306 severity: Severity::Warning,
307 code: IssueType::NotSupported,
308 expression: unevaluated.location.clone(),
309 rule: Rule::Message(MessageId::ConstraintUnevaluated),
310 message: describe(unevaluated),
311 })
312 }));
313 if issues.is_empty() {
314 issues.push(outcome_issue(&Issue {
315 severity: Severity::Information,
316 code: IssueType::Informational,
317 expression: String::new(),
318 rule: Rule::Message(MessageId::AllOk),
319 message: "no issues, and every constraint met was evaluated".to_owned(),
320 }));
321 }
322 Self {
323 issue: issues,
324 ..Self::default()
325 }
326 }
327}
328
329fn describe(unevaluated: &Unevaluated) -> String {
331 let what = match &unevaluated.constraint {
332 Constraint::Slicing => "the slicing".to_owned(),
333 Constraint::Discriminator { kind, path } => {
334 format!("the {kind} discriminator {path}")
335 }
336 Constraint::DiscriminatorValueSet { path, value_set } => {
337 format!("the discriminator {path} over the value set {value_set}")
338 }
339 Constraint::Invariant { key, .. } => format!("the invariant {key}"),
340 Constraint::Binding {
341 strength,
342 value_set,
343 } => match value_set {
344 Some(value_set) => format!("the {strength} binding to {value_set}"),
345 None => format!("the {strength} binding"),
346 },
347 Constraint::TypeProfile { profiles } => {
348 format!("the type profiles {}", profiles.join(", "))
349 }
350 Constraint::TargetProfile { profiles } => {
351 format!("the target profiles {}", profiles.join(", "))
352 }
353 Constraint::ClaimedProfile { url } => format!("the claimed profile {url}"),
354 Constraint::ExtensionContext => "the extension context".to_owned(),
355 };
356 format!(
357 "{what} on {} of {} was not evaluated: {}",
358 unevaluated.element, unevaluated.structure, unevaluated.reason
359 )
360}
361
362fn outcome_issue(issue: &Issue) -> OperationOutcomeIssue {
364 let coding = match &issue.rule {
365 Rule::Message(id) => Coding {
366 system: Some(primitives::Uri::from(MESSAGE_SYSTEM.to_owned())),
367 code: Some(primitives::Code::from(id.code().to_owned())),
368 ..Coding::default()
369 },
370 Rule::Constraint { key, source } => Coding {
371 system: Some(primitives::Uri::from(source.clone())),
372 code: Some(primitives::Code::from(key.clone())),
373 ..Coding::default()
374 },
375 };
376 OperationOutcomeIssue {
377 severity: primitives::Code::from(issue.severity.code().to_owned()),
378 code: primitives::Code::from(issue.code.code().to_owned()),
379 details: Some(CodeableConcept {
380 coding: vec![coding],
381 text: Some(primitives::String::from(issue.message.clone())),
382 ..CodeableConcept::default()
383 }),
384 expression: if issue.expression.is_empty() {
385 Vec::new()
386 } else {
387 vec![primitives::String::from(issue.expression.clone())]
388 },
389 ..OperationOutcomeIssue::default()
390 }
391}