git.iko.soy/rowtype-yoga/purescript-yoga-fetch-om
on main help sign in
Initial commit: yoga-fetch-om implementation
commit6a452da28321b9531e21f4a6d1a1e260919b4fbe authorMark <mark.eibes@gmail.com> dateSun 8 Feb 2026, 15:00 (8 months ago)
23 files changed+1880
.github/workflows/ci.yml+16 .gitignore+6 README.md+229 package.json+33 spago.yaml+56 src/Yoga/Fetch/Om.purs+106 src/Yoga/Fetch/Om/BuildUrl.purs+142 src/Yoga/Fetch/Om/ClientFn.purs+142 src/Yoga/Fetch/Om/ExtractParams.purs+52 src/Yoga/Fetch/Om/MakeRequest.purs+90 src/Yoga/Fetch/Om/ParseResponse.purs+105 src/Yoga/Fetch/Om/Simple.purs+244 src/Yoga/Fetch/Om/SplitParams.purs+60 src/Yoga/Fetch/Om/SplitResponses.purs+83 src/Yoga/Fetch/Om/Variant.purs+48 test/BuildUrl.Spec.purs+113 test/Complete.Example.purs+118 test/Example.purs+39 test/Simple.Spec.purs+69 test/SplitParams.Spec.purs+76 test/Test.Main.purs+20 test/Variant.Spec.purs+24 vitest.config.js+9
.github/workflows/ci.ymlnew file+16
@@ -0,0 +1,16 @@
1+name: CI
2+
3+on:
4+ push:
5+ branches: [main]
6+ pull_request:
7+ branches: [main]
8+
9+jobs:
10+ build:
11+ runs-on: ubuntu-latest
12+ steps:
13+ - uses: actions/checkout@v4
14+ - uses: rowtype-yoga/purescript-yoga-actions/.github/actions/setup@main
15+ - run: spago build
16+ - run: spago test
.gitignorenew file+6
@@ -0,0 +1,6 @@
1+output/
2+output-compile-fail/
3+node_modules/
4+.spago/
5+.psc-ide-port
6+*.lock
README.mdnew file+229
@@ -0,0 +1,229 @@
1+# purescript-yoga-fetch-om
2+
3+Derive type-safe fetch clients from `purescript-yoga-http-api` route definitions.
4+
5+## Concept
6+
7+Similar to how `purescript-yoga-fastify-om` derives servers from HTTP API definitions, this library derives **clients** from the same definitions using the Fetch API.
8+
9+## Installation
10+
11+```bash
12+spago install yoga-fetch-om
13+```
14+
15+## Quick Start
16+
17+### 1. Define your API using yoga-http-api
18+
19+```purescript
20+import Yoga.Fetch.Om
21+
22+type UserAPI =
23+ { getUser ::
24+ Route GET
25+ (Path ("users" / "id" : Int))
26+ (Request {})
27+ ( ok :: { body :: User }
28+ , notFound :: { body :: ErrorMessage }
29+ )
30+ , createUser ::
31+ Route POST
32+ (Path "users")
33+ (Request { body :: JSON CreateUserRequest })
34+ ( created :: { body :: User }
35+ , badRequest :: { body :: ErrorMessage }
36+ )
37+ }
38+```
39+
40+### 2. Derive a type-safe client
41+
42+```purescript
43+api = deriveAPI "https://api.example.com" (Proxy :: _ UserAPI)
44+```
45+
46+### 3. Use it!
47+
48+```purescript
49+main = do
50+ -- Compiler ensures you pass the right parameters
51+ result <- api.getUser { id: 42 }
52+
53+ -- Pattern match on all possible responses
54+ result # Variant.match
55+ { ok: \(Response { body: user }) ->
56+ log $ "User: " <> user.name
57+ , notFound: \_ ->
58+ log "User not found"
59+ }
60+```
61+
62+## Features
63+
64+### ✅ Type-Safe Parameters
65+
66+The compiler ensures you provide exactly the right parameters:
67+
68+```purescript
69+-- Path parameters
70+api.getUser { id: 42 }
71+
72+-- Query parameters
73+api.listUsers { limit: 10, offset: 20 }
74+
75+-- Request body
76+api.createUser { body: { name: "Alice", email: "alice@example.com" } }
77+
78+-- Mixed parameters
79+api.updateUser { id: 42, body: { name: "Alice Updated" } }
80+```
81+
82+### ✅ Exhaustive Response Handling
83+
84+Variant types ensure you handle all possible responses:
85+
86+```purescript
87+result <- api.createUser { body: newUser }
88+
89+result # Variant.match
90+ { created: \(Response { body }) -> log $ "Created: " <> show body.id
91+ , badRequest: \(Response { body }) -> log $ "Error: " <> body.error
92+ -- Forgot a case? Compile error!
93+ }
94+```
95+
96+### ✅ Single Source of Truth
97+
98+Define your API once, use it everywhere:
99+
100+```purescript
101+-- Server (yoga-fastify-om)
102+server = buildServer apiRoutes handlers
103+
104+-- Client (yoga-fetch-om)
105+client = deriveAPI baseUrl (Proxy :: _ apiRoutes)
106+```
107+
108+Changes to routes automatically update both client and server!
109+
110+### ✅ Automatic Everything
111+
112+No manual client code needed:
113+- ✅ URL building with path parameter substitution
114+- ✅ Query string construction
115+- ✅ JSON request serialization
116+- ✅ JSON response parsing
117+- ✅ Status code → variant mapping
118+- ✅ CORS and credentials handling
119+
120+### ✅ Integration with yoga-om
121+
122+Works seamlessly with the Om monad:
123+
124+```purescript
125+getUserProfile :: Om AppContext AppErrors User
126+getUserProfile = do
127+ { api, userId } <- ask
128+ result <- liftAff $ api.getUser { id: userId }
129+ case result of
130+ ok -> pure result.body
131+ notFound -> throw { userNotFound: userId }
132+```
133+
134+## Complete Example
135+
136+See [`test/Complete.Example.purs`](test/Complete.Example.purs) for a full working example with:
137+- GET requests with path parameters
138+- GET requests with query parameters
139+- POST/PUT requests with JSON bodies
140+- DELETE requests
141+- Complete error handling
142+
143+## How It Works
144+
145+The library uses PureScript's type system to:
146+
147+1. **Extract parameters** from route definitions at compile time
148+ - Path params from segments: `"users" / "id" : Int`
149+ - Query params from `:?` operator: `:? { limit :: Int }`
150+ - Body params from Request type: `{ body :: JSON User }`
151+
152+2. **Build URLs** by:
153+ - Getting the path pattern: `/users/:id`
154+ - Substituting path params: `/users/42`
155+ - Appending query params: `?limit=10&offset=20`
156+
157+3. **Make requests** using:
158+ - `js-fetch` for the fetch API
159+ - `js-promise-aff` to convert Promises → Aff
160+ - `yoga-json` for JSON serialization
161+
162+4. **Parse responses** by:
163+ - Mapping status codes to variant labels (200 → "ok", 404 → "notFound")
164+ - Parsing JSON response bodies
165+ - Constructing typed Response values
166+
167+All of this happens automatically based on your route types!
168+
169+## API Reference
170+
171+### `deriveAPI`
172+
173+```purescript
174+deriveAPI :: String -> Proxy (Record routes) -> Record clients
175+```
176+
177+Derives a record of client functions from a record of routes.
178+
179+**Parameters:**
180+- `baseUrl` - The base URL of the API (e.g., `"https://api.example.com"`)
181+- `routes` - Proxy of your API route definitions
182+
183+**Returns:** A record where each route becomes a function taking params and returning `Aff (Variant response)`
184+
185+### `deriveClientFn`
186+
187+```purescript
188+deriveClientFn :: String -> Proxy (Route method segments request response) -> ClientFn params response
189+```
190+
191+Derives a single client function from a route. Usually you'll use `deriveAPI` instead, but this is useful for individual routes.
192+
193+## Roadmap
194+
195+### Current Version (0.1.0)
196+- ✅ Core client derivation
197+- ✅ All HTTP methods (GET, POST, PUT, PATCH, DELETE)
198+- ✅ Path, query, and body parameters
199+- ✅ JSON encoding/decoding
200+- ✅ Variant response handling
201+- ✅ CORS support
202+
203+### Future
204+- ⏳ Request/response headers support
205+- ⏳ Authentication (Bearer tokens, API keys)
206+- ⏳ Request/response interceptors
207+- ⏳ Retry logic with exponential backoff
208+- ⏳ Timeout configuration
209+- ⏳ FormData and file upload support
210+- ⏳ Progress events
211+- ⏳ Request cancellation
212+
213+## Contributing
214+
215+Contributions welcome! This library follows the patterns established by the yoga ecosystem:
216+- Type-level programming for automatic derivation
217+- Integration with yoga-json for serialization
218+- Consistent API design with yoga-fastify-om
219+
220+## License
221+
222+MIT
223+
224+## Related Projects
225+
226+- [purescript-yoga-http-api](https://github.com/rowtype-yoga/purescript-yoga-http-api) - HTTP API type definitions
227+- [purescript-yoga-fastify-om](https://github.com/rowtype-yoga/purescript-yoga-fastify-om) - Server-side counterpart
228+- [purescript-yoga-json](https://github.com/rowtype-yoga/purescript-yoga-json) - JSON serialization
229+- [purescript-yoga-om](https://github.com/rowtype-yoga/purescript-yoga-om) - Om monad for effects and DI
package.jsonnew file+33
@@ -0,0 +1,33 @@
1+{
2+ "name": "purescript-yoga-fetch-om",
3+ "version": "0.1.0",
4+ "description": "Derive type-safe fetch clients from yoga HTTP API definitions",
5+ "type": "module",
6+ "scripts": {
7+ "build": "spago build",
8+ "test": "spago test",
9+ "clean": "rm -rf output .spago",
10+ "watch": "spago build --watch",
11+ "repl": "spago repl",
12+ "docs": "spago docs",
13+ "bundle": "spago bundle-app --main Complete.Example --to dist/index.js"
14+ },
15+ "keywords": [
16+ "purescript",
17+ "fetch",
18+ "http",
19+ "client",
20+ "api",
21+ "yoga",
22+ "type-safe"
23+ ],
24+ "author": "",
25+ "license": "MIT",
26+ "devDependencies": {
27+ "jsdom": "^28.0.0",
28+ "purescript": "^0.15.15",
29+ "spago": "^0.93.42",
30+ "vitest": "^2.1.8"
31+ },
32+ "dependencies": {}
33+}
spago.yamlnew file+56
@@ -0,0 +1,56 @@
1+workspace:
2+ packageSet:
3+ registry: 51.1.0
4+ extraPackages:
5+ yoga-http-api:
6+ path: ../purescript-yoga-http-api
7+ yoga-json:
8+ path: ../purescript-yoga-json
9+ yoga-om:
10+ path: ../purescript-yoga-om
11+ vitest:
12+ path: ../purescript-vitest
13+
14+package:
15+ name: yoga-fetch-om
16+ publish:
17+ version: 0.1.0
18+ license: MIT
19+ location:
20+ githubOwner: rowtype-yoga
21+ githubRepo: purescript-yoga-fetch-om
22+ include:
23+ - LICENSE
24+ - README.md
25+ - CHANGELOG.md
26+ exclude:
27+ - .github
28+ dependencies:
29+ - aff: ">=7.0.0 <8.0.0"
30+ - arrays: ">=7.0.0 <8.0.0"
31+ - effect: ">=4.0.0 <5.0.0"
32+ - either: ">=6.0.0 <7.0.0"
33+ - foldable-traversable: ">=6.0.0 <7.0.0"
34+ - foreign: ">=7.0.0 <8.0.0"
35+ - foreign-object: ">=4.0.0 <5.0.0"
36+ - maybe: ">=6.0.0 <7.0.0"
37+ - newtype: ">=5.0.0 <6.0.0"
38+ - prelude: ">=6.0.0 <7.0.0"
39+ - record: ">=4.0.0 <5.0.0"
40+ - strings: ">=6.0.0 <7.0.0"
41+ - tuples: ">=7.0.0 <8.0.0"
42+ - typelevel-prelude: ">=7.0.0 <8.0.0"
43+ - variant: ">=8.0.0 <9.0.0"
44+ - yoga-http-api: ">=0.1.0 <0.2.0"
45+ - yoga-json: ">=5.0.0 <6.0.0"
46+ - yoga-om: ">=0.1.0 <0.2.0"
47+ - js-fetch: ">=1.0.0 <2.0.0"
48+ - js-promise-aff: ">=1.0.0 <2.0.0"
49+ - http-methods: ">=6.0.0 <7.0.0"
50+ - undefined-is-not-a-problem: ">=1.0.0 <2.0.0"
51+ - untagged-union: ">=1.0.0 <2.0.0"
52+ test:
53+ main: Test.Main
54+ dependencies:
55+ - vitest: ">=1.0.0 <2.0.0"
56+ - console: ">=6.0.0 <7.0.0"
src/Yoga/Fetch/Om.pursnew file+106
@@ -0,0 +1,106 @@
1+module Yoga.Fetch.Om
2+ ( class DeriveClient
3+ , deriveClient
4+ , class DeriveClientFn
5+ , deriveClientFn
6+ , class DeriveClientRL
7+ , deriveClientRL
8+ , module Yoga.HTTP.API.Route
9+ , module Yoga.HTTP.API.Path
10+ , module Yoga.Fetch.Om.Simple
11+ ) where
12+
13+import Prelude
14+
15+import Data.Symbol (class IsSymbol)
16+import Data.Variant (Variant)
17+import Prim.Row as Row
18+import Prim.RowList (class RowToList, RowList)
19+import Prim.RowList as RL
20+import Record as Record
21+import Type.Proxy (Proxy(..))
22+import Unsafe.Coerce (unsafeCoerce)
23+import Yoga.Fetch.Om.BuildUrl (class BuildUrl, buildUrl)
24+import Yoga.Fetch.Om.Simple (class DecodeResponse, decodeResponse, FetchError, FetchResponse, get, getWithHeaders, delete, deleteWithHeaders, delete_, post, postWithHeaders, post_, put, putWithHeaders, put_, patch, patchWithHeaders, patch_)
25+import Yoga.Fetch.Om.ClientFn (class BuildClientFn, class CheckBodyIsUnit, class CheckRowEmpty, buildClientFn)
26+import Yoga.Fetch.Om.ExtractParams (class ExtractRequestBody, class ExtractRequestHeaders)
27+import Yoga.Fetch.Om.MakeRequest (class MakeRequest, class SerializeBody, makeRequest, serializeBody)
28+import Yoga.Fetch.Om.ParseResponse (class ParseResponse, parseResponse)
29+import Yoga.Fetch.Om.SplitResponses (class SplitResponses)
30+import Yoga.Fetch.Om.Variant (class VariantOrValue, variantOrValue)
31+import Yoga.HTTP.API.Path (Path, Root, Lit, Capture, PathCons, Param, QueryParams, Required, type (/), type (:), type (:?), class PathPattern)
32+import Yoga.HTTP.API.Route (Route(..), GET, POST, PUT, DELETE, PATCH, Response(..), JSON, FormData, NoBody)
33+import Yoga.HTTP.API.Route.Handler (class SegmentPathParams, class SegmentQueryParams)
34+import Yoga.Om (Om, fromAff)
35+
36+class DeriveClientFn :: forall k1. Type -> k1 -> Type -> Row Type -> Row Type -> Row Type -> Type -> Type -> Constraint
37+class
38+ DeriveClientFn method segments request response errorRow successRow result fn
39+ | method segments request response -> errorRow successRow result fn where
40+ deriveClientFn :: String -> Proxy (Route method segments request response) -> fn
41+
42+instance
43+ ( SegmentPathParams segments pathParams
44+ , SegmentQueryParams segments queryParams
45+ , ExtractRequestBody request body
46+ , ExtractRequestHeaders request headers
47+ , PathPattern segments
48+ , BuildUrl segments pathParams queryParams
49+ , MakeRequest method
50+ , SerializeBody body
51+ , SplitResponses response successRow errorRow
52+ , ParseResponse errorRow successRow
53+ , RowToList successRow successRL
54+ , VariantOrValue successRL successRow result
55+ , Row.Union pathParams queryParams pathQuery
56+ , Row.Nub pathQuery pathQuery
57+ , RowToList pathQuery pathQueryRL
58+ , CheckRowEmpty headers headersFlag
59+ , CheckBodyIsUnit body bodyFlag
60+ , BuildClientFn pathQueryRL headersFlag bodyFlag body pathQuery headers errorRow result fn
61+ ) =>
62+ DeriveClientFn method segments request response errorRow successRow result fn where
63+ deriveClientFn baseUrl _ =
64+ buildClientFn (Proxy :: _ pathQueryRL) (Proxy :: _ headersFlag) (Proxy :: _ bodyFlag) impl
65+ where
66+ impl :: Record pathQuery -> Record headers -> body -> Om {} errorRow result
67+ impl pathQueryRec _headers bodyVal = do
68+ let url = buildUrl baseUrl (Proxy :: _ segments) pathParamsRec queryParamsRec
69+ fetchResp <- makeRequest (Proxy :: _ method) url (serializeBody bodyVal) # fromAff
70+ variant <- parseResponse fetchResp :: Om {} errorRow (Variant successRow)
71+ variantOrValue (Proxy :: _ successRL) variant # pure
72+ where
73+ pathParamsRec = unsafeCoerce pathQueryRec :: Record pathParams
74+ queryParamsRec = unsafeCoerce pathQueryRec :: Record queryParams
75+
76+class DeriveClient :: Row Type -> Row Type -> Constraint
77+class DeriveClient routesRow clientsRow | routesRow -> clientsRow where
78+ deriveClient :: String -> Proxy (Record routesRow) -> Record clientsRow
79+
80+instance
81+ ( RowToList routesRow rl
82+ , DeriveClientRL rl () clientsRow
83+ ) =>
84+ DeriveClient routesRow clientsRow where
85+ deriveClient baseUrl _ = deriveClientRL baseUrl (Proxy :: _ rl) {}
86+
87+class DeriveClientRL :: RowList Type -> Row Type -> Row Type -> Constraint
88+class DeriveClientRL rl acc out | rl acc -> out where
89+ deriveClientRL :: String -> Proxy rl -> Record acc -> Record out
90+
91+instance DeriveClientRL RL.Nil acc acc where
92+ deriveClientRL _ _ acc = acc
93+
94+instance
95+ ( IsSymbol label
96+ , DeriveClientFn method segments request response errorRow successRow result fn
97+ , DeriveClientRL tail acc1 acc2
98+ , Row.Cons label fn acc2 out
99+ , Row.Lacks label acc2
100+ , Row.Cons label (Route method segments request response) routeTail routeRow
101+ ) =>
102+ DeriveClientRL (RL.Cons label (Route method segments request response) tail) acc1 out where
103+ deriveClientRL baseUrl _ acc = Record.insert (Proxy :: _ label) clientFn rest
104+ where
105+ clientFn = deriveClientFn baseUrl (Proxy :: _ (Route method segments request response))
106+ rest = deriveClientRL baseUrl (Proxy :: _ tail) acc
src/Yoga/Fetch/Om/BuildUrl.pursnew file+142
@@ -0,0 +1,142 @@
1+module Yoga.Fetch.Om.BuildUrl
2+ ( class BuildUrl
3+ , buildUrl
4+ , class SubstitutePathParams
5+ , substitutePathParams
6+ , class AppendQueryParams
7+ , appendQueryParams
8+ , class AppendQueryParamsRL
9+ , appendQueryParamsRL
10+ , class SubstitutePathParamsRL
11+ , substitutePathParamsRL
12+ , class SerializeParam
13+ , serializeParam
14+ ) where
15+
16+import Prelude
17+
18+import Data.Array as Array
19+import Data.Maybe (Maybe(..))
20+import Data.String (Pattern(..), Replacement(..))
21+import Data.String as String
22+import Data.Symbol (class IsSymbol, reflectSymbol)
23+import Prim.Row as Row
24+import Prim.RowList (class RowToList, RowList)
25+import Prim.RowList as RL
26+import Record as Record
27+import Type.Proxy (Proxy(..))
28+import Yoga.HTTP.API.Path (class PathPattern, pathPattern)
29+
30+class BuildUrl :: forall k. k -> Row Type -> Row Type -> Constraint
31+class BuildUrl segments pathParams queryParams | segments -> pathParams queryParams where
32+ buildUrl :: String -> Proxy segments -> Record pathParams -> Record queryParams -> String
33+
34+instance
35+ ( PathPattern segments
36+ , SubstitutePathParams pathParams
37+ , AppendQueryParams queryParams
38+ ) =>
39+ BuildUrl segments pathParams queryParams where
40+ buildUrl baseUrl segmentsProxy pathParamsRec queryParamsRec = withQueryParams
41+ where
42+ pattern = pathPattern segmentsProxy
43+ withBase = case baseUrl of
44+ "" -> pattern
45+ base -> base <> pattern
46+ withPathParams = substitutePathParams (Proxy :: _ pathParams) pathParamsRec withBase
47+ withQueryParams = appendQueryParams (Proxy :: _ queryParams) queryParamsRec withPathParams
48+
49+class SubstitutePathParams :: Row Type -> Constraint
50+class SubstitutePathParams params where
51+ substitutePathParams :: Proxy params -> Record params -> String -> String
52+
53+instance
54+ ( RowToList params rl
55+ , SubstitutePathParamsRL rl params
56+ ) =>
57+ SubstitutePathParams params where
58+ substitutePathParams _ = substitutePathParamsRL (Proxy :: _ rl)
59+
60+class SubstitutePathParamsRL :: RowList Type -> Row Type -> Constraint
61+class SubstitutePathParamsRL rl r where
62+ substitutePathParamsRL :: Proxy rl -> Record r -> String -> String
63+
64+instance SubstitutePathParamsRL RL.Nil r where
65+ substitutePathParamsRL _ _ url = url
66+
67+instance
68+ ( IsSymbol name
69+ , SerializeParam ty
70+ , SubstitutePathParamsRL tail r
71+ , Row.Cons name ty tailRow r
72+ ) =>
73+ SubstitutePathParamsRL (RL.Cons name ty tail) r where
74+ substitutePathParamsRL _ rec url = substitutePathParamsRL (Proxy :: _ tail) rec replaced
75+ where
76+ value = Record.get (Proxy :: _ name) rec
77+ pattern = Pattern (":" <> reflectSymbol (Proxy :: _ name))
78+ replaced = String.replaceAll pattern (Replacement (serializeParam value)) url
79+
80+class AppendQueryParams :: Row Type -> Constraint
81+class AppendQueryParams params where
82+ appendQueryParams :: Proxy params -> Record params -> String -> String
83+
84+instance
85+ ( RowToList params rl
86+ , AppendQueryParamsRL rl params
87+ ) =>
88+ AppendQueryParams params where
89+ appendQueryParams _ rec url = url <> queryString
90+ where
91+ pairs = appendQueryParamsRL (Proxy :: _ rl) rec []
92+ queryString = case pairs of
93+ [] -> ""
94+ _ -> "?" <> String.joinWith "&" pairs
95+
96+class AppendQueryParamsRL :: RowList Type -> Row Type -> Constraint
97+class AppendQueryParamsRL rl r where
98+ appendQueryParamsRL :: Proxy rl -> Record r -> Array String -> Array String
99+
100+instance AppendQueryParamsRL RL.Nil r where
101+ appendQueryParamsRL _ _ acc = acc
102+
103+instance
104+ ( IsSymbol name
105+ , SerializeParam ty
106+ , AppendQueryParamsRL tail r
107+ , Row.Cons name (Maybe ty) tailRow r
108+ ) =>
109+ AppendQueryParamsRL (RL.Cons name (Maybe ty) tail) r where
110+ appendQueryParamsRL _ rec acc = appendQueryParamsRL (Proxy :: _ tail) rec acc'
111+ where
112+ acc' = case Record.get (Proxy :: _ name) rec of
113+ Nothing -> acc
114+ Just value -> Array.snoc acc (reflectSymbol (Proxy :: _ name) <> "=" <> serializeParam value)
115+
116+else instance
117+ ( IsSymbol name
118+ , SerializeParam ty
119+ , AppendQueryParamsRL tail r
120+ , Row.Cons name ty tailRow r
121+ ) =>
122+ AppendQueryParamsRL (RL.Cons name ty tail) r where
123+ appendQueryParamsRL _ rec acc =
124+ appendQueryParamsRL (Proxy :: _ tail) rec (Array.snoc acc pair)
125+ where
126+ pair = reflectSymbol (Proxy :: _ name) <> "=" <> serializeParam (Record.get (Proxy :: _ name) rec)
127+
128+class SerializeParam :: Type -> Constraint
129+class SerializeParam a where
130+ serializeParam :: a -> String
131+
132+instance SerializeParam String where
133+ serializeParam = identity
134+
135+instance SerializeParam Int where
136+ serializeParam = show
137+
138+instance SerializeParam Number where
139+ serializeParam = show
140+
141+instance SerializeParam Boolean where
142+ serializeParam = show
src/Yoga/Fetch/Om/ClientFn.pursnew file+142
@@ -0,0 +1,142 @@
1+module Yoga.Fetch.Om.ClientFn
2+ ( class BuildClientFn
3+ , buildClientFn
4+ , class CheckBodyIsUnit
5+ , class CheckRowEmpty
6+ , IsUnit
7+ , IsNotUnit
8+ , IsEmpty
9+ , IsNonEmpty
10+ ) where
11+
12+import Data.Unit (Unit, unit)
13+import Prim.RowList (class RowToList, RowList)
14+import Prim.RowList as RL
15+import Type.Proxy (Proxy)
16+import Unsafe.Coerce (unsafeCoerce)
17+import Yoga.Om (Om)
18+
19+data IsUnit
20+data IsNotUnit
21+data IsEmpty
22+data IsNonEmpty
23+
24+class CheckBodyIsUnit :: Type -> Type -> Constraint
25+class CheckBodyIsUnit body flag | body -> flag
26+
27+instance CheckBodyIsUnit Unit IsUnit
28+else instance CheckBodyIsUnit body IsNotUnit
29+
30+class CheckRowEmpty :: Row Type -> Type -> Constraint
31+class CheckRowEmpty row flag | row -> flag
32+
33+instance RowToList row RL.Nil => CheckRowEmpty row IsEmpty
34+else instance CheckRowEmpty row IsNonEmpty
35+
36+class BuildClientFn
37+ :: RowList Type -> Type -> Type -> Type -> Row Type -> Row Type -> Row Type -> Type -> Type -> Constraint
38+class
39+ BuildClientFn pathQueryRL headersFlag bodyFlag body pathQuery headers errorRow result fn
40+ | pathQueryRL headersFlag bodyFlag body pathQuery headers errorRow result -> fn where
41+ buildClientFn
42+ :: Proxy pathQueryRL
43+ -> Proxy headersFlag
44+ -> Proxy bodyFlag
45+ -> (Record pathQuery -> Record headers -> body -> Om {} errorRow result)
46+ -> fn
47+
48+instance
49+ BuildClientFn RL.Nil
50+ IsEmpty
51+ IsUnit
52+ Unit
53+ pq
54+ h
55+ errorRow
56+ result
57+ (Om {} errorRow result) where
58+ buildClientFn _ _ _ f = f (unsafeCoerce {}) (unsafeCoerce {}) unit
59+
60+instance
61+ BuildClientFn RL.Nil
62+ IsEmpty
63+ IsNotUnit
64+ body
65+ pq
66+ h
67+ errorRow
68+ result
69+ (body -> Om {} errorRow result) where
70+ buildClientFn _ _ _ f = \b -> f (unsafeCoerce {}) (unsafeCoerce {}) b
71+
72+instance
73+ BuildClientFn RL.Nil
74+ IsNonEmpty
75+ IsUnit
76+ Unit
77+ pq
78+ h
79+ errorRow
80+ result
81+ (Record h -> Om {} errorRow result) where
82+ buildClientFn _ _ _ f = \hdrs -> f (unsafeCoerce {}) hdrs unit
83+
84+instance
85+ BuildClientFn RL.Nil
86+ IsNonEmpty
87+ IsNotUnit
88+ body
89+ pq
90+ h
91+ errorRow
92+ result
93+ (Record h -> body -> Om {} errorRow result) where
94+ buildClientFn _ _ _ f = \hdrs b -> f (unsafeCoerce {}) hdrs b
95+
96+instance
97+ BuildClientFn (RL.Cons n t tl)
98+ IsEmpty
99+ IsUnit
100+ Unit
101+ pq
102+ h
103+ errorRow
104+ result
105+ (Record pq -> Om {} errorRow result) where
106+ buildClientFn _ _ _ f = \pqr -> f pqr (unsafeCoerce {}) unit
107+
108+instance
109+ BuildClientFn (RL.Cons n t tl)
110+ IsEmpty
111+ IsNotUnit
112+ body
113+ pq
114+ h
115+ errorRow
116+ result
117+ (Record pq -> body -> Om {} errorRow result) where
118+ buildClientFn _ _ _ f = \pqr b -> f pqr (unsafeCoerce {}) b
119+
120+instance
121+ BuildClientFn (RL.Cons n t tl)
122+ IsNonEmpty
123+ IsUnit
124+ Unit
125+ pq
126+ h
127+ errorRow
128+ result
129+ (Record pq -> Record h -> Om {} errorRow result) where
130+ buildClientFn _ _ _ f = \pqr hdrs -> f pqr hdrs unit
131+
132+instance
133+ BuildClientFn (RL.Cons n t tl)
134+ IsNonEmpty
135+ IsNotUnit
136+ body
137+ pq
138+ h
139+ errorRow
140+ result
141+ (Record pq -> Record h -> body -> Om {} errorRow result) where
142+ buildClientFn _ _ _ f = f
src/Yoga/Fetch/Om/ExtractParams.pursnew file+52
@@ -0,0 +1,52 @@
1+module Yoga.Fetch.Om.ExtractParams
2+ ( class ExtractRequestHeaders
3+ , class ExtractRequestBody
4+ , class FindBodyType
5+ , class UnwrapEncoding
6+ , class FindHeaders
7+ ) where
8+
9+import Data.Unit (Unit)
10+import Prim.RowList (class RowToList, RowList)
11+import Prim.RowList as RL
12+import Yoga.HTTP.API.Route.Encoding (JSON, FormData, NoBody)
13+
14+class ExtractRequestBody :: Type -> Type -> Constraint
15+class ExtractRequestBody request body | request -> body
16+
17+instance
18+ ( RowToList row rl
19+ , FindBodyType rl encoding
20+ , UnwrapEncoding encoding body
21+ ) =>
22+ ExtractRequestBody (Record row) body
23+
24+class ExtractRequestHeaders :: Type -> Row Type -> Constraint
25+class ExtractRequestHeaders request headers | request -> headers
26+
27+instance
28+ ( RowToList row rl
29+ , FindHeaders rl headers
30+ ) =>
31+ ExtractRequestHeaders (Record row) headers
32+
33+class FindBodyType :: RowList Type -> Type -> Constraint
34+class FindBodyType rl encoding | rl -> encoding
35+
36+instance FindBodyType RL.Nil NoBody
37+instance FindBodyType (RL.Cons "body" encoding tail) encoding
38+else instance FindBodyType tail encoding => FindBodyType (RL.Cons label ty tail) encoding
39+
40+class UnwrapEncoding :: Type -> Type -> Constraint
41+class UnwrapEncoding encoding body | encoding -> body
42+
43+instance UnwrapEncoding (JSON ty) ty
44+instance UnwrapEncoding (FormData ty) ty
45+instance UnwrapEncoding NoBody Unit
46+
47+class FindHeaders :: RowList Type -> Row Type -> Constraint
48+class FindHeaders rl headers | rl -> headers
49+
50+instance FindHeaders RL.Nil ()
51+instance FindHeaders (RL.Cons "headers" (Record h) tail) h
52+else instance FindHeaders tail headers => FindHeaders (RL.Cons label ty tail) headers
src/Yoga/Fetch/Om/MakeRequest.pursnew file+90
@@ -0,0 +1,90 @@
1+module Yoga.Fetch.Om.MakeRequest
2+ ( class MakeRequest
3+ , httpMethod
4+ , makeRequest
5+ , class SerializeBody
6+ , serializeBody
7+ ) where
8+
9+import Prelude
10+
11+import Data.HTTP.Method (Method(..))
12+import Data.Maybe (Maybe(..))
13+import Data.Tuple.Nested ((/\))
14+import Effect.Aff (Aff)
15+import Effect.Class (liftEffect)
16+import JS.Fetch as Fetch
17+import JS.Fetch.Headers as Headers
18+import JS.Fetch.Request as Request
19+import JS.Fetch.RequestBody as Body
20+import JS.Fetch.RequestCache as Cache
21+import JS.Fetch.RequestCredentials as Credentials
22+import JS.Fetch.RequestMode as Mode
23+import JS.Fetch.ReferrerPolicy as ReferrerPolicy
24+import JS.Fetch.Referrer as Referrer
25+import JS.Fetch.Integrity (Integrity(..))
26+import JS.Fetch.Duplex as Duplex
27+import JS.Fetch.Response (Response) as FetchResponse
28+import Promise.Aff as Promise
29+import Type.Proxy (Proxy)
30+import Yoga.HTTP.API.Route.Method as Method
31+import Yoga.JSON (class WriteForeign, writeJSON)
32+
33+class MakeRequest :: Type -> Constraint
34+class MakeRequest method where
35+ httpMethod :: Proxy method -> Method
36+
37+instance MakeRequest Method.GET where
38+ httpMethod _ = GET
39+
40+instance MakeRequest Method.POST where
41+ httpMethod _ = POST
42+
43+instance MakeRequest Method.PUT where
44+ httpMethod _ = PUT
45+
46+instance MakeRequest Method.DELETE where
47+ httpMethod _ = DELETE
48+
49+instance MakeRequest Method.PATCH where
50+ httpMethod _ = PATCH
51+
52+makeRequest
53+ :: forall method
54+ . MakeRequest method
55+ => Proxy method
56+ -> String
57+ -> Maybe String
58+ -> Aff FetchResponse.Response
59+makeRequest proxy url maybeBody = do
60+ request <- Request.new url options # liftEffect
61+ Promise.toAffE $ Fetch.fetch request
62+ where
63+ method = httpMethod proxy
64+ headers = case maybeBody of
65+ Nothing -> Headers.empty
66+ Just _ -> Headers.fromFoldable [ "Content-Type" /\ "application/json" ]
67+ body = case maybeBody of
68+ Nothing -> Body.empty
69+ Just b -> Body.fromString b
70+ options =
71+ { method
72+ , headers
73+ , body
74+ , credentials: Credentials.Include
75+ , mode: Mode.Cors
76+ , referrer: Referrer.ReferrerClient
77+ , referrerPolicy: ReferrerPolicy.NoReferrer
78+ , integrity: Integrity ""
79+ , duplex: Duplex.Half
80+ , cache: Cache.Default
81+ }
82+
83+class SerializeBody :: Type -> Constraint
84+class SerializeBody body where
85+ serializeBody :: body -> Maybe String
86+
87+instance SerializeBody Unit where
88+ serializeBody _ = Nothing
89+else instance WriteForeign body => SerializeBody body where
90+ serializeBody b = Just (writeJSON b)
src/Yoga/Fetch/Om/ParseResponse.pursnew file+105
@@ -0,0 +1,105 @@
1+module Yoga.Fetch.Om.ParseResponse
2+ ( class ParseResponse
3+ , parseResponse
4+ , class ParseSuccessRL
5+ , parseSuccessRL
6+ , class ParseErrorRL
7+ , parseErrorRL
8+ ) where
9+
10+import Prelude
11+
12+import Control.Monad.Error.Class (throwError)
13+import Data.Maybe (Maybe(..))
14+import Data.Symbol (class IsSymbol)
15+import Data.Variant (Variant)
16+import Data.Variant as Variant
17+import Effect.Aff (Aff)
18+import Effect.Aff as Aff
19+import Effect.Exception as Exception
20+import JS.Fetch.Response (Response) as FetchResponse
21+import JS.Fetch.Response as Fetch
22+import Prim.Row as Row
23+import Prim.RowList (class RowToList, RowList)
24+import Prim.RowList as RL
25+import Promise.Aff as Promise
26+import Type.Proxy (Proxy(..))
27+import Yoga.HTTP.API.Route.StatusCode (class StatusCodeMap, statusCodeFor, StatusCode(..))
28+import Yoga.JSON (class ReadForeign, readJSON_)
29+import Yoga.Om (Om, fromAff)
30+import Yoga.Om.Error (Exception)
31+
32+class ParseResponse (errorRow :: Row Type) (successRow :: Row Type) where
33+ parseResponse :: FetchResponse.Response -> Om {} errorRow (Variant successRow)
34+
35+instance
36+ ( RowToList successRow successRL
37+ , RowToList errorRow errorRL
38+ , ParseSuccessRL successRL successRow
39+ , ParseErrorRL errorRL errorRow
40+ ) =>
41+ ParseResponse errorRow successRow where
42+ parseResponse fetchResp = do
43+ let status = Fetch.status fetchResp
44+ jsonText <- Promise.toAffE (Fetch.text fetchResp) # fromAff
45+ if status >= 200 && status < 300 then
46+ parseSuccessRL (Proxy :: _ successRL) status jsonText # fromAff
47+ else if status >= 400 then
48+ parseErrorRL (Proxy :: _ errorRL) (Proxy :: _ errorRow) status jsonText
49+ else
50+ throwError (Variant.inj (Proxy :: _ "exception") (Exception.error ("Unexpected HTTP status code: " <> show status)))
51+
52+class ParseSuccessRL (rl :: RowList Type) (successRow :: Row Type) | rl -> successRow where
53+ parseSuccessRL :: Proxy rl -> Int -> String -> Aff (Variant successRow)
54+
55+instance ParseSuccessRL RL.Nil successRow where
56+ parseSuccessRL _ status _ =
57+ Aff.throwError (Exception.error ("Unexpected success status code: " <> show status))
58+
59+else instance
60+ ( IsSymbol label
61+ , StatusCodeMap label
62+ , ReadForeign body
63+ , Row.Cons label body tailRow successRow
64+ , Row.Lacks label tailRow
65+ , ParseSuccessRL tailRL successRow
66+ ) =>
67+ ParseSuccessRL (RL.Cons label body tailRL) successRow where
68+ parseSuccessRL _ actualStatus jsonText = do
69+ let StatusCode expected = statusCodeFor (Proxy :: _ label)
70+ if actualStatus == expected then case readJSON_ jsonText of
71+ Nothing -> Aff.throwError (Exception.error "JSON parse error")
72+ Just parsed -> pure (Variant.inj (Proxy :: _ label) parsed)
73+ else
74+ parseSuccessRL (Proxy :: _ tailRL) actualStatus jsonText
75+
76+class ParseErrorRL (rl :: RowList Type) (errorRow :: Row Type) | rl -> errorRow where
77+ parseErrorRL
78+ :: forall successRow
79+ . Proxy rl
80+ -> Proxy errorRow
81+ -> Int
82+ -> String
83+ -> Om {} errorRow (Variant successRow)
84+
85+instance ParseErrorRL RL.Nil errorRow where
86+ parseErrorRL _ _ status _ =
87+ throwError (Variant.inj (Proxy :: _ "exception") (Exception.error ("Unexpected error status code: " <> show status)))
88+
89+else instance
90+ ( IsSymbol label
91+ , StatusCodeMap label
92+ , ReadForeign body
93+ , Row.Cons label body tailRow errorRow
94+ , Row.Lacks label tailRow
95+ , Row.Cons label body tailExc (Exception errorRow)
96+ , ParseErrorRL tailRL errorRow
97+ ) =>
98+ ParseErrorRL (RL.Cons label body tailRL) errorRow where
99+ parseErrorRL _ errorProxy actualStatus jsonText = do
100+ let StatusCode expected = statusCodeFor (Proxy :: _ label)
101+ if actualStatus == expected then case readJSON_ jsonText of
102+ Nothing -> throwError (Variant.inj (Proxy :: _ "exception") (Exception.error "JSON parse error"))
103+ Just parsed -> throwError (Variant.inj (Proxy :: _ label) parsed)
104+ else
105+ parseErrorRL (Proxy :: _ tailRL) errorProxy actualStatus jsonText
src/Yoga/Fetch/Om/Simple.pursnew file+244
@@ -0,0 +1,244 @@
1+module Yoga.Fetch.Om.Simple
2+ ( class DecodeResponse
3+ , decodeResponse
4+ , FetchError
5+ , FetchResponse
6+ , get
7+ , getWithHeaders
8+ , delete
9+ , deleteWithHeaders
10+ , delete_
11+ , post
12+ , postWithHeaders
13+ , post_
14+ , put
15+ , putWithHeaders
16+ , put_
17+ , patch
18+ , patchWithHeaders
19+ , patch_
20+ ) where
21+
22+import Prelude
23+
24+import Data.HTTP.Method (Method(..))
25+import Data.Maybe (Maybe(..))
26+import Data.Tuple.Nested ((/\))
27+import Effect.Class (liftEffect)
28+import JS.Fetch as Fetch
29+import JS.Fetch.Duplex as Duplex
30+import JS.Fetch.Headers (Headers)
31+import JS.Fetch.Headers as Headers
32+import JS.Fetch.Integrity (Integrity(..))
33+import JS.Fetch.Referrer as Referrer
34+import JS.Fetch.ReferrerPolicy as ReferrerPolicy
35+import JS.Fetch.Request as Request
36+import JS.Fetch.RequestBody as Body
37+import JS.Fetch.RequestCache as Cache
38+import JS.Fetch.RequestCredentials as Credentials
39+import JS.Fetch.RequestMode as Mode
40+import JS.Fetch.Response as Resp
41+import Promise.Aff as Promise
42+import Type.Row.Homogeneous (class Homogeneous)
43+import Yoga.HTTP.API.Route.Encoding (PlainText)
44+import Yoga.JSON (class ReadForeign, class WriteForeign, readJSON_, writeJSON)
45+import Yoga.Om (Om, fromAff, throw)
46+
47+type FetchError = { status :: Int, body :: String }
48+
49+type FetchResponse a = { headers :: Headers, body :: a }
50+
51+class DecodeResponse :: forall k. k -> Type -> Constraint
52+class DecodeResponse a result | a -> result where
53+ decodeResponse :: String -> Maybe result
54+
55+instance DecodeResponse (PlainText String) String where
56+ decodeResponse = Just
57+
58+else instance ReadForeign a => DecodeResponse a a where
59+ decodeResponse = readJSON_
60+
61+simpleFetch
62+ :: forall @a result headers ctx err
63+ . DecodeResponse a result
64+ => Homogeneous headers String
65+ => Method
66+ -> Record headers
67+ -> String
68+ -> Maybe String
69+ -> Om ctx (fetchError :: FetchError | err) (FetchResponse result)
70+simpleFetch method headers url maybeBody = do
71+ resp <- doFetch # fromAff
72+ respText <- Promise.toAffE (Resp.text resp) # fromAff
73+ let status = Resp.status resp
74+ let respHeaders = Resp.headers resp
75+ if status >= 200 && status < 300 then
76+ case decodeResponse @a respText of
77+ Nothing -> throw { fetchError: { status, body: respText } }
78+ Just body -> pure { headers: respHeaders, body }
79+ else
80+ throw { fetchError: { status, body: respText } }
81+ where
82+ doFetch = do
83+ request <- Request.new url options # liftEffect
84+ Promise.toAffE $ Fetch.fetch request
85+ contentTypeHeaders = case maybeBody of
86+ Nothing -> Headers.empty
87+ Just _ -> Headers.fromFoldable [ "Content-Type" /\ "application/json" ]
88+ allHeaders = Headers.fromFoldable
89+ (Headers.toArray contentTypeHeaders <> Headers.toArray (Headers.fromRecord headers))
90+ reqBody = case maybeBody of
91+ Nothing -> Body.empty
92+ Just b -> Body.fromString b
93+ options =
94+ { method
95+ , headers: allHeaders
96+ , body: reqBody
97+ , credentials: Credentials.Include
98+ , mode: Mode.Cors
99+ , referrer: Referrer.ReferrerClient
100+ , referrerPolicy: ReferrerPolicy.NoReferrer
101+ , integrity: Integrity ""
102+ , duplex: Duplex.Half
103+ , cache: Cache.Default
104+ }
105+
106+get
107+ :: forall @a result headers ctx err
108+ . DecodeResponse a result
109+ => Homogeneous headers String
110+ => Record headers
111+ -> String
112+ -> Om ctx (fetchError :: FetchError | err) result
113+get headers url = _.body <$> simpleFetch @a GET headers url Nothing
114+
115+getWithHeaders
116+ :: forall @a result headers ctx err
117+ . DecodeResponse a result
118+ => Homogeneous headers String
119+ => Record headers
120+ -> String
121+ -> Om ctx (fetchError :: FetchError | err) (FetchResponse result)
122+getWithHeaders headers url = simpleFetch @a GET headers url Nothing
123+
124+delete
125+ :: forall @a result headers ctx err
126+ . DecodeResponse a result
127+ => Homogeneous headers String
128+ => Record headers
129+ -> String
130+ -> Om ctx (fetchError :: FetchError | err) result
131+delete headers url = _.body <$> simpleFetch @a DELETE headers url Nothing
132+
133+deleteWithHeaders
134+ :: forall @a result headers ctx err
135+ . DecodeResponse a result
136+ => Homogeneous headers String
137+ => Record headers
138+ -> String
139+ -> Om ctx (fetchError :: FetchError | err) (FetchResponse result)
140+deleteWithHeaders headers url = simpleFetch @a DELETE headers url Nothing
141+
142+delete_
143+ :: forall headers ctx err
144+ . Homogeneous headers String
145+ => Record headers
146+ -> String
147+ -> Om ctx (fetchError :: FetchError | err) Unit
148+delete_ headers url = void $ simpleFetch @(PlainText String) DELETE headers url Nothing
149+
150+post
151+ :: forall @a result headers body ctx err
152+ . DecodeResponse a result
153+ => Homogeneous headers String
154+ => WriteForeign body
155+ => Record headers
156+ -> String
157+ -> body
158+ -> Om ctx (fetchError :: FetchError | err) result
159+post headers url body = _.body <$> simpleFetch @a POST headers url (Just (writeJSON body))
160+
161+postWithHeaders
162+ :: forall @a result headers body ctx err
163+ . DecodeResponse a result
164+ => Homogeneous headers String
165+ => WriteForeign body
166+ => Record headers
167+ -> String
168+ -> body
169+ -> Om ctx (fetchError :: FetchError | err) (FetchResponse result)
170+postWithHeaders headers url body = simpleFetch @a POST headers url (Just (writeJSON body))
171+
172+post_
173+ :: forall headers body ctx err
174+ . Homogeneous headers String
175+ => WriteForeign body
176+ => Record headers
177+ -> String
178+ -> body
179+ -> Om ctx (fetchError :: FetchError | err) Unit
180+post_ headers url body = void $ simpleFetch @(PlainText String) POST headers url (Just (writeJSON body))
181+
182+put
183+ :: forall @a result headers body ctx err
184+ . DecodeResponse a result
185+ => Homogeneous headers String
186+ => WriteForeign body
187+ => Record headers
188+ -> String
189+ -> body
190+ -> Om ctx (fetchError :: FetchError | err) result
191+put headers url body = _.body <$> simpleFetch @a PUT headers url (Just (writeJSON body))
192+
193+putWithHeaders
194+ :: forall @a result headers body ctx err
195+ . DecodeResponse a result
196+ => Homogeneous headers String
197+ => WriteForeign body
198+ => Record headers
199+ -> String
200+ -> body
201+ -> Om ctx (fetchError :: FetchError | err) (FetchResponse result)
202+putWithHeaders headers url body = simpleFetch @a PUT headers url (Just (writeJSON body))
203+
204+put_
205+ :: forall headers body ctx err
206+ . Homogeneous headers String
207+ => WriteForeign body
208+ => Record headers
209+ -> String
210+ -> body
211+ -> Om ctx (fetchError :: FetchError | err) Unit
212+put_ headers url body = void $ simpleFetch @(PlainText String) PUT headers url (Just (writeJSON body))
213+
214+patch
215+ :: forall @a result headers body ctx err
216+ . DecodeResponse a result
217+ => Homogeneous headers String
218+ => WriteForeign body
219+ => Record headers
220+ -> String
221+ -> body
222+ -> Om ctx (fetchError :: FetchError | err) result
223+patch headers url body = _.body <$> simpleFetch @a PATCH headers url (Just (writeJSON body))
224+
225+patchWithHeaders
226+ :: forall @a result headers body ctx err
227+ . DecodeResponse a result
228+ => Homogeneous headers String
229+ => WriteForeign body
230+ => Record headers
231+ -> String
232+ -> body
233+ -> Om ctx (fetchError :: FetchError | err) (FetchResponse result)
234+patchWithHeaders headers url body = simpleFetch @a PATCH headers url (Just (writeJSON body))
235+
236+patch_
237+ :: forall headers body ctx err
238+ . Homogeneous headers String
239+ => WriteForeign body
240+ => Record headers
241+ -> String
242+ -> body
243+ -> Om ctx (fetchError :: FetchError | err) Unit
244+patch_ headers url body = void $ simpleFetch @(PlainText String) PATCH headers url (Just (writeJSON body))
src/Yoga/Fetch/Om/SplitParams.pursnew file+60
@@ -0,0 +1,60 @@
1+module Yoga.Fetch.Om.SplitParams
2+ ( class SplitParams
3+ , splitParams
4+ , class ExtractSubRecord
5+ , extractSubRecord
6+ ) where
7+
8+import Data.Symbol (class IsSymbol)
9+import Prim.Row as Row
10+import Prim.RowList (class RowToList, RowList)
11+import Prim.RowList as RL
12+import Record as Record
13+import Type.Proxy (Proxy(..))
14+
15+class SplitParams :: Row Type -> Row Type -> Row Type -> Row Type -> Constraint
16+class
17+ SplitParams allParams pathParams queryParams bodyParams
18+ | allParams -> pathParams queryParams bodyParams where
19+ splitParams
20+ :: Record allParams
21+ -> { path :: Record pathParams
22+ , query :: Record queryParams
23+ , body :: Record bodyParams
24+ }
25+
26+instance
27+ ( RowToList pathParams pathRL
28+ , RowToList queryParams queryRL
29+ , RowToList bodyParams bodyRL
30+ , ExtractSubRecord pathRL allParams pathParams
31+ , ExtractSubRecord queryRL allParams queryParams
32+ , ExtractSubRecord bodyRL allParams bodyParams
33+ ) =>
34+ SplitParams allParams pathParams queryParams bodyParams where
35+ splitParams allParamsRec =
36+ { path: extractSubRecord (Proxy :: _ pathRL) allParamsRec
37+ , query: extractSubRecord (Proxy :: _ queryRL) allParamsRec
38+ , body: extractSubRecord (Proxy :: _ bodyRL) allParamsRec
39+ }
40+
41+class ExtractSubRecord :: RowList Type -> Row Type -> Row Type -> Constraint
42+class ExtractSubRecord rl source target | rl -> target where
43+ extractSubRecord :: Proxy rl -> Record source -> Record target
44+
45+instance ExtractSubRecord RL.Nil source () where
46+ extractSubRecord _ _ = {}
47+
48+instance
49+ ( IsSymbol label
50+ , Row.Cons label ty sourceTail source
51+ , Row.Cons label ty targetTail target
52+ , Row.Lacks label targetTail
53+ , ExtractSubRecord tail source targetTail
54+ ) =>
55+ ExtractSubRecord (RL.Cons label ty tail) source target where
56+ extractSubRecord _ sourceRec = Record.insert labelProxy value rest
57+ where
58+ labelProxy = Proxy :: _ label
59+ value = Record.get labelProxy sourceRec
60+ rest = extractSubRecord (Proxy :: _ tail) sourceRec
src/Yoga/Fetch/Om/SplitResponses.pursnew file+83
@@ -0,0 +1,83 @@
1+module Yoga.Fetch.Om.SplitResponses
2+ ( class IsSuccessStatus
3+ , class ExtractBody
4+ , class SplitResponses
5+ , class SplitResponsesRL
6+ , class DispatchResponse
7+ ) where
8+
9+import Prim.Boolean (True, False)
10+import Prim.Row as Row
11+import Prim.RowList (RowList)
12+import Prim.RowList as RL
13+import Yoga.HTTP.API.Route (Response)
14+
15+class IsSuccessStatus (label :: Symbol) (isSuccess :: Boolean) | label -> isSuccess
16+
17+instance IsSuccessStatus "ok" True
18+else instance IsSuccessStatus "created" True
19+else instance IsSuccessStatus "accepted" True
20+else instance IsSuccessStatus "nonAuthoritativeInformation" True
21+else instance IsSuccessStatus "noContent" True
22+else instance IsSuccessStatus "resetContent" True
23+else instance IsSuccessStatus "partialContent" True
24+else instance IsSuccessStatus "multiStatus" True
25+else instance IsSuccessStatus "alreadyReported" True
26+else instance IsSuccessStatus "imUsed" True
27+else instance IsSuccessStatus label False
28+
29+class ExtractBody (responseType :: Type) (bodyType :: Type) | responseType -> bodyType
30+
31+instance ExtractBody (Response headers body) body
32+
33+else instance
34+ ( Row.Cons "body" body rest row
35+ ) =>
36+ ExtractBody (Record row) body
37+
38+class
39+ SplitResponses (allRow :: Row Type) (successRow :: Row Type) (errorRow :: Row Type)
40+ | allRow -> successRow errorRow
41+
42+instance
43+ ( RL.RowToList allRow rl
44+ , SplitResponsesRL rl successRow errorRow
45+ ) =>
46+ SplitResponses allRow successRow errorRow
47+
48+class
49+ SplitResponsesRL (rl :: RowList Type) (successRow :: Row Type) (errorRow :: Row Type)
50+ | rl -> successRow errorRow
51+
52+instance SplitResponsesRL RL.Nil () ()
53+
54+else instance
55+ ( IsSuccessStatus label isSuccess
56+ , ExtractBody ty body
57+ , SplitResponsesRL tail successTail errorTail
58+ , DispatchResponse isSuccess label body successTail errorTail successRow errorRow
59+ ) =>
60+ SplitResponsesRL (RL.Cons label ty tail) successRow errorRow
61+
62+class
63+ DispatchResponse
64+ (isSuccess :: Boolean)
65+ (label :: Symbol)
66+ (body :: Type)
67+ (successTail :: Row Type)
68+ (errorTail :: Row Type)
69+ (successRow :: Row Type)
70+ (errorRow :: Row Type)
71+ | isSuccess label body successTail errorTail -> successRow errorRow
72+
73+instance
74+ ( Row.Cons label body successTail successRow
75+ , Row.Lacks label successTail
76+ ) =>
77+ DispatchResponse True label body successTail errorTail successRow errorTail
78+
79+instance
80+ ( Row.Cons label body errorTail errorRow
81+ , Row.Lacks label errorTail
82+ ) =>
83+ DispatchResponse False label body successTail errorTail successTail errorRow
src/Yoga/Fetch/Om/Variant.pursnew file+48
@@ -0,0 +1,48 @@
1+module Yoga.Fetch.Om.Variant
2+ ( class OnlyVariant
3+ , onlyImpl
4+ , only
5+ , class VariantOrValue
6+ , variantOrValue
7+ ) where
8+
9+import Prelude
10+
11+import Data.Symbol (class IsSymbol)
12+import Data.Variant (Variant)
13+import Data.Variant as Variant
14+import Prim.Row as Row
15+import Prim.RowList (class RowToList, RowList)
16+import Prim.RowList as RL
17+import Type.Proxy (Proxy(..))
18+
19+class OnlyVariant :: RowList Type -> Row Type -> Type -> Constraint
20+class OnlyVariant rl row a | rl -> row a where
21+ onlyImpl :: Proxy rl -> Variant row -> a
22+
23+instance
24+ ( IsSymbol label
25+ , Row.Cons label a () row
26+ ) =>
27+ OnlyVariant (RL.Cons label a RL.Nil) row a where
28+ onlyImpl _ variant = Variant.on (Proxy :: _ label) identity Variant.case_ variant
29+
30+only :: forall row rl a. RowToList row rl => OnlyVariant rl row a => Variant row -> a
31+only = onlyImpl (Proxy :: _ rl)
32+
33+-- Typeclass to determine return type: unwrapped value for single option, Variant for multiple
34+class VariantOrValue :: RowList Type -> Row Type -> Type -> Constraint
35+class VariantOrValue rl row result | rl row -> result where
36+ variantOrValue :: Proxy rl -> Variant row -> result
37+
38+-- Single element: return the unwrapped value
39+instance
40+ ( IsSymbol label
41+ , Row.Cons label a () row
42+ ) =>
43+ VariantOrValue (RL.Cons label a RL.Nil) row a where
44+ variantOrValue _ variant = Variant.on (Proxy :: _ label) identity Variant.case_ variant
45+
46+-- Multiple elements: return the Variant as-is
47+instance VariantOrValue (RL.Cons label a (RL.Cons label2 a2 tail)) row (Variant row) where
48+ variantOrValue _ variant = variant
test/BuildUrl.Spec.pursnew file+113
@@ -0,0 +1,113 @@
1+module BuildUrl.Spec where
2+
3+import Prelude
4+
5+import Data.Maybe (Maybe(..))
6+import Effect (Effect)
7+import Effect.Aff (Aff)
8+import Type.Proxy (Proxy(..))
9+import Yoga.Fetch.Om.BuildUrl (buildUrl, substitutePathParams, appendQueryParams)
10+import Yoga.HTTP.API.Path (Path, type (/), type (:))
11+import ViTest (ViTest, describe, test)
12+import ViTest.Expect (expectToBe)
13+
14+expectToEqual :: forall a. Eq a => a -> a -> Aff Unit
15+expectToEqual expected actual = expectToBe true (expected == actual)
16+
17+--------------------------------------------------------------------------------
18+-- SubstitutePathParams Tests
19+--------------------------------------------------------------------------------
20+
21+testSubstitutePathParams :: Effect ViTest
22+testSubstitutePathParams = describe "substitutePathParams" $ do
23+ _ <- test "substitutes single path param" do
24+ let
25+ result = substitutePathParams (Proxy :: _ (id :: Int)) { id: 42 } "/users/:id"
26+ expectToEqual "/users/42" result
27+
28+ _ <- test "substitutes multiple path params" do
29+ let
30+ result = substitutePathParams (Proxy :: _ (userId :: Int, postId :: Int))
31+ { userId: 1, postId: 99 }
32+ "/users/:userId/posts/:postId"
33+ expectToEqual "/users/1/posts/99" result
34+
35+ _ <- test "handles String params" do
36+ let
37+ result = substitutePathParams (Proxy :: _ (slug :: String))
38+ { slug: "hello-world" }
39+ "/posts/:slug"
40+ expectToEqual "/posts/hello-world" result
41+
42+ test "handles empty params" do
43+ let
44+ result = substitutePathParams (Proxy :: _ ()) {} "/users"
45+ expectToEqual "/users" result
46+
47+--------------------------------------------------------------------------------
48+-- AppendQueryParams Tests
49+--------------------------------------------------------------------------------
50+
51+testAppendQueryParams :: Effect ViTest
52+testAppendQueryParams = describe "appendQueryParams" $ do
53+ _ <- test "appends single query param" do
54+ let
55+ result = appendQueryParams (Proxy :: _ (limit :: Int)) { limit: 10 } "/users"
56+ expectToEqual "/users?limit=10" result
57+
58+ _ <- test "appends multiple query params" do
59+ let
60+ result = appendQueryParams (Proxy :: _ (limit :: Int, offset :: Int))
61+ { limit: 10, offset: 20 }
62+ "/users"
63+ expectToEqual "/users?limit=10&offset=20" result
64+
65+ _ <- test "handles Maybe params - Just" do
66+ let
67+ result = appendQueryParams (Proxy :: _ (limit :: Maybe Int))
68+ { limit: Just 10 }
69+ "/users"
70+ expectToEqual "/users?limit=10" result
71+
72+ _ <- test "handles Maybe params - Nothing" do
73+ let
74+ result = appendQueryParams (Proxy :: _ (limit :: Maybe Int))
75+ { limit: Nothing }
76+ "/users"
77+ expectToEqual "/users" result
78+
79+ _ <- test "handles mixed Maybe and required params" do
80+ let
81+ result = appendQueryParams (Proxy :: _ (limit :: Maybe Int, offset :: Int))
82+ { limit: Nothing, offset: 20 }
83+ "/users"
84+ expectToEqual "/users?offset=20" result
85+
86+ test "handles empty query params" do
87+ let
88+ result = appendQueryParams (Proxy :: _ ()) {} "/users"
89+ expectToEqual "/users" result
90+
91+--------------------------------------------------------------------------------
92+-- BuildUrl Tests
93+--------------------------------------------------------------------------------
94+
95+testBuildUrl :: Effect ViTest
96+testBuildUrl = describe "buildUrl" $ do
97+ _ <- test "builds URL with path params only" do
98+ let
99+ result = buildUrl
100+ "https://api.example.com"
101+ (Proxy :: _ (Path ("users" / "id" : Int)))
102+ { id: 42 }
103+ {}
104+ expectToEqual "https://api.example.com/users/42" result
105+
106+ test "builds URL with query params only" do
107+ let
108+ result = buildUrl
109+ "https://api.example.com"
110+ (Proxy :: _ (Path "users"))
111+ {}
112+ { limit: 10, offset: 20 }
113+ expectToEqual "https://api.example.com/users?limit=10&offset=20" result
test/Complete.Example.pursnew file+118
@@ -0,0 +1,118 @@
1+module Complete.Example where
2+
3+import Prelude
4+
5+import Data.Array (length)
6+import Data.Maybe (Maybe(..))
7+import Effect.Class.Console (log)
8+import Type.Proxy (Proxy(..))
9+import Yoga.Fetch.Om (GET, POST, PUT, DELETE, Route, JSON, Path, type (/), type (:), type (:?), deriveClient)
10+import Yoga.Om (Om, handleErrors)
11+
12+type User =
13+ { id :: Int
14+ , name :: String
15+ , email :: String
16+ }
17+
18+type CreateUserRequest =
19+ { name :: String
20+ , email :: String
21+ }
22+
23+type UpdateUserRequest =
24+ { name :: String
25+ , email :: String
26+ }
27+
28+type ErrorMessage =
29+ { error :: String
30+ }
31+
32+type UserAPI =
33+ { getUser ::
34+ Route GET
35+ (Path ("users" / "id" : Int))
36+ {}
37+ ( ok :: { body :: User }
38+ , notFound :: { body :: ErrorMessage }
39+ )
40+ , listUsers ::
41+ Route GET
42+ (Path "users" :? { limit :: Int, offset :: Int })
43+ {}
44+ ( ok :: { body :: Array User }
45+ )
46+ , createUser ::
47+ Route POST
48+ (Path "users")
49+ { body :: JSON CreateUserRequest }
50+ ( created :: { body :: User }
51+ , badRequest :: { body :: ErrorMessage }
52+ )
53+ , updateUser ::
54+ Route PUT
55+ (Path ("users" / "id" : Int))
56+ { body :: JSON UpdateUserRequest }
57+ ( ok :: { body :: User }
58+ , notFound :: { body :: ErrorMessage }
59+ , badRequest :: { body :: ErrorMessage }
60+ )
61+ , deleteUser ::
62+ Route DELETE
63+ (Path ("users" / "id" : Int))
64+ {}
65+ ( noContent :: { body :: {} }
66+ , notFound :: { body :: ErrorMessage }
67+ )
68+ }
69+
70+api
71+ :: { createUser :: CreateUserRequest -> Om {} (badRequest :: ErrorMessage) User
72+ , deleteUser :: { id :: Int } -> Om {} (notFound :: ErrorMessage) {}
73+ , getUser :: { id :: Int } -> Om {} (notFound :: ErrorMessage) User
74+ , listUsers :: { limit :: Maybe Int, offset :: Maybe Int } -> Om {} () (Array User)
75+ , updateUser :: { id :: Int } -> UpdateUserRequest -> Om {} (badRequest :: ErrorMessage, notFound :: ErrorMessage) User
76+ }
77+api = deriveClient "https://api.example.com" (Proxy :: Proxy UserAPI)
78+
79+exampleGetUser :: Om {} (notFound :: ErrorMessage) Unit
80+exampleGetUser = do
81+ user <- api.getUser { id: 42 }
82+ log $ "Found user: " <> user.name <> " (" <> user.email <> ")"
83+
84+exampleGetUserHandled :: Om {} () Unit
85+exampleGetUserHandled = do
86+ user <- api.getUser { id: 42 }
87+ # handleErrors
88+ { notFound: \err -> do
89+ log $ "User not found: " <> err.error
90+ pure { id: 0, name: "unknown", email: "" }
91+ }
92+ log $ "Found user: " <> user.name
93+
94+exampleListUsers :: Om {} () Unit
95+exampleListUsers = do
96+ users <- api.listUsers { limit: Just 10, offset: Just 0 }
97+ log $ "Found " <> show (length users) <> " users"
98+
99+exampleCreateUser :: Om {} (badRequest :: ErrorMessage) Unit
100+exampleCreateUser = do
101+ user <- api.createUser
102+ { name: "Alice"
103+ , email: "alice@example.com"
104+ }
105+ log $ "Created user with ID: " <> show user.id
106+
107+exampleUpdateUser :: Om {} (notFound :: ErrorMessage, badRequest :: ErrorMessage) Unit
108+exampleUpdateUser = do
109+ user <- api.updateUser { id: 42 }
110+ { name: "Alice Updated"
111+ , email: "alice.new@example.com"
112+ }
113+ log $ "Updated user: " <> user.name
114+
115+exampleDeleteUser :: Om {} (notFound :: ErrorMessage) Unit
116+exampleDeleteUser = do
117+ _ <- api.deleteUser { id: 42 }
118+ log "User deleted successfully"
test/Example.pursnew file+39
@@ -0,0 +1,39 @@
1+module Example where
2+
3+import Yoga.HTTP.API.Route (GET, POST, DELETE, Route)
4+import Yoga.HTTP.API.Route.Encoding (JSON)
5+import Yoga.HTTP.API.Path (Path, type (/), type (:), type (:?))
6+
7+type User = { id :: Int, name :: String, email :: String }
8+type CreateUserRequest = { name :: String, email :: String }
9+type ErrorMessage = { error :: String }
10+
11+type UserAPI =
12+ { getUser ::
13+ Route GET
14+ (Path ("users" / "id" : Int))
15+ {}
16+ ( ok :: { body :: User }
17+ , notFound :: { body :: ErrorMessage }
18+ )
19+ , listUsers ::
20+ Route GET
21+ (Path "users" :? { limit :: Int, offset :: Int })
22+ {}
23+ ( ok :: { body :: Array User }
24+ )
25+ , createUser ::
26+ Route POST
27+ (Path "users")
28+ { body :: JSON CreateUserRequest }
29+ ( created :: { body :: User }
30+ , badRequest :: { body :: ErrorMessage }
31+ )
32+ , deleteUser ::
33+ Route DELETE
34+ (Path ("users" / "id" : Int))
35+ {}
36+ ( noContent :: { body :: {} }
37+ , notFound :: { body :: ErrorMessage }
38+ )
39+ }
test/Simple.Spec.pursnew file+69
@@ -0,0 +1,69 @@
1+module Simple.Spec where
2+
3+import Prelude
4+
5+import Effect.Class.Console (log)
6+import Yoga.Fetch.Om.Simple (FetchError, FetchResponse, get, getWithHeaders, post, post_, put, put_, patch, patch_, delete, delete_)
7+import Yoga.HTTP.API.Route.Encoding (PlainText)
8+import Yoga.Om (Om, handleErrors)
9+
10+type User = { id :: Int, name :: String, email :: String }
11+type CreateUser = { name :: String, email :: String }
12+
13+exampleGet :: forall ctx err. Om ctx (fetchError :: FetchError | err) User
14+exampleGet = get @User {} "https://api.example.com/users/42"
15+
16+exampleGetWithAuth :: forall ctx err. Om ctx (fetchError :: FetchError | err) User
17+exampleGetWithAuth = get @User { authorization: "Bearer abc123" } "https://api.example.com/users/42"
18+
19+exampleGetPlainText :: forall ctx err. Om ctx (fetchError :: FetchError | err) String
20+exampleGetPlainText = get @(PlainText String) {} "https://api.example.com/health"
21+
22+exampleGetWithHeaders :: forall ctx err. Om ctx (fetchError :: FetchError | err) (FetchResponse User)
23+exampleGetWithHeaders = do
24+ { headers, body: user } <- getWithHeaders @User {} "https://api.example.com/users/42"
25+ log $ "Got user: " <> user.name
26+ pure { headers, body: user }
27+
28+examplePost :: forall ctx err. Om ctx (fetchError :: FetchError | err) User
29+examplePost = post @User {} "https://api.example.com/users" { name: "Alice", email: "alice@example.com" }
30+
31+examplePostFireAndForget :: forall ctx err. Om ctx (fetchError :: FetchError | err) Unit
32+examplePostFireAndForget = post_ {} "https://api.example.com/users" { name: "Alice", email: "alice@example.com" }
33+
34+examplePut :: forall ctx err. Om ctx (fetchError :: FetchError | err) User
35+examplePut = put @User {} "https://api.example.com/users/42" { name: "Bob", email: "bob@example.com" }
36+
37+examplePut_ :: forall ctx err. Om ctx (fetchError :: FetchError | err) Unit
38+examplePut_ = put_ {} "https://api.example.com/users/42" { name: "Bob", email: "bob@example.com" }
39+
40+examplePatch :: forall ctx err. Om ctx (fetchError :: FetchError | err) User
41+examplePatch = patch @User {} "https://api.example.com/users/42" { name: "Bob" }
42+
43+examplePatch_ :: forall ctx err. Om ctx (fetchError :: FetchError | err) Unit
44+examplePatch_ = patch_ {} "https://api.example.com/users/42" { name: "Bob" }
45+
46+exampleDelete :: forall ctx err. Om ctx (fetchError :: FetchError | err) User
47+exampleDelete = delete @User {} "https://api.example.com/users/42"
48+
49+exampleDelete_ :: forall ctx err. Om ctx (fetchError :: FetchError | err) Unit
50+exampleDelete_ = delete_ {} "https://api.example.com/users/42"
51+
52+exampleHandleError :: forall ctx. Om ctx () Unit
53+exampleHandleError = do
54+ user <- get @User {} "https://api.example.com/users/42"
55+ # handleErrors { fetchError: \_ -> pure { id: 0, name: "unknown", email: "" } }
56+ log $ "Got user: " <> user.name
57+
58+exampleComposition :: forall ctx. Om ctx () Unit
59+exampleComposition = do
60+ user <- post @User {} "https://api.example.com/users" { name: "Alice", email: "alice@example.com" }
61+ # handleErrors
62+ { fetchError: \e -> do
63+ log $ "Failed with status " <> show e.status <> ": " <> e.body
64+ pure { id: 0, name: "", email: "" }
65+ }
66+ _ <- put @User {} ("https://api.example.com/users/" <> show user.id) { name: "Alice Updated", email: user.email }
67+ # handleErrors { fetchError: \_ -> pure user }
68+ delete_ {} ("https://api.example.com/users/" <> show user.id)
69+ # handleErrors { fetchError: \_ -> pure unit }
test/SplitParams.Spec.pursnew file+76
@@ -0,0 +1,76 @@
1+module SplitParams.Spec where
2+
3+import Prelude
4+
5+import Effect (Effect)
6+import Effect.Aff (Aff)
7+import Yoga.Fetch.Om.SplitParams (splitParams)
8+import ViTest (ViTest, describe, test)
9+import ViTest.Expect (expectToBe)
10+
11+expectToEqual :: forall a. Eq a => a -> a -> Aff Unit
12+expectToEqual expected actual = expectToBe true (expected == actual)
13+
14+--------------------------------------------------------------------------------
15+-- SplitParams Tests
16+--------------------------------------------------------------------------------
17+
18+testSplitParams :: Effect ViTest
19+testSplitParams = describe "splitParams" $ do
20+ _ <- test "splits path and query params" do
21+ let
22+ allParams = { id: 42, limit: 10, offset: 20 }
23+ result =
24+ splitParams allParams
25+ :: { path :: { id :: Int }
26+ , query :: { limit :: Int, offset :: Int }
27+ , body :: {}
28+ }
29+ expectToEqual 42 result.path.id
30+ expectToEqual 10 result.query.limit
31+ expectToEqual 20 result.query.offset
32+ expectToEqual {} result.body
33+
34+ _ <- test "splits path and body params" do
35+ let
36+ allParams = { id: 42, name: "Alice", email: "alice@example.com" }
37+ result =
38+ splitParams allParams
39+ :: { path :: { id :: Int }
40+ , query :: {}
41+ , body :: { name :: String, email :: String }
42+ }
43+ expectToEqual 42 result.path.id
44+ expectToEqual {} result.query
45+ expectToEqual "Alice" result.body.name
46+ expectToEqual "alice@example.com" result.body.email
47+
48+ _ <- test "splits all three param types" do
49+ let
50+ allParams =
51+ { id: 42
52+ , limit: 10
53+ , name: "Bob"
54+ }
55+ result =
56+ splitParams allParams
57+ :: { path :: { id :: Int }
58+ , query :: { limit :: Int }
59+ , body :: { name :: String }
60+ }
61+ expectToEqual 42 result.path.id
62+ expectToEqual 10 result.query.limit
63+ expectToEqual "Bob" result.body.name
64+
65+ test "handles empty splits" do
66+ let
67+ allParams = { id: 42 }
68+ result =
69+ splitParams allParams
70+ :: { path :: { id :: Int }
71+ , query :: {}
72+ , body :: {}
73+ }
74+ expectToEqual 42 result.path.id
75+ expectToEqual {} result.query
76+ expectToEqual {} result.body
test/Test.Main.pursnew file+20
@@ -0,0 +1,20 @@
1+module Test.Main where
2+
3+import Prelude
4+
5+import BuildUrl.Spec as BuildUrl
6+import SplitParams.Spec as SplitParams
7+import Variant.Spec as Variant
8+import Effect (Effect)
9+import ViTest (ViTest, viTest)
10+
11+spec :: Effect ViTest
12+spec = do
13+ _ <- BuildUrl.testSubstitutePathParams
14+ _ <- BuildUrl.testAppendQueryParams
15+ _ <- BuildUrl.testBuildUrl
16+ _ <- SplitParams.testSplitParams
17+ Variant.testOnly
18+
19+main :: ViTest
20+main = viTest spec
test/Variant.Spec.pursnew file+24
@@ -0,0 +1,24 @@
1+module Variant.Spec where
2+
3+import Prelude
4+
5+import Data.Variant (Variant)
6+import Data.Variant as Variant
7+import Effect (Effect)
8+import Effect.Aff (Aff)
9+import Type.Proxy (Proxy(..))
10+import ViTest (ViTest, describe, test)
11+import ViTest.Expect (expectToBe)
12+import Yoga.Fetch.Om.Variant (only)
13+
14+expectToEqual :: forall a. Eq a => a -> a -> Aff Unit
15+expectToEqual expected actual = expectToBe true (expected == actual)
16+
17+testOnly :: Effect ViTest
18+testOnly = describe "only" do
19+ test "extracts single variant value - record" do
20+ let
21+ variant :: Variant (ok :: { id :: Int, name :: String })
22+ variant = Variant.inj (Proxy :: _ "ok") { id: 42, name: "Alice" }
23+ result = only variant
24+ expectToEqual { id: 42, name: "Alice" } result
vitest.config.jsnew file+9
@@ -0,0 +1,9 @@
1+import { defineConfig } from "vitest/config";
2+
3+export default defineConfig({
4+ test: {
5+ include: ["output/Test.Main/index.js"],
6+ globals: true,
7+ environment: "jsdom",
8+ },
9+});