1use std::time::Duration;
43use serde::{Deserialize, Serialize};
44
45mod plan;
46mod error;
47mod validator;
48mod steps;
49
50pub use plan::{ValidationPlan, Step, SchemaPlan, ProfilesPlan, ConstraintsPlan, TerminologyPlan, ReferencesPlan, BundlePlan};
51pub use error::ConfigError;
52pub use validator::{Validator, ValidationOutcome, ValidationIssue, IssueSeverity, IssueCode};
53
54#[derive(Debug, Clone, Serialize, Deserialize)]
59pub struct ValidatorConfig {
60 #[serde(skip_serializing_if = "Option::is_none")]
61 pub preset: Option<Preset>,
62 #[serde(default)]
63 pub fhir: FhirConfig,
64 #[serde(default)]
65 pub report: ReportConfig,
66 #[serde(default)]
67 pub exec: ExecConfig,
68 #[serde(default)]
69 pub schema: SchemaConfig,
70 #[serde(default)]
71 pub constraints: ConstraintsConfig,
72 #[serde(default)]
73 pub profiles: ProfilesConfig,
74 #[serde(default)]
75 pub terminology: TerminologyConfig,
76 #[serde(default)]
77 pub references: ReferencesConfig,
78 #[serde(default)]
79 pub bundles: BundleConfig,
80}
81
82#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
83pub enum Preset {
84 Ingestion,
85 Authoring,
86 Server,
87 Publication,
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize)]
95pub struct FhirConfig {
96 #[serde(default = "default_fhir_version")]
97 pub version: FhirVersion,
98 #[serde(default)]
99 pub allow_version_mismatch: bool,
100}
101
102fn default_fhir_version() -> FhirVersion {
103 FhirVersion::R5
104}
105
106impl Default for FhirConfig {
107 fn default() -> Self {
108 Self {
109 version: FhirVersion::R5,
110 allow_version_mismatch: false,
111 }
112 }
113}
114
115#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
116pub enum FhirVersion {
117 R4,
118 R5,
119}
120
121#[derive(Debug, Clone, Serialize, Deserialize)]
126pub struct ExecConfig {
127 #[serde(default)]
128 pub fail_fast: bool,
129 #[serde(default = "default_max_issues")]
130 pub max_issues: usize,
131}
132
133fn default_max_issues() -> usize {
134 1000
135}
136
137impl Default for ExecConfig {
138 fn default() -> Self {
139 Self {
140 fail_fast: false,
141 max_issues: 1000,
142 }
143 }
144}
145
146#[derive(Debug, Clone, Serialize, Deserialize)]
151pub struct ReportConfig {
152 #[serde(default = "default_include_warnings")]
153 pub include_warnings: bool,
154 #[serde(default)]
155 pub include_information: bool,
156}
157
158fn default_include_warnings() -> bool {
159 true
160}
161
162impl Default for ReportConfig {
163 fn default() -> Self {
164 Self {
165 include_warnings: true,
166 include_information: false,
167 }
168 }
169}
170
171#[derive(Debug, Clone, Serialize, Deserialize)]
176pub struct SchemaConfig {
177 #[serde(default = "default_schema_mode")]
178 pub mode: SchemaMode,
179 #[serde(default)]
180 pub allow_unknown_elements: bool,
181 #[serde(default)]
182 pub allow_modifier_extensions: bool,
183}
184
185fn default_schema_mode() -> SchemaMode {
186 SchemaMode::On
187}
188
189#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
190pub enum SchemaMode {
191 Off,
192 On,
193}
194
195impl Default for SchemaConfig {
196 fn default() -> Self {
197 Self {
198 mode: SchemaMode::On,
199 allow_unknown_elements: false,
200 allow_modifier_extensions: false,
201 }
202 }
203}
204
205#[derive(Debug, Clone, Serialize, Deserialize)]
210pub struct ConstraintsConfig {
211 #[serde(default)]
212 pub mode: ConstraintsMode,
213 #[serde(default)]
214 pub best_practice: BestPracticeMode,
215 #[serde(default)]
216 pub suppress: Vec<ConstraintId>,
217 #[serde(default)]
218 pub level_overrides: Vec<ConstraintLevelOverride>,
219}
220
221#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
222#[derive(Default)]
223pub enum ConstraintsMode {
224 Off,
225 InvariantsOnly,
226 #[default]
227 Full,
228}
229
230
231#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
232#[derive(Default)]
233pub enum BestPracticeMode {
234 #[default]
235 Ignore,
236 Warn,
237 Error,
238}
239
240
241#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
242pub struct ConstraintId(pub String);
243
244#[derive(Debug, Clone, Serialize, Deserialize)]
245pub struct ConstraintLevelOverride {
246 pub id: ConstraintId,
247 pub level: IssueLevel,
248}
249
250#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
251pub enum IssueLevel {
252 Error,
253 Warning,
254 Information,
255}
256
257impl Default for ConstraintsConfig {
258 fn default() -> Self {
259 Self {
260 mode: ConstraintsMode::Full,
261 best_practice: BestPracticeMode::Ignore,
262 suppress: vec![],
263 level_overrides: vec![],
264 }
265 }
266}
267
268#[derive(Debug, Clone, Serialize, Deserialize)]
273pub struct ProfilesConfig {
274 #[serde(default)]
275 pub mode: ProfilesMode,
276 #[serde(skip_serializing_if = "Option::is_none")]
280 pub explicit_profiles: Option<Vec<String>>,
281}
282
283#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
284#[derive(Default)]
285pub enum ProfilesMode {
286 #[default]
287 Off,
288 On,
289}
290
291
292impl Default for ProfilesConfig {
293 fn default() -> Self {
294 Self {
295 mode: ProfilesMode::Off,
296 explicit_profiles: None,
297 }
298 }
299}
300
301#[derive(Debug, Clone, Serialize, Deserialize)]
306pub struct TerminologyConfig {
307 #[serde(default)]
308 pub mode: TerminologyMode,
309 #[serde(default)]
310 pub extensible_handling: ExtensibleHandling,
311 #[serde(with = "duration_millis", default = "default_terminology_timeout")]
312 pub timeout: Duration,
313 #[serde(default)]
314 pub on_timeout: TimeoutPolicy,
315 #[serde(default)]
316 pub cache: CachePolicy,
317}
318
319fn default_terminology_timeout() -> Duration {
320 Duration::from_millis(1500)
321}
322
323#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
324#[derive(Default)]
325pub enum TerminologyMode {
326 #[default]
327 Off,
328 Local,
329 Remote,
330 Hybrid,
331}
332
333
334#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
335#[derive(Default)]
336pub enum ExtensibleHandling {
337 Ignore,
338 #[default]
339 Warn,
340 Error,
341}
342
343
344#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
345#[derive(Default)]
346pub enum TimeoutPolicy {
347 Skip,
348 #[default]
349 Warn,
350 Error,
351}
352
353
354#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
355#[derive(Default)]
356pub enum CachePolicy {
357 None,
358 #[default]
359 Memory,
360}
361
362
363impl Default for TerminologyConfig {
364 fn default() -> Self {
365 Self {
366 mode: TerminologyMode::Off,
367 extensible_handling: ExtensibleHandling::Warn,
368 timeout: Duration::from_millis(1500),
369 on_timeout: TimeoutPolicy::Warn,
370 cache: CachePolicy::Memory,
371 }
372 }
373}
374
375mod duration_millis {
377 use std::time::Duration;
378 use serde::{Deserialize, Deserializer, Serializer};
379
380 pub fn serialize<S>(duration: &Duration, serializer: S) -> Result<S::Ok, S::Error>
381 where
382 S: Serializer,
383 {
384 serializer.serialize_u64(duration.as_millis() as u64)
385 }
386
387 pub fn deserialize<'de, D>(deserializer: D) -> Result<Duration, D::Error>
388 where
389 D: Deserializer<'de>,
390 {
391 let millis = u64::deserialize(deserializer)?;
392 Ok(Duration::from_millis(millis))
393 }
394}
395
396#[derive(Debug, Clone, Serialize, Deserialize)]
401pub struct ReferencesConfig {
402 #[serde(default)]
403 pub mode: ReferenceMode,
404 #[serde(default = "default_allow_external")]
405 pub allow_external: bool,
406}
407
408fn default_allow_external() -> bool {
409 true
410}
411
412#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
413#[derive(Default)]
414pub enum ReferenceMode {
415 #[default]
416 Off,
417 TypeOnly,
418 Existence,
419 Full,
420}
421
422
423impl Default for ReferencesConfig {
424 fn default() -> Self {
425 Self {
426 mode: ReferenceMode::Off,
427 allow_external: true,
428 }
429 }
430}
431
432#[derive(Debug, Clone, Serialize, Deserialize)]
437pub struct BundleConfig {
438 #[serde(default)]
439 pub mode: BundleMode,
440}
441
442#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
443#[derive(Default)]
444pub enum BundleMode {
445 #[default]
446 Off,
447 On,
448}
449
450
451impl Default for BundleConfig {
452 fn default() -> Self {
453 Self {
454 mode: BundleMode::Off,
455 }
456 }
457}
458
459impl ValidatorConfig {
464 pub fn preset(p: Preset) -> Self {
465 let mut cfg = Self::defaults();
466 cfg.preset = Some(p);
467
468 match p {
469 Preset::Ingestion => {
470 cfg.schema.mode = SchemaMode::On;
471 cfg.constraints.mode = ConstraintsMode::Off;
472 cfg.profiles.mode = ProfilesMode::Off;
473 cfg.terminology.mode = TerminologyMode::Off;
474 cfg.references.mode = ReferenceMode::Off;
475 }
476 Preset::Authoring => {
477 cfg.schema.mode = SchemaMode::On;
478 cfg.profiles.mode = ProfilesMode::On;
479 cfg.constraints.mode = ConstraintsMode::Full;
480 cfg.terminology.mode = TerminologyMode::Local;
481 }
482 Preset::Server => {
483 cfg.schema.mode = SchemaMode::On;
484 cfg.profiles.mode = ProfilesMode::On;
485 cfg.constraints.mode = ConstraintsMode::Full;
486 cfg.terminology.mode = TerminologyMode::Hybrid;
487 cfg.references.mode = ReferenceMode::Existence;
488 }
489 Preset::Publication => {
490 cfg.schema.mode = SchemaMode::On;
491 cfg.profiles.mode = ProfilesMode::On;
492 cfg.constraints.mode = ConstraintsMode::Full;
493 cfg.constraints.best_practice = BestPracticeMode::Warn;
494 cfg.terminology.mode = TerminologyMode::Remote;
495 cfg.references.mode = ReferenceMode::Full;
496 }
497 }
498
499 cfg
500 }
501
502 pub fn defaults() -> Self {
503 Self {
504 preset: None,
505 fhir: FhirConfig {
506 version: FhirVersion::R5,
507 allow_version_mismatch: false,
508 },
509 report: ReportConfig::default(),
510 exec: ExecConfig::default(),
511 schema: SchemaConfig::default(),
512 constraints: ConstraintsConfig::default(),
513 profiles: ProfilesConfig::default(),
514 terminology: TerminologyConfig::default(),
515 references: ReferencesConfig::default(),
516 bundles: BundleConfig::default(),
517 }
518 }
519
520 pub fn compile(&self) -> Result<ValidationPlan, ConfigError> {
521 if self.references.mode == ReferenceMode::Full
523 && self.terminology.mode == TerminologyMode::Off
524 {
525 return Err(ConfigError::TerminologyRequiredForFullRef);
526 }
527
528 let mut steps = Vec::new();
529
530 if self.schema.mode == SchemaMode::On {
531 steps.push(Step::Schema(SchemaPlan::from(&self.schema)));
532 }
533 if self.profiles.mode == ProfilesMode::On {
534 steps.push(Step::Profiles(ProfilesPlan::from(&self.profiles)));
535 }
536 if self.constraints.mode != ConstraintsMode::Off {
537 steps.push(Step::Constraints(ConstraintsPlan::from(&self.constraints)));
538 }
539 if self.terminology.mode != TerminologyMode::Off {
540 steps.push(Step::Terminology(TerminologyPlan::from(
541 &self.terminology,
542 )));
543 }
544 if self.references.mode != ReferenceMode::Off {
545 steps.push(Step::References(ReferencesPlan::from(&self.references)));
546 }
547 if self.bundles.mode == BundleMode::On {
548 steps.push(Step::Bundles(BundlePlan::from(&self.bundles)));
549 }
550
551 Ok(ValidationPlan {
552 steps,
553 fail_fast: self.exec.fail_fast,
554 max_issues: self.exec.max_issues,
555 })
556 }
557
558 pub fn from_yaml(yaml: &str) -> Result<Self, serde_yaml::Error> {
559 serde_yaml::from_str(yaml)
560 }
561
562 pub fn to_yaml(&self) -> Result<String, serde_yaml::Error> {
563 serde_yaml::to_string(self)
564 }
565
566 pub fn builder() -> ValidatorConfigBuilder {
567 ValidatorConfigBuilder::default()
568 }
569}
570
571impl Default for ValidatorConfig {
572 fn default() -> Self {
573 Self::defaults()
574 }
575}
576
577#[derive(Debug, Default, Clone)]
582pub struct ValidatorConfigBuilder {
583 cfg: Option<ValidatorConfig>,
584}
585
586impl ValidatorConfigBuilder {
587 pub fn preset(mut self, p: Preset) -> Self {
588 self.cfg = Some(ValidatorConfig::preset(p));
589 self
590 }
591
592 pub fn fhir_version(mut self, version: FhirVersion) -> Self {
593 self.cfg().fhir.version = version;
594 self
595 }
596
597 pub fn schema_mode(mut self, mode: SchemaMode) -> Self {
598 self.cfg().schema.mode = mode;
599 self
600 }
601
602 pub fn constraints_mode(mut self, mode: ConstraintsMode) -> Self {
603 self.cfg().constraints.mode = mode;
604 self
605 }
606
607 pub fn profiles_mode(mut self, mode: ProfilesMode) -> Self {
608 self.cfg().profiles.mode = mode;
609 self
610 }
611
612 pub fn terminology_mode(mut self, mode: TerminologyMode) -> Self {
613 self.cfg().terminology.mode = mode;
614 self
615 }
616
617 pub fn reference_mode(mut self, mode: ReferenceMode) -> Self {
618 self.cfg().references.mode = mode;
619 self
620 }
621
622 pub fn fail_fast(mut self, fail_fast: bool) -> Self {
623 self.cfg().exec.fail_fast = fail_fast;
624 self
625 }
626
627 pub fn max_issues(mut self, max: usize) -> Self {
628 self.cfg().exec.max_issues = max;
629 self
630 }
631
632 pub fn build(self) -> ValidatorConfig {
633 self.cfg.unwrap_or_default()
634 }
635
636 fn cfg(&mut self) -> &mut ValidatorConfig {
637 self.cfg.get_or_insert_with(ValidatorConfig::defaults)
638 }
639}
640
641#[cfg(test)]
642mod tests {
643 use super::*;
644
645 #[test]
646 fn test_preset_ingestion() {
647 let cfg = ValidatorConfig::preset(Preset::Ingestion);
648 assert_eq!(cfg.schema.mode, SchemaMode::On);
649 assert_eq!(cfg.constraints.mode, ConstraintsMode::Off);
650 assert_eq!(cfg.terminology.mode, TerminologyMode::Off);
651 }
652
653 #[test]
654 fn test_builder() {
655 let cfg = ValidatorConfig::builder()
656 .preset(Preset::Server)
657 .terminology_mode(TerminologyMode::Local)
658 .fail_fast(true)
659 .build();
660
661 assert_eq!(cfg.preset, Some(Preset::Server));
662 assert_eq!(cfg.terminology.mode, TerminologyMode::Local);
663 assert!(cfg.exec.fail_fast);
664 }
665
666 #[test]
667 fn test_yaml_roundtrip() {
668 let cfg = ValidatorConfig::preset(Preset::Authoring);
669 let yaml = cfg.to_yaml().unwrap();
670 let parsed = ValidatorConfig::from_yaml(&yaml).unwrap();
671 assert_eq!(cfg.preset, parsed.preset);
672 assert_eq!(cfg.schema.mode, parsed.schema.mode);
673 }
674
675 #[test]
676 fn test_compile_validation() {
677 let cfg = ValidatorConfig::builder()
678 .reference_mode(ReferenceMode::Full)
679 .terminology_mode(TerminologyMode::Off)
680 .build();
681
682 let result = cfg.compile();
683 assert!(matches!(result, Err(ConfigError::TerminologyRequiredForFullRef)));
684 }
685}