// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0
// The two addressing schemes a schema is written in: the URI references `$id`,
// `$ref` and `$anchor` carry, and the JSON Pointer a fragment beginning with
// `/` is (RFC 6901).
//
// URI reading and relative-reference resolution are moonurl's: the five
// components, the merge and the dot segments are one algorithm, and a second
// copy of it would only be subtly wrong. What is here is the pointer, which is a
// different addressing scheme, and the two places where the two meet.
///|
/// A reference split into the part that names a document and the part that names
/// a place inside it. The fragment comes back without its `#`.
fn split_fragment(reference : String) -> (String, String) {
let url = @moonurl.Url::decode(reference) catch {
_ => return (reference, "")
}
match url.fragment {
Some(fragment) => {
let document : @moonurl.Url = { ..url, fragment: None, }
(document.encode(), fragment)
}
None => (reference, "")
}
}
///|
/// `reference` read against `base`, RFC 3986 §5.2.
///
/// A reference moonurl will not read is left as it was written: nothing can be
/// said about where it lands, and a reference that resolves to nothing becomes a
/// fault rather than a guess.
fn resolve(base : String, reference : String) -> String {
let root = @moonurl.Url::decode(base) catch { _ => @moonurl.Url::new() }
let target = @moonurl.Url::decode(reference, base=root) catch {
_ => return reference
}
target.encode()
}
///|
/// The value a JSON Pointer points at, or `None` when the document has nothing
/// there (RFC 6901 §4).
fn pointer(document : Json, path : String) -> Json? {
if path == "" {
return Some(document)
}
if !path.has_prefix("/") {
return None
}
let mut here = document
for token in split_pointer(path) {
match here {
Object(members) =>
match members.get(token) {
Some(next) => here = next
None => return None
}
Array(items) => {
let index = index_of(token)
if index < 0 || index >= items.length() {
return None
}
here = items[index]
}
_ => return None
}
}
Some(here)
}
///|
/// A pointer's reference tokens, each unescaped: `~1` is a `/` that belongs to a
/// name and `~0` a `~`, undone in that order so a name holding `~1` survives.
fn split_pointer(path : String) -> Array[String] {
let text = path[:]
let out : Array[String] = []
let token = StringBuilder()
let mut i = 1
while i <= text.length() {
if i == text.length() || text[i] == '/' {
out.push(unescape(token.to_string()))
token.reset()
} else if text[i] == '%' && i + 2 < text.length() {
// A pointer that arrived inside a URI is percent-encoded.
match (nibble(text[i + 1].to_int()), nibble(text[i + 2].to_int())) {
(Some(hi), Some(lo)) => {
token.write_char(((hi << 4) | lo).unsafe_to_char())
i = i + 2
}
_ => token.write_string(text[i:i + 1].to_owned())
}
} else {
token.write_string(text[i:i + 1].to_owned())
}
i = i + 1
}
out
}
///|
/// `~1` and `~0` read back as the characters they stand for.
fn unescape(token : String) -> String {
let text = token[:]
let out = StringBuilder()
let mut i = 0
while i < text.length() {
if text[i] == '~' && i + 1 < text.length() {
match text[i + 1] {
'1' => {
out.write_char('/')
i = i + 2
continue
}
'0' => {
out.write_char('~')
i = i + 2
continue
}
_ => ()
}
}
out.write_string(text[i:i + 1].to_owned())
i = i + 1
}
out.to_string()
}
///|
/// A reference token as the array index it spells, or `-1` when it spells none.
/// A leading zero is not an index, which is what keeps `01` out of an array.
fn index_of(token : String) -> Int32 {
if token.length() == 0 {
return -1
}
if token.length() > 1 && token[:][0] == '0' {
return -1
}
let mut value = 0
for c in token {
if !is_digit(c.to_int()) {
return -1
}
value = value * 10 + (c.to_int() - 0x30)
if value < 0 {
return -1
}
}
value
}
///|
fn is_digit(code : Int32) -> Bool {
code >= 0x30 && code <= 0x39
}
///|
fn nibble(code : Int32) -> Int32? {
if code >= 0x30 && code <= 0x39 {
Some(code - 0x30)
} else if code >= 0x41 && code <= 0x46 {
Some(code - 0x41 + 10)
} else if code >= 0x61 && code <= 0x66 {
Some(code - 0x61 + 10)
} else {
None
}
}
///|
/// One step deeper in a pointer, with the characters a reference token escapes.
fn step(path : String, token : String) -> String {
let out = StringBuilder()
out.write_string(path)
out.write_char('/')
for c in token {
match c {
'~' => out.write_string("~0")
'/' => out.write_string("~1")
_ => out.write_char(c)
}
}
out.to_string()
}