use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Severity {
Error,
Warning,
Information,
}
impl Severity {
fn rank(&self) -> u8 {
match self {
Severity::Error => 2,
Severity::Warning => 1,
Severity::Information => 0,
}
}
}
impl PartialOrd for Severity {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Severity {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.rank().cmp(&other.rank())
}
}
impl std::fmt::Display for Severity {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Severity::Error => write!(f, "error"),
Severity::Warning => write!(f, "warning"),
Severity::Information => write!(f, "information"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum BindingStrength {
Required,
Extensible,
Preferred,
Example,
}
impl BindingStrength {
pub fn from_fhir_str(s: &str) -> Option<Self> {
match s {
"required" => Some(BindingStrength::Required),
"extensible" => Some(BindingStrength::Extensible),
"preferred" => Some(BindingStrength::Preferred),
"example" => Some(BindingStrength::Example),
_ => None,
}
}
}
impl std::fmt::Display for BindingStrength {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
BindingStrength::Required => write!(f, "required"),
BindingStrength::Extensible => write!(f, "extensible"),
BindingStrength::Preferred => write!(f, "preferred"),
BindingStrength::Example => write!(f, "example"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ElementBinding {
pub path: String,
pub strength: BindingStrength,
pub value_set_url: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
}
impl ElementBinding {
pub fn new(
path: impl Into<String>,
strength: BindingStrength,
value_set_url: impl Into<String>,
) -> Self {
Self {
path: path.into(),
strength,
value_set_url: value_set_url.into(),
description: None,
}
}
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct Invariant {
pub key: String,
pub severity: Severity,
pub human: String,
pub expression: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub xpath: Option<String>,
}
impl Invariant {
pub fn new(
key: impl Into<String>,
severity: Severity,
human: impl Into<String>,
expression: impl Into<String>,
) -> Self {
Self {
key: key.into(),
severity,
human: human.into(),
expression: expression.into(),
xpath: None,
}
}
pub fn with_xpath(mut self, xpath: impl Into<String>) -> Self {
self.xpath = Some(xpath.into());
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ElementCardinality {
pub path: String,
pub min: usize,
pub max: Option<usize>,
}
impl ElementCardinality {
pub fn new(path: impl Into<String>, min: usize, max: Option<usize>) -> Self {
Self {
path: path.into(),
min,
max,
}
}
pub fn is_unbounded(&self) -> bool {
self.max.is_none()
}
pub fn is_required(&self) -> bool {
self.min > 0
}
pub fn is_array(&self) -> bool {
self.max.is_none_or(|m| m > 1)
}
pub fn to_fhir_notation(&self) -> String {
match self.max {
None => format!("{}..*", self.min),
Some(max) => format!("{}..{}", self.min, max),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_severity_ordering() {
assert!(Severity::Error > Severity::Warning);
assert!(Severity::Warning > Severity::Information);
}
#[test]
fn test_invariant_creation() {
let inv = Invariant::new(
"pat-1",
Severity::Error,
"Name is required",
"name.exists()",
);
assert_eq!(inv.key, "pat-1");
assert_eq!(inv.severity, Severity::Error);
assert_eq!(inv.human, "Name is required");
assert_eq!(inv.expression, "name.exists()");
assert!(inv.xpath.is_none());
}
#[test]
fn test_invariant_with_xpath() {
let inv = Invariant::new("obs-1", Severity::Warning, "Code required", "code.exists()")
.with_xpath("f:code");
assert!(inv.xpath.is_some());
}
#[test]
fn test_element_cardinality_creation() {
let card = ElementCardinality::new("Patient.identifier", 0, None);
assert_eq!(card.path, "Patient.identifier");
assert_eq!(card.min, 0);
assert!(card.max.is_none());
assert!(card.is_unbounded());
assert!(!card.is_required());
assert!(card.is_array());
}
#[test]
fn test_element_cardinality_required_single() {
let card = ElementCardinality::new("Observation.code", 1, Some(1));
assert!(card.is_required());
assert!(!card.is_unbounded());
assert!(!card.is_array());
assert_eq!(card.to_fhir_notation(), "1..1");
}
#[test]
fn test_element_cardinality_optional_array() {
let card = ElementCardinality::new("Patient.name", 0, None);
assert!(!card.is_required());
assert!(card.is_unbounded());
assert!(card.is_array());
assert_eq!(card.to_fhir_notation(), "0..*");
}
#[test]
fn test_element_cardinality_bounded_array() {
let card = ElementCardinality::new("Patient.photo", 0, Some(5));
assert!(!card.is_required());
assert!(!card.is_unbounded());
assert!(card.is_array());
assert_eq!(card.to_fhir_notation(), "0..5");
}
#[test]
fn test_invariant_with_xpath_value() {
let inv = Invariant::new("obs-1", Severity::Warning, "Code required", "code.exists()")
.with_xpath("f:code");
assert_eq!(inv.xpath.unwrap(), "f:code");
}
#[test]
fn test_invariant_serialization() {
let inv = Invariant::new("test-1", Severity::Error, "Test", "true");
let json = serde_json::to_string(&inv).unwrap();
assert!(json.contains("test-1"));
assert!(json.contains("error"));
}
#[test]
fn test_binding_strength_parsing() {
assert_eq!(
BindingStrength::from_fhir_str("required"),
Some(BindingStrength::Required)
);
assert_eq!(
BindingStrength::from_fhir_str("extensible"),
Some(BindingStrength::Extensible)
);
assert_eq!(
BindingStrength::from_fhir_str("preferred"),
Some(BindingStrength::Preferred)
);
assert_eq!(
BindingStrength::from_fhir_str("example"),
Some(BindingStrength::Example)
);
assert_eq!(BindingStrength::from_fhir_str("invalid"), None);
}
#[test]
fn test_element_binding_creation() {
let binding = ElementBinding::new(
"Patient.gender",
BindingStrength::Required,
"http://hl7.org/fhir/ValueSet/administrative-gender",
);
assert_eq!(binding.path, "Patient.gender");
assert_eq!(binding.strength, BindingStrength::Required);
assert_eq!(
binding.value_set_url,
"http://hl7.org/fhir/ValueSet/administrative-gender"
);
assert!(binding.description.is_none());
}
#[test]
fn test_element_binding_with_description() {
let binding = ElementBinding::new(
"Patient.gender",
BindingStrength::Required,
"http://hl7.org/fhir/ValueSet/administrative-gender",
)
.with_description("The gender of the patient");
assert!(binding.description.is_some());
assert_eq!(binding.description.unwrap(), "The gender of the patient");
}
}