src/openapi.bend

ezmcp@0.1.0.0 · 1,499 lines · 169 definitions

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.
11import Base
12import emerging-ezjson@1.2.0.0/main.bend as Json
13import emerging-ezjson@1.2.0.0/src/value.bend as Value
14import emerging-ezhttp@0.21.0.0/src/http.bend as Http
15import emerging-ezhttp@0.21.0.0/src/pct.bend as Pct
16import emerging-ezhttp@0.21.0.0/src/qs.bend as Qs
17import ezmcp@0.1.0.0/src/server.bend as Server
18
19# where a parameter goes (Parameter Object `in`); InNone for a location
20# OpenAPI does not have
21type 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
29type 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)
35type 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
42type 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
50def 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
58def 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
68def Mem() -> Data:
69  Sigma<&2, &2, String, _ => Value.Json>
70
71# members as the cells of an object, in order
72def 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
80def object(ps: List<&2, Mem()>) -> Value.Json:
81  Value.JObj{cells(ps)}
82
83def 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
93def members(val: Value.Json) -> List<&2, Mem()>:
94  match val:
95    case Value.JObj{pairs}:
96      members.go(pairs)
97    case other:
98      []
99
100def 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
117def 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
124def 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
137def 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`
145def 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
152def 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`
164def 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
172def 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
178def 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
186def 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
194def 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
202def is_null(val: Value.Json) -> Bool:
203  match val:
204    case Value.JNull{}:
205      True{}
206    case other:
207      False{}
208
209def 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`
217def 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
221def 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
226def 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)
238def token(text: String) -> String:
239  token.go(text)
240
241def 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
254def 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
262def 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
271def 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
275def 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
279def 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
286def 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
298def 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
305def 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)
316def 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
320def cyclic() -> Value.Json:
321  Json.obj([("type", Json.str("object"))])
322
323def 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
334def 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
347def 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
365def 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
396def depth() -> Nat:
397  64n
398
399# a value of the document with its local references followed
400def 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
407def extras() -> List<&2, String>:
408  ["nullable", "example", "xml", "externalDocs", "deprecated", "readOnly",
409    "writeOnly"]
410
411# members less every one named by `keys`
412def 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
420def number(+typ: Value.Json) -> Value.Json:
421  Bool.pick(Value.Json, is_str(typ, "integer"), Json.str("number"), typ)
422
423def 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
430def 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
440def 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
452def 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
459def 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
473def 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
479type Kids is Data:
480  NoKids{}
481  PropKids{cells: Value.Json}
482  ItemKid{item: Value.Json}
483
484def kids.props(props: Value.Json) -> Kids:
485  match props:
486    case Value.JObj{cs}:
487      PropKids{cs}
488    case other:
489      NoKids{}
490
491def kids.item(+item: Value.Json) -> Kids:
492  Bool.pick(Kids, Json.is_obj(item), ItemKid{item}, NoKids{})
493
494def 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
501def 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
511def kids(+ps: List<&2, Mem()>) -> Kids:
512  +typ = value(ps, "type")
513  kids.of(is_str(typ, "object"), is_str(typ, "array"), ps)
514
515def 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
530type Prop is Data:
531  PMap{}
532  PKeep{}
533  PDrop{}
534
535def 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
544def 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
561type 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
569def 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
591def 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 `-`
598def 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
604def 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
613def 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
620def 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
634def 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
641def 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
648def 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
659def 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 `}`
664def is_param(+part: String) -> Bool:
665  Bool.and(String.starts_with(part, "{"), String.ends_with(part, "}"))
666
667def 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
679def 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
686def gen.param(last: Bool, +part: String) -> String:
687  match last:
688    case True{}:
689      "By" ++ title(part)
690    case False{}:
691      ""
692
693def 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
700def 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
708def 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
719def 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`)
725def 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
730def 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
743def 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
747def 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
762def 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`)
771def 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`
782def 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
788def 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
797def 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
808type In is Data:
809  In{props: List<&2, Mem()>, req: List<&2, String>}
810
811def 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
820def 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`
828def 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`
836def 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
845def 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
856def 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
868def 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
872def 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
879def media.first(ms: List<&2, Mem()>) -> String:
880  match ms:
881    case []:
882      ""
883    case (key, _v) <> _rest:
884      key
885
886def 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 ("")
895def media(+content: Value.Json) -> String:
896  media.at(schema.ok(Json.get(Json.get(content, "application/json"), "schema")),
897    content)
898
899def 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
906def 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
911def 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
918def 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
925def 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
937def 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
941def 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
949def 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
958def 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
968def methods() -> List<&2, String>:
969  ["get", "put", "post", "delete", "options", "head", "patch", "trace"]
970
971# a request body's media type, "" for none
972def 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`
981def 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
998def 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
1015def 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
1032def 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`)
1042def ops(+doc: Value.Json) -> List<&2, Op>:
1043  paths(members(Json.get(doc, "paths")), doc)
1044
1045# an operation's tool
1046def 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
1052def 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
1058def 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
1065def 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
1073def arg(+args: Value.Json, +name: String) -> Maybe<&2, Value.Json>:
1074  arg.at(Json.has(args, name), Json.get(args, name))
1075
1076def 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
1091def plain.item(val: Value.Json) -> String:
1092  match val:
1093    case Value.JNull{}:
1094      ""
1095    case other:
1096      plain.scalar(other)
1097
1098def 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
1108def 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
1115def 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`
1129def 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
1139def fill(path: String, +name: String, val: Value.Json) -> String:
1140  swap(path, "{" ++ name ++ "}", Pct.quote(False{}, plain(val)))
1141
1142def 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
1149def 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)
1158def 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
1166type 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
1174def 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
1195def 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
1200def 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
1214def 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
1221def 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
1228def 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
1238def 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
1246def 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
1254def 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
1262def 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
1278def 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 `{`
1293def 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
1302def 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
1310def 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
1319def kind.int(val: Value.Json) -> Bool:
1320  match val:
1321    case Value.JNum{raw}:
1322      int.text(raw)
1323    case other:
1324      False{}
1325
1326def 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
1335def kind.num(val: Value.Json) -> Bool:
1336  match val:
1337    case Value.JNum{_raw}:
1338      True{}
1339    case other:
1340      False{}
1341
1342def 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
1350def 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
1372def 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
1383def 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`
1393def 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
1405def 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
1412def 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`
1423def 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
1430def 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
1442def 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
1450def 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
1459def wanted(sch: Value.Json, val: Value.Json) -> String:
1460  wanted.of(Json.as_str(Json.get(sch, "type")), val)
1461
1462def 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`)
1477def 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
1485def 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
1495def 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)))