maru

    A minimal MoonBit validation library that constructs a typed value while validating, without holding a schema as a value.

    json
    validation
    Download zip
    Author
    Version
    0.2.0
    License
    MIT
    Last updated
    13 days ago
    Downloads
    13

    #maru

    A minimal MoonBit validation library that constructs a typed value while validating, without holding a schema as a value.

    ///|
    test {
    let json : Json = { "name": "Ada", "age": 36 }
    let obj = @maru.from_json(json).object()
    let name = obj.field("name").string().nonempty().value()
    let age = obj.field("age").int().range(0, 150).value()
    assert_eq(name, "Ada")
    assert_eq(age, 36)
    }

    #Install

    moon add kosei28/maru

    Then import it in moon.pkg:

    import { "kosei28/maru" }

    #Design

    • No schema-as-value
    • No validator builder or delayed evaluation
    • Keep fetching, type checks, and value checks separate
    • Propagate errors with MoonBit raise
    • maru types are thin wrappers that only hold a value and a path
    • Start with from_json to wrap JSON as a Value
    • Finish with .value() to unwrap to a plain MoonBit value
    • Parse unsupported types yourself via Value::raw()

    #Model

    Raw Json, object fields, and array elements are all the same Value.

    ///|
    test {
    let value = @maru.from_json({ "ok": true })
    assert_eq(value.object().field("ok").bool().value(), true)
    }

    value.null() value.bool() value.int() value.int64() value.double() value.string() value.array() value.object()

    #Type conversion and checks

    After converting to a primitive, the result is Checked[T] and still carries a path.

    Value ↓ int() Checked[Int] ↓ range(...) Checked[Int] ↓ value() Int

    Main checks:

    .nonempty() .min_len(n) .max_len(n) .email() .min(n) .max(n) .range(min, max) .refine(predicate, message)

    On failure, maru raises @maru.Invalid. The message includes the path where it failed.

    users[2].email: invalid email

    #Building a struct

    ///|
    fn User::parse(value : @maru.Value) -> User raise @maru.Invalid {
    let obj = value.object()
    User::{
    name: obj.field("name").string().nonempty().value(),
    email: obj.field("email").string().email().value(),
    age: obj.field("age").int().range(0, 150).value(),
    rating: obj.field("rating").double().range(0.0, 5.0).value(),
    nickname: obj
    .optional_field("nickname")
    .map(v => v.string().max_len(30).value()),
    }
    }

    From the top level, call User::parse(@maru.from_json(json)).

    #Objects

    Fetching a field and checking its type are separate.

    Object ↓ field("name") Value ↓ string() Checked[String]

    • field is required. A missing key is an error
    • optional_field returns None only when the key is absent

    ///|
    test {
    let obj = @maru.from_json({ "name": "Ada" }).object()
    let nickname = obj
    .optional_field("nickname")
    .map(v => v.string().max_len(30).value())
    assert_eq(nickname, None)
    }

    #Arrays

    Array elements are Values as well.

    ///|
    test {
    let scores = @maru.from_json({ "scores": [10.0, 99.5] })
    .object()
    .field("scores")
    .array()
    .map(v => v.double().range(0.0, 100.0).value())
    .value()
    assert_eq(scores, [10.0, 99.5])
    }

    #Multiple candidates

    first_of applies parsers to the same Value in order and returns the first success.

    ///|
    test {
    let id = @maru.first_of(@maru.from_json(7), [
    v => v.string().value(),
    v => v.int().value().to_string(),
    ])
    assert_eq(id, "7")
    }

    nullable values can be written the same way.

    ///|
    test {
    let nickname : String? = @maru.first_of(@maru.from_json(null), [
    v => { v.null(); None },
    v => Some(v.string().value()),
    ])
    assert_eq(nickname, None)
    }

    #Raw JSON

    Value.raw() is the escape hatch for types maru does not convert directly. Checked[T] has no raw().

    from_json(json) -> Value Value.raw() -> Json Checked[T].value() -> T

    Length

    pub(open) trait Length {
    fn length(Self) -> Int
    }

    Types that have a length, used by nonempty, min_len, and max_len.
    impl Length for String
    impl Length for Array[T]

    Invalid

    pub(all) suberror Invalid {
    Invalid(Path, String)
    } derive(Eq)

    A validation failure, carrying the JSON path where it occurred.
    impl Show for Invalid

    Invalid::to_string

    fn Invalid::to_string(self : Invalid) -> String

    Checked

    type Checked[T]

    A successfully converted value that still carries its JSON path.

    Checked::email

    fn Checked::email(self : Checked[String]) -> Checked[String] raise Invalid

    Require a string to look like an email address.

    Checked::map

    fn[A, B] Checked::map(self : Checked[Array[A]], f : (A) -> B raise?) -> Checked[Array[B]] raise?

    Map each array element. Errors keep the element's path.

    Checked::max

    fn[T : Compare + Show + Eq] Checked::max(self : Checked[T], bound : T) -> Checked[T] raise Invalid

    Require self <= bound.

    Checked::max_len

    fn[T : Length] Checked::max_len(self : Checked[T], n : Int) -> Checked[T] raise Invalid

    Require a maximum length for strings and arrays.

    Checked::min

    fn[T : Compare + Show + Eq] Checked::min(self : Checked[T], bound : T) -> Checked[T] raise Invalid

    Require self >= bound.

    Checked::min_len

    fn[T : Length] Checked::min_len(self : Checked[T], n : Int) -> Checked[T] raise Invalid

    Require a minimum length for strings and arrays.

    Checked::nonempty

    fn[T : Length] Checked::nonempty(self : Checked[T]) -> Checked[T] raise Invalid

    Reject empty strings and arrays.

    Checked::range

    fn[T : Compare + Show + Eq] Checked::range(self : Checked[T], min : T, max : T) -> Checked[T] raise Invalid

    Require min <= self <= max.

    Checked::refine

    fn[T] Checked::refine(self : Checked[T], predicate : (T) -> Bool, message : String) -> Checked[T] raise Invalid

    Apply a custom predicate. The message is used when it returns false.

    Checked::value

    fn[T] Checked::value(self : Checked[T]) -> T

    Unwrap the converted value.

    Object

    type Object

    An object together with the path used in error messages.

    Object::field

    fn Object::field(self : Object, name : String) -> Value raise Invalid

    Get a required field. Missing keys raise; JSON null is still a Value.

    Object::optional_field

    fn Object::optional_field(self : Object, name : String) -> Value?

    Get an optional field.

    Returns None only when the key is absent. JSON null is Some(Value).

    Path

    pub(all) enum Path {
    Root
    Field(Path, String)
    Index(Path, Int)
    } derive(Eq)

    A location inside a JSON value.

    Field names are joined with ., and array indices are written as [n]. The root path is empty.
    impl Show for Path

    Path::to_string

    fn Path::to_string(self : Path) -> String

    Value

    type Value

    A JSON value together with the path used in error messages.

    Value::array

    fn Value::array(self : Value) -> Checked[Array[Value]] raise Invalid

    Convert this value to an array of Values.

    Value::bool

    fn Value::bool(self : Value) -> Checked[Bool] raise Invalid

    Convert this value to Bool.

    Value::double

    fn Value::double(self : Value) -> Checked[Double] raise Invalid

    Convert this value to Double.

    Value::int

    fn Value::int(self : Value) -> Checked[Int] raise Invalid

    Convert this value to Int.

    JSON numbers must be finite integers in the 32-bit range.

    Value::int64

    fn Value::int64(self : Value) -> Checked[Int64] raise Invalid

    Convert this value to Int64.

    JSON numbers must be finite integers in the 64-bit range.

    Value::null

    fn Value::null(self : Value) -> Unit raise Invalid

    Require this value to be JSON null.

    Value::object

    fn Value::object(self : Value) -> Object raise Invalid

    Convert this value to an object.

    Value::raw

    fn Value::raw(self : Value) -> Json

    Return the underlying JSON. Use this as an escape hatch for types that maru does not convert directly.

    Value::string

    fn Value::string(self : Value) -> Checked[String] raise Invalid

    Convert this value to String.

    first_of

    fn[T] first_of(value : Value, parsers : Array[(Value) -> T raise Invalid]) -> T raise Invalid

    Try each parser in order and return the first success.

    Raises when every parser fails.

    from_json

    fn from_json(json : Json) -> Value

    Wrap JSON as a Value so it can be validated while a typed value is built.