///|
/// A JSON tree cannot be converted to URI Template values under this adapter.
/// Only strings, null, arrays of strings/null, and objects of strings/null
/// are accepted. Numeric and boolean coercion is deliberately left to callers.
pub(all) enum JsonBridgeError {
RootMustBeObject
UnsupportedValue(String)
}
///|
pub fn JsonBridgeError::code(self : JsonBridgeError) -> String {
match self {
RootMustBeObject => "root_must_be_object"
UnsupportedValue(_) => "unsupported_value"
}
}
///|
pub fn JsonBridgeError::path(self : JsonBridgeError) -> String {
match self {
RootMustBeObject => ""
UnsupportedValue(path) => path
}
}
///|
fn pointer_escape(input : String) -> String {
let out = StringBuilder()
for ch in input {
if ch == '~' {
out.write_string("~0")
} else if ch == '/' {
out.write_string("~1")
} else {
out.write_char(ch)
}
}
out.to_string()
}
///|
fn pointer_child(parent : String, name : String) -> String {
parent + "/" + pointer_escape(name)
}
///|
/// Convert one JSON value. JSON null is an undefined variable or member.
pub fn value_from_json(
input : Json,
path? : String = "",
) -> Result[Value?, JsonBridgeError] {
match input {
Null => Ok(None)
String(text) => Ok(Some(Scalar(text)))
Array(items) => {
let converted : Array[String?] = []
for i, item in items {
match item {
Null => converted.push(None)
String(text) => converted.push(Some(text))
_ => return Err(UnsupportedValue(pointer_child(path, i.to_string())))
}
}
Ok(Some(SparseList(converted)))
}
Object(fields) => {
let names : Array[String] = []
for name, _ in fields {
names.push(name)
}
names.sort_by(fn(a, b) { a.lexical_compare(b) })
let converted : Array[(String, String?)] = []
for name in names {
match fields.get(name) {
Some(Null) => converted.push((name, None))
Some(String(text)) => converted.push((name, Some(text)))
Some(_) => return Err(UnsupportedValue(pointer_child(path, name)))
None => ()
}
}
Ok(Some(SparseAssoc(converted)))
}
_ => Err(UnsupportedValue(path))
}
}
///|
/// Convert a JSON object to ordered bindings. Object keys are sorted so
/// associative expansion is deterministic across MoonBit backends.
pub fn bindings_from_json(
input : Json,
) -> Result[Array[(String, Value)], JsonBridgeError] {
let fields = match input {
Object(fields) => fields
_ => return Err(RootMustBeObject)
}
let names : Array[String] = []
for name, _ in fields {
names.push(name)
}
names.sort_by(fn(a, b) { a.lexical_compare(b) })
let bindings : Array[(String, Value)] = []
for name in names {
match fields.get(name) {
None => ()
Some(value) =>
match value_from_json(value, path=pointer_child("", name)) {
Ok(Some(converted)) => bindings.push((name, converted))
Ok(None) => ()
Err(error) => return Err(error)
}
}
}
Ok(bindings)
}