// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0
///|
/// Split a dotted path into its segments.
///
/// A dot separates members; `\.` is a literal dot, so a key that contains one
/// can still be named. An empty segment — `a..b`, `.a`, `a.` — is refused
/// rather than skipped, because a path with a hole in it is a typo.
pub fn segments(path : String) -> Array[String] raise Refused {
let out = []
let view = path[:]
let current = StringBuilder()
let mut i = 0
let length = view.length()
while i < length {
let c = view.at(i).to_int()
if c == '\\'.to_int() &&
i + 1 < length &&
view.at(i + 1).to_int() == '.'.to_int() {
current.write_char('.')
i += 2
continue
}
if c == '.'.to_int() {
if current.is_empty() {
raise Path(got=path, at=i)
}
out.push(current.to_string())
current.reset()
i += 1
continue
}
current.write_char(c.unsafe_to_char())
i += 1
}
if current.is_empty() {
if out.is_empty() && path == "" {
return out
}
raise Path(got=path, at=i)
}
out.push(current.to_string())
out
}
///|
/// The value at `path`, or nothing.
///
/// A segment of digits indexes into a list, so `servers.0.host` names the first
/// server's host without a second notation to learn.
pub fn Node::find(self : Node, path : String) -> Node? {
let parts = segments(path) catch { _ => return None }
if parts.is_empty() {
return Some(self)
}
let mut here = self
for part in parts {
let next = match here.value {
Map(table) => table.get(part)
List(items) =>
match whole(part[:]) {
Some(index) => {
let index = index.to_int()
if index >= 0 && index < items.length() {
Some(items[index])
} else {
None
}
}
None => None
}
_ => None
}
match next {
Some(node) => here = node
None => return None
}
}
Some(here)
}
///|
/// The value at `path`, or a [`Missing`] refusal naming the path.
pub fn Node::need(self : Node, path : String) -> Node raise Refused {
match self.find(path) {
Some(node) => node
None => raise Missing(path~, want="a value")
}
}
///|
/// The text at `path`.
///
/// A value of another shape is refused with the path in the sentence, because a
/// caller who asked for `db.port` wants to be told about `db.port` rather than
/// about a position in a file.
pub fn Node::need_text(self : Node, path : String) -> String raise Refused {
let node = self.need(path)
match node.value {
Text(text) => text
_ =>
raise Type(
want="text",
got=node.kind(),
path~,
source=node.source,
at=node.at,
)
}
}
///|
/// The integer at `path`.
pub fn Node::need_int(self : Node, path : String) -> Int64 raise Refused {
let node = self.need(path)
match node.value {
Int(n) => n
Real(x) =>
if x == x.trunc() && x.abs() < 9.007199254740992e15 {
x.to_int64()
} else {
raise Type(
want="integer",
got="real",
path~,
source=node.source,
at=node.at,
)
}
_ =>
raise Type(
want="integer",
got=node.kind(),
path~,
source=node.source,
at=node.at,
)
}
}
///|
/// The boolean at `path`.
pub fn Node::need_bool(self : Node, path : String) -> Bool raise Refused {
let node = self.need(path)
match node.value {
Bool(flag) => flag
_ =>
raise Type(
want="boolean",
got=node.kind(),
path~,
source=node.source,
at=node.at,
)
}
}
///|
/// Whether a path is present.
pub fn Node::has(self : Node, path : String) -> Bool {
self.find(path) is Some(_)
}