input.bend

bend-kit-camber@0.11.0.0 · 303 lines · 0 definitions

1# Checked typed inputs; bodies are consumed once, without caching or decompression.
2import Base
3import bend-kit-http@0.35.0.0/http.bend as Http
4import bend-kit-router@0.2.0.0/target.bend as Target
5import bend-kit-json@0.6.1.1/json.bend as Json
6import bend-kit-bytes@0.3.2.0/bytes.bend as Bytes
7import bend-kit-url@0.4.1.0/url.bend as Url
8import bend-kit-camber@0.11.0.0/response.bend as Response
9
10# Expected HTTP status, not an exception; the dispatch executor maps it once.
11def checked(~I: Type, value: Maybe<&1, I>) -> Result<&2, &1, U32, I>:
12  match value:
13    case None{}: Fail{400}
14    case Some{input}: Done{input}
15
16def decimal.step(ok: Bool, +acc: U32, digit: U32, next: U32 -> Maybe<&2, U32>) -> Maybe<&2, U32>:
17  match ok:
18    case False{}: None{}
19    case True{}: next((acc * 10 + digit : U32))
20
21def decimal.go(text: String, +acc: U32) -> Maybe<&2, U32>:
22  match text:
23    case SNil{}: Some{acc}
24    case SCon{Chr{+c}, rest}:
25      +digit = (c - 48 : U32)
26      decimal.step(U32.is_ge(c, 48) && U32.is_le(c, 57) && (U32.is_lt(acc, 429496729) || (U32.is_eq(acc, 429496729) && U32.is_le(digit, 5))), acc, digit, n => decimal.go(rest, n))
27
28# Nonempty ASCII decimal only; arithmetic happens only after the overflow guard.
29def u32(text: String) -> Maybe<&2, U32>:
30  match text:
31    case SNil{}: None{}
32    case SCon{c, rest}: decimal.go(SCon{c, rest}, 0)
33
34def scalar.convert(~I: Data, value: Maybe<&2, I>) -> Result<&2, &1, U32, I>:
35  match value:
36    case None{}: Fail{400}
37    case Some{input}: Done{input}
38
39def path.value(~I: Data, ~convert: String -> Maybe<&2, I>, pair: Map<&2, String> & String) -> Result<&2, &1, U32, I>:
40  (params, value) = pair
41  scalar.convert(~I, convert(value))
42
43def path.present(~I: Data, ~convert: String -> Maybe<&2, I>, present: Bool, params: Map<&2, String>, key: String) -> Result<&2, &1, U32, I>:
44  match present:
45    case False{}: Fail{400}
46    case True{}:
47      path.value(~I, ~convert, Map.get(String, "", params, key))
48
49def path.found(~I: Data, ~convert: String -> Maybe<&2, I>, key: String, pair: Map<&2, String> & Bool) -> Result<&2, &1, U32, I>:
50  (params, present) = pair
51  path.present(~I, ~convert, present, params, key)
52
53def path(~I: Data, ~convert: String -> Maybe<&2, I>, params: Map<&2, String>, +key: String) -> Result<&2, &1, U32, I>:
54  path.found(~I, ~convert, key, Map.has(&2, String, params, key))
55
56
57def query.keep(same: Bool, value: String, rest: List<&2, String>) -> List<&2, String>:
58  match same:
59    case True{}: value <> rest
60    case False{}: rest
61def query.values(xs: List<&2, Target.Query>, +key: String) -> List<&2, String>:
62  match xs:
63    case Nil{}: Nil{}
64    case Target.Query{+name, value} <> rest:
65      query.keep(String.eq(name, key), value, query.values(rest, key))
66
67
68def scalar.values(~I: Data, ~convert: String -> Maybe<&2, I>, values: List<&2, String>) -> Result<&2, &1, U32, I>:
69  match values:
70    case value <> Nil{}: scalar.convert(~I, convert(value))
71    case _: Fail{400}
72
73def query.scalar(~I: Data, ~convert: String -> Maybe<&2, I>, query: List<&2, Target.Query>, key: String) -> Result<&2, &1, U32, I>:
74  scalar.values(~I, ~convert, query.values(query, key))
75
76def list.values(~I: Data, ~convert: String -> Maybe<&2, I>, values: List<&2, String>) -> Result<&2, &1, U32, List<&2, I>>:
77  match values:
78    case Nil{}: Done{Nil{}}
79    case value <> rest:
80      do Result<&2, &1, U32, List<&2, I>>:
81        head : I <- scalar.convert(~I, convert(value))
82        tail : List<&2, I> <- list.values(~I, ~convert, rest)
83        return head <> tail
84
85def query.list(~I: Data, ~convert: String -> Maybe<&2, I>, query: List<&2, Target.Query>, key: String) -> Result<&2, &1, U32, List<&2, I>>:
86  list.values(~I, ~convert, query.values(query, key))
87
88# Reuse Target's RFC 3629 state machine directly over the owned packed array.
89def utf8.step(pair: Array<U32> & U32, state: Target.Utf) -> Array<U32> & Maybe<&2, Target.Utf>:
90  (bytes, byte) = pair
91  Target.Utf{+need, lo, hi, cp, out} = state
92  (bytes, Target.utf.step(U32.is_eq(need, 0), byte, need, lo, hi, cp, out))
93
94def utf8.go(fuel: Nat, pair: Array<U32> & Maybe<&2, Target.Utf>, +index: U32) -> Array<U32> & Maybe<&2, String>:
95  match fuel:
96    case 0n:
97      (bytes, state) = pair
98      (bytes, Target.utf.finish(state))
99    case 1n+rest:
100      (bytes, state) = pair
101      match state:
102        case None{}: (bytes, None{})
103        case Some{state}: utf8.go(rest, utf8.step(Bytes.peek(bytes, index), state), (index + 1 : U32))
104
105def utf8.borrowed(len: U32, pair: Array<U32> & Maybe<&2, String>) -> Http.Body & Maybe<&2, String>:
106  (buffer, text) = pair
107  (Bytes.Bytes{len, buffer}, text)
108
109def utf8.borrow(bytes: Http.Body) -> Http.Body & Maybe<&2, String>:
110  Bytes.Bytes{+len, buffer} = bytes
111  utf8.borrowed(len, utf8.go(U32.to_nat(len), (buffer, Some{Target.Utf{0, 128, 191, 0, ""}}), 0))
112
113def utf8.text(pair: Http.Body & Maybe<&2, String>) -> Maybe<&2, String>:
114  (bytes, text) = pair
115  text
116
117def utf8(bytes: Http.Body) -> Maybe<&2, String>:
118  utf8.text(utf8.borrow(bytes))
119
120# Split only unquoted semicolons, retaining quoted parameter syntax for checks.
121def media.part(split: Bool, +c: U32, +quoted: Bool, +escaped: Bool, acc: String, parts: List<&2, String>) -> Bool & Bool & String & List<&2, String>:
122  match split:
123    case True{}: (False{}, False{}, "", String.reverse(acc) <> parts)
124    case False{}:
125      (Bool.pick(Bool, U32.is_eq(c, 34) && Bool.not(escaped), Bool.not(quoted), quoted), quoted && U32.is_eq(c, 92) && Bool.not(escaped), SCon{Chr{c}, acc}, parts)
126
127def media.end(invalid: Bool, acc: String, parts: List<&2, String>) -> List<&2, String>:
128  match invalid:
129    case True{}: ["invalid"]
130    case False{}: List.reverse(&2, String, String.reverse(acc) <> parts)
131
132def media.parts(text: String, state: Bool & Bool & String & List<&2, String>) -> List<&2, String>:
133  match text:
134    case SNil{}:
135      (quoted, escaped, acc, parts) = state
136      media.end(quoted || escaped, acc, parts)
137    case SCon{Chr{+c}, rest}:
138      (+quoted, escaped, acc, parts) = state
139      media.parts(rest, media.part(U32.is_eq(c, 59) && Bool.not(quoted), c, quoted, escaped, acc, parts))
140
141# Compare the decoded quoted-string charset without allocating a second value.
142def media.charset.quoted(text: String, expected: String, escaped: Bool) -> Bool:
143  match text:
144    case SNil{}: False{}
145    case SCon{Chr{+c}, rest}:
146      match c:
147        case 92:
148          match expected:
149            case SNil{}: False{}
150            case SCon{Chr{want}, next}:
151              match escaped:
152                case True{}: False{}
153                case False{}: media.charset.quoted(rest, SCon{Chr{want}, next}, True{})
154        case _:
155          match expected:
156            case SNil{}: U32.is_eq(c, 34) && Bool.not(escaped) && String.eq(rest, "")
157            case SCon{Chr{want}, next}: U32.is_eq(c, want) && media.charset.quoted(rest, next, False{})
158
159def media.charset.start(quoted: Bool, c: U32, rest: String) -> Bool:
160  match quoted:
161    case True{}: media.charset.quoted(rest, "utf-8", False{})
162    case False{}: String.eq(SCon{Chr{c}, rest}, "utf-8")
163
164def media.charset.value(value: String) -> Bool:
165  match value:
166    case SNil{}: False{}
167    case SCon{Chr{+c}, rest}: media.charset.start(U32.is_eq(c, 34), c, rest)
168
169def media.parameter(pair: String & String) -> Bool:
170  (key, value) = pair
171  +name = String.to_lower(Http.trim(key))
172  +charset = String.to_lower(Http.trim(value))
173  Bool.not(String.eq(name, "charset")) || media.charset.value(charset)
174
175def media.charset(parts: List<&2, String>) -> Bool:
176  match parts:
177    case Nil{}: True{}
178    case part <> rest: media.parameter(Url.take_to(part, 61)) && media.charset(rest)
179
180def media.type(json: Bool, +name: String) -> Bool:
181  match json:
182    case True{}:
183      String.eq(name, "application/json") || (String.starts_with(name, "application/") && String.ends_with(name, "+json") && Nat.is_gt(String.length(name), 17n) && Response.name(String.drop(name, 12n)))
184    case False{}:
185      String.starts_with(name, "text/") && Response.name(String.drop(name, 5n))
186
187def media.content(json: Bool, parts: List<&2, String>) -> Bool:
188  match parts:
189    case Nil{}: False{}
190    case name <> rest: media.type(json, String.to_lower(Http.trim(name))) && media.charset(rest)
191
192def media.encoding(values: List<&2, String>) -> Bool:
193  match values:
194    case Nil{}: True{}
195    case value <> rest: String.eq(value, "identity") && media.encoding(rest)
196
197def media.headers(json: Bool, types: List<&2, String>, encodings: List<&2, String>) -> Bool:
198  match types:
199    case value <> Nil{}: media.content(json, media.parts(value, (False{}, False{}, "", Nil{}))) && media.encoding(Http.ce.split(encodings))
200    case _: False{}
201
202def media(json: Bool, +headers: Map<&2, List<&2, String>>) -> Bool:
203  media.headers(json, Http.fields(headers, "content-type"), Http.fields(headers, "content-encoding"))
204
205def raw(body: Http.Body) -> Result<&2, &1, U32, Http.Body>:
206  Done{body}
207
208def text.accepted(accepted: Bool, body: Http.Body) -> Result<&2, &1, U32, String>:
209  match accepted:
210    case False{}: Fail{415}
211    case True{}: scalar.convert(~String, utf8(body))
212
213def text(headers: Map<&2, List<&2, String>>, body: Http.Body) -> Result<&2, &1, U32, String>:
214  text.accepted(media(False{}, headers), body)
215
216# An explicit stack avoids mutually recursive tree walkers. Each decoded object
217# has its own key set; escaped and literal spellings compare as the same text.
218type Parent is Type:
219  Object{key: Bytes.Bytes, rest: List<&1, Bytes.Bytes & Json.Val>, acc: List<&1, Bytes.Bytes & Json.Val>, seen: Set()}
220  Array{rest: List<&1, Json.Val>, acc: List<&1, Json.Val>}
221
222type Walk is Type:
223  Value{value: Json.Val, parents: List<&1, Parent>}
224  Fields{fields: List<&1, Bytes.Bytes & Json.Val>, acc: List<&1, Bytes.Bytes & Json.Val>, seen: Set(), parents: List<&1, Parent>}
225  Items{values: List<&1, Json.Val>, acc: List<&1, Json.Val>, parents: List<&1, Parent>}
226  Returned{value: Json.Val, parents: List<&1, Parent>}
227
228def unique.field(repeated: Bool, key: Bytes.Bytes, value: Json.Val, rest: List<&1, Bytes.Bytes & Json.Val>, acc: List<&1, Bytes.Bytes & Json.Val>, seen: Set(), parents: List<&1, Parent>) -> Maybe<&1, Walk>:
229  match repeated:
230    case True{}: None{}
231    case False{}: Some{Value{value, Object{key, rest, acc, seen} <> parents}}
232
233def unique.seen(pair: Set() & Bool, name: String, key: Bytes.Bytes, value: Json.Val, rest: List<&1, Bytes.Bytes & Json.Val>, acc: List<&1, Bytes.Bytes & Json.Val>, parents: List<&1, Parent>) -> Maybe<&1, Walk>:
234  (seen, repeated) = pair
235  unique.field(repeated, key, value, rest, acc, Set.add(seen, name), parents)
236
237def unique.key(text: Maybe<&2, String>, key: Bytes.Bytes, value: Json.Val, rest: List<&1, Bytes.Bytes & Json.Val>, acc: List<&1, Bytes.Bytes & Json.Val>, seen: Set(), parents: List<&1, Parent>) -> Maybe<&1, Walk>:
238  match text:
239    case None{}: None{}
240    case Some{+name}:
241      unique.seen(Set.has(seen, name), name, key, value, rest, acc, parents)
242
243def unique.borrowed(pair: Http.Body & Maybe<&2, String>, value: Json.Val, rest: List<&1, Bytes.Bytes & Json.Val>, acc: List<&1, Bytes.Bytes & Json.Val>, seen: Set(), parents: List<&1, Parent>) -> Maybe<&1, Walk>:
244  (key, text) = pair
245  unique.key(text, key, value, rest, acc, seen, parents)
246
247def unique.step(step: Walk) -> Maybe<&1, Walk>:
248  match step:
249    case Value{value, parents}:
250      match value:
251        case Json.Obj{fields}: Some{Fields{fields, Nil{}, Set.new(), parents}}
252        case Json.Arr{values}: Some{Items{values, Nil{}, parents}}
253        case Json.Null{}: Some{Returned{Json.Null{}, parents}}
254        case Json.Flag{on}: Some{Returned{Json.Flag{on}, parents}}
255        case Json.Num{number}: Some{Returned{Json.Num{number}, parents}}
256        case Json.Str{text}: Some{Returned{Json.Str{text}, parents}}
257    case Fields{fields, acc, seen, parents}:
258      match fields:
259        case Nil{}: Some{Returned{Json.Obj{List.reverse(&1, Bytes.Bytes & Json.Val, acc)}, parents}}
260        case (key, value) <> rest:
261          unique.borrowed(utf8.borrow(key), value, rest, acc, seen, parents)
262    case Items{values, acc, parents}:
263      match values:
264        case Nil{}: Some{Returned{Json.Arr{List.reverse(&1, Json.Val, acc)}, parents}}
265        case value <> rest: Some{Value{value, Array{rest, acc} <> parents}}
266    case Returned{value, parents}:
267      match parents:
268        case Nil{}: Some{Returned{value, Nil{}}}
269        case Object{key, rest, acc, seen} <> parents: Some{Fields{rest, (key, value) <> acc, seen, parents}}
270        case Array{rest, acc} <> parents: Some{Items{rest, value <> acc, parents}}
271
272def unique(fuel: Nat, step: Maybe<&1, Walk>) -> Maybe<&1, Json.Val>:
273  match fuel:
274    case 0n: None{}
275    case 1n+rest:
276      match step:
277        case None{}: None{}
278        case Some{Returned{value, Nil{}}}: Some{value}
279        case Some{state}: unique(rest, unique.step(state))
280
281def json.parsed(fuel: Nat, value: Maybe<&1, Json.Val>) -> Result<&2, &1, U32, Json.Val>:
282  match value:
283    case None{}: Fail{400}
284    case Some{value}: checked(~Json.Val, unique(fuel, Some{Value{value, Nil{}}}))
285
286def json.accepted(accepted: Bool, cap: U32, body: Http.Body) -> Result<&2, &1, U32, Json.Val>:
287  match accepted:
288    case False{}: Fail{415}
289    case True{}:
290      Bytes.Bytes{+len, buffer} = body
291      json.parsed(Nat.add(Nat.mul(U32.to_nat(len), 4n), 8n), Json.parse.strict.bytes(Bytes.Bytes{len, buffer}, cap))
292
293def json.with_depth(headers: Map<&2, List<&2, String>>, body: Http.Body, cap: U32) -> Result<&2, &1, U32, Json.Val>:
294  json.accepted(media(True{}, headers), cap, body)
295
296def json(headers: Map<&2, List<&2, String>>, body: Http.Body) -> Result<&2, &1, U32, Json.Val>:
297  json.with_depth(headers, body, 64)
298
299# Exact number text remains packed until this explicit checked conversion.
300def number.u32(value: Json.Val) -> Result<&2, &1, U32, U32>:
301  match value:
302    case Json.Num{bytes}: scalar.convert(~U32, u32(Bytes.to_string(bytes)))
303    case _: Fail{400}