#![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)"
)]
pub mod flat;
pub mod structured;
use indexmap::IndexMap;
use serde_json::Value;
#[derive(Debug, Clone, Default, PartialEq)]
pub struct SimChild {
pub indexed: bool,
pub occurrences: Vec<SimNode>,
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct SimNode {
pub attrs: IndexMap<String, Value>,
pub children: IndexMap<String, SimChild>,
}
impl SimNode {
#[must_use]
pub fn is_empty(&self) -> bool {
self.attrs.is_empty() && self.children.is_empty()
}
#[must_use]
pub fn is_bare_leaf(&self) -> bool {
self.children.is_empty() && self.attrs.len() == 1 && self.attrs.contains_key("")
}
#[must_use]
pub fn bare(&self) -> Option<&Value> {
self.attrs.get("")
}
pub fn occurrence_mut(&mut self, name: &str, index: Option<u32>) -> &mut SimNode {
if index.is_some() {
self.children.entry(name.to_owned()).or_default().indexed = true;
}
self.place_mut(name, index.unwrap_or(0))
}
#[expect(
clippy::as_conversions,
clippy::indexing_slicing,
reason = "the loop immediately below grows `occurrences` until `len() > i`, so the index is in bounds by construction (the u32 → usize widening is lossless on every supported target); the fn returns `&mut SimNode`, not an Option"
)]
pub fn place_mut(&mut self, name: &str, i: u32) -> &mut SimNode {
let child = self.children.entry(name.to_owned()).or_default();
let i = i as usize;
while child.occurrences.len() <= i {
child.occurrences.push(SimNode::default());
}
&mut child.occurrences[i]
}
#[must_use]
pub fn child(&self, name: &str) -> Option<&SimNode> {
self.children.get(name).and_then(|c| c.occurrences.first())
}
pub fn prune_empty(&mut self) {
for child in self.children.values_mut() {
for occ in &mut child.occurrences {
occ.prune_empty();
}
while child.occurrences.last().is_some_and(SimNode::is_empty) {
child.occurrences.pop();
}
}
self.children.retain(|_, c| !c.occurrences.is_empty());
}
}
pub type SimDocument = SimNode;
#[must_use]
pub fn is_present(v: &Value) -> bool {
match v {
Value::Null => false,
Value::String(s) => !s.is_empty(),
Value::Array(a) => !a.is_empty(),
Value::Object(o) => !o.is_empty(),
Value::Number(_) | Value::Bool(_) => true,
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn occurrence_mut_fills_and_marks_indexed() {
let mut root = SimNode::default();
root.occurrence_mut("event", Some(2))
.attrs
.insert(String::new(), json!("x"));
let child = &root.children["event"];
assert!(child.indexed);
assert_eq!(child.occurrences.len(), 3);
assert!(child.occurrences[0].is_empty());
assert_eq!(child.occurrences[2].bare(), Some(&json!("x")));
}
#[test]
fn prune_empty_keeps_interior_holes_and_trims_trailing() {
let mut root = SimNode::default();
root.occurrence_mut("event", Some(0))
.attrs
.insert(String::new(), json!("a"));
root.occurrence_mut("event", Some(2))
.attrs
.insert(String::new(), json!("b"));
let _ = root.occurrence_mut("event", Some(4));
let _ = root.occurrence_mut("dropped", Some(1));
root.prune_empty();
let child = &root.children["event"];
assert_eq!(child.occurrences.len(), 3);
assert!(child.occurrences[1].is_empty());
assert_eq!(child.occurrences[2].bare(), Some(&json!("b")));
assert!(!root.children.contains_key("dropped"));
}
#[test]
fn bare_leaf_detection() {
let mut n = SimNode::default();
n.attrs.insert(String::new(), json!("2024-01-15T10:30:00Z"));
assert!(n.is_bare_leaf());
n.attrs.insert("magnitude".to_owned(), json!(1));
assert!(!n.is_bare_leaf());
}
}