///|
/// Parse a plain JSON Pointer. URI fragments are deliberately not accepted.
pub fn pointer_tokens(path : String) -> Array[String] raise ConfigError {
if path == "" {
return []
}
guard path.strip_prefix("/") is Some(tail) else { raise InvalidPointer(path) }
let parts = tail.split("/").to_array()
if parts.length() > 128 {
raise LimitExceeded("pointer exceeds 128 segments")
}
parts.map(part => {
let chars = part.to_array()
let out : Array[Char] = []
let mut i = 0
while i < chars.length() {
if chars[i] == '~' {
if i + 1 >= chars.length() {
raise InvalidPointer(path)
}
match chars[i + 1] {
'0' => out.push('~')
'1' => out.push('/')
_ => raise InvalidPointer(path)
}
i = i + 2
} else {
out.push(chars[i])
i = i + 1
}
}
String::from_array(out)
})
}
///|
/// RFC array indices forbid signs, whitespace and leading zeroes.
fn array_index(
token : String,
length : Int,
insertion : Bool,
) -> Int raise ConfigError {
if token == "-" && insertion {
return length
}
let chars = token.to_array()
if chars.is_empty() || (chars.length() > 1 && chars[0] == '0') {
raise InvalidIndex(token)
}
let mut n = 0
for c in chars {
if c < '0' || c > '9' {
raise InvalidIndex(token)
}
let digit = c.to_int() - '0'.to_int()
if n > length / 10 || (n == length / 10 && digit > length % 10) {
raise InvalidIndex(token)
}
n = n * 10 + digit
}
if n >= length && !insertion {
raise InvalidIndex(token)
}
n
}
///|
fn get_at(
value : Json,
tokens : Array[String],
offset : Int,
path : String,
) -> Json raise ConfigError {
if offset == tokens.length() {
return value
}
let token = tokens[offset]
match value {
Object(o) => {
guard o.get(token) is Some(child) else { raise MissingPath(path) }
get_at(child, tokens, offset + 1, path)
}
Array(a) =>
get_at(a[array_index(token, a.length(), false)], tokens, offset + 1, path)
_ => raise MissingPath(path)
}
}
///|
/// Return an independent value, rather than an alias into the caller's tree.
pub fn get(document : Json, path : String) -> Json raise ConfigError {
validate_document(document)
clone_json(get_at(document, pointer_tokens(path), 0, path))
}
///|
priv enum EditMode {
Insert
Delete
Update
}
///|
/// Copy every edited ancestor; untouched input is never mutated.
fn edit_at(
value : Json,
tokens : Array[String],
offset : Int,
path : String,
mode : EditMode,
replacement : Json,
) -> Json raise ConfigError {
if offset == tokens.length() {
// This API always returns a JSON document; root deletion has no such result.
if mode is Delete {
raise InvalidPatch("root deletion is not supported")
}
return clone_json(replacement)
}
let token = tokens[offset]
let last = offset + 1 == tokens.length()
match value {
Object(o) => {
let out = o.copy()
if last {
match mode {
Insert => out[token] = clone_json(replacement)
Delete => {
if !o.contains(token) {
raise MissingPath(path)
}
out.remove(token)
}
Update => {
if !o.contains(token) {
raise MissingPath(path)
}
out[token] = clone_json(replacement)
}
}
} else {
guard o.get(token) is Some(child) else { raise MissingPath(path) }
out[token] = edit_at(child, tokens, offset + 1, path, mode, replacement)
}
Json::object(out)
}
Array(a) => {
let out = a.copy()
let index = array_index(token, a.length(), last && mode is Insert)
if last {
match mode {
Insert => out.insert(index, clone_json(replacement))
Delete => {
let _ = out.remove(index)
}
Update => out[index] = clone_json(replacement)
}
} else {
out[index] = edit_at(
a[index],
tokens,
offset + 1,
path,
mode,
replacement,
)
}
Json::array(out)
}
_ => raise MissingPath(path)
}
}