input.bend
bend-kit-camber@0.6.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.30.0.0/http.bend as Http
4import bend-kit-router@0.2.0.0/target.bend as Target
5import bend-kit-json@0.5.1.0/json.bend as Json
6import bend-kit-bytes@0.3.0.0/bytes.bend as Bytes
7import bend-kit-url@0.4.1.0/url.bend as Url
8import bend-kit-camber@0.6.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}