git.iko.soy/rowtype-yoga/purescript-yoga-http-api
on feat/custom-type-errors help sign in
31 files changed+1877−460
since splitsnapshots
unifiedsplit
spago.lock+247−75 spago.yaml+15−15 src/Yoga/HTTP/API/Route.purs+3−1 src/Yoga/HTTP/API/Route/Handler.purs+14−15 src/Yoga/HTTP/API/Route/OpenAPI.purs+67−59 src/Yoga/HTTP/API/Route/RenderMethod.purs+1−1 src/Yoga/HTTP/API/Route/Response.purs+186−62 src/Yoga/HTTP/API/Route/Route.purs+5−6 src/Yoga/HTTP/API/Route/RouteHandler.purs+1−2 src/Yoga/HTTP/API/Route/StatusCode.purs+1−1 src/Yoga/HTTP/API/UnionTrick.js+3 src/Yoga/HTTP/API/UnionTrick.purs+163 test-compile-fail/cases/BodyTypeMismatch.purs+2−2 test-compile-fail/cases/DuplicateCaptureNames.purs+2−2 test-compile-fail/cases/HandlerSignatureMismatch.purs+2−2 test-compile-fail/cases/InvalidResponseRecordStructure.purs+2−2 test-compile-fail/cases/MissingRequiredQueryParam.purs+2−2 test-compile-fail/cases/SchemaNameCollision.purs+3−3 test-compile-fail/cases/WrongResponseVariantLabel.purs+2−2 test/Test/APIRecordTest.purs+7−7 test/Test/BearerAuthTest.purs+23−23 test/Test/ComprehensiveExample.purs+21−23 test/Test/ContentTypeTest.purs+16−52 test/Test/CookieAuthTest.purs+12−12 test/Test/ExamplesTest.purs+6−21 test/Test/OpenAPIInfoTest.purs+22−55 test/Test/ProjectTrackerExample.purs+499 test/Test/ProjectTrackerIntegration.purs.disabled+195 test/Test/ProjectTrackerLayers.purs.disabled+340 test/Test/SchemaComponentTest.purs+8−8 test/Test/VariantResponseTest.purs+7−7
spago.lock+247−75
@@ -6,16 +6,13 @@
66 "core": {
77 "dependencies": [
88 {
9− "aff": ">=7.0.0 <8.0.0"
9+ "aff": ">=7.1.0 <8.0.0"
1010 },
1111 {
12− "arrays": ">=7.0.0 <8.0.0"
13− },
14− {
15− "effect": ">=4.0.0 <5.0.0"
12+ "arrays": ">=7.3.0 <8.0.0"
1613 },
1714 {
18− "either": ">=6.0.0 <7.0.0"
15+ "either": ">=6.1.0 <7.0.0"
1916 },
2017 {
2118 "foldable-traversable": ">=6.0.0 <7.0.0"
@@ -24,11 +21,17 @@
2421 "foreign": ">=7.0.0 <8.0.0"
2522 },
2623 {
27− "foreign-object": ">=4.0.0 <5.0.0"
24+ "foreign-object": ">=4.1.0 <5.0.0"
25+ },
26+ {
27+ "identity": ">=6.0.0 <7.0.0"
2828 },
2929 {
3030 "integers": ">=6.0.0 <7.0.0"
3131 },
32+ {
33+ "literals": ">=1.0.2 <2.0.0"
34+ },
3235 {
3336 "maybe": ">=6.0.0 <7.0.0"
3437 },
@@ -36,28 +39,31 @@
3639 "newtype": ">=5.0.0 <6.0.0"
3740 },
3841 {
39− "prelude": ">=6.0.0 <7.0.0"
42+ "prelude": ">=6.0.1 <7.0.0"
4043 },
4144 {
4245 "record": ">=4.0.0 <5.0.0"
4346 },
4447 {
45− "strings": ">=6.0.0 <7.0.0"
48+ "strings": ">=6.0.1 <7.0.0"
4649 },
4750 {
4851 "tuples": ">=7.0.0 <8.0.0"
4952 },
5053 {
51− "typelevel-prelude": ">=7.0.0 <8.0.0"
54+ "undefined-is-not-a-problem": ">=1.1.0 <2.0.0"
5255 },
5356 {
5457 "unsafe-coerce": ">=6.0.0 <7.0.0"
5558 },
59+ {
60+ "untagged-union": ">=1.0.0 <2.0.0"
61+ },
5662 {
5763 "variant": ">=8.0.0 <9.0.0"
5864 },
5965 {
60− "yoga-json": ">=5.0.0 <6.0.0"
66+ "yoga-json": ">=5.2.0 <6.0.0"
6167 }
6268 ]
6369 },
@@ -69,17 +75,26 @@
6975 {
7076 "console": ">=6.0.0 <7.0.0"
7177 },
78+ {
79+ "datetime": ">=6.0.0 <7.0.0"
80+ },
7281 {
7382 "foreign-object": ">=4.0.0 <5.0.0"
7483 },
7584 {
7685 "integers": ">=6.0.0 <7.0.0"
7786 },
87+ {
88+ "refs": ">=6.0.0 <7.0.0"
89+ },
7890 {
7991 "strings": ">=6.0.0 <7.0.0"
8092 },
8193 {
8294 "vitest": ">=1.0.0 <2.0.0"
95+ },
96+ {
97+ "yoga-om": ">=1.2.2 <2.0.0"
8398 }
8499 ]
85100 }
@@ -87,14 +102,14 @@
87102 },
88103 "package_set": {
89104 "address": {
90− "registry": "51.1.0"
105+ "registry": "63.2.1"
91106 },
92107 "compiler": ">=0.15.15 <0.16.0",
93108 "content": {
94109 "abc-parser": "2.0.1",
95110 "ace": "9.1.0",
96111 "address-rfc2821": "0.1.1",
97− "aff": "7.1.0",
112+ "aff": "8.0.0",
98113 "aff-bus": "6.0.0",
99114 "aff-coroutines": "9.0.0",
100115 "aff-promise": "4.0.0",
@@ -113,7 +128,7 @@
113128 "argonaut-traversals": "10.0.0",
114129 "argparse-basic": "2.0.0",
115130 "array-builder": "0.1.2",
116− "array-search": "0.5.6",
131+ "array-search": "0.6.0",
117132 "arraybuffer": "13.2.0",
118133 "arraybuffer-builder": "3.1.0",
119134 "arraybuffer-types": "3.0.2",
@@ -122,8 +137,8 @@
122137 "arrays-zipper": "2.0.1",
123138 "ask": "1.0.0",
124139 "assert": "6.0.0",
125− "assert-multiple": "0.3.4",
126− "avar": "5.0.0",
140+ "assert-multiple": "0.4.0",
141+ "avar": "5.0.1",
127142 "b64": "0.0.8",
128143 "barbies": "1.0.1",
129144 "barlow-lens": "0.9.0",
@@ -137,7 +152,7 @@
137152 "canvas-action": "9.0.0",
138153 "cartesian": "1.0.6",
139154 "catenable-lists": "7.0.0",
140− "cbor-stream": "1.0.4",
155+ "cbor-stream": "1.3.0",
141156 "chameleon": "1.0.0",
142157 "chameleon-halogen": "1.0.3",
143158 "chameleon-react-basic": "1.1.0",
@@ -145,7 +160,7 @@
145160 "chameleon-transformers": "1.0.0",
146161 "channel": "1.0.0",
147162 "checked-exceptions": "3.1.1",
148− "choku": "1.0.1",
163+ "choku": "1.0.2",
149164 "classless": "0.1.1",
150165 "classless-arbitrary": "0.1.1",
151166 "classless-decode-json": "0.1.1",
@@ -153,7 +168,7 @@
153168 "classnames": "2.0.0",
154169 "codec": "6.1.0",
155170 "codec-argonaut": "10.0.0",
156− "codec-json": "1.2.0",
171+ "codec-json": "2.0.0",
157172 "colors": "7.0.1",
158173 "concur-core": "0.5.0",
159174 "concur-react": "0.5.0",
@@ -165,8 +180,9 @@
165180 "convertable-options": "1.0.0",
166181 "coroutines": "7.0.0",
167182 "css": "6.0.0",
183+ "css-class-name-extractor": "0.0.4",
168184 "css-frameworks": "1.0.1",
169− "csv-stream": "2.0.2",
185+ "csv-stream": "2.3.0",
170186 "data-mvc": "0.0.2",
171187 "datetime": "6.1.0",
172188 "datetime-parsing": "0.2.0",
@@ -178,32 +194,34 @@
178194 "deno": "0.0.5",
179195 "dissect": "1.0.0",
180196 "distributive": "6.0.0",
197+ "dodo-printer": "2.2.3",
181198 "dom-filereader": "7.0.0",
182199 "dom-indexed": "12.0.0",
183200 "dom-simple": "0.4.0",
184201 "dotenv": "4.0.3",
185202 "droplet": "0.6.0",
186203 "dts": "1.0.0",
187− "dual-numbers": "1.0.2",
204+ "dual-numbers": "1.0.3",
188205 "dynamic-buffer": "3.0.1",
189206 "echarts-simple": "0.0.1",
190207 "effect": "4.0.0",
191208 "either": "6.1.0",
192− "elmish": "0.11.3",
209+ "elmish": "0.13.0",
193210 "elmish-enzyme": "0.1.1",
194− "elmish-hooks": "0.10.0",
195− "elmish-html": "0.8.2",
211+ "elmish-hooks": "0.10.3",
212+ "elmish-html": "0.9.0",
196213 "elmish-testing-library": "0.3.2",
197214 "email-validate": "7.0.0",
198215 "encoding": "0.0.9",
199216 "enums": "6.0.1",
200− "env-names": "0.3.4",
217+ "env-names": "0.4.0",
201218 "error": "2.0.0",
202219 "eta-conversion": "0.3.2",
203− "exceptions": "6.0.0",
220+ "exceptions": "6.1.0",
204221 "exists": "6.0.0",
205222 "exitcodes": "4.0.0",
206223 "expect-inferred": "3.0.0",
224+ "ezfetch": "1.1.0",
207225 "fahrtwind": "2.0.0",
208226 "fallback": "0.1.0",
209227 "fast-vect": "1.2.0",
@@ -242,7 +260,7 @@
242260 "generate-values": "1.0.1",
243261 "generic-router": "0.0.1",
244262 "geojson": "0.0.5",
245− "geometry-plane": "1.0.3",
263+ "geometria": "2.2.0",
246264 "gojs": "0.1.1",
247265 "grain": "3.0.0",
248266 "grain-router": "3.0.0",
@@ -255,7 +273,7 @@
255273 "halogen-css": "10.0.0",
256274 "halogen-echarts-simple": "0.0.4",
257275 "halogen-formless": "4.0.3",
258− "halogen-helix": "1.0.0",
276+ "halogen-helix": "1.0.1",
259277 "halogen-hooks": "0.6.3",
260278 "halogen-hooks-extra": "0.9.0",
261279 "halogen-infinite-scroll": "1.1.0",
@@ -272,6 +290,7 @@
272290 "homogeneous": "0.4.0",
273291 "http-methods": "6.0.0",
274292 "httpurple": "4.0.0",
293+ "huffman": "0.4.0",
275294 "humdrum": "0.0.1",
276295 "hyrule": "2.3.8",
277296 "identity": "6.0.0",
@@ -293,19 +312,20 @@
293312 "js-date": "8.0.0",
294313 "js-fetch": "0.2.1",
295314 "js-fileio": "3.0.0",
296− "js-intl": "1.0.4",
315+ "js-intl": "1.1.4",
297316 "js-iterators": "0.1.1",
298317 "js-maps": "0.1.2",
299318 "js-promise": "1.0.0",
300319 "js-promise-aff": "1.0.0",
301320 "js-timers": "6.1.0",
302321 "js-uri": "3.1.0",
322+ "jsdom": "1.0.0",
303323 "json": "1.1.0",
304324 "json-codecs": "5.0.0",
305325 "justifill": "0.5.0",
306326 "jwt": "0.0.9",
307327 "labeled-data": "0.2.0",
308− "language-cst-parser": "0.14.0",
328+ "language-cst-parser": "0.14.1",
309329 "lazy": "6.0.0",
310330 "lazy-joe": "1.0.0",
311331 "lcg": "4.0.0",
@@ -319,7 +339,7 @@
319339 "logging-journald": "0.4.0",
320340 "lumi-components": "18.0.0",
321341 "machines": "7.0.0",
322− "maps-eager": "0.4.1",
342+ "maps-eager": "0.5.0",
323343 "marionette": "1.0.0",
324344 "marionette-react-basic-hooks": "0.1.1",
325345 "marked": "0.1.0",
@@ -355,35 +375,41 @@
355375 "node-child-process": "11.1.0",
356376 "node-event-emitter": "3.0.0",
357377 "node-execa": "5.0.0",
358− "node-fs": "9.1.0",
359− "node-glob-basic": "1.3.0",
378+ "node-fs": "9.2.0",
379+ "node-glob-basic": "2.0.0",
360380 "node-http": "9.1.0",
361381 "node-http2": "1.1.1",
362382 "node-human-signals": "1.0.0",
363383 "node-net": "5.1.0",
364384 "node-os": "5.1.0",
365− "node-path": "5.0.0",
385+ "node-path": "5.0.1",
366386 "node-process": "11.2.0",
367− "node-readline": "8.1.0",
387+ "node-readline": "8.1.1",
368388 "node-sqlite3": "8.0.0",
369− "node-stream-pipes": "1.4.1",
370− "node-streams": "9.0.0",
389+ "node-stream-pipes": "2.1.6",
390+ "node-streams": "9.0.1",
371391 "node-tls": "0.3.1",
372392 "node-url": "7.0.1",
393+ "node-workerbees": "0.3.1",
373394 "node-zlib": "0.4.0",
374395 "nonempty": "7.0.0",
375396 "now": "6.0.0",
376397 "npm-package-json": "2.0.0",
377398 "nullable": "6.0.0",
378− "numberfield": "0.1.0",
399+ "numberfield": "0.2.2",
379400 "numbers": "9.0.1",
380401 "oak": "3.1.1",
381402 "oak-debug": "1.2.2",
382403 "object-maps": "0.3.0",
383404 "ocarina": "1.5.4",
384− "open-folds": "6.3.0",
385− "open-memoize": "6.1.0",
386− "open-pairing": "6.1.0",
405+ "oooooooooorrrrrrrmm-lib": "0.0.1",
406+ "open-colors-scales-and-schemes": "1.0.0",
407+ "open-folds": "6.4.0",
408+ "open-foreign-generic": "11.0.3",
409+ "open-memoize": "6.2.0",
410+ "open-mkdirp-aff": "1.2.0",
411+ "open-pairing": "6.2.0",
412+ "open-smolder": "12.0.2",
387413 "options": "7.0.0",
388414 "optparse": "5.0.1",
389415 "ordered-collections": "3.2.0",
@@ -406,14 +432,16 @@
406432 "pointed-list": "0.5.1",
407433 "polymorphic-vectors": "4.0.0",
408434 "posix-types": "6.0.0",
409− "postgresql": "1.6.3",
435+ "postgresql": "2.0.20",
410436 "precise": "6.0.0",
411437 "precise-datetime": "7.0.0",
412− "prelude": "6.0.1",
438+ "prelude": "6.0.2",
413439 "prettier-printer": "3.0.0",
440+ "printf": "0.1.0",
441+ "priority-queue": "0.1.2",
414442 "profunctor": "6.0.1",
415443 "profunctor-lenses": "8.0.0",
416− "protobuf": "4.3.0",
444+ "protobuf": "4.4.0",
417445 "psa-utils": "8.0.0",
418446 "psci-support": "6.0.0",
419447 "punycode": "1.0.0",
@@ -431,7 +459,8 @@
431459 "react-basic": "17.0.0",
432460 "react-basic-classic": "3.0.0",
433461 "react-basic-dnd": "10.1.0",
434− "react-basic-dom": "6.1.0",
462+ "react-basic-dom": "7.0.0",
463+ "react-basic-dom-beta": "0.1.1",
435464 "react-basic-emotion": "7.1.0",
436465 "react-basic-hooks": "8.2.0",
437466 "react-basic-storybook": "2.0.0",
@@ -450,6 +479,7 @@
450479 "record-studio": "1.0.4",
451480 "refs": "6.0.0",
452481 "remotedata": "5.0.1",
482+ "repr": "0.5.0",
453483 "resize-observer": "1.0.0",
454484 "resource": "2.0.1",
455485 "resourcet": "1.0.0",
@@ -457,6 +487,7 @@
457487 "return": "0.2.0",
458488 "ring-modules": "5.0.1",
459489 "rito": "0.3.4",
490+ "roman": "0.4.0",
460491 "rough-notation": "1.0.2",
461492 "routing": "11.0.0",
462493 "routing-duplex": "0.7.0",
@@ -467,6 +498,7 @@
467498 "selection-foldable": "0.2.0",
468499 "selective-functors": "1.0.1",
469500 "semirings": "7.0.0",
501+ "shuffle": "1.1.0",
470502 "signal": "13.0.0",
471503 "simple-emitter": "3.0.1",
472504 "simple-i18n": "2.0.1",
@@ -475,14 +507,17 @@
475507 "simple-ulid": "3.0.0",
476508 "sized-matrices": "1.0.0",
477509 "sized-vectors": "5.0.2",
478− "slug": "3.0.8",
510+ "slug": "3.1.0",
479511 "small-ffi": "4.0.1",
480512 "soundfonts": "4.1.0",
481− "sparse-matrices": "1.3.0",
482− "sparse-polynomials": "2.0.5",
483− "spec": "7.6.0",
484− "spec-mocha": "5.1.0",
485− "spec-quickcheck": "5.0.0",
513+ "sparse-matrices": "2.0.1",
514+ "sparse-polynomials": "3.0.1",
515+ "spec": "8.1.1",
516+ "spec-discovery": "8.4.0",
517+ "spec-mocha": "5.1.1",
518+ "spec-node": "0.0.3",
519+ "spec-quickcheck": "5.0.2",
520+ "spec-reporter-xunit": "0.7.1",
486521 "splitmix": "2.1.0",
487522 "ssrs": "1.0.0",
488523 "st": "6.2.0",
@@ -498,15 +533,20 @@
498533 "systemd-journald": "0.3.0",
499534 "tagged": "4.0.2",
500535 "tailrec": "6.1.0",
536+ "tanstack-query": "2.0.0",
501537 "tecton": "0.2.1",
502538 "tecton-halogen": "0.2.0",
503539 "test-unit": "17.0.0",
504540 "thermite": "6.3.1",
505541 "thermite-dom": "0.3.1",
506542 "these": "6.0.0",
543+ "threading": "0.0.3",
544+ "tidy": "0.11.1",
545+ "tidy-codegen": "4.0.1",
546+ "tldr": "0.0.0",
507547 "toestand": "0.9.0",
508548 "transformation-matrix": "1.0.1",
509− "transformers": "6.0.0",
549+ "transformers": "6.1.0",
510550 "tree-rose": "4.0.2",
511551 "ts-bridge": "4.0.0",
512552 "tuples": "7.0.0",
@@ -535,6 +575,7 @@
535575 "untagged-to-tagged": "0.1.4",
536576 "untagged-union": "1.0.0",
537577 "uri": "9.0.0",
578+ "url-immutable": "1.0.0",
538579 "uuid": "9.0.0",
539580 "uuidv4": "1.0.0",
540581 "validation": "6.0.0",
@@ -544,7 +585,8 @@
544585 "vectors": "2.1.0",
545586 "versions": "7.0.0",
546587 "visx": "0.0.2",
547− "web-clipboard": "5.0.0",
588+ "vitest": "1.0.0",
589+ "web-clipboard": "6.0.0",
548590 "web-cssom": "2.0.0",
549591 "web-cssom-view": "0.1.0",
550592 "web-dom": "6.0.0",
@@ -572,30 +614,26 @@
572614 "webextension-polyfill": "0.1.0",
573615 "webgpu": "0.0.1",
574616 "which": "2.0.0",
617+ "whine-core": "0.0.28",
575618 "xterm": "1.0.0",
576619 "yoga-fetch": "1.0.1",
577620 "yoga-json": "5.1.0",
578621 "yoga-om": "0.1.0",
579622 "yoga-postgres": "6.0.0",
623+ "yoga-react-dom": "1.0.1",
624+ "yoga-subtlecrypto": "0.1.0",
580625 "yoga-tree": "1.0.0",
581626 "z3": "0.0.2",
582627 "zipperarray": "2.0.0"
583628 }
584629 },
585− "extra_packages": {
586− "vitest": {
587− "path": "../purescript-vitest"
588− },
589− "yoga-json": {
590− "path": "../purescript-yoga-json"
591− }
592− }
630+ "extra_packages": {}
593631 },
594632 "packages": {
595633 "aff": {
596634 "type": "registry",
597− "version": "7.1.0",
598− "integrity": "sha256-/nOpDQVnKIoP1pQ1ZZBYpadK0TuMlG4WwufGzJpsfD0=",
635+ "version": "8.0.0",
636+ "integrity": "sha256-9BRIrSdRoYybfiUmBqgKJ66cPqxYHb277bVWQMn/9sg=",
599637 "dependencies": [
600638 "control",
601639 "datetime",
@@ -634,6 +672,20 @@
634672 "unsafe-coerce"
635673 ]
636674 },
675+ "avar": {
676+ "type": "registry",
677+ "version": "5.0.1",
678+ "integrity": "sha256-8V4SxF4TIWZ9Ik1P4lPzIRUAZeFBe8w5WA2VcCEKsIk=",
679+ "dependencies": [
680+ "aff",
681+ "effect",
682+ "either",
683+ "exceptions",
684+ "functions",
685+ "maybe",
686+ "prelude"
687+ ]
688+ },
637689 "bifunctors": {
638690 "type": "registry",
639691 "version": "6.0.0",
@@ -773,8 +825,8 @@
773825 },
774826 "exceptions": {
775827 "type": "registry",
776− "version": "6.0.0",
777− "integrity": "sha256-z65UIKhOyVXzV8QUxtky4OiYVESNPegtuBFUdAddbTM=",
828+ "version": "6.1.0",
829+ "integrity": "sha256-nRrr+N0wk0TUFOWLR7e1n1oxcXo/X345bQUtoY9lW6A=",
778830 "dependencies": [
779831 "effect",
780832 "either",
@@ -910,6 +962,20 @@
910962 "unfoldable"
911963 ]
912964 },
965+ "heterogeneous": {
966+ "type": "registry",
967+ "version": "0.6.0",
968+ "integrity": "sha256-nQgk2iHYe5LIi6ZzREgWaCU3y8oCwdhDSukT6xVMWdk=",
969+ "dependencies": [
970+ "either",
971+ "foldable-traversable",
972+ "functors",
973+ "prelude",
974+ "record",
975+ "tuples",
976+ "variant"
977+ ]
978+ },
913979 "identity": {
914980 "type": "registry",
915981 "version": "6.0.0",
@@ -1026,6 +1092,20 @@
10261092 "unfoldable"
10271093 ]
10281094 },
1095+ "literals": {
1096+ "type": "registry",
1097+ "version": "1.0.2",
1098+ "integrity": "sha256-ZkGRFD6gb8etYvyMYpX1Z7lx1Tql7R1VGnD1Dzinup8=",
1099+ "dependencies": [
1100+ "integers",
1101+ "maybe",
1102+ "numbers",
1103+ "partial",
1104+ "prelude",
1105+ "typelevel-prelude",
1106+ "unsafe-coerce"
1107+ ]
1108+ },
10291109 "maybe": {
10301110 "type": "registry",
10311111 "version": "6.0.0",
@@ -1144,8 +1224,8 @@
11441224 },
11451225 "prelude": {
11461226 "type": "registry",
1147− "version": "6.0.1",
1148− "integrity": "sha256-qicw3g9AkjIxyVp73o1dF9BiTCMWouPk9rYe6AASBCU=",
1227+ "version": "6.0.2",
1228+ "integrity": "sha256-IqykrDRquGUD/LRSad7y75hZv3bUsGQ4knc8tdez8Qw=",
11491229 "dependencies": []
11501230 },
11511231 "profunctor": {
@@ -1173,6 +1253,19 @@
11731253 "unsafe-coerce"
11741254 ]
11751255 },
1256+ "record-studio": {
1257+ "type": "registry",
1258+ "version": "1.0.4",
1259+ "integrity": "sha256-CRnFvSXpDDzio+buibUxFU9zPZDd7/lzrYC1Ya3FuIw=",
1260+ "dependencies": [
1261+ "heterogeneous",
1262+ "lists",
1263+ "prelude",
1264+ "record",
1265+ "typelevel-prelude",
1266+ "unsafe-coerce"
1267+ ]
1268+ },
11761269 "refs": {
11771270 "type": "registry",
11781271 "version": "6.0.0",
@@ -1242,8 +1335,8 @@
12421335 },
12431336 "transformers": {
12441337 "type": "registry",
1245− "version": "6.0.0",
1246− "integrity": "sha256-Nl3yXAIeeTzP6MyrkazkK7pbgeVqUm+ynLv7U5cpJYQ=",
1338+ "version": "6.1.0",
1339+ "integrity": "sha256-QJmgNT/y7ljPHSsXaW4zxF/982BFiQ90KNL1g/NtEOQ=",
12471340 "dependencies": [
12481341 "control",
12491342 "distributive",
@@ -1256,6 +1349,7 @@
12561349 "maybe",
12571350 "newtype",
12581351 "prelude",
1352+ "st",
12591353 "tailrec",
12601354 "tuples",
12611355 "unfoldable"
@@ -1286,6 +1380,39 @@
12861380 "type-equality"
12871381 ]
12881382 },
1383+ "uncurried-transformers": {
1384+ "type": "registry",
1385+ "version": "1.1.0",
1386+ "integrity": "sha256-AqqBMzVLzozVoDorE3QcVVRDkLd7O3624mRHCDnPhaI=",
1387+ "dependencies": [
1388+ "control",
1389+ "effect",
1390+ "either",
1391+ "functions",
1392+ "identity",
1393+ "prelude",
1394+ "safe-coerce",
1395+ "tailrec",
1396+ "transformers",
1397+ "tuples"
1398+ ]
1399+ },
1400+ "undefined-is-not-a-problem": {
1401+ "type": "registry",
1402+ "version": "1.1.0",
1403+ "integrity": "sha256-0YkVzpcKi40T43go7/f8uLVIYgir6lGzOytL6LPfiog=",
1404+ "dependencies": [
1405+ "effect",
1406+ "either",
1407+ "foreign",
1408+ "maybe",
1409+ "newtype",
1410+ "prelude",
1411+ "tuples",
1412+ "type-equality",
1413+ "unsafe-coerce"
1414+ ]
1415+ },
12891416 "unfoldable": {
12901417 "type": "registry",
12911418 "version": "6.0.0",
@@ -1304,6 +1431,22 @@
13041431 "integrity": "sha256-0L1QsaY20OILjfU5TV72d3U/tSjhmL9hJ32WMp857Lk=",
13051432 "dependencies": []
13061433 },
1434+ "untagged-union": {
1435+ "type": "registry",
1436+ "version": "1.0.0",
1437+ "integrity": "sha256-MQvpb13TZEBoi9n/AoQ7ugniSQqbN6XQmAjii9pqJRk=",
1438+ "dependencies": [
1439+ "either",
1440+ "foreign",
1441+ "foreign-object",
1442+ "literals",
1443+ "maybe",
1444+ "newtype",
1445+ "prelude",
1446+ "tuples",
1447+ "unsafe-coerce"
1448+ ]
1449+ },
13071450 "variant": {
13081451 "type": "registry",
13091452 "version": "8.0.0",
@@ -1321,12 +1464,12 @@
13211464 ]
13221465 },
13231466 "vitest": {
1324− "type": "local",
1325− "path": "../purescript-vitest",
1467+ "type": "registry",
1468+ "version": "1.0.0",
1469+ "integrity": "sha256-grMOE1y+Ld5u8NZQ5OL32Rk+MzYt0DQI3Wi4L8b9hm8=",
13261470 "dependencies": [
13271471 "aff",
13281472 "effect",
1329− "either",
13301473 "exceptions",
13311474 "js-promise",
13321475 "js-promise-aff",
@@ -1429,8 +1572,9 @@
14291572 ]
14301573 },
14311574 "yoga-json": {
1432− "type": "local",
1433− "path": "../purescript-yoga-json",
1575+ "type": "registry",
1576+ "version": "5.1.0",
1577+ "integrity": "sha256-DK+KyaQ7YyWo8p3SuZPPEMoVKy6HF1iV4rGFPXgBygw=",
14341578 "dependencies": [
14351579 "arrays",
14361580 "bifunctors",
@@ -1456,7 +1600,6 @@
14561600 "partial",
14571601 "prelude",
14581602 "record",
1459− "safe-coerce",
14601603 "strings",
14611604 "transformers",
14621605 "tuples",
@@ -1466,6 +1609,35 @@
14661609 "yoga-tree"
14671610 ]
14681611 },
1612+ "yoga-om": {
1613+ "type": "registry",
1614+ "version": "0.1.0",
1615+ "integrity": "sha256-0EGWo/UOVLzZ14DPtdv0C+bTvNO49wNgtTBIXI6QHqU=",
1616+ "dependencies": [
1617+ "aff",
1618+ "avar",
1619+ "console",
1620+ "control",
1621+ "datetime",
1622+ "effect",
1623+ "either",
1624+ "exceptions",
1625+ "functions",
1626+ "maybe",
1627+ "newtype",
1628+ "parallel",
1629+ "prelude",
1630+ "record",
1631+ "record-studio",
1632+ "tailrec",
1633+ "transformers",
1634+ "tuples",
1635+ "typelevel-prelude",
1636+ "uncurried-transformers",
1637+ "unsafe-coerce",
1638+ "variant"
1639+ ]
1640+ },
14691641 "yoga-tree": {
14701642 "type": "registry",
14711643 "version": "1.0.0",
spago.yaml+15−15
@@ -1,11 +1,6 @@
11workspace:
22 packageSet:
3− registry: 51.1.0
4− extraPackages:
5− yoga-json:
6− path: ../purescript-yoga-json
7− vitest:
8− path: ../purescript-vitest
3+ registry: 63.2.1
94
105package:
116 name: yoga-http-api
@@ -16,24 +11,26 @@ package:
1611 githubOwner: rowtype-yoga
1712 githubRepo: purescript-yoga-http-api
1813 dependencies:
19− - aff: ">=7.0.0 <8.0.0"
20− - arrays: ">=7.0.0 <8.0.0"
21− - effect: ">=4.0.0 <5.0.0"
22− - either: ">=6.0.0 <7.0.0"
14+ - aff: ">=7.1.0 <8.0.0"
15+ - arrays: ">=7.3.0 <8.0.0"
16+ - either: ">=6.1.0 <7.0.0"
2317 - foldable-traversable: ">=6.0.0 <7.0.0"
2418 - foreign: ">=7.0.0 <8.0.0"
25− - foreign-object: ">=4.0.0 <5.0.0"
19+ - foreign-object: ">=4.1.0 <5.0.0"
20+ - identity: ">=6.0.0 <7.0.0"
2621 - integers: ">=6.0.0 <7.0.0"
22+ - literals: ">=1.0.2 <2.0.0"
2723 - maybe: ">=6.0.0 <7.0.0"
2824 - newtype: ">=5.0.0 <6.0.0"
29− - prelude: ">=6.0.0 <7.0.0"
25+ - prelude: ">=6.0.1 <7.0.0"
3026 - record: ">=4.0.0 <5.0.0"
31− - strings: ">=6.0.0 <7.0.0"
27+ - strings: ">=6.0.1 <7.0.0"
3228 - tuples: ">=7.0.0 <8.0.0"
33− - typelevel-prelude: ">=7.0.0 <8.0.0"
29+ - undefined-is-not-a-problem: ">=1.1.0 <2.0.0"
3430 - unsafe-coerce: ">=6.0.0 <7.0.0"
31+ - untagged-union: ">=1.0.0 <2.0.0"
3532 - variant: ">=8.0.0 <9.0.0"
36− - yoga-json: ">=5.0.0 <6.0.0"
33+ - yoga-json: ">=5.2.0 <6.0.0"
3734 test:
3835 main: Test.Operators
3936 dependencies:
@@ -43,3 +40,6 @@ package:
4340 - arrays: ">=7.0.0 <8.0.0"
4441 - strings: ">=6.0.0 <7.0.0"
4542 - integers: ">=6.0.0 <7.0.0"
43+ - yoga-om: ">=1.2.2 <2.0.0"
44+ - refs: ">=6.0.0 <7.0.0"
45+ - datetime: ">=6.0.0 <7.0.0"
src/Yoga/HTTP/API/Route.purs+3−1
@@ -13,13 +13,14 @@ module Yoga.HTTP.API.Route
1313 , module Yoga.HTTP.API.Route.OpenAPIMetadata
1414 , module Yoga.HTTP.API.Route.Route
1515 , module Yoga.HTTP.API.Route.RouteHandler
16+ , module Yoga.HTTP.API.UnionTrick
1617 ) where
1718
1819import Yoga.HTTP.API.Path (Path, Root, Lit, Capture, PathCons, Param, QueryParams, Required, type (/), type (:), type (:?), class PathPattern, pathPattern, class ParseParam, parseParam, class ParsePath, parsePath)
1920import Yoga.HTTP.API.Route.Auth (BearerToken(..), BasicAuth(..), ApiKeyHeader(..), ApiKeyCookie(..), DigestAuth(..))
2021import Yoga.HTTP.API.Route.BearerToken (BearerToken(..))
2122import Yoga.HTTP.API.Route.Encoding (JSON, FormData, MultipartFormData, PlainText, XML, CustomContentType, NoBody)
22−import Yoga.HTTP.API.Route.Handler (HandlerFn, Request)
23+import Yoga.HTTP.API.Route.Handler (HandlerFn, NoRequest)
2324import Yoga.HTTP.API.Route.HeaderError (HeaderError(..))
2425import Yoga.HTTP.API.Route.HeaderValue (class HeaderValue, class HeaderValueType, headerValueType, parseHeader, printHeader)
2526import Yoga.HTTP.API.Route.Method (DELETE, GET, PATCH, POST, PUT)
@@ -29,3 +30,4 @@ import Yoga.HTTP.API.Route.Response (Response(..), ResponseData, respondNoHeader
2930import Yoga.HTTP.API.Route.Route (Route(..), class ConvertResponseVariant, class ConvertResponseVariantRL)
3031import Yoga.HTTP.API.Route.RouteHandler (Handler, class RouteHandler, mkHandler, runHandler, class APIHandlers, apiHandlers, class ApiRecord)
3132import Yoga.HTTP.API.Route.StatusCode (StatusCode(..), class StatusCodeMap, statusCodeFor, statusCodeToString)
33+import Yoga.HTTP.API.UnionTrick (class CoerceConfig, coerceConfig, class CoerceToUor, coerceToUor, uorToMaybe, UndefinedOr)
src/Yoga/HTTP/API/Route/Handler.purs+14−15
@@ -1,6 +1,6 @@
11module Yoga.HTTP.API.Route.Handler
22 ( HandlerFn
3− , Request(..)
3+ , NoRequest
44 , class DefaultRequestFields
55 , class DefaultRequestFieldsRL
66 , class SegmentPathParams
@@ -23,18 +23,17 @@ import Yoga.HTTP.API.Path (Path, PathCons, Capture, Param, QueryParams, Required
2323import Yoga.HTTP.API.Route.Encoding (JSON, NoBody)
2424
2525--------------------------------------------------------------------------------
26−-- Request Data Type
26+-- NoRequest: Type alias for empty request (no headers, cookies, or body)
2727--------------------------------------------------------------------------------
2828
29−-- | Request data combining headers and body
29+-- | Type alias for routes with no request data (no headers, cookies, or body).
3030-- |
3131-- | Usage:
32−-- | Request { body :: JSON User } -- body only
33−-- | Request { headers :: { auth :: String } } -- headers only
34−-- | Request { body :: JSON User, headers :: { auth :: String } } -- both
35−-- | Request {} -- neither
36−data Request :: Type -> Type
37−data Request r = Request r
32+-- | Route GET "health" NoRequest (ok :: { body :: String })
33+-- |
34+-- | Equivalent to:
35+-- | Route GET "health" {} (ok :: { body :: String })
36+type NoRequest = Record ()
3837
3938--------------------------------------------------------------------------------
4039-- Handler Type
@@ -162,7 +161,7 @@ instance encodingBodyNoBody :: EncodingBody NoBody Unit
162161-- RequestHeaders: Extract headers row from a request type
163162--------------------------------------------------------------------------------
164163
165−-- | Extract the headers row from a Request type.
164+-- | Extract the headers row from a request record type.
166165-- |
167166-- | The request is expected to have a `headers` field.
168167class RequestHeaders (request :: Type) (headers :: Row Type) | request -> headers
@@ -170,13 +169,13 @@ class RequestHeaders (request :: Type) (headers :: Row Type) | request -> header
170169instance requestHeadersRequest ::
171170 ( Row.Cons "headers" (Record headers) _rest requestRow
172171 ) =>
173− RequestHeaders (Request (Record requestRow)) headers
172+ RequestHeaders (Record requestRow) headers
174173
175174--------------------------------------------------------------------------------
176175-- RequestCookies: Extract cookies row from a request type
177176--------------------------------------------------------------------------------
178177
179−-- | Extract the cookies row from a Request type.
178+-- | Extract the cookies row from a request record type.
180179-- |
181180-- | The request is expected to have a `cookies` field.
182181class RequestCookies (request :: Type) (cookies :: Row Type) | request -> cookies
@@ -184,13 +183,13 @@ class RequestCookies (request :: Type) (cookies :: Row Type) | request -> cookie
184183instance requestCookiesRequest ::
185184 ( Row.Cons "cookies" (Record cookies) _rest requestRow
186185 ) =>
187− RequestCookies (Request (Record requestRow)) cookies
186+ RequestCookies (Record requestRow) cookies
188187
189188--------------------------------------------------------------------------------
190189-- RequestBody: Extract body encoding from a request type
191190--------------------------------------------------------------------------------
192191
193−-- | Extract the body encoding type from a Request type.
192+-- | Extract the body encoding type from a request record type.
194193-- |
195194-- | The request is expected to have a `body` field.
196195class RequestBody (request :: Type) (encoding :: Type) | request -> encoding
@@ -198,7 +197,7 @@ class RequestBody (request :: Type) (encoding :: Type) | request -> encoding
198197instance requestBodyRequest ::
199198 ( Row.Cons "body" encoding _rest requestRow
200199 ) =>
201− RequestBody (Request (Record requestRow)) encoding
200+ RequestBody (Record requestRow) encoding
202201
203202--------------------------------------------------------------------------------
204203-- DefaultRequestFields: Compute defaults for missing request fields
src/Yoga/HTTP/API/Route/OpenAPI.purs+67−59
@@ -82,6 +82,9 @@ import Data.Array as Array
8282import Data.Foldable (foldl)
8383import Data.Maybe (Maybe(..), maybe)
8484import Data.String as String
85+import Data.Identity (Identity)
86+import Data.Undefined.NoProblem (Opt, toMaybe, undefined)
87+import Yoga.HTTP.API.UnionTrick (UndefinedOr, uorToMaybe, class CoerceFields, coerceFields)
8588import Data.String.Regex (replace)
8689import Data.String.Regex.Flags (global)
8790import Data.String.Regex.Unsafe (unsafeRegex)
@@ -97,7 +100,7 @@ import Unsafe.Coerce (unsafeCoerce)
97100import Yoga.HTTP.API.Route.Auth (BearerToken, BasicAuth, ApiKeyHeader, ApiKeyCookie, DigestAuth)
98101import Yoga.HTTP.API.Route.Encoding (JSON, FormData, MultipartFormData, PlainText, XML, CustomContentType, NoBody)
99102import Yoga.HTTP.API.Route.HeaderValue (class HeaderValueType, headerValueType)
100−import Yoga.HTTP.API.Route.OpenAPIMetadata (Description, Example, Format, Minimum, Maximum, Pattern, MinLength, MaxLength, Title, Nullable, Default, Deprecated, Enum, Schema, Callback, Examples, Link, class HasDescription, description, class HasExample, example, class HasFormat, format, class HasDeprecated, deprecated, class HasMinimum, minimum, class HasMaximum, maximum, class HasPattern, pattern, class HasMinLength, minLength, class HasMaxLength, maxLength, class HasTitle, title, class HasNullable, nullable, class HasDefault, default, class HasEnum, enum, class HasExamples, examples, class HasOperationMetadata, operationMetadata, class HasLinks, links)
103+import Yoga.HTTP.API.Route.OpenAPIMetadata (Description, Example, Format, Minimum, Maximum, Pattern, MinLength, MaxLength, Title, Nullable, Default, Deprecated, Enum, Schema, Callback, Examples, class HasDescription, description, class HasExample, example, class HasFormat, format, class HasDeprecated, deprecated, class HasMinimum, minimum, class HasMaximum, maximum, class HasPattern, pattern, class HasMinLength, minLength, class HasMaxLength, maxLength, class HasTitle, title, class HasNullable, nullable, class HasDefault, default, class HasEnum, enum, class HasExamples, examples)
101104import Yoga.HTTP.API.Route.RenderMethod (class RenderMethod, renderMethod)
102105import Yoga.HTTP.API.Route.Response (class ToResponse)
103106import Yoga.HTTP.API.Route.StatusCode (class StatusCodeMap, statusCodeFor, statusCodeToString)
@@ -770,6 +773,7 @@ else instance
770773--------------------------------------------------------------------------------
771774
772775-- | Render a PureScript type as an OpenAPI JSON schema
776+class RenderJSONSchema :: forall k. k -> Constraint
773777class RenderJSONSchema ty where
774778 renderJSONSchema :: Proxy ty -> Foreign
775779
@@ -1891,35 +1895,67 @@ type ServerObject =
18911895 , description :: Maybe String
18921896 }
18931897
1898+-- | Row type for contact information (all fields will be coerced)
1899+type ContactInfoR =
1900+ ( name :: String
1901+ , url :: String
1902+ , email :: String
1903+ )
1904+
1905+-- | Contact info record
1906+type ContactInfo = { | ContactInfoR }
1907+
1908+-- | Row type for license information
1909+type LicenseInfoR =
1910+ ( name :: String
1911+ , url :: String
1912+ )
1913+
1914+-- | License info record
1915+type LicenseInfo = { | LicenseInfoR }
1916+
1917+-- | Row type for OpenAPI info object
1918+type OpenAPIInfoR f =
1919+ ( title :: String
1920+ , version :: String
1921+ , description :: f String
1922+ , contact :: f ContactInfo
1923+ , license :: f LicenseInfo
1924+ )
1925+
1926+-- | OpenAPI info with all required fields
1927+type OpenAPIInfo = { | OpenAPIInfoR Identity }
1928+
1929+-- | OpenAPI info with optional fields (using UndefinedOr)
1930+type OpenAPIInfoUor = { | OpenAPIInfoR UndefinedOr }
1931+
18941932-- | Build a complete OpenAPI 3.0 spec from a type-level collection of routes.
18951933-- |
18961934-- | Examples:
1897−-- | -- Basic usage:
1935+-- | -- Basic usage (omit all optional fields):
18981936-- | buildOpenAPISpec @MyAPI
18991937-- | { title: "My API", version: "1.0.0" }
19001938-- |
1901−-- | -- With servers:
1902−-- | buildOpenAPISpec' @MyAPI
1939+-- | -- With optional fields:
1940+-- | buildOpenAPISpec @MyAPI
19031941-- | { title: "My API"
19041942-- | , version: "1.0.0"
1905−-- | , servers: Just
1906−-- | [ { url: "https://api.example.com", description: Just "Production server" }
1907−-- | , { url: "https://staging-api.example.com", description: Just "Staging server" }
1908−-- | ]
1943+-- | , description: "A comprehensive API"
1944+-- | , contact: { name: "Support", email: "api@example.com" }
19091945-- | }
1946+-- |
1947+-- | Note: Uses the CoerceFields typeclass for type-safe coercion of optional fields.
1948+-- | For nested records (contact/license), use `undefined` from Literals.Undefined for optional fields.
1949+-- | The typeclass verifies at compile-time that each field can be safely coerced.
19101950buildOpenAPISpec
1911− :: forall @routes
1951+ :: forall @routes r
19121952 . CollectOperations routes
19131953 => CollectRouteSchemas routes
19141954 => ValidateSchemaNames routes
1915− => { title :: String
1916− , version :: String
1917− , description :: Maybe String
1918− , contact :: Maybe { name :: Maybe String, url :: Maybe String, email :: Maybe String }
1919− , license :: Maybe { name :: String, url :: Maybe String }
1920− }
1955+ => CoerceFields r (OpenAPIInfoR UndefinedOr)
1956+ => { | r }
19211957 -> OpenAPISpec
1922−buildOpenAPISpec info = buildOpenAPISpec' @routes info { servers: Nothing }
1958+buildOpenAPISpec given = buildOpenAPISpec' @routes (coerceFields given) { servers: undefined }
19231959
19241960-- | Build a complete OpenAPI 3.0 spec with optional servers configuration.
19251961buildOpenAPISpec'
@@ -1927,13 +1963,8 @@ buildOpenAPISpec'
19271963 . CollectOperations routes
19281964 => CollectRouteSchemas routes
19291965 => ValidateSchemaNames routes
1930− => { title :: String
1931− , version :: String
1932− , description :: Maybe String
1933− , contact :: Maybe { name :: Maybe String, url :: Maybe String, email :: Maybe String }
1934− , license :: Maybe { name :: String, url :: Maybe String }
1935− }
1936− -> { servers :: Maybe (Array ServerObject) }
1966+ => OpenAPIInfoUor
1967+ -> { servers :: Opt (Array ServerObject) }
19371968 -> OpenAPISpec
19381969buildOpenAPISpec' info config =
19391970 let
@@ -1954,48 +1985,25 @@ buildOpenAPISpec' info config =
19541985 [ Tuple "title" (writeImpl info.title)
19551986 , Tuple "version" (writeImpl info.version)
19561987 ]
1957− infoWithDescription = case info.description of
1958− Nothing -> infoBase
1959− Just desc -> FObject.insert "description" (writeImpl desc) infoBase
1960− infoWithContact = case info.contact of
1961− Nothing -> infoWithDescription
1962− Just contact ->
1963− let
1964− contactBase = FObject.empty
1965− withName = case contact.name of
1966− Nothing -> contactBase
1967− Just name -> FObject.insert "name" (writeImpl name) contactBase
1968− withUrl = case contact.url of
1969− Nothing -> withName
1970− Just url -> FObject.insert "url" (writeImpl url) withName
1971− withEmail = case contact.email of
1972− Nothing -> withUrl
1973− Just email -> FObject.insert "email" (writeImpl email) withUrl
1974− in
1975− if FObject.isEmpty withEmail then infoWithDescription
1976− else FObject.insert "contact" (unsafeCoerce withEmail) infoWithDescription
1977− infoWithLicense = case info.license of
1978− Nothing -> infoWithContact
1979− Just license ->
1980− let
1981− licenseBase = FObject.singleton "name" (writeImpl license.name)
1982− licenseWithUrl = case license.url of
1983− Nothing -> licenseBase
1984− Just url -> FObject.insert "url" (writeImpl url) licenseBase
1985− in
1986− FObject.insert "license" (unsafeCoerce licenseWithUrl) infoWithContact
1988+ infoWithDescription = maybe infoBase (\desc -> FObject.insert "description" (writeImpl desc) infoBase) (uorToMaybe info.description)
1989+ infoWithContact = maybe infoWithDescription
1990+ ( \contact ->
1991+ FObject.insert "contact" (writeImpl contact) infoWithDescription
1992+ )
1993+ (uorToMaybe info.contact)
1994+ infoWithLicense = maybe infoWithContact
1995+ ( \license ->
1996+ FObject.insert "license" (writeImpl license) infoWithContact
1997+ )
1998+ (uorToMaybe info.license)
19871999
19882000 baseSpec = FObject.fromFoldable
19892001 [ Tuple "openapi" (unsafeCoerce "3.0.0")
19902002 , Tuple "info" (unsafeCoerce infoWithLicense)
19912003 , Tuple "paths" (unsafeCoerce paths)
19922004 ]
1993− withComponents = case components of
1994− Nothing -> baseSpec
1995− Just c -> FObject.insert "components" c baseSpec
1996− withServers = case config.servers of
1997− Nothing -> withComponents
1998− Just servers -> FObject.insert "servers" (writeImpl servers) withComponents
2005+ withComponents = maybe baseSpec (\c -> FObject.insert "components" c baseSpec) components
2006+ withServers = maybe withComponents (\servers -> FObject.insert "servers" (writeImpl servers) withComponents) (toMaybe config.servers)
19992007 in
20002008 unsafeCoerce $ withServers
20012009
src/Yoga/HTTP/API/Route/RenderMethod.purs+1−1
@@ -3,7 +3,7 @@ module Yoga.HTTP.API.Route.RenderMethod
33 , renderMethod
44 ) where
55
6−import Type.Proxy (Proxy(..))
6+import Type.Proxy (Proxy)
77import Yoga.HTTP.API.Route.Method (DELETE, GET, PATCH, POST, PUT)
88
99--------------------------------------------------------------------------------
src/Yoga/HTTP/API/Route/Response.purs+186−62
@@ -378,250 +378,374 @@ respondNotModified =
378378-- Status Code Helper Functions
379379--------------------------------------------------------------------------------
380380
381+continue :: forall body r1. body -> Variant (continue :: Response () body | r1)
381382continue = respondNoHeaders @"continue"
382383
383−continueWith = respondWith (Proxy :: Proxy "continue")
384+continueWith :: forall headers body r1. Record headers -> body -> Variant (continue :: Response headers body | r1)
385+continueWith headers body = respondWith (Proxy :: Proxy "continue") headers body
384386
387+switchingProtocols :: forall body r1. body -> Variant (switchingProtocols :: Response () body | r1)
385388switchingProtocols = respondNoHeaders @"switchingProtocols"
386389
387−switchingProtocolsWith = respondWith (Proxy :: Proxy "switchingProtocols")
390+switchingProtocolsWith :: forall headers body r1. Record headers -> body -> Variant (switchingProtocols :: Response headers body | r1)
391+switchingProtocolsWith headers body = respondWith (Proxy :: Proxy "switchingProtocols") headers body
388392
393+processing :: forall body r1. body -> Variant (processing :: Response () body | r1)
389394processing = respondNoHeaders @"processing"
390395
391−processingWith = respondWith (Proxy :: Proxy "processing")
396+processingWith :: forall headers body r1. Record headers -> body -> Variant (processing :: Response headers body | r1)
397+processingWith headers body = respondWith (Proxy :: Proxy "processing") headers body
392398
399+earlyHints :: forall body r1. body -> Variant (earlyHints :: Response () body | r1)
393400earlyHints = respondNoHeaders @"earlyHints"
394401
395−earlyHintsWith = respondWith (Proxy :: Proxy "earlyHints")
402+earlyHintsWith :: forall headers body r1. Record headers -> body -> Variant (earlyHints :: Response headers body | r1)
403+earlyHintsWith headers body = respondWith (Proxy :: Proxy "earlyHints") headers body
396404
405+ok :: forall body r1. body -> Variant (ok :: Response () body | r1)
397406ok = respondNoHeaders @"ok"
398407
399−okWith = respondWith (Proxy :: Proxy "ok")
408+okWith :: forall headers body r1. Record headers -> body -> Variant (ok :: Response headers body | r1)
409+okWith headers body = respondWith (Proxy :: Proxy "ok") headers body
400410
411+created :: forall body r1. body -> Variant (created :: Response () body | r1)
401412created = respondNoHeaders @"created"
402413
403−createdWith = respondWith (Proxy :: Proxy "created")
414+createdWith :: forall headers body r1. Record headers -> body -> Variant (created :: Response headers body | r1)
415+createdWith headers body = respondWith (Proxy :: Proxy "created") headers body
404416
417+accepted :: forall body r1. body -> Variant (accepted :: Response () body | r1)
405418accepted = respondNoHeaders @"accepted"
406419
407−acceptedWith = respondWith (Proxy :: Proxy "accepted")
420+acceptedWith :: forall headers body r1. Record headers -> body -> Variant (accepted :: Response headers body | r1)
421+acceptedWith headers body = respondWith (Proxy :: Proxy "accepted") headers body
408422
423+nonAuthoritativeInformation :: forall body r1. body -> Variant (nonAuthoritativeInformation :: Response () body | r1)
409424nonAuthoritativeInformation = respondNoHeaders @"nonAuthoritativeInformation"
410425
411−nonAuthoritativeInformationWith = respondWith (Proxy :: Proxy "nonAuthoritativeInformation")
426+nonAuthoritativeInformationWith :: forall headers body r1. Record headers -> body -> Variant (nonAuthoritativeInformation :: Response headers body | r1)
427+nonAuthoritativeInformationWith headers body = respondWith (Proxy :: Proxy "nonAuthoritativeInformation") headers body
412428
429+noContent :: forall body r1. body -> Variant (noContent :: Response () body | r1)
413430noContent = respondNoHeaders @"noContent"
414431
415−noContentWith = respondWith (Proxy :: Proxy "noContent")
432+noContentWith :: forall headers body r1. Record headers -> body -> Variant (noContent :: Response headers body | r1)
433+noContentWith headers body = respondWith (Proxy :: Proxy "noContent") headers body
416434
435+resetContent :: forall body r1. body -> Variant (resetContent :: Response () body | r1)
417436resetContent = respondNoHeaders @"resetContent"
418437
419−resetContentWith = respondWith (Proxy :: Proxy "resetContent")
438+resetContentWith :: forall headers body r1. Record headers -> body -> Variant (resetContent :: Response headers body | r1)
439+resetContentWith headers body = respondWith (Proxy :: Proxy "resetContent") headers body
420440
441+partialContent :: forall body r1. body -> Variant (partialContent :: Response () body | r1)
421442partialContent = respondNoHeaders @"partialContent"
422443
423−partialContentWith = respondWith (Proxy :: Proxy "partialContent")
444+partialContentWith :: forall headers body r1. Record headers -> body -> Variant (partialContent :: Response headers body | r1)
445+partialContentWith headers body = respondWith (Proxy :: Proxy "partialContent") headers body
424446
447+multiStatus :: forall body r1. body -> Variant (multiStatus :: Response () body | r1)
425448multiStatus = respondNoHeaders @"multiStatus"
426449
427−multiStatusWith = respondWith (Proxy :: Proxy "multiStatus")
450+multiStatusWith :: forall headers body r1. Record headers -> body -> Variant (multiStatus :: Response headers body | r1)
451+multiStatusWith headers body = respondWith (Proxy :: Proxy "multiStatus") headers body
428452
453+alreadyReported :: forall body r1. body -> Variant (alreadyReported :: Response () body | r1)
429454alreadyReported = respondNoHeaders @"alreadyReported"
430455
431−alreadyReportedWith = respondWith (Proxy :: Proxy "alreadyReported")
456+alreadyReportedWith :: forall headers body r1. Record headers -> body -> Variant (alreadyReported :: Response headers body | r1)
457+alreadyReportedWith headers body = respondWith (Proxy :: Proxy "alreadyReported") headers body
432458
459+imUsed :: forall body r1. body -> Variant (imUsed :: Response () body | r1)
433460imUsed = respondNoHeaders @"imUsed"
434461
435−imUsedWith = respondWith (Proxy :: Proxy "imUsed")
462+imUsedWith :: forall headers body r1. Record headers -> body -> Variant (imUsed :: Response headers body | r1)
463+imUsedWith headers body = respondWith (Proxy :: Proxy "imUsed") headers body
436464
465+multipleChoices :: forall body r1. body -> Variant (multipleChoices :: Response () body | r1)
437466multipleChoices = respondNoHeaders @"multipleChoices"
438467
439−multipleChoicesWith = respondWith (Proxy :: Proxy "multipleChoices")
468+multipleChoicesWith :: forall headers body r1. Record headers -> body -> Variant (multipleChoices :: Response headers body | r1)
469+multipleChoicesWith headers body = respondWith (Proxy :: Proxy "multipleChoices") headers body
440470
471+movedPermanently :: forall body r1. body -> Variant (movedPermanently :: Response () body | r1)
441472movedPermanently = respondNoHeaders @"movedPermanently"
442473
443−movedPermanentlyWith = respondWith (Proxy :: Proxy "movedPermanently")
474+movedPermanentlyWith :: forall headers body r1. Record headers -> body -> Variant (movedPermanently :: Response headers body | r1)
475+movedPermanentlyWith headers body = respondWith (Proxy :: Proxy "movedPermanently") headers body
444476
477+found :: forall body r1. body -> Variant (found :: Response () body | r1)
445478found = respondNoHeaders @"found"
446479
447−foundWith = respondWith (Proxy :: Proxy "found")
480+foundWith :: forall headers body r1. Record headers -> body -> Variant (found :: Response headers body | r1)
481+foundWith headers body = respondWith (Proxy :: Proxy "found") headers body
448482
483+seeOther :: forall body r1. body -> Variant (seeOther :: Response () body | r1)
449484seeOther = respondNoHeaders @"seeOther"
450485
451−seeOtherWith = respondWith (Proxy :: Proxy "seeOther")
486+seeOtherWith :: forall headers body r1. Record headers -> body -> Variant (seeOther :: Response headers body | r1)
487+seeOtherWith headers body = respondWith (Proxy :: Proxy "seeOther") headers body
452488
489+notModified :: forall body r1. body -> Variant (notModified :: Response () body | r1)
453490notModified = respondNoHeaders @"notModified"
454491
455−notModifiedWith = respondWith (Proxy :: Proxy "notModified")
492+notModifiedWith :: forall headers body r1. Record headers -> body -> Variant (notModified :: Response headers body | r1)
493+notModifiedWith headers body = respondWith (Proxy :: Proxy "notModified") headers body
456494
495+useProxy :: forall body r1. body -> Variant (useProxy :: Response () body | r1)
457496useProxy = respondNoHeaders @"useProxy"
458497
459−useProxyWith = respondWith (Proxy :: Proxy "useProxy")
498+useProxyWith :: forall headers body r1. Record headers -> body -> Variant (useProxy :: Response headers body | r1)
499+useProxyWith headers body = respondWith (Proxy :: Proxy "useProxy") headers body
460500
501+temporaryRedirect :: forall body r1. body -> Variant (temporaryRedirect :: Response () body | r1)
461502temporaryRedirect = respondNoHeaders @"temporaryRedirect"
462503
463−temporaryRedirectWith = respondWith (Proxy :: Proxy "temporaryRedirect")
504+temporaryRedirectWith :: forall headers body r1. Record headers -> body -> Variant (temporaryRedirect :: Response headers body | r1)
505+temporaryRedirectWith headers body = respondWith (Proxy :: Proxy "temporaryRedirect") headers body
464506
507+permanentRedirect :: forall body r1. body -> Variant (permanentRedirect :: Response () body | r1)
465508permanentRedirect = respondNoHeaders @"permanentRedirect"
466509
467−permanentRedirectWith = respondWith (Proxy :: Proxy "permanentRedirect")
510+permanentRedirectWith :: forall headers body r1. Record headers -> body -> Variant (permanentRedirect :: Response headers body | r1)
511+permanentRedirectWith headers body = respondWith (Proxy :: Proxy "permanentRedirect") headers body
468512
513+badRequest :: forall body r1. body -> Variant (badRequest :: Response () body | r1)
469514badRequest = respondNoHeaders @"badRequest"
470515
471−badRequestWith = respondWith (Proxy :: Proxy "badRequest")
516+badRequestWith :: forall headers body r1. Record headers -> body -> Variant (badRequest :: Response headers body | r1)
517+badRequestWith headers body = respondWith (Proxy :: Proxy "badRequest") headers body
472518
519+unauthorized :: forall body r1. body -> Variant (unauthorized :: Response () body | r1)
473520unauthorized = respondNoHeaders @"unauthorized"
474521
475−unauthorizedWith = respondWith (Proxy :: Proxy "unauthorized")
522+unauthorizedWith :: forall headers body r1. Record headers -> body -> Variant (unauthorized :: Response headers body | r1)
523+unauthorizedWith headers body = respondWith (Proxy :: Proxy "unauthorized") headers body
476524
525+paymentRequired :: forall body r1. body -> Variant (paymentRequired :: Response () body | r1)
477526paymentRequired = respondNoHeaders @"paymentRequired"
478527
479−paymentRequiredWith = respondWith (Proxy :: Proxy "paymentRequired")
528+paymentRequiredWith :: forall headers body r1. Record headers -> body -> Variant (paymentRequired :: Response headers body | r1)
529+paymentRequiredWith headers body = respondWith (Proxy :: Proxy "paymentRequired") headers body
480530
531+forbidden :: forall body r1. body -> Variant (forbidden :: Response () body | r1)
481532forbidden = respondNoHeaders @"forbidden"
482533
483−forbiddenWith = respondWith (Proxy :: Proxy "forbidden")
534+forbiddenWith :: forall headers body r1. Record headers -> body -> Variant (forbidden :: Response headers body | r1)
535+forbiddenWith headers body = respondWith (Proxy :: Proxy "forbidden") headers body
484536
537+notFound :: forall body r1. body -> Variant (notFound :: Response () body | r1)
485538notFound = respondNoHeaders @"notFound"
486539
487−notFoundWith = respondWith (Proxy :: Proxy "notFound")
540+notFoundWith :: forall headers body r1. Record headers -> body -> Variant (notFound :: Response headers body | r1)
541+notFoundWith headers body = respondWith (Proxy :: Proxy "notFound") headers body
488542
543+methodNotAllowed :: forall body r1. body -> Variant (methodNotAllowed :: Response () body | r1)
489544methodNotAllowed = respondNoHeaders @"methodNotAllowed"
490545
491−methodNotAllowedWith = respondWith (Proxy :: Proxy "methodNotAllowed")
546+methodNotAllowedWith :: forall headers body r1. Record headers -> body -> Variant (methodNotAllowed :: Response headers body | r1)
547+methodNotAllowedWith headers body = respondWith (Proxy :: Proxy "methodNotAllowed") headers body
492548
549+notAcceptable :: forall body r1. body -> Variant (notAcceptable :: Response () body | r1)
493550notAcceptable = respondNoHeaders @"notAcceptable"
494551
495−notAcceptableWith = respondWith (Proxy :: Proxy "notAcceptable")
552+notAcceptableWith :: forall headers body r1. Record headers -> body -> Variant (notAcceptable :: Response headers body | r1)
553+notAcceptableWith headers body = respondWith (Proxy :: Proxy "notAcceptable") headers body
496554
555+proxyAuthenticationRequired :: forall body r1. body -> Variant (proxyAuthenticationRequired :: Response () body | r1)
497556proxyAuthenticationRequired = respondNoHeaders @"proxyAuthenticationRequired"
498557
499−proxyAuthenticationRequiredWith = respondWith (Proxy :: Proxy "proxyAuthenticationRequired")
558+proxyAuthenticationRequiredWith :: forall headers body r1. Record headers -> body -> Variant (proxyAuthenticationRequired :: Response headers body | r1)
559+proxyAuthenticationRequiredWith headers body = respondWith (Proxy :: Proxy "proxyAuthenticationRequired") headers body
500560
561+requestTimeout :: forall body r1. body -> Variant (requestTimeout :: Response () body | r1)
501562requestTimeout = respondNoHeaders @"requestTimeout"
502563
503−requestTimeoutWith = respondWith (Proxy :: Proxy "requestTimeout")
564+requestTimeoutWith :: forall headers body r1. Record headers -> body -> Variant (requestTimeout :: Response headers body | r1)
565+requestTimeoutWith headers body = respondWith (Proxy :: Proxy "requestTimeout") headers body
504566
567+conflict :: forall body r1. body -> Variant (conflict :: Response () body | r1)
505568conflict = respondNoHeaders @"conflict"
506569
507−conflictWith = respondWith (Proxy :: Proxy "conflict")
570+conflictWith :: forall headers body r1. Record headers -> body -> Variant (conflict :: Response headers body | r1)
571+conflictWith headers body = respondWith (Proxy :: Proxy "conflict") headers body
508572
573+gone :: forall body r1. body -> Variant (gone :: Response () body | r1)
509574gone = respondNoHeaders @"gone"
510575
511−goneWith = respondWith (Proxy :: Proxy "gone")
576+goneWith :: forall headers body r1. Record headers -> body -> Variant (gone :: Response headers body | r1)
577+goneWith headers body = respondWith (Proxy :: Proxy "gone") headers body
512578
579+lengthRequired :: forall body r1. body -> Variant (lengthRequired :: Response () body | r1)
513580lengthRequired = respondNoHeaders @"lengthRequired"
514581
515−lengthRequiredWith = respondWith (Proxy :: Proxy "lengthRequired")
582+lengthRequiredWith :: forall headers body r1. Record headers -> body -> Variant (lengthRequired :: Response headers body | r1)
583+lengthRequiredWith headers body = respondWith (Proxy :: Proxy "lengthRequired") headers body
516584
585+preconditionFailed :: forall body r1. body -> Variant (preconditionFailed :: Response () body | r1)
517586preconditionFailed = respondNoHeaders @"preconditionFailed"
518587
519−preconditionFailedWith = respondWith (Proxy :: Proxy "preconditionFailed")
588+preconditionFailedWith :: forall headers body r1. Record headers -> body -> Variant (preconditionFailed :: Response headers body | r1)
589+preconditionFailedWith headers body = respondWith (Proxy :: Proxy "preconditionFailed") headers body
520590
591+payloadTooLarge :: forall body r1. body -> Variant (payloadTooLarge :: Response () body | r1)
521592payloadTooLarge = respondNoHeaders @"payloadTooLarge"
522593
523−payloadTooLargeWith = respondWith (Proxy :: Proxy "payloadTooLarge")
594+payloadTooLargeWith :: forall headers body r1. Record headers -> body -> Variant (payloadTooLarge :: Response headers body | r1)
595+payloadTooLargeWith headers body = respondWith (Proxy :: Proxy "payloadTooLarge") headers body
524596
597+uriTooLong :: forall body r1. body -> Variant (uriTooLong :: Response () body | r1)
525598uriTooLong = respondNoHeaders @"uriTooLong"
526599
527−uriTooLongWith = respondWith (Proxy :: Proxy "uriTooLong")
600+uriTooLongWith :: forall headers body r1. Record headers -> body -> Variant (uriTooLong :: Response headers body | r1)
601+uriTooLongWith headers body = respondWith (Proxy :: Proxy "uriTooLong") headers body
528602
603+unsupportedMediaType :: forall body r1. body -> Variant (unsupportedMediaType :: Response () body | r1)
529604unsupportedMediaType = respondNoHeaders @"unsupportedMediaType"
530605
531−unsupportedMediaTypeWith = respondWith (Proxy :: Proxy "unsupportedMediaType")
606+unsupportedMediaTypeWith :: forall headers body r1. Record headers -> body -> Variant (unsupportedMediaType :: Response headers body | r1)
607+unsupportedMediaTypeWith headers body = respondWith (Proxy :: Proxy "unsupportedMediaType") headers body
532608
609+rangeNotSatisfiable :: forall body r1. body -> Variant (rangeNotSatisfiable :: Response () body | r1)
533610rangeNotSatisfiable = respondNoHeaders @"rangeNotSatisfiable"
534611
535−rangeNotSatisfiableWith = respondWith (Proxy :: Proxy "rangeNotSatisfiable")
612+rangeNotSatisfiableWith :: forall headers body r1. Record headers -> body -> Variant (rangeNotSatisfiable :: Response headers body | r1)
613+rangeNotSatisfiableWith headers body = respondWith (Proxy :: Proxy "rangeNotSatisfiable") headers body
536614
615+expectationFailed :: forall body r1. body -> Variant (expectationFailed :: Response () body | r1)
537616expectationFailed = respondNoHeaders @"expectationFailed"
538617
539−expectationFailedWith = respondWith (Proxy :: Proxy "expectationFailed")
618+expectationFailedWith :: forall headers body r1. Record headers -> body -> Variant (expectationFailed :: Response headers body | r1)
619+expectationFailedWith headers body = respondWith (Proxy :: Proxy "expectationFailed") headers body
540620
621+imATeapot :: forall body r1. body -> Variant (imATeapot :: Response () body | r1)
541622imATeapot = respondNoHeaders @"imATeapot"
542623
543−imATeapotWith = respondWith (Proxy :: Proxy "imATeapot")
624+imATeapotWith :: forall headers body r1. Record headers -> body -> Variant (imATeapot :: Response headers body | r1)
625+imATeapotWith headers body = respondWith (Proxy :: Proxy "imATeapot") headers body
544626
627+misdirectedRequest :: forall body r1. body -> Variant (misdirectedRequest :: Response () body | r1)
545628misdirectedRequest = respondNoHeaders @"misdirectedRequest"
546629
547−misdirectedRequestWith = respondWith (Proxy :: Proxy "misdirectedRequest")
630+misdirectedRequestWith :: forall headers body r1. Record headers -> body -> Variant (misdirectedRequest :: Response headers body | r1)
631+misdirectedRequestWith headers body = respondWith (Proxy :: Proxy "misdirectedRequest") headers body
548632
633+unprocessableEntity :: forall body r1. body -> Variant (unprocessableEntity :: Response () body | r1)
549634unprocessableEntity = respondNoHeaders @"unprocessableEntity"
550635
551−unprocessableEntityWith = respondWith (Proxy :: Proxy "unprocessableEntity")
636+unprocessableEntityWith :: forall headers body r1. Record headers -> body -> Variant (unprocessableEntity :: Response headers body | r1)
637+unprocessableEntityWith headers body = respondWith (Proxy :: Proxy "unprocessableEntity") headers body
552638
639+locked :: forall body r1. body -> Variant (locked :: Response () body | r1)
553640locked = respondNoHeaders @"locked"
554641
555−lockedWith = respondWith (Proxy :: Proxy "locked")
642+lockedWith :: forall headers body r1. Record headers -> body -> Variant (locked :: Response headers body | r1)
643+lockedWith headers body = respondWith (Proxy :: Proxy "locked") headers body
556644
645+failedDependency :: forall body r1. body -> Variant (failedDependency :: Response () body | r1)
557646failedDependency = respondNoHeaders @"failedDependency"
558647
559−failedDependencyWith = respondWith (Proxy :: Proxy "failedDependency")
648+failedDependencyWith :: forall headers body r1. Record headers -> body -> Variant (failedDependency :: Response headers body | r1)
649+failedDependencyWith headers body = respondWith (Proxy :: Proxy "failedDependency") headers body
560650
651+tooEarly :: forall body r1. body -> Variant (tooEarly :: Response () body | r1)
561652tooEarly = respondNoHeaders @"tooEarly"
562653
563−tooEarlyWith = respondWith (Proxy :: Proxy "tooEarly")
654+tooEarlyWith :: forall headers body r1. Record headers -> body -> Variant (tooEarly :: Response headers body | r1)
655+tooEarlyWith headers body = respondWith (Proxy :: Proxy "tooEarly") headers body
564656
657+upgradeRequired :: forall body r1. body -> Variant (upgradeRequired :: Response () body | r1)
565658upgradeRequired = respondNoHeaders @"upgradeRequired"
566659
567−upgradeRequiredWith = respondWith (Proxy :: Proxy "upgradeRequired")
660+upgradeRequiredWith :: forall headers body r1. Record headers -> body -> Variant (upgradeRequired :: Response headers body | r1)
661+upgradeRequiredWith headers body = respondWith (Proxy :: Proxy "upgradeRequired") headers body
568662
663+preconditionRequired :: forall body r1. body -> Variant (preconditionRequired :: Response () body | r1)
569664preconditionRequired = respondNoHeaders @"preconditionRequired"
570665
571−preconditionRequiredWith = respondWith (Proxy :: Proxy "preconditionRequired")
666+preconditionRequiredWith :: forall headers body r1. Record headers -> body -> Variant (preconditionRequired :: Response headers body | r1)
667+preconditionRequiredWith headers body = respondWith (Proxy :: Proxy "preconditionRequired") headers body
572668
669+tooManyRequests :: forall body r1. body -> Variant (tooManyRequests :: Response () body | r1)
573670tooManyRequests = respondNoHeaders @"tooManyRequests"
574671
575−tooManyRequestsWith = respondWith (Proxy :: Proxy "tooManyRequests")
672+tooManyRequestsWith :: forall headers body r1. Record headers -> body -> Variant (tooManyRequests :: Response headers body | r1)
673+tooManyRequestsWith headers body = respondWith (Proxy :: Proxy "tooManyRequests") headers body
576674
675+requestHeaderFieldsTooLarge :: forall body r1. body -> Variant (requestHeaderFieldsTooLarge :: Response () body | r1)
577676requestHeaderFieldsTooLarge = respondNoHeaders @"requestHeaderFieldsTooLarge"
578677
579−requestHeaderFieldsTooLargeWith = respondWith (Proxy :: Proxy "requestHeaderFieldsTooLarge")
678+requestHeaderFieldsTooLargeWith :: forall headers body r1. Record headers -> body -> Variant (requestHeaderFieldsTooLarge :: Response headers body | r1)
679+requestHeaderFieldsTooLargeWith headers body = respondWith (Proxy :: Proxy "requestHeaderFieldsTooLarge") headers body
580680
681+unavailableForLegalReasons :: forall body r1. body -> Variant (unavailableForLegalReasons :: Response () body | r1)
581682unavailableForLegalReasons = respondNoHeaders @"unavailableForLegalReasons"
582683
583−unavailableForLegalReasonsWith = respondWith (Proxy :: Proxy "unavailableForLegalReasons")
684+unavailableForLegalReasonsWith :: forall headers body r1. Record headers -> body -> Variant (unavailableForLegalReasons :: Response headers body | r1)
685+unavailableForLegalReasonsWith headers body = respondWith (Proxy :: Proxy "unavailableForLegalReasons") headers body
584686
687+internalServerError :: forall body r1. body -> Variant (internalServerError :: Response () body | r1)
585688internalServerError = respondNoHeaders @"internalServerError"
586689
587−internalServerErrorWith = respondWith (Proxy :: Proxy "internalServerError")
690+internalServerErrorWith :: forall headers body r1. Record headers -> body -> Variant (internalServerError :: Response headers body | r1)
691+internalServerErrorWith headers body = respondWith (Proxy :: Proxy "internalServerError") headers body
588692
693+notImplemented :: forall body r1. body -> Variant (notImplemented :: Response () body | r1)
589694notImplemented = respondNoHeaders @"notImplemented"
590695
591−notImplementedWith = respondWith (Proxy :: Proxy "notImplemented")
696+notImplementedWith :: forall headers body r1. Record headers -> body -> Variant (notImplemented :: Response headers body | r1)
697+notImplementedWith headers body = respondWith (Proxy :: Proxy "notImplemented") headers body
592698
699+badGateway :: forall body r1. body -> Variant (badGateway :: Response () body | r1)
593700badGateway = respondNoHeaders @"badGateway"
594701
595−badGatewayWith = respondWith (Proxy :: Proxy "badGateway")
702+badGatewayWith :: forall headers body r1. Record headers -> body -> Variant (badGateway :: Response headers body | r1)
703+badGatewayWith headers body = respondWith (Proxy :: Proxy "badGateway") headers body
596704
705+serviceUnavailable :: forall body r1. body -> Variant (serviceUnavailable :: Response () body | r1)
597706serviceUnavailable = respondNoHeaders @"serviceUnavailable"
598707
599−serviceUnavailableWith = respondWith (Proxy :: Proxy "serviceUnavailable")
708+serviceUnavailableWith :: forall headers body r1. Record headers -> body -> Variant (serviceUnavailable :: Response headers body | r1)
709+serviceUnavailableWith headers body = respondWith (Proxy :: Proxy "serviceUnavailable") headers body
600710
711+gatewayTimeout :: forall body r1. body -> Variant (gatewayTimeout :: Response () body | r1)
601712gatewayTimeout = respondNoHeaders @"gatewayTimeout"
602713
603−gatewayTimeoutWith = respondWith (Proxy :: Proxy "gatewayTimeout")
714+gatewayTimeoutWith :: forall headers body r1. Record headers -> body -> Variant (gatewayTimeout :: Response headers body | r1)
715+gatewayTimeoutWith headers body = respondWith (Proxy :: Proxy "gatewayTimeout") headers body
604716
717+httpVersionNotSupported :: forall body r1. body -> Variant (httpVersionNotSupported :: Response () body | r1)
605718httpVersionNotSupported = respondNoHeaders @"httpVersionNotSupported"
606719
607−httpVersionNotSupportedWith = respondWith (Proxy :: Proxy "httpVersionNotSupported")
720+httpVersionNotSupportedWith :: forall headers body r1. Record headers -> body -> Variant (httpVersionNotSupported :: Response headers body | r1)
721+httpVersionNotSupportedWith headers body = respondWith (Proxy :: Proxy "httpVersionNotSupported") headers body
608722
723+variantAlsoNegotiates :: forall body r1. body -> Variant (variantAlsoNegotiates :: Response () body | r1)
609724variantAlsoNegotiates = respondNoHeaders @"variantAlsoNegotiates"
610725
611−variantAlsoNegotiatesWith = respondWith (Proxy :: Proxy "variantAlsoNegotiates")
726+variantAlsoNegotiatesWith :: forall headers body r1. Record headers -> body -> Variant (variantAlsoNegotiates :: Response headers body | r1)
727+variantAlsoNegotiatesWith headers body = respondWith (Proxy :: Proxy "variantAlsoNegotiates") headers body
612728
729+insufficientStorage :: forall body r1. body -> Variant (insufficientStorage :: Response () body | r1)
613730insufficientStorage = respondNoHeaders @"insufficientStorage"
614731
615−insufficientStorageWith = respondWith (Proxy :: Proxy "insufficientStorage")
732+insufficientStorageWith :: forall headers body r1. Record headers -> body -> Variant (insufficientStorage :: Response headers body | r1)
733+insufficientStorageWith headers body = respondWith (Proxy :: Proxy "insufficientStorage") headers body
616734
735+loopDetected :: forall body r1. body -> Variant (loopDetected :: Response () body | r1)
617736loopDetected = respondNoHeaders @"loopDetected"
618737
619−loopDetectedWith = respondWith (Proxy :: Proxy "loopDetected")
738+loopDetectedWith :: forall headers body r1. Record headers -> body -> Variant (loopDetected :: Response headers body | r1)
739+loopDetectedWith headers body = respondWith (Proxy :: Proxy "loopDetected") headers body
620740
741+notExtended :: forall body r1. body -> Variant (notExtended :: Response () body | r1)
621742notExtended = respondNoHeaders @"notExtended"
622743
623−notExtendedWith = respondWith (Proxy :: Proxy "notExtended")
744+notExtendedWith :: forall headers body r1. Record headers -> body -> Variant (notExtended :: Response headers body | r1)
745+notExtendedWith headers body = respondWith (Proxy :: Proxy "notExtended") headers body
624746
747+networkAuthenticationRequired :: forall body r1. body -> Variant (networkAuthenticationRequired :: Response () body | r1)
625748networkAuthenticationRequired = respondNoHeaders @"networkAuthenticationRequired"
626749
627−networkAuthenticationRequiredWith = respondWith (Proxy :: Proxy "networkAuthenticationRequired")
750+networkAuthenticationRequiredWith :: forall headers body r1. Record headers -> body -> Variant (networkAuthenticationRequired :: Response headers body | r1)
751+networkAuthenticationRequiredWith headers body = respondWith (Proxy :: Proxy "networkAuthenticationRequired") headers body
src/Yoga/HTTP/API/Route/Route.purs+5−6
@@ -12,9 +12,8 @@ import Prim.Row (class Cons, class Lacks, class Union)
1212import Prim.RowList (class RowToList, RowList)
1313import Prim.RowList as RL
1414import Type.Proxy (Proxy(..))
15−import Unsafe.Coerce (unsafeCoerce)
1615import Yoga.HTTP.API.Path (class PathPattern, pathPattern)
17−import Yoga.HTTP.API.Route.Handler (Request, class DefaultRequestFields, class SegmentPathParams, class SegmentQueryParams)
16+import Yoga.HTTP.API.Route.Handler (class DefaultRequestFields, class SegmentPathParams, class SegmentQueryParams)
1817import Yoga.HTTP.API.Route.OpenAPI (class CollectOperations, class RenderHeadersSchema, renderHeadersSchema, class RenderCookieParamsSchema, renderCookieParamsSchema, class RenderPathParamsSchema, renderPathParamsSchema, class RenderQueryParamsSchema, renderQueryParamsSchema, class RenderRequestBodySchema, renderRequestBodySchema, class RenderVariantResponseSchemaRL, renderVariantResponseSchemaRL, class DetectSecurity, detectSecurity, class DetectCookieSecurity, detectCookieSecurity, class ToOpenAPI, toOpenAPIImpl, class CollectSchemas, collectSchemas, class CollectVariantSchemasRL, collectVariantSchemasRL, class CollectRouteSchemas, class CollectSchemaNames, class CollectVariantSchemaNames, class CollectRouteSchemaNames)
1918import Yoga.HTTP.API.Route.OpenAPIMetadata (class HasOperationMetadata, operationMetadata)
2019import Yoga.HTTP.API.Route.RenderMethod (class RenderMethod, renderMethod)
@@ -67,9 +66,9 @@ instance
6766 , RenderRequestBodySchema encoding
6867 , RowToList userResp rl
6968 , RenderVariantResponseSchemaRL rl
70− , HasOperationMetadata (Route method segments (Request (Record partialRequest)) userResp)
69+ , HasOperationMetadata (Route method segments (Record partialRequest) userResp)
7170 ) =>
72− ToOpenAPI (Route method segments (Request (Record partialRequest)) userResp) where
71+ ToOpenAPI (Route method segments (Record partialRequest) userResp) where
7372 toOpenAPIImpl proxy =
7473 let
7574 methodStr = renderMethod (Proxy :: Proxy method)
@@ -122,7 +121,7 @@ instance
122121 , RowToList userResp rl
123122 , CollectVariantSchemasRL rl
124123 ) =>
125− CollectRouteSchemas (Route method segments (Request (Record partialRequest)) userResp) where
124+ CollectRouteSchemas (Route method segments (Record partialRequest) userResp) where
126125 collectRouteSchemas _ =
127126 let
128127 requestSchemas = collectSchemas (Proxy :: Proxy encoding)
@@ -138,4 +137,4 @@ instance
138137 , CollectVariantSchemaNames rl respNames
139138 , Union reqNames respNames names
140139 ) =>
141− CollectRouteSchemaNames (Route method segments (Request (Record partialRequest)) userResp) names
140+ CollectRouteSchemaNames (Route method segments (Record partialRequest) userResp) names
src/Yoga/HTTP/API/Route/RouteHandler.purs+1−2
@@ -14,7 +14,6 @@ import Prim.RowList (class RowToList)
1414import Unsafe.Coerce (unsafeCoerce)
1515import Yoga.HTTP.API.Route.Handler
1616 ( HandlerFn
17− , Request
1817 , class DefaultRequestFields
1918 , class EncodingBody
2019 , class SegmentPathParams
@@ -42,7 +41,7 @@ instance
4241 , ConvertResponseVariant userResp respVariant
4342 ) =>
4443 RouteHandler
45− (Route method segments (Request (Record partialRequest)) userResp)
44+ (Route method segments (Record partialRequest) userResp)
4645 pathParams
4746 queryParams
4847 fullHeaders
src/Yoga/HTTP/API/Route/StatusCode.purs+1−1
@@ -10,7 +10,7 @@ module Yoga.HTTP.API.Route.StatusCode
1010
1111import Prelude
1212
13−import Type.Proxy (Proxy(..))
13+import Type.Proxy (Proxy)
1414import Data.Newtype (class Newtype)
1515import Prim.TypeError (class Fail, Text, Above, Quote, Beside)
1616
src/Yoga/HTTP/API/UnionTrick.jsnew file+3
@@ -0,0 +1,3 @@
1+export const uorToMaybeImpl = (just) => (nothing) => (value) => {
2+ return value === undefined ? nothing : just(value);
3+};
src/Yoga/HTTP/API/UnionTrick.pursnew file+163
@@ -0,0 +1,163 @@
1+-- | A library for the "Union Trick" pattern - ergonomic APIs with optional fields.
2+-- |
3+-- | This module provides utilities for creating APIs that accept records with optional
4+-- | fields without requiring explicit Maybe/Opt constructors.
5+-- |
6+-- | Example usage:
7+-- | ```purescript
8+-- | type MyConfigR f =
9+-- | ( host :: String
10+-- | , port :: Int
11+-- | , ssl :: f Boolean
12+-- | )
13+-- |
14+-- | type MyConfigUor = { | MyConfigR UndefinedOr }
15+-- |
16+-- | myFunction :: forall r. CoerceConfig r (MyConfigR UndefinedOr) => { | r } -> Result
17+-- | myFunction given = myFunctionImpl (coerceConfig given)
18+-- | ```
19+module Yoga.HTTP.API.UnionTrick
20+ ( class CoerceConfig
21+ , coerceConfig
22+ , class CoerceFields
23+ , coerceFields
24+ , coerce
25+ , class CoerceFieldsRL
26+ , class LookupField
27+ , class CoerceFieldType
28+ , class CoerceToUor
29+ , coerceToUor
30+ , uorToMaybe
31+ , module ReExports
32+ ) where
33+
34+import Prelude
35+
36+import Data.Identity (Identity(..))
37+import Data.Maybe (Maybe(..))
38+import Literals.Undefined (Undefined)
39+import Prim.Row as Row
40+import Prim.RowList (class RowToList, RowList, Cons, Nil)
41+import Untagged.Union (OneOf, UndefinedOr)
42+import Untagged.Union (UndefinedOr) as ReExports
43+import Unsafe.Coerce (unsafeCoerce)
44+
45+-- | A typeclass for coercing user-provided records to UndefinedOr-based config records.
46+-- |
47+-- | This provides a more constrained version of unsafeCoerce that documents the intent
48+-- | and can have instances for specific patterns.
49+-- |
50+-- | The constraint `Union given missing full` ensures that the given record is a subset
51+-- | of the full expected record type, providing some compile-time safety.
52+class CoerceConfig :: Row Type -> Row Type -> Constraint
53+class CoerceConfig given full where
54+ coerceConfig :: { | given } -> { | full }
55+
56+-- | Generic instance that works for any record coercion when Union constraint is satisfied.
57+-- |
58+-- | This uses unsafeCoerce internally but is constrained to only work when the given
59+-- | record is a valid subset of the expected record.
60+instance Row.Union given missing full => CoerceConfig given full where
61+ coerceConfig = unsafeCoerce
62+
63+-- | A more sophisticated coercion typeclass that works field-by-field.
64+-- |
65+-- | This allows coercing records where each field in `given` can be coerced to the
66+-- | corresponding field in `full`. For example:
67+-- | { name :: String } -> { name :: UndefinedOr String }
68+-- |
69+-- | This uses RowList processing to check each field individually.
70+class CoerceFields :: Row Type -> Row Type -> Constraint
71+class CoerceFields given full where
72+ coerceFields :: { | given } -> { | full }
73+
74+instance (RowToList given givenRL, RowToList full fullRL, CoerceFieldsRL givenRL fullRL) => CoerceFields given full where
75+ coerceFields = unsafeCoerce
76+
77+coerce :: forall @full given. CoerceFields given (full (OneOf Undefined)) => { | given } -> { | full (OneOf Undefined) }
78+coerce = coerceFields
79+
80+-- | Process RowLists field-by-field to ensure each field can be coerced.
81+-- | This uses a lookup-based approach: for each field in `given`, we look it up
82+-- | in `full` by label and check if the types can coerce.
83+class CoerceFieldsRL :: RowList Type -> RowList Type -> Constraint
84+class CoerceFieldsRL givenRL fullRL
85+
86+-- | Base case: empty given record can coerce to anything
87+instance CoerceFieldsRL Nil fullRL
88+
89+-- | Recursive case: look up the current field in the full RowList and check coercion
90+instance
91+ ( LookupField label fullRL fullType
92+ , CoerceFieldType givenType fullType
93+ , CoerceFieldsRL givenRL fullRL
94+ ) =>
95+ CoerceFieldsRL (Cons label givenType givenRL) fullRL
96+
97+-- | Helper typeclass to look up a field by label in a RowList
98+class LookupField :: Symbol -> RowList Type -> Type -> Constraint
99+class LookupField label rl ty | label rl -> ty
100+
101+-- | Found the field with matching label
102+instance LookupField label (Cons label ty tail) ty
103+
104+-- | Field not here, continue searching
105+else instance LookupField label tail ty => LookupField label (Cons other ty' tail) ty
106+
107+-- | A typeclass for checking if a field type can be coerced.
108+-- |
109+-- | This allows:
110+-- | - String -> UndefinedOr String
111+-- | - { a :: String, b :: Undefined } -> UndefinedOr { a :: String, b :: String }
112+-- | - Undefined -> String (for optional nested fields)
113+-- | - String -> String (identity)
114+-- | - a -> Identity a
115+-- | - etc.
116+class CoerceFieldType :: Type -> Type -> Constraint
117+class CoerceFieldType from to
118+
119+-- | Undefined to any type (for optional nested fields)
120+instance CoerceFieldType Undefined a
121+
122+-- | Record with potentially Undefined fields to UndefinedOr Record
123+-- | This handles cases like { name :: String, url :: Undefined } -> UndefinedOr { name :: String, url :: String }
124+else instance (RowToList r1 rl1, RowToList r2 rl2, CoerceFieldsRL rl1 rl2) => CoerceFieldType (Record r1) (UndefinedOr (Record r2))
125+
126+-- | Plain type to UndefinedOr
127+else instance CoerceFieldType a (UndefinedOr a)
128+
129+-- | Plain type to Identity wrapper
130+else instance CoerceFieldType a (Identity a)
131+
132+-- | Identity coercion (most general, last resort)
133+else instance CoerceFieldType a a
134+
135+-- | A typeclass for coercing plain values to UndefinedOr-wrapped values.
136+-- |
137+-- | This is used for individual fields rather than whole records.
138+class CoerceToUor :: Type -> Type -> Constraint
139+class CoerceToUor a b | b -> a where
140+ coerceToUor :: a -> b
141+
142+-- | Coerce a plain value to UndefinedOr
143+instance CoerceToUor a (UndefinedOr a) where
144+ coerceToUor = unsafeCoerce
145+
146+-- | Identity functor passes through
147+else instance CoerceToUor a (Identity a) where
148+ coerceToUor = Identity
149+
150+-- | Already wrapped - pass through
151+else instance CoerceToUor a a where
152+ coerceToUor = identity
153+
154+-- | Convert UndefinedOr to Maybe by checking for undefined at runtime.
155+-- |
156+-- | This is safer than unsafeCoerce as it properly handles the JavaScript undefined value.
157+foreign import uorToMaybeImpl :: forall a. (forall b. b -> Maybe b) -> (forall b. Maybe b) -> UndefinedOr a -> Maybe a
158+
159+-- | Convert UndefinedOr to Maybe.
160+-- |
161+-- | Returns Nothing if the value is undefined, Just otherwise.
162+uorToMaybe :: forall a. UndefinedOr a -> Maybe a
163+uorToMaybe = uorToMaybeImpl Just Nothing
test-compile-fail/cases/BodyTypeMismatch.purs+2−2
@@ -5,14 +5,14 @@ import Prelude
55import Data.Variant (Variant)
66import Effect.Aff (Aff)
77import Yoga.HTTP.API.Path (Path, Lit)
8−import Yoga.HTTP.API.Route.Handler (Request)
8+import Yoga.HTTP.API.Route.Handler
99import Yoga.HTTP.API.Route.Method (GET)
1010import Yoga.HTTP.API.Route.Response (Response, respondNoHeaders)
1111import Yoga.HTTP.API.Route.Route (Route)
1212import Yoga.HTTP.API.Route.RouteHandler (Handler, mkHandler)
1313
1414-- Route has no body (defaults to NoBody/Unit), but handler uses body as String
15−type MyRoute = Route GET (Path (Lit "health")) (Request {}) (ok :: { body :: String })
15+type MyRoute = Route GET (Path (Lit "health")) {} (ok :: { body :: String })
1616
1717test :: Handler MyRoute
1818test = mkHandler \{ body } ->
test-compile-fail/cases/DuplicateCaptureNames.purs+2−2
@@ -7,14 +7,14 @@ import Data.Variant (Variant)
77import Effect.Aff (Aff)
88import Yoga.HTTP.API.Path (Path, Lit, Capture, type (/), type (:))
99import Yoga.HTTP.API.Route.Encoding (NoBody)
10−import Yoga.HTTP.API.Route.Handler (Request)
10+import Yoga.HTTP.API.Route.Handler
1111import Yoga.HTTP.API.Route.Method (GET)
1212import Yoga.HTTP.API.Route.Response (Response, respondNoHeaders)
1313import Yoga.HTTP.API.Route.Route (Route)
1414import Yoga.HTTP.API.Route.RouteHandler (Handler, mkHandler)
1515
1616-- Two captures both named "id" with different types
17−type MyRoute = Route GET (Path (Capture "id" Int / Capture "id" String)) (Request {}) (ok :: { body :: String })
17+type MyRoute = Route GET (Path (Capture "id" Int / Capture "id" String)) {} (ok :: { body :: String })
1818
1919test :: Handler MyRoute
2020test = mkHandler \{ path: { id } } ->
test-compile-fail/cases/HandlerSignatureMismatch.purs+2−2
@@ -7,13 +7,13 @@ import Data.Variant (Variant)
77import Effect.Aff (Aff)
88import Yoga.HTTP.API.Path (Path, Lit, Capture, type (/), type (:))
99import Yoga.HTTP.API.Route.Encoding (NoBody)
10−import Yoga.HTTP.API.Route.Handler (Request)
10+import Yoga.HTTP.API.Route.Handler
1111import Yoga.HTTP.API.Route.Method (GET)
1212import Yoga.HTTP.API.Route.Response (Response, respondNoHeaders)
1313import Yoga.HTTP.API.Route.Route (Route)
1414import Yoga.HTTP.API.Route.RouteHandler (Handler, mkHandler)
1515
16−type MyRoute = Route GET (Path (Lit "users" / Capture "id" Int)) (Request {}) (ok :: { body :: String })
16+type MyRoute = Route GET (Path (Lit "users" / Capture "id" Int)) {} (ok :: { body :: String })
1717
1818-- Handler takes { id :: String } but route has Capture "id" Int
1919test :: Handler MyRoute
test-compile-fail/cases/InvalidResponseRecordStructure.purs+2−2
@@ -3,14 +3,14 @@ module Test.CompileFail.InvalidResponseRecordStructure where
33
44import Prelude
55import Yoga.HTTP.API.Path (Path, Lit)
6−import Yoga.HTTP.API.Route.Handler (Request)
6+import Yoga.HTTP.API.Route.Handler
77import Yoga.HTTP.API.Route.Method (GET)
88import Yoga.HTTP.API.Route.Route (Route)
99import Yoga.HTTP.API.Route.RouteHandler (Handler, mkHandler)
1010import Yoga.HTTP.API.Route.Response (respondNoHeaders)
1111
1212-- Response record has "status" field which is not "body" or "headers"
13−type MyRoute = Route GET (Path (Lit "health")) (Request {}) (ok :: { body :: String, status :: Int })
13+type MyRoute = Route GET (Path (Lit "health")) {} (ok :: { body :: String, status :: Int })
1414
1515test :: Handler MyRoute
1616test = mkHandler \_ ->
test-compile-fail/cases/MissingRequiredQueryParam.purs+2−2
@@ -5,7 +5,7 @@ import Prelude
55import Data.Variant (Variant)
66import Effect.Aff (Aff)
77import Yoga.HTTP.API.Path (Path, Lit, QueryParams, type (:?))
8−import Yoga.HTTP.API.Route.Handler (Request)
8+import Yoga.HTTP.API.Route.Handler
99import Yoga.HTTP.API.Route.Method (GET)
1010import Yoga.HTTP.API.Route.Response (Response, respondNoHeaders)
1111import Yoga.HTTP.API.Route.Route (Route)
@@ -13,7 +13,7 @@ import Yoga.HTTP.API.Route.RouteHandler (Handler, mkHandler)
1313
1414-- Query param "limit" is optional (no Required wrapper), so handler gets Maybe Int
1515-- But handler tries to use it as plain Int
16−type MyRoute = Route GET (Path (Lit "items") :? (limit :: Int)) (Request {}) (ok :: { body :: String })
16+type MyRoute = Route GET (Path (Lit "items") :? (limit :: Int)) {} (ok :: { body :: String })
1717
1818test :: Handler MyRoute
1919test = mkHandler \{ query: { limit } } ->
test-compile-fail/cases/SchemaNameCollision.purs+3−3
@@ -4,7 +4,7 @@ module Test.CompileFail.SchemaNameCollision where
44import Data.Maybe (Maybe(..))
55import Yoga.HTTP.API.Path (Path, Lit)
66import Yoga.HTTP.API.Route.Encoding (JSON)
7−import Yoga.HTTP.API.Route.Handler (Request)
7+import Yoga.HTTP.API.Route.Handler
88import Yoga.HTTP.API.Route.Method (GET, POST)
99import Yoga.HTTP.API.Route.OpenAPIMetadata (Schema)
1010import Yoga.HTTP.API.Route.OpenAPI (buildOpenAPISpec, OpenAPISpec)
@@ -14,8 +14,8 @@ type UserV1 = { id :: Int, name :: String }
1414type UserV2 = { id :: Int, email :: String, age :: Int }
1515
1616type CollisionAPI =
17− { getUser :: Route GET (Path (Lit "users")) (Request {}) (ok :: { body :: Schema "User" UserV1 })
18− , createUser :: Route POST (Path (Lit "users")) (Request { body :: JSON (Schema "User" UserV2) }) (created :: { body :: Schema "User" UserV2 })
17+ { getUser :: Route GET (Path (Lit "users")) {} (ok :: { body :: Schema "User" UserV1 })
18+ , createUser :: Route POST (Path (Lit "users")) { body :: JSON (Schema "User" UserV2) } (created :: { body :: Schema "User" UserV2 })
1919 }
2020
2121spec :: OpenAPISpec
test-compile-fail/cases/WrongResponseVariantLabel.purs+2−2
@@ -5,14 +5,14 @@ import Prelude
55import Data.Variant (Variant)
66import Effect.Aff (Aff)
77import Yoga.HTTP.API.Path (Path, Lit)
8−import Yoga.HTTP.API.Route.Handler (Request)
8+import Yoga.HTTP.API.Route.Handler
99import Yoga.HTTP.API.Route.Method (GET)
1010import Yoga.HTTP.API.Route.Response (Response, respondNoHeaders)
1111import Yoga.HTTP.API.Route.Route (Route)
1212import Yoga.HTTP.API.Route.RouteHandler (Handler, mkHandler)
1313
1414-- Route only has "ok" response, no "notFound"
15−type MyRoute = Route GET (Path (Lit "health")) (Request {}) (ok :: { body :: String })
15+type MyRoute = Route GET (Path (Lit "health")) {} (ok :: { body :: String })
1616
1717test :: Handler MyRoute
1818test = mkHandler \_ ->
test/Test/APIRecordTest.purs+7−7
@@ -9,7 +9,7 @@ import Effect (Effect)
99import Effect.Aff (Aff)
1010import Type.Proxy (Proxy(..))
1111import Yoga.HTTP.API.Path (type (/), type (:))
12−import Yoga.HTTP.API.Route (GET, POST, Route, Request, JSON, respondNoHeaders, buildOpenAPISpec, collectOperations, mkHandler, apiHandlers)
12+import Yoga.HTTP.API.Route (GET, POST, Route, JSON, respondNoHeaders, buildOpenAPISpec, collectOperations, mkHandler, apiHandlers)
1313import Yoga.JSON (writeJSON)
1414import ViTest (ViTest, describe, test)
1515import ViTest.Expect (expectToBe)
@@ -24,9 +24,9 @@ expectToEqual expected actual = expectToBe true (expected == actual)
2424type User = { id :: Int, name :: String }
2525
2626type TestAPI =
27− { getUser :: Route GET ("users" / "id" : Int) (Request {}) (ok :: { body :: User })
28− , listUsers :: Route GET "users" (Request {}) (ok :: { body :: Array User })
29− , createUser :: Route POST "users" (Request { body :: JSON User }) (created :: { body :: User })
27+ { getUser :: Route GET ("users" / "id" : Int) {} (ok :: { body :: User })
28+ , listUsers :: Route GET "users" {} (ok :: { body :: Array User })
29+ , createUser :: Route POST "users" { body :: JSON User } (created :: { body :: User })
3030 }
3131
3232--------------------------------------------------------------------------------
@@ -75,20 +75,20 @@ testOperationIds = describe "API Record - OperationId" do
7575testBuildOpenAPISpec :: Effect ViTest
7676testBuildOpenAPISpec = describe "API Record - buildOpenAPISpec" do
7777 _ <- test "generates valid OpenAPI spec with operationIds" do
78− let spec = buildOpenAPISpec @TestAPI { title: "Test API", version: "1.0.0", description: Nothing, contact: Nothing, license: Nothing }
78+ let spec = buildOpenAPISpec @TestAPI { title: "Test API", version: "1.0.0" }
7979 let json = writeJSON spec
8080 expectToBe true (String.contains (String.Pattern "\"operationId\":\"getUser\"") json)
8181 expectToBe true (String.contains (String.Pattern "\"operationId\":\"listUsers\"") json)
8282 expectToBe true (String.contains (String.Pattern "\"operationId\":\"createUser\"") json)
8383
8484 _ <- test "contains paths" do
85− let spec = buildOpenAPISpec @TestAPI { title: "Test API", version: "1.0.0", description: Nothing, contact: Nothing, license: Nothing }
85+ let spec = buildOpenAPISpec @TestAPI { title: "Test API", version: "1.0.0" }
8686 let json = writeJSON spec
8787 expectToBe true (String.contains (String.Pattern "/users/{id}") json)
8888 expectToBe true (String.contains (String.Pattern "/users") json)
8989
9090 test "contains API info" do
91− let spec = buildOpenAPISpec @TestAPI { title: "Test API", version: "1.0.0", description: Nothing, contact: Nothing, license: Nothing }
91+ let spec = buildOpenAPISpec @TestAPI { title: "Test API", version: "1.0.0" }
9292 let json = writeJSON spec
9393 expectToBe true (String.contains (String.Pattern "Test API") json)
9494 expectToBe true (String.contains (String.Pattern "1.0.0") json)
test/Test/BearerAuthTest.purs+23−23
@@ -6,7 +6,7 @@ import Data.Array as Array
66import Data.Maybe (Maybe(..))
77import Data.String as String
88import Effect (Effect)
9−import Yoga.HTTP.API.Route (GET, Route, Request, buildOpenAPISpec)
9+import Yoga.HTTP.API.Route (GET, Route, buildOpenAPISpec)
1010import Yoga.HTTP.API.Route.Auth (BearerToken, BasicAuth, ApiKeyHeader, DigestAuth)
1111import Yoga.JSON (writeJSON)
1212import Test.OpenAPIValidation (validate)
@@ -18,25 +18,25 @@ import ViTest.Expect (expectToBe)
1818--------------------------------------------------------------------------------
1919
2020type BearerAuthAPI =
21− { getProtected :: Route GET "protected" (Request { headers :: { authorization :: BearerToken } }) (ok :: { body :: { message :: String } })
21+ { getProtected :: Route GET "protected" { headers :: { authorization :: BearerToken } } (ok :: { body :: { message :: String } })
2222 }
2323
2424type BasicAuthAPI =
25− { getProtected :: Route GET "protected" (Request { headers :: { authorization :: BasicAuth } }) (ok :: { body :: { message :: String } })
25+ { getProtected :: Route GET "protected" { headers :: { authorization :: BasicAuth } } (ok :: { body :: { message :: String } })
2626 }
2727
2828type ApiKeyAPI =
29− { getProtected :: Route GET "protected" (Request { headers :: { apiKey :: ApiKeyHeader } }) (ok :: { body :: { message :: String } })
29+ { getProtected :: Route GET "protected" { headers :: { apiKey :: ApiKeyHeader } } (ok :: { body :: { message :: String } })
3030 }
3131
3232type DigestAuthAPI =
33− { getProtected :: Route GET "protected" (Request { headers :: { authorization :: DigestAuth } }) (ok :: { body :: { message :: String } })
33+ { getProtected :: Route GET "protected" { headers :: { authorization :: DigestAuth } } (ok :: { body :: { message :: String } })
3434 }
3535
3636type MultiAuthAPI =
37− { bearerEndpoint :: Route GET "bearer" (Request { headers :: { authorization :: BearerToken } }) (ok :: { body :: { message :: String } })
38− , basicEndpoint :: Route GET "basic" (Request { headers :: { authorization :: BasicAuth } }) (ok :: { body :: { message :: String } })
39− , apiKeyEndpoint :: Route GET "apikey" (Request { headers :: { xApiKey :: ApiKeyHeader } }) (ok :: { body :: { message :: String } })
37+ { bearerEndpoint :: Route GET "bearer" { headers :: { authorization :: BearerToken } } (ok :: { body :: { message :: String } })
38+ , basicEndpoint :: Route GET "basic" { headers :: { authorization :: BasicAuth } } (ok :: { body :: { message :: String } })
39+ , apiKeyEndpoint :: Route GET "apikey" { headers :: { xApiKey :: ApiKeyHeader } } (ok :: { body :: { message :: String } })
4040 }
4141
4242--------------------------------------------------------------------------------
@@ -46,44 +46,44 @@ type MultiAuthAPI =
4646testBearerTokenAuth :: Effect ViTest
4747testBearerTokenAuth = describe "BearerToken Authentication" do
4848 _ <- test "BearerToken does not appear as a regular header parameter" do
49− let spec = buildOpenAPISpec @BearerAuthAPI { title: "Auth API", version: "1.0.0", description: Nothing, contact: Nothing, license: Nothing }
49+ let spec = buildOpenAPISpec @BearerAuthAPI { title: "Auth API", version: "1.0.0" }
5050 let json = writeJSON spec
5151 let hasAuthParam = String.contains (String.Pattern "\"name\":\"authorization\"") json
5252 expectToBe false hasAuthParam
5353
5454 _ <- test "BearerToken appears in security requirements" do
55− let spec = buildOpenAPISpec @BearerAuthAPI { title: "Auth API", version: "1.0.0", description: Nothing, contact: Nothing, license: Nothing }
55+ let spec = buildOpenAPISpec @BearerAuthAPI { title: "Auth API", version: "1.0.0" }
5656 let json = writeJSON spec
5757 let hasBearerAuth = String.contains (String.Pattern "bearerAuth") json
5858 expectToBe true hasBearerAuth
5959
6060 _ <- test "BearerToken security scheme has correct structure" do
61− let spec = buildOpenAPISpec @BearerAuthAPI { title: "Auth API", version: "1.0.0", description: Nothing, contact: Nothing, license: Nothing }
61+ let spec = buildOpenAPISpec @BearerAuthAPI { title: "Auth API", version: "1.0.0" }
6262 let json = writeJSON spec
6363 expectToBe true (String.contains (String.Pattern "\"type\":\"http\"") json)
6464 expectToBe true (String.contains (String.Pattern "\"scheme\":\"bearer\"") json)
6565
6666 test "BearerToken API generates valid OpenAPI 3.0 schema" do
67− let spec = buildOpenAPISpec @BearerAuthAPI { title: "Auth API", version: "1.0.0", description: Nothing, contact: Nothing, license: Nothing }
67+ let spec = buildOpenAPISpec @BearerAuthAPI { title: "Auth API", version: "1.0.0" }
6868 let result = validate spec
6969 expectToBe true (Array.null result.errors)
7070
7171testBasicAuth :: Effect ViTest
7272testBasicAuth = describe "BasicAuth Authentication" do
7373 _ <- test "BasicAuth does not appear as a regular header parameter" do
74− let spec = buildOpenAPISpec @BasicAuthAPI { title: "Auth API", version: "1.0.0", description: Nothing, contact: Nothing, license: Nothing }
74+ let spec = buildOpenAPISpec @BasicAuthAPI { title: "Auth API", version: "1.0.0" }
7575 let json = writeJSON spec
7676 let hasAuthParam = String.contains (String.Pattern "\"name\":\"authorization\"") json
7777 expectToBe false hasAuthParam
7878
7979 _ <- test "BasicAuth appears in security requirements" do
80− let spec = buildOpenAPISpec @BasicAuthAPI { title: "Auth API", version: "1.0.0", description: Nothing, contact: Nothing, license: Nothing }
80+ let spec = buildOpenAPISpec @BasicAuthAPI { title: "Auth API", version: "1.0.0" }
8181 let json = writeJSON spec
8282 let hasBasicAuth = String.contains (String.Pattern "basicAuth") json
8383 expectToBe true hasBasicAuth
8484
8585 test "BasicAuth security scheme has correct structure" do
86− let spec = buildOpenAPISpec @BasicAuthAPI { title: "Auth API", version: "1.0.0", description: Nothing, contact: Nothing, license: Nothing }
86+ let spec = buildOpenAPISpec @BasicAuthAPI { title: "Auth API", version: "1.0.0" }
8787 let json = writeJSON spec
8888 expectToBe true (String.contains (String.Pattern "\"type\":\"http\"") json)
8989 expectToBe true (String.contains (String.Pattern "\"scheme\":\"basic\"") json)
@@ -91,20 +91,20 @@ testBasicAuth = describe "BasicAuth Authentication" do
9191testApiKeyAuth :: Effect ViTest
9292testApiKeyAuth = describe "ApiKey Authentication" do
9393 _ <- test "ApiKeyHeader does not appear as a regular header parameter" do
94− let spec = buildOpenAPISpec @ApiKeyAPI { title: "Auth API", version: "1.0.0", description: Nothing, contact: Nothing, license: Nothing }
94+ let spec = buildOpenAPISpec @ApiKeyAPI { title: "Auth API", version: "1.0.0" }
9595 let json = writeJSON spec
9696 -- Check that parameters array is empty (no header parameters)
9797 let hasEmptyParams = String.contains (String.Pattern "\"parameters\":[]") json
9898 expectToBe true hasEmptyParams
9999
100100 _ <- test "ApiKey appears in security requirements" do
101− let spec = buildOpenAPISpec @ApiKeyAPI { title: "Auth API", version: "1.0.0", description: Nothing, contact: Nothing, license: Nothing }
101+ let spec = buildOpenAPISpec @ApiKeyAPI { title: "Auth API", version: "1.0.0" }
102102 let json = writeJSON spec
103103 let hasApiKey = String.contains (String.Pattern "apiKeyApiKey") json
104104 expectToBe true hasApiKey
105105
106106 test "ApiKey security scheme has correct structure" do
107− let spec = buildOpenAPISpec @ApiKeyAPI { title: "Auth API", version: "1.0.0", description: Nothing, contact: Nothing, license: Nothing }
107+ let spec = buildOpenAPISpec @ApiKeyAPI { title: "Auth API", version: "1.0.0" }
108108 let json = writeJSON spec
109109 expectToBe true (String.contains (String.Pattern "\"type\":\"apiKey\"") json)
110110 expectToBe true (String.contains (String.Pattern "\"in\":\"header\"") json)
@@ -112,19 +112,19 @@ testApiKeyAuth = describe "ApiKey Authentication" do
112112testDigestAuth :: Effect ViTest
113113testDigestAuth = describe "DigestAuth Authentication" do
114114 _ <- test "DigestAuth does not appear as a regular header parameter" do
115− let spec = buildOpenAPISpec @DigestAuthAPI { title: "Auth API", version: "1.0.0", description: Nothing, contact: Nothing, license: Nothing }
115+ let spec = buildOpenAPISpec @DigestAuthAPI { title: "Auth API", version: "1.0.0" }
116116 let json = writeJSON spec
117117 let hasAuthParam = String.contains (String.Pattern "\"name\":\"authorization\"") json
118118 expectToBe false hasAuthParam
119119
120120 _ <- test "DigestAuth appears in security requirements" do
121− let spec = buildOpenAPISpec @DigestAuthAPI { title: "Auth API", version: "1.0.0", description: Nothing, contact: Nothing, license: Nothing }
121+ let spec = buildOpenAPISpec @DigestAuthAPI { title: "Auth API", version: "1.0.0" }
122122 let json = writeJSON spec
123123 let hasDigestAuth = String.contains (String.Pattern "digestAuth") json
124124 expectToBe true hasDigestAuth
125125
126126 test "DigestAuth security scheme has correct structure" do
127− let spec = buildOpenAPISpec @DigestAuthAPI { title: "Auth API", version: "1.0.0", description: Nothing, contact: Nothing, license: Nothing }
127+ let spec = buildOpenAPISpec @DigestAuthAPI { title: "Auth API", version: "1.0.0" }
128128 let json = writeJSON spec
129129 expectToBe true (String.contains (String.Pattern "\"type\":\"http\"") json)
130130 expectToBe true (String.contains (String.Pattern "\"scheme\":\"digest\"") json)
@@ -132,14 +132,14 @@ testDigestAuth = describe "DigestAuth Authentication" do
132132testMultipleAuthTypes :: Effect ViTest
133133testMultipleAuthTypes = describe "Multiple Authentication Types" do
134134 _ <- test "All auth types appear in security schemes" do
135− let spec = buildOpenAPISpec @MultiAuthAPI { title: "Auth API", version: "1.0.0", description: Nothing, contact: Nothing, license: Nothing }
135+ let spec = buildOpenAPISpec @MultiAuthAPI { title: "Auth API", version: "1.0.0" }
136136 let json = writeJSON spec
137137 expectToBe true (String.contains (String.Pattern "bearerAuth") json)
138138 expectToBe true (String.contains (String.Pattern "basicAuth") json)
139139 expectToBe true (String.contains (String.Pattern "xApiKeyApiKey") json)
140140
141141 test "None of the auth headers appear as regular parameters" do
142− let spec = buildOpenAPISpec @MultiAuthAPI { title: "Auth API", version: "1.0.0", description: Nothing, contact: Nothing, license: Nothing }
142+ let spec = buildOpenAPISpec @MultiAuthAPI { title: "Auth API", version: "1.0.0" }
143143 let json = writeJSON spec
144144 -- Check that all endpoints have empty parameters arrays
145145 let hasEmptyParams = String.contains (String.Pattern "\"parameters\":[]") json
test/Test/ComprehensiveExample.purs+21−23
@@ -3,11 +3,11 @@ module Test.ComprehensiveExample where
33import Prelude
44
55import Data.Array as Array
6−import Data.Maybe (Maybe(..))
76import Effect (Effect)
7+import Literals.Undefined (undefined)
88import Type.Function (type (#))
99import Yoga.HTTP.API.Path (type (:), type (/), type (:?))
10−import Yoga.HTTP.API.Route (GET, POST, PUT, DELETE, Route, Request, buildOpenAPISpec)
10+import Yoga.HTTP.API.Route (GET, POST, PUT, DELETE, Route, buildOpenAPISpec, NoRequest)
1111import Yoga.HTTP.API.Route.Auth (BearerToken, ApiKeyHeader, BasicAuth)
1212import Yoga.HTTP.API.Route.Encoding (JSON, PlainText, XML)
1313import Yoga.HTTP.API.Route.OpenAPIMetadata (Description, Example, ExampleValue, Examples, Schema)
@@ -21,65 +21,63 @@ type ComprehensiveAPI =
2121 { -- Simple GET with path parameter and authentication
2222 getUser ::
2323 Route GET ("users" / "id" : Int)
24− (Request { headers :: { authorization :: BearerToken } })
24+ { headers :: { authorization :: BearerToken } }
2525 (ok :: { body :: Schema "User" User })
2626
2727 , -- List users with query parameters
2828 listUsers ::
2929 Route GET ("users" :? { limit :: Int # Example "10", offset :: Int # Example "0" })
30− (Request {})
30+ NoRequest
3131 (ok :: { body :: Array User })
3232
3333 , -- POST with JSON body and schema
3434 createUser ::
3535 Route POST "users"
36− (Request { body :: JSON (Schema "CreateUserRequest" CreateUserRequest) })
36+ { body :: JSON (Schema "CreateUserRequest" CreateUserRequest) }
3737 (created :: { body :: Schema "User" User })
3838
3939 , -- PUT with authentication and JSON body
4040 updateUser ::
4141 Route PUT ("users" / "id" : Int)
42− ( Request
43− { headers :: { authorization :: BearerToken }
44− , body :: JSON UpdateUserRequest
45− }
46− )
42+ { headers :: { authorization :: BearerToken }
43+ , body :: JSON UpdateUserRequest
44+ }
4745 (ok :: { body :: User })
4846
4947 , -- DELETE with authentication
5048 deleteUser ::
5149 Route DELETE ("users" / "id" : Int)
52− (Request { headers :: { authorization :: BearerToken } })
50+ { headers :: { authorization :: BearerToken } }
5351 (noContent :: { body :: {} })
5452
5553 , -- Multiple content types - XML
5654 exportUser ::
5755 Route GET ("users" / "id" : Int / "export")
58− (Request {})
56+ NoRequest
5957 (ok :: { body :: XML User })
6058
6159 , -- Plain text response
6260 healthCheck ::
6361 Route GET "health"
64− (Request {})
62+ NoRequest
6563 (ok :: { body :: PlainText String })
6664
6765 , -- API Key authentication
6866 getStats ::
6967 Route GET "stats"
70− (Request { headers :: { xApiKey :: ApiKeyHeader } })
68+ { headers :: { xApiKey :: ApiKeyHeader } }
7169 (ok :: { body :: Stats })
7270
7371 , -- Basic auth
7472 login ::
7573 Route POST "login"
76− (Request { headers :: { authorization :: BasicAuth } })
74+ { headers :: { authorization :: BasicAuth } }
7775 (ok :: { body :: { token :: String # Description "JWT access token" } })
7876
7977 , -- Path with examples
8078 getUserPosts ::
8179 Route GET ("users" / "userId" : (Int # Examples (alice :: ExampleValue "1", bob :: ExampleValue "2")) / "posts")
82− (Request {})
80+ NoRequest
8381 (ok :: { body :: Array Post })
8482 }
8583
@@ -131,15 +129,15 @@ testComprehensiveExample = describe "Comprehensive API Example" do
131129 spec = buildOpenAPISpec @ComprehensiveAPI
132130 { title: "Comprehensive Example API"
133131 , version: "1.0.0"
134− , description: Just "A complete example showcasing yoga-http-api features including authentication, multiple content types, metadata, schemas, and examples"
135− , contact: Just
136− { name: Just "API Team"
137− , email: Just "api@example.com"
138− , url: Just "https://example.com/support"
132+ , description: "A complete example showcasing yoga-http-api features including authentication, multiple content types, metadata, schemas, and examples"
133+ , contact:
134+ { name: "API Team"
135+ , email: "api@example.com"
136+ , url: "https://example.com/support"
139137 }
140− , license: Just
138+ , license:
141139 { name: "MIT"
142− , url: Just "https://opensource.org/licenses/MIT"
140+ , url: "https://opensource.org/licenses/MIT"
143141 }
144142 }
145143 json = writeJSON spec
test/Test/ContentTypeTest.purs+16−52
@@ -5,7 +5,7 @@ import Prelude
55import Data.Maybe (Maybe(..))
66import Data.String as String
77import Effect (Effect)
8−import Yoga.HTTP.API.Route (GET, POST, PUT, Route, Request, buildOpenAPISpec)
8+import Yoga.HTTP.API.Route (GET, POST, PUT, Route, buildOpenAPISpec)
99import Yoga.HTTP.API.Route.Encoding (JSON, FormData, MultipartFormData, PlainText, XML, CustomContentType)
1010import Yoga.JSON (writeJSON)
1111import ViTest (ViTest, describe, test)
@@ -21,12 +21,9 @@ testRequestContentTypes :: Effect ViTest
2121testRequestContentTypes = describe "Request Content Types" do
2222 _ <- test "JSON request body generates application/json" do
2323 let
24− spec = buildOpenAPISpec @{ createUser :: Route POST "users" (Request { body :: JSON User }) (ok :: { body :: User }) }
24+ spec = buildOpenAPISpec @{ createUser :: Route POST "users" { body :: JSON User } (ok :: { body :: User }) }
2525 { title: "API"
2626 , version: "1.0.0"
27− , description: Nothing
28− , contact: Nothing
29− , license: Nothing
3027 }
3128 json = writeJSON spec
3229 expectToBe true (String.contains (String.Pattern "\"application/json\"") json)
@@ -34,12 +31,9 @@ testRequestContentTypes = describe "Request Content Types" do
3431
3532 _ <- test "FormData request body generates application/x-www-form-urlencoded" do
3633 let
37− spec = buildOpenAPISpec @{ createUser :: Route POST "users" (Request { body :: FormData User }) (ok :: { body :: JSON User }) }
34+ spec = buildOpenAPISpec @{ createUser :: Route POST "users" { body :: FormData User } (ok :: { body :: JSON User }) }
3835 { title: "API"
3936 , version: "1.0.0"
40− , description: Nothing
41− , contact: Nothing
42− , license: Nothing
4337 }
4438 json = writeJSON spec
4539 expectToBe true (String.contains (String.Pattern "\"application/x-www-form-urlencoded\"") json)
@@ -47,48 +41,36 @@ testRequestContentTypes = describe "Request Content Types" do
4741
4842 _ <- test "MultipartFormData request body generates multipart/form-data" do
4943 let
50− spec = buildOpenAPISpec @{ uploadFile :: Route POST "upload" (Request { body :: MultipartFormData FileUpload }) (ok :: { body :: { success :: Boolean } }) }
44+ spec = buildOpenAPISpec @{ uploadFile :: Route POST "upload" { body :: MultipartFormData FileUpload } (ok :: { body :: { success :: Boolean } }) }
5145 { title: "API"
5246 , version: "1.0.0"
53− , description: Nothing
54− , contact: Nothing
55− , license: Nothing
5647 }
5748 json = writeJSON spec
5849 expectToBe true (String.contains (String.Pattern "\"multipart/form-data\"") json)
5950
6051 _ <- test "PlainText request body generates text/plain" do
6152 let
62− spec = buildOpenAPISpec @{ echo :: Route POST "echo" (Request { body :: PlainText String }) (ok :: { body :: PlainText String }) }
53+ spec = buildOpenAPISpec @{ echo :: Route POST "echo" { body :: PlainText String } (ok :: { body :: PlainText String }) }
6354 { title: "API"
6455 , version: "1.0.0"
65− , description: Nothing
66− , contact: Nothing
67− , license: Nothing
6856 }
6957 json = writeJSON spec
7058 expectToBe true (String.contains (String.Pattern "\"text/plain\"") json)
7159
7260 _ <- test "XML request body generates application/xml" do
7361 let
74− spec = buildOpenAPISpec @{ updateDoc :: Route PUT "document" (Request { body :: XML XmlDocument }) (ok :: { body :: XML XmlDocument }) }
62+ spec = buildOpenAPISpec @{ updateDoc :: Route PUT "document" { body :: XML XmlDocument } (ok :: { body :: XML XmlDocument }) }
7563 { title: "API"
7664 , version: "1.0.0"
77− , description: Nothing
78− , contact: Nothing
79− , license: Nothing
8065 }
8166 json = writeJSON spec
8267 expectToBe true (String.contains (String.Pattern "\"application/xml\"") json)
8368
8469 test "CustomContentType request body generates custom MIME type" do
8570 let
86− spec = buildOpenAPISpec @{ customEndpoint :: Route POST "custom" (Request { body :: CustomContentType "application/vnd.api+json" User }) (ok :: { body :: User }) }
71+ spec = buildOpenAPISpec @{ customEndpoint :: Route POST "custom" { body :: CustomContentType "application/vnd.api+json" User } (ok :: { body :: User }) }
8772 { title: "API"
8873 , version: "1.0.0"
89− , description: Nothing
90− , contact: Nothing
91− , license: Nothing
9274 }
9375 json = writeJSON spec
9476 expectToBe true (String.contains (String.Pattern "\"application/vnd.api+json\"") json)
@@ -97,60 +79,45 @@ testResponseContentTypes :: Effect ViTest
9779testResponseContentTypes = describe "Response Content Types" do
9880 _ <- test "JSON response body generates application/json" do
9981 let
100− spec = buildOpenAPISpec @{ getUser :: Route GET "users" (Request {}) (ok :: { body :: JSON User }) }
82+ spec = buildOpenAPISpec @{ getUser :: Route GET "users" {} (ok :: { body :: JSON User }) }
10183 { title: "API"
10284 , version: "1.0.0"
103− , description: Nothing
104− , contact: Nothing
105− , license: Nothing
10685 }
10786 json = writeJSON spec
10887 expectToBe true (String.contains (String.Pattern "\"application/json\"") json)
10988
11089 _ <- test "PlainText response body generates text/plain" do
11190 let
112− spec = buildOpenAPISpec @{ getText :: Route GET "text" (Request {}) (ok :: { body :: PlainText String }) }
91+ spec = buildOpenAPISpec @{ getText :: Route GET "text" {} (ok :: { body :: PlainText String }) }
11392 { title: "API"
11493 , version: "1.0.0"
115− , description: Nothing
116− , contact: Nothing
117− , license: Nothing
11894 }
11995 json = writeJSON spec
12096 expectToBe true (String.contains (String.Pattern "\"text/plain\"") json)
12197
12298 _ <- test "XML response body generates application/xml" do
12399 let
124− spec = buildOpenAPISpec @{ getDoc :: Route GET "document" (Request {}) (ok :: { body :: XML XmlDocument }) }
100+ spec = buildOpenAPISpec @{ getDoc :: Route GET "document" {} (ok :: { body :: XML XmlDocument }) }
125101 { title: "API"
126102 , version: "1.0.0"
127− , description: Nothing
128− , contact: Nothing
129− , license: Nothing
130103 }
131104 json = writeJSON spec
132105 expectToBe true (String.contains (String.Pattern "\"application/xml\"") json)
133106
134107 _ <- test "CustomContentType response body generates custom MIME type" do
135108 let
136− spec = buildOpenAPISpec @{ customGet :: Route GET "custom" (Request {}) (ok :: { body :: CustomContentType "application/vnd.api+json" User }) }
109+ spec = buildOpenAPISpec @{ customGet :: Route GET "custom" {} (ok :: { body :: CustomContentType "application/vnd.api+json" User }) }
137110 { title: "API"
138111 , version: "1.0.0"
139− , description: Nothing
140− , contact: Nothing
141− , license: Nothing
142112 }
143113 json = writeJSON spec
144114 expectToBe true (String.contains (String.Pattern "\"application/vnd.api+json\"") json)
145115
146116 test "Unwrapped response body defaults to application/json for backward compatibility" do
147117 let
148− spec = buildOpenAPISpec @{ getUser :: Route GET "users" (Request {}) (ok :: { body :: User }) }
118+ spec = buildOpenAPISpec @{ getUser :: Route GET "users" {} (ok :: { body :: User }) }
149119 { title: "API"
150120 , version: "1.0.0"
151− , description: Nothing
152− , contact: Nothing
153− , license: Nothing
154121 }
155122 json = writeJSON spec
156123 expectToBe true (String.contains (String.Pattern "\"application/json\"") json)
@@ -161,16 +128,13 @@ testMixedContentTypes = describe "Mixed Content Types in API" do
161128 let
162129 spec = buildOpenAPISpec
163130 @
164− { createUser :: Route POST "users" (Request { body :: JSON User }) (ok :: { body :: JSON User })
165− , uploadFile :: Route POST "upload" (Request { body :: MultipartFormData FileUpload }) (ok :: { body :: { success :: Boolean } })
166− , echo :: Route POST "echo" (Request { body :: PlainText String }) (ok :: { body :: PlainText String })
167− , getDoc :: Route GET "document" (Request {}) (ok :: { body :: XML XmlDocument })
131+ { createUser :: Route POST "users" { body :: JSON User } (ok :: { body :: JSON User })
132+ , uploadFile :: Route POST "upload" { body :: MultipartFormData FileUpload } (ok :: { body :: { success :: Boolean } })
133+ , echo :: Route POST "echo" { body :: PlainText String } (ok :: { body :: PlainText String })
134+ , getDoc :: Route GET "document" {} (ok :: { body :: XML XmlDocument })
168135 }
169136 { title: "Multi-Content API"
170137 , version: "1.0.0"
171− , description: Nothing
172− , contact: Nothing
173− , license: Nothing
174138 }
175139 json = writeJSON spec
176140 expectToBe true (String.contains (String.Pattern "\"application/json\"") json)
test/Test/CookieAuthTest.purs+12−12
@@ -5,7 +5,7 @@ import Prelude
55import Data.Maybe (Maybe(..))
66import Data.String as String
77import Effect (Effect)
8−import Yoga.HTTP.API.Route (GET, Route, Request, buildOpenAPISpec)
8+import Yoga.HTTP.API.Route (GET, Route, buildOpenAPISpec)
99import Yoga.HTTP.API.Route.Auth (ApiKeyCookie)
1010import Yoga.JSON (writeJSON)
1111import ViTest (ViTest, describe, test)
@@ -16,15 +16,15 @@ import ViTest.Expect (expectToBe)
1616--------------------------------------------------------------------------------
1717
1818type ApiKeyCookieAPI =
19− { getProtected :: Route GET "protected" (Request { cookies :: { sessionId :: ApiKeyCookie } }) (ok :: { body :: { message :: String } })
19+ { getProtected :: Route GET "protected" { cookies :: { sessionId :: ApiKeyCookie } } (ok :: { body :: { message :: String } })
2020 }
2121
2222type RegularCookieParamsAPI =
23− { getData :: Route GET "data" (Request { cookies :: { preference :: String, theme :: String } }) (ok :: { body :: { data :: String } })
23+ { getData :: Route GET "data" { cookies :: { preference :: String, theme :: String } } (ok :: { body :: { data :: String } })
2424 }
2525
2626type MixedCookieAPI =
27− { getResource :: Route GET "resource" (Request { cookies :: { sessionId :: ApiKeyCookie, userId :: String } }) (ok :: { body :: { resource :: String } })
27+ { getResource :: Route GET "resource" { cookies :: { sessionId :: ApiKeyCookie, userId :: String } } (ok :: { body :: { resource :: String } })
2828 }
2929
3030--------------------------------------------------------------------------------
@@ -34,20 +34,20 @@ type MixedCookieAPI =
3434testApiKeyCookieAuth :: Effect ViTest
3535testApiKeyCookieAuth = describe "ApiKeyCookie Authentication" do
3636 _ <- test "ApiKeyCookie does not appear as a regular cookie parameter" do
37− let spec = buildOpenAPISpec @ApiKeyCookieAPI { title: "Cookie Auth API", version: "1.0.0", description: Nothing, contact: Nothing, license: Nothing }
37+ let spec = buildOpenAPISpec @ApiKeyCookieAPI { title: "Cookie Auth API", version: "1.0.0" }
3838 let json = writeJSON spec
3939 -- Check that parameters array is empty (no cookie parameters)
4040 let hasEmptyParams = String.contains (String.Pattern "\"parameters\":[]") json
4141 expectToBe true hasEmptyParams
4242
4343 _ <- test "ApiKeyCookie appears in security requirements" do
44− let spec = buildOpenAPISpec @ApiKeyCookieAPI { title: "Cookie Auth API", version: "1.0.0", description: Nothing, contact: Nothing, license: Nothing }
44+ let spec = buildOpenAPISpec @ApiKeyCookieAPI { title: "Cookie Auth API", version: "1.0.0" }
4545 let json = writeJSON spec
4646 let hasSessionIdCookie = String.contains (String.Pattern "sessionIdCookie") json
4747 expectToBe true hasSessionIdCookie
4848
4949 test "ApiKeyCookie security scheme has correct structure" do
50− let spec = buildOpenAPISpec @ApiKeyCookieAPI { title: "Cookie Auth API", version: "1.0.0", description: Nothing, contact: Nothing, license: Nothing }
50+ let spec = buildOpenAPISpec @ApiKeyCookieAPI { title: "Cookie Auth API", version: "1.0.0" }
5151 let json = writeJSON spec
5252 expectToBe true (String.contains (String.Pattern "\"type\":\"apiKey\"") json)
5353 expectToBe true (String.contains (String.Pattern "\"in\":\"cookie\"") json)
@@ -56,7 +56,7 @@ testApiKeyCookieAuth = describe "ApiKeyCookie Authentication" do
5656testRegularCookieParams :: Effect ViTest
5757testRegularCookieParams = describe "Regular Cookie Parameters" do
5858 _ <- test "Regular cookies appear as cookie parameters" do
59− let spec = buildOpenAPISpec @RegularCookieParamsAPI { title: "Cookie Params API", version: "1.0.0", description: Nothing, contact: Nothing, license: Nothing }
59+ let spec = buildOpenAPISpec @RegularCookieParamsAPI { title: "Cookie Params API", version: "1.0.0" }
6060 let json = writeJSON spec
6161 let hasPreferenceParam = String.contains (String.Pattern "\"name\":\"preference\"") json && String.contains (String.Pattern "\"in\":\"cookie\"") json
6262 let hasThemeParam = String.contains (String.Pattern "\"name\":\"theme\"") json && String.contains (String.Pattern "\"in\":\"cookie\"") json
@@ -64,7 +64,7 @@ testRegularCookieParams = describe "Regular Cookie Parameters" do
6464 expectToBe true hasThemeParam
6565
6666 test "Regular cookies do not appear in security schemes" do
67− let spec = buildOpenAPISpec @RegularCookieParamsAPI { title: "Cookie Params API", version: "1.0.0", description: Nothing, contact: Nothing, license: Nothing }
67+ let spec = buildOpenAPISpec @RegularCookieParamsAPI { title: "Cookie Params API", version: "1.0.0" }
6868 let json = writeJSON spec
6969 let hasPreferenceCookie = String.contains (String.Pattern "preferenceCookie") json && String.contains (String.Pattern "securitySchemes") json
7070 let hasThemeCookie = String.contains (String.Pattern "themeCookie") json && String.contains (String.Pattern "securitySchemes") json
@@ -74,13 +74,13 @@ testRegularCookieParams = describe "Regular Cookie Parameters" do
7474testMixedCookies :: Effect ViTest
7575testMixedCookies = describe "Mixed Cookie Types (Auth + Regular)" do
7676 _ <- test "Auth cookie appears in security schemes" do
77− let spec = buildOpenAPISpec @MixedCookieAPI { title: "Mixed Cookie API", version: "1.0.0", description: Nothing, contact: Nothing, license: Nothing }
77+ let spec = buildOpenAPISpec @MixedCookieAPI { title: "Mixed Cookie API", version: "1.0.0" }
7878 let json = writeJSON spec
7979 let hasSessionIdAuth = String.contains (String.Pattern "sessionIdCookie") json
8080 expectToBe true hasSessionIdAuth
8181
8282 _ <- test "Auth cookie does not appear as regular parameter" do
83− let spec = buildOpenAPISpec @MixedCookieAPI { title: "Mixed Cookie API", version: "1.0.0", description: Nothing, contact: Nothing, license: Nothing }
83+ let spec = buildOpenAPISpec @MixedCookieAPI { title: "Mixed Cookie API", version: "1.0.0" }
8484 let json = writeJSON spec
8585 -- sessionId should not appear as a parameter (only userId should)
8686 -- Check that the JSON doesn't contain a parameter object for sessionId
@@ -88,7 +88,7 @@ testMixedCookies = describe "Mixed Cookie Types (Auth + Regular)" do
8888 expectToBe false hasSessionIdParam
8989
9090 test "Regular cookie appears as cookie parameter" do
91− let spec = buildOpenAPISpec @MixedCookieAPI { title: "Mixed Cookie API", version: "1.0.0", description: Nothing, contact: Nothing, license: Nothing }
91+ let spec = buildOpenAPISpec @MixedCookieAPI { title: "Mixed Cookie API", version: "1.0.0" }
9292 let json = writeJSON spec
9393 let hasUserIdParam = String.contains (String.Pattern "\"name\":\"userId\"") json && String.contains (String.Pattern "\"in\":\"cookie\"") json
9494 expectToBe true hasUserIdParam
test/Test/ExamplesTest.purs+6−21
@@ -8,7 +8,7 @@ import Effect (Effect)
88import Type.Function (type (#))
99import Type.Proxy (Proxy(..))
1010import Yoga.HTTP.API.Path (type (/), type (:))
11−import Yoga.HTTP.API.Route (GET, POST, Route, Request, buildOpenAPISpec)
11+import Yoga.HTTP.API.Route (GET, POST, Route, buildOpenAPISpec)
1212import Yoga.HTTP.API.Route.Encoding (JSON)
1313import Yoga.HTTP.API.Route.OpenAPIMetadata (Example, Examples, ExampleValue, ExampleWithSummary, ExampleObject, example, examples)
1414import Yoga.JSON (writeJSON)
@@ -36,7 +36,7 @@ testExamplesMetadataExtraction = describe "Examples Metadata - Type-level extrac
3636 expectToBe true (isJust result)
3737
3838type TestAPIWithPathExamples =
39− { getUser :: Route GET ("users" / "userId" : (Int # Examples (basic :: ExampleValue "123", premium :: ExampleWithSummary "456" "Premium user"))) (Request {}) (ok :: { body :: { id :: Int, name :: String } })
39+ { getUser :: Route GET ("users" / "userId" : (Int # Examples (basic :: ExampleValue "123", premium :: ExampleWithSummary "456" "Premium user"))) {} (ok :: { body :: { id :: Int, name :: String } })
4040 }
4141
4242testExamplesInParameters :: Effect ViTest
@@ -46,9 +46,6 @@ testExamplesInParameters = describe "Examples in Parameters" do
4646 spec = buildOpenAPISpec @TestAPIWithPathExamples
4747 { title: "Test API"
4848 , version: "1.0.0"
49− , description: Nothing
50− , contact: Nothing
51− , license: Nothing
5249 }
5350 json = writeJSON spec
5451 expectToBe true (String.contains (String.Pattern "users") json)
@@ -59,7 +56,7 @@ type User =
5956 }
6057
6158type TestAPIWithRequestBodyExamples =
62− { createUser :: Route POST "users" (Request { body :: JSON User }) (ok :: { body :: { id :: Int } })
59+ { createUser :: Route POST "users" { body :: JSON User } (ok :: { body :: { id :: Int } })
6360 }
6461
6562testExamplesInRequestBody :: Effect ViTest
@@ -69,15 +66,12 @@ testExamplesInRequestBody = describe "Examples in Request Body" do
6966 spec = buildOpenAPISpec @TestAPIWithRequestBodyExamples
7067 { title: "Test API"
7168 , version: "1.0.0"
72− , description: Nothing
73− , contact: Nothing
74− , license: Nothing
7569 }
7670 json = writeJSON spec
7771 expectToBe true (String.contains (String.Pattern "users") json)
7872
7973type TestAPIWithComplexExamples =
80− { getItem :: Route GET ("items" / "itemId" : (Int # Examples (basic :: ExampleWithSummary "42" "A basic item", special :: ExampleObject "999" "Special item" "A very special item" ""))) (Request {}) (ok :: { body :: { id :: Int } })
74+ { getItem :: Route GET ("items" / "itemId" : (Int # Examples (basic :: ExampleWithSummary "42" "A basic item", special :: ExampleObject "999" "Special item" "A very special item" ""))) {} (ok :: { body :: { id :: Int } })
8175 }
8276
8377testExamplesWithSummary :: Effect ViTest
@@ -87,9 +81,6 @@ testExamplesWithSummary = describe "Examples with Summary" do
8781 spec = buildOpenAPISpec @TestAPIWithComplexExamples
8882 { title: "Test API"
8983 , version: "1.0.0"
90− , description: Nothing
91− , contact: Nothing
92− , license: Nothing
9384 }
9485 json = writeJSON spec
9586 expectToBe true (String.contains (String.Pattern "items") json)
@@ -111,7 +102,7 @@ testExamplesBackwardCompatibility = describe "Backward Compatibility - Single Ex
111102 expectToBe true (isJust exsResult)
112103
113104type TestAPIWithQueryExamples =
114− { searchItems :: Route GET "items" (Request { query :: { limit :: Int # Examples (small :: ExampleValue "10", large :: ExampleValue "100") } }) (ok :: { body :: { items :: Array String } })
105+ { searchItems :: Route GET "items" { query :: { limit :: Int # Examples (small :: ExampleValue "10", large :: ExampleValue "100") } } (ok :: { body :: { items :: Array String } })
115106 }
116107
117108testExamplesInQueryParams :: Effect ViTest
@@ -121,15 +112,12 @@ testExamplesInQueryParams = describe "Examples in Query Parameters" do
121112 spec = buildOpenAPISpec @TestAPIWithQueryExamples
122113 { title: "Test API"
123114 , version: "1.0.0"
124− , description: Nothing
125− , contact: Nothing
126− , license: Nothing
127115 }
128116 json = writeJSON spec
129117 expectToBe true (String.contains (String.Pattern "items") json)
130118
131119type TestAPIWithExternalExample =
132− { getResource :: Route GET ("resource" / "id" : (String # Examples (inline :: ExampleObject "abc123" "Inline example" "An example with inline value" "", external :: ExampleObject "" "External example" "An example from external URL" "https://example.com/resource.json"))) (Request {}) (ok :: { body :: { data :: String } })
120+ { getResource :: Route GET ("resource" / "id" : (String # Examples (inline :: ExampleObject "abc123" "Inline example" "An example with inline value" "", external :: ExampleObject "" "External example" "An example from external URL" "https://example.com/resource.json"))) {} (ok :: { body :: { data :: String } })
133121 }
134122
135123testComplexExampleObject :: Effect ViTest
@@ -139,9 +127,6 @@ testComplexExampleObject = describe "Complex Example Objects" do
139127 spec = buildOpenAPISpec @TestAPIWithExternalExample
140128 { title: "Test API"
141129 , version: "1.0.0"
142− , description: Nothing
143− , contact: Nothing
144− , license: Nothing
145130 }
146131 json = writeJSON spec
147132 expectToBe true (String.contains (String.Pattern "resource") json)
test/Test/OpenAPIInfoTest.purs+22−55
@@ -2,16 +2,15 @@ module Test.OpenAPIInfoTest where
22
33import Prelude
44
5−import Data.Maybe (Maybe(..))
65import Data.String as String
76import Effect (Effect)
8−import Yoga.HTTP.API.Route (GET, Route, Request, buildOpenAPISpec)
7+import Yoga.HTTP.API.Route (GET, Route, buildOpenAPISpec)
98import Yoga.JSON (writeJSON)
109import ViTest (ViTest, describe, test)
1110import ViTest.Expect (expectToBe)
1211
1312type SimpleAPI =
14− { getRoot :: Route GET "" (Request {}) (ok :: { body :: { message :: String } })
13+ { getRoot :: Route GET "" {} (ok :: { body :: { message :: String } })
1514 }
1615
1716testInfoDescription :: Effect ViTest
@@ -21,9 +20,7 @@ testInfoDescription = describe "OpenAPI Info - Description" do
2120 spec = buildOpenAPISpec @SimpleAPI
2221 { title: "Test API"
2322 , version: "1.0.0"
24− , description: Just "This is a test API for demonstrating OpenAPI info enhancements"
25− , contact: Nothing
26− , license: Nothing
23+ , description: "This is a test API for demonstrating OpenAPI info enhancements"
2724 }
2825 json = writeJSON spec
2926 expectToBe true (String.contains (String.Pattern "\"description\":\"This is a test API for demonstrating OpenAPI info enhancements\"") json)
@@ -33,9 +30,6 @@ testInfoDescription = describe "OpenAPI Info - Description" do
3330 spec = buildOpenAPISpec @SimpleAPI
3431 { title: "Test API"
3532 , version: "1.0.0"
36− , description: Nothing
37− , contact: Nothing
38− , license: Nothing
3933 }
4034 json = writeJSON spec
4135 -- Check that description is not in the info object (it will appear in responses, which is required by OpenAPI)
@@ -49,13 +43,11 @@ testInfoContact = describe "OpenAPI Info - Contact" do
4943 spec = buildOpenAPISpec @SimpleAPI
5044 { title: "Test API"
5145 , version: "1.0.0"
52− , description: Nothing
53− , contact: Just
54− { name: Just "API Support"
55− , url: Just "https://api.example.com/support"
56− , email: Just "support@example.com"
46+ , contact:
47+ { name: "API Support"
48+ , url: "https://api.example.com/support"
49+ , email: "support@example.com"
5750 }
58− , license: Nothing
5951 }
6052 json = writeJSON spec
6153 expectToBe true (String.contains (String.Pattern "\"contact\"") json)
@@ -68,13 +60,7 @@ testInfoContact = describe "OpenAPI Info - Contact" do
6860 spec = buildOpenAPISpec @SimpleAPI
6961 { title: "Test API"
7062 , version: "1.0.0"
71− , description: Nothing
72− , contact: Just
73− { name: Just "API Team"
74− , url: Nothing
75− , email: Nothing
76− }
77− , license: Nothing
63+ , contact: { name: "API Team" }
7864 }
7965 json = writeJSON spec
8066 expectToBe true (String.contains (String.Pattern "\"contact\"") json)
@@ -85,13 +71,7 @@ testInfoContact = describe "OpenAPI Info - Contact" do
8571 spec = buildOpenAPISpec @SimpleAPI
8672 { title: "Test API"
8773 , version: "1.0.0"
88− , description: Nothing
89− , contact: Just
90− { name: Nothing
91− , url: Nothing
92− , email: Just "info@example.com"
93− }
94− , license: Nothing
74+ , contact: { email: "info@example.com" }
9575 }
9676 json = writeJSON spec
9777 expectToBe true (String.contains (String.Pattern "\"contact\"") json)
@@ -102,13 +82,10 @@ testInfoContact = describe "OpenAPI Info - Contact" do
10282 spec = buildOpenAPISpec @SimpleAPI
10383 { title: "Test API"
10484 , version: "1.0.0"
105− , description: Nothing
106− , contact: Just
107− { name: Just "Support Team"
108− , url: Just "https://support.example.com"
109− , email: Nothing
85+ , contact:
86+ { name: "Support Team"
87+ , url: "https://support.example.com"
11088 }
111− , license: Nothing
11289 }
11390 json = writeJSON spec
11491 expectToBe true (String.contains (String.Pattern "\"contact\"") json)
@@ -120,9 +97,6 @@ testInfoContact = describe "OpenAPI Info - Contact" do
12097 spec = buildOpenAPISpec @SimpleAPI
12198 { title: "Test API"
12299 , version: "1.0.0"
123− , description: Nothing
124− , contact: Nothing
125− , license: Nothing
126100 }
127101 json = writeJSON spec
128102 expectToBe false (String.contains (String.Pattern "\"contact\"") json)
@@ -134,9 +108,7 @@ testInfoLicense = describe "OpenAPI Info - License" do
134108 spec = buildOpenAPISpec @SimpleAPI
135109 { title: "Test API"
136110 , version: "1.0.0"
137− , description: Nothing
138− , contact: Nothing
139− , license: Just { name: "MIT", url: Nothing }
111+ , license: { name: "MIT" }
140112 }
141113 json = writeJSON spec
142114 expectToBe true (String.contains (String.Pattern "\"license\"") json)
@@ -147,11 +119,9 @@ testInfoLicense = describe "OpenAPI Info - License" do
147119 spec = buildOpenAPISpec @SimpleAPI
148120 { title: "Test API"
149121 , version: "1.0.0"
150− , description: Nothing
151− , contact: Nothing
152− , license: Just
122+ , license:
153123 { name: "Apache 2.0"
154− , url: Just "https://www.apache.org/licenses/LICENSE-2.0.html"
124+ , url: "https://www.apache.org/licenses/LICENSE-2.0.html"
155125 }
156126 }
157127 json = writeJSON spec
@@ -164,9 +134,6 @@ testInfoLicense = describe "OpenAPI Info - License" do
164134 spec = buildOpenAPISpec @SimpleAPI
165135 { title: "Test API"
166136 , version: "1.0.0"
167− , description: Nothing
168− , contact: Nothing
169− , license: Nothing
170137 }
171138 json = writeJSON spec
172139 expectToBe false (String.contains (String.Pattern "\"license\"") json)
@@ -178,15 +145,15 @@ testInfoAllFields = describe "OpenAPI Info - All Fields Together" do
178145 spec = buildOpenAPISpec @SimpleAPI
179146 { title: "Complete API"
180147 , version: "2.0.0"
181− , description: Just "A comprehensive API with full metadata"
182− , contact: Just
183− { name: Just "API Team"
184− , url: Just "https://example.com/contact"
185− , email: Just "api@example.com"
148+ , description: "A comprehensive API with full metadata"
149+ , contact:
150+ { name: "API Team"
151+ , url: "https://example.com/contact"
152+ , email: "api@example.com"
186153 }
187− , license: Just
154+ , license:
188155 { name: "MIT"
189− , url: Just "https://opensource.org/licenses/MIT"
156+ , url: "https://opensource.org/licenses/MIT"
190157 }
191158 }
192159 json = writeJSON spec
test/Test/ProjectTrackerExample.pursnew file+499
@@ -0,0 +1,499 @@
1+module Test.ProjectTrackerExample where
2+
3+import Prelude
4+
5+import Data.Array as Array
6+import Data.Generic.Rep (class Generic)
7+import Data.Maybe (Maybe)
8+import Data.Tuple.Nested (type (/\))
9+import Effect (Effect)
10+import Literals.Undefined (undefined)
11+import Effect.Aff (Aff)
12+import Type.Function (type (#))
13+import Yoga.HTTP.API.Path (type (:), type (/), type (:?))
14+import Yoga.HTTP.API.Route (GET, POST, PUT, PATCH, DELETE, Route, buildOpenAPISpec, NoRequest)
15+import Yoga.HTTP.API.Route.Auth (BearerToken)
16+import Yoga.HTTP.API.Route.Encoding (JSON, MultipartFormData)
17+import Yoga.HTTP.API.Route.OpenAPI (class CollectSchemaNames, class CollectSchemas, class RenderJSONSchema)
18+import Foreign.Object as Object
19+import Yoga.HTTP.API.Route.OpenAPIMetadata (Description, Example, Examples, ExampleValue, Schema)
20+import Yoga.HTTP.API.Route.StatusCode (StatusCode(..))
21+import Yoga.JSON as JSON
22+import Yoga.JSON (writeJSON)
23+import Yoga.JSON.Generics (genericReadForeignEnum, genericWriteForeignEnum)
24+import Test.OpenAPIValidation (validate)
25+import ViTest (ViTest, describe, test)
26+import ViTest.Expect (expectToBe)
27+
28+--------------------------------------------------------------------------------
29+-- Domain Models
30+--------------------------------------------------------------------------------
31+
32+-- | Project with metadata
33+type Project =
34+ { id :: Int # Description "Unique project identifier"
35+ , name :: String # Description "Project name"
36+ , description :: String # Description "Project description"
37+ , status :: ProjectStatus # Description "Current project status"
38+ , createdAt :: Int # Description "Project creation timestamp (Unix epoch)"
39+ , createdBy :: Int # Description "User ID who created the project"
40+ }
41+
42+-- | Project status enum
43+data ProjectStatus = Active | Archived | OnHold
44+
45+derive instance Eq ProjectStatus
46+derive instance Generic ProjectStatus _
47+
48+instance JSON.ReadForeign ProjectStatus where
49+ readImpl = genericReadForeignEnum { toConstructorName: identity }
50+
51+instance JSON.WriteForeign ProjectStatus where
52+ writeImpl = genericWriteForeignEnum { toConstructorName: identity }
53+
54+instance RenderJSONSchema ProjectStatus where
55+ renderJSONSchema _ = JSON.writeImpl
56+ { type: "string"
57+ , enum: [ "Active", "Archived", "OnHold" ]
58+ }
59+
60+instance CollectSchemas ProjectStatus where
61+ collectSchemas _ = Object.empty
62+
63+instance CollectSchemaNames ProjectStatus ()
64+
65+-- | Task with full details
66+type Task =
67+ { id :: Int # Description "Unique task identifier"
68+ , projectId :: Int # Description "Parent project ID"
69+ , title :: String # Description "Task title"
70+ , description :: String # Description "Task description"
71+ , status :: TaskStatus # Description "Current task status"
72+ , priority :: Priority # Description "Task priority level"
73+ , assigneeId :: Maybe Int # Description "Assigned user ID"
74+ , createdAt :: Int # Description "Task creation timestamp (Unix epoch)"
75+ , updatedAt :: Int # Description "Last update timestamp (Unix epoch)"
76+ , dueDate :: Maybe Int # Description "Optional due date (Unix epoch)"
77+ }
78+
79+-- | Task status workflow
80+data TaskStatus = Todo | InProgress | InReview | Done | Blocked
81+
82+derive instance Eq TaskStatus
83+derive instance Generic TaskStatus _
84+
85+instance JSON.ReadForeign TaskStatus where
86+ readImpl = genericReadForeignEnum { toConstructorName: identity }
87+
88+instance JSON.WriteForeign TaskStatus where
89+ writeImpl = genericWriteForeignEnum { toConstructorName: identity }
90+
91+instance RenderJSONSchema TaskStatus where
92+ renderJSONSchema _ = JSON.writeImpl
93+ { type: "string"
94+ , enum: [ "Todo", "InProgress", "InReview", "Done", "Blocked" ]
95+ }
96+
97+instance CollectSchemas TaskStatus where
98+ collectSchemas _ = Object.empty
99+
100+instance CollectSchemaNames TaskStatus ()
101+
102+-- | Task priority levels
103+data Priority = Low | Medium | High | Urgent
104+
105+derive instance Eq Priority
106+derive instance Generic Priority _
107+
108+instance JSON.ReadForeign Priority where
109+ readImpl = genericReadForeignEnum { toConstructorName: identity }
110+
111+instance JSON.WriteForeign Priority where
112+ writeImpl = genericWriteForeignEnum { toConstructorName: identity }
113+
114+instance RenderJSONSchema Priority where
115+ renderJSONSchema _ = JSON.writeImpl
116+ { type: "string"
117+ , enum: [ "Low", "Medium", "High", "Urgent" ]
118+ }
119+
120+instance CollectSchemas Priority where
121+ collectSchemas _ = Object.empty
122+
123+instance CollectSchemaNames Priority ()
124+
125+-- | Comment on a task
126+type Comment =
127+ { id :: Int # Description "Unique comment identifier"
128+ , taskId :: Int # Description "Parent task ID"
129+ , authorId :: Int # Description "Comment author user ID"
130+ , content :: String # Description "Comment text content"
131+ , createdAt :: Int # Description "Comment creation timestamp (Unix epoch)"
132+ }
133+
134+-- | File attachment metadata
135+type Attachment =
136+ { id :: Int # Description "Unique attachment identifier"
137+ , taskId :: Int # Description "Parent task ID"
138+ , fileName :: String # Description "Original file name"
139+ , fileSize :: Int # Description "File size in bytes"
140+ , contentType :: String # Description "MIME type"
141+ , uploadedBy :: Int # Description "User ID who uploaded"
142+ , uploadedAt :: Int # Description "Upload timestamp (Unix epoch)"
143+ , url :: String # Description "Download URL"
144+ }
145+
146+-- | Task history entry
147+type TaskHistoryEntry =
148+ { id :: Int # Description "Unique history entry identifier"
149+ , taskId :: Int # Description "Parent task ID"
150+ , changedBy :: Int # Description "User ID who made the change"
151+ , changedAt :: Int # Description "Change timestamp (Unix epoch)"
152+ , field :: String # Description "Field that was changed"
153+ , oldValue :: Maybe String # Description "Previous value"
154+ , newValue :: String # Description "New value"
155+ }
156+
157+-- | User representation
158+type User =
159+ { id :: Int # Description "Unique user identifier"
160+ , username :: String # Description "Username"
161+ , email :: String # Description "Email address"
162+ , fullName :: String # Description "Full name"
163+ , role :: UserRole # Description "User role"
164+ }
165+
166+-- | User roles for authorization
167+data UserRole = Admin | ProjectManager | Developer | Viewer
168+
169+derive instance Eq UserRole
170+derive instance Generic UserRole _
171+
172+instance JSON.ReadForeign UserRole where
173+ readImpl = genericReadForeignEnum { toConstructorName: identity }
174+
175+instance JSON.WriteForeign UserRole where
176+ writeImpl = genericWriteForeignEnum { toConstructorName: identity }
177+
178+instance RenderJSONSchema UserRole where
179+ renderJSONSchema _ = JSON.writeImpl
180+ { type: "string"
181+ , enum: [ "Admin", "ProjectManager", "Developer", "Viewer" ]
182+ }
183+
184+instance CollectSchemas UserRole where
185+ collectSchemas _ = Object.empty
186+
187+instance CollectSchemaNames UserRole ()
188+
189+--------------------------------------------------------------------------------
190+-- Request/Response Types
191+--------------------------------------------------------------------------------
192+
193+-- | Request to create a project
194+type CreateProjectRequest =
195+ { name :: String # Example "\"Mobile App Redesign\""
196+ , description :: String # Example "\"Redesign the mobile app with new UI/UX\""
197+ }
198+
199+-- | Request to update a project
200+type UpdateProjectRequest =
201+ { name :: Maybe String
202+ , description :: Maybe String
203+ , status :: Maybe ProjectStatus
204+ }
205+
206+-- | Request to create a task
207+type CreateTaskRequest =
208+ { title :: String # Examples (short :: ExampleValue "\"Fix bug\"", detailed :: ExampleValue "\"Fix login validation bug\"")
209+ , description :: String # Example "\"The login form doesn't validate email properly\""
210+ , priority :: Priority # Example "High"
211+ , assigneeId :: Maybe Int # Example "42"
212+ , dueDate :: Maybe Int
213+ }
214+
215+-- | Request to update a task
216+type UpdateTaskRequest =
217+ { title :: Maybe String
218+ , description :: Maybe String
219+ , status :: Maybe TaskStatus
220+ , priority :: Maybe Priority
221+ , assigneeId :: Maybe Int
222+ , dueDate :: Maybe Int
223+ }
224+
225+-- | Request to assign a task
226+type AssignTaskRequest =
227+ { assigneeId :: Int # Example "42"
228+ }
229+
230+-- | Request to add a comment
231+type CreateCommentRequest =
232+ { content :: String # Example "\"I've started working on this\""
233+ }
234+
235+-- | File upload in multipart form
236+type FileUpload =
237+ { file :: String # Description "File contents (binary data)"
238+ }
239+
240+-- | Pagination query parameters
241+type PaginationParams =
242+ { limit :: Int # Example "20" # Description "Number of items per page"
243+ , offset :: Int # Example "0" # Description "Number of items to skip"
244+ }
245+
246+-- | Task filter parameters
247+type TaskFilterParams =
248+ { status :: Maybe TaskStatus
249+ , priority :: Maybe Priority
250+ , assigneeId :: Maybe Int
251+ } /\ PaginationParams
252+
253+-- | Error response
254+type ErrorResponse =
255+ { error :: String # Description "Error message"
256+ , code :: String # Description "Error code"
257+ }
258+
259+--------------------------------------------------------------------------------
260+-- API Definition
261+--------------------------------------------------------------------------------
262+
263+-- | Complete Project Tracker API
264+type ProjectTrackerAPI =
265+ { -- Projects
266+ listProjects ::
267+ Route GET ("projects" :? PaginationParams)
268+ { headers :: { authorization :: BearerToken } }
269+ ( ok :: { body :: Schema "ProjectList" (Array Project) }
270+ )
271+
272+ , getProject ::
273+ Route GET ("projects" / "id" : Int)
274+ { headers :: { authorization :: BearerToken } }
275+ ( ok :: { body :: Schema "Project" Project }
276+ , notFound :: { body :: ErrorResponse }
277+ )
278+
279+ , createProject ::
280+ Route POST "projects"
281+ { headers :: { authorization :: BearerToken }
282+ , body :: JSON (Schema "CreateProjectRequest" CreateProjectRequest)
283+ }
284+ ( created :: { body :: Schema "Project" Project }
285+ , unauthorized :: { body :: ErrorResponse }
286+ )
287+
288+ , updateProject ::
289+ Route PUT ("projects" / "id" : Int)
290+ { headers :: { authorization :: BearerToken }
291+ , body :: JSON (Schema "UpdateProjectRequest" UpdateProjectRequest)
292+ }
293+ ( ok :: { body :: Schema "Project" Project }
294+ , notFound :: { body :: ErrorResponse }
295+ , unauthorized :: { body :: ErrorResponse }
296+ )
297+
298+ , archiveProject ::
299+ Route DELETE ("projects" / "id" : Int)
300+ { headers :: { authorization :: BearerToken } }
301+ ( noContent :: { body :: {} }
302+ , notFound :: { body :: ErrorResponse }
303+ , unauthorized :: { body :: ErrorResponse }
304+ )
305+
306+ -- Tasks
307+ , listTasks ::
308+ Route GET ("projects" / "projectId" : Int / "tasks" :? TaskFilterParams)
309+ { headers :: { authorization :: BearerToken } }
310+ ( ok :: { body :: Schema "TaskList" (Array Task) }
311+ , notFound :: { body :: ErrorResponse }
312+ )
313+
314+ , getTask ::
315+ Route GET ("tasks" / "id" : Int)
316+ { headers :: { authorization :: BearerToken } }
317+ ( ok :: { body :: Schema "Task" Task }
318+ , notFound :: { body :: ErrorResponse }
319+ )
320+
321+ , createTask ::
322+ Route POST ("projects" / "projectId" : Int / "tasks")
323+ { headers :: { authorization :: BearerToken }
324+ , body :: JSON (Schema "CreateTaskRequest" CreateTaskRequest)
325+ }
326+ ( created :: { body :: Schema "Task" Task }
327+ , notFound :: { body :: ErrorResponse }
328+ , unauthorized :: { body :: ErrorResponse }
329+ )
330+
331+ , updateTask ::
332+ Route PUT ("tasks" / "id" : Int)
333+ { headers :: { authorization :: BearerToken }
334+ , body :: JSON (Schema "UpdateTaskRequest" UpdateTaskRequest)
335+ }
336+ ( ok :: { body :: Schema "Task" Task }
337+ , notFound :: { body :: ErrorResponse }
338+ , unauthorized :: { body :: ErrorResponse }
339+ )
340+
341+ , assignTask ::
342+ Route PATCH ("tasks" / "id" : Int / "assign")
343+ { headers :: { authorization :: BearerToken }
344+ , body :: JSON (Schema "AssignTaskRequest" AssignTaskRequest)
345+ }
346+ ( ok :: { body :: Schema "Task" Task }
347+ , notFound :: { body :: ErrorResponse }
348+ , unauthorized :: { body :: ErrorResponse }
349+ )
350+
351+ , deleteTask ::
352+ Route DELETE ("tasks" / "id" : Int)
353+ { headers :: { authorization :: BearerToken } }
354+ ( noContent :: { body :: {} }
355+ , notFound :: { body :: ErrorResponse }
356+ , unauthorized :: { body :: ErrorResponse }
357+ )
358+
359+ -- Comments
360+ , listComments ::
361+ Route GET ("tasks" / "taskId" : Int / "comments" :? PaginationParams)
362+ { headers :: { authorization :: BearerToken } }
363+ ( ok :: { body :: Schema "CommentList" (Array Comment) }
364+ , notFound :: { body :: ErrorResponse }
365+ )
366+
367+ , addComment ::
368+ Route POST ("tasks" / "taskId" : Int / "comments")
369+ { headers :: { authorization :: BearerToken }
370+ , body :: JSON (Schema "CreateCommentRequest" CreateCommentRequest)
371+ }
372+ ( created :: { body :: Schema "Comment" Comment }
373+ , notFound :: { body :: ErrorResponse }
374+ )
375+
376+ -- Attachments
377+ , listAttachments ::
378+ Route GET ("tasks" / "taskId" : Int / "attachments")
379+ { headers :: { authorization :: BearerToken } }
380+ ( ok :: { body :: Schema "AttachmentList" (Array Attachment) }
381+ , notFound :: { body :: ErrorResponse }
382+ )
383+
384+ , uploadAttachment ::
385+ Route POST ("tasks" / "taskId" : Int / "attachments")
386+ { headers :: { authorization :: BearerToken }
387+ , body :: MultipartFormData (Schema "FileUpload" FileUpload)
388+ }
389+ ( created :: { body :: Schema "Attachment" Attachment }
390+ , notFound :: { body :: ErrorResponse }
391+ , badRequest :: { body :: ErrorResponse }
392+ )
393+
394+ , deleteAttachment ::
395+ Route DELETE ("attachments" / "id" : Int)
396+ { headers :: { authorization :: BearerToken } }
397+ ( noContent :: { body :: {} }
398+ , notFound :: { body :: ErrorResponse }
399+ , unauthorized :: { body :: ErrorResponse }
400+ )
401+
402+ -- Task History
403+ , getTaskHistory ::
404+ Route GET ("tasks" / "taskId" : Int / "history")
405+ { headers :: { authorization :: BearerToken } }
406+ ( ok :: { body :: Schema "TaskHistory" (Array TaskHistoryEntry) }
407+ , notFound :: { body :: ErrorResponse }
408+ )
409+ }
410+
411+--------------------------------------------------------------------------------
412+-- Service Layer Types (for OmLayer pattern)
413+--------------------------------------------------------------------------------
414+
415+-- | Project repository interface
416+type ProjectRepo =
417+ { findAll :: PaginationParams -> Aff (Array Project)
418+ , findById :: Int -> Aff (Maybe Project)
419+ , create :: CreateProjectRequest -> Int -> Aff Project
420+ , update :: Int -> UpdateProjectRequest -> Aff (Maybe Project)
421+ , archive :: Int -> Aff Boolean
422+ }
423+
424+-- | Task repository interface
425+type TaskRepo =
426+ { findByProject :: Int -> TaskFilterParams -> Aff (Array Task)
427+ , findById :: Int -> Aff (Maybe Task)
428+ , create :: Int -> CreateTaskRequest -> Int -> Aff Task
429+ , update :: Int -> UpdateTaskRequest -> Aff (Maybe Task)
430+ , delete :: Int -> Aff Boolean
431+ , assign :: Int -> Int -> Aff (Maybe Task)
432+ }
433+
434+-- | Comment repository interface
435+type CommentRepo =
436+ { findByTask :: Int -> PaginationParams -> Aff (Array Comment)
437+ , create :: Int -> CreateCommentRequest -> Int -> Aff Comment
438+ }
439+
440+-- | Attachment repository interface
441+type AttachmentRepo =
442+ { findByTask :: Int -> Aff (Array Attachment)
443+ , create :: Int -> FileUpload -> Int -> Aff Attachment
444+ , delete :: Int -> Aff Boolean
445+ }
446+
447+-- | Task history repository interface
448+type TaskHistoryRepo =
449+ { findByTask :: Int -> Aff (Array TaskHistoryEntry)
450+ , recordChange :: Int -> Int -> String -> Maybe String -> String -> Aff TaskHistoryEntry
451+ }
452+
453+-- | Authentication service interface
454+type AuthService =
455+ { authenticate :: BearerToken -> Aff (Maybe User)
456+ , authorize :: User -> String -> Aff Boolean
457+ }
458+
459+-- Note: Repository implementations moved to Test.ProjectTrackerLayers module
460+
461+--------------------------------------------------------------------------------
462+-- Tests
463+--------------------------------------------------------------------------------
464+
465+testProjectTrackerAPI :: Effect ViTest
466+testProjectTrackerAPI = describe "Project Tracker API" do
467+ _ <- test "generates valid OpenAPI 3.0 schema" do
468+ let
469+ spec = buildOpenAPISpec @ProjectTrackerAPI
470+ { title: "Project Tracker API"
471+ , version: "1.0.0"
472+ , description: "A comprehensive project and task management API showcasing authentication, CRUD operations, file uploads, and audit trails"
473+ , contact:
474+ { name: "API Team"
475+ , email: "api@projecttracker.com"
476+ , url: "https://projecttracker.com/support"
477+ }
478+ , license:
479+ { name: "MIT"
480+ , url: "https://opensource.org/licenses/MIT"
481+ }
482+ }
483+ result = validate spec
484+ expectToBe true (Array.null result.errors)
485+
486+ _ <- test "includes all project endpoints" do
487+ let spec = buildOpenAPISpec @ProjectTrackerAPI { title: "Project Tracker", version: "1.0.0" }
488+ let json = writeJSON spec
489+ expectToBe true (json /= "")
490+
491+ _ <- test "includes authentication in security schemes" do
492+ let spec = buildOpenAPISpec @ProjectTrackerAPI { title: "Project Tracker", version: "1.0.0" }
493+ let json = writeJSON spec
494+ expectToBe true (json /= "")
495+
496+ test "generates comprehensive API spec with all features" do
497+ let spec = buildOpenAPISpec @ProjectTrackerAPI { title: "Project Tracker", version: "1.0.0" }
498+ let json = writeJSON spec
499+ expectToBe true (json /= "")
test/Test/ProjectTrackerIntegration.purs.disablednew file+195
@@ -0,0 +1,195 @@
1+module Test.ProjectTrackerIntegration where
2+
3+import Prelude
4+
5+import Data.Array as Array
6+import Data.Maybe (Maybe(..))
7+import Data.Tuple.Nested ((/\))
8+import Effect (Effect)
9+import Effect.Aff (launchAff_)
10+import Effect.Class (liftEffect)
11+import Effect.Console as Console
12+import Test.ProjectTrackerExample (CreateCommentRequest, CreateProjectRequest, CreateTaskRequest, Priority(..), TaskFilterParams)
13+import Test.ProjectTrackerLayers (AppConfig, DbConfig, Services, runWithServices)
14+import ViTest (ViTest, describe, test)
15+import ViTest.Expect (expectToBe)
16+import Yoga.Om as Om
17+
18+--------------------------------------------------------------------------------
19+-- Integration Tests using OmLayer Pattern
20+--------------------------------------------------------------------------------
21+
22+testProjectTrackerIntegration :: Effect ViTest
23+testProjectTrackerIntegration = describe "Project Tracker Integration (OmLayer)" do
24+ test "complete workflow: create project → create tasks → add comments" do
25+ launchAff_ do
26+ let
27+ appConfig = { apiName: "Project Tracker", version: "1.0.0" }
28+ dbConfig = { connectionString: "memory://", poolSize: 10 }
29+
30+ runWithServices appConfig dbConfig do
31+ services <- Om.ask
32+
33+ -- Step 1: Create a project
34+ let createProjectReq = { name: "Mobile App Redesign", description: "Complete UI/UX overhaul" }
35+ project <- services.projectRepo.create createProjectReq 1 # Om.liftAff
36+ liftEffect $ Console.log $ "✓ Created project #" <> show project.id <> ": " <> project.name
37+ liftEffect $ expectToBe "Mobile App Redesign" project.name
38+
39+ -- Step 2: Create multiple tasks
40+ let
41+ task1Req = { title: "Design mockups", description: "Create Figma designs", priority: High, assigneeId: Just 2, dueDate: Nothing }
42+ task2Req = { title: "Implement login screen", description: "Build the login UI", priority: High, assigneeId: Just 3, dueDate: Nothing }
43+ task3Req = { title: "Write unit tests", description: "Test coverage for login", priority: Medium, assigneeId: Nothing, dueDate: Nothing }
44+
45+ task1 <- services.taskRepo.create project.id task1Req 1 # Om.liftAff
46+ task2 <- services.taskRepo.create project.id task2Req 1 # Om.liftAff
47+ task3 <- services.taskRepo.create project.id task3Req 1 # Om.liftAff
48+
49+ liftEffect $ Console.log $ "✓ Created task #" <> show task1.id <> ": " <> task1.title
50+ liftEffect $ Console.log $ "✓ Created task #" <> show task2.id <> ": " <> task2.title
51+ liftEffect $ Console.log $ "✓ Created task #" <> show task3.id <> ": " <> task3.title
52+
53+ -- Step 3: List all tasks for the project
54+ let filterParams = { status: Nothing, priority: Nothing, assigneeId: Nothing } /\ { limit: 10, offset: 0 }
55+ tasks <- services.taskRepo.findByProject project.id filterParams # Om.liftAff
56+ liftEffect $ Console.log $ "✓ Found " <> show (Array.length tasks) <> " tasks"
57+ liftEffect $ expectToBe 3 (Array.length tasks)
58+
59+ -- Step 4: Add comments to a task
60+ let
61+ comment1Req = { content: "I've started working on the designs" }
62+ comment2Req = { content: "First draft is ready for review" }
63+
64+ comment1 <- services.commentRepo.create task1.id comment1Req 2 # Om.liftAff
65+ comment2 <- services.commentRepo.create task1.id comment2Req 2 # Om.liftAff
66+
67+ liftEffect $ Console.log $ "✓ Added comment #" <> show comment1.id <> " to task #" <> show task1.id
68+ liftEffect $ Console.log $ "✓ Added comment #" <> show comment2.id <> " to task #" <> show task1.id
69+
70+ -- Step 5: List comments for the task
71+ let pagination = { limit: 10, offset: 0 }
72+ comments <- services.commentRepo.findByTask task1.id pagination # Om.liftAff
73+ liftEffect $ Console.log $ "✓ Found " <> show (Array.length comments) <> " comments"
74+ liftEffect $ expectToBe 2 (Array.length comments)
75+
76+ -- Step 6: Assign a task
77+ assignedTask <- services.taskRepo.assign task3.id 4 # Om.liftAff
78+ case assignedTask of
79+ Nothing -> liftEffect $ Console.log "✗ Failed to assign task"
80+ Just t -> do
81+ let
82+ assigneeIdNum = case t.assigneeId of
83+ Nothing -> 0
84+ Just uid -> uid
85+ liftEffect $ Console.log $ "✓ Assigned task #" <> show t.id <> " to user #" <> show assigneeIdNum
86+ liftEffect $ expectToBe (Just 4) t.assigneeId
87+
88+ -- Step 7: Update a task
89+ let updateReq = { title: Just "Design mockups (updated)", description: Nothing, status: Nothing, priority: Nothing, assigneeId: Nothing, dueDate: Nothing }
90+ updatedTask <- services.taskRepo.update task1.id updateReq # Om.liftAff
91+ case updatedTask of
92+ Nothing -> liftEffect $ Console.log "✗ Failed to update task"
93+ Just t -> do
94+ liftEffect $ Console.log $ "✓ Updated task #" <> show t.id
95+ liftEffect $ expectToBe "Design mockups (updated)" t.title
96+
97+ -- Step 8: Record task history
98+ historyEntry <- services.taskHistoryRepo.recordChange task1.id 1 "title" (Just "Design mockups") "Design mockups (updated)" # Om.liftAff
99+ liftEffect $ Console.log $ "✓ Recorded history entry #" <> show historyEntry.id
100+
101+ -- Step 9: Get task history
102+ history <- services.taskHistoryRepo.findByTask task1.id # Om.liftAff
103+ liftEffect $ Console.log $ "✓ Found " <> show (Array.length history) <> " history entries"
104+ liftEffect $ expectToBe 1 (Array.length history)
105+
106+ liftEffect $ Console.log "✓ Complete workflow test passed!"
107+
108+ test "authentication and authorization flow" do
109+ launchAff_ do
110+ let
111+ appConfig = { apiName: "Project Tracker", version: "1.0.0" }
112+ dbConfig = { connectionString: "memory://", poolSize: 10 }
113+
114+ runWithServices appConfig dbConfig do
115+ services <- Om.ask
116+
117+ -- Test authentication
118+ let token = "Bearer test-token-123" -- This would be a BearerToken in real code
119+ liftEffect $ Console.log "✓ Authentication service available"
120+
121+ -- Test authorization
122+ liftEffect $ Console.log "✓ Authorization checks ready"
123+
124+ -- In a real implementation, you'd check permissions here
125+ -- user <- services.authService.authenticate token # Om.liftAff
126+ -- authorized <- services.authService.authorize user "projects:create" # Om.liftAff
127+
128+ liftEffect $ Console.log "✓ Auth flow test passed!"
129+
130+ test "filter tasks by status and priority" do
131+ launchAff_ do
132+ let
133+ appConfig = { apiName: "Project Tracker", version: "1.0.0" }
134+ dbConfig = { connectionString: "memory://", poolSize: 10 }
135+
136+ runWithServices appConfig dbConfig do
137+ services <- Om.ask
138+
139+ -- Create a project
140+ let createProjectReq = { name: "Backend Services", description: "API development" }
141+ project <- services.projectRepo.create createProjectReq 1 # Om.liftAff
142+
143+ -- Create tasks with different priorities
144+ let
145+ highPriorityTask = { title: "Critical bug", description: "Fix production issue", priority: High, assigneeId: Nothing, dueDate: Nothing }
146+ mediumPriorityTask = { title: "Add feature", description: "New endpoint", priority: Medium, assigneeId: Nothing, dueDate: Nothing }
147+
148+ _ <- services.taskRepo.create project.id highPriorityTask 1 # Om.liftAff
149+ _ <- services.taskRepo.create project.id mediumPriorityTask 1 # Om.liftAff
150+
151+ -- List all tasks (no filters)
152+ let allTasksFilter = { status: Nothing, priority: Nothing, assigneeId: Nothing } /\ { limit: 10, offset: 0 }
153+ allTasks <- services.taskRepo.findByProject project.id allTasksFilter # Om.liftAff
154+ liftEffect $ Console.log $ "✓ Found " <> show (Array.length allTasks) <> " tasks (no filter)"
155+ liftEffect $ expectToBe 2 (Array.length allTasks)
156+
157+ liftEffect $ Console.log "✓ Task filtering test passed!"
158+
159+ test "attachment upload and retrieval" do
160+ launchAff_ do
161+ let
162+ appConfig = { apiName: "Project Tracker", version: "1.0.0" }
163+ dbConfig = { connectionString: "memory://", poolSize: 10 }
164+
165+ runWithServices appConfig dbConfig do
166+ services <- Om.ask
167+
168+ -- Create a project and task
169+ let createProjectReq = { name: "Documentation", description: "Update docs" }
170+ project <- services.projectRepo.create createProjectReq 1 # Om.liftAff
171+
172+ let taskReq = { title: "Upload diagrams", description: "Add architecture diagrams", priority: Medium, assigneeId: Nothing, dueDate: Nothing }
173+ task <- services.taskRepo.create project.id taskReq 1 # Om.liftAff
174+
175+ -- Upload an attachment
176+ let fileUpload = { file: "fake-file-contents" }
177+ attachment <- services.attachmentRepo.create task.id fileUpload 1 # Om.liftAff
178+ liftEffect $ Console.log $ "✓ Uploaded attachment #" <> show attachment.id <> ": " <> attachment.fileName
179+ liftEffect $ expectToBe "uploaded-file.txt" attachment.fileName
180+
181+ -- List attachments
182+ attachments <- services.attachmentRepo.findByTask task.id # Om.liftAff
183+ liftEffect $ Console.log $ "✓ Found " <> show (Array.length attachments) <> " attachments"
184+ liftEffect $ expectToBe 1 (Array.length attachments)
185+
186+ -- Delete attachment
187+ deleted <- services.attachmentRepo.delete attachment.id # Om.liftAff
188+ liftEffect $ expectToBe true deleted
189+ liftEffect $ Console.log "✓ Deleted attachment"
190+
191+ -- Verify deletion
192+ attachmentsAfter <- services.attachmentRepo.findByTask task.id # Om.liftAff
193+ liftEffect $ expectToBe 0 (Array.length attachmentsAfter)
194+
195+ liftEffect $ Console.log "✓ Attachment workflow test passed!"
test/Test/ProjectTrackerLayers.purs.disablednew file+340
@@ -0,0 +1,340 @@
1+module Test.ProjectTrackerLayers where
2+
3+import Prelude
4+
5+import Data.Array as Array
6+import Data.Maybe (Maybe(..))
7+import Data.Tuple.Nested (type (/\))
8+import Effect (Effect)
9+import Effect.Aff (Aff)
10+import Effect.Class (liftEffect)
11+import Effect.Console as Console
12+import Effect.Ref as Ref
13+import Test.ProjectTrackerExample (Attachment, AttachmentRepo, AuthService, Comment, CommentRepo, CreateCommentRequest, CreateProjectRequest, CreateTaskRequest, FileUpload, PaginationParams, Priority, Project, ProjectRepo, ProjectStatus(..), Task, TaskFilterParams, TaskHistoryEntry, TaskHistoryRepo, TaskRepo, TaskStatus(..), UpdateProjectRequest, UpdateTaskRequest, User, UserRole(..))
14+import Type.Function (type (#))
15+import Yoga.Om (Om)
16+import Yoga.Om as Om
17+
18+--------------------------------------------------------------------------------
19+-- OmLayer Service Configuration and Layers
20+--------------------------------------------------------------------------------
21+
22+-- | Application configuration
23+type AppConfig =
24+ { apiName :: String
25+ , version :: String
26+ }
27+
28+-- | Database configuration (placeholder for real DB)
29+type DbConfig =
30+ { connectionString :: String
31+ , poolSize :: Int
32+ }
33+
34+-- | All services provided by the application
35+type Services =
36+ { projectRepo :: ProjectRepo
37+ , taskRepo :: TaskRepo
38+ , commentRepo :: CommentRepo
39+ , attachmentRepo :: AttachmentRepo
40+ , taskHistoryRepo :: TaskHistoryRepo
41+ , authService :: AuthService
42+ }
43+
44+--------------------------------------------------------------------------------
45+-- Repository Implementations (In-Memory)
46+--------------------------------------------------------------------------------
47+
48+makeInMemoryProjectRepo :: Effect ProjectRepo
49+makeInMemoryProjectRepo = do
50+ projectsRef <- Ref.new []
51+ pure
52+ { findAll: \_ -> Ref.read projectsRef # liftEffect
53+ , findById: \id -> do
54+ projects <- Ref.read projectsRef # liftEffect
55+ pure $ Array.find (\p -> p.id == id) projects
56+ , create: \req userId -> do
57+ projects <- Ref.read projectsRef # liftEffect
58+ let
59+ newId = Array.length projects + 1
60+ now = 0 -- placeholder timestamp
61+ newProject =
62+ { id: newId
63+ , name: req.name
64+ , description: req.description
65+ , status: Active
66+ , createdAt: now
67+ , createdBy: userId
68+ }
69+ Ref.write (Array.snoc projects newProject) projectsRef # liftEffect
70+ pure newProject
71+ , update: \id req -> do
72+ projects <- Ref.read projectsRef # liftEffect
73+ case Array.findIndex (\p -> p.id == id) projects of
74+ Nothing -> pure Nothing
75+ Just idx -> case Array.index projects idx of
76+ Nothing -> pure Nothing
77+ Just project -> do
78+ let
79+ updated = project
80+ { name = case req.name of
81+ Nothing -> project.name
82+ Just n -> n
83+ , description = case req.description of
84+ Nothing -> project.description
85+ Just d -> d
86+ }
87+ let
88+ updatedProjects = case Array.updateAt idx updated projects of
89+ Nothing -> projects
90+ Just arr -> arr
91+ Ref.write updatedProjects projectsRef # liftEffect
92+ pure $ Just updated
93+ , archive: \id -> do
94+ projects <- Ref.read projectsRef # liftEffect
95+ case Array.findIndex (\p -> p.id == id) projects of
96+ Nothing -> pure false
97+ Just _ -> do
98+ let filtered = Array.filter (\p -> p.id /= id) projects
99+ Ref.write filtered projectsRef # liftEffect
100+ pure true
101+ }
102+
103+makeInMemoryTaskRepo :: Effect TaskRepo
104+makeInMemoryTaskRepo = do
105+ tasksRef <- Ref.new []
106+ pure
107+ { findByProject: \projectId _filters -> do
108+ tasks <- Ref.read tasksRef # liftEffect
109+ pure $ Array.filter (\t -> t.projectId == projectId) tasks
110+ , findById: \id -> do
111+ tasks <- Ref.read tasksRef # liftEffect
112+ pure $ Array.find (\t -> t.id == id) tasks
113+ , create: \projectId req userId -> do
114+ tasks <- Ref.read tasksRef # liftEffect
115+ let
116+ newId = Array.length tasks + 1
117+ now = 0 -- placeholder timestamp
118+ newTask =
119+ { id: newId
120+ , projectId
121+ , title: req.title
122+ , description: req.description
123+ , status: Todo
124+ , priority: req.priority
125+ , assigneeId: req.assigneeId
126+ , createdAt: now
127+ , updatedAt: now
128+ , dueDate: req.dueDate
129+ }
130+ Ref.write (Array.snoc tasks newTask) tasksRef # liftEffect
131+ pure newTask
132+ , update: \id req -> do
133+ tasks <- Ref.read tasksRef # liftEffect
134+ case Array.findIndex (\t -> t.id == id) tasks of
135+ Nothing -> pure Nothing
136+ Just idx -> case Array.index tasks idx of
137+ Nothing -> pure Nothing
138+ Just task -> do
139+ let
140+ now = 0 -- placeholder timestamp
141+ updated = task
142+ { title = case req.title of
143+ Nothing -> task.title
144+ Just t -> t
145+ , description = case req.description of
146+ Nothing -> task.description
147+ Just d -> d
148+ , status = case req.status of
149+ Nothing -> task.status
150+ Just s -> s
151+ , priority = case req.priority of
152+ Nothing -> task.priority
153+ Just p -> p
154+ , assigneeId = case req.assigneeId of
155+ Nothing -> task.assigneeId
156+ Just a -> Just a
157+ , updatedAt = now
158+ }
159+ let
160+ updatedTasks = case Array.updateAt idx updated tasks of
161+ Nothing -> tasks
162+ Just arr -> arr
163+ Ref.write updatedTasks tasksRef # liftEffect
164+ pure $ Just updated
165+ , delete: \id -> do
166+ tasks <- Ref.read tasksRef # liftEffect
167+ case Array.findIndex (\t -> t.id == id) tasks of
168+ Nothing -> pure false
169+ Just _ -> do
170+ let filtered = Array.filter (\t -> t.id /= id) tasks
171+ Ref.write filtered tasksRef # liftEffect
172+ pure true
173+ , assign: \taskId assigneeId -> do
174+ tasks <- Ref.read tasksRef # liftEffect
175+ case Array.findIndex (\t -> t.id == taskId) tasks of
176+ Nothing -> pure Nothing
177+ Just idx -> case Array.index tasks idx of
178+ Nothing -> pure Nothing
179+ Just task -> do
180+ let
181+ now = 0 -- placeholder timestamp
182+ updated = task { assigneeId = Just assigneeId, updatedAt = now }
183+ updatedTasks = case Array.updateAt idx updated tasks of
184+ Nothing -> tasks
185+ Just arr -> arr
186+ Ref.write updatedTasks tasksRef # liftEffect
187+ pure $ Just updated
188+ }
189+
190+makeInMemoryCommentRepo :: Effect CommentRepo
191+makeInMemoryCommentRepo = do
192+ commentsRef <- Ref.new []
193+ pure
194+ { findByTask: \taskId _pagination -> do
195+ comments <- Ref.read commentsRef # liftEffect
196+ pure $ Array.filter (\c -> c.taskId == taskId) comments
197+ , create: \taskId req authorId -> do
198+ comments <- Ref.read commentsRef # liftEffect
199+ let
200+ newId = Array.length comments + 1
201+ now = 0 -- placeholder timestamp
202+ newComment =
203+ { id: newId
204+ , taskId
205+ , authorId
206+ , content: req.content
207+ , createdAt: now
208+ }
209+ Ref.write (Array.snoc comments newComment) commentsRef # liftEffect
210+ pure newComment
211+ }
212+
213+makeInMemoryAttachmentRepo :: Effect AttachmentRepo
214+makeInMemoryAttachmentRepo = do
215+ attachmentsRef <- Ref.new []
216+ pure
217+ { findByTask: \taskId -> do
218+ attachments <- Ref.read attachmentsRef # liftEffect
219+ pure $ Array.filter (\a -> a.taskId == taskId) attachments
220+ , create: \taskId upload userId -> do
221+ attachments <- Ref.read attachmentsRef # liftEffect
222+ let
223+ newId = Array.length attachments + 1
224+ now = 0 -- placeholder timestamp
225+ newAttachment =
226+ { id: newId
227+ , taskId
228+ , fileName: "uploaded-file.txt"
229+ , fileSize: 1024
230+ , contentType: "text/plain"
231+ , uploadedBy: userId
232+ , uploadedAt: now
233+ , url: "/attachments/" <> show newId
234+ }
235+ Ref.write (Array.snoc attachments newAttachment) attachmentsRef # liftEffect
236+ pure newAttachment
237+ , delete: \id -> do
238+ attachments <- Ref.read attachmentsRef # liftEffect
239+ case Array.findIndex (\a -> a.id == id) attachments of
240+ Nothing -> pure false
241+ Just _ -> do
242+ let filtered = Array.filter (\a -> a.id /= id) attachments
243+ Ref.write filtered attachmentsRef # liftEffect
244+ pure true
245+ }
246+
247+makeInMemoryTaskHistoryRepo :: Effect TaskHistoryRepo
248+makeInMemoryTaskHistoryRepo = do
249+ historyRef <- Ref.new []
250+ pure
251+ { findByTask: \taskId -> do
252+ history <- Ref.read historyRef # liftEffect
253+ pure $ Array.filter (\h -> h.taskId == taskId) history
254+ , recordChange: \taskId userId field oldVal newVal -> do
255+ history <- Ref.read historyRef # liftEffect
256+ let
257+ newId = Array.length history + 1
258+ now = 0 -- placeholder timestamp
259+ entry =
260+ { id: newId
261+ , taskId
262+ , changedBy: userId
263+ , changedAt: now
264+ , field
265+ , oldValue: oldVal
266+ , newValue: newVal
267+ }
268+ Ref.write (Array.snoc history entry) historyRef # liftEffect
269+ pure entry
270+ }
271+
272+makeInMemoryAuthService :: Effect AuthService
273+makeInMemoryAuthService = pure
274+ { authenticate: \token -> pure $ Just
275+ { id: 1
276+ , username: "testuser"
277+ , email: "test@example.com"
278+ , fullName: "Test User"
279+ , role: Developer
280+ }
281+ , authorize: \user action -> pure true
282+ }
283+
284+--------------------------------------------------------------------------------
285+-- Om-based Service Initialization (OmLayer Pattern)
286+--------------------------------------------------------------------------------
287+
288+-- | Initialize all services with configuration from Om context
289+-- This demonstrates the OmLayer pattern using Om monad for dependency injection
290+initializeServices :: forall r. Om (appConfig :: AppConfig, dbConfig :: DbConfig # r) Unit Services
291+initializeServices = do
292+ { appConfig, dbConfig } <- Om.ask
293+
294+ -- Log initialization
295+ liftEffect $ Console.log $ "Initializing " <> appConfig.apiName <> " v" <> appConfig.version
296+ liftEffect $ Console.log $ "Database: " <> dbConfig.connectionString <> " (pool size: " <> show dbConfig.poolSize <> ")"
297+
298+ -- Initialize all repositories (these would connect to real DBs in production)
299+ projectRepo <- makeInMemoryProjectRepo # liftEffect
300+ taskRepo <- makeInMemoryTaskRepo # liftEffect
301+ commentRepo <- makeInMemoryCommentRepo # liftEffect
302+ attachmentRepo <- makeInMemoryAttachmentRepo # liftEffect
303+ taskHistoryRepo <- makeInMemoryTaskHistoryRepo # liftEffect
304+ authService <- makeInMemoryAuthService # liftEffect
305+
306+ liftEffect $ Console.log "✓ All services initialized"
307+
308+ pure
309+ { projectRepo
310+ , taskRepo
311+ , commentRepo
312+ , attachmentRepo
313+ , taskHistoryRepo
314+ , authService
315+ }
316+
317+-- | Run the application with all services initialized
318+-- This is the main entry point that:
319+-- 1. Takes configuration (appConfig, dbConfig)
320+-- 2. Initializes all services (repositories, auth)
321+-- 3. Runs the action with services in the Om context
322+-- 4. Handles any exceptions
323+runWithServices :: forall a. AppConfig -> DbConfig -> Om Services Unit a -> Aff a
324+runWithServices appConfig dbConfig action = do
325+ let configContext = { appConfig, dbConfig }
326+ -- First initialize services with config context
327+ services <- Om.runOm configContext
328+ { exception: \_ -> pure
329+ { projectRepo: bottom
330+ , taskRepo: bottom
331+ , commentRepo: bottom
332+ , attachmentRepo: bottom
333+ , taskHistoryRepo: bottom
334+ , authService: bottom
335+ }
336+ }
337+ initializeServices
338+
339+ -- Then run the action with services context
340+ Om.runOm services { exception: \_ -> pure unit } action
test/Test/SchemaComponentTest.purs+8−8
@@ -6,7 +6,7 @@ import Data.Maybe (Maybe(..))
66import Data.String as String
77import Effect (Effect)
88import Yoga.HTTP.API.Path (type (:), type (/))
9−import Yoga.HTTP.API.Route (GET, POST, Route, Request, JSON, Schema, buildOpenAPISpec)
9+import Yoga.HTTP.API.Route (GET, POST, Route, JSON, Schema, buildOpenAPISpec)
1010import Yoga.JSON (writeJSON)
1111import ViTest (ViTest, describe, test)
1212import ViTest.Expect (expectToBe)
@@ -17,33 +17,33 @@ type Post = { id :: Int, title :: String, authorId :: Int }
1717type SchemaAPI =
1818 { getUser ::
1919 Route GET ("users" / "id" : Int)
20− (Request {})
20+ {}
2121 (ok :: { body :: Schema "User" User })
2222 , createUser ::
2323 Route POST "users"
24− (Request { body :: JSON (Schema "User" User) })
24+ { body :: JSON (Schema "User" User) }
2525 (created :: { body :: Schema "User" User })
26− , listPosts :: Route GET "posts" (Request {}) (ok :: { body :: Array (Schema "Post" Post) })
26+ , listPosts :: Route GET "posts" {} (ok :: { body :: Array (Schema "Post" Post) })
2727 }
2828
2929testSchemaComponents :: Effect ViTest
3030testSchemaComponents = describe "Schema Components" do
3131 _ <- test "generates $ref in response body" do
3232 let
33− spec = buildOpenAPISpec @SchemaAPI { title: "Test", version: "1.0.0", description: Nothing, contact: Nothing, license: Nothing }
33+ spec = buildOpenAPISpec @SchemaAPI { title: "Test", version: "1.0.0" }
3434 json = writeJSON spec
3535 expectToBe true (String.contains (String.Pattern "\"$ref\":\"#/components/schemas/User\"") json)
3636
3737 _ <- test "generates schema definition in components" do
3838 let
39− spec = buildOpenAPISpec @SchemaAPI { title: "Test", version: "1.0.0", description: Nothing, contact: Nothing, license: Nothing }
39+ spec = buildOpenAPISpec @SchemaAPI { title: "Test", version: "1.0.0" }
4040 json = writeJSON spec
4141 expectToBe true (String.contains (String.Pattern "components") json)
4242 expectToBe true (String.contains (String.Pattern "\"schemas\"") json)
4343
4444 _ <- test "defines User schema with properties" do
4545 let
46− spec = buildOpenAPISpec @SchemaAPI { title: "Test", version: "1.0.0", description: Nothing, contact: Nothing, license: Nothing }
46+ spec = buildOpenAPISpec @SchemaAPI { title: "Test", version: "1.0.0" }
4747 json = writeJSON spec
4848 -- Schema should be in components, not inline
4949 expectToBe true (String.contains (String.Pattern "\"User\"") json)
@@ -52,6 +52,6 @@ testSchemaComponents = describe "Schema Components" do
5252
5353 test "collects nested schemas from arrays" do
5454 let
55− spec = buildOpenAPISpec @SchemaAPI { title: "Test", version: "1.0.0", description: Nothing, contact: Nothing, license: Nothing }
55+ spec = buildOpenAPISpec @SchemaAPI { title: "Test", version: "1.0.0" }
5656 json = writeJSON spec
5757 expectToBe true (String.contains (String.Pattern "\"Post\"") json)
test/Test/VariantResponseTest.purs+7−7
@@ -10,7 +10,7 @@ import Effect.Aff (Aff)
1010import Type.Proxy (Proxy(..))
1111import Yoga.HTTP.API.Route.StatusCode (StatusCode(..))
1212import Yoga.HTTP.API.Path (Root)
13−import Yoga.HTTP.API.Route (GET, POST, PUT, Route, Request, Response(..), respondNoHeaders, respondWith, toOpenAPI, statusCodeFor, statusCodeToString)
13+import Yoga.HTTP.API.Route (GET, POST, PUT, Route, Response(..), respondNoHeaders, respondWith, toOpenAPI, statusCodeFor, statusCodeToString)
1414import Yoga.HTTP.API.Route.Response (respond) as Response
1515import ViTest (ViTest, describe, test)
1616import ViTest.Expect (expectToBe)
@@ -25,14 +25,14 @@ expectToEqual expected actual = expectToBe true (expected == actual)
2525
2626-- Simple variant route with ok and notFound responses
2727type SimpleVariantRoute = Route GET Root
28− (Request {})
28+ {}
2929 ( ok :: { body :: String }
3030 , notFound :: { body :: String }
3131 )
3232
3333-- Variant route with multiple status codes
3434type MultiStatusRoute = Route POST Root
35− (Request { headers :: { authorization :: String } })
35+ { headers :: { authorization :: String } }
3636 ( created :: { headers :: { "Location" :: String }, body :: User }
3737 , badRequest :: { body :: ErrorMessage }
3838 , unauthorized :: { body :: ErrorMessage }
@@ -40,7 +40,7 @@ type MultiStatusRoute = Route POST Root
4040
4141-- Variant route with many response types
4242type ComplexVariantRoute = Route PUT Root
43− (Request {})
43+ {}
4444 ( ok :: { body :: User }
4545 , created :: { headers :: { "Location" :: String }, body :: User }
4646 , badRequest :: { body :: ErrorMessage }
@@ -242,7 +242,7 @@ testSimpleVariantOpenAPI = describe "OpenAPI Generation - Simple Variant" $ do
242242 let
243243 result = toOpenAPI
244244 @( Route GET Root
245− (Request {})
245+ {}
246246 ( ok :: { body :: String }
247247 , notFound :: { body :: String }
248248 )
@@ -265,7 +265,7 @@ testComplexVariantOpenAPI = describe "OpenAPI Generation - Complex Variant" $ do
265265 let
266266 result = toOpenAPI
267267 @( Route POST Root
268− (Request { headers :: { authorization :: String } })
268+ { headers :: { authorization :: String } }
269269 ( created :: { headers :: { "Location" :: String }, body :: User }
270270 , badRequest :: { body :: ErrorMessage }
271271 , unauthorized :: { body :: ErrorMessage }
@@ -299,7 +299,7 @@ testVariantWithHeaders = describe "OpenAPI Generation - Variant with Response He
299299 let
300300 result = toOpenAPI
301301 @( Route POST Root
302− (Request {})
302+ {}
303303 ( created :: { headers :: { "Location" :: String, "X-Request-Id" :: String }, body :: User }
304304 )
305305 )