src/openapi.bend
ezmcp@0.1.0.0 · 1,499 lines · 169 definitions
1
2
3
4
5
6
7
8
9
10
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
20
21type Place is Data:
22 InPath{}
23 InQuery{}
24 InHeader{}
25 InCookie{}
26 InNone{}
27
28
29type Param is Data:
30 Param{name: String, place: Place}
31
32
33
34
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
40
41
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
48
49
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
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
66
67
68def Mem() -> Data:
69 Sigma<&2, &2, String, _ => Value.Json>
70
71
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
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
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
116
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
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
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
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
172def value(ps: List<&2, Mem()>, key: String) -> Value.Json:
173 Maybe.default(&2, Value.Json, lookup(ps, key), Json.null())
174
175
176
177
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
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
194def is_true(val: Value.Json) -> Bool:
195 match val:
196 case Value.JBool{flag}:
197 flag
198 case other:
199 False{}
200
201
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
217def is_str(val: Value.Json, +str: String) -> Bool:
218 is_str.of(Json.as_str(val), str)
219
220
221def str.at(val: Value.Json, key: String) -> String:
222 Maybe.default(&2, String, Json.as_str(Json.get(val, key)), "")
223
224
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
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
253
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
270
271def resolve(+doc: Value.Json, +ref: String) -> Maybe<&2, Value.Json>:
272 resolve.at(String.starts_with(ref, "#/"), doc, ref)
273
274
275def ref.of(pairs: Value.Json) -> Maybe<&2, String>:
276 Json.as_str(Value.find(pairs, "$ref"))
277
278
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
313
314
315
316def merge(+sib: List<&2, Mem()>, +target: Value.Json) -> Value.Json:
317 merge.of(List.is_empty(&2, Mem(), sib), sib, target)
318
319
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
361
362
363
364
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
395
396def depth() -> Nat:
397 64n
398
399
400def dereferenced(+doc: Value.Json, val: Value.Json) -> Value.Json:
401 deref(depth(), val, doc, [])
402
403
404
405
406
407def extras() -> List<&2, String>:
408 ["nullable", "example", "xml", "externalDocs", "deprecated", "readOnly",
409 "writeOnly"]
410
411
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
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
438
439
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
471
472
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
478
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
509
510
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
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
560
561type Read is Data:
562 ASchema{}
563 AProps{}
564
565
566
567
568
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
591def schema(val: Value.Json) -> Value.Json:
592 shape(depth(), val, ASchema{})
593
594
595
596
597
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
612
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
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
656
657
658
659def title(text: String) -> String:
660 capital(String.reverse(strip.lead(String.reverse(
661 strip.lead(title.go(String.to_lower(text)), '{')), '}')))
662
663
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
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
716
717
718
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
724
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
729
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
740
741
742
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
761
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
770
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
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
796
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
806
807
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
819
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
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
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
853
854
855
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
894
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
933
934
935
936
937def input.body(+rb: Value.Json, +mt: String, st: In) -> In:
938 input.body.at(String.is_empty(mt), rb, mt, st)
939
940
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
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
957
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
965
966
967
968def methods() -> List<&2, String>:
969 ["get", "put", "post", "delete", "options", "head", "patch", "trace"]
970
971
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
980
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
1014
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
1031
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
1042def ops(+doc: Value.Json) -> List<&2, Op>:
1043 paths(members(Json.get(doc, "paths")), doc)
1044
1045
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
1052def name(oper: Op) -> String:
1053 match oper:
1054 case Op{got, description, input, method, path, params, media}:
1055 got
1056
1057
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
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
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
1106
1107
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
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
1138
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
1157
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
1166type Req is Data:
1167 Req{path: String, query: List<&2, Qs.Kv()>, headers: List<&2, Http.Header>}
1168
1169
1170
1171
1172
1173
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
1213
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
1236
1237
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
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
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
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
1289
1290
1291
1292
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
1300
1301
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
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
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
1370
1371
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
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
1392
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
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
1421
1422
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
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
1458
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
1476
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
1493
1494
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)))