git.iko.soy/rowtype-yoga/purescript-yoga-http-api
on feat/custom-type-errors help sign in
spago.lock+247−75
since splitsnapshots
unifiedsplit
@@ -6,16 +6,13 @@
6 "core": {
6 "core": {
7 "dependencies": [
7 "dependencies": [
8 {
8 {
9− "aff": ">=7.0.0 <8.0.0"
9+ "aff": ">=7.1.0 <8.0.0"
10 },
10 },
11 {
11 {
12− "arrays": ">=7.0.0 <8.0.0"
12+ "arrays": ">=7.3.0 <8.0.0"
13− },
14− {
15− "effect": ">=4.0.0 <5.0.0"
16 },
13 },
17 {
14 {
18− "either": ">=6.0.0 <7.0.0"
15+ "either": ">=6.1.0 <7.0.0"
19 },
16 },
20 {
17 {
21 "foldable-traversable": ">=6.0.0 <7.0.0"
18 "foldable-traversable": ">=6.0.0 <7.0.0"
@@ -24,11 +21,17 @@
24 "foreign": ">=7.0.0 <8.0.0"
21 "foreign": ">=7.0.0 <8.0.0"
25 },
22 },
26 {
23 {
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"
28 },
28 },
29 {
29 {
30 "integers": ">=6.0.0 <7.0.0"
30 "integers": ">=6.0.0 <7.0.0"
31 },
31 },
32+ {
33+ "literals": ">=1.0.2 <2.0.0"
34+ },
32 {
35 {
33 "maybe": ">=6.0.0 <7.0.0"
36 "maybe": ">=6.0.0 <7.0.0"
34 },
37 },
@@ -36,28 +39,31 @@
36 "newtype": ">=5.0.0 <6.0.0"
39 "newtype": ">=5.0.0 <6.0.0"
37 },
40 },
38 {
41 {
39− "prelude": ">=6.0.0 <7.0.0"
42+ "prelude": ">=6.0.1 <7.0.0"
40 },
43 },
41 {
44 {
42 "record": ">=4.0.0 <5.0.0"
45 "record": ">=4.0.0 <5.0.0"
43 },
46 },
44 {
47 {
45− "strings": ">=6.0.0 <7.0.0"
48+ "strings": ">=6.0.1 <7.0.0"
46 },
49 },
47 {
50 {
48 "tuples": ">=7.0.0 <8.0.0"
51 "tuples": ">=7.0.0 <8.0.0"
49 },
52 },
50 {
53 {
51− "typelevel-prelude": ">=7.0.0 <8.0.0"
54+ "undefined-is-not-a-problem": ">=1.1.0 <2.0.0"
52 },
55 },
53 {
56 {
54 "unsafe-coerce": ">=6.0.0 <7.0.0"
57 "unsafe-coerce": ">=6.0.0 <7.0.0"
55 },
58 },
59+ {
60+ "untagged-union": ">=1.0.0 <2.0.0"
61+ },
56 {
62 {
57 "variant": ">=8.0.0 <9.0.0"
63 "variant": ">=8.0.0 <9.0.0"
58 },
64 },
59 {
65 {
60− "yoga-json": ">=5.0.0 <6.0.0"
66+ "yoga-json": ">=5.2.0 <6.0.0"
61 }
67 }
62 ]
68 ]
63 },
69 },
@@ -69,17 +75,26 @@
69 {
75 {
70 "console": ">=6.0.0 <7.0.0"
76 "console": ">=6.0.0 <7.0.0"
71 },
77 },
78+ {
79+ "datetime": ">=6.0.0 <7.0.0"
80+ },
72 {
81 {
73 "foreign-object": ">=4.0.0 <5.0.0"
82 "foreign-object": ">=4.0.0 <5.0.0"
74 },
83 },
75 {
84 {
76 "integers": ">=6.0.0 <7.0.0"
85 "integers": ">=6.0.0 <7.0.0"
77 },
86 },
87+ {
88+ "refs": ">=6.0.0 <7.0.0"
89+ },
78 {
90 {
79 "strings": ">=6.0.0 <7.0.0"
91 "strings": ">=6.0.0 <7.0.0"
80 },
92 },
81 {
93 {
82 "vitest": ">=1.0.0 <2.0.0"
94 "vitest": ">=1.0.0 <2.0.0"
95+ },
96+ {
97+ "yoga-om": ">=1.2.2 <2.0.0"
83 }
98 }
84 ]
99 ]
85 }
100 }
@@ -87,14 +102,14 @@
87 },
102 },
88 "package_set": {
103 "package_set": {
89 "address": {
104 "address": {
90− "registry": "51.1.0"
105+ "registry": "63.2.1"
91 },
106 },
92 "compiler": ">=0.15.15 <0.16.0",
107 "compiler": ">=0.15.15 <0.16.0",
93 "content": {
108 "content": {
94 "abc-parser": "2.0.1",
109 "abc-parser": "2.0.1",
95 "ace": "9.1.0",
110 "ace": "9.1.0",
96 "address-rfc2821": "0.1.1",
111 "address-rfc2821": "0.1.1",
97− "aff": "7.1.0",
112+ "aff": "8.0.0",
98 "aff-bus": "6.0.0",
113 "aff-bus": "6.0.0",
99 "aff-coroutines": "9.0.0",
114 "aff-coroutines": "9.0.0",
100 "aff-promise": "4.0.0",
115 "aff-promise": "4.0.0",
@@ -113,7 +128,7 @@
113 "argonaut-traversals": "10.0.0",
128 "argonaut-traversals": "10.0.0",
114 "argparse-basic": "2.0.0",
129 "argparse-basic": "2.0.0",
115 "array-builder": "0.1.2",
130 "array-builder": "0.1.2",
116− "array-search": "0.5.6",
131+ "array-search": "0.6.0",
117 "arraybuffer": "13.2.0",
132 "arraybuffer": "13.2.0",
118 "arraybuffer-builder": "3.1.0",
133 "arraybuffer-builder": "3.1.0",
119 "arraybuffer-types": "3.0.2",
134 "arraybuffer-types": "3.0.2",
@@ -122,8 +137,8 @@
122 "arrays-zipper": "2.0.1",
137 "arrays-zipper": "2.0.1",
123 "ask": "1.0.0",
138 "ask": "1.0.0",
124 "assert": "6.0.0",
139 "assert": "6.0.0",
125− "assert-multiple": "0.3.4",
140+ "assert-multiple": "0.4.0",
126− "avar": "5.0.0",
141+ "avar": "5.0.1",
127 "b64": "0.0.8",
142 "b64": "0.0.8",
128 "barbies": "1.0.1",
143 "barbies": "1.0.1",
129 "barlow-lens": "0.9.0",
144 "barlow-lens": "0.9.0",
@@ -137,7 +152,7 @@
137 "canvas-action": "9.0.0",
152 "canvas-action": "9.0.0",
138 "cartesian": "1.0.6",
153 "cartesian": "1.0.6",
139 "catenable-lists": "7.0.0",
154 "catenable-lists": "7.0.0",
140− "cbor-stream": "1.0.4",
155+ "cbor-stream": "1.3.0",
141 "chameleon": "1.0.0",
156 "chameleon": "1.0.0",
142 "chameleon-halogen": "1.0.3",
157 "chameleon-halogen": "1.0.3",
143 "chameleon-react-basic": "1.1.0",
158 "chameleon-react-basic": "1.1.0",
@@ -145,7 +160,7 @@
145 "chameleon-transformers": "1.0.0",
160 "chameleon-transformers": "1.0.0",
146 "channel": "1.0.0",
161 "channel": "1.0.0",
147 "checked-exceptions": "3.1.1",
162 "checked-exceptions": "3.1.1",
148− "choku": "1.0.1",
163+ "choku": "1.0.2",
149 "classless": "0.1.1",
164 "classless": "0.1.1",
150 "classless-arbitrary": "0.1.1",
165 "classless-arbitrary": "0.1.1",
151 "classless-decode-json": "0.1.1",
166 "classless-decode-json": "0.1.1",
@@ -153,7 +168,7 @@
153 "classnames": "2.0.0",
168 "classnames": "2.0.0",
154 "codec": "6.1.0",
169 "codec": "6.1.0",
155 "codec-argonaut": "10.0.0",
170 "codec-argonaut": "10.0.0",
156− "codec-json": "1.2.0",
171+ "codec-json": "2.0.0",
157 "colors": "7.0.1",
172 "colors": "7.0.1",
158 "concur-core": "0.5.0",
173 "concur-core": "0.5.0",
159 "concur-react": "0.5.0",
174 "concur-react": "0.5.0",
@@ -165,8 +180,9 @@
165 "convertable-options": "1.0.0",
180 "convertable-options": "1.0.0",
166 "coroutines": "7.0.0",
181 "coroutines": "7.0.0",
167 "css": "6.0.0",
182 "css": "6.0.0",
183+ "css-class-name-extractor": "0.0.4",
168 "css-frameworks": "1.0.1",
184 "css-frameworks": "1.0.1",
169− "csv-stream": "2.0.2",
185+ "csv-stream": "2.3.0",
170 "data-mvc": "0.0.2",
186 "data-mvc": "0.0.2",
171 "datetime": "6.1.0",
187 "datetime": "6.1.0",
172 "datetime-parsing": "0.2.0",
188 "datetime-parsing": "0.2.0",
@@ -178,32 +194,34 @@
178 "deno": "0.0.5",
194 "deno": "0.0.5",
179 "dissect": "1.0.0",
195 "dissect": "1.0.0",
180 "distributive": "6.0.0",
196 "distributive": "6.0.0",
197+ "dodo-printer": "2.2.3",
181 "dom-filereader": "7.0.0",
198 "dom-filereader": "7.0.0",
182 "dom-indexed": "12.0.0",
199 "dom-indexed": "12.0.0",
183 "dom-simple": "0.4.0",
200 "dom-simple": "0.4.0",
184 "dotenv": "4.0.3",
201 "dotenv": "4.0.3",
185 "droplet": "0.6.0",
202 "droplet": "0.6.0",
186 "dts": "1.0.0",
203 "dts": "1.0.0",
187− "dual-numbers": "1.0.2",
204+ "dual-numbers": "1.0.3",
188 "dynamic-buffer": "3.0.1",
205 "dynamic-buffer": "3.0.1",
189 "echarts-simple": "0.0.1",
206 "echarts-simple": "0.0.1",
190 "effect": "4.0.0",
207 "effect": "4.0.0",
191 "either": "6.1.0",
208 "either": "6.1.0",
192− "elmish": "0.11.3",
209+ "elmish": "0.13.0",
193 "elmish-enzyme": "0.1.1",
210 "elmish-enzyme": "0.1.1",
194− "elmish-hooks": "0.10.0",
211+ "elmish-hooks": "0.10.3",
195− "elmish-html": "0.8.2",
212+ "elmish-html": "0.9.0",
196 "elmish-testing-library": "0.3.2",
213 "elmish-testing-library": "0.3.2",
197 "email-validate": "7.0.0",
214 "email-validate": "7.0.0",
198 "encoding": "0.0.9",
215 "encoding": "0.0.9",
199 "enums": "6.0.1",
216 "enums": "6.0.1",
200− "env-names": "0.3.4",
217+ "env-names": "0.4.0",
201 "error": "2.0.0",
218 "error": "2.0.0",
202 "eta-conversion": "0.3.2",
219 "eta-conversion": "0.3.2",
203− "exceptions": "6.0.0",
220+ "exceptions": "6.1.0",
204 "exists": "6.0.0",
221 "exists": "6.0.0",
205 "exitcodes": "4.0.0",
222 "exitcodes": "4.0.0",
206 "expect-inferred": "3.0.0",
223 "expect-inferred": "3.0.0",
224+ "ezfetch": "1.1.0",
207 "fahrtwind": "2.0.0",
225 "fahrtwind": "2.0.0",
208 "fallback": "0.1.0",
226 "fallback": "0.1.0",
209 "fast-vect": "1.2.0",
227 "fast-vect": "1.2.0",
@@ -242,7 +260,7 @@
242 "generate-values": "1.0.1",
260 "generate-values": "1.0.1",
243 "generic-router": "0.0.1",
261 "generic-router": "0.0.1",
244 "geojson": "0.0.5",
262 "geojson": "0.0.5",
245− "geometry-plane": "1.0.3",
263+ "geometria": "2.2.0",
246 "gojs": "0.1.1",
264 "gojs": "0.1.1",
247 "grain": "3.0.0",
265 "grain": "3.0.0",
248 "grain-router": "3.0.0",
266 "grain-router": "3.0.0",
@@ -255,7 +273,7 @@
255 "halogen-css": "10.0.0",
273 "halogen-css": "10.0.0",
256 "halogen-echarts-simple": "0.0.4",
274 "halogen-echarts-simple": "0.0.4",
257 "halogen-formless": "4.0.3",
275 "halogen-formless": "4.0.3",
258− "halogen-helix": "1.0.0",
276+ "halogen-helix": "1.0.1",
259 "halogen-hooks": "0.6.3",
277 "halogen-hooks": "0.6.3",
260 "halogen-hooks-extra": "0.9.0",
278 "halogen-hooks-extra": "0.9.0",
261 "halogen-infinite-scroll": "1.1.0",
279 "halogen-infinite-scroll": "1.1.0",
@@ -272,6 +290,7 @@
272 "homogeneous": "0.4.0",
290 "homogeneous": "0.4.0",
273 "http-methods": "6.0.0",
291 "http-methods": "6.0.0",
274 "httpurple": "4.0.0",
292 "httpurple": "4.0.0",
293+ "huffman": "0.4.0",
275 "humdrum": "0.0.1",
294 "humdrum": "0.0.1",
276 "hyrule": "2.3.8",
295 "hyrule": "2.3.8",
277 "identity": "6.0.0",
296 "identity": "6.0.0",
@@ -293,19 +312,20 @@
293 "js-date": "8.0.0",
312 "js-date": "8.0.0",
294 "js-fetch": "0.2.1",
313 "js-fetch": "0.2.1",
295 "js-fileio": "3.0.0",
314 "js-fileio": "3.0.0",
296− "js-intl": "1.0.4",
315+ "js-intl": "1.1.4",
297 "js-iterators": "0.1.1",
316 "js-iterators": "0.1.1",
298 "js-maps": "0.1.2",
317 "js-maps": "0.1.2",
299 "js-promise": "1.0.0",
318 "js-promise": "1.0.0",
300 "js-promise-aff": "1.0.0",
319 "js-promise-aff": "1.0.0",
301 "js-timers": "6.1.0",
320 "js-timers": "6.1.0",
302 "js-uri": "3.1.0",
321 "js-uri": "3.1.0",
322+ "jsdom": "1.0.0",
303 "json": "1.1.0",
323 "json": "1.1.0",
304 "json-codecs": "5.0.0",
324 "json-codecs": "5.0.0",
305 "justifill": "0.5.0",
325 "justifill": "0.5.0",
306 "jwt": "0.0.9",
326 "jwt": "0.0.9",
307 "labeled-data": "0.2.0",
327 "labeled-data": "0.2.0",
308− "language-cst-parser": "0.14.0",
328+ "language-cst-parser": "0.14.1",
309 "lazy": "6.0.0",
329 "lazy": "6.0.0",
310 "lazy-joe": "1.0.0",
330 "lazy-joe": "1.0.0",
311 "lcg": "4.0.0",
331 "lcg": "4.0.0",
@@ -319,7 +339,7 @@
319 "logging-journald": "0.4.0",
339 "logging-journald": "0.4.0",
320 "lumi-components": "18.0.0",
340 "lumi-components": "18.0.0",
321 "machines": "7.0.0",
341 "machines": "7.0.0",
322− "maps-eager": "0.4.1",
342+ "maps-eager": "0.5.0",
323 "marionette": "1.0.0",
343 "marionette": "1.0.0",
324 "marionette-react-basic-hooks": "0.1.1",
344 "marionette-react-basic-hooks": "0.1.1",
325 "marked": "0.1.0",
345 "marked": "0.1.0",
@@ -355,35 +375,41 @@
355 "node-child-process": "11.1.0",
375 "node-child-process": "11.1.0",
356 "node-event-emitter": "3.0.0",
376 "node-event-emitter": "3.0.0",
357 "node-execa": "5.0.0",
377 "node-execa": "5.0.0",
358− "node-fs": "9.1.0",
378+ "node-fs": "9.2.0",
359− "node-glob-basic": "1.3.0",
379+ "node-glob-basic": "2.0.0",
360 "node-http": "9.1.0",
380 "node-http": "9.1.0",
361 "node-http2": "1.1.1",
381 "node-http2": "1.1.1",
362 "node-human-signals": "1.0.0",
382 "node-human-signals": "1.0.0",
363 "node-net": "5.1.0",
383 "node-net": "5.1.0",
364 "node-os": "5.1.0",
384 "node-os": "5.1.0",
365− "node-path": "5.0.0",
385+ "node-path": "5.0.1",
366 "node-process": "11.2.0",
386 "node-process": "11.2.0",
367− "node-readline": "8.1.0",
387+ "node-readline": "8.1.1",
368 "node-sqlite3": "8.0.0",
388 "node-sqlite3": "8.0.0",
369− "node-stream-pipes": "1.4.1",
389+ "node-stream-pipes": "2.1.6",
370− "node-streams": "9.0.0",
390+ "node-streams": "9.0.1",
371 "node-tls": "0.3.1",
391 "node-tls": "0.3.1",
372 "node-url": "7.0.1",
392 "node-url": "7.0.1",
393+ "node-workerbees": "0.3.1",
373 "node-zlib": "0.4.0",
394 "node-zlib": "0.4.0",
374 "nonempty": "7.0.0",
395 "nonempty": "7.0.0",
375 "now": "6.0.0",
396 "now": "6.0.0",
376 "npm-package-json": "2.0.0",
397 "npm-package-json": "2.0.0",
377 "nullable": "6.0.0",
398 "nullable": "6.0.0",
378− "numberfield": "0.1.0",
399+ "numberfield": "0.2.2",
379 "numbers": "9.0.1",
400 "numbers": "9.0.1",
380 "oak": "3.1.1",
401 "oak": "3.1.1",
381 "oak-debug": "1.2.2",
402 "oak-debug": "1.2.2",
382 "object-maps": "0.3.0",
403 "object-maps": "0.3.0",
383 "ocarina": "1.5.4",
404 "ocarina": "1.5.4",
405+ "oooooooooorrrrrrrmm-lib": "0.0.1",
406+ "open-colors-scales-and-schemes": "1.0.0",
384− "open-folds": "6.3.0",
407+ "open-folds": "6.4.0",
408+ "open-foreign-generic": "11.0.3",
385− "open-memoize": "6.1.0",
409+ "open-memoize": "6.2.0",
410+ "open-mkdirp-aff": "1.2.0",
386− "open-pairing": "6.1.0",
411+ "open-pairing": "6.2.0",
412+ "open-smolder": "12.0.2",
387 "options": "7.0.0",
413 "options": "7.0.0",
388 "optparse": "5.0.1",
414 "optparse": "5.0.1",
389 "ordered-collections": "3.2.0",
415 "ordered-collections": "3.2.0",
@@ -406,14 +432,16 @@
406 "pointed-list": "0.5.1",
432 "pointed-list": "0.5.1",
407 "polymorphic-vectors": "4.0.0",
433 "polymorphic-vectors": "4.0.0",
408 "posix-types": "6.0.0",
434 "posix-types": "6.0.0",
409− "postgresql": "1.6.3",
435+ "postgresql": "2.0.20",
410 "precise": "6.0.0",
436 "precise": "6.0.0",
411 "precise-datetime": "7.0.0",
437 "precise-datetime": "7.0.0",
412− "prelude": "6.0.1",
438+ "prelude": "6.0.2",
413 "prettier-printer": "3.0.0",
439 "prettier-printer": "3.0.0",
440+ "printf": "0.1.0",
441+ "priority-queue": "0.1.2",
414 "profunctor": "6.0.1",
442 "profunctor": "6.0.1",
415 "profunctor-lenses": "8.0.0",
443 "profunctor-lenses": "8.0.0",
416− "protobuf": "4.3.0",
444+ "protobuf": "4.4.0",
417 "psa-utils": "8.0.0",
445 "psa-utils": "8.0.0",
418 "psci-support": "6.0.0",
446 "psci-support": "6.0.0",
419 "punycode": "1.0.0",
447 "punycode": "1.0.0",
@@ -431,7 +459,8 @@
431 "react-basic": "17.0.0",
459 "react-basic": "17.0.0",
432 "react-basic-classic": "3.0.0",
460 "react-basic-classic": "3.0.0",
433 "react-basic-dnd": "10.1.0",
461 "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",
435 "react-basic-emotion": "7.1.0",
464 "react-basic-emotion": "7.1.0",
436 "react-basic-hooks": "8.2.0",
465 "react-basic-hooks": "8.2.0",
437 "react-basic-storybook": "2.0.0",
466 "react-basic-storybook": "2.0.0",
@@ -450,6 +479,7 @@
450 "record-studio": "1.0.4",
479 "record-studio": "1.0.4",
451 "refs": "6.0.0",
480 "refs": "6.0.0",
452 "remotedata": "5.0.1",
481 "remotedata": "5.0.1",
482+ "repr": "0.5.0",
453 "resize-observer": "1.0.0",
483 "resize-observer": "1.0.0",
454 "resource": "2.0.1",
484 "resource": "2.0.1",
455 "resourcet": "1.0.0",
485 "resourcet": "1.0.0",
@@ -457,6 +487,7 @@
457 "return": "0.2.0",
487 "return": "0.2.0",
458 "ring-modules": "5.0.1",
488 "ring-modules": "5.0.1",
459 "rito": "0.3.4",
489 "rito": "0.3.4",
490+ "roman": "0.4.0",
460 "rough-notation": "1.0.2",
491 "rough-notation": "1.0.2",
461 "routing": "11.0.0",
492 "routing": "11.0.0",
462 "routing-duplex": "0.7.0",
493 "routing-duplex": "0.7.0",
@@ -467,6 +498,7 @@
467 "selection-foldable": "0.2.0",
498 "selection-foldable": "0.2.0",
468 "selective-functors": "1.0.1",
499 "selective-functors": "1.0.1",
469 "semirings": "7.0.0",
500 "semirings": "7.0.0",
501+ "shuffle": "1.1.0",
470 "signal": "13.0.0",
502 "signal": "13.0.0",
471 "simple-emitter": "3.0.1",
503 "simple-emitter": "3.0.1",
472 "simple-i18n": "2.0.1",
504 "simple-i18n": "2.0.1",
@@ -475,14 +507,17 @@
475 "simple-ulid": "3.0.0",
507 "simple-ulid": "3.0.0",
476 "sized-matrices": "1.0.0",
508 "sized-matrices": "1.0.0",
477 "sized-vectors": "5.0.2",
509 "sized-vectors": "5.0.2",
478− "slug": "3.0.8",
510+ "slug": "3.1.0",
479 "small-ffi": "4.0.1",
511 "small-ffi": "4.0.1",
480 "soundfonts": "4.1.0",
512 "soundfonts": "4.1.0",
481− "sparse-matrices": "1.3.0",
513+ "sparse-matrices": "2.0.1",
482− "sparse-polynomials": "2.0.5",
514+ "sparse-polynomials": "3.0.1",
483− "spec": "7.6.0",
515+ "spec": "8.1.1",
516+ "spec-discovery": "8.4.0",
484− "spec-mocha": "5.1.0",
517+ "spec-mocha": "5.1.1",
518+ "spec-node": "0.0.3",
485− "spec-quickcheck": "5.0.0",
519+ "spec-quickcheck": "5.0.2",
520+ "spec-reporter-xunit": "0.7.1",
486 "splitmix": "2.1.0",
521 "splitmix": "2.1.0",
487 "ssrs": "1.0.0",
522 "ssrs": "1.0.0",
488 "st": "6.2.0",
523 "st": "6.2.0",
@@ -498,15 +533,20 @@
498 "systemd-journald": "0.3.0",
533 "systemd-journald": "0.3.0",
499 "tagged": "4.0.2",
534 "tagged": "4.0.2",
500 "tailrec": "6.1.0",
535 "tailrec": "6.1.0",
536+ "tanstack-query": "2.0.0",
501 "tecton": "0.2.1",
537 "tecton": "0.2.1",
502 "tecton-halogen": "0.2.0",
538 "tecton-halogen": "0.2.0",
503 "test-unit": "17.0.0",
539 "test-unit": "17.0.0",
504 "thermite": "6.3.1",
540 "thermite": "6.3.1",
505 "thermite-dom": "0.3.1",
541 "thermite-dom": "0.3.1",
506 "these": "6.0.0",
542 "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",
507 "toestand": "0.9.0",
547 "toestand": "0.9.0",
508 "transformation-matrix": "1.0.1",
548 "transformation-matrix": "1.0.1",
509− "transformers": "6.0.0",
549+ "transformers": "6.1.0",
510 "tree-rose": "4.0.2",
550 "tree-rose": "4.0.2",
511 "ts-bridge": "4.0.0",
551 "ts-bridge": "4.0.0",
512 "tuples": "7.0.0",
552 "tuples": "7.0.0",
@@ -535,6 +575,7 @@
535 "untagged-to-tagged": "0.1.4",
575 "untagged-to-tagged": "0.1.4",
536 "untagged-union": "1.0.0",
576 "untagged-union": "1.0.0",
537 "uri": "9.0.0",
577 "uri": "9.0.0",
578+ "url-immutable": "1.0.0",
538 "uuid": "9.0.0",
579 "uuid": "9.0.0",
539 "uuidv4": "1.0.0",
580 "uuidv4": "1.0.0",
540 "validation": "6.0.0",
581 "validation": "6.0.0",
@@ -544,7 +585,8 @@
544 "vectors": "2.1.0",
585 "vectors": "2.1.0",
545 "versions": "7.0.0",
586 "versions": "7.0.0",
546 "visx": "0.0.2",
587 "visx": "0.0.2",
588+ "vitest": "1.0.0",
547− "web-clipboard": "5.0.0",
589+ "web-clipboard": "6.0.0",
548 "web-cssom": "2.0.0",
590 "web-cssom": "2.0.0",
549 "web-cssom-view": "0.1.0",
591 "web-cssom-view": "0.1.0",
550 "web-dom": "6.0.0",
592 "web-dom": "6.0.0",
@@ -572,30 +614,26 @@
572 "webextension-polyfill": "0.1.0",
614 "webextension-polyfill": "0.1.0",
573 "webgpu": "0.0.1",
615 "webgpu": "0.0.1",
574 "which": "2.0.0",
616 "which": "2.0.0",
617+ "whine-core": "0.0.28",
575 "xterm": "1.0.0",
618 "xterm": "1.0.0",
576 "yoga-fetch": "1.0.1",
619 "yoga-fetch": "1.0.1",
577 "yoga-json": "5.1.0",
620 "yoga-json": "5.1.0",
578 "yoga-om": "0.1.0",
621 "yoga-om": "0.1.0",
579 "yoga-postgres": "6.0.0",
622 "yoga-postgres": "6.0.0",
623+ "yoga-react-dom": "1.0.1",
624+ "yoga-subtlecrypto": "0.1.0",
580 "yoga-tree": "1.0.0",
625 "yoga-tree": "1.0.0",
581 "z3": "0.0.2",
626 "z3": "0.0.2",
582 "zipperarray": "2.0.0"
627 "zipperarray": "2.0.0"
583 }
628 }
584 },
629 },
585− "extra_packages": {
630+ "extra_packages": {}
586− "vitest": {
587− "path": "../purescript-vitest"
588− },
589− "yoga-json": {
590− "path": "../purescript-yoga-json"
591− }
592− }
593 },
631 },
594 "packages": {
632 "packages": {
595 "aff": {
633 "aff": {
596 "type": "registry",
634 "type": "registry",
597− "version": "7.1.0",
635+ "version": "8.0.0",
598− "integrity": "sha256-/nOpDQVnKIoP1pQ1ZZBYpadK0TuMlG4WwufGzJpsfD0=",
636+ "integrity": "sha256-9BRIrSdRoYybfiUmBqgKJ66cPqxYHb277bVWQMn/9sg=",
599 "dependencies": [
637 "dependencies": [
600 "control",
638 "control",
601 "datetime",
639 "datetime",
@@ -634,6 +672,20 @@
634 "unsafe-coerce"
672 "unsafe-coerce"
635 ]
673 ]
636 },
674 },
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+ },
637 "bifunctors": {
689 "bifunctors": {
638 "type": "registry",
690 "type": "registry",
639 "version": "6.0.0",
691 "version": "6.0.0",
@@ -773,8 +825,8 @@
773 },
825 },
774 "exceptions": {
826 "exceptions": {
775 "type": "registry",
827 "type": "registry",
776− "version": "6.0.0",
828+ "version": "6.1.0",
777− "integrity": "sha256-z65UIKhOyVXzV8QUxtky4OiYVESNPegtuBFUdAddbTM=",
829+ "integrity": "sha256-nRrr+N0wk0TUFOWLR7e1n1oxcXo/X345bQUtoY9lW6A=",
778 "dependencies": [
830 "dependencies": [
779 "effect",
831 "effect",
780 "either",
832 "either",
@@ -910,6 +962,20 @@
910 "unfoldable"
962 "unfoldable"
911 ]
963 ]
912 },
964 },
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+ },
913 "identity": {
979 "identity": {
914 "type": "registry",
980 "type": "registry",
915 "version": "6.0.0",
981 "version": "6.0.0",
@@ -1026,6 +1092,20 @@
1026 "unfoldable"
1092 "unfoldable"
1027 ]
1093 ]
1028 },
1094 },
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+ },
1029 "maybe": {
1109 "maybe": {
1030 "type": "registry",
1110 "type": "registry",
1031 "version": "6.0.0",
1111 "version": "6.0.0",
@@ -1144,8 +1224,8 @@
1144 },
1224 },
1145 "prelude": {
1225 "prelude": {
1146 "type": "registry",
1226 "type": "registry",
1147− "version": "6.0.1",
1227+ "version": "6.0.2",
1148− "integrity": "sha256-qicw3g9AkjIxyVp73o1dF9BiTCMWouPk9rYe6AASBCU=",
1228+ "integrity": "sha256-IqykrDRquGUD/LRSad7y75hZv3bUsGQ4knc8tdez8Qw=",
1149 "dependencies": []
1229 "dependencies": []
1150 },
1230 },
1151 "profunctor": {
1231 "profunctor": {
@@ -1173,6 +1253,19 @@
1173 "unsafe-coerce"
1253 "unsafe-coerce"
1174 ]
1254 ]
1175 },
1255 },
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+ },
1176 "refs": {
1269 "refs": {
1177 "type": "registry",
1270 "type": "registry",
1178 "version": "6.0.0",
1271 "version": "6.0.0",
@@ -1242,8 +1335,8 @@
1242 },
1335 },
1243 "transformers": {
1336 "transformers": {
1244 "type": "registry",
1337 "type": "registry",
1245− "version": "6.0.0",
1338+ "version": "6.1.0",
1246− "integrity": "sha256-Nl3yXAIeeTzP6MyrkazkK7pbgeVqUm+ynLv7U5cpJYQ=",
1339+ "integrity": "sha256-QJmgNT/y7ljPHSsXaW4zxF/982BFiQ90KNL1g/NtEOQ=",
1247 "dependencies": [
1340 "dependencies": [
1248 "control",
1341 "control",
1249 "distributive",
1342 "distributive",
@@ -1256,6 +1349,7 @@
1256 "maybe",
1349 "maybe",
1257 "newtype",
1350 "newtype",
1258 "prelude",
1351 "prelude",
1352+ "st",
1259 "tailrec",
1353 "tailrec",
1260 "tuples",
1354 "tuples",
1261 "unfoldable"
1355 "unfoldable"
@@ -1286,6 +1380,39 @@
1286 "type-equality"
1380 "type-equality"
1287 ]
1381 ]
1288 },
1382 },
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+ },
1289 "unfoldable": {
1416 "unfoldable": {
1290 "type": "registry",
1417 "type": "registry",
1291 "version": "6.0.0",
1418 "version": "6.0.0",
@@ -1304,6 +1431,22 @@
1304 "integrity": "sha256-0L1QsaY20OILjfU5TV72d3U/tSjhmL9hJ32WMp857Lk=",
1431 "integrity": "sha256-0L1QsaY20OILjfU5TV72d3U/tSjhmL9hJ32WMp857Lk=",
1305 "dependencies": []
1432 "dependencies": []
1306 },
1433 },
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+ },
1307 "variant": {
1450 "variant": {
1308 "type": "registry",
1451 "type": "registry",
1309 "version": "8.0.0",
1452 "version": "8.0.0",
@@ -1321,12 +1464,12 @@
1321 ]
1464 ]
1322 },
1465 },
1323 "vitest": {
1466 "vitest": {
1324− "type": "local",
1467+ "type": "registry",
1325− "path": "../purescript-vitest",
1468+ "version": "1.0.0",
1469+ "integrity": "sha256-grMOE1y+Ld5u8NZQ5OL32Rk+MzYt0DQI3Wi4L8b9hm8=",
1326 "dependencies": [
1470 "dependencies": [
1327 "aff",
1471 "aff",
1328 "effect",
1472 "effect",
1329− "either",
1330 "exceptions",
1473 "exceptions",
1331 "js-promise",
1474 "js-promise",
1332 "js-promise-aff",
1475 "js-promise-aff",
@@ -1429,8 +1572,9 @@
1429 ]
1572 ]
1430 },
1573 },
1431 "yoga-json": {
1574 "yoga-json": {
1432− "type": "local",
1575+ "type": "registry",
1433− "path": "../purescript-yoga-json",
1576+ "version": "5.1.0",
1577+ "integrity": "sha256-DK+KyaQ7YyWo8p3SuZPPEMoVKy6HF1iV4rGFPXgBygw=",
1434 "dependencies": [
1578 "dependencies": [
1435 "arrays",
1579 "arrays",
1436 "bifunctors",
1580 "bifunctors",
@@ -1456,7 +1600,6 @@
1456 "partial",
1600 "partial",
1457 "prelude",
1601 "prelude",
1458 "record",
1602 "record",
1459− "safe-coerce",
1460 "strings",
1603 "strings",
1461 "transformers",
1604 "transformers",
1462 "tuples",
1605 "tuples",
@@ -1466,6 +1609,35 @@
1466 "yoga-tree"
1609 "yoga-tree"
1467 ]
1610 ]
1468 },
1611 },
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+ },
1469 "yoga-tree": {
1641 "yoga-tree": {
1470 "type": "registry",
1642 "type": "registry",
1471 "version": "1.0.0",
1643 "version": "1.0.0",
spago.yaml+15−15
@@ -1,11 +1,6 @@
1workspace:
1workspace:
2 packageSet:
2 packageSet:
3− registry: 51.1.0
3+ registry: 63.2.1
4− extraPackages:
5− yoga-json:
6− path: ../purescript-yoga-json
7− vitest:
8− path: ../purescript-vitest
9
4
10package:
5package:
11 name: yoga-http-api
6 name: yoga-http-api
@@ -16,24 +11,26 @@ package:
16 githubOwner: rowtype-yoga
11 githubOwner: rowtype-yoga
17 githubRepo: purescript-yoga-http-api
12 githubRepo: purescript-yoga-http-api
18 dependencies:
13 dependencies:
19− - aff: ">=7.0.0 <8.0.0"
14+ - aff: ">=7.1.0 <8.0.0"
20− - arrays: ">=7.0.0 <8.0.0"
15+ - arrays: ">=7.3.0 <8.0.0"
21− - effect: ">=4.0.0 <5.0.0"
22− - either: ">=6.0.0 <7.0.0"
16+ - either: ">=6.1.0 <7.0.0"
23 - foldable-traversable: ">=6.0.0 <7.0.0"
17 - foldable-traversable: ">=6.0.0 <7.0.0"
24 - foreign: ">=7.0.0 <8.0.0"
18 - 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"
26 - integers: ">=6.0.0 <7.0.0"
21 - integers: ">=6.0.0 <7.0.0"
22+ - literals: ">=1.0.2 <2.0.0"
27 - maybe: ">=6.0.0 <7.0.0"
23 - maybe: ">=6.0.0 <7.0.0"
28 - newtype: ">=5.0.0 <6.0.0"
24 - newtype: ">=5.0.0 <6.0.0"
29− - prelude: ">=6.0.0 <7.0.0"
25+ - prelude: ">=6.0.1 <7.0.0"
30 - record: ">=4.0.0 <5.0.0"
26 - record: ">=4.0.0 <5.0.0"
31− - strings: ">=6.0.0 <7.0.0"
27+ - strings: ">=6.0.1 <7.0.0"
32 - tuples: ">=7.0.0 <8.0.0"
28 - 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"
34 - unsafe-coerce: ">=6.0.0 <7.0.0"
30 - unsafe-coerce: ">=6.0.0 <7.0.0"
31+ - untagged-union: ">=1.0.0 <2.0.0"
35 - variant: ">=8.0.0 <9.0.0"
32 - 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"
37 test:
34 test:
38 main: Test.Operators
35 main: Test.Operators
39 dependencies:
36 dependencies:
@@ -43,3 +40,6 @@ package:
43 - arrays: ">=7.0.0 <8.0.0"
40 - arrays: ">=7.0.0 <8.0.0"
44 - strings: ">=6.0.0 <7.0.0"
41 - strings: ">=6.0.0 <7.0.0"
45 - integers: ">=6.0.0 <7.0.0"
42 - 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
13 , module Yoga.HTTP.API.Route.OpenAPIMetadata
13 , module Yoga.HTTP.API.Route.OpenAPIMetadata
14 , module Yoga.HTTP.API.Route.Route
14 , module Yoga.HTTP.API.Route.Route
15 , module Yoga.HTTP.API.Route.RouteHandler
15 , module Yoga.HTTP.API.Route.RouteHandler
16+ , module Yoga.HTTP.API.UnionTrick
16 ) where
17 ) where
17
18
18import Yoga.HTTP.API.Path (Path, Root, Lit, Capture, PathCons, Param, QueryParams, Required, type (/), type (:), type (:?), class PathPattern, pathPattern, class ParseParam, parseParam, class ParsePath, parsePath)
19import Yoga.HTTP.API.Path (Path, Root, Lit, Capture, PathCons, Param, QueryParams, Required, type (/), type (:), type (:?), class PathPattern, pathPattern, class ParseParam, parseParam, class ParsePath, parsePath)
19import Yoga.HTTP.API.Route.Auth (BearerToken(..), BasicAuth(..), ApiKeyHeader(..), ApiKeyCookie(..), DigestAuth(..))
20import Yoga.HTTP.API.Route.Auth (BearerToken(..), BasicAuth(..), ApiKeyHeader(..), ApiKeyCookie(..), DigestAuth(..))
20import Yoga.HTTP.API.Route.BearerToken (BearerToken(..))
21import Yoga.HTTP.API.Route.BearerToken (BearerToken(..))
21import Yoga.HTTP.API.Route.Encoding (JSON, FormData, MultipartFormData, PlainText, XML, CustomContentType, NoBody)
22import 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)
23import Yoga.HTTP.API.Route.HeaderError (HeaderError(..))
24import Yoga.HTTP.API.Route.HeaderError (HeaderError(..))
24import Yoga.HTTP.API.Route.HeaderValue (class HeaderValue, class HeaderValueType, headerValueType, parseHeader, printHeader)
25import Yoga.HTTP.API.Route.HeaderValue (class HeaderValue, class HeaderValueType, headerValueType, parseHeader, printHeader)
25import Yoga.HTTP.API.Route.Method (DELETE, GET, PATCH, POST, PUT)
26import Yoga.HTTP.API.Route.Method (DELETE, GET, PATCH, POST, PUT)
@@ -29,3 +30,4 @@ import Yoga.HTTP.API.Route.Response (Response(..), ResponseData, respondNoHeader
29import Yoga.HTTP.API.Route.Route (Route(..), class ConvertResponseVariant, class ConvertResponseVariantRL)
30import Yoga.HTTP.API.Route.Route (Route(..), class ConvertResponseVariant, class ConvertResponseVariantRL)
30import Yoga.HTTP.API.Route.RouteHandler (Handler, class RouteHandler, mkHandler, runHandler, class APIHandlers, apiHandlers, class ApiRecord)
31import Yoga.HTTP.API.Route.RouteHandler (Handler, class RouteHandler, mkHandler, runHandler, class APIHandlers, apiHandlers, class ApiRecord)
31import Yoga.HTTP.API.Route.StatusCode (StatusCode(..), class StatusCodeMap, statusCodeFor, statusCodeToString)
32import 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 @@
1module Yoga.HTTP.API.Route.Handler
1module Yoga.HTTP.API.Route.Handler
2 ( HandlerFn
2 ( HandlerFn
3− , Request(..)
3+ , NoRequest
4 , class DefaultRequestFields
4 , class DefaultRequestFields
5 , class DefaultRequestFieldsRL
5 , class DefaultRequestFieldsRL
6 , class SegmentPathParams
6 , class SegmentPathParams
@@ -23,18 +23,17 @@ import Yoga.HTTP.API.Path (Path, PathCons, Capture, Param, QueryParams, Required
23import Yoga.HTTP.API.Route.Encoding (JSON, NoBody)
23import Yoga.HTTP.API.Route.Encoding (JSON, NoBody)
24
24
25--------------------------------------------------------------------------------
25--------------------------------------------------------------------------------
26−-- Request Data Type
26+-- NoRequest: Type alias for empty request (no headers, cookies, or body)
27--------------------------------------------------------------------------------
27--------------------------------------------------------------------------------
28
28
29−-- | Request data combining headers and body
29+-- | Type alias for routes with no request data (no headers, cookies, or body).
30-- |
30-- |
31-- | Usage:
31-- | Usage:
32−-- | Request { body :: JSON User } -- body only
32+-- | Route GET "health" NoRequest (ok :: { body :: String })
33−-- | Request { headers :: { auth :: String } } -- headers only
33+-- |
34−-- | Request { body :: JSON User, headers :: { auth :: String } } -- both
34+-- | Equivalent to:
35−-- | Request {} -- neither
35+-- | Route GET "health" {} (ok :: { body :: String })
36−data Request :: Type -> Type
36+type NoRequest = Record ()
37−data Request r = Request r
38
37
39--------------------------------------------------------------------------------
38--------------------------------------------------------------------------------
40-- Handler Type
39-- Handler Type
@@ -162,7 +161,7 @@ instance encodingBodyNoBody :: EncodingBody NoBody Unit
162-- RequestHeaders: Extract headers row from a request type
161-- RequestHeaders: Extract headers row from a request type
163--------------------------------------------------------------------------------
162--------------------------------------------------------------------------------
164
163
165−-- | Extract the headers row from a Request type.
164+-- | Extract the headers row from a request record type.
166-- |
165-- |
167-- | The request is expected to have a `headers` field.
166-- | The request is expected to have a `headers` field.
168class RequestHeaders (request :: Type) (headers :: Row Type) | request -> headers
167class RequestHeaders (request :: Type) (headers :: Row Type) | request -> headers
@@ -170,13 +169,13 @@ class RequestHeaders (request :: Type) (headers :: Row Type) | request -> header
170instance requestHeadersRequest ::
169instance requestHeadersRequest ::
171 ( Row.Cons "headers" (Record headers) _rest requestRow
170 ( Row.Cons "headers" (Record headers) _rest requestRow
172 ) =>
171 ) =>
173− RequestHeaders (Request (Record requestRow)) headers
172+ RequestHeaders (Record requestRow) headers
174
173
175--------------------------------------------------------------------------------
174--------------------------------------------------------------------------------
176-- RequestCookies: Extract cookies row from a request type
175-- RequestCookies: Extract cookies row from a request type
177--------------------------------------------------------------------------------
176--------------------------------------------------------------------------------
178
177
179−-- | Extract the cookies row from a Request type.
178+-- | Extract the cookies row from a request record type.
180-- |
179-- |
181-- | The request is expected to have a `cookies` field.
180-- | The request is expected to have a `cookies` field.
182class RequestCookies (request :: Type) (cookies :: Row Type) | request -> cookies
181class RequestCookies (request :: Type) (cookies :: Row Type) | request -> cookies
@@ -184,13 +183,13 @@ class RequestCookies (request :: Type) (cookies :: Row Type) | request -> cookie
184instance requestCookiesRequest ::
183instance requestCookiesRequest ::
185 ( Row.Cons "cookies" (Record cookies) _rest requestRow
184 ( Row.Cons "cookies" (Record cookies) _rest requestRow
186 ) =>
185 ) =>
187− RequestCookies (Request (Record requestRow)) cookies
186+ RequestCookies (Record requestRow) cookies
188
187
189--------------------------------------------------------------------------------
188--------------------------------------------------------------------------------
190-- RequestBody: Extract body encoding from a request type
189-- RequestBody: Extract body encoding from a request type
191--------------------------------------------------------------------------------
190--------------------------------------------------------------------------------
192
191
193−-- | Extract the body encoding type from a Request type.
192+-- | Extract the body encoding type from a request record type.
194-- |
193-- |
195-- | The request is expected to have a `body` field.
194-- | The request is expected to have a `body` field.
196class RequestBody (request :: Type) (encoding :: Type) | request -> encoding
195class RequestBody (request :: Type) (encoding :: Type) | request -> encoding
@@ -198,7 +197,7 @@ class RequestBody (request :: Type) (encoding :: Type) | request -> encoding
198instance requestBodyRequest ::
197instance requestBodyRequest ::
199 ( Row.Cons "body" encoding _rest requestRow
198 ( Row.Cons "body" encoding _rest requestRow
200 ) =>
199 ) =>
201− RequestBody (Request (Record requestRow)) encoding
200+ RequestBody (Record requestRow) encoding
202
201
203--------------------------------------------------------------------------------
202--------------------------------------------------------------------------------
204-- DefaultRequestFields: Compute defaults for missing request fields
203-- 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
82import Data.Foldable (foldl)
82import Data.Foldable (foldl)
83import Data.Maybe (Maybe(..), maybe)
83import Data.Maybe (Maybe(..), maybe)
84import Data.String as String
84import 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)
85import Data.String.Regex (replace)
88import Data.String.Regex (replace)
86import Data.String.Regex.Flags (global)
89import Data.String.Regex.Flags (global)
87import Data.String.Regex.Unsafe (unsafeRegex)
90import Data.String.Regex.Unsafe (unsafeRegex)
@@ -97,7 +100,7 @@ import Unsafe.Coerce (unsafeCoerce)
97import Yoga.HTTP.API.Route.Auth (BearerToken, BasicAuth, ApiKeyHeader, ApiKeyCookie, DigestAuth)
100import Yoga.HTTP.API.Route.Auth (BearerToken, BasicAuth, ApiKeyHeader, ApiKeyCookie, DigestAuth)
98import Yoga.HTTP.API.Route.Encoding (JSON, FormData, MultipartFormData, PlainText, XML, CustomContentType, NoBody)
101import Yoga.HTTP.API.Route.Encoding (JSON, FormData, MultipartFormData, PlainText, XML, CustomContentType, NoBody)
99import Yoga.HTTP.API.Route.HeaderValue (class HeaderValueType, headerValueType)
102import 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)
101import Yoga.HTTP.API.Route.RenderMethod (class RenderMethod, renderMethod)
104import Yoga.HTTP.API.Route.RenderMethod (class RenderMethod, renderMethod)
102import Yoga.HTTP.API.Route.Response (class ToResponse)
105import Yoga.HTTP.API.Route.Response (class ToResponse)
103import Yoga.HTTP.API.Route.StatusCode (class StatusCodeMap, statusCodeFor, statusCodeToString)
106import Yoga.HTTP.API.Route.StatusCode (class StatusCodeMap, statusCodeFor, statusCodeToString)
@@ -770,6 +773,7 @@ else instance
770--------------------------------------------------------------------------------
773--------------------------------------------------------------------------------
771
774
772-- | Render a PureScript type as an OpenAPI JSON schema
775-- | Render a PureScript type as an OpenAPI JSON schema
776+class RenderJSONSchema :: forall k. k -> Constraint
773class RenderJSONSchema ty where
777class RenderJSONSchema ty where
774 renderJSONSchema :: Proxy ty -> Foreign
778 renderJSONSchema :: Proxy ty -> Foreign
775
779
@@ -1891,35 +1895,67 @@ type ServerObject =
1891 , description :: Maybe String
1895 , description :: Maybe String
1892 }
1896 }
1893
1897
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+
1894-- | Build a complete OpenAPI 3.0 spec from a type-level collection of routes.
1932-- | Build a complete OpenAPI 3.0 spec from a type-level collection of routes.
1895-- |
1933-- |
1896-- | Examples:
1934-- | Examples:
1897−-- | -- Basic usage:
1935+-- | -- Basic usage (omit all optional fields):
1898-- | buildOpenAPISpec @MyAPI
1936-- | buildOpenAPISpec @MyAPI
1899-- | { title: "My API", version: "1.0.0" }
1937-- | { title: "My API", version: "1.0.0" }
1900-- |
1938-- |
1901−-- | -- With servers:
1939+-- | -- With optional fields:
1902−-- | buildOpenAPISpec' @MyAPI
1940+-- | buildOpenAPISpec @MyAPI
1903-- | { title: "My API"
1941-- | { title: "My API"
1904-- | , version: "1.0.0"
1942-- | , version: "1.0.0"
1905−-- | , servers: Just
1943+-- | , description: "A comprehensive API"
1906−-- | [ { url: "https://api.example.com", description: Just "Production server" }
1944+-- | , contact: { name: "Support", email: "api@example.com" }
1907−-- | , { url: "https://staging-api.example.com", description: Just "Staging server" }
1908−-- | ]
1909-- | }
1945-- | }
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.
1910buildOpenAPISpec
1950buildOpenAPISpec
1911− :: forall @routes
1951+ :: forall @routes r
1912 . CollectOperations routes
1952 . CollectOperations routes
1913 => CollectRouteSchemas routes
1953 => CollectRouteSchemas routes
1914 => ValidateSchemaNames routes
1954 => ValidateSchemaNames routes
1915− => { title :: String
1955+ => CoerceFields r (OpenAPIInfoR UndefinedOr)
1916− , version :: String
1956+ => { | r }
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− }
1921 -> OpenAPISpec
1957 -> OpenAPISpec
1922−buildOpenAPISpec info = buildOpenAPISpec' @routes info { servers: Nothing }
1958+buildOpenAPISpec given = buildOpenAPISpec' @routes (coerceFields given) { servers: undefined }
1923
1959
1924-- | Build a complete OpenAPI 3.0 spec with optional servers configuration.
1960-- | Build a complete OpenAPI 3.0 spec with optional servers configuration.
1925buildOpenAPISpec'
1961buildOpenAPISpec'
@@ -1927,13 +1963,8 @@ buildOpenAPISpec'
1927 . CollectOperations routes
1963 . CollectOperations routes
1928 => CollectRouteSchemas routes
1964 => CollectRouteSchemas routes
1929 => ValidateSchemaNames routes
1965 => ValidateSchemaNames routes
1930− => { title :: String
1966+ => OpenAPIInfoUor
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) }
1967+ -> { servers :: Opt (Array ServerObject) }
1937 -> OpenAPISpec
1968 -> OpenAPISpec
1938buildOpenAPISpec' info config =
1969buildOpenAPISpec' info config =
1939 let
1970 let
@@ -1954,48 +1985,25 @@ buildOpenAPISpec' info config =
1954 [ Tuple "title" (writeImpl info.title)
1985 [ Tuple "title" (writeImpl info.title)
1955 , Tuple "version" (writeImpl info.version)
1986 , Tuple "version" (writeImpl info.version)
1956 ]
1987 ]
1957− infoWithDescription = case info.description of
1958− Nothing -> infoBase
1959− Just desc -> FObject.insert "description" (writeImpl desc) infoBase
1988+ infoWithDescription = maybe infoBase (\desc -> FObject.insert "description" (writeImpl desc) infoBase) (uorToMaybe info.description)
1960− infoWithContact = case info.contact of
1961− Nothing -> infoWithDescription
1989+ infoWithContact = maybe infoWithDescription
1962− Just contact ->
1990+ ( \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
1991+ FObject.insert "contact" (writeImpl contact) infoWithDescription
1968− withUrl = case contact.url of
1992+ )
1969− Nothing -> withName
1993+ (uorToMaybe info.contact)
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
1994+ infoWithLicense = maybe infoWithContact
1979− Just license ->
1995+ ( \license ->
1980− let
1981− licenseBase = FObject.singleton "name" (writeImpl license.name)
1996+ FObject.insert "license" (writeImpl license) infoWithContact
1982− licenseWithUrl = case license.url of
1997+ )
1983− Nothing -> licenseBase
1998+ (uorToMaybe info.license)
1984− Just url -> FObject.insert "url" (writeImpl url) licenseBase
1985− in
1986− FObject.insert "license" (unsafeCoerce licenseWithUrl) infoWithContact
1987
1999
1988 baseSpec = FObject.fromFoldable
2000 baseSpec = FObject.fromFoldable
1989 [ Tuple "openapi" (unsafeCoerce "3.0.0")
2001 [ Tuple "openapi" (unsafeCoerce "3.0.0")
1990 , Tuple "info" (unsafeCoerce infoWithLicense)
2002 , Tuple "info" (unsafeCoerce infoWithLicense)
1991 , Tuple "paths" (unsafeCoerce paths)
2003 , Tuple "paths" (unsafeCoerce paths)
1992 ]
2004 ]
1993− withComponents = case components of
1994− Nothing -> baseSpec
1995− Just c -> FObject.insert "components" c baseSpec
2005+ withComponents = maybe baseSpec (\c -> FObject.insert "components" c baseSpec) components
1996− withServers = case config.servers of
2006+ withServers = maybe withComponents (\servers -> FObject.insert "servers" (writeImpl servers) withComponents) (toMaybe config.servers)
1997− Nothing -> withComponents
1998− Just servers -> FObject.insert "servers" (writeImpl servers) withComponents
1999 in
2007 in
2000 unsafeCoerce $ withServers
2008 unsafeCoerce $ withServers
2001
2009
src/Yoga/HTTP/API/Route/RenderMethod.purs+1−1
@@ -3,7 +3,7 @@ module Yoga.HTTP.API.Route.RenderMethod
3 , renderMethod
3 , renderMethod
4 ) where
4 ) where
5
5
6−import Type.Proxy (Proxy(..))
6+import Type.Proxy (Proxy)
7import Yoga.HTTP.API.Route.Method (DELETE, GET, PATCH, POST, PUT)
7import Yoga.HTTP.API.Route.Method (DELETE, GET, PATCH, POST, PUT)
8
8
9--------------------------------------------------------------------------------
9--------------------------------------------------------------------------------
src/Yoga/HTTP/API/Route/Response.purs+186−62
@@ -378,250 +378,374 @@ respondNotModified =
378-- Status Code Helper Functions
378-- Status Code Helper Functions
379--------------------------------------------------------------------------------
379--------------------------------------------------------------------------------
380
380
381+continue :: forall body r1. body -> Variant (continue :: Response () body | r1)
381continue = respondNoHeaders @"continue"
382continue = respondNoHeaders @"continue"
382
383
384+continueWith :: forall headers body r1. Record headers -> body -> Variant (continue :: Response headers body | r1)
383−continueWith = respondWith (Proxy :: Proxy "continue")
385+continueWith headers body = respondWith (Proxy :: Proxy "continue") headers body
384
386
387+switchingProtocols :: forall body r1. body -> Variant (switchingProtocols :: Response () body | r1)
385switchingProtocols = respondNoHeaders @"switchingProtocols"
388switchingProtocols = respondNoHeaders @"switchingProtocols"
386
389
390+switchingProtocolsWith :: forall headers body r1. Record headers -> body -> Variant (switchingProtocols :: Response headers body | r1)
387−switchingProtocolsWith = respondWith (Proxy :: Proxy "switchingProtocols")
391+switchingProtocolsWith headers body = respondWith (Proxy :: Proxy "switchingProtocols") headers body
388
392
393+processing :: forall body r1. body -> Variant (processing :: Response () body | r1)
389processing = respondNoHeaders @"processing"
394processing = respondNoHeaders @"processing"
390
395
396+processingWith :: forall headers body r1. Record headers -> body -> Variant (processing :: Response headers body | r1)
391−processingWith = respondWith (Proxy :: Proxy "processing")
397+processingWith headers body = respondWith (Proxy :: Proxy "processing") headers body
392
398
399+earlyHints :: forall body r1. body -> Variant (earlyHints :: Response () body | r1)
393earlyHints = respondNoHeaders @"earlyHints"
400earlyHints = respondNoHeaders @"earlyHints"
394
401
402+earlyHintsWith :: forall headers body r1. Record headers -> body -> Variant (earlyHints :: Response headers body | r1)
395−earlyHintsWith = respondWith (Proxy :: Proxy "earlyHints")
403+earlyHintsWith headers body = respondWith (Proxy :: Proxy "earlyHints") headers body
396
404
405+ok :: forall body r1. body -> Variant (ok :: Response () body | r1)
397ok = respondNoHeaders @"ok"
406ok = respondNoHeaders @"ok"
398
407
408+okWith :: forall headers body r1. Record headers -> body -> Variant (ok :: Response headers body | r1)
399−okWith = respondWith (Proxy :: Proxy "ok")
409+okWith headers body = respondWith (Proxy :: Proxy "ok") headers body
400
410
411+created :: forall body r1. body -> Variant (created :: Response () body | r1)
401created = respondNoHeaders @"created"
412created = respondNoHeaders @"created"
402
413
414+createdWith :: forall headers body r1. Record headers -> body -> Variant (created :: Response headers body | r1)
403−createdWith = respondWith (Proxy :: Proxy "created")
415+createdWith headers body = respondWith (Proxy :: Proxy "created") headers body
404
416
417+accepted :: forall body r1. body -> Variant (accepted :: Response () body | r1)
405accepted = respondNoHeaders @"accepted"
418accepted = respondNoHeaders @"accepted"
406
419
420+acceptedWith :: forall headers body r1. Record headers -> body -> Variant (accepted :: Response headers body | r1)
407−acceptedWith = respondWith (Proxy :: Proxy "accepted")
421+acceptedWith headers body = respondWith (Proxy :: Proxy "accepted") headers body
408
422
423+nonAuthoritativeInformation :: forall body r1. body -> Variant (nonAuthoritativeInformation :: Response () body | r1)
409nonAuthoritativeInformation = respondNoHeaders @"nonAuthoritativeInformation"
424nonAuthoritativeInformation = respondNoHeaders @"nonAuthoritativeInformation"
410
425
426+nonAuthoritativeInformationWith :: forall headers body r1. Record headers -> body -> Variant (nonAuthoritativeInformation :: Response headers body | r1)
411−nonAuthoritativeInformationWith = respondWith (Proxy :: Proxy "nonAuthoritativeInformation")
427+nonAuthoritativeInformationWith headers body = respondWith (Proxy :: Proxy "nonAuthoritativeInformation") headers body
412
428
429+noContent :: forall body r1. body -> Variant (noContent :: Response () body | r1)
413noContent = respondNoHeaders @"noContent"
430noContent = respondNoHeaders @"noContent"
414
431
432+noContentWith :: forall headers body r1. Record headers -> body -> Variant (noContent :: Response headers body | r1)
415−noContentWith = respondWith (Proxy :: Proxy "noContent")
433+noContentWith headers body = respondWith (Proxy :: Proxy "noContent") headers body
416
434
435+resetContent :: forall body r1. body -> Variant (resetContent :: Response () body | r1)
417resetContent = respondNoHeaders @"resetContent"
436resetContent = respondNoHeaders @"resetContent"
418
437
438+resetContentWith :: forall headers body r1. Record headers -> body -> Variant (resetContent :: Response headers body | r1)
419−resetContentWith = respondWith (Proxy :: Proxy "resetContent")
439+resetContentWith headers body = respondWith (Proxy :: Proxy "resetContent") headers body
420
440
441+partialContent :: forall body r1. body -> Variant (partialContent :: Response () body | r1)
421partialContent = respondNoHeaders @"partialContent"
442partialContent = respondNoHeaders @"partialContent"
422
443
444+partialContentWith :: forall headers body r1. Record headers -> body -> Variant (partialContent :: Response headers body | r1)
423−partialContentWith = respondWith (Proxy :: Proxy "partialContent")
445+partialContentWith headers body = respondWith (Proxy :: Proxy "partialContent") headers body
424
446
447+multiStatus :: forall body r1. body -> Variant (multiStatus :: Response () body | r1)
425multiStatus = respondNoHeaders @"multiStatus"
448multiStatus = respondNoHeaders @"multiStatus"
426
449
450+multiStatusWith :: forall headers body r1. Record headers -> body -> Variant (multiStatus :: Response headers body | r1)
427−multiStatusWith = respondWith (Proxy :: Proxy "multiStatus")
451+multiStatusWith headers body = respondWith (Proxy :: Proxy "multiStatus") headers body
428
452
453+alreadyReported :: forall body r1. body -> Variant (alreadyReported :: Response () body | r1)
429alreadyReported = respondNoHeaders @"alreadyReported"
454alreadyReported = respondNoHeaders @"alreadyReported"
430
455
456+alreadyReportedWith :: forall headers body r1. Record headers -> body -> Variant (alreadyReported :: Response headers body | r1)
431−alreadyReportedWith = respondWith (Proxy :: Proxy "alreadyReported")
457+alreadyReportedWith headers body = respondWith (Proxy :: Proxy "alreadyReported") headers body
432
458
459+imUsed :: forall body r1. body -> Variant (imUsed :: Response () body | r1)
433imUsed = respondNoHeaders @"imUsed"
460imUsed = respondNoHeaders @"imUsed"
434
461
462+imUsedWith :: forall headers body r1. Record headers -> body -> Variant (imUsed :: Response headers body | r1)
435−imUsedWith = respondWith (Proxy :: Proxy "imUsed")
463+imUsedWith headers body = respondWith (Proxy :: Proxy "imUsed") headers body
436
464
465+multipleChoices :: forall body r1. body -> Variant (multipleChoices :: Response () body | r1)
437multipleChoices = respondNoHeaders @"multipleChoices"
466multipleChoices = respondNoHeaders @"multipleChoices"
438
467
468+multipleChoicesWith :: forall headers body r1. Record headers -> body -> Variant (multipleChoices :: Response headers body | r1)
439−multipleChoicesWith = respondWith (Proxy :: Proxy "multipleChoices")
469+multipleChoicesWith headers body = respondWith (Proxy :: Proxy "multipleChoices") headers body
440
470
471+movedPermanently :: forall body r1. body -> Variant (movedPermanently :: Response () body | r1)
441movedPermanently = respondNoHeaders @"movedPermanently"
472movedPermanently = respondNoHeaders @"movedPermanently"
442
473
474+movedPermanentlyWith :: forall headers body r1. Record headers -> body -> Variant (movedPermanently :: Response headers body | r1)
443−movedPermanentlyWith = respondWith (Proxy :: Proxy "movedPermanently")
475+movedPermanentlyWith headers body = respondWith (Proxy :: Proxy "movedPermanently") headers body
444
476
477+found :: forall body r1. body -> Variant (found :: Response () body | r1)
445found = respondNoHeaders @"found"
478found = respondNoHeaders @"found"
446
479
480+foundWith :: forall headers body r1. Record headers -> body -> Variant (found :: Response headers body | r1)
447−foundWith = respondWith (Proxy :: Proxy "found")
481+foundWith headers body = respondWith (Proxy :: Proxy "found") headers body
448
482
483+seeOther :: forall body r1. body -> Variant (seeOther :: Response () body | r1)
449seeOther = respondNoHeaders @"seeOther"
484seeOther = respondNoHeaders @"seeOther"
450
485
486+seeOtherWith :: forall headers body r1. Record headers -> body -> Variant (seeOther :: Response headers body | r1)
451−seeOtherWith = respondWith (Proxy :: Proxy "seeOther")
487+seeOtherWith headers body = respondWith (Proxy :: Proxy "seeOther") headers body
452
488
489+notModified :: forall body r1. body -> Variant (notModified :: Response () body | r1)
453notModified = respondNoHeaders @"notModified"
490notModified = respondNoHeaders @"notModified"
454
491
492+notModifiedWith :: forall headers body r1. Record headers -> body -> Variant (notModified :: Response headers body | r1)
455−notModifiedWith = respondWith (Proxy :: Proxy "notModified")
493+notModifiedWith headers body = respondWith (Proxy :: Proxy "notModified") headers body
456
494
495+useProxy :: forall body r1. body -> Variant (useProxy :: Response () body | r1)
457useProxy = respondNoHeaders @"useProxy"
496useProxy = respondNoHeaders @"useProxy"
458
497
498+useProxyWith :: forall headers body r1. Record headers -> body -> Variant (useProxy :: Response headers body | r1)
459−useProxyWith = respondWith (Proxy :: Proxy "useProxy")
499+useProxyWith headers body = respondWith (Proxy :: Proxy "useProxy") headers body
460
500
501+temporaryRedirect :: forall body r1. body -> Variant (temporaryRedirect :: Response () body | r1)
461temporaryRedirect = respondNoHeaders @"temporaryRedirect"
502temporaryRedirect = respondNoHeaders @"temporaryRedirect"
462
503
504+temporaryRedirectWith :: forall headers body r1. Record headers -> body -> Variant (temporaryRedirect :: Response headers body | r1)
463−temporaryRedirectWith = respondWith (Proxy :: Proxy "temporaryRedirect")
505+temporaryRedirectWith headers body = respondWith (Proxy :: Proxy "temporaryRedirect") headers body
464
506
507+permanentRedirect :: forall body r1. body -> Variant (permanentRedirect :: Response () body | r1)
465permanentRedirect = respondNoHeaders @"permanentRedirect"
508permanentRedirect = respondNoHeaders @"permanentRedirect"
466
509
510+permanentRedirectWith :: forall headers body r1. Record headers -> body -> Variant (permanentRedirect :: Response headers body | r1)
467−permanentRedirectWith = respondWith (Proxy :: Proxy "permanentRedirect")
511+permanentRedirectWith headers body = respondWith (Proxy :: Proxy "permanentRedirect") headers body
468
512
513+badRequest :: forall body r1. body -> Variant (badRequest :: Response () body | r1)
469badRequest = respondNoHeaders @"badRequest"
514badRequest = respondNoHeaders @"badRequest"
470
515
516+badRequestWith :: forall headers body r1. Record headers -> body -> Variant (badRequest :: Response headers body | r1)
471−badRequestWith = respondWith (Proxy :: Proxy "badRequest")
517+badRequestWith headers body = respondWith (Proxy :: Proxy "badRequest") headers body
472
518
519+unauthorized :: forall body r1. body -> Variant (unauthorized :: Response () body | r1)
473unauthorized = respondNoHeaders @"unauthorized"
520unauthorized = respondNoHeaders @"unauthorized"
474
521
522+unauthorizedWith :: forall headers body r1. Record headers -> body -> Variant (unauthorized :: Response headers body | r1)
475−unauthorizedWith = respondWith (Proxy :: Proxy "unauthorized")
523+unauthorizedWith headers body = respondWith (Proxy :: Proxy "unauthorized") headers body
476
524
525+paymentRequired :: forall body r1. body -> Variant (paymentRequired :: Response () body | r1)
477paymentRequired = respondNoHeaders @"paymentRequired"
526paymentRequired = respondNoHeaders @"paymentRequired"
478
527
528+paymentRequiredWith :: forall headers body r1. Record headers -> body -> Variant (paymentRequired :: Response headers body | r1)
479−paymentRequiredWith = respondWith (Proxy :: Proxy "paymentRequired")
529+paymentRequiredWith headers body = respondWith (Proxy :: Proxy "paymentRequired") headers body
480
530
531+forbidden :: forall body r1. body -> Variant (forbidden :: Response () body | r1)
481forbidden = respondNoHeaders @"forbidden"
532forbidden = respondNoHeaders @"forbidden"
482
533
534+forbiddenWith :: forall headers body r1. Record headers -> body -> Variant (forbidden :: Response headers body | r1)
483−forbiddenWith = respondWith (Proxy :: Proxy "forbidden")
535+forbiddenWith headers body = respondWith (Proxy :: Proxy "forbidden") headers body
484
536
537+notFound :: forall body r1. body -> Variant (notFound :: Response () body | r1)
485notFound = respondNoHeaders @"notFound"
538notFound = respondNoHeaders @"notFound"
486
539
540+notFoundWith :: forall headers body r1. Record headers -> body -> Variant (notFound :: Response headers body | r1)
487−notFoundWith = respondWith (Proxy :: Proxy "notFound")
541+notFoundWith headers body = respondWith (Proxy :: Proxy "notFound") headers body
488
542
543+methodNotAllowed :: forall body r1. body -> Variant (methodNotAllowed :: Response () body | r1)
489methodNotAllowed = respondNoHeaders @"methodNotAllowed"
544methodNotAllowed = respondNoHeaders @"methodNotAllowed"
490
545
546+methodNotAllowedWith :: forall headers body r1. Record headers -> body -> Variant (methodNotAllowed :: Response headers body | r1)
491−methodNotAllowedWith = respondWith (Proxy :: Proxy "methodNotAllowed")
547+methodNotAllowedWith headers body = respondWith (Proxy :: Proxy "methodNotAllowed") headers body
492
548
549+notAcceptable :: forall body r1. body -> Variant (notAcceptable :: Response () body | r1)
493notAcceptable = respondNoHeaders @"notAcceptable"
550notAcceptable = respondNoHeaders @"notAcceptable"
494
551
552+notAcceptableWith :: forall headers body r1. Record headers -> body -> Variant (notAcceptable :: Response headers body | r1)
495−notAcceptableWith = respondWith (Proxy :: Proxy "notAcceptable")
553+notAcceptableWith headers body = respondWith (Proxy :: Proxy "notAcceptable") headers body
496
554
555+proxyAuthenticationRequired :: forall body r1. body -> Variant (proxyAuthenticationRequired :: Response () body | r1)
497proxyAuthenticationRequired = respondNoHeaders @"proxyAuthenticationRequired"
556proxyAuthenticationRequired = respondNoHeaders @"proxyAuthenticationRequired"
498
557
558+proxyAuthenticationRequiredWith :: forall headers body r1. Record headers -> body -> Variant (proxyAuthenticationRequired :: Response headers body | r1)
499−proxyAuthenticationRequiredWith = respondWith (Proxy :: Proxy "proxyAuthenticationRequired")
559+proxyAuthenticationRequiredWith headers body = respondWith (Proxy :: Proxy "proxyAuthenticationRequired") headers body
500
560
561+requestTimeout :: forall body r1. body -> Variant (requestTimeout :: Response () body | r1)
501requestTimeout = respondNoHeaders @"requestTimeout"
562requestTimeout = respondNoHeaders @"requestTimeout"
502
563
564+requestTimeoutWith :: forall headers body r1. Record headers -> body -> Variant (requestTimeout :: Response headers body | r1)
503−requestTimeoutWith = respondWith (Proxy :: Proxy "requestTimeout")
565+requestTimeoutWith headers body = respondWith (Proxy :: Proxy "requestTimeout") headers body
504
566
567+conflict :: forall body r1. body -> Variant (conflict :: Response () body | r1)
505conflict = respondNoHeaders @"conflict"
568conflict = respondNoHeaders @"conflict"
506
569
570+conflictWith :: forall headers body r1. Record headers -> body -> Variant (conflict :: Response headers body | r1)
507−conflictWith = respondWith (Proxy :: Proxy "conflict")
571+conflictWith headers body = respondWith (Proxy :: Proxy "conflict") headers body
508
572
573+gone :: forall body r1. body -> Variant (gone :: Response () body | r1)
509gone = respondNoHeaders @"gone"
574gone = respondNoHeaders @"gone"
510
575
576+goneWith :: forall headers body r1. Record headers -> body -> Variant (gone :: Response headers body | r1)
511−goneWith = respondWith (Proxy :: Proxy "gone")
577+goneWith headers body = respondWith (Proxy :: Proxy "gone") headers body
512
578
579+lengthRequired :: forall body r1. body -> Variant (lengthRequired :: Response () body | r1)
513lengthRequired = respondNoHeaders @"lengthRequired"
580lengthRequired = respondNoHeaders @"lengthRequired"
514
581
582+lengthRequiredWith :: forall headers body r1. Record headers -> body -> Variant (lengthRequired :: Response headers body | r1)
515−lengthRequiredWith = respondWith (Proxy :: Proxy "lengthRequired")
583+lengthRequiredWith headers body = respondWith (Proxy :: Proxy "lengthRequired") headers body
516
584
585+preconditionFailed :: forall body r1. body -> Variant (preconditionFailed :: Response () body | r1)
517preconditionFailed = respondNoHeaders @"preconditionFailed"
586preconditionFailed = respondNoHeaders @"preconditionFailed"
518
587
588+preconditionFailedWith :: forall headers body r1. Record headers -> body -> Variant (preconditionFailed :: Response headers body | r1)
519−preconditionFailedWith = respondWith (Proxy :: Proxy "preconditionFailed")
589+preconditionFailedWith headers body = respondWith (Proxy :: Proxy "preconditionFailed") headers body
520
590
591+payloadTooLarge :: forall body r1. body -> Variant (payloadTooLarge :: Response () body | r1)
521payloadTooLarge = respondNoHeaders @"payloadTooLarge"
592payloadTooLarge = respondNoHeaders @"payloadTooLarge"
522
593
594+payloadTooLargeWith :: forall headers body r1. Record headers -> body -> Variant (payloadTooLarge :: Response headers body | r1)
523−payloadTooLargeWith = respondWith (Proxy :: Proxy "payloadTooLarge")
595+payloadTooLargeWith headers body = respondWith (Proxy :: Proxy "payloadTooLarge") headers body
524
596
597+uriTooLong :: forall body r1. body -> Variant (uriTooLong :: Response () body | r1)
525uriTooLong = respondNoHeaders @"uriTooLong"
598uriTooLong = respondNoHeaders @"uriTooLong"
526
599
600+uriTooLongWith :: forall headers body r1. Record headers -> body -> Variant (uriTooLong :: Response headers body | r1)
527−uriTooLongWith = respondWith (Proxy :: Proxy "uriTooLong")
601+uriTooLongWith headers body = respondWith (Proxy :: Proxy "uriTooLong") headers body
528
602
603+unsupportedMediaType :: forall body r1. body -> Variant (unsupportedMediaType :: Response () body | r1)
529unsupportedMediaType = respondNoHeaders @"unsupportedMediaType"
604unsupportedMediaType = respondNoHeaders @"unsupportedMediaType"
530
605
606+unsupportedMediaTypeWith :: forall headers body r1. Record headers -> body -> Variant (unsupportedMediaType :: Response headers body | r1)
531−unsupportedMediaTypeWith = respondWith (Proxy :: Proxy "unsupportedMediaType")
607+unsupportedMediaTypeWith headers body = respondWith (Proxy :: Proxy "unsupportedMediaType") headers body
532
608
609+rangeNotSatisfiable :: forall body r1. body -> Variant (rangeNotSatisfiable :: Response () body | r1)
533rangeNotSatisfiable = respondNoHeaders @"rangeNotSatisfiable"
610rangeNotSatisfiable = respondNoHeaders @"rangeNotSatisfiable"
534
611
612+rangeNotSatisfiableWith :: forall headers body r1. Record headers -> body -> Variant (rangeNotSatisfiable :: Response headers body | r1)
535−rangeNotSatisfiableWith = respondWith (Proxy :: Proxy "rangeNotSatisfiable")
613+rangeNotSatisfiableWith headers body = respondWith (Proxy :: Proxy "rangeNotSatisfiable") headers body
536
614
615+expectationFailed :: forall body r1. body -> Variant (expectationFailed :: Response () body | r1)
537expectationFailed = respondNoHeaders @"expectationFailed"
616expectationFailed = respondNoHeaders @"expectationFailed"
538
617
618+expectationFailedWith :: forall headers body r1. Record headers -> body -> Variant (expectationFailed :: Response headers body | r1)
539−expectationFailedWith = respondWith (Proxy :: Proxy "expectationFailed")
619+expectationFailedWith headers body = respondWith (Proxy :: Proxy "expectationFailed") headers body
540
620
621+imATeapot :: forall body r1. body -> Variant (imATeapot :: Response () body | r1)
541imATeapot = respondNoHeaders @"imATeapot"
622imATeapot = respondNoHeaders @"imATeapot"
542
623
624+imATeapotWith :: forall headers body r1. Record headers -> body -> Variant (imATeapot :: Response headers body | r1)
543−imATeapotWith = respondWith (Proxy :: Proxy "imATeapot")
625+imATeapotWith headers body = respondWith (Proxy :: Proxy "imATeapot") headers body
544
626
627+misdirectedRequest :: forall body r1. body -> Variant (misdirectedRequest :: Response () body | r1)
545misdirectedRequest = respondNoHeaders @"misdirectedRequest"
628misdirectedRequest = respondNoHeaders @"misdirectedRequest"
546
629
630+misdirectedRequestWith :: forall headers body r1. Record headers -> body -> Variant (misdirectedRequest :: Response headers body | r1)
547−misdirectedRequestWith = respondWith (Proxy :: Proxy "misdirectedRequest")
631+misdirectedRequestWith headers body = respondWith (Proxy :: Proxy "misdirectedRequest") headers body
548
632
633+unprocessableEntity :: forall body r1. body -> Variant (unprocessableEntity :: Response () body | r1)
549unprocessableEntity = respondNoHeaders @"unprocessableEntity"
634unprocessableEntity = respondNoHeaders @"unprocessableEntity"
550
635
636+unprocessableEntityWith :: forall headers body r1. Record headers -> body -> Variant (unprocessableEntity :: Response headers body | r1)
551−unprocessableEntityWith = respondWith (Proxy :: Proxy "unprocessableEntity")
637+unprocessableEntityWith headers body = respondWith (Proxy :: Proxy "unprocessableEntity") headers body
552
638
639+locked :: forall body r1. body -> Variant (locked :: Response () body | r1)
553locked = respondNoHeaders @"locked"
640locked = respondNoHeaders @"locked"
554
641
642+lockedWith :: forall headers body r1. Record headers -> body -> Variant (locked :: Response headers body | r1)
555−lockedWith = respondWith (Proxy :: Proxy "locked")
643+lockedWith headers body = respondWith (Proxy :: Proxy "locked") headers body
556
644
645+failedDependency :: forall body r1. body -> Variant (failedDependency :: Response () body | r1)
557failedDependency = respondNoHeaders @"failedDependency"
646failedDependency = respondNoHeaders @"failedDependency"
558
647
648+failedDependencyWith :: forall headers body r1. Record headers -> body -> Variant (failedDependency :: Response headers body | r1)
559−failedDependencyWith = respondWith (Proxy :: Proxy "failedDependency")
649+failedDependencyWith headers body = respondWith (Proxy :: Proxy "failedDependency") headers body
560
650
651+tooEarly :: forall body r1. body -> Variant (tooEarly :: Response () body | r1)
561tooEarly = respondNoHeaders @"tooEarly"
652tooEarly = respondNoHeaders @"tooEarly"
562
653
654+tooEarlyWith :: forall headers body r1. Record headers -> body -> Variant (tooEarly :: Response headers body | r1)
563−tooEarlyWith = respondWith (Proxy :: Proxy "tooEarly")
655+tooEarlyWith headers body = respondWith (Proxy :: Proxy "tooEarly") headers body
564
656
657+upgradeRequired :: forall body r1. body -> Variant (upgradeRequired :: Response () body | r1)
565upgradeRequired = respondNoHeaders @"upgradeRequired"
658upgradeRequired = respondNoHeaders @"upgradeRequired"
566
659
660+upgradeRequiredWith :: forall headers body r1. Record headers -> body -> Variant (upgradeRequired :: Response headers body | r1)
567−upgradeRequiredWith = respondWith (Proxy :: Proxy "upgradeRequired")
661+upgradeRequiredWith headers body = respondWith (Proxy :: Proxy "upgradeRequired") headers body
568
662
663+preconditionRequired :: forall body r1. body -> Variant (preconditionRequired :: Response () body | r1)
569preconditionRequired = respondNoHeaders @"preconditionRequired"
664preconditionRequired = respondNoHeaders @"preconditionRequired"
570
665
666+preconditionRequiredWith :: forall headers body r1. Record headers -> body -> Variant (preconditionRequired :: Response headers body | r1)
571−preconditionRequiredWith = respondWith (Proxy :: Proxy "preconditionRequired")
667+preconditionRequiredWith headers body = respondWith (Proxy :: Proxy "preconditionRequired") headers body
572
668
669+tooManyRequests :: forall body r1. body -> Variant (tooManyRequests :: Response () body | r1)
573tooManyRequests = respondNoHeaders @"tooManyRequests"
670tooManyRequests = respondNoHeaders @"tooManyRequests"
574
671
672+tooManyRequestsWith :: forall headers body r1. Record headers -> body -> Variant (tooManyRequests :: Response headers body | r1)
575−tooManyRequestsWith = respondWith (Proxy :: Proxy "tooManyRequests")
673+tooManyRequestsWith headers body = respondWith (Proxy :: Proxy "tooManyRequests") headers body
576
674
675+requestHeaderFieldsTooLarge :: forall body r1. body -> Variant (requestHeaderFieldsTooLarge :: Response () body | r1)
577requestHeaderFieldsTooLarge = respondNoHeaders @"requestHeaderFieldsTooLarge"
676requestHeaderFieldsTooLarge = respondNoHeaders @"requestHeaderFieldsTooLarge"
578
677
678+requestHeaderFieldsTooLargeWith :: forall headers body r1. Record headers -> body -> Variant (requestHeaderFieldsTooLarge :: Response headers body | r1)
579−requestHeaderFieldsTooLargeWith = respondWith (Proxy :: Proxy "requestHeaderFieldsTooLarge")
679+requestHeaderFieldsTooLargeWith headers body = respondWith (Proxy :: Proxy "requestHeaderFieldsTooLarge") headers body
580
680
681+unavailableForLegalReasons :: forall body r1. body -> Variant (unavailableForLegalReasons :: Response () body | r1)
581unavailableForLegalReasons = respondNoHeaders @"unavailableForLegalReasons"
682unavailableForLegalReasons = respondNoHeaders @"unavailableForLegalReasons"
582
683
684+unavailableForLegalReasonsWith :: forall headers body r1. Record headers -> body -> Variant (unavailableForLegalReasons :: Response headers body | r1)
583−unavailableForLegalReasonsWith = respondWith (Proxy :: Proxy "unavailableForLegalReasons")
685+unavailableForLegalReasonsWith headers body = respondWith (Proxy :: Proxy "unavailableForLegalReasons") headers body
584
686
687+internalServerError :: forall body r1. body -> Variant (internalServerError :: Response () body | r1)
585internalServerError = respondNoHeaders @"internalServerError"
688internalServerError = respondNoHeaders @"internalServerError"
586
689
690+internalServerErrorWith :: forall headers body r1. Record headers -> body -> Variant (internalServerError :: Response headers body | r1)
587−internalServerErrorWith = respondWith (Proxy :: Proxy "internalServerError")
691+internalServerErrorWith headers body = respondWith (Proxy :: Proxy "internalServerError") headers body
588
692
693+notImplemented :: forall body r1. body -> Variant (notImplemented :: Response () body | r1)
589notImplemented = respondNoHeaders @"notImplemented"
694notImplemented = respondNoHeaders @"notImplemented"
590
695
696+notImplementedWith :: forall headers body r1. Record headers -> body -> Variant (notImplemented :: Response headers body | r1)
591−notImplementedWith = respondWith (Proxy :: Proxy "notImplemented")
697+notImplementedWith headers body = respondWith (Proxy :: Proxy "notImplemented") headers body
592
698
699+badGateway :: forall body r1. body -> Variant (badGateway :: Response () body | r1)
593badGateway = respondNoHeaders @"badGateway"
700badGateway = respondNoHeaders @"badGateway"
594
701
702+badGatewayWith :: forall headers body r1. Record headers -> body -> Variant (badGateway :: Response headers body | r1)
595−badGatewayWith = respondWith (Proxy :: Proxy "badGateway")
703+badGatewayWith headers body = respondWith (Proxy :: Proxy "badGateway") headers body
596
704
705+serviceUnavailable :: forall body r1. body -> Variant (serviceUnavailable :: Response () body | r1)
597serviceUnavailable = respondNoHeaders @"serviceUnavailable"
706serviceUnavailable = respondNoHeaders @"serviceUnavailable"
598
707
708+serviceUnavailableWith :: forall headers body r1. Record headers -> body -> Variant (serviceUnavailable :: Response headers body | r1)
599−serviceUnavailableWith = respondWith (Proxy :: Proxy "serviceUnavailable")
709+serviceUnavailableWith headers body = respondWith (Proxy :: Proxy "serviceUnavailable") headers body
600
710
711+gatewayTimeout :: forall body r1. body -> Variant (gatewayTimeout :: Response () body | r1)
601gatewayTimeout = respondNoHeaders @"gatewayTimeout"
712gatewayTimeout = respondNoHeaders @"gatewayTimeout"
602
713
714+gatewayTimeoutWith :: forall headers body r1. Record headers -> body -> Variant (gatewayTimeout :: Response headers body | r1)
603−gatewayTimeoutWith = respondWith (Proxy :: Proxy "gatewayTimeout")
715+gatewayTimeoutWith headers body = respondWith (Proxy :: Proxy "gatewayTimeout") headers body
604
716
717+httpVersionNotSupported :: forall body r1. body -> Variant (httpVersionNotSupported :: Response () body | r1)
605httpVersionNotSupported = respondNoHeaders @"httpVersionNotSupported"
718httpVersionNotSupported = respondNoHeaders @"httpVersionNotSupported"
606
719
720+httpVersionNotSupportedWith :: forall headers body r1. Record headers -> body -> Variant (httpVersionNotSupported :: Response headers body | r1)
607−httpVersionNotSupportedWith = respondWith (Proxy :: Proxy "httpVersionNotSupported")
721+httpVersionNotSupportedWith headers body = respondWith (Proxy :: Proxy "httpVersionNotSupported") headers body
608
722
723+variantAlsoNegotiates :: forall body r1. body -> Variant (variantAlsoNegotiates :: Response () body | r1)
609variantAlsoNegotiates = respondNoHeaders @"variantAlsoNegotiates"
724variantAlsoNegotiates = respondNoHeaders @"variantAlsoNegotiates"
610
725
726+variantAlsoNegotiatesWith :: forall headers body r1. Record headers -> body -> Variant (variantAlsoNegotiates :: Response headers body | r1)
611−variantAlsoNegotiatesWith = respondWith (Proxy :: Proxy "variantAlsoNegotiates")
727+variantAlsoNegotiatesWith headers body = respondWith (Proxy :: Proxy "variantAlsoNegotiates") headers body
612
728
729+insufficientStorage :: forall body r1. body -> Variant (insufficientStorage :: Response () body | r1)
613insufficientStorage = respondNoHeaders @"insufficientStorage"
730insufficientStorage = respondNoHeaders @"insufficientStorage"
614
731
732+insufficientStorageWith :: forall headers body r1. Record headers -> body -> Variant (insufficientStorage :: Response headers body | r1)
615−insufficientStorageWith = respondWith (Proxy :: Proxy "insufficientStorage")
733+insufficientStorageWith headers body = respondWith (Proxy :: Proxy "insufficientStorage") headers body
616
734
735+loopDetected :: forall body r1. body -> Variant (loopDetected :: Response () body | r1)
617loopDetected = respondNoHeaders @"loopDetected"
736loopDetected = respondNoHeaders @"loopDetected"
618
737
738+loopDetectedWith :: forall headers body r1. Record headers -> body -> Variant (loopDetected :: Response headers body | r1)
619−loopDetectedWith = respondWith (Proxy :: Proxy "loopDetected")
739+loopDetectedWith headers body = respondWith (Proxy :: Proxy "loopDetected") headers body
620
740
741+notExtended :: forall body r1. body -> Variant (notExtended :: Response () body | r1)
621notExtended = respondNoHeaders @"notExtended"
742notExtended = respondNoHeaders @"notExtended"
622
743
744+notExtendedWith :: forall headers body r1. Record headers -> body -> Variant (notExtended :: Response headers body | r1)
623−notExtendedWith = respondWith (Proxy :: Proxy "notExtended")
745+notExtendedWith headers body = respondWith (Proxy :: Proxy "notExtended") headers body
624
746
747+networkAuthenticationRequired :: forall body r1. body -> Variant (networkAuthenticationRequired :: Response () body | r1)
625networkAuthenticationRequired = respondNoHeaders @"networkAuthenticationRequired"
748networkAuthenticationRequired = respondNoHeaders @"networkAuthenticationRequired"
626
749
750+networkAuthenticationRequiredWith :: forall headers body r1. Record headers -> body -> Variant (networkAuthenticationRequired :: Response headers body | r1)
627−networkAuthenticationRequiredWith = respondWith (Proxy :: Proxy "networkAuthenticationRequired")
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)
12import Prim.RowList (class RowToList, RowList)
12import Prim.RowList (class RowToList, RowList)
13import Prim.RowList as RL
13import Prim.RowList as RL
14import Type.Proxy (Proxy(..))
14import Type.Proxy (Proxy(..))
15−import Unsafe.Coerce (unsafeCoerce)
16import Yoga.HTTP.API.Path (class PathPattern, pathPattern)
15import 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)
18import 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)
17import 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)
19import Yoga.HTTP.API.Route.OpenAPIMetadata (class HasOperationMetadata, operationMetadata)
18import Yoga.HTTP.API.Route.OpenAPIMetadata (class HasOperationMetadata, operationMetadata)
20import Yoga.HTTP.API.Route.RenderMethod (class RenderMethod, renderMethod)
19import Yoga.HTTP.API.Route.RenderMethod (class RenderMethod, renderMethod)
@@ -67,9 +66,9 @@ instance
67 , RenderRequestBodySchema encoding
66 , RenderRequestBodySchema encoding
68 , RowToList userResp rl
67 , RowToList userResp rl
69 , RenderVariantResponseSchemaRL rl
68 , RenderVariantResponseSchemaRL rl
70− , HasOperationMetadata (Route method segments (Request (Record partialRequest)) userResp)
69+ , HasOperationMetadata (Route method segments (Record partialRequest) userResp)
71 ) =>
70 ) =>
72− ToOpenAPI (Route method segments (Request (Record partialRequest)) userResp) where
71+ ToOpenAPI (Route method segments (Record partialRequest) userResp) where
73 toOpenAPIImpl proxy =
72 toOpenAPIImpl proxy =
74 let
73 let
75 methodStr = renderMethod (Proxy :: Proxy method)
74 methodStr = renderMethod (Proxy :: Proxy method)
@@ -122,7 +121,7 @@ instance
122 , RowToList userResp rl
121 , RowToList userResp rl
123 , CollectVariantSchemasRL rl
122 , CollectVariantSchemasRL rl
124 ) =>
123 ) =>
125− CollectRouteSchemas (Route method segments (Request (Record partialRequest)) userResp) where
124+ CollectRouteSchemas (Route method segments (Record partialRequest) userResp) where
126 collectRouteSchemas _ =
125 collectRouteSchemas _ =
127 let
126 let
128 requestSchemas = collectSchemas (Proxy :: Proxy encoding)
127 requestSchemas = collectSchemas (Proxy :: Proxy encoding)
@@ -138,4 +137,4 @@ instance
138 , CollectVariantSchemaNames rl respNames
137 , CollectVariantSchemaNames rl respNames
139 , Union reqNames respNames names
138 , Union reqNames respNames names
140 ) =>
139 ) =>
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)
14import Unsafe.Coerce (unsafeCoerce)
14import Unsafe.Coerce (unsafeCoerce)
15import Yoga.HTTP.API.Route.Handler
15import Yoga.HTTP.API.Route.Handler
16 ( HandlerFn
16 ( HandlerFn
17− , Request
18 , class DefaultRequestFields
17 , class DefaultRequestFields
19 , class EncodingBody
18 , class EncodingBody
20 , class SegmentPathParams
19 , class SegmentPathParams
@@ -42,7 +41,7 @@ instance
42 , ConvertResponseVariant userResp respVariant
41 , ConvertResponseVariant userResp respVariant
43 ) =>
42 ) =>
44 RouteHandler
43 RouteHandler
45− (Route method segments (Request (Record partialRequest)) userResp)
44+ (Route method segments (Record partialRequest) userResp)
46 pathParams
45 pathParams
47 queryParams
46 queryParams
48 fullHeaders
47 fullHeaders
src/Yoga/HTTP/API/Route/StatusCode.purs+1−1
@@ -10,7 +10,7 @@ module Yoga.HTTP.API.Route.StatusCode
10
10
11import Prelude
11import Prelude
12
12
13−import Type.Proxy (Proxy(..))
13+import Type.Proxy (Proxy)
14import Data.Newtype (class Newtype)
14import Data.Newtype (class Newtype)
15import Prim.TypeError (class Fail, Text, Above, Quote, Beside)
15import Prim.TypeError (class Fail, Text, Above, Quote, Beside)
16
16
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
5import Data.Variant (Variant)
5import Data.Variant (Variant)
6import Effect.Aff (Aff)
6import Effect.Aff (Aff)
7import Yoga.HTTP.API.Path (Path, Lit)
7import Yoga.HTTP.API.Path (Path, Lit)
8−import Yoga.HTTP.API.Route.Handler (Request)
8+import Yoga.HTTP.API.Route.Handler
9import Yoga.HTTP.API.Route.Method (GET)
9import Yoga.HTTP.API.Route.Method (GET)
10import Yoga.HTTP.API.Route.Response (Response, respondNoHeaders)
10import Yoga.HTTP.API.Route.Response (Response, respondNoHeaders)
11import Yoga.HTTP.API.Route.Route (Route)
11import Yoga.HTTP.API.Route.Route (Route)
12import Yoga.HTTP.API.Route.RouteHandler (Handler, mkHandler)
12import Yoga.HTTP.API.Route.RouteHandler (Handler, mkHandler)
13
13
14-- Route has no body (defaults to NoBody/Unit), but handler uses body as String
14-- 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 })
16
16
17test :: Handler MyRoute
17test :: Handler MyRoute
18test = mkHandler \{ body } ->
18test = mkHandler \{ body } ->
test-compile-fail/cases/DuplicateCaptureNames.purs+2−2
@@ -7,14 +7,14 @@ import Data.Variant (Variant)
7import Effect.Aff (Aff)
7import Effect.Aff (Aff)
8import Yoga.HTTP.API.Path (Path, Lit, Capture, type (/), type (:))
8import Yoga.HTTP.API.Path (Path, Lit, Capture, type (/), type (:))
9import Yoga.HTTP.API.Route.Encoding (NoBody)
9import Yoga.HTTP.API.Route.Encoding (NoBody)
10−import Yoga.HTTP.API.Route.Handler (Request)
10+import Yoga.HTTP.API.Route.Handler
11import Yoga.HTTP.API.Route.Method (GET)
11import Yoga.HTTP.API.Route.Method (GET)
12import Yoga.HTTP.API.Route.Response (Response, respondNoHeaders)
12import Yoga.HTTP.API.Route.Response (Response, respondNoHeaders)
13import Yoga.HTTP.API.Route.Route (Route)
13import Yoga.HTTP.API.Route.Route (Route)
14import Yoga.HTTP.API.Route.RouteHandler (Handler, mkHandler)
14import Yoga.HTTP.API.Route.RouteHandler (Handler, mkHandler)
15
15
16-- Two captures both named "id" with different types
16-- 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 })
18
18
19test :: Handler MyRoute
19test :: Handler MyRoute
20test = mkHandler \{ path: { id } } ->
20test = mkHandler \{ path: { id } } ->
test-compile-fail/cases/HandlerSignatureMismatch.purs+2−2
@@ -7,13 +7,13 @@ import Data.Variant (Variant)
7import Effect.Aff (Aff)
7import Effect.Aff (Aff)
8import Yoga.HTTP.API.Path (Path, Lit, Capture, type (/), type (:))
8import Yoga.HTTP.API.Path (Path, Lit, Capture, type (/), type (:))
9import Yoga.HTTP.API.Route.Encoding (NoBody)
9import Yoga.HTTP.API.Route.Encoding (NoBody)
10−import Yoga.HTTP.API.Route.Handler (Request)
10+import Yoga.HTTP.API.Route.Handler
11import Yoga.HTTP.API.Route.Method (GET)
11import Yoga.HTTP.API.Route.Method (GET)
12import Yoga.HTTP.API.Route.Response (Response, respondNoHeaders)
12import Yoga.HTTP.API.Route.Response (Response, respondNoHeaders)
13import Yoga.HTTP.API.Route.Route (Route)
13import Yoga.HTTP.API.Route.Route (Route)
14import Yoga.HTTP.API.Route.RouteHandler (Handler, mkHandler)
14import Yoga.HTTP.API.Route.RouteHandler (Handler, mkHandler)
15
15
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 })
17
17
18-- Handler takes { id :: String } but route has Capture "id" Int
18-- Handler takes { id :: String } but route has Capture "id" Int
19test :: Handler MyRoute
19test :: Handler MyRoute
test-compile-fail/cases/InvalidResponseRecordStructure.purs+2−2
@@ -3,14 +3,14 @@ module Test.CompileFail.InvalidResponseRecordStructure where
3
3
4import Prelude
4import Prelude
5import Yoga.HTTP.API.Path (Path, Lit)
5import Yoga.HTTP.API.Path (Path, Lit)
6−import Yoga.HTTP.API.Route.Handler (Request)
6+import Yoga.HTTP.API.Route.Handler
7import Yoga.HTTP.API.Route.Method (GET)
7import Yoga.HTTP.API.Route.Method (GET)
8import Yoga.HTTP.API.Route.Route (Route)
8import Yoga.HTTP.API.Route.Route (Route)
9import Yoga.HTTP.API.Route.RouteHandler (Handler, mkHandler)
9import Yoga.HTTP.API.Route.RouteHandler (Handler, mkHandler)
10import Yoga.HTTP.API.Route.Response (respondNoHeaders)
10import Yoga.HTTP.API.Route.Response (respondNoHeaders)
11
11
12-- Response record has "status" field which is not "body" or "headers"
12-- 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 })
14
14
15test :: Handler MyRoute
15test :: Handler MyRoute
16test = mkHandler \_ ->
16test = mkHandler \_ ->
test-compile-fail/cases/MissingRequiredQueryParam.purs+2−2
@@ -5,7 +5,7 @@ import Prelude
5import Data.Variant (Variant)
5import Data.Variant (Variant)
6import Effect.Aff (Aff)
6import Effect.Aff (Aff)
7import Yoga.HTTP.API.Path (Path, Lit, QueryParams, type (:?))
7import Yoga.HTTP.API.Path (Path, Lit, QueryParams, type (:?))
8−import Yoga.HTTP.API.Route.Handler (Request)
8+import Yoga.HTTP.API.Route.Handler
9import Yoga.HTTP.API.Route.Method (GET)
9import Yoga.HTTP.API.Route.Method (GET)
10import Yoga.HTTP.API.Route.Response (Response, respondNoHeaders)
10import Yoga.HTTP.API.Route.Response (Response, respondNoHeaders)
11import Yoga.HTTP.API.Route.Route (Route)
11import Yoga.HTTP.API.Route.Route (Route)
@@ -13,7 +13,7 @@ import Yoga.HTTP.API.Route.RouteHandler (Handler, mkHandler)
13
13
14-- Query param "limit" is optional (no Required wrapper), so handler gets Maybe Int
14-- Query param "limit" is optional (no Required wrapper), so handler gets Maybe Int
15-- But handler tries to use it as plain Int
15-- 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 })
17
17
18test :: Handler MyRoute
18test :: Handler MyRoute
19test = mkHandler \{ query: { limit } } ->
19test = mkHandler \{ query: { limit } } ->
test-compile-fail/cases/SchemaNameCollision.purs+3−3
@@ -4,7 +4,7 @@ module Test.CompileFail.SchemaNameCollision where
4import Data.Maybe (Maybe(..))
4import Data.Maybe (Maybe(..))
5import Yoga.HTTP.API.Path (Path, Lit)
5import Yoga.HTTP.API.Path (Path, Lit)
6import Yoga.HTTP.API.Route.Encoding (JSON)
6import Yoga.HTTP.API.Route.Encoding (JSON)
7−import Yoga.HTTP.API.Route.Handler (Request)
7+import Yoga.HTTP.API.Route.Handler
8import Yoga.HTTP.API.Route.Method (GET, POST)
8import Yoga.HTTP.API.Route.Method (GET, POST)
9import Yoga.HTTP.API.Route.OpenAPIMetadata (Schema)
9import Yoga.HTTP.API.Route.OpenAPIMetadata (Schema)
10import Yoga.HTTP.API.Route.OpenAPI (buildOpenAPISpec, OpenAPISpec)
10import Yoga.HTTP.API.Route.OpenAPI (buildOpenAPISpec, OpenAPISpec)
@@ -14,8 +14,8 @@ type UserV1 = { id :: Int, name :: String }
14type UserV2 = { id :: Int, email :: String, age :: Int }
14type UserV2 = { id :: Int, email :: String, age :: Int }
15
15
16type CollisionAPI =
16type CollisionAPI =
17− { getUser :: Route GET (Path (Lit "users")) (Request {}) (ok :: { body :: Schema "User" UserV1 })
17+ { getUser :: Route GET (Path (Lit "users")) {} (ok :: { body :: Schema "User" UserV1 })
18− , createUser :: Route POST (Path (Lit "users")) (Request { body :: JSON (Schema "User" UserV2) }) (created :: { body :: Schema "User" UserV2 })
18+ , createUser :: Route POST (Path (Lit "users")) { body :: JSON (Schema "User" UserV2) } (created :: { body :: Schema "User" UserV2 })
19 }
19 }
20
20
21spec :: OpenAPISpec
21spec :: OpenAPISpec
test-compile-fail/cases/WrongResponseVariantLabel.purs+2−2
@@ -5,14 +5,14 @@ import Prelude
5import Data.Variant (Variant)
5import Data.Variant (Variant)
6import Effect.Aff (Aff)
6import Effect.Aff (Aff)
7import Yoga.HTTP.API.Path (Path, Lit)
7import Yoga.HTTP.API.Path (Path, Lit)
8−import Yoga.HTTP.API.Route.Handler (Request)
8+import Yoga.HTTP.API.Route.Handler
9import Yoga.HTTP.API.Route.Method (GET)
9import Yoga.HTTP.API.Route.Method (GET)
10import Yoga.HTTP.API.Route.Response (Response, respondNoHeaders)
10import Yoga.HTTP.API.Route.Response (Response, respondNoHeaders)
11import Yoga.HTTP.API.Route.Route (Route)
11import Yoga.HTTP.API.Route.Route (Route)
12import Yoga.HTTP.API.Route.RouteHandler (Handler, mkHandler)
12import Yoga.HTTP.API.Route.RouteHandler (Handler, mkHandler)
13
13
14-- Route only has "ok" response, no "notFound"
14-- 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 })
16
16
17test :: Handler MyRoute
17test :: Handler MyRoute
18test = mkHandler \_ ->
18test = mkHandler \_ ->
test/Test/APIRecordTest.purs+7−7
@@ -9,7 +9,7 @@ import Effect (Effect)
9import Effect.Aff (Aff)
9import Effect.Aff (Aff)
10import Type.Proxy (Proxy(..))
10import Type.Proxy (Proxy(..))
11import Yoga.HTTP.API.Path (type (/), type (:))
11import Yoga.HTTP.API.Path (type (/), type (:))
12−import Yoga.HTTP.API.Route (GET, POST, Route, Request, JSON, respondNoHeaders, buildOpenAPISpec, collectOperations, mkHandler, apiHandlers)
12+import Yoga.HTTP.API.Route (GET, POST, Route, JSON, respondNoHeaders, buildOpenAPISpec, collectOperations, mkHandler, apiHandlers)
13import Yoga.JSON (writeJSON)
13import Yoga.JSON (writeJSON)
14import ViTest (ViTest, describe, test)
14import ViTest (ViTest, describe, test)
15import ViTest.Expect (expectToBe)
15import ViTest.Expect (expectToBe)
@@ -24,9 +24,9 @@ expectToEqual expected actual = expectToBe true (expected == actual)
24type User = { id :: Int, name :: String }
24type User = { id :: Int, name :: String }
25
25
26type TestAPI =
26type TestAPI =
27− { getUser :: Route GET ("users" / "id" : Int) (Request {}) (ok :: { body :: User })
27+ { getUser :: Route GET ("users" / "id" : Int) {} (ok :: { body :: User })
28− , listUsers :: Route GET "users" (Request {}) (ok :: { body :: Array User })
28+ , listUsers :: Route GET "users" {} (ok :: { body :: Array User })
29− , createUser :: Route POST "users" (Request { body :: JSON User }) (created :: { body :: User })
29+ , createUser :: Route POST "users" { body :: JSON User } (created :: { body :: User })
30 }
30 }
31
31
32--------------------------------------------------------------------------------
32--------------------------------------------------------------------------------
@@ -75,20 +75,20 @@ testOperationIds = describe "API Record - OperationId" do
75testBuildOpenAPISpec :: Effect ViTest
75testBuildOpenAPISpec :: Effect ViTest
76testBuildOpenAPISpec = describe "API Record - buildOpenAPISpec" do
76testBuildOpenAPISpec = describe "API Record - buildOpenAPISpec" do
77 _ <- test "generates valid OpenAPI spec with operationIds" do
77 _ <- 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" }
79 let json = writeJSON spec
79 let json = writeJSON spec
80 expectToBe true (String.contains (String.Pattern "\"operationId\":\"getUser\"") json)
80 expectToBe true (String.contains (String.Pattern "\"operationId\":\"getUser\"") json)
81 expectToBe true (String.contains (String.Pattern "\"operationId\":\"listUsers\"") json)
81 expectToBe true (String.contains (String.Pattern "\"operationId\":\"listUsers\"") json)
82 expectToBe true (String.contains (String.Pattern "\"operationId\":\"createUser\"") json)
82 expectToBe true (String.contains (String.Pattern "\"operationId\":\"createUser\"") json)
83
83
84 _ <- test "contains paths" do
84 _ <- 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" }
86 let json = writeJSON spec
86 let json = writeJSON spec
87 expectToBe true (String.contains (String.Pattern "/users/{id}") json)
87 expectToBe true (String.contains (String.Pattern "/users/{id}") json)
88 expectToBe true (String.contains (String.Pattern "/users") json)
88 expectToBe true (String.contains (String.Pattern "/users") json)
89
89
90 test "contains API info" do
90 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" }
92 let json = writeJSON spec
92 let json = writeJSON spec
93 expectToBe true (String.contains (String.Pattern "Test API") json)
93 expectToBe true (String.contains (String.Pattern "Test API") json)
94 expectToBe true (String.contains (String.Pattern "1.0.0") json)
94 expectToBe true (String.contains (String.Pattern "1.0.0") json)
test/Test/BearerAuthTest.purs+23−23
@@ -6,7 +6,7 @@ import Data.Array as Array
6import Data.Maybe (Maybe(..))
6import Data.Maybe (Maybe(..))
7import Data.String as String
7import Data.String as String
8import Effect (Effect)
8import Effect (Effect)
9−import Yoga.HTTP.API.Route (GET, Route, Request, buildOpenAPISpec)
9+import Yoga.HTTP.API.Route (GET, Route, buildOpenAPISpec)
10import Yoga.HTTP.API.Route.Auth (BearerToken, BasicAuth, ApiKeyHeader, DigestAuth)
10import Yoga.HTTP.API.Route.Auth (BearerToken, BasicAuth, ApiKeyHeader, DigestAuth)
11import Yoga.JSON (writeJSON)
11import Yoga.JSON (writeJSON)
12import Test.OpenAPIValidation (validate)
12import Test.OpenAPIValidation (validate)
@@ -18,25 +18,25 @@ import ViTest.Expect (expectToBe)
18--------------------------------------------------------------------------------
18--------------------------------------------------------------------------------
19
19
20type BearerAuthAPI =
20type BearerAuthAPI =
21− { getProtected :: Route GET "protected" (Request { headers :: { authorization :: BearerToken } }) (ok :: { body :: { message :: String } })
21+ { getProtected :: Route GET "protected" { headers :: { authorization :: BearerToken } } (ok :: { body :: { message :: String } })
22 }
22 }
23
23
24type BasicAuthAPI =
24type BasicAuthAPI =
25− { getProtected :: Route GET "protected" (Request { headers :: { authorization :: BasicAuth } }) (ok :: { body :: { message :: String } })
25+ { getProtected :: Route GET "protected" { headers :: { authorization :: BasicAuth } } (ok :: { body :: { message :: String } })
26 }
26 }
27
27
28type ApiKeyAPI =
28type ApiKeyAPI =
29− { getProtected :: Route GET "protected" (Request { headers :: { apiKey :: ApiKeyHeader } }) (ok :: { body :: { message :: String } })
29+ { getProtected :: Route GET "protected" { headers :: { apiKey :: ApiKeyHeader } } (ok :: { body :: { message :: String } })
30 }
30 }
31
31
32type DigestAuthAPI =
32type DigestAuthAPI =
33− { getProtected :: Route GET "protected" (Request { headers :: { authorization :: DigestAuth } }) (ok :: { body :: { message :: String } })
33+ { getProtected :: Route GET "protected" { headers :: { authorization :: DigestAuth } } (ok :: { body :: { message :: String } })
34 }
34 }
35
35
36type MultiAuthAPI =
36type MultiAuthAPI =
37− { bearerEndpoint :: Route GET "bearer" (Request { headers :: { authorization :: BearerToken } }) (ok :: { body :: { message :: String } })
37+ { bearerEndpoint :: Route GET "bearer" { headers :: { authorization :: BearerToken } } (ok :: { body :: { message :: String } })
38− , basicEndpoint :: Route GET "basic" (Request { headers :: { authorization :: BasicAuth } }) (ok :: { body :: { message :: String } })
38+ , basicEndpoint :: Route GET "basic" { headers :: { authorization :: BasicAuth } } (ok :: { body :: { message :: String } })
39− , apiKeyEndpoint :: Route GET "apikey" (Request { headers :: { xApiKey :: ApiKeyHeader } }) (ok :: { body :: { message :: String } })
39+ , apiKeyEndpoint :: Route GET "apikey" { headers :: { xApiKey :: ApiKeyHeader } } (ok :: { body :: { message :: String } })
40 }
40 }
41
41
42--------------------------------------------------------------------------------
42--------------------------------------------------------------------------------
@@ -46,44 +46,44 @@ type MultiAuthAPI =
46testBearerTokenAuth :: Effect ViTest
46testBearerTokenAuth :: Effect ViTest
47testBearerTokenAuth = describe "BearerToken Authentication" do
47testBearerTokenAuth = describe "BearerToken Authentication" do
48 _ <- test "BearerToken does not appear as a regular header parameter" do
48 _ <- test "BearerToken does not appear as a regular header parameter" do
49− let spec = buildOpenAPISpec @BearerAuthAPI { title: "Auth API", version: "1.0.0", description: Nothing, contact: Nothing, license: Nothing }
49+ let spec = buildOpenAPISpec @BearerAuthAPI { title: "Auth API", version: "1.0.0" }
50 let json = writeJSON spec
50 let json = writeJSON spec
51 let hasAuthParam = String.contains (String.Pattern "\"name\":\"authorization\"") json
51 let hasAuthParam = String.contains (String.Pattern "\"name\":\"authorization\"") json
52 expectToBe false hasAuthParam
52 expectToBe false hasAuthParam
53
53
54 _ <- test "BearerToken appears in security requirements" do
54 _ <- test "BearerToken appears in security requirements" do
55− let spec = buildOpenAPISpec @BearerAuthAPI { title: "Auth API", version: "1.0.0", description: Nothing, contact: Nothing, license: Nothing }
55+ let spec = buildOpenAPISpec @BearerAuthAPI { title: "Auth API", version: "1.0.0" }
56 let json = writeJSON spec
56 let json = writeJSON spec
57 let hasBearerAuth = String.contains (String.Pattern "bearerAuth") json
57 let hasBearerAuth = String.contains (String.Pattern "bearerAuth") json
58 expectToBe true hasBearerAuth
58 expectToBe true hasBearerAuth
59
59
60 _ <- test "BearerToken security scheme has correct structure" do
60 _ <- test "BearerToken security scheme has correct structure" do
61− let spec = buildOpenAPISpec @BearerAuthAPI { title: "Auth API", version: "1.0.0", description: Nothing, contact: Nothing, license: Nothing }
61+ let spec = buildOpenAPISpec @BearerAuthAPI { title: "Auth API", version: "1.0.0" }
62 let json = writeJSON spec
62 let json = writeJSON spec
63 expectToBe true (String.contains (String.Pattern "\"type\":\"http\"") json)
63 expectToBe true (String.contains (String.Pattern "\"type\":\"http\"") json)
64 expectToBe true (String.contains (String.Pattern "\"scheme\":\"bearer\"") json)
64 expectToBe true (String.contains (String.Pattern "\"scheme\":\"bearer\"") json)
65
65
66 test "BearerToken API generates valid OpenAPI 3.0 schema" do
66 test "BearerToken API generates valid OpenAPI 3.0 schema" do
67− let spec = buildOpenAPISpec @BearerAuthAPI { title: "Auth API", version: "1.0.0", description: Nothing, contact: Nothing, license: Nothing }
67+ let spec = buildOpenAPISpec @BearerAuthAPI { title: "Auth API", version: "1.0.0" }
68 let result = validate spec
68 let result = validate spec
69 expectToBe true (Array.null result.errors)
69 expectToBe true (Array.null result.errors)
70
70
71testBasicAuth :: Effect ViTest
71testBasicAuth :: Effect ViTest
72testBasicAuth = describe "BasicAuth Authentication" do
72testBasicAuth = describe "BasicAuth Authentication" do
73 _ <- test "BasicAuth does not appear as a regular header parameter" do
73 _ <- test "BasicAuth does not appear as a regular header parameter" do
74− let spec = buildOpenAPISpec @BasicAuthAPI { title: "Auth API", version: "1.0.0", description: Nothing, contact: Nothing, license: Nothing }
74+ let spec = buildOpenAPISpec @BasicAuthAPI { title: "Auth API", version: "1.0.0" }
75 let json = writeJSON spec
75 let json = writeJSON spec
76 let hasAuthParam = String.contains (String.Pattern "\"name\":\"authorization\"") json
76 let hasAuthParam = String.contains (String.Pattern "\"name\":\"authorization\"") json
77 expectToBe false hasAuthParam
77 expectToBe false hasAuthParam
78
78
79 _ <- test "BasicAuth appears in security requirements" do
79 _ <- test "BasicAuth appears in security requirements" do
80− let spec = buildOpenAPISpec @BasicAuthAPI { title: "Auth API", version: "1.0.0", description: Nothing, contact: Nothing, license: Nothing }
80+ let spec = buildOpenAPISpec @BasicAuthAPI { title: "Auth API", version: "1.0.0" }
81 let json = writeJSON spec
81 let json = writeJSON spec
82 let hasBasicAuth = String.contains (String.Pattern "basicAuth") json
82 let hasBasicAuth = String.contains (String.Pattern "basicAuth") json
83 expectToBe true hasBasicAuth
83 expectToBe true hasBasicAuth
84
84
85 test "BasicAuth security scheme has correct structure" do
85 test "BasicAuth security scheme has correct structure" do
86− let spec = buildOpenAPISpec @BasicAuthAPI { title: "Auth API", version: "1.0.0", description: Nothing, contact: Nothing, license: Nothing }
86+ let spec = buildOpenAPISpec @BasicAuthAPI { title: "Auth API", version: "1.0.0" }
87 let json = writeJSON spec
87 let json = writeJSON spec
88 expectToBe true (String.contains (String.Pattern "\"type\":\"http\"") json)
88 expectToBe true (String.contains (String.Pattern "\"type\":\"http\"") json)
89 expectToBe true (String.contains (String.Pattern "\"scheme\":\"basic\"") json)
89 expectToBe true (String.contains (String.Pattern "\"scheme\":\"basic\"") json)
@@ -91,20 +91,20 @@ testBasicAuth = describe "BasicAuth Authentication" do
91testApiKeyAuth :: Effect ViTest
91testApiKeyAuth :: Effect ViTest
92testApiKeyAuth = describe "ApiKey Authentication" do
92testApiKeyAuth = describe "ApiKey Authentication" do
93 _ <- test "ApiKeyHeader does not appear as a regular header parameter" do
93 _ <- test "ApiKeyHeader does not appear as a regular header parameter" do
94− let spec = buildOpenAPISpec @ApiKeyAPI { title: "Auth API", version: "1.0.0", description: Nothing, contact: Nothing, license: Nothing }
94+ let spec = buildOpenAPISpec @ApiKeyAPI { title: "Auth API", version: "1.0.0" }
95 let json = writeJSON spec
95 let json = writeJSON spec
96 -- Check that parameters array is empty (no header parameters)
96 -- Check that parameters array is empty (no header parameters)
97 let hasEmptyParams = String.contains (String.Pattern "\"parameters\":[]") json
97 let hasEmptyParams = String.contains (String.Pattern "\"parameters\":[]") json
98 expectToBe true hasEmptyParams
98 expectToBe true hasEmptyParams
99
99
100 _ <- test "ApiKey appears in security requirements" do
100 _ <- test "ApiKey appears in security requirements" do
101− let spec = buildOpenAPISpec @ApiKeyAPI { title: "Auth API", version: "1.0.0", description: Nothing, contact: Nothing, license: Nothing }
101+ let spec = buildOpenAPISpec @ApiKeyAPI { title: "Auth API", version: "1.0.0" }
102 let json = writeJSON spec
102 let json = writeJSON spec
103 let hasApiKey = String.contains (String.Pattern "apiKeyApiKey") json
103 let hasApiKey = String.contains (String.Pattern "apiKeyApiKey") json
104 expectToBe true hasApiKey
104 expectToBe true hasApiKey
105
105
106 test "ApiKey security scheme has correct structure" do
106 test "ApiKey security scheme has correct structure" do
107− let spec = buildOpenAPISpec @ApiKeyAPI { title: "Auth API", version: "1.0.0", description: Nothing, contact: Nothing, license: Nothing }
107+ let spec = buildOpenAPISpec @ApiKeyAPI { title: "Auth API", version: "1.0.0" }
108 let json = writeJSON spec
108 let json = writeJSON spec
109 expectToBe true (String.contains (String.Pattern "\"type\":\"apiKey\"") json)
109 expectToBe true (String.contains (String.Pattern "\"type\":\"apiKey\"") json)
110 expectToBe true (String.contains (String.Pattern "\"in\":\"header\"") json)
110 expectToBe true (String.contains (String.Pattern "\"in\":\"header\"") json)
@@ -112,19 +112,19 @@ testApiKeyAuth = describe "ApiKey Authentication" do
112testDigestAuth :: Effect ViTest
112testDigestAuth :: Effect ViTest
113testDigestAuth = describe "DigestAuth Authentication" do
113testDigestAuth = describe "DigestAuth Authentication" do
114 _ <- test "DigestAuth does not appear as a regular header parameter" do
114 _ <- test "DigestAuth does not appear as a regular header parameter" do
115− let spec = buildOpenAPISpec @DigestAuthAPI { title: "Auth API", version: "1.0.0", description: Nothing, contact: Nothing, license: Nothing }
115+ let spec = buildOpenAPISpec @DigestAuthAPI { title: "Auth API", version: "1.0.0" }
116 let json = writeJSON spec
116 let json = writeJSON spec
117 let hasAuthParam = String.contains (String.Pattern "\"name\":\"authorization\"") json
117 let hasAuthParam = String.contains (String.Pattern "\"name\":\"authorization\"") json
118 expectToBe false hasAuthParam
118 expectToBe false hasAuthParam
119
119
120 _ <- test "DigestAuth appears in security requirements" do
120 _ <- test "DigestAuth appears in security requirements" do
121− let spec = buildOpenAPISpec @DigestAuthAPI { title: "Auth API", version: "1.0.0", description: Nothing, contact: Nothing, license: Nothing }
121+ let spec = buildOpenAPISpec @DigestAuthAPI { title: "Auth API", version: "1.0.0" }
122 let json = writeJSON spec
122 let json = writeJSON spec
123 let hasDigestAuth = String.contains (String.Pattern "digestAuth") json
123 let hasDigestAuth = String.contains (String.Pattern "digestAuth") json
124 expectToBe true hasDigestAuth
124 expectToBe true hasDigestAuth
125
125
126 test "DigestAuth security scheme has correct structure" do
126 test "DigestAuth security scheme has correct structure" do
127− let spec = buildOpenAPISpec @DigestAuthAPI { title: "Auth API", version: "1.0.0", description: Nothing, contact: Nothing, license: Nothing }
127+ let spec = buildOpenAPISpec @DigestAuthAPI { title: "Auth API", version: "1.0.0" }
128 let json = writeJSON spec
128 let json = writeJSON spec
129 expectToBe true (String.contains (String.Pattern "\"type\":\"http\"") json)
129 expectToBe true (String.contains (String.Pattern "\"type\":\"http\"") json)
130 expectToBe true (String.contains (String.Pattern "\"scheme\":\"digest\"") json)
130 expectToBe true (String.contains (String.Pattern "\"scheme\":\"digest\"") json)
@@ -132,14 +132,14 @@ testDigestAuth = describe "DigestAuth Authentication" do
132testMultipleAuthTypes :: Effect ViTest
132testMultipleAuthTypes :: Effect ViTest
133testMultipleAuthTypes = describe "Multiple Authentication Types" do
133testMultipleAuthTypes = describe "Multiple Authentication Types" do
134 _ <- test "All auth types appear in security schemes" do
134 _ <- test "All auth types appear in security schemes" do
135− let spec = buildOpenAPISpec @MultiAuthAPI { title: "Auth API", version: "1.0.0", description: Nothing, contact: Nothing, license: Nothing }
135+ let spec = buildOpenAPISpec @MultiAuthAPI { title: "Auth API", version: "1.0.0" }
136 let json = writeJSON spec
136 let json = writeJSON spec
137 expectToBe true (String.contains (String.Pattern "bearerAuth") json)
137 expectToBe true (String.contains (String.Pattern "bearerAuth") json)
138 expectToBe true (String.contains (String.Pattern "basicAuth") json)
138 expectToBe true (String.contains (String.Pattern "basicAuth") json)
139 expectToBe true (String.contains (String.Pattern "xApiKeyApiKey") json)
139 expectToBe true (String.contains (String.Pattern "xApiKeyApiKey") json)
140
140
141 test "None of the auth headers appear as regular parameters" do
141 test "None of the auth headers appear as regular parameters" do
142− let spec = buildOpenAPISpec @MultiAuthAPI { title: "Auth API", version: "1.0.0", description: Nothing, contact: Nothing, license: Nothing }
142+ let spec = buildOpenAPISpec @MultiAuthAPI { title: "Auth API", version: "1.0.0" }
143 let json = writeJSON spec
143 let json = writeJSON spec
144 -- Check that all endpoints have empty parameters arrays
144 -- Check that all endpoints have empty parameters arrays
145 let hasEmptyParams = String.contains (String.Pattern "\"parameters\":[]") json
145 let hasEmptyParams = String.contains (String.Pattern "\"parameters\":[]") json
test/Test/ComprehensiveExample.purs+21−23
@@ -3,11 +3,11 @@ module Test.ComprehensiveExample where
3import Prelude
3import Prelude
4
4
5import Data.Array as Array
5import Data.Array as Array
6−import Data.Maybe (Maybe(..))
7import Effect (Effect)
6import Effect (Effect)
7+import Literals.Undefined (undefined)
8import Type.Function (type (#))
8import Type.Function (type (#))
9import Yoga.HTTP.API.Path (type (:), type (/), type (:?))
9import Yoga.HTTP.API.Path (type (:), type (/), type (:?))
10−import Yoga.HTTP.API.Route (GET, POST, PUT, DELETE, Route, Request, buildOpenAPISpec)
10+import Yoga.HTTP.API.Route (GET, POST, PUT, DELETE, Route, buildOpenAPISpec, NoRequest)
11import Yoga.HTTP.API.Route.Auth (BearerToken, ApiKeyHeader, BasicAuth)
11import Yoga.HTTP.API.Route.Auth (BearerToken, ApiKeyHeader, BasicAuth)
12import Yoga.HTTP.API.Route.Encoding (JSON, PlainText, XML)
12import Yoga.HTTP.API.Route.Encoding (JSON, PlainText, XML)
13import Yoga.HTTP.API.Route.OpenAPIMetadata (Description, Example, ExampleValue, Examples, Schema)
13import Yoga.HTTP.API.Route.OpenAPIMetadata (Description, Example, ExampleValue, Examples, Schema)
@@ -21,65 +21,63 @@ type ComprehensiveAPI =
21 { -- Simple GET with path parameter and authentication
21 { -- Simple GET with path parameter and authentication
22 getUser ::
22 getUser ::
23 Route GET ("users" / "id" : Int)
23 Route GET ("users" / "id" : Int)
24− (Request { headers :: { authorization :: BearerToken } })
24+ { headers :: { authorization :: BearerToken } }
25 (ok :: { body :: Schema "User" User })
25 (ok :: { body :: Schema "User" User })
26
26
27 , -- List users with query parameters
27 , -- List users with query parameters
28 listUsers ::
28 listUsers ::
29 Route GET ("users" :? { limit :: Int # Example "10", offset :: Int # Example "0" })
29 Route GET ("users" :? { limit :: Int # Example "10", offset :: Int # Example "0" })
30− (Request {})
30+ NoRequest
31 (ok :: { body :: Array User })
31 (ok :: { body :: Array User })
32
32
33 , -- POST with JSON body and schema
33 , -- POST with JSON body and schema
34 createUser ::
34 createUser ::
35 Route POST "users"
35 Route POST "users"
36− (Request { body :: JSON (Schema "CreateUserRequest" CreateUserRequest) })
36+ { body :: JSON (Schema "CreateUserRequest" CreateUserRequest) }
37 (created :: { body :: Schema "User" User })
37 (created :: { body :: Schema "User" User })
38
38
39 , -- PUT with authentication and JSON body
39 , -- PUT with authentication and JSON body
40 updateUser ::
40 updateUser ::
41 Route PUT ("users" / "id" : Int)
41 Route PUT ("users" / "id" : Int)
42− ( Request
43− { headers :: { authorization :: BearerToken }
42+ { headers :: { authorization :: BearerToken }
44− , body :: JSON UpdateUserRequest
43+ , body :: JSON UpdateUserRequest
45− }
44+ }
46− )
47 (ok :: { body :: User })
45 (ok :: { body :: User })
48
46
49 , -- DELETE with authentication
47 , -- DELETE with authentication
50 deleteUser ::
48 deleteUser ::
51 Route DELETE ("users" / "id" : Int)
49 Route DELETE ("users" / "id" : Int)
52− (Request { headers :: { authorization :: BearerToken } })
50+ { headers :: { authorization :: BearerToken } }
53 (noContent :: { body :: {} })
51 (noContent :: { body :: {} })
54
52
55 , -- Multiple content types - XML
53 , -- Multiple content types - XML
56 exportUser ::
54 exportUser ::
57 Route GET ("users" / "id" : Int / "export")
55 Route GET ("users" / "id" : Int / "export")
58− (Request {})
56+ NoRequest
59 (ok :: { body :: XML User })
57 (ok :: { body :: XML User })
60
58
61 , -- Plain text response
59 , -- Plain text response
62 healthCheck ::
60 healthCheck ::
63 Route GET "health"
61 Route GET "health"
64− (Request {})
62+ NoRequest
65 (ok :: { body :: PlainText String })
63 (ok :: { body :: PlainText String })
66
64
67 , -- API Key authentication
65 , -- API Key authentication
68 getStats ::
66 getStats ::
69 Route GET "stats"
67 Route GET "stats"
70− (Request { headers :: { xApiKey :: ApiKeyHeader } })
68+ { headers :: { xApiKey :: ApiKeyHeader } }
71 (ok :: { body :: Stats })
69 (ok :: { body :: Stats })
72
70
73 , -- Basic auth
71 , -- Basic auth
74 login ::
72 login ::
75 Route POST "login"
73 Route POST "login"
76− (Request { headers :: { authorization :: BasicAuth } })
74+ { headers :: { authorization :: BasicAuth } }
77 (ok :: { body :: { token :: String # Description "JWT access token" } })
75 (ok :: { body :: { token :: String # Description "JWT access token" } })
78
76
79 , -- Path with examples
77 , -- Path with examples
80 getUserPosts ::
78 getUserPosts ::
81 Route GET ("users" / "userId" : (Int # Examples (alice :: ExampleValue "1", bob :: ExampleValue "2")) / "posts")
79 Route GET ("users" / "userId" : (Int # Examples (alice :: ExampleValue "1", bob :: ExampleValue "2")) / "posts")
82− (Request {})
80+ NoRequest
83 (ok :: { body :: Array Post })
81 (ok :: { body :: Array Post })
84 }
82 }
85
83
@@ -131,15 +129,15 @@ testComprehensiveExample = describe "Comprehensive API Example" do
131 spec = buildOpenAPISpec @ComprehensiveAPI
129 spec = buildOpenAPISpec @ComprehensiveAPI
132 { title: "Comprehensive Example API"
130 { title: "Comprehensive Example API"
133 , version: "1.0.0"
131 , version: "1.0.0"
134− , description: Just "A complete example showcasing yoga-http-api features including authentication, multiple content types, metadata, schemas, and examples"
132+ , description: "A complete example showcasing yoga-http-api features including authentication, multiple content types, metadata, schemas, and examples"
135− , contact: Just
133+ , contact:
136− { name: Just "API Team"
134+ { name: "API Team"
137− , email: Just "api@example.com"
135+ , email: "api@example.com"
138− , url: Just "https://example.com/support"
136+ , url: "https://example.com/support"
139 }
137 }
140− , license: Just
138+ , license:
141 { name: "MIT"
139 { name: "MIT"
142− , url: Just "https://opensource.org/licenses/MIT"
140+ , url: "https://opensource.org/licenses/MIT"
143 }
141 }
144 }
142 }
145 json = writeJSON spec
143 json = writeJSON spec
test/Test/ContentTypeTest.purs+16−52
@@ -5,7 +5,7 @@ import Prelude
5import Data.Maybe (Maybe(..))
5import Data.Maybe (Maybe(..))
6import Data.String as String
6import Data.String as String
7import Effect (Effect)
7import Effect (Effect)
8−import Yoga.HTTP.API.Route (GET, POST, PUT, Route, Request, buildOpenAPISpec)
8+import Yoga.HTTP.API.Route (GET, POST, PUT, Route, buildOpenAPISpec)
9import Yoga.HTTP.API.Route.Encoding (JSON, FormData, MultipartFormData, PlainText, XML, CustomContentType)
9import Yoga.HTTP.API.Route.Encoding (JSON, FormData, MultipartFormData, PlainText, XML, CustomContentType)
10import Yoga.JSON (writeJSON)
10import Yoga.JSON (writeJSON)
11import ViTest (ViTest, describe, test)
11import ViTest (ViTest, describe, test)
@@ -21,12 +21,9 @@ testRequestContentTypes :: Effect ViTest
21testRequestContentTypes = describe "Request Content Types" do
21testRequestContentTypes = describe "Request Content Types" do
22 _ <- test "JSON request body generates application/json" do
22 _ <- test "JSON request body generates application/json" do
23 let
23 let
24− spec = buildOpenAPISpec @{ createUser :: Route POST "users" (Request { body :: JSON User }) (ok :: { body :: User }) }
24+ spec = buildOpenAPISpec @{ createUser :: Route POST "users" { body :: JSON User } (ok :: { body :: User }) }
25 { title: "API"
25 { title: "API"
26 , version: "1.0.0"
26 , version: "1.0.0"
27− , description: Nothing
28− , contact: Nothing
29− , license: Nothing
30 }
27 }
31 json = writeJSON spec
28 json = writeJSON spec
32 expectToBe true (String.contains (String.Pattern "\"application/json\"") json)
29 expectToBe true (String.contains (String.Pattern "\"application/json\"") json)
@@ -34,12 +31,9 @@ testRequestContentTypes = describe "Request Content Types" do
34
31
35 _ <- test "FormData request body generates application/x-www-form-urlencoded" do
32 _ <- test "FormData request body generates application/x-www-form-urlencoded" do
36 let
33 let
37− spec = buildOpenAPISpec @{ createUser :: Route POST "users" (Request { body :: FormData User }) (ok :: { body :: JSON User }) }
34+ spec = buildOpenAPISpec @{ createUser :: Route POST "users" { body :: FormData User } (ok :: { body :: JSON User }) }
38 { title: "API"
35 { title: "API"
39 , version: "1.0.0"
36 , version: "1.0.0"
40− , description: Nothing
41− , contact: Nothing
42− , license: Nothing
43 }
37 }
44 json = writeJSON spec
38 json = writeJSON spec
45 expectToBe true (String.contains (String.Pattern "\"application/x-www-form-urlencoded\"") json)
39 expectToBe true (String.contains (String.Pattern "\"application/x-www-form-urlencoded\"") json)
@@ -47,48 +41,36 @@ testRequestContentTypes = describe "Request Content Types" do
47
41
48 _ <- test "MultipartFormData request body generates multipart/form-data" do
42 _ <- test "MultipartFormData request body generates multipart/form-data" do
49 let
43 let
50− spec = buildOpenAPISpec @{ uploadFile :: Route POST "upload" (Request { body :: MultipartFormData FileUpload }) (ok :: { body :: { success :: Boolean } }) }
44+ spec = buildOpenAPISpec @{ uploadFile :: Route POST "upload" { body :: MultipartFormData FileUpload } (ok :: { body :: { success :: Boolean } }) }
51 { title: "API"
45 { title: "API"
52 , version: "1.0.0"
46 , version: "1.0.0"
53− , description: Nothing
54− , contact: Nothing
55− , license: Nothing
56 }
47 }
57 json = writeJSON spec
48 json = writeJSON spec
58 expectToBe true (String.contains (String.Pattern "\"multipart/form-data\"") json)
49 expectToBe true (String.contains (String.Pattern "\"multipart/form-data\"") json)
59
50
60 _ <- test "PlainText request body generates text/plain" do
51 _ <- test "PlainText request body generates text/plain" do
61 let
52 let
62− spec = buildOpenAPISpec @{ echo :: Route POST "echo" (Request { body :: PlainText String }) (ok :: { body :: PlainText String }) }
53+ spec = buildOpenAPISpec @{ echo :: Route POST "echo" { body :: PlainText String } (ok :: { body :: PlainText String }) }
63 { title: "API"
54 { title: "API"
64 , version: "1.0.0"
55 , version: "1.0.0"
65− , description: Nothing
66− , contact: Nothing
67− , license: Nothing
68 }
56 }
69 json = writeJSON spec
57 json = writeJSON spec
70 expectToBe true (String.contains (String.Pattern "\"text/plain\"") json)
58 expectToBe true (String.contains (String.Pattern "\"text/plain\"") json)
71
59
72 _ <- test "XML request body generates application/xml" do
60 _ <- test "XML request body generates application/xml" do
73 let
61 let
74− spec = buildOpenAPISpec @{ updateDoc :: Route PUT "document" (Request { body :: XML XmlDocument }) (ok :: { body :: XML XmlDocument }) }
62+ spec = buildOpenAPISpec @{ updateDoc :: Route PUT "document" { body :: XML XmlDocument } (ok :: { body :: XML XmlDocument }) }
75 { title: "API"
63 { title: "API"
76 , version: "1.0.0"
64 , version: "1.0.0"
77− , description: Nothing
78− , contact: Nothing
79− , license: Nothing
80 }
65 }
81 json = writeJSON spec
66 json = writeJSON spec
82 expectToBe true (String.contains (String.Pattern "\"application/xml\"") json)
67 expectToBe true (String.contains (String.Pattern "\"application/xml\"") json)
83
68
84 test "CustomContentType request body generates custom MIME type" do
69 test "CustomContentType request body generates custom MIME type" do
85 let
70 let
86− spec = buildOpenAPISpec @{ customEndpoint :: Route POST "custom" (Request { body :: CustomContentType "application/vnd.api+json" User }) (ok :: { body :: User }) }
71+ spec = buildOpenAPISpec @{ customEndpoint :: Route POST "custom" { body :: CustomContentType "application/vnd.api+json" User } (ok :: { body :: User }) }
87 { title: "API"
72 { title: "API"
88 , version: "1.0.0"
73 , version: "1.0.0"
89− , description: Nothing
90− , contact: Nothing
91− , license: Nothing
92 }
74 }
93 json = writeJSON spec
75 json = writeJSON spec
94 expectToBe true (String.contains (String.Pattern "\"application/vnd.api+json\"") json)
76 expectToBe true (String.contains (String.Pattern "\"application/vnd.api+json\"") json)
@@ -97,60 +79,45 @@ testResponseContentTypes :: Effect ViTest
97testResponseContentTypes = describe "Response Content Types" do
79testResponseContentTypes = describe "Response Content Types" do
98 _ <- test "JSON response body generates application/json" do
80 _ <- test "JSON response body generates application/json" do
99 let
81 let
100− spec = buildOpenAPISpec @{ getUser :: Route GET "users" (Request {}) (ok :: { body :: JSON User }) }
82+ spec = buildOpenAPISpec @{ getUser :: Route GET "users" {} (ok :: { body :: JSON User }) }
101 { title: "API"
83 { title: "API"
102 , version: "1.0.0"
84 , version: "1.0.0"
103− , description: Nothing
104− , contact: Nothing
105− , license: Nothing
106 }
85 }
107 json = writeJSON spec
86 json = writeJSON spec
108 expectToBe true (String.contains (String.Pattern "\"application/json\"") json)
87 expectToBe true (String.contains (String.Pattern "\"application/json\"") json)
109
88
110 _ <- test "PlainText response body generates text/plain" do
89 _ <- test "PlainText response body generates text/plain" do
111 let
90 let
112− spec = buildOpenAPISpec @{ getText :: Route GET "text" (Request {}) (ok :: { body :: PlainText String }) }
91+ spec = buildOpenAPISpec @{ getText :: Route GET "text" {} (ok :: { body :: PlainText String }) }
113 { title: "API"
92 { title: "API"
114 , version: "1.0.0"
93 , version: "1.0.0"
115− , description: Nothing
116− , contact: Nothing
117− , license: Nothing
118 }
94 }
119 json = writeJSON spec
95 json = writeJSON spec
120 expectToBe true (String.contains (String.Pattern "\"text/plain\"") json)
96 expectToBe true (String.contains (String.Pattern "\"text/plain\"") json)
121
97
122 _ <- test "XML response body generates application/xml" do
98 _ <- test "XML response body generates application/xml" do
123 let
99 let
124− spec = buildOpenAPISpec @{ getDoc :: Route GET "document" (Request {}) (ok :: { body :: XML XmlDocument }) }
100+ spec = buildOpenAPISpec @{ getDoc :: Route GET "document" {} (ok :: { body :: XML XmlDocument }) }
125 { title: "API"
101 { title: "API"
126 , version: "1.0.0"
102 , version: "1.0.0"
127− , description: Nothing
128− , contact: Nothing
129− , license: Nothing
130 }
103 }
131 json = writeJSON spec
104 json = writeJSON spec
132 expectToBe true (String.contains (String.Pattern "\"application/xml\"") json)
105 expectToBe true (String.contains (String.Pattern "\"application/xml\"") json)
133
106
134 _ <- test "CustomContentType response body generates custom MIME type" do
107 _ <- test "CustomContentType response body generates custom MIME type" do
135 let
108 let
136− spec = buildOpenAPISpec @{ customGet :: Route GET "custom" (Request {}) (ok :: { body :: CustomContentType "application/vnd.api+json" User }) }
109+ spec = buildOpenAPISpec @{ customGet :: Route GET "custom" {} (ok :: { body :: CustomContentType "application/vnd.api+json" User }) }
137 { title: "API"
110 { title: "API"
138 , version: "1.0.0"
111 , version: "1.0.0"
139− , description: Nothing
140− , contact: Nothing
141− , license: Nothing
142 }
112 }
143 json = writeJSON spec
113 json = writeJSON spec
144 expectToBe true (String.contains (String.Pattern "\"application/vnd.api+json\"") json)
114 expectToBe true (String.contains (String.Pattern "\"application/vnd.api+json\"") json)
145
115
146 test "Unwrapped response body defaults to application/json for backward compatibility" do
116 test "Unwrapped response body defaults to application/json for backward compatibility" do
147 let
117 let
148− spec = buildOpenAPISpec @{ getUser :: Route GET "users" (Request {}) (ok :: { body :: User }) }
118+ spec = buildOpenAPISpec @{ getUser :: Route GET "users" {} (ok :: { body :: User }) }
149 { title: "API"
119 { title: "API"
150 , version: "1.0.0"
120 , version: "1.0.0"
151− , description: Nothing
152− , contact: Nothing
153− , license: Nothing
154 }
121 }
155 json = writeJSON spec
122 json = writeJSON spec
156 expectToBe true (String.contains (String.Pattern "\"application/json\"") json)
123 expectToBe true (String.contains (String.Pattern "\"application/json\"") json)
@@ -161,16 +128,13 @@ testMixedContentTypes = describe "Mixed Content Types in API" do
161 let
128 let
162 spec = buildOpenAPISpec
129 spec = buildOpenAPISpec
163 @
130 @
164− { createUser :: Route POST "users" (Request { body :: JSON User }) (ok :: { body :: JSON User })
131+ { createUser :: Route POST "users" { body :: JSON User } (ok :: { body :: JSON User })
165− , uploadFile :: Route POST "upload" (Request { body :: MultipartFormData FileUpload }) (ok :: { body :: { success :: Boolean } })
132+ , uploadFile :: Route POST "upload" { body :: MultipartFormData FileUpload } (ok :: { body :: { success :: Boolean } })
166− , echo :: Route POST "echo" (Request { body :: PlainText String }) (ok :: { body :: PlainText String })
133+ , echo :: Route POST "echo" { body :: PlainText String } (ok :: { body :: PlainText String })
167− , getDoc :: Route GET "document" (Request {}) (ok :: { body :: XML XmlDocument })
134+ , getDoc :: Route GET "document" {} (ok :: { body :: XML XmlDocument })
168 }
135 }
169 { title: "Multi-Content API"
136 { title: "Multi-Content API"
170 , version: "1.0.0"
137 , version: "1.0.0"
171− , description: Nothing
172− , contact: Nothing
173− , license: Nothing
174 }
138 }
175 json = writeJSON spec
139 json = writeJSON spec
176 expectToBe true (String.contains (String.Pattern "\"application/json\"") json)
140 expectToBe true (String.contains (String.Pattern "\"application/json\"") json)
test/Test/CookieAuthTest.purs+12−12
@@ -5,7 +5,7 @@ import Prelude
5import Data.Maybe (Maybe(..))
5import Data.Maybe (Maybe(..))
6import Data.String as String
6import Data.String as String
7import Effect (Effect)
7import Effect (Effect)
8−import Yoga.HTTP.API.Route (GET, Route, Request, buildOpenAPISpec)
8+import Yoga.HTTP.API.Route (GET, Route, buildOpenAPISpec)
9import Yoga.HTTP.API.Route.Auth (ApiKeyCookie)
9import Yoga.HTTP.API.Route.Auth (ApiKeyCookie)
10import Yoga.JSON (writeJSON)
10import Yoga.JSON (writeJSON)
11import ViTest (ViTest, describe, test)
11import ViTest (ViTest, describe, test)
@@ -16,15 +16,15 @@ import ViTest.Expect (expectToBe)
16--------------------------------------------------------------------------------
16--------------------------------------------------------------------------------
17
17
18type ApiKeyCookieAPI =
18type ApiKeyCookieAPI =
19− { getProtected :: Route GET "protected" (Request { cookies :: { sessionId :: ApiKeyCookie } }) (ok :: { body :: { message :: String } })
19+ { getProtected :: Route GET "protected" { cookies :: { sessionId :: ApiKeyCookie } } (ok :: { body :: { message :: String } })
20 }
20 }
21
21
22type RegularCookieParamsAPI =
22type RegularCookieParamsAPI =
23− { getData :: Route GET "data" (Request { cookies :: { preference :: String, theme :: String } }) (ok :: { body :: { data :: String } })
23+ { getData :: Route GET "data" { cookies :: { preference :: String, theme :: String } } (ok :: { body :: { data :: String } })
24 }
24 }
25
25
26type MixedCookieAPI =
26type MixedCookieAPI =
27− { getResource :: Route GET "resource" (Request { cookies :: { sessionId :: ApiKeyCookie, userId :: String } }) (ok :: { body :: { resource :: String } })
27+ { getResource :: Route GET "resource" { cookies :: { sessionId :: ApiKeyCookie, userId :: String } } (ok :: { body :: { resource :: String } })
28 }
28 }
29
29
30--------------------------------------------------------------------------------
30--------------------------------------------------------------------------------
@@ -34,20 +34,20 @@ type MixedCookieAPI =
34testApiKeyCookieAuth :: Effect ViTest
34testApiKeyCookieAuth :: Effect ViTest
35testApiKeyCookieAuth = describe "ApiKeyCookie Authentication" do
35testApiKeyCookieAuth = describe "ApiKeyCookie Authentication" do
36 _ <- test "ApiKeyCookie does not appear as a regular cookie parameter" do
36 _ <- test "ApiKeyCookie does not appear as a regular cookie parameter" do
37− let spec = buildOpenAPISpec @ApiKeyCookieAPI { title: "Cookie Auth API", version: "1.0.0", description: Nothing, contact: Nothing, license: Nothing }
37+ let spec = buildOpenAPISpec @ApiKeyCookieAPI { title: "Cookie Auth API", version: "1.0.0" }
38 let json = writeJSON spec
38 let json = writeJSON spec
39 -- Check that parameters array is empty (no cookie parameters)
39 -- Check that parameters array is empty (no cookie parameters)
40 let hasEmptyParams = String.contains (String.Pattern "\"parameters\":[]") json
40 let hasEmptyParams = String.contains (String.Pattern "\"parameters\":[]") json
41 expectToBe true hasEmptyParams
41 expectToBe true hasEmptyParams
42
42
43 _ <- test "ApiKeyCookie appears in security requirements" do
43 _ <- test "ApiKeyCookie appears in security requirements" do
44− let spec = buildOpenAPISpec @ApiKeyCookieAPI { title: "Cookie Auth API", version: "1.0.0", description: Nothing, contact: Nothing, license: Nothing }
44+ let spec = buildOpenAPISpec @ApiKeyCookieAPI { title: "Cookie Auth API", version: "1.0.0" }
45 let json = writeJSON spec
45 let json = writeJSON spec
46 let hasSessionIdCookie = String.contains (String.Pattern "sessionIdCookie") json
46 let hasSessionIdCookie = String.contains (String.Pattern "sessionIdCookie") json
47 expectToBe true hasSessionIdCookie
47 expectToBe true hasSessionIdCookie
48
48
49 test "ApiKeyCookie security scheme has correct structure" do
49 test "ApiKeyCookie security scheme has correct structure" do
50− let spec = buildOpenAPISpec @ApiKeyCookieAPI { title: "Cookie Auth API", version: "1.0.0", description: Nothing, contact: Nothing, license: Nothing }
50+ let spec = buildOpenAPISpec @ApiKeyCookieAPI { title: "Cookie Auth API", version: "1.0.0" }
51 let json = writeJSON spec
51 let json = writeJSON spec
52 expectToBe true (String.contains (String.Pattern "\"type\":\"apiKey\"") json)
52 expectToBe true (String.contains (String.Pattern "\"type\":\"apiKey\"") json)
53 expectToBe true (String.contains (String.Pattern "\"in\":\"cookie\"") json)
53 expectToBe true (String.contains (String.Pattern "\"in\":\"cookie\"") json)
@@ -56,7 +56,7 @@ testApiKeyCookieAuth = describe "ApiKeyCookie Authentication" do
56testRegularCookieParams :: Effect ViTest
56testRegularCookieParams :: Effect ViTest
57testRegularCookieParams = describe "Regular Cookie Parameters" do
57testRegularCookieParams = describe "Regular Cookie Parameters" do
58 _ <- test "Regular cookies appear as cookie parameters" do
58 _ <- test "Regular cookies appear as cookie parameters" do
59− let spec = buildOpenAPISpec @RegularCookieParamsAPI { title: "Cookie Params API", version: "1.0.0", description: Nothing, contact: Nothing, license: Nothing }
59+ let spec = buildOpenAPISpec @RegularCookieParamsAPI { title: "Cookie Params API", version: "1.0.0" }
60 let json = writeJSON spec
60 let json = writeJSON spec
61 let hasPreferenceParam = String.contains (String.Pattern "\"name\":\"preference\"") json && String.contains (String.Pattern "\"in\":\"cookie\"") json
61 let hasPreferenceParam = String.contains (String.Pattern "\"name\":\"preference\"") json && String.contains (String.Pattern "\"in\":\"cookie\"") json
62 let hasThemeParam = String.contains (String.Pattern "\"name\":\"theme\"") json && String.contains (String.Pattern "\"in\":\"cookie\"") json
62 let hasThemeParam = String.contains (String.Pattern "\"name\":\"theme\"") json && String.contains (String.Pattern "\"in\":\"cookie\"") json
@@ -64,7 +64,7 @@ testRegularCookieParams = describe "Regular Cookie Parameters" do
64 expectToBe true hasThemeParam
64 expectToBe true hasThemeParam
65
65
66 test "Regular cookies do not appear in security schemes" do
66 test "Regular cookies do not appear in security schemes" do
67− let spec = buildOpenAPISpec @RegularCookieParamsAPI { title: "Cookie Params API", version: "1.0.0", description: Nothing, contact: Nothing, license: Nothing }
67+ let spec = buildOpenAPISpec @RegularCookieParamsAPI { title: "Cookie Params API", version: "1.0.0" }
68 let json = writeJSON spec
68 let json = writeJSON spec
69 let hasPreferenceCookie = String.contains (String.Pattern "preferenceCookie") json && String.contains (String.Pattern "securitySchemes") json
69 let hasPreferenceCookie = String.contains (String.Pattern "preferenceCookie") json && String.contains (String.Pattern "securitySchemes") json
70 let hasThemeCookie = String.contains (String.Pattern "themeCookie") json && String.contains (String.Pattern "securitySchemes") json
70 let hasThemeCookie = String.contains (String.Pattern "themeCookie") json && String.contains (String.Pattern "securitySchemes") json
@@ -74,13 +74,13 @@ testRegularCookieParams = describe "Regular Cookie Parameters" do
74testMixedCookies :: Effect ViTest
74testMixedCookies :: Effect ViTest
75testMixedCookies = describe "Mixed Cookie Types (Auth + Regular)" do
75testMixedCookies = describe "Mixed Cookie Types (Auth + Regular)" do
76 _ <- test "Auth cookie appears in security schemes" do
76 _ <- test "Auth cookie appears in security schemes" do
77− let spec = buildOpenAPISpec @MixedCookieAPI { title: "Mixed Cookie API", version: "1.0.0", description: Nothing, contact: Nothing, license: Nothing }
77+ let spec = buildOpenAPISpec @MixedCookieAPI { title: "Mixed Cookie API", version: "1.0.0" }
78 let json = writeJSON spec
78 let json = writeJSON spec
79 let hasSessionIdAuth = String.contains (String.Pattern "sessionIdCookie") json
79 let hasSessionIdAuth = String.contains (String.Pattern "sessionIdCookie") json
80 expectToBe true hasSessionIdAuth
80 expectToBe true hasSessionIdAuth
81
81
82 _ <- test "Auth cookie does not appear as regular parameter" do
82 _ <- test "Auth cookie does not appear as regular parameter" do
83− let spec = buildOpenAPISpec @MixedCookieAPI { title: "Mixed Cookie API", version: "1.0.0", description: Nothing, contact: Nothing, license: Nothing }
83+ let spec = buildOpenAPISpec @MixedCookieAPI { title: "Mixed Cookie API", version: "1.0.0" }
84 let json = writeJSON spec
84 let json = writeJSON spec
85 -- sessionId should not appear as a parameter (only userId should)
85 -- sessionId should not appear as a parameter (only userId should)
86 -- Check that the JSON doesn't contain a parameter object for sessionId
86 -- Check that the JSON doesn't contain a parameter object for sessionId
@@ -88,7 +88,7 @@ testMixedCookies = describe "Mixed Cookie Types (Auth + Regular)" do
88 expectToBe false hasSessionIdParam
88 expectToBe false hasSessionIdParam
89
89
90 test "Regular cookie appears as cookie parameter" do
90 test "Regular cookie appears as cookie parameter" do
91− let spec = buildOpenAPISpec @MixedCookieAPI { title: "Mixed Cookie API", version: "1.0.0", description: Nothing, contact: Nothing, license: Nothing }
91+ let spec = buildOpenAPISpec @MixedCookieAPI { title: "Mixed Cookie API", version: "1.0.0" }
92 let json = writeJSON spec
92 let json = writeJSON spec
93 let hasUserIdParam = String.contains (String.Pattern "\"name\":\"userId\"") json && String.contains (String.Pattern "\"in\":\"cookie\"") json
93 let hasUserIdParam = String.contains (String.Pattern "\"name\":\"userId\"") json && String.contains (String.Pattern "\"in\":\"cookie\"") json
94 expectToBe true hasUserIdParam
94 expectToBe true hasUserIdParam
test/Test/ExamplesTest.purs+6−21
@@ -8,7 +8,7 @@ import Effect (Effect)
8import Type.Function (type (#))
8import Type.Function (type (#))
9import Type.Proxy (Proxy(..))
9import Type.Proxy (Proxy(..))
10import Yoga.HTTP.API.Path (type (/), type (:))
10import Yoga.HTTP.API.Path (type (/), type (:))
11−import Yoga.HTTP.API.Route (GET, POST, Route, Request, buildOpenAPISpec)
11+import Yoga.HTTP.API.Route (GET, POST, Route, buildOpenAPISpec)
12import Yoga.HTTP.API.Route.Encoding (JSON)
12import Yoga.HTTP.API.Route.Encoding (JSON)
13import Yoga.HTTP.API.Route.OpenAPIMetadata (Example, Examples, ExampleValue, ExampleWithSummary, ExampleObject, example, examples)
13import Yoga.HTTP.API.Route.OpenAPIMetadata (Example, Examples, ExampleValue, ExampleWithSummary, ExampleObject, example, examples)
14import Yoga.JSON (writeJSON)
14import Yoga.JSON (writeJSON)
@@ -36,7 +36,7 @@ testExamplesMetadataExtraction = describe "Examples Metadata - Type-level extrac
36 expectToBe true (isJust result)
36 expectToBe true (isJust result)
37
37
38type TestAPIWithPathExamples =
38type TestAPIWithPathExamples =
39− { getUser :: Route GET ("users" / "userId" : (Int # Examples (basic :: ExampleValue "123", premium :: ExampleWithSummary "456" "Premium user"))) (Request {}) (ok :: { body :: { id :: Int, name :: String } })
39+ { getUser :: Route GET ("users" / "userId" : (Int # Examples (basic :: ExampleValue "123", premium :: ExampleWithSummary "456" "Premium user"))) {} (ok :: { body :: { id :: Int, name :: String } })
40 }
40 }
41
41
42testExamplesInParameters :: Effect ViTest
42testExamplesInParameters :: Effect ViTest
@@ -46,9 +46,6 @@ testExamplesInParameters = describe "Examples in Parameters" do
46 spec = buildOpenAPISpec @TestAPIWithPathExamples
46 spec = buildOpenAPISpec @TestAPIWithPathExamples
47 { title: "Test API"
47 { title: "Test API"
48 , version: "1.0.0"
48 , version: "1.0.0"
49− , description: Nothing
50− , contact: Nothing
51− , license: Nothing
52 }
49 }
53 json = writeJSON spec
50 json = writeJSON spec
54 expectToBe true (String.contains (String.Pattern "users") json)
51 expectToBe true (String.contains (String.Pattern "users") json)
@@ -59,7 +56,7 @@ type User =
59 }
56 }
60
57
61type TestAPIWithRequestBodyExamples =
58type TestAPIWithRequestBodyExamples =
62− { createUser :: Route POST "users" (Request { body :: JSON User }) (ok :: { body :: { id :: Int } })
59+ { createUser :: Route POST "users" { body :: JSON User } (ok :: { body :: { id :: Int } })
63 }
60 }
64
61
65testExamplesInRequestBody :: Effect ViTest
62testExamplesInRequestBody :: Effect ViTest
@@ -69,15 +66,12 @@ testExamplesInRequestBody = describe "Examples in Request Body" do
69 spec = buildOpenAPISpec @TestAPIWithRequestBodyExamples
66 spec = buildOpenAPISpec @TestAPIWithRequestBodyExamples
70 { title: "Test API"
67 { title: "Test API"
71 , version: "1.0.0"
68 , version: "1.0.0"
72− , description: Nothing
73− , contact: Nothing
74− , license: Nothing
75 }
69 }
76 json = writeJSON spec
70 json = writeJSON spec
77 expectToBe true (String.contains (String.Pattern "users") json)
71 expectToBe true (String.contains (String.Pattern "users") json)
78
72
79type TestAPIWithComplexExamples =
73type TestAPIWithComplexExamples =
80− { getItem :: Route GET ("items" / "itemId" : (Int # Examples (basic :: ExampleWithSummary "42" "A basic item", special :: ExampleObject "999" "Special item" "A very special item" ""))) (Request {}) (ok :: { body :: { id :: Int } })
74+ { getItem :: Route GET ("items" / "itemId" : (Int # Examples (basic :: ExampleWithSummary "42" "A basic item", special :: ExampleObject "999" "Special item" "A very special item" ""))) {} (ok :: { body :: { id :: Int } })
81 }
75 }
82
76
83testExamplesWithSummary :: Effect ViTest
77testExamplesWithSummary :: Effect ViTest
@@ -87,9 +81,6 @@ testExamplesWithSummary = describe "Examples with Summary" do
87 spec = buildOpenAPISpec @TestAPIWithComplexExamples
81 spec = buildOpenAPISpec @TestAPIWithComplexExamples
88 { title: "Test API"
82 { title: "Test API"
89 , version: "1.0.0"
83 , version: "1.0.0"
90− , description: Nothing
91− , contact: Nothing
92− , license: Nothing
93 }
84 }
94 json = writeJSON spec
85 json = writeJSON spec
95 expectToBe true (String.contains (String.Pattern "items") json)
86 expectToBe true (String.contains (String.Pattern "items") json)
@@ -111,7 +102,7 @@ testExamplesBackwardCompatibility = describe "Backward Compatibility - Single Ex
111 expectToBe true (isJust exsResult)
102 expectToBe true (isJust exsResult)
112
103
113type TestAPIWithQueryExamples =
104type TestAPIWithQueryExamples =
114− { searchItems :: Route GET "items" (Request { query :: { limit :: Int # Examples (small :: ExampleValue "10", large :: ExampleValue "100") } }) (ok :: { body :: { items :: Array String } })
105+ { searchItems :: Route GET "items" { query :: { limit :: Int # Examples (small :: ExampleValue "10", large :: ExampleValue "100") } } (ok :: { body :: { items :: Array String } })
115 }
106 }
116
107
117testExamplesInQueryParams :: Effect ViTest
108testExamplesInQueryParams :: Effect ViTest
@@ -121,15 +112,12 @@ testExamplesInQueryParams = describe "Examples in Query Parameters" do
121 spec = buildOpenAPISpec @TestAPIWithQueryExamples
112 spec = buildOpenAPISpec @TestAPIWithQueryExamples
122 { title: "Test API"
113 { title: "Test API"
123 , version: "1.0.0"
114 , version: "1.0.0"
124− , description: Nothing
125− , contact: Nothing
126− , license: Nothing
127 }
115 }
128 json = writeJSON spec
116 json = writeJSON spec
129 expectToBe true (String.contains (String.Pattern "items") json)
117 expectToBe true (String.contains (String.Pattern "items") json)
130
118
131type TestAPIWithExternalExample =
119type TestAPIWithExternalExample =
132− { getResource :: Route GET ("resource" / "id" : (String # Examples (inline :: ExampleObject "abc123" "Inline example" "An example with inline value" "", external :: ExampleObject "" "External example" "An example from external URL" "https://example.com/resource.json"))) (Request {}) (ok :: { body :: { data :: String } })
120+ { getResource :: Route GET ("resource" / "id" : (String # Examples (inline :: ExampleObject "abc123" "Inline example" "An example with inline value" "", external :: ExampleObject "" "External example" "An example from external URL" "https://example.com/resource.json"))) {} (ok :: { body :: { data :: String } })
133 }
121 }
134
122
135testComplexExampleObject :: Effect ViTest
123testComplexExampleObject :: Effect ViTest
@@ -139,9 +127,6 @@ testComplexExampleObject = describe "Complex Example Objects" do
139 spec = buildOpenAPISpec @TestAPIWithExternalExample
127 spec = buildOpenAPISpec @TestAPIWithExternalExample
140 { title: "Test API"
128 { title: "Test API"
141 , version: "1.0.0"
129 , version: "1.0.0"
142− , description: Nothing
143− , contact: Nothing
144− , license: Nothing
145 }
130 }
146 json = writeJSON spec
131 json = writeJSON spec
147 expectToBe true (String.contains (String.Pattern "resource") json)
132 expectToBe true (String.contains (String.Pattern "resource") json)
test/Test/OpenAPIInfoTest.purs+22−55
@@ -2,16 +2,15 @@ module Test.OpenAPIInfoTest where
2
2
3import Prelude
3import Prelude
4
4
5−import Data.Maybe (Maybe(..))
6import Data.String as String
5import Data.String as String
7import Effect (Effect)
6import Effect (Effect)
8−import Yoga.HTTP.API.Route (GET, Route, Request, buildOpenAPISpec)
7+import Yoga.HTTP.API.Route (GET, Route, buildOpenAPISpec)
9import Yoga.JSON (writeJSON)
8import Yoga.JSON (writeJSON)
10import ViTest (ViTest, describe, test)
9import ViTest (ViTest, describe, test)
11import ViTest.Expect (expectToBe)
10import ViTest.Expect (expectToBe)
12
11
13type SimpleAPI =
12type SimpleAPI =
14− { getRoot :: Route GET "" (Request {}) (ok :: { body :: { message :: String } })
13+ { getRoot :: Route GET "" {} (ok :: { body :: { message :: String } })
15 }
14 }
16
15
17testInfoDescription :: Effect ViTest
16testInfoDescription :: Effect ViTest
@@ -21,9 +20,7 @@ testInfoDescription = describe "OpenAPI Info - Description" do
21 spec = buildOpenAPISpec @SimpleAPI
20 spec = buildOpenAPISpec @SimpleAPI
22 { title: "Test API"
21 { title: "Test API"
23 , version: "1.0.0"
22 , version: "1.0.0"
24− , description: Just "This is a test API for demonstrating OpenAPI info enhancements"
23+ , description: "This is a test API for demonstrating OpenAPI info enhancements"
25− , contact: Nothing
26− , license: Nothing
27 }
24 }
28 json = writeJSON spec
25 json = writeJSON spec
29 expectToBe true (String.contains (String.Pattern "\"description\":\"This is a test API for demonstrating OpenAPI info enhancements\"") json)
26 expectToBe true (String.contains (String.Pattern "\"description\":\"This is a test API for demonstrating OpenAPI info enhancements\"") json)
@@ -33,9 +30,6 @@ testInfoDescription = describe "OpenAPI Info - Description" do
33 spec = buildOpenAPISpec @SimpleAPI
30 spec = buildOpenAPISpec @SimpleAPI
34 { title: "Test API"
31 { title: "Test API"
35 , version: "1.0.0"
32 , version: "1.0.0"
36− , description: Nothing
37− , contact: Nothing
38− , license: Nothing
39 }
33 }
40 json = writeJSON spec
34 json = writeJSON spec
41 -- Check that description is not in the info object (it will appear in responses, which is required by OpenAPI)
35 -- Check that description is not in the info object (it will appear in responses, which is required by OpenAPI)
@@ -49,13 +43,11 @@ testInfoContact = describe "OpenAPI Info - Contact" do
49 spec = buildOpenAPISpec @SimpleAPI
43 spec = buildOpenAPISpec @SimpleAPI
50 { title: "Test API"
44 { title: "Test API"
51 , version: "1.0.0"
45 , version: "1.0.0"
52− , description: Nothing
53− , contact: Just
46+ , contact:
54− { name: Just "API Support"
47+ { name: "API Support"
55− , url: Just "https://api.example.com/support"
48+ , url: "https://api.example.com/support"
56− , email: Just "support@example.com"
49+ , email: "support@example.com"
57 }
50 }
58− , license: Nothing
59 }
51 }
60 json = writeJSON spec
52 json = writeJSON spec
61 expectToBe true (String.contains (String.Pattern "\"contact\"") json)
53 expectToBe true (String.contains (String.Pattern "\"contact\"") json)
@@ -68,13 +60,7 @@ testInfoContact = describe "OpenAPI Info - Contact" do
68 spec = buildOpenAPISpec @SimpleAPI
60 spec = buildOpenAPISpec @SimpleAPI
69 { title: "Test API"
61 { title: "Test API"
70 , version: "1.0.0"
62 , version: "1.0.0"
71− , description: Nothing
72− , contact: Just
73− { name: Just "API Team"
63+ , contact: { name: "API Team" }
74− , url: Nothing
75− , email: Nothing
76− }
77− , license: Nothing
78 }
64 }
79 json = writeJSON spec
65 json = writeJSON spec
80 expectToBe true (String.contains (String.Pattern "\"contact\"") json)
66 expectToBe true (String.contains (String.Pattern "\"contact\"") json)
@@ -85,13 +71,7 @@ testInfoContact = describe "OpenAPI Info - Contact" do
85 spec = buildOpenAPISpec @SimpleAPI
71 spec = buildOpenAPISpec @SimpleAPI
86 { title: "Test API"
72 { title: "Test API"
87 , version: "1.0.0"
73 , version: "1.0.0"
88− , description: Nothing
89− , contact: Just
90− { name: Nothing
91− , url: Nothing
92− , email: Just "info@example.com"
74+ , contact: { email: "info@example.com" }
93− }
94− , license: Nothing
95 }
75 }
96 json = writeJSON spec
76 json = writeJSON spec
97 expectToBe true (String.contains (String.Pattern "\"contact\"") json)
77 expectToBe true (String.contains (String.Pattern "\"contact\"") json)
@@ -102,13 +82,10 @@ testInfoContact = describe "OpenAPI Info - Contact" do
102 spec = buildOpenAPISpec @SimpleAPI
82 spec = buildOpenAPISpec @SimpleAPI
103 { title: "Test API"
83 { title: "Test API"
104 , version: "1.0.0"
84 , version: "1.0.0"
105− , description: Nothing
106− , contact: Just
85+ , contact:
107− { name: Just "Support Team"
86+ { name: "Support Team"
108− , url: Just "https://support.example.com"
87+ , url: "https://support.example.com"
109− , email: Nothing
110 }
88 }
111− , license: Nothing
112 }
89 }
113 json = writeJSON spec
90 json = writeJSON spec
114 expectToBe true (String.contains (String.Pattern "\"contact\"") json)
91 expectToBe true (String.contains (String.Pattern "\"contact\"") json)
@@ -120,9 +97,6 @@ testInfoContact = describe "OpenAPI Info - Contact" do
120 spec = buildOpenAPISpec @SimpleAPI
97 spec = buildOpenAPISpec @SimpleAPI
121 { title: "Test API"
98 { title: "Test API"
122 , version: "1.0.0"
99 , version: "1.0.0"
123− , description: Nothing
124− , contact: Nothing
125− , license: Nothing
126 }
100 }
127 json = writeJSON spec
101 json = writeJSON spec
128 expectToBe false (String.contains (String.Pattern "\"contact\"") json)
102 expectToBe false (String.contains (String.Pattern "\"contact\"") json)
@@ -134,9 +108,7 @@ testInfoLicense = describe "OpenAPI Info - License" do
134 spec = buildOpenAPISpec @SimpleAPI
108 spec = buildOpenAPISpec @SimpleAPI
135 { title: "Test API"
109 { title: "Test API"
136 , version: "1.0.0"
110 , version: "1.0.0"
137− , description: Nothing
138− , contact: Nothing
139− , license: Just { name: "MIT", url: Nothing }
111+ , license: { name: "MIT" }
140 }
112 }
141 json = writeJSON spec
113 json = writeJSON spec
142 expectToBe true (String.contains (String.Pattern "\"license\"") json)
114 expectToBe true (String.contains (String.Pattern "\"license\"") json)
@@ -147,11 +119,9 @@ testInfoLicense = describe "OpenAPI Info - License" do
147 spec = buildOpenAPISpec @SimpleAPI
119 spec = buildOpenAPISpec @SimpleAPI
148 { title: "Test API"
120 { title: "Test API"
149 , version: "1.0.0"
121 , version: "1.0.0"
150− , description: Nothing
151− , contact: Nothing
152− , license: Just
122+ , license:
153 { name: "Apache 2.0"
123 { name: "Apache 2.0"
154− , url: Just "https://www.apache.org/licenses/LICENSE-2.0.html"
124+ , url: "https://www.apache.org/licenses/LICENSE-2.0.html"
155 }
125 }
156 }
126 }
157 json = writeJSON spec
127 json = writeJSON spec
@@ -164,9 +134,6 @@ testInfoLicense = describe "OpenAPI Info - License" do
164 spec = buildOpenAPISpec @SimpleAPI
134 spec = buildOpenAPISpec @SimpleAPI
165 { title: "Test API"
135 { title: "Test API"
166 , version: "1.0.0"
136 , version: "1.0.0"
167− , description: Nothing
168− , contact: Nothing
169− , license: Nothing
170 }
137 }
171 json = writeJSON spec
138 json = writeJSON spec
172 expectToBe false (String.contains (String.Pattern "\"license\"") json)
139 expectToBe false (String.contains (String.Pattern "\"license\"") json)
@@ -178,15 +145,15 @@ testInfoAllFields = describe "OpenAPI Info - All Fields Together" do
178 spec = buildOpenAPISpec @SimpleAPI
145 spec = buildOpenAPISpec @SimpleAPI
179 { title: "Complete API"
146 { title: "Complete API"
180 , version: "2.0.0"
147 , version: "2.0.0"
181− , description: Just "A comprehensive API with full metadata"
148+ , description: "A comprehensive API with full metadata"
182− , contact: Just
149+ , contact:
183− { name: Just "API Team"
150+ { name: "API Team"
184− , url: Just "https://example.com/contact"
151+ , url: "https://example.com/contact"
185− , email: Just "api@example.com"
152+ , email: "api@example.com"
186 }
153 }
187− , license: Just
154+ , license:
188 { name: "MIT"
155 { name: "MIT"
189− , url: Just "https://opensource.org/licenses/MIT"
156+ , url: "https://opensource.org/licenses/MIT"
190 }
157 }
191 }
158 }
192 json = writeJSON spec
159 json = writeJSON spec
test/Test/ProjectTrackerExample.pursnew file+499
@@ -0,0 +1,499 @@
1+module Test.ProjectTrackerExample where
2+
3+import Prelude
4+
5+import Data.Array as Array
6+import Data.Generic.Rep (class Generic)
7+import Data.Maybe (Maybe)
8+import Data.Tuple.Nested (type (/\))
9+import Effect (Effect)
10+import Literals.Undefined (undefined)
11+import Effect.Aff (Aff)
12+import Type.Function (type (#))
13+import Yoga.HTTP.API.Path (type (:), type (/), type (:?))
14+import Yoga.HTTP.API.Route (GET, POST, PUT, PATCH, DELETE, Route, buildOpenAPISpec, NoRequest)
15+import Yoga.HTTP.API.Route.Auth (BearerToken)
16+import Yoga.HTTP.API.Route.Encoding (JSON, MultipartFormData)
17+import Yoga.HTTP.API.Route.OpenAPI (class CollectSchemaNames, class CollectSchemas, class RenderJSONSchema)
18+import Foreign.Object as Object
19+import Yoga.HTTP.API.Route.OpenAPIMetadata (Description, Example, Examples, ExampleValue, Schema)
20+import Yoga.HTTP.API.Route.StatusCode (StatusCode(..))
21+import Yoga.JSON as JSON
22+import Yoga.JSON (writeJSON)
23+import Yoga.JSON.Generics (genericReadForeignEnum, genericWriteForeignEnum)
24+import Test.OpenAPIValidation (validate)
25+import ViTest (ViTest, describe, test)
26+import ViTest.Expect (expectToBe)
27+
28+--------------------------------------------------------------------------------
29+-- Domain Models
30+--------------------------------------------------------------------------------
31+
32+-- | Project with metadata
33+type Project =
34+ { id :: Int # Description "Unique project identifier"
35+ , name :: String # Description "Project name"
36+ , description :: String # Description "Project description"
37+ , status :: ProjectStatus # Description "Current project status"
38+ , createdAt :: Int # Description "Project creation timestamp (Unix epoch)"
39+ , createdBy :: Int # Description "User ID who created the project"
40+ }
41+
42+-- | Project status enum
43+data ProjectStatus = Active | Archived | OnHold
44+
45+derive instance Eq ProjectStatus
46+derive instance Generic ProjectStatus _
47+
48+instance JSON.ReadForeign ProjectStatus where
49+ readImpl = genericReadForeignEnum { toConstructorName: identity }
50+
51+instance JSON.WriteForeign ProjectStatus where
52+ writeImpl = genericWriteForeignEnum { toConstructorName: identity }
53+
54+instance RenderJSONSchema ProjectStatus where
55+ renderJSONSchema _ = JSON.writeImpl
56+ { type: "string"
57+ , enum: [ "Active", "Archived", "OnHold" ]
58+ }
59+
60+instance CollectSchemas ProjectStatus where
61+ collectSchemas _ = Object.empty
62+
63+instance CollectSchemaNames ProjectStatus ()
64+
65+-- | Task with full details
66+type Task =
67+ { id :: Int # Description "Unique task identifier"
68+ , projectId :: Int # Description "Parent project ID"
69+ , title :: String # Description "Task title"
70+ , description :: String # Description "Task description"
71+ , status :: TaskStatus # Description "Current task status"
72+ , priority :: Priority # Description "Task priority level"
73+ , assigneeId :: Maybe Int # Description "Assigned user ID"
74+ , createdAt :: Int # Description "Task creation timestamp (Unix epoch)"
75+ , updatedAt :: Int # Description "Last update timestamp (Unix epoch)"
76+ , dueDate :: Maybe Int # Description "Optional due date (Unix epoch)"
77+ }
78+
79+-- | Task status workflow
80+data TaskStatus = Todo | InProgress | InReview | Done | Blocked
81+
82+derive instance Eq TaskStatus
83+derive instance Generic TaskStatus _
84+
85+instance JSON.ReadForeign TaskStatus where
86+ readImpl = genericReadForeignEnum { toConstructorName: identity }
87+
88+instance JSON.WriteForeign TaskStatus where
89+ writeImpl = genericWriteForeignEnum { toConstructorName: identity }
90+
91+instance RenderJSONSchema TaskStatus where
92+ renderJSONSchema _ = JSON.writeImpl
93+ { type: "string"
94+ , enum: [ "Todo", "InProgress", "InReview", "Done", "Blocked" ]
95+ }
96+
97+instance CollectSchemas TaskStatus where
98+ collectSchemas _ = Object.empty
99+
100+instance CollectSchemaNames TaskStatus ()
101+
102+-- | Task priority levels
103+data Priority = Low | Medium | High | Urgent
104+
105+derive instance Eq Priority
106+derive instance Generic Priority _
107+
108+instance JSON.ReadForeign Priority where
109+ readImpl = genericReadForeignEnum { toConstructorName: identity }
110+
111+instance JSON.WriteForeign Priority where
112+ writeImpl = genericWriteForeignEnum { toConstructorName: identity }
113+
114+instance RenderJSONSchema Priority where
115+ renderJSONSchema _ = JSON.writeImpl
116+ { type: "string"
117+ , enum: [ "Low", "Medium", "High", "Urgent" ]
118+ }
119+
120+instance CollectSchemas Priority where
121+ collectSchemas _ = Object.empty
122+
123+instance CollectSchemaNames Priority ()
124+
125+-- | Comment on a task
126+type Comment =
127+ { id :: Int # Description "Unique comment identifier"
128+ , taskId :: Int # Description "Parent task ID"
129+ , authorId :: Int # Description "Comment author user ID"
130+ , content :: String # Description "Comment text content"
131+ , createdAt :: Int # Description "Comment creation timestamp (Unix epoch)"
132+ }
133+
134+-- | File attachment metadata
135+type Attachment =
136+ { id :: Int # Description "Unique attachment identifier"
137+ , taskId :: Int # Description "Parent task ID"
138+ , fileName :: String # Description "Original file name"
139+ , fileSize :: Int # Description "File size in bytes"
140+ , contentType :: String # Description "MIME type"
141+ , uploadedBy :: Int # Description "User ID who uploaded"
142+ , uploadedAt :: Int # Description "Upload timestamp (Unix epoch)"
143+ , url :: String # Description "Download URL"
144+ }
145+
146+-- | Task history entry
147+type TaskHistoryEntry =
148+ { id :: Int # Description "Unique history entry identifier"
149+ , taskId :: Int # Description "Parent task ID"
150+ , changedBy :: Int # Description "User ID who made the change"
151+ , changedAt :: Int # Description "Change timestamp (Unix epoch)"
152+ , field :: String # Description "Field that was changed"
153+ , oldValue :: Maybe String # Description "Previous value"
154+ , newValue :: String # Description "New value"
155+ }
156+
157+-- | User representation
158+type User =
159+ { id :: Int # Description "Unique user identifier"
160+ , username :: String # Description "Username"
161+ , email :: String # Description "Email address"
162+ , fullName :: String # Description "Full name"
163+ , role :: UserRole # Description "User role"
164+ }
165+
166+-- | User roles for authorization
167+data UserRole = Admin | ProjectManager | Developer | Viewer
168+
169+derive instance Eq UserRole
170+derive instance Generic UserRole _
171+
172+instance JSON.ReadForeign UserRole where
173+ readImpl = genericReadForeignEnum { toConstructorName: identity }
174+
175+instance JSON.WriteForeign UserRole where
176+ writeImpl = genericWriteForeignEnum { toConstructorName: identity }
177+
178+instance RenderJSONSchema UserRole where
179+ renderJSONSchema _ = JSON.writeImpl
180+ { type: "string"
181+ , enum: [ "Admin", "ProjectManager", "Developer", "Viewer" ]
182+ }
183+
184+instance CollectSchemas UserRole where
185+ collectSchemas _ = Object.empty
186+
187+instance CollectSchemaNames UserRole ()
188+
189+--------------------------------------------------------------------------------
190+-- Request/Response Types
191+--------------------------------------------------------------------------------
192+
193+-- | Request to create a project
194+type CreateProjectRequest =
195+ { name :: String # Example "\"Mobile App Redesign\""
196+ , description :: String # Example "\"Redesign the mobile app with new UI/UX\""
197+ }
198+
199+-- | Request to update a project
200+type UpdateProjectRequest =
201+ { name :: Maybe String
202+ , description :: Maybe String
203+ , status :: Maybe ProjectStatus
204+ }
205+
206+-- | Request to create a task
207+type CreateTaskRequest =
208+ { title :: String # Examples (short :: ExampleValue "\"Fix bug\"", detailed :: ExampleValue "\"Fix login validation bug\"")
209+ , description :: String # Example "\"The login form doesn't validate email properly\""
210+ , priority :: Priority # Example "High"
211+ , assigneeId :: Maybe Int # Example "42"
212+ , dueDate :: Maybe Int
213+ }
214+
215+-- | Request to update a task
216+type UpdateTaskRequest =
217+ { title :: Maybe String
218+ , description :: Maybe String
219+ , status :: Maybe TaskStatus
220+ , priority :: Maybe Priority
221+ , assigneeId :: Maybe Int
222+ , dueDate :: Maybe Int
223+ }
224+
225+-- | Request to assign a task
226+type AssignTaskRequest =
227+ { assigneeId :: Int # Example "42"
228+ }
229+
230+-- | Request to add a comment
231+type CreateCommentRequest =
232+ { content :: String # Example "\"I've started working on this\""
233+ }
234+
235+-- | File upload in multipart form
236+type FileUpload =
237+ { file :: String # Description "File contents (binary data)"
238+ }
239+
240+-- | Pagination query parameters
241+type PaginationParams =
242+ { limit :: Int # Example "20" # Description "Number of items per page"
243+ , offset :: Int # Example "0" # Description "Number of items to skip"
244+ }
245+
246+-- | Task filter parameters
247+type TaskFilterParams =
248+ { status :: Maybe TaskStatus
249+ , priority :: Maybe Priority
250+ , assigneeId :: Maybe Int
251+ } /\ PaginationParams
252+
253+-- | Error response
254+type ErrorResponse =
255+ { error :: String # Description "Error message"
256+ , code :: String # Description "Error code"
257+ }
258+
259+--------------------------------------------------------------------------------
260+-- API Definition
261+--------------------------------------------------------------------------------
262+
263+-- | Complete Project Tracker API
264+type ProjectTrackerAPI =
265+ { -- Projects
266+ listProjects ::
267+ Route GET ("projects" :? PaginationParams)
268+ { headers :: { authorization :: BearerToken } }
269+ ( ok :: { body :: Schema "ProjectList" (Array Project) }
270+ )
271+
272+ , getProject ::
273+ Route GET ("projects" / "id" : Int)
274+ { headers :: { authorization :: BearerToken } }
275+ ( ok :: { body :: Schema "Project" Project }
276+ , notFound :: { body :: ErrorResponse }
277+ )
278+
279+ , createProject ::
280+ Route POST "projects"
281+ { headers :: { authorization :: BearerToken }
282+ , body :: JSON (Schema "CreateProjectRequest" CreateProjectRequest)
283+ }
284+ ( created :: { body :: Schema "Project" Project }
285+ , unauthorized :: { body :: ErrorResponse }
286+ )
287+
288+ , updateProject ::
289+ Route PUT ("projects" / "id" : Int)
290+ { headers :: { authorization :: BearerToken }
291+ , body :: JSON (Schema "UpdateProjectRequest" UpdateProjectRequest)
292+ }
293+ ( ok :: { body :: Schema "Project" Project }
294+ , notFound :: { body :: ErrorResponse }
295+ , unauthorized :: { body :: ErrorResponse }
296+ )
297+
298+ , archiveProject ::
299+ Route DELETE ("projects" / "id" : Int)
300+ { headers :: { authorization :: BearerToken } }
301+ ( noContent :: { body :: {} }
302+ , notFound :: { body :: ErrorResponse }
303+ , unauthorized :: { body :: ErrorResponse }
304+ )
305+
306+ -- Tasks
307+ , listTasks ::
308+ Route GET ("projects" / "projectId" : Int / "tasks" :? TaskFilterParams)
309+ { headers :: { authorization :: BearerToken } }
310+ ( ok :: { body :: Schema "TaskList" (Array Task) }
311+ , notFound :: { body :: ErrorResponse }
312+ )
313+
314+ , getTask ::
315+ Route GET ("tasks" / "id" : Int)
316+ { headers :: { authorization :: BearerToken } }
317+ ( ok :: { body :: Schema "Task" Task }
318+ , notFound :: { body :: ErrorResponse }
319+ )
320+
321+ , createTask ::
322+ Route POST ("projects" / "projectId" : Int / "tasks")
323+ { headers :: { authorization :: BearerToken }
324+ , body :: JSON (Schema "CreateTaskRequest" CreateTaskRequest)
325+ }
326+ ( created :: { body :: Schema "Task" Task }
327+ , notFound :: { body :: ErrorResponse }
328+ , unauthorized :: { body :: ErrorResponse }
329+ )
330+
331+ , updateTask ::
332+ Route PUT ("tasks" / "id" : Int)
333+ { headers :: { authorization :: BearerToken }
334+ , body :: JSON (Schema "UpdateTaskRequest" UpdateTaskRequest)
335+ }
336+ ( ok :: { body :: Schema "Task" Task }
337+ , notFound :: { body :: ErrorResponse }
338+ , unauthorized :: { body :: ErrorResponse }
339+ )
340+
341+ , assignTask ::
342+ Route PATCH ("tasks" / "id" : Int / "assign")
343+ { headers :: { authorization :: BearerToken }
344+ , body :: JSON (Schema "AssignTaskRequest" AssignTaskRequest)
345+ }
346+ ( ok :: { body :: Schema "Task" Task }
347+ , notFound :: { body :: ErrorResponse }
348+ , unauthorized :: { body :: ErrorResponse }
349+ )
350+
351+ , deleteTask ::
352+ Route DELETE ("tasks" / "id" : Int)
353+ { headers :: { authorization :: BearerToken } }
354+ ( noContent :: { body :: {} }
355+ , notFound :: { body :: ErrorResponse }
356+ , unauthorized :: { body :: ErrorResponse }
357+ )
358+
359+ -- Comments
360+ , listComments ::
361+ Route GET ("tasks" / "taskId" : Int / "comments" :? PaginationParams)
362+ { headers :: { authorization :: BearerToken } }
363+ ( ok :: { body :: Schema "CommentList" (Array Comment) }
364+ , notFound :: { body :: ErrorResponse }
365+ )
366+
367+ , addComment ::
368+ Route POST ("tasks" / "taskId" : Int / "comments")
369+ { headers :: { authorization :: BearerToken }
370+ , body :: JSON (Schema "CreateCommentRequest" CreateCommentRequest)
371+ }
372+ ( created :: { body :: Schema "Comment" Comment }
373+ , notFound :: { body :: ErrorResponse }
374+ )
375+
376+ -- Attachments
377+ , listAttachments ::
378+ Route GET ("tasks" / "taskId" : Int / "attachments")
379+ { headers :: { authorization :: BearerToken } }
380+ ( ok :: { body :: Schema "AttachmentList" (Array Attachment) }
381+ , notFound :: { body :: ErrorResponse }
382+ )
383+
384+ , uploadAttachment ::
385+ Route POST ("tasks" / "taskId" : Int / "attachments")
386+ { headers :: { authorization :: BearerToken }
387+ , body :: MultipartFormData (Schema "FileUpload" FileUpload)
388+ }
389+ ( created :: { body :: Schema "Attachment" Attachment }
390+ , notFound :: { body :: ErrorResponse }
391+ , badRequest :: { body :: ErrorResponse }
392+ )
393+
394+ , deleteAttachment ::
395+ Route DELETE ("attachments" / "id" : Int)
396+ { headers :: { authorization :: BearerToken } }
397+ ( noContent :: { body :: {} }
398+ , notFound :: { body :: ErrorResponse }
399+ , unauthorized :: { body :: ErrorResponse }
400+ )
401+
402+ -- Task History
403+ , getTaskHistory ::
404+ Route GET ("tasks" / "taskId" : Int / "history")
405+ { headers :: { authorization :: BearerToken } }
406+ ( ok :: { body :: Schema "TaskHistory" (Array TaskHistoryEntry) }
407+ , notFound :: { body :: ErrorResponse }
408+ )
409+ }
410+
411+--------------------------------------------------------------------------------
412+-- Service Layer Types (for OmLayer pattern)
413+--------------------------------------------------------------------------------
414+
415+-- | Project repository interface
416+type ProjectRepo =
417+ { findAll :: PaginationParams -> Aff (Array Project)
418+ , findById :: Int -> Aff (Maybe Project)
419+ , create :: CreateProjectRequest -> Int -> Aff Project
420+ , update :: Int -> UpdateProjectRequest -> Aff (Maybe Project)
421+ , archive :: Int -> Aff Boolean
422+ }
423+
424+-- | Task repository interface
425+type TaskRepo =
426+ { findByProject :: Int -> TaskFilterParams -> Aff (Array Task)
427+ , findById :: Int -> Aff (Maybe Task)
428+ , create :: Int -> CreateTaskRequest -> Int -> Aff Task
429+ , update :: Int -> UpdateTaskRequest -> Aff (Maybe Task)
430+ , delete :: Int -> Aff Boolean
431+ , assign :: Int -> Int -> Aff (Maybe Task)
432+ }
433+
434+-- | Comment repository interface
435+type CommentRepo =
436+ { findByTask :: Int -> PaginationParams -> Aff (Array Comment)
437+ , create :: Int -> CreateCommentRequest -> Int -> Aff Comment
438+ }
439+
440+-- | Attachment repository interface
441+type AttachmentRepo =
442+ { findByTask :: Int -> Aff (Array Attachment)
443+ , create :: Int -> FileUpload -> Int -> Aff Attachment
444+ , delete :: Int -> Aff Boolean
445+ }
446+
447+-- | Task history repository interface
448+type TaskHistoryRepo =
449+ { findByTask :: Int -> Aff (Array TaskHistoryEntry)
450+ , recordChange :: Int -> Int -> String -> Maybe String -> String -> Aff TaskHistoryEntry
451+ }
452+
453+-- | Authentication service interface
454+type AuthService =
455+ { authenticate :: BearerToken -> Aff (Maybe User)
456+ , authorize :: User -> String -> Aff Boolean
457+ }
458+
459+-- Note: Repository implementations moved to Test.ProjectTrackerLayers module
460+
461+--------------------------------------------------------------------------------
462+-- Tests
463+--------------------------------------------------------------------------------
464+
465+testProjectTrackerAPI :: Effect ViTest
466+testProjectTrackerAPI = describe "Project Tracker API" do
467+ _ <- test "generates valid OpenAPI 3.0 schema" do
468+ let
469+ spec = buildOpenAPISpec @ProjectTrackerAPI
470+ { title: "Project Tracker API"
471+ , version: "1.0.0"
472+ , description: "A comprehensive project and task management API showcasing authentication, CRUD operations, file uploads, and audit trails"
473+ , contact:
474+ { name: "API Team"
475+ , email: "api@projecttracker.com"
476+ , url: "https://projecttracker.com/support"
477+ }
478+ , license:
479+ { name: "MIT"
480+ , url: "https://opensource.org/licenses/MIT"
481+ }
482+ }
483+ result = validate spec
484+ expectToBe true (Array.null result.errors)
485+
486+ _ <- test "includes all project endpoints" do
487+ let spec = buildOpenAPISpec @ProjectTrackerAPI { title: "Project Tracker", version: "1.0.0" }
488+ let json = writeJSON spec
489+ expectToBe true (json /= "")
490+
491+ _ <- test "includes authentication in security schemes" do
492+ let spec = buildOpenAPISpec @ProjectTrackerAPI { title: "Project Tracker", version: "1.0.0" }
493+ let json = writeJSON spec
494+ expectToBe true (json /= "")
495+
496+ test "generates comprehensive API spec with all features" do
497+ let spec = buildOpenAPISpec @ProjectTrackerAPI { title: "Project Tracker", version: "1.0.0" }
498+ let json = writeJSON spec
499+ expectToBe true (json /= "")
test/Test/ProjectTrackerIntegration.purs.disablednew file+195
@@ -0,0 +1,195 @@
1+module Test.ProjectTrackerIntegration where
2+
3+import Prelude
4+
5+import Data.Array as Array
6+import Data.Maybe (Maybe(..))
7+import Data.Tuple.Nested ((/\))
8+import Effect (Effect)
9+import Effect.Aff (launchAff_)
10+import Effect.Class (liftEffect)
11+import Effect.Console as Console
12+import Test.ProjectTrackerExample (CreateCommentRequest, CreateProjectRequest, CreateTaskRequest, Priority(..), TaskFilterParams)
13+import Test.ProjectTrackerLayers (AppConfig, DbConfig, Services, runWithServices)
14+import ViTest (ViTest, describe, test)
15+import ViTest.Expect (expectToBe)
16+import Yoga.Om as Om
17+
18+--------------------------------------------------------------------------------
19+-- Integration Tests using OmLayer Pattern
20+--------------------------------------------------------------------------------
21+
22+testProjectTrackerIntegration :: Effect ViTest
23+testProjectTrackerIntegration = describe "Project Tracker Integration (OmLayer)" do
24+ test "complete workflow: create project → create tasks → add comments" do
25+ launchAff_ do
26+ let
27+ appConfig = { apiName: "Project Tracker", version: "1.0.0" }
28+ dbConfig = { connectionString: "memory://", poolSize: 10 }
29+
30+ runWithServices appConfig dbConfig do
31+ services <- Om.ask
32+
33+ -- Step 1: Create a project
34+ let createProjectReq = { name: "Mobile App Redesign", description: "Complete UI/UX overhaul" }
35+ project <- services.projectRepo.create createProjectReq 1 # Om.liftAff
36+ liftEffect $ Console.log $ "✓ Created project #" <> show project.id <> ": " <> project.name
37+ liftEffect $ expectToBe "Mobile App Redesign" project.name
38+
39+ -- Step 2: Create multiple tasks
40+ let
41+ task1Req = { title: "Design mockups", description: "Create Figma designs", priority: High, assigneeId: Just 2, dueDate: Nothing }
42+ task2Req = { title: "Implement login screen", description: "Build the login UI", priority: High, assigneeId: Just 3, dueDate: Nothing }
43+ task3Req = { title: "Write unit tests", description: "Test coverage for login", priority: Medium, assigneeId: Nothing, dueDate: Nothing }
44+
45+ task1 <- services.taskRepo.create project.id task1Req 1 # Om.liftAff
46+ task2 <- services.taskRepo.create project.id task2Req 1 # Om.liftAff
47+ task3 <- services.taskRepo.create project.id task3Req 1 # Om.liftAff
48+
49+ liftEffect $ Console.log $ "✓ Created task #" <> show task1.id <> ": " <> task1.title
50+ liftEffect $ Console.log $ "✓ Created task #" <> show task2.id <> ": " <> task2.title
51+ liftEffect $ Console.log $ "✓ Created task #" <> show task3.id <> ": " <> task3.title
52+
53+ -- Step 3: List all tasks for the project
54+ let filterParams = { status: Nothing, priority: Nothing, assigneeId: Nothing } /\ { limit: 10, offset: 0 }
55+ tasks <- services.taskRepo.findByProject project.id filterParams # Om.liftAff
56+ liftEffect $ Console.log $ "✓ Found " <> show (Array.length tasks) <> " tasks"
57+ liftEffect $ expectToBe 3 (Array.length tasks)
58+
59+ -- Step 4: Add comments to a task
60+ let
61+ comment1Req = { content: "I've started working on the designs" }
62+ comment2Req = { content: "First draft is ready for review" }
63+
64+ comment1 <- services.commentRepo.create task1.id comment1Req 2 # Om.liftAff
65+ comment2 <- services.commentRepo.create task1.id comment2Req 2 # Om.liftAff
66+
67+ liftEffect $ Console.log $ "✓ Added comment #" <> show comment1.id <> " to task #" <> show task1.id
68+ liftEffect $ Console.log $ "✓ Added comment #" <> show comment2.id <> " to task #" <> show task1.id
69+
70+ -- Step 5: List comments for the task
71+ let pagination = { limit: 10, offset: 0 }
72+ comments <- services.commentRepo.findByTask task1.id pagination # Om.liftAff
73+ liftEffect $ Console.log $ "✓ Found " <> show (Array.length comments) <> " comments"
74+ liftEffect $ expectToBe 2 (Array.length comments)
75+
76+ -- Step 6: Assign a task
77+ assignedTask <- services.taskRepo.assign task3.id 4 # Om.liftAff
78+ case assignedTask of
79+ Nothing -> liftEffect $ Console.log "✗ Failed to assign task"
80+ Just t -> do
81+ let
82+ assigneeIdNum = case t.assigneeId of
83+ Nothing -> 0
84+ Just uid -> uid
85+ liftEffect $ Console.log $ "✓ Assigned task #" <> show t.id <> " to user #" <> show assigneeIdNum
86+ liftEffect $ expectToBe (Just 4) t.assigneeId
87+
88+ -- Step 7: Update a task
89+ let updateReq = { title: Just "Design mockups (updated)", description: Nothing, status: Nothing, priority: Nothing, assigneeId: Nothing, dueDate: Nothing }
90+ updatedTask <- services.taskRepo.update task1.id updateReq # Om.liftAff
91+ case updatedTask of
92+ Nothing -> liftEffect $ Console.log "✗ Failed to update task"
93+ Just t -> do
94+ liftEffect $ Console.log $ "✓ Updated task #" <> show t.id
95+ liftEffect $ expectToBe "Design mockups (updated)" t.title
96+
97+ -- Step 8: Record task history
98+ historyEntry <- services.taskHistoryRepo.recordChange task1.id 1 "title" (Just "Design mockups") "Design mockups (updated)" # Om.liftAff
99+ liftEffect $ Console.log $ "✓ Recorded history entry #" <> show historyEntry.id
100+
101+ -- Step 9: Get task history
102+ history <- services.taskHistoryRepo.findByTask task1.id # Om.liftAff
103+ liftEffect $ Console.log $ "✓ Found " <> show (Array.length history) <> " history entries"
104+ liftEffect $ expectToBe 1 (Array.length history)
105+
106+ liftEffect $ Console.log "✓ Complete workflow test passed!"
107+
108+ test "authentication and authorization flow" do
109+ launchAff_ do
110+ let
111+ appConfig = { apiName: "Project Tracker", version: "1.0.0" }
112+ dbConfig = { connectionString: "memory://", poolSize: 10 }
113+
114+ runWithServices appConfig dbConfig do
115+ services <- Om.ask
116+
117+ -- Test authentication
118+ let token = "Bearer test-token-123" -- This would be a BearerToken in real code
119+ liftEffect $ Console.log "✓ Authentication service available"
120+
121+ -- Test authorization
122+ liftEffect $ Console.log "✓ Authorization checks ready"
123+
124+ -- In a real implementation, you'd check permissions here
125+ -- user <- services.authService.authenticate token # Om.liftAff
126+ -- authorized <- services.authService.authorize user "projects:create" # Om.liftAff
127+
128+ liftEffect $ Console.log "✓ Auth flow test passed!"
129+
130+ test "filter tasks by status and priority" do
131+ launchAff_ do
132+ let
133+ appConfig = { apiName: "Project Tracker", version: "1.0.0" }
134+ dbConfig = { connectionString: "memory://", poolSize: 10 }
135+
136+ runWithServices appConfig dbConfig do
137+ services <- Om.ask
138+
139+ -- Create a project
140+ let createProjectReq = { name: "Backend Services", description: "API development" }
141+ project <- services.projectRepo.create createProjectReq 1 # Om.liftAff
142+
143+ -- Create tasks with different priorities
144+ let
145+ highPriorityTask = { title: "Critical bug", description: "Fix production issue", priority: High, assigneeId: Nothing, dueDate: Nothing }
146+ mediumPriorityTask = { title: "Add feature", description: "New endpoint", priority: Medium, assigneeId: Nothing, dueDate: Nothing }
147+
148+ _ <- services.taskRepo.create project.id highPriorityTask 1 # Om.liftAff
149+ _ <- services.taskRepo.create project.id mediumPriorityTask 1 # Om.liftAff
150+
151+ -- List all tasks (no filters)
152+ let allTasksFilter = { status: Nothing, priority: Nothing, assigneeId: Nothing } /\ { limit: 10, offset: 0 }
153+ allTasks <- services.taskRepo.findByProject project.id allTasksFilter # Om.liftAff
154+ liftEffect $ Console.log $ "✓ Found " <> show (Array.length allTasks) <> " tasks (no filter)"
155+ liftEffect $ expectToBe 2 (Array.length allTasks)
156+
157+ liftEffect $ Console.log "✓ Task filtering test passed!"
158+
159+ test "attachment upload and retrieval" do
160+ launchAff_ do
161+ let
162+ appConfig = { apiName: "Project Tracker", version: "1.0.0" }
163+ dbConfig = { connectionString: "memory://", poolSize: 10 }
164+
165+ runWithServices appConfig dbConfig do
166+ services <- Om.ask
167+
168+ -- Create a project and task
169+ let createProjectReq = { name: "Documentation", description: "Update docs" }
170+ project <- services.projectRepo.create createProjectReq 1 # Om.liftAff
171+
172+ let taskReq = { title: "Upload diagrams", description: "Add architecture diagrams", priority: Medium, assigneeId: Nothing, dueDate: Nothing }
173+ task <- services.taskRepo.create project.id taskReq 1 # Om.liftAff
174+
175+ -- Upload an attachment
176+ let fileUpload = { file: "fake-file-contents" }
177+ attachment <- services.attachmentRepo.create task.id fileUpload 1 # Om.liftAff
178+ liftEffect $ Console.log $ "✓ Uploaded attachment #" <> show attachment.id <> ": " <> attachment.fileName
179+ liftEffect $ expectToBe "uploaded-file.txt" attachment.fileName
180+
181+ -- List attachments
182+ attachments <- services.attachmentRepo.findByTask task.id # Om.liftAff
183+ liftEffect $ Console.log $ "✓ Found " <> show (Array.length attachments) <> " attachments"
184+ liftEffect $ expectToBe 1 (Array.length attachments)
185+
186+ -- Delete attachment
187+ deleted <- services.attachmentRepo.delete attachment.id # Om.liftAff
188+ liftEffect $ expectToBe true deleted
189+ liftEffect $ Console.log "✓ Deleted attachment"
190+
191+ -- Verify deletion
192+ attachmentsAfter <- services.attachmentRepo.findByTask task.id # Om.liftAff
193+ liftEffect $ expectToBe 0 (Array.length attachmentsAfter)
194+
195+ liftEffect $ Console.log "✓ Attachment workflow test passed!"
test/Test/ProjectTrackerLayers.purs.disablednew file+340
@@ -0,0 +1,340 @@
1+module Test.ProjectTrackerLayers where
2+
3+import Prelude
4+
5+import Data.Array as Array
6+import Data.Maybe (Maybe(..))
7+import Data.Tuple.Nested (type (/\))
8+import Effect (Effect)
9+import Effect.Aff (Aff)
10+import Effect.Class (liftEffect)
11+import Effect.Console as Console
12+import Effect.Ref as Ref
13+import Test.ProjectTrackerExample (Attachment, AttachmentRepo, AuthService, Comment, CommentRepo, CreateCommentRequest, CreateProjectRequest, CreateTaskRequest, FileUpload, PaginationParams, Priority, Project, ProjectRepo, ProjectStatus(..), Task, TaskFilterParams, TaskHistoryEntry, TaskHistoryRepo, TaskRepo, TaskStatus(..), UpdateProjectRequest, UpdateTaskRequest, User, UserRole(..))
14+import Type.Function (type (#))
15+import Yoga.Om (Om)
16+import Yoga.Om as Om
17+
18+--------------------------------------------------------------------------------
19+-- OmLayer Service Configuration and Layers
20+--------------------------------------------------------------------------------
21+
22+-- | Application configuration
23+type AppConfig =
24+ { apiName :: String
25+ , version :: String
26+ }
27+
28+-- | Database configuration (placeholder for real DB)
29+type DbConfig =
30+ { connectionString :: String
31+ , poolSize :: Int
32+ }
33+
34+-- | All services provided by the application
35+type Services =
36+ { projectRepo :: ProjectRepo
37+ , taskRepo :: TaskRepo
38+ , commentRepo :: CommentRepo
39+ , attachmentRepo :: AttachmentRepo
40+ , taskHistoryRepo :: TaskHistoryRepo
41+ , authService :: AuthService
42+ }
43+
44+--------------------------------------------------------------------------------
45+-- Repository Implementations (In-Memory)
46+--------------------------------------------------------------------------------
47+
48+makeInMemoryProjectRepo :: Effect ProjectRepo
49+makeInMemoryProjectRepo = do
50+ projectsRef <- Ref.new []
51+ pure
52+ { findAll: \_ -> Ref.read projectsRef # liftEffect
53+ , findById: \id -> do
54+ projects <- Ref.read projectsRef # liftEffect
55+ pure $ Array.find (\p -> p.id == id) projects
56+ , create: \req userId -> do
57+ projects <- Ref.read projectsRef # liftEffect
58+ let
59+ newId = Array.length projects + 1
60+ now = 0 -- placeholder timestamp
61+ newProject =
62+ { id: newId
63+ , name: req.name
64+ , description: req.description
65+ , status: Active
66+ , createdAt: now
67+ , createdBy: userId
68+ }
69+ Ref.write (Array.snoc projects newProject) projectsRef # liftEffect
70+ pure newProject
71+ , update: \id req -> do
72+ projects <- Ref.read projectsRef # liftEffect
73+ case Array.findIndex (\p -> p.id == id) projects of
74+ Nothing -> pure Nothing
75+ Just idx -> case Array.index projects idx of
76+ Nothing -> pure Nothing
77+ Just project -> do
78+ let
79+ updated = project
80+ { name = case req.name of
81+ Nothing -> project.name
82+ Just n -> n
83+ , description = case req.description of
84+ Nothing -> project.description
85+ Just d -> d
86+ }
87+ let
88+ updatedProjects = case Array.updateAt idx updated projects of
89+ Nothing -> projects
90+ Just arr -> arr
91+ Ref.write updatedProjects projectsRef # liftEffect
92+ pure $ Just updated
93+ , archive: \id -> do
94+ projects <- Ref.read projectsRef # liftEffect
95+ case Array.findIndex (\p -> p.id == id) projects of
96+ Nothing -> pure false
97+ Just _ -> do
98+ let filtered = Array.filter (\p -> p.id /= id) projects
99+ Ref.write filtered projectsRef # liftEffect
100+ pure true
101+ }
102+
103+makeInMemoryTaskRepo :: Effect TaskRepo
104+makeInMemoryTaskRepo = do
105+ tasksRef <- Ref.new []
106+ pure
107+ { findByProject: \projectId _filters -> do
108+ tasks <- Ref.read tasksRef # liftEffect
109+ pure $ Array.filter (\t -> t.projectId == projectId) tasks
110+ , findById: \id -> do
111+ tasks <- Ref.read tasksRef # liftEffect
112+ pure $ Array.find (\t -> t.id == id) tasks
113+ , create: \projectId req userId -> do
114+ tasks <- Ref.read tasksRef # liftEffect
115+ let
116+ newId = Array.length tasks + 1
117+ now = 0 -- placeholder timestamp
118+ newTask =
119+ { id: newId
120+ , projectId
121+ , title: req.title
122+ , description: req.description
123+ , status: Todo
124+ , priority: req.priority
125+ , assigneeId: req.assigneeId
126+ , createdAt: now
127+ , updatedAt: now
128+ , dueDate: req.dueDate
129+ }
130+ Ref.write (Array.snoc tasks newTask) tasksRef # liftEffect
131+ pure newTask
132+ , update: \id req -> do
133+ tasks <- Ref.read tasksRef # liftEffect
134+ case Array.findIndex (\t -> t.id == id) tasks of
135+ Nothing -> pure Nothing
136+ Just idx -> case Array.index tasks idx of
137+ Nothing -> pure Nothing
138+ Just task -> do
139+ let
140+ now = 0 -- placeholder timestamp
141+ updated = task
142+ { title = case req.title of
143+ Nothing -> task.title
144+ Just t -> t
145+ , description = case req.description of
146+ Nothing -> task.description
147+ Just d -> d
148+ , status = case req.status of
149+ Nothing -> task.status
150+ Just s -> s
151+ , priority = case req.priority of
152+ Nothing -> task.priority
153+ Just p -> p
154+ , assigneeId = case req.assigneeId of
155+ Nothing -> task.assigneeId
156+ Just a -> Just a
157+ , updatedAt = now
158+ }
159+ let
160+ updatedTasks = case Array.updateAt idx updated tasks of
161+ Nothing -> tasks
162+ Just arr -> arr
163+ Ref.write updatedTasks tasksRef # liftEffect
164+ pure $ Just updated
165+ , delete: \id -> do
166+ tasks <- Ref.read tasksRef # liftEffect
167+ case Array.findIndex (\t -> t.id == id) tasks of
168+ Nothing -> pure false
169+ Just _ -> do
170+ let filtered = Array.filter (\t -> t.id /= id) tasks
171+ Ref.write filtered tasksRef # liftEffect
172+ pure true
173+ , assign: \taskId assigneeId -> do
174+ tasks <- Ref.read tasksRef # liftEffect
175+ case Array.findIndex (\t -> t.id == taskId) tasks of
176+ Nothing -> pure Nothing
177+ Just idx -> case Array.index tasks idx of
178+ Nothing -> pure Nothing
179+ Just task -> do
180+ let
181+ now = 0 -- placeholder timestamp
182+ updated = task { assigneeId = Just assigneeId, updatedAt = now }
183+ updatedTasks = case Array.updateAt idx updated tasks of
184+ Nothing -> tasks
185+ Just arr -> arr
186+ Ref.write updatedTasks tasksRef # liftEffect
187+ pure $ Just updated
188+ }
189+
190+makeInMemoryCommentRepo :: Effect CommentRepo
191+makeInMemoryCommentRepo = do
192+ commentsRef <- Ref.new []
193+ pure
194+ { findByTask: \taskId _pagination -> do
195+ comments <- Ref.read commentsRef # liftEffect
196+ pure $ Array.filter (\c -> c.taskId == taskId) comments
197+ , create: \taskId req authorId -> do
198+ comments <- Ref.read commentsRef # liftEffect
199+ let
200+ newId = Array.length comments + 1
201+ now = 0 -- placeholder timestamp
202+ newComment =
203+ { id: newId
204+ , taskId
205+ , authorId
206+ , content: req.content
207+ , createdAt: now
208+ }
209+ Ref.write (Array.snoc comments newComment) commentsRef # liftEffect
210+ pure newComment
211+ }
212+
213+makeInMemoryAttachmentRepo :: Effect AttachmentRepo
214+makeInMemoryAttachmentRepo = do
215+ attachmentsRef <- Ref.new []
216+ pure
217+ { findByTask: \taskId -> do
218+ attachments <- Ref.read attachmentsRef # liftEffect
219+ pure $ Array.filter (\a -> a.taskId == taskId) attachments
220+ , create: \taskId upload userId -> do
221+ attachments <- Ref.read attachmentsRef # liftEffect
222+ let
223+ newId = Array.length attachments + 1
224+ now = 0 -- placeholder timestamp
225+ newAttachment =
226+ { id: newId
227+ , taskId
228+ , fileName: "uploaded-file.txt"
229+ , fileSize: 1024
230+ , contentType: "text/plain"
231+ , uploadedBy: userId
232+ , uploadedAt: now
233+ , url: "/attachments/" <> show newId
234+ }
235+ Ref.write (Array.snoc attachments newAttachment) attachmentsRef # liftEffect
236+ pure newAttachment
237+ , delete: \id -> do
238+ attachments <- Ref.read attachmentsRef # liftEffect
239+ case Array.findIndex (\a -> a.id == id) attachments of
240+ Nothing -> pure false
241+ Just _ -> do
242+ let filtered = Array.filter (\a -> a.id /= id) attachments
243+ Ref.write filtered attachmentsRef # liftEffect
244+ pure true
245+ }
246+
247+makeInMemoryTaskHistoryRepo :: Effect TaskHistoryRepo
248+makeInMemoryTaskHistoryRepo = do
249+ historyRef <- Ref.new []
250+ pure
251+ { findByTask: \taskId -> do
252+ history <- Ref.read historyRef # liftEffect
253+ pure $ Array.filter (\h -> h.taskId == taskId) history
254+ , recordChange: \taskId userId field oldVal newVal -> do
255+ history <- Ref.read historyRef # liftEffect
256+ let
257+ newId = Array.length history + 1
258+ now = 0 -- placeholder timestamp
259+ entry =
260+ { id: newId
261+ , taskId
262+ , changedBy: userId
263+ , changedAt: now
264+ , field
265+ , oldValue: oldVal
266+ , newValue: newVal
267+ }
268+ Ref.write (Array.snoc history entry) historyRef # liftEffect
269+ pure entry
270+ }
271+
272+makeInMemoryAuthService :: Effect AuthService
273+makeInMemoryAuthService = pure
274+ { authenticate: \token -> pure $ Just
275+ { id: 1
276+ , username: "testuser"
277+ , email: "test@example.com"
278+ , fullName: "Test User"
279+ , role: Developer
280+ }
281+ , authorize: \user action -> pure true
282+ }
283+
284+--------------------------------------------------------------------------------
285+-- Om-based Service Initialization (OmLayer Pattern)
286+--------------------------------------------------------------------------------
287+
288+-- | Initialize all services with configuration from Om context
289+-- This demonstrates the OmLayer pattern using Om monad for dependency injection
290+initializeServices :: forall r. Om (appConfig :: AppConfig, dbConfig :: DbConfig # r) Unit Services
291+initializeServices = do
292+ { appConfig, dbConfig } <- Om.ask
293+
294+ -- Log initialization
295+ liftEffect $ Console.log $ "Initializing " <> appConfig.apiName <> " v" <> appConfig.version
296+ liftEffect $ Console.log $ "Database: " <> dbConfig.connectionString <> " (pool size: " <> show dbConfig.poolSize <> ")"
297+
298+ -- Initialize all repositories (these would connect to real DBs in production)
299+ projectRepo <- makeInMemoryProjectRepo # liftEffect
300+ taskRepo <- makeInMemoryTaskRepo # liftEffect
301+ commentRepo <- makeInMemoryCommentRepo # liftEffect
302+ attachmentRepo <- makeInMemoryAttachmentRepo # liftEffect
303+ taskHistoryRepo <- makeInMemoryTaskHistoryRepo # liftEffect
304+ authService <- makeInMemoryAuthService # liftEffect
305+
306+ liftEffect $ Console.log "✓ All services initialized"
307+
308+ pure
309+ { projectRepo
310+ , taskRepo
311+ , commentRepo
312+ , attachmentRepo
313+ , taskHistoryRepo
314+ , authService
315+ }
316+
317+-- | Run the application with all services initialized
318+-- This is the main entry point that:
319+-- 1. Takes configuration (appConfig, dbConfig)
320+-- 2. Initializes all services (repositories, auth)
321+-- 3. Runs the action with services in the Om context
322+-- 4. Handles any exceptions
323+runWithServices :: forall a. AppConfig -> DbConfig -> Om Services Unit a -> Aff a
324+runWithServices appConfig dbConfig action = do
325+ let configContext = { appConfig, dbConfig }
326+ -- First initialize services with config context
327+ services <- Om.runOm configContext
328+ { exception: \_ -> pure
329+ { projectRepo: bottom
330+ , taskRepo: bottom
331+ , commentRepo: bottom
332+ , attachmentRepo: bottom
333+ , taskHistoryRepo: bottom
334+ , authService: bottom
335+ }
336+ }
337+ initializeServices
338+
339+ -- Then run the action with services context
340+ Om.runOm services { exception: \_ -> pure unit } action
test/Test/SchemaComponentTest.purs+8−8
@@ -6,7 +6,7 @@ import Data.Maybe (Maybe(..))
6import Data.String as String
6import Data.String as String
7import Effect (Effect)
7import Effect (Effect)
8import Yoga.HTTP.API.Path (type (:), type (/))
8import Yoga.HTTP.API.Path (type (:), type (/))
9−import Yoga.HTTP.API.Route (GET, POST, Route, Request, JSON, Schema, buildOpenAPISpec)
9+import Yoga.HTTP.API.Route (GET, POST, Route, JSON, Schema, buildOpenAPISpec)
10import Yoga.JSON (writeJSON)
10import Yoga.JSON (writeJSON)
11import ViTest (ViTest, describe, test)
11import ViTest (ViTest, describe, test)
12import ViTest.Expect (expectToBe)
12import ViTest.Expect (expectToBe)
@@ -17,33 +17,33 @@ type Post = { id :: Int, title :: String, authorId :: Int }
17type SchemaAPI =
17type SchemaAPI =
18 { getUser ::
18 { getUser ::
19 Route GET ("users" / "id" : Int)
19 Route GET ("users" / "id" : Int)
20− (Request {})
20+ {}
21 (ok :: { body :: Schema "User" User })
21 (ok :: { body :: Schema "User" User })
22 , createUser ::
22 , createUser ::
23 Route POST "users"
23 Route POST "users"
24− (Request { body :: JSON (Schema "User" User) })
24+ { body :: JSON (Schema "User" User) }
25 (created :: { body :: Schema "User" User })
25 (created :: { body :: Schema "User" User })
26− , listPosts :: Route GET "posts" (Request {}) (ok :: { body :: Array (Schema "Post" Post) })
26+ , listPosts :: Route GET "posts" {} (ok :: { body :: Array (Schema "Post" Post) })
27 }
27 }
28
28
29testSchemaComponents :: Effect ViTest
29testSchemaComponents :: Effect ViTest
30testSchemaComponents = describe "Schema Components" do
30testSchemaComponents = describe "Schema Components" do
31 _ <- test "generates $ref in response body" do
31 _ <- test "generates $ref in response body" do
32 let
32 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" }
34 json = writeJSON spec
34 json = writeJSON spec
35 expectToBe true (String.contains (String.Pattern "\"$ref\":\"#/components/schemas/User\"") json)
35 expectToBe true (String.contains (String.Pattern "\"$ref\":\"#/components/schemas/User\"") json)
36
36
37 _ <- test "generates schema definition in components" do
37 _ <- test "generates schema definition in components" do
38 let
38 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" }
40 json = writeJSON spec
40 json = writeJSON spec
41 expectToBe true (String.contains (String.Pattern "components") json)
41 expectToBe true (String.contains (String.Pattern "components") json)
42 expectToBe true (String.contains (String.Pattern "\"schemas\"") json)
42 expectToBe true (String.contains (String.Pattern "\"schemas\"") json)
43
43
44 _ <- test "defines User schema with properties" do
44 _ <- test "defines User schema with properties" do
45 let
45 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" }
47 json = writeJSON spec
47 json = writeJSON spec
48 -- Schema should be in components, not inline
48 -- Schema should be in components, not inline
49 expectToBe true (String.contains (String.Pattern "\"User\"") json)
49 expectToBe true (String.contains (String.Pattern "\"User\"") json)
@@ -52,6 +52,6 @@ testSchemaComponents = describe "Schema Components" do
52
52
53 test "collects nested schemas from arrays" do
53 test "collects nested schemas from arrays" do
54 let
54 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" }
56 json = writeJSON spec
56 json = writeJSON spec
57 expectToBe true (String.contains (String.Pattern "\"Post\"") json)
57 expectToBe true (String.contains (String.Pattern "\"Post\"") json)
test/Test/VariantResponseTest.purs+7−7
@@ -10,7 +10,7 @@ import Effect.Aff (Aff)
10import Type.Proxy (Proxy(..))
10import Type.Proxy (Proxy(..))
11import Yoga.HTTP.API.Route.StatusCode (StatusCode(..))
11import Yoga.HTTP.API.Route.StatusCode (StatusCode(..))
12import Yoga.HTTP.API.Path (Root)
12import 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)
14import Yoga.HTTP.API.Route.Response (respond) as Response
14import Yoga.HTTP.API.Route.Response (respond) as Response
15import ViTest (ViTest, describe, test)
15import ViTest (ViTest, describe, test)
16import ViTest.Expect (expectToBe)
16import ViTest.Expect (expectToBe)
@@ -25,14 +25,14 @@ expectToEqual expected actual = expectToBe true (expected == actual)
25
25
26-- Simple variant route with ok and notFound responses
26-- Simple variant route with ok and notFound responses
27type SimpleVariantRoute = Route GET Root
27type SimpleVariantRoute = Route GET Root
28− (Request {})
28+ {}
29 ( ok :: { body :: String }
29 ( ok :: { body :: String }
30 , notFound :: { body :: String }
30 , notFound :: { body :: String }
31 )
31 )
32
32
33-- Variant route with multiple status codes
33-- Variant route with multiple status codes
34type MultiStatusRoute = Route POST Root
34type MultiStatusRoute = Route POST Root
35− (Request { headers :: { authorization :: String } })
35+ { headers :: { authorization :: String } }
36 ( created :: { headers :: { "Location" :: String }, body :: User }
36 ( created :: { headers :: { "Location" :: String }, body :: User }
37 , badRequest :: { body :: ErrorMessage }
37 , badRequest :: { body :: ErrorMessage }
38 , unauthorized :: { body :: ErrorMessage }
38 , unauthorized :: { body :: ErrorMessage }
@@ -40,7 +40,7 @@ type MultiStatusRoute = Route POST Root
40
40
41-- Variant route with many response types
41-- Variant route with many response types
42type ComplexVariantRoute = Route PUT Root
42type ComplexVariantRoute = Route PUT Root
43− (Request {})
43+ {}
44 ( ok :: { body :: User }
44 ( ok :: { body :: User }
45 , created :: { headers :: { "Location" :: String }, body :: User }
45 , created :: { headers :: { "Location" :: String }, body :: User }
46 , badRequest :: { body :: ErrorMessage }
46 , badRequest :: { body :: ErrorMessage }
@@ -242,7 +242,7 @@ testSimpleVariantOpenAPI = describe "OpenAPI Generation - Simple Variant" $ do
242 let
242 let
243 result = toOpenAPI
243 result = toOpenAPI
244 @( Route GET Root
244 @( Route GET Root
245− (Request {})
245+ {}
246 ( ok :: { body :: String }
246 ( ok :: { body :: String }
247 , notFound :: { body :: String }
247 , notFound :: { body :: String }
248 )
248 )
@@ -265,7 +265,7 @@ testComplexVariantOpenAPI = describe "OpenAPI Generation - Complex Variant" $ do
265 let
265 let
266 result = toOpenAPI
266 result = toOpenAPI
267 @( Route POST Root
267 @( Route POST Root
268− (Request { headers :: { authorization :: String } })
268+ { headers :: { authorization :: String } }
269 ( created :: { headers :: { "Location" :: String }, body :: User }
269 ( created :: { headers :: { "Location" :: String }, body :: User }
270 , badRequest :: { body :: ErrorMessage }
270 , badRequest :: { body :: ErrorMessage }
271 , unauthorized :: { body :: ErrorMessage }
271 , unauthorized :: { body :: ErrorMessage }
@@ -299,7 +299,7 @@ testVariantWithHeaders = describe "OpenAPI Generation - Variant with Response He
299 let
299 let
300 result = toOpenAPI
300 result = toOpenAPI
301 @( Route POST Root
301 @( Route POST Root
302− (Request {})
302+ {}
303 ( created :: { headers :: { "Location" :: String, "X-Request-Id" :: String }, body :: User }
303 ( created :: { headers :: { "Location" :: String, "X-Request-Id" :: String }, body :: User }
304 )
304 )
305 )
305 )