git.iko.soy/rowtype-yoga/purescript-yoga-http-api
on feat/custom-type-errors help sign in
33 files changed+2023−446
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+8−8 test/Test/BearerAuthTest.purs+30−24 test/Test/ComprehensiveExample.purs+150 test/Test/ContentTypeTest.purs+16−53 test/Test/CookieAuthTest.purs+12−13 test/Test/ExamplesTest.purs+7−22 test/Test/OpenAPIInfoTest.purs+22−56 test/Test/OpenAPIValidation.purs+5−2 test/Test/Operators.purs+2 test/Test/ProjectTrackerExample.purs+499 test/Test/ProjectTrackerIntegration.purs.disabled+195 test/Test/ProjectTrackerLayers.purs.disabled+340 test/Test/SchemaComponentTest.purs+9−9 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+8−8
@@ -8,8 +8,8 @@ import Data.String as String
88import Effect (Effect)
99import Effect.Aff (Aff)
1010import Type.Proxy (Proxy(..))
11−import Yoga.HTTP.API.Path (Path, Lit, type (/), type (:))
12−import Yoga.HTTP.API.Route (GET, POST, Route, Request, JSON, respondNoHeaders, buildOpenAPISpec, collectOperations, mkHandler, apiHandlers)
11+import Yoga.HTTP.API.Path (type (/), type (:))
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 (Path ("users" / "id" : Int)) (Request {}) (ok :: { body :: User })
28− , listUsers :: Route GET (Path (Lit "users")) (Request {}) (ok :: { body :: Array User })
29− , createUser :: Route POST (Path (Lit "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+30−24
@@ -2,13 +2,14 @@ module Test.BearerAuthTest where
22
33import Prelude
44
5+import Data.Array as Array
56import Data.Maybe (Maybe(..))
67import Data.String as String
78import Effect (Effect)
8−import Yoga.HTTP.API.Path (Path, Lit)
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)
12+import Test.OpenAPIValidation (validate)
1213import ViTest (ViTest, describe, test)
1314import ViTest.Expect (expectToBe)
1415
@@ -17,25 +18,25 @@ import ViTest.Expect (expectToBe)
1718--------------------------------------------------------------------------------
1819
1920type BearerAuthAPI =
20− { getProtected :: Route GET (Path (Lit "protected")) (Request { headers :: { authorization :: BearerToken } }) (ok :: { body :: { message :: String } })
21+ { getProtected :: Route GET "protected" { headers :: { authorization :: BearerToken } } (ok :: { body :: { message :: String } })
2122 }
2223
2324type BasicAuthAPI =
24− { getProtected :: Route GET (Path (Lit "protected")) (Request { headers :: { authorization :: BasicAuth } }) (ok :: { body :: { message :: String } })
25+ { getProtected :: Route GET "protected" { headers :: { authorization :: BasicAuth } } (ok :: { body :: { message :: String } })
2526 }
2627
2728type ApiKeyAPI =
28− { getProtected :: Route GET (Path (Lit "protected")) (Request { headers :: { apiKey :: ApiKeyHeader } }) (ok :: { body :: { message :: String } })
29+ { getProtected :: Route GET "protected" { headers :: { apiKey :: ApiKeyHeader } } (ok :: { body :: { message :: String } })
2930 }
3031
3132type DigestAuthAPI =
32− { getProtected :: Route GET (Path (Lit "protected")) (Request { headers :: { authorization :: DigestAuth } }) (ok :: { body :: { message :: String } })
33+ { getProtected :: Route GET "protected" { headers :: { authorization :: DigestAuth } } (ok :: { body :: { message :: String } })
3334 }
3435
3536type MultiAuthAPI =
36− { bearerEndpoint :: Route GET (Path (Lit "bearer")) (Request { headers :: { authorization :: BearerToken } }) (ok :: { body :: { message :: String } })
37− , basicEndpoint :: Route GET (Path (Lit "basic")) (Request { headers :: { authorization :: BasicAuth } }) (ok :: { body :: { message :: String } })
38− , apiKeyEndpoint :: Route GET (Path (Lit "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 } })
3940 }
4041
4142--------------------------------------------------------------------------------
@@ -45,39 +46,44 @@ type MultiAuthAPI =
4546testBearerTokenAuth :: Effect ViTest
4647testBearerTokenAuth = describe "BearerToken Authentication" do
4748 _ <- test "BearerToken does not appear as a regular header parameter" do
48− 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" }
4950 let json = writeJSON spec
5051 let hasAuthParam = String.contains (String.Pattern "\"name\":\"authorization\"") json
5152 expectToBe false hasAuthParam
5253
5354 _ <- test "BearerToken appears in security requirements" do
54− 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" }
5556 let json = writeJSON spec
5657 let hasBearerAuth = String.contains (String.Pattern "bearerAuth") json
5758 expectToBe true hasBearerAuth
5859
59− test "BearerToken security scheme has correct structure" do
60− let spec = buildOpenAPISpec @BearerAuthAPI { title: "Auth API", version: "1.0.0", description: Nothing, contact: Nothing, license: Nothing }
60+ _ <- test "BearerToken security scheme has correct structure" do
61+ let spec = buildOpenAPISpec @BearerAuthAPI { title: "Auth API", version: "1.0.0" }
6162 let json = writeJSON spec
6263 expectToBe true (String.contains (String.Pattern "\"type\":\"http\"") json)
6364 expectToBe true (String.contains (String.Pattern "\"scheme\":\"bearer\"") json)
6465
66+ test "BearerToken API generates valid OpenAPI 3.0 schema" do
67+ let spec = buildOpenAPISpec @BearerAuthAPI { title: "Auth API", version: "1.0.0" }
68+ let result = validate spec
69+ expectToBe true (Array.null result.errors)
70+
6571testBasicAuth :: Effect ViTest
6672testBasicAuth = describe "BasicAuth Authentication" do
6773 _ <- test "BasicAuth does not appear as a regular header parameter" do
68− 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" }
6975 let json = writeJSON spec
7076 let hasAuthParam = String.contains (String.Pattern "\"name\":\"authorization\"") json
7177 expectToBe false hasAuthParam
7278
7379 _ <- test "BasicAuth appears in security requirements" do
74− 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" }
7581 let json = writeJSON spec
7682 let hasBasicAuth = String.contains (String.Pattern "basicAuth") json
7783 expectToBe true hasBasicAuth
7884
7985 test "BasicAuth security scheme has correct structure" do
80− 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" }
8187 let json = writeJSON spec
8288 expectToBe true (String.contains (String.Pattern "\"type\":\"http\"") json)
8389 expectToBe true (String.contains (String.Pattern "\"scheme\":\"basic\"") json)
@@ -85,20 +91,20 @@ testBasicAuth = describe "BasicAuth Authentication" do
8591testApiKeyAuth :: Effect ViTest
8692testApiKeyAuth = describe "ApiKey Authentication" do
8793 _ <- test "ApiKeyHeader does not appear as a regular header parameter" do
88− 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" }
8995 let json = writeJSON spec
9096 -- Check that parameters array is empty (no header parameters)
9197 let hasEmptyParams = String.contains (String.Pattern "\"parameters\":[]") json
9298 expectToBe true hasEmptyParams
9399
94100 _ <- test "ApiKey appears in security requirements" do
95− 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" }
96102 let json = writeJSON spec
97103 let hasApiKey = String.contains (String.Pattern "apiKeyApiKey") json
98104 expectToBe true hasApiKey
99105
100106 test "ApiKey security scheme has correct structure" do
101− 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" }
102108 let json = writeJSON spec
103109 expectToBe true (String.contains (String.Pattern "\"type\":\"apiKey\"") json)
104110 expectToBe true (String.contains (String.Pattern "\"in\":\"header\"") json)
@@ -106,19 +112,19 @@ testApiKeyAuth = describe "ApiKey Authentication" do
106112testDigestAuth :: Effect ViTest
107113testDigestAuth = describe "DigestAuth Authentication" do
108114 _ <- test "DigestAuth does not appear as a regular header parameter" do
109− 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" }
110116 let json = writeJSON spec
111117 let hasAuthParam = String.contains (String.Pattern "\"name\":\"authorization\"") json
112118 expectToBe false hasAuthParam
113119
114120 _ <- test "DigestAuth appears in security requirements" do
115− 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" }
116122 let json = writeJSON spec
117123 let hasDigestAuth = String.contains (String.Pattern "digestAuth") json
118124 expectToBe true hasDigestAuth
119125
120126 test "DigestAuth security scheme has correct structure" do
121− 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" }
122128 let json = writeJSON spec
123129 expectToBe true (String.contains (String.Pattern "\"type\":\"http\"") json)
124130 expectToBe true (String.contains (String.Pattern "\"scheme\":\"digest\"") json)
@@ -126,14 +132,14 @@ testDigestAuth = describe "DigestAuth Authentication" do
126132testMultipleAuthTypes :: Effect ViTest
127133testMultipleAuthTypes = describe "Multiple Authentication Types" do
128134 _ <- test "All auth types appear in security schemes" do
129− 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" }
130136 let json = writeJSON spec
131137 expectToBe true (String.contains (String.Pattern "bearerAuth") json)
132138 expectToBe true (String.contains (String.Pattern "basicAuth") json)
133139 expectToBe true (String.contains (String.Pattern "xApiKeyApiKey") json)
134140
135141 test "None of the auth headers appear as regular parameters" do
136− 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" }
137143 let json = writeJSON spec
138144 -- Check that all endpoints have empty parameters arrays
139145 let hasEmptyParams = String.contains (String.Pattern "\"parameters\":[]") json
test/Test/ComprehensiveExample.pursnew file+150
@@ -0,0 +1,150 @@
1+module Test.ComprehensiveExample where
2+
3+import Prelude
4+
5+import Data.Array as Array
6+import Effect (Effect)
7+import Literals.Undefined (undefined)
8+import Type.Function (type (#))
9+import Yoga.HTTP.API.Path (type (:), type (/), type (:?))
10+import Yoga.HTTP.API.Route (GET, POST, PUT, DELETE, Route, buildOpenAPISpec, NoRequest)
11+import Yoga.HTTP.API.Route.Auth (BearerToken, ApiKeyHeader, BasicAuth)
12+import Yoga.HTTP.API.Route.Encoding (JSON, PlainText, XML)
13+import Yoga.HTTP.API.Route.OpenAPIMetadata (Description, Example, ExampleValue, Examples, Schema)
14+import Yoga.JSON (writeJSON)
15+import Test.OpenAPIValidation (validate)
16+import ViTest (ViTest, describe, test)
17+import ViTest.Expect (expectToBe)
18+
19+-- | A comprehensive example API showcasing all major features
20+type ComprehensiveAPI =
21+ { -- Simple GET with path parameter and authentication
22+ getUser ::
23+ Route GET ("users" / "id" : Int)
24+ { headers :: { authorization :: BearerToken } }
25+ (ok :: { body :: Schema "User" User })
26+
27+ , -- List users with query parameters
28+ listUsers ::
29+ Route GET ("users" :? { limit :: Int # Example "10", offset :: Int # Example "0" })
30+ NoRequest
31+ (ok :: { body :: Array User })
32+
33+ , -- POST with JSON body and schema
34+ createUser ::
35+ Route POST "users"
36+ { body :: JSON (Schema "CreateUserRequest" CreateUserRequest) }
37+ (created :: { body :: Schema "User" User })
38+
39+ , -- PUT with authentication and JSON body
40+ updateUser ::
41+ Route PUT ("users" / "id" : Int)
42+ { headers :: { authorization :: BearerToken }
43+ , body :: JSON UpdateUserRequest
44+ }
45+ (ok :: { body :: User })
46+
47+ , -- DELETE with authentication
48+ deleteUser ::
49+ Route DELETE ("users" / "id" : Int)
50+ { headers :: { authorization :: BearerToken } }
51+ (noContent :: { body :: {} })
52+
53+ , -- Multiple content types - XML
54+ exportUser ::
55+ Route GET ("users" / "id" : Int / "export")
56+ NoRequest
57+ (ok :: { body :: XML User })
58+
59+ , -- Plain text response
60+ healthCheck ::
61+ Route GET "health"
62+ NoRequest
63+ (ok :: { body :: PlainText String })
64+
65+ , -- API Key authentication
66+ getStats ::
67+ Route GET "stats"
68+ { headers :: { xApiKey :: ApiKeyHeader } }
69+ (ok :: { body :: Stats })
70+
71+ , -- Basic auth
72+ login ::
73+ Route POST "login"
74+ { headers :: { authorization :: BasicAuth } }
75+ (ok :: { body :: { token :: String # Description "JWT access token" } })
76+
77+ , -- Path with examples
78+ getUserPosts ::
79+ Route GET ("users" / "userId" : (Int # Examples (alice :: ExampleValue "1", bob :: ExampleValue "2")) / "posts")
80+ NoRequest
81+ (ok :: { body :: Array Post })
82+ }
83+
84+-- | User type with metadata
85+type User =
86+ { id :: Int
87+ , name :: String # Description "User's full name"
88+ , email :: String # Description "User's email address"
89+ , age :: Int
90+ , role :: String # Description "User role: admin, user, or guest"
91+ }
92+
93+-- | Request to create a user
94+type CreateUserRequest =
95+ { name :: String # Examples (short :: ExampleValue "\"Jo\"", normal :: ExampleValue "\"John Doe\"")
96+ , email :: String # Example "\"user@example.com\""
97+ , age :: Int # Example "25"
98+ , role :: String # Example "\"user\""
99+ }
100+
101+-- | Request to update a user
102+type UpdateUserRequest =
103+ { name :: String
104+ , email :: String
105+ , age :: Int
106+ }
107+
108+-- | Statistics type
109+type Stats =
110+ { activeUsers :: Int # Description "Number of currently active users"
111+ , totalRequests :: Int # Description "Total API requests"
112+ , uptime :: Int # Description "Server uptime in seconds"
113+ }
114+
115+-- | Post type
116+type Post =
117+ { id :: Int
118+ , userId :: Int
119+ , title :: String # Description "Post title"
120+ , content :: String # Description "Post content"
121+ , published :: Boolean
122+ }
123+
124+-- | Test demonstrating the comprehensive API
125+testComprehensiveExample :: Effect ViTest
126+testComprehensiveExample = describe "Comprehensive API Example" do
127+ test "generates complete OpenAPI spec with all features" do
128+ let
129+ spec = buildOpenAPISpec @ComprehensiveAPI
130+ { title: "Comprehensive Example API"
131+ , version: "1.0.0"
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"
137+ }
138+ , license:
139+ { name: "MIT"
140+ , url: "https://opensource.org/licenses/MIT"
141+ }
142+ }
143+ json = writeJSON spec
144+
145+ -- Verify it generates valid OpenAPI
146+ let result = validate spec
147+ expectToBe true (Array.null result.errors)
148+
149+ -- Check it has our endpoints
150+ expectToBe true (json /= "")
test/Test/ContentTypeTest.purs+16−53
@@ -5,8 +5,7 @@ import Prelude
55import Data.Maybe (Maybe(..))
66import Data.String as String
77import Effect (Effect)
8−import Yoga.HTTP.API.Path (Path, Lit)
9−import Yoga.HTTP.API.Route (GET, POST, PUT, Route, Request, buildOpenAPISpec)
8+import Yoga.HTTP.API.Route (GET, POST, PUT, Route, buildOpenAPISpec)
109import Yoga.HTTP.API.Route.Encoding (JSON, FormData, MultipartFormData, PlainText, XML, CustomContentType)
1110import Yoga.JSON (writeJSON)
1211import ViTest (ViTest, describe, test)
@@ -22,12 +21,9 @@ testRequestContentTypes :: Effect ViTest
2221testRequestContentTypes = describe "Request Content Types" do
2322 _ <- test "JSON request body generates application/json" do
2423 let
25− spec = buildOpenAPISpec @{ createUser :: Route POST (Path (Lit "users")) (Request { body :: JSON User }) (ok :: { body :: User }) }
24+ spec = buildOpenAPISpec @{ createUser :: Route POST "users" { body :: JSON User } (ok :: { body :: User }) }
2625 { title: "API"
2726 , version: "1.0.0"
28− , description: Nothing
29− , contact: Nothing
30− , license: Nothing
3127 }
3228 json = writeJSON spec
3329 expectToBe true (String.contains (String.Pattern "\"application/json\"") json)
@@ -35,12 +31,9 @@ testRequestContentTypes = describe "Request Content Types" do
3531
3632 _ <- test "FormData request body generates application/x-www-form-urlencoded" do
3733 let
38− spec = buildOpenAPISpec @{ createUser :: Route POST (Path (Lit "users")) (Request { body :: FormData User }) (ok :: { body :: JSON User }) }
34+ spec = buildOpenAPISpec @{ createUser :: Route POST "users" { body :: FormData User } (ok :: { body :: JSON User }) }
3935 { title: "API"
4036 , version: "1.0.0"
41− , description: Nothing
42− , contact: Nothing
43− , license: Nothing
4437 }
4538 json = writeJSON spec
4639 expectToBe true (String.contains (String.Pattern "\"application/x-www-form-urlencoded\"") json)
@@ -48,48 +41,36 @@ testRequestContentTypes = describe "Request Content Types" do
4841
4942 _ <- test "MultipartFormData request body generates multipart/form-data" do
5043 let
51− spec = buildOpenAPISpec @{ uploadFile :: Route POST (Path (Lit "upload")) (Request { body :: MultipartFormData FileUpload }) (ok :: { body :: { success :: Boolean } }) }
44+ spec = buildOpenAPISpec @{ uploadFile :: Route POST "upload" { body :: MultipartFormData FileUpload } (ok :: { body :: { success :: Boolean } }) }
5245 { title: "API"
5346 , version: "1.0.0"
54− , description: Nothing
55− , contact: Nothing
56− , license: Nothing
5747 }
5848 json = writeJSON spec
5949 expectToBe true (String.contains (String.Pattern "\"multipart/form-data\"") json)
6050
6151 _ <- test "PlainText request body generates text/plain" do
6252 let
63− spec = buildOpenAPISpec @{ echo :: Route POST (Path (Lit "echo")) (Request { body :: PlainText String }) (ok :: { body :: PlainText String }) }
53+ spec = buildOpenAPISpec @{ echo :: Route POST "echo" { body :: PlainText String } (ok :: { body :: PlainText String }) }
6454 { title: "API"
6555 , version: "1.0.0"
66− , description: Nothing
67− , contact: Nothing
68− , license: Nothing
6956 }
7057 json = writeJSON spec
7158 expectToBe true (String.contains (String.Pattern "\"text/plain\"") json)
7259
7360 _ <- test "XML request body generates application/xml" do
7461 let
75− spec = buildOpenAPISpec @{ updateDoc :: Route PUT (Path (Lit "document")) (Request { body :: XML XmlDocument }) (ok :: { body :: XML XmlDocument }) }
62+ spec = buildOpenAPISpec @{ updateDoc :: Route PUT "document" { body :: XML XmlDocument } (ok :: { body :: XML XmlDocument }) }
7663 { title: "API"
7764 , version: "1.0.0"
78− , description: Nothing
79− , contact: Nothing
80− , license: Nothing
8165 }
8266 json = writeJSON spec
8367 expectToBe true (String.contains (String.Pattern "\"application/xml\"") json)
8468
8569 test "CustomContentType request body generates custom MIME type" do
8670 let
87− spec = buildOpenAPISpec @{ customEndpoint :: Route POST (Path (Lit "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 }) }
8872 { title: "API"
8973 , version: "1.0.0"
90− , description: Nothing
91− , contact: Nothing
92− , license: Nothing
9374 }
9475 json = writeJSON spec
9576 expectToBe true (String.contains (String.Pattern "\"application/vnd.api+json\"") json)
@@ -98,60 +79,45 @@ testResponseContentTypes :: Effect ViTest
9879testResponseContentTypes = describe "Response Content Types" do
9980 _ <- test "JSON response body generates application/json" do
10081 let
101− spec = buildOpenAPISpec @{ getUser :: Route GET (Path (Lit "users")) (Request {}) (ok :: { body :: JSON User }) }
82+ spec = buildOpenAPISpec @{ getUser :: Route GET "users" {} (ok :: { body :: JSON User }) }
10283 { title: "API"
10384 , version: "1.0.0"
104− , description: Nothing
105− , contact: Nothing
106− , license: Nothing
10785 }
10886 json = writeJSON spec
10987 expectToBe true (String.contains (String.Pattern "\"application/json\"") json)
11088
11189 _ <- test "PlainText response body generates text/plain" do
11290 let
113− spec = buildOpenAPISpec @{ getText :: Route GET (Path (Lit "text")) (Request {}) (ok :: { body :: PlainText String }) }
91+ spec = buildOpenAPISpec @{ getText :: Route GET "text" {} (ok :: { body :: PlainText String }) }
11492 { title: "API"
11593 , version: "1.0.0"
116− , description: Nothing
117− , contact: Nothing
118− , license: Nothing
11994 }
12095 json = writeJSON spec
12196 expectToBe true (String.contains (String.Pattern "\"text/plain\"") json)
12297
12398 _ <- test "XML response body generates application/xml" do
12499 let
125− spec = buildOpenAPISpec @{ getDoc :: Route GET (Path (Lit "document")) (Request {}) (ok :: { body :: XML XmlDocument }) }
100+ spec = buildOpenAPISpec @{ getDoc :: Route GET "document" {} (ok :: { body :: XML XmlDocument }) }
126101 { title: "API"
127102 , version: "1.0.0"
128− , description: Nothing
129− , contact: Nothing
130− , license: Nothing
131103 }
132104 json = writeJSON spec
133105 expectToBe true (String.contains (String.Pattern "\"application/xml\"") json)
134106
135107 _ <- test "CustomContentType response body generates custom MIME type" do
136108 let
137− spec = buildOpenAPISpec @{ customGet :: Route GET (Path (Lit "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 }) }
138110 { title: "API"
139111 , version: "1.0.0"
140− , description: Nothing
141− , contact: Nothing
142− , license: Nothing
143112 }
144113 json = writeJSON spec
145114 expectToBe true (String.contains (String.Pattern "\"application/vnd.api+json\"") json)
146115
147116 test "Unwrapped response body defaults to application/json for backward compatibility" do
148117 let
149− spec = buildOpenAPISpec @{ getUser :: Route GET (Path (Lit "users")) (Request {}) (ok :: { body :: User }) }
118+ spec = buildOpenAPISpec @{ getUser :: Route GET "users" {} (ok :: { body :: User }) }
150119 { title: "API"
151120 , version: "1.0.0"
152− , description: Nothing
153− , contact: Nothing
154− , license: Nothing
155121 }
156122 json = writeJSON spec
157123 expectToBe true (String.contains (String.Pattern "\"application/json\"") json)
@@ -162,16 +128,13 @@ testMixedContentTypes = describe "Mixed Content Types in API" do
162128 let
163129 spec = buildOpenAPISpec
164130 @
165− { createUser :: Route POST (Path (Lit "users")) (Request { body :: JSON User }) (ok :: { body :: JSON User })
166− , uploadFile :: Route POST (Path (Lit "upload")) (Request { body :: MultipartFormData FileUpload }) (ok :: { body :: { success :: Boolean } })
167− , echo :: Route POST (Path (Lit "echo")) (Request { body :: PlainText String }) (ok :: { body :: PlainText String })
168− , getDoc :: Route GET (Path (Lit "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 })
169135 }
170136 { title: "Multi-Content API"
171137 , version: "1.0.0"
172− , description: Nothing
173− , contact: Nothing
174− , license: Nothing
175138 }
176139 json = writeJSON spec
177140 expectToBe true (String.contains (String.Pattern "\"application/json\"") json)
test/Test/CookieAuthTest.purs+12−13
@@ -5,8 +5,7 @@ import Prelude
55import Data.Maybe (Maybe(..))
66import Data.String as String
77import Effect (Effect)
8−import Yoga.HTTP.API.Path (Path, Lit)
9−import Yoga.HTTP.API.Route (GET, Route, Request, buildOpenAPISpec)
8+import Yoga.HTTP.API.Route (GET, Route, buildOpenAPISpec)
109import Yoga.HTTP.API.Route.Auth (ApiKeyCookie)
1110import Yoga.JSON (writeJSON)
1211import ViTest (ViTest, describe, test)
@@ -17,15 +16,15 @@ import ViTest.Expect (expectToBe)
1716--------------------------------------------------------------------------------
1817
1918type ApiKeyCookieAPI =
20− { getProtected :: Route GET (Path (Lit "protected")) (Request { cookies :: { sessionId :: ApiKeyCookie } }) (ok :: { body :: { message :: String } })
19+ { getProtected :: Route GET "protected" { cookies :: { sessionId :: ApiKeyCookie } } (ok :: { body :: { message :: String } })
2120 }
2221
2322type RegularCookieParamsAPI =
24− { getData :: Route GET (Path (Lit "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 } })
2524 }
2625
2726type MixedCookieAPI =
28− { getResource :: Route GET (Path (Lit "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 } })
2928 }
3029
3130--------------------------------------------------------------------------------
@@ -35,20 +34,20 @@ type MixedCookieAPI =
3534testApiKeyCookieAuth :: Effect ViTest
3635testApiKeyCookieAuth = describe "ApiKeyCookie Authentication" do
3736 _ <- test "ApiKeyCookie does not appear as a regular cookie parameter" do
38− 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" }
3938 let json = writeJSON spec
4039 -- Check that parameters array is empty (no cookie parameters)
4140 let hasEmptyParams = String.contains (String.Pattern "\"parameters\":[]") json
4241 expectToBe true hasEmptyParams
4342
4443 _ <- test "ApiKeyCookie appears in security requirements" do
45− 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" }
4645 let json = writeJSON spec
4746 let hasSessionIdCookie = String.contains (String.Pattern "sessionIdCookie") json
4847 expectToBe true hasSessionIdCookie
4948
5049 test "ApiKeyCookie security scheme has correct structure" do
51− 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" }
5251 let json = writeJSON spec
5352 expectToBe true (String.contains (String.Pattern "\"type\":\"apiKey\"") json)
5453 expectToBe true (String.contains (String.Pattern "\"in\":\"cookie\"") json)
@@ -57,7 +56,7 @@ testApiKeyCookieAuth = describe "ApiKeyCookie Authentication" do
5756testRegularCookieParams :: Effect ViTest
5857testRegularCookieParams = describe "Regular Cookie Parameters" do
5958 _ <- test "Regular cookies appear as cookie parameters" do
60− 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" }
6160 let json = writeJSON spec
6261 let hasPreferenceParam = String.contains (String.Pattern "\"name\":\"preference\"") json && String.contains (String.Pattern "\"in\":\"cookie\"") json
6362 let hasThemeParam = String.contains (String.Pattern "\"name\":\"theme\"") json && String.contains (String.Pattern "\"in\":\"cookie\"") json
@@ -65,7 +64,7 @@ testRegularCookieParams = describe "Regular Cookie Parameters" do
6564 expectToBe true hasThemeParam
6665
6766 test "Regular cookies do not appear in security schemes" do
68− 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" }
6968 let json = writeJSON spec
7069 let hasPreferenceCookie = String.contains (String.Pattern "preferenceCookie") json && String.contains (String.Pattern "securitySchemes") json
7170 let hasThemeCookie = String.contains (String.Pattern "themeCookie") json && String.contains (String.Pattern "securitySchemes") json
@@ -75,13 +74,13 @@ testRegularCookieParams = describe "Regular Cookie Parameters" do
7574testMixedCookies :: Effect ViTest
7675testMixedCookies = describe "Mixed Cookie Types (Auth + Regular)" do
7776 _ <- test "Auth cookie appears in security schemes" do
78− 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" }
7978 let json = writeJSON spec
8079 let hasSessionIdAuth = String.contains (String.Pattern "sessionIdCookie") json
8180 expectToBe true hasSessionIdAuth
8281
8382 _ <- test "Auth cookie does not appear as regular parameter" do
84− 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" }
8584 let json = writeJSON spec
8685 -- sessionId should not appear as a parameter (only userId should)
8786 -- Check that the JSON doesn't contain a parameter object for sessionId
@@ -89,7 +88,7 @@ testMixedCookies = describe "Mixed Cookie Types (Auth + Regular)" do
8988 expectToBe false hasSessionIdParam
9089
9190 test "Regular cookie appears as cookie parameter" do
92− 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" }
9392 let json = writeJSON spec
9493 let hasUserIdParam = String.contains (String.Pattern "\"name\":\"userId\"") json && String.contains (String.Pattern "\"in\":\"cookie\"") json
9594 expectToBe true hasUserIdParam
test/Test/ExamplesTest.purs+7−22
@@ -7,8 +7,8 @@ import Data.String as String
77import Effect (Effect)
88import Type.Function (type (#))
99import Type.Proxy (Proxy(..))
10−import Yoga.HTTP.API.Path (Path, Lit, Capture, type (/))
11−import Yoga.HTTP.API.Route (GET, POST, Route, Request, buildOpenAPISpec)
10+import Yoga.HTTP.API.Path (type (/), type (:))
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 (Path (Lit "users" / Capture "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 (Path (Lit "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 (Path (Lit "items" / Capture "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 (Path (Lit "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 (Path (Lit "resource" / Capture "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−56
@@ -2,17 +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.Path (Path, Lit)
9−import Yoga.HTTP.API.Route (GET, Route, Request, buildOpenAPISpec)
7+import Yoga.HTTP.API.Route (GET, Route, buildOpenAPISpec)
108import Yoga.JSON (writeJSON)
119import ViTest (ViTest, describe, test)
1210import ViTest.Expect (expectToBe)
1311
1412type SimpleAPI =
15− { getRoot :: Route GET (Path (Lit "")) (Request {}) (ok :: { body :: { message :: String } })
13+ { getRoot :: Route GET "" {} (ok :: { body :: { message :: String } })
1614 }
1715
1816testInfoDescription :: Effect ViTest
@@ -22,9 +20,7 @@ testInfoDescription = describe "OpenAPI Info - Description" do
2220 spec = buildOpenAPISpec @SimpleAPI
2321 { title: "Test API"
2422 , version: "1.0.0"
25− , description: Just "This is a test API for demonstrating OpenAPI info enhancements"
26− , contact: Nothing
27− , license: Nothing
23+ , description: "This is a test API for demonstrating OpenAPI info enhancements"
2824 }
2925 json = writeJSON spec
3026 expectToBe true (String.contains (String.Pattern "\"description\":\"This is a test API for demonstrating OpenAPI info enhancements\"") json)
@@ -34,9 +30,6 @@ testInfoDescription = describe "OpenAPI Info - Description" do
3430 spec = buildOpenAPISpec @SimpleAPI
3531 { title: "Test API"
3632 , version: "1.0.0"
37− , description: Nothing
38− , contact: Nothing
39− , license: Nothing
4033 }
4134 json = writeJSON spec
4235 -- Check that description is not in the info object (it will appear in responses, which is required by OpenAPI)
@@ -50,13 +43,11 @@ testInfoContact = describe "OpenAPI Info - Contact" do
5043 spec = buildOpenAPISpec @SimpleAPI
5144 { title: "Test API"
5245 , version: "1.0.0"
53− , description: Nothing
54− , contact: Just
55− { name: Just "API Support"
56− , url: Just "https://api.example.com/support"
57− , email: Just "support@example.com"
46+ , contact:
47+ { name: "API Support"
48+ , url: "https://api.example.com/support"
49+ , email: "support@example.com"
5850 }
59− , license: Nothing
6051 }
6152 json = writeJSON spec
6253 expectToBe true (String.contains (String.Pattern "\"contact\"") json)
@@ -69,13 +60,7 @@ testInfoContact = describe "OpenAPI Info - Contact" do
6960 spec = buildOpenAPISpec @SimpleAPI
7061 { title: "Test API"
7162 , version: "1.0.0"
72− , description: Nothing
73− , contact: Just
74− { name: Just "API Team"
75− , url: Nothing
76− , email: Nothing
77− }
78− , license: Nothing
63+ , contact: { name: "API Team" }
7964 }
8065 json = writeJSON spec
8166 expectToBe true (String.contains (String.Pattern "\"contact\"") json)
@@ -86,13 +71,7 @@ testInfoContact = describe "OpenAPI Info - Contact" do
8671 spec = buildOpenAPISpec @SimpleAPI
8772 { title: "Test API"
8873 , version: "1.0.0"
89− , description: Nothing
90− , contact: Just
91− { name: Nothing
92− , url: Nothing
93− , email: Just "info@example.com"
94− }
95− , license: Nothing
74+ , contact: { email: "info@example.com" }
9675 }
9776 json = writeJSON spec
9877 expectToBe true (String.contains (String.Pattern "\"contact\"") json)
@@ -103,13 +82,10 @@ testInfoContact = describe "OpenAPI Info - Contact" do
10382 spec = buildOpenAPISpec @SimpleAPI
10483 { title: "Test API"
10584 , version: "1.0.0"
106− , description: Nothing
107− , contact: Just
108− { name: Just "Support Team"
109− , url: Just "https://support.example.com"
110− , email: Nothing
85+ , contact:
86+ { name: "Support Team"
87+ , url: "https://support.example.com"
11188 }
112− , license: Nothing
11389 }
11490 json = writeJSON spec
11591 expectToBe true (String.contains (String.Pattern "\"contact\"") json)
@@ -121,9 +97,6 @@ testInfoContact = describe "OpenAPI Info - Contact" do
12197 spec = buildOpenAPISpec @SimpleAPI
12298 { title: "Test API"
12399 , version: "1.0.0"
124− , description: Nothing
125− , contact: Nothing
126− , license: Nothing
127100 }
128101 json = writeJSON spec
129102 expectToBe false (String.contains (String.Pattern "\"contact\"") json)
@@ -135,9 +108,7 @@ testInfoLicense = describe "OpenAPI Info - License" do
135108 spec = buildOpenAPISpec @SimpleAPI
136109 { title: "Test API"
137110 , version: "1.0.0"
138− , description: Nothing
139− , contact: Nothing
140− , license: Just { name: "MIT", url: Nothing }
111+ , license: { name: "MIT" }
141112 }
142113 json = writeJSON spec
143114 expectToBe true (String.contains (String.Pattern "\"license\"") json)
@@ -148,11 +119,9 @@ testInfoLicense = describe "OpenAPI Info - License" do
148119 spec = buildOpenAPISpec @SimpleAPI
149120 { title: "Test API"
150121 , version: "1.0.0"
151− , description: Nothing
152− , contact: Nothing
153− , license: Just
122+ , license:
154123 { name: "Apache 2.0"
155− , url: Just "https://www.apache.org/licenses/LICENSE-2.0.html"
124+ , url: "https://www.apache.org/licenses/LICENSE-2.0.html"
156125 }
157126 }
158127 json = writeJSON spec
@@ -165,9 +134,6 @@ testInfoLicense = describe "OpenAPI Info - License" do
165134 spec = buildOpenAPISpec @SimpleAPI
166135 { title: "Test API"
167136 , version: "1.0.0"
168− , description: Nothing
169− , contact: Nothing
170− , license: Nothing
171137 }
172138 json = writeJSON spec
173139 expectToBe false (String.contains (String.Pattern "\"license\"") json)
@@ -179,15 +145,15 @@ testInfoAllFields = describe "OpenAPI Info - All Fields Together" do
179145 spec = buildOpenAPISpec @SimpleAPI
180146 { title: "Complete API"
181147 , version: "2.0.0"
182− , description: Just "A comprehensive API with full metadata"
183− , contact: Just
184− { name: Just "API Team"
185− , url: Just "https://example.com/contact"
186− , 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"
187153 }
188− , license: Just
154+ , license:
189155 { name: "MIT"
190− , url: Just "https://opensource.org/licenses/MIT"
156+ , url: "https://opensource.org/licenses/MIT"
191157 }
192158 }
193159 json = writeJSON spec
test/Test/OpenAPIValidation.purs+5−2
@@ -1,8 +1,11 @@
11module Test.OpenAPIValidation (validate) where
22
3+import Prelude
34import Foreign (Foreign)
5+import Yoga.HTTP.API.Route.OpenAPI (OpenAPISpec)
6+import Yoga.JSON (write)
47
58foreign import validateImpl :: Foreign -> { errors :: Array { message :: String } }
69
7−validate :: Foreign -> { errors :: Array { message :: String } }
8−validate = validateImpl
10+validate :: OpenAPISpec -> { errors :: Array { message :: String } }
11+validate = validateImpl <<< write
test/Test/Operators.purs+2
@@ -14,6 +14,7 @@ import Test.CookieAuthTest as CookieAuthTest
1414import Test.OpenAPIInfoTest as OpenAPIInfoTest
1515import Test.ContentTypeTest as ContentTypeTest
1616import Test.ExamplesTest as ExamplesTest
17+import Test.ComprehensiveExample as ComprehensiveExample
1718
1819spec :: Effect ViTest
1920spec = do
@@ -65,6 +66,7 @@ spec = do
6566 _ <- ExamplesTest.testExamplesBackwardCompatibility
6667 _ <- ExamplesTest.testExamplesInQueryParams
6768 _ <- ExamplesTest.testComplexExampleObject
69+ _ <- ComprehensiveExample.testComprehensiveExample
6870 SchemaComponentTest.testSchemaComponents
6971
7072main :: ViTest
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+9−9
@@ -5,8 +5,8 @@ import Prelude
55import Data.Maybe (Maybe(..))
66import Data.String as String
77import Effect (Effect)
8−import Yoga.HTTP.API.Path (Path, Lit, type (:), type (/))
9−import Yoga.HTTP.API.Route (GET, POST, Route, Request, JSON, Schema, buildOpenAPISpec)
8+import Yoga.HTTP.API.Path (type (:), type (/))
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 (Path (Lit "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 )