// Runtime library for the MoonBit code generated by atdmbt.
//
// Readers have type `(Json, Path) -> T raise JsonError` and writers have
// type `(T) -> Json`. Combinators such as `read_list` build readers and
// writers for composite types from the readers and writers of their
// components.
///|
/// Location of a JSON value within the root JSON value, used in error
/// messages.
pub(all) enum Path {
Root
Key(Path, String)
Index(Path, Int)
}
///|
/// The path of the field `key` of the object at this path.
pub fn Path::key(self : Path, key : String) -> Path {
Key(self, key)
}
///|
/// The path of the element `i` of the array at this path.
pub fn Path::index(self : Path, i : Int) -> Path {
Index(self, i)
}
///|
/// Format a path in the JSONPath syntax, e.g. `$.items[2].id`.
pub fn Path::to_string(self : Path) -> String {
let buf = StringBuilder()
fn go(p : Path) -> Unit {
match p {
Root => buf.write_string("$")
Key(p, k) => {
go(p)
let simple = k != "" &&
k
.iter()
.all(c => {
(c >= 'a' && c <= 'z') ||
(c >= 'A' && c <= 'Z') ||
(c >= '0' && c <= '9') ||
c == '_'
})
if simple {
buf.write_string(".")
buf.write_string(k)
} else {
buf.write_string("[")
buf.write_string(Json::string(k).stringify())
buf.write_string("]")
}
}
Index(p, i) => {
go(p)
buf.write_string("[\{i}]")
}
}
}
go(self)
buf.to_string()
}
///|
pub impl Show for Path with fn output(self, logger) {
logger.write_string(self.to_string())
}
///|
/// Error raised when a JSON value doesn't have the expected shape.
pub(all) suberror JsonError {
JsonError(String)
} derive(Eq, Debug)
///|
pub impl Show for JsonError with fn output(self, logger) {
match self {
JsonError(msg) => logger.write_string(msg)
}
}
///|
/// The error message.
pub fn JsonError::message(self : JsonError) -> String {
match self {
JsonError(msg) => msg
}
}
///|
/// A short representation of a JSON value for error messages.
fn excerpt(x : Json) -> String {
let s = x.stringify()
if s.length() > 200 {
String::from_iter(s.iter().take(200)) + "…"
} else {
s
}
}
///|
/// Fail with a message indicating the type that was expected.
pub fn[T] bad_json(
expected : String,
x : Json,
path : Path,
) -> T raise JsonError {
raise JsonError(
"incompatible JSON value where type '\{expected}' was expected: '\{excerpt(x)}' at \{path}",
)
}
///|
/// Fail because a required field is missing.
pub fn[T] missing_field(
type_name : String,
json_field_name : String,
path : Path,
) -> T raise JsonError {
raise JsonError(
"missing field '\{json_field_name}' in JSON object of type '\{type_name}' at \{path}",
)
}
// Parsing and printing
///|
/// Print JSON compactly, or with the given indentation.
pub fn stringify(x : Json, indent? : Int = 0) -> String {
x.stringify(indent~)
}
// Readers for the predefined types
///|
/// Read `null` as `unit`.
pub fn read_unit(x : Json, path : Path) -> Unit raise JsonError {
match x {
Null => ()
_ => bad_json("unit", x, path)
}
}
///|
/// Read a boolean.
pub fn read_bool(x : Json, path : Path) -> Bool raise JsonError {
match x {
True => true
False => false
_ => bad_json("bool", x, path)
}
}
///|
/// Whether the text of a JSON number is an integer literal (no fraction,
/// no exponent), like the integers accepted by atdgen.
fn is_integer_literal(r : String) -> Bool {
r != "" && r.iter().all(c => (c >= '0' && c <= '9') || c == '-')
}
///|
/// Read an integer that must fit in 32 bits.
///
/// When the text of the number is known (see `parse`), it must be an
/// integer literal: `1.0` or `1e3` are rejected, like with atdgen.
/// Otherwise, the number must be an integral value within range.
pub fn read_int(x : Json, path : Path) -> Int raise JsonError {
match x {
Number(_, repr=Some(r)) if is_integer_literal(r) =>
@string.parse_int(r) catch {
_ => bad_json("int", x, path)
}
Number(n, repr=None) if n == n.floor() &&
n >= -2147483648.0 &&
n <= 2147483647.0 => n.to_int()
_ => bad_json("int", x, path)
}
}
///|
/// Read a 64-bit integer, using the exact text of the number when
/// available (see `read_int`).
pub fn read_int64(x : Json, path : Path) -> Int64 raise JsonError {
match x {
Number(_, repr=Some(r)) if is_integer_literal(r) =>
@string.parse_int64(r) catch {
_ => bad_json("int64", x, path)
}
Number(n, repr=None) if n == n.floor() &&
n >= -9223372036854775808.0 &&
n < 9223372036854775808.0 => n.to_int64()
_ => bad_json("int64", x, path)
}
}
///|
/// Read an integer represented as a JSON string (``).
pub fn read_int_of_string(x : Json, path : Path) -> Int raise JsonError {
match x {
String(s) => @string.parse_int(s) catch { _ => bad_json("int", x, path) }
_ => bad_json("int", x, path)
}
}
///|
/// Read a 64-bit integer represented as a JSON string.
pub fn read_int64_of_string(x : Json, path : Path) -> Int64 raise JsonError {
match x {
String(s) =>
@string.parse_int64(s) catch {
_ => bad_json("int64", x, path)
}
_ => bad_json("int64", x, path)
}
}
///|
/// Read a number.
pub fn read_float(x : Json, path : Path) -> Double raise JsonError {
match x {
Number(n, ..) => n
_ => bad_json("float", x, path)
}
}
///|
/// Read a string.
pub fn read_string(x : Json, path : Path) -> String raise JsonError {
match x {
String(s) => s
_ => bad_json("string", x, path)
}
}
///|
/// Read any JSON value (ATD type `abstract`).
#warnings("-unused_error_type")
pub fn read_json(x : Json, _path : Path) -> Json raise JsonError {
x
}
// Writers for the predefined types
///|
/// Write `unit` as `null`.
pub fn write_unit(_x : Unit) -> Json {
Json::null()
}
///|
/// Write a boolean.
pub fn write_bool(x : Bool) -> Json {
Json::boolean(x)
}
///|
/// Write an integer.
pub fn write_int(x : Int) -> Json {
Json::number(x.to_double())
}
///|
/// Write a 64-bit integer, preserving all its digits.
pub fn write_int64(x : Int64) -> Json {
Json::number(x.to_double(), repr=x.to_string())
}
///|
/// Write an integer as a JSON string (``).
pub fn write_int_to_string(x : Int) -> Json {
Json::string(x.to_string())
}
///|
/// Write a 64-bit integer as a JSON string.
pub fn write_int64_to_string(x : Int64) -> Json {
Json::string(x.to_string())
}
///|
/// Write a number.
pub fn write_float(x : Double) -> Json {
Json::number(x)
}
///|
/// The smallest positive normal number, OCaml's `min_float`.
let min_normal_float : Double = 2.2250738585072014e-308
///|
/// Write a number rounded to an integer (``), exactly like
/// atdgen: positive numbers are rounded half up; other numbers are
/// formatted with C's `%.0f`, which rounds half to even (and may print
/// `-0`). Non-finite numbers can't be represented in JSON; they are written
/// as `NaN` or `Infinity`, which is not valid JSON (atdgen raises an error).
pub fn write_float_as_int(x : Double) -> Json {
if x.is_nan() || x.is_inf() {
return Json::number(x)
}
let text = if x >= min_normal_float {
format_integral((x + 0.5).trunc())
} else {
format_float_fixed0(x)
}
let value = @string.parse_double(text) catch { _ => x }
Json::number(value, repr=text)
}
///|
/// Build a writer for numbers with at most `precision` significant digits
/// (``), formatted like atdgen does (C's `%.Ng`).
pub fn write_float_prec(precision : Int) -> (Double) -> Json {
x => {
if x.is_nan() || x.is_inf() {
Json::number(x)
} else {
// like Yojson, precisions outside of 1..16 mean 17
let p = if precision >= 1 && precision <= 16 { precision } else { 17 }
let s = format_float_g(x, p)
let s = if s.iter().all(c => (c >= '0' && c <= '9') || c == '-') {
s + ".0"
} else {
s
}
let v = @string.parse_double(s) catch { _ => x }
Json::number(v, repr=s)
}
}
}
///|
/// Write a string.
pub fn write_string(x : String) -> Json {
Json::string(x)
}
///|
/// Write any JSON value (ATD type `abstract`).
pub fn write_json(x : Json) -> Json {
x
}
// Combinators
///|
/// Reader for `t list`.
pub fn[T] read_list(
read_elt : (Json, Path) -> T raise JsonError,
) -> (Json, Path) -> Array[T] raise JsonError {
(x, path) => {
match x {
Array(l) => {
let res = []
for i, v in l {
res.push(read_elt(v, path.index(i)))
}
res
}
_ => bad_json("array", x, path)
}
}
}
///|
/// Writer for `t list`.
pub fn[T] write_list(write_elt : (T) -> Json) -> (Array[T]) -> Json {
l => Json::array(l.map(write_elt))
}
///|
/// Reader for `t option`, encoded as `"None"` or `["Some", x]`.
pub fn[T] read_option(
read_elt : (Json, Path) -> T raise JsonError,
) -> (Json, Path) -> T? raise JsonError {
(x, path) => {
match x {
String("None") => None
Array([String("Some"), v]) => Some(read_elt(v, path.index(1)))
_ => bad_json("option", x, path)
}
}
}
///|
/// Writer for `t option`.
pub fn[T] write_option(write_elt : (T) -> Json) -> (T?) -> Json {
x => {
match x {
None => Json::string("None")
Some(v) => Json::array([Json::string("Some"), write_elt(v)])
}
}
}
///|
/// Reader for `t nullable`, encoded as `null` or the value itself.
pub fn[T] read_nullable(
read_elt : (Json, Path) -> T raise JsonError,
) -> (Json, Path) -> T? raise JsonError {
(x, path) => {
match x {
Null => None
_ => Some(read_elt(x, path))
}
}
}
///|
/// Writer for `t nullable`.
pub fn[T] write_nullable(write_elt : (T) -> Json) -> (T?) -> Json {
x => {
match x {
None => Json::null()
Some(v) => write_elt(v)
}
}
}
///|
/// Check that a JSON value is an array of length `n` and return its
/// elements.
pub fn read_tuple(
x : Json,
path : Path,
n : Int,
) -> Array[Json] raise JsonError {
match x {
Array(l) if l.length() == n => l
_ => bad_json("tuple of length \{n}", x, path)
}
}
///|
/// Reader for `(string * t) list ` represented as an
/// array of pairs.
pub fn[T] read_assoc_object_into_array(
read_value : (Json, Path) -> T raise JsonError,
) -> (Json, Path) -> Array[(String, T)] raise JsonError {
(x, path) => {
match x {
Object(o) => {
let res = []
for k, v in o {
res.push((k, read_value(v, path.key(k))))
}
res
}
_ => bad_json("object", x, path)
}
}
}
///|
/// Writer for `(string * t) list ` represented as an
/// array of pairs.
pub fn[T] write_assoc_array_to_object(
write_value : (T) -> Json,
) -> (Array[(String, T)]) -> Json {
l => {
let o : Map[String, Json] = Map([])
for kv in l {
o[kv.0] = write_value(kv.1)
}
Json::object(o)
}
}
///|
/// Reader for `(string * t) list `.
pub fn[T] read_assoc_object_into_map(
read_value : (Json, Path) -> T raise JsonError,
) -> (Json, Path) -> Map[String, T] raise JsonError {
(x, path) => {
match x {
Object(o) => {
let res = Map([])
for k, v in o {
res[k] = read_value(v, path.key(k))
}
res
}
_ => bad_json("object", x, path)
}
}
}
///|
/// Writer for `(string * t) list `.
pub fn[T] write_assoc_map_to_object(
write_value : (T) -> Json,
) -> (Map[String, T]) -> Json {
m => {
let o : Map[String, Json] = Map([])
for k, v in m {
o[k] = write_value(v)
}
Json::object(o)
}
}
///|
/// Reader for `(k * v) list `, encoded as a JSON array of
/// pairs.
pub fn[K : Hash + Eq, V] read_assoc_array_into_map(
read_key : (Json, Path) -> K raise JsonError,
read_value : (Json, Path) -> V raise JsonError,
) -> (Json, Path) -> Map[K, V] raise JsonError {
(x, path) => {
match x {
Array(l) => {
let res = Map([])
for i, pair in l {
let p = path.index(i)
match pair {
Array([k, v]) =>
res[read_key(k, p.index(0))] = read_value(v, p.index(1))
_ => bad_json("pair", pair, p)
}
}
res
}
_ => bad_json("array", x, path)
}
}
}
///|
/// Writer for `(k * v) list `.
pub fn[K, V] write_assoc_map_to_array(
write_key : (K) -> Json,
write_value : (V) -> Json,
) -> (Map[K, V]) -> Json {
m => {
let res = []
for k, v in m {
res.push(Json::array([write_key(k), write_value(v)]))
}
Json::array(res)
}
}
// Records
///|
/// Check that a JSON value is an object and return its fields.
pub fn read_object(
x : Json,
path : Path,
type_name : String,
) -> Map[String, Json] raise JsonError {
match x {
Object(o) => o
_ => bad_json(type_name, x, path)
}
}
///|
/// Look up the field of an object, treating `null` like a missing field
/// unless `keep_nulls` is set.
pub fn get_field(
o : Map[String, Json],
key : String,
keep_nulls? : Bool = false,
) -> Json? {
match o.get(key) {
Some(Null) if !keep_nulls => None
x => x
}
}
///|
/// Read a required field.
pub fn[T] read_required_field(
o : Map[String, Json],
type_name : String,
key : String,
read_value : (Json, Path) -> T raise JsonError,
path : Path,
) -> T raise JsonError {
match o.get(key) {
Some(v) => read_value(v, path.key(key))
None => missing_field(type_name, key, path)
}
}
///|
/// Read an optional field, missing or `null` meaning `None`.
pub fn[T] read_optional_field(
o : Map[String, Json],
key : String,
read_value : (Json, Path) -> T raise JsonError,
path : Path,
keep_nulls? : Bool = false,
) -> T? raise JsonError {
match get_field(o, key, keep_nulls~) {
Some(v) => Some(read_value(v, path.key(key)))
None => None
}
}
///|
/// If the JSON value is an object with a single field, return it.
/// This is used for reading sum types represented as JSON objects
/// (``).
pub fn read_single_field(x : Json) -> (String, Json)? {
match x {
Object(o) if o.length() == 1 =>
for k, v in o {
break Some((k, v))
} nobreak {
None
}
_ => None
}
}