use ::serde::Serialize;
use ::serde_json::Value;
use crate::meta::ElementMeta;
#[must_use]
pub fn to_summary_value<T: Serialize>(
table: &'static [ElementMeta],
resource: &T,
resource_type: &str,
) -> Value {
let mut value = ::serde_json::to_value(resource).unwrap_or(Value::Null);
prune_to_summary(table, &mut value, resource_type);
if let Value::Object(map) = &mut value {
map.entry("resourceType".to_string())
.or_insert_with(|| Value::String(resource_type.to_string()));
}
value
}
pub fn prune_to_summary(
table: &'static [ElementMeta],
value: &mut Value,
resource_type: &str,
) {
let Value::Object(map) = value else { return };
let bases = summary_bases(table, resource_type);
map.retain(|key, _| keep_in_summary(key, &bases));
}
fn summary_bases(
table: &'static [ElementMeta],
resource_type: &str,
) -> Vec<(String, bool, bool)> {
let prefix = format!("{resource_type}.");
let prefix_len = prefix.len();
table
.iter()
.filter(|e| e.path.starts_with(&prefix))
.filter(|e| !e.path[prefix_len..].contains('.')) .map(|e| {
let leaf = &e.path[prefix_len..];
let is_choice = leaf.ends_with("[x]");
let base = leaf.trim_end_matches("[x]").to_string();
(base, e.is_summary || e.min >= 1, is_choice)
})
.collect()
}
fn keep_in_summary(key: &str, bases: &[(String, bool, bool)]) -> bool {
if key == "resourceType" {
return true;
}
let bare = key.strip_prefix('_').unwrap_or(key);
bases.iter().any(|(base, is_summary, is_choice)| {
*is_summary
&& (bare == base
|| (*is_choice
&& bare.starts_with(base.as_str())
&& bare[base.len()..].chars().next().is_some_and(char::is_uppercase)))
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::meta::TypeRef;
static TABLE: &[ElementMeta] = &[
ElementMeta { path: "Patient.deceased[x]", min: 0, max: "1", is_summary: true, binding: None, types: &[TypeRef { code: "boolean", target_profiles: &[] }] },
ElementMeta { path: "Patient.gender", min: 0, max: "1", is_summary: true, binding: None, types: &[TypeRef { code: "code", target_profiles: &[] }] },
ElementMeta { path: "Patient.multipleBirth[x]", min: 0, max: "1", is_summary: false, binding: None, types: &[TypeRef { code: "integer", target_profiles: &[] }] },
ElementMeta { path: "Patient.name.family", min: 0, max: "1", is_summary: false, binding: None, types: &[TypeRef { code: "string", target_profiles: &[] }] },
ElementMeta { path: "Patient.photo", min: 0, max: "*", is_summary: false, binding: None, types: &[TypeRef { code: "Attachment", target_profiles: &[] }] },
];
#[test]
fn keeps_summary_elements_and_drops_the_rest() {
let mut value = ::serde_json::json!({
"gender": "male",
"photo": [{ "title": "portrait" }]
});
prune_to_summary(TABLE, &mut value, "Patient");
assert!(value.get("gender").is_some());
assert!(value.get("photo").is_none());
}
#[test]
fn choice_elements_match_every_typed_key() {
let mut value = ::serde_json::json!({
"deceasedBoolean": true,
"multipleBirthInteger": 2
});
prune_to_summary(TABLE, &mut value, "Patient");
assert!(value.get("deceasedBoolean").is_some());
assert!(value.get("multipleBirthInteger").is_none());
}
#[test]
fn extension_siblings_follow_their_element() {
let mut value = ::serde_json::json!({
"gender": "male",
"_gender": { "id": "g1" },
"photo": [],
"_photo": [null]
});
prune_to_summary(TABLE, &mut value, "Patient");
assert!(value.get("_gender").is_some());
assert!(value.get("_photo").is_none());
}
#[test]
fn resource_type_is_always_kept_and_injected() {
#[derive(::serde::Serialize)]
struct Bare {
gender: &'static str,
}
let value = to_summary_value(TABLE, &Bare { gender: "male" }, "Patient");
assert_eq!(value.get("resourceType").unwrap(), "Patient");
}
}