///|
/// The string representation used for flattened JSON paths.
pub(all) enum PathFormat {
/// RFC 6901 JSON Pointer, such as `/users/0/name`.
JsonPointer
/// JavaScript-style property and index access, such as `users[0].name`.
JavaScript
} derive(Eq, Debug)
///|
/// Controls whether arrays are expanded into indexed paths.
pub(all) enum ArrayMode {
/// Expands array elements into indexed paths.
Expand
/// Preserves each array as a single `Json` leaf.
Preserve
} derive(Eq, Debug)
///|
/// A failure while decoding or reconstructing flattened JSON paths.
pub(all) suberror FlattenJsonError {
/// A flattened leaf contains a non-empty object that must be expanded instead.
ObjectValue(String)
/// A JSON Pointer key is indistinguishable from an array index.
AmbiguousPointerToken(String)
/// A path does not conform to the selected path format.
InvalidPath(String)
/// A path exceeds 16,384 UTF-16 code units.
PathLengthExceeded(Int)
/// A path exceeds 256 segments.
PathDepthExceeded(Int)
/// Multiple flattened paths require incompatible values at the same location.
PathConflict(String)
/// Reconstructed array indices contain a gap.
ArrayIndexGap(String)
} derive(Eq, Debug)
///|
priv enum BuildNode {
Value(Json)
Object(Map[String, BuildNode])
Array(Map[Int, BuildNode])
}
///|
/// Flattens JSON containers into path-keyed JSON leaf values.
///
/// The caller must select RFC 6901 JSON Pointer or JavaScript-style paths.
/// Objects are traversed recursively. Arrays are expanded by default; pass
/// `array_mode=Preserve` to retain each array as a single `Json` leaf. Primitive
/// values and empty arrays remain `Json` leaves. A root empty object produces an
/// empty map, while nested empty objects remain `Json` leaves. JSON Pointer also
/// rejects object keys matching RFC 6901 array-index syntax because they cannot
/// be distinguished from array indices during reconstruction; JavaScript paths
/// preserve them as bracket-quoted keys.
pub fn flatten(
json : Json,
format : PathFormat,
array_mode? : ArrayMode = Expand,
) -> Map[String, Json] raise FlattenJsonError {
let flattened : Map[String, Json] = Map([])
flatten_value(json, [], format, array_mode, flattened)
flattened
}
///|
fn flatten_value(
value : Json,
path : Array[PathSegment],
format : PathFormat,
array_mode : ArrayMode,
flattened : Map[String, Json],
) -> Unit raise FlattenJsonError {
match value {
Json::Object(object) if object.is_empty() && !path.is_empty() =>
flattened[checked_path(path, format)] = value
Json::Object(object) =>
for key, child in object {
let child_path = [..path, Key(key)]
let path_string = checked_path(child_path, format)
if format == JsonPointer && is_array_index_token(key) {
raise AmbiguousPointerToken(path_string)
}
flatten_value(child, child_path, format, array_mode, flattened)
}
Json::Array(values) if array_mode == Expand && !values.is_empty() =>
for index, child in values {
let child_path = [..path, Index(index)]
ignore(checked_path(child_path, format))
flatten_value(child, child_path, format, array_mode, flattened)
}
_ => flattened[checked_path(path, format)] = value
}
}
///|
/// Reconstructs JSON from path-keyed leaf values.
///
/// `format` must match the format passed to `flatten`. Non-empty object values
/// are rejected because they must be represented by their flattened children;
/// empty objects are valid leaves.
/// Paths longer than 16,384 UTF-16 code units or deeper than 256 segments are
/// rejected to keep reconstruction within a bounded typed-error boundary.
pub fn from_flatten_json(
flattened : Map[String, Json],
format : PathFormat,
) -> Json raise FlattenJsonError {
if flattened.is_empty() {
return Json::empty_object()
}
let mut root : BuildNode? = None
for path, value in flattened {
match value {
Json::Object(object) if !object.is_empty() => raise ObjectValue(path)
_ => ()
}
let segments = parse_path(path, format)
root = Some(insert_value(root, segments, 0, path, value))
}
build_json(root.unwrap(), "")
}
///|
fn insert_value(
node : BuildNode?,
segments : Array[PathSegment],
index : Int,
path : String,
value : Json,
) -> BuildNode raise FlattenJsonError {
if index == segments.length() {
guard node is None else { raise PathConflict(path) }
return Value(value)
}
match segments[index] {
Key(key) => {
let children = match node {
None => Map([])
Some(Object(children)) => children
Some(_) => raise PathConflict(path)
}
children[key] = insert_value(
children.get(key),
segments,
index + 1,
path,
value,
)
Object(children)
}
Index(array_index) => {
let children = match node {
None => Map([])
Some(Array(children)) => children
Some(_) => raise PathConflict(path)
}
children[array_index] = insert_value(
children.get(array_index),
segments,
index + 1,
path,
value,
)
Array(children)
}
}
}
///|
fn build_json(node : BuildNode, path : String) -> Json raise FlattenJsonError {
match node {
Value(value) => value
Object(children) => {
let object : Map[String, Json] = Map([])
for key, child in children {
object[key] = build_json(child, child_path(path, key))
}
Json::object(object)
}
Array(children) => {
let values : Array[Json] = []
for index = 0; index < children.length(); index = index + 1 {
guard children.get(index) is Some(child) else {
raise ArrayIndexGap(path)
}
values.push(build_json(child, "\{path}[\{index}]"))
}
Json::array(values)
}
}
}
///|
fn child_path(path : String, key : String) -> String {
if path == "" {
key
} else {
"\{path}.\{key}"
}
}