fhir 1.2.0

Fast Healthcare Interoperability Resources (FHIR) data model for Rust: the complete FHIR R5, R4, and R3 resources, datatypes, and code systems as serde-serializable types, plus a spec-driven code generator.
//! Lightweight FHIR R4 validation.
//!
//! Provides format checks for the FHIR R4 primitive datatypes (the
//! regular-expression constraints from the specification, implemented without
//! an external regex dependency), and bridges validation results into an R4
//! `OperationOutcome`.
//!
//! The [`Validate`] trait and [`ValidationIssue`] type are shared by every FHIR
//! release and live in [`crate::validate`]; this module re-exports them so R4
//! code can use `fhir::r4::validate::Validate`. Recursive validation of complex
//! types and resources is generated by `#[derive(Validate)]`.
//!
//! # Examples
//!
//! ```
//! use fhir::r4::types::Id;
//! use fhir::r4::validate::Validate;
//!
//! assert!(Id("patient-1".to_string()).validate().is_empty());
//! assert!(!Id("bad id!".to_string()).validate().is_empty());
//! ```

use crate::r4::types;
use crate::validate::{is_valid_code, is_valid_id, is_valid_uri_like};

pub use crate::validate::{Validate, ValidationIssue};

impl From<ValidationIssue> for crate::r4::resources::operation_outcome::OperationOutcomeIssue {
    fn from(issue: ValidationIssue) -> Self {
        use crate::r4::codes::{IssueSeverity, IssueType};
        use crate::r4::coded::Coded;
        Self {
            severity: Coded::Known(IssueSeverity::Error),
            code: Coded::Known(IssueType::Invalid),
            diagnostics: Some(types::String(issue.message)),
            expression: vec![types::String(issue.path)],
            ..Default::default()
        }
    }
}

/// Bridge validation results into a FHIR `OperationOutcome` — each
/// [`ValidationIssue`] becomes an `issue` entry with `error` severity and an
/// `invalid` code, its message in `diagnostics` and its path in `expression`.
///
/// ```
/// use fhir::r4::resources::Patient;
/// use fhir::r4::resources::operation_outcome::OperationOutcome;
/// use fhir::r4::types::Uri;
/// use fhir::r4::validate::Validate;
///
/// let mut patient = Patient::default();
/// patient.implicit_rules = Some(Uri(" bad ".to_string()));
///
/// let outcome: OperationOutcome = patient.validate().into();
/// assert_eq!(outcome.issue.len(), 1);
/// assert_eq!(outcome.issue[0].expression[0].0, "implicit_rules.uri");
/// ```
impl From<Vec<ValidationIssue>> for crate::r4::resources::operation_outcome::OperationOutcome {
    fn from(issues: Vec<ValidationIssue>) -> Self {
        use crate::r4::codes::{IssueSeverity, IssueType};
        use crate::r4::coded::Coded;
        use crate::r4::resources::operation_outcome::{OperationOutcome, OperationOutcomeIssue};

        let mut items: Vec<OperationOutcomeIssue> = issues.into_iter().map(Into::into).collect();
        if items.is_empty() {
            // `OperationOutcome.issue` is 1..*; represent "no problems" as an
            // information-severity issue.
            items.push(OperationOutcomeIssue {
                severity: Coded::Known(IssueSeverity::Information),
                code: Coded::Known(IssueType::Informational),
                diagnostics: Some(types::String("No issues detected.".to_string())),
                ..Default::default()
            });
        }
        // `OperationOutcome` has a `1..*` field, so it has no `Default` to
        // fall back on and every field is named explicitly.
        OperationOutcome {
            issue: ::vec1::Vec1::try_from_vec(items).expect("non-empty"),
            id: None,
            meta: None,
            implicit_rules: None,
            implicit_rules_ext: None,
            language: None,
            language_ext: None,
            text: None,
            contained: Vec::new(),
            extension: Vec::new(),
            modifier_extension: Vec::new(),
        }
    }
}

impl Validate for types::Code {
    fn validate(&self) -> Vec<ValidationIssue> {
        if is_valid_code(&self.0) {
            vec![]
        } else {
            vec![ValidationIssue::new("code", format!("invalid FHIR code: {:?}", self.0))]
        }
    }
}

impl Validate for types::Id {
    fn validate(&self) -> Vec<ValidationIssue> {
        if is_valid_id(&self.0) {
            vec![]
        } else {
            vec![ValidationIssue::new("id", format!("invalid FHIR id: {:?}", self.0))]
        }
    }
}

impl Validate for types::Oid {
    fn validate(&self) -> Vec<ValidationIssue> {
        if self.0.starts_with("urn:oid:") {
            vec![]
        } else {
            vec![ValidationIssue::new("oid", "FHIR oid must start with `urn:oid:`")]
        }
    }
}

impl Validate for types::Uuid {
    fn validate(&self) -> Vec<ValidationIssue> {
        if self.0.starts_with("urn:uuid:") {
            vec![]
        } else {
            vec![ValidationIssue::new("uuid", "FHIR uuid must start with `urn:uuid:`")]
        }
    }
}

impl Validate for types::Uri {
    fn validate(&self) -> Vec<ValidationIssue> {
        if is_valid_uri_like(&self.0) {
            vec![]
        } else {
            vec![ValidationIssue::new(
                "uri",
                "FHIR uri must be non-empty and not surrounded by whitespace",
            )]
        }
    }
}

impl Validate for types::Canonical {
    fn validate(&self) -> Vec<ValidationIssue> {
        if is_valid_uri_like(&self.0) {
            vec![]
        } else {
            vec![ValidationIssue::new(
                "canonical",
                "must be non-empty and not surrounded by whitespace",
            )]
        }
    }
}

impl Validate for types::Url {
    fn validate(&self) -> Vec<ValidationIssue> {
        if is_valid_uri_like(&self.0) {
            vec![]
        } else {
            vec![ValidationIssue::new("url", "must be non-empty and not surrounded by whitespace")]
        }
    }
}

/// Primitives whose Rust representation already guarantees validity, so there
/// is no further structural constraint to check here.
macro_rules! always_valid {
    ($($t:ty),* $(,)?) => {
        $(impl Validate for $t {
            fn validate(&self) -> Vec<ValidationIssue> { Vec::new() }
        })*
    };
}

always_valid!(
    types::Base64Binary,
    types::Boolean,
    types::Date,
    types::DateTime,
    types::Decimal,
    types::Instant,
    types::Integer,
    types::Markdown,
    types::PositiveInt,
    types::String,
    types::Time,
    types::UnsignedInt,
    types::Xhtml,
);

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn code_ok() {
        assert!(types::Code("final".to_string()).is_valid());
        assert!(types::Code("entered in error".to_string()).is_valid());
    }

    #[test]
    fn code_bad() {
        assert!(!types::Code(" leading".to_string()).is_valid());
        assert!(!types::Code("double  space".to_string()).is_valid());
        assert!(!types::Code(String::new()).is_valid());
    }

    #[test]
    fn id_ok_and_bad() {
        assert!(types::Id("abc-123.4".to_string()).is_valid());
        assert!(!types::Id("bad id!".to_string()).is_valid());
        assert!(!types::Id("x".repeat(65)).is_valid());
    }

    #[test]
    fn required_binding_flags_unknown_code() {
        use crate::r4::coded::Coded;
        use crate::r4::codes::AdministrativeGender;
        use crate::r4::resources::Patient;

        let mut patient = Patient {
            gender: Some(Coded::Known(AdministrativeGender::Male)),
            ..Default::default()
        };
        assert!(patient.validate().is_empty());

        patient.gender = Some(Coded::Unknown("robot".to_string()));
        let issues = patient.validate();
        assert_eq!(issues.len(), 1);
        assert_eq!(issues[0].path, "gender.code");
    }

    #[test]
    fn invariant_ext_1() {
        use crate::r4::choice::Primitive;
        use crate::r4::types::extension::{Extension, ExtensionValue};
        use crate::r4::types::{Boolean, String as FhirString};

        let url = || FhirString("http://example.org/x".to_string());

        // Neither value nor extension -> violates ext-1.
        let neither = Extension { url: url(), ..Default::default() };
        assert!(neither.validate().iter().any(|i| i.message.contains("ext-1")));

        // Exactly a value -> ok.
        let value_only = Extension {
            url: url(),
            value: Some(ExtensionValue::Boolean(Primitive::new(Boolean(true)))),
            ..Default::default()
        };
        assert!(!value_only.validate().iter().any(|i| i.message.contains("ext-1")));
    }

    #[test]
    fn invariant_dom_2() {
        use crate::r4::resources::Patient;

        let patient = Patient {
            contained: vec![serde_json::json!({
                "resourceType": "Observation",
                "id": "o1",
                "contained": [{ "resourceType": "Patient", "id": "nested" }]
            })],
            ..Default::default()
        };
        assert!(patient.validate().iter().any(|i| i.message.contains("dom-2")));
    }

    #[test]
    fn derived_validate_recurses_with_dotted_path() {
        let coding = types::Coding {
            code: Some(types::Code("bad  code".to_string())),
            ..Default::default()
        };
        let issues = coding.validate();
        assert_eq!(issues.len(), 1, "{issues:?}");
        assert_eq!(issues[0].path, "code.code");
    }
}