src/protocol.bend
bendonnect-rpc@0.1.0.0 · 583 lines · 93 definitions
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19import Base
20import bendonnect-rpc@0.1.0.0/src/code.bend as Code
21import bend-kit-bytes@0.3.2.0/bytes.bend as Bytes
22import bend-kit-url@0.4.1.0/url.bend as Url
23
24type Codec is Data:
25 Json{}
26 Proto{}
27 Custom{name: String}
28
29type CallKind is Data:
30 Unary{}
31 UnaryGet{}
32 Stream{}
33
34type Spec is Type:
35 Spec{procedure: String, kind: CallKind, codec: Codec, timeout_ms: Maybe<&2, U32>,
36 encoding: String, accept: List<&2, String>, message: Bytes.Bytes}
37
38type Reject is Data:
39 Status{status: U32, message: String}
40 Connect{code: Code.Code, message: String}
41
42
43type Ct is Data:
44 Ct{stream: Bool, codec: Codec}
45
46def supported_encodings() -> List<&2, String>:
47 ["identity"]
48
49
50
51def codec_name(c: Codec) -> String:
52 match c:
53 case Json{}:
54 "json"
55 case Proto{}:
56 "proto"
57 case Custom{n}:
58 n
59
60def content_type(+stream: Bool, c: Codec) -> String:
61 match stream:
62 case True{}:
63 "application/connect+" ++ codec_name(c)
64 case False{}:
65 "application/" ++ codec_name(c)
66
67def unary_content_type(c: Codec) -> String:
68 content_type(False{}, c)
69
70def stream_content_type(c: Codec) -> String:
71 content_type(True{}, c)
72
73
74def codec_of(+name: String) -> Maybe<&2, Codec>:
75 Bool.pick(Maybe<&2, Codec>, String.eq(name, "json"), Some{Json{}},
76 Bool.pick(Maybe<&2, Codec>, String.eq(name, "proto"), Some{Proto{}},
77 Bool.pick(Maybe<&2, Codec>, String.is_empty(name), None{}, Some{Custom{name}})))
78
79def media.of(r: String & String) -> String:
80 (a, b) = r
81 a
82
83
84def media(s: String) -> String:
85 media.of(Url.take_to(s, 59))
86
87def ct_make(stream: Bool, mc: Maybe<&2, Codec>) -> Maybe<&2, Ct>:
88 match mc:
89 case None{}:
90 None{}
91 case Some{c}:
92 Some{Ct{stream, c}}
93
94def ct_split(stream: Bool, app: Bool, +m: String) -> Maybe<&2, Ct>:
95 match stream:
96 case True{}:
97 ct_make(True{}, codec_of(String.drop(m, 20n)))
98 case False{}:
99 match app:
100 case True{}:
101 ct_make(False{}, codec_of(String.drop(m, 12n)))
102 case False{}:
103 None{}
104
105
106def parse_content_type(+ct: String) -> Maybe<&2, Ct>:
107 +m = String.to_lower(String.trim(media(ct)))
108 ct_split(String.starts_with(m, "application/connect+"), String.starts_with(m, "application/"), m)
109
110def ct_stream(c: Ct) -> Bool:
111 match c:
112 case Ct{s, k}:
113 s
114
115def ct_codec(c: Ct) -> Codec:
116 match c:
117 case Ct{s, k}:
118 k
119
120def ct_kind(c: Ct) -> CallKind:
121 match c:
122 case Ct{s, k}:
123 match s:
124 case True{}:
125 Stream{}
126 case False{}:
127 Unary{}
128
129
130
131def hdr.of(r: Map<&2, List<&2, String>> & List<&2, String>) -> Maybe<&2, String>:
132 (m, v) = r
133 match v:
134 case Nil{}:
135 None{}
136 case Con{x, rest}:
137 Some{x}
138
139
140def hdr(+h: Map<&2, List<&2, String>>, +k: String) -> Maybe<&2, String>:
141 hdr.of(Map.get(List<&2, String>, Nil{}, h, k))
142
143def hdr_all.of(r: Map<&2, List<&2, String>> & List<&2, String>) -> Maybe<&2, String>:
144 (m, v) = r
145 match v:
146 case Nil{}:
147 None{}
148 case Con{x, rest}:
149 Some{String.join(Con{x, rest}, ",")}
150
151
152def hdr_all(+h: Map<&2, List<&2, String>>, +k: String) -> Maybe<&2, String>:
153 hdr_all.of(Map.get(List<&2, String>, Nil{}, h, k))
154
155def bad_media() -> Result<&2, &1, Reject, Ct>:
156 Fail{Status{415, "unsupported content-type: want application/<codec> or application/connect+<codec>"}}
157
158def content_type_of.fin(m: Maybe<&2, Ct>) -> Result<&2, &1, Reject, Ct>:
159 match m:
160 case None{}:
161 bad_media()
162 case Some{c}:
163 Done{c}
164
165def content_type_of.of(m: Maybe<&2, String>) -> Result<&2, &1, Reject, Ct>:
166 match m:
167 case None{}:
168 bad_media()
169 case Some{s}:
170 content_type_of.fin(parse_content_type(s))
171
172def content_type_of(+h: Map<&2, List<&2, String>>) -> Result<&2, &1, Reject, Ct>:
173 content_type_of.of(hdr(h, "content-type"))
174
175def version_ok(ok: Bool) -> Result<&2, &1, Reject, Unit>:
176 match ok:
177 case True{}:
178 Done{Unit{}}
179 case False{}:
180 Fail{Status{400, "unsupported connect-protocol-version: want \"1\""}}
181
182def check_version.of(v: Maybe<&2, String>) -> Result<&2, &1, Reject, Unit>:
183 match v:
184 case None{}:
185 Done{Unit{}}
186 case Some{s}:
187 version_ok(String.eq(s, "1"))
188
189
190def check_version(+h: Map<&2, List<&2, String>>) -> Result<&2, &1, Reject, Unit>:
191 check_version.of(hdr(h, "connect-protocol-version"))
192
193
194
195def digit(+c: U32) -> Bool:
196 Bool.and(U32.is_ge(c, 48), U32.is_le(c, 57))
197
198
199def sat(+acc: U32, +d: U32) -> U32:
200 Bool.pick(U32, U32.is_gt(acc, 429496729), 4294967295,
201 Bool.pick(U32, Bool.and(U32.is_eq(acc, 429496729), U32.is_gt(d, 5)), 4294967295,
202 (acc * 10 + d : U32)))
203
204def pt.pick(ok: Bool, rest: Unit -> Maybe<&2, U32>) -> Maybe<&2, U32>:
205 match ok:
206 case True{}:
207 rest(Unit{})
208 case False{}:
209 None{}
210
211def pt.go(s: String, +n: U32, +acc: U32) -> Maybe<&2, U32>:
212 match s:
213 case SNil{}:
214 Some{acc}
215 case SCon{Chr{+c}, t}:
216 pt.pick(Bool.and(digit(c), U32.is_lt(n, 10)), u => pt.go(t, (n + 1 : U32), sat(acc, (c - 48 : U32))))
217
218def pt.start(+s: String, empty: Bool) -> Maybe<&2, U32>:
219 match empty:
220 case True{}:
221 None{}
222 case False{}:
223 pt.go(s, 0, 0)
224
225
226def parse_timeout(+s: String) -> Maybe<&2, U32>:
227 pt.start(s, String.is_empty(s))
228
229def timeout.chk(m: Maybe<&2, U32>, +s: String) -> Result<&2, &1, Reject, Maybe<&2, U32>>:
230 match m:
231 case None{}:
232 Fail{Connect{Code.InvalidArgument{}, "invalid connect-timeout-ms \"" ++ s ++ "\""}}
233 case Some{t}:
234 Done{Some{t}}
235
236def timeout.of(v: Maybe<&2, String>) -> Result<&2, &1, Reject, Maybe<&2, U32>>:
237 match v:
238 case None{}:
239 Done{None{}}
240 case Some{+s}:
241 timeout.chk(parse_timeout(s), s)
242
243def timeout_of(+h: Map<&2, List<&2, String>>) -> Result<&2, &1, Reject, Maybe<&2, U32>>:
244 timeout.of(hdr(h, "connect-timeout-ms"))
245
246
247
248def norm.pick(empty: Bool, +e: String) -> String:
249 match empty:
250 case True{}:
251 "identity"
252 case False{}:
253 e
254
255def norm_enc(+s: String) -> String:
256 +e = String.to_lower(String.trim(s))
257 norm.pick(String.is_empty(e), e)
258
259def enc.fin(ok: Bool, +e: String) -> Result<&2, &1, Reject, String>:
260 match ok:
261 case True{}:
262 Done{e}
263 case False{}:
264 Fail{Connect{Code.Unimplemented{}, "unknown compression \"" ++ e ++ "\": supported encodings are " ++ String.join(supported_encodings(), ", ")}}
265
266
267def check_enc(+raw: String) -> Result<&2, &1, Reject, String>:
268 +e = norm_enc(raw)
269 enc.fin(String.eq(e, "identity"), e)
270
271def get_enc.of(v: Maybe<&2, String>) -> Result<&2, &1, Reject, String>:
272 match v:
273 case None{}:
274 Done{"identity"}
275 case Some{s}:
276 check_enc(s)
277
278def get_enc(+h: Map<&2, List<&2, String>>, +k: String) -> Result<&2, &1, Reject, String>:
279 get_enc.of(hdr(h, k))
280
281def acc.item(+s: String) -> String:
282 +e = String.to_lower(String.trim(media(s)))
283 e
284
285def acc.cons(empty: Bool, x: String, r: List<&2, String>) -> List<&2, String>:
286 match empty:
287 case True{}:
288 r
289 case False{}:
290 x <> r
291
292def acc.item2(+x: String, r: List<&2, String>) -> List<&2, String>:
293 acc.cons(String.is_empty(x), x, r)
294
295def acc.go(xs: List<&2, String>) -> List<&2, String>:
296 match xs:
297 case Nil{}:
298 Nil{}
299 case Con{h, t}:
300 acc.item2(acc.item(h), acc.go(t))
301
302def accept_list(+s: String) -> List<&2, String>:
303 acc.go(String.split(s, ','))
304
305def accept.of(v: Maybe<&2, String>, +enc: String) -> List<&2, String>:
306 match v:
307 case None{}:
308 [enc]
309 case Some{s}:
310 accept_list(s)
311
312def accept_of(+h: Map<&2, List<&2, String>>, +k: String, +enc: String) -> List<&2, String>:
313 accept.of(hdr_all(h, k), enc)
314
315
316
317def bad_path() -> Result<&2, &1, Reject, String>:
318 Fail{Status{404, "not found: want <prefix>/<package.Service>/<Method>"}}
319
320def proc.fin(ok: Bool, +p: String) -> Result<&2, &1, Reject, String>:
321 match ok:
322 case True{}:
323 Done{p}
324 case False{}:
325 bad_path()
326
327def proc.parts(xs: List<&2, String>, +p: String) -> Result<&2, &1, Reject, String>:
328 match xs:
329 case Con{+s, Con{+m, Nil{}}}:
330 proc.fin(Bool.and(Bool.not(String.is_empty(s)), Bool.not(String.is_empty(m))), p)
331 case _:
332 bad_path()
333
334def proc.split(+p: String) -> Result<&2, &1, Reject, String>:
335 proc.parts(String.split(p, '/'), p)
336
337def proc.slash(ok: Bool, +rest: String) -> Result<&2, &1, Reject, String>:
338 match ok:
339 case True{}:
340 proc.split(String.drop(rest, 1n))
341 case False{}:
342 bad_path()
343
344def proc.pre(ok: Bool, +path: String, +prefix: String) -> Result<&2, &1, Reject, String>:
345 match ok:
346 case True{}:
347 +rest = String.drop(path, String.length(prefix))
348 proc.slash(String.starts_with(rest, "/"), rest)
349 case False{}:
350 bad_path()
351
352
353def parse_proc(+path: String, +prefix: String) -> Result<&2, &1, Reject, String>:
354 proc.pre(String.starts_with(path, prefix), path, prefix)
355
356
357
358def post_enc_key(stream: Bool) -> String:
359 match stream:
360 case True{}:
361 "connect-content-encoding"
362 case False{}:
363 "content-encoding"
364
365def post_acc_key(stream: Bool) -> String:
366 match stream:
367 case True{}:
368 "connect-accept-encoding"
369 case False{}:
370 "accept-encoding"
371
372def post_req(+path: String, +headers: Map<&2, List<&2, String>>, +prefix: String) -> Result<&2, &1, Reject, Spec>:
373 do Result<&2, &1, Reject, Spec>:
374 proc : String <- parse_proc(path, prefix)
375 +ct : Ct <- content_type_of(headers)
376 ok : Unit <- check_version(headers)
377 t : Maybe<&2, U32> <- timeout_of(headers)
378 +enc : String <- get_enc(headers, post_enc_key(ct_stream(ct)))
379 return Spec{proc, ct_kind(ct), ct_codec(ct), t, enc, accept_of(headers, post_acc_key(ct_stream(ct)), enc), Bytes.new(0)}
380
381
382
383def qfind.pick(hit: Bool, v: String, rest: Unit -> Maybe<&2, String>) -> Maybe<&2, String>:
384 match hit:
385 case True{}:
386 Some{v}
387 case False{}:
388 rest(Unit{})
389
390def qfind.kv(r: String & String, +name: String, rest: Unit -> Maybe<&2, String>) -> Maybe<&2, String>:
391 (k, v) = r
392 qfind.pick(String.eq(k, name), v, rest)
393
394
395def qfind(xs: List<&2, String>, +name: String) -> Maybe<&2, String>:
396 match xs:
397 case Nil{}:
398 None{}
399 case Con{h, t}:
400 qfind.kv(Url.take_to(h, 61), name, u => qfind(t, name))
401
402def qparam(+query: String, +name: String) -> Maybe<&2, String>:
403 qfind(String.split(query, '&'), name)
404
405def need.fin(m: Maybe<&2, String>, +name: String) -> Result<&2, &1, Reject, String>:
406 match m:
407 case None{}:
408 Fail{Status{400, "missing query parameter \"" ++ name ++ "\""}}
409 case Some{v}:
410 Done{v}
411
412def need_param(+query: String, +name: String) -> Result<&2, &1, Reject, String>:
413 need.fin(qparam(query, name), name)
414
415def qversion.ok(ok: Bool) -> Result<&2, &1, Reject, Unit>:
416 match ok:
417 case True{}:
418 Done{Unit{}}
419 case False{}:
420 Fail{Status{400, "unsupported connect query parameter: want \"v1\""}}
421
422def qversion.of(m: Maybe<&2, String>) -> Result<&2, &1, Reject, Unit>:
423 match m:
424 case None{}:
425 Done{Unit{}}
426 case Some{s}:
427 qversion.ok(String.eq(s, "v1"))
428
429
430def check_qversion(+query: String) -> Result<&2, &1, Reject, Unit>:
431 qversion.of(qparam(query, "connect"))
432
433def b64flag.of(m: Maybe<&2, String>) -> Bool:
434 match m:
435 case None{}:
436 False{}
437 case Some{s}:
438 String.eq(s, "1")
439
440def codec.fin(m: Maybe<&2, Codec>) -> Result<&2, &1, Reject, Codec>:
441 match m:
442 case None{}:
443 Fail{Status{400, "empty encoding query parameter"}}
444 case Some{c}:
445 Done{c}
446
447def comp.of(m: Maybe<&2, String>) -> Result<&2, &1, Reject, String>:
448 match m:
449 case None{}:
450 Done{"identity"}
451 case Some{s}:
452 check_enc(s)
453
454def compression_of(+query: String) -> Result<&2, &1, Reject, String>:
455 comp.of(qparam(query, "compression"))
456
457
458
459def b64val(+c: U32) -> Maybe<&2, U32>:
460 Bool.pick(Maybe<&2, U32>, Bool.and(U32.is_ge(c, 65), U32.is_le(c, 90)), Some{(c - 65 : U32)},
461 Bool.pick(Maybe<&2, U32>, Bool.and(U32.is_ge(c, 97), U32.is_le(c, 122)), Some{(c - 71 : U32)},
462 Bool.pick(Maybe<&2, U32>, Bool.and(U32.is_ge(c, 48), U32.is_le(c, 57)), Some{(c + 4 : U32)},
463 Bool.pick(Maybe<&2, U32>, Bool.or(U32.is_eq(c, 43), U32.is_eq(c, 45)), Some{62},
464 Bool.pick(Maybe<&2, U32>, Bool.or(U32.is_eq(c, 47), U32.is_eq(c, 95)), Some{63}, None{})))))
465
466def b64vals.cons(m: Maybe<&2, U32>, r: Maybe<&2, List<&2, U32>>) -> Maybe<&2, List<&2, U32>>:
467 match m:
468 case None{}:
469 None{}
470 case Some{v}:
471 match r:
472 case None{}:
473 None{}
474 case Some{l}:
475 Some{v <> l}
476
477def b64vals(s: String) -> Maybe<&2, List<&2, U32>>:
478 match s:
479 case SNil{}:
480 Some{Nil{}}
481 case SCon{Chr{+c}, t}:
482 b64vals.cons(b64val(c), b64vals(t))
483
484def b64count(xs: List<&2, U32>, +acc: U32) -> U32:
485 match xs:
486 case Nil{}:
487 acc
488 case Con{h, t}:
489 b64count(t, (acc + 1 : U32))
490
491def b64out(+n: U32) -> U32:
492 +r = (n % 4 : U32)
493 ((n / 4 : U32) * 3 + r - U32.min(r, 1) : U32)
494
495def b64set(out: Bytes.Bytes, +i: U32, +v: U32) -> Bytes.Bytes:
496 Bytes.set(out, i, (v .&. 255 : U32))
497
498
499def b64go(vs: List<&2, U32>, out: Bytes.Bytes, +i: U32) -> Maybe<&1, Bytes.Bytes>:
500 match vs:
501 case Nil{}:
502 Some{out}
503 case Con{+a, Nil{}}:
504 None{}
505 case Con{+a, Con{+b, Nil{}}}:
506 Some{b64set(out, i, ((a << 2n) .|. (b >> 4n) : U32))}
507 case Con{+a, Con{+b, Con{+c, Nil{}}}}:
508 Some{b64set(b64set(out, i, ((a << 2n) .|. (b >> 4n) : U32)), (i + 1 : U32), (((b .&. 15 : U32) << 4n) .|. (c >> 2n) : U32))}
509 case Con{+a, Con{+b, Con{+c, Con{+d, rest}}}}:
510 b64go(rest, b64set(b64set(b64set(out, i, ((a << 2n) .|. (b >> 4n) : U32)), (i + 1 : U32), (((b .&. 15 : U32) << 4n) .|. (c >> 2n) : U32)), (i + 2 : U32), (((c .&. 3 : U32) << 6n) .|. d : U32)), (i + 3 : U32))
511
512def b64build(vs: List<&2, U32>, +n: U32) -> Maybe<&1, Bytes.Bytes>:
513 b64go(vs, Bytes.new(b64out(n)), 0)
514
515def b64run(m: Maybe<&2, List<&2, U32>>) -> Maybe<&1, Bytes.Bytes>:
516 match m:
517 case None{}:
518 None{}
519 case Some{+vs}:
520 b64build(vs, b64count(vs, 0))
521
522def b64strip(+s: String) -> String:
523 Bool.pick(String, String.ends_with(s, "=="), String.take(s, Nat.sub(String.length(s), 2n)),
524 Bool.pick(String, String.ends_with(s, "="), String.take(s, Nat.sub(String.length(s), 1n)), s))
525
526
527def b64url_decode(+s: String) -> Maybe<&1, Bytes.Bytes>:
528 b64run(b64vals(b64strip(s)))
529
530def msg.bytes(m: Maybe<&1, Bytes.Bytes>) -> Result<&2, &1, Reject, Bytes.Bytes>:
531 match m:
532 case None{}:
533 Fail{Status{400, "invalid base64 in message query parameter"}}
534 case Some{b}:
535 Done{b}
536
537def msg.kind(b64: Bool, +s: String) -> Result<&2, &1, Reject, Bytes.Bytes>:
538 match b64:
539 case True{}:
540 msg.bytes(b64url_decode(s))
541 case False{}:
542 Done{Bytes.from_string(s)}
543
544def msg.of(m: Maybe<&2, String>, +b64: Bool) -> Result<&2, &1, Reject, Bytes.Bytes>:
545 match m:
546 case None{}:
547 Fail{Status{400, "invalid percent-encoding in message query parameter"}}
548 case Some{s}:
549 msg.kind(b64, s)
550
551
552def decode_message(+raw: String, +b64: Bool) -> Result<&2, &1, Reject, Bytes.Bytes>:
553 msg.of(Url.pct.decode(raw), b64)
554
555def get_req(+path: String, +query: String, +headers: Map<&2, List<&2, String>>, +prefix: String) -> Result<&2, &1, Reject, Spec>:
556 do Result<&2, &1, Reject, Spec>:
557 proc : String <- parse_proc(path, prefix)
558 ok : Unit <- check_version(headers)
559 ok2 : Unit <- check_qversion(query)
560 es : String <- need_param(query, "encoding")
561 codec : Codec <- codec.fin(codec_of(String.to_lower(es)))
562 raw : String <- need_param(query, "message")
563 msg : Bytes.Bytes <- decode_message(raw, b64flag.of(qparam(query, "base64")))
564 +comp : String <- compression_of(query)
565 t : Maybe<&2, U32> <- timeout_of(headers)
566 return Spec{proc, UnaryGet{}, codec, t, comp, accept_of(headers, "accept-encoding", comp), msg}
567
568
569
570def route(post: Bool, get: Bool, +path: String, +query: String, +headers: Map<&2, List<&2, String>>, +prefix: String) -> Result<&2, &1, Reject, Spec>:
571 match post:
572 case True{}:
573 post_req(path, headers, prefix)
574 case False{}:
575 match get:
576 case True{}:
577 get_req(path, query, headers, prefix)
578 case False{}:
579 Fail{Status{405, "method not allowed: want POST or GET"}}
580
581
582def classify(+method: String, +path: String, +query: String, +headers: Map<&2, List<&2, String>>, +prefix: String) -> Result<&2, &1, Reject, Spec>:
583 route(String.eq(method, "POST"), String.eq(method, "GET"), path, query, headers, prefix)