git.iko.soy/rowtype-yoga/purescript-yoga-http-api
on feat/custom-type-errors help sign in
Initial commit: pure HTTP API route definitions, Path DSL, and OpenAPI generation
1db6318Mark
@@ -0,0 +1,4 @@
1+.spago
2+output
3+node_modules
4+.purs-repl
spago.locknew file+1305
@@ -0,0 +1,1305 @@
1+{
2+ "workspace": {
3+ "packages": {
4+ "yoga-http-api": {
5+ "path": "./",
6+ "core": {
7+ "dependencies": [
8+ {
9+ "aff": ">=7.0.0 <8.0.0"
10+ },
11+ {
12+ "arrays": ">=7.0.0 <8.0.0"
13+ },
14+ {
15+ "effect": ">=4.0.0 <5.0.0"
16+ },
17+ {
18+ "either": ">=6.0.0 <7.0.0"
19+ },
20+ {
21+ "foldable-traversable": ">=6.0.0 <7.0.0"
22+ },
23+ {
24+ "foreign": ">=7.0.0 <8.0.0"
25+ },
26+ {
27+ "foreign-object": ">=4.0.0 <5.0.0"
28+ },
29+ {
30+ "integers": ">=6.0.0 <7.0.0"
31+ },
32+ {
33+ "maybe": ">=6.0.0 <7.0.0"
34+ },
35+ {
36+ "newtype": ">=5.0.0 <6.0.0"
37+ },
38+ {
39+ "prelude": ">=6.0.0 <7.0.0"
40+ },
41+ {
42+ "record": ">=4.0.0 <5.0.0"
43+ },
44+ {
45+ "strings": ">=6.0.0 <7.0.0"
46+ },
47+ {
48+ "tuples": ">=7.0.0 <8.0.0"
49+ },
50+ {
51+ "typelevel-prelude": ">=7.0.0 <8.0.0"
52+ },
53+ {
54+ "unsafe-coerce": ">=6.0.0 <7.0.0"
55+ },
56+ {
57+ "variant": ">=8.0.0 <9.0.0"
58+ },
59+ {
60+ "yoga-json": ">=5.0.0 <6.0.0"
61+ }
62+ ]
63+ },
64+ "test": {
65+ "dependencies": []
66+ }
67+ }
68+ },
69+ "package_set": {
70+ "address": {
71+ "registry": "51.1.0"
72+ },
73+ "compiler": ">=0.15.15 <0.16.0",
74+ "content": {
75+ "abc-parser": "2.0.1",
76+ "ace": "9.1.0",
77+ "address-rfc2821": "0.1.1",
78+ "aff": "7.1.0",
79+ "aff-bus": "6.0.0",
80+ "aff-coroutines": "9.0.0",
81+ "aff-promise": "4.0.0",
82+ "aff-retry": "2.0.0",
83+ "affjax": "13.0.0",
84+ "affjax-node": "1.0.0",
85+ "affjax-web": "1.0.0",
86+ "ansi": "7.0.0",
87+ "apexcharts": "0.5.0",
88+ "applicative-phases": "1.0.0",
89+ "argonaut": "9.0.0",
90+ "argonaut-aeson-generic": "0.4.1",
91+ "argonaut-codecs": "9.1.0",
92+ "argonaut-core": "7.0.0",
93+ "argonaut-generic": "8.0.0",
94+ "argonaut-traversals": "10.0.0",
95+ "argparse-basic": "2.0.0",
96+ "array-builder": "0.1.2",
97+ "array-search": "0.5.6",
98+ "arraybuffer": "13.2.0",
99+ "arraybuffer-builder": "3.1.0",
100+ "arraybuffer-types": "3.0.2",
101+ "arrays": "7.3.0",
102+ "arrays-extra": "0.6.1",
103+ "arrays-zipper": "2.0.1",
104+ "ask": "1.0.0",
105+ "assert": "6.0.0",
106+ "assert-multiple": "0.3.4",
107+ "avar": "5.0.0",
108+ "b64": "0.0.8",
109+ "barbies": "1.0.1",
110+ "barlow-lens": "0.9.0",
111+ "bifunctors": "6.0.0",
112+ "bigints": "7.0.1",
113+ "bolson": "0.3.9",
114+ "bookhound": "0.1.7",
115+ "bower-json": "3.0.0",
116+ "call-by-name": "4.0.1",
117+ "canvas": "6.0.0",
118+ "canvas-action": "9.0.0",
119+ "cartesian": "1.0.6",
120+ "catenable-lists": "7.0.0",
121+ "cbor-stream": "1.0.4",
122+ "chameleon": "1.0.0",
123+ "chameleon-halogen": "1.0.3",
124+ "chameleon-react-basic": "1.1.0",
125+ "chameleon-styled": "2.5.0",
126+ "chameleon-transformers": "1.0.0",
127+ "channel": "1.0.0",
128+ "checked-exceptions": "3.1.1",
129+ "choku": "1.0.1",
130+ "classless": "0.1.1",
131+ "classless-arbitrary": "0.1.1",
132+ "classless-decode-json": "0.1.1",
133+ "classless-encode-json": "0.1.3",
134+ "classnames": "2.0.0",
135+ "codec": "6.1.0",
136+ "codec-argonaut": "10.0.0",
137+ "codec-json": "1.2.0",
138+ "colors": "7.0.1",
139+ "concur-core": "0.5.0",
140+ "concur-react": "0.5.0",
141+ "concurrent-queues": "3.0.0",
142+ "console": "6.1.0",
143+ "const": "6.0.0",
144+ "contravariant": "6.0.0",
145+ "control": "6.0.0",
146+ "convertable-options": "1.0.0",
147+ "coroutines": "7.0.0",
148+ "css": "6.0.0",
149+ "css-frameworks": "1.0.1",
150+ "csv-stream": "2.0.2",
151+ "data-mvc": "0.0.2",
152+ "datetime": "6.1.0",
153+ "datetime-parsing": "0.2.0",
154+ "debounce": "0.1.0",
155+ "debug": "6.0.2",
156+ "decimals": "7.1.0",
157+ "default-values": "1.0.1",
158+ "deku": "0.9.23",
159+ "deno": "0.0.5",
160+ "dissect": "1.0.0",
161+ "distributive": "6.0.0",
162+ "dom-filereader": "7.0.0",
163+ "dom-indexed": "12.0.0",
164+ "dom-simple": "0.4.0",
165+ "dotenv": "4.0.3",
166+ "droplet": "0.6.0",
167+ "dts": "1.0.0",
168+ "dual-numbers": "1.0.2",
169+ "dynamic-buffer": "3.0.1",
170+ "echarts-simple": "0.0.1",
171+ "effect": "4.0.0",
172+ "either": "6.1.0",
173+ "elmish": "0.11.3",
174+ "elmish-enzyme": "0.1.1",
175+ "elmish-hooks": "0.10.0",
176+ "elmish-html": "0.8.2",
177+ "elmish-testing-library": "0.3.2",
178+ "email-validate": "7.0.0",
179+ "encoding": "0.0.9",
180+ "enums": "6.0.1",
181+ "env-names": "0.3.4",
182+ "error": "2.0.0",
183+ "eta-conversion": "0.3.2",
184+ "exceptions": "6.0.0",
185+ "exists": "6.0.0",
186+ "exitcodes": "4.0.0",
187+ "expect-inferred": "3.0.0",
188+ "fahrtwind": "2.0.0",
189+ "fallback": "0.1.0",
190+ "fast-vect": "1.2.0",
191+ "fetch": "4.1.0",
192+ "fetch-argonaut": "1.0.1",
193+ "fetch-core": "5.1.0",
194+ "fetch-yoga-json": "1.1.0",
195+ "ffi-simple": "0.5.1",
196+ "fft-js": "0.1.0",
197+ "filterable": "5.0.0",
198+ "fix-functor": "0.1.0",
199+ "fixed-points": "7.0.0",
200+ "fixed-precision": "5.0.0",
201+ "flame": "1.3.0",
202+ "float32": "2.0.0",
203+ "fmt": "0.2.1",
204+ "foldable-traversable": "6.0.0",
205+ "foldable-traversable-extra": "0.0.6",
206+ "foreign": "7.0.0",
207+ "foreign-object": "4.1.0",
208+ "foreign-readwrite": "3.4.0",
209+ "forgetmenot": "0.1.0",
210+ "fork": "6.0.0",
211+ "form-urlencoded": "7.0.0",
212+ "formatters": "7.0.0",
213+ "framer-motion": "1.0.1",
214+ "free": "7.1.0",
215+ "freeap": "7.0.0",
216+ "freer-free": "0.0.1",
217+ "freet": "7.0.0",
218+ "functions": "6.0.0",
219+ "functor1": "3.0.0",
220+ "functors": "5.0.0",
221+ "fuzzy": "0.4.0",
222+ "gen": "4.0.0",
223+ "generate-values": "1.0.1",
224+ "generic-router": "0.0.1",
225+ "geojson": "0.0.5",
226+ "geometry-plane": "1.0.3",
227+ "gojs": "0.1.1",
228+ "grain": "3.0.0",
229+ "grain-router": "3.0.0",
230+ "grain-virtualized": "3.0.0",
231+ "graphs": "8.1.0",
232+ "group": "4.1.1",
233+ "halogen": "7.0.0",
234+ "halogen-bootstrap5": "5.3.2",
235+ "halogen-canvas": "1.0.0",
236+ "halogen-css": "10.0.0",
237+ "halogen-echarts-simple": "0.0.4",
238+ "halogen-formless": "4.0.3",
239+ "halogen-helix": "1.0.0",
240+ "halogen-hooks": "0.6.3",
241+ "halogen-hooks-extra": "0.9.0",
242+ "halogen-infinite-scroll": "1.1.0",
243+ "halogen-store": "0.5.4",
244+ "halogen-storybook": "2.0.0",
245+ "halogen-subscriptions": "2.0.0",
246+ "halogen-svg-elems": "8.0.0",
247+ "halogen-typewriter": "1.0.4",
248+ "halogen-vdom": "8.0.0",
249+ "halogen-vdom-string-renderer": "0.5.0",
250+ "halogen-xterm": "2.0.0",
251+ "heckin": "2.0.1",
252+ "heterogeneous": "0.6.0",
253+ "homogeneous": "0.4.0",
254+ "http-methods": "6.0.0",
255+ "httpurple": "4.0.0",
256+ "humdrum": "0.0.1",
257+ "hyrule": "2.3.8",
258+ "identity": "6.0.0",
259+ "identy": "4.0.1",
260+ "indexed-db": "1.0.0",
261+ "indexed-monad": "3.0.0",
262+ "int64": "3.0.0",
263+ "integers": "6.0.0",
264+ "interpolate": "5.0.2",
265+ "intersection-observer": "1.0.1",
266+ "invariant": "6.0.0",
267+ "jarilo": "1.0.1",
268+ "jelly": "0.10.0",
269+ "jelly-router": "0.3.0",
270+ "jelly-signal": "0.4.0",
271+ "jest": "1.0.0",
272+ "js-abort-controller": "1.0.0",
273+ "js-bigints": "2.2.1",
274+ "js-date": "8.0.0",
275+ "js-fetch": "0.2.1",
276+ "js-fileio": "3.0.0",
277+ "js-intl": "1.0.4",
278+ "js-iterators": "0.1.1",
279+ "js-maps": "0.1.2",
280+ "js-promise": "1.0.0",
281+ "js-promise-aff": "1.0.0",
282+ "js-timers": "6.1.0",
283+ "js-uri": "3.1.0",
284+ "json": "1.1.0",
285+ "json-codecs": "5.0.0",
286+ "justifill": "0.5.0",
287+ "jwt": "0.0.9",
288+ "labeled-data": "0.2.0",
289+ "language-cst-parser": "0.14.0",
290+ "lazy": "6.0.0",
291+ "lazy-joe": "1.0.0",
292+ "lcg": "4.0.0",
293+ "leibniz": "5.0.0",
294+ "leveldb": "1.0.1",
295+ "liminal": "1.0.1",
296+ "linalg": "6.0.0",
297+ "lists": "7.0.0",
298+ "literals": "1.0.2",
299+ "logging": "3.0.0",
300+ "logging-journald": "0.4.0",
301+ "lumi-components": "18.0.0",
302+ "machines": "7.0.0",
303+ "maps-eager": "0.4.1",
304+ "marionette": "1.0.0",
305+ "marionette-react-basic-hooks": "0.1.1",
306+ "marked": "0.1.0",
307+ "matrices": "5.0.1",
308+ "matryoshka": "1.0.0",
309+ "maybe": "6.0.0",
310+ "media-types": "6.0.0",
311+ "meowclient": "1.0.0",
312+ "midi": "4.0.0",
313+ "milkis": "9.0.0",
314+ "minibench": "4.0.1",
315+ "mmorph": "7.0.0",
316+ "monad-control": "5.0.0",
317+ "monad-logger": "1.3.1",
318+ "monad-loops": "0.5.0",
319+ "monad-unlift": "1.0.1",
320+ "monoid-extras": "0.0.1",
321+ "monoidal": "0.16.0",
322+ "morello": "0.4.0",
323+ "mote": "3.0.0",
324+ "motsunabe": "2.0.0",
325+ "mvc": "0.0.1",
326+ "mysql": "6.0.1",
327+ "n3": "0.1.0",
328+ "nano-id": "1.1.0",
329+ "nanoid": "0.1.0",
330+ "naturals": "3.0.0",
331+ "nested-functor": "0.2.1",
332+ "newtype": "5.0.0",
333+ "nextjs": "0.1.1",
334+ "nextui": "0.2.0",
335+ "node-buffer": "9.0.0",
336+ "node-child-process": "11.1.0",
337+ "node-event-emitter": "3.0.0",
338+ "node-execa": "5.0.0",
339+ "node-fs": "9.1.0",
340+ "node-glob-basic": "1.3.0",
341+ "node-http": "9.1.0",
342+ "node-http2": "1.1.1",
343+ "node-human-signals": "1.0.0",
344+ "node-net": "5.1.0",
345+ "node-os": "5.1.0",
346+ "node-path": "5.0.0",
347+ "node-process": "11.2.0",
348+ "node-readline": "8.1.0",
349+ "node-sqlite3": "8.0.0",
350+ "node-stream-pipes": "1.4.1",
351+ "node-streams": "9.0.0",
352+ "node-tls": "0.3.1",
353+ "node-url": "7.0.1",
354+ "node-zlib": "0.4.0",
355+ "nonempty": "7.0.0",
356+ "now": "6.0.0",
357+ "npm-package-json": "2.0.0",
358+ "nullable": "6.0.0",
359+ "numberfield": "0.1.0",
360+ "numbers": "9.0.1",
361+ "oak": "3.1.1",
362+ "oak-debug": "1.2.2",
363+ "object-maps": "0.3.0",
364+ "ocarina": "1.5.4",
365+ "open-folds": "6.3.0",
366+ "open-memoize": "6.1.0",
367+ "open-pairing": "6.1.0",
368+ "options": "7.0.0",
369+ "optparse": "5.0.1",
370+ "ordered-collections": "3.2.0",
371+ "ordered-set": "0.4.0",
372+ "orders": "6.0.0",
373+ "owoify": "1.2.0",
374+ "pairs": "9.0.1",
375+ "parallel": "7.0.0",
376+ "parsing": "10.2.0",
377+ "parsing-dataview": "3.2.4",
378+ "partial": "4.0.0",
379+ "pathy": "9.0.0",
380+ "pha": "0.13.0",
381+ "phaser": "0.7.0",
382+ "phylio": "1.1.2",
383+ "pipes": "8.0.0",
384+ "pirates-charm": "0.0.1",
385+ "pmock": "0.9.0",
386+ "point-free": "1.0.0",
387+ "pointed-list": "0.5.1",
388+ "polymorphic-vectors": "4.0.0",
389+ "posix-types": "6.0.0",
390+ "postgresql": "1.6.3",
391+ "precise": "6.0.0",
392+ "precise-datetime": "7.0.0",
393+ "prelude": "6.0.1",
394+ "prettier-printer": "3.0.0",
395+ "profunctor": "6.0.1",
396+ "profunctor-lenses": "8.0.0",
397+ "protobuf": "4.3.0",
398+ "psa-utils": "8.0.0",
399+ "psci-support": "6.0.0",
400+ "punycode": "1.0.0",
401+ "qualified-do": "2.2.0",
402+ "quantities": "12.2.0",
403+ "quickcheck": "8.0.1",
404+ "quickcheck-combinators": "0.1.3",
405+ "quickcheck-laws": "7.0.0",
406+ "quickcheck-utf8": "0.0.0",
407+ "random": "6.0.0",
408+ "rationals": "6.0.0",
409+ "rdf": "0.1.0",
410+ "react": "11.0.0",
411+ "react-aria": "0.2.0",
412+ "react-basic": "17.0.0",
413+ "react-basic-classic": "3.0.0",
414+ "react-basic-dnd": "10.1.0",
415+ "react-basic-dom": "6.1.0",
416+ "react-basic-emotion": "7.1.0",
417+ "react-basic-hooks": "8.2.0",
418+ "react-basic-storybook": "2.0.0",
419+ "react-dom": "8.0.0",
420+ "react-halo": "3.0.0",
421+ "react-icons": "1.1.5",
422+ "react-markdown": "0.1.0",
423+ "react-testing-library": "4.0.1",
424+ "react-virtuoso": "1.0.0",
425+ "reactix": "0.6.1",
426+ "read": "1.0.1",
427+ "recharts": "1.1.0",
428+ "record": "4.0.0",
429+ "record-extra": "5.0.1",
430+ "record-ptional-fields": "0.1.2",
431+ "record-studio": "1.0.4",
432+ "refs": "6.0.0",
433+ "remotedata": "5.0.1",
434+ "resize-observer": "1.0.0",
435+ "resource": "2.0.1",
436+ "resourcet": "1.0.0",
437+ "result": "1.0.3",
438+ "return": "0.2.0",
439+ "ring-modules": "5.0.1",
440+ "rito": "0.3.4",
441+ "rough-notation": "1.0.2",
442+ "routing": "11.0.0",
443+ "routing-duplex": "0.7.0",
444+ "run": "5.0.0",
445+ "safe-coerce": "2.0.0",
446+ "safely": "4.0.1",
447+ "school-of-music": "1.3.0",
448+ "selection-foldable": "0.2.0",
449+ "selective-functors": "1.0.1",
450+ "semirings": "7.0.0",
451+ "signal": "13.0.0",
452+ "simple-emitter": "3.0.1",
453+ "simple-i18n": "2.0.1",
454+ "simple-json": "9.0.0",
455+ "simple-json-generics": "0.2.1",
456+ "simple-ulid": "3.0.0",
457+ "sized-matrices": "1.0.0",
458+ "sized-vectors": "5.0.2",
459+ "slug": "3.0.8",
460+ "small-ffi": "4.0.1",
461+ "soundfonts": "4.1.0",
462+ "sparse-matrices": "1.3.0",
463+ "sparse-polynomials": "2.0.5",
464+ "spec": "7.6.0",
465+ "spec-mocha": "5.1.0",
466+ "spec-quickcheck": "5.0.0",
467+ "splitmix": "2.1.0",
468+ "ssrs": "1.0.0",
469+ "st": "6.2.0",
470+ "statistics": "0.3.2",
471+ "strictlypositiveint": "1.0.1",
472+ "string-parsers": "8.0.0",
473+ "strings": "6.0.1",
474+ "strings-extra": "4.0.0",
475+ "stringutils": "0.0.12",
476+ "substitute": "0.2.3",
477+ "supply": "0.2.0",
478+ "svg-parser": "3.0.0",
479+ "systemd-journald": "0.3.0",
480+ "tagged": "4.0.2",
481+ "tailrec": "6.1.0",
482+ "tecton": "0.2.1",
483+ "tecton-halogen": "0.2.0",
484+ "test-unit": "17.0.0",
485+ "thermite": "6.3.1",
486+ "thermite-dom": "0.3.1",
487+ "these": "6.0.0",
488+ "toestand": "0.9.0",
489+ "transformation-matrix": "1.0.1",
490+ "transformers": "6.0.0",
491+ "tree-rose": "4.0.2",
492+ "ts-bridge": "4.0.0",
493+ "tuples": "7.0.0",
494+ "two-or-more": "1.0.0",
495+ "type-equality": "4.0.1",
496+ "typedenv": "2.0.1",
497+ "typelevel": "6.0.0",
498+ "typelevel-lists": "2.1.0",
499+ "typelevel-peano": "1.0.1",
500+ "typelevel-prelude": "7.0.0",
501+ "typelevel-regex": "0.0.3",
502+ "typelevel-rows": "0.1.0",
503+ "typisch": "0.4.0",
504+ "uint": "7.0.0",
505+ "ulid": "3.0.1",
506+ "uncurried-transformers": "1.1.0",
507+ "undefined": "2.0.0",
508+ "undefined-is-not-a-problem": "1.1.0",
509+ "unfoldable": "6.0.0",
510+ "unicode": "6.0.0",
511+ "unique": "0.6.1",
512+ "unlift": "1.0.1",
513+ "unordered-collections": "3.1.0",
514+ "unsafe-coerce": "6.0.0",
515+ "unsafe-reference": "5.0.0",
516+ "untagged-to-tagged": "0.1.4",
517+ "untagged-union": "1.0.0",
518+ "uri": "9.0.0",
519+ "uuid": "9.0.0",
520+ "uuidv4": "1.0.0",
521+ "validation": "6.0.0",
522+ "variant": "8.0.0",
523+ "variant-encodings": "2.0.0",
524+ "vectorfield": "1.0.1",
525+ "vectors": "2.1.0",
526+ "versions": "7.0.0",
527+ "visx": "0.0.2",
528+ "web-clipboard": "5.0.0",
529+ "web-cssom": "2.0.0",
530+ "web-cssom-view": "0.1.0",
531+ "web-dom": "6.0.0",
532+ "web-dom-parser": "8.0.0",
533+ "web-dom-xpath": "3.0.0",
534+ "web-encoding": "3.0.0",
535+ "web-events": "4.0.0",
536+ "web-fetch": "4.0.1",
537+ "web-file": "4.0.0",
538+ "web-geometry": "0.1.0",
539+ "web-html": "4.1.0",
540+ "web-pointerevents": "2.0.0",
541+ "web-proletarian": "1.0.0",
542+ "web-promise": "3.2.0",
543+ "web-resize-observer": "2.1.0",
544+ "web-router": "1.0.0",
545+ "web-socket": "4.0.0",
546+ "web-storage": "5.0.0",
547+ "web-streams": "4.0.0",
548+ "web-touchevents": "4.0.0",
549+ "web-uievents": "5.0.0",
550+ "web-url": "2.0.0",
551+ "web-workers": "1.1.0",
552+ "web-xhr": "5.0.1",
553+ "webextension-polyfill": "0.1.0",
554+ "webgpu": "0.0.1",
555+ "which": "2.0.0",
556+ "xterm": "1.0.0",
557+ "yoga-fetch": "1.0.1",
558+ "yoga-json": "5.1.0",
559+ "yoga-om": "0.1.0",
560+ "yoga-postgres": "6.0.0",
561+ "yoga-tree": "1.0.0",
562+ "z3": "0.0.2",
563+ "zipperarray": "2.0.0"
564+ }
565+ },
566+ "extra_packages": {
567+ "yoga-json": {
568+ "path": "../purescript-yoga-json"
569+ }
570+ }
571+ },
572+ "packages": {
573+ "aff": {
574+ "type": "registry",
575+ "version": "7.1.0",
576+ "integrity": "sha256-/nOpDQVnKIoP1pQ1ZZBYpadK0TuMlG4WwufGzJpsfD0=",
577+ "dependencies": [
578+ "control",
579+ "datetime",
580+ "effect",
581+ "either",
582+ "exceptions",
583+ "functions",
584+ "newtype",
585+ "parallel",
586+ "partial",
587+ "prelude",
588+ "st",
589+ "tailrec",
590+ "transformers",
591+ "unsafe-coerce"
592+ ]
593+ },
594+ "arrays": {
595+ "type": "registry",
596+ "version": "7.3.0",
597+ "integrity": "sha256-GD4z7LCi9wzi7FCQqbWhvs8paoUK9NO+HwgXZ+KP8Rk=",
598+ "dependencies": [
599+ "bifunctors",
600+ "control",
601+ "foldable-traversable",
602+ "functions",
603+ "maybe",
604+ "nonempty",
605+ "partial",
606+ "prelude",
607+ "safe-coerce",
608+ "st",
609+ "tailrec",
610+ "tuples",
611+ "unfoldable",
612+ "unsafe-coerce"
613+ ]
614+ },
615+ "bifunctors": {
616+ "type": "registry",
617+ "version": "6.0.0",
618+ "integrity": "sha256-tXhmlMiyVy6mbUreVZQt/2EGEsPr0T1PjJi+YCG5J7E=",
619+ "dependencies": [
620+ "const",
621+ "either",
622+ "newtype",
623+ "prelude",
624+ "tuples"
625+ ]
626+ },
627+ "catenable-lists": {
628+ "type": "registry",
629+ "version": "7.0.0",
630+ "integrity": "sha256-7+i/MHBLIRh604iCNT4ENWRWE/wuHIotER4kpdX9Ve0=",
631+ "dependencies": [
632+ "control",
633+ "foldable-traversable",
634+ "lists",
635+ "maybe",
636+ "prelude",
637+ "tuples",
638+ "unfoldable"
639+ ]
640+ },
641+ "const": {
642+ "type": "registry",
643+ "version": "6.0.0",
644+ "integrity": "sha256-S+m0lQsxnji0z1XRUmkn+3RFuWJ5CXWuIr1u3i8Bfoc=",
645+ "dependencies": [
646+ "invariant",
647+ "newtype",
648+ "prelude"
649+ ]
650+ },
651+ "contravariant": {
652+ "type": "registry",
653+ "version": "6.0.0",
654+ "integrity": "sha256-nuckEWqL80NaPmR/l9AUyrfycNh18XcvlZV9PAamwqc=",
655+ "dependencies": [
656+ "const",
657+ "either",
658+ "newtype",
659+ "prelude",
660+ "tuples"
661+ ]
662+ },
663+ "control": {
664+ "type": "registry",
665+ "version": "6.0.0",
666+ "integrity": "sha256-0vxyYh5FL+ilxgLo954w1LqCUC0RSIivNNlb9CaNUQg=",
667+ "dependencies": [
668+ "newtype",
669+ "prelude"
670+ ]
671+ },
672+ "datetime": {
673+ "type": "registry",
674+ "version": "6.1.0",
675+ "integrity": "sha256-tbv6eDXy6QOD7YSknxYSbsDF32OC2JrNbSBpzMs7Doc=",
676+ "dependencies": [
677+ "bifunctors",
678+ "control",
679+ "either",
680+ "enums",
681+ "foldable-traversable",
682+ "functions",
683+ "gen",
684+ "integers",
685+ "lists",
686+ "maybe",
687+ "newtype",
688+ "numbers",
689+ "ordered-collections",
690+ "partial",
691+ "prelude",
692+ "tuples"
693+ ]
694+ },
695+ "distributive": {
696+ "type": "registry",
697+ "version": "6.0.0",
698+ "integrity": "sha256-netyJK4B32PDYHD0tF2AenKgo3urzbI770ycM0pArAo=",
699+ "dependencies": [
700+ "identity",
701+ "newtype",
702+ "prelude",
703+ "tuples",
704+ "type-equality"
705+ ]
706+ },
707+ "effect": {
708+ "type": "registry",
709+ "version": "4.0.0",
710+ "integrity": "sha256-DdqU3bncRTjgMVPPRlAuGHVeajSPEcRaRhNuio7if6s=",
711+ "dependencies": [
712+ "prelude"
713+ ]
714+ },
715+ "either": {
716+ "type": "registry",
717+ "version": "6.1.0",
718+ "integrity": "sha256-tBHx3PgtH4GZOApZCDkcM7szRTYJinrUrVGWazQCUUg=",
719+ "dependencies": [
720+ "control",
721+ "invariant",
722+ "maybe",
723+ "prelude"
724+ ]
725+ },
726+ "enums": {
727+ "type": "registry",
728+ "version": "6.0.1",
729+ "integrity": "sha256-sdZOmLX5+5pASGpvVyT0vSD5BBYQjZiayjV5XiPutso=",
730+ "dependencies": [
731+ "control",
732+ "either",
733+ "gen",
734+ "maybe",
735+ "newtype",
736+ "nonempty",
737+ "partial",
738+ "prelude",
739+ "tuples",
740+ "unfoldable"
741+ ]
742+ },
743+ "exceptions": {
744+ "type": "registry",
745+ "version": "6.0.0",
746+ "integrity": "sha256-z65UIKhOyVXzV8QUxtky4OiYVESNPegtuBFUdAddbTM=",
747+ "dependencies": [
748+ "effect",
749+ "either",
750+ "maybe",
751+ "prelude"
752+ ]
753+ },
754+ "exists": {
755+ "type": "registry",
756+ "version": "6.0.0",
757+ "integrity": "sha256-vtLbrWNaI+pzx/2fw2AQnCovoLtcosClIhjO26QKkh8=",
758+ "dependencies": [
759+ "unsafe-coerce"
760+ ]
761+ },
762+ "foldable-traversable": {
763+ "type": "registry",
764+ "version": "6.0.0",
765+ "integrity": "sha256-NGGWyCio/xjce6fPP7y3wwg7CheqllID6aJudDxbWhA=",
766+ "dependencies": [
767+ "bifunctors",
768+ "const",
769+ "control",
770+ "either",
771+ "functors",
772+ "identity",
773+ "maybe",
774+ "newtype",
775+ "orders",
776+ "prelude",
777+ "tuples"
778+ ]
779+ },
780+ "foreign": {
781+ "type": "registry",
782+ "version": "7.0.0",
783+ "integrity": "sha256-jiDRMVQfovGsbbA/FzbzYf6iWk9i2b5gNrIjz+gFfsI=",
784+ "dependencies": [
785+ "either",
786+ "functions",
787+ "integers",
788+ "lists",
789+ "maybe",
790+ "prelude",
791+ "strings",
792+ "transformers",
793+ "unsafe-coerce"
794+ ]
795+ },
796+ "foreign-object": {
797+ "type": "registry",
798+ "version": "4.1.0",
799+ "integrity": "sha256-x/Q7r80z/vmHRKvPwhYmHP/DnJciPtsyGfRTMzayKIU=",
800+ "dependencies": [
801+ "arrays",
802+ "foldable-traversable",
803+ "functions",
804+ "gen",
805+ "lists",
806+ "maybe",
807+ "prelude",
808+ "st",
809+ "tailrec",
810+ "tuples",
811+ "typelevel-prelude",
812+ "unfoldable",
813+ "unsafe-coerce"
814+ ]
815+ },
816+ "free": {
817+ "type": "registry",
818+ "version": "7.1.0",
819+ "integrity": "sha256-tUBInfUpRQEw4u94GWcYW7EYdmSdDHSRn88a+C0eltU=",
820+ "dependencies": [
821+ "catenable-lists",
822+ "control",
823+ "distributive",
824+ "either",
825+ "exists",
826+ "foldable-traversable",
827+ "invariant",
828+ "lazy",
829+ "maybe",
830+ "prelude",
831+ "tailrec",
832+ "transformers",
833+ "tuples",
834+ "unsafe-coerce"
835+ ]
836+ },
837+ "functions": {
838+ "type": "registry",
839+ "version": "6.0.0",
840+ "integrity": "sha256-0jgtOr+SFO4UScKc40oA/9Vdcf2rLn3h3vZlY0KfqVo=",
841+ "dependencies": [
842+ "prelude"
843+ ]
844+ },
845+ "functors": {
846+ "type": "registry",
847+ "version": "5.0.0",
848+ "integrity": "sha256-W1o/4wcp6S3chodYL8uixlXK2mfr1L+SzM1R+whqUco=",
849+ "dependencies": [
850+ "bifunctors",
851+ "const",
852+ "contravariant",
853+ "control",
854+ "distributive",
855+ "either",
856+ "invariant",
857+ "maybe",
858+ "newtype",
859+ "prelude",
860+ "profunctor",
861+ "tuples",
862+ "unsafe-coerce"
863+ ]
864+ },
865+ "gen": {
866+ "type": "registry",
867+ "version": "4.0.0",
868+ "integrity": "sha256-kyOateeOtQa07vfTMYeSuCKdelgKE1R65fwKkZ10wsY=",
869+ "dependencies": [
870+ "either",
871+ "foldable-traversable",
872+ "identity",
873+ "maybe",
874+ "newtype",
875+ "nonempty",
876+ "prelude",
877+ "tailrec",
878+ "tuples",
879+ "unfoldable"
880+ ]
881+ },
882+ "identity": {
883+ "type": "registry",
884+ "version": "6.0.0",
885+ "integrity": "sha256-RY/iXPpxpqvKHYfJeFVNI1J06kohB9rbtWYuha8t0LE=",
886+ "dependencies": [
887+ "control",
888+ "invariant",
889+ "newtype",
890+ "prelude"
891+ ]
892+ },
893+ "integers": {
894+ "type": "registry",
895+ "version": "6.0.0",
896+ "integrity": "sha256-Y8yozC1uHRLcruobEyv5/bMKzE1BoKJ/olHv6o+bgm8=",
897+ "dependencies": [
898+ "maybe",
899+ "numbers",
900+ "prelude"
901+ ]
902+ },
903+ "invariant": {
904+ "type": "registry",
905+ "version": "6.0.0",
906+ "integrity": "sha256-HyoT1I5qIHRq8RSZji6zZDvqnpVjLw748utrKwZtqsw=",
907+ "dependencies": [
908+ "control",
909+ "prelude"
910+ ]
911+ },
912+ "js-bigints": {
913+ "type": "registry",
914+ "version": "2.2.1",
915+ "integrity": "sha256-r1rE6Olv5b3JrP32lY2uKJ+eBRTsrVnC983K8VWwqe4=",
916+ "dependencies": [
917+ "integers",
918+ "maybe",
919+ "prelude"
920+ ]
921+ },
922+ "js-date": {
923+ "type": "registry",
924+ "version": "8.0.0",
925+ "integrity": "sha256-DNQrp4xYc8o80jtxo0aOkUeXGi6h0Xidkk2ZQ8Y4l5Y=",
926+ "dependencies": [
927+ "datetime",
928+ "effect",
929+ "enums",
930+ "foreign",
931+ "functions",
932+ "integers",
933+ "maybe",
934+ "prelude"
935+ ]
936+ },
937+ "lazy": {
938+ "type": "registry",
939+ "version": "6.0.0",
940+ "integrity": "sha256-MHRC+1Pc+AjortBp4b/e1e6KpEpsDwaJBNBB7TOL+1I=",
941+ "dependencies": [
942+ "control",
943+ "foldable-traversable",
944+ "invariant",
945+ "prelude"
946+ ]
947+ },
948+ "lists": {
949+ "type": "registry",
950+ "version": "7.0.0",
951+ "integrity": "sha256-/mGd7wLoU+wsTcqii7SSUByxsvwvjnu2PjwAD47yYb4=",
952+ "dependencies": [
953+ "bifunctors",
954+ "control",
955+ "foldable-traversable",
956+ "lazy",
957+ "maybe",
958+ "newtype",
959+ "nonempty",
960+ "partial",
961+ "prelude",
962+ "tailrec",
963+ "tuples",
964+ "unfoldable"
965+ ]
966+ },
967+ "maybe": {
968+ "type": "registry",
969+ "version": "6.0.0",
970+ "integrity": "sha256-8X6XEgtZ5lqfkEuc1hWOofnL6mwhk8gSJTxwvbR/SbY=",
971+ "dependencies": [
972+ "control",
973+ "invariant",
974+ "newtype",
975+ "prelude"
976+ ]
977+ },
978+ "newtype": {
979+ "type": "registry",
980+ "version": "5.0.0",
981+ "integrity": "sha256-dt6w0cty4OS+Pjt9fsh3iU6ottqSU83mdAZkaEEvo6c=",
982+ "dependencies": [
983+ "prelude",
984+ "safe-coerce"
985+ ]
986+ },
987+ "nonempty": {
988+ "type": "registry",
989+ "version": "7.0.0",
990+ "integrity": "sha256-Ctkq8/+KiGqCE53Y/LvibA/coy7ev9HtRZU+GYS7yfQ=",
991+ "dependencies": [
992+ "control",
993+ "foldable-traversable",
994+ "maybe",
995+ "prelude",
996+ "tuples",
997+ "unfoldable"
998+ ]
999+ },
1000+ "nullable": {
1001+ "type": "registry",
1002+ "version": "6.0.0",
1003+ "integrity": "sha256-CDAZk+L9Sc0GCFTkE8n+qdp4Ys8avvnkU+m2hVALt7M=",
1004+ "dependencies": [
1005+ "functions",
1006+ "maybe",
1007+ "prelude"
1008+ ]
1009+ },
1010+ "numbers": {
1011+ "type": "registry",
1012+ "version": "9.0.1",
1013+ "integrity": "sha256-ZydYckgwRAnRaFC7pu6fADhzdwX+UqHZrfJyet64zls=",
1014+ "dependencies": [
1015+ "functions",
1016+ "maybe",
1017+ "prelude"
1018+ ]
1019+ },
1020+ "ordered-collections": {
1021+ "type": "registry",
1022+ "version": "3.2.0",
1023+ "integrity": "sha256-NaEXc2cz/7lJfxPWEja4XlZbGxuRqJwFKQJjsJf51kQ=",
1024+ "dependencies": [
1025+ "arrays",
1026+ "control",
1027+ "foldable-traversable",
1028+ "functions",
1029+ "gen",
1030+ "lists",
1031+ "maybe",
1032+ "newtype",
1033+ "partial",
1034+ "prelude",
1035+ "safe-coerce",
1036+ "tailrec",
1037+ "tuples",
1038+ "unfoldable"
1039+ ]
1040+ },
1041+ "orders": {
1042+ "type": "registry",
1043+ "version": "6.0.0",
1044+ "integrity": "sha256-nSMxZK7wxyDULkjZqvMyB71mVo6b0AyLLHosDVbv1XM=",
1045+ "dependencies": [
1046+ "newtype",
1047+ "prelude"
1048+ ]
1049+ },
1050+ "parallel": {
1051+ "type": "registry",
1052+ "version": "7.0.0",
1053+ "integrity": "sha256-bAGpVhrFz2IyHkA4MR7+tE/19FZ7dzYBdYPgK5svkqw=",
1054+ "dependencies": [
1055+ "control",
1056+ "effect",
1057+ "either",
1058+ "foldable-traversable",
1059+ "functors",
1060+ "maybe",
1061+ "newtype",
1062+ "prelude",
1063+ "profunctor",
1064+ "refs",
1065+ "transformers"
1066+ ]
1067+ },
1068+ "partial": {
1069+ "type": "registry",
1070+ "version": "4.0.0",
1071+ "integrity": "sha256-mdrlJBAgFMh79hNRW39uv7c+BwnnaOrAMmdh7+VhEhs=",
1072+ "dependencies": []
1073+ },
1074+ "prelude": {
1075+ "type": "registry",
1076+ "version": "6.0.1",
1077+ "integrity": "sha256-qicw3g9AkjIxyVp73o1dF9BiTCMWouPk9rYe6AASBCU=",
1078+ "dependencies": []
1079+ },
1080+ "profunctor": {
1081+ "type": "registry",
1082+ "version": "6.0.1",
1083+ "integrity": "sha256-Ow0mJC+PIIRq/a3mzCWY5vxuTgR/e+Ee68+mWeSRnWY=",
1084+ "dependencies": [
1085+ "control",
1086+ "distributive",
1087+ "either",
1088+ "exists",
1089+ "invariant",
1090+ "newtype",
1091+ "prelude",
1092+ "tuples"
1093+ ]
1094+ },
1095+ "record": {
1096+ "type": "registry",
1097+ "version": "4.0.0",
1098+ "integrity": "sha256-WQ8fqp4X1wTA70J4qUcPW4Z7DCwx6e5KJnT/EuTdWSc=",
1099+ "dependencies": [
1100+ "functions",
1101+ "prelude",
1102+ "unsafe-coerce"
1103+ ]
1104+ },
1105+ "refs": {
1106+ "type": "registry",
1107+ "version": "6.0.0",
1108+ "integrity": "sha256-KKD5G9dR2SauuDGULUJsxjqDOEi1Jqm+2Sq5U2mY8YU=",
1109+ "dependencies": [
1110+ "effect",
1111+ "prelude"
1112+ ]
1113+ },
1114+ "safe-coerce": {
1115+ "type": "registry",
1116+ "version": "2.0.0",
1117+ "integrity": "sha256-EJrxtKt5xC7TYjBZrSBO/J7Aw3DmtilYjt4olpAXWVc=",
1118+ "dependencies": [
1119+ "unsafe-coerce"
1120+ ]
1121+ },
1122+ "st": {
1123+ "type": "registry",
1124+ "version": "6.2.0",
1125+ "integrity": "sha256-WI4MEkkwUd4pnwZQ3G8VKWgX5t1RSNJludm5e9boaRo=",
1126+ "dependencies": [
1127+ "effect",
1128+ "partial",
1129+ "prelude",
1130+ "tailrec",
1131+ "unsafe-coerce"
1132+ ]
1133+ },
1134+ "strings": {
1135+ "type": "registry",
1136+ "version": "6.0.1",
1137+ "integrity": "sha256-FNto2hoNW5Da6W6crIk77YXMagBvOGHZE1kO7eZ1vLM=",
1138+ "dependencies": [
1139+ "arrays",
1140+ "control",
1141+ "either",
1142+ "enums",
1143+ "foldable-traversable",
1144+ "gen",
1145+ "integers",
1146+ "maybe",
1147+ "newtype",
1148+ "nonempty",
1149+ "partial",
1150+ "prelude",
1151+ "tailrec",
1152+ "tuples",
1153+ "unfoldable",
1154+ "unsafe-coerce"
1155+ ]
1156+ },
1157+ "tailrec": {
1158+ "type": "registry",
1159+ "version": "6.1.0",
1160+ "integrity": "sha256-xrorCfP0xV1wubs27idQCHQqdvSUuqmmL/am7Ji1wVU=",
1161+ "dependencies": [
1162+ "bifunctors",
1163+ "effect",
1164+ "either",
1165+ "identity",
1166+ "maybe",
1167+ "partial",
1168+ "prelude",
1169+ "refs"
1170+ ]
1171+ },
1172+ "transformers": {
1173+ "type": "registry",
1174+ "version": "6.0.0",
1175+ "integrity": "sha256-Nl3yXAIeeTzP6MyrkazkK7pbgeVqUm+ynLv7U5cpJYQ=",
1176+ "dependencies": [
1177+ "control",
1178+ "distributive",
1179+ "effect",
1180+ "either",
1181+ "exceptions",
1182+ "foldable-traversable",
1183+ "identity",
1184+ "lazy",
1185+ "maybe",
1186+ "newtype",
1187+ "prelude",
1188+ "tailrec",
1189+ "tuples",
1190+ "unfoldable"
1191+ ]
1192+ },
1193+ "tuples": {
1194+ "type": "registry",
1195+ "version": "7.0.0",
1196+ "integrity": "sha256-BKy7EfK8S1KeTCwyWqyW1wwcpSAQuh3IsczSVgS6fzY=",
1197+ "dependencies": [
1198+ "control",
1199+ "invariant",
1200+ "prelude"
1201+ ]
1202+ },
1203+ "type-equality": {
1204+ "type": "registry",
1205+ "version": "4.0.1",
1206+ "integrity": "sha256-BBqYOSnAKmayRvR5mtZoCf4SlYs2ipatD/+5nQx73aw=",
1207+ "dependencies": []
1208+ },
1209+ "typelevel-prelude": {
1210+ "type": "registry",
1211+ "version": "7.0.0",
1212+ "integrity": "sha256-+pHi14/40mTj/+lmCWqL0T7iXIXD1+NSg/KItyyoB0A=",
1213+ "dependencies": [
1214+ "prelude",
1215+ "type-equality"
1216+ ]
1217+ },
1218+ "unfoldable": {
1219+ "type": "registry",
1220+ "version": "6.0.0",
1221+ "integrity": "sha256-BK/6By1sKp/ztk+9CF3q43SV0QDTLVdsGqFerKHSmbg=",
1222+ "dependencies": [
1223+ "foldable-traversable",
1224+ "maybe",
1225+ "partial",
1226+ "prelude",
1227+ "tuples"
1228+ ]
1229+ },
1230+ "unsafe-coerce": {
1231+ "type": "registry",
1232+ "version": "6.0.0",
1233+ "integrity": "sha256-0L1QsaY20OILjfU5TV72d3U/tSjhmL9hJ32WMp857Lk=",
1234+ "dependencies": []
1235+ },
1236+ "variant": {
1237+ "type": "registry",
1238+ "version": "8.0.0",
1239+ "integrity": "sha256-Ydrgl8liWEwYQOWW76PdZtg3vxKpoKgsnmFyTLgZkoU=",
1240+ "dependencies": [
1241+ "control",
1242+ "enums",
1243+ "foldable-traversable",
1244+ "lists",
1245+ "maybe",
1246+ "partial",
1247+ "prelude",
1248+ "type-equality",
1249+ "unsafe-coerce"
1250+ ]
1251+ },
1252+ "yoga-json": {
1253+ "type": "local",
1254+ "path": "../purescript-yoga-json",
1255+ "dependencies": [
1256+ "arrays",
1257+ "bifunctors",
1258+ "control",
1259+ "datetime",
1260+ "effect",
1261+ "either",
1262+ "exceptions",
1263+ "foldable-traversable",
1264+ "foreign",
1265+ "foreign-object",
1266+ "free",
1267+ "identity",
1268+ "integers",
1269+ "js-bigints",
1270+ "js-date",
1271+ "lists",
1272+ "maybe",
1273+ "newtype",
1274+ "nullable",
1275+ "numbers",
1276+ "ordered-collections",
1277+ "partial",
1278+ "prelude",
1279+ "record",
1280+ "safe-coerce",
1281+ "strings",
1282+ "transformers",
1283+ "tuples",
1284+ "typelevel-prelude",
1285+ "unsafe-coerce",
1286+ "variant",
1287+ "yoga-tree"
1288+ ]
1289+ },
1290+ "yoga-tree": {
1291+ "type": "registry",
1292+ "version": "1.0.0",
1293+ "integrity": "sha256-s8rIVEQkRi1VrMc92T9ceXcY0X6r5A4QqT6f7GgmDQI=",
1294+ "dependencies": [
1295+ "arrays",
1296+ "control",
1297+ "foldable-traversable",
1298+ "free",
1299+ "maybe",
1300+ "prelude",
1301+ "tailrec"
1302+ ]
1303+ }
1304+ }
1305+}
spago.yamlnew file+34
@@ -0,0 +1,34 @@
1+workspace:
2+ packageSet:
3+ registry: 51.1.0
4+ extraPackages:
5+ yoga-json:
6+ path: ../purescript-yoga-json
7+
8+package:
9+ name: yoga-http-api
10+ publish:
11+ version: 0.1.0
12+ license: MIT
13+ location:
14+ githubOwner: rowtype-yoga
15+ githubRepo: purescript-yoga-http-api
16+ dependencies:
17+ - aff: ">=7.0.0 <8.0.0"
18+ - arrays: ">=7.0.0 <8.0.0"
19+ - effect: ">=4.0.0 <5.0.0"
20+ - either: ">=6.0.0 <7.0.0"
21+ - foldable-traversable: ">=6.0.0 <7.0.0"
22+ - foreign: ">=7.0.0 <8.0.0"
23+ - foreign-object: ">=4.0.0 <5.0.0"
24+ - integers: ">=6.0.0 <7.0.0"
25+ - maybe: ">=6.0.0 <7.0.0"
26+ - newtype: ">=5.0.0 <6.0.0"
27+ - prelude: ">=6.0.0 <7.0.0"
28+ - record: ">=4.0.0 <5.0.0"
29+ - strings: ">=6.0.0 <7.0.0"
30+ - tuples: ">=7.0.0 <8.0.0"
31+ - typelevel-prelude: ">=7.0.0 <8.0.0"
32+ - unsafe-coerce: ">=6.0.0 <7.0.0"
33+ - variant: ">=8.0.0 <9.0.0"
34+ - yoga-json: ">=5.0.0 <6.0.0"
src/Yoga/HTTP/API/Path.pursnew file+326
@@ -0,0 +1,326 @@
1+module Yoga.HTTP.API.Path
2+ ( Path
3+ , Root
4+ , Lit
5+ , Capture
6+ , PathCons
7+ , type (/)
8+ , Param
9+ , type (:>)
10+ , type (:)
11+ , QueryParams
12+ , type (:?)
13+ , Required
14+ , class PathPattern
15+ , pathPattern
16+ , class PathPatternSegs
17+ , pathPatternSegs
18+ -- , class PathParams -- TODO: Export when instances are implemented
19+ , class ParseParam
20+ , parseParam
21+ , class ParsePath
22+ , parsePath
23+ ) where
24+
25+import Prelude
26+
27+import Data.Array (intercalate, uncons)
28+import Data.Array as Array
29+import Data.Either (Either(..))
30+import Data.Int as Int
31+import Data.Maybe (Maybe(..))
32+import Data.String (Pattern(..), split)
33+import Data.Symbol (class IsSymbol, reflectSymbol)
34+import Prim.Row as Row
35+import Record as Record
36+import Type.Proxy (Proxy(..))
37+import Unsafe.Coerce (unsafeCoerce)
38+
39+--------------------------------------------------------------------------------
40+-- Type-Level Path DSL
41+--------------------------------------------------------------------------------
42+
43+-- | Type-level path representation
44+-- |
45+-- | Examples:
46+-- | Path Root -- /
47+-- | Path (Lit "users") -- /users
48+-- | Path (Lit "users" / Capture "id" Int) -- /users/:id
49+-- | Path (Lit "users" / Capture "id" Int / Lit "posts") -- /users/:id/posts
50+data Path :: forall k. k -> Type
51+data Path segments
52+
53+-- | Root path representing "/"
54+-- |
55+-- | Example:
56+-- | Path Root -- /
57+data Root
58+
59+-- | Literal path segment (wrapper for Symbol to work around PureScript 0.15 limitations)
60+-- |
61+-- | In PureScript 0.15, we can't use bare Symbols like Path "users" due to kind inference.
62+-- | Instead, use: Path (Lit "users")
63+-- |
64+-- | Example:
65+-- | Path (Lit "users") -- /users
66+-- | Path (Lit "users" / Capture "id" Int) -- /users/:id
67+data Lit (segment :: Symbol)
68+
69+-- | Capture a path parameter with a name and type
70+-- |
71+-- | Example:
72+-- | Capture "id" Int -- captures :id as an Int
73+-- | Capture "name" String -- captures :name as a String
74+data Capture (name :: Symbol) (ty :: Type)
75+
76+-- | Infix operator for building paths
77+-- |
78+-- | Example:
79+-- | Lit "users" / Capture "id" Int / Lit "posts"
80+infixr 6 type PathCons as /
81+
82+data PathCons :: forall k1 k2. k1 -> k2 -> Type
83+data PathCons left right
84+
85+-- | Sugar for Capture — use with bare Symbols in the path DSL
86+-- |
87+-- | Example:
88+-- | Path ("users" / "id" :> Int / "posts")
89+-- | -- equivalent to: Path (Lit "users" / Capture "id" Int / Lit "posts")
90+data Param (name :: Symbol) (ty :: Type)
91+
92+infixr 8 type Param as :>
93+infixr 8 type Param as :
94+
95+-- | Attach query parameters to a path
96+-- |
97+-- | Example:
98+-- | Path ("users" / "id" :> Int) :? { limit :: Int, offset :: Required Int }
99+data QueryParams :: forall k. k -> Type -> Type
100+data QueryParams path params
101+
102+infixl 1 type QueryParams as :?
103+
104+-- | Marker for required query parameters (default is optional → Maybe)
105+-- |
106+-- | Example:
107+-- | Path ("users") :? (limit :: Int, offset :: Required Int)
108+-- | -- limit parses as Maybe Int, offset parses as Int (fails if missing)
109+data Required :: Type -> Type
110+data Required a
111+
112+--------------------------------------------------------------------------------
113+-- PathPattern: Generate URL patterns for Fastify
114+--------------------------------------------------------------------------------
115+
116+-- | Generate a Fastify-compatible URL pattern from a path type
117+-- |
118+-- | Examples:
119+-- | pathPattern (Proxy :: _ (Path Root)) = "/"
120+-- | pathPattern (Proxy :: _ (Path (Lit "users"))) = "/users"
121+-- | pathPattern (Proxy :: _ (Path (Lit "users" / Capture "id" Int))) = "/users/:id"
122+-- | pathPattern (Proxy :: _ ("users" / "id" : Int)) = "/users/:id"
123+class PathPattern :: forall k. k -> Constraint
124+class PathPattern path where
125+ pathPattern :: Proxy path -> String
126+
127+-- QueryParams wrapper delegates to inner path
128+instance pathPatternQueryParams ::
129+ ( PathPattern path
130+ ) =>
131+ PathPattern (QueryParams path params) where
132+ pathPattern _ = pathPattern (Proxy :: Proxy path)
133+
134+-- Path delegates to PathPatternSegs for the inner segments
135+else instance pathPatternPath ::
136+ PathPatternSegs segs =>
137+ PathPattern (Path segs) where
138+ pathPattern _ = pathPatternSegs (Proxy :: Proxy segs)
139+
140+-- Bare segments (no Path wrapper) delegate to PathPatternSegs
141+else instance pathPatternBare ::
142+ PathPatternSegs segs =>
143+ PathPattern segs where
144+ pathPattern _ = pathPatternSegs (Proxy :: Proxy segs)
145+
146+-- | Internal poly-kinded class for generating URL patterns from path segments.
147+class PathPatternSegs :: forall k. k -> Constraint
148+class PathPatternSegs segs where
149+ pathPatternSegs :: Proxy segs -> String
150+
151+-- Root
152+instance pathPatternSegsRoot :: PathPatternSegs Root where
153+ pathPatternSegs _ = "/"
154+
155+-- "/" symbol (alias for Root)
156+else instance pathPatternSegsSlash :: PathPatternSegs "/" where
157+ pathPatternSegs _ = "/"
158+
159+-- Lit wrapper
160+else instance pathPatternSegsLit :: IsSymbol s => PathPatternSegs (Lit s) where
161+ pathPatternSegs _ = "/" <> reflectSymbol (Proxy :: Proxy s)
162+
163+-- Capture
164+else instance pathPatternSegsCapture :: IsSymbol name => PathPatternSegs (Capture name ty) where
165+ pathPatternSegs _ = "/:" <> reflectSymbol (Proxy :: Proxy name)
166+
167+-- Param sugar
168+else instance pathPatternSegsParam :: IsSymbol name => PathPatternSegs (Param name ty) where
169+ pathPatternSegs _ = "/:" <> reflectSymbol (Proxy :: Proxy name)
170+
171+-- PathCons with Lit on left
172+else instance pathPatternSegsLitCons ::
173+ ( IsSymbol s
174+ , PathPatternSegs rest
175+ ) =>
176+ PathPatternSegs (PathCons (Lit s) rest) where
177+ pathPatternSegs _ = "/" <> reflectSymbol (Proxy :: Proxy s) <> pathPatternSegs (Proxy :: Proxy rest)
178+
179+-- PathCons with Capture on left
180+else instance pathPatternSegsCaptureCons ::
181+ ( IsSymbol name
182+ , PathPatternSegs rest
183+ ) =>
184+ PathPatternSegs (PathCons (Capture name ty) rest) where
185+ pathPatternSegs _ = "/:" <> reflectSymbol (Proxy :: Proxy name) <> pathPatternSegs (Proxy :: Proxy rest)
186+
187+-- PathCons with Param on left
188+else instance pathPatternSegsParamCons ::
189+ ( IsSymbol name
190+ , PathPatternSegs rest
191+ ) =>
192+ PathPatternSegs (PathCons (Param name ty) rest) where
193+ pathPatternSegs _ = "/:" <> reflectSymbol (Proxy :: Proxy name) <> pathPatternSegs (Proxy :: Proxy rest)
194+
195+-- PathCons with bare Symbol on left (from :> sugar)
196+else instance pathPatternSegsSymbolCons ::
197+ ( IsSymbol s
198+ , PathPatternSegs rest
199+ ) =>
200+ PathPatternSegs (PathCons s rest) where
201+ pathPatternSegs _ = "/" <> reflectSymbol (Proxy :: Proxy s) <> pathPatternSegs (Proxy :: Proxy rest)
202+
203+-- Path wrapper (unwrap and delegate)
204+else instance pathPatternSegsPath :: PathPatternSegs segs => PathPatternSegs (Path segs) where
205+ pathPatternSegs _ = pathPatternSegs (Proxy :: Proxy segs)
206+
207+-- Bare Symbol (single segment, from :> sugar)
208+else instance pathPatternSegsSymbol :: IsSymbol s => PathPatternSegs s where
209+ pathPatternSegs _ = "/" <> reflectSymbol (Proxy :: Proxy s)
210+
211+--------------------------------------------------------------------------------
212+-- PathParams: Extract capture types into a record row
213+--------------------------------------------------------------------------------
214+
215+-- | Extract path parameter names and types into a record row type
216+-- |
217+-- | Examples:
218+-- | PathParams (Path @"users") ()
219+-- | PathParams (Path (@"users" / Capture "id" Int)) (id :: Int)
220+-- | PathParams (Path (@"users" / Capture "userId" Int / @"posts" / Capture "postId" Int))
221+-- | (userId :: Int, postId :: Int)
222+class PathParams (path :: Type) (params :: Row Type) | path -> params
223+
224+-- TODO: Add PathParams instances when needed
225+
226+--------------------------------------------------------------------------------
227+-- ParsePath: Parse URL string into typed path parameters
228+--------------------------------------------------------------------------------
229+
230+-- | Parse a value from a String (used for path captures)
231+class ParseParam (ty :: Type) where
232+ parseParam :: String -> Either String ty
233+
234+instance parseParamString :: ParseParam String where
235+ parseParam = Right
236+
237+instance parseParamInt :: ParseParam Int where
238+ parseParam s = case Int.fromString s of
239+ Nothing -> Left $ "Expected an integer but got: " <> s
240+ Just n -> Right n
241+
242+instance parseParamNumber :: ParseParam Number where
243+ parseParam s = case Int.fromString s of
244+ Nothing -> Left $ "Expected a number but got: " <> s
245+ Just n -> Right (Int.toNumber n) -- Simple version, doesn't handle floats
246+
247+-- | Parse a URL string into a record of typed path parameters
248+-- |
249+-- | Examples:
250+-- | parsePath @(Path Root) "/" = Just {}
251+-- | parsePath @(Path (Lit "users")) "/users" = Just {}
252+-- | parsePath @(Path (Lit "users" / Capture "id" Int)) "/users/123" = Just { id: 123 }
253+class ParsePath (path :: Type) (params :: Row Type) | path -> params where
254+ parsePath :: Proxy path -> String -> Maybe (Record params)
255+
256+-- Base case: Root path - just verify it's "/"
257+instance parsePathRoot :: ParsePath (Path Root) () where
258+ parsePath _ url = if url == "/" then Just {} else Nothing
259+
260+-- Base case: Just a Lit segment, no captures - verify the path matches
261+instance parsePathLit :: IsSymbol s => ParsePath (Path (Lit s)) () where
262+ parsePath _ url =
263+ let
264+ expected = "/" <> reflectSymbol (Proxy :: Proxy s)
265+ in
266+ if url == expected then Just {} else Nothing
267+
268+-- Base case: Just a capture - parse single segment
269+instance parsePathCapture ::
270+ ( IsSymbol name
271+ , ParseParam ty
272+ ) =>
273+ ParsePath (Path (Capture name ty)) (name :: ty) where
274+ parsePath _ url = do
275+ -- Remove leading "/"
276+ let segments = Array.filter (_ /= "") $ split (Pattern "/") url
277+ case segments of
278+ [ segment ] -> case (parseParam segment :: Either String ty) of
279+ Left _ -> Nothing
280+ Right value -> pure $ unsafeCoerce { value } -- Simplified: we'd need proper record construction
281+ _ -> Nothing
282+
283+-- Recursive case: Lit segment followed by more
284+instance parsePathLitCons ::
285+ ( IsSymbol s
286+ , ParsePath (Path rest) params
287+ ) =>
288+ ParsePath (Path (Lit s / rest)) params where
289+ parsePath _ url = do
290+ -- Split URL into segments
291+ let segments = Array.filter (_ /= "") $ split (Pattern "/") url
292+ case uncons segments of
293+ Just { head, tail } -> do
294+ -- Check if first segment matches the literal
295+ let expected = reflectSymbol (Proxy :: Proxy s)
296+ if head == expected then parsePath (Proxy :: _ (Path rest)) ("/" <> intercalate "/" tail)
297+ else Nothing
298+ Nothing -> Nothing
299+
300+-- Recursive case: Capture followed by more
301+-- This is complex because we need to:
302+-- 1. Parse the captured value
303+-- 2. Parse the rest of the path
304+-- 3. Merge the results into a single record
305+instance parsePathCaptureCons ::
306+ ( IsSymbol name
307+ , ParseParam ty
308+ , ParsePath (Path rest) restParams
309+ , Row.Cons name ty restParams fullParams
310+ , Row.Lacks name restParams
311+ ) =>
312+ ParsePath (Path (Capture name ty / rest)) fullParams where
313+ parsePath _ url = do
314+ -- Split URL into segments
315+ let segments = Array.filter (_ /= "") $ split (Pattern "/") url
316+ case uncons segments of
317+ Just { head, tail } -> do
318+ -- Parse the captured segment
319+ value <- case parseParam head of
320+ Left _ -> Nothing
321+ Right v -> Just v
322+ -- Parse the rest
323+ restRecord <- parsePath (Proxy :: _ (Path rest)) ("/" <> intercalate "/" tail)
324+ -- Merge: add the captured value to the rest record
325+ pure $ Record.insert (Proxy :: _ name) value restRecord
326+ Nothing -> Nothing
src/Yoga/HTTP/API/Route/BearerToken.pursnew file+37
@@ -0,0 +1,37 @@
1+module Yoga.HTTP.API.Route.BearerToken
2+ ( BearerToken(..)
3+ ) where
4+
5+import Prelude
6+
7+import Data.Either (Either(..))
8+import Data.Newtype (class Newtype)
9+import Data.String as String
10+import Yoga.HTTP.API.Route.HeaderValue (class HeaderValue, class HeaderValueType)
11+
12+--------------------------------------------------------------------------------
13+-- BearerToken
14+--------------------------------------------------------------------------------
15+
16+-- | Bearer token newtype that validates the "Bearer " prefix
17+-- |
18+-- | Example:
19+-- | parseHeader "Bearer abc123" :: Either String BearerToken
20+-- | = Right (BearerToken "abc123")
21+-- | parseHeader "abc123" :: Either String BearerToken
22+-- | = Left "missing 'Bearer ' prefix"
23+newtype BearerToken = BearerToken String
24+
25+derive instance Eq BearerToken
26+derive newtype instance Show BearerToken
27+derive instance Newtype BearerToken _
28+
29+instance HeaderValue BearerToken where
30+ parseHeader s =
31+ if String.take 7 s == "Bearer " then Right $ BearerToken $ String.drop 7 s
32+ else Left "missing 'Bearer ' prefix"
33+
34+ printHeader (BearerToken token) = "Bearer " <> token
35+
36+instance HeaderValueType BearerToken where
37+ headerValueType _ = "string"
src/Yoga/HTTP/API/Route/Encoding.pursnew file+25
@@ -0,0 +1,25 @@
1+module Yoga.HTTP.API.Route.Encoding
2+ ( JSON
3+ , FormData
4+ , NoBody
5+ ) where
6+
7+-- | JSON-encoded request body
8+-- |
9+-- | Example:
10+-- | Request { body :: JSON User }
11+data JSON :: Type -> Type
12+data JSON a
13+
14+-- | Form data encoded request body (application/x-www-form-urlencoded or multipart/form-data)
15+-- |
16+-- | Example:
17+-- | Request { body :: FormData { username :: String, password :: String } }
18+data FormData :: Type -> Type
19+data FormData a
20+
21+-- | No request body (for GET, DELETE, etc.)
22+-- |
23+-- | Example:
24+-- | Request {} -- NoBody is the default when body is omitted
25+data NoBody
src/Yoga/HTTP/API/Route/Handler.pursnew file+226
@@ -0,0 +1,226 @@
1+module Yoga.HTTP.API.Route.Handler
2+ ( HandlerFn
3+ , Request(..)
4+ , class DefaultRequestFields
5+ , class DefaultRequestFieldsRL
6+ , class SegmentPathParams
7+ , class SegmentQueryParams
8+ , class SegmentQueryParamsRL
9+ , class EncodingBody
10+ , class CaptureParams
11+ , class RequestHeaders
12+ , class RequestBody
13+ ) where
14+
15+import Data.Maybe (Maybe)
16+import Data.Unit (Unit)
17+import Data.Variant (Variant)
18+import Effect.Aff (Aff)
19+import Prim.Row as Row
20+import Prim.RowList as RL
21+import Yoga.HTTP.API.Path (Path, PathCons, Capture, Param, QueryParams, Required)
22+import Yoga.HTTP.API.Route.Encoding (JSON, NoBody)
23+
24+--------------------------------------------------------------------------------
25+-- Request Data Type
26+--------------------------------------------------------------------------------
27+
28+-- | Request data combining headers and body
29+-- |
30+-- | Usage:
31+-- | Request { body :: JSON User } -- body only
32+-- | Request { headers :: { auth :: String } } -- headers only
33+-- | Request { body :: JSON User, headers :: { auth :: String } } -- both
34+-- | Request {} -- neither
35+data Request :: Type -> Type
36+data Request r = Request r
37+
38+--------------------------------------------------------------------------------
39+-- Handler Type
40+--------------------------------------------------------------------------------
41+
42+-- | Type-safe handler tied to a route's computed types.
43+-- |
44+-- | Usage:
45+-- | myHandler :: Handler (id :: Int) (limit :: Maybe Int) (authorization :: BearerToken) User
46+-- | (ok :: ResponseData () (Array Post), notFound :: ResponseData () ErrorMessage)
47+-- | myHandler { path, query, headers, body } = do
48+-- | -- path :: { id :: Int }
49+-- | -- query :: { limit :: Maybe Int }
50+-- | -- headers :: { authorization :: BearerToken }
51+-- | -- body :: User
52+-- | pure $ respondNoHeaders (Proxy :: _ "ok") []
53+type HandlerFn pathParams queryParams reqHeaders body respVariant =
54+ { path :: Record pathParams
55+ , query :: Record queryParams
56+ , headers :: Record reqHeaders
57+ , body :: body
58+ }
59+ -> Aff (Variant respVariant)
60+
61+--------------------------------------------------------------------------------
62+-- SegmentPathParams: Extract path capture row from segments
63+--------------------------------------------------------------------------------
64+
65+-- | Extract the row of typed path parameters from a path segments type.
66+class SegmentPathParams :: forall k. k -> Row Type -> Constraint
67+class SegmentPathParams segments params | segments -> params
68+
69+instance segmentPathParamsQueryParams ::
70+ CaptureParams path params =>
71+ SegmentPathParams (QueryParams path (Record q)) params
72+
73+else instance segmentPathParamsCatchAll ::
74+ CaptureParams segs params =>
75+ SegmentPathParams segs params
76+
77+--------------------------------------------------------------------------------
78+-- SegmentQueryParams: Extract query param row from segments
79+--------------------------------------------------------------------------------
80+
81+-- | Extract the row of typed query parameters from a path segments type.
82+class SegmentQueryParams :: forall k. k -> Row Type -> Constraint
83+class SegmentQueryParams segments query | segments -> query
84+
85+instance segmentQueryParamsQP ::
86+ ( RL.RowToList params rl
87+ , SegmentQueryParamsRL rl query
88+ ) =>
89+ SegmentQueryParams (QueryParams path (Record params)) query
90+
91+else instance segmentQueryParamsCatchAll :: SegmentQueryParams segs ()
92+
93+-- | RowList-based processing of query param rows.
94+-- | Required ty → ty (plain), otherwise → Maybe ty
95+class SegmentQueryParamsRL (rl :: RL.RowList Type) (query :: Row Type) | rl -> query
96+
97+instance segmentQueryParamsRLNil :: SegmentQueryParamsRL RL.Nil ()
98+
99+instance segmentQueryParamsRLConsRequired ::
100+ ( SegmentQueryParamsRL tail tailQuery
101+ , Row.Cons name ty tailQuery query
102+ , Row.Lacks name tailQuery
103+ ) =>
104+ SegmentQueryParamsRL (RL.Cons name (Required ty) tail) query
105+
106+else instance segmentQueryParamsRLConsOptional ::
107+ ( SegmentQueryParamsRL tail tailQuery
108+ , Row.Cons name (Maybe ty) tailQuery query
109+ , Row.Lacks name tailQuery
110+ ) =>
111+ SegmentQueryParamsRL (RL.Cons name ty tail) query
112+
113+--------------------------------------------------------------------------------
114+-- CaptureParams: Extract captures from path segments
115+--------------------------------------------------------------------------------
116+
117+-- | Walk the path segments and collect all Capture/Param entries into a row.
118+class CaptureParams :: forall k. k -> Row Type -> Constraint
119+class CaptureParams segs (params :: Row Type) | segs -> params
120+
121+-- PathCons: merge left and right (most specific, check first)
122+instance captureParamsCons ::
123+ ( CaptureParams left leftParams
124+ , CaptureParams right rightParams
125+ , Row.Union leftParams rightParams params
126+ , Row.Nub params params
127+ ) =>
128+ CaptureParams (PathCons left right) params
129+
130+-- Capture: one param
131+else instance captureParamsCapture ::
132+ ( Row.Cons name ty () params
133+ , Row.Lacks name ()
134+ ) =>
135+ CaptureParams (Capture name ty) params
136+
137+-- Param sugar: one param
138+else instance captureParamsParam ::
139+ ( Row.Cons name ty () params
140+ , Row.Lacks name ()
141+ ) =>
142+ CaptureParams (Param name ty) params
143+
144+-- Path wrapper: unwrap and delegate
145+else instance captureParamsPath :: CaptureParams segs params => CaptureParams (Path segs) params
146+
147+-- Catch-all for literals (Symbol, Lit, Root, etc.): no params
148+else instance captureParamsDefault :: CaptureParams s ()
149+
150+--------------------------------------------------------------------------------
151+-- EncodingBody: Unwrap encoding phantom type to the actual body type
152+--------------------------------------------------------------------------------
153+
154+-- | Map encoding phantom types to the runtime body type the handler receives.
155+class EncodingBody (encoding :: Type) (body :: Type) | encoding -> body
156+
157+instance encodingBodyJSON :: EncodingBody (JSON a) a
158+instance encodingBodyNoBody :: EncodingBody NoBody Unit
159+
160+--------------------------------------------------------------------------------
161+-- RequestHeaders: Extract headers row from a request type
162+--------------------------------------------------------------------------------
163+
164+-- | Extract the headers row from a Request type.
165+-- |
166+-- | The request is expected to have a `headers` field.
167+class RequestHeaders (request :: Type) (headers :: Row Type) | request -> headers
168+
169+instance requestHeadersRequest ::
170+ ( Row.Cons "headers" (Record headers) _rest requestRow
171+ ) =>
172+ RequestHeaders (Request (Record requestRow)) headers
173+
174+--------------------------------------------------------------------------------
175+-- RequestBody: Extract body encoding from a request type
176+--------------------------------------------------------------------------------
177+
178+-- | Extract the body encoding type from a Request type.
179+-- |
180+-- | The request is expected to have a `body` field.
181+class RequestBody (request :: Type) (encoding :: Type) | request -> encoding
182+
183+instance requestBodyRequest ::
184+ ( Row.Cons "body" encoding _rest requestRow
185+ ) =>
186+ RequestBody (Request (Record requestRow)) encoding
187+
188+--------------------------------------------------------------------------------
189+-- DefaultRequestFields: Compute defaults for missing request fields
190+--------------------------------------------------------------------------------
191+
192+-- | Compute defaults for missing request fields using RowList.
193+-- |
194+-- | When a Request omits `headers`, defaults to `()`.
195+-- | When a Request omits `body`, defaults to `NoBody`.
196+class DefaultRequestFields (partialRequest :: Row Type) (fullHeaders :: Row Type) (fullEncoding :: Type) | partialRequest -> fullHeaders fullEncoding
197+
198+instance defaultRequestFieldsImpl ::
199+ ( RL.RowToList partialRequest rl
200+ , DefaultRequestFieldsRL rl partialRequest fullHeaders fullEncoding
201+ ) =>
202+ DefaultRequestFields partialRequest fullHeaders fullEncoding
203+
204+-- | RowList-based implementation
205+class DefaultRequestFieldsRL (rl :: RL.RowList Type) (partialRequest :: Row Type) (fullHeaders :: Row Type) (fullEncoding :: Type) | rl partialRequest -> fullHeaders fullEncoding
206+
207+-- Empty list - no fields present, default both
208+instance defaultFieldsRLNil :: DefaultRequestFieldsRL RL.Nil partialRequest () NoBody
209+
210+-- headers present
211+instance defaultFieldsRLHeadersCons ::
212+ ( DefaultRequestFieldsRL tail partialRequest () encoding
213+ ) =>
214+ DefaultRequestFieldsRL (RL.Cons "headers" (Record headers) tail) partialRequest headers encoding
215+
216+-- body present
217+else instance defaultFieldsRLBodyCons ::
218+ ( DefaultRequestFieldsRL tail partialRequest headers NoBody
219+ ) =>
220+ DefaultRequestFieldsRL (RL.Cons "body" encoding tail) partialRequest headers encoding
221+
222+-- Other fields (catch-all for unrecognized fields)
223+else instance defaultFieldsRLOtherCons ::
224+ ( DefaultRequestFieldsRL tail partialRequest headers encoding
225+ ) =>
226+ DefaultRequestFieldsRL (RL.Cons otherLabel otherType tail) partialRequest headers encoding
src/Yoga/HTTP/API/Route/HeaderError.pursnew file+20
@@ -0,0 +1,20 @@
1+module Yoga.HTTP.API.Route.HeaderError
2+ ( HeaderError(..)
3+ ) where
4+
5+import Prelude
6+
7+--------------------------------------------------------------------------------
8+-- Header Errors
9+--------------------------------------------------------------------------------
10+
11+-- | Typed errors for header parsing
12+data HeaderError
13+ = MissingHeader String
14+ | InvalidHeaderValue String String -- header name, error message
15+
16+derive instance Eq HeaderError
17+
18+instance Show HeaderError where
19+ show (MissingHeader name) = "Missing required header: " <> name
20+ show (InvalidHeaderValue name msg) = "Invalid header '" <> name <> "': " <> msg
src/Yoga/HTTP/API/Route/HeaderValue.pursnew file+75
@@ -0,0 +1,75 @@
1+module Yoga.HTTP.API.Route.HeaderValue
2+ ( class HeaderValue
3+ , parseHeader
4+ , printHeader
5+ , class HeaderValueType
6+ , headerValueType
7+ ) where
8+
9+import Prelude
10+
11+import Data.Either (Either(..))
12+import Data.Int as Int
13+import Data.Maybe (Maybe(..))
14+import Type.Proxy (Proxy(..))
15+
16+--------------------------------------------------------------------------------
17+-- HeaderValue Typeclass
18+--------------------------------------------------------------------------------
19+
20+-- | Typeclass for values that can be parsed from and printed to HTTP header strings
21+-- |
22+-- | This mirrors the `ParseParam` pattern from path parsing and allows headers
23+-- | to be typed beyond just strings.
24+-- |
25+-- | Returns Either String for detailed error messages (the string is the reason, not the header name).
26+-- |
27+-- | Examples:
28+-- | parseHeader "42" :: Either String Int = Right 42
29+-- | parseHeader "bad" :: Either String Int = Left "not a valid integer"
30+-- | printHeader 42 = "42"
31+-- | parseHeader "hello" :: Either String String = Right "hello"
32+class HeaderValue a where
33+ parseHeader :: String -> Either String a
34+ printHeader :: a -> String
35+
36+-- | String headers are pass-through (identity)
37+instance HeaderValue String where
38+ parseHeader = Right
39+ printHeader = identity
40+
41+-- | Integer headers are parsed from strings
42+instance HeaderValue Int where
43+ parseHeader s = case Int.fromString s of
44+ Just i -> Right i
45+ Nothing -> Left $ "not a valid integer (got: " <> s <> ")"
46+ printHeader = show
47+
48+-- | Optional headers (Maybe a) where a has HeaderValue
49+-- | This instance allows headers to be optional
50+-- | Note: parseHeader always succeeds for Maybe types (returns Just or Nothing)
51+instance HeaderValue a => HeaderValue (Maybe a) where
52+ parseHeader s = Right
53+ ( case parseHeader s of
54+ Right val -> Just val
55+ Left _ -> Nothing
56+ )
57+ printHeader Nothing = ""
58+ printHeader (Just a) = printHeader a
59+
60+--------------------------------------------------------------------------------
61+-- HeaderValueType Typeclass (for OpenAPI)
62+--------------------------------------------------------------------------------
63+
64+-- | Get OpenAPI type string for a HeaderValue type
65+class HeaderValueType (ty :: Type) where
66+ headerValueType :: Proxy ty -> String
67+
68+instance HeaderValueType String where
69+ headerValueType _ = "string"
70+
71+else instance HeaderValueType Int where
72+ headerValueType _ = "integer"
73+
74+else instance HeaderValueType a => HeaderValueType (Maybe a) where
75+ headerValueType _ = headerValueType (Proxy :: Proxy a)
src/Yoga/HTTP/API/Route/Method.pursnew file+26
@@ -0,0 +1,26 @@
1+module Yoga.HTTP.API.Route.Method
2+ ( GET
3+ , POST
4+ , PUT
5+ , DELETE
6+ , PATCH
7+ ) where
8+
9+--------------------------------------------------------------------------------
10+-- HTTP Method Types
11+--------------------------------------------------------------------------------
12+
13+-- | HTTP GET method
14+data GET
15+
16+-- | HTTP POST method
17+data POST
18+
19+-- | HTTP PUT method
20+data PUT
21+
22+-- | HTTP DELETE method
23+data DELETE
24+
25+-- | HTTP PATCH method
26+data PATCH
src/Yoga/HTTP/API/Route/OpenAPI.jsnew file+9
@@ -0,0 +1,9 @@
1+export const unsafeParseJSON = (str) => JSON.parse(str);
2+
3+export const stripKeys = (keys) => (obj) => {
4+ const result = { ...obj };
5+ for (const key of keys) {
6+ delete result[key];
7+ }
8+ return result;
9+};
src/Yoga/HTTP/API/Route/OpenAPI.pursnew file+1005
@@ -0,0 +1,1005 @@
1+module Yoga.HTTP.API.Route.OpenAPI
2+ ( class RenderHeadersSchema
3+ , renderHeadersSchema
4+ , class RenderHeadersSchemaRL
5+ , renderHeadersSchemaRL
6+ , class RenderPathParamsSchema
7+ , renderPathParamsSchema
8+ , class RenderPathParamsSchemaRL
9+ , renderPathParamsSchemaRL
10+ , class RenderQueryParamsSchema
11+ , renderQueryParamsSchema
12+ , class RenderQueryParamsSchemaRL
13+ , renderQueryParamsSchemaRL
14+ , class RenderRequestBodySchema
15+ , renderRequestBodySchema
16+ , class RenderResponseHeadersSchema
17+ , renderResponseHeadersSchema
18+ , class RenderResponseHeadersSchemaRL
19+ , renderResponseHeadersSchemaRL
20+ , class RenderResponseSchema
21+ , renderResponseSchema
22+ , class RenderVariantResponseSchema
23+ , renderVariantResponseSchema
24+ , class RenderVariantResponseSchemaRL
25+ , renderVariantResponseSchemaRL
26+ , class RenderJSONSchema
27+ , renderJSONSchema
28+ , class RenderRecordSchemaRL
29+ , renderRecordSchemaRL
30+ , getRequiredFields
31+ , class DetectSecurity
32+ , detectSecurity
33+ , class DetectSecurityRL
34+ , detectSecurityRL
35+ , class ToOpenAPI
36+ , toOpenAPIImpl
37+ , toOpenAPI
38+ , class CollectOperations
39+ , collectOperations
40+ , OperationEntry
41+ , buildOpenAPISpec
42+ , buildOpenAPISpec'
43+ , ServerObject
44+ , OpenAPISpec
45+ , ResponseHeaderObject
46+ ) where
47+
48+import Prelude
49+
50+import Data.Foldable (foldl)
51+import Data.Maybe (Maybe(..), maybe)
52+import Data.String.Regex (replace)
53+import Data.String.Regex.Flags (global)
54+import Data.String.Regex.Unsafe (unsafeRegex)
55+import Data.Symbol (class IsSymbol, reflectSymbol)
56+import Data.Tuple (Tuple(..))
57+import Data.Tuple.Nested (type (/\), (/\))
58+import Foreign (Foreign)
59+import Foreign.Object as FObject
60+import Prim.Row as Row
61+import Prim.RowList (class RowToList, RowList, Cons, Nil)
62+import Prim.RowList as RL
63+import Type.Proxy (Proxy(..))
64+import Unsafe.Coerce (unsafeCoerce)
65+import Yoga.HTTP.API.Route.BearerToken (BearerToken)
66+import Yoga.HTTP.API.Route.Encoding (JSON, FormData, NoBody)
67+import Yoga.HTTP.API.Route.HeaderValue (class HeaderValueType, headerValueType)
68+import Yoga.HTTP.API.Route.OpenAPIMetadata (Description, Example, Format, Minimum, Maximum, Pattern, MinLength, MaxLength, Title, Nullable, Default, Deprecated, Enum, 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 HasOperationMetadata, operationMetadata)
69+import Yoga.HTTP.API.Route.Response (class ToResponse)
70+import Yoga.HTTP.API.Route.StatusCode (class StatusCodeMap, statusCodeFor, statusCodeToString)
71+import Yoga.JSON (class WriteForeign, writeImpl)
72+
73+--------------------------------------------------------------------------------
74+-- Helper Functions for Building OpenAPI Objects
75+--------------------------------------------------------------------------------
76+
77+-- | Build a schema object with all supported OpenAPI schema properties
78+buildSchema
79+ :: { type :: String
80+ , format :: Maybe String
81+ , example :: Maybe String
82+ , minimum :: Maybe Int
83+ , maximum :: Maybe Int
84+ , pattern :: Maybe String
85+ , minLength :: Maybe Int
86+ , maxLength :: Maybe Int
87+ , title :: Maybe String
88+ , nullable :: Boolean
89+ , default :: Maybe String
90+ , enum :: Maybe (Array String)
91+ }
92+ -> Foreign
93+buildSchema s =
94+ let
95+ ins :: forall a. String -> a -> FObject.Object Foreign -> FObject.Object Foreign
96+ ins key val obj = FObject.insert key (unsafeCoerce val) obj
97+
98+ insMaybe :: forall a. String -> Maybe a -> FObject.Object Foreign -> FObject.Object Foreign
99+ insMaybe key valMaybe obj = maybe obj (\v -> ins key v obj) valMaybe
100+ base = FObject.fromFoldable [ Tuple "type" (unsafeCoerce s.type) ]
101+ in
102+ unsafeCoerce $ base
103+ # insMaybe "format" s.format
104+ # insMaybe "example" s.example
105+ # insMaybe "minimum" s.minimum
106+ # insMaybe "maximum" s.maximum
107+ # insMaybe "pattern" s.pattern
108+ # insMaybe "minLength" s.minLength
109+ # insMaybe "maxLength" s.maxLength
110+ # insMaybe "title" s.title
111+ # (if s.nullable then ins "nullable" true else identity)
112+ # insMaybe "default" s.default
113+ # insMaybe "enum" s.enum
114+
115+-- | Build a parameter object with all metadata
116+buildParameter
117+ :: { name :: String
118+ , in :: String
119+ , required :: Boolean
120+ , schema :: Foreign
121+ , description :: Maybe String
122+ , deprecated :: Maybe Boolean
123+ }
124+ -> Foreign
125+buildParameter { name, in: inStr, required, schema, description: descMaybe, deprecated: deprecatedMaybe } =
126+ let
127+ base = FObject.fromFoldable
128+ [ Tuple "name" (unsafeCoerce name)
129+ , Tuple "in" (unsafeCoerce inStr)
130+ , Tuple "required" (unsafeCoerce required)
131+ , Tuple "schema" schema
132+ ]
133+ withDesc = maybe base (\d -> FObject.insert "description" (unsafeCoerce d) base) descMaybe
134+ withDeprecated = maybe withDesc (\dep -> FObject.insert "deprecated" (unsafeCoerce dep) withDesc) deprecatedMaybe
135+ in
136+ unsafeCoerce withDeprecated
137+
138+--------------------------------------------------------------------------------
139+-- OpenAPI Generation
140+--------------------------------------------------------------------------------
141+
142+-- | Render headers row as OpenAPI parameter array
143+class RenderHeadersSchema (headers :: Row Type) where
144+ renderHeadersSchema :: Proxy headers -> Array Foreign
145+
146+instance (RowToList headers rl, RenderHeadersSchemaRL rl headers) => RenderHeadersSchema headers where
147+ renderHeadersSchema _ = renderHeadersSchemaRL (Proxy :: Proxy rl)
148+
149+-- | Helper class using RowList
150+class RenderHeadersSchemaRL (rl :: RowList Type) (headers :: Row Type) | rl -> headers where
151+ renderHeadersSchemaRL :: Proxy rl -> Array Foreign
152+
153+instance RenderHeadersSchemaRL RL.Nil () where
154+ renderHeadersSchemaRL _ = []
155+
156+-- Required header (non-Maybe type)
157+instance
158+ ( IsSymbol name
159+ , HeaderValueType ty
160+ , HasDescription ty
161+ , HasExample ty
162+ , HasFormat ty
163+ , HasMinimum ty
164+ , HasMaximum ty
165+ , HasPattern ty
166+ , HasMinLength ty
167+ , HasMaxLength ty
168+ , HasTitle ty
169+ , HasNullable ty
170+ , HasDefault ty
171+ , HasDeprecated ty
172+ , HasEnum ty
173+ , RenderHeadersSchemaRL tail tailRow
174+ , Row.Cons name ty tailRow headers
175+ , Row.Lacks name tailRow
176+ ) =>
177+ RenderHeadersSchemaRL (RL.Cons name ty tail) headers where
178+ renderHeadersSchemaRL _ =
179+ let
180+ p = Proxy :: Proxy ty
181+ schema = buildSchema
182+ { type: headerValueType p
183+ , format: format p
184+ , example: example p
185+ , minimum: minimum p
186+ , maximum: maximum p
187+ , pattern: pattern p
188+ , minLength: minLength p
189+ , maxLength: maxLength p
190+ , title: title p
191+ , nullable: nullable p
192+ , default: default p
193+ , enum: enum p
194+ }
195+ param = buildParameter
196+ { name: reflectSymbol (Proxy :: Proxy name)
197+ , in: "header"
198+ , required: true
199+ , schema
200+ , description: description p
201+ , deprecated: if deprecated p then Just true else Nothing
202+ }
203+ rest = renderHeadersSchemaRL (Proxy :: Proxy tail)
204+ in
205+ [ param ] <> rest
206+
207+--------------------------------------------------------------------------------
208+-- Security Detection
209+--------------------------------------------------------------------------------
210+
211+-- | Detect security requirements in request headers
212+-- | Returns an array of security requirement objects (e.g., [{ bearerAuth: [] }])
213+class DetectSecurity (headers :: Row Type) where
214+ detectSecurity :: Proxy headers -> Array Foreign
215+
216+instance (RowToList headers rl, DetectSecurityRL rl) => DetectSecurity headers where
217+ detectSecurity _ = detectSecurityRL (Proxy :: Proxy rl)
218+
219+-- | Helper class using RowList to find security-related headers
220+class DetectSecurityRL (rl :: RowList Type) where
221+ detectSecurityRL :: Proxy rl -> Array Foreign
222+
223+-- Base case: no headers = no security
224+instance DetectSecurityRL RL.Nil where
225+ detectSecurityRL _ = []
226+
227+-- Case: BearerToken found (with any metadata wrappers)
228+instance DetectSecurityRL tail => DetectSecurityRL (RL.Cons name BearerToken tail) where
229+ detectSecurityRL _ =
230+ let
231+ bearerAuth = unsafeCoerce $ FObject.singleton "bearerAuth" ([] :: Array String)
232+ in
233+ [ bearerAuth ]
234+
235+-- Case: BearerToken wrapped in Description
236+else instance DetectSecurityRL tail => DetectSecurityRL (RL.Cons name (Description desc BearerToken) tail) where
237+ detectSecurityRL _ =
238+ let
239+ bearerAuth = unsafeCoerce $ FObject.singleton "bearerAuth" ([] :: Array String)
240+ in
241+ [ bearerAuth ]
242+
243+-- Case: BearerToken wrapped in Example
244+else instance DetectSecurityRL tail => DetectSecurityRL (RL.Cons name (Example ex BearerToken) tail) where
245+ detectSecurityRL _ =
246+ let
247+ bearerAuth = unsafeCoerce $ FObject.singleton "bearerAuth" ([] :: Array String)
248+ in
249+ [ bearerAuth ]
250+
251+-- Case: BearerToken wrapped in Deprecated
252+else instance DetectSecurityRL tail => DetectSecurityRL (RL.Cons name (Deprecated BearerToken) tail) where
253+ detectSecurityRL _ =
254+ let
255+ bearerAuth = unsafeCoerce $ FObject.singleton "bearerAuth" ([] :: Array String)
256+ in
257+ [ bearerAuth ]
258+
259+-- Case: Non-security header, recurse
260+else instance (DetectSecurityRL tail, HeaderValueType ty) => DetectSecurityRL (RL.Cons name ty tail) where
261+ detectSecurityRL _ = detectSecurityRL (Proxy :: Proxy tail)
262+
263+--------------------------------------------------------------------------------
264+-- Path Parameters Schema Generation
265+--------------------------------------------------------------------------------
266+
267+-- | Render path parameters row as OpenAPI parameter array
268+class RenderPathParamsSchema (params :: Row Type) where
269+ renderPathParamsSchema :: Proxy params -> Array Foreign
270+
271+instance (RowToList params rl, RenderPathParamsSchemaRL rl params) => RenderPathParamsSchema params where
272+ renderPathParamsSchema _ = renderPathParamsSchemaRL (Proxy :: Proxy rl)
273+
274+-- | Helper class using RowList
275+class RenderPathParamsSchemaRL (rl :: RowList Type) (params :: Row Type) | rl -> params where
276+ renderPathParamsSchemaRL :: Proxy rl -> Array Foreign
277+
278+instance RenderPathParamsSchemaRL RL.Nil () where
279+ renderPathParamsSchemaRL _ = []
280+
281+-- Path parameters are always required
282+instance
283+ ( IsSymbol name
284+ , HeaderValueType ty
285+ , HasDescription ty
286+ , HasExample ty
287+ , HasFormat ty
288+ , HasMinimum ty
289+ , HasMaximum ty
290+ , HasPattern ty
291+ , HasMinLength ty
292+ , HasMaxLength ty
293+ , HasTitle ty
294+ , HasNullable ty
295+ , HasDefault ty
296+ , HasDeprecated ty
297+ , HasEnum ty
298+ , RenderPathParamsSchemaRL tail tailRow
299+ , Row.Cons name ty tailRow params
300+ , Row.Lacks name tailRow
301+ ) =>
302+ RenderPathParamsSchemaRL (RL.Cons name ty tail) params where
303+ renderPathParamsSchemaRL _ =
304+ let
305+ p = Proxy :: Proxy ty
306+ schema = buildSchema
307+ { type: headerValueType p
308+ , format: format p
309+ , example: example p
310+ , minimum: minimum p
311+ , maximum: maximum p
312+ , pattern: pattern p
313+ , minLength: minLength p
314+ , maxLength: maxLength p
315+ , title: title p
316+ , nullable: nullable p
317+ , default: default p
318+ , enum: enum p
319+ }
320+ param = buildParameter
321+ { name: reflectSymbol (Proxy :: Proxy name)
322+ , in: "path"
323+ , required: true
324+ , schema
325+ , description: description p
326+ , deprecated: if deprecated p then Just true else Nothing
327+ }
328+ rest = renderPathParamsSchemaRL (Proxy :: Proxy tail)
329+ in
330+ [ param ] <> rest
331+
332+--------------------------------------------------------------------------------
333+-- Query Parameters Schema Generation
334+--------------------------------------------------------------------------------
335+
336+-- | Render query parameters row as OpenAPI parameter array
337+class RenderQueryParamsSchema (params :: Row Type) where
338+ renderQueryParamsSchema :: Proxy params -> Array Foreign
339+
340+instance (RowToList params rl, RenderQueryParamsSchemaRL rl params) => RenderQueryParamsSchema params where
341+ renderQueryParamsSchema _ = renderQueryParamsSchemaRL (Proxy :: Proxy rl)
342+
343+-- | Helper class using RowList
344+class RenderQueryParamsSchemaRL (rl :: RowList Type) (params :: Row Type) | rl -> params where
345+ renderQueryParamsSchemaRL :: Proxy rl -> Array Foreign
346+
347+instance RenderQueryParamsSchemaRL RL.Nil () where
348+ renderQueryParamsSchemaRL _ = []
349+
350+-- Query parameter instance
351+instance
352+ ( IsSymbol name
353+ , HeaderValueType ty
354+ , HasDescription ty
355+ , HasExample ty
356+ , HasFormat ty
357+ , HasMinimum ty
358+ , HasMaximum ty
359+ , HasPattern ty
360+ , HasMinLength ty
361+ , HasMaxLength ty
362+ , HasTitle ty
363+ , HasNullable ty
364+ , HasDefault ty
365+ , HasDeprecated ty
366+ , HasEnum ty
367+ , RenderQueryParamsSchemaRL tail tailRow
368+ , Row.Cons name ty tailRow params
369+ , Row.Lacks name tailRow
370+ ) =>
371+ RenderQueryParamsSchemaRL (RL.Cons name ty tail) params where
372+ renderQueryParamsSchemaRL _ =
373+ let
374+ p = Proxy :: Proxy ty
375+ schema = buildSchema
376+ { type: headerValueType p
377+ , format: format p
378+ , example: example p
379+ , minimum: minimum p
380+ , maximum: maximum p
381+ , pattern: pattern p
382+ , minLength: minLength p
383+ , maxLength: maxLength p
384+ , title: title p
385+ , nullable: nullable p
386+ , default: default p
387+ , enum: enum p
388+ }
389+ param = buildParameter
390+ { name: reflectSymbol (Proxy :: Proxy name)
391+ , in: "query"
392+ , required: false
393+ , schema
394+ , description: description p
395+ , deprecated: if deprecated p then Just true else Nothing
396+ }
397+ rest = renderQueryParamsSchemaRL (Proxy :: Proxy tail)
398+ in
399+ [ param ] <> rest
400+
401+--------------------------------------------------------------------------------
402+-- JSON Schema Generation (Type Introspection)
403+--------------------------------------------------------------------------------
404+
405+-- | Render a PureScript type as an OpenAPI JSON schema
406+class RenderJSONSchema ty where
407+ renderJSONSchema :: Proxy ty -> Foreign
408+
409+-- Primitive types
410+instance RenderJSONSchema String where
411+ renderJSONSchema _ = buildSchema
412+ { type: "string"
413+ , format: Nothing
414+ , example: Nothing
415+ , minimum: Nothing
416+ , maximum: Nothing
417+ , pattern: Nothing
418+ , minLength: Nothing
419+ , maxLength: Nothing
420+ , title: Nothing
421+ , nullable: false
422+ , default: Nothing
423+ , enum: Nothing
424+ }
425+
426+instance RenderJSONSchema Int where
427+ renderJSONSchema _ = buildSchema
428+ { type: "integer"
429+ , format: Nothing
430+ , example: Nothing
431+ , minimum: Nothing
432+ , maximum: Nothing
433+ , pattern: Nothing
434+ , minLength: Nothing
435+ , maxLength: Nothing
436+ , title: Nothing
437+ , nullable: false
438+ , default: Nothing
439+ , enum: Nothing
440+ }
441+
442+instance RenderJSONSchema Number where
443+ renderJSONSchema _ = buildSchema
444+ { type: "number"
445+ , format: Nothing
446+ , example: Nothing
447+ , minimum: Nothing
448+ , maximum: Nothing
449+ , pattern: Nothing
450+ , minLength: Nothing
451+ , maxLength: Nothing
452+ , title: Nothing
453+ , nullable: false
454+ , default: Nothing
455+ , enum: Nothing
456+ }
457+
458+instance RenderJSONSchema Boolean where
459+ renderJSONSchema _ = buildSchema
460+ { type: "boolean"
461+ , format: Nothing
462+ , example: Nothing
463+ , minimum: Nothing
464+ , maximum: Nothing
465+ , pattern: Nothing
466+ , minLength: Nothing
467+ , maxLength: Nothing
468+ , title: Nothing
469+ , nullable: false
470+ , default: Nothing
471+ , enum: Nothing
472+ }
473+
474+instance renderJSONSchemaUnit :: RenderJSONSchema Unit where
475+ renderJSONSchema _ = buildSchema
476+ { type: "null"
477+ , format: Nothing
478+ , example: Nothing
479+ , minimum: Nothing
480+ , maximum: Nothing
481+ , pattern: Nothing
482+ , minLength: Nothing
483+ , maxLength: Nothing
484+ , title: Nothing
485+ , nullable: false
486+ , default: Nothing
487+ , enum: Nothing
488+ }
489+
490+-- Array type
491+instance renderJSONSchemaArray :: RenderJSONSchema a => RenderJSONSchema (Array a) where
492+ renderJSONSchema _ =
493+ let
494+ itemSchema = renderJSONSchema (Proxy :: Proxy a)
495+ base = FObject.fromFoldable
496+ [ Tuple "type" (unsafeCoerce "array")
497+ , Tuple "items" itemSchema
498+ ]
499+ in
500+ unsafeCoerce base
501+
502+-- Maybe type (nullable)
503+instance renderJSONSchemaMaybe :: RenderJSONSchema a => RenderJSONSchema (Maybe a) where
504+ renderJSONSchema _ = renderJSONSchema (Proxy :: Proxy a)
505+ # \schema ->
506+ let
507+ obj = unsafeCoerce schema :: FObject.Object Foreign
508+ in
509+ unsafeCoerce $ FObject.insert "nullable" (unsafeCoerce true) obj
510+
511+-- Record type
512+instance renderJSONSchemaRecord :: (RowToList row rl, RenderRecordSchemaRL rl row) => RenderJSONSchema (Record row) where
513+ renderJSONSchema _ =
514+ let
515+ properties = renderRecordSchemaRL (Proxy :: Proxy rl)
516+ required = getRequiredFields (Proxy :: Proxy rl)
517+ base = FObject.fromFoldable
518+ [ Tuple "type" (unsafeCoerce "object")
519+ , Tuple "properties" (unsafeCoerce properties)
520+ ]
521+ withRequired =
522+ if required == [] then base
523+ else FObject.insert "required" (unsafeCoerce required) base
524+ in
525+ unsafeCoerce withRequired
526+
527+-- JSON encoding wrapper (unwrap and render inner type)
528+instance RenderJSONSchema a => RenderJSONSchema (JSON a) where
529+ renderJSONSchema _ = renderJSONSchema (Proxy :: Proxy a)
530+
531+-- FormData encoding wrapper (unwrap and render inner type)
532+instance RenderJSONSchema a => RenderJSONSchema (FormData a) where
533+ renderJSONSchema _ = renderJSONSchema (Proxy :: Proxy a)
534+
535+-- NoBody encoding (no schema)
536+instance RenderJSONSchema NoBody where
537+ renderJSONSchema _ = unsafeCoerce { type: "null" }
538+
539+-- Metadata wrapper instances: Each unwraps and enriches the inner type's schema with its metadata
540+instance (RenderJSONSchema a, IsSymbol desc) => RenderJSONSchema (Description desc a) where
541+ renderJSONSchema _ =
542+ let
543+ innerSchema = renderJSONSchema (Proxy :: Proxy a)
544+ innerObj = unsafeCoerce innerSchema :: FObject.Object Foreign
545+ descStr = reflectSymbol (Proxy :: Proxy desc)
546+ withDesc =
547+ if descStr == "" then innerObj
548+ else FObject.insert "description" (unsafeCoerce descStr) innerObj
549+ in
550+ unsafeCoerce withDesc
551+
552+instance (RenderJSONSchema a, IsSymbol ex) => RenderJSONSchema (Example ex a) where
553+ renderJSONSchema _ =
554+ let
555+ innerSchema = renderJSONSchema (Proxy :: Proxy a)
556+ innerObj = unsafeCoerce innerSchema :: FObject.Object Foreign
557+ exStr = reflectSymbol (Proxy :: Proxy ex)
558+ withEx =
559+ if exStr == "" then innerObj
560+ else FObject.insert "example" (unsafeCoerce exStr) innerObj
561+ in
562+ unsafeCoerce withEx
563+
564+instance (RenderJSONSchema a, IsSymbol fmt) => RenderJSONSchema (Format fmt a) where
565+ renderJSONSchema _ =
566+ let
567+ innerSchema = renderJSONSchema (Proxy :: Proxy a)
568+ innerObj = unsafeCoerce innerSchema :: FObject.Object Foreign
569+ fmtStr = reflectSymbol (Proxy :: Proxy fmt)
570+ withFmt =
571+ if fmtStr == "" then innerObj
572+ else FObject.insert "format" (unsafeCoerce fmtStr) innerObj
573+ in
574+ unsafeCoerce withFmt
575+
576+instance RenderJSONSchema a => RenderJSONSchema (Nullable a) where
577+ renderJSONSchema _ =
578+ let
579+ innerSchema = renderJSONSchema (Proxy :: Proxy a)
580+ innerObj = unsafeCoerce innerSchema :: FObject.Object Foreign
581+ withNullable = FObject.insert "nullable" (unsafeCoerce true) innerObj
582+ in
583+ unsafeCoerce withNullable
584+
585+instance RenderJSONSchema a => RenderJSONSchema (Deprecated a) where
586+ renderJSONSchema _ =
587+ let
588+ innerSchema = renderJSONSchema (Proxy :: Proxy a)
589+ innerObj = unsafeCoerce innerSchema :: FObject.Object Foreign
590+ withDeprecated = FObject.insert "deprecated" (unsafeCoerce true) innerObj
591+ in
592+ unsafeCoerce withDeprecated
593+
594+instance (RenderJSONSchema a, HasEnum (Enum a)) => RenderJSONSchema (Enum a) where
595+ renderJSONSchema _ =
596+ let
597+ innerSchema = renderJSONSchema (Proxy :: Proxy a)
598+ innerObj = unsafeCoerce innerSchema :: FObject.Object Foreign
599+ enumValues = enum (Proxy :: Proxy (Enum a))
600+ withEnum = case enumValues of
601+ Nothing -> innerObj
602+ Just vals -> FObject.insert "enum" (unsafeCoerce vals) innerObj
603+ in
604+ unsafeCoerce withEnum
605+
606+-- Other metadata wrappers (Minimum, Maximum, Pattern, MinLength, MaxLength, Title, Default)
607+-- are similar but require more complex constraints, so we'll skip them for now in body schemas
608+
609+-- | Helper class to render record fields as OpenAPI properties
610+class RenderRecordSchemaRL (rl :: RowList Type) (row :: Row Type) | rl -> row where
611+ renderRecordSchemaRL :: Proxy rl -> FObject.Object Foreign
612+ getRequiredFields :: Proxy rl -> Array String
613+
614+instance RenderRecordSchemaRL RL.Nil () where
615+ renderRecordSchemaRL _ = FObject.empty
616+ getRequiredFields _ = []
617+
618+-- Required field (non-Maybe)
619+instance
620+ ( IsSymbol name
621+ , RenderJSONSchema ty
622+ , RenderRecordSchemaRL tail tailRow
623+ , Row.Cons name ty tailRow row
624+ , Row.Lacks name tailRow
625+ ) =>
626+ RenderRecordSchemaRL (RL.Cons name ty tail) row where
627+ renderRecordSchemaRL _ =
628+ let
629+ fieldName = reflectSymbol (Proxy :: Proxy name)
630+ fieldSchema = renderJSONSchema (Proxy :: Proxy ty)
631+ rest = renderRecordSchemaRL (Proxy :: Proxy tail)
632+ in
633+ FObject.insert fieldName fieldSchema rest
634+ getRequiredFields _ =
635+ let
636+ fieldName = reflectSymbol (Proxy :: Proxy name)
637+ rest = getRequiredFields (Proxy :: Proxy tail)
638+ in
639+ [ fieldName ] <> rest
640+
641+--------------------------------------------------------------------------------
642+-- Request Body Schema Generation
643+--------------------------------------------------------------------------------
644+
645+-- | Render request body schema for OpenAPI requestBody section
646+-- | Returns Nothing for NoBody, Just requestBody object for JSON/FormData
647+class RenderRequestBodySchema (encoding :: Type) where
648+ renderRequestBodySchema :: Proxy encoding -> Maybe { required :: Boolean, content :: Foreign }
649+
650+-- NoBody: no request body
651+instance RenderRequestBodySchema NoBody where
652+ renderRequestBodySchema _ = Nothing
653+
654+-- JSON: request body with application/json content type
655+instance RenderJSONSchema a => RenderRequestBodySchema (JSON a) where
656+ renderRequestBodySchema _ = Just
657+ { required: true
658+ , content: unsafeCoerce
659+ { "application/json":
660+ { schema: renderJSONSchema (Proxy :: Proxy a) }
661+ }
662+ }
663+
664+-- FormData: request body with application/x-www-form-urlencoded content type
665+instance RenderJSONSchema a => RenderRequestBodySchema (FormData a) where
666+ renderRequestBodySchema _ = Just
667+ { required: true
668+ , content: unsafeCoerce
669+ { "application/x-www-form-urlencoded":
670+ { schema: renderJSONSchema (Proxy :: Proxy a) }
671+ }
672+ }
673+
674+--------------------------------------------------------------------------------
675+-- Response Headers Schema Generation
676+--------------------------------------------------------------------------------
677+
678+-- | Response header object type with all metadata
679+type ResponseHeaderObject =
680+ { schema :: Foreign
681+ , description :: Maybe String
682+ , example :: Maybe String
683+ , deprecated :: Maybe Boolean
684+ }
685+
686+-- | Build a response header object with metadata
687+buildResponseHeader
688+ :: { type :: String
689+ , description :: Maybe String
690+ , example :: Maybe String
691+ , deprecated :: Maybe Boolean
692+ , format :: Maybe String
693+ , enum :: Maybe (Array String)
694+ }
695+ -> ResponseHeaderObject
696+buildResponseHeader { type: typeStr, description: desc, example: ex, deprecated: dep, format: fmt, enum: enumVals } =
697+ let
698+ ins :: forall a. String -> a -> FObject.Object Foreign -> FObject.Object Foreign
699+ ins key val obj = FObject.insert key (unsafeCoerce val) obj
700+
701+ insMaybe :: forall a. String -> Maybe a -> FObject.Object Foreign -> FObject.Object Foreign
702+ insMaybe key valMaybe obj = maybe obj (\v -> ins key v obj) valMaybe
703+
704+ schema = unsafeCoerce $ FObject.fromFoldable [ Tuple "type" (unsafeCoerce typeStr) ]
705+ # insMaybe "format" fmt
706+ # insMaybe "enum" enumVals
707+ in
708+ { schema
709+ , description: desc
710+ , example: ex
711+ , deprecated: if dep == Just true then Just true else Nothing
712+ }
713+
714+-- | Render response headers row as OpenAPI header object (for responses section)
715+class RenderResponseHeadersSchema (headers :: Row Type) where
716+ renderResponseHeadersSchema :: Proxy headers -> FObject.Object ResponseHeaderObject
717+
718+instance (RowToList headers rl, RenderResponseHeadersSchemaRL rl headers) => RenderResponseHeadersSchema headers where
719+ renderResponseHeadersSchema _ = renderResponseHeadersSchemaRL (Proxy :: Proxy rl)
720+
721+-- | Helper class using RowList
722+class RenderResponseHeadersSchemaRL (rl :: RowList Type) (headers :: Row Type) | rl -> headers where
723+ renderResponseHeadersSchemaRL :: Proxy rl -> FObject.Object ResponseHeaderObject
724+
725+instance RenderResponseHeadersSchemaRL RL.Nil () where
726+ renderResponseHeadersSchemaRL _ = FObject.empty
727+
728+-- Response header instance
729+instance
730+ ( IsSymbol name
731+ , HeaderValueType ty
732+ , HasDescription ty
733+ , HasExample ty
734+ , HasDeprecated ty
735+ , HasFormat ty
736+ , HasEnum ty
737+ , RenderResponseHeadersSchemaRL tail tailRow
738+ , Row.Cons name ty tailRow headers
739+ , Row.Lacks name tailRow
740+ ) =>
741+ RenderResponseHeadersSchemaRL (RL.Cons name ty tail) headers where
742+ renderResponseHeadersSchemaRL _ =
743+ let
744+ p = Proxy :: Proxy ty
745+ headerName = reflectSymbol (Proxy :: Proxy name)
746+ header = buildResponseHeader
747+ { type: headerValueType p
748+ , description: description p
749+ , example: example p
750+ , deprecated: if deprecated p then Just true else Nothing
751+ , format: format p
752+ , enum: enum p
753+ }
754+ rest = renderResponseHeadersSchemaRL (Proxy :: Proxy tail)
755+ in
756+ FObject.insert headerName header rest
757+
758+--------------------------------------------------------------------------------
759+-- Complete Response Schema Generation
760+--------------------------------------------------------------------------------
761+
762+-- | Render complete response object for OpenAPI (status 200 with headers and body)
763+class RenderResponseSchema (headers :: Row Type) (body :: Type) where
764+ renderResponseSchema
765+ :: Proxy headers
766+ -> Proxy body
767+ -> { "200" ::
768+ { description :: String
769+ , headers :: FObject.Object ResponseHeaderObject
770+ , content ::
771+ { "application/json" ::
772+ { schema :: Foreign }
773+ }
774+ }
775+ }
776+
777+instance (RenderResponseHeadersSchema headers, RenderJSONSchema body) => RenderResponseSchema headers body where
778+ renderResponseSchema headersProxy bodyProxy =
779+ let
780+ headers = renderResponseHeadersSchema headersProxy
781+ bodySchema = renderJSONSchema bodyProxy
782+ in
783+ { "200":
784+ { description: "Successful response"
785+ , headers: headers
786+ , content:
787+ { "application/json":
788+ { schema: bodySchema }
789+ }
790+ }
791+ }
792+
793+--------------------------------------------------------------------------------
794+-- Variant Response Schema Generation
795+--------------------------------------------------------------------------------
796+
797+-- | Type alias for OpenAPI response object
798+type ResponseObject =
799+ { description :: String
800+ , headers :: FObject.Object ResponseHeaderObject
801+ , content :: { "application/json" :: { schema :: Foreign } }
802+ }
803+
804+-- | Render variant response schema as OpenAPI responses object
805+-- | Maps each variant case to an HTTP status code and response object
806+class RenderVariantResponseSchema (variantRow :: Row Type) where
807+ renderVariantResponseSchema
808+ :: Proxy variantRow
809+ -> FObject.Object ResponseObject
810+
811+instance (RowToList variantRow rl, RenderVariantResponseSchemaRL rl) => RenderVariantResponseSchema variantRow where
812+ renderVariantResponseSchema _ = renderVariantResponseSchemaRL (Proxy :: Proxy rl)
813+
814+-- | Helper class that processes RowList for variant responses
815+class RenderVariantResponseSchemaRL (rl :: RowList Type) where
816+ renderVariantResponseSchemaRL
817+ :: Proxy rl
818+ -> FObject.Object ResponseObject
819+
820+-- Base case: empty RowList
821+instance renderVariantResponseSchemaRLNil :: RenderVariantResponseSchemaRL Nil where
822+ renderVariantResponseSchemaRL _ = FObject.empty
823+
824+-- Recursive case: process Response/ResponseData (or record syntax via ToResponse)
825+instance renderVariantResponseSchemaRLCons ::
826+ ( IsSymbol label
827+ , StatusCodeMap label
828+ , ToResponse recordType headers body
829+ , RenderResponseHeadersSchema headers
830+ , RenderJSONSchema body
831+ , RenderVariantResponseSchemaRL tail
832+ ) =>
833+ RenderVariantResponseSchemaRL (Cons label recordType tail) where
834+ renderVariantResponseSchemaRL _ =
835+ let
836+ statusCode = statusCodeFor (Proxy :: Proxy label)
837+ statusCodeStr = statusCodeToString statusCode
838+ headersObj = renderResponseHeadersSchema (Proxy :: Proxy headers)
839+ bodySchema = renderJSONSchema (Proxy :: Proxy body)
840+ responseObj =
841+ { description: "Successful response"
842+ , headers: headersObj
843+ , content:
844+ { "application/json":
845+ { schema: bodySchema }
846+ }
847+ }
848+ rest = renderVariantResponseSchemaRL (Proxy :: Proxy tail)
849+ in
850+ FObject.insert statusCodeStr responseObj rest
851+
852+--------------------------------------------------------------------------------
853+-- ToOpenAPI
854+--------------------------------------------------------------------------------
855+
856+-- | Generate complete OpenAPI operation object for a route
857+-- | Note: The instance for this typeclass is defined in Route.purs
858+-- | since it depends on the Route type defined there
859+class ToOpenAPI (route :: Type) where
860+ toOpenAPIImpl :: Proxy route -> String
861+
862+toOpenAPI :: forall @a. ToOpenAPI a => String
863+toOpenAPI = toOpenAPIImpl (Proxy :: Proxy a)
864+
865+--------------------------------------------------------------------------------
866+-- CollectOperations: Walk a type-level structure of routes
867+--------------------------------------------------------------------------------
868+
869+type OperationEntry =
870+ { method :: String
871+ , path :: String
872+ , operation :: String
873+ }
874+
875+-- | Collect OpenAPI operations from a type-level structure of routes.
876+-- | Use Tuple (/\) to combine multiple routes:
877+-- | CollectOperations (RouteA /\ RouteB /\ RouteC)
878+class CollectOperations (routes :: Type) where
879+ collectOperations :: Proxy routes -> Array OperationEntry
880+
881+-- Tuple: recurse into both sides
882+instance (CollectOperations a, CollectOperations b) => CollectOperations (Tuple a b) where
883+ collectOperations _ = collectOperations (Proxy :: Proxy a) <> collectOperations (Proxy :: Proxy b)
884+
885+-- Note: Route instance is defined in Route.purs to avoid import cycles
886+
887+--------------------------------------------------------------------------------
888+-- buildOpenAPISpec: Assemble a complete OpenAPI 3.0 document
889+--------------------------------------------------------------------------------
890+
891+-- | Opaque type representing a complete OpenAPI specification document.
892+foreign import data OpenAPISpec :: Type
893+
894+instance WriteForeign OpenAPISpec where
895+ writeImpl = unsafeCoerce
896+
897+-- | Detect if any operations use bearer token authentication
898+-- | and build the securitySchemes component
899+buildSecuritySchemes :: Array OperationEntry -> FObject.Object Foreign
900+buildSecuritySchemes ops =
901+ let
902+ hasSecurity = ops # foldl checkOp false
903+ where
904+ checkOp acc entry =
905+ let
906+ opObj = unsafeCoerce (unsafeParseJSON entry.operation) :: FObject.Object Foreign
907+ securityField = FObject.lookup "security" opObj
908+ in
909+ acc || case securityField of
910+ Nothing -> false
911+ Just _ -> true
912+ in
913+ if hasSecurity then
914+ FObject.singleton "bearerAuth"
915+ ( unsafeCoerce
916+ { type: "http"
917+ , scheme: "bearer"
918+ , bearerFormat: "JWT"
919+ }
920+ )
921+ else
922+ FObject.empty
923+
924+-- | Type for OpenAPI server object
925+type ServerObject =
926+ { url :: String
927+ , description :: Maybe String
928+ }
929+
930+-- | Build a complete OpenAPI 3.0 spec from a type-level collection of routes.
931+-- |
932+-- | Examples:
933+-- | -- Basic usage:
934+-- | buildOpenAPISpec @(HealthRoute /\ UserRoute)
935+-- | { title: "My API", version: "1.0.0" }
936+-- |
937+-- | -- With servers:
938+-- | buildOpenAPISpec' @(HealthRoute /\ UserRoute)
939+-- | { title: "My API"
940+-- | , version: "1.0.0"
941+-- | , servers: Just
942+-- | [ { url: "https://api.example.com", description: Just "Production server" }
943+-- | , { url: "https://staging-api.example.com", description: Just "Staging server" }
944+-- | ]
945+-- | }
946+buildOpenAPISpec
947+ :: forall @routes
948+ . CollectOperations routes
949+ => { title :: String, version :: String }
950+ -> OpenAPISpec
951+buildOpenAPISpec info = buildOpenAPISpec' @routes info { servers: Nothing }
952+
953+-- | Build a complete OpenAPI 3.0 spec with optional servers configuration.
954+buildOpenAPISpec'
955+ :: forall @routes
956+ . CollectOperations routes
957+ => { title :: String, version :: String }
958+ -> { servers :: Maybe (Array ServerObject) }
959+ -> OpenAPISpec
960+buildOpenAPISpec' info config =
961+ let
962+ ops = collectOperations (Proxy :: Proxy routes)
963+ paths = groupByPath ops
964+ securitySchemes = buildSecuritySchemes ops
965+ components =
966+ if FObject.isEmpty securitySchemes then Nothing
967+ else Just { securitySchemes }
968+ baseSpec = FObject.fromFoldable
969+ [ Tuple "openapi" (unsafeCoerce "3.0.0")
970+ , Tuple "info" (unsafeCoerce { title: info.title, version: info.version })
971+ , Tuple "paths" (unsafeCoerce paths)
972+ ]
973+ withComponents = case components of
974+ Nothing -> baseSpec
975+ Just c -> FObject.insert "components" (unsafeCoerce c) baseSpec
976+ withServers = case config.servers of
977+ Nothing -> withComponents
978+ Just servers -> FObject.insert "servers" (writeImpl servers) withComponents
979+ in
980+ unsafeCoerce $ withServers
981+
982+-- | Convert `:param` to `{param}` for OpenAPI path format
983+toOpenAPIPath :: String -> String
984+toOpenAPIPath = replace (unsafeRegex ":([a-zA-Z][a-zA-Z0-9]*)" global) "{$1}"
985+
986+-- | Group operations by path, then by method
987+-- | Returns FObject (FObject Foreign) — path -> method -> operation
988+groupByPath :: Array OperationEntry -> FObject.Object (FObject.Object Foreign)
989+groupByPath = foldl insertOp FObject.empty
990+ where
991+ insertOp acc entry =
992+ let
993+ pathKey = toOpenAPIPath entry.path
994+ methodMap = FObject.lookup pathKey acc # case _ of
995+ Nothing -> FObject.empty
996+ Just m -> m
997+ -- Parse the operation JSON string and strip method/path keys
998+ opForeign = stripKeys [ "method", "path" ] (unsafeParseJSON entry.operation)
999+ methodMap' = FObject.insert entry.method opForeign methodMap
1000+ in
1001+ FObject.insert pathKey methodMap' acc
1002+
1003+-- FFI helpers
1004+foreign import unsafeParseJSON :: String -> Foreign
1005+foreign import stripKeys :: Array String -> Foreign -> Foreign
src/Yoga/HTTP/API/Route/OpenAPIMetadata.pursnew file+896
@@ -0,0 +1,896 @@
1+module Yoga.HTTP.API.Route.OpenAPIMetadata
2+ ( class HasDescription
3+ , description
4+ , class HasExample
5+ , example
6+ , class HasFormat
7+ , format
8+ , class HasMinimum
9+ , minimum
10+ , class HasMaximum
11+ , maximum
12+ , class HasPattern
13+ , pattern
14+ , class HasMinLength
15+ , minLength
16+ , class HasMaxLength
17+ , maxLength
18+ , class HasTitle
19+ , title
20+ , class HasNullable
21+ , nullable
22+ , class HasDefault
23+ , default
24+ , class HasDeprecated
25+ , deprecated
26+ , class HasEnum
27+ , enum
28+ , class GenericEnumValues
29+ , genericEnumValues
30+ , class HasOperationMetadata
31+ , operationMetadata
32+ , OperationMetadata
33+ , Description
34+ , Example
35+ , Format
36+ , Minimum
37+ , Maximum
38+ , Pattern
39+ , MinLength
40+ , MaxLength
41+ , Title
42+ , Nullable
43+ , Default
44+ , Deprecated
45+ , Enum
46+ ) where
47+
48+import Prelude
49+
50+import Data.Either (Either(..))
51+import Data.Generic.Rep (class Generic)
52+import Data.Generic.Rep as GR
53+import Data.Maybe (Maybe(..))
54+import Data.Reflectable (class Reflectable, reflectType)
55+import Data.String.CodeUnits as SCU
56+import Data.String.Regex as Regex
57+import Data.String.Regex.Flags (noFlags)
58+import Data.Symbol (class IsSymbol, reflectSymbol)
59+import Foreign (Foreign)
60+import Prim.TypeError (class Warn, Text)
61+import Type.Proxy (Proxy(..))
62+import Unsafe.Coerce (unsafeCoerce)
63+import Yoga.HTTP.API.Path (class ParseParam, parseParam)
64+import Yoga.HTTP.API.Route.HeaderValue (class HeaderValue, parseHeader, printHeader, class HeaderValueType, headerValueType)
65+import Yoga.JSON (class ReadForeign, class WriteForeign, readImpl, writeImpl)
66+
67+--------------------------------------------------------------------------------
68+-- Metadata Type Wrappers
69+--------------------------------------------------------------------------------
70+
71+-- | Attach a description to a type.
72+-- | Example: Int # Description "The unique identifier for a user"
73+data Description :: Symbol -> Type -> Type
74+data Description desc a
75+
76+-- | Attach an example value to a type.
77+-- | Example: Int # Example "123"
78+data Example :: Symbol -> Type -> Type
79+data Example exampleValue a
80+
81+-- | Attach a format annotation to a type.
82+-- | Example: String # Format "email"
83+data Format :: Symbol -> Type -> Type
84+data Format formatStr a
85+
86+-- | Set a minimum value constraint.
87+-- | Example: Int # Minimum 1
88+data Minimum :: Int -> Type -> Type
89+data Minimum minVal a
90+
91+-- | Set a maximum value constraint.
92+-- | Example: Int # Maximum 100
93+data Maximum :: Int -> Type -> Type
94+data Maximum maxVal a
95+
96+-- | Set a regex pattern constraint.
97+-- | Example: String # Pattern "^[a-z]+$"
98+data Pattern :: Symbol -> Type -> Type
99+data Pattern pat a
100+
101+-- | Set a minimum string length constraint.
102+-- | Example: String # MinLength 1
103+data MinLength :: Int -> Type -> Type
104+data MinLength minLen a
105+
106+-- | Set a maximum string length constraint.
107+-- | Example: String # MaxLength 255
108+data MaxLength :: Int -> Type -> Type
109+data MaxLength maxLen a
110+
111+-- | Attach a title to a type.
112+-- | Example: String # Title "UserName"
113+data Title :: Symbol -> Type -> Type
114+data Title t a
115+
116+-- | Mark a type as nullable.
117+-- | Example: String # Nullable
118+data Nullable :: Type -> Type
119+data Nullable a
120+
121+-- | Set a default value.
122+-- | Example: Int # Default "10"
123+data Default :: Symbol -> Type -> Type
124+data Default val a
125+
126+-- | Mark a type as deprecated.
127+-- | Example: Int # Deprecated
128+data Deprecated :: Type -> Type
129+data Deprecated a
130+
131+-- | Wrapper to use an enum type (sum type with no-argument constructors) in routes.
132+-- | The type parameter should be a Generic sum type, and enum values will be
133+-- | automatically extracted from its constructor names.
134+-- |
135+-- | Example:
136+-- | data Status = Pending | Active | Completed
137+-- | derive instance Generic Status _
138+-- |
139+-- | type StatusParam = Enum Status
140+data Enum :: Type -> Type
141+data Enum a
142+
143+--------------------------------------------------------------------------------
144+-- Metadata Extraction Typeclasses
145+--
146+-- Each Has* class matches its own wrapper and recurses through all others.
147+--------------------------------------------------------------------------------
148+
149+-- Helper: all wrappers that a Has* class needs to recurse through.
150+-- When adding a new wrapper, add a recurse instance to every Has* class
151+-- and add the new Has* class with recurse instances for all existing wrappers.
152+
153+class HasDescription :: Type -> Constraint
154+class HasDescription ty where
155+ description :: Proxy ty -> Maybe String
156+
157+instance IsSymbol desc => HasDescription (Description desc a) where
158+ description _ = Just (reflectSymbol (Proxy :: Proxy desc))
159+else instance HasDescription a => HasDescription (Example ex a) where
160+ description _ = description (Proxy :: Proxy a)
161+else instance HasDescription a => HasDescription (Format fmt a) where
162+ description _ = description (Proxy :: Proxy a)
163+else instance HasDescription a => HasDescription (Minimum v a) where
164+ description _ = description (Proxy :: Proxy a)
165+else instance HasDescription a => HasDescription (Maximum v a) where
166+ description _ = description (Proxy :: Proxy a)
167+else instance HasDescription a => HasDescription (Pattern pat a) where
168+ description _ = description (Proxy :: Proxy a)
169+else instance HasDescription a => HasDescription (MinLength v a) where
170+ description _ = description (Proxy :: Proxy a)
171+else instance HasDescription a => HasDescription (MaxLength v a) where
172+ description _ = description (Proxy :: Proxy a)
173+else instance HasDescription a => HasDescription (Title t a) where
174+ description _ = description (Proxy :: Proxy a)
175+else instance HasDescription a => HasDescription (Nullable a) where
176+ description _ = description (Proxy :: Proxy a)
177+else instance HasDescription a => HasDescription (Default val a) where
178+ description _ = description (Proxy :: Proxy a)
179+else instance HasDescription a => HasDescription (Deprecated a) where
180+ description _ = description (Proxy :: Proxy a)
181+else instance HasDescription a => HasDescription (Enum a) where
182+ description _ = description (Proxy :: Proxy a)
183+else instance HasDescription ty where
184+ description _ = Nothing
185+
186+class HasExample :: Type -> Constraint
187+class HasExample ty where
188+ example :: Proxy ty -> Maybe String
189+
190+instance IsSymbol ex => HasExample (Example ex a) where
191+ example _ = Just (reflectSymbol (Proxy :: Proxy ex))
192+else instance HasExample a => HasExample (Description desc a) where
193+ example _ = example (Proxy :: Proxy a)
194+else instance HasExample a => HasExample (Format fmt a) where
195+ example _ = example (Proxy :: Proxy a)
196+else instance HasExample a => HasExample (Minimum v a) where
197+ example _ = example (Proxy :: Proxy a)
198+else instance HasExample a => HasExample (Maximum v a) where
199+ example _ = example (Proxy :: Proxy a)
200+else instance HasExample a => HasExample (Pattern pat a) where
201+ example _ = example (Proxy :: Proxy a)
202+else instance HasExample a => HasExample (MinLength v a) where
203+ example _ = example (Proxy :: Proxy a)
204+else instance HasExample a => HasExample (MaxLength v a) where
205+ example _ = example (Proxy :: Proxy a)
206+else instance HasExample a => HasExample (Title t a) where
207+ example _ = example (Proxy :: Proxy a)
208+else instance HasExample a => HasExample (Nullable a) where
209+ example _ = example (Proxy :: Proxy a)
210+else instance HasExample a => HasExample (Default val a) where
211+ example _ = example (Proxy :: Proxy a)
212+else instance HasExample a => HasExample (Deprecated a) where
213+ example _ = example (Proxy :: Proxy a)
214+else instance HasExample a => HasExample (Enum a) where
215+ example _ = example (Proxy :: Proxy a)
216+else instance HasExample ty where
217+ example _ = Nothing
218+
219+class HasFormat :: Type -> Constraint
220+class HasFormat ty where
221+ format :: Proxy ty -> Maybe String
222+
223+instance IsSymbol fmt => HasFormat (Format fmt a) where
224+ format _ = Just (reflectSymbol (Proxy :: Proxy fmt))
225+else instance HasFormat a => HasFormat (Description desc a) where
226+ format _ = format (Proxy :: Proxy a)
227+else instance HasFormat a => HasFormat (Example ex a) where
228+ format _ = format (Proxy :: Proxy a)
229+else instance HasFormat a => HasFormat (Minimum v a) where
230+ format _ = format (Proxy :: Proxy a)
231+else instance HasFormat a => HasFormat (Maximum v a) where
232+ format _ = format (Proxy :: Proxy a)
233+else instance HasFormat a => HasFormat (Pattern pat a) where
234+ format _ = format (Proxy :: Proxy a)
235+else instance HasFormat a => HasFormat (MinLength v a) where
236+ format _ = format (Proxy :: Proxy a)
237+else instance HasFormat a => HasFormat (MaxLength v a) where
238+ format _ = format (Proxy :: Proxy a)
239+else instance HasFormat a => HasFormat (Title t a) where
240+ format _ = format (Proxy :: Proxy a)
241+else instance HasFormat a => HasFormat (Nullable a) where
242+ format _ = format (Proxy :: Proxy a)
243+else instance HasFormat a => HasFormat (Default val a) where
244+ format _ = format (Proxy :: Proxy a)
245+else instance HasFormat a => HasFormat (Deprecated a) where
246+ format _ = format (Proxy :: Proxy a)
247+else instance HasFormat a => HasFormat (Enum a) where
248+ format _ = format (Proxy :: Proxy a)
249+else instance HasFormat ty where
250+ format _ = Nothing
251+
252+class HasMinimum :: Type -> Constraint
253+class HasMinimum ty where
254+ minimum :: Proxy ty -> Maybe Int
255+
256+instance Reflectable v Int => HasMinimum (Minimum v a) where
257+ minimum _ = Just (reflectType (Proxy :: Proxy v))
258+else instance HasMinimum a => HasMinimum (Description desc a) where
259+ minimum _ = minimum (Proxy :: Proxy a)
260+else instance HasMinimum a => HasMinimum (Example ex a) where
261+ minimum _ = minimum (Proxy :: Proxy a)
262+else instance HasMinimum a => HasMinimum (Format fmt a) where
263+ minimum _ = minimum (Proxy :: Proxy a)
264+else instance HasMinimum a => HasMinimum (Maximum v a) where
265+ minimum _ = minimum (Proxy :: Proxy a)
266+else instance HasMinimum a => HasMinimum (Pattern pat a) where
267+ minimum _ = minimum (Proxy :: Proxy a)
268+else instance HasMinimum a => HasMinimum (MinLength v a) where
269+ minimum _ = minimum (Proxy :: Proxy a)
270+else instance HasMinimum a => HasMinimum (MaxLength v a) where
271+ minimum _ = minimum (Proxy :: Proxy a)
272+else instance HasMinimum a => HasMinimum (Title t a) where
273+ minimum _ = minimum (Proxy :: Proxy a)
274+else instance HasMinimum a => HasMinimum (Nullable a) where
275+ minimum _ = minimum (Proxy :: Proxy a)
276+else instance HasMinimum a => HasMinimum (Default val a) where
277+ minimum _ = minimum (Proxy :: Proxy a)
278+else instance HasMinimum a => HasMinimum (Deprecated a) where
279+ minimum _ = minimum (Proxy :: Proxy a)
280+else instance HasMinimum a => HasMinimum (Enum a) where
281+ minimum _ = minimum (Proxy :: Proxy a)
282+else instance HasMinimum ty where
283+ minimum _ = Nothing
284+
285+class HasMaximum :: Type -> Constraint
286+class HasMaximum ty where
287+ maximum :: Proxy ty -> Maybe Int
288+
289+instance Reflectable v Int => HasMaximum (Maximum v a) where
290+ maximum _ = Just (reflectType (Proxy :: Proxy v))
291+else instance HasMaximum a => HasMaximum (Description desc a) where
292+ maximum _ = maximum (Proxy :: Proxy a)
293+else instance HasMaximum a => HasMaximum (Example ex a) where
294+ maximum _ = maximum (Proxy :: Proxy a)
295+else instance HasMaximum a => HasMaximum (Format fmt a) where
296+ maximum _ = maximum (Proxy :: Proxy a)
297+else instance HasMaximum a => HasMaximum (Minimum v a) where
298+ maximum _ = maximum (Proxy :: Proxy a)
299+else instance HasMaximum a => HasMaximum (Pattern pat a) where
300+ maximum _ = maximum (Proxy :: Proxy a)
301+else instance HasMaximum a => HasMaximum (MinLength v a) where
302+ maximum _ = maximum (Proxy :: Proxy a)
303+else instance HasMaximum a => HasMaximum (MaxLength v a) where
304+ maximum _ = maximum (Proxy :: Proxy a)
305+else instance HasMaximum a => HasMaximum (Title t a) where
306+ maximum _ = maximum (Proxy :: Proxy a)
307+else instance HasMaximum a => HasMaximum (Nullable a) where
308+ maximum _ = maximum (Proxy :: Proxy a)
309+else instance HasMaximum a => HasMaximum (Default val a) where
310+ maximum _ = maximum (Proxy :: Proxy a)
311+else instance HasMaximum a => HasMaximum (Deprecated a) where
312+ maximum _ = maximum (Proxy :: Proxy a)
313+else instance HasMaximum a => HasMaximum (Enum a) where
314+ maximum _ = maximum (Proxy :: Proxy a)
315+else instance HasMaximum ty where
316+ maximum _ = Nothing
317+
318+class HasPattern :: Type -> Constraint
319+class HasPattern ty where
320+ pattern :: Proxy ty -> Maybe String
321+
322+instance IsSymbol pat => HasPattern (Pattern pat a) where
323+ pattern _ = Just (reflectSymbol (Proxy :: Proxy pat))
324+else instance HasPattern a => HasPattern (Description desc a) where
325+ pattern _ = pattern (Proxy :: Proxy a)
326+else instance HasPattern a => HasPattern (Example ex a) where
327+ pattern _ = pattern (Proxy :: Proxy a)
328+else instance HasPattern a => HasPattern (Format fmt a) where
329+ pattern _ = pattern (Proxy :: Proxy a)
330+else instance HasPattern a => HasPattern (Minimum v a) where
331+ pattern _ = pattern (Proxy :: Proxy a)
332+else instance HasPattern a => HasPattern (Maximum v a) where
333+ pattern _ = pattern (Proxy :: Proxy a)
334+else instance HasPattern a => HasPattern (MinLength v a) where
335+ pattern _ = pattern (Proxy :: Proxy a)
336+else instance HasPattern a => HasPattern (MaxLength v a) where
337+ pattern _ = pattern (Proxy :: Proxy a)
338+else instance HasPattern a => HasPattern (Title t a) where
339+ pattern _ = pattern (Proxy :: Proxy a)
340+else instance HasPattern a => HasPattern (Nullable a) where
341+ pattern _ = pattern (Proxy :: Proxy a)
342+else instance HasPattern a => HasPattern (Default val a) where
343+ pattern _ = pattern (Proxy :: Proxy a)
344+else instance HasPattern a => HasPattern (Deprecated a) where
345+ pattern _ = pattern (Proxy :: Proxy a)
346+else instance HasPattern a => HasPattern (Enum a) where
347+ pattern _ = pattern (Proxy :: Proxy a)
348+else instance HasPattern ty where
349+ pattern _ = Nothing
350+
351+class HasMinLength :: Type -> Constraint
352+class HasMinLength ty where
353+ minLength :: Proxy ty -> Maybe Int
354+
355+instance Reflectable v Int => HasMinLength (MinLength v a) where
356+ minLength _ = Just (reflectType (Proxy :: Proxy v))
357+else instance HasMinLength a => HasMinLength (Description desc a) where
358+ minLength _ = minLength (Proxy :: Proxy a)
359+else instance HasMinLength a => HasMinLength (Example ex a) where
360+ minLength _ = minLength (Proxy :: Proxy a)
361+else instance HasMinLength a => HasMinLength (Format fmt a) where
362+ minLength _ = minLength (Proxy :: Proxy a)
363+else instance HasMinLength a => HasMinLength (Minimum v a) where
364+ minLength _ = minLength (Proxy :: Proxy a)
365+else instance HasMinLength a => HasMinLength (Maximum v a) where
366+ minLength _ = minLength (Proxy :: Proxy a)
367+else instance HasMinLength a => HasMinLength (Pattern pat a) where
368+ minLength _ = minLength (Proxy :: Proxy a)
369+else instance HasMinLength a => HasMinLength (MaxLength v a) where
370+ minLength _ = minLength (Proxy :: Proxy a)
371+else instance HasMinLength a => HasMinLength (Title t a) where
372+ minLength _ = minLength (Proxy :: Proxy a)
373+else instance HasMinLength a => HasMinLength (Nullable a) where
374+ minLength _ = minLength (Proxy :: Proxy a)
375+else instance HasMinLength a => HasMinLength (Default val a) where
376+ minLength _ = minLength (Proxy :: Proxy a)
377+else instance HasMinLength a => HasMinLength (Deprecated a) where
378+ minLength _ = minLength (Proxy :: Proxy a)
379+else instance HasMinLength a => HasMinLength (Enum a) where
380+ minLength _ = minLength (Proxy :: Proxy a)
381+else instance HasMinLength ty where
382+ minLength _ = Nothing
383+
384+class HasMaxLength :: Type -> Constraint
385+class HasMaxLength ty where
386+ maxLength :: Proxy ty -> Maybe Int
387+
388+instance Reflectable v Int => HasMaxLength (MaxLength v a) where
389+ maxLength _ = Just (reflectType (Proxy :: Proxy v))
390+else instance HasMaxLength a => HasMaxLength (Description desc a) where
391+ maxLength _ = maxLength (Proxy :: Proxy a)
392+else instance HasMaxLength a => HasMaxLength (Example ex a) where
393+ maxLength _ = maxLength (Proxy :: Proxy a)
394+else instance HasMaxLength a => HasMaxLength (Format fmt a) where
395+ maxLength _ = maxLength (Proxy :: Proxy a)
396+else instance HasMaxLength a => HasMaxLength (Minimum v a) where
397+ maxLength _ = maxLength (Proxy :: Proxy a)
398+else instance HasMaxLength a => HasMaxLength (Maximum v a) where
399+ maxLength _ = maxLength (Proxy :: Proxy a)
400+else instance HasMaxLength a => HasMaxLength (Pattern pat a) where
401+ maxLength _ = maxLength (Proxy :: Proxy a)
402+else instance HasMaxLength a => HasMaxLength (MinLength v a) where
403+ maxLength _ = maxLength (Proxy :: Proxy a)
404+else instance HasMaxLength a => HasMaxLength (Title t a) where
405+ maxLength _ = maxLength (Proxy :: Proxy a)
406+else instance HasMaxLength a => HasMaxLength (Nullable a) where
407+ maxLength _ = maxLength (Proxy :: Proxy a)
408+else instance HasMaxLength a => HasMaxLength (Default val a) where
409+ maxLength _ = maxLength (Proxy :: Proxy a)
410+else instance HasMaxLength a => HasMaxLength (Deprecated a) where
411+ maxLength _ = maxLength (Proxy :: Proxy a)
412+else instance HasMaxLength a => HasMaxLength (Enum a) where
413+ maxLength _ = maxLength (Proxy :: Proxy a)
414+else instance HasMaxLength ty where
415+ maxLength _ = Nothing
416+
417+class HasTitle :: Type -> Constraint
418+class HasTitle ty where
419+ title :: Proxy ty -> Maybe String
420+
421+instance IsSymbol t => HasTitle (Title t a) where
422+ title _ = Just (reflectSymbol (Proxy :: Proxy t))
423+else instance HasTitle a => HasTitle (Description desc a) where
424+ title _ = title (Proxy :: Proxy a)
425+else instance HasTitle a => HasTitle (Example ex a) where
426+ title _ = title (Proxy :: Proxy a)
427+else instance HasTitle a => HasTitle (Format fmt a) where
428+ title _ = title (Proxy :: Proxy a)
429+else instance HasTitle a => HasTitle (Minimum v a) where
430+ title _ = title (Proxy :: Proxy a)
431+else instance HasTitle a => HasTitle (Maximum v a) where
432+ title _ = title (Proxy :: Proxy a)
433+else instance HasTitle a => HasTitle (Pattern pat a) where
434+ title _ = title (Proxy :: Proxy a)
435+else instance HasTitle a => HasTitle (MinLength v a) where
436+ title _ = title (Proxy :: Proxy a)
437+else instance HasTitle a => HasTitle (MaxLength v a) where
438+ title _ = title (Proxy :: Proxy a)
439+else instance HasTitle a => HasTitle (Nullable a) where
440+ title _ = title (Proxy :: Proxy a)
441+else instance HasTitle a => HasTitle (Default val a) where
442+ title _ = title (Proxy :: Proxy a)
443+else instance HasTitle a => HasTitle (Deprecated a) where
444+ title _ = title (Proxy :: Proxy a)
445+else instance HasTitle a => HasTitle (Enum a) where
446+ title _ = title (Proxy :: Proxy a)
447+else instance HasTitle ty where
448+ title _ = Nothing
449+
450+class HasNullable :: Type -> Constraint
451+class HasNullable ty where
452+ nullable :: Proxy ty -> Boolean
453+
454+instance HasNullable (Nullable a) where
455+ nullable _ = true
456+else instance HasNullable a => HasNullable (Description desc a) where
457+ nullable _ = nullable (Proxy :: Proxy a)
458+else instance HasNullable a => HasNullable (Example ex a) where
459+ nullable _ = nullable (Proxy :: Proxy a)
460+else instance HasNullable a => HasNullable (Format fmt a) where
461+ nullable _ = nullable (Proxy :: Proxy a)
462+else instance HasNullable a => HasNullable (Minimum v a) where
463+ nullable _ = nullable (Proxy :: Proxy a)
464+else instance HasNullable a => HasNullable (Maximum v a) where
465+ nullable _ = nullable (Proxy :: Proxy a)
466+else instance HasNullable a => HasNullable (Pattern pat a) where
467+ nullable _ = nullable (Proxy :: Proxy a)
468+else instance HasNullable a => HasNullable (MinLength v a) where
469+ nullable _ = nullable (Proxy :: Proxy a)
470+else instance HasNullable a => HasNullable (MaxLength v a) where
471+ nullable _ = nullable (Proxy :: Proxy a)
472+else instance HasNullable a => HasNullable (Title t a) where
473+ nullable _ = nullable (Proxy :: Proxy a)
474+else instance HasNullable a => HasNullable (Default val a) where
475+ nullable _ = nullable (Proxy :: Proxy a)
476+else instance HasNullable a => HasNullable (Deprecated a) where
477+ nullable _ = nullable (Proxy :: Proxy a)
478+else instance HasNullable a => HasNullable (Enum a) where
479+ nullable _ = nullable (Proxy :: Proxy a)
480+else instance HasNullable ty where
481+ nullable _ = false
482+
483+class HasDefault :: Type -> Constraint
484+class HasDefault ty where
485+ default :: Proxy ty -> Maybe String
486+
487+instance IsSymbol val => HasDefault (Default val a) where
488+ default _ = Just (reflectSymbol (Proxy :: Proxy val))
489+else instance HasDefault a => HasDefault (Description desc a) where
490+ default _ = default (Proxy :: Proxy a)
491+else instance HasDefault a => HasDefault (Example ex a) where
492+ default _ = default (Proxy :: Proxy a)
493+else instance HasDefault a => HasDefault (Format fmt a) where
494+ default _ = default (Proxy :: Proxy a)
495+else instance HasDefault a => HasDefault (Minimum v a) where
496+ default _ = default (Proxy :: Proxy a)
497+else instance HasDefault a => HasDefault (Maximum v a) where
498+ default _ = default (Proxy :: Proxy a)
499+else instance HasDefault a => HasDefault (Pattern pat a) where
500+ default _ = default (Proxy :: Proxy a)
501+else instance HasDefault a => HasDefault (MinLength v a) where
502+ default _ = default (Proxy :: Proxy a)
503+else instance HasDefault a => HasDefault (MaxLength v a) where
504+ default _ = default (Proxy :: Proxy a)
505+else instance HasDefault a => HasDefault (Title t a) where
506+ default _ = default (Proxy :: Proxy a)
507+else instance HasDefault a => HasDefault (Nullable a) where
508+ default _ = default (Proxy :: Proxy a)
509+else instance HasDefault a => HasDefault (Deprecated a) where
510+ default _ = default (Proxy :: Proxy a)
511+else instance HasDefault a => HasDefault (Enum a) where
512+ default _ = default (Proxy :: Proxy a)
513+else instance HasDefault ty where
514+ default _ = Nothing
515+
516+class HasDeprecated :: Type -> Constraint
517+class HasDeprecated ty where
518+ deprecated :: Proxy ty -> Boolean
519+
520+instance HasDeprecated (Deprecated a) where
521+ deprecated _ = true
522+else instance HasDeprecated a => HasDeprecated (Description desc a) where
523+ deprecated _ = deprecated (Proxy :: Proxy a)
524+else instance HasDeprecated a => HasDeprecated (Example ex a) where
525+ deprecated _ = deprecated (Proxy :: Proxy a)
526+else instance HasDeprecated a => HasDeprecated (Format fmt a) where
527+ deprecated _ = deprecated (Proxy :: Proxy a)
528+else instance HasDeprecated a => HasDeprecated (Minimum v a) where
529+ deprecated _ = deprecated (Proxy :: Proxy a)
530+else instance HasDeprecated a => HasDeprecated (Maximum v a) where
531+ deprecated _ = deprecated (Proxy :: Proxy a)
532+else instance HasDeprecated a => HasDeprecated (Pattern pat a) where
533+ deprecated _ = deprecated (Proxy :: Proxy a)
534+else instance HasDeprecated a => HasDeprecated (MinLength v a) where
535+ deprecated _ = deprecated (Proxy :: Proxy a)
536+else instance HasDeprecated a => HasDeprecated (MaxLength v a) where
537+ deprecated _ = deprecated (Proxy :: Proxy a)
538+else instance HasDeprecated a => HasDeprecated (Title t a) where
539+ deprecated _ = deprecated (Proxy :: Proxy a)
540+else instance HasDeprecated a => HasDeprecated (Nullable a) where
541+ deprecated _ = deprecated (Proxy :: Proxy a)
542+else instance HasDeprecated a => HasDeprecated (Default val a) where
543+ deprecated _ = deprecated (Proxy :: Proxy a)
544+else instance HasDeprecated a => HasDeprecated (Enum a) where
545+ deprecated _ = deprecated (Proxy :: Proxy a)
546+else instance HasDeprecated ty where
547+ deprecated _ = false
548+
549+class HasEnum :: Type -> Constraint
550+class HasEnum ty where
551+ enum :: Proxy ty -> Maybe (Array String)
552+
553+-- | Extract enum values from a Generic sum type representation.
554+-- | Walks through GR.Sum constructors and collects constructor names.
555+class GenericEnumValues :: Type -> Constraint
556+class GenericEnumValues rep where
557+ genericEnumValues :: Proxy rep -> Array String
558+
559+-- Walk sum types (left and right branches)
560+instance (GenericEnumValues a, GenericEnumValues b) => GenericEnumValues (GR.Sum a b) where
561+ genericEnumValues _ =
562+ genericEnumValues (Proxy :: Proxy a) <> genericEnumValues (Proxy :: Proxy b)
563+
564+-- Extract constructor name from no-argument constructors
565+instance IsSymbol name => GenericEnumValues (GR.Constructor name GR.NoArguments) where
566+ genericEnumValues _ = [ reflectSymbol (Proxy :: Proxy name) ]
567+
568+-- Main instance: extract enum values from a Generic type wrapped in Enum
569+instance (Generic a rep, GenericEnumValues rep) => HasEnum (Enum a) where
570+ enum _ = Just (genericEnumValues (Proxy :: Proxy rep))
571+else instance HasEnum a => HasEnum (Description desc a) where
572+ enum _ = enum (Proxy :: Proxy a)
573+else instance HasEnum a => HasEnum (Example ex a) where
574+ enum _ = enum (Proxy :: Proxy a)
575+else instance HasEnum a => HasEnum (Format fmt a) where
576+ enum _ = enum (Proxy :: Proxy a)
577+else instance HasEnum a => HasEnum (Minimum v a) where
578+ enum _ = enum (Proxy :: Proxy a)
579+else instance HasEnum a => HasEnum (Maximum v a) where
580+ enum _ = enum (Proxy :: Proxy a)
581+else instance HasEnum a => HasEnum (Pattern pat a) where
582+ enum _ = enum (Proxy :: Proxy a)
583+else instance HasEnum a => HasEnum (MinLength v a) where
584+ enum _ = enum (Proxy :: Proxy a)
585+else instance HasEnum a => HasEnum (MaxLength v a) where
586+ enum _ = enum (Proxy :: Proxy a)
587+else instance HasEnum a => HasEnum (Title t a) where
588+ enum _ = enum (Proxy :: Proxy a)
589+else instance HasEnum a => HasEnum (Nullable a) where
590+ enum _ = enum (Proxy :: Proxy a)
591+else instance HasEnum a => HasEnum (Default val a) where
592+ enum _ = enum (Proxy :: Proxy a)
593+else instance HasEnum a => HasEnum (Deprecated a) where
594+ enum _ = enum (Proxy :: Proxy a)
595+else instance HasEnum ty where
596+ enum _ = Nothing
597+
598+--------------------------------------------------------------------------------
599+-- Operation-Level Metadata
600+--------------------------------------------------------------------------------
601+
602+type OperationMetadata =
603+ { summary :: Maybe String
604+ , description :: Maybe String
605+ , operationId :: Maybe String
606+ , tags :: Array String
607+ , deprecated :: Boolean
608+ }
609+
610+class HasOperationMetadata :: Type -> Constraint
611+class HasOperationMetadata route where
612+ operationMetadata :: Proxy route -> OperationMetadata
613+
614+instance (Warn (Text "Using default operation metadata")) => HasOperationMetadata route where
615+ operationMetadata _ =
616+ { summary: Nothing
617+ , description: Nothing
618+ , operationId: Nothing
619+ , tags: []
620+ , deprecated: false
621+ }
622+
623+--------------------------------------------------------------------------------
624+-- Metadata Wrapper Transparency (Runtime)
625+--
626+-- All wrappers are phantom types — transparent at runtime via unsafeCoerce.
627+--------------------------------------------------------------------------------
628+
629+-- HeaderValue instances
630+instance HeaderValue a => HeaderValue (Description desc a) where
631+ parseHeader s = unsafeCoerce (parseHeader s :: Either String a)
632+ printHeader x = printHeader (unsafeCoerce x :: a)
633+
634+instance HeaderValue a => HeaderValue (Example ex a) where
635+ parseHeader s = unsafeCoerce (parseHeader s :: Either String a)
636+ printHeader x = printHeader (unsafeCoerce x :: a)
637+
638+instance HeaderValue a => HeaderValue (Format fmt a) where
639+ parseHeader s = unsafeCoerce (parseHeader s :: Either String a)
640+ printHeader x = printHeader (unsafeCoerce x :: a)
641+
642+instance (Reflectable v Int, HeaderValue a) => HeaderValue (Minimum v a) where
643+ parseHeader s = case (unsafeCoerce :: Either String a -> Either String Int) (parseHeader s :: Either String a) of
644+ Left err -> Left err
645+ Right val ->
646+ let
647+ minVal = reflectType (Proxy :: Proxy v)
648+ in
649+ if val < minVal then Left ("Value " <> show val <> " is less than minimum " <> show minVal)
650+ else Right (unsafeCoerce val)
651+ printHeader x = printHeader (unsafeCoerce x :: a)
652+
653+instance (Reflectable v Int, HeaderValue a) => HeaderValue (Maximum v a) where
654+ parseHeader s = case (unsafeCoerce :: Either String a -> Either String Int) (parseHeader s :: Either String a) of
655+ Left err -> Left err
656+ Right val ->
657+ let
658+ maxVal = reflectType (Proxy :: Proxy v)
659+ in
660+ if val > maxVal then Left ("Value " <> show val <> " exceeds maximum " <> show maxVal)
661+ else Right (unsafeCoerce val)
662+ printHeader x = printHeader (unsafeCoerce x :: a)
663+
664+instance (IsSymbol pat, HeaderValue a) => HeaderValue (Pattern pat a) where
665+ parseHeader s = case Regex.regex (reflectSymbol (Proxy :: Proxy pat)) noFlags of
666+ Left err -> Left ("Invalid regex pattern: " <> err)
667+ Right re ->
668+ if Regex.test re s then unsafeCoerce (parseHeader s :: Either String a)
669+ else Left ("Value '" <> s <> "' does not match pattern: " <> reflectSymbol (Proxy :: Proxy pat))
670+ printHeader x = printHeader (unsafeCoerce x :: a)
671+
672+instance (Reflectable v Int, HeaderValue a) => HeaderValue (MinLength v a) where
673+ parseHeader s =
674+ let
675+ minLen = reflectType (Proxy :: Proxy v)
676+ in
677+ if SCU.length s < minLen then Left ("String length " <> show (SCU.length s) <> " is less than minimum " <> show minLen)
678+ else unsafeCoerce (parseHeader s :: Either String a)
679+ printHeader x = printHeader (unsafeCoerce x :: a)
680+
681+instance (Reflectable v Int, HeaderValue a) => HeaderValue (MaxLength v a) where
682+ parseHeader s =
683+ let
684+ maxLen = reflectType (Proxy :: Proxy v)
685+ in
686+ if SCU.length s > maxLen then Left ("String length " <> show (SCU.length s) <> " exceeds maximum " <> show maxLen)
687+ else unsafeCoerce (parseHeader s :: Either String a)
688+ printHeader x = printHeader (unsafeCoerce x :: a)
689+
690+instance HeaderValue a => HeaderValue (Title t a) where
691+ parseHeader s = unsafeCoerce (parseHeader s :: Either String a)
692+ printHeader x = printHeader (unsafeCoerce x :: a)
693+
694+instance HeaderValue a => HeaderValue (Nullable a) where
695+ parseHeader s = unsafeCoerce (parseHeader s :: Either String a)
696+ printHeader x = printHeader (unsafeCoerce x :: a)
697+
698+instance HeaderValue a => HeaderValue (Default val a) where
699+ parseHeader s = unsafeCoerce (parseHeader s :: Either String a)
700+ printHeader x = printHeader (unsafeCoerce x :: a)
701+
702+instance HeaderValue a => HeaderValue (Deprecated a) where
703+ parseHeader s = unsafeCoerce (parseHeader s :: Either String a)
704+ printHeader x = printHeader (unsafeCoerce x :: a)
705+
706+instance HeaderValue a => HeaderValue (Enum a) where
707+ parseHeader s = unsafeCoerce (parseHeader s :: Either String a)
708+ printHeader x = printHeader (unsafeCoerce x :: a)
709+
710+-- HeaderValueType instances
711+instance HeaderValueType a => HeaderValueType (Description desc a) where
712+ headerValueType _ = headerValueType (Proxy :: Proxy a)
713+
714+instance HeaderValueType a => HeaderValueType (Example ex a) where
715+ headerValueType _ = headerValueType (Proxy :: Proxy a)
716+
717+instance HeaderValueType a => HeaderValueType (Format fmt a) where
718+ headerValueType _ = headerValueType (Proxy :: Proxy a)
719+
720+instance HeaderValueType a => HeaderValueType (Minimum v a) where
721+ headerValueType _ = headerValueType (Proxy :: Proxy a)
722+
723+instance HeaderValueType a => HeaderValueType (Maximum v a) where
724+ headerValueType _ = headerValueType (Proxy :: Proxy a)
725+
726+instance HeaderValueType a => HeaderValueType (Pattern pat a) where
727+ headerValueType _ = headerValueType (Proxy :: Proxy a)
728+
729+instance HeaderValueType a => HeaderValueType (MinLength v a) where
730+ headerValueType _ = headerValueType (Proxy :: Proxy a)
731+
732+instance HeaderValueType a => HeaderValueType (MaxLength v a) where
733+ headerValueType _ = headerValueType (Proxy :: Proxy a)
734+
735+instance HeaderValueType a => HeaderValueType (Title t a) where
736+ headerValueType _ = headerValueType (Proxy :: Proxy a)
737+
738+instance HeaderValueType a => HeaderValueType (Nullable a) where
739+ headerValueType _ = headerValueType (Proxy :: Proxy a)
740+
741+instance HeaderValueType a => HeaderValueType (Default val a) where
742+ headerValueType _ = headerValueType (Proxy :: Proxy a)
743+
744+instance HeaderValueType a => HeaderValueType (Deprecated a) where
745+ headerValueType _ = headerValueType (Proxy :: Proxy a)
746+
747+instance HeaderValueType a => HeaderValueType (Enum a) where
748+ headerValueType _ = headerValueType (Proxy :: Proxy a)
749+
750+-- ParseParam instances
751+instance ParseParam a => ParseParam (Description desc a) where
752+ parseParam s = unsafeCoerce (parseParam s :: Either String a)
753+
754+instance ParseParam a => ParseParam (Example ex a) where
755+ parseParam s = unsafeCoerce (parseParam s :: Either String a)
756+
757+instance ParseParam a => ParseParam (Format fmt a) where
758+ parseParam s = unsafeCoerce (parseParam s :: Either String a)
759+
760+instance (Reflectable v Int, ParseParam a) => ParseParam (Minimum v a) where
761+ parseParam s = case (unsafeCoerce :: Either String a -> Either String Int) (parseParam s :: Either String a) of
762+ Left err -> Left err
763+ Right val ->
764+ let
765+ minVal = reflectType (Proxy :: Proxy v)
766+ in
767+ if val < minVal then Left ("Value " <> show val <> " is less than minimum " <> show minVal)
768+ else Right (unsafeCoerce val)
769+
770+instance (Reflectable v Int, ParseParam a) => ParseParam (Maximum v a) where
771+ parseParam s = case (unsafeCoerce :: Either String a -> Either String Int) (parseParam s :: Either String a) of
772+ Left err -> Left err
773+ Right val ->
774+ let
775+ maxVal = reflectType (Proxy :: Proxy v)
776+ in
777+ if val > maxVal then Left ("Value " <> show val <> " exceeds maximum " <> show maxVal)
778+ else Right (unsafeCoerce val)
779+
780+instance (IsSymbol pat, ParseParam a) => ParseParam (Pattern pat a) where
781+ parseParam s = case Regex.regex (reflectSymbol (Proxy :: Proxy pat)) noFlags of
782+ Left err -> Left ("Invalid regex pattern: " <> err)
783+ Right re ->
784+ if Regex.test re s then unsafeCoerce (parseParam s :: Either String a)
785+ else Left ("Value '" <> s <> "' does not match pattern: " <> reflectSymbol (Proxy :: Proxy pat))
786+
787+instance (Reflectable v Int, ParseParam a) => ParseParam (MinLength v a) where
788+ parseParam s =
789+ let
790+ minLen = reflectType (Proxy :: Proxy v)
791+ in
792+ if SCU.length s < minLen then Left ("String length " <> show (SCU.length s) <> " is less than minimum " <> show minLen)
793+ else unsafeCoerce (parseParam s :: Either String a)
794+
795+instance (Reflectable v Int, ParseParam a) => ParseParam (MaxLength v a) where
796+ parseParam s =
797+ let
798+ maxLen = reflectType (Proxy :: Proxy v)
799+ in
800+ if SCU.length s > maxLen then Left ("String length " <> show (SCU.length s) <> " exceeds maximum " <> show maxLen)
801+ else unsafeCoerce (parseParam s :: Either String a)
802+
803+instance ParseParam a => ParseParam (Title t a) where
804+ parseParam s = unsafeCoerce (parseParam s :: Either String a)
805+
806+instance ParseParam a => ParseParam (Nullable a) where
807+ parseParam s = unsafeCoerce (parseParam s :: Either String a)
808+
809+instance ParseParam a => ParseParam (Default val a) where
810+ parseParam s = unsafeCoerce (parseParam s :: Either String a)
811+
812+instance ParseParam a => ParseParam (Deprecated a) where
813+ parseParam s = unsafeCoerce (parseParam s :: Either String a)
814+
815+instance ParseParam a => ParseParam (Enum a) where
816+ parseParam s = unsafeCoerce (parseParam s :: Either String a)
817+
818+-- WriteForeign instances
819+instance WriteForeign a => WriteForeign (Description desc a) where
820+ writeImpl x = writeImpl (unsafeCoerce x :: a)
821+
822+instance WriteForeign a => WriteForeign (Example ex a) where
823+ writeImpl x = writeImpl (unsafeCoerce x :: a)
824+
825+instance WriteForeign a => WriteForeign (Format fmt a) where
826+ writeImpl x = writeImpl (unsafeCoerce x :: a)
827+
828+instance WriteForeign a => WriteForeign (Minimum v a) where
829+ writeImpl x = writeImpl (unsafeCoerce x :: a)
830+
831+instance WriteForeign a => WriteForeign (Maximum v a) where
832+ writeImpl x = writeImpl (unsafeCoerce x :: a)
833+
834+instance WriteForeign a => WriteForeign (Pattern pat a) where
835+ writeImpl x = writeImpl (unsafeCoerce x :: a)
836+
837+instance WriteForeign a => WriteForeign (MinLength v a) where
838+ writeImpl x = writeImpl (unsafeCoerce x :: a)
839+
840+instance WriteForeign a => WriteForeign (MaxLength v a) where
841+ writeImpl x = writeImpl (unsafeCoerce x :: a)
842+
843+instance WriteForeign a => WriteForeign (Title t a) where
844+ writeImpl x = writeImpl (unsafeCoerce x :: a)
845+
846+instance WriteForeign a => WriteForeign (Nullable a) where
847+ writeImpl x = writeImpl (unsafeCoerce x :: a)
848+
849+instance WriteForeign a => WriteForeign (Default val a) where
850+ writeImpl x = writeImpl (unsafeCoerce x :: a)
851+
852+instance WriteForeign a => WriteForeign (Deprecated a) where
853+ writeImpl x = writeImpl (unsafeCoerce x :: a)
854+
855+instance WriteForeign a => WriteForeign (Enum a) where
856+ writeImpl x = writeImpl (unsafeCoerce x :: a)
857+
858+-- ReadForeign instances
859+instance ReadForeign a => ReadForeign (Description desc a) where
860+ readImpl = unsafeCoerce (readImpl :: Foreign -> _ a)
861+
862+instance ReadForeign a => ReadForeign (Example ex a) where
863+ readImpl = unsafeCoerce (readImpl :: Foreign -> _ a)
864+
865+instance ReadForeign a => ReadForeign (Format fmt a) where
866+ readImpl = unsafeCoerce (readImpl :: Foreign -> _ a)
867+
868+instance ReadForeign a => ReadForeign (Minimum v a) where
869+ readImpl = unsafeCoerce (readImpl :: Foreign -> _ a)
870+
871+instance ReadForeign a => ReadForeign (Maximum v a) where
872+ readImpl = unsafeCoerce (readImpl :: Foreign -> _ a)
873+
874+instance ReadForeign a => ReadForeign (Pattern pat a) where
875+ readImpl = unsafeCoerce (readImpl :: Foreign -> _ a)
876+
877+instance ReadForeign a => ReadForeign (MinLength v a) where
878+ readImpl = unsafeCoerce (readImpl :: Foreign -> _ a)
879+
880+instance ReadForeign a => ReadForeign (MaxLength v a) where
881+ readImpl = unsafeCoerce (readImpl :: Foreign -> _ a)
882+
883+instance ReadForeign a => ReadForeign (Title t a) where
884+ readImpl = unsafeCoerce (readImpl :: Foreign -> _ a)
885+
886+instance ReadForeign a => ReadForeign (Nullable a) where
887+ readImpl = unsafeCoerce (readImpl :: Foreign -> _ a)
888+
889+instance ReadForeign a => ReadForeign (Default val a) where
890+ readImpl = unsafeCoerce (readImpl :: Foreign -> _ a)
891+
892+instance ReadForeign a => ReadForeign (Deprecated a) where
893+ readImpl = unsafeCoerce (readImpl :: Foreign -> _ a)
894+
895+instance ReadForeign a => ReadForeign (Enum a) where
896+ readImpl = unsafeCoerce (readImpl :: Foreign -> _ a)
src/Yoga/HTTP/API/Route/RenderMethod.pursnew file+34
@@ -0,0 +1,34 @@
1+module Yoga.HTTP.API.Route.RenderMethod
2+ ( class RenderMethod
3+ , renderMethod
4+ ) where
5+
6+import Type.Proxy (Proxy(..))
7+import Yoga.HTTP.API.Route.Method (DELETE, GET, PATCH, POST, PUT)
8+
9+--------------------------------------------------------------------------------
10+-- RenderMethod Typeclass
11+--------------------------------------------------------------------------------
12+
13+-- | Render HTTP method to lowercase string (OpenAPI format)
14+class RenderMethod (method :: Type) where
15+ renderMethod :: Proxy method -> String
16+
17+--------------------------------------------------------------------------------
18+-- HTTP Method Instances
19+--------------------------------------------------------------------------------
20+
21+instance RenderMethod GET where
22+ renderMethod _ = "get"
23+
24+instance RenderMethod POST where
25+ renderMethod _ = "post"
26+
27+instance RenderMethod PUT where
28+ renderMethod _ = "put"
29+
30+instance RenderMethod DELETE where
31+ renderMethod _ = "delete"
32+
33+instance RenderMethod PATCH where
34+ renderMethod _ = "patch"
src/Yoga/HTTP/API/Route/Response.pursnew file+170
@@ -0,0 +1,170 @@
1+module Yoga.HTTP.API.Route.Response
2+ ( Response(..)
3+ , ResponseData
4+ , class ToResponse
5+ , class ToResponseRL
6+ , respond
7+ , respondWith
8+ , respondNoHeaders
9+ , respondNoBody
10+ , respondNothing
11+ , respondNoContent
12+ , respondNotModified
13+ , module Data.Variant
14+ ) where
15+
16+import Data.Symbol (class IsSymbol)
17+import Data.Unit (Unit, unit)
18+import Data.Variant (Variant)
19+import Data.Variant as Variant
20+import Prim.Row (class Cons)
21+import Prim.RowList as RL
22+import Type.Proxy (Proxy(..))
23+import Unsafe.Coerce (unsafeCoerce)
24+
25+--------------------------------------------------------------------------------
26+-- Response Data Type
27+--------------------------------------------------------------------------------
28+
29+-- | Response data combining headers and body
30+-- | This is a data type (not type alias) to work with type class instances
31+data Response headers body = Response
32+ { headers :: Record headers
33+ , body :: body
34+ }
35+
36+-- | Deprecated alias for backwards compatibility
37+type ResponseData headers body = Response headers body
38+
39+--------------------------------------------------------------------------------
40+-- ToResponse: Convert record syntax to Response type
41+--------------------------------------------------------------------------------
42+
43+-- | Convert user-friendly record syntax to Response type.
44+-- |
45+-- | Supports:
46+-- | Response headers body → Response headers body (identity)
47+-- | { body :: User } → Response () User
48+-- | { headers :: { "Location" :: String }, body :: User } → Response ("Location" :: String) User
49+class ToResponse (recordType :: Type) (headers :: Row Type) (body :: Type) | recordType -> headers body
50+
51+-- Identity instance: Response is already Response
52+instance toResponseIdentity :: ToResponse (Response headers body) headers body
53+
54+-- Record instance: delegate to RowList-based helper
55+else instance toResponseRecord ::
56+ ( RL.RowToList recordRow rl
57+ , ToResponseRL rl headers body
58+ ) =>
59+ ToResponse (Record recordRow) headers body
60+
61+-- | RowList-based helper to distinguish records with/without headers.
62+-- | Instance heads are distinguishable by the RowList structure.
63+class ToResponseRL (rl :: RL.RowList Type) (headers :: Row Type) (body :: Type) | rl -> headers body
64+
65+-- { body :: b, headers :: Record h } (headers comes before body in sorted RowList)
66+instance toResponseRLBodyHeaders ::
67+ ToResponseRL (RL.Cons "body" body (RL.Cons "headers" (Record headers) RL.Nil)) headers body
68+
69+-- { body :: b } (no headers)
70+else instance toResponseRLBodyOnly ::
71+ ToResponseRL (RL.Cons "body" body RL.Nil) () body
72+
73+--------------------------------------------------------------------------------
74+-- Response Construction Helpers
75+--------------------------------------------------------------------------------
76+
77+-- | Construct a variant response with full control over headers and body
78+-- |
79+-- | Example:
80+-- | respond (Proxy :: _ "created")
81+-- | (Response { headers: { "Location": "/users/123" }, body: user })
82+respond
83+ :: forall label headers body r1 r2
84+ . IsSymbol label
85+ => Cons label (Response headers body) r1 r2
86+ => Proxy label
87+ -> Response headers body
88+ -> Variant r2
89+respond label rd = Variant.inj label rd
90+
91+-- | Construct a variant response with separate headers and body arguments
92+-- |
93+-- | Example:
94+-- | respondWith (Proxy :: _ "created")
95+-- | { "Location": "/users/123" }
96+-- | user
97+respondWith
98+ :: forall label headers body r1 r2
99+ . IsSymbol label
100+ => Cons label (Response headers body) r1 r2
101+ => Proxy label
102+ -> Record headers
103+ -> body
104+ -> Variant r2
105+respondWith label headers body =
106+ Variant.inj label (Response { headers, body })
107+
108+-- | Construct a variant response with no custom headers (most common case)
109+-- |
110+-- | Example:
111+-- | respondNoHeaders (Proxy :: _ "ok") user
112+-- | respondNoHeaders (Proxy :: _ "notFound") { error: "Not found" }
113+respondNoHeaders
114+ :: forall @label body r1 r2
115+ . IsSymbol label
116+ => Cons label (Response () body) r1 r2
117+ => body
118+ -> Variant r2
119+respondNoHeaders body =
120+ Variant.inj (Proxy :: Proxy label) (Response { headers: {}, body })
121+
122+-- | Construct a variant response with headers but no body
123+-- |
124+-- | Example:
125+-- | respondNoBody (Proxy :: _ "noContent")
126+-- | { "X-Resource-Id": "123" }
127+respondNoBody
128+ :: forall label headers r1 r2
129+ . IsSymbol label
130+ => Cons label (Response headers Unit) r1 r2
131+ => Proxy label
132+ -> Record headers
133+ -> Variant r2
134+respondNoBody label headers =
135+ Variant.inj label (Response { headers, body: unit })
136+
137+-- | Construct a variant response with neither headers nor body
138+-- |
139+-- | Example:
140+-- | respondNothing (Proxy :: _ "noContent")
141+-- | respondNothing (Proxy :: _ "notModified")
142+respondNothing
143+ :: forall @label r1 r2
144+ . IsSymbol label
145+ => Cons label (Response () Unit) r1 r2
146+ => Variant r2
147+respondNothing =
148+ Variant.inj (Proxy :: Proxy label) (Response { headers: {}, body: unit })
149+
150+-- | Construct a 204 No Content response (no headers, no body)
151+-- |
152+-- | Example:
153+-- | respondNoContent
154+respondNoContent
155+ :: forall r1 r2
156+ . Cons "noContent" (Response () Unit) r1 r2
157+ => Variant r2
158+respondNoContent =
159+ unsafeCoerce (Variant.inj (Proxy :: Proxy "noContent") (Response { headers: {}, body: unit }))
160+
161+-- | Construct a 304 Not Modified response (no headers, no body)
162+-- |
163+-- | Example:
164+-- | respondNotModified
165+respondNotModified
166+ :: forall r1 r2
167+ . Cons "notModified" (Response () Unit) r1 r2
168+ => Variant r2
169+respondNotModified =
170+ unsafeCoerce (Variant.inj (Proxy :: Proxy "notModified") (Response { headers: {}, body: unit }))
src/Yoga/HTTP/API/Route/Route.pursnew file+127
@@ -0,0 +1,127 @@
1+module Yoga.HTTP.API.Route.Route
2+ ( Route(..)
3+ , route
4+ , class ConvertResponseVariant
5+ , class ConvertResponseVariantRL
6+ ) where
7+
8+import Prelude
9+
10+import Data.Array as Array
11+import Data.Maybe (Maybe(..))
12+import Prim.Row (class Cons, class Lacks, class Union)
13+import Prim.RowList (class RowToList, RowList)
14+import Prim.RowList as RL
15+import Type.Proxy (Proxy(..))
16+import Yoga.HTTP.API.Path (class PathPattern, pathPattern)
17+import Yoga.HTTP.API.Route.Handler (Request, class DefaultRequestFields, class SegmentPathParams, class SegmentQueryParams)
18+import Yoga.HTTP.API.Route.OpenAPI (class CollectOperations, class RenderHeadersSchema, renderHeadersSchema, class RenderPathParamsSchema, renderPathParamsSchema, class RenderQueryParamsSchema, renderQueryParamsSchema, class RenderRequestBodySchema, renderRequestBodySchema, class RenderVariantResponseSchemaRL, renderVariantResponseSchemaRL, class DetectSecurity, detectSecurity, class ToOpenAPI, toOpenAPIImpl)
19+import Yoga.HTTP.API.Route.OpenAPIMetadata (class HasOperationMetadata, operationMetadata)
20+import Yoga.HTTP.API.Route.RenderMethod (class RenderMethod, renderMethod)
21+import Yoga.HTTP.API.Route.Response (Response, class ToResponse)
22+import Yoga.JSON (writeJSON)
23+
24+data Route :: forall k. Type -> k -> Type -> Row Type -> Type
25+data Route method segments request respVariant = Route
26+
27+--------------------------------------------------------------------------------
28+-- ConvertResponseVariant: Convert record syntax to Response types in variant
29+--------------------------------------------------------------------------------
30+
31+-- | Convert a variant row with record syntax to Response types.
32+-- | Input: ( ok :: { body :: User }, notFound :: { body :: ErrorMsg } )
33+-- | Output: ( ok :: Response () User, notFound :: Response () ErrorMsg )
34+class ConvertResponseVariant (userRow :: Row Type) (internalRow :: Row Type) | userRow -> internalRow
35+
36+instance convertResponseVariantImpl ::
37+ ( RowToList userRow rl
38+ , ConvertResponseVariantRL rl () internalRow
39+ ) =>
40+ ConvertResponseVariant userRow internalRow
41+
42+class ConvertResponseVariantRL (rl :: RowList Type) (acc :: Row Type) (out :: Row Type) | rl acc -> out
43+
44+instance convertResponseVariantRLNil :: ConvertResponseVariantRL RL.Nil acc acc
45+
46+instance convertResponseVariantRLCons ::
47+ ( ToResponse recordType headers body
48+ , ConvertResponseVariantRL tail acc1 acc2
49+ , Cons label (Response headers body) acc2 out
50+ , Lacks label acc2
51+ ) =>
52+ ConvertResponseVariantRL (RL.Cons label recordType tail) acc1 out
53+
54+-- | Smart constructor for Route that allows partial request records.
55+-- |
56+-- | Users can specify only the fields they need:
57+-- | route (Proxy :: _ GET) (Proxy :: _ path) (Proxy :: _ (Request {})) -- no headers or body
58+-- | route (Proxy :: _ GET) (Proxy :: _ path) (Proxy :: _ (Request { body :: JSON User })) -- only body
59+-- | route (Proxy :: _ GET) (Proxy :: _ path) (Proxy :: _ (Request { headers :: { auth :: String } })) -- only headers
60+route
61+ :: forall method segments partialRequest o_ fullHeaders fullBody userRespVariant internalRespVariant
62+ . Union partialRequest o_ (headers :: fullHeaders, body :: fullBody)
63+ => ConvertResponseVariant userRespVariant internalRespVariant
64+ => Proxy method
65+ -> Proxy segments
66+ -> Proxy (Request (Record partialRequest))
67+ -> Proxy userRespVariant
68+ -> Route method segments (Request (Record partialRequest)) userRespVariant
69+route _ _ _ _ = Route
70+
71+instance
72+ ( RenderMethod method
73+ , PathPattern segments
74+ , DefaultRequestFields partialRequest reqHeaders encoding
75+ , RenderHeadersSchema reqHeaders
76+ , DetectSecurity reqHeaders
77+ , SegmentPathParams segments pathParams
78+ , RenderPathParamsSchema pathParams
79+ , SegmentQueryParams segments queryParams
80+ , RenderQueryParamsSchema queryParams
81+ , RenderRequestBodySchema encoding
82+ , RowToList userResp rl
83+ , RenderVariantResponseSchemaRL rl
84+ , HasOperationMetadata (Route method segments (Request (Record partialRequest)) userResp)
85+ ) =>
86+ ToOpenAPI (Route method segments (Request (Record partialRequest)) userResp) where
87+ toOpenAPIImpl proxy =
88+ let
89+ methodStr = renderMethod (Proxy :: Proxy method)
90+ pathStr = pathPattern (Proxy :: Proxy segments)
91+ headerParams = renderHeadersSchema (Proxy :: Proxy reqHeaders)
92+ pathParams = renderPathParamsSchema (Proxy :: Proxy pathParams)
93+ queryParams = renderQueryParamsSchema (Proxy :: Proxy queryParams)
94+ parameters = headerParams <> pathParams <> queryParams
95+ requestBody = renderRequestBodySchema (Proxy :: Proxy encoding)
96+ responses = renderVariantResponseSchemaRL (Proxy :: Proxy rl)
97+ security = detectSecurity (Proxy :: Proxy reqHeaders)
98+ metadata = operationMetadata proxy
99+ operation =
100+ { method: methodStr
101+ , path: pathStr
102+ , parameters
103+ , requestBody
104+ , responses
105+ , security: if Array.length security == 0 then Nothing else Just security
106+ , summary: metadata.summary
107+ , description: metadata.description
108+ , operationId: metadata.operationId
109+ , tags: metadata.tags
110+ , deprecated: if metadata.deprecated then Just true else Nothing
111+ }
112+ in
113+ writeJSON operation
114+
115+-- CollectOperations instance for Route
116+instance
117+ ( RenderMethod method
118+ , PathPattern segments
119+ , ToOpenAPI (Route method segments request resp)
120+ ) =>
121+ CollectOperations (Route method segments request resp) where
122+ collectOperations _ =
123+ [ { method: renderMethod (Proxy :: Proxy method)
124+ , path: pathPattern (Proxy :: Proxy segments)
125+ , operation: toOpenAPIImpl (Proxy :: Proxy (Route method segments request resp))
126+ }
127+ ]
src/Yoga/HTTP/API/Route/RouteHandler.pursnew file+68
@@ -0,0 +1,68 @@
1+module Yoga.HTTP.API.Route.RouteHandler
2+ ( Handler
3+ , class RouteHandler
4+ , mkHandler
5+ , runHandler
6+ ) where
7+
8+import Prim.Row as Row
9+import Unsafe.Coerce (unsafeCoerce)
10+import Yoga.HTTP.API.Route.Handler
11+ ( HandlerFn
12+ , Request
13+ , class DefaultRequestFields
14+ , class EncodingBody
15+ , class SegmentPathParams
16+ , class SegmentQueryParams
17+ )
18+import Yoga.HTTP.API.Route.Route (Route, class ConvertResponseVariant)
19+
20+-- | Type class that computes the handler function type from a Route type.
21+class
22+ RouteHandler
23+ (route :: Type)
24+ (pathParams :: Row Type)
25+ (queryParams :: Row Type)
26+ (reqHeaders :: Row Type)
27+ (body :: Type)
28+ (respVariant :: Row Type)
29+ | route -> pathParams queryParams reqHeaders body respVariant
30+
31+instance
32+ ( Row.Union partialRequest o_ (headers :: Record fullHeaders, body :: fullEncoding)
33+ , DefaultRequestFields partialRequest fullHeaders fullEncoding
34+ , SegmentPathParams segments pathParams
35+ , SegmentQueryParams segments queryParams
36+ , EncodingBody fullEncoding body
37+ , ConvertResponseVariant userResp respVariant
38+ ) =>
39+ RouteHandler
40+ (Route method segments (Request (Record partialRequest)) userResp)
41+ pathParams
42+ queryParams
43+ fullHeaders
44+ body
45+ respVariant
46+
47+-- | A handler tied to a specific route type.
48+-- |
49+-- | Usage:
50+-- | userHandler :: Handler UserRoute
51+-- | userHandler = mkHandler \{ path } -> ...
52+foreign import data Handler :: Type -> Type
53+
54+-- | Create a Handler from a function matching the route's type.
55+mkHandler
56+ :: forall route pathParams queryParams reqHeaders body respVariant
57+ . RouteHandler route pathParams queryParams reqHeaders body respVariant
58+ => HandlerFn pathParams queryParams reqHeaders body respVariant
59+ -> Handler route
60+mkHandler = unsafeCoerce
61+
62+-- | Extract the handler function from a Handler.
63+runHandler
64+ :: forall route pathParams queryParams reqHeaders body respVariant
65+ . RouteHandler route pathParams queryParams reqHeaders body respVariant
66+ => Handler route
67+ -> HandlerFn pathParams queryParams reqHeaders body respVariant
68+runHandler = unsafeCoerce
src/Yoga/HTTP/API/Route/StatusCode.pursnew file+234
@@ -0,0 +1,234 @@
1+module Yoga.HTTP.API.Route.StatusCode
2+ ( class StatusCodeMap
3+ , statusCodeFor
4+ , statusCodeToString
5+ , StatusCode(..)
6+ ) where
7+
8+import Prelude
9+
10+import Type.Proxy (Proxy(..))
11+import Data.Newtype (class Newtype)
12+
13+newtype StatusCode = StatusCode Int
14+
15+derive instance Newtype StatusCode _
16+derive newtype instance Show StatusCode
17+derive newtype instance Eq StatusCode
18+derive newtype instance Ord StatusCode
19+
20+--------------------------------------------------------------------------------
21+-- Status Code Mapping
22+--------------------------------------------------------------------------------
23+
24+-- | Map variant constructor names (Symbols) to HTTP status codes
25+-- |
26+-- | This typeclass enables convention-based status code mapping where
27+-- | the variant label name determines the HTTP status code.
28+-- |
29+-- | Users can extend this by adding their own instances for custom status codes.
30+class StatusCodeMap (sym :: Symbol) where
31+ statusCodeFor :: Proxy sym -> StatusCode
32+
33+--------------------------------------------------------------------------------
34+-- Standard HTTP Status Code Instances
35+--------------------------------------------------------------------------------
36+
37+-- 1xx Informational
38+instance StatusCodeMap "continue" where
39+ statusCodeFor _ = StatusCode 100
40+
41+instance StatusCodeMap "switchingProtocols" where
42+ statusCodeFor _ = StatusCode 101
43+
44+instance StatusCodeMap "processing" where
45+ statusCodeFor _ = StatusCode 102
46+
47+instance StatusCodeMap "earlyHints" where
48+ statusCodeFor _ = StatusCode 103
49+
50+-- 2xx Success
51+instance StatusCodeMap "ok" where
52+ statusCodeFor _ = StatusCode 200
53+
54+instance StatusCodeMap "created" where
55+ statusCodeFor _ = StatusCode 201
56+
57+instance StatusCodeMap "accepted" where
58+ statusCodeFor _ = StatusCode 202
59+
60+instance StatusCodeMap "nonAuthoritativeInformation" where
61+ statusCodeFor _ = StatusCode 203
62+
63+instance StatusCodeMap "noContent" where
64+ statusCodeFor _ = StatusCode 204
65+
66+instance StatusCodeMap "resetContent" where
67+ statusCodeFor _ = StatusCode 205
68+
69+instance StatusCodeMap "partialContent" where
70+ statusCodeFor _ = StatusCode 206
71+
72+instance StatusCodeMap "multiStatus" where
73+ statusCodeFor _ = StatusCode 207
74+
75+instance StatusCodeMap "alreadyReported" where
76+ statusCodeFor _ = StatusCode 208
77+
78+instance StatusCodeMap "imUsed" where
79+ statusCodeFor _ = StatusCode 226
80+
81+-- 3xx Redirection
82+instance StatusCodeMap "multipleChoices" where
83+ statusCodeFor _ = StatusCode 300
84+
85+instance StatusCodeMap "movedPermanently" where
86+ statusCodeFor _ = StatusCode 301
87+
88+instance StatusCodeMap "found" where
89+ statusCodeFor _ = StatusCode 302
90+
91+instance StatusCodeMap "seeOther" where
92+ statusCodeFor _ = StatusCode 303
93+
94+instance StatusCodeMap "notModified" where
95+ statusCodeFor _ = StatusCode 304
96+
97+instance StatusCodeMap "useProxy" where
98+ statusCodeFor _ = StatusCode 305
99+
100+instance StatusCodeMap "temporaryRedirect" where
101+ statusCodeFor _ = StatusCode 307
102+
103+instance StatusCodeMap "permanentRedirect" where
104+ statusCodeFor _ = StatusCode 308
105+
106+-- 4xx Client Errors
107+instance StatusCodeMap "badRequest" where
108+ statusCodeFor _ = StatusCode 400
109+
110+instance StatusCodeMap "unauthorized" where
111+ statusCodeFor _ = StatusCode 401
112+
113+instance StatusCodeMap "paymentRequired" where
114+ statusCodeFor _ = StatusCode 402
115+
116+instance StatusCodeMap "forbidden" where
117+ statusCodeFor _ = StatusCode 403
118+
119+instance StatusCodeMap "notFound" where
120+ statusCodeFor _ = StatusCode 404
121+
122+instance StatusCodeMap "methodNotAllowed" where
123+ statusCodeFor _ = StatusCode 405
124+
125+instance StatusCodeMap "notAcceptable" where
126+ statusCodeFor _ = StatusCode 406
127+
128+instance StatusCodeMap "proxyAuthenticationRequired" where
129+ statusCodeFor _ = StatusCode 407
130+
131+instance StatusCodeMap "requestTimeout" where
132+ statusCodeFor _ = StatusCode 408
133+
134+instance StatusCodeMap "conflict" where
135+ statusCodeFor _ = StatusCode 409
136+
137+instance StatusCodeMap "gone" where
138+ statusCodeFor _ = StatusCode 410
139+
140+instance StatusCodeMap "lengthRequired" where
141+ statusCodeFor _ = StatusCode 411
142+
143+instance StatusCodeMap "preconditionFailed" where
144+ statusCodeFor _ = StatusCode 412
145+
146+instance StatusCodeMap "payloadTooLarge" where
147+ statusCodeFor _ = StatusCode 413
148+
149+instance StatusCodeMap "uriTooLong" where
150+ statusCodeFor _ = StatusCode 414
151+
152+instance StatusCodeMap "unsupportedMediaType" where
153+ statusCodeFor _ = StatusCode 415
154+
155+instance StatusCodeMap "rangeNotSatisfiable" where
156+ statusCodeFor _ = StatusCode 416
157+
158+instance StatusCodeMap "expectationFailed" where
159+ statusCodeFor _ = StatusCode 417
160+
161+instance StatusCodeMap "imATeapot" where
162+ statusCodeFor _ = StatusCode 418
163+
164+instance StatusCodeMap "misdirectedRequest" where
165+ statusCodeFor _ = StatusCode 421
166+
167+instance StatusCodeMap "unprocessableEntity" where
168+ statusCodeFor _ = StatusCode 422
169+
170+instance StatusCodeMap "locked" where
171+ statusCodeFor _ = StatusCode 423
172+
173+instance StatusCodeMap "failedDependency" where
174+ statusCodeFor _ = StatusCode 424
175+
176+instance StatusCodeMap "tooEarly" where
177+ statusCodeFor _ = StatusCode 425
178+
179+instance StatusCodeMap "upgradeRequired" where
180+ statusCodeFor _ = StatusCode 426
181+
182+instance StatusCodeMap "preconditionRequired" where
183+ statusCodeFor _ = StatusCode 428
184+
185+instance StatusCodeMap "tooManyRequests" where
186+ statusCodeFor _ = StatusCode 429
187+
188+instance StatusCodeMap "requestHeaderFieldsTooLarge" where
189+ statusCodeFor _ = StatusCode 431
190+
191+instance StatusCodeMap "unavailableForLegalReasons" where
192+ statusCodeFor _ = StatusCode 451
193+
194+-- 5xx Server Errors
195+instance StatusCodeMap "internalServerError" where
196+ statusCodeFor _ = StatusCode 500
197+
198+instance StatusCodeMap "notImplemented" where
199+ statusCodeFor _ = StatusCode 501
200+
201+instance StatusCodeMap "badGateway" where
202+ statusCodeFor _ = StatusCode 502
203+
204+instance StatusCodeMap "serviceUnavailable" where
205+ statusCodeFor _ = StatusCode 503
206+
207+instance StatusCodeMap "gatewayTimeout" where
208+ statusCodeFor _ = StatusCode 504
209+
210+instance StatusCodeMap "httpVersionNotSupported" where
211+ statusCodeFor _ = StatusCode 505
212+
213+instance StatusCodeMap "variantAlsoNegotiates" where
214+ statusCodeFor _ = StatusCode 506
215+
216+instance StatusCodeMap "insufficientStorage" where
217+ statusCodeFor _ = StatusCode 507
218+
219+instance StatusCodeMap "loopDetected" where
220+ statusCodeFor _ = StatusCode 508
221+
222+instance StatusCodeMap "notExtended" where
223+ statusCodeFor _ = StatusCode 510
224+
225+instance StatusCodeMap "networkAuthenticationRequired" where
226+ statusCodeFor _ = StatusCode 511
227+
228+--------------------------------------------------------------------------------
229+-- Helper Functions
230+--------------------------------------------------------------------------------
231+
232+-- | Convert StatusCode to String for OpenAPI generation
233+statusCodeToString :: StatusCode -> String
234+statusCodeToString (StatusCode n) = show n