///|
/// Select a value using an RFC 6901 JSON Pointer. The empty pointer selects
/// the document root; object keys use `~0` and `~1` escaping.
pub fn select_pointer(
value : JsonValue,
pointer : String,
) -> Result[JsonValue, String] {
let segments = match parse_pointer(pointer) {
Ok(segments) => segments
Err(error) => return Err(error)
}
let mut current = value
for segment in segments {
current = match select_segment(current, segment) {
Ok(next) => next
Err(error) => return Err(error)
}
}
Ok(current)
}
///|
pub fn select_document(
input : String,
pointer : String,
) -> Result[JsonValue, String] {
let value = match parse(input) {
Ok(value) => value
Err(error) => return Err(error.message())
}
select_pointer(value, pointer)
}
///|
/// Return canonical path/value pairs in depth-first order. This is useful for
/// audit UIs that need to show exactly which values contributed to a digest.
pub fn enumerate_paths(value : JsonValue) -> Array[(String, String)] {
let paths : Array[(String, String)] = []
enumerate_value(value, "", paths)
paths
}
///|
fn enumerate_value(
value : JsonValue,
path : String,
paths : Array[(String, String)],
) -> Unit {
paths.push((path, value_preview(value)))
match value {
Array(values) => {
let mut index = 0
while index < values.length() {
enumerate_value(values[index], path + "/" + index.to_string(), paths)
index = index + 1
}
}
Object(entries) =>
for entry in entries {
enumerate_value(
entry.1,
path + "/" + pointer_escape_local(entry.0),
paths,
)
}
_ => ()
}
}
///|
fn parse_pointer(pointer : String) -> Result[Array[String], String] {
if pointer == "" {
return Ok([])
}
let chars : Array[Char] = []
for char in pointer {
chars.push(char)
}
if chars.length() == 0 || chars[0] != '/' {
return Err("JSON Pointer must be empty or start with '/'")
}
let segments : Array[String] = []
let mut current = ""
let mut index = 1
while index <= chars.length() {
if index == chars.length() || chars[index] == '/' {
segments.push(current)
current = ""
index = index + 1
} else if chars[index] == '~' {
if index + 1 >= chars.length() {
return Err("JSON Pointer escape '~' must be followed by 0 or 1")
}
if chars[index + 1] == '0' {
current = current + "~"
} else if chars[index + 1] == '1' {
current = current + "/"
} else {
return Err("JSON Pointer escape '~' must be followed by 0 or 1")
}
index = index + 2
} else {
current = current + chars[index].to_string()
index = index + 1
}
}
Ok(segments)
}
///|
fn select_segment(
value : JsonValue,
segment : String,
) -> Result[JsonValue, String] {
match value {
Object(entries) => {
for entry in entries {
if entry.0 == segment {
return Ok(entry.1)
}
}
Err("object key not found: " + segment)
}
Array(values) => {
let index = match parse_array_index(segment) {
Some(index) => index
None =>
return Err("array pointer segment is not a valid index: " + segment)
}
if index < values.length() {
Ok(values[index])
} else {
Err("array index out of range: " + segment)
}
}
_ => Err("cannot select '" + segment + "' from a scalar value")
}
}
///|
fn parse_array_index(segment : String) -> Int? {
if segment == "" {
return None
}
if segment.length() > 1 && segment[0] == '0' {
return None
}
let index = @string.parse_int(segment) catch { _ => return None }
if index < 0 {
None
} else {
Some(index)
}
}
///|
fn pointer_escape_local(value : String) -> String {
let mut output = ""
for char in value {
if char == '~' {
output = output + "~0"
} else if char == '/' {
output = output + "~1"
} else {
output = output + char.to_string()
}
}
output
}