// MoonBit bindings for orjson's JSON engine (the yyjson fork vendored by
// orjson), exposing orjson-compatible parsing and serialization semantics.
//
// Mirrors `orjson.loads` / `orjson.dumps`:
// - parsing accepts only strict RFC 8259 JSON (no comments, trailing commas,
// NaN/Infinity literals, ...), like orjson
// - non-negative integers decode as `UInt`, negative integers as `Int`,
// numbers outside the 64-bit integer ranges decode as `Float`
// - serialization escapes strings and formats doubles exactly like orjson
// - `stringify_with(indent=2)` matches `orjson.OPT_INDENT_2`, `sort_keys`
// matches `OPT_SORT_KEYS` and `append_newline` matches `OPT_APPEND_NEWLINE`
///|
/// A JSON value.
pub(all) enum Json {
/// `null`.
Null
/// `true` or `false`.
Bool(Bool)
/// A signed 64-bit integer (always negative when produced by `parse`).
Int(Int64)
/// An unsigned 64-bit integer (non-negative values from `parse`).
UInt(UInt64)
/// A double-precision floating point number.
Float(Double)
/// A string.
String(String)
/// An array.
Array(Array[Json])
/// An object.
Object(Map[String, Json])
/// A pre-encoded JSON fragment emitted verbatim, like `orjson.Fragment`.
///
/// The content is not checked or parsed at all: dumping `Raw("{invalid")`
/// yields `{invalid`.
Raw(String)
}
///|
/// Errors raised by this package.
pub(all) suberror JsonError {
/// The input is not valid JSON. The first field is an orjson-compatible
/// error message (including the `line`/`column`/`char` suffix), the second
/// field is the 0-based character position of the error.
DecodeError(String, Int32)
/// The value cannot be serialized (e.g. a string containing a lone
/// surrogate, or nesting deeper than 254 levels).
EncodeError(String)
}
///|
/// Parse a JSON document from a `String` (mirrors `orjson.loads`).
///
/// The input must be valid UTF-8 once encoded; strings containing lone
/// surrogates are rejected.
pub fn parse(input : String) -> Json raise JsonError {
let handle = parse_string_ffi(input)
decode(handle)
}
///|
/// Parse a JSON document from UTF-8 `Bytes` (mirrors `orjson.loads`).
///
/// Invalid UTF-8 input is rejected.
pub fn parse_bytes(input : Bytes) -> Json raise JsonError {
let handle = parse_bytes_ffi(input)
decode(handle)
}
///|
fn decode(handle : ParseHandle) -> Json raise JsonError {
if parse_handle_is_null(handle) != 0 {
raise JsonError::EncodeError("failed to allocate memory")
}
if parse_handle_ok(handle) == 0 {
let message = parse_handle_error(handle)
let position = parse_handle_error_pos(handle)
parse_handle_free(handle)
raise JsonError::DecodeError(message, position)
}
let json = convert(parse_handle_root(handle))
parse_handle_free(handle)
json
}
///|
fn convert(node : Node) -> Json {
match node_type(node) {
0 => Json::Null
1 => Json::Bool(node_bool(node) != 0)
2 => Json::Int(node_int(node))
3 => Json::UInt(node_uint(node))
4 => Json::Float(node_float(node))
5 => Json::String(node_string(node))
7 => {
let size = node_size(node)
let items : Array[Json] = []
for i = 0; i < size; i = i + 1 {
items.push(convert(node_array_get(node, i)))
}
Json::Array(items)
}
8 => {
let size = node_size(node)
let map : Map[String, Json] = Map([])
for i = 0; i < size; i = i + 1 {
map.set(node_object_key(node, i), convert(node_object_get(node, i)))
}
Json::Object(map)
}
_ => Json::Null
}
}
///|
/// Maximum nesting depth of containers accepted by `stringify`, matching
/// orjson's "Recursion limit reached" limit.
const MAX_DEPTH : Int32 = 254
///|
/// Serialize `value` to compact JSON (mirrors `orjson.dumps`).
pub fn stringify(value : Json) -> String raise JsonError {
stringify_with(value)
}
///|
/// Serialize `value` to JSON with options.
///
/// Parameters:
///
/// - `indent`: spaces per nesting level; `2` matches `orjson.OPT_INDENT_2`.
/// `0` (default) produces compact output. Empty containers are always
/// written as `[]` / `{}`.
/// - `sort_keys`: sort object keys by code point, like `OPT_SORT_KEYS`.
/// - `append_newline`: append a trailing `\n`, like `OPT_APPEND_NEWLINE`.
pub fn stringify_with(
value : Json,
indent~ : Int32 = 0,
sort_keys~ : Bool = false,
append_newline~ : Bool = false,
) -> String raise JsonError {
let tree = build(value, 0)
let handle = dump_ffi(
tree,
indent,
if sort_keys {
1
} else {
0
},
if append_newline {
1
} else {
0
},
)
tree_free_ffi(tree)
if dump_handle_is_null(handle) != 0 {
raise JsonError::EncodeError("failed to allocate memory")
}
if dump_ok(handle) == 0 {
let message = dump_error(handle)
dump_free(handle)
raise JsonError::EncodeError(message)
}
let text = dump_text(handle)
dump_free(handle)
text
}
///|
fn build(value : Json, depth : Int32) -> Node raise JsonError {
match value {
Json::Null => build_null_ffi()
Json::Bool(b) => build_bool_ffi(if b { 1 } else { 0 })
Json::Int(i) => build_int_ffi(i)
Json::UInt(u) => build_uint_ffi(u)
Json::Float(f) => build_float_ffi(f)
Json::String(s) => build_string_checked(s)
Json::Raw(s) => build_raw_checked(s)
Json::Array(items) => {
if depth >= MAX_DEPTH {
raise JsonError::EncodeError("Recursion limit reached")
}
let node = build_array_ffi()
if node_is_null(node) != 0 {
raise JsonError::EncodeError("failed to allocate memory")
}
errdefer tree_free_ffi(node)
for item in items {
let child = build(item, depth + 1)
if array_push_ffi(node, child) != 0 {
tree_free_ffi(child)
raise JsonError::EncodeError(build_error_ffi())
}
}
node
}
Json::Object(map) => {
if depth >= MAX_DEPTH {
raise JsonError::EncodeError("Recursion limit reached")
}
let node = build_object_ffi()
if node_is_null(node) != 0 {
raise JsonError::EncodeError("failed to allocate memory")
}
errdefer tree_free_ffi(node)
for key, value in map {
let child = build(value, depth + 1)
if object_put_ffi(node, key, child) != 0 {
tree_free_ffi(child)
raise JsonError::EncodeError(build_error_ffi())
}
}
node
}
}
}
///|
fn build_string_checked(value : String) -> Node raise JsonError {
let node = build_string_ffi(value)
if node_is_null(node) != 0 {
raise JsonError::EncodeError(build_error_ffi())
}
node
}
///|
fn build_raw_checked(value : String) -> Node raise JsonError {
let node = build_raw_ffi(value)
if node_is_null(node) != 0 {
raise JsonError::EncodeError(build_error_ffi())
}
node
}