#![expect(
clippy::disallowed_types,
reason = "canonical JSON / Simplified Formats operate on the wire value by definition \
(ITS-JSON; ITS-REST Simplified Formats) — serde_json::Value IS the subject matter \
(#1694)"
)]
use serde::Serialize;
use serde::de::DeserializeOwned;
#[cfg(feature = "schema-validation")]
pub const RM_SCHEMA_JSON: &str = include_str!("../schemas/json/openehr_rm_1.1.0_all.json");
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct JsonParseError {
message: String,
location: Option<(usize, usize)>,
path: Vec<String>,
}
impl JsonParseError {
#[must_use]
pub fn custom(message: impl Into<String>) -> Self {
Self {
message: message.into(),
location: None,
path: Vec::new(),
}
}
#[must_use]
pub fn unknown_field(field: &str, ty: &str, known: &[&str]) -> Self {
if known.is_empty() {
return Self::custom(format!(
"unknown field `{field}` on `{ty}`, there are no fields"
));
}
let list = known
.iter()
.map(|name| format!("`{name}`"))
.collect::<Vec<_>>()
.join(", ");
Self::custom(format!(
"unknown field `{field}` on `{ty}`, expected one of {list}"
))
}
#[must_use]
pub fn in_field(mut self, field: &str) -> Self {
self.path.insert(0, format!(".{field}"));
self
}
#[must_use]
pub fn in_index(mut self, index: usize) -> Self {
self.path.insert(0, format!("[{index}]"));
self
}
#[must_use]
pub fn message(&self) -> &str {
&self.message
}
#[must_use]
pub fn path(&self) -> &[String] {
&self.path
}
#[must_use]
pub fn location(&self) -> Option<(usize, usize)> {
self.location
}
}
fn from_path_error(error: &serde_path_to_error::Error<serde_json::Error>) -> JsonParseError {
let inner = error.inner();
let path: Vec<String> = error
.path()
.iter()
.map(|segment| match segment {
serde_path_to_error::Segment::Seq { index } => format!("[{index}]"),
serde_path_to_error::Segment::Map { key } => format!(".{key}"),
serde_path_to_error::Segment::Enum { variant } => format!(".{variant}"),
serde_path_to_error::Segment::Unknown => ".?".to_owned(),
})
.collect();
let location = match (inner.line(), inner.column()) {
(0, _) => None,
(line, column) => Some((line, column)),
};
JsonParseError {
message: inner.to_string(),
location,
path,
}
}
impl std::fmt::Display for JsonParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.message)?;
if !self.path.is_empty() {
write!(f, " (at ${})", self.path.join(""))?;
}
Ok(())
}
}
impl std::error::Error for JsonParseError {}
#[must_use]
#[expect(
clippy::expect_used,
reason = "`serde_json::to_string` can only fail on an I/O error (the writer is an in-memory String), a non-string map key, or a `Serialize` impl that returns Err; the emitted spec-type impls key every map with `String` and never return Err, so the Err variant is unreachable here — the Book ch9 sanctioned escape for a logically-impossible Err"
)]
pub fn to_canonical_json<T: Serialize + ?Sized>(value: &T) -> String {
serde_json::to_string(value).expect("an openEHR spec value should always serialize")
}
#[must_use]
#[expect(
clippy::expect_used,
reason = "`serde_json::to_value` fails only on a non-string map key or a `Serialize` impl that returns Err; the emitted spec-type impls do neither, so the Err variant is unreachable here — the Book ch9 sanctioned escape for a logically-impossible Err"
)]
pub fn to_canonical_value<T: Serialize + ?Sized>(value: &T) -> serde_json::Value {
serde_json::to_value(value).expect("an openEHR spec value should always serialize")
}
pub fn from_canonical_json<T: DeserializeOwned>(json: &str) -> Result<T, JsonParseError> {
match serde_json::from_str::<T>(json) {
Ok(value) => Ok(value),
Err(plain) => Err(located_in_str::<T>(json, &plain)),
}
}
fn located_in_str<T: DeserializeOwned>(json: &str, plain: &serde_json::Error) -> JsonParseError {
let mut deserializer = serde_json::Deserializer::from_str(json);
match serde_path_to_error::deserialize::<_, T>(&mut deserializer) {
Err(located) => from_path_error(&located),
Ok(_) => JsonParseError::custom(plain.to_string()),
}
}
pub fn from_canonical_value<T: DeserializeOwned>(
value: &serde_json::Value,
) -> Result<T, JsonParseError> {
match T::deserialize(value) {
Ok(decoded) => Ok(decoded),
Err(plain) => Err(match serde_path_to_error::deserialize::<_, T>(value) {
Err(located) => from_path_error(&located),
Ok(_) => JsonParseError::custom(plain.to_string()),
}),
}
}
#[cfg(feature = "schema-validation")]
static RM_VALIDATOR: std::sync::LazyLock<Result<jsonschema::Validator, String>> =
std::sync::LazyLock::new(|| {
let schema: serde_json::Value =
serde_json::from_str(RM_SCHEMA_JSON).map_err(|e| format!("parse RM schema: {e}"))?;
jsonschema::validator_for(&schema).map_err(|e| format!("compile RM schema: {e}"))
});
pub fn reject_undeclared_keys(value: &serde_json::Value) -> Result<(), JsonParseError> {
match value {
serde_json::Value::Object(map) => {
if let Some(ty) = map.get("_type").and_then(serde_json::Value::as_str)
&& let Some(declared) =
crate::json_codec::generated::structural::declared_fields(ty)
&& let Some(key) = map
.keys()
.find(|k| k.as_str() != "_type" && declared.binary_search(&k.as_str()).is_err())
{
return Err(JsonParseError::unknown_field(key, ty, declared).in_field(key));
}
for (key, child) in map {
reject_undeclared_keys(child).map_err(|e| e.in_field(key))?;
}
Ok(())
}
serde_json::Value::Array(items) => {
for (index, child) in items.iter().enumerate() {
reject_undeclared_keys(child).map_err(|e| e.in_index(index))?;
}
Ok(())
}
_ => Ok(()),
}
}
#[cfg(feature = "schema-validation")]
static RM_CLASS_VALIDATOR: std::sync::LazyLock<Result<jsonschema::Validator, String>> =
std::sync::LazyLock::new(|| {
let mut schema: serde_json::Value =
serde_json::from_str(RM_SCHEMA_JSON).map_err(|e| format!("parse RM schema: {e}"))?;
let names: Vec<String> = schema
.get("definitions")
.and_then(serde_json::Value::as_object)
.map(|definitions| definitions.keys().cloned().collect())
.ok_or_else(|| "the RM schema has no definitions".to_owned())?;
let discriminator = "_type";
let mut root = vec![serde_json::json!({
"required": [discriminator],
"properties": { (discriminator): { "enum": names } }
})];
root.extend(names.iter().map(|class| {
serde_json::json!({
"if": { "properties": { (discriminator): { "const": class } } },
"then": { "$ref": format!("#/definitions/{class}") }
})
}));
if let Some(map) = schema.as_object_mut() {
map.insert("allOf".to_owned(), serde_json::Value::Array(root));
}
jsonschema::validator_for(&schema).map_err(|e| format!("compile RM schema: {e}"))
});
#[cfg(feature = "schema-validation")]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SchemaRoot {
Published,
EveryClass,
}
#[cfg(feature = "schema-validation")]
pub fn validate_canonical(value: &serde_json::Value, root: SchemaRoot) -> Result<(), Vec<String>> {
let validator = match root {
SchemaRoot::Published => &RM_VALIDATOR,
SchemaRoot::EveryClass => &RM_CLASS_VALIDATOR,
};
collect_errors(validator, value)
}
#[cfg(feature = "schema-validation")]
fn collect_errors(
validator: &Result<jsonschema::Validator, String>,
value: &serde_json::Value,
) -> Result<(), Vec<String>> {
match validator {
Ok(validator) => {
let errors: Vec<String> = validator
.iter_errors(value)
.map(|e| format!("{} (at {})", e, e.instance_path()))
.collect();
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
Err(e) => Err(vec![e.clone()]),
}
}