git.iko.soy/rowtype-yoga/purescript-yoga-fetch-om
on main help sign in
Polymorphic client types, remove unsafeCoerce from ClientFunction
57eec00Mark
src/Yoga/Fetch/Om.purs+5−5
@@ -31,7 +31,7 @@ import JS.Fetch.Headers as Headers
31import Type.Row.Homogeneous (class Homogeneous)
31import Type.Row.Homogeneous (class Homogeneous)
32import Yoga.Fetch.Om.BuildUrl (class BuildUrl, buildUrl)
32import Yoga.Fetch.Om.BuildUrl (class BuildUrl, buildUrl)
33import Yoga.Fetch.Om.Simple (class DecodeResponse, decodeResponse, FetchError, FetchResponse, get, getWithHeaders, delete, deleteWithHeaders, delete_, post, postWithHeaders, post_, put, putWithHeaders, put_, patch, patchWithHeaders, patch_)
33import Yoga.Fetch.Om.Simple (class DecodeResponse, decodeResponse, FetchError, FetchResponse, get, getWithHeaders, delete, deleteWithHeaders, delete_, post, postWithHeaders, post_, put, putWithHeaders, put_, patch, patchWithHeaders, patch_)
34−import Yoga.Fetch.Om.ClientFn (class BuildClientFn, class CheckBodyIsUnit, buildClientFn)
34+import Yoga.Fetch.Om.ClientFunction (class BuildClientFn, class CheckBodyIsUnit, buildClientFn)
35import Yoga.Fetch.Om.ExtractParams (class ExtractRequestBody, class ExtractRequestHeaders)
35import Yoga.Fetch.Om.ExtractParams (class ExtractRequestBody, class ExtractRequestHeaders)
36import Yoga.Fetch.Om.MakeRequest (class MakeRequest, class SerializeBody, makeRequest, serializeBody)
36import Yoga.Fetch.Om.MakeRequest (class MakeRequest, class SerializeBody, makeRequest, serializeBody)
37import Yoga.Fetch.Om.ParseResponse (class ParseResponse, parseResponse)
37import Yoga.Fetch.Om.ParseResponse (class ParseResponse, parseResponse)
@@ -110,16 +110,16 @@ class DeriveClient routesRow clientsRow | routesRow -> clientsRow where
110-- | type UserAPI = { getUser :: Route ... }
110-- | type UserAPI = { getUser :: Route ... }
111-- | api = client @UserAPI "https://api.example.com"
111-- | api = client @UserAPI "https://api.example.com"
112-- | ```
112-- | ```
113−client :: forall @routes routesRow clientsRow. RecordRow routes routesRow => DeriveClient routesRow clientsRow => String -> Record clientsRow
113+client :: forall @routes routesRow clientsRow polyClientsRow. RecordRow routes routesRow => DeriveClient routesRow clientsRow => String -> Record polyClientsRow
114−client baseUrl = deriveClientImpl baseUrl (Proxy :: _ { | routesRow })
114+client baseUrl = polymorphic (deriveClientImpl baseUrl (Proxy :: _ { | routesRow }) :: Record clientsRow)
115
115
116-- | Deprecated: Use `client` with VTA instead
116-- | Deprecated: Use `client` with VTA instead
117-- |
117-- |
118-- | ```purescript
118-- | ```purescript
119-- | api = deriveClient @UserAPI "https://api.example.com"
119-- | api = deriveClient @UserAPI "https://api.example.com"
120-- | ```
120-- | ```
121−deriveClient :: forall @routesRow clientsRow. DeriveClient routesRow clientsRow => String -> Record clientsRow
121+deriveClient :: forall @routesRow clientsRow polyClientsRow. DeriveClient routesRow clientsRow => String -> Record polyClientsRow
122−deriveClient baseUrl = deriveClientImpl baseUrl (Proxy :: _ { | routesRow })
122+deriveClient baseUrl = polymorphic (deriveClientImpl baseUrl (Proxy :: _ { | routesRow }) :: Record clientsRow)
123
123
124-- | Cast a generated client to a polymorphic type signature
124-- | Cast a generated client to a polymorphic type signature
125-- | This allows you to add explicit forall quantification for better documentation
125-- | This allows you to add explicit forall quantification for better documentation
src/Yoga/Fetch/Om/ClientFn.purs → src/Yoga/Fetch/Om/ClientFunction.pursrenamed+20−19
@@ -1,4 +1,4 @@
1−module Yoga.Fetch.Om.ClientFn
1+module Yoga.Fetch.Om.ClientFunction
2 ( class BuildClientFn
2 ( class BuildClientFn
3 , buildClientFn
3 , buildClientFn
4 , class CheckBodyIsUnit
4 , class CheckBodyIsUnit
@@ -10,7 +10,6 @@ import Data.Unit (Unit, unit)
10import Prim.RowList (RowList)
10import Prim.RowList (RowList)
11import Prim.RowList as RL
11import Prim.RowList as RL
12import Type.Proxy (Proxy)
12import Type.Proxy (Proxy)
13−import Unsafe.Coerce (unsafeCoerce)
14import Yoga.Om (Om)
13import Yoga.Om (Om)
15
14
16data IsUnit
15data IsUnit
@@ -26,7 +25,9 @@ class BuildClientFn
26 :: RowList Type -> RowList Type -> Type -> Type -> Row Type -> Row Type -> Row Type -> Type -> Type -> Constraint
25 :: RowList Type -> RowList Type -> Type -> Type -> Row Type -> Row Type -> Row Type -> Type -> Type -> Constraint
27class
26class
28 BuildClientFn pathQueryRL headersRL bodyFlag body pathQuery headers errorRow result fn
27 BuildClientFn pathQueryRL headersRL bodyFlag body pathQuery headers errorRow result fn
28+ | pathQueryRL -> pathQuery
29+ , headersRL -> headers
29− | pathQueryRL headersRL bodyFlag body pathQuery headers errorRow result -> fn where
30+ , pathQueryRL headersRL bodyFlag body errorRow result -> fn where
30 buildClientFn
31 buildClientFn
31 :: Proxy pathQueryRL
32 :: Proxy pathQueryRL
32 -> Proxy headersRL
33 -> Proxy headersRL
@@ -40,12 +41,12 @@ instance
40 RL.Nil
41 RL.Nil
41 IsUnit
42 IsUnit
42 Unit
43 Unit
43− pq
44+ ()
44− h
45+ ()
45 errorRow
46 errorRow
46 result
47 result
47 (Om (Record ()) errorRow result) where
48 (Om (Record ()) errorRow result) where
48− buildClientFn _ _ _ f = unsafeCoerce (f (unsafeCoerce {}) (unsafeCoerce {}) unit)
49+ buildClientFn _ _ _ f = f {} {} unit
49
50
50-- No path/query, no headers, with body
51-- No path/query, no headers, with body
51instance
52instance
@@ -53,12 +54,12 @@ instance
53 RL.Nil
54 RL.Nil
54 IsNotUnit
55 IsNotUnit
55 body
56 body
56− pq
57+ ()
57− h
58+ ()
58 errorRow
59 errorRow
59 result
60 result
60 (body -> Om (Record ()) errorRow result) where
61 (body -> Om (Record ()) errorRow result) where
61− buildClientFn _ _ _ f = unsafeCoerce (\b -> f (unsafeCoerce {}) (unsafeCoerce {}) b)
62+ buildClientFn _ _ _ f b = f {} {} b
62
63
63-- No path/query, with headers, no body
64-- No path/query, with headers, no body
64instance
65instance
@@ -66,12 +67,12 @@ instance
66 (RL.Cons n t tl)
67 (RL.Cons n t tl)
67 IsUnit
68 IsUnit
68 Unit
69 Unit
69− pq
70+ ()
70 h
71 h
71 errorRow
72 errorRow
72 result
73 result
73 (Record h -> Om (Record ()) errorRow result) where
74 (Record h -> Om (Record ()) errorRow result) where
74− buildClientFn _ _ _ f = unsafeCoerce (\hdrs -> f (unsafeCoerce {}) hdrs unit)
75+ buildClientFn _ _ _ f hdrs = f {} hdrs unit
75
76
76-- No path/query, with headers, with body
77-- No path/query, with headers, with body
77instance
78instance
@@ -79,12 +80,12 @@ instance
79 (RL.Cons n t tl)
80 (RL.Cons n t tl)
80 IsNotUnit
81 IsNotUnit
81 body
82 body
82− pq
83+ ()
83 h
84 h
84 errorRow
85 errorRow
85 result
86 result
86 (Record h -> body -> Om (Record ()) errorRow result) where
87 (Record h -> body -> Om (Record ()) errorRow result) where
87− buildClientFn _ _ _ f = unsafeCoerce (\hdrs b -> f (unsafeCoerce {}) hdrs b)
88+ buildClientFn _ _ _ f hdrs b = f {} hdrs b
88
89
89-- With path/query, no headers, no body
90-- With path/query, no headers, no body
90instance
91instance
@@ -93,11 +94,11 @@ instance
93 IsUnit
94 IsUnit
94 Unit
95 Unit
95 pq
96 pq
96− h
97+ ()
97 errorRow
98 errorRow
98 result
99 result
99 (Record pq -> Om (Record ()) errorRow result) where
100 (Record pq -> Om (Record ()) errorRow result) where
100− buildClientFn _ _ _ f = unsafeCoerce (\pqr -> f pqr (unsafeCoerce {}) unit)
101+ buildClientFn _ _ _ f pqr = f pqr {} unit
101
102
102-- With path/query, no headers, with body
103-- With path/query, no headers, with body
103instance
104instance
@@ -106,11 +107,11 @@ instance
106 IsNotUnit
107 IsNotUnit
107 body
108 body
108 pq
109 pq
109− h
110+ ()
110 errorRow
111 errorRow
111 result
112 result
112 (Record pq -> body -> Om (Record ()) errorRow result) where
113 (Record pq -> body -> Om (Record ()) errorRow result) where
113− buildClientFn _ _ _ f = unsafeCoerce (\pqr b -> f pqr (unsafeCoerce {}) b)
114+ buildClientFn _ _ _ f pqr b = f pqr {} b
114
115
115-- With path/query, with headers, no body
116-- With path/query, with headers, no body
116instance
117instance
@@ -123,7 +124,7 @@ instance
123 errorRow
124 errorRow
124 result
125 result
125 (Record pq -> Record h -> Om (Record ()) errorRow result) where
126 (Record pq -> Record h -> Om (Record ()) errorRow result) where
126− buildClientFn _ _ _ f = unsafeCoerce (\pqr hdrs -> f pqr hdrs unit)
127+ buildClientFn _ _ _ f pqr hdrs = f pqr hdrs unit
127
128
128-- With path/query, with headers, with body
129-- With path/query, with headers, with body
129instance
130instance
@@ -136,4 +137,4 @@ instance
136 errorRow
137 errorRow
137 result
138 result
138 (Record pq -> Record h -> body -> Om (Record ()) errorRow result) where
139 (Record pq -> Record h -> body -> Om (Record ()) errorRow result) where
139− buildClientFn _ _ _ f = unsafeCoerce f
140+ buildClientFn _ _ _ f = f
src/Yoga/Fetch/Om/MakeRequest.purs+1−1
@@ -81,7 +81,7 @@ makeRequest proxy url customHeaders maybeBody = do
81 , body
81 , body
82 , credentials: Credentials.SameOrigin
82 , credentials: Credentials.SameOrigin
83 , mode: Mode.Cors
83 , mode: Mode.Cors
84− , referrer: Referrer.ReferrerNone
84+ , referrer: Referrer.ReferrerUrl ""
85 , referrerPolicy: ReferrerPolicy.NoReferrer
85 , referrerPolicy: ReferrerPolicy.NoReferrer
86 , integrity: Integrity ""
86 , integrity: Integrity ""
87 , duplex: Duplex.Half
87 , duplex: Duplex.Half
test/Complete.Example.purs+2−2
@@ -5,7 +5,7 @@ import Prelude
5import Data.Array (length)
5import Data.Array (length)
6import Data.Maybe (Maybe(..))
6import Data.Maybe (Maybe(..))
7import Effect.Class.Console (log)
7import Effect.Class.Console (log)
8−import Yoga.Fetch.Om (GET, POST, PUT, DELETE, Route, JSON, Path, type (/), type (:), type (:?), client, polymorphic)
8+import Yoga.Fetch.Om (GET, POST, PUT, DELETE, Route, JSON, Path, type (/), type (:), type (:?), client)
9import Yoga.Om (Om, handleErrors)
9import Yoga.Om (Om, handleErrors)
10
10
11type User =
11type User =
@@ -83,7 +83,7 @@ api
83 , listUsers :: { limit :: Maybe Int, offset :: Maybe Int } -> Om ctx err (Array User)
83 , listUsers :: { limit :: Maybe Int, offset :: Maybe Int } -> Om ctx err (Array User)
84 , updateUser :: { id :: Int } -> UpdateUserRequest -> Om ctx (badRequest :: ErrorMessage, notFound :: ErrorMessage | err) User
84 , updateUser :: { id :: Int } -> UpdateUserRequest -> Om ctx (badRequest :: ErrorMessage, notFound :: ErrorMessage | err) User
85 }
85 }
86−api = polymorphic $ client @UserAPI "https://api.example.com"
86+api = client @UserAPI "https://api.example.com"
87
87
88exampleGetUser :: Om {} (notFound :: ErrorMessage) Unit
88exampleGetUser :: Om {} (notFound :: ErrorMessage) Unit
89exampleGetUser = do
89exampleGetUser = do
test/Polymorphic.Spec.pursnew file+60
@@ -0,0 +1,60 @@
1+module Polymorphic.Spec where
2+
3+import Prelude
4+
5+import Effect (Effect)
6+import Effect.Class (liftEffect)
7+import Effect.Ref as Ref
8+import Yoga.Fetch.Om (GET, Route, Path, type (/), type (:), client)
9+import Yoga.Om (Om, ask, runOm)
10+import ViTest (ViTest, describe, test)
11+import ViTest.Expect (expectToBe)
12+
13+type User = { id :: Int, name :: String }
14+
15+type TestAPI =
16+ { getUser :: Route GET (Path ("users" / "id" : Int)) {} (ok :: { body :: User })
17+ }
18+
19+api
20+ :: forall ctx err
21+ . { getUser :: { id :: Int } -> Om ctx err User
22+ }
23+api = client @TestAPI "http://localhost:44931"
24+
25+testPolymorphic :: Effect ViTest
26+testPolymorphic = describe "polymorphic client" do
27+ _ <- test "fetches user with { token :: String } context" do
28+ tokenRef <- liftEffect $ Ref.new ""
29+ runOm { token: "secret123" } { exception: \_ -> pure unit } do
30+ ctx <- ask
31+ liftEffect $ Ref.write ctx.token tokenRef
32+ user <- api.getUser { id: 42 }
33+ liftEffect $ Ref.write (ctx.token <> ":" <> user.name) tokenRef
34+ val <- liftEffect $ Ref.read tokenRef
35+ expectToBe val "secret123:User 42"
36+
37+ _ <- test "fetches user with { apiKey :: String, userId :: Int } context" do
38+ resultRef <- liftEffect $ Ref.new ""
39+ runOm { apiKey: "mykey", userId: 7 } { exception: \_ -> pure unit } do
40+ ctx <- ask
41+ user <- api.getUser { id: ctx.userId }
42+ liftEffect $ Ref.write (ctx.apiKey <> ":" <> user.name) resultRef
43+ val <- liftEffect $ Ref.read resultRef
44+ expectToBe val "mykey:User 7"
45+
46+ test "same api.getUser works in two different contexts" do
47+ ref1 <- liftEffect $ Ref.new ""
48+ ref2 <- liftEffect $ Ref.new ""
49+ runOm { token: "tok1" } { exception: \_ -> pure unit } do
50+ ctx <- ask
51+ user <- api.getUser { id: 1 }
52+ liftEffect $ Ref.write (ctx.token <> ":" <> user.name) ref1
53+ runOm { endpoint: "prod", region: "eu" } { exception: \_ -> pure unit } do
54+ ctx <- ask
55+ user <- api.getUser { id: 2 }
56+ liftEffect $ Ref.write (ctx.endpoint <> "-" <> ctx.region <> ":" <> user.name) ref2
57+ val1 <- liftEffect $ Ref.read ref1
58+ val2 <- liftEffect $ Ref.read ref2
59+ expectToBe val1 "tok1:User 1"
60+ expectToBe val2 "prod-eu:User 2"
test/Test.Main.purs+3−1
@@ -4,6 +4,7 @@ import Prelude
4
4
5import BuildUrl.Spec as BuildUrl
5import BuildUrl.Spec as BuildUrl
6import MakeRequest.Spec as MakeRequest
6import MakeRequest.Spec as MakeRequest
7+import Polymorphic.Spec as Polymorphic
7import SplitParams.Spec as SplitParams
8import SplitParams.Spec as SplitParams
8import Variant.Spec as Variant
9import Variant.Spec as Variant
9import Effect (Effect)
10import Effect (Effect)
@@ -16,7 +17,8 @@ spec = do
16 _ <- BuildUrl.testBuildUrl
17 _ <- BuildUrl.testBuildUrl
17 _ <- SplitParams.testSplitParams
18 _ <- SplitParams.testSplitParams
18 _ <- Variant.testOnly
19 _ <- Variant.testOnly
19− MakeRequest.testToHeaders
20+ _ <- MakeRequest.testToHeaders
21+ Polymorphic.testPolymorphic
20
22
21main :: ViTest
23main :: ViTest
22main = viTest spec
24main = viTest spec
test/setup.jsnew file+9
@@ -0,0 +1,9 @@
1+import { listen, close } from "./test-server.js";
2+
3+export async function setup() {
4+ await listen();
5+}
6+
7+export async function teardown() {
8+ await close();
9+}
test/test-server.jsnew file+27
@@ -0,0 +1,27 @@
1+import { createServer } from "node:http";
2+
3+const server = createServer((req, res) => {
4+ const url = new URL(req.url, "http://localhost:44931");
5+ const match = url.pathname.match(/^\/users\/(\d+)$/);
6+ if (match && req.method === "GET") {
7+ res.writeHead(200, { "Content-Type": "application/json" });
8+ res.end(
9+ JSON.stringify({ id: parseInt(match[1]), name: "User " + match[1] }),
10+ );
11+ return;
12+ }
13+ res.writeHead(404);
14+ res.end("Not Found");
15+});
16+
17+export function listen() {
18+ return new Promise((resolve) => {
19+ server.listen(44931, () => resolve());
20+ });
21+}
22+
23+export function close() {
24+ return new Promise((resolve) => {
25+ server.close(() => resolve());
26+ });
27+}
vitest.config.js+2−1
@@ -3,7 +3,8 @@ import { defineConfig } from "vitest/config";
3export default defineConfig({
3export default defineConfig({
4 test: {
4 test: {
5 include: ["output/Test.Main/index.js"],
5 include: ["output/Test.Main/index.js"],
6− globals: true,
7 environment: "jsdom",
6 environment: "jsdom",
7+ globals: true,
8+ globalSetup: ["test/setup.js"],
8 },
9 },
9});
10});