http_json/http_json.bend source
http_json/http_json.bend on the hub · documented module
import Baseimport ../json/json.bend as Jimport ../utf8/utf8.bend as Uimport ../http/status.bend as Simport ../http/headers.bend as Himport ../http/request.bend as Reqimport ../http/response.bend as Res# JSON bodies for http: decode a request's body into your type, and answer# with a JSON value# why a body did not decodetype DecodeError is Data: UnsupportedContentType{} BodyTooLarge{} InvalidJson{error: J.Error} InvalidValue{error: J.Access}# the largest body decode reads: 1 MiBdef limit() -> U32: 1048576def lower.go(up: Bool, +x: U32) -> U32: match up: case True{}: (x + 32 : U32) case False{}: xdef lower(+x: U32) -> U32: lower.go(Bool.and(U32.is_ge(x, 65), U32.is_le(x, 90)), x)# the media type, lowercased, spaces dropped, reversed: it ends at ';'def media.go(s: String, acc: String) -> String: match s: case SNil{}: acc case SCon{';', _}: acc case SCon{' ', t}: media.go(t, acc) case SCon{'\t', t}: media.go(t, acc) case SCon{Chr{+x}, t}: media.go(t, SCon{Chr{lower(x)}, acc})def starts(s: String, p: String) -> Bool: match s p: case _ SNil{}: True{} case SCon{x, st} SCon{y, pt}: Bool.and(Char.is_eq(x, y), starts(st, pt)) case SNil{} SCon{_, _}: False{}def same(a: String, b: String) -> Bool: match a b: case SNil{} SNil{}: True{} case SCon{x, at} SCon{y, bt}: Bool.and(Char.is_eq(x, y), same(at, bt)) case _ _: False{}def is_json.of(+rev: String) -> Bool: Bool.or(same(rev, "nosj/noitacilppa"), starts(rev, "nosj+"))# application/json, or a type with the +json suffix (RFC 6839 §3.1)def is_json(m: Maybe<&2, String>) -> Bool: match m: case Some{s}: is_json.of(media.go(s, "")) case None{}: False{}# a body and its length, counted in one readtype Sized is Data: Sized{n: U32, body: List<&2, U32>}def measure(bs: List<&2, U32>, +n: U32, acc: List<&2, U32>) -> Sized: match bs: case Nil{}: Sized{n, List.reverse(&2, U32, acc)} case Con{b, t}: measure(t, (n + 1 : U32), Con{b, acc})def decode.value(-A: Data, r: Result<&2, &2, J.Access, A>) -> Result<&2, &2, DecodeError, A>: match r: case Done{a}: Done{a} case Fail{e}: Fail{InvalidValue{e}}def decode.json(-A: Data, f: J.Json -> Result<&2, &2, J.Access, A>, r: Result<&2, &2, J.Error, J.Json>) -> Result<&2, &2, DecodeError, A>: match r: case Done{j}: decode.value(A, f(j)) case Fail{e}: Fail{InvalidJson{e}}def decode.fits(-A: Data, f: J.Json -> Result<&2, &2, J.Access, A>, ok: Bool, bs: List<&2, U32>) -> Result<&2, &2, DecodeError, A>: match ok: case True{}: decode.json(A, f, J.parse_bytes(bs)) case False{}: Fail{BodyTooLarge{}}def decode.sized(-A: Data, f: J.Json -> Result<&2, &2, J.Access, A>, s: Sized) -> Result<&2, &2, DecodeError, A>: match s: case Sized{n, bs}: decode.fits(A, f, U32.is_le(n, limit()), bs)def decode.typed(-A: Data, f: J.Json -> Result<&2, &2, J.Access, A>, ok: Bool, bs: List<&2, U32>) -> Result<&2, &2, DecodeError, A>: match ok: case True{}: decode.sized(A, f, measure(bs, 0, [])) case False{}: Fail{UnsupportedContentType{}}# the body as your type: f reads the parsed JSON, e.g. with J.string_atdef decode(-A: Data, f: J.Json -> Result<&2, &2, J.Access, A>, r: Req.Request) -> Result<&2, &2, DecodeError, A>: match r: case Req.Request{_, _, hs, body}: decode.typed(A, f, is_json(H.get(hs, "content-type")), body)# a JSON answerdef response(s: S.Status, j: J.Json) -> Res.Response: Res.typed(s, "application/json", U.encode(J.encode(j)))def error_body(msg: String) -> J.Json: J.JObj{[J.Field{"error", J.JStr{msg}}]}# the answer to a body that did not decode: 415, 413 or 400, with# {"error": why}def error(e: DecodeError) -> Res.Response: match e: case UnsupportedContentType{}: response(S.unsupported_media_type(), error_body("content-type must be application/json")) case BodyTooLarge{}: response(S.content_too_large(), error_body("body too large")) case InvalidJson{x}: response(S.bad_request(), error_body("invalid JSON: " ++ J.message(x))) case InvalidValue{a}: response(S.bad_request(), error_body(J.message_access(a)))