use crate::r5::types;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ValidationIssue {
pub path: String,
pub message: String,
}
impl ValidationIssue {
fn new(path: &str, message: impl Into<String>) -> Self {
Self {
path: path.to_string(),
message: message.into(),
}
}
}
pub trait Validate {
fn validate(&self) -> Vec<ValidationIssue>;
fn is_valid(&self) -> bool {
self.validate().is_empty()
}
}
impl<T: Validate> Validate for Option<T> {
fn validate(&self) -> Vec<ValidationIssue> {
self.as_ref().map(Validate::validate).unwrap_or_default()
}
}
impl<T: Validate> Validate for Vec<T> {
fn validate(&self) -> Vec<ValidationIssue> {
self.iter().flat_map(Validate::validate).collect()
}
}
fn is_valid_code(s: &str) -> bool {
!s.is_empty()
&& s == s.trim()
&& !s.split(' ').any(str::is_empty)
&& !s.chars().any(|c| c != ' ' && c.is_whitespace())
}
fn is_valid_id(s: &str) -> bool {
(1..=64).contains(&s.len())
&& s.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '.')
}
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 self.0.trim() == self.0 && !self.0.is_empty() {
vec![]
} else {
vec![ValidationIssue::new("uri", "FHIR uri must be non-empty and not surrounded by whitespace")]
}
}
}
impl<T: Validate> Validate for Box<T> {
fn validate(&self) -> Vec<ValidationIssue> {
(**self).validate()
}
}
impl Validate for ::serde_json::Value {
fn validate(&self) -> Vec<ValidationIssue> {
Vec::new()
}
}
impl Validate for ::std::string::String {
fn validate(&self) -> Vec<ValidationIssue> {
Vec::new()
}
}
impl Validate for types::Canonical {
fn validate(&self) -> Vec<ValidationIssue> {
if !self.0.is_empty() && self.0.trim() == 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 !self.0.is_empty() && self.0.trim() == self.0 {
vec![]
} else {
vec![ValidationIssue::new("url", "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::Integer64,
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 option_and_vec() {
let good: Option<types::Code> = Some(types::Code("ok".to_string()));
assert!(good.validate().is_empty());
let bad = vec![types::Id("ok".to_string()), types::Id("no!".to_string())];
assert_eq!(bad.validate().len(), 1);
}
#[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");
let ok = types::Coding {
code: Some(types::Code("final".to_string())),
..Default::default()
};
assert!(ok.validate().is_empty());
}
}