src/schema.bend source
src/schema.bend on the hub · documented module
import Baseimport ./json.bend as Jsonimport ./server.bend as Webtype Field<-S: Data> is Data: Field{name: String, required: Bool, schema: S}type Schema is Data: Text{min: Nat, max: Nat} UInt{min: U32, max: U32} Boolean{} Nullable{inner: Schema} Sequence{inner: Schema, min: Nat, max: Nat} Object{fields: List<&2, Field<Schema>>, strict: Bool}type Verdict is Data: Valid{} Invalid{path: List<&2, String>, code: String}type Task is Data: Check{schema: Schema, path: List<&2, String>, value: Json.Json} Failure{path: List<&2, String>, code: String}def check(ok: Bool, path: List<&2, String>, code: String, rest: List<&2, Task>) -> List<&2, Task>: match ok: case True{}: rest case False{}: [Failure{path, code}]def text(min: Nat, max: Nat, path: List<&2, String>, value: Json.Json, rest: List<&2, Task>) -> List<&2, Task>: match value: case Json.JsonString{+s}: check(Bool.and(Nat.is_ge(String.length(s), min), Nat.is_le(String.length(s), max)), path, "length", rest) case other: [Failure{path, "type"}]def uint_value(min: U32, max: U32, path: List<&2, String>, value: Maybe<&2, U32>, rest: List<&2, Task>) -> List<&2, Task>: match value: case Some{+n}: check(Bool.and(U32.is_ge(n, min), U32.is_le(n, max)), path, "range", rest) case None{}: [Failure{path, "integer"}]def uint(min: U32, max: U32, path: List<&2, String>, value: Json.Json, rest: List<&2, Task>) -> List<&2, Task>: match value: case Json.JsonNumber{s}: uint_value(min, max, path, U32.read(s), rest) case other: [Failure{path, "type"}]def boolean(path: List<&2, String>, value: Json.Json, rest: List<&2, Task>) -> List<&2, Task>: match value: case Json.JsonBool{b}: rest case other: [Failure{path, "type"}]def nullable(inner: Schema, path: List<&2, String>, value: Json.Json, rest: List<&2, Task>) -> List<&2, Task>: match value: case Json.JsonNull{}: rest case other: Check{inner, path, other} <> restdef child(path: List<&2, String>, name: String) -> List<&2, String>: List.append(&2, String, path, [name])def items(+schema: Schema, +path: List<&2, String>, values: List<&2, Json.Json>, +index: Nat, rest: List<&2, Task>) -> List<&2, Task>: match values: case Nil{}: rest case Con{value, tail}: Check{schema, child(path, Nat.show(index)), value} <> items(schema, path, tail, 1n+index, rest)def sequence(inner: Schema, min: Nat, max: Nat, +path: List<&2, String>, value: Json.Json, rest: List<&2, Task>) -> List<&2, Task>: match value: case Json.JsonArray{+values}: check(Bool.and(Nat.is_ge(List.length(&2, Json.Json, values), min), Nat.is_le(List.length(&2, Json.Json, values), max)), path, "length", items(inner, path, values, 0n, rest)) case other: [Failure{path, "type"}]def field_value(required: Bool, schema: Schema, path: List<&2, String>, value: Maybe<Json.Json>, rest: List<&2, Task>) -> List<&2, Task>: match value: case Some{v}: Check{schema, path, v} <> rest case None{}: check(Bool.not(required), path, "required", rest)def fields(rules: List<&2, Field<Schema>>, +path: List<&2, String>, +entries: List<&2, Sigma<&2, &2, String, _ => Json.Json>>, rest: List<&2, Task>) -> List<&2, Task>: match rules: case Nil{}: rest case Con{Field{+name, required, schema}, tail}: field_value(required, schema, child(path, name), Json.lookup(name, entries), fields(tail, path, entries, rest))def named_step(found: Bool, next: Unit -> Bool) -> Bool: match found: case True{}: True{} case False{}: next(Unit{})def named(+name: String, rules: List<&2, Field<Schema>>) -> Bool: match rules: case Nil{}: False{} case Con{Field{key, required, schema}, tail}: named_step(String.eq(name, key), u => named(name, tail))def extras(+rules: List<&2, Field<Schema>>, +path: List<&2, String>, entries: List<&2, Sigma<&2, &2, String, _ => Json.Json>>, rest: List<&2, Task>) -> List<&2, Task>: match entries: case Nil{}: rest case Con{(+name, value), tail}: check(named(name, rules), child(path, name), "unknown", extras(rules, path, tail, rest))def strict(enabled: Bool, rules: List<&2, Field<Schema>>, path: List<&2, String>, entries: List<&2, Sigma<&2, &2, String, _ => Json.Json>>, rest: List<&2, Task>) -> List<&2, Task>: match enabled: case False{}: rest case True{}: extras(rules, path, entries, rest)def object(+rules: List<&2, Field<Schema>>, closed: Bool, +path: List<&2, String>, value: Json.Json, rest: List<&2, Task>) -> List<&2, Task>: match value: case Json.JsonObject{+entries}: fields(rules, path, entries, strict(closed, rules, path, entries, rest)) case other: [Failure{path, "type"}]def expand(schema: Schema, path: List<&2, String>, value: Json.Json, rest: List<&2, Task>) -> List<&2, Task>: match schema: case Text{min, max}: text(min, max, path, value, rest) case UInt{min, max}: uint(min, max, path, value, rest) case Boolean{}: boolean(path, value, rest) case Nullable{inner}: nullable(inner, path, value, rest) case Sequence{inner, min, max}: sequence(inner, min, max, path, value, rest) case Object{rules, closed}: object(rules, closed, path, value, rest)# Worklist expansion terminates for finite schema/data; this is tested, not a law.@unsafedef run(tasks: List<&2, Task>) -> Verdict: match tasks: case Nil{}: Valid{} case Con{Failure{path, code}, rest}: Invalid{path, code} case Con{Check{schema, path, value}, rest}: run(expand(schema, path, value, rest))def validate(schema: Schema, value: Json.Json) -> Verdict: run([Check{schema, Nil{}, value}])def path_json(path: List<&2, String>) -> List<&2, Json.Json>: match path: case Nil{}: Nil{} case Con{segment, tail}: Json.string(segment) <> path_json(tail)def error(path: List<&2, String>, code: String) -> IO(Web.Reply): Web.json_value(422, Json.object([("error", Json.object([ ("code", Json.string("validation_failed")), ("message", Json.string("Request validation failed")), ("path", Json.array(path_json(path))), ("reason", Json.string(code))]))]))def accepted(result: Verdict, value: Json.Json, next: Json.Json -> IO(Web.Reply)) -> IO(Web.Reply): match result: case Valid{}: next(value) case Invalid{path, code}: error(path, code)def apply(schema: Schema, +value: Json.Json, next: Json.Json -> IO(Web.Reply)) -> IO(Web.Reply): accepted(validate(schema, value), value, next)