git.iko.soy/rowtype-yoga/purescript-yoga-fetch-om
on main help sign in
Replace client argument adapter with direct calls
1cd6e7dMark
.github/workflows/ci.yml+1
@@ -14,3 +14,4 @@ jobs:
14 - uses: rowtype-yoga/purescript-yoga-actions/.github/actions/setup@main
14 - uses: rowtype-yoga/purescript-yoga-actions/.github/actions/setup@main
15 - run: spago build
15 - run: spago build
16 - run: spago test
16 - run: spago test
17+ - run: bun compile-fail-tests/run.js
README.md+24−6
@@ -8,6 +8,12 @@ Derive type-safe fetch clients from `purescript-yoga-http-api` route definitions
8spago install yoga-fetch-om
8spago install yoga-fetch-om
9```
9```
10
10
11+Optional query examples below use [`justifill`](https://pursuit.purescript.org/packages/purescript-justifill):
12+
13+```bash
14+spago install justifill
15+```
16+
11## Quick Start
17## Quick Start
12
18
13### Example 1: Simple GET and POST Requests
19### Example 1: Simple GET and POST Requests
@@ -30,8 +36,13 @@ main = do
30### Example 2: Type-Safe API Client with Full CRUD
36### Example 2: Type-Safe API Client with Full CRUD
31
37
32```purescript
38```purescript
39+import Justifill (justifill)
40+
33type UserAPI =
41type UserAPI =
42+ { health ::
43+ Route GET "health" {}
44+ ( ok :: { body :: { status :: String } } )
34− { getUser ::
45+ , getUser ::
35 Route GET ("users" / "id" : Int) {}
46 Route GET ("users" / "id" : Int) {}
36 ( ok :: { body :: User }
47 ( ok :: { body :: User }
37 , notFound :: { body :: ErrorMessage }
48 , notFound :: { body :: ErrorMessage }
@@ -59,7 +70,8 @@ type UserAPI =
59
70
60api = client @UserAPI "https://api.example.com"
71api = client @UserAPI "https://api.example.com"
61
72
62−-- Create a user
73+-- Generated methods use ordinary function application.
74+health <- api.health
63user <- api.createUser { name: "Alice", email: "alice@example.com" }
75user <- api.createUser { name: "Alice", email: "alice@example.com" }
64 # handleErrors
76 # handleErrors
65 { badRequest: \err -> do
77 { badRequest: \err -> do
@@ -67,15 +79,21 @@ user <- api.createUser { name: "Alice", email: "alice@example.com" }
67 throw err
79 throw err
68 }
80 }
69
81
70−-- Update with path param + body
82+-- Path parameters and request bodies remain separate arguments.
83+updated <- api.updateUser
84+ { id: user.id }
71−updated <- api.updateUser { id: user.id } { name: "Alice Updated", email: user.email }
85+ { name: "Alice Updated", email: user.email }
72 # handleErrors
86 # handleErrors
73 { notFound: \_ -> throw userNotFound
87 { notFound: \_ -> throw userNotFound
74 , badRequest: \err -> throw validationError
88 , badRequest: \err -> throw validationError
75 }
89 }
76
90
77−-- Query with pagination
91+-- Optional query parameters are explicit `Maybe` fields. Justifill can wrap
92+-- supplied values in `Just` and fill omitted fields with `Nothing`.
78−users <- api.listUsers { limit: 10, offset: 0 }
93+users <- api.listUsers (justifill { limit: 10 })
94+
95+-- The equivalent call without Justifill:
96+allUsers <- api.listUsers { limit: Nothing, offset: Nothing }
79```
97```
80
98
81## More Examples
99## More Examples
bun.lock+108−15
@@ -4,7 +4,16 @@
4 "": {
4 "": {
5 "name": "purescript-yoga-fetch-om",
5 "name": "purescript-yoga-fetch-om",
6 "devDependencies": {
6 "devDependencies": {
7+ "@fastify/cookie": "^11.1.2",
8+ "@fastify/cors": "^11.3.0",
9+ "@fastify/formbody": "^9.0.0",
10+ "@fastify/helmet": "^13.1.0",
11+ "@fastify/multipart": "^10.1.0",
12+ "@fastify/rate-limit": "^11.2.0",
13+ "@fastify/websocket": "^11.3.0",
14+ "argon2": "^0.45.1",
7 "fastify": "^5.7.4",
15 "fastify": "^5.7.4",
16+ "jose": "^6.2.8",
8 "jsdom": "^28.0.0",
17 "jsdom": "^28.0.0",
9 "purescript": "^0.15.15",
18 "purescript": "^0.15.15",
10 "spago": "0.93.42",
19 "spago": "0.93.42",
@@ -33,6 +42,8 @@
33
42
34 "@csstools/css-tokenizer": ["@csstools/css-tokenizer@4.0.0", "", {}, "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA=="],
43 "@csstools/css-tokenizer": ["@csstools/css-tokenizer@4.0.0", "", {}, "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA=="],
35
44
45+ "@epic-web/invariant": ["@epic-web/invariant@1.0.0", "", {}, "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA=="],
46+
36 "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.21.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ=="],
47 "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.21.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ=="],
37
48
38 "@esbuild/android-arm": ["@esbuild/android-arm@0.21.5", "", { "os": "android", "cpu": "arm" }, "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg=="],
49 "@esbuild/android-arm": ["@esbuild/android-arm@0.21.5", "", { "os": "android", "cpu": "arm" }, "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg=="],
@@ -83,16 +94,34 @@
83
94
84 "@fastify/ajv-compiler": ["@fastify/ajv-compiler@4.0.5", "", { "dependencies": { "ajv": "^8.12.0", "ajv-formats": "^3.0.1", "fast-uri": "^3.0.0" } }, "sha512-KoWKW+MhvfTRWL4qrhUwAAZoaChluo0m0vbiJlGMt2GXvL4LVPQEjt8kSpHI3IBq5Rez8fg+XeH3cneztq+C7A=="],
95 "@fastify/ajv-compiler": ["@fastify/ajv-compiler@4.0.5", "", { "dependencies": { "ajv": "^8.12.0", "ajv-formats": "^3.0.1", "fast-uri": "^3.0.0" } }, "sha512-KoWKW+MhvfTRWL4qrhUwAAZoaChluo0m0vbiJlGMt2GXvL4LVPQEjt8kSpHI3IBq5Rez8fg+XeH3cneztq+C7A=="],
85
96
97+ "@fastify/busboy": ["@fastify/busboy@3.2.1", "", {}, "sha512-tgK4O+57iz5ycYNGXE5ZWj1ES03lD2XnnBYWSbU/3wYZRMQzCUq7Ycds/RdyBZQeL5MU4fxBt6lzbIWf/Bickw=="],
98+
99+ "@fastify/cookie": ["@fastify/cookie@11.1.2", "", { "dependencies": { "cookie": "^2.0.0", "fastify-plugin": "^6.0.0" } }, "sha512-Dtrpk/YOGUsbRMvP/8ZqPpwnMRv0qSqodFdoQ2B589Obc7jw4s4Qla+cV72Bsm7WsZJnqlYFX/i7uSBq0xzg6g=="],
100+
101+ "@fastify/cors": ["@fastify/cors@11.3.0", "", { "dependencies": { "fastify-plugin": "^6.0.0", "toad-cache": "^3.7.0" } }, "sha512-ggQGua+xHv1MvePbPr0v//xLYEsCXbWspquXCJS9Ot5YoRXq8J8ZWzHnxDBVnbtXosvistXo6LtNzOJswf64Fw=="],
102+
103+ "@fastify/deepmerge": ["@fastify/deepmerge@3.2.1", "", {}, "sha512-N5Oqvltoa2r9z1tbx4xjky0oRR60v+T47Ic4J1ukoVQcptLOrIdRnCSdTGmOmajZuHVKlTnfcmrjyqsGEW1ztA=="],
104+
86 "@fastify/error": ["@fastify/error@4.2.0", "", {}, "sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ=="],
105 "@fastify/error": ["@fastify/error@4.2.0", "", {}, "sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ=="],
87
106
88 "@fastify/fast-json-stringify-compiler": ["@fastify/fast-json-stringify-compiler@5.0.3", "", { "dependencies": { "fast-json-stringify": "^6.0.0" } }, "sha512-uik7yYHkLr6fxd8hJSZ8c+xF4WafPK+XzneQDPU+D10r5X19GW8lJcom2YijX2+qtFF1ENJlHXKFM9ouXNJYgQ=="],
107 "@fastify/fast-json-stringify-compiler": ["@fastify/fast-json-stringify-compiler@5.0.3", "", { "dependencies": { "fast-json-stringify": "^6.0.0" } }, "sha512-uik7yYHkLr6fxd8hJSZ8c+xF4WafPK+XzneQDPU+D10r5X19GW8lJcom2YijX2+qtFF1ENJlHXKFM9ouXNJYgQ=="],
89
108
109+ "@fastify/formbody": ["@fastify/formbody@9.0.0", "", { "dependencies": { "fast-querystring": "^1.1.2", "fastify-plugin": "^6.0.0" } }, "sha512-T/af26CSrUARBCvsEmv+DJLPfZlrRKESzqironxP1j7qzuLyKcoZtj6MuTGShuKx1THXugoie2oFbUJxXfGFzA=="],
110+
90 "@fastify/forwarded": ["@fastify/forwarded@3.0.1", "", {}, "sha512-JqDochHFqXs3C3Ml3gOY58zM7OqO9ENqPo0UqAjAjH8L01fRZqwX9iLeX34//kiJubF7r2ZQHtBRU36vONbLlw=="],
111 "@fastify/forwarded": ["@fastify/forwarded@3.0.1", "", {}, "sha512-JqDochHFqXs3C3Ml3gOY58zM7OqO9ENqPo0UqAjAjH8L01fRZqwX9iLeX34//kiJubF7r2ZQHtBRU36vONbLlw=="],
91
112
113+ "@fastify/helmet": ["@fastify/helmet@13.1.0", "", { "dependencies": { "fastify-plugin": "^6.0.0", "helmet": "^8.0.0" } }, "sha512-SvVOU0IrzYJW1BvSkfq9G1WUdW3dnaRUvg6m0BtgGMBmML62No0VmSu087jecH58SFbicbREgZTPJ89mAguupA=="],
114+
92 "@fastify/merge-json-schemas": ["@fastify/merge-json-schemas@0.2.1", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A=="],
115 "@fastify/merge-json-schemas": ["@fastify/merge-json-schemas@0.2.1", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A=="],
93
116
117+ "@fastify/multipart": ["@fastify/multipart@10.1.0", "", { "dependencies": { "@fastify/busboy": "^3.0.0", "@fastify/deepmerge": "^3.0.0", "@fastify/error": "^4.0.0", "fastify-plugin": "^6.0.0", "secure-json-parse": "^4.0.0" } }, "sha512-b2r6CovmLQvLFJ5HJDtxVigZjcO9TwkRGslhiNQxsGJwilm5B3eqvXPOVLudC44zXMXJITpQ/iEATIE7zoXqcQ=="],
118+
94 "@fastify/proxy-addr": ["@fastify/proxy-addr@5.1.0", "", { "dependencies": { "@fastify/forwarded": "^3.0.0", "ipaddr.js": "^2.1.0" } }, "sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw=="],
119 "@fastify/proxy-addr": ["@fastify/proxy-addr@5.1.0", "", { "dependencies": { "@fastify/forwarded": "^3.0.0", "ipaddr.js": "^2.1.0" } }, "sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw=="],
95
120
121+ "@fastify/rate-limit": ["@fastify/rate-limit@11.2.0", "", { "dependencies": { "@lukeed/ms": "^2.0.2", "fastify-plugin": "^6.0.0", "ip-address": "^10.2.0", "toad-cache": "^3.7.0" } }, "sha512-X7osJd4XSvMoejYrnJkSZYYjY1eNYoBqhjlzf1RakC2204qExFqZFTKj5+T7VuzA/iUI9Z3UoSqQRkB2HpG0oQ=="],
122+
123+ "@fastify/websocket": ["@fastify/websocket@11.3.0", "", { "dependencies": { "duplexify": "^4.1.3", "fastify-plugin": "^6.0.0", "ws": "^8.16.0" } }, "sha512-g89ag4BCcD9YP5wBZXixzoLnuf5j89p/sXFcfpCiv2pdEkYYukBEoK3heVzqsp0EAtszVDc2BBZG0KZqeAShIA=="],
124+
96 "@gar/promisify": ["@gar/promisify@1.1.3", "", {}, "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw=="],
125 "@gar/promisify": ["@gar/promisify@1.1.3", "", {}, "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw=="],
97
126
98 "@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="],
127 "@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="],
@@ -101,6 +130,8 @@
101
130
102 "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
131 "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
103
132
133+ "@lukeed/ms": ["@lukeed/ms@2.0.2", "", {}, "sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA=="],
134+
104 "@nodelib/fs.scandir": ["@nodelib/fs.scandir@3.0.0", "", { "dependencies": { "@nodelib/fs.stat": "3.0.0", "run-parallel": "^1.2.0" } }, "sha512-ktI9+PxfHYtKjF3cLTUAh2N+b8MijCRPNwKJNqTVdL0gB0QxLU2rIRaZ1t71oEa3YBDE6bukH1sR0+CDnpp/Mg=="],
135 "@nodelib/fs.scandir": ["@nodelib/fs.scandir@3.0.0", "", { "dependencies": { "@nodelib/fs.stat": "3.0.0", "run-parallel": "^1.2.0" } }, "sha512-ktI9+PxfHYtKjF3cLTUAh2N+b8MijCRPNwKJNqTVdL0gB0QxLU2rIRaZ1t71oEa3YBDE6bukH1sR0+CDnpp/Mg=="],
105
136
106 "@nodelib/fs.stat": ["@nodelib/fs.stat@3.0.0", "", {}, "sha512-2tQOI38s19P9i7X/Drt0v8iMA+KMsgdhB/dyPER+e+2Y8L1Z7QvnuRdW/uLuf5YRFUYmnj4bMA6qCuZHFI1GDQ=="],
137 "@nodelib/fs.stat": ["@nodelib/fs.stat@3.0.0", "", {}, "sha512-2tQOI38s19P9i7X/Drt0v8iMA+KMsgdhB/dyPER+e+2Y8L1Z7QvnuRdW/uLuf5YRFUYmnj4bMA6qCuZHFI1GDQ=="],
@@ -111,6 +142,8 @@
111
142
112 "@npmcli/move-file": ["@npmcli/move-file@2.0.1", "", { "dependencies": { "mkdirp": "^1.0.4", "rimraf": "^3.0.2" } }, "sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ=="],
143 "@npmcli/move-file": ["@npmcli/move-file@2.0.1", "", { "dependencies": { "mkdirp": "^1.0.4", "rimraf": "^3.0.2" } }, "sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ=="],
113
144
145+ "@phc/format": ["@phc/format@1.0.0", "", {}, "sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ=="],
146+
114 "@pinojs/redact": ["@pinojs/redact@0.4.0", "", {}, "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg=="],
147 "@pinojs/redact": ["@pinojs/redact@0.4.0", "", {}, "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg=="],
115
148
116 "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="],
149 "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="],
@@ -203,6 +236,8 @@
203
236
204 "aproba": ["aproba@1.2.0", "", {}, "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw=="],
237 "aproba": ["aproba@1.2.0", "", {}, "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw=="],
205
238
239+ "argon2": ["argon2@0.45.1", "", { "dependencies": { "@phc/format": "^1.0.0", "cross-env": "^10.1.0", "node-addon-api": "^8.9.0", "node-gyp-build": "^4.8.4" } }, "sha512-skm+/WCjkGqCQxF7FG1LuZXM5yvbFjgbfiCGsud2oLgaDhh6b6dbH0b1EkghbM+xx4Bj8Ape+KKgixoIlWZicQ=="],
240+
206 "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
241 "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
207
242
208 "asn1": ["asn1@0.2.6", "", { "dependencies": { "safer-buffer": "~2.1.0" } }, "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ=="],
243 "asn1": ["asn1@0.2.6", "", { "dependencies": { "safer-buffer": "~2.1.0" } }, "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ=="],
@@ -269,7 +304,7 @@
269
304
270 "concat-stream": ["concat-stream@1.6.2", "", { "dependencies": { "buffer-from": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^2.2.2", "typedarray": "^0.0.6" } }, "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw=="],
305 "concat-stream": ["concat-stream@1.6.2", "", { "dependencies": { "buffer-from": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^2.2.2", "typedarray": "^0.0.6" } }, "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw=="],
271
306
272− "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
307+ "cookie": ["cookie@2.0.1", "", {}, "sha512-yuToqVvRrj6pfDXREyQAAv8SkAEk/8GS3jQRTiUMm66TVtBYmqQeoEjL2Lmq8Rpo6271vH76InTChTitEAm65w=="],
273
308
274 "copy-concurrently": ["copy-concurrently@1.0.5", "", { "dependencies": { "aproba": "^1.1.1", "fs-write-stream-atomic": "^1.0.8", "iferr": "^0.1.5", "mkdirp": "^0.5.1", "rimraf": "^2.5.4", "run-queue": "^1.0.0" } }, "sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A=="],
309 "copy-concurrently": ["copy-concurrently@1.0.5", "", { "dependencies": { "aproba": "^1.1.1", "fs-write-stream-atomic": "^1.0.8", "iferr": "^0.1.5", "mkdirp": "^0.5.1", "rimraf": "^2.5.4", "run-queue": "^1.0.0" } }, "sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A=="],
275
310
@@ -277,6 +312,8 @@
277
312
278 "cpu-features": ["cpu-features@0.0.10", "", { "dependencies": { "buildcheck": "~0.0.6", "nan": "^2.19.0" } }, "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA=="],
313 "cpu-features": ["cpu-features@0.0.10", "", { "dependencies": { "buildcheck": "~0.0.6", "nan": "^2.19.0" } }, "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA=="],
279
314
315+ "cross-env": ["cross-env@10.1.0", "", { "dependencies": { "@epic-web/invariant": "^1.0.0", "cross-spawn": "^7.0.6" }, "bin": { "cross-env": "dist/bin/cross-env.js", "cross-env-shell": "dist/bin/cross-env-shell.js" } }, "sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw=="],
316+
280 "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
317 "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
281
318
282 "css-tree": ["css-tree@3.1.0", "", { "dependencies": { "mdn-data": "2.12.2", "source-map-js": "^1.0.1" } }, "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w=="],
319 "css-tree": ["css-tree@3.1.0", "", { "dependencies": { "mdn-data": "2.12.2", "source-map-js": "^1.0.1" } }, "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w=="],
@@ -307,7 +344,7 @@
307
344
308 "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
345 "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
309
346
310− "duplexify": ["duplexify@3.7.1", "", { "dependencies": { "end-of-stream": "^1.0.0", "inherits": "^2.0.1", "readable-stream": "^2.0.0", "stream-shift": "^1.0.0" } }, "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g=="],
347+ "duplexify": ["duplexify@4.1.3", "", { "dependencies": { "end-of-stream": "^1.4.1", "inherits": "^2.0.3", "readable-stream": "^3.1.1", "stream-shift": "^1.0.2" } }, "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA=="],
311
348
312 "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="],
349 "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="],
313
350
@@ -349,6 +386,8 @@
349
386
350 "fastify": ["fastify@5.7.4", "", { "dependencies": { "@fastify/ajv-compiler": "^4.0.5", "@fastify/error": "^4.0.0", "@fastify/fast-json-stringify-compiler": "^5.0.0", "@fastify/proxy-addr": "^5.0.0", "abstract-logging": "^2.0.1", "avvio": "^9.0.0", "fast-json-stringify": "^6.0.0", "find-my-way": "^9.0.0", "light-my-request": "^6.0.0", "pino": "^10.1.0", "process-warning": "^5.0.0", "rfdc": "^1.3.1", "secure-json-parse": "^4.0.0", "semver": "^7.6.0", "toad-cache": "^3.7.0" } }, "sha512-e6l5NsRdaEP8rdD8VR0ErJASeyaRbzXYpmkrpr2SuvuMq6Si3lvsaVy5C+7gLanEkvjpMDzBXWE5HPeb/hgTxA=="],
387 "fastify": ["fastify@5.7.4", "", { "dependencies": { "@fastify/ajv-compiler": "^4.0.5", "@fastify/error": "^4.0.0", "@fastify/fast-json-stringify-compiler": "^5.0.0", "@fastify/proxy-addr": "^5.0.0", "abstract-logging": "^2.0.1", "avvio": "^9.0.0", "fast-json-stringify": "^6.0.0", "find-my-way": "^9.0.0", "light-my-request": "^6.0.0", "pino": "^10.1.0", "process-warning": "^5.0.0", "rfdc": "^1.3.1", "secure-json-parse": "^4.0.0", "semver": "^7.6.0", "toad-cache": "^3.7.0" } }, "sha512-e6l5NsRdaEP8rdD8VR0ErJASeyaRbzXYpmkrpr2SuvuMq6Si3lvsaVy5C+7gLanEkvjpMDzBXWE5HPeb/hgTxA=="],
351
388
389+ "fastify-plugin": ["fastify-plugin@6.0.0", "", {}, "sha512-fZOty7z3O7vOliF6d8bHE3wiEh1KcNnKEQensSgTk9C1DvN6nRLS++XVd86v33Hw/8u9Un8A1zDrQ8ujcQDHEg=="],
390+
352 "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="],
391 "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="],
353
392
354 "figgy-pudding": ["figgy-pudding@3.5.2", "", {}, "sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw=="],
393 "figgy-pudding": ["figgy-pudding@3.5.2", "", {}, "sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw=="],
@@ -391,6 +430,8 @@
391
430
392 "has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="],
431 "has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="],
393
432
433+ "helmet": ["helmet@8.3.0", "", {}, "sha512-Qgpiaws3Sm30Av8Eah6sjMCZZwjlBu+E68rhpCWBshY1lb09HtLwj5GviX0OyQIn+ulUS0iX0AxN5n3tLZzz1w=="],
434+
394 "html-encoding-sniffer": ["html-encoding-sniffer@6.0.0", "", { "dependencies": { "@exodus/bytes": "^1.6.0" } }, "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg=="],
435 "html-encoding-sniffer": ["html-encoding-sniffer@6.0.0", "", { "dependencies": { "@exodus/bytes": "^1.6.0" } }, "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg=="],
395
436
396 "http-cache-semantics": ["http-cache-semantics@4.2.0", "", {}, "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ=="],
437 "http-cache-semantics": ["http-cache-semantics@4.2.0", "", {}, "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ=="],
@@ -419,7 +460,7 @@
419
460
420 "ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="],
461 "ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="],
421
462
422− "ip-address": ["ip-address@10.1.0", "", {}, "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q=="],
463+ "ip-address": ["ip-address@10.5.0", "", {}, "sha512-R5SnVLJmgYYvf2F2ZgwSBnelz5G4q5AxIC277GDfUaNbrZKNANcBC7RHqYYePlszf4kBolVkJauG0ZjHHFh55g=="],
423
464
424 "ipaddr.js": ["ipaddr.js@2.3.0", "", {}, "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg=="],
465 "ipaddr.js": ["ipaddr.js@2.3.0", "", {}, "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg=="],
425
466
@@ -447,6 +488,8 @@
447
488
448 "jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
489 "jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
449
490
491+ "jose": ["jose@6.2.8", "", {}, "sha512-Bsdjwm3Qsd/P0jR+BHDe3LytDfY7WBq2HmCCLIwuVRHMuEC9ae7/R474GIUdF1NgCyZjzVo/A9DOiOBtXq8ZoQ=="],
492+
450 "jsdom": ["jsdom@28.0.0", "", { "dependencies": { "@acemir/cssom": "^0.9.31", "@asamuzakjp/dom-selector": "^6.7.6", "@exodus/bytes": "^1.11.0", "cssstyle": "^5.3.7", "data-urls": "^7.0.0", "decimal.js": "^10.6.0", "html-encoding-sniffer": "^6.0.0", "http-proxy-agent": "^7.0.2", "https-proxy-agent": "^7.0.6", "is-potential-custom-element-name": "^1.0.1", "parse5": "^8.0.0", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", "tough-cookie": "^6.0.0", "undici": "^7.20.0", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^8.0.1", "whatwg-mimetype": "^5.0.0", "whatwg-url": "^16.0.0", "xml-name-validator": "^5.0.0" }, "peerDependencies": { "canvas": "^3.0.0" }, "optionalPeers": ["canvas"] }, "sha512-KDYJgZ6T2TKdU8yBfYueq5EPG/EylMsBvCaenWMJb2OXmjgczzwveRCoJ+Hgj1lXPDyasvrgneSn4GBuR1hYyA=="],
493 "jsdom": ["jsdom@28.0.0", "", { "dependencies": { "@acemir/cssom": "^0.9.31", "@asamuzakjp/dom-selector": "^6.7.6", "@exodus/bytes": "^1.11.0", "cssstyle": "^5.3.7", "data-urls": "^7.0.0", "decimal.js": "^10.6.0", "html-encoding-sniffer": "^6.0.0", "http-proxy-agent": "^7.0.2", "https-proxy-agent": "^7.0.6", "is-potential-custom-element-name": "^1.0.1", "parse5": "^8.0.0", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", "tough-cookie": "^6.0.0", "undici": "^7.20.0", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^8.0.1", "whatwg-mimetype": "^5.0.0", "whatwg-url": "^16.0.0", "xml-name-validator": "^5.0.0" }, "peerDependencies": { "canvas": "^3.0.0" }, "optionalPeers": ["canvas"] }, "sha512-KDYJgZ6T2TKdU8yBfYueq5EPG/EylMsBvCaenWMJb2OXmjgczzwveRCoJ+Hgj1lXPDyasvrgneSn4GBuR1hYyA=="],
451
494
452 "json-schema-ref-resolver": ["json-schema-ref-resolver@3.0.0", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A=="],
495 "json-schema-ref-resolver": ["json-schema-ref-resolver@3.0.0", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A=="],
@@ -523,6 +566,10 @@
523
566
524 "node-abi": ["node-abi@3.87.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ=="],
567 "node-abi": ["node-abi@3.87.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ=="],
525
568
569+ "node-addon-api": ["node-addon-api@8.9.2", "", {}, "sha512-VijLXbi3UACN69I0JVXJsX4tjACjNoQDgv2gTF6sx2wWEi8tkSg2eX8p5gSIFi8z2+DL3oHmY6OyKce38SDolg=="],
570+
571+ "node-gyp-build": ["node-gyp-build@4.8.4", "", { "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", "node-gyp-build-test": "build-test.js" } }, "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ=="],
572+
526 "npm-run-path": ["npm-run-path@3.1.0", "", { "dependencies": { "path-key": "^3.0.0" } }, "sha512-Dbl4A/VfiVGLgQv29URL9xshU8XDY1GeLy+fsaZ1AA8JDSfjvr5P5+pzRbWqRSBxk6/DW7MIh8lTM/PaGnP2kg=="],
573 "npm-run-path": ["npm-run-path@3.1.0", "", { "dependencies": { "path-key": "^3.0.0" } }, "sha512-Dbl4A/VfiVGLgQv29URL9xshU8XDY1GeLy+fsaZ1AA8JDSfjvr5P5+pzRbWqRSBxk6/DW7MIh8lTM/PaGnP2kg=="],
527
574
528 "on-exit-leak-free": ["on-exit-leak-free@2.1.2", "", {}, "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA=="],
575 "on-exit-leak-free": ["on-exit-leak-free@2.1.2", "", {}, "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA=="],
@@ -593,7 +640,7 @@
593
640
594 "rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="],
641 "rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="],
595
642
596− "readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
643+ "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
597
644
598 "readline-sync": ["readline-sync@1.4.10", "", {}, "sha512-gNva8/6UAe8QYepIQH/jQ2qn91Qj0B9sYjMBBs3QOB8F2CXcKgLxQaJRP76sWVRQt+QU+8fAkCbCvjjMFu7Ycw=="],
645 "readline-sync": ["readline-sync@1.4.10", "", {}, "sha512-gNva8/6UAe8QYepIQH/jQ2qn91Qj0B9sYjMBBs3QOB8F2CXcKgLxQaJRP76sWVRQt+QU+8fAkCbCvjjMFu7Ycw=="],
599
646
@@ -687,7 +734,7 @@
687
734
688 "string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
735 "string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
689
736
690− "string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
737+ "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
691
738
692 "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
739 "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
693
740
@@ -779,6 +826,8 @@
779
826
780 "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
827 "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
781
828
829+ "ws": ["ws@8.21.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw=="],
830+
782 "wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="],
831 "wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="],
783
832
784 "xhr2": ["xhr2@0.2.1", "", {}, "sha512-sID0rrVCqkVNUn8t6xuv9+6FViXjUVXq8H5rWOH2rz9fDNQEd4g0EA2XlcEdJXRz5BMEn4O1pJFdT+z4YHhoWw=="],
833 "xhr2": ["xhr2@0.2.1", "", {}, "sha512-sID0rrVCqkVNUn8t6xuv9+6FViXjUVXq8H5rWOH2rz9fDNQEd4g0EA2XlcEdJXRz5BMEn4O1pJFdT+z4YHhoWw=="],
@@ -807,8 +856,6 @@
807
856
808 "@npmcli/move-file/rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="],
857 "@npmcli/move-file/rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="],
809
858
810− "bl/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
811−
812 "cacache/chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="],
859 "cacache/chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="],
813
860
814 "cacache/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
861 "cacache/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
@@ -817,12 +864,22 @@
817
864
818 "chalk/supports-color": ["supports-color@5.5.0", "", { "dependencies": { "has-flag": "^3.0.0" } }, "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow=="],
865 "chalk/supports-color": ["supports-color@5.5.0", "", { "dependencies": { "has-flag": "^3.0.0" } }, "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow=="],
819
866
867+ "concat-stream/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
868+
820 "cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
869 "cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
821
870
871+ "flush-write-stream/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
872+
822 "foreground-child/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
873 "foreground-child/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
823
874
875+ "from2/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
876+
824 "fs-minipass/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
877 "fs-minipass/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
825
878
879+ "fs-write-stream-atomic/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
880+
881+ "light-my-request/cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
882+
826 "light-my-request/process-warning": ["process-warning@4.0.1", "", {}, "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q=="],
883 "light-my-request/process-warning": ["process-warning@4.0.1", "", {}, "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q=="],
827
884
828 "make-fetch-happen/cacache": ["cacache@16.1.3", "", { "dependencies": { "@npmcli/fs": "^2.1.0", "@npmcli/move-file": "^2.0.0", "chownr": "^2.0.0", "fs-minipass": "^2.1.0", "glob": "^8.0.1", "infer-owner": "^1.0.4", "lru-cache": "^7.7.1", "minipass": "^3.1.6", "minipass-collect": "^1.0.2", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "mkdirp": "^1.0.4", "p-map": "^4.0.0", "promise-inflight": "^1.0.1", "rimraf": "^3.0.2", "ssri": "^9.0.0", "tar": "^6.1.11", "unique-filename": "^2.0.0" } }, "sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ=="],
885 "make-fetch-happen/cacache": ["cacache@16.1.3", "", { "dependencies": { "@npmcli/fs": "^2.1.0", "@npmcli/move-file": "^2.0.0", "chownr": "^2.0.0", "fs-minipass": "^2.1.0", "glob": "^8.0.1", "infer-owner": "^1.0.4", "lru-cache": "^7.7.1", "minipass": "^3.1.6", "minipass-collect": "^1.0.2", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "mkdirp": "^1.0.4", "p-map": "^4.0.0", "promise-inflight": "^1.0.1", "rimraf": "^3.0.2", "ssri": "^9.0.0", "tar": "^6.1.11", "unique-filename": "^2.0.0" } }, "sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ=="],
@@ -853,27 +910,31 @@
853
910
854 "minipass-sized/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
911 "minipass-sized/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
855
912
913+ "mississippi/duplexify": ["duplexify@3.7.1", "", { "dependencies": { "end-of-stream": "^1.0.0", "inherits": "^2.0.1", "readable-stream": "^2.0.0", "stream-shift": "^1.0.0" } }, "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g=="],
914+
915+ "parallel-transform/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
916+
856 "path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
917 "path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
857
918
919+ "pumpify/duplexify": ["duplexify@3.7.1", "", { "dependencies": { "end-of-stream": "^1.0.0", "inherits": "^2.0.1", "readable-stream": "^2.0.0", "stream-shift": "^1.0.0" } }, "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g=="],
920+
858 "pumpify/pump": ["pump@2.0.1", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA=="],
921 "pumpify/pump": ["pump@2.0.1", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA=="],
859
922
860 "purescript-installer/env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="],
923 "purescript-installer/env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="],
861
924
862 "purescript-installer/tar": ["tar@6.2.1", "", { "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", "minipass": "^5.0.0", "minizlib": "^2.1.1", "mkdirp": "^1.0.3", "yallist": "^4.0.0" } }, "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A=="],
925 "purescript-installer/tar": ["tar@6.2.1", "", { "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", "minipass": "^5.0.0", "minizlib": "^2.1.1", "mkdirp": "^1.0.3", "yallist": "^4.0.0" } }, "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A=="],
863
926
864− "readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
865−
866 "rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
927 "rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
867
928
868 "slice-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
929 "slice-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
869
930
870− "socks-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="],
931+ "socks/ip-address": ["ip-address@10.1.0", "", {}, "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q=="],
871
932
872− "string_decoder/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
933+ "socks-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="],
873
934
874 "tar-fs/chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="],
935 "tar-fs/chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="],
875
936
876− "tar-stream/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
937+ "through2/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
877
938
878 "wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
939 "wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
879
940
@@ -887,14 +948,28 @@
887
948
888 "@npmcli/move-file/rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
949 "@npmcli/move-file/rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
889
950
890− "bl/readable-stream/string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
891−
892 "cacache/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
951 "cacache/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
893
952
894 "cacache/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
953 "cacache/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
895
954
955+ "concat-stream/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
956+
957+ "concat-stream/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
958+
959+ "flush-write-stream/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
960+
961+ "flush-write-stream/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
962+
963+ "from2/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
964+
965+ "from2/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
966+
896 "fs-minipass/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
967 "fs-minipass/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
897
968
969+ "fs-write-stream-atomic/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
970+
971+ "fs-write-stream-atomic/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
972+
898 "make-fetch-happen/cacache/chownr": ["chownr@2.0.0", "", {}, "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ=="],
973 "make-fetch-happen/cacache/chownr": ["chownr@2.0.0", "", {}, "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ=="],
899
974
900 "make-fetch-happen/cacache/glob": ["glob@8.1.0", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^5.0.1", "once": "^1.3.0" } }, "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ=="],
975 "make-fetch-happen/cacache/glob": ["glob@8.1.0", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^5.0.1", "once": "^1.3.0" } }, "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ=="],
@@ -925,6 +1000,14 @@
925
1000
926 "minipass-sized/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
1001 "minipass-sized/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
927
1002
1003+ "mississippi/duplexify/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
1004+
1005+ "parallel-transform/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
1006+
1007+ "parallel-transform/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
1008+
1009+ "pumpify/duplexify/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
1010+
928 "purescript-installer/tar/chownr": ["chownr@2.0.0", "", {}, "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ=="],
1011 "purescript-installer/tar/chownr": ["chownr@2.0.0", "", {}, "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ=="],
929
1012
930 "purescript-installer/tar/minipass": ["minipass@5.0.0", "", {}, "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ=="],
1013 "purescript-installer/tar/minipass": ["minipass@5.0.0", "", {}, "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ=="],
@@ -939,7 +1022,9 @@
939
1022
940 "slice-ansi/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
1023 "slice-ansi/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
941
1024
1025+ "through2/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
1026+
942− "tar-stream/readable-stream/string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
1027+ "through2/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
943
1028
944 "wrap-ansi-cjs/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
1029 "wrap-ansi-cjs/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
945
1030
@@ -961,6 +1046,14 @@
961
1046
962 "make-fetch-happen/cacache/unique-filename/unique-slug": ["unique-slug@3.0.0", "", { "dependencies": { "imurmurhash": "^0.1.4" } }, "sha512-8EyMynh679x/0gqE9fT9oilG+qEt+ibFyqjuVTsZn1+CMxH+XLlpvr2UZx4nVcCwTpx81nICr2JQFkM+HPLq4w=="],
1047 "make-fetch-happen/cacache/unique-filename/unique-slug": ["unique-slug@3.0.0", "", { "dependencies": { "imurmurhash": "^0.1.4" } }, "sha512-8EyMynh679x/0gqE9fT9oilG+qEt+ibFyqjuVTsZn1+CMxH+XLlpvr2UZx4nVcCwTpx81nICr2JQFkM+HPLq4w=="],
963
1048
1049+ "mississippi/duplexify/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
1050+
1051+ "mississippi/duplexify/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
1052+
1053+ "pumpify/duplexify/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
1054+
1055+ "pumpify/duplexify/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
1056+
964 "purescript-installer/tar/minizlib/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
1057 "purescript-installer/tar/minizlib/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
965
1058
966 "rimraf/glob/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
1059 "rimraf/glob/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
compile-fail-tests/ClientBadBodyType.pursnew file+12
@@ -0,0 +1,12 @@
1+-- EXPECT: String
2+module CompileFail.ClientBadBodyType where
3+
4+import Yoga.Fetch.Om (JSON, POST, Route, client)
5+
6+type UserApi =
7+ { createUser :: Route POST "users" { body :: JSON { name :: String } } (ok :: { body :: {} })
8+ }
9+
10+api = client @UserApi ""
11+
12+bad = api.createUser { name: 42 }
compile-fail-tests/ClientBadCurriedBodyType.pursnew file+12
@@ -0,0 +1,12 @@
1+-- EXPECT: String
2+module CompileFail.ClientBadCurriedBodyType where
3+
4+import Yoga.Fetch.Om (JSON, POST, Route, client, type (/), type (:))
5+
6+type UserApi =
7+ { updateUser :: Route POST ("users" / "id" : Int) { body :: JSON { name :: String } } (ok :: { body :: {} })
8+ }
9+
10+api = client @UserApi ""
11+
12+bad = api.updateUser { id: 1 } { name: 42 }
compile-fail-tests/ClientBadHeaderType.pursnew file+13
@@ -0,0 +1,13 @@
1+-- EXPECT: BearerToken
2+module CompileFail.ClientBadHeaderType where
3+
4+import Yoga.Fetch.Om (GET, Route, client)
5+import Yoga.HTTP.API.Route (BearerToken)
6+
7+type UserApi =
8+ { me :: Route GET "me" { headers :: { authorization :: BearerToken } } (ok :: { body :: {} })
9+ }
10+
11+api = client @UserApi ""
12+
13+bad = api.me { authorization: "secret" }
compile-fail-tests/ClientBadOptionalQueryType.pursnew file+13
@@ -0,0 +1,13 @@
1+-- EXPECT: limit
2+module CompileFail.ClientBadOptionalQueryType where
3+
4+import Justifill (justifill)
5+import Yoga.Fetch.Om (GET, Route, client, type (:?))
6+
7+type UserApi =
8+ { listUsers :: Route GET ("users" :? { limit :: Int, offset :: Int }) {} (ok :: { body :: {} })
9+ }
10+
11+api = client @UserApi ""
12+
13+bad = api.listUsers (justifill { limit: "10" })
compile-fail-tests/ClientBadPathType.pursnew file+12
@@ -0,0 +1,12 @@
1+-- EXPECT: Int
2+module CompileFail.ClientBadPathType where
3+
4+import Yoga.Fetch.Om (GET, Route, client, type (/), type (:))
5+
6+type UserApi =
7+ { getUser :: Route GET ("users" / "id" : Int) {} (ok :: { body :: {} })
8+ }
9+
10+api = client @UserApi ""
11+
12+bad = api.getUser { id: "1" }
compile-fail-tests/ClientExtraBodyField.pursnew file+12
@@ -0,0 +1,12 @@
1+-- EXPECT: extra
2+module CompileFail.ClientExtraBodyField where
3+
4+import Yoga.Fetch.Om (JSON, POST, Route, client)
5+
6+type UserApi =
7+ { createUser :: Route POST "users" { body :: JSON { name :: String } } (ok :: { body :: {} })
8+ }
9+
10+api = client @UserApi ""
11+
12+bad = api.createUser { name: "Alice", extra: true }
compile-fail-tests/ClientExtraHeaderField.pursnew file+13
@@ -0,0 +1,13 @@
1+-- EXPECT: extra
2+module CompileFail.ClientExtraHeaderField where
3+
4+import Yoga.Fetch.Om (GET, Route, client)
5+import Yoga.HTTP.API.Route (BearerToken(..))
6+
7+type UserApi =
8+ { me :: Route GET "me" { headers :: { authorization :: BearerToken } } (ok :: { body :: {} })
9+ }
10+
11+api = client @UserApi ""
12+
13+bad = api.me { authorization: BearerToken "secret", extra: true }
compile-fail-tests/ClientExtraOptionalQueryField.pursnew file+13
@@ -0,0 +1,13 @@
1+-- EXPECT: extra
2+module CompileFail.ClientExtraOptionalQueryField where
3+
4+import Justifill (justifill)
5+import Yoga.Fetch.Om (GET, Route, client, type (:?))
6+
7+type UserApi =
8+ { listUsers :: Route GET ("users" :? { limit :: Int, offset :: Int }) {} (ok :: { body :: {} })
9+ }
10+
11+api = client @UserApi ""
12+
13+bad = api.listUsers (justifill { limit: 1, extra: "nope" })
compile-fail-tests/ClientExtraPathField.pursnew file+12
@@ -0,0 +1,12 @@
1+-- EXPECT: extra
2+module CompileFail.ClientExtraPathField where
3+
4+import Yoga.Fetch.Om (GET, Route, client, type (/), type (:))
5+
6+type UserApi =
7+ { getUser :: Route GET ("users" / "id" : Int) {} (ok :: { body :: {} })
8+ }
9+
10+api = client @UserApi ""
11+
12+bad = api.getUser { id: 1, extra: true }
compile-fail-tests/ClientMissingBodyArgument.pursnew file+12
@@ -0,0 +1,12 @@
1+-- EXPECT: Function
2+module CompileFail.ClientMissingBodyArgument where
3+
4+import Yoga.Fetch.Om (JSON, POST, Route, client, type (/), type (:))
5+import Yoga.Om (Om)
6+
7+type UserApi =
8+ { updateUser :: Route POST ("users" / "id" : Int) { body :: JSON { token :: String } } (ok :: { body :: {} })
9+ }
10+
11+api :: forall ctx err. { updateUser :: { id :: Int } -> Om { | ctx } err {} }
12+api = client @UserApi ""
compile-fail-tests/ClientMissingBodyField.pursnew file+12
@@ -0,0 +1,12 @@
1+-- EXPECT: name :: String
2+module CompileFail.ClientMissingBodyField where
3+
4+import Yoga.Fetch.Om (JSON, POST, Route, client)
5+
6+type UserApi =
7+ { createUser :: Route POST "users" { body :: JSON { name :: String, age :: Int } } (ok :: { body :: {} })
8+ }
9+
10+api = client @UserApi ""
11+
12+bad = api.createUser { age: 42 }
compile-fail-tests/ClientMissingHeaderArgument.pursnew file+13
@@ -0,0 +1,13 @@
1+-- EXPECT: Function
2+module CompileFail.ClientMissingHeaderArgument where
3+
4+import Yoga.Fetch.Om (GET, Route, client, type (/), type (:))
5+import Yoga.HTTP.API.Route (BearerToken)
6+import Yoga.Om (Om)
7+
8+type UserApi =
9+ { getUser :: Route GET ("users" / "id" : Int) { headers :: { authorization :: BearerToken } } (ok :: { body :: {} })
10+ }
11+
12+api :: forall ctx err. { getUser :: { id :: Int } -> Om { | ctx } err {} }
13+api = client @UserApi ""
compile-fail-tests/ClientMissingHeaderField.pursnew file+13
@@ -0,0 +1,13 @@
1+-- EXPECT: authorization :: BearerToken
2+module CompileFail.ClientMissingHeaderField where
3+
4+import Yoga.Fetch.Om (GET, Route, client)
5+import Yoga.HTTP.API.Route (BearerToken)
6+
7+type UserApi =
8+ { me :: Route GET "me" { headers :: { authorization :: BearerToken } } (ok :: { body :: {} })
9+ }
10+
11+api = client @UserApi ""
12+
13+bad = api.me {}
compile-fail-tests/ClientMissingPathField.pursnew file+12
@@ -0,0 +1,12 @@
1+-- EXPECT: id :: Int
2+module CompileFail.ClientMissingPathField where
3+
4+import Yoga.Fetch.Om (GET, Route, client, type (/), type (:))
5+
6+type UserApi =
7+ { getUser :: Route GET ("users" / "id" : Int) {} (ok :: { body :: {} })
8+ }
9+
10+api = client @UserApi ""
11+
12+bad = api.getUser {}
compile-fail-tests/ClientMissingRequiredQuery.pursnew file+13
@@ -0,0 +1,13 @@
1+-- EXPECT: id :: Int
2+module CompileFail.ClientMissingRequiredQuery where
3+
4+import Justifill (justifill)
5+import Yoga.Fetch.Om (GET, Required, Route, client, type (:?))
6+
7+type UserApi =
8+ { getUser :: Route GET ("users" :? { id :: Required Int, token :: Required String }) {} (ok :: { body :: {} })
9+ }
10+
11+api = client @UserApi ""
12+
13+bad = api.getUser (justifill { token: "secret" })
compile-fail-tests/ClientMissingThirdArgument.pursnew file+13
@@ -0,0 +1,13 @@
1+-- EXPECT: Function
2+module CompileFail.ClientMissingThirdArgument where
3+
4+import Yoga.Fetch.Om (JSON, POST, Route, client, type (/), type (:))
5+import Yoga.HTTP.API.Route (BearerToken)
6+import Yoga.Om (Om)
7+
8+type UserApi =
9+ { updateUser :: Route POST ("users" / "id" : Int) { headers :: { authorization :: BearerToken }, body :: JSON { token :: String } } (ok :: { body :: {} })
10+ }
11+
12+api :: forall ctx err. { updateUser :: { id :: Int } -> { authorization :: BearerToken } -> Om { | ctx } err {} }
13+api = client @UserApi ""
compile-fail-tests/run.jsnew file+101
@@ -0,0 +1,101 @@
1+import { mkdtempSync, readdirSync, readFileSync, rmSync } from "node:fs";
2+import { tmpdir } from "node:os";
3+import { basename, join } from "node:path";
4+
5+const cwd = new URL("..", import.meta.url).pathname;
6+const casesDir = join(cwd, "compile-fail-tests");
7+const decoder = new TextDecoder();
8+
9+function run(args, options = {}) {
10+ const proc = Bun.spawnSync(args, {
11+ cwd,
12+ stdout: options.stdout ?? "pipe",
13+ stderr: options.stderr ?? "pipe",
14+ });
15+ return {
16+ code: proc.exitCode,
17+ stdout: proc.stdout ? decoder.decode(proc.stdout) : "",
18+ stderr: proc.stderr ? decoder.decode(proc.stderr) : "",
19+ };
20+}
21+
22+function printResult(label, result) {
23+ console.error(`\n${label} failed with exit code ${result.code}`);
24+ if (result.stdout) console.error(result.stdout);
25+ if (result.stderr) console.error(result.stderr);
26+}
27+
28+function expectedSubstring(file) {
29+ const firstLine = readFileSync(file, "utf8").split("\n", 1)[0];
30+ return firstLine.startsWith("-- EXPECT: ") ? firstLine.slice("-- EXPECT: ".length) : null;
31+}
32+
33+const sourcesResult = run(["bunx", "spago", "sources", "--quiet", "--json"]);
34+if (sourcesResult.code !== 0) {
35+ printResult("spago sources", sourcesResult);
36+ process.exit(1);
37+}
38+
39+const excludedSourceGlobs = new Set([
40+ "test/**/*.purs",
41+ "../purescript-yoga-fastify-om/src/**/*.purs",
42+ "../purescript-yoga-fastify-om/local-packages/yoga-fastify/src/**/*.purs",
43+]);
44+
45+const librarySources = JSON.parse(sourcesResult.stdout)
46+ .filter((source) => !excludedSourceGlobs.has(source));
47+
48+const cases = readdirSync(casesDir)
49+ .filter((file) => file.endsWith(".purs"))
50+ .sort()
51+ .map((file) => join("compile-fail-tests", file));
52+
53+const libOutput = mkdtempSync(join(tmpdir(), "yoga-fetch-om-lib-"));
54+
55+try {
56+ const libResult = run(["bunx", "purs", "compile", ...librarySources, "--output", libOutput], { stdout: "ignore" });
57+ if (libResult.code !== 0) {
58+ printResult("library compile", libResult);
59+ process.exit(1);
60+ }
61+
62+ let failed = 0;
63+
64+ for (const testCase of cases) {
65+ const failOutput = mkdtempSync(join(tmpdir(), "yoga-fetch-om-compile-fail-"));
66+ try {
67+ const result = run([
68+ "bunx",
69+ "purs",
70+ "compile",
71+ ...librarySources,
72+ testCase,
73+ "--output",
74+ failOutput,
75+ ]);
76+
77+ const output = `${result.stdout}\n${result.stderr}`;
78+ const expected = expectedSubstring(join(cwd, testCase));
79+ const name = basename(testCase, ".purs");
80+
81+ if (result.code === 0) {
82+ console.error(`FAIL ${name} - compiled successfully (should have failed)`);
83+ failed++;
84+ } else if (expected && !output.includes(expected)) {
85+ printResult(`FAIL ${name}`, result);
86+ console.error(`Expected failure to contain: ${expected}`);
87+ failed++;
88+ } else {
89+ console.log(`PASS ${name}`);
90+ }
91+ } finally {
92+ rmSync(failOutput, { recursive: true, force: true });
93+ }
94+ }
95+
96+ if (failed > 0) {
97+ process.exit(1);
98+ }
99+} finally {
100+ rmSync(libOutput, { recursive: true, force: true });
101+}
package.json+10
@@ -6,6 +6,7 @@
6 "scripts": {
6 "scripts": {
7 "build": "spago build",
7 "build": "spago build",
8 "test": "spago test",
8 "test": "spago test",
9+ "test:compile-fail": "bun compile-fail-tests/run.js",
9 "clean": "rm -rf output .spago",
10 "clean": "rm -rf output .spago",
10 "watch": "spago build --watch",
11 "watch": "spago build --watch",
11 "repl": "spago repl",
12 "repl": "spago repl",
@@ -24,7 +25,16 @@
24 "author": "",
25 "author": "",
25 "license": "MIT",
26 "license": "MIT",
26 "devDependencies": {
27 "devDependencies": {
28+ "@fastify/cookie": "^11.1.2",
29+ "@fastify/cors": "^11.3.0",
30+ "@fastify/formbody": "^9.0.0",
31+ "@fastify/helmet": "^13.1.0",
32+ "@fastify/multipart": "^10.1.0",
33+ "@fastify/rate-limit": "^11.2.0",
34+ "@fastify/websocket": "^11.3.0",
35+ "argon2": "^0.45.1",
27 "fastify": "^5.7.4",
36 "fastify": "^5.7.4",
37+ "jose": "^6.2.8",
28 "jsdom": "^28.0.0",
38 "jsdom": "^28.0.0",
29 "purescript": "^0.15.15",
39 "purescript": "^0.15.15",
30 "spago": "0.93.42",
40 "spago": "0.93.42",
spago.lock+32−19
@@ -60,7 +60,7 @@
60 "variant": ">=8.0.0 <9.0.0"
60 "variant": ">=8.0.0 <9.0.0"
61 },
61 },
62 {
62 {
63− "yoga-http-api": ">=0.3.2 <0.4.0"
63+ "yoga-http-api": ">=0.3.4 <0.4.0"
64 },
64 },
65 {
65 {
66 "yoga-json": ">=5.2.1 <6.0.0"
66 "yoga-json": ">=5.2.1 <6.0.0"
@@ -163,6 +163,9 @@
163 {
163 {
164 "console": ">=6.0.0 <7.0.0"
164 "console": ">=6.0.0 <7.0.0"
165 },
165 },
166+ {
167+ "justifill": ">=0.5.0 <0.6.0"
168+ },
166 {
169 {
167 "refs": ">=6.0.0 <7.0.0"
170 "refs": ">=6.0.0 <7.0.0"
168 },
171 },
@@ -173,7 +176,7 @@
173 "spec-discovery": ">=8.0.0 <9.0.0"
176 "spec-discovery": ">=8.0.0 <9.0.0"
174 },
177 },
175 {
178 {
176− "yoga-fastify-om": ">=0.4.5 <0.5.0"
179+ "yoga-fastify-om": ">=0.4.7 <0.5.0"
177 }
180 }
178 ],
181 ],
179 "build_plan": [
182 "build_plan": [
@@ -216,6 +219,7 @@
216 "js-promise",
219 "js-promise",
217 "js-promise-aff",
220 "js-promise-aff",
218 "js-uri",
221 "js-uri",
222+ "justifill",
219 "lazy",
223 "lazy",
220 "lists",
224 "lists",
221 "literals",
225 "literals",
@@ -277,17 +281,7 @@
277 }
281 }
278 }
282 }
279 },
283 },
280− "extra_packages": {
284+ "extra_packages": {}
281− "yoga-fastify": {
282− "path": "../purescript-yoga-fastify-om/local-packages/yoga-fastify"
283− },
284− "yoga-fastify-om": {
285− "path": "../purescript-yoga-fastify-om"
286− },
287− "yoga-http-api": {
288− "path": "../purescript-yoga-fastify-om/local-packages/yoga-http-api"
289− }
290− }
291 },
285 },
292 "packages": {
286 "packages": {
293 "aff": {
287 "aff": {
@@ -864,6 +858,17 @@
864 "prelude"
858 "prelude"
865 ]
859 ]
866 },
860 },
861+ "justifill": {
862+ "type": "registry",
863+ "version": "0.5.0",
864+ "integrity": "sha256-IwoDO72+IROcUvFIwnronb/ny8SepssgTUG3Qp2TEKg=",
865+ "dependencies": [
866+ "maybe",
867+ "prelude",
868+ "record",
869+ "typelevel-prelude"
870+ ]
871+ },
867 "lazy": {
872 "lazy": {
868 "type": "registry",
873 "type": "registry",
869 "version": "6.0.0",
874 "version": "6.0.0",
@@ -1640,8 +1645,8 @@
1640 },
1645 },
1641 "yoga-fastify": {
1646 "yoga-fastify": {
1642 "type": "registry",
1647 "type": "registry",
1643− "version": "0.5.3",
1648+ "version": "0.5.4",
1644− "integrity": "sha256-KeAoEYXXNH2gHEl5Y7j9uMo5ipByLZp/p7u6pgQn6KU=",
1649+ "integrity": "sha256-YenjePRdiApMPpcvkXlzre7c4fIuvk5T19Z2uEAWhR4=",
1645 "dependencies": [
1650 "dependencies": [
1646 "aff",
1651 "aff",
1647 "arrays",
1652 "arrays",
@@ -1667,8 +1672,8 @@
1667 },
1672 },
1668 "yoga-fastify-om": {
1673 "yoga-fastify-om": {
1669 "type": "registry",
1674 "type": "registry",
1670− "version": "0.4.5",
1675+ "version": "0.4.7",
1671− "integrity": "sha256-fPDRni+HjddpI7GPad5U2kVHxMPOZIsvTPFZtOPpiWI=",
1676+ "integrity": "sha256-KGQ7lHI7pcSiEQoEq6j6K+3XilcPy8wQ/M9C1y+uaEk=",
1672 "dependencies": [
1677 "dependencies": [
1673 "aff",
1678 "aff",
1674 "console",
1679 "console",
@@ -1680,10 +1685,12 @@
1680 "integers",
1685 "integers",
1681 "maybe",
1686 "maybe",
1682 "node-buffer",
1687 "node-buffer",
1688+ "numbers",
1683 "prelude",
1689 "prelude",
1684 "record",
1690 "record",
1685 "refs",
1691 "refs",
1686 "routing-duplex",
1692 "routing-duplex",
1693+ "strings",
1687 "transformers",
1694 "transformers",
1688 "typelevel-prelude",
1695 "typelevel-prelude",
1689 "unsafe-coerce",
1696 "unsafe-coerce",
@@ -1698,19 +1705,25 @@
1698 },
1705 },
1699 "yoga-http-api": {
1706 "yoga-http-api": {
1700 "type": "registry",
1707 "type": "registry",
1701− "version": "0.3.2",
1708+ "version": "0.3.4",
1702− "integrity": "sha256-SiL07TYVjHPiQjMhnkYdoI3KrL6gNn1W1LigCN+80jA=",
1709+ "integrity": "sha256-DSIJ8YWr5vB5IMpyaBALM+fx57lz3AAAR0MGtfHeWxo=",
1703 "dependencies": [
1710 "dependencies": [
1704 "aff",
1711 "aff",
1712+ "arraybuffer-types",
1705 "arrays",
1713 "arrays",
1714+ "effect",
1706 "either",
1715 "either",
1707 "foldable-traversable",
1716 "foldable-traversable",
1708 "foreign",
1717 "foreign",
1709 "foreign-object",
1718 "foreign-object",
1710 "identity",
1719 "identity",
1711 "integers",
1720 "integers",
1721+ "js-promise",
1722+ "js-promise-aff",
1712 "maybe",
1723 "maybe",
1713 "newtype",
1724 "newtype",
1725+ "nullable",
1726+ "numbers",
1714 "prelude",
1727 "prelude",
1715 "record",
1728 "record",
1716 "strings",
1729 "strings",
spago.yaml+4−10
@@ -1,11 +1,4 @@
1−workspace:
1+workspace: {}
2− extraPackages:
3− yoga-fastify-om:
4− path: ../purescript-yoga-fastify-om
5− yoga-http-api:
6− path: ../purescript-yoga-fastify-om/local-packages/yoga-http-api
7− yoga-fastify:
8− path: ../purescript-yoga-fastify-om/local-packages/yoga-fastify
9package:
2package:
10 name: yoga-fetch-om
3 name: yoga-fetch-om
11 publish:
4 publish:
@@ -39,7 +32,7 @@ package:
39 - typelevel-prelude: ">=7.0.0 <8.0.0"
32 - typelevel-prelude: ">=7.0.0 <8.0.0"
40 - unsafe-coerce: ">=6.0.0 <7.0.0"
33 - unsafe-coerce: ">=6.0.0 <7.0.0"
41 - variant: ">=8.0.0 <9.0.0"
34 - variant: ">=8.0.0 <9.0.0"
42− - yoga-http-api: ">=0.3.2 <0.4.0"
35+ - yoga-http-api: ">=0.3.4 <0.4.0"
43 - yoga-json: ">=5.2.1 <6.0.0"
36 - yoga-json: ">=5.2.1 <6.0.0"
44 - yoga-om: ">=1.5.2 <2.0.0"
37 - yoga-om: ">=1.5.2 <2.0.0"
45 - yoga-om-strom: ">=0.4.3 <0.5.0"
38 - yoga-om-strom: ">=0.4.3 <0.5.0"
@@ -48,6 +41,7 @@ package:
48 dependencies:
41 dependencies:
49 - spec: ">=8.0.0 <9.0.0"
42 - spec: ">=8.0.0 <9.0.0"
50 - spec-discovery: ">=8.0.0 <9.0.0"
43 - spec-discovery: ">=8.0.0 <9.0.0"
44+ - justifill: ">=0.5.0 <0.6.0"
51 - console: ">=6.0.0 <7.0.0"
45 - console: ">=6.0.0 <7.0.0"
52− - yoga-fastify-om: ">=0.4.5 <0.5.0"
46+ - yoga-fastify-om: ">=0.4.7 <0.5.0"
53 - refs: ">=6.0.0 <7.0.0"
47 - refs: ">=6.0.0 <7.0.0"
src/Yoga/Fetch/Om.jsnew file+1
@@ -0,0 +1 @@
1+export const mergeRecords = (left) => (right) => ({ ...right, ...left });
src/Yoga/Fetch/Om.purs+139−22
@@ -1,5 +1,8 @@
1module Yoga.Fetch.Om
1module Yoga.Fetch.Om
2 ( client
2 ( client
3+ , clientWith
4+ , ClientInputs
5+ , ClientWithConfig
3 , deriveClient
6 , deriveClient
4 , class DeriveClient
7 , class DeriveClient
5 , deriveClientImpl
8 , deriveClientImpl
@@ -7,13 +10,18 @@ module Yoga.Fetch.Om
7 , deriveClientRL
10 , deriveClientRL
8 , class DeriveClientFn
11 , class DeriveClientFn
9 , deriveClientFn
12 , deriveClientFn
13+ , class DeriveClientFnWith
14+ , deriveClientFnWith
15+ , class DeriveClientWith
16+ , deriveClientWithImpl
17+ , class DeriveClientWithRL
18+ , deriveClientWithRL
10 , class RecordRow
19 , class RecordRow
11 , class ToHeaders
20 , class ToHeaders
12 , toHeaders
21 , toHeaders
13 , module Yoga.HTTP.API.Route
22 , module Yoga.HTTP.API.Route
14 , module Yoga.HTTP.API.Path
23 , module Yoga.HTTP.API.Path
15 , plainText
24 , plainText
16− , class HasAllFields
17 , module Yoga.Fetch.Om.StreamDecode
25 , module Yoga.Fetch.Om.StreamDecode
18 , module Yoga.Fetch.Om.Simple
26 , module Yoga.Fetch.Om.Simple
19 ) where
27 ) where
@@ -35,7 +43,6 @@ import Yoga.Fetch.Om.BuildUrl (class BuildUrl, buildUrl)
35import Yoga.Fetch.Om.Simple (class DecodeResponse, decodeResponse, FetchError, FetchResponse, get, getWithHeaders, delete, deleteWithHeaders, delete_, post, postWithHeaders, post_, put, putWithHeaders, put_, patch, patchWithHeaders, patch_)
43import Yoga.Fetch.Om.Simple (class DecodeResponse, decodeResponse, FetchError, FetchResponse, get, getWithHeaders, delete, deleteWithHeaders, delete_, post, postWithHeaders, post_, put, putWithHeaders, put_, patch, patchWithHeaders, patch_)
36import Yoga.Fetch.Om.StreamDecode (class StreamDecode, decodeStream)
44import Yoga.Fetch.Om.StreamDecode (class StreamDecode, decodeStream)
37import Yoga.Fetch.Om.ClientFunction (class BuildClientFn, class CheckBodyIsUnit, buildClientFn)
45import Yoga.Fetch.Om.ClientFunction (class BuildClientFn, class CheckBodyIsUnit, buildClientFn)
38−import Yoga.Fetch.Om.ReadOptional (class ReadPartialQuery, readPartialQuery)
39import Yoga.Fetch.Om.ExtractParams (class ExtractRequestBody, class ExtractBodyEncoding, class ExtractRequestHeaders)
46import Yoga.Fetch.Om.ExtractParams (class ExtractRequestBody, class ExtractBodyEncoding, class ExtractRequestHeaders)
40import Yoga.Fetch.Om.MakeRequest (class MakeRequest, class BodyEncoding, makeRequest, encodingContentType, encodeBody)
47import Yoga.Fetch.Om.MakeRequest (class MakeRequest, class BodyEncoding, makeRequest, encodingContentType, encodeBody)
41import Yoga.Fetch.Om.ParseResponse (class ParseResponse, parseResponse)
48import Yoga.Fetch.Om.ParseResponse (class ParseResponse, parseResponse)
@@ -44,7 +51,20 @@ import Yoga.Fetch.Om.Variant (class VariantOrValue, variantOrValue)
44import Yoga.HTTP.API.Path (Path, Root, Lit, Capture, PathCons, Param, QueryParams, Required, type (/), type (:), type (:?), class PathPattern)
51import Yoga.HTTP.API.Path (Path, Root, Lit, Capture, PathCons, Param, QueryParams, Required, type (/), type (:), type (:?), class PathPattern)
45import Yoga.HTTP.API.Route (Route(..), GET, POST, PUT, DELETE, PATCH, Response(..), JSON, FormData, PlainText, Streaming, NoBody, class HeaderValue, printHeader)
52import Yoga.HTTP.API.Route (Route(..), GET, POST, PUT, DELETE, PATCH, Response(..), JSON, FormData, PlainText, Streaming, NoBody, class HeaderValue, printHeader)
46import Yoga.HTTP.API.Route.Handler (class SegmentPathParams, class SegmentQueryParams)
53import Yoga.HTTP.API.Route.Handler (class SegmentPathParams, class SegmentQueryParams)
47−import Yoga.Om (class ToOm, Om, toOm)
54+import Yoga.Om (Om, toOm)
55+
56+type ClientInputs providedHeaders providedQuery =
57+ { headers :: Record providedHeaders
58+ , query :: Record providedQuery
59+ }
60+
61+type ClientWithConfig ctx err providedHeaders providedQuery =
62+ { baseUrl :: Om { | ctx } err String
63+ , provide :: Om { | ctx } err (ClientInputs providedHeaders providedQuery)
64+ }
65+
66+
67+foreign import mergeRecords :: forall left right out. Record left -> Record right -> Record out
48
68
49-- | Extract row type from Record type
69-- | Extract row type from Record type
50class RecordRow :: Type -> Row Type -> Constraint
70class RecordRow :: Type -> Row Type -> Constraint
@@ -74,11 +94,11 @@ else instance
74 rest = toHeaders (Proxy :: Proxy tail) (unsafeCoerce headers :: Record tailRow)
94 rest = toHeaders (Proxy :: Proxy tail) (unsafeCoerce headers :: Record tailRow)
75
95
76class DeriveClientFn
96class DeriveClientFn
77− :: forall k1. Type -> k1 -> Type -> Row Type -> Row Type -> Row Type -> Row Type -> Row Type -> Type -> Type -> Constraint
97+ :: forall k1. Type -> k1 -> Type -> Row Type -> Row Type -> Row Type -> Row Type -> Type -> Type -> Constraint
78class
98class
79− DeriveClientFn method segments request response ctx extraErr given routeErrors result fn
99+ DeriveClientFn method segments request response ctx extraErr routeErrors result fn
80 | method segments request response -> routeErrors result
100 | method segments request response -> routeErrors result
81− , method segments request response ctx extraErr given -> fn where
101+ , method segments request response ctx extraErr -> fn where
82 deriveClientFn :: String -> Proxy (Route method segments request response) -> fn
102 deriveClientFn :: String -> Proxy (Route method segments request response) -> fn
83
103
84instance
104instance
@@ -101,17 +121,13 @@ instance
101 , RowToList headers headersRL
121 , RowToList headers headersRL
102 , ToHeaders headersRL headers
122 , ToHeaders headersRL headers
103 , CheckBodyIsUnit body bodyFlag
123 , CheckBodyIsUnit body bodyFlag
104− , RowToList pathParams pathParamsRL
105− , HasAllFields pathParamsRL given
106 , Row.Union routeErrors extraErr errRow
124 , Row.Union routeErrors extraErr errRow
107− , BuildClientFn pathQueryRL headersRL bodyFlag body pathQuery headers given { | ctx } errRow result fn
125+ , BuildClientFn pathQueryRL headersRL bodyFlag body pathQuery headers (Record pathQuery) { | ctx } errRow result fn
108 ) =>
126 ) =>
109− DeriveClientFn method segments request response ctx extraErr given routeErrors result fn where
127+ DeriveClientFn method segments request response ctx extraErr routeErrors result fn where
110 deriveClientFn baseUrl _ =
128 deriveClientFn baseUrl _ =
111− buildClientFn (Proxy :: _ pathQueryRL) (Proxy :: _ headersRL) (Proxy :: _ bodyFlag) convert impl
129+ buildClientFn (Proxy :: _ pathQueryRL) (Proxy :: _ headersRL) (Proxy :: _ bodyFlag) identity impl
112 where
130 where
113− convert :: Record given -> Record pathQuery
114− convert = unsafeCoerce
115 impl :: Record pathQuery -> Record headers -> body -> Om { | ctx } errRow result
131 impl :: Record pathQuery -> Record headers -> body -> Om { | ctx } errRow result
116 impl pathQueryRec headersRec bodyVal = widenOm do
132 impl pathQueryRec headersRec bodyVal = widenOm do
117 let url = buildUrl baseUrl (Proxy :: _ segments) pathParamsRec queryParamsRec
133 let url = buildUrl baseUrl (Proxy :: _ segments) pathParamsRec queryParamsRec
@@ -126,15 +142,83 @@ instance
126 widenOm :: Om (Record ()) routeErrors ~> Om { | ctx } errRow
142 widenOm :: Om (Record ()) routeErrors ~> Om { | ctx } errRow
127 widenOm = unsafeCoerce
143 widenOm = unsafeCoerce
128
144
145+class DeriveClientFnWith
146+ :: forall k1. Type -> k1 -> Type -> Row Type -> Row Type -> Row Type -> Row Type -> Row Type -> Row Type -> Type -> Type -> Constraint
147+class
148+ DeriveClientFnWith method segments request response providedHeaders providedQuery ctx extraErr routeErrors result fn
149+ | method segments request response -> routeErrors result
150+ , method segments request response providedHeaders providedQuery ctx extraErr -> fn where
151+ deriveClientFnWith
152+ :: ClientWithConfig ctx extraErr providedHeaders providedQuery
153+ -> Proxy (Route method segments request response)
154+ -> fn
155+
156+instance
157+ ( SegmentPathParams segments pathParams
158+ , SegmentQueryParams segments queryParams
159+ , ExtractRequestBody request body
160+ , ExtractBodyEncoding request encoding
161+ , ExtractRequestHeaders request headers
162+ , PathPattern segments
163+ , BuildUrl segments pathParams queryParams
164+ , MakeRequest method
165+ , BodyEncoding encoding body
166+ , SplitResponses response successRow routeErrors
167+ , Row.Union pathParams queryParams pathQuery
168+ , Row.Nub pathQuery pathQuery
169+ , ParseResponse routeErrors successRow
170+ , RowToList successRow successRL
171+ , VariantOrValue successRL successRow result
172+ , Row.Union routeErrors extraErr errRow
173+ , Row.Union providedQuery remainingQuery queryParams
174+ , Row.Union pathParams remainingQuery callerPathQuery
175+ , Row.Nub callerPathQuery callerPathQuery
176+ , RowToList callerPathQuery callerPathQueryRL
177+ , RowToList headers headersRL
178+ , ToHeaders headersRL headers
179+ , Row.Union providedHeaders remainingHeaders headers
180+ , Row.Nub headers headers
181+ , RowToList remainingHeaders remainingHeadersRL
182+ , CheckBodyIsUnit body bodyFlag
183+ , BuildClientFn callerPathQueryRL remainingHeadersRL bodyFlag body callerPathQuery remainingHeaders (Record callerPathQuery) { | ctx } errRow result fn
184+ ) =>
185+ DeriveClientFnWith method segments request response providedHeaders providedQuery ctx extraErr routeErrors result fn where
186+ deriveClientFnWith config _ =
187+ buildClientFn (Proxy :: _ callerPathQueryRL) (Proxy :: _ remainingHeadersRL) (Proxy :: _ bodyFlag) identity impl
188+ where
189+ impl :: Record callerPathQuery -> Record remainingHeaders -> body -> Om { | ctx } errRow result
190+ impl pathQueryRec headersRec bodyVal = do
191+ baseUrl <- widenExtra config.baseUrl
192+ provided <- widenExtra config.provide
193+ let pathParamsRec = unsafeCoerce pathQueryRec :: Record pathParams
194+ let queryParamsRec = mergeRecords provided.query (unsafeCoerce pathQueryRec :: Record remainingQuery) :: Record queryParams
195+ let fullHeaders = mergeRecords provided.headers headersRec :: Record headers
196+ let url = buildUrl baseUrl (Proxy :: _ segments) pathParamsRec queryParamsRec
197+ let hdrs = toHeaders (Proxy :: _ headersRL) fullHeaders
198+ let ct = encodingContentType (Proxy :: _ encoding)
199+ fetchResp <- makeRequest (Proxy :: _ method) url hdrs ct (encodeBody @encoding bodyVal) # toOm # widenRoute
200+ variant <- parseResponse fetchResp # widenRoute :: Om { | ctx } errRow (Variant successRow)
201+ variantOrValue (Proxy :: _ successRL) variant # pure
202+ where
203+ widenExtra :: Om { | ctx } extraErr ~> Om { | ctx } errRow
204+ widenExtra = unsafeCoerce
205+ widenRoute :: Om (Record ()) routeErrors ~> Om { | ctx } errRow
206+ widenRoute = unsafeCoerce
207+
129class DeriveClient :: Row Type -> Row Type -> Row Type -> Row Type -> Constraint
208class DeriveClient :: Row Type -> Row Type -> Row Type -> Row Type -> Constraint
130class DeriveClient ctx extraErr routesRow clientsRow | routesRow ctx extraErr -> clientsRow where
209class DeriveClient ctx extraErr routesRow clientsRow | routesRow ctx extraErr -> clientsRow where
131 deriveClientImpl :: String -> Proxy (Record routesRow) -> Record clientsRow
210 deriveClientImpl :: String -> Proxy (Record routesRow) -> Record clientsRow
132
211
133−-- | Derive API client functions from route definitions using VTA
212+-- | Derive API client functions from route definitions using VTA.
213+-- | Generated fields use ordinary function application. Path parameters are
214+-- | plain records; optional query parameters are explicit `Maybe` fields.
134-- |
215-- |
135-- | ```purescript
216-- | ```purescript
136-- | type UserAPI = { getUser :: Route ... }
217-- | type UserAPI = { getUser :: Route ... }
137-- | api = client @UserAPI "https://api.example.com"
218-- | api = client @UserAPI "https://api.example.com"
219+-- | user <- api.getUser { id: 42 }
220+-- | users <- api.listUsers { limit: Just 10, offset: Nothing }
221+-- | health <- api.health
138-- | ```
222-- | ```
139client :: forall @routes routesRow ctx extraErr clientsRow. RecordRow routes routesRow => DeriveClient ctx extraErr routesRow clientsRow => String -> Record clientsRow
223client :: forall @routes routesRow ctx extraErr clientsRow. RecordRow routes routesRow => DeriveClient ctx extraErr routesRow clientsRow => String -> Record clientsRow
140client baseUrl = deriveClientImpl @ctx @extraErr baseUrl (Proxy :: _ { | routesRow })
224client baseUrl = deriveClientImpl @ctx @extraErr baseUrl (Proxy :: _ { | routesRow })
@@ -147,17 +231,18 @@ client baseUrl = deriveClientImpl @ctx @extraErr baseUrl (Proxy :: _ { | routesR
147deriveClient :: forall @routesRow ctx extraErr clientsRow. DeriveClient ctx extraErr routesRow clientsRow => String -> Record clientsRow
231deriveClient :: forall @routesRow ctx extraErr clientsRow. DeriveClient ctx extraErr routesRow clientsRow => String -> Record clientsRow
148deriveClient baseUrl = deriveClientImpl @ctx @extraErr baseUrl (Proxy :: _ { | routesRow })
232deriveClient baseUrl = deriveClientImpl @ctx @extraErr baseUrl (Proxy :: _ { | routesRow })
149
233
150−-- | Checks that all fields in the RowList exist in the given row.
234+clientWith
151−-- | Used to enforce required path params are present in the client argument.
235+ :: forall @routes @providedHeaders @providedQuery routesRow ctx extraErr clientsRow
152−class HasAllFields (rl :: RL.RowList Type) (given :: Row Type)
236+ . RecordRow routes routesRow
237+ => DeriveClientWith providedHeaders providedQuery ctx extraErr routesRow clientsRow
238+ => ClientWithConfig ctx extraErr providedHeaders providedQuery
239+ -> Record clientsRow
240+clientWith config = deriveClientWithImpl @providedHeaders @providedQuery @ctx @extraErr config (Proxy :: _ { | routesRow })
153
241
154−instance HasAllFields RL.Nil given
155−instance (Row.Cons name ty rest given, HasAllFields tail given) => HasAllFields (RL.Cons name ty tail) given
156
242
157plainText :: PlainText -> String
243plainText :: PlainText -> String
158plainText = unsafeCoerce
244plainText = unsafeCoerce
159
245
160−
161instance
246instance
162 ( RowToList routesRow rl
247 ( RowToList routesRow rl
163 , DeriveClientRL ctx extraErr rl () clientsRow
248 , DeriveClientRL ctx extraErr rl () clientsRow
@@ -165,6 +250,38 @@ instance
165 DeriveClient ctx extraErr routesRow clientsRow where
250 DeriveClient ctx extraErr routesRow clientsRow where
166 deriveClientImpl baseUrl _ = deriveClientRL @ctx @extraErr baseUrl (Proxy :: _ rl) {}
251 deriveClientImpl baseUrl _ = deriveClientRL @ctx @extraErr baseUrl (Proxy :: _ rl) {}
167
252
253+class DeriveClientWith :: Row Type -> Row Type -> Row Type -> Row Type -> Row Type -> Row Type -> Constraint
254+class DeriveClientWith providedHeaders providedQuery ctx extraErr routesRow clientsRow | providedHeaders providedQuery routesRow ctx extraErr -> clientsRow where
255+ deriveClientWithImpl :: ClientWithConfig ctx extraErr providedHeaders providedQuery -> Proxy (Record routesRow) -> Record clientsRow
256+
257+instance
258+ ( RowToList routesRow rl
259+ , DeriveClientWithRL providedHeaders providedQuery ctx extraErr rl () clientsRow
260+ ) =>
261+ DeriveClientWith providedHeaders providedQuery ctx extraErr routesRow clientsRow where
262+ deriveClientWithImpl config _ = deriveClientWithRL @providedHeaders @providedQuery @ctx @extraErr config (Proxy :: _ rl) {}
263+
264+class DeriveClientWithRL :: Row Type -> Row Type -> Row Type -> Row Type -> RowList Type -> Row Type -> Row Type -> Constraint
265+class DeriveClientWithRL providedHeaders providedQuery ctx extraErr rl acc out | providedHeaders providedQuery ctx extraErr rl acc -> out where
266+ deriveClientWithRL :: ClientWithConfig ctx extraErr providedHeaders providedQuery -> Proxy rl -> Record acc -> Record out
267+
268+instance DeriveClientWithRL providedHeaders providedQuery ctx extraErr RL.Nil acc acc where
269+ deriveClientWithRL _ _ acc = acc
270+
271+instance
272+ ( IsSymbol label
273+ , DeriveClientFnWith method segments request response providedHeaders providedQuery ctx extraErr routeErrors result fn
274+ , DeriveClientWithRL providedHeaders providedQuery ctx extraErr tail acc1 acc2
275+ , Row.Cons label fn acc2 out
276+ , Row.Lacks label acc2
277+ , Row.Cons label (Route method segments request response) routeTail routeRow
278+ ) =>
279+ DeriveClientWithRL providedHeaders providedQuery ctx extraErr (RL.Cons label (Route method segments request response) tail) acc1 out where
280+ deriveClientWithRL config _ acc = Record.insert (Proxy :: _ label) clientFn rest
281+ where
282+ clientFn = deriveClientFnWith @_ @_ @_ @_ @providedHeaders @providedQuery @ctx @extraErr config (Proxy :: _ (Route method segments request response))
283+ rest = deriveClientWithRL @providedHeaders @providedQuery @ctx @extraErr config (Proxy :: _ tail) acc
284+
168class DeriveClientRL :: Row Type -> Row Type -> RowList Type -> Row Type -> Row Type -> Constraint
285class DeriveClientRL :: Row Type -> Row Type -> RowList Type -> Row Type -> Row Type -> Constraint
169class DeriveClientRL ctx extraErr rl acc out | ctx extraErr rl acc -> out where
286class DeriveClientRL ctx extraErr rl acc out | ctx extraErr rl acc -> out where
170 deriveClientRL :: String -> Proxy rl -> Record acc -> Record out
287 deriveClientRL :: String -> Proxy rl -> Record acc -> Record out
@@ -174,7 +291,7 @@ instance DeriveClientRL ctx extraErr RL.Nil acc acc where
174
291
175instance
292instance
176 ( IsSymbol label
293 ( IsSymbol label
177− , DeriveClientFn method segments request response ctx extraErr given routeErrors result fn
294+ , DeriveClientFn method segments request response ctx extraErr routeErrors result fn
178 , DeriveClientRL ctx extraErr tail acc1 acc2
295 , DeriveClientRL ctx extraErr tail acc1 acc2
179 , Row.Cons label fn acc2 out
296 , Row.Cons label fn acc2 out
180 , Row.Lacks label acc2
297 , Row.Lacks label acc2
@@ -183,5 +300,5 @@ instance
183 DeriveClientRL ctx extraErr (RL.Cons label (Route method segments request response) tail) acc1 out where
300 DeriveClientRL ctx extraErr (RL.Cons label (Route method segments request response) tail) acc1 out where
184 deriveClientRL baseUrl _ acc = Record.insert (Proxy :: _ label) clientFn rest
301 deriveClientRL baseUrl _ acc = Record.insert (Proxy :: _ label) clientFn rest
185 where
302 where
186− clientFn = deriveClientFn @_ @_ @_ @_ @ctx @extraErr @given baseUrl (Proxy :: _ (Route method segments request response))
303+ clientFn = deriveClientFn @_ @_ @_ @_ @ctx @extraErr baseUrl (Proxy :: _ (Route method segments request response))
187 rest = deriveClientRL @ctx @extraErr baseUrl (Proxy :: _ tail) acc
304 rest = deriveClientRL @ctx @extraErr baseUrl (Proxy :: _ tail) acc
src/Yoga/Fetch/Om/BuildUrl.purs+30−1
@@ -114,8 +114,22 @@ class AppendQueryParamsRL rl r where
114instance AppendQueryParamsRL RL.Nil r where
114instance AppendQueryParamsRL RL.Nil r where
115 appendQueryParamsRL _ _ acc = acc
115 appendQueryParamsRL _ _ acc = acc
116
116
117−-- Maybe ty: read with undefined-awareness — missing field → skip
117+-- Maybe (Array ty): optional repeated query param — missing/empty field → no pairs
118instance
118instance
119+ ( IsSymbol name
120+ , SerializeParam ty
121+ , AppendQueryParamsRL tail r
122+ ) =>
123+ AppendQueryParamsRL (RL.Cons name (Maybe (Array ty)) tail) r where
124+ appendQueryParamsRL _ rec acc = appendQueryParamsRL (Proxy :: _ tail) rec acc'
125+ where
126+ encName = encodeURIComponent_ (reflectSymbol (Proxy :: _ name))
127+ acc' = case (readOptional @name rec :: Maybe (Array ty)) of
128+ Nothing -> acc
129+ Just values -> acc <> map (\v -> encName <> "=" <> encodeURIComponent_ (serializeParam v)) values
130+
131+-- Maybe ty: read with undefined-awareness — missing field → skip
132+else instance
119 ( IsSymbol name
133 ( IsSymbol name
120 , SerializeParam ty
134 , SerializeParam ty
121 , AppendQueryParamsRL tail r
135 , AppendQueryParamsRL tail r
@@ -127,6 +141,21 @@ instance
127 Nothing -> acc
141 Nothing -> acc
128 Just value -> Array.snoc acc (encodeURIComponent_ (reflectSymbol (Proxy :: _ name)) <> "=" <> encodeURIComponent_ (serializeParam value))
142 Just value -> Array.snoc acc (encodeURIComponent_ (reflectSymbol (Proxy :: _ name)) <> "=" <> encodeURIComponent_ (serializeParam value))
129
143
144+-- Array ty: emit one `name=value` pair per element (repeated keys)
145+else instance
146+ ( IsSymbol name
147+ , SerializeParam ty
148+ , AppendQueryParamsRL tail r
149+ , Row.Cons name (Array ty) tailRow r
150+ ) =>
151+ AppendQueryParamsRL (RL.Cons name (Array ty) tail) r where
152+ appendQueryParamsRL _ rec acc =
153+ appendQueryParamsRL (Proxy :: _ tail) rec (acc <> pairs)
154+ where
155+ encName = encodeURIComponent_ (reflectSymbol (Proxy :: _ name))
156+ values = Record.get (Proxy :: _ name) rec
157+ pairs = map (\v -> encName <> "=" <> encodeURIComponent_ (serializeParam v)) values
158+
130-- bare ty (Required query param): always present
159-- bare ty (Required query param): always present
131else instance
160else instance
132 ( IsSymbol name
161 ( IsSymbol name
src/Yoga/Fetch/Om/ClientFunction.purs+16−16
@@ -22,17 +22,17 @@ instance CheckBodyIsUnit Unit IsUnit
22else instance CheckBodyIsUnit body IsNotUnit
22else instance CheckBodyIsUnit body IsNotUnit
23
23
24class BuildClientFn
24class BuildClientFn
25− :: RowList Type -> RowList Type -> Type -> Type -> Row Type -> Row Type -> Row Type -> Type -> Row Type -> Type -> Type -> Constraint
25+ :: RowList Type -> RowList Type -> Type -> Type -> Row Type -> Row Type -> Type -> Type -> Row Type -> Type -> Type -> Constraint
26class
26class
27− BuildClientFn pathQueryRL headersRL bodyFlag body pathQuery headers given ctx errorRow result fn
27+ BuildClientFn pathQueryRL headersRL bodyFlag body pathQuery headers arg ctx errorRow result fn
28 | pathQueryRL -> pathQuery
28 | pathQueryRL -> pathQuery
29 , headersRL -> headers
29 , headersRL -> headers
30− , pathQueryRL headersRL bodyFlag body given ctx errorRow result -> fn where
30+ , pathQueryRL headersRL bodyFlag body arg ctx errorRow result -> fn where
31 buildClientFn
31 buildClientFn
32 :: Proxy pathQueryRL
32 :: Proxy pathQueryRL
33 -> Proxy headersRL
33 -> Proxy headersRL
34 -> Proxy bodyFlag
34 -> Proxy bodyFlag
35− -> (Record given -> Record pathQuery)
35+ -> (arg -> Record pathQuery)
36 -> (Record pathQuery -> Record headers -> body -> Om ctx errorRow result)
36 -> (Record pathQuery -> Record headers -> body -> Om ctx errorRow result)
37 -> fn
37 -> fn
38
38
@@ -44,7 +44,7 @@ instance
44 Unit
44 Unit
45 ()
45 ()
46 ()
46 ()
47− given
47+ arg
48 ctx
48 ctx
49 errorRow
49 errorRow
50 result
50 result
@@ -59,7 +59,7 @@ instance
59 body
59 body
60 ()
60 ()
61 ()
61 ()
62− given
62+ arg
63 ctx
63 ctx
64 errorRow
64 errorRow
65 result
65 result
@@ -74,7 +74,7 @@ instance
74 Unit
74 Unit
75 ()
75 ()
76 h
76 h
77− given
77+ arg
78 ctx
78 ctx
79 errorRow
79 errorRow
80 result
80 result
@@ -89,7 +89,7 @@ instance
89 body
89 body
90 ()
90 ()
91 h
91 h
92− given
92+ arg
93 ctx
93 ctx
94 errorRow
94 errorRow
95 result
95 result
@@ -104,11 +104,11 @@ instance
104 Unit
104 Unit
105 pq
105 pq
106 ()
106 ()
107− given
107+ arg
108 ctx
108 ctx
109 errorRow
109 errorRow
110 result
110 result
111− (Record given -> Om ctx errorRow result) where
111+ (arg -> Om ctx errorRow result) where
112 buildClientFn _ _ _ convert f pqr = f (convert pqr) {} unit
112 buildClientFn _ _ _ convert f pqr = f (convert pqr) {} unit
113
113
114-- With path/query, no headers, with body
114-- With path/query, no headers, with body
@@ -119,11 +119,11 @@ instance
119 body
119 body
120 pq
120 pq
121 ()
121 ()
122− given
122+ arg
123 ctx
123 ctx
124 errorRow
124 errorRow
125 result
125 result
126− (Record given -> body -> Om ctx errorRow result) where
126+ (arg -> body -> Om ctx errorRow result) where
127 buildClientFn _ _ _ convert f pqr b = f (convert pqr) {} b
127 buildClientFn _ _ _ convert f pqr b = f (convert pqr) {} b
128
128
129-- With path/query, with headers, no body
129-- With path/query, with headers, no body
@@ -134,11 +134,11 @@ instance
134 Unit
134 Unit
135 pq
135 pq
136 h
136 h
137− given
137+ arg
138 ctx
138 ctx
139 errorRow
139 errorRow
140 result
140 result
141− (Record given -> Record h -> Om ctx errorRow result) where
141+ (arg -> Record h -> Om ctx errorRow result) where
142 buildClientFn _ _ _ convert f pqr hdrs = f (convert pqr) hdrs unit
142 buildClientFn _ _ _ convert f pqr hdrs = f (convert pqr) hdrs unit
143
143
144-- With path/query, with headers, with body
144-- With path/query, with headers, with body
@@ -149,9 +149,9 @@ instance
149 body
149 body
150 pq
150 pq
151 h
151 h
152− given
152+ arg
153 ctx
153 ctx
154 errorRow
154 errorRow
155 result
155 result
156− (Record given -> Record h -> body -> Om ctx errorRow result) where
156+ (arg -> Record h -> body -> Om ctx errorRow result) where
157 buildClientFn _ _ _ convert f pqr hdrs b = f (convert pqr) hdrs b
157 buildClientFn _ _ _ convert f pqr hdrs b = f (convert pqr) hdrs b
src/Yoga/Fetch/Om/ParseResponse.purs+16−1
@@ -33,7 +33,7 @@ import Yoga.HTTP.API.Route.Encoding (PlainText, Streaming)
33import Yoga.HTTP.API.Route.StatusCode (class StatusCodeMap, statusCodeFor, StatusCode(..))
33import Yoga.HTTP.API.Route.StatusCode (class StatusCodeMap, statusCodeFor, StatusCode(..))
34import Yoga.JSON (class ReadForeign, readJSON)
34import Yoga.JSON (class ReadForeign, readJSON)
35import Yoga.JSON.Error (withStringErrors)
35import Yoga.JSON.Error (withStringErrors)
36−import Yoga.Om (class ToOm, Om, toOm)
36+import Yoga.Om (Om, toOm)
37import Yoga.Om.Error (Exception)
37import Yoga.Om.Error (Exception)
38import Yoga.Om.Strom (Strom)
38import Yoga.Om.Strom (Strom)
39import Yoga.Om.Strom.WebStream as WebStream
39import Yoga.Om.Strom.WebStream as WebStream
@@ -106,6 +106,21 @@ else instance
106 else
106 else
107 parseSuccessRL (Proxy :: _ tailRL) actualStatus fetchResp
107 parseSuccessRL (Proxy :: _ tailRL) actualStatus fetchResp
108
108
109+
110+else instance
111+ ( IsSymbol label
112+ , StatusCodeMap label
113+ , Row.Cons label Unit tailRow successRow
114+ , Row.Lacks label tailRow
115+ , ParseSuccessRL tailRL successRow
116+ ) =>
117+ ParseSuccessRL (RL.Cons label Unit tailRL) successRow where
118+ parseSuccessRL _ actualStatus fetchResp = do
119+ let StatusCode expected = statusCodeFor (Proxy :: _ label)
120+ if actualStatus == expected then
121+ pure (Variant.inj (Proxy :: _ label) unit)
122+ else
123+ parseSuccessRL (Proxy :: _ tailRL) actualStatus fetchResp
109else instance
124else instance
110 ( IsSymbol label
125 ( IsSymbol label
111 , StatusCodeMap label
126 , StatusCodeMap label
src/Yoga/Fetch/Om/QueryDefaults.pursdeleted−135
@@ -1,135 +0,0 @@
1−module Yoga.Fetch.Om.QueryDefaults
2− ( class BuildMaybeQuery
3− , buildMaybeQuery
4− , class HasField
5− , class BuildMaybeQueryField
6− , buildMaybeQueryField
7− , class SplitQueryRow
8− , class SplitQueryRowRL
9− , class BuildQueryFromPartial
10− , buildQueryFromPartial
11− , class BuildQueryFromPartialRL
12− , buildQueryFromPartialRL
13− ) where
14−
15−import Data.Maybe (Maybe(..))
16−import Data.Symbol (class IsSymbol)
17−import Prim.Boolean (True, False)
18−import Prim.Row as Row
19−import Prim.RowList (class RowToList, RowList)
20−import Prim.RowList as RL
21−import Record as Record
22−import Type.Proxy (Proxy(..))
23−
24−-- | Check whether a label exists in a RowList
25−class HasField (name :: Symbol) (rl :: RowList Type) (has :: Boolean) | name rl -> has
26−
27−instance HasField name RL.Nil False
28−instance HasField name (RL.Cons name ty tail) True
29−else instance HasField name tail has => HasField name (RL.Cons other ty tail) has
30−
31−-- | Get a Maybe-wrapped value from a record, based on whether the field exists
32−class BuildMaybeQueryField (has :: Boolean) (name :: Symbol) (ty :: Type) (provided :: Row Type) where
33− buildMaybeQueryField :: Proxy has -> Proxy name -> Record provided -> Maybe ty
34−
35−instance (IsSymbol name, Row.Cons name ty rest provided) =>
36− BuildMaybeQueryField True name ty provided where
37− buildMaybeQueryField _ _ provided = Just (Record.get (Proxy :: _ name) provided)
38−
39−instance BuildMaybeQueryField False name ty provided where
40− buildMaybeQueryField _ _ _ = Nothing
41−
42−-- | Build a full Maybe-wrapped query record from a partial provided record.
43−-- | Fields present in provided get Just, missing fields get Nothing.
44−class BuildMaybeQuery (rl :: RowList Type) (providedRL :: RowList Type) (provided :: Row Type) (queryParams :: Row Type) | rl -> queryParams where
45− buildMaybeQuery :: Proxy rl -> Proxy providedRL -> Record provided -> Record queryParams
46−
47−instance BuildMaybeQuery RL.Nil providedRL provided () where
48− buildMaybeQuery _ _ _ = {}
49−
50−instance
51− ( IsSymbol name
52− , HasField name providedRL has
53− , BuildMaybeQueryField has name ty provided
54− , BuildMaybeQuery tail providedRL provided tailQuery
55− , Row.Cons name (Maybe ty) tailQuery queryParams
56− , Row.Lacks name tailQuery
57− ) =>
58− BuildMaybeQuery (RL.Cons name ty tail) providedRL provided queryParams where
59− buildMaybeQuery _ prl provided = Record.insert (Proxy :: _ name) val rest
60− where
61− val = buildMaybeQueryField (Proxy :: _ has) (Proxy :: _ name) provided
62− rest = buildMaybeQuery (Proxy :: _ tail) prl provided
63−
64−-- | Split a query params row into optional raw types and required raw types.
65−-- | Maybe ty → optional with raw type ty
66−-- | ty (no Maybe) → required with raw type ty (from Required query params)
67−class SplitQueryRow (queryParams :: Row Type) (optionalRaw :: Row Type) (requiredRaw :: Row Type) | queryParams -> optionalRaw requiredRaw
68−
69−instance (RowToList queryParams rl, SplitQueryRowRL rl optionalRaw requiredRaw) => SplitQueryRow queryParams optionalRaw requiredRaw
70−
71−class SplitQueryRowRL (rl :: RowList Type) (optionalRaw :: Row Type) (requiredRaw :: Row Type) | rl -> optionalRaw requiredRaw
72−
73−instance SplitQueryRowRL RL.Nil () ()
74−
75−-- Maybe ty field → optional
76−instance
77− ( SplitQueryRowRL tail optTail reqTail
78− , Row.Cons name ty optTail optionalRaw
79− , Row.Lacks name optTail
80− ) =>
81− SplitQueryRowRL (RL.Cons name (Maybe ty) tail) optionalRaw reqTail
82−
83−-- Non-Maybe field → required (from Required query params)
84−else instance
85− ( SplitQueryRowRL tail optTail reqTail
86− , Row.Cons name ty reqTail requiredRaw
87− , Row.Lacks name reqTail
88− ) =>
89− SplitQueryRowRL (RL.Cons name ty tail) optTail requiredRaw
90−
91−-- | Build full queryParams from a partial record that contains path params,
92−-- | required query params, and optionally some optional query params.
93−-- | Walks the queryParams RowList:
94−-- | Maybe ty field → check if raw value provided, wrap in Just or Nothing
95−-- | bare ty field → copy from provided (required, always present)
96−class BuildQueryFromPartial (queryParams :: Row Type) (provided :: Row Type) where
97− buildQueryFromPartial :: Record provided -> Record queryParams
98−
99−instance (RowToList queryParams rl, RowToList provided prl, BuildQueryFromPartialRL rl prl provided queryParams) => BuildQueryFromPartial queryParams provided where
100− buildQueryFromPartial = buildQueryFromPartialRL (Proxy :: _ rl) (Proxy :: _ prl)
101−
102−class BuildQueryFromPartialRL (rl :: RowList Type) (providedRL :: RowList Type) (provided :: Row Type) (queryParams :: Row Type) | rl -> queryParams where
103− buildQueryFromPartialRL :: Proxy rl -> Proxy providedRL -> Record provided -> Record queryParams
104−
105−instance BuildQueryFromPartialRL RL.Nil providedRL provided () where
106− buildQueryFromPartialRL _ _ _ = {}
107−
108−-- Maybe ty: check if field is provided, wrap in Just or use Nothing
109−instance
110− ( IsSymbol name
111− , HasField name providedRL has
112− , BuildMaybeQueryField has name ty provided
113− , BuildQueryFromPartialRL tail providedRL provided tailQuery
114− , Row.Cons name (Maybe ty) tailQuery queryParams
115− , Row.Lacks name tailQuery
116− ) =>
117− BuildQueryFromPartialRL (RL.Cons name (Maybe ty) tail) providedRL provided queryParams where
118− buildQueryFromPartialRL _ prl provided = Record.insert (Proxy :: _ name) val rest
119− where
120− val = buildMaybeQueryField (Proxy :: _ has) (Proxy :: _ name) provided
121− rest = buildQueryFromPartialRL (Proxy :: _ tail) prl provided
122−
123−-- bare ty (Required): always present in provided, copy directly
124−else instance
125− ( IsSymbol name
126− , Row.Cons name ty rest provided
127− , BuildQueryFromPartialRL tail providedRL provided tailQuery
128− , Row.Cons name ty tailQuery queryParams
129− , Row.Lacks name tailQuery
130− ) =>
131− BuildQueryFromPartialRL (RL.Cons name ty tail) providedRL provided queryParams where
132− buildQueryFromPartialRL _ prl provided = Record.insert (Proxy :: _ name) val rest
133− where
134− val = Record.get (Proxy :: _ name) provided
135− rest = buildQueryFromPartialRL (Proxy :: _ tail) prl provided
test/BodylessResponse.Example.purs+1−1
@@ -23,5 +23,5 @@ type ResourceAPI =
23-- This must typecheck: the derived client exposes notModified/unauthorized as Unit errors.
23-- This must typecheck: the derived client exposes notModified/unauthorized as Unit errors.
24api
24api
25 :: forall ctx err
25 :: forall ctx err
26− . { getResource :: Om ctx (notModified :: Unit, unauthorized :: Unit | err) Resource }
26+ . { getResource :: Om (Record ctx) (notModified :: Unit, unauthorized :: Unit | err) Resource }
27api = client @ResourceAPI "https://api.example.com"
27api = client @ResourceAPI "https://api.example.com"
test/BodylessResponse.Spec.purs+31−4
@@ -12,9 +12,9 @@ import Test.Spec (Spec, around, describe, it)
12import Test.Spec.Assertions (shouldEqual)
12import Test.Spec.Assertions (shouldEqual)
13import Yoga.Fastify.Fastify as F
13import Yoga.Fastify.Fastify as F
14import Yoga.Fastify.Fastify (Port(..), Host(..), RouteURL(..), StatusCode(..))
14import Yoga.Fastify.Fastify (Port(..), Host(..), RouteURL(..), StatusCode(..))
15−import Yoga.Fastify.Om (createOmFastify, getOm)
15+import Yoga.Fastify.Om (createOmFastify, deleteOm, getOm)
16−import Yoga.Fetch.Om (GET, Route, type (/), client)
16+import Yoga.Fetch.Om (DELETE, GET, Route, type (/), type (:), client)
17−import Yoga.Om (Om, runOm)
17+import Yoga.Om (runOm)
18
18
19type Resource = { id :: Int, name :: String }
19type Resource = { id :: Int, name :: String }
20
20
@@ -27,9 +27,15 @@ type ResourceAPI =
27 , notModified :: {}
27 , notModified :: {}
28 , unauthorized :: {}
28 , unauthorized :: {}
29 )
29 )
30+ , deleteUser ::
31+ Route DELETE
32+ ("bodyless-test" / "users" / "id" : Int)
33+ {}
34+ ( noContent :: {}
35+ , notFound :: { body :: { error :: String } }
36+ )
30 }
37 }
31
38
32−api :: forall ctx err. { getResource :: Om ctx (notModified :: Unit, unauthorized :: Unit | err) Resource }
33api = client @ResourceAPI "http://localhost:44932"
39api = client @ResourceAPI "http://localhost:44932"
34
40
35spec :: Spec Unit
41spec :: Spec Unit
@@ -68,6 +74,15 @@ spec = around withServer $ describe "bodyless responses (notModified / unauthori
68 api.getResource
74 api.getResource
69 liftAff $ resource `shouldEqual` { id: 1, name: "test" }
75 liftAff $ resource `shouldEqual` { id: 1, name: "test" }
70
76
77+ it "204 No Content returns Unit for noContent shorthand" \serverMode -> do
78+ liftEffect $ Ref.write "deleted" serverMode
79+ result <- runOm {}
80+ { exception: \_ -> pure false
81+ , notFound: \_ -> pure false
82+ }
83+ (api.deleteUser { id: 1 } $> true)
84+ liftAff $ result `shouldEqual` true
85+
71withServer :: forall a. (Ref String -> Aff a) -> Aff a
86withServer :: forall a. (Ref String -> Aff a) -> Aff a
72withServer test = bracket acquire release (\{ serverMode } -> test serverMode)
87withServer test = bracket acquire release (\{ serverMode } -> test serverMode)
73 where
88 where
@@ -77,6 +92,7 @@ withServer test = bracket acquire release (\{ serverMode } -> test serverMode)
77 f <- F.fastify {}
92 f <- F.fastify {}
78 omApp <- createOmFastify {} f
93 omApp <- createOmFastify {} f
79 getOm (RouteURL "/bodyless-test/resource") (handler serverMode) omApp
94 getOm (RouteURL "/bodyless-test/resource") (handler serverMode) omApp
95+ deleteOm (RouteURL "/bodyless-test/users/:id") (deleteHandler serverMode) omApp
80 pure f
96 pure f
81 void $ F.listen { port: Port 44932, host: Host "0.0.0.0" } fastify
97 void $ F.listen { port: Port 44932, host: Host "0.0.0.0" } fastify
82 pure { fastify, serverMode }
98 pure { fastify, serverMode }
@@ -96,3 +112,14 @@ withServer test = bracket acquire release (\{ serverMode } -> test serverMode)
96 void $ F.status (StatusCode 200) reply # liftEffect
112 void $ F.status (StatusCode 200) reply # liftEffect
97 void $ F.header "content-type" "application/json" reply # liftEffect
113 void $ F.header "content-type" "application/json" reply # liftEffect
98 F.sendJson (unsafeToForeign { id: 1, name: "test" }) reply # liftAff
114 F.sendJson (unsafeToForeign { id: 1, name: "test" }) reply # liftAff
115+
116+ deleteHandler serverMode reply = do
117+ mode <- liftEffect $ Ref.read serverMode
118+ case mode of
119+ "deleted" -> do
120+ void $ F.status (StatusCode 204) reply # liftEffect
121+ F.send (unsafeToForeign "") reply # liftAff
122+ _ -> do
123+ void $ F.status (StatusCode 404) reply # liftEffect
124+ void $ F.header "content-type" "application/json" reply # liftEffect
125+ F.sendJson (unsafeToForeign { error: "Missing user" }) reply # liftAff
test/BuildUrl.Spec.purs+21
@@ -80,6 +80,27 @@ spec = do
80 let result = appendQueryParams @(search :: String) { search: "hello world&more=yes" } "/users"
80 let result = appendQueryParams @(search :: String) { search: "hello world&more=yes" } "/users"
81 result `shouldEqual` "/users?search=hello%20world%26more%3Dyes"
81 result `shouldEqual` "/users?search=hello%20world%26more%3Dyes"
82
82
83+ it "repeats Array query params one pair per element" do
84+ let
85+ result = appendQueryParams @(categories :: Array Int)
86+ { categories: [ 3030, 103130 ] }
87+ "/users"
88+ result `shouldEqual` "/users?categories=3030&categories=103130"
89+
90+ it "omits empty Array query params" do
91+ let
92+ result = appendQueryParams @(categories :: Array Int)
93+ { categories: [] }
94+ "/users"
95+ result `shouldEqual` "/users"
96+
97+ it "mixes Array and bare query params" do
98+ let
99+ result = appendQueryParams @(query :: String, categories :: Array Int)
100+ { query: "foo", categories: [ 1, 2 ] }
101+ "/users"
102+ result `shouldEqual` "/users?categories=1&categories=2&query=foo"
103+
83 describe "buildUrl" do
104 describe "buildUrl" do
84 it "builds URL with path params only" do
105 it "builds URL with path params only" do
85 let
106 let
test/Complete.Example.purs+14−19
@@ -3,9 +3,9 @@ module Complete.Example where
3import Prelude
3import Prelude
4
4
5import Data.Array (length)
5import Data.Array (length)
6−import Data.Maybe (Maybe(..))
6+import Justifill (justifill)
7import Effect.Class.Console (log)
7import Effect.Class.Console (log)
8−import Yoga.Fetch.Om (GET, POST, PUT, DELETE, Route, JSON, Path, type (/), type (:), type (:?), client)
8+import Yoga.Fetch.Om (GET, POST, PUT, DELETE, Route, JSON, type (/), type (:), type (:?), client)
9import Yoga.HTTP.API.Route (BearerToken(..))
9import Yoga.HTTP.API.Route (BearerToken(..))
10import Yoga.Om (Om, handleErrors)
10import Yoga.Om (Om, handleErrors)
11
11
@@ -31,43 +31,37 @@ type ErrorMessage =
31
31
32type UserAPI =
32type UserAPI =
33 { getUser ::
33 { getUser ::
34− Route GET
35− (Path ("users" / "id" : Int))
34+ Route GET ("users" / "id" : Int)
36 {}
35 {}
37 ( ok :: { body :: User }
36 ( ok :: { body :: User }
38 , notFound :: { body :: ErrorMessage }
37 , notFound :: { body :: ErrorMessage }
39 )
38 )
40 , listUsers ::
39 , listUsers ::
41− Route GET
42− (Path "users" :? { limit :: Int, offset :: Int })
40+ Route GET ("users" :? { limit :: Int, offset :: Int })
43 {}
41 {}
44 ( ok :: { body :: Array User }
42 ( ok :: { body :: Array User }
45 )
43 )
46 , createUser ::
44 , createUser ::
47− Route POST
45+ Route POST "users"
48− (Path "users")
49 { body :: JSON CreateUserRequest }
46 { body :: JSON CreateUserRequest }
50 ( created :: { body :: User }
47 ( created :: { body :: User }
51 , badRequest :: { body :: ErrorMessage }
48 , badRequest :: { body :: ErrorMessage }
52 )
49 )
53 , updateUser ::
50 , updateUser ::
54− Route PUT
55− (Path ("users" / "id" : Int))
51+ Route PUT ("users" / "id" : Int)
56 { body :: JSON UpdateUserRequest }
52 { body :: JSON UpdateUserRequest }
57 ( ok :: { body :: User }
53 ( ok :: { body :: User }
58 , notFound :: { body :: ErrorMessage }
54 , notFound :: { body :: ErrorMessage }
59 , badRequest :: { body :: ErrorMessage }
55 , badRequest :: { body :: ErrorMessage }
60 )
56 )
61 , deleteUser ::
57 , deleteUser ::
62− Route DELETE
58+ Route DELETE ("users" / "id" : Int)
63− (Path ("users" / "id" : Int))
64 {}
59 {}
65− ( noContent :: { body :: {} }
60+ ( noContent :: {}
66 , notFound :: { body :: ErrorMessage }
61 , notFound :: { body :: ErrorMessage }
67 )
62 )
68 , createUserAuth ::
63 , createUserAuth ::
69− Route POST
64+ Route POST "users"
70− (Path "users")
71 { headers :: Record (authorization :: BearerToken), body :: JSON CreateUserRequest }
65 { headers :: Record (authorization :: BearerToken), body :: JSON CreateUserRequest }
72 ( created :: { body :: User }
66 ( created :: { body :: User }
73 , badRequest :: { body :: ErrorMessage }
67 , badRequest :: { body :: ErrorMessage }
@@ -85,15 +79,15 @@ exampleGetUserHandled :: Om {} () Unit
85exampleGetUserHandled = do
79exampleGetUserHandled = do
86 user <- api.getUser { id: 42 }
80 user <- api.getUser { id: 42 }
87 # handleErrors
81 # handleErrors
88− { notFound: \err -> do
82+ { notFound: \err -> ado
89 log $ "User not found: " <> err.error
83 log $ "User not found: " <> err.error
90− pure { id: 0, name: "unknown", email: "" }
84+ in { id: 0, name: "unknown", email: "" }
91 }
85 }
92 log $ "Found user: " <> user.name
86 log $ "Found user: " <> user.name
93
87
94exampleListUsers :: Om {} () Unit
88exampleListUsers :: Om {} () Unit
95exampleListUsers = do
89exampleListUsers = do
96− users <- api.listUsers { limit: Just 10, offset: Just 0 }
90+ users <- api.listUsers (justifill { limit: 10, offset: 0 })
97 log $ "Found " <> show (length users) <> " users"
91 log $ "Found " <> show (length users) <> " users"
98
92
99exampleCreateUser :: Om {} (badRequest :: ErrorMessage) Unit
93exampleCreateUser :: Om {} (badRequest :: ErrorMessage) Unit
@@ -106,7 +100,8 @@ exampleCreateUser = do
106
100
107exampleUpdateUser :: Om {} (notFound :: ErrorMessage, badRequest :: ErrorMessage) Unit
101exampleUpdateUser :: Om {} (notFound :: ErrorMessage, badRequest :: ErrorMessage) Unit
108exampleUpdateUser = do
102exampleUpdateUser = do
109− user <- api.updateUser { id: 42 }
103+ user <- api.updateUser
104+ { id: 42 }
110 { name: "Alice Updated"
105 { name: "Alice Updated"
111 , email: "alice.new@example.com"
106 , email: "alice.new@example.com"
112 }
107 }
test/Example.purs+13−8
@@ -4,41 +4,46 @@ import Yoga.HTTP.API.Route (GET, POST, DELETE, Route, BearerToken)
4import Yoga.HTTP.API.Route.Encoding (JSON)
4import Yoga.HTTP.API.Route.Encoding (JSON)
5import Yoga.HTTP.API.Path (Path, type (/), type (:), type (:?))
5import Yoga.HTTP.API.Path (Path, type (/), type (:), type (:?))
6
6
7−type User = { id :: Int, name :: String, email :: String }
7+type User = { id :: UserId, name :: UserName, email :: UserMail }
8−type CreateUserRequest = { name :: String, email :: String }
8+type CreateUserRequest = { name :: String, email :: UserMail }
9type ErrorMessage = { error :: String }
9type ErrorMessage = { error :: String }
10+newtype UserId = UserId Int
11+newtype UserName = UserName String
12+newtype UserMail = UserMail String
13+newtype Limit = Limit Int
14+newtype Offset = Offset Int
10
15
11type UserAPI =
16type UserAPI =
12 { getUser ::
17 { getUser ::
13 Route GET
18 Route GET
14− (Path ("users" / "id" : Int))
19+ ("users" / "id" : UserId)
15 {}
20 {}
16 ( ok :: { body :: User }
21 ( ok :: { body :: User }
17 , notFound :: { body :: ErrorMessage }
22 , notFound :: { body :: ErrorMessage }
18 )
23 )
19 , listUsers ::
24 , listUsers ::
20 Route GET
25 Route GET
21− (Path "users" :? { limit :: Int, offset :: Int })
26+ ("users" :? { limit :: Limit, offset :: Offset })
22 {}
27 {}
23 ( ok :: { body :: Array User }
28 ( ok :: { body :: Array User }
24 )
29 )
25 , createUser ::
30 , createUser ::
26 Route POST
31 Route POST
27− (Path "users")
32+ "users"
28 { body :: JSON CreateUserRequest }
33 { body :: JSON CreateUserRequest }
29 ( created :: { body :: User }
34 ( created :: { body :: User }
30 , badRequest :: { body :: ErrorMessage }
35 , badRequest :: { body :: ErrorMessage }
31 )
36 )
32 , deleteUser ::
37 , deleteUser ::
33 Route DELETE
38 Route DELETE
34− (Path ("users" / "id" : Int))
39+ ("users" / "id" : UserId)
35 {}
40 {}
36− ( noContent :: { body :: {} }
41+ ( noContent :: {}
37 , notFound :: { body :: ErrorMessage }
42 , notFound :: { body :: ErrorMessage }
38 )
43 )
39 , createUserAuth ::
44 , createUserAuth ::
40 Route POST
45 Route POST
41− (Path "users")
46+ "users"
42 { headers :: Record (authorization :: BearerToken), body :: JSON CreateUserRequest }
47 { headers :: Record (authorization :: BearerToken), body :: JSON CreateUserRequest }
43 ( created :: { body :: User }
48 ( created :: { body :: User }
44 , badRequest :: { body :: ErrorMessage }
49 , badRequest :: { body :: ErrorMessage }
test/PlainTextResponse.Spec.purs+1−1
@@ -29,7 +29,7 @@ type TestAPI =
29 )
29 )
30 }
30 }
31
31
32−api :: { double :: forall ctx err. Payload -> Om ctx (badRequest :: String | err) Result }
32+api :: { double :: forall ctx err. Payload -> Om (Record ctx) (badRequest :: String | err) Result }
33api = client @TestAPI "http://localhost:44933"
33api = client @TestAPI "http://localhost:44933"
34
34
35spec :: Spec Unit
35spec :: Spec Unit
test/Polymorphic.Spec.purs+20−17
@@ -12,7 +12,8 @@ import Yoga.Fastify.Fastify (Port(..), Host(..))
12import Yoga.Fastify.Om.API (registerAPI)
12import Yoga.Fastify.Om.API (registerAPI)
13import Yoga.Fastify.Om.Route (Handler, handle, respond)
13import Yoga.Fastify.Om.Route (Handler, handle, respond)
14import Yoga.Fetch.Om (GET, Route, type (/), type (:), client)
14import Yoga.Fetch.Om (GET, Route, type (/), type (:), client)
15−import Yoga.Om (Om, ask, runOm)
15+import Yoga.Om (ask, runOm)
16+import Control.Monad.Error.Class (throwError)
16
17
17type User = { id :: Int, name :: String }
18type User = { id :: Int, name :: String }
18
19
@@ -27,26 +28,28 @@ api = client @TestAPI "http://localhost:44931"
27spec :: Spec Unit
28spec :: Spec Unit
28spec = around withServer $ describe "polymorphic client" do
29spec = around withServer $ describe "polymorphic client" do
29 it "fetches user with { token :: String } context" \_ ->
30 it "fetches user with { token :: String } context" \_ ->
30− runOm { token: "secret123" } { exception: \_ -> pure unit } do
31+ runOm { token: "secret123" } { exception: throwError } do
31− ctx <- ask
32+ { token } <- ask
32− user <- api.getUser { id: 42 }
33+ { name } <- api.getUser { id: 42 }
33− liftAff $ (ctx.token <> ":" <> user.name) `shouldEqual` "secret123:User 42"
34+ liftAff $ (token <> ":" <> name) `shouldEqual` "secret123:User 42"
34
35
35 it "fetches user with { apiKey :: String, userId :: Int } context" \_ ->
36 it "fetches user with { apiKey :: String, userId :: Int } context" \_ ->
36− runOm { apiKey: "mykey", userId: 7 } { exception: \_ -> pure unit } do
37+ runOm { apiKey: "mykey", userId: 7 } { exception: throwError } do
37− ctx <- ask
38+ { apiKey, userId } <- ask
38− user <- api.getUser { id: ctx.userId }
39+ { name } <- api.getUser { id: userId }
39− liftAff $ (ctx.apiKey <> ":" <> user.name) `shouldEqual` "mykey:User 7"
40+ liftAff $ (apiKey <> ":" <> name) `shouldEqual` "mykey:User 7"
40
41
41 it "same api.getUser works in two different contexts" \_ -> do
42 it "same api.getUser works in two different contexts" \_ -> do
43+
42− runOm { token: "tok1" } { exception: \_ -> pure unit } do
44+ runOm { token: "tok1" } { exception: throwError } do
43− ctx <- ask
45+ { token } <- ask
44− user <- api.getUser { id: 1 }
46+ { name } <- api.getUser { id: 1 }
45− liftAff $ (ctx.token <> ":" <> user.name) `shouldEqual` "tok1:User 1"
47+ liftAff $ (token <> ":" <> name) `shouldEqual` "tok1:User 1"
48+
46− runOm { endpoint: "prod", region: "eu" } { exception: \_ -> pure unit } do
49+ runOm { endpoint: "prod", region: "eu" } { exception: throwError } do
47− ctx <- ask
50+ { endpoint, region } <- ask
48− user <- api.getUser { id: 2 }
51+ { name } <- api.getUser { id: 2 }
49− liftAff $ (ctx.endpoint <> "-" <> ctx.region <> ":" <> user.name) `shouldEqual` "prod-eu:User 2"
52+ liftAff $ (endpoint <> "-" <> region <> ":" <> name) `shouldEqual` "prod-eu:User 2"
50
53
51withServer :: forall a. (Unit -> _ a) -> _ a
54withServer :: forall a. (Unit -> _ a) -> _ a
52withServer test = bracket acquire release (\_ -> test unit)
55withServer test = bracket acquire release (\_ -> test unit)
test/Roundtrip.Spec.purs+84−18
@@ -6,14 +6,17 @@ import Data.Maybe (Maybe(..))
6import Effect.Aff (bracket)
6import Effect.Aff (bracket)
7import Effect.Aff.Class (liftAff)
7import Effect.Aff.Class (liftAff)
8import Effect.Class (liftEffect)
8import Effect.Class (liftEffect)
9+import Effect.Ref as Ref
10+import Effect.Exception as Exn
11+import Justifill (justifill)
9import Test.Spec (Spec, around, describe, it)
12import Test.Spec (Spec, around, describe, it)
10import Test.Spec.Assertions (shouldEqual)
13import Test.Spec.Assertions (shouldEqual)
11import Yoga.Fastify.Fastify as F
14import Yoga.Fastify.Fastify as F
12import Yoga.Fastify.Fastify (Port(..), Host(..))
15import Yoga.Fastify.Fastify (Port(..), Host(..))
13import Yoga.Fastify.Om.API (registerAPI)
16import Yoga.Fastify.Om.API (registerAPI)
14import Yoga.Fastify.Om.Route (Handler, handle, respond, reject, BearerToken(..))
17import Yoga.Fastify.Om.Route (Handler, handle, respond, reject, BearerToken(..))
15−import Yoga.Fetch.Om (GET, POST, PUT, DELETE, Route, JSON, type (/), type (:), type (:?), client)
18+import Yoga.Fetch.Om (GET, POST, PUT, DELETE, Required, Route, JSON, type (/), type (:), type (:?), client, clientWith)
16−import Yoga.Om (Om, ask, handleErrors, runOm)
19+import Yoga.Om (ask, handleErrors, runOm)
17
20
18-- Types
21-- Types
19
22
@@ -23,6 +26,7 @@ type ErrorMsg = { error :: String }
23
26
24-- Route definitions shared by server and client
27-- Route definitions shared by server and client
25
28
29+
26type GetUserRoute = Route GET
30type GetUserRoute = Route GET
27 ("users" / "id" : Int)
31 ("users" / "id" : Int)
28 {}
32 {}
@@ -68,6 +72,28 @@ type SearchRoute = Route GET
68 {}
72 {}
69 (ok :: { body :: { active :: Boolean, minScore :: Number } })
73 (ok :: { body :: { active :: Boolean, minScore :: Number } })
70
74
75+type InjectedTenantRoute = Route GET
76+ ("tenant-users" / "id" : Int :? { tenant :: Required String })
77+ { headers :: { authorization :: BearerToken, requestid :: String } }
78+ ( ok :: { body :: User }
79+ , unauthorized :: { body :: ErrorMsg }
80+ )
81+
82+type InjectedAPI =
83+ { getTenantUser :: InjectedTenantRoute
84+ }
85+
86+injectedApi tokenRef =
87+ clientWith @InjectedAPI
88+ { baseUrl: pure "http://localhost:44932"
89+ , provide: do
90+ ctx <- ask
91+ token <- liftEffect $ Ref.read tokenRef
92+ pure
93+ { headers: { authorization: BearerToken token, requestid: ctx.requestid }
94+ , query: { tenant: ctx.tenant }
95+ }
96+ }
71type API =
97type API =
72 { getUser :: GetUserRoute
98 { getUser :: GetUserRoute
73 , listUsers :: ListUsersRoute
99 , listUsers :: ListUsersRoute
@@ -82,6 +108,7 @@ api = client @API "http://localhost:44932"
82
108
83-- Handlers
109-- Handlers
84
110
111+
85getUserHandler :: Handler GetUserRoute ()
112getUserHandler :: Handler GetUserRoute ()
86getUserHandler = handle do
113getUserHandler = handle do
87 { path } <- ask
114 { path } <- ask
@@ -147,11 +174,19 @@ searchHandler = handle do
147 Nothing -> 0.0
174 Nothing -> 0.0
148 respond @"ok" { active, minScore }
175 respond @"ok" { active, minScore }
149
176
177+tenantUserHandler :: Handler InjectedTenantRoute ()
178+tenantUserHandler = handle do
179+ { path, query, headers } <- ask
180+ case headers.authorization of
181+ BearerToken "secret" -> respond @"ok" { id: path.id, name: query.tenant <> ":" <> headers.requestid, email: "tenant@test.com" }
182+ _ -> reject @"unauthorized" { error: "Invalid token" }
183+
150-- Test suite
184-- Test suite
151
185
152spec :: Spec Unit
186spec :: Spec Unit
153spec = around withServer $ describe "server ↔ client roundtrip" do
187spec = around withServer $ describe "server ↔ client roundtrip" do
154
188
189+
155 describe "GET with path params" do
190 describe "GET with path params" do
156 it "returns user for valid id" \_ ->
191 it "returns user for valid id" \_ ->
157 runOm {} { exception: \_ -> pure unit } do
192 runOm {} { exception: \_ -> pure unit } do
@@ -168,12 +203,12 @@ spec = around withServer $ describe "server ↔ client roundtrip" do
168 describe "GET with query params" do
203 describe "GET with query params" do
169 it "passes query params correctly" \_ ->
204 it "passes query params correctly" \_ ->
170 runOm {} { exception: \_ -> pure unit } do
205 runOm {} { exception: \_ -> pure unit } do
171− users <- api.listUsers { limit: 1 }
206+ users <- api.listUsers (justifill { limit: 1 })
172 liftAff $ (map _.name users) `shouldEqual` ["Alice"]
207 liftAff $ (map _.name users) `shouldEqual` ["Alice"]
173
208
174 it "handles no query params" \_ ->
209 it "handles no query params" \_ ->
175 runOm {} { exception: \_ -> pure unit } do
210 runOm {} { exception: \_ -> pure unit } do
176− users <- api.listUsers {}
211+ users <- api.listUsers (justifill {})
177 liftAff $ (map _.name users) `shouldEqual` ["Alice", "Bob", "Charlie"]
212 liftAff $ (map _.name users) `shouldEqual` ["Alice", "Bob", "Charlie"]
178
213
179 describe "POST with JSON body" do
214 describe "POST with JSON body" do
@@ -195,12 +230,23 @@ spec = around withServer $ describe "server ↔ client roundtrip" do
195 describe "PUT with path params and JSON body" do
230 describe "PUT with path params and JSON body" do
196 it "sends path params and body together" \_ ->
231 it "sends path params and body together" \_ ->
197 runOm {} { exception: \_ -> pure unit } do
232 runOm {} { exception: \_ -> pure unit } do
233+ user <- api.updateUser
234+ { id: 42 }
198− user <- api.updateUser { id: 42 } { name: "Updated", email: "u@t.com" }
235+ { name: "Updated", email: "u@t.com" }
199 # handleErrors { notFound: \e -> pure { id: 0, name: e.error, email: "" } }
236 # handleErrors { notFound: \e -> pure { id: 0, name: e.error, email: "" } }
200 liftAff do
237 liftAff do
201 user.id `shouldEqual` 42
238 user.id `shouldEqual` 42
202 user.name `shouldEqual` "Updated"
239 user.name `shouldEqual` "Updated"
203
240
241+ it "supports partial application before the body argument" \_ ->
242+ runOm {} { exception: \_ -> pure unit } do
243+ let update42 = api.updateUser { id: 42 }
244+ user <- update42 { name: "Curried", email: "curried@t.com" }
245+ # handleErrors { notFound: \e -> pure { id: 0, name: e.error, email: "" } }
246+ liftAff do
247+ user.id `shouldEqual` 42
248+ user.name `shouldEqual` "Curried"
249+
204 describe "DELETE with path params" do
250 describe "DELETE with path params" do
205 it "deletes successfully" \_ ->
251 it "deletes successfully" \_ ->
206 runOm {} { exception: \_ -> pure unit } do
252 runOm {} { exception: \_ -> pure unit } do
@@ -230,41 +276,61 @@ spec = around withServer $ describe "server ↔ client roundtrip" do
230 describe "GET with Boolean and Number query params" do
276 describe "GET with Boolean and Number query params" do
231 it "passes Boolean and Number query params" \_ ->
277 it "passes Boolean and Number query params" \_ ->
232 runOm {} { exception: \_ -> pure unit } do
278 runOm {} { exception: \_ -> pure unit } do
233− result <- api.search { active: true, minScore: 3.14 }
279+ result <- api.search (justifill { active: true, minScore: 3.14 })
234 liftAff do
280 liftAff do
235 result.active `shouldEqual` true
281 result.active `shouldEqual` true
236 result.minScore `shouldEqual` 3.14
282 result.minScore `shouldEqual` 3.14
237
283
238 it "handles missing optional query params" \_ ->
284 it "handles missing optional query params" \_ ->
239 runOm {} { exception: \_ -> pure unit } do
285 runOm {} { exception: \_ -> pure unit } do
240− result <- api.search {}
286+ result <- api.search (justifill {})
241 liftAff do
287 liftAff do
242 result.active `shouldEqual` false
288 result.active `shouldEqual` false
243 result.minScore `shouldEqual` 0.0
289 result.minScore `shouldEqual` 0.0
244
290
245− describe "partial query params — no wrapper needed" do
291+ describe "optional query params" do
246− it "passes subset of query params directly" \_ ->
292+ it "accepts a selected optional query parameter" \_ ->
247 runOm {} { exception: \_ -> pure unit } do
293 runOm {} { exception: \_ -> pure unit } do
248− users <- api.listUsers { limit: 1 }
294+ users <- api.listUsers (justifill { limit: 1 })
249 liftAff $ (map _.name users) `shouldEqual` ["Alice"]
295 liftAff $ (map _.name users) `shouldEqual` ["Alice"]
250
296
251− it "passes all query params directly" \_ ->
297+ it "accepts all optional query parameters" \_ ->
252 runOm {} { exception: \_ -> pure unit } do
298 runOm {} { exception: \_ -> pure unit } do
253− users <- api.listUsers { limit: 2, offset: 0 }
299+ users <- api.listUsers (justifill { limit: 2, offset: 0 })
254 liftAff $ (map _.name users) `shouldEqual` ["Alice", "Bob"]
300 liftAff $ (map _.name users) `shouldEqual` ["Alice", "Bob"]
255
301
256− it "passes empty record for all defaults" \_ ->
302+ it "fills omitted optional query parameters with Nothing" \_ ->
257 runOm {} { exception: \_ -> pure unit } do
303 runOm {} { exception: \_ -> pure unit } do
258− users <- api.listUsers {}
304+ users <- api.listUsers (justifill {})
259 liftAff $ (map _.name users) `shouldEqual` ["Alice", "Bob", "Charlie"]
305 liftAff $ (map _.name users) `shouldEqual` ["Alice", "Bob", "Charlie"]
260
306
261− it "passes partial Boolean query param" \_ ->
307+ it "passes selected Boolean query param" \_ ->
262 runOm {} { exception: \_ -> pure unit } do
308 runOm {} { exception: \_ -> pure unit } do
263− result <- api.search { active: true }
309+ result <- api.search (justifill { active: true })
264 liftAff do
310 liftAff do
265 result.active `shouldEqual` true
311 result.active `shouldEqual` true
266 result.minScore `shouldEqual` 0.0
312 result.minScore `shouldEqual` 0.0
267
313
314+ describe "clientWith injected inputs" do
315+ it "reads provided headers and query params from Om context for each call" \_ -> do
316+ tokenRef <- liftEffect $ Ref.new "secret"
317+ let injected = injectedApi tokenRef
318+ user <- runOm { tenant: "acme", requestid: "req-1" }
319+ { exception: \e -> pure { id: -1, name: Exn.message e, email: "" }
320+ , unauthorized: \e -> pure { id: -2, name: e.error, email: "" }
321+ }
322+ (injected.getTenantUser { id: 7 })
323+ liftAff do
324+ user.id `shouldEqual` 7
325+ user.name `shouldEqual` "acme:req-1"
326+ liftEffect $ Ref.write "wrong" tokenRef
327+ rejected <- runOm { tenant: "acme", requestid: "req-2" }
328+ { exception: \e -> pure { id: -1, name: Exn.message e, email: "" }
329+ , unauthorized: \e -> pure { id: -2, name: e.error, email: "" }
330+ }
331+ (injected.getTenantUser { id: 8 })
332+ liftAff $ rejected.name `shouldEqual` "Invalid token"
333+
268-- Server setup
334-- Server setup
269
335
270withServer :: forall a. (Unit -> _ a) -> _ a
336withServer :: forall a. (Unit -> _ a) -> _ a
@@ -281,8 +347,8 @@ withServer test = bracket acquire release (\_ -> test unit)
281 , deleteUser: deleteUserHandler
347 , deleteUser: deleteUserHandler
282 , me: meHandler
348 , me: meHandler
283 , search: searchHandler
349 , search: searchHandler
284− }
350+ } f
285− f
351+ registerAPI @InjectedAPI { getTenantUser: tenantUserHandler } f
286 pure f
352 pure f
287 void $ F.listen { port: Port 44932, host: Host "0.0.0.0" } fastify
353 void $ F.listen { port: Port 44932, host: Host "0.0.0.0" } fastify
288 pure fastify
354 pure fastify