src/error.bend

bendonnect-rpc@0.1.0.0 · 498 lines · 66 definitions

1# Connect Error and EndStreamResponse (JSON). Pure.
2# See https://connectrpc.com/docs/protocol/#error-end-stream
3#
4# DEVIATION: Detail.type is a Bend keyword, so the field is named type_name.
5#
6# Wire shapes:
7#   Error:     {"code":"unavailable","message":"..","details":[{"type":"..","value":"<unpadded b64>","debug":{..}}]}
8#              message/details are omitted when empty; "debug" only when present.
9#   EndStream: {"error":{..},"metadata":{"k":["v"]}}  ({} on success with no metadata)
10#
11# Decoding notes: details whose type/value is missing or whose value is not
12# valid base64 are dropped (clients must not rely on details). Metadata keys
13# are lowercased; metadata entries that are not arrays are skipped.
14import Base
15import bend-kit-bytes@0.3.2.0/bytes.bend as Bytes
16import bend-kit-encoding@0.4.0.0/encoding.bend as Enc
17import bend-kit-json@0.6.1.1/json.bend as Json
18import bendonnect-rpc@0.1.0.0/src/code.bend as Code
19import bendonnect-rpc@0.1.0.0/src/b64.bend as B64
20
21type Detail is Type:
22  Detail{type_name: String, value: Bytes.Bytes, debug: Maybe<&1, Json.Val>}
23
24type Error is Type:
25  Error{code: Code.Code, message: String, details: List<&1, Detail>}
26
27type EndStream is Type:
28  EndStream{error: Maybe<&1, Error>, metadata: Map<&2, List<&2, String>>}
29
30def new(code: Code.Code, message: String) -> Error:
31  Error{code, message, Nil{}}
32
33# ---------------------------------------------------------------- helpers
34
35def str_val(+s: String) -> Json.Val:
36  Json.Str{Enc.utf8.encode(s)}
37
38def kv(+k: String, v: Json.Val) -> Bytes.Bytes & Json.Val:
39  (Enc.utf8.encode(k), v)
40
41# ---------------------------------------------------------------- encoding
42
43def debug_part(m: Maybe<&1, Json.Val>) -> List<&1, Bytes.Bytes & Json.Val>:
44  match m:
45    case None{}:
46      Nil{}
47    case Some{v}:
48      Con{kv("debug", v), Nil{}}
49
50def detail_to_json(d: Detail) -> Json.Val:
51  Detail{+ty, value, debug} = d
52  Json.Obj{Con{kv("type", str_val(ty)), Con{kv("value", str_val(B64.encode_std_raw(value))), debug_part(debug)}}}
53
54def details_json(ds: List<&1, Detail>) -> List<&1, Json.Val>:
55  match ds:
56    case Nil{}:
57      Nil{}
58    case Con{h, t}:
59      Con{detail_to_json(h), details_json(t)}
60
61def details_part(ds: List<&1, Detail>, rest: List<&1, Bytes.Bytes & Json.Val>) -> List<&1, Bytes.Bytes & Json.Val>:
62  match ds:
63    case Nil{}:
64      rest
65    case Con{h, t}:
66      Con{kv("details", Json.Arr{Con{detail_to_json(h), details_json(t)}}), rest}
67
68def message_part.go(empty: Bool, m: String, rest: List<&1, Bytes.Bytes & Json.Val>) -> List<&1, Bytes.Bytes & Json.Val>:
69  match empty:
70    case True{}:
71      rest
72    case False{}:
73      Con{kv("message", str_val(m)), rest}
74
75def message_part(+m: String, rest: List<&1, Bytes.Bytes & Json.Val>) -> List<&1, Bytes.Bytes & Json.Val>:
76  message_part.go(String.is_empty(m), m, rest)
77
78def to_json(e: Error) -> Json.Val:
79  Error{code, message, details} = e
80  Json.Obj{Con{kv("code", str_val(Code.to_string(code))), message_part(message, details_part(details, Nil{}))}}
81
82def encode(e: Error) -> Bytes.Bytes:
83  Json.encode.bytes(to_json(e))
84
85# ---------------------------------------------------------------- decoding
86
87# Up to three fields picked out of an object by name.
88type Fs is Type:
89  Fs{a: Maybe<&1, Json.Val>, b: Maybe<&1, Json.Val>, c: Maybe<&1, Json.Val>}
90
91def fs.empty() -> Fs:
92  Fs{None{}, None{}, None{}}
93
94def put1(v: Json.Val, f: Fs) -> Fs:
95  Fs{a, b, c} = f
96  Fs{Some{v}, b, c}
97
98def put2(v: Json.Val, f: Fs) -> Fs:
99  Fs{a, b, c} = f
100  Fs{a, Some{v}, c}
101
102def put3(v: Json.Val, f: Fs) -> Fs:
103  Fs{a, b, c} = f
104  Fs{a, b, Some{v}}
105
106def pick.put3(i3: Bool, v: Json.Val, f: Fs) -> Fs:
107  match i3:
108    case True{}:
109      put3(v, f)
110    case False{}:
111      f
112
113def pick.put2(i2: Bool, i3: Bool, v: Json.Val, f: Fs) -> Fs:
114  match i2:
115    case True{}:
116      put2(v, f)
117    case False{}:
118      pick.put3(i3, v, f)
119
120def pick.put(i1: Bool, i2: Bool, i3: Bool, v: Json.Val, f: Fs) -> Fs:
121  match i1:
122    case True{}:
123      put1(v, f)
124    case False{}:
125      pick.put2(i2, i3, v, f)
126
127def pick.slot(+ks: String, +n1: String, +n2: String, +n3: String, v: Json.Val, f: Fs) -> Fs:
128  pick.put(String.eq(ks, n1), String.eq(ks, n2), String.eq(ks, n3), v, f)
129
130def pick.go(kvs: List<&1, Bytes.Bytes & Json.Val>, +n1: String, +n2: String, +n3: String, f: Fs) -> Fs:
131  match kvs:
132    case Nil{}:
133      f
134    case Con{p, rest}:
135      (k, v) = p
136      pick.go(rest, n1, n2, n3, pick.slot(Enc.utf8.decode(k), n1, n2, n3, v, f))
137
138def unknown_of(m: Maybe<&2, Code.Code>) -> Code.Code:
139  match m:
140    case None{}:
141      Code.Unknown{}
142    case Some{c}:
143      c
144
145def code_str(s: Bytes.Bytes) -> Maybe<&2, Code.Code>:
146  Some{unknown_of(Code.from_string(Enc.utf8.decode(s)))}
147
148def code_of.val(v: Json.Val) -> Maybe<&2, Code.Code>:
149  match v:
150    case Json.Str{s}:
151      code_str(s)
152    case Json.Null{}:
153      None{}
154    case Json.Flag{on}:
155      None{}
156    case Json.Num{s}:
157      None{}
158    case Json.Arr{xs}:
159      None{}
160    case Json.Obj{kvs}:
161      None{}
162
163# A present string code (unrecognized names become Unknown); anything else is None.
164def code_of(m: Maybe<&1, Json.Val>) -> Maybe<&2, Code.Code>:
165  match m:
166    case None{}:
167      None{}
168    case Some{v}:
169      code_of.val(v)
170
171def str_of.val(v: Json.Val) -> String:
172  match v:
173    case Json.Str{s}:
174      Enc.utf8.decode(s)
175    case Json.Null{}:
176      ""
177    case Json.Flag{on}:
178      ""
179    case Json.Num{s}:
180      ""
181    case Json.Arr{xs}:
182      ""
183    case Json.Obj{kvs}:
184      ""
185
186def str_of(m: Maybe<&1, Json.Val>) -> String:
187  match m:
188    case None{}:
189      ""
190    case Some{v}:
191      str_of.val(v)
192
193def opt_str.val(v: Json.Val) -> Maybe<&2, String>:
194  match v:
195    case Json.Str{s}:
196      Some{Enc.utf8.decode(s)}
197    case Json.Null{}:
198      None{}
199    case Json.Flag{on}:
200      None{}
201    case Json.Num{s}:
202      None{}
203    case Json.Arr{xs}:
204      None{}
205    case Json.Obj{kvs}:
206      None{}
207
208def opt_str(m: Maybe<&1, Json.Val>) -> Maybe<&2, String>:
209  match m:
210    case None{}:
211      None{}
212    case Some{v}:
213      opt_str.val(v)
214
215def opt_b64(m: Maybe<&2, String>) -> Maybe<&1, Bytes.Bytes>:
216  match m:
217    case None{}:
218      None{}
219    case Some{+s}:
220      B64.decode(s)
221
222def detail_build(ty: Maybe<&2, String>, vb: Maybe<&1, Bytes.Bytes>, debug: Maybe<&1, Json.Val>) -> Maybe<&1, Detail>:
223  match ty vb:
224    case Some{t} Some{b}:
225      Some{Detail{t, b, debug}}
226    case _ _:
227      None{}
228
229def detail_fields(f: Fs) -> Maybe<&1, Detail>:
230  Fs{a, b, c} = f
231  detail_build(opt_str(a), opt_b64(opt_str(b)), c)
232
233def detail_of(v: Json.Val) -> Maybe<&1, Detail>:
234  match v:
235    case Json.Obj{kvs}:
236      detail_fields(pick.go(kvs, "type", "value", "debug", fs.empty()))
237    case Json.Null{}:
238      None{}
239    case Json.Flag{on}:
240      None{}
241    case Json.Num{s}:
242      None{}
243    case Json.Str{s}:
244      None{}
245    case Json.Arr{xs}:
246      None{}
247
248def details_push(m: Maybe<&1, Detail>, rest: List<&1, Detail>) -> List<&1, Detail>:
249  match m:
250    case Some{d}:
251      Con{d, rest}
252    case None{}:
253      rest
254
255def details_go(xs: List<&1, Json.Val>) -> List<&1, Detail>:
256  match xs:
257    case Nil{}:
258      Nil{}
259    case Con{h, t}:
260      details_push(detail_of(h), details_go(t))
261
262def details_val(v: Json.Val) -> List<&1, Detail>:
263  match v:
264    case Json.Arr{xs}:
265      details_go(xs)
266    case Json.Null{}:
267      Nil{}
268    case Json.Flag{on}:
269      Nil{}
270    case Json.Num{s}:
271      Nil{}
272    case Json.Str{s}:
273      Nil{}
274    case Json.Obj{kvs}:
275      Nil{}
276
277def details_of(m: Maybe<&1, Json.Val>) -> List<&1, Detail>:
278  match m:
279    case None{}:
280      Nil{}
281    case Some{v}:
282      details_val(v)
283
284def error_build(c: Maybe<&2, Code.Code>, msg: String, ds: List<&1, Detail>) -> Maybe<&1, Error>:
285  match c:
286    case None{}:
287      None{}
288    case Some{code}:
289      Some{Error{code, msg, ds}}
290
291def error_fields(f: Fs) -> Maybe<&1, Error>:
292  Fs{a, b, c} = f
293  error_build(code_of(a), str_of(b), details_of(c))
294
295# {}, {"code":null}, a non-string code, or a non-object are invalid (None).
296def from_json(v: Json.Val) -> Maybe<&1, Error>:
297  match v:
298    case Json.Obj{kvs}:
299      error_fields(pick.go(kvs, "code", "message", "details", fs.empty()))
300    case Json.Null{}:
301      None{}
302    case Json.Flag{on}:
303      None{}
304    case Json.Num{s}:
305      None{}
306    case Json.Str{s}:
307      None{}
308    case Json.Arr{xs}:
309      None{}
310
311def decode.val(m: Maybe<&1, Json.Val>) -> Maybe<&1, Error>:
312  match m:
313    case None{}:
314      None{}
315    case Some{v}:
316      from_json(v)
317
318def decode(b: Bytes.Bytes) -> Maybe<&1, Error>:
319  decode.val(Json.parse.bytes(b))
320
321# ---------------------------------------------------------------- client: from_http
322
323def status_text(+s: U32) -> String:
324  Bool.pick(String, U32.is_eq(s, 400), "Bad Request",
325  Bool.pick(String, U32.is_eq(s, 401), "Unauthorized",
326  Bool.pick(String, U32.is_eq(s, 403), "Forbidden",
327  Bool.pick(String, U32.is_eq(s, 404), "Not Found",
328  Bool.pick(String, U32.is_eq(s, 429), "Too Many Requests",
329  Bool.pick(String, U32.is_eq(s, 500), "Internal Server Error",
330  Bool.pick(String, U32.is_eq(s, 502), "Bad Gateway",
331  Bool.pick(String, U32.is_eq(s, 503), "Service Unavailable",
332  Bool.pick(String, U32.is_eq(s, 504), "Gateway Timeout",
333  String.append("HTTP ", U32.show(s)))))))))))
334
335def from_http.pick(m: Maybe<&1, Error>, +status: U32) -> Error:
336  match m:
337    case Some{e}:
338      e
339    case None{}:
340      Error{Code.from_http(status), status_text(status), Nil{}}
341
342# Client side of a non-200 unary response: the Connect error in the body if
343# it parses, else a code inferred from the status with the status text as message.
344def from_http(+status: U32, body: Bytes.Bytes) -> Error:
345  from_http.pick(decode(body), status)
346
347# ---------------------------------------------------------------- EndStream
348
349def strs_val(vs: List<&2, String>) -> List<&1, Json.Val>:
350  match vs:
351    case Nil{}:
352      Nil{}
353    case Con{h, t}:
354      Con{str_val(h), strs_val(t)}
355
356def meta_entries(xs: List<&2, Sigma<&2, &2, String, _ => List<&2, String>>>) -> List<&1, Bytes.Bytes & Json.Val>:
357  match xs:
358    case Nil{}:
359      Nil{}
360    case Con{Tuple{+k, v}, rest}:
361      Con{kv(k, Json.Arr{strs_val(v)}), meta_entries(rest)}
362
363def meta_part(xs: List<&2, Sigma<&2, &2, String, _ => List<&2, String>>>) -> List<&1, Bytes.Bytes & Json.Val>:
364  match xs:
365    case Nil{}:
366      Nil{}
367    case Con{h, t}:
368      Con{kv("metadata", Json.Obj{meta_entries(Con{h, t})}), Nil{}}
369
370def err_part(e: Maybe<&1, Error>, rest: List<&1, Bytes.Bytes & Json.Val>) -> List<&1, Bytes.Bytes & Json.Val>:
371  match e:
372    case None{}:
373      rest
374    case Some{x}:
375      Con{kv("error", to_json(x)), rest}
376
377def end_encode(e: EndStream) -> Bytes.Bytes:
378  EndStream{error, metadata} = e
379  Json.encode.bytes(Json.Obj{err_part(error, meta_part(Map.to_list(&2, List<&2, String>, metadata)))})
380
381def strs_push(v: Json.Val, rest: List<&2, String>) -> List<&2, String>:
382  match v:
383    case Json.Str{s}:
384      Con{Enc.utf8.decode(s), rest}
385    case Json.Null{}:
386      rest
387    case Json.Flag{on}:
388      rest
389    case Json.Num{s}:
390      rest
391    case Json.Arr{xs}:
392      rest
393    case Json.Obj{kvs}:
394      rest
395
396def strs_of(xs: List<&1, Json.Val>) -> List<&2, String>:
397  match xs:
398    case Nil{}:
399      Nil{}
400    case Con{h, t}:
401      strs_push(h, strs_of(t))
402
403def meta_set(+k: String, v: Json.Val, acc: Map<&2, List<&2, String>>) -> Map<&2, List<&2, String>>:
404  match v:
405    case Json.Arr{xs}:
406      Map.set(&2, List<&2, String>, acc, String.to_lower(k), strs_of(xs))
407    case Json.Null{}:
408      acc
409    case Json.Flag{on}:
410      acc
411    case Json.Num{s}:
412      acc
413    case Json.Str{s}:
414      acc
415    case Json.Obj{kvs}:
416      acc
417
418def meta_map(kvs: List<&1, Bytes.Bytes & Json.Val>, acc: Map<&2, List<&2, String>>) -> Map<&2, List<&2, String>>:
419  match kvs:
420    case Nil{}:
421      acc
422    case Con{p, rest}:
423      (k, v) = p
424      meta_map(rest, meta_set(Enc.utf8.decode(k), v, acc))
425
426def meta_val(v: Json.Val) -> Maybe<&2, Map<&2, List<&2, String>>>:
427  match v:
428    case Json.Obj{kvs}:
429      Some{meta_map(kvs, Map.new(&2, List<&2, String>))}
430    case Json.Null{}:
431      Some{Map.new(&2, List<&2, String>)}
432    case Json.Flag{on}:
433      None{}
434    case Json.Num{s}:
435      None{}
436    case Json.Str{s}:
437      None{}
438    case Json.Arr{xs}:
439      None{}
440
441def meta_of(m: Maybe<&1, Json.Val>) -> Maybe<&2, Map<&2, List<&2, String>>>:
442  match m:
443    case None{}:
444      Some{Map.new(&2, List<&2, String>)}
445    case Some{v}:
446      meta_val(v)
447
448# absent error is fine (Some None); present-but-invalid error is None.
449def err_val(m: Maybe<&1, Error>) -> Maybe<&1, Maybe<&1, Error>>:
450  match m:
451    case None{}:
452      None{}
453    case Some{e}:
454      Some{Some{e}}
455
456def err_of(m: Maybe<&1, Json.Val>) -> Maybe<&1, Maybe<&1, Error>>:
457  match m:
458    case None{}:
459      Some{None{}}
460    case Some{v}:
461      err_val(from_json(v))
462
463def end_build(e: Maybe<&1, Maybe<&1, Error>>, m: Maybe<&2, Map<&2, List<&2, String>>>) -> Maybe<&1, EndStream>:
464  match e m:
465    case Some{err} Some{md}:
466      Some{EndStream{err, md}}
467    case _ _:
468      None{}
469
470def end_fields(f: Fs) -> Maybe<&1, EndStream>:
471  Fs{a, b, c} = f
472  end_build(err_of(a), meta_of(b))
473
474def end_from_json(v: Json.Val) -> Maybe<&1, EndStream>:
475  match v:
476    case Json.Obj{kvs}:
477      end_fields(pick.go(kvs, "error", "metadata", "_", fs.empty()))
478    case Json.Null{}:
479      None{}
480    case Json.Flag{on}:
481      None{}
482    case Json.Num{s}:
483      None{}
484    case Json.Str{s}:
485      None{}
486    case Json.Arr{xs}:
487      None{}
488
489def end_decode.val(m: Maybe<&1, Json.Val>) -> Maybe<&1, EndStream>:
490  match m:
491    case None{}:
492      None{}
493    case Some{v}:
494      end_from_json(v)
495
496# {"error":null}, {"error":{}} and {"error":{"code":null}} are invalid (None).
497def end_decode(b: Bytes.Bytes) -> Maybe<&1, EndStream>:
498  end_decode.val(Json.parse.bytes(b))