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":"","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. 14 import Base 15 import bend-kit-bytes@0.3.2.0/bytes.bend as Bytes 16 import bend-kit-encoding@0.4.0.0/encoding.bend as Enc 17 import bend-kit-json@0.6.1.1/json.bend as Json 18 import ./code.bend as Code 19 import ./b64.bend as B64 20 21 type Detail is Type: 22 Detail{type_name: String, value: Bytes.Bytes, debug: Maybe<&1, Json.Val>} 23 24 type Error is Type: 25 Error{code: Code.Code, message: String, details: List<&1, Detail>} 26 27 type EndStream is Type: 28 EndStream{error: Maybe<&1, Error>, metadata: Map<&2, List<&2, String>>} 29 30 def new(code: Code.Code, message: String) -> Error: 31 Error{code, message, Nil{}} 32 33 # ---------------------------------------------------------------- helpers 34 35 def str_val(+s: String) -> Json.Val: 36 Json.Str{Enc.utf8.encode(s)} 37 38 def kv(+k: String, v: Json.Val) -> Bytes.Bytes & Json.Val: 39 (Enc.utf8.encode(k), v) 40 41 # ---------------------------------------------------------------- encoding 42 43 def 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 50 def 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 54 def 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 61 def 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 68 def 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 75 def 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 78 def 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 82 def 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. 88 type Fs is Type: 89 Fs{a: Maybe<&1, Json.Val>, b: Maybe<&1, Json.Val>, c: Maybe<&1, Json.Val>} 90 91 def fs.empty() -> Fs: 92 Fs{None{}, None{}, None{}} 93 94 def put1(v: Json.Val, f: Fs) -> Fs: 95 Fs{a, b, c} = f 96 Fs{Some{v}, b, c} 97 98 def put2(v: Json.Val, f: Fs) -> Fs: 99 Fs{a, b, c} = f 100 Fs{a, Some{v}, c} 101 102 def put3(v: Json.Val, f: Fs) -> Fs: 103 Fs{a, b, c} = f 104 Fs{a, b, Some{v}} 105 106 def 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 113 def 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 120 def 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 127 def 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 130 def 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 138 def 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 145 def code_str(s: Bytes.Bytes) -> Maybe<&2, Code.Code>: 146 Some{unknown_of(Code.from_string(Enc.utf8.decode(s)))} 147 148 def 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. 164 def 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 171 def 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 186 def 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 193 def 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 208 def 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 215 def 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 222 def 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 229 def 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 233 def 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 248 def 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 255 def 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 262 def 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 277 def 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 284 def 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 291 def 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). 296 def 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 311 def 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 318 def decode(b: Bytes.Bytes) -> Maybe<&1, Error>: 319 decode.val(Json.parse.bytes(b)) 320 321 # ---------------------------------------------------------------- client: from_http 322 323 def 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 335 def 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. 344 def from_http(+status: U32, body: Bytes.Bytes) -> Error: 345 from_http.pick(decode(body), status) 346 347 # ---------------------------------------------------------------- EndStream 348 349 def 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 356 def 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 363 def 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 370 def 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 377 def 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 381 def 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 396 def 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 403 def 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 418 def 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 426 def 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 441 def 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. 449 def 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 456 def 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 463 def 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 470 def end_fields(f: Fs) -> Maybe<&1, EndStream>: 471 Fs{a, b, c} = f 472 end_build(err_of(a), meta_of(b)) 473 474 def 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 489 def 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). 497 def end_decode(b: Bytes.Bytes) -> Maybe<&1, EndStream>: 498 end_decode.val(Json.parse.bytes(b))