iyi

Standard library, 0.18.0

std/json

JSON, read and written in iyi: a byte lexer and a recursive parser into Any, a streaming PullParser, a Builder that writes a document one value at a time, and JSON.parse / JSON.parse? / JSON.build / JSON.to_json / JSON.from_json on top of them.

import std/json to use it. Source: src/std/json.iyi at v0.18.0.

What a document becomes: null is Nil, true/false are Bool, an integer literal that fits Int64 is Int64 (-0 is the integer 0), every other number is Float64 (a fraction, an exponent, or an integer past what Int64 holds, read as the nearest double), a string is String with its escapes decoded (\uXXXX pairs into one code point), an array is Array(Any) and an object is Hash(String, Any) in the order the keys were written.

What the reader refuses, each with the line and column it stopped at: anything that is not a JSON document (bare words, leading zeros, a trailing comma, a raw control character, invalid UTF-8 or a lone surrogate inside a string, an invalid escape, a byte order mark, text after the root value, nesting past 256 levels, a number too large for a Float64) and an object that names the same key twice, in JSON.parse and in the PullParser alike. JSON.parse panics with that sentence; JSON.parse? answers nil. Nor does JSON.to_json write such an object: two keys of a Hash written as one text (nil and "null", 1 and "1") are refused, naming both.

What the writer refuses: NaN and Infinity, which are not JSON numbers; a value where an object key is due, a field outside an object, a second value at the document root, a container closed that was never opened, and to_s of a document with a container still open or no value written. Every control byte under 0x20 and the byte 0x7f are written as \u00XX; bytes in a string that are not a code point are written as U+FFFD, one per ill-formed subpart, so what is written is UTF-8 the reader takes back; to_ascii_json also writes every non-ASCII code point as \uXXXX, with a surrogate pair past the basic plane, and U+FFFD as \ufffd.

ToJSON is the trait a type implements to be written by JSON.to_json; Any implements it, and so do the prelude's scalars, arrays and hashes through write_value.

serializablemacro

derive serializable, in a struct or class whose fields are written with their types: the type reads and writes itself as a JSON object, a key per field, named as the field is.

  pub struct Coord
    getter x : Float64
    @[JSON::Field(key: "label")]
    getter name : String
    derive serializable
  end
  Coord.from_json(%({"x": 1.5, "label": "a"})).x   # => 1.5
  coord.to_json                                    # => {"x":1.5,"label":"a"}

A field's value is read and written as JSON.read_value and JSON.write_value read and write its type: the built-in values, an enum, an Array or Hash(String, V) of them, an Any, another serializable type. A key the type has no field for is passed over; a field with no key keeps its default when it is written with one, as does a field with a default whose key holds null where its type does not admit nil; a field with neither is refused by name unless its type admits nil, which it then holds. @[JSON::Field(...)] renames a field, leaves it out either way, leaves a nil one out, or reads and writes it through a converter (see JSON::Field). It is what the other library's include JSON::Serializable writes, and the derive runs once, in the type's own module (SPEC.md II.4), so a consumer calls the methods without the macro.

Each field's options are read off the type when its methods are compiled, in one begin so what a field's annotation says is known to every line after it; and refused there, with the field named, when they do not make sense.

std/jsoniyi doc src/std/json.iyi
pub macro serializable(declaration)

Anystruct

One JSON value of any kind, the result of JSON.parse. The as_x methods panic when the value is of another kind; the as_x? ones answer nil.

std/jsoniyi doc src/std/json.iyi
pub struct Any
  alias Type = Nil | Bool | Int64 | Float64 | String | Array(Any) | Hash(String, Any)
  # Equal when the values are: an integer equals the float with the same
  # value, arrays element by element, objects key by key in any order.
  def ==(other : Any) : Bool
  def [](index : Int32) : Any
  def [](key : String) : Any
  def []?(index : Int32) : Any | ::Nil
  def []?(key : String) : Any | ::Nil
  def as_a : Array(Any)
  def as_a? : Array(Any) | ::Nil
  def as_bool : Bool
  def as_bool? : Bool | ::Nil
  # A float, or an integer read as one.
  def as_f : Float64
  def as_f? : Float64 | ::Nil
  def as_h : Hash(String, Any)
  def as_h? : Hash(String, Any) | ::Nil
  def as_i : Int32
  def as_i64 : Int64
  def as_i64? : Int64 | ::Nil
  def as_i? : Int32 | ::Nil
  def as_nil : Nil
  def as_s : String
  def as_s? : String | ::Nil
  def dig(key : String) : Any
  def dig(index : Int32) : Any
  def dig(k : String, *subkeys : String | Int32) : Any
  def dig(idx : Int32, *subkeys : String | Int32) : Any
  def dig?(key : String) : Any | ::Nil
  def dig?(index : Int32) : Any | ::Nil
  def dig?(k : String, *subkeys : String | Int32) : Any | ::Nil
  def dig?(idx : Int32, *subkeys : String | Int32) : Any | ::Nil
  protected def float_value? : Float64 | ::Nil
  # Consistent with `==`: a whole float hashes as the integer it equals, an
  # object's hash does not depend on the order its keys were written in.
  def hash : Int32
  def initialize(raw : Type)
  def inspect : String
  # The kind of value this is, in the word a refusal uses: `null`, `bool`,
  # `integer`, `float`, `string`, `array` or `object`.
  def kind : String
  def self.new(val : Int32) : Any
  def raw : Type
  # How many elements an array or an object holds. A scalar has no size.
  def size : Int32
  def size? : Int32 | ::Nil
  def to_ascii_json : String
  def to_json : String
  def to_pretty_json(indent : String = "  ") : String
  # A string as itself and null as nothing, as `YAML::Any#to_s` and the
  # other library answer them; a number, a boolean or a collection as its
  # JSON text. A string was its JSON text too, so `"hello #{doc["name"]}"`
  # printed `hello "Ada"`, and null printed `null`.
  def to_s : String
end

Builderclass

Writes a document one value at a time. indent= makes it pretty; ascii_only= escapes every non-ASCII code point.

std/jsoniyi doc src/std/json.iyi
pub class Builder
  def array(& : (-> Nil)) : Nil
  def ascii_only : Bool
  def ascii_only=(value : Bool) : Bool
  def bool(val : Bool) : Nil
  # Whether no value has been written yet.
  def empty? : Bool
  def end_array : Nil
  def end_object : Nil
  def field(name : String, val : Any) : Nil
  def field(name : String, val : String) : Nil
  def field(name : String, val : Int32) : Nil
  def field(name : String, val : Int64) : Nil
  def field(name : String, val : Float64) : Nil
  def field(name : String, val : Bool) : Nil
  def field(name : String, val : Nil) : Nil
  # A key; the value that follows it is written by the next call. A key
  # whose text is one this object already holds is refused, naming both:
  # the reader refuses a duplicate key, and the builder wrote one - a key
  # given twice, or two Strings whose bytes are not UTF-8 and are both
  # written as U+FFFD (`{"a\xFF" => 1, "a\xFE" => 2}` was written
  # `{"a\uFFFD":1,"a\uFFFD":2}`, and `JSON.parse` refused it).
  def field(name : String) : Nil
  def field(name : String, & : (-> Nil)) : Nil
  def indent=(spaces : Int32) : Nil
  def indent=(str : String) : Nil
  def initialize
  def max_nesting : Int32
  def max_nesting=(value : Int32) : Int32
  def null : Nil
  def number(val : Int32) : Nil
  def number(val : Int64) : Nil
  def number(val : UInt8) : Nil
  def number(val : UInt64) : Nil
  def number(val : Float64) : Nil
  def object(& : (-> Nil)) : Nil
  # Text written as it is: the caller's own JSON.
  def raw(str : String) : Nil
  def start_array : Nil
  def start_object : Nil
  def string(val : String) : Nil
  # The document's text. A container still open or no value written at all
  # is not a document, and is refused.
  def to_s : String
end

JSONstruct

std/jsoniyi doc src/std/json.iyi
pub struct JSON
  # How `derive serializable` writes and reads one field, the other
  # library's `JSON::Field`:
  #
  #     @[JSON::Field(key: "userName")]
  #     getter user_name : String
  #
  # * `key:` the object key, where it is not the field's own name;
  # * `ignore: true` neither written nor read, `ignore_serialize: true` not
  #   written, `ignore_deserialize: true` not read - a field that is not
  #   read keeps its default, or is nil, and needs one or the other;
  # * `emit_null: false` a nil field left out of the object. A nil field is
  #   written as `null` otherwise, where the other library leaves it out
  #   unless asked;
  # * `converter:` a type whose `self.to_json(value, json : Builder)` writes
  #   the field and `self.from_json(pull : PullParser)` reads it.
  #
  # An option it does not know, a key that is not a string, and two fields
  # written under one key are refused when the type is compiled.
  annotation Field
  end
  # Text written through a `Builder` the block is handed. `indent` is a
  # count of spaces or the string one level is indented by. A block that
  # writes no value, or leaves a container open, is refused.
  def self.build(indent : String | Int32 | Nil = nil, & : (Builder -> Nil)) : String
  # The document read into `type`: `Nil`, `Bool`, `Int32`, `Int64`, `UInt8`,
  # `UInt64`, `Float64`, `String`, `Any`, a type with `derive serializable`,
  # one of those or `nil`, or an `Array` or `Hash(String, V)` of those. An
  # integer outside the type is refused, as is anything after the root
  # value.
  def self.from_json(str : String, type : T.class) : T forall T
  def initialize
  # The document as an `Any`; panics with the line and column of the first
  # thing it refuses.
  def self.parse(input : String) : Any
  # The document as an `Any`, or `nil` when it is not one.
  def self.parse?(input : String) : Any | ::Nil
  def self.read_value(pull : PullParser, type : Nil.class) : Nil
  def self.read_value(pull : PullParser, type : Bool.class) : Bool
  def self.read_value(pull : PullParser, type : Int32.class) : Int32
  def self.read_value(pull : PullParser, type : Int64.class) : Int64
  def self.read_value(pull : PullParser, type : UInt8.class) : UInt8
  # An integer past Int64 reaches the reader as a float that still carries
  # its digits; those are read here, so the whole UInt64 range comes back.
  def self.read_value(pull : PullParser, type : UInt64.class) : UInt64
  def self.read_value(pull : PullParser, type : Float64.class) : Float64
  def self.read_value(pull : PullParser, type : String.class) : String
  def self.read_value(pull : PullParser, type : Any.class) : Any
  def self.read_value(pull : PullParser, type : Array(T).class) : Array(T) forall T
  def self.read_value(pull : PullParser, type : Hash(String, V).class) : Hash(String, V) forall V
  # A type that reads itself: the `initialize(pull)` a `derive
  # serializable` wrote. A type that admits nil reads `null` as the nil and
  # anything else as the other type (`x || raise`, as the derive asks it),
  # the way `write_or_null` writes it. That was `T.new(pull)` too, and
  # `Int32 | Nil` has no such `new`, so `JSON.from_json("5", Int32?)` and
  # `Array(Int32?)` did not build.
  def self.read_value(pull : PullParser, type : T.class) : T forall T
  # Any value `write_value` knows: `nil`, a `Bool`, a prelude integer, a
  # `Float64`, a `String`, an `Any`, an `Array` or `Hash` of those, or a type
  # that implements `ToJSON`; each of them or `nil`.
  def self.to_json(obj : T) : String forall T
  def self.write_value(val : Nil, b : Builder) : Nil
  def self.write_value(val : Bool, b : Builder) : Nil
  def self.write_value(val : Int32, b : Builder) : Nil
  def self.write_value(val : Int64, b : Builder) : Nil
  def self.write_value(val : UInt8, b : Builder) : Nil
  def self.write_value(val : UInt64, b : Builder) : Nil
  def self.write_value(val : Float64, b : Builder) : Nil
  def self.write_value(val : String, b : Builder) : Nil
  def self.write_value(val : Any, b : Builder) : Nil
  def self.write_value(val : Array(T), b : Builder) : Nil forall T
  # A String key is written as it is, and two of them are never one text.
  def self.write_value(val : Hash(String, V), b : Builder) : Nil forall V
  # Any other key is written by its `to_s`, `nil` as `"null"`, the word a
  # JSON null is, so two keys can be one text: `{nil => 1, "null" => 2}`
  # was written `{"null":1,"null":2}`, which `JSON.parse` refuses
  # ("duplicate key 'null' at line 1, column 11"). The texts written are
  # kept, and a repeat is refused, naming both keys.
  def self.write_value(val : Hash(K, V), b : Builder) : Nil forall K, V
  # A `ToJSON` value writes itself. An enum member is written as the other
  # library writes one: its name underscored (`Color::DarkRed` is
  # `"dark_red"`), and a `@[Flags]` enum as the array of its members'
  # names; a value no member has is written as its number. Told apart here
  # and not by an overload restricted to `Enum`, which a program built
  # from this module's artifact read as a class and could not pass an enum
  # to.
  def self.write_value(val : T, b : Builder) : Nil forall T
end

Parserclass

Reads a whole document into an Any. Like the lexer it records the first failure instead of panicking; JSON.parse and JSON.parse? decide what to do with it.

std/jsoniyi doc src/std/json.iyi
pub class Parser
  def error : String | ::Nil
  def initialize(input : String)
  def parse : Any
end

PullParserclass

Reads a document one event at a time: kind names what the reader is on, the read_x methods take it and move on. The grammar is checked as it is read, so a missing comma or colon is refused where it is, with a panic.

std/jsoniyi doc src/std/json.iyi
pub class PullParser
  enum Kind : Int32
    Null = 0
    Bool = 1
    Int = 2
    Float = 3
    String = 4
    BeginArray = 5
    EndArray = 6
    BeginObject = 7
    EndObject = 8
    EOF = 9
    # Returns `true` if this enum value equals `BeginArray`
    def begin_array? : ::Bool
    # Returns `true` if this enum value equals `BeginObject`
    def begin_object? : ::Bool
    # Returns `true` if this enum value equals `Bool`
    def bool? : ::Bool
    # Returns `true` if this enum value equals `EndArray`
    def end_array? : ::Bool
    # Returns `true` if this enum value equals `EndObject`
    def end_object? : ::Bool
    # Returns `true` if this enum value equals `EOF`
    def eof? : ::Bool
    # Returns `true` if this enum value equals `Float`
    def float? : ::Bool
    # Returns `true` if this enum value equals `Int`
    def int? : ::Bool
    # Returns `true` if this enum value equals `Null`
    def null? : ::Bool
    # Returns `true` if this enum value equals `String`
    def string? : ::Bool
  end
  def bool_value : Bool
  def column_number : Int32
  def float_value : Float64
  def initialize(input : String)
  def int_value : Int64
  def kind : Kind
  def line_number : Int32
  def max_nesting : Int32
  def max_nesting=(value : Int32) : Int32
  # Reads an object, yields once at the value of `key`, and skips the rest.
  def on_key(key : String, & : (-> Nil)) : Nil
  # The number the reader is on, as it was written.
  def raw_value : String
  # The value the reader is on, as an `Any`.
  def read_any : Any
  def read_array(& : (-> Nil)) : Nil
  def read_begin_array : Nil
  def read_begin_object : Nil
  def read_bool : Bool
  def read_end_array : Nil
  def read_end_object : Nil
  # A float, or an integer read as one.
  def read_float : Float64
  def read_int : Int64
  def read_next : Kind
  def read_null : Nil
  def read_null? : Bool
  # Yields each key of the object the reader is on, with the reader at its
  # value. A key named twice is refused, as `JSON.parse` refuses it.
  def read_object(& : (String -> Nil)) : Nil
  def read_object_key : String
  # The value the reader is on, written into `json` as it is read. A number
  # is written as its own text, the text it was read from: rebuilt from the
  # Int64 or Float64 it parsed to, `12345678901234567890` came back as
  # `1.2345678901234567e+19` and `1E+2` as `100.0`, and a UInt64 id passed
  # through here did not read back as itself.
  def read_raw(json : Builder) : Nil
  # The value the reader is on, as JSON text.
  def read_raw : String
  def read_string : String
  # Passes over the value the reader is on, whole.
  def skip : Nil
  def string_value : String
end

ToJSONtrait

A type that can write itself into a Builder. JSON.to_json accepts any value whose type implements it, as a derive serializable type does.

std/jsoniyi doc src/std/json.iyi
pub trait ToJSON
  abstract def to_json(json : Builder) : Nil
end

ToJSON for Anyimpl

std/jsoniyi doc src/std/json.iyi
impl ToJSON for Any
  def to_json(json : Builder) : Nil
end