// The atproto data model -- the eight kinds of value a record can be made of.
//
// https://atproto.com/specs/data-model
//
// This is what stands in for `Json` throughout the library, and the difference
// is not cosmetic. JSON has one number type and no bytes; the atproto data
// model has integers only and two byte-ish kinds (`bytes` and `cid-link`) that
// JSON can only spell as tagged objects. A caller handed a `Json` would have to
// know that `{"$link": "..."}` is a CID rather than a map with one odd key --
// which is exactly the knowledge a typed client exists to remove.
//
// Two consequences of "integers only" worth stating:
//
// - There is no `Float` case, and `1.5` is a decode error rather than
// something that rounds. DAG-CBOR forbids floats, so a float that got in
// here could not be written back out.
// - Integers are `Int64`, but the protocol's own limit is 53 bits, because
// that is what a JavaScript number holds exactly. Nothing here enforces 53
// bits; a server that sends more is unusual, not malformed.
///|
/// A value in the atproto data model.
pub(all) enum LexValue {
Null
Bool(Bool)
Int(Int64)
Str(String)
/// JSON: `{"$bytes": ""}`.
Bytes(Bytes)
/// JSON: `{"$link": "bafy..."}`.
Link(@syntax.Cid)
Arr(Array[LexValue])
Obj(Map[String, LexValue])
} derive(Eq, Debug)
///|
/// The kind name, for error messages. Matches the Lexicon spelling of each
/// type, so a mismatch reads in the same vocabulary the schema uses.
pub fn LexValue::kind(self : Self) -> String {
match self {
Null => "null"
Bool(_) => "boolean"
Int(_) => "integer"
Str(_) => "string"
Bytes(_) => "bytes"
Link(_) => "cid-link"
Arr(_) => "array"
Obj(_) => "object"
}
}
///|
pub fn LexValue::as_bool(self : Self) -> Bool? {
match self {
Bool(b) => Some(b)
_ => None
}
}
///|
pub fn LexValue::as_int(self : Self) -> Int64? {
match self {
Int(i) => Some(i)
_ => None
}
}
///|
pub fn LexValue::as_string(self : Self) -> String? {
match self {
Str(s) => Some(s)
_ => None
}
}
///|
pub fn LexValue::as_bytes(self : Self) -> Bytes? {
match self {
Bytes(b) => Some(b)
_ => None
}
}
///|
pub fn LexValue::as_link(self : Self) -> @syntax.Cid? {
match self {
Link(c) => Some(c)
_ => None
}
}
///|
pub fn LexValue::as_array(self : Self) -> Array[LexValue]? {
match self {
Arr(a) => Some(a)
_ => None
}
}
///|
pub fn LexValue::as_object(self : Self) -> Map[String, LexValue]? {
match self {
Obj(o) => Some(o)
_ => None
}
}
///|
/// One field of an object, or `None` if this is not an object or has no such
/// field. The two cases are deliberately not distinguished: a caller reaching
/// for a field on a non-object has already gone wrong somewhere else.
pub fn LexValue::get(self : Self, key : String) -> LexValue? {
match self {
Obj(o) => o.get(key)
_ => None
}
}
///|
/// The `$type` discriminator, if this is a tagged object.
///
/// Every record and every open-union member carries one, and it is what
/// dispatch is done on -- never the shape of the object, which is how
/// TypeScript does it and how rsky's untagged unions go wrong.
pub fn LexValue::type_tag(self : Self) -> String? {
match self.get("$type") {
Some(Str(t)) => Some(t)
_ => None
}
}
///|
/// Parses JSON into the data model, decoding `$link` and `$bytes` on the way.
///
/// This and `stringify` are the only doors between JSON and everything above,
/// which is what keeps `Json` out of the library's public API.
pub fn LexValue::parse(text : String) -> LexValue raise DecodeError {
let json = @json.parse(text) catch {
e => raise DecodeError(path="", reason="invalid JSON: \{e}")
}
of_json(json, "")
}
///|
pub fn LexValue::stringify(self : Self) -> String {
to_json(self).stringify()
}