use core::cmp::Ordering;
use openehr::aql::AqlQuery;
use openehr::base::{Interval, iso8601};
use openehr::path::Pathable;
use openehr::rm::common::{LocatableAttrs, PartyIdentified};
use openehr::rm::data_structures::{Element, ItemTree};
use openehr::rm::data_types::{
CodePhrase, DataValue, DvCount, DvDate, DvDateTime, DvIdentifier, DvMultimedia, DvOrdered,
DvQuantity, DvText,
};
use openehr::rm::ehr::{Composition, EntryAttrs, Evaluation};
use openehr::security::{ChainKey, RedactionRule, Redactor, Sensitive};
use openehr::terminology::{self, composition_category};
use openehr::validation::Validate;
const MARKER: &str = "ZZ-DISTINCTIVE-MARKER-9999";
fn at(name: &str, node: &str) -> LocatableAttrs {
LocatableAttrs::named(name, node).expect("literal attrs")
}
fn composition_containing(marker: &str) -> Composition {
let data = ItemTree::new(
at("tree", "at0001"),
vec![
Element::new(
at("HIV status", "at0011"),
DataValue::Text(DvText::new(marker).unwrap()),
)
.into(),
],
);
let evaluation = Evaluation::new(
at("Problem", "openEHR-EHR-EVALUATION.problem.v1").with_archetype_details(
openehr::rm::common::Archetyped::new("openEHR-EHR-EVALUATION.problem.v1", "1.1.0")
.unwrap(),
),
EntryAttrs::about_subject(
CodePhrase::new("ISO_639-1", "en").unwrap(),
CodePhrase::new("IANA_character-sets", "UTF-8").unwrap(),
),
data.into(),
);
Composition::new(
at("Encounter", "openEHR-EHR-COMPOSITION.encounter.v1").with_archetype_details(
openehr::rm::common::Archetyped::new("openEHR-EHR-COMPOSITION.encounter.v1", "1.1.0")
.unwrap(),
),
composition_category::EVENT,
PartyIdentified::named("Dr A Nurse").unwrap().into(),
CodePhrase::new("ISO_639-1", "en").unwrap(),
CodePhrase::new("ISO_3166-1", "GB").unwrap(),
)
.unwrap()
.with_content(evaluation.into())
}
#[test]
fn display_never_reveals_an_identifier_or_a_media_blob() {
let id = DvIdentifier::new(MARKER)
.unwrap()
.with_type("NHS number")
.with_issuer("NHS England");
assert!(!format!("{id}").contains(MARKER));
assert_eq!(format!("{id}"), "NHS number issued by NHS England");
let media = DvMultimedia::inline(
CodePhrase::new("IANA_media-types", "image/png").unwrap(),
MARKER.as_bytes().to_vec(),
);
assert!(!format!("{media:?}").contains(MARKER));
let wrapped = Sensitive::new(MARKER.to_owned());
assert!(!format!("{wrapped}").contains(MARKER));
assert!(!format!("{wrapped:?}").contains(MARKER));
assert_eq!(wrapped.expose(), MARKER);
}
#[test]
fn no_construction_error_echoes_a_submitted_value() {
let failures: Vec<String> = vec![
DvText::new("").unwrap_err().to_string(),
DvQuantity::new(f64::NAN, "mg").unwrap_err().to_string(),
DvQuantity::new(1.0, "").unwrap_err().to_string(),
DvIdentifier::new("").unwrap_err().to_string(),
Element::new_null(at("x", "at0001"), "999")
.unwrap_err()
.to_string(),
DvDate::new(MARKER).unwrap_err().to_string(),
];
for message in &failures[..5] {
assert!(!message.contains(MARKER), "{message}");
assert!(message.contains("invalid"), "{message}");
}
assert!(
failures[5].contains(MARKER),
"lexical errors do name their input"
);
}
#[test]
fn a_validation_report_names_paths_and_never_values() {
let json = format!(r#"{{"name": {{"value": "{MARKER}"}}, "archetype_node_id": "at0004"}}"#);
let element: Element = serde_json::from_str(&json).unwrap();
let report = element.validate();
assert!(!report.is_empty());
assert!(!report.to_string().contains(MARKER), "{report}");
}
#[test]
fn a_chain_checkpoint_carries_no_patient_data() {
let key = ChainKey::new("k1", vec![3u8; 32]).unwrap();
let mut chain = openehr::security::Chain::new();
chain
.append("uid::sys::1", &composition_containing(MARKER), Some(&key))
.unwrap();
let checkpoint = chain.checkpoint();
assert!(!checkpoint.contains(MARKER), "{checkpoint}");
assert!(checkpoint.contains("entries=1"));
}
#[test]
fn redaction_masks_and_reports_a_count_not_a_category() {
let (redacted, count) = Redactor::new()
.with_rule(RedactionRule::node_id("at0011"))
.redact_counting(&composition_containing(MARKER))
.unwrap();
let json = serde_json::to_string(&redacted).unwrap();
assert!(!json.contains(MARKER), "the value survived redaction");
assert!(json.contains(terminology::null_flavour::MASKED));
assert!(json.contains("Dr A Nurse"));
let element = redacted
.item_at_path("/content/data/items[at0011]")
.expect("the element is still there");
assert_eq!(element.type_name(), "ELEMENT");
assert_eq!(count.masked, 1);
assert!(!count.to_string().contains("HIV"));
assert!(redacted.validate().is_empty());
}
#[test]
fn every_undecidable_comparison_answers_none() {
let month: iso8601::Date = "2024-05".parse().unwrap();
let day: iso8601::Date = "2024-05-17".parse().unwrap();
assert_eq!(month.semantic_cmp(&day), None);
let local: iso8601::Time = "11:00:00".parse().unwrap();
let utc: iso8601::Time = "11:00:00Z".parse().unwrap();
assert_eq!(local.semantic_cmp(&utc), None);
let twelve_months: iso8601::Duration = "P12M".parse().unwrap();
let one_year: iso8601::Duration = "P1Y".parse().unwrap();
assert_eq!(twelve_months.semantic_cmp(&one_year), None);
let mg = DataValue::Quantity(DvQuantity::new(5.0, "mg").unwrap());
let ml = DataValue::Quantity(DvQuantity::new(5.0, "mL").unwrap());
assert_eq!(mg.semantic_cmp(&ml), None);
assert!(!mg.is_strictly_comparable_to(&ml));
let count = DataValue::Count(openehr::rm::data_types::DvCount::new(5));
assert_eq!(mg.semantic_cmp(&count), None);
let april: iso8601::Date = "2024-04".parse().unwrap();
assert_eq!(april.semantic_cmp(&day), Some(Ordering::Less));
let more = DataValue::Quantity(DvQuantity::new(6.0, "mg").unwrap());
assert_eq!(mg.semantic_cmp(&more), Some(Ordering::Less));
}
#[test]
fn an_ambiguous_path_refuses_instead_of_choosing() {
let data = ItemTree::new(
at("tree", "at0001"),
vec![
Element::new(
at("Systolic", "at0004"),
DataValue::Quantity(DvQuantity::new(184.0, "mm[Hg]").unwrap()),
)
.into(),
Element::new(
at("Diastolic", "at0005"),
DataValue::Quantity(DvQuantity::new(96.0, "mm[Hg]").unwrap()),
)
.into(),
],
);
let data: openehr::rm::data_structures::ItemStructure = data.into();
assert!(data.path_exists("/items/value/magnitude").unwrap());
assert!(!data.path_unique("/items/value/magnitude").unwrap());
assert_eq!(
data.items_at_path("/items/value/magnitude").unwrap().len(),
2
);
assert!(data.item_at_path("/items/value/magnitude").is_err());
assert!(
data.item_at_path("/items['Diastolic']/value/magnitude")
.is_ok()
);
}
#[test]
fn an_interval_refuses_bounds_it_cannot_order() {
let month = DvDate::new("2024-05").unwrap();
let day = DvDate::new("2024-05-17").unwrap();
assert!(Interval::closed(month, day).is_err());
let ok = Interval::closed(
DvDate::new("2024-04").unwrap(),
DvDate::new("2024-05-17").unwrap(),
);
assert!(ok.is_ok());
}
#[test]
fn unimplemented_operations_refuse_and_cite_the_spec() {
use openehr::rm::data_structures::{IntervalEvent, ItemSingle};
let data = ItemSingle::new(
at("d", "at0001"),
Element::new(
at("v", "at0002"),
DataValue::Count(openehr::rm::data_types::DvCount::new(1)),
),
);
let event = IntervalEvent::new(
at("monthly", "at0006"),
DvDateTime::new("2026-03-31T08:00:00Z").unwrap(),
data.into(),
openehr::rm::data_types::DvDuration::new("P1M").unwrap(),
terminology::event_math_function::TOTAL,
)
.unwrap();
let err = event.interval_start_time().unwrap_err();
assert!(matches!(err, openehr::Error::Unsupported { .. }));
assert!(err.to_string().contains("spec/"), "{err}");
}
#[test]
fn the_four_null_flavours_remain_four() {
let flavours = [
terminology::null_flavour::NO_INFORMATION,
terminology::null_flavour::UNKNOWN,
terminology::null_flavour::MASKED,
terminology::null_flavour::NOT_APPLICABLE,
];
let mut codes = std::collections::HashSet::new();
for code in flavours {
let element = Element::new_null(at("x", "at0001"), code).unwrap();
assert!(element.is_null());
assert_eq!(element.null_flavour_code(), Some(code));
assert_eq!(
element.is_masked(),
code == terminology::null_flavour::MASKED
);
codes.insert(code);
let json = serde_json::to_string(&element).unwrap();
let back: Element = serde_json::from_str(&json).unwrap();
assert_eq!(back.null_flavour_code(), Some(code));
}
assert_eq!(codes.len(), 4);
assert!(Element::new_null(at("x", "at0001"), "999").is_err());
}
#[test]
fn aql_refuses_what_it_does_not_model_and_says_where_that_is_recorded() {
for text in ["SELECT * FROM COMPOSITION c", "SELECT c/uid FROM VERSION v"] {
let err = text.parse::<AqlQuery>().unwrap_err();
assert!(err.reason.contains("Q12.9"), "{err}");
}
}
#[test]
fn aql_catches_a_path_rooted_at_an_unbound_alias() {
let query: AqlQuery = "SELECT o/value FROM COMPOSITION c CONTAINS OBSERVATION obs"
.parse()
.unwrap();
assert!(query.check().is_err());
let fixed: AqlQuery = "SELECT obs/value FROM COMPOSITION c CONTAINS OBSERVATION obs"
.parse()
.unwrap();
assert!(fixed.check().is_ok());
}
#[test]
fn no_document_this_crate_can_build_carries_a_non_finite_float() {
use openehr::rm::data_types::{DvCodedText, DvProportion, DvScale, ProportionKind};
let symbol = || {
DvCodedText::new("symbol", CodePhrase::new("local", "at0001").unwrap()).unwrap()
};
for (name, value) in [
("NaN", f64::NAN),
("+inf", f64::INFINITY),
("-inf", f64::NEG_INFINITY),
] {
assert!(
DvQuantity::new(value, "mm[Hg]").is_err(),
"DV_QUANTITY accepted a magnitude of {name}"
);
assert!(
DvScale::new(value, symbol()).is_err(),
"DV_SCALE accepted a value of {name}"
);
assert!(
DvQuantity::new(1.0, "mm[Hg]")
.unwrap()
.with_accuracy(value, false)
.is_err(),
"DV_AMOUNT accepted an accuracy of {name}"
);
assert!(
DvProportion::new(value, 1.0, ProportionKind::Ratio).is_err(),
"DV_PROPORTION accepted a numerator of {name}"
);
assert!(
DvProportion::new(1.0, value, ProportionKind::Ratio).is_err(),
"DV_PROPORTION accepted a denominator of {name}"
);
}
assert_eq!(serde_json::to_string(&f64::NAN).unwrap(), "null");
assert_eq!(
openehr::security::to_canonical_string(&f64::INFINITY).unwrap(),
"null"
);
}
#[test]
fn a_forged_tag_is_refused() {
let key = ChainKey::new("k1", vec![7u8; 32]).unwrap();
let other = ChainKey::new("k1", vec![9u8; 32]).unwrap();
let mut chain = openehr::security::Chain::new();
chain.append("v1", &"content", Some(&key)).unwrap();
assert!(matches!(
chain.verify(&[&key]),
openehr::security::ChainStatus::Verified
));
assert!(matches!(
chain.verify(&[&other]),
openehr::security::ChainStatus::Broken {
reason: openehr::security::BreakReason::TagMismatch,
..
}
));
}
#[test]
fn every_redaction_rule_kind_withholds_what_it_names() {
let masked = |rule: RedactionRule| {
let (redacted, count) = Redactor::new()
.with_rule(rule)
.redact_counting(&composition_containing(MARKER))
.unwrap();
let json = serde_json::to_string(&redacted).unwrap();
(json.contains(MARKER), count.masked)
};
assert_eq!(masked(RedactionRule::node_id("at0011")), (false, 1));
assert_eq!(masked(RedactionRule::name("HIV status")), (false, 1));
assert_eq!(
masked(RedactionRule::archetype_root(
"openEHR-EHR-EVALUATION.problem.v1"
)),
(false, 1)
);
assert_eq!(masked(RedactionRule::node_id("at9999")), (true, 0));
assert_eq!(masked(RedactionRule::name("Blood pressure")), (true, 0));
assert_eq!(
masked(RedactionRule::archetype_root("openEHR-EHR-OBSERVATION.other.v1")),
(true, 0)
);
}
#[test]
fn a_redaction_count_reports_numbers_and_the_rules_are_kept() {
let redactor = Redactor::new()
.with_rule(RedactionRule::node_id("at0011"))
.with_rule(RedactionRule::name("something else"));
assert_eq!(redactor.rules().len(), 2, "a rule was dropped");
let (_, count) = redactor
.redact_counting(&composition_containing(MARKER))
.unwrap();
assert_eq!(count.masked, 1);
assert!(count.examined >= count.masked);
let shown = count.to_string();
assert!(shown.contains('1'), "no number in {shown:?}");
assert!(!shown.contains("HIV"), "a count must not name what it withheld");
}
#[test]
fn redaction_distinguishes_a_leaf_from_a_branch_by_shape() {
use openehr::rm::data_structures::{Cluster, Item, ItemTree};
let leaf = |name: &str, node: &str, text: &str| {
Item::Element(Element::new(
at(name, node),
DataValue::Text(DvText::new(text).unwrap()),
))
};
let tree = ItemTree::new(
at("tree", "at0001"),
vec![
leaf("Top", "at0020", "top"),
Item::Cluster(
Cluster::new(
at("Group", "at0021"),
vec![leaf("Inner A", "at0022", "a"), leaf("Inner B", "at0023", "b")],
)
.unwrap(),
),
],
);
let evaluation = Evaluation::new(
at("Problem", "openEHR-EHR-EVALUATION.problem.v1").with_archetype_details(
openehr::rm::common::Archetyped::new("openEHR-EHR-EVALUATION.problem.v1", "1.1.0")
.unwrap(),
),
EntryAttrs::about_subject(
CodePhrase::new("ISO_639-1", "en").unwrap(),
CodePhrase::new("IANA_character-sets", "UTF-8").unwrap(),
),
tree.into(),
);
let composition = Composition::new(
at("Encounter", "openEHR-EHR-COMPOSITION.encounter.v1").with_archetype_details(
openehr::rm::common::Archetyped::new("openEHR-EHR-COMPOSITION.encounter.v1", "1.1.0")
.unwrap(),
),
composition_category::EVENT,
PartyIdentified::named("Dr A Nurse").unwrap().into(),
CodePhrase::new("ISO_639-1", "en").unwrap(),
CodePhrase::new("ISO_3166-1", "GB").unwrap(),
)
.unwrap()
.with_content(evaluation.into());
let (_, count) = Redactor::new()
.with_rule(RedactionRule::node_id("at9999"))
.redact_counting(&composition)
.unwrap();
assert_eq!(count.examined, 3, "a branch was counted as a leaf");
assert_eq!(count.masked, 0);
let (redacted, count) = Redactor::new()
.with_rule(RedactionRule::node_id("at0021"))
.redact_counting(&composition)
.unwrap();
assert_eq!(count.masked, 0, "a cluster was masked as though it were a value");
let json = serde_json::to_string(&redacted).unwrap();
assert!(json.contains('a') && json.contains('b'), "the branch survived");
let (_, count) = Redactor::new()
.with_rule(RedactionRule::node_id("at0022"))
.redact_counting(&composition)
.unwrap();
assert_eq!(count.masked, 1);
}
#[test]
fn an_untagged_element_is_still_recognised_and_withheld() {
use openehr::rm::data_structures::ItemSingle;
let composition = Composition::new(
at("Encounter", "openEHR-EHR-COMPOSITION.encounter.v1").with_archetype_details(
openehr::rm::common::Archetyped::new("openEHR-EHR-COMPOSITION.encounter.v1", "1.1.0")
.unwrap(),
),
composition_category::EVENT,
PartyIdentified::named("Dr A Nurse").unwrap().into(),
CodePhrase::new("ISO_639-1", "en").unwrap(),
CodePhrase::new("ISO_3166-1", "GB").unwrap(),
)
.unwrap()
.with_content(
Evaluation::new(
at("Problem", "openEHR-EHR-EVALUATION.problem.v1").with_archetype_details(
openehr::rm::common::Archetyped::new("openEHR-EHR-EVALUATION.problem.v1", "1.1.0")
.unwrap(),
),
EntryAttrs::about_subject(
CodePhrase::new("ISO_639-1", "en").unwrap(),
CodePhrase::new("IANA_character-sets", "UTF-8").unwrap(),
),
ItemSingle::new(
at("single", "at0030"),
Element::new(
at("HIV status", "at0031"),
DataValue::Text(DvText::new(MARKER).unwrap()),
),
)
.into(),
)
.into(),
);
let raw = serde_json::to_string(&composition).unwrap();
assert!(raw.contains(MARKER));
assert!(
!raw.contains(r#""at0031","_type":"ELEMENT""#),
"the element is expected to be untagged here"
);
let (redacted, count) = Redactor::new()
.with_rule(RedactionRule::node_id("at0031"))
.redact_counting(&composition)
.unwrap();
assert_eq!(count.masked, 1, "an untagged element was not recognised");
assert!(!serde_json::to_string(&redacted).unwrap().contains(MARKER));
}
#[test]
fn a_uri_that_never_saw_a_constructor_is_reported_rather_than_panicking() {
let uri: openehr::rm::data_types::DvUri =
serde_json::from_str(r#"{"value":"nocolon"}"#).expect("serde writes the field in");
assert_eq!(uri.scheme(), "");
assert_eq!(uri.rest(), "");
let report = DataValue::Uri(uri).validate();
let named: Vec<_> = report
.violations()
.iter()
.map(|v| (v.class, v.invariant))
.collect();
assert!(
named.contains(&("DV_URI", "Uri_well_formed")),
"a malformed URI must be reported, got {named:?}"
);
}
#[test]
fn an_empty_uri_is_reported_under_openehrs_own_invariant_name() {
let uri: openehr::rm::data_types::DvUri =
serde_json::from_str(r#"{"value":""}"#).expect("serde writes the field in");
let report = DataValue::Uri(uri).validate();
let named: Vec<_> = report
.violations()
.iter()
.map(|v| (v.class, v.invariant))
.collect();
assert_eq!(named, vec![("DV_URI", "Value_valid")], "got {named:?}");
}
#[test]
fn an_ehr_uri_deserialized_with_a_foreign_scheme_is_reported() {
let uri: openehr::rm::data_types::DvEhrUri =
serde_json::from_str(r#"{"value":"https://example.org/x"}"#)
.expect("serde writes the field in");
assert_eq!(uri.scheme(), "https", "the parse gate is the one it skipped");
let report = DataValue::EhrUri(uri).validate();
let named: Vec<_> = report
.violations()
.iter()
.map(|v| (v.class, v.invariant))
.collect();
assert_eq!(named, vec![("DV_EHR_URI", "Scheme_valid")], "got {named:?}");
}
#[test]
fn a_link_target_is_validated_on_every_locatable_that_carries_it() {
let element: Element = serde_json::from_str(
r#"{
"name": {"value": "Problem"},
"archetype_node_id": "at0002",
"links": [{
"meaning": {"value": "because of"},
"type": {"value": "issue"},
"target": {"value": "https://example.org/elsewhere"}
}],
"value": {"_type": "DV_TEXT", "value": "hypertension"}
}"#,
)
.expect("serde writes the fields in");
let report = element.validate();
let found = report
.violations()
.iter()
.find(|v| v.class == "DV_EHR_URI")
.expect("the link target must be reported");
assert_eq!(found.invariant, "Scheme_valid");
assert_eq!(
found.path, "/links[0]/target",
"a violation must name the path to the offending node (`L10.4`)"
);
}
#[test]
fn equality_and_order_disagree_by_design_and_neither_is_partial_ord() {
fn distinct_but_ordered_equal<T>(what: &str, a: &T, b: &T)
where
T: PartialEq + core::fmt::Debug + DvOrdered,
{
assert_ne!(a, b, "{what}: these are different stored values");
assert_eq!(
a.semantic_cmp(b),
Some(Ordering::Equal),
"{what}: they denote the same point and must order equal"
);
}
let range = openehr::base::Interval::closed(
DataValue::Count(DvCount::new(0)),
DataValue::Count(DvCount::new(10)),
)
.unwrap();
distinct_but_ordered_equal(
"DV_DATE_TIME written two ways",
&DvDateTime::new("2026-08-01T11:00:00Z").unwrap(),
&DvDateTime::new("2026-08-01T12:00:00+01:00").unwrap(),
);
distinct_but_ordered_equal(
"DV_QUANTITY differing only in precision",
&DvQuantity::new(5.0, "mg").unwrap().with_precision(1).unwrap(),
&DvQuantity::new(5.0, "mg").unwrap().with_precision(2).unwrap(),
);
distinct_but_ordered_equal(
"DV_QUANTITY differing only in units_display_name",
&DvQuantity::new(5.0, "mg").unwrap().with_units_display_name("mg"),
&DvQuantity::new(5.0, "mg").unwrap(),
);
distinct_but_ordered_equal(
"DV_COUNT differing only in its normal range",
&DvCount::new(5).with_normal_range(range),
&DvCount::new(5),
);
let utc = DataValue::DateTime(DvDateTime::new("2026-08-01T11:00:00Z").unwrap());
let offset = DataValue::DateTime(DvDateTime::new("2026-08-01T12:00:00+01:00").unwrap());
assert_ne!(utc, offset);
assert_eq!(utc.semantic_cmp(&offset), Some(Ordering::Equal));
}
#[test]
fn a_reference_range_is_unmoved_by_how_an_instant_is_spelled() {
let range = openehr::base::Interval::closed(
DvDateTime::new("2026-08-01T11:00:00Z").unwrap(),
DvDateTime::new("2026-08-01T13:00:00Z").unwrap(),
)
.unwrap();
assert!(range.contains(&DvDateTime::new("2026-08-01T11:00:00Z").unwrap()));
assert!(range.contains(&DvDateTime::new("2026-08-01T12:00:00+01:00").unwrap()));
assert!(!range.contains(&DvDateTime::new("2026-08-01T14:00:00Z").unwrap()));
let month = DvDateTime::new("2026-08").unwrap();
assert_eq!(month.semantic_cmp(&DvDateTime::new("2026-08-01T11:00:00Z").unwrap()), None);
assert!(!range.contains(&month), "an incomparable value was admitted");
let open = openehr::base::Interval::open(
DvDateTime::new("2026-08-01T11:00:00Z").unwrap(),
DvDateTime::new("2026-08-01T13:00:00Z").unwrap(),
)
.unwrap();
assert!(!open.contains(&DvDateTime::new("2026-08-01T11:00:00Z").unwrap()));
assert!(!open.contains(&DvDateTime::new("2026-08-01T12:00:00+01:00").unwrap()));
assert!(open.contains(&DvDateTime::new("2026-08-01T12:00:00Z").unwrap()));
}
#[test]
fn an_aql_string_literal_is_not_mangled_by_the_lexer() {
for text in ["Müller", "日本語", "Ω", "naïve café", "\u{59a}\u{7fc}"] {
let query: AqlQuery = format!("SELECT c FROM EHR e WHERE c/name/value = '{text}'")
.parse()
.unwrap_or_else(|e| panic!("{text:?} must parse: {e}"));
assert!(
query.to_string().contains(text),
"{text:?} was corrupted to {:?}",
query.to_string()
);
}
}
#[test]
fn aql_rendering_round_trips_through_the_parser() {
for text in [
"SELECT c FROM (EHR e CONTAINS COMPOSITION c) OR EHR x",
"SELECT c FROM EHR e CONTAINS (COMPOSITION c OR EHR x)",
"SELECT c FROM (EHR e CONTAINS COMPOSITION c) CONTAINS OBSERVATION o",
"SELECT c FROM EHR e CONTAINS COMPOSITION c",
r"SELECT c FROM EHR e WHERE c/name/value = 'it\'s'",
r"SELECT c FROM EHR e WHERE c/name/value = 'back\\slash'",
"SELECT c FROM EHR e WHERE c/name/value = 'Müller'",
] {
let once: AqlQuery = text.parse().unwrap_or_else(|e| panic!("{text:?}: {e}"));
let rendered = once.to_string();
let twice: AqlQuery = rendered
.parse()
.unwrap_or_else(|e| panic!("{text:?} rendered to {rendered:?}, which will not reparse: {e}"));
assert_eq!(
twice.to_string(),
rendered,
"rendering {text:?} is not idempotent"
);
assert_eq!(twice, once, "reparsing {text:?} produced a different query");
}
}
#[test]
fn a_high_precision_magnitude_survives_repeated_canonical_round_trips() {
let source = r#"{"_type":"DV_QUANTITY","magnitude":0.000000444444444444444444444444444444444444444444441569995,"units":"m"}"#;
let mut value: DataValue = serde_json::from_str(source).expect("parses");
let mut seen = Vec::new();
for _ in 0..4 {
let canonical = openehr::security::to_canonical_string(&value).expect("canonicalises");
seen.push(canonical.clone());
value = serde_json::from_str(&canonical).expect("re-parses");
}
assert_eq!(
seen[0], seen[1],
"the magnitude moved between the first and second canonicalisation — \
`float_roundtrip` may have been dropped from serde_json's features (A-38)"
);
assert!(
seen.iter().all(|s| *s == seen[0]),
"canonical form is not a fixed point: {seen:?}"
);
let text = "1.5777777777770001";
assert_eq!(
serde_json::from_str::<f64>(text).expect("parses").to_bits(),
text.parse::<f64>().expect("parses").to_bits(),
"serde_json and core disagree about a float by one ULP (A-38)"
);
}
#[test]
fn a_caller_can_read_every_code_the_crate_declines_to_check() {
let composition = composition_containing("irrelevant");
assert_eq!(composition.language().code_string(), "en");
assert_eq!(composition.territory().code_string(), "GB");
let entry = composition
.entries()
.next()
.expect("the fixture holds one entry");
assert_eq!(entry.entry_attrs().language().code_string(), "en");
assert_eq!(entry.entry_attrs().encoding().code_string(), "UTF-8");
let plain = DvText::new("x").unwrap();
assert!(plain.language().is_none() && plain.encoding().is_none());
let tagged = DvText::new("x")
.unwrap()
.with_language(CodePhrase::new("ISO_639-1", "fr").unwrap())
.with_encoding(CodePhrase::new("IANA_character-sets", "UTF-8").unwrap());
assert_eq!(tagged.language().expect("set above").code_string(), "fr");
assert_eq!(tagged.encoding().expect("set above").code_string(), "UTF-8");
let multimedia: DvMultimedia = serde_json::from_str(
r#"{
"media_type": {"terminology_id": {"value": "IANA_media-types"},
"code_string": "image/png"},
"charset": {"terminology_id": {"value": "IANA_character-sets"},
"code_string": "UTF-8"},
"language": {"terminology_id": {"value": "ISO_639-1"}, "code_string": "de"}
}"#,
)
.expect("a multimedia value with all three codes");
assert_eq!(multimedia.media_type().code_string(), "image/png");
let encapsulated = multimedia.encapsulated();
assert_eq!(
encapsulated.charset().expect("set above").code_string(),
"UTF-8"
);
assert_eq!(
encapsulated.language().expect("set above").code_string(),
"de"
);
}