git.iko.soy/rowtype-yoga/purescript-yoga-fetch-om
on main help sign in
Replace client argument adapter with direct calls
commit1cd6e7dc204d5607c250fe3c223469a82ddcc3db authorMark <mark.eibes@gmail.com> dateMon 17 Aug 2026, 21:37 (1 month ago) parent44ba2b3
37 files changed+867−293
.github/workflows/ci.yml+1 README.md+24−6 bun.lock+108−15 compile-fail-tests/ClientBadBodyType.purs+12 compile-fail-tests/ClientBadCurriedBodyType.purs+12 compile-fail-tests/ClientBadHeaderType.purs+13 compile-fail-tests/ClientBadOptionalQueryType.purs+13 compile-fail-tests/ClientBadPathType.purs+12 compile-fail-tests/ClientExtraBodyField.purs+12 compile-fail-tests/ClientExtraHeaderField.purs+13 compile-fail-tests/ClientExtraOptionalQueryField.purs+13 compile-fail-tests/ClientExtraPathField.purs+12 compile-fail-tests/ClientMissingBodyArgument.purs+12 compile-fail-tests/ClientMissingBodyField.purs+12 compile-fail-tests/ClientMissingHeaderArgument.purs+13 compile-fail-tests/ClientMissingHeaderField.purs+13 compile-fail-tests/ClientMissingPathField.purs+12 compile-fail-tests/ClientMissingRequiredQuery.purs+13 compile-fail-tests/ClientMissingThirdArgument.purs+13 compile-fail-tests/run.js+101 package.json+10 spago.lock+32−19 spago.yaml+4−10 src/Yoga/Fetch/Om.js+1 src/Yoga/Fetch/Om.purs+139−22 src/Yoga/Fetch/Om/BuildUrl.purs+30−1 src/Yoga/Fetch/Om/ClientFunction.purs+16−16 src/Yoga/Fetch/Om/ParseResponse.purs+16−1 src/Yoga/Fetch/Om/QueryDefaults.purs−135 test/BodylessResponse.Example.purs+1−1 test/BodylessResponse.Spec.purs+31−4 test/BuildUrl.Spec.purs+21 test/Complete.Example.purs+14−19 test/Example.purs+13−8 test/PlainTextResponse.Spec.purs+1−1 test/Polymorphic.Spec.purs+20−17 test/Roundtrip.Spec.purs+84−18
.github/workflows/ci.yml+1
@@ -14,3 +14,4 @@ jobs:
1414 - uses: rowtype-yoga/purescript-yoga-actions/.github/actions/setup@main
1515 - run: spago build
1616 - 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
88spago install yoga-fetch-om
99```
1010
11+Optional query examples below use [`justifill`](https://pursuit.purescript.org/packages/purescript-justifill):
12+
13+```bash
14+spago install justifill
15+```
16+
1117## Quick Start
1218
1319### Example 1: Simple GET and POST Requests
@@ -30,8 +36,13 @@ main = do
3036### Example 2: Type-Safe API Client with Full CRUD
3137
3238```purescript
39+import Justifill (justifill)
40+
3341type UserAPI =
34− { getUser ::
42+ { health ::
43+ Route GET "health" {}
44+ ( ok :: { body :: { status :: String } } )
45+ , getUser ::
3546 Route GET ("users" / "id" : Int) {}
3647 ( ok :: { body :: User }
3748 , notFound :: { body :: ErrorMessage }
@@ -59,7 +70,8 @@ type UserAPI =
5970
6071api = client @UserAPI "https://api.example.com"
6172
62−-- Create a user
73+-- Generated methods use ordinary function application.
74+health <- api.health
6375user <- api.createUser { name: "Alice", email: "alice@example.com" }
6476 # handleErrors
6577 { badRequest: \err -> do
@@ -67,15 +79,21 @@ user <- api.createUser { name: "Alice", email: "alice@example.com" }
6779 throw err
6880 }
6981
70−-- Update with path param + body
71−updated <- api.updateUser { id: user.id } { name: "Alice Updated", email: user.email }
82+-- Path parameters and request bodies remain separate arguments.
83+updated <- api.updateUser
84+ { id: user.id }
85+ { name: "Alice Updated", email: user.email }
7286 # handleErrors
7387 { notFound: \_ -> throw userNotFound
7488 , badRequest: \err -> throw validationError
7589 }
7690
77−-- Query with pagination
78−users <- api.listUsers { limit: 10, offset: 0 }
91+-- Optional query parameters are explicit `Maybe` fields. Justifill can wrap
92+-- supplied values in `Just` and fill omitted fields with `Nothing`.
93+users <- api.listUsers (justifill { limit: 10 })
94+
95+-- The equivalent call without Justifill:
96+allUsers <- api.listUsers { limit: Nothing, offset: Nothing }
7997```
8098
8199## More Examples
bun.lock+108−15
@@ -4,7 +4,16 @@
44 "": {
55 "name": "purescript-yoga-fetch-om",
66 "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",
715 "fastify": "^5.7.4",
16+ "jose": "^6.2.8",
817 "jsdom": "^28.0.0",
918 "purescript": "^0.15.15",
1019 "spago": "0.93.42",
@@ -33,6 +42,8 @@
3342
3443 "@csstools/css-tokenizer": ["@csstools/css-tokenizer@4.0.0", "", {}, "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA=="],
3544
45+ "@epic-web/invariant": ["@epic-web/invariant@1.0.0", "", {}, "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA=="],
46+
3647 "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.21.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ=="],
3748
3849 "@esbuild/android-arm": ["@esbuild/android-arm@0.21.5", "", { "os": "android", "cpu": "arm" }, "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg=="],
@@ -83,16 +94,34 @@
8394
8495 "@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=="],
8596
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+
86105 "@fastify/error": ["@fastify/error@4.2.0", "", {}, "sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ=="],
87106
88107 "@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=="],
89108
109+ "@fastify/formbody": ["@fastify/formbody@9.0.0", "", { "dependencies": { "fast-querystring": "^1.1.2", "fastify-plugin": "^6.0.0" } }, "sha512-T/af26CSrUARBCvsEmv+DJLPfZlrRKESzqironxP1j7qzuLyKcoZtj6MuTGShuKx1THXugoie2oFbUJxXfGFzA=="],
110+
90111 "@fastify/forwarded": ["@fastify/forwarded@3.0.1", "", {}, "sha512-JqDochHFqXs3C3Ml3gOY58zM7OqO9ENqPo0UqAjAjH8L01fRZqwX9iLeX34//kiJubF7r2ZQHtBRU36vONbLlw=="],
91112
113+ "@fastify/helmet": ["@fastify/helmet@13.1.0", "", { "dependencies": { "fastify-plugin": "^6.0.0", "helmet": "^8.0.0" } }, "sha512-SvVOU0IrzYJW1BvSkfq9G1WUdW3dnaRUvg6m0BtgGMBmML62No0VmSu087jecH58SFbicbREgZTPJ89mAguupA=="],
114+
92115 "@fastify/merge-json-schemas": ["@fastify/merge-json-schemas@0.2.1", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A=="],
93116
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+
94119 "@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=="],
95120
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+
96125 "@gar/promisify": ["@gar/promisify@1.1.3", "", {}, "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw=="],
97126
98127 "@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 @@
101130
102131 "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
103132
133+ "@lukeed/ms": ["@lukeed/ms@2.0.2", "", {}, "sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA=="],
134+
104135 "@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=="],
105136
106137 "@nodelib/fs.stat": ["@nodelib/fs.stat@3.0.0", "", {}, "sha512-2tQOI38s19P9i7X/Drt0v8iMA+KMsgdhB/dyPER+e+2Y8L1Z7QvnuRdW/uLuf5YRFUYmnj4bMA6qCuZHFI1GDQ=="],
@@ -111,6 +142,8 @@
111142
112143 "@npmcli/move-file": ["@npmcli/move-file@2.0.1", "", { "dependencies": { "mkdirp": "^1.0.4", "rimraf": "^3.0.2" } }, "sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ=="],
113144
145+ "@phc/format": ["@phc/format@1.0.0", "", {}, "sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ=="],
146+
114147 "@pinojs/redact": ["@pinojs/redact@0.4.0", "", {}, "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg=="],
115148
116149 "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="],
@@ -203,6 +236,8 @@
203236
204237 "aproba": ["aproba@1.2.0", "", {}, "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw=="],
205238
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+
206241 "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
207242
208243 "asn1": ["asn1@0.2.6", "", { "dependencies": { "safer-buffer": "~2.1.0" } }, "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ=="],
@@ -269,7 +304,7 @@
269304
270305 "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=="],
271306
272− "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
307+ "cookie": ["cookie@2.0.1", "", {}, "sha512-yuToqVvRrj6pfDXREyQAAv8SkAEk/8GS3jQRTiUMm66TVtBYmqQeoEjL2Lmq8Rpo6271vH76InTChTitEAm65w=="],
273308
274309 "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=="],
275310
@@ -277,6 +312,8 @@
277312
278313 "cpu-features": ["cpu-features@0.0.10", "", { "dependencies": { "buildcheck": "~0.0.6", "nan": "^2.19.0" } }, "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA=="],
279314
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+
280317 "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
281318
282319 "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 @@
307344
308345 "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
309346
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=="],
311348
312349 "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="],
313350
@@ -349,6 +386,8 @@
349386
350387 "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=="],
351388
389+ "fastify-plugin": ["fastify-plugin@6.0.0", "", {}, "sha512-fZOty7z3O7vOliF6d8bHE3wiEh1KcNnKEQensSgTk9C1DvN6nRLS++XVd86v33Hw/8u9Un8A1zDrQ8ujcQDHEg=="],
390+
352391 "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="],
353392
354393 "figgy-pudding": ["figgy-pudding@3.5.2", "", {}, "sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw=="],
@@ -391,6 +430,8 @@
391430
392431 "has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="],
393432
433+ "helmet": ["helmet@8.3.0", "", {}, "sha512-Qgpiaws3Sm30Av8Eah6sjMCZZwjlBu+E68rhpCWBshY1lb09HtLwj5GviX0OyQIn+ulUS0iX0AxN5n3tLZzz1w=="],
434+
394435 "html-encoding-sniffer": ["html-encoding-sniffer@6.0.0", "", { "dependencies": { "@exodus/bytes": "^1.6.0" } }, "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg=="],
395436
396437 "http-cache-semantics": ["http-cache-semantics@4.2.0", "", {}, "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ=="],
@@ -419,7 +460,7 @@
419460
420461 "ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="],
421462
422− "ip-address": ["ip-address@10.1.0", "", {}, "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q=="],
463+ "ip-address": ["ip-address@10.5.0", "", {}, "sha512-R5SnVLJmgYYvf2F2ZgwSBnelz5G4q5AxIC277GDfUaNbrZKNANcBC7RHqYYePlszf4kBolVkJauG0ZjHHFh55g=="],
423464
424465 "ipaddr.js": ["ipaddr.js@2.3.0", "", {}, "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg=="],
425466
@@ -447,6 +488,8 @@
447488
448489 "jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
449490
491+ "jose": ["jose@6.2.8", "", {}, "sha512-Bsdjwm3Qsd/P0jR+BHDe3LytDfY7WBq2HmCCLIwuVRHMuEC9ae7/R474GIUdF1NgCyZjzVo/A9DOiOBtXq8ZoQ=="],
492+
450493 "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=="],
451494
452495 "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 @@
523566
524567 "node-abi": ["node-abi@3.87.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ=="],
525568
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+
526573 "npm-run-path": ["npm-run-path@3.1.0", "", { "dependencies": { "path-key": "^3.0.0" } }, "sha512-Dbl4A/VfiVGLgQv29URL9xshU8XDY1GeLy+fsaZ1AA8JDSfjvr5P5+pzRbWqRSBxk6/DW7MIh8lTM/PaGnP2kg=="],
527574
528575 "on-exit-leak-free": ["on-exit-leak-free@2.1.2", "", {}, "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA=="],
@@ -593,7 +640,7 @@
593640
594641 "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=="],
595642
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=="],
597644
598645 "readline-sync": ["readline-sync@1.4.10", "", {}, "sha512-gNva8/6UAe8QYepIQH/jQ2qn91Qj0B9sYjMBBs3QOB8F2CXcKgLxQaJRP76sWVRQt+QU+8fAkCbCvjjMFu7Ycw=="],
599646
@@ -687,7 +734,7 @@
687734
688735 "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=="],
689736
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=="],
691738
692739 "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
693740
@@ -779,6 +826,8 @@
779826
780827 "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
781828
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+
782831 "wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="],
783832
784833 "xhr2": ["xhr2@0.2.1", "", {}, "sha512-sID0rrVCqkVNUn8t6xuv9+6FViXjUVXq8H5rWOH2rz9fDNQEd4g0EA2XlcEdJXRz5BMEn4O1pJFdT+z4YHhoWw=="],
@@ -807,8 +856,6 @@
807856
808857 "@npmcli/move-file/rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="],
809858
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−
812859 "cacache/chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="],
813860
814861 "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 @@
817864
818865 "chalk/supports-color": ["supports-color@5.5.0", "", { "dependencies": { "has-flag": "^3.0.0" } }, "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow=="],
819866
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+
820869 "cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
821870
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+
822873 "foreground-child/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
823874
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+
824877 "fs-minipass/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
825878
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+
826883 "light-my-request/process-warning": ["process-warning@4.0.1", "", {}, "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q=="],
827884
828885 "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 @@
853910
854911 "minipass-sized/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
855912
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+
856917 "path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
857918
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+
858921 "pumpify/pump": ["pump@2.0.1", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA=="],
859922
860923 "purescript-installer/env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="],
861924
862925 "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=="],
863926
864− "readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
865−
866927 "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=="],
867928
868929 "slice-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
869930
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=="],
871932
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=="],
873934
874935 "tar-fs/chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="],
875936
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=="],
877938
878939 "wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
879940
@@ -887,14 +948,28 @@
887948
888949 "@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=="],
889950
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−
892951 "cacache/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
893952
894953 "cacache/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
895954
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+
896967 "fs-minipass/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
897968
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+
898973 "make-fetch-happen/cacache/chownr": ["chownr@2.0.0", "", {}, "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ=="],
899974
900975 "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 @@
9251000
9261001 "minipass-sized/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
9271002
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+
9281011 "purescript-installer/tar/chownr": ["chownr@2.0.0", "", {}, "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ=="],
9291012
9301013 "purescript-installer/tar/minipass": ["minipass@5.0.0", "", {}, "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ=="],
@@ -939,7 +1022,9 @@
9391022
9401023 "slice-ansi/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
9411024
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=="],
1025+ "through2/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
1026+
1027+ "through2/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
9431028
9441029 "wrap-ansi-cjs/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
9451030
@@ -961,6 +1046,14 @@
9611046
9621047 "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=="],
9631048
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+
9641057 "purescript-installer/tar/minizlib/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
9651058
9661059 "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 @@
66 "scripts": {
77 "build": "spago build",
88 "test": "spago test",
9+ "test:compile-fail": "bun compile-fail-tests/run.js",
910 "clean": "rm -rf output .spago",
1011 "watch": "spago build --watch",
1112 "repl": "spago repl",
@@ -24,7 +25,16 @@
2425 "author": "",
2526 "license": "MIT",
2627 "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",
2736 "fastify": "^5.7.4",
37+ "jose": "^6.2.8",
2838 "jsdom": "^28.0.0",
2939 "purescript": "^0.15.15",
3040 "spago": "0.93.42",
spago.lock+32−19
@@ -60,7 +60,7 @@
6060 "variant": ">=8.0.0 <9.0.0"
6161 },
6262 {
63− "yoga-http-api": ">=0.3.2 <0.4.0"
63+ "yoga-http-api": ">=0.3.4 <0.4.0"
6464 },
6565 {
6666 "yoga-json": ">=5.2.1 <6.0.0"
@@ -163,6 +163,9 @@
163163 {
164164 "console": ">=6.0.0 <7.0.0"
165165 },
166+ {
167+ "justifill": ">=0.5.0 <0.6.0"
168+ },
166169 {
167170 "refs": ">=6.0.0 <7.0.0"
168171 },
@@ -173,7 +176,7 @@
173176 "spec-discovery": ">=8.0.0 <9.0.0"
174177 },
175178 {
176− "yoga-fastify-om": ">=0.4.5 <0.5.0"
179+ "yoga-fastify-om": ">=0.4.7 <0.5.0"
177180 }
178181 ],
179182 "build_plan": [
@@ -216,6 +219,7 @@
216219 "js-promise",
217220 "js-promise-aff",
218221 "js-uri",
222+ "justifill",
219223 "lazy",
220224 "lists",
221225 "literals",
@@ -277,17 +281,7 @@
277281 }
278282 }
279283 },
280− "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− }
284+ "extra_packages": {}
291285 },
292286 "packages": {
293287 "aff": {
@@ -864,6 +858,17 @@
864858 "prelude"
865859 ]
866860 },
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+ },
867872 "lazy": {
868873 "type": "registry",
869874 "version": "6.0.0",
@@ -1640,8 +1645,8 @@
16401645 },
16411646 "yoga-fastify": {
16421647 "type": "registry",
1643− "version": "0.5.3",
1644− "integrity": "sha256-KeAoEYXXNH2gHEl5Y7j9uMo5ipByLZp/p7u6pgQn6KU=",
1648+ "version": "0.5.4",
1649+ "integrity": "sha256-YenjePRdiApMPpcvkXlzre7c4fIuvk5T19Z2uEAWhR4=",
16451650 "dependencies": [
16461651 "aff",
16471652 "arrays",
@@ -1667,8 +1672,8 @@
16671672 },
16681673 "yoga-fastify-om": {
16691674 "type": "registry",
1670− "version": "0.4.5",
1671− "integrity": "sha256-fPDRni+HjddpI7GPad5U2kVHxMPOZIsvTPFZtOPpiWI=",
1675+ "version": "0.4.7",
1676+ "integrity": "sha256-KGQ7lHI7pcSiEQoEq6j6K+3XilcPy8wQ/M9C1y+uaEk=",
16721677 "dependencies": [
16731678 "aff",
16741679 "console",
@@ -1680,10 +1685,12 @@
16801685 "integers",
16811686 "maybe",
16821687 "node-buffer",
1688+ "numbers",
16831689 "prelude",
16841690 "record",
16851691 "refs",
16861692 "routing-duplex",
1693+ "strings",
16871694 "transformers",
16881695 "typelevel-prelude",
16891696 "unsafe-coerce",
@@ -1698,19 +1705,25 @@
16981705 },
16991706 "yoga-http-api": {
17001707 "type": "registry",
1701− "version": "0.3.2",
1702− "integrity": "sha256-SiL07TYVjHPiQjMhnkYdoI3KrL6gNn1W1LigCN+80jA=",
1708+ "version": "0.3.4",
1709+ "integrity": "sha256-DSIJ8YWr5vB5IMpyaBALM+fx57lz3AAAR0MGtfHeWxo=",
17031710 "dependencies": [
17041711 "aff",
1712+ "arraybuffer-types",
17051713 "arrays",
1714+ "effect",
17061715 "either",
17071716 "foldable-traversable",
17081717 "foreign",
17091718 "foreign-object",
17101719 "identity",
17111720 "integers",
1721+ "js-promise",
1722+ "js-promise-aff",
17121723 "maybe",
17131724 "newtype",
1725+ "nullable",
1726+ "numbers",
17141727 "prelude",
17151728 "record",
17161729 "strings",
spago.yaml+4−10
@@ -1,11 +1,4 @@
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
1+workspace: {}
92package:
103 name: yoga-fetch-om
114 publish:
@@ -39,7 +32,7 @@ package:
3932 - typelevel-prelude: ">=7.0.0 <8.0.0"
4033 - unsafe-coerce: ">=6.0.0 <7.0.0"
4134 - 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"
4336 - yoga-json: ">=5.2.1 <6.0.0"
4437 - yoga-om: ">=1.5.2 <2.0.0"
4538 - yoga-om-strom: ">=0.4.3 <0.5.0"
@@ -48,6 +41,7 @@ package:
4841 dependencies:
4942 - spec: ">=8.0.0 <9.0.0"
5043 - spec-discovery: ">=8.0.0 <9.0.0"
44+ - justifill: ">=0.5.0 <0.6.0"
5145 - 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"
5347 - 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 @@
11module Yoga.Fetch.Om
22 ( client
3+ , clientWith
4+ , ClientInputs
5+ , ClientWithConfig
36 , deriveClient
47 , class DeriveClient
58 , deriveClientImpl
@@ -7,13 +10,18 @@ module Yoga.Fetch.Om
710 , deriveClientRL
811 , class DeriveClientFn
912 , deriveClientFn
13+ , class DeriveClientFnWith
14+ , deriveClientFnWith
15+ , class DeriveClientWith
16+ , deriveClientWithImpl
17+ , class DeriveClientWithRL
18+ , deriveClientWithRL
1019 , class RecordRow
1120 , class ToHeaders
1221 , toHeaders
1322 , module Yoga.HTTP.API.Route
1423 , module Yoga.HTTP.API.Path
1524 , plainText
16− , class HasAllFields
1725 , module Yoga.Fetch.Om.StreamDecode
1826 , module Yoga.Fetch.Om.Simple
1927 ) where
@@ -35,7 +43,6 @@ import Yoga.Fetch.Om.BuildUrl (class BuildUrl, buildUrl)
3543import Yoga.Fetch.Om.Simple (class DecodeResponse, decodeResponse, FetchError, FetchResponse, get, getWithHeaders, delete, deleteWithHeaders, delete_, post, postWithHeaders, post_, put, putWithHeaders, put_, patch, patchWithHeaders, patch_)
3644import Yoga.Fetch.Om.StreamDecode (class StreamDecode, decodeStream)
3745import Yoga.Fetch.Om.ClientFunction (class BuildClientFn, class CheckBodyIsUnit, buildClientFn)
38−import Yoga.Fetch.Om.ReadOptional (class ReadPartialQuery, readPartialQuery)
3946import Yoga.Fetch.Om.ExtractParams (class ExtractRequestBody, class ExtractBodyEncoding, class ExtractRequestHeaders)
4047import Yoga.Fetch.Om.MakeRequest (class MakeRequest, class BodyEncoding, makeRequest, encodingContentType, encodeBody)
4148import Yoga.Fetch.Om.ParseResponse (class ParseResponse, parseResponse)
@@ -44,7 +51,20 @@ import Yoga.Fetch.Om.Variant (class VariantOrValue, variantOrValue)
4451import Yoga.HTTP.API.Path (Path, Root, Lit, Capture, PathCons, Param, QueryParams, Required, type (/), type (:), type (:?), class PathPattern)
4552import Yoga.HTTP.API.Route (Route(..), GET, POST, PUT, DELETE, PATCH, Response(..), JSON, FormData, PlainText, Streaming, NoBody, class HeaderValue, printHeader)
4653import 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
4868
4969-- | Extract row type from Record type
5070class RecordRow :: Type -> Row Type -> Constraint
@@ -74,11 +94,11 @@ else instance
7494 rest = toHeaders (Proxy :: Proxy tail) (unsafeCoerce headers :: Record tailRow)
7595
7696class 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
7898class
79− DeriveClientFn method segments request response ctx extraErr given routeErrors result fn
99+ DeriveClientFn method segments request response ctx extraErr routeErrors result fn
80100 | 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
82102 deriveClientFn :: String -> Proxy (Route method segments request response) -> fn
83103
84104instance
@@ -101,17 +121,13 @@ instance
101121 , RowToList headers headersRL
102122 , ToHeaders headersRL headers
103123 , CheckBodyIsUnit body bodyFlag
104− , RowToList pathParams pathParamsRL
105− , HasAllFields pathParamsRL given
106124 , 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
108126 ) =>
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
110128 deriveClientFn baseUrl _ =
111− buildClientFn (Proxy :: _ pathQueryRL) (Proxy :: _ headersRL) (Proxy :: _ bodyFlag) convert impl
129+ buildClientFn (Proxy :: _ pathQueryRL) (Proxy :: _ headersRL) (Proxy :: _ bodyFlag) identity impl
112130 where
113− convert :: Record given -> Record pathQuery
114− convert = unsafeCoerce
115131 impl :: Record pathQuery -> Record headers -> body -> Om { | ctx } errRow result
116132 impl pathQueryRec headersRec bodyVal = widenOm do
117133 let url = buildUrl baseUrl (Proxy :: _ segments) pathParamsRec queryParamsRec
@@ -126,15 +142,83 @@ instance
126142 widenOm :: Om (Record ()) routeErrors ~> Om { | ctx } errRow
127143 widenOm = unsafeCoerce
128144
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+
129208class DeriveClient :: Row Type -> Row Type -> Row Type -> Row Type -> Constraint
130209class DeriveClient ctx extraErr routesRow clientsRow | routesRow ctx extraErr -> clientsRow where
131210 deriveClientImpl :: String -> Proxy (Record routesRow) -> Record clientsRow
132211
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.
134215-- |
135216-- | ```purescript
136217-- | type UserAPI = { getUser :: Route ... }
137218-- | 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
138222-- | ```
139223client :: forall @routes routesRow ctx extraErr clientsRow. RecordRow routes routesRow => DeriveClient ctx extraErr routesRow clientsRow => String -> Record clientsRow
140224client baseUrl = deriveClientImpl @ctx @extraErr baseUrl (Proxy :: _ { | routesRow })
@@ -147,17 +231,18 @@ client baseUrl = deriveClientImpl @ctx @extraErr baseUrl (Proxy :: _ { | routesR
147231deriveClient :: forall @routesRow ctx extraErr clientsRow. DeriveClient ctx extraErr routesRow clientsRow => String -> Record clientsRow
148232deriveClient baseUrl = deriveClientImpl @ctx @extraErr baseUrl (Proxy :: _ { | routesRow })
149233
150−-- | Checks that all fields in the RowList exist in the given row.
151−-- | Used to enforce required path params are present in the client argument.
152−class HasAllFields (rl :: RL.RowList Type) (given :: Row Type)
234+clientWith
235+ :: forall @routes @providedHeaders @providedQuery routesRow ctx extraErr clientsRow
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 })
153241
154−instance HasAllFields RL.Nil given
155−instance (Row.Cons name ty rest given, HasAllFields tail given) => HasAllFields (RL.Cons name ty tail) given
156242
157243plainText :: PlainText -> String
158244plainText = unsafeCoerce
159245
160−
161246instance
162247 ( RowToList routesRow rl
163248 , DeriveClientRL ctx extraErr rl () clientsRow
@@ -165,6 +250,38 @@ instance
165250 DeriveClient ctx extraErr routesRow clientsRow where
166251 deriveClientImpl baseUrl _ = deriveClientRL @ctx @extraErr baseUrl (Proxy :: _ rl) {}
167252
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+
168285class DeriveClientRL :: Row Type -> Row Type -> RowList Type -> Row Type -> Row Type -> Constraint
169286class DeriveClientRL ctx extraErr rl acc out | ctx extraErr rl acc -> out where
170287 deriveClientRL :: String -> Proxy rl -> Record acc -> Record out
@@ -174,7 +291,7 @@ instance DeriveClientRL ctx extraErr RL.Nil acc acc where
174291
175292instance
176293 ( 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
178295 , DeriveClientRL ctx extraErr tail acc1 acc2
179296 , Row.Cons label fn acc2 out
180297 , Row.Lacks label acc2
@@ -183,5 +300,5 @@ instance
183300 DeriveClientRL ctx extraErr (RL.Cons label (Route method segments request response) tail) acc1 out where
184301 deriveClientRL baseUrl _ acc = Record.insert (Proxy :: _ label) clientFn rest
185302 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))
187304 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
114114instance AppendQueryParamsRL RL.Nil r where
115115 appendQueryParamsRL _ _ acc = acc
116116
117−-- Maybe ty: read with undefined-awareness — missing field → skip
117+-- Maybe (Array ty): optional repeated query param — missing/empty field → no pairs
118118instance
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
119133 ( IsSymbol name
120134 , SerializeParam ty
121135 , AppendQueryParamsRL tail r
@@ -127,6 +141,21 @@ instance
127141 Nothing -> acc
128142 Just value -> Array.snoc acc (encodeURIComponent_ (reflectSymbol (Proxy :: _ name)) <> "=" <> encodeURIComponent_ (serializeParam value))
129143
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+
130159-- bare ty (Required query param): always present
131160else instance
132161 ( IsSymbol name
src/Yoga/Fetch/Om/ClientFunction.purs+16−16
@@ -22,17 +22,17 @@ instance CheckBodyIsUnit Unit IsUnit
2222else instance CheckBodyIsUnit body IsNotUnit
2323
2424class 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
2626class
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
2828 | pathQueryRL -> pathQuery
2929 , headersRL -> headers
30− , pathQueryRL headersRL bodyFlag body given ctx errorRow result -> fn where
30+ , pathQueryRL headersRL bodyFlag body arg ctx errorRow result -> fn where
3131 buildClientFn
3232 :: Proxy pathQueryRL
3333 -> Proxy headersRL
3434 -> Proxy bodyFlag
35− -> (Record given -> Record pathQuery)
35+ -> (arg -> Record pathQuery)
3636 -> (Record pathQuery -> Record headers -> body -> Om ctx errorRow result)
3737 -> fn
3838
@@ -44,7 +44,7 @@ instance
4444 Unit
4545 ()
4646 ()
47− given
47+ arg
4848 ctx
4949 errorRow
5050 result
@@ -59,7 +59,7 @@ instance
5959 body
6060 ()
6161 ()
62− given
62+ arg
6363 ctx
6464 errorRow
6565 result
@@ -74,7 +74,7 @@ instance
7474 Unit
7575 ()
7676 h
77− given
77+ arg
7878 ctx
7979 errorRow
8080 result
@@ -89,7 +89,7 @@ instance
8989 body
9090 ()
9191 h
92− given
92+ arg
9393 ctx
9494 errorRow
9595 result
@@ -104,11 +104,11 @@ instance
104104 Unit
105105 pq
106106 ()
107− given
107+ arg
108108 ctx
109109 errorRow
110110 result
111− (Record given -> Om ctx errorRow result) where
111+ (arg -> Om ctx errorRow result) where
112112 buildClientFn _ _ _ convert f pqr = f (convert pqr) {} unit
113113
114114-- With path/query, no headers, with body
@@ -119,11 +119,11 @@ instance
119119 body
120120 pq
121121 ()
122− given
122+ arg
123123 ctx
124124 errorRow
125125 result
126− (Record given -> body -> Om ctx errorRow result) where
126+ (arg -> body -> Om ctx errorRow result) where
127127 buildClientFn _ _ _ convert f pqr b = f (convert pqr) {} b
128128
129129-- With path/query, with headers, no body
@@ -134,11 +134,11 @@ instance
134134 Unit
135135 pq
136136 h
137− given
137+ arg
138138 ctx
139139 errorRow
140140 result
141− (Record given -> Record h -> Om ctx errorRow result) where
141+ (arg -> Record h -> Om ctx errorRow result) where
142142 buildClientFn _ _ _ convert f pqr hdrs = f (convert pqr) hdrs unit
143143
144144-- With path/query, with headers, with body
@@ -149,9 +149,9 @@ instance
149149 body
150150 pq
151151 h
152− given
152+ arg
153153 ctx
154154 errorRow
155155 result
156− (Record given -> Record h -> body -> Om ctx errorRow result) where
156+ (arg -> Record h -> body -> Om ctx errorRow result) where
157157 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)
3333import Yoga.HTTP.API.Route.StatusCode (class StatusCodeMap, statusCodeFor, StatusCode(..))
3434import Yoga.JSON (class ReadForeign, readJSON)
3535import Yoga.JSON.Error (withStringErrors)
36−import Yoga.Om (class ToOm, Om, toOm)
36+import Yoga.Om (Om, toOm)
3737import Yoga.Om.Error (Exception)
3838import Yoga.Om.Strom (Strom)
3939import Yoga.Om.Strom.WebStream as WebStream
@@ -106,6 +106,21 @@ else instance
106106 else
107107 parseSuccessRL (Proxy :: _ tailRL) actualStatus fetchResp
108108
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
109124else instance
110125 ( IsSymbol label
111126 , 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 =
2323-- This must typecheck: the derived client exposes notModified/unauthorized as Unit errors.
2424api
2525 :: forall ctx err
26− . { getResource :: Om ctx (notModified :: Unit, unauthorized :: Unit | err) Resource }
26+ . { getResource :: Om (Record ctx) (notModified :: Unit, unauthorized :: Unit | err) Resource }
2727api = client @ResourceAPI "https://api.example.com"
test/BodylessResponse.Spec.purs+31−4
@@ -12,9 +12,9 @@ import Test.Spec (Spec, around, describe, it)
1212import Test.Spec.Assertions (shouldEqual)
1313import Yoga.Fastify.Fastify as F
1414import Yoga.Fastify.Fastify (Port(..), Host(..), RouteURL(..), StatusCode(..))
15−import Yoga.Fastify.Om (createOmFastify, getOm)
16−import Yoga.Fetch.Om (GET, Route, type (/), client)
17−import Yoga.Om (Om, runOm)
15+import Yoga.Fastify.Om (createOmFastify, deleteOm, getOm)
16+import Yoga.Fetch.Om (DELETE, GET, Route, type (/), type (:), client)
17+import Yoga.Om (runOm)
1818
1919type Resource = { id :: Int, name :: String }
2020
@@ -27,9 +27,15 @@ type ResourceAPI =
2727 , notModified :: {}
2828 , unauthorized :: {}
2929 )
30+ , deleteUser ::
31+ Route DELETE
32+ ("bodyless-test" / "users" / "id" : Int)
33+ {}
34+ ( noContent :: {}
35+ , notFound :: { body :: { error :: String } }
36+ )
3037 }
3138
32−api :: forall ctx err. { getResource :: Om ctx (notModified :: Unit, unauthorized :: Unit | err) Resource }
3339api = client @ResourceAPI "http://localhost:44932"
3440
3541spec :: Spec Unit
@@ -68,6 +74,15 @@ spec = around withServer $ describe "bodyless responses (notModified / unauthori
6874 api.getResource
6975 liftAff $ resource `shouldEqual` { id: 1, name: "test" }
7076
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+
7186withServer :: forall a. (Ref String -> Aff a) -> Aff a
7287withServer test = bracket acquire release (\{ serverMode } -> test serverMode)
7388 where
@@ -77,6 +92,7 @@ withServer test = bracket acquire release (\{ serverMode } -> test serverMode)
7792 f <- F.fastify {}
7893 omApp <- createOmFastify {} f
7994 getOm (RouteURL "/bodyless-test/resource") (handler serverMode) omApp
95+ deleteOm (RouteURL "/bodyless-test/users/:id") (deleteHandler serverMode) omApp
8096 pure f
8197 void $ F.listen { port: Port 44932, host: Host "0.0.0.0" } fastify
8298 pure { fastify, serverMode }
@@ -96,3 +112,14 @@ withServer test = bracket acquire release (\{ serverMode } -> test serverMode)
96112 void $ F.status (StatusCode 200) reply # liftEffect
97113 void $ F.header "content-type" "application/json" reply # liftEffect
98114 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
8080 let result = appendQueryParams @(search :: String) { search: "hello world&more=yes" } "/users"
8181 result `shouldEqual` "/users?search=hello%20world%26more%3Dyes"
8282
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+
83104 describe "buildUrl" do
84105 it "builds URL with path params only" do
85106 let
test/Complete.Example.purs+14−19
@@ -3,9 +3,9 @@ module Complete.Example where
33import Prelude
44
55import Data.Array (length)
6−import Data.Maybe (Maybe(..))
6+import Justifill (justifill)
77import 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)
99import Yoga.HTTP.API.Route (BearerToken(..))
1010import Yoga.Om (Om, handleErrors)
1111
@@ -31,43 +31,37 @@ type ErrorMessage =
3131
3232type UserAPI =
3333 { getUser ::
34− Route GET
35− (Path ("users" / "id" : Int))
34+ Route GET ("users" / "id" : Int)
3635 {}
3736 ( ok :: { body :: User }
3837 , notFound :: { body :: ErrorMessage }
3938 )
4039 , listUsers ::
41− Route GET
42− (Path "users" :? { limit :: Int, offset :: Int })
40+ Route GET ("users" :? { limit :: Int, offset :: Int })
4341 {}
4442 ( ok :: { body :: Array User }
4543 )
4644 , createUser ::
47− Route POST
48− (Path "users")
45+ Route POST "users"
4946 { body :: JSON CreateUserRequest }
5047 ( created :: { body :: User }
5148 , badRequest :: { body :: ErrorMessage }
5249 )
5350 , updateUser ::
54− Route PUT
55− (Path ("users" / "id" : Int))
51+ Route PUT ("users" / "id" : Int)
5652 { body :: JSON UpdateUserRequest }
5753 ( ok :: { body :: User }
5854 , notFound :: { body :: ErrorMessage }
5955 , badRequest :: { body :: ErrorMessage }
6056 )
6157 , deleteUser ::
62− Route DELETE
63− (Path ("users" / "id" : Int))
58+ Route DELETE ("users" / "id" : Int)
6459 {}
65− ( noContent :: { body :: {} }
60+ ( noContent :: {}
6661 , notFound :: { body :: ErrorMessage }
6762 )
6863 , createUserAuth ::
69− Route POST
70− (Path "users")
64+ Route POST "users"
7165 { headers :: Record (authorization :: BearerToken), body :: JSON CreateUserRequest }
7266 ( created :: { body :: User }
7367 , badRequest :: { body :: ErrorMessage }
@@ -85,15 +79,15 @@ exampleGetUserHandled :: Om {} () Unit
8579exampleGetUserHandled = do
8680 user <- api.getUser { id: 42 }
8781 # handleErrors
88− { notFound: \err -> do
82+ { notFound: \err -> ado
8983 log $ "User not found: " <> err.error
90− pure { id: 0, name: "unknown", email: "" }
84+ in { id: 0, name: "unknown", email: "" }
9185 }
9286 log $ "Found user: " <> user.name
9387
9488exampleListUsers :: Om {} () Unit
9589exampleListUsers = do
96− users <- api.listUsers { limit: Just 10, offset: Just 0 }
90+ users <- api.listUsers (justifill { limit: 10, offset: 0 })
9791 log $ "Found " <> show (length users) <> " users"
9892
9993exampleCreateUser :: Om {} (badRequest :: ErrorMessage) Unit
@@ -106,7 +100,8 @@ exampleCreateUser = do
106100
107101exampleUpdateUser :: Om {} (notFound :: ErrorMessage, badRequest :: ErrorMessage) Unit
108102exampleUpdateUser = do
109− user <- api.updateUser { id: 42 }
103+ user <- api.updateUser
104+ { id: 42 }
110105 { name: "Alice Updated"
111106 , email: "alice.new@example.com"
112107 }
test/Example.purs+13−8
@@ -4,41 +4,46 @@ import Yoga.HTTP.API.Route (GET, POST, DELETE, Route, BearerToken)
44import Yoga.HTTP.API.Route.Encoding (JSON)
55import Yoga.HTTP.API.Path (Path, type (/), type (:), type (:?))
66
7−type User = { id :: Int, name :: String, email :: String }
8−type CreateUserRequest = { name :: String, email :: String }
7+type User = { id :: UserId, name :: UserName, email :: UserMail }
8+type CreateUserRequest = { name :: String, email :: UserMail }
99type 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
1015
1116type UserAPI =
1217 { getUser ::
1318 Route GET
14− (Path ("users" / "id" : Int))
19+ ("users" / "id" : UserId)
1520 {}
1621 ( ok :: { body :: User }
1722 , notFound :: { body :: ErrorMessage }
1823 )
1924 , listUsers ::
2025 Route GET
21− (Path "users" :? { limit :: Int, offset :: Int })
26+ ("users" :? { limit :: Limit, offset :: Offset })
2227 {}
2328 ( ok :: { body :: Array User }
2429 )
2530 , createUser ::
2631 Route POST
27− (Path "users")
32+ "users"
2833 { body :: JSON CreateUserRequest }
2934 ( created :: { body :: User }
3035 , badRequest :: { body :: ErrorMessage }
3136 )
3237 , deleteUser ::
3338 Route DELETE
34− (Path ("users" / "id" : Int))
39+ ("users" / "id" : UserId)
3540 {}
36− ( noContent :: { body :: {} }
41+ ( noContent :: {}
3742 , notFound :: { body :: ErrorMessage }
3843 )
3944 , createUserAuth ::
4045 Route POST
41− (Path "users")
46+ "users"
4247 { headers :: Record (authorization :: BearerToken), body :: JSON CreateUserRequest }
4348 ( created :: { body :: User }
4449 , badRequest :: { body :: ErrorMessage }
test/PlainTextResponse.Spec.purs+1−1
@@ -29,7 +29,7 @@ type TestAPI =
2929 )
3030 }
3131
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 }
3333api = client @TestAPI "http://localhost:44933"
3434
3535spec :: Spec Unit
test/Polymorphic.Spec.purs+20−17
@@ -12,7 +12,8 @@ import Yoga.Fastify.Fastify (Port(..), Host(..))
1212import Yoga.Fastify.Om.API (registerAPI)
1313import Yoga.Fastify.Om.Route (Handler, handle, respond)
1414import 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)
1617
1718type User = { id :: Int, name :: String }
1819
@@ -27,26 +28,28 @@ api = client @TestAPI "http://localhost:44931"
2728spec :: Spec Unit
2829spec = around withServer $ describe "polymorphic client" do
2930 it "fetches user with { token :: String } context" \_ ->
30− runOm { token: "secret123" } { exception: \_ -> pure unit } do
31− ctx <- ask
32− user <- api.getUser { id: 42 }
33− liftAff $ (ctx.token <> ":" <> user.name) `shouldEqual` "secret123:User 42"
31+ runOm { token: "secret123" } { exception: throwError } do
32+ { token } <- ask
33+ { name } <- api.getUser { id: 42 }
34+ liftAff $ (token <> ":" <> name) `shouldEqual` "secret123:User 42"
3435
3536 it "fetches user with { apiKey :: String, userId :: Int } context" \_ ->
36− runOm { apiKey: "mykey", userId: 7 } { exception: \_ -> pure unit } do
37− ctx <- ask
38− user <- api.getUser { id: ctx.userId }
39− liftAff $ (ctx.apiKey <> ":" <> user.name) `shouldEqual` "mykey:User 7"
37+ runOm { apiKey: "mykey", userId: 7 } { exception: throwError } do
38+ { apiKey, userId } <- ask
39+ { name } <- api.getUser { id: userId }
40+ liftAff $ (apiKey <> ":" <> name) `shouldEqual` "mykey:User 7"
4041
4142 it "same api.getUser works in two different contexts" \_ -> do
42− runOm { token: "tok1" } { exception: \_ -> pure unit } do
43− ctx <- ask
44− user <- api.getUser { id: 1 }
45− liftAff $ (ctx.token <> ":" <> user.name) `shouldEqual` "tok1:User 1"
46− runOm { endpoint: "prod", region: "eu" } { exception: \_ -> pure unit } do
47− ctx <- ask
48− user <- api.getUser { id: 2 }
49− liftAff $ (ctx.endpoint <> "-" <> ctx.region <> ":" <> user.name) `shouldEqual` "prod-eu:User 2"
43+
44+ runOm { token: "tok1" } { exception: throwError } do
45+ { token } <- ask
46+ { name } <- api.getUser { id: 1 }
47+ liftAff $ (token <> ":" <> name) `shouldEqual` "tok1:User 1"
48+
49+ runOm { endpoint: "prod", region: "eu" } { exception: throwError } do
50+ { endpoint, region } <- ask
51+ { name } <- api.getUser { id: 2 }
52+ liftAff $ (endpoint <> "-" <> region <> ":" <> name) `shouldEqual` "prod-eu:User 2"
5053
5154withServer :: forall a. (Unit -> _ a) -> _ a
5255withServer test = bracket acquire release (\_ -> test unit)
test/Roundtrip.Spec.purs+84−18
@@ -6,14 +6,17 @@ import Data.Maybe (Maybe(..))
66import Effect.Aff (bracket)
77import Effect.Aff.Class (liftAff)
88import Effect.Class (liftEffect)
9+import Effect.Ref as Ref
10+import Effect.Exception as Exn
11+import Justifill (justifill)
912import Test.Spec (Spec, around, describe, it)
1013import Test.Spec.Assertions (shouldEqual)
1114import Yoga.Fastify.Fastify as F
1215import Yoga.Fastify.Fastify (Port(..), Host(..))
1316import Yoga.Fastify.Om.API (registerAPI)
1417import Yoga.Fastify.Om.Route (Handler, handle, respond, reject, BearerToken(..))
15−import Yoga.Fetch.Om (GET, POST, PUT, DELETE, Route, JSON, type (/), type (:), type (:?), client)
16−import Yoga.Om (Om, ask, handleErrors, runOm)
18+import Yoga.Fetch.Om (GET, POST, PUT, DELETE, Required, Route, JSON, type (/), type (:), type (:?), client, clientWith)
19+import Yoga.Om (ask, handleErrors, runOm)
1720
1821-- Types
1922
@@ -23,6 +26,7 @@ type ErrorMsg = { error :: String }
2326
2427-- Route definitions shared by server and client
2528
29+
2630type GetUserRoute = Route GET
2731 ("users" / "id" : Int)
2832 {}
@@ -68,6 +72,28 @@ type SearchRoute = Route GET
6872 {}
6973 (ok :: { body :: { active :: Boolean, minScore :: Number } })
7074
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+ }
7197type API =
7298 { getUser :: GetUserRoute
7399 , listUsers :: ListUsersRoute
@@ -82,6 +108,7 @@ api = client @API "http://localhost:44932"
82108
83109-- Handlers
84110
111+
85112getUserHandler :: Handler GetUserRoute ()
86113getUserHandler = handle do
87114 { path } <- ask
@@ -147,11 +174,19 @@ searchHandler = handle do
147174 Nothing -> 0.0
148175 respond @"ok" { active, minScore }
149176
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+
150184-- Test suite
151185
152186spec :: Spec Unit
153187spec = around withServer $ describe "server ↔ client roundtrip" do
154188
189+
155190 describe "GET with path params" do
156191 it "returns user for valid id" \_ ->
157192 runOm {} { exception: \_ -> pure unit } do
@@ -168,12 +203,12 @@ spec = around withServer $ describe "server ↔ client roundtrip" do
168203 describe "GET with query params" do
169204 it "passes query params correctly" \_ ->
170205 runOm {} { exception: \_ -> pure unit } do
171− users <- api.listUsers { limit: 1 }
206+ users <- api.listUsers (justifill { limit: 1 })
172207 liftAff $ (map _.name users) `shouldEqual` ["Alice"]
173208
174209 it "handles no query params" \_ ->
175210 runOm {} { exception: \_ -> pure unit } do
176− users <- api.listUsers {}
211+ users <- api.listUsers (justifill {})
177212 liftAff $ (map _.name users) `shouldEqual` ["Alice", "Bob", "Charlie"]
178213
179214 describe "POST with JSON body" do
@@ -195,12 +230,23 @@ spec = around withServer $ describe "server ↔ client roundtrip" do
195230 describe "PUT with path params and JSON body" do
196231 it "sends path params and body together" \_ ->
197232 runOm {} { exception: \_ -> pure unit } do
198− user <- api.updateUser { id: 42 } { name: "Updated", email: "u@t.com" }
233+ user <- api.updateUser
234+ { id: 42 }
235+ { name: "Updated", email: "u@t.com" }
199236 # handleErrors { notFound: \e -> pure { id: 0, name: e.error, email: "" } }
200237 liftAff do
201238 user.id `shouldEqual` 42
202239 user.name `shouldEqual` "Updated"
203240
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+
204250 describe "DELETE with path params" do
205251 it "deletes successfully" \_ ->
206252 runOm {} { exception: \_ -> pure unit } do
@@ -230,41 +276,61 @@ spec = around withServer $ describe "server ↔ client roundtrip" do
230276 describe "GET with Boolean and Number query params" do
231277 it "passes Boolean and Number query params" \_ ->
232278 runOm {} { exception: \_ -> pure unit } do
233− result <- api.search { active: true, minScore: 3.14 }
279+ result <- api.search (justifill { active: true, minScore: 3.14 })
234280 liftAff do
235281 result.active `shouldEqual` true
236282 result.minScore `shouldEqual` 3.14
237283
238284 it "handles missing optional query params" \_ ->
239285 runOm {} { exception: \_ -> pure unit } do
240− result <- api.search {}
286+ result <- api.search (justifill {})
241287 liftAff do
242288 result.active `shouldEqual` false
243289 result.minScore `shouldEqual` 0.0
244290
245− describe "partial query params — no wrapper needed" do
246− it "passes subset of query params directly" \_ ->
291+ describe "optional query params" do
292+ it "accepts a selected optional query parameter" \_ ->
247293 runOm {} { exception: \_ -> pure unit } do
248− users <- api.listUsers { limit: 1 }
294+ users <- api.listUsers (justifill { limit: 1 })
249295 liftAff $ (map _.name users) `shouldEqual` ["Alice"]
250296
251− it "passes all query params directly" \_ ->
297+ it "accepts all optional query parameters" \_ ->
252298 runOm {} { exception: \_ -> pure unit } do
253− users <- api.listUsers { limit: 2, offset: 0 }
299+ users <- api.listUsers (justifill { limit: 2, offset: 0 })
254300 liftAff $ (map _.name users) `shouldEqual` ["Alice", "Bob"]
255301
256− it "passes empty record for all defaults" \_ ->
302+ it "fills omitted optional query parameters with Nothing" \_ ->
257303 runOm {} { exception: \_ -> pure unit } do
258− users <- api.listUsers {}
304+ users <- api.listUsers (justifill {})
259305 liftAff $ (map _.name users) `shouldEqual` ["Alice", "Bob", "Charlie"]
260306
261− it "passes partial Boolean query param" \_ ->
307+ it "passes selected Boolean query param" \_ ->
262308 runOm {} { exception: \_ -> pure unit } do
263− result <- api.search { active: true }
309+ result <- api.search (justifill { active: true })
264310 liftAff do
265311 result.active `shouldEqual` true
266312 result.minScore `shouldEqual` 0.0
267313
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+
268334-- Server setup
269335
270336withServer :: forall a. (Unit -> _ a) -> _ a
@@ -281,8 +347,8 @@ withServer test = bracket acquire release (\_ -> test unit)
281347 , deleteUser: deleteUserHandler
282348 , me: meHandler
283349 , search: searchHandler
284− }
285− f
350+ } f
351+ registerAPI @InjectedAPI { getTenantUser: tenantUserHandler } f
286352 pure f
287353 void $ F.listen { port: Port 44932, host: Host "0.0.0.0" } fastify
288354 pure fastify