1 # ezmcp/src/openapi: an OpenAPI 3.0 or 3.1 document as an MCP tool table, 2 # and a tool call as the HTTP request it stands for, with no IO. Every 3 # operation of the document's paths is one tool: its name is the 4 # operation's id, its arguments are the operation's parameters by name and 5 # its request body as `requestBody`. A call's arguments are checked against 6 # the tool's input schema, then filled into the path template, the query 7 # string and the header fields. The caller reads the document and sends the 8 # request. Where the specifications leave a choice open, the choice is 9 # openapi-mcp-generator's (v4), so a server it generated keeps its tools. 10 # SPEC.md MCP-OAS lists what is guaranteed. 11 import Base 12 import 0xa81e161008ecc2f297c0a3065cb9b188/main.bend as Json 13 import 0xa81e161008ecc2f297c0a3065cb9b188/src/value.bend as Value 14 import 0xedb2120a04a47dc18c3a756ec7c62750/src/http.bend as Http 15 import 0xedb2120a04a47dc18c3a756ec7c62750/src/pct.bend as Pct 16 import 0xedb2120a04a47dc18c3a756ec7c62750/src/qs.bend as Qs 17 import ./server.bend as Server 18 19 # where a parameter goes (Parameter Object `in`); InNone for a location 20 # OpenAPI does not have 21 type Place is Data: 22 InPath{} 23 InQuery{} 24 InHeader{} 25 InCookie{} 26 InNone{} 27 28 # a parameter a request is built from: its name and where it goes 29 type Param is Data: 30 Param{name: String, place: Place} 31 32 # one operation as a tool: the tool's name, description and input schema, 33 # and what its calls are sent as: the method (upper case), the path 34 # template, the parameters, and the request body's media type ("" for none) 35 type Op is Data: 36 Op{name: String, description: String, input: Value.Json, method: String, 37 path: String, params: List<&2, Param>, media: String} 38 39 # the request a tool call stands for: a method, a target (the path and its 40 # query), the header fields and the body; or the path template when a 41 # template expression is still in it 42 type Call is Data: 43 Send{method: String, target: String, headers: List<&2, Http.Header>, 44 body: Maybe<&2, String>} 45 Unfilled{path: String} 46 47 # --- searches --- 48 49 # true at a hit, else the rest of the search 50 def or.else(hit: Bool, rest: Unit -> Bool) -> Bool: 51 match hit: 52 case True{}: 53 True{} 54 case False{}: 55 rest(Unit{}) 56 57 # false at a miss, else the rest of the check 58 def and.then(ok: Bool, rest: Unit -> Bool) -> Bool: 59 match ok: 60 case True{}: 61 rest(Unit{}) 62 case False{}: 63 False{} 64 65 # --- object members --- 66 67 # an object member: its name and value, reusable 68 def Mem() -> Data: 69 Sigma<&2, &2, String, _ => Value.Json> 70 71 # members as the cells of an object, in order 72 def cells(ps: List<&2, Mem()>) -> Value.Json: 73 match ps: 74 case []: 75 Value.JNil{} 76 case (key, val) <> rest: 77 Value.JPair{key, val, cells(rest)} 78 79 # the object of members, in order 80 def object(ps: List<&2, Mem()>) -> Value.Json: 81 Value.JObj{cells(ps)} 82 83 def members.go(cs: Value.Json) -> List<&2, Mem()>: 84 match cs: 85 case Value.JPair{key, val, rest}: 86 (key, val) <> members.go(rest) 87 case Value.JBind{src, +n, val, rest}: 88 (Value.span.str(src, n, U32.is_zero(n)), val) <> members.go(rest) 89 case other: 90 [] 91 92 # an object's members in order; none for any other value 93 def members(val: Value.Json) -> List<&2, Mem()>: 94 match val: 95 case Value.JObj{pairs}: 96 members.go(pairs) 97 case other: 98 [] 99 100 def put.at( 101 hit: Bool, 102 k2: String, 103 v2: Value.Json, 104 rest: List<&2, Mem()>, 105 key: String, 106 val: Value.Json, 107 more: Unit -> List<&2, Mem()> 108 ) -> List<&2, Mem()>: 109 match hit: 110 case True{}: 111 (key, val) <> rest 112 case False{}: 113 (k2, v2) <> more(Unit{}) 114 115 # members with `key` set to `val`: in the place of the first member of that 116 # name, else last, as a JavaScript assignment puts it 117 def put(ps: List<&2, Mem()>, +key: String, +val: Value.Json) -> List<&2, Mem()>: 118 match ps: 119 case []: 120 [(key, val)] 121 case (+k2, v2) <> +rest: 122 put.at(String.eq(k2, key), k2, v2, rest, key, val, _u => put(rest, key, val)) 123 124 def drop.at( 125 hit: Bool, 126 k2: String, 127 v2: Value.Json, 128 rest: List<&2, Mem()> 129 ) -> List<&2, Mem()>: 130 match hit: 131 case True{}: 132 rest 133 case False{}: 134 (k2, v2) <> rest 135 136 # members less every one named `key`, the rest in order 137 def drop(ps: List<&2, Mem()>, +key: String) -> List<&2, Mem()>: 138 match ps: 139 case []: 140 [] 141 case (+k2, v2) <> rest: 142 drop.at(String.eq(k2, key), k2, v2, drop(rest, key)) 143 144 # whether members have one named `key` 145 def named(ps: List<&2, Mem()>, +key: String) -> Bool: 146 match ps: 147 case []: 148 False{} 149 case (+k2, _v) <> rest: 150 or.else(String.eq(k2, key), _u => named(rest, key)) 151 152 def lookup.at( 153 hit: Bool, 154 val: Value.Json, 155 more: Unit -> Maybe<&2, Value.Json> 156 ) -> Maybe<&2, Value.Json>: 157 match hit: 158 case True{}: 159 Some{val} 160 case False{}: 161 more(Unit{}) 162 163 # the value of the first member named `key` 164 def lookup(ps: List<&2, Mem()>, +key: String) -> Maybe<&2, Value.Json>: 165 match ps: 166 case []: 167 None{} 168 case (k2, val) <> rest: 169 lookup.at(String.eq(k2, key), val, _u => lookup(rest, key)) 170 171 # a member's value, null when there is none 172 def value(ps: List<&2, Mem()>, key: String) -> Value.Json: 173 Maybe.default(&2, Value.Json, lookup(ps, key), Json.null()) 174 175 # --- JSON values as JavaScript reads them --- 176 177 # a string when it is not empty, as JavaScript's `||` takes it 178 def full(text: Maybe<&2, String>) -> Maybe<&2, String>: 179 match text: 180 case None{}: 181 None{} 182 case Some{+str}: 183 Bool.pick(Maybe<&2, String>, String.is_empty(str), None{}, Some{str}) 184 185 # the first of two maybe strings that is there 186 def first(lead: Maybe<&2, String>, next: Maybe<&2, String>) -> Maybe<&2, String>: 187 match lead: 188 case Some{str}: 189 Some{str} 190 case None{}: 191 next 192 193 # whether a value is JSON true 194 def is_true(val: Value.Json) -> Bool: 195 match val: 196 case Value.JBool{flag}: 197 flag 198 case other: 199 False{} 200 201 # whether a value is JSON null 202 def is_null(val: Value.Json) -> Bool: 203 match val: 204 case Value.JNull{}: 205 True{} 206 case other: 207 False{} 208 209 def is_str.of(text: Maybe<&2, String>, +str: String) -> Bool: 210 match text: 211 case None{}: 212 False{} 213 case Some{got}: 214 String.eq(got, str) 215 216 # whether a value is the string `str` 217 def is_str(val: Value.Json, +str: String) -> Bool: 218 is_str.of(Json.as_str(val), str) 219 220 # a member's string, "" when it is not one 221 def str.at(val: Value.Json, key: String) -> String: 222 Maybe.default(&2, String, Json.as_str(Json.get(val, key)), "") 223 224 # --- references (Reference Object, RFC 6901) --- 225 226 def token.go(cs: String) -> String: 227 match cs: 228 case SNil{}: 229 SNil{} 230 case SCon{'~', SCon{'1', rest}}: 231 SCon{'/', token.go(rest)} 232 case SCon{'~', SCon{'0', rest}}: 233 SCon{'~', token.go(rest)} 234 case SCon{ch, rest}: 235 SCon{ch, token.go(rest)} 236 237 # one reference token decoded: `~1` as `/` and `~0` as `~` (RFC 6901 ยง4) 238 def token(text: String) -> String: 239 token.go(text) 240 241 def walk.at( 242 hit: Bool, 243 next: Value.Json, 244 more: Value.Json -> Maybe<&2, Value.Json> 245 ) -> Maybe<&2, Value.Json>: 246 match hit: 247 case True{}: 248 more(next) 249 case False{}: 250 None{} 251 252 # the value at a pointer's tokens, each a member name, or none when a member 253 # is missing 254 def walk(toks: List<&2, String>, +val: Value.Json) -> Maybe<&2, Value.Json>: 255 match toks: 256 case []: 257 Some{val} 258 case tok <> rest: 259 +key = token(tok) 260 walk.at(Json.has(val, key), Json.get(val, key), got => walk(rest, got)) 261 262 def resolve.at(local: Bool, +doc: Value.Json, ref: String) -> Maybe<&2, Value.Json>: 263 match local: 264 case True{}: 265 walk(String.split(String.drop(ref, 2n), '/'), doc) 266 case False{}: 267 None{} 268 269 # what a local reference (`#/` and a JSON Pointer) points to in the 270 # document; none for a reference that is not local or points to nothing 271 def resolve(+doc: Value.Json, +ref: String) -> Maybe<&2, Value.Json>: 272 resolve.at(String.starts_with(ref, "#/"), doc, ref) 273 274 # a `$ref` member's string, when the cells have one 275 def ref.of(pairs: Value.Json) -> Maybe<&2, String>: 276 Json.as_str(Value.find(pairs, "$ref")) 277 278 # whether a reference is one being followed already 279 def seen(refs: List<&2, String>, +ref: String) -> Bool: 280 match refs: 281 case []: 282 False{} 283 case +got <> rest: 284 or.else(String.eq(got, ref), _u => seen(rest, ref)) 285 286 def merge.one( 287 hit: Bool, 288 +sib: List<&2, Mem()>, 289 key: String, 290 val: Value.Json 291 ) -> List<&2, Mem()>: 292 match hit: 293 case True{}: 294 sib 295 case False{}: 296 List.append(&2, Mem(), sib, [(key, val)]) 297 298 def merge.add(ts: List<&2, Mem()>, +sib: List<&2, Mem()>) -> List<&2, Mem()>: 299 match ts: 300 case []: 301 sib 302 case (+key, val) <> rest: 303 merge.add(rest, merge.one(named(sib, key), sib, key, val)) 304 305 def merge.of(empty: Bool, sib: List<&2, Mem()>, +target: Value.Json) -> Value.Json: 306 match empty: 307 case True{}: 308 target 309 case False{}: 310 object(merge.add(members(target), sib)) 311 312 # a reference's target with the reference object's other members: the 313 # target itself when there are none; else those members first, then the 314 # target's members they do not name (Reference Object: a sibling 315 # `description` overrides the target's) 316 def merge(+sib: List<&2, Mem()>, +target: Value.Json) -> Value.Json: 317 merge.of(List.is_empty(&2, Mem(), sib), sib, target) 318 319 # the schema a reference cycle stands for 320 def cyclic() -> Value.Json: 321 Json.obj([("type", Json.str("object"))]) 322 323 def deref.found( 324 found: Maybe<&2, Value.Json>, 325 plain: Unit -> Value.Json, 326 follow: Value.Json -> Value.Json 327 ) -> Value.Json: 328 match found: 329 case None{}: 330 plain(Unit{}) 331 case Some{target}: 332 merge(drop(members(plain(Unit{})), "$ref"), follow(target)) 333 334 def deref.ref( 335 cycle: Bool, 336 +ref: String, 337 +doc: Value.Json, 338 plain: Unit -> Value.Json, 339 follow: String -> Value.Json -> Value.Json 340 ) -> Value.Json: 341 match cycle: 342 case True{}: 343 cyclic() 344 case False{}: 345 deref.found(resolve(doc, ref), plain, follow(ref)) 346 347 def deref.obj( 348 ref: Maybe<&2, String>, 349 +doc: Value.Json, 350 +refs: List<&2, String>, 351 plain: Unit -> Value.Json, 352 follow: String -> Value.Json -> Value.Json 353 ) -> Value.Json: 354 match ref: 355 case None{}: 356 plain(Unit{}) 357 case Some{+got}: 358 deref.ref(seen(refs, got), got, doc, plain, follow) 359 360 # a value with every local reference in it replaced by its target, merged 361 # with the reference object's other members (`merge`); a reference to one 362 # of `refs`, the references being followed, is a cycle (`cyclic`), and one 363 # that does not resolve is left as it is. `fuel` bounds how deep values and 364 # references nest 365 def deref( 366 fuel: Nat, 367 val: Value.Json, 368 +doc: Value.Json, 369 +refs: List<&2, String> 370 ) -> Value.Json: 371 match fuel: 372 case 0n: 373 val 374 case 1n+(+less): 375 match val: 376 case Value.JObj{+pairs}: 377 deref.obj(ref.of(pairs), doc, refs, 378 _u => Value.JObj{deref(1n+less, pairs, doc, refs)}, 379 ref => target => deref(less, target, doc, ref <> refs)) 380 case Value.JArr{items}: 381 Value.JArr{deref(1n+less, items, doc, refs)} 382 case Value.JCons{head, tail}: 383 Value.JCons{deref(1n+less, head, doc, refs), 384 deref(1n+less, tail, doc, refs)} 385 case Value.JPair{key, item, rest}: 386 Value.JPair{key, deref(1n+less, item, doc, refs), 387 deref(1n+less, rest, doc, refs)} 388 case Value.JBind{src, n, item, rest}: 389 Value.JBind{src, n, deref(1n+less, item, doc, refs), 390 deref(1n+less, rest, doc, refs)} 391 case other: 392 other 393 394 # how deep a value's references and schemas may nest before the rest is 395 # left as it is 396 def depth() -> Nat: 397 64n 398 399 # a value of the document with its local references followed 400 def dereferenced(+doc: Value.Json, val: Value.Json) -> Value.Json: 401 deref(depth(), val, doc, []) 402 403 # --- schemas (Schema Object) --- 404 405 # the OpenAPI 3.0 fields of a Schema Object that are not JSON Schema, which 406 # an input schema leaves out 407 def extras() -> List<&2, String>: 408 ["nullable", "example", "xml", "externalDocs", "deprecated", "readOnly", 409 "writeOnly"] 410 411 # members less every one named by `keys` 412 def drop.all(keys: List<&2, String>, ps: List<&2, Mem()>) -> List<&2, Mem()>: 413 match keys: 414 case []: 415 ps 416 case key <> rest: 417 drop.all(rest, drop(ps, key)) 418 419 # `integer` as JSON Schema's `number`, any other type as it is 420 def number(+typ: Value.Json) -> Value.Json: 421 Bool.pick(Value.Json, is_str(typ, "integer"), Json.str("number"), typ) 422 423 def with.null.arr(has: Bool, +items: List<&2, Value.Json>) -> Value.Json: 424 match has: 425 case True{}: 426 Json.arr(items) 427 case False{}: 428 Json.arr(List.append(&2, Value.Json, items, [Json.str("null")])) 429 430 def any.null(items: List<&2, Value.Json>) -> Bool: 431 match items: 432 case []: 433 False{} 434 case item <> rest: 435 or.else(is_str(item, "null"), _u => any.null(rest)) 436 437 # a type that also admits null (OpenAPI 3.0 `nullable`): a type name as an 438 # array of it and "null", an array of names with "null" added when it lacks 439 # it, anything else as it is 440 def with.null(typ: Value.Json) -> Value.Json: 441 match typ: 442 case Value.JStr{str}: 443 Json.arr([Value.JStr{str}, Json.str("null")]) 444 case Value.JSpan{src, +n}: 445 Json.arr([Value.JSpan{src, n}, Json.str("null")]) 446 case Value.JArr{+cs}: 447 with.null.arr(any.null(Json.items(Value.JArr{cs})), 448 Json.items(Value.JArr{cs})) 449 case other: 450 other 451 452 def typed.null(nullable: Bool, +typ: Value.Json) -> Value.Json: 453 match nullable: 454 case True{}: 455 with.null(number(typ)) 456 case False{}: 457 number(typ) 458 459 def typed.of( 460 ps: List<&2, Mem()>, 461 typ: Maybe<&2, Value.Json>, 462 nullable: Bool 463 ) -> List<&2, Mem()>: 464 match typ: 465 case None{}: 466 ps 467 case Some{+got}: 468 put(ps, "type", typed.null(nullable, got)) 469 470 # a schema's own members as an input schema has them: the OpenAPI 3.0 471 # extras left out, `integer` as `number`, and `nullable` true adding null to 472 # an explicit type, every other member as it is, in order 473 def own(+ps: List<&2, Mem()>) -> List<&2, Mem()>: 474 typed.of(drop.all(extras(), ps), lookup(ps, "type"), 475 is_true(value(ps, "nullable"))) 476 477 # what a mapped schema recurses into: its properties when its type is 478 # "object", its items when its type is "array", else nothing 479 type Kids is Data: 480 NoKids{} 481 PropKids{cells: Value.Json} 482 ItemKid{item: Value.Json} 483 484 def kids.props(props: Value.Json) -> Kids: 485 match props: 486 case Value.JObj{cs}: 487 PropKids{cs} 488 case other: 489 NoKids{} 490 491 def kids.item(+item: Value.Json) -> Kids: 492 Bool.pick(Kids, Json.is_obj(item), ItemKid{item}, NoKids{}) 493 494 def kids.array(array: Bool, +ps: List<&2, Mem()>) -> Kids: 495 match array: 496 case True{}: 497 kids.item(value(ps, "items")) 498 case False{}: 499 NoKids{} 500 501 def kids.of(object: Bool, array: Bool, +ps: List<&2, Mem()>) -> Kids: 502 match object: 503 case True{}: 504 kids.props(value(ps, "properties")) 505 case False{}: 506 kids.array(array, ps) 507 508 # what a schema's members recurse into: its properties' cells when its 509 # type is "object" and its properties an object, its items when its type is 510 # "array" and its items an object, else nothing 511 def kids(+ps: List<&2, Mem()>) -> Kids: 512 +typ = value(ps, "type") 513 kids.of(is_str(typ, "object"), is_str(typ, "array"), ps) 514 515 def kids.put( 516 +ps: List<&2, Mem()>, 517 kid: Kids, 518 props: Value.Json -> Value.Json, 519 item: Value.Json -> Value.Json 520 ) -> Value.Json: 521 match kid: 522 case NoKids{}: 523 object(ps) 524 case PropKids{cs}: 525 object(put(ps, "properties", Value.JObj{props(cs)})) 526 case ItemKid{got}: 527 object(put(ps, "items", item(got))) 528 529 # whether a property's value is a schema to map, kept as it is, or left out 530 type Prop is Data: 531 PMap{} 532 PKeep{} 533 PDrop{} 534 535 def prop.of(val: Value.Json) -> Prop: 536 match val: 537 case Value.JObj{_c}: 538 PMap{} 539 case Value.JBool{_b}: 540 PKeep{} 541 case other: 542 PDrop{} 543 544 def prop.put( 545 kind: Prop, 546 key: String, 547 +val: Value.Json, 548 go: Value.Json -> Value.Json, 549 rest: Value.Json 550 ) -> Value.Json: 551 match kind: 552 case PMap{}: 553 Value.JPair{key, go(val), rest} 554 case PKeep{}: 555 Value.JPair{key, val, rest} 556 case PDrop{}: 557 rest 558 559 # what `shape` reads its value as: a schema, or the cells of a properties 560 # object 561 type Read is Data: 562 ASchema{} 563 AProps{} 564 565 # a schema as an input schema (`own`), its properties' and items' schemas 566 # mapped the same way; or the cells of a properties object with every 567 # schema value mapped, a boolean kept and any other value left out. `fuel` 568 # bounds how deep schemas nest 569 def shape(fuel: Nat, val: Value.Json, read: Read) -> Value.Json: 570 match fuel: 571 case 0n: 572 val 573 case 1n+(+less): 574 match val read: 575 case Value.JObj{pairs} ASchema{}: 576 +ps = own(members(Value.JObj{pairs})) 577 kids.put(ps, kids(ps), cs => shape(less, cs, AProps{}), 578 got => shape(less, got, ASchema{})) 579 case Value.JPair{key, +item, rest} AProps{}: 580 prop.put(prop.of(item), key, item, got => shape(less, got, ASchema{}), 581 shape(1n+less, rest, AProps{})) 582 case Value.JBind{src, +n, +item, rest} AProps{}: 583 prop.put(prop.of(item), Value.span.str(src, n, U32.is_zero(n)), item, 584 got => shape(less, got, ASchema{}), shape(1n+less, rest, AProps{})) 585 case other ASchema{}: 586 other 587 case other AProps{}: 588 Value.JNil{} 589 590 # a Schema Object as the JSON Schema of an input 591 def schema(val: Value.Json) -> Value.Json: 592 shape(depth(), val, ASchema{}) 593 594 # --- names (Operation Object operationId) --- 595 596 # whether a character may stand in a tool name: an ASCII letter or digit, 597 # `_` or `-` 598 def name.ok(+cp: U32) -> Bool: 599 Bool.or(Bool.and(U32.is_le(65, cp), U32.is_le(cp, 90)), 600 Bool.or(Bool.and(U32.is_le(97, cp), U32.is_le(cp, 122)), 601 Bool.or(Bool.and(U32.is_le(48, cp), U32.is_le(cp, 57)), 602 Bool.or(U32.is_eq(cp, 95), U32.is_eq(cp, 45))))) 603 604 def name.char(ok: Bool, +cp: U32) -> Char: 605 match ok: 606 case True{}: 607 Chr{cp} 608 case False{}: 609 '_' 610 611 # a string as a tool name: each character that may not stand in one 612 # (`name.ok`) as `_`, one for one 613 def sanitize(text: String) -> String: 614 match text: 615 case SNil{}: 616 SNil{} 617 case SCon{Chr{+cp}, rest}: 618 SCon{name.char(name.ok(cp), cp), sanitize(rest)} 619 620 def title.go(cs: String) -> String: 621 match cs: 622 case SNil{}: 623 SNil{} 624 case SCon{'-', SCon{ch, rest}}: 625 SCon{Char.to_upper(ch), title.go(rest)} 626 case SCon{'_', SCon{ch, rest}}: 627 SCon{Char.to_upper(ch), title.go(rest)} 628 case SCon{'/', SCon{ch, rest}}: 629 SCon{Char.to_upper(ch), title.go(rest)} 630 case SCon{ch, rest}: 631 SCon{ch, title.go(rest)} 632 633 # a string with its first character in upper case 634 def capital(text: String) -> String: 635 match text: 636 case SNil{}: 637 SNil{} 638 case SCon{ch, rest}: 639 SCon{Char.to_upper(ch), rest} 640 641 def strip.lead.at(hit: Bool, ch: Char, rest: String) -> String: 642 match hit: 643 case True{}: 644 rest 645 case False{}: 646 SCon{ch, rest} 647 648 def strip.lead(text: String, +want: Char) -> String: 649 match text: 650 case SNil{}: 651 SNil{} 652 case SCon{+ch, rest}: 653 strip.lead.at(Char.is_eq(ch, want), ch, rest) 654 655 # a path segment in title case, as openapi-mcp-generator writes one: lower 656 # case, each `-`, `_` or `/` and the character after it as that character 657 # in upper case, less a leading `{` and a trailing `}`, the first character 658 # in upper case 659 def title(text: String) -> String: 660 capital(String.reverse(strip.lead(String.reverse( 661 strip.lead(title.go(String.to_lower(text)), '{')), '}'))) 662 663 # whether a path segment is a template expression, `{` to `}` 664 def is_param(+part: String) -> Bool: 665 Bool.and(String.starts_with(part, "{"), String.ends_with(part, "}")) 666 667 def segments.at( 668 empty: Bool, 669 part: String, 670 rest: List<&2, String> 671 ) -> List<&2, String>: 672 match empty: 673 case True{}: 674 rest 675 case False{}: 676 part <> rest 677 678 # the pieces of a split path that are not empty 679 def segments(parts: List<&2, String>) -> List<&2, String>: 680 match parts: 681 case []: 682 [] 683 case +part <> rest: 684 segments.at(String.is_empty(part), part, segments(rest)) 685 686 def gen.param(last: Bool, +part: String) -> String: 687 match last: 688 case True{}: 689 "By" ++ title(part) 690 case False{}: 691 "" 692 693 def gen.part(param: Bool, +part: String, last: Bool) -> String: 694 match param: 695 case True{}: 696 gen.param(last, part) 697 case False{}: 698 title(part) 699 700 def gen.parts(parts: List<&2, String>) -> String: 701 match parts: 702 case []: 703 "" 704 case +part <> +rest: 705 gen.part(is_param(part), part, List.is_empty(&2, String, rest)) ++ 706 gen.parts(rest) 707 708 def gen.root(bare: Bool, +name: String) -> String: 709 match bare: 710 case True{}: 711 name ++ "Root" 712 case False{}: 713 name 714 715 # an operation id made from the method and the path, for an operation with 716 # none, as openapi-mcp-generator makes it: the method, each path segment in 717 # title case, a parameter segment only when it is the last and then after 718 # "By", "Root" when there are no segments, the first letter in upper case 719 def generated(+method: String, path: String) -> String: 720 +name = String.to_lower(method) ++ gen.parts(segments(String.split(path, '/'))) 721 capital(gen.root(String.eq(name, String.to_lower(method)), name)) 722 723 # a tool's name: the operation's id when it is a non-empty string, else one 724 # made from the method and path (`generated`), as a tool name (`sanitize`) 725 def name.of(id: Maybe<&2, String>, +method: String, path: String) -> String: 726 sanitize(Maybe.default(&2, String, full(id), generated(method, path))) 727 728 # a tool's description: the operation's description, else its summary, 729 # each when it is a non-empty string, else the method and path it runs 730 def describe( 731 desc: Maybe<&2, String>, 732 summary: Maybe<&2, String>, 733 +method: String, 734 path: String 735 ) -> String: 736 Maybe.default(&2, String, first(full(desc), full(summary)), 737 "Executes " ++ String.to_upper(method) ++ " " ++ path) 738 739 # --- parameters (Parameter Object) --- 740 741 # whether two Parameter Objects are one parameter: the same name and the 742 # same location 743 def same(+left: Value.Json, +right: Value.Json) -> Bool: 744 Bool.and(String.eq(str.at(left, "name"), str.at(right, "name")), 745 String.eq(str.at(left, "in"), str.at(right, "in"))) 746 747 def params.put.at( 748 hit: Bool, 749 old: Value.Json, 750 rest: List<&2, Value.Json>, 751 new: Value.Json, 752 more: Unit -> List<&2, Value.Json> 753 ) -> List<&2, Value.Json>: 754 match hit: 755 case True{}: 756 new <> rest 757 case False{}: 758 old <> more(Unit{}) 759 760 # parameters with `new` in the place of the first that is the same 761 # parameter (`same`), else last 762 def params.put(ps: List<&2, Value.Json>, +new: Value.Json) -> List<&2, Value.Json>: 763 match ps: 764 case []: 765 [new] 766 case +old <> +rest: 767 params.put.at(same(old, new), old, rest, new, _u => params.put(rest, new)) 768 769 # a Path Item's parameters `ps`, each of its operation's `ops` in turn put 770 # in them (`params.put`) 771 def params.merge( 772 ops: List<&2, Value.Json>, 773 ps: List<&2, Value.Json> 774 ) -> List<&2, Value.Json>: 775 match ops: 776 case []: 777 ps 778 case one <> rest: 779 params.merge(rest, params.put(ps, one)) 780 781 # where a parameter goes, from its `in` 782 def place(+loc: String) -> Place: 783 Bool.pick(Place, String.eq(loc, "path"), InPath{}, 784 Bool.pick(Place, String.eq(loc, "query"), InQuery{}, 785 Bool.pick(Place, String.eq(loc, "header"), InHeader{}, 786 Bool.pick(Place, String.eq(loc, "cookie"), InCookie{}, InNone{})))) 787 788 def param.of(name: Maybe<&2, String>, +loc: String) -> List<&2, Param>: 789 match name: 790 case None{}: 791 [] 792 case Some{got}: 793 [Param{got, place(loc)}] 794 795 # the parameters a request is built from: each with a non-empty name, and 796 # where it goes 797 def params.of(ps: List<&2, Value.Json>) -> List<&2, Param>: 798 match ps: 799 case []: 800 [] 801 case +one <> rest: 802 List.append(&2, Param, param.of(full(Json.as_str(Json.get(one, "name"))), 803 str.at(one, "in")), params.of(rest)) 804 805 # --- the input schema --- 806 807 # an input schema being built: its properties and its required names 808 type In is Data: 809 In{props: List<&2, Mem()>, req: List<&2, String>} 810 811 def described.at(is_obj: Bool, +val: Value.Json, desc: String) -> Value.Json: 812 match is_obj: 813 case True{}: 814 object(put(members(val), "description", Json.str(desc))) 815 case False{}: 816 val 817 818 # a schema with its description set to `desc`, in its place or last, when 819 # it is an object and there is one to set 820 def described(+val: Value.Json, desc: Maybe<&2, String>) -> Value.Json: 821 match desc: 822 case None{}: 823 val 824 case Some{got}: 825 described.at(Json.is_obj(val), val, got) 826 827 # `name` added to the end of the required names when `want` 828 def need(req: List<&2, String>, name: String, want: Bool) -> List<&2, String>: 829 match want: 830 case True{}: 831 List.append(&2, String, req, [name]) 832 case False{}: 833 req 834 835 # whether a value stands as a schema: an object or `true` 836 def schema.ok(val: Value.Json) -> Bool: 837 match val: 838 case Value.JObj{_c}: 839 True{} 840 case Value.JBool{flag}: 841 flag 842 case other: 843 False{} 844 845 def input.param.put(+name: String, +one: Value.Json, st: In) -> In: 846 match st: 847 case In{props, req}: 848 In{put(props, name, described(schema(Json.get(one, "schema")), 849 full(Json.as_str(Json.get(one, "description"))))), 850 need(req, name, is_true(Json.get(one, "required")))} 851 852 # a parameter as an input property: one with a name and a schema is the 853 # property of that name, its schema mapped (`schema`) and described by the 854 # parameter's description when that is a non-empty string, and required 855 # when the parameter is; any other leaves the input as it is 856 def input.param.of( 857 name: Maybe<&2, String>, 858 ok: Bool, 859 +one: Value.Json, 860 st: In 861 ) -> In: 862 match name ok: 863 case Some{+got} True{}: 864 input.param.put(got, one, st) 865 case _n _ok: 866 st 867 868 def input.param(+one: Value.Json, st: In) -> In: 869 input.param.of(full(Json.as_str(Json.get(one, "name"))), 870 schema.ok(Json.get(one, "schema")), one, st) 871 872 def input.params(ps: List<&2, Value.Json>, st: In) -> In: 873 match ps: 874 case []: 875 st 876 case one <> rest: 877 input.params(rest, input.param(one, st)) 878 879 def media.first(ms: List<&2, Mem()>) -> String: 880 match ms: 881 case []: 882 "" 883 case (key, _v) <> _rest: 884 key 885 886 def media.at(json: Bool, +content: Value.Json) -> String: 887 match json: 888 case True{}: 889 "application/json" 890 case False{}: 891 media.first(members(content)) 892 893 # a request body's media type: application/json when its content has a 894 # schema for it, else the first media type of its content, else none ("") 895 def media(+content: Value.Json) -> String: 896 media.at(schema.ok(Json.get(Json.get(content, "application/json"), "schema")), 897 content) 898 899 def body.json(+rb: Value.Json) -> Value.Json: 900 +bs = schema(Json.get(Json.get(Json.get(rb, "content"), "application/json"), 901 "schema")) 902 described(bs, first(full(Json.as_str(Json.get(rb, "description"))), 903 first(full(Json.as_str(Json.get(bs, "description"))), 904 Some{"The JSON request body."}))) 905 906 def body.text(+rb: Value.Json, mt: String) -> Value.Json: 907 Json.obj([("type", Json.str("string")), ("description", Json.str( 908 Maybe.default(&2, String, full(Json.as_str(Json.get(rb, "description"))), 909 "Request body (content type: " ++ mt ++ ")")))]) 910 911 def body.schema(json: Bool, +rb: Value.Json, mt: String) -> Value.Json: 912 match json: 913 case True{}: 914 body.json(rb) 915 case False{}: 916 body.text(rb, mt) 917 918 def input.body.of(+rb: Value.Json, +mt: String, st: In) -> In: 919 match st: 920 case In{props, req}: 921 In{put(props, "requestBody", 922 body.schema(String.eq(mt, "application/json"), rb, mt)), 923 need(req, "requestBody", is_true(Json.get(rb, "required")))} 924 925 def input.body.at(none: Bool, +rb: Value.Json, +mt: String, st: In) -> In: 926 match none: 927 case True{}: 928 st 929 case False{}: 930 input.body.of(rb, mt, st) 931 932 # a Request Body Object as the `requestBody` property, with its media type 933 # `mt` (`media`): application/json's schema, mapped and described by the 934 # body's description, else the schema's, else "The JSON request body."; 935 # for another media type a string; required when the body is. A body of 936 # no media type adds nothing 937 def input.body(+rb: Value.Json, +mt: String, st: In) -> In: 938 input.body.at(String.is_empty(mt), rb, mt, st) 939 940 # strings as JSON strings 941 def strs(xs: List<&2, String>) -> List<&2, Value.Json>: 942 match xs: 943 case []: 944 [] 945 case str <> rest: 946 Json.str(str) <> strs(rest) 947 948 # the `required` member of an input schema, none for no names 949 def required(req: List<&2, String>) -> List<&2, Mem()>: 950 match req: 951 case []: 952 [] 953 case one <> rest: 954 [("required", Json.arr(strs(one <> rest)))] 955 956 # the input schema built: an object schema of the properties, with the 957 # required names when there are any 958 def input.of(st: In) -> Value.Json: 959 match st: 960 case In{props, req}: 961 object(List.append(&2, Mem(), [("type", Json.str("object")), 962 ("properties", object(props))], required(req))) 963 964 # --- operations (Paths Object, Path Item Object, Operation Object) --- 965 966 # the methods a Path Item may have an operation for, in the order their 967 # tools are listed 968 def methods() -> List<&2, String>: 969 ["get", "put", "post", "delete", "options", "head", "patch", "trace"] 970 971 # a request body's media type, "" for none 972 def media.of(+rb: Value.Json, has: Bool) -> String: 973 match has: 974 case True{}: 975 media(Json.get(rb, "content")) 976 case False{}: 977 "" 978 979 # one operation of a Path Item at `path`, `oper` its Operation Object, its 980 # references followed in `doc`, after the Path Item's parameters `ips` 981 def op( 982 +doc: Value.Json, 983 +path: String, 984 +method: String, 985 ips: List<&2, Value.Json>, 986 +oper: Value.Json 987 ) -> Op: 988 +ps = params.merge(Json.items(dereferenced(doc, 989 Json.get(oper, "parameters"))), ips) 990 +rb = dereferenced(doc, Json.get(oper, "requestBody")) 991 +mt = media.of(rb, Json.is_obj(rb)) 992 Op{name.of(Json.as_str(Json.get(oper, "operationId")), method, path), 993 describe(Json.as_str(Json.get(oper, "description")), 994 Json.as_str(Json.get(oper, "summary")), method, path), 995 input.of(input.body(rb, mt, input.params(ps, In{[], []}))), 996 String.to_upper(method), path, params.of(ps), mt} 997 998 def item.at( 999 hit: Bool, 1000 +doc: Value.Json, 1001 +path: String, 1002 +method: String, 1003 +ips: List<&2, Value.Json>, 1004 +oper: Value.Json, 1005 rest: List<&2, Op> 1006 ) -> List<&2, Op>: 1007 match hit: 1008 case True{}: 1009 op(doc, path, method, ips, oper) <> rest 1010 case False{}: 1011 rest 1012 1013 # a Path Item's operations for the methods `ms`, in their order: one for 1014 # each method whose member is an object (`op`), none for any other 1015 def item( 1016 ms: List<&2, String>, 1017 +doc: Value.Json, 1018 +path: String, 1019 +it: Value.Json, 1020 +ips: List<&2, Value.Json> 1021 ) -> List<&2, Op>: 1022 match ms: 1023 case []: 1024 [] 1025 case +method <> rest: 1026 +oper = Json.get(it, method) 1027 item.at(Json.is_obj(oper), doc, path, method, ips, oper, 1028 item(rest, doc, path, it, ips)) 1029 1030 # the operations of the Path Items `ps`, in order, each one's in the order 1031 # of `methods`, after its parameters with their references followed 1032 def paths(ps: List<&2, Mem()>, +doc: Value.Json) -> List<&2, Op>: 1033 match ps: 1034 case []: 1035 [] 1036 case (+path, +it) <> rest: 1037 List.append(&2, Op, item(methods(), doc, path, it, 1038 Json.items(dereferenced(doc, Json.get(it, "parameters")))), 1039 paths(rest, doc)) 1040 1041 # every operation of a document, in the order of its paths (`paths`) 1042 def ops(+doc: Value.Json) -> List<&2, Op>: 1043 paths(members(Json.get(doc, "paths")), doc) 1044 1045 # an operation's tool 1046 def tool(oper: Op) -> Server.Tool: 1047 match oper: 1048 case Op{name, description, input, method, path, params, media}: 1049 Server.Tool{name, description, input} 1050 1051 # an operation's tool name 1052 def name(oper: Op) -> String: 1053 match oper: 1054 case Op{got, description, input, method, path, params, media}: 1055 got 1056 1057 # an operation under another tool name, all else as it was 1058 def renamed(oper: Op, to: String) -> Op: 1059 match oper: 1060 case Op{name, description, input, method, path, params, media}: 1061 Op{to, description, input, method, path, params, media} 1062 1063 # --- calls --- 1064 1065 def arg.at(has: Bool, +val: Value.Json) -> Maybe<&2, Value.Json>: 1066 match has: 1067 case True{}: 1068 Bool.pick(Maybe<&2, Value.Json>, is_null(val), None{}, Some{val}) 1069 case False{}: 1070 None{} 1071 1072 # an argument's value when it is there and not null 1073 def arg(+args: Value.Json, +name: String) -> Maybe<&2, Value.Json>: 1074 arg.at(Json.has(args, name), Json.get(args, name)) 1075 1076 def plain.scalar(val: Value.Json) -> String: 1077 match val: 1078 case Value.JStr{str}: 1079 str 1080 case Value.JSpan{src, +n}: 1081 Value.span.str(src, n, U32.is_zero(n)) 1082 case Value.JNum{raw}: 1083 raw 1084 case Value.JBool{True{}}: 1085 "true" 1086 case Value.JBool{False{}}: 1087 "false" 1088 case other: 1089 Json.print(other) 1090 1091 def plain.item(val: Value.Json) -> String: 1092 match val: 1093 case Value.JNull{}: 1094 "" 1095 case other: 1096 plain.scalar(other) 1097 1098 def plain.items(items: List<&2, Value.Json>) -> List<&2, String>: 1099 match items: 1100 case []: 1101 [] 1102 case one <> rest: 1103 plain.item(one) <> plain.items(rest) 1104 1105 # a value as the text of a parameter: a string as itself, a number as its 1106 # text, a boolean as `true` or `false`, an array as its items' texts joined 1107 # by `,` (style simple), anything else as its JSON 1108 def plain(val: Value.Json) -> String: 1109 match val: 1110 case Value.JArr{+cs}: 1111 String.join(plain.items(Json.items(Value.JArr{cs})), ",") 1112 case other: 1113 plain.scalar(other) 1114 1115 def swap.at( 1116 hit: Bool, 1117 +text: String, 1118 +pat: String, 1119 rep: String, 1120 more: Unit -> String 1121 ) -> String: 1122 match hit: 1123 case True{}: 1124 rep ++ String.drop(text, String.length(pat)) 1125 case False{}: 1126 more(Unit{}) 1127 1128 # a string with its first `pat` replaced by `rep` 1129 def swap(text: String, +pat: String, +rep: String) -> String: 1130 match text: 1131 case SNil{}: 1132 SNil{} 1133 case SCon{+ch, +rest}: 1134 swap.at(String.starts_with(SCon{ch, rest}, pat), SCon{ch, rest}, pat, rep, 1135 _u => SCon{ch, swap(rest, pat, rep)}) 1136 1137 # a path template with its first `{name}` replaced by the value's text, 1138 # percent-encoded as a path segment 1139 def fill(path: String, +name: String, val: Value.Json) -> String: 1140 swap(path, "{" ++ name ++ "}", Pct.quote(False{}, plain(val))) 1141 1142 def query.item(+name: String, val: Value.Json) -> List<&2, Qs.Kv()>: 1143 match val: 1144 case Value.JNull{}: 1145 [] 1146 case other: 1147 [(name, plain.scalar(other))] 1148 1149 def query.items(+name: String, items: List<&2, Value.Json>) -> List<&2, Qs.Kv()>: 1150 match items: 1151 case []: 1152 [] 1153 case one <> rest: 1154 List.append(&2, Qs.Kv(), query.item(name, one), query.items(name, rest)) 1155 1156 # a query parameter's pairs: one for each item of an array that is not 1157 # null, else one of the value's text (style form, explode true) 1158 def query(+name: String, val: Value.Json) -> List<&2, Qs.Kv()>: 1159 match val: 1160 case Value.JArr{+cs}: 1161 query.items(name, Json.items(Value.JArr{cs})) 1162 case other: 1163 [(name, plain(other))] 1164 1165 # a request being built: the path, the query's pairs and the header fields 1166 type Req is Data: 1167 Req{path: String, query: List<&2, Qs.Kv()>, headers: List<&2, Http.Header>} 1168 1169 # a parameter's value put in a request: a path parameter filled into the 1170 # path (`fill`), a query parameter's pairs added to the query (`query`), a 1171 # header parameter a header field of its name in lower case and its text 1172 # (`plain`); a cookie parameter, or one of no known location, changes 1173 # nothing 1174 def req.put.at( 1175 place: Place, 1176 +name: String, 1177 val: Value.Json, 1178 path: String, 1179 pairs: List<&2, Qs.Kv()>, 1180 hs: List<&2, Http.Header> 1181 ) -> Req: 1182 match place: 1183 case InPath{}: 1184 Req{fill(path, name, val), pairs, hs} 1185 case InQuery{}: 1186 Req{path, List.append(&2, Qs.Kv(), pairs, query(name, val)), hs} 1187 case InHeader{}: 1188 Req{path, pairs, List.append(&2, Http.Header, hs, 1189 [Http.H{String.to_lower(name), plain(val)}])} 1190 case InCookie{}: 1191 Req{path, pairs, hs} 1192 case InNone{}: 1193 Req{path, pairs, hs} 1194 1195 def req.put(place: Place, +name: String, val: Value.Json, sofar: Req) -> Req: 1196 match sofar: 1197 case Req{path, pairs, hs}: 1198 req.put.at(place, name, val, path, pairs, hs) 1199 1200 def req.arg( 1201 got: Maybe<&2, Value.Json>, 1202 place: Place, 1203 +name: String, 1204 sofar: Req 1205 ) -> Req: 1206 match got: 1207 case None{}: 1208 sofar 1209 case Some{val}: 1210 req.put(place, name, val, sofar) 1211 1212 # a request with each parameter that has an argument there and not null 1213 # put in it (`req.put`), in order 1214 def req.params(ps: List<&2, Param>, +args: Value.Json, sofar: Req) -> Req: 1215 match ps: 1216 case []: 1217 sofar 1218 case Param{+name, place} <> rest: 1219 req.params(rest, args, req.arg(arg(args, name), place, name, sofar)) 1220 1221 def body.text.of(json: Bool, val: Value.Json) -> String: 1222 match json: 1223 case True{}: 1224 Json.print(val) 1225 case False{}: 1226 plain.scalar(val) 1227 1228 def body.of(+mt: String, got: Maybe<&2, Value.Json>) -> Maybe<&2, String>: 1229 match got: 1230 case None{}: 1231 None{} 1232 case Some{val}: 1233 Some{body.text.of(String.eq(mt, "application/json"), val)} 1234 1235 # a request's body: the `requestBody` argument, when the operation has a 1236 # body and the argument is there and not null, as JSON for 1237 # application/json, else a string as itself and anything else as its JSON 1238 def body.arg(none: Bool, +mt: String, +args: Value.Json) -> Maybe<&2, String>: 1239 match none: 1240 case True{}: 1241 None{} 1242 case False{}: 1243 body.of(mt, arg(args, "requestBody")) 1244 1245 # the header field naming a body's media type, when there is a body 1246 def ctype(+mt: String, body: Maybe<&2, String>) -> List<&2, Http.Header>: 1247 match body: 1248 case None{}: 1249 [] 1250 case Some{_b}: 1251 [Http.H{"Content-Type", mt}] 1252 1253 # a query string: none for no pairs, else `?` and the pairs form-encoded 1254 def query.text(pairs: List<&2, Qs.Kv()>) -> String: 1255 match pairs: 1256 case []: 1257 "" 1258 case one <> rest: 1259 "?" ++ Qs.urlencode(one <> rest) 1260 1261 # the request built, or the path when a template expression is still in it 1262 def call.send( 1263 unfilled: Bool, 1264 method: String, 1265 +mt: String, 1266 +body: Maybe<&2, String>, 1267 path: String, 1268 pairs: List<&2, Qs.Kv()>, 1269 hs: List<&2, Http.Header> 1270 ) -> Call: 1271 match unfilled: 1272 case True{}: 1273 Unfilled{path} 1274 case False{}: 1275 Send{method, path ++ query.text(pairs), List.append(&2, Http.Header, 1276 Http.H{"Accept", "application/json"} <> hs, ctype(mt, body)), body} 1277 1278 def call.of( 1279 method: String, 1280 +mt: String, 1281 +body: Maybe<&2, String>, 1282 built: Req 1283 ) -> Call: 1284 match built: 1285 case Req{+path, pairs, hs}: 1286 call.send(String.contains(path, "{"), method, mt, body, path, pairs, hs) 1287 1288 # the request a call of an operation stands for, with these arguments: 1289 # each parameter with an argument there and not null put in the request 1290 # (`req.params`), the header fields Accept: application/json, then the 1291 # header parameters', then Content-Type when there is a body (`body.arg`); 1292 # or Unfilled when the path still holds a `{` 1293 def call(oper: Op, +args: Value.Json) -> Call: 1294 match oper: 1295 case Op{name, description, input, method, path, params, +mt}: 1296 call.of(method, mt, body.arg(String.is_empty(mt), mt, args), 1297 req.params(params, args, Req{path, [], []})) 1298 1299 # --- checking a call's arguments --- 1300 1301 # whether every character is a digit 1302 def digits(text: String) -> Bool: 1303 match text: 1304 case SNil{}: 1305 True{} 1306 case SCon{ch, rest}: 1307 and.then(Char.is_digit(ch), _u => digits(rest)) 1308 1309 # whether a number's text is an integer: an optional `-`, then digits 1310 def int.text(text: String) -> Bool: 1311 match text: 1312 case SNil{}: 1313 False{} 1314 case SCon{'-', +rest}: 1315 Bool.and(Bool.not(String.is_empty(rest)), digits(rest)) 1316 case SCon{ch, rest}: 1317 digits(SCon{ch, rest}) 1318 1319 def kind.int(val: Value.Json) -> Bool: 1320 match val: 1321 case Value.JNum{raw}: 1322 int.text(raw) 1323 case other: 1324 False{} 1325 1326 def kind.str(val: Value.Json) -> Bool: 1327 match val: 1328 case Value.JStr{_s}: 1329 True{} 1330 case Value.JSpan{_src, _n}: 1331 True{} 1332 case other: 1333 False{} 1334 1335 def kind.num(val: Value.Json) -> Bool: 1336 match val: 1337 case Value.JNum{_raw}: 1338 True{} 1339 case other: 1340 False{} 1341 1342 def kind.bool(val: Value.Json) -> Bool: 1343 match val: 1344 case Value.JBool{_b}: 1345 True{} 1346 case other: 1347 False{} 1348 1349 # the JSON type of a value, as JSON Schema names it 1350 def kind.name(val: Value.Json) -> String: 1351 match val: 1352 case Value.JNull{}: 1353 "null" 1354 case Value.JBool{_b}: 1355 "boolean" 1356 case Value.JNum{_r}: 1357 "number" 1358 case Value.JStr{_s}: 1359 "string" 1360 case Value.JSpan{_src, _n}: 1361 "string" 1362 case Value.JArr{_c}: 1363 "array" 1364 case Value.JObj{_c}: 1365 "object" 1366 case other: 1367 "unknown" 1368 1369 # whether a value is of the JSON Schema type named `typ`: string, number, 1370 # integer (a number whose text is an integer), boolean, array, object or 1371 # null; true for a name JSON Schema does not have 1372 def kind(+typ: String, +val: Value.Json) -> Bool: 1373 +int = kind.int(val) 1374 Bool.pick(Bool, String.eq(typ, "integer"), int, 1375 Bool.pick(Bool, String.eq(typ, "string"), kind.str(val), 1376 Bool.pick(Bool, String.eq(typ, "number"), kind.num(val), 1377 Bool.pick(Bool, String.eq(typ, "boolean"), kind.bool(val), 1378 Bool.pick(Bool, String.eq(typ, "array"), Json.is_arr(val), 1379 Bool.pick(Bool, String.eq(typ, "object"), Json.is_obj(val), 1380 Bool.pick(Bool, String.eq(typ, "null"), is_null(val), True{}))))))) 1381 1382 # whether a value is of one of the types an array of names lists 1383 def kinds(types: List<&2, Value.Json>, +val: Value.Json) -> Bool: 1384 match types: 1385 case []: 1386 False{} 1387 case one <> rest: 1388 or.else(kind(Maybe.default(&2, String, Json.as_str(one), ""), val), 1389 _u => kinds(rest, val)) 1390 1391 # whether a value is of a schema's `type`: the type it names, or one of the 1392 # types an array of names lists; true for a schema with no `type` 1393 def typed(typ: Value.Json, +val: Value.Json) -> Bool: 1394 match typ: 1395 case Value.JStr{str}: 1396 kind(str, val) 1397 case Value.JSpan{src, +n}: 1398 kind(Value.span.str(src, n, U32.is_zero(n)), val) 1399 case Value.JArr{+cs}: 1400 kinds(Json.items(Value.JArr{cs}), val) 1401 case other: 1402 True{} 1403 1404 # whether a value is of one of the `anyOf` alternatives' types 1405 def alts(choices: List<&2, Value.Json>, +val: Value.Json) -> Bool: 1406 match choices: 1407 case []: 1408 False{} 1409 case one <> rest: 1410 or.else(typed(Json.get(one, "type"), val), _u => alts(rest, val)) 1411 1412 def same.json(+val: Value.Json, items: List<&2, Value.Json>) -> Bool: 1413 match items: 1414 case []: 1415 False{} 1416 case one <> rest: 1417 or.else(String.eq(Json.print(one), Json.print(val)), 1418 _u => same.json(val, rest)) 1419 1420 # whether a value fits a property's schema, one level deep: its `type` 1421 # (`typed`), one of its `anyOf` alternatives' types when it has `anyOf`, 1422 # and one of its `enum` values when it has `enum` 1423 def fits(+sch: Value.Json, +val: Value.Json) -> Bool: 1424 +any = Json.get(sch, "anyOf") 1425 +en = Json.get(sch, "enum") 1426 Bool.and(typed(Json.get(sch, "type"), val), 1427 Bool.and(Bool.or(Bool.not(Json.is_arr(any)), alts(Json.items(any), val)), 1428 Bool.or(Bool.not(Json.is_arr(en)), same.json(val, Json.items(en))))) 1429 1430 def missing.at( 1431 has: Bool, 1432 name: String, 1433 more: Unit -> Maybe<&2, String> 1434 ) -> Maybe<&2, String>: 1435 match has: 1436 case True{}: 1437 more(Unit{}) 1438 case False{}: 1439 Some{name ++ ": Required"} 1440 1441 # what the first required name the arguments do not have is missing 1442 def missing(req: List<&2, Value.Json>, +args: Value.Json) -> Maybe<&2, String>: 1443 match req: 1444 case []: 1445 None{} 1446 case one <> rest: 1447 +name = Maybe.default(&2, String, Json.as_str(one), "") 1448 missing.at(Json.has(args, name), name, _u => missing(rest, args)) 1449 1450 def wanted.of(typ: Maybe<&2, String>, val: Value.Json) -> String: 1451 match typ: 1452 case Some{str}: 1453 "Expected " ++ str ++ ", received " ++ kind.name(val) 1454 case None{}: 1455 "Invalid input" 1456 1457 # what is wrong with a value that does not fit its schema: the type it 1458 # should have had, when the schema names one, and the type it has 1459 def wanted(sch: Value.Json, val: Value.Json) -> String: 1460 wanted.of(Json.as_str(Json.get(sch, "type")), val) 1461 1462 def bad.at( 1463 ok: Bool, 1464 name: String, 1465 sch: Value.Json, 1466 val: Value.Json, 1467 more: Unit -> Maybe<&2, String> 1468 ) -> Maybe<&2, String>: 1469 match ok: 1470 case True{}: 1471 more(Unit{}) 1472 case False{}: 1473 Some{name ++ ": " ++ wanted(sch, val)} 1474 1475 # what is wrong with the first property whose argument is there and does 1476 # not fit its schema (`fits`) 1477 def bad(ps: List<&2, Mem()>, +args: Value.Json) -> Maybe<&2, String>: 1478 match ps: 1479 case []: 1480 None{} 1481 case (+name, +sch) <> rest: 1482 bad.at(Bool.or(Bool.not(Json.has(args, name)), fits(sch, Json.get(args, 1483 name))), name, sch, Json.get(args, name), _u => bad(rest, args)) 1484 1485 def check.of(name: String, why: Maybe<&2, String>) -> Maybe<&2, String>: 1486 match why: 1487 case None{}: 1488 None{} 1489 case Some{got}: 1490 Some{"Invalid arguments for tool '" ++ name ++ "': " ++ got} 1491 1492 # what is wrong with a call's arguments, when anything is: a required name 1493 # they do not have (`missing`), else a property they have whose value does 1494 # not fit its schema (`bad`); none when neither 1495 def check(oper: Op, +args: Value.Json) -> Maybe<&2, String>: 1496 match oper: 1497 case Op{name, description, +input, method, path, params, media}: 1498 check.of(name, first(missing(Json.items(Json.get(input, "required")), 1499 args), bad(members(Json.get(input, "properties")), args)))