// Reading and writing the wire shapes every message shares: a JSON object whose keys are the
// schema's camelCase names, absent members left out rather than written as null.
///|
/// Builds an object, dropping the members that are absent.
fn json_of(pairs : Array[(String, Json?)]) -> Json {
let out : Map[String, Json] = Map([])
for pair in pairs {
let (key, value) = pair
if value is Some(value) {
out[key] = value
}
}
Json::object(out)
}
///|
/// Adds the discriminator a tagged union carries to the object its payload wrote.
fn json_tagged(payload : Json, key : String, tag : String) -> Json {
let out = match payload {
Object(fields) => fields
_ => Map([])
}
out[key] = Json::string(tag)
Json::object(out)
}
///|
/// Lays a union's payload flat into the object that describes it.
fn merge_json(base : Json, payload : Json) -> Json {
let fields = match base {
Object(fields) => fields
_ => Map([])
}
if payload is Object(payload) {
payload.each(fn(key, value) { fields[key] = value })
}
Json::object(fields)
}
///|
fn[A : @json.ToJson] json_list_of(values : Array[A]?) -> Json? {
json_list(values, value => write(value))
}
///|
fn json_string(value : String?) -> Json? {
value.map(Json::string)
}
///|
fn json_bool(value : Bool?) -> Json? {
value.map(Json::boolean)
}
///|
fn json_int(value : Int64?) -> Json? {
value.map(value => Json::number(value.to_double(), repr=value.to_string()))
}
///|
fn json_float(value : Double?) -> Json? {
value.map(value => Json::number(value))
}
///|
fn[A] json_list(values : Array[A]?, write : (A) -> Json) -> Json? {
guard values is Some(values) else { return None }
Some(Json::array(values.map(write)))
}
///|
/// Encodes through `ToJson`, so the codecs below read as one expression per member.
fn[A : @json.ToJson] write(value : A) -> Json {
@json.ToJson::to_json(value)
}
///|
/// Decodes through `FromJson`, for the same reason.
fn[A : @json.FromJson] read(
json : Json,
path : @json.JsonPath,
) -> A raise @json.JsonDecodeError {
@json.from_json(json, path~)
}
///|
/// Decodes a whole message body, where no path is being tracked yet. A body that does not decode is
/// the other side's fault, so it raises `Malformed`.
fn[A : @json.FromJson] read_root(json : Json) -> A raise Refused {
@json.from_json(json) catch {
@json.JsonDecodeError((path, reason)) =>
raise Refused::Malformed(reason="\{path}: \{reason}")
}
}
///|
fn kind_name(json : Json) -> String {
match json {
Null => "null"
True | False => "a boolean"
Number(_, ..) => "a number"
String(_) => "a string"
Array(_) => "an array"
Object(_) => "an object"
}
}
///|
fn expect_object(
json : Json,
path : @json.JsonPath,
) -> Map[String, Json] raise @json.JsonDecodeError {
guard json is Object(fields) else {
raise @json.JsonDecodeError(
(path, "want an object, got \{kind_name(json)}"),
)
}
fields
}
///|
fn want(
path : @json.JsonPath,
expected : String,
got : Json,
) -> @json.JsonDecodeError {
@json.JsonDecodeError((path, "want \{expected}, got \{kind_name(got)}"))
}
///|
fn field_of(
fields : Map[String, Json],
key : String,
_path : @json.JsonPath,
) -> Json? {
match fields.get(key) {
None | Some(Null) => None
Some(value) => Some(value)
}
}
///|
fn required(
fields : Map[String, Json],
key : String,
path : @json.JsonPath,
) -> Json raise @json.JsonDecodeError {
match fields.get(key) {
None | Some(Null) =>
raise @json.JsonDecodeError((path.add_key(key), "missing"))
Some(value) => value
}
}
///|
fn text_of(
value : Json,
path : @json.JsonPath,
) -> String raise @json.JsonDecodeError {
guard value is String(text) else { raise want(path, "a string", value) }
text
}
///|
fn bool_of(
value : Json,
path : @json.JsonPath,
) -> Bool raise @json.JsonDecodeError {
match value {
True => true
False => false
_ => raise want(path, "a boolean", value)
}
}
///|
/// Numbers arrive as doubles; an integer member rejects a fraction rather than rounding it.
fn int_of(
value : Json,
path : @json.JsonPath,
) -> Int64 raise @json.JsonDecodeError {
guard value is Number(number, ..) && number == number.to_int64().to_double() else {
raise want(path, "an integer", value)
}
number.to_int64()
}
///|
fn float_of(
value : Json,
path : @json.JsonPath,
) -> Double raise @json.JsonDecodeError {
guard value is Number(number, ..) else { raise want(path, "a number", value) }
number
}
///|
fn array_of(
value : Json,
path : @json.JsonPath,
) -> Array[Json] raise @json.JsonDecodeError {
guard value is Array(items) else { raise want(path, "an array", value) }
items
}
///|
fn string_member(
fields : Map[String, Json],
key : String,
path : @json.JsonPath,
) -> String? raise @json.JsonDecodeError {
match field_of(fields, key, path) {
None => None
Some(value) => Some(text_of(value, path.add_key(key)))
}
}
///|
fn required_string(
fields : Map[String, Json],
key : String,
path : @json.JsonPath,
) -> String raise @json.JsonDecodeError {
text_of(required(fields, key, path), path.add_key(key))
}
///|
fn bool_member(
fields : Map[String, Json],
key : String,
path : @json.JsonPath,
) -> Bool? raise @json.JsonDecodeError {
match field_of(fields, key, path) {
None => None
Some(value) => Some(bool_of(value, path.add_key(key)))
}
}
///|
fn int_member(
fields : Map[String, Json],
key : String,
path : @json.JsonPath,
) -> Int64? raise @json.JsonDecodeError {
match field_of(fields, key, path) {
None => None
Some(value) => Some(int_of(value, path.add_key(key)))
}
}
///|
fn float_member(
fields : Map[String, Json],
key : String,
path : @json.JsonPath,
) -> Double? raise @json.JsonDecodeError {
match field_of(fields, key, path) {
None => None
Some(value) => Some(float_of(value, path.add_key(key)))
}
}
///|
/// A nested object decoded by its own `FromJson`.
fn[A : @json.FromJson] object_member(
fields : Map[String, Json],
key : String,
path : @json.JsonPath,
) -> A? raise @json.JsonDecodeError {
guard field_of(fields, key, path) is Some(value) else { return None }
Some(read(value, path.add_key(key)))
}
///|
/// A required nested object.
fn[A : @json.FromJson] required_object(
fields : Map[String, Json],
key : String,
path : @json.JsonPath,
) -> A raise @json.JsonDecodeError {
read(required(fields, key, path), path.add_key(key))
}
///|
/// A list member, each item with its own path for error messages.
fn[A] list_member(
fields : Map[String, Json],
key : String,
path : @json.JsonPath,
decode : (Json, @json.JsonPath) -> A raise @json.JsonDecodeError,
) -> Array[A]? raise @json.JsonDecodeError {
guard field_of(fields, key, path) is Some(value) else { return None }
let at = path.add_key(key)
Some(decode_list(value, at, decode))
}
///|
fn[A] decode_list(
value : Json,
path : @json.JsonPath,
decode : (Json, @json.JsonPath) -> A raise @json.JsonDecodeError,
) -> Array[A] raise @json.JsonDecodeError {
let out = []
for index, item in array_of(value, path) {
out.push(decode(item, path.add_index(index)))
}
out
}
///|
/// A required list member.
fn[A] required_list(
fields : Map[String, Json],
key : String,
path : @json.JsonPath,
decode : (Json, @json.JsonPath) -> A raise @json.JsonDecodeError,
) -> Array[A] raise @json.JsonDecodeError {
decode_list(required(fields, key, path), path.add_key(key), decode)
}
///|
/// A required list of objects, each decoded by its own `FromJson`.
fn[A : @json.FromJson] required_objects(
fields : Map[String, Json],
key : String,
path : @json.JsonPath,
) -> Array[A] raise @json.JsonDecodeError {
required_list(fields, key, path, (item, at) => read(item, at))
}
///|
fn meta_of(fields : Map[String, Json], path : @json.JsonPath) -> Json? {
ignore(path)
field_of(fields, "_meta", path)
}
///|
/// An enum member, read through its own name table.
fn[A] enum_member(
fields : Map[String, Json],
key : String,
path : @json.JsonPath,
of : (String) -> A?,
) -> A? raise @json.JsonDecodeError {
guard field_of(fields, key, path) is Some(value) else { return None }
let at = path.add_key(key)
match of(text_of(value, at)) {
Some(variant) => Some(variant)
None => raise @json.JsonDecodeError((at, "unknown \{key}"))
}
}
///|
/// A required enum member.
fn[A] required_enum(
fields : Map[String, Json],
key : String,
path : @json.JsonPath,
of : (String) -> A?,
) -> A raise @json.JsonDecodeError {
let at = path.add_key(key)
match of(required_string(fields, key, path)) {
Some(variant) => variant
None => raise @json.JsonDecodeError((at, "unknown \{key}"))
}
}