///|
/// The result of converting a document to a data tree.
pub struct Conversion {
tree : @data_tree.DataTree?
diagnostics : ReadOnlyArray[@diagrams.Diagnostic]
}
///|
/// Convert a JSON document to a structured-data model. Identities come from
/// ordered ownership paths: the root value is `v`, member 2 of the value of
/// root member 0 is `m/0/2` and its value is `v/0/2`. Names and scalar text
/// never define identity, so repeated members stay separate.
///
/// The model keeps a scalar root and empty root containers as they are.
/// PlantUML 1.2026.9 draws a scalar root as a one-element array and an empty
/// root container as an array with one empty string; this library does not
/// copy that projection. The title is the last `title` statement.
pub fn to_data_tree(
document : Document,
id? : String,
limits~ : @envelope.Limits = @envelope.Limits::new(),
) -> Conversion {
let diagnostics : Array[@diagrams.Diagnostic] = []
fn failed(code : String, message : String) -> Conversion {
diagnostics.push(@diagrams.Diagnostic::error(code, message))
{ tree: None, diagnostics: ReadOnlyArray::from_array(diagnostics), }
}
if !limits.valid() {
return failed("json/invalid-limits", "JSON limits must be positive")
}
let view = id.unwrap_or(document.name.unwrap_or("json"))
let nodes : Array[@data_tree.Node] = []
let mut work = 0
// Pre-order with an explicit stack: (value, path).
let stack : Array[(Value, Array[Int32])] = [(document.value, [])]
while stack.pop() is Some((value, path)) {
work += 1
if work > limits.work {
return failed("json/work-limit", "JSON conversion exceeds its work limit")
}
if nodes.length() >= limits.nodes {
return failed("json/node-limit", "JSON data exceeds the node limit")
}
if path.length() > limits.depth {
return failed(
"json/depth-limit",
"JSON nesting exceeds the depth limit of \{limits.depth}",
)
}
let node_id = @data.value_id(path)
let child_path = (i : Int32) => {
let next = path.copy()
next.push(i)
next
}
let node = match value.kind {
Object(members) => {
let entries = []
for i, entry in members {
let p = child_path(i)
entries.push(
@data_tree.Member::named(
@data.member_id(p),
entry.name.text,
@data.value_id(p),
source?=entry.span,
),
)
}
for i in 0.. {
let entries = []
for i, item in items {
let p = child_path(i)
entries.push(
@data_tree.Member::index(
@data.member_id(p),
i,
@data.value_id(p),
source?=item.span,
),
)
}
for i in 0..
@data_tree.Node::new(node_id, Text(s.text), source?=value.span)
Number(lexeme) =>
@data_tree.Node::new(node_id, Number(lexeme), source?=value.span)
Boolean(b) =>
@data_tree.Node::new(node_id, Boolean(b), source?=value.span)
Null => @data_tree.Node::new(node_id, Null, source?=value.span)
}
nodes.push(node)
}
let title = document.statements
.iter()
.fold(init=None, (last, statement) => {
match statement {
Title(text) => Some(text)
_ => last
}
})
let base = @data_tree.DataTree::new(view, @data.value_id([]), nodes, title?)
let resolution = @data.resolve(
base,
document.highlights,
"json",
work_limit=limits.work,
)
diagnostics.append(resolution.diagnostics)
if !resolution.complete {
return { tree: None, diagnostics: ReadOnlyArray::from_array(diagnostics), }
}
let tree = @data_tree.DataTree::new(
view,
@data.value_id([]),
nodes,
highlights=resolution.highlights,
title?,
)
let invalid = @data_tree.validate(
tree,
limits=@data_tree.Limits::new(
nodes=limits.nodes,
depth=limits.depth,
work=limits.work,
),
)
if !invalid.is_empty() {
diagnostics.append(invalid)
return { tree: None, diagnostics: ReadOnlyArray::from_array(diagnostics), }
}
{ tree: Some(tree), diagnostics: ReadOnlyArray::from_array(diagnostics), }
}