mod parser;
use indexmap::IndexMap;
use crate::v1_1::lexer::Token;
use crate::v1_1::position::line_col;
#[derive(Debug, Clone, PartialEq)]
pub enum OdinValue {
Object(IndexMap<String, OdinValue>),
KeyedList(Vec<(OdinKey, OdinValue)>),
List(Vec<OdinValue>),
Typed {
rm_type: String,
value: Box<OdinValue>,
},
PathList(Vec<String>),
Empty,
String(String),
Integer(i64),
Real(f64),
Boolean(bool),
Character(char),
Date(String),
Time(String),
DateTime(String),
Duration(String),
Interval(OdinInterval),
TermCode(String),
Uri(String),
PlugIn {
syntax: String,
text: String,
},
Path(String),
ListContinue,
}
impl OdinValue {
#[must_use]
pub fn paths(&self) -> Vec<String> {
let mut out = Vec::new();
collect_paths(self, "", &mut out);
out
}
}
fn collect_paths(value: &OdinValue, prefix: &str, out: &mut Vec<String>) {
match value {
OdinValue::Object(members) => {
for (name, child) in members {
let path = format!("{prefix}/{name}");
out.push(path.clone());
collect_paths(child, &path, out);
}
}
OdinValue::KeyedList(entries) => {
for (key, child) in entries {
let path = if prefix.is_empty() || prefix.ends_with(']') {
format!("{prefix}/[{}]", key.path_text())
} else {
format!("{prefix}[{}]", key.path_text())
};
out.push(path.clone());
collect_paths(child, &path, out);
}
}
OdinValue::Typed { value: inner, .. } => collect_paths(inner, prefix, out),
_ => {}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum OdinKey {
String(String),
Integer(i64),
Date(String),
Time(String),
DateTime(String),
}
impl OdinKey {
#[must_use]
pub fn path_text(&self) -> String {
match self {
Self::String(text) => format!("\"{text}\""),
Self::Integer(value) => value.to_string(),
Self::Date(text) | Self::Time(text) | Self::DateTime(text) => text.clone(),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum OdinInterval {
Range {
lower: Option<Box<OdinValue>>,
lower_included: bool,
upper: Option<Box<OdinValue>>,
upper_included: bool,
},
PlusMinus {
centre: Box<OdinValue>,
delta: Box<OdinValue>,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum OdinErrorKind {
UnrecognisedToken,
UnexpectedToken,
DuplicateAttribute(String),
DuplicateKey(String),
NestingTooDeep(usize),
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[error("ODIN syntax error at line {line}, column {column}: {message}")]
pub struct OdinError {
pub kind: OdinErrorKind,
pub message: String,
pub line: usize,
pub column: usize,
pub span: std::ops::Range<usize>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct OdinDocument {
pub schema: Option<OdinSchemaId>,
pub root: OdinValue,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OdinSchemaId {
pub name: String,
pub uri: String,
}
pub fn parse(src: &str) -> Result<OdinValue, OdinError> {
parse_document(src).map(|document| document.root)
}
pub fn parse_document(src: &str) -> Result<OdinDocument, OdinError> {
let spanned =
crate::v1_1::lexer::lex(crate::v1_1::lexer::Language::Odin, src).map_err(|failure| {
let (line, column) = line_col(src, failure.span.start);
OdinError {
kind: OdinErrorKind::UnrecognisedToken,
message: "unrecognised token".to_owned(),
line,
column,
span: failure.span,
}
})?;
if let Err((index, exceeded)) = crate::nesting::check_bracket_nesting(
&spanned,
|t| t.token == Token::SymLt,
|t| t.token == Token::SymGt,
) {
let offset = spanned.get(index).map_or(src.len(), |t| t.span.start);
let (line, column) = line_col(src, offset);
return Err(OdinError {
kind: OdinErrorKind::NestingTooDeep(exceeded.limit),
message: exceeded.to_string(),
line,
column,
span: offset..offset,
});
}
parser::parse_tokens(&spanned).map_err(|located| {
let (line, column) = line_col(src, located.offset);
let (kind, message) = match located.failure {
parser::Failure::Syntax(_) => (
OdinErrorKind::UnexpectedToken,
"unexpected token".to_owned(),
),
parser::Failure::DuplicateAttribute { name, .. } => (
OdinErrorKind::DuplicateAttribute(name.clone()),
format!("duplicate sibling attribute {name:?} (VDATU)"),
),
parser::Failure::DuplicateKey { key, .. } => (
OdinErrorKind::DuplicateKey(key.clone()),
format!("duplicate sibling container key [{key}] (VDOBU)"),
),
};
OdinError {
kind,
message,
line,
column,
span: located.offset..located.offset,
}
})
}
#[cfg(test)]
mod tests {
use super::*;
fn obj(src: &str) -> IndexMap<String, OdinValue> {
match parse(src).unwrap_or_else(|e| panic!("parse failed: {e}")) {
OdinValue::Object(m) => m,
other => panic!("expected object, got {other:?}"),
}
}
#[test]
fn simple_string_attribute() {
let m = obj(r#"lifecycle_state = <"published">"#);
assert_eq!(
m.get("lifecycle_state"),
Some(&OdinValue::String("published".to_owned()))
);
}
#[test]
fn term_code_and_uri_leaves() {
let m = obj("original_language = <[ISO_639-1::en]>");
assert_eq!(
m.get("original_language"),
Some(&OdinValue::TermCode("[ISO_639-1::en]".to_owned()))
);
let m = obj("target = <http://loinc.org/id/9272-6>");
assert_eq!(
m.get("target"),
Some(&OdinValue::Uri("<http://loinc.org/id/9272-6>".to_owned()))
);
}
#[test]
fn nested_object_and_keyed_list() {
let src = r#"
details = <
["en"] = <
language = <[ISO_639-1::en]>
keywords = <"ADL", "test">
>
>
"#;
let m = obj(src);
let OdinValue::KeyedList(entries) = m.get("details").expect("details") else {
panic!("expected keyed list");
};
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].0, OdinKey::String("en".to_owned()));
let OdinValue::Object(inner) = &entries[0].1 else {
panic!("expected inner object");
};
assert_eq!(
inner.get("keywords"),
Some(&OdinValue::List(vec![
OdinValue::String("ADL".to_owned()),
OdinValue::String("test".to_owned()),
]))
);
}
#[test]
fn integer_real_boolean_and_intervals() {
let m = obj("a = <5>\nb = <-3.5>\nc = <True>");
assert_eq!(m.get("a"), Some(&OdinValue::Integer(5)));
assert_eq!(m.get("b"), Some(&OdinValue::Real(-3.5)));
assert_eq!(m.get("c"), Some(&OdinValue::Boolean(true)));
let m = obj("range = <|0..10|>");
let OdinValue::Interval(OdinInterval::Range {
lower,
lower_included,
upper,
upper_included,
}) = m.get("range").expect("range")
else {
panic!("expected range interval");
};
assert_eq!(lower.as_deref(), Some(&OdinValue::Integer(0)));
assert_eq!(upper.as_deref(), Some(&OdinValue::Integer(10)));
assert!(*lower_included && *upper_included);
}
#[test]
fn open_list_and_partial_date() {
let m = obj(r#"c = <"a", ...>"#);
assert_eq!(
m.get("c"),
Some(&OdinValue::List(vec![
OdinValue::String("a".to_owned()),
OdinValue::ListContinue,
]))
);
let m = obj("d = <2004-06-??>");
assert_eq!(m.get("d"), Some(&OdinValue::Date("2004-06-??".to_owned())));
}
#[test]
fn typed_cast_and_reference_path() {
let m = obj("q = (Interval<Quantity>) <|0..1|>");
let OdinValue::Typed { rm_type, value } = m.get("q").expect("q") else {
panic!("expected typed cast");
};
assert_eq!(rm_type, "Interval<Quantity>");
assert!(matches!(**value, OdinValue::Interval(_)));
let m = obj("ref = </data[id2]/items>");
assert_eq!(
m.get("ref"),
Some(&OdinValue::PathList(vec!["/data[id2]/items".to_owned()]))
);
}
#[test]
fn namespaced_type_casts_parse_and_keep_the_dotted_name() {
for qualified in [
"org.openehr.rm.ehr.content.ENTRY",
"Core.Abstractions.Relationships.Relationship",
"org.openehr.rm.data_types.text.DV_TEXT",
] {
let m = obj(&format!("a = ({qualified}) <b = <1>>"));
let OdinValue::Typed { rm_type, value } = m.get("a").expect("a") else {
panic!("expected a typed cast for {qualified}");
};
assert_eq!(rm_type, qualified);
assert!(matches!(**value, OdinValue::Object(_)));
}
}
#[test]
fn namespaced_and_plain_generic_type_casts_parse() {
for (src, expected) in [
("Interval<Quantity>", "Interval<Quantity>"),
(
"List<org.openehr.rm.ehr.content.ENTRY>",
"List<org.openehr.rm.ehr.content.ENTRY>",
),
(
"org.openehr.base.foundation_types.Interval<org.openehr.base.Quantity>",
"org.openehr.base.foundation_types.Interval<org.openehr.base.Quantity>",
),
(
"Hash<org.openehr.rm.HOTEL,String>",
"Hash<org.openehr.rm.HOTEL,String>",
),
] {
let m = obj(&format!("a = ({src}) <...>"));
let OdinValue::Typed { rm_type, .. } = m.get("a").expect("a") else {
panic!("expected a typed cast for {src}");
};
assert_eq!(rm_type, expected);
}
}
#[test]
fn a_lowercase_terminal_segment_is_not_a_type_identifier() {
assert!(parse("a = (org.openehr.rm.content) <b = <1>>").is_err());
}
#[test]
fn illegal_escape_is_error() {
assert!(parse(r#"a = <"bad \d">"#).is_err());
}
#[test]
fn empty_sections_appear_anywhere() {
let m = obj("address = <...>");
assert_eq!(m.get("address"), Some(&OdinValue::Empty));
let m = obj("a = <b = <...> c = <d = <...>>>\ne = <[1] = <...>>\nf = (PENSION) <...>");
let OdinValue::Object(inner) = m.get("a").expect("a") else {
panic!("expected object");
};
assert_eq!(inner.get("b"), Some(&OdinValue::Empty));
let OdinValue::KeyedList(entries) = m.get("e").expect("e") else {
panic!("expected keyed list");
};
assert_eq!(entries[0].1, OdinValue::Empty);
assert_eq!(
m.get("f"),
Some(&OdinValue::Typed {
rm_type: "PENSION".to_owned(),
value: Box::new(OdinValue::Empty),
})
);
}
#[test]
fn partial_date_time_family() {
for src in [
"2004-06-11T10:30:??",
"2004-06-11T10:??:??",
"2004-06-11T??:??:??",
"2004-06-??T??:??:??",
"2004-??-??T??:??:??",
] {
let m = obj(&format!("d = <{src}>"));
assert_eq!(
m.get("d"),
Some(&OdinValue::DateTime(src.to_owned())),
"{src}"
);
}
}
#[test]
fn space_separated_date_time_normalises() {
let m = obj("time_committed = <2004-11-02 09:31:04+1000>");
assert_eq!(
m.get("time_committed"),
Some(&OdinValue::DateTime("2004-11-02T09:31:04+1000".to_owned()))
);
}
#[test]
fn integer_exponents_and_case_insensitive_booleans() {
let m = obj("a = <29e6>\nb = <2900e-2>\nc = <TRUE>\nd = <fAlSe>");
assert_eq!(m.get("a"), Some(&OdinValue::Integer(29_000_000)));
assert_eq!(m.get("b"), Some(&OdinValue::Integer(29)));
assert_eq!(m.get("c"), Some(&OdinValue::Boolean(true)));
assert_eq!(m.get("d"), Some(&OdinValue::Boolean(false)));
assert!(parse("a = <29e-2>").is_err());
}
#[test]
fn unbounded_interval_endpoints() {
for src in ["|0..infinity|", "|0..*|", "|0..INFINITY|"] {
let m = obj(&format!("r = <{src}>"));
let Some(OdinValue::Interval(OdinInterval::Range {
lower,
lower_included,
upper,
upper_included,
})) = m.get("r")
else {
panic!("expected range interval for {src}");
};
assert_eq!(lower.as_deref(), Some(&OdinValue::Integer(0)), "{src}");
assert!(*lower_included, "{src}");
assert_eq!(upper.as_deref(), None, "{src}");
assert!(!*upper_included, "{src}");
}
let m = obj("r = <|-infinity..5.0|>");
let Some(OdinValue::Interval(OdinInterval::Range { lower, upper, .. })) = m.get("r") else {
panic!("expected range interval");
};
assert_eq!(lower.as_deref(), None);
assert_eq!(upper.as_deref(), Some(&OdinValue::Real(5.0)));
}
#[test]
fn local_term_codes_are_leaf_values() {
let m = obj("a = <[at0200]>\nb = <[at0200], ...>\nc = <[at0010.2]>");
assert_eq!(
m.get("a"),
Some(&OdinValue::TermCode("[at0200]".to_owned()))
);
assert_eq!(
m.get("b"),
Some(&OdinValue::List(vec![
OdinValue::TermCode("[at0200]".to_owned()),
OdinValue::ListContinue,
]))
);
assert_eq!(
m.get("c"),
Some(&OdinValue::TermCode("[at0010.2]".to_owned()))
);
let map = obj("k = <[1] = <\"x\">>");
let Some(OdinValue::KeyedList(entries)) = map.get("k") else {
panic!("expected keyed list");
};
assert_eq!(entries[0].0, OdinKey::Integer(1));
}
#[test]
fn duplicate_sibling_container_keys_are_refused() {
for src in [
"k = <[1] = <1> [1] = <2>>",
r#"k = <["a"] = <1> ["a"] = <2>>"#,
"k = <[1] = <1> [01] = <2>>",
"[1] = <1>\n[1] = <2>",
] {
let err = parse(src).expect_err("duplicate keys must be refused (VDOBU)");
assert!(
matches!(err.kind, OdinErrorKind::DuplicateKey(_)),
"{src}: {err:?}"
);
}
assert!(parse("k = <[1] = <1> [2] = <2>>").is_ok());
assert!(parse("p = <[1] = <1>>\nq = <[1] = <2>>").is_ok());
}
#[test]
fn integer_keys_take_signs_and_evaluate_exponents() {
let cases = [
("k = <[-1] = <1>>", -1i64),
("k = <[+2] = <1>>", 2),
("k = <[29e2] = <1>>", 2900),
];
for (src, expected) in cases {
let m = obj(src);
let Some(OdinValue::KeyedList(entries)) = m.get("k") else {
panic!("{src}: expected keyed list");
};
assert_eq!(entries[0].0, OdinKey::Integer(expected), "{src}");
}
assert!(parse("k = <[99999999999999999999] = <1>>").is_err());
assert!(parse(r#"k = <[-"a"] = <1>>"#).is_err());
}
#[test]
fn duplicate_sibling_attributes_are_refused() {
let err = parse("a = <1>\nb = <2>\na = <3>").expect_err("duplicate `a`");
assert_eq!(err.kind, OdinErrorKind::DuplicateAttribute("a".to_owned()));
let err = parse("outer = <x = <1> x = <2>>").expect_err("duplicate nested `x`");
assert_eq!(err.kind, OdinErrorKind::DuplicateAttribute("x".to_owned()));
assert!(parse("p = <x = <1>>\nq = <x = <2>>").is_ok());
}
#[test]
fn value_words_parse_as_attribute_names() {
let m = obj("true = <1>\nfalse = <2>\ninfinity = <3>\nTrue = <4>");
assert_eq!(m.get("true"), Some(&OdinValue::Integer(1)));
assert_eq!(m.get("false"), Some(&OdinValue::Integer(2)));
assert_eq!(m.get("infinity"), Some(&OdinValue::Integer(3)));
assert_eq!(m.get("True"), Some(&OdinValue::Integer(4)));
let m = obj("a = <true>\nr = <|0..infinity|>");
assert_eq!(m.get("a"), Some(&OdinValue::Boolean(true)));
let Some(OdinValue::Interval(OdinInterval::Range { upper, .. })) = m.get("r") else {
panic!("expected a range interval");
};
assert_eq!(upper.as_deref(), None);
}
#[test]
fn semicolons_separate_blocks_with_no_semantic_difference() {
let with = parse(r#"term = <text = <"plan">; description = <"The clinician's advice">>"#)
.expect("the §Semi-colons example with separators should parse");
let without = parse(r#"term = <text = <"plan"> description = <"The clinician's advice">>"#)
.expect("the §Semi-colons example without separators should parse");
assert_eq!(with, without);
let keyed_with = parse(r#"k = <["a"] = <1>; ["b"] = <2>;>"#)
.expect("keyed objects with separators should parse");
let keyed_without =
parse(r#"k = <["a"] = <1> ["b"] = <2>>"#).expect("keyed objects should parse");
assert_eq!(keyed_with, keyed_without);
}
#[test]
fn multi_line_string_leaders_are_stripped() {
let m = obj(
" text = <\"And now the STORM-BLAST came, and he\n Was tyrannous and strong :\n And chased us south along.\">",
);
assert_eq!(
m.get("text"),
Some(&OdinValue::String(
"And now the STORM-BLAST came, and he\nWas tyrannous and strong :\nAnd chased us south along."
.to_owned()
))
);
let m = obj(r#"t = <"a "phrase".">"#);
assert_eq!(
m.get("t"),
Some(&OdinValue::String("a "phrase".".to_owned()))
);
}
}