///|
/// A location inside a CBOR document.
pub(all) enum CborPathSegment {
Key(String)
Index(Int)
} derive(Eq, Debug)
///|
/// Return a readable path fragment for diagnostics and logs.
pub fn cbor_path_segment_to_string(segment : CborPathSegment) -> String {
match segment {
Key(name) => "." + name
Index(index) => "[" + index.to_string() + "]"
}
}
///|
/// Format a path without introducing a dependency on a JSON pointer package.
pub fn cbor_path_to_string(path : Array[CborPathSegment]) -> String {
let result = StringBuilder::new()
if path.length() == 0 {
result.write_string("$")
} else {
result.write_string("$")
for segment in path {
result.write_string(cbor_path_segment_to_string(segment))
}
}
result.to_string()
}
///|
fn query_failure(path : Array[CborPathSegment], message : String) -> CborError {
SemanticError("query " + cbor_path_to_string(path) + ": " + message)
}
///|
fn query_failure_at(
path : Array[CborPathSegment],
segment : CborPathSegment,
message : String,
) -> CborError {
let extended = []
for item in path {
extended.push(item)
}
extended.push(segment)
query_failure(extended, message)
}
///|
fn map_value(value : CborValue, key : String) -> CborValue? {
match value {
Map(entries) => {
for entry in entries {
match entry.0 {
Text(name) => if name == key { return Some(entry.1) }
_ => ()
}
}
None
}
_ => None
}
}
///|
fn map_value_required(
value : CborValue,
key : String,
path : Array[CborPathSegment],
) -> CborValue raise CborError {
match map_value(value, key) {
Some(found) => found
None => raise query_failure_at(path, Key(key), "object field is missing")
}
}
///|
fn array_value(
value : CborValue,
index : Int,
path : Array[CborPathSegment],
) -> CborValue raise CborError {
match value {
Array(items) => {
if index < 0 || index >= items.length() {
raise query_failure_at(
path,
Index(index),
"array index is out of range",
)
}
items[index]
}
_ => raise query_failure_at(path, Index(index), "value is not an array")
}
}
///|
/// Resolve a path one segment at a time and return the selected value.
pub fn cbor_query_at(
value : CborValue,
path : Array[CborPathSegment],
) -> CborValue raise CborError {
let mut current = value
let consumed = []
for segment in path {
match segment {
Key(name) => current = map_value_required(current, name, consumed)
Index(index) => current = array_value(current, index, consumed)
}
consumed.push(segment)
}
current
}
///|
/// Resolve a path, returning `None` only when a field is absent.
pub fn cbor_query_optional(
value : CborValue,
path : Array[CborPathSegment],
) -> CborValue? raise CborError {
if path.length() == 0 {
return Some(value)
}
let mut current = value
let consumed = []
for segment in path {
match segment {
Key(name) =>
match current {
Map(entries) => {
let mut found = None
for entry in entries {
match entry.0 {
Text(key) => if key == name { found = Some(entry.1) }
_ => ()
}
}
match found {
Some(next) => current = next
None => return None
}
}
_ =>
raise query_failure_at(
consumed,
Key(name),
"value is not an object",
)
}
Index(index) => current = array_value(current, index, consumed)
}
consumed.push(segment)
}
Some(current)
}
///|
/// Read a text field at a nested path.
pub fn cbor_query_text(
value : CborValue,
path : Array[CborPathSegment],
) -> String raise CborError {
match cbor_query_at(value, path) {
Text(text) => text
_ => raise query_failure(path, "expected text")
}
}
///|
/// Read an integer field at a nested path without narrowing unsafely.
pub fn cbor_query_int64(
value : CborValue,
path : Array[CborPathSegment],
) -> Int64 raise CborError {
match cbor_query_at(value, path) {
Integer(number) => number
Unsigned(number) =>
if number > 0x7FFFFFFFFFFFFFFFUL {
raise query_failure(path, "unsigned integer exceeds Int64 range")
} else {
number.reinterpret_as_int64()
}
_ => raise query_failure(path, "expected integer")
}
}
///|
/// Read a full-width unsigned integer field.
pub fn cbor_query_uint64(
value : CborValue,
path : Array[CborPathSegment],
) -> UInt64 raise CborError {
match cbor_query_at(value, path) {
Unsigned(number) => number
Integer(number) =>
if number < 0L {
raise query_failure(path, "negative integer cannot be UInt64")
} else {
number.reinterpret_as_uint64()
}
_ => raise query_failure(path, "expected unsigned integer")
}
}
///|
/// Read a boolean field at a nested path.
pub fn cbor_query_bool(
value : CborValue,
path : Array[CborPathSegment],
) -> Bool raise CborError {
match cbor_query_at(value, path) {
Simple(20) => false
Simple(21) => true
_ => raise query_failure(path, "expected bool")
}
}
///|
/// Read a byte string field at a nested path.
pub fn cbor_query_bytes(
value : CborValue,
path : Array[CborPathSegment],
) -> Bytes raise CborError {
match cbor_query_at(value, path) {
Bytes(bytes) => bytes
_ => raise query_failure(path, "expected byte string")
}
}
///|
/// Read a floating-point field, accepting integer CBOR numbers as well.
pub fn cbor_query_double(
value : CborValue,
path : Array[CborPathSegment],
) -> Double raise CborError {
match cbor_query_at(value, path) {
Float64(number) => number
Integer(number) => number.to_double()
Unsigned(number) => number.to_double()
_ => raise query_failure(path, "expected number")
}
}
///|
/// Return whether an object contains a text key.
pub fn cbor_object_has(value : CborValue, key : String) -> Bool {
match map_value(value, key) {
Some(_) => true
None => false
}
}
///|
/// Read a top-level object field without constructing a path.
pub fn cbor_object_get(value : CborValue, key : String) -> CborValue? {
map_value(value, key)
}
///|
/// Return all text keys in insertion order.
pub fn cbor_object_keys(value : CborValue) -> Array[String] {
let keys = []
match value {
Map(entries) =>
for entry in entries {
match entry.0 {
Text(name) => keys.push(name)
_ => ()
}
}
_ => ()
}
keys
}
///|
/// Project an object onto a list of keys, preserving the requested order.
pub fn cbor_object_project(
value : CborValue,
keys : Array[String],
) -> CborValue raise CborError {
match value {
Map(_) => {
let result = []
for key in keys {
match map_value(value, key) {
Some(found) => result.push((Text(key), found))
None =>
raise SemanticError("query projection field is missing: " + key)
}
}
Map(result)
}
_ => raise SemanticError("query projection requires an object")
}
}
///|
/// Return a copy of an object with a text field inserted or replaced.
pub fn cbor_object_set(
value : CborValue,
key : String,
replacement : CborValue,
) -> CborValue raise CborError {
match value {
Map(entries) => {
let result = []
let mut replaced = false
for entry in entries {
match entry.0 {
Text(name) if name == key => {
result.push((Text(key), replacement))
replaced = true
}
_ => result.push(entry)
}
}
if !replaced {
result.push((Text(key), replacement))
}
Map(result)
}
_ => raise SemanticError("object set requires an object")
}
}
///|
/// Return a copy of an object with a field removed.
pub fn cbor_object_remove(
value : CborValue,
key : String,
) -> CborValue raise CborError {
match value {
Map(entries) => {
let result = []
for entry in entries {
match entry.0 {
Text(name) if name == key => ()
_ => result.push(entry)
}
}
Map(result)
}
_ => raise SemanticError("object remove requires an object")
}
}
///|
/// Replace a nested path while preserving all untouched values.
pub fn cbor_set_path(
value : CborValue,
path : Array[CborPathSegment],
replacement : CborValue,
) -> CborValue raise CborError {
if path.length() == 0 {
return replacement
}
let head = path[0]
let tail = path[1:]
match head {
Key(name) => {
let child = map_value_required(value, name, [])
let updated = cbor_set_path(child, tail.to_owned(), replacement)
cbor_object_set(value, name, updated)
}
Index(index) =>
match value {
Array(items) => {
if index < 0 || index >= items.length() {
raise query_failure(path, "array index is out of range")
}
let updated = cbor_set_path(
items[index],
tail.to_owned(),
replacement,
)
let result = []
for item in items {
result.push(item)
}
result[index] = updated
Array(result)
}
_ => raise query_failure(path, "value is not an array")
}
}
}
///|
/// Merge two objects; fields from `overlay` replace fields from `base`.
pub fn cbor_object_merge(
base : CborValue,
overlay : CborValue,
) -> CborValue raise CborError {
match (base, overlay) {
(Map(base_entries), Map(overlay_entries)) => {
let result = []
for entry in base_entries {
result.push(entry)
}
for overlay_entry in overlay_entries {
match overlay_entry.0 {
Text(name) => {
let mut replaced = false
for i = 0; i < result.length(); i = i + 1 {
match result[i].0 {
Text(existing) if existing == name => {
result[i] = (Text(name), overlay_entry.1)
replaced = true
}
_ => ()
}
}
if !replaced {
result.push(overlay_entry)
}
}
_ => raise SemanticError("object merge requires text keys")
}
}
Map(result)
}
_ => raise SemanticError("object merge requires two objects")
}
}
///|
/// Convert a value to a compact log-safe diagnostic with a length limit.
pub fn cbor_query_preview(value : CborValue, max_chars : Int) -> String {
let rendered = diagnostic(value)
if max_chars <= 0 {
return ""
}
if rendered.length() <= max_chars {
rendered
} else {
rendered[0:max_chars].to_owned() + "..."
}
}
///|
/// Count descendants in a document for lightweight telemetry.
pub fn cbor_value_node_count(value : CborValue) -> Int {
match value {
Array(items) => {
let mut total = 1
for item in items {
total = total + cbor_value_node_count(item)
}
total
}
Map(entries) => {
let mut total = 1
for entry in entries {
total = total + cbor_value_node_count(entry.0)
total = total + cbor_value_node_count(entry.1)
}
total
}
Tag(_, item) => 1 + cbor_value_node_count(item)
_ => 1
}
}
///|
/// Return the maximum nesting depth of a document.
pub fn cbor_value_depth(value : CborValue) -> Int {
match value {
Array(items) => {
let mut deepest = 0
for item in items {
let depth = cbor_value_depth(item)
if depth > deepest {
deepest = depth
}
}
deepest + 1
}
Map(entries) => {
let mut deepest = 0
for entry in entries {
let key_depth = cbor_value_depth(entry.0)
let value_depth = cbor_value_depth(entry.1)
if key_depth > deepest {
deepest = key_depth
}
if value_depth > deepest {
deepest = value_depth
}
}
deepest + 1
}
Tag(_, item) => 1 + cbor_value_depth(item)
_ => 1
}
}