use crate::r3::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::r3::resources::operation_outcome::OperationOutcomeIssue {
fn from(issue: ValidationIssue) -> Self {
use crate::r3::codes::{IssueSeverity, IssueType};
use crate::r3::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()
}
}
}
impl From<Vec<ValidationIssue>> for crate::r3::resources::operation_outcome::OperationOutcome {
fn from(issues: Vec<ValidationIssue>) -> Self {
use crate::r3::codes::{IssueSeverity, IssueType};
use crate::r3::coded::Coded;
use crate::r3::resources::operation_outcome::{OperationOutcome, OperationOutcomeIssue};
let mut items: Vec<OperationOutcomeIssue> = issues.into_iter().map(Into::into).collect();
if items.is_empty() {
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 {
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",
)]
}
}
}
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::r3::coded::Coded;
use crate::r3::codes::AdministrativeGender;
use crate::r3::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::r3::choice::Primitive;
use crate::r3::types::extension::{Extension, ExtensionValue};
use crate::r3::types::{Boolean, Uri};
let url = || Uri("http://example.org/x".to_string());
let neither = Extension { url: url(), ..Default::default() };
assert!(neither.validate().iter().any(|i| i.message.contains("ext-1")));
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::r3::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");
}
}