git.iko.soy/rowtype-yoga/purescript-yoga-fetch-om
on main help sign in
commit575f70870aa5b83a51b40b76854d5d6f2d636666 authorMark <mark.eibes@gmail.com> dateSat 18 Apr 2026, 12:40 (5 months ago) parent21b4c0c
7 files changed+226−42
spago.lock+183 src/Yoga/Fetch/Om.purs+20−12 src/Yoga/Fetch/Om/Simple.purs−6 test/Complete.Example.purs+11−16 test/Example.purs+2−2 test/MakeRequest.Spec.purs+5 test/Polymorphic.Spec.purs+5−6
spago.lock+183
@@ -71,6 +71,91 @@
7171 {
7272 "yoga-om-strom": ">=0.4.3 <0.5.0"
7373 }
74+ ],
75+ "build_plan": [
76+ "aff",
77+ "aff-retry",
78+ "ansi",
79+ "arraybuffer-types",
80+ "arrays",
81+ "avar",
82+ "bifunctors",
83+ "catenable-lists",
84+ "console",
85+ "const",
86+ "contravariant",
87+ "control",
88+ "datetime",
89+ "distributive",
90+ "effect",
91+ "either",
92+ "enums",
93+ "exceptions",
94+ "exists",
95+ "foldable-traversable",
96+ "foreign",
97+ "foreign-object",
98+ "fork",
99+ "free",
100+ "functions",
101+ "functors",
102+ "gen",
103+ "heterogeneous",
104+ "http-methods",
105+ "identity",
106+ "integers",
107+ "invariant",
108+ "js-bigints",
109+ "js-date",
110+ "js-fetch",
111+ "js-promise",
112+ "js-promise-aff",
113+ "lazy",
114+ "lists",
115+ "literals",
116+ "maybe",
117+ "media-types",
118+ "mmorph",
119+ "newtype",
120+ "nonempty",
121+ "now",
122+ "nullable",
123+ "numbers",
124+ "ordered-collections",
125+ "orders",
126+ "parallel",
127+ "partial",
128+ "pipes",
129+ "prelude",
130+ "profunctor",
131+ "random",
132+ "record",
133+ "record-studio",
134+ "refs",
135+ "safe-coerce",
136+ "spec",
137+ "st",
138+ "strings",
139+ "tailrec",
140+ "transformers",
141+ "tuples",
142+ "type-equality",
143+ "typelevel-prelude",
144+ "uncurried-transformers",
145+ "undefined-is-not-a-problem",
146+ "unfoldable",
147+ "unsafe-coerce",
148+ "untagged-union",
149+ "variant",
150+ "web-events",
151+ "web-file",
152+ "web-streams",
153+ "yoga-http-api",
154+ "yoga-json",
155+ "yoga-om",
156+ "yoga-om-strom",
157+ "yoga-options",
158+ "yoga-tree"
74159 ]
75160 },
76161 "test": {
@@ -90,6 +175,104 @@
90175 {
91176 "yoga-fastify-om": ">=0.4.5 <0.5.0"
92177 }
178+ ],
179+ "build_plan": [
180+ "aff",
181+ "aff-retry",
182+ "ansi",
183+ "argonaut-codecs",
184+ "argonaut-core",
185+ "arraybuffer-types",
186+ "arrays",
187+ "avar",
188+ "bifunctors",
189+ "catenable-lists",
190+ "console",
191+ "const",
192+ "contravariant",
193+ "control",
194+ "datetime",
195+ "distributive",
196+ "effect",
197+ "either",
198+ "enums",
199+ "exceptions",
200+ "exists",
201+ "exitcodes",
202+ "foldable-traversable",
203+ "foreign",
204+ "foreign-object",
205+ "fork",
206+ "free",
207+ "functions",
208+ "functors",
209+ "gen",
210+ "heterogeneous",
211+ "identity",
212+ "integers",
213+ "invariant",
214+ "js-bigints",
215+ "js-date",
216+ "js-promise",
217+ "js-promise-aff",
218+ "js-uri",
219+ "lazy",
220+ "lists",
221+ "literals",
222+ "maybe",
223+ "mmorph",
224+ "newtype",
225+ "node-buffer",
226+ "node-event-emitter",
227+ "node-fs",
228+ "node-path",
229+ "node-process",
230+ "node-streams",
231+ "nonempty",
232+ "now",
233+ "nullable",
234+ "numbers",
235+ "open-memoize",
236+ "optparse",
237+ "ordered-collections",
238+ "orders",
239+ "parallel",
240+ "partial",
241+ "pipes",
242+ "posix-types",
243+ "prelude",
244+ "profunctor",
245+ "random",
246+ "record",
247+ "record-studio",
248+ "refs",
249+ "routing-duplex",
250+ "safe-coerce",
251+ "spec",
252+ "spec-discovery",
253+ "spec-node",
254+ "st",
255+ "strings",
256+ "tailrec",
257+ "transformers",
258+ "tuples",
259+ "type-equality",
260+ "typelevel-prelude",
261+ "uncurried-transformers",
262+ "undefined-is-not-a-problem",
263+ "unfoldable",
264+ "unsafe-coerce",
265+ "untagged-union",
266+ "variant",
267+ "yoga-fastify",
268+ "yoga-fastify-om",
269+ "yoga-http-api",
270+ "yoga-json",
271+ "yoga-om",
272+ "yoga-om-layer",
273+ "yoga-om-strom",
274+ "yoga-options",
275+ "yoga-tree"
93276 ]
94277 }
95278 }
src/Yoga/Fetch/Om.purs+20−12
@@ -19,7 +19,8 @@ module Yoga.Fetch.Om
1919
2020import Prelude
2121
22−import Data.Symbol (class IsSymbol)
22+import Data.Symbol (class IsSymbol, reflectSymbol)
23+import Data.Tuple.Nested ((/\))
2324import Data.Variant (Variant)
2425import Prim.Row as Row
2526import Prim.RowList (class RowToList, RowList)
@@ -29,7 +30,6 @@ import Type.Proxy (Proxy(..))
2930import Unsafe.Coerce (unsafeCoerce)
3031import JS.Fetch.Headers (Headers)
3132import JS.Fetch.Headers as Headers
32−import Type.Row.Homogeneous (class Homogeneous)
3333import Yoga.Fetch.Om.BuildUrl (class BuildUrl, buildUrl)
3434import Yoga.Fetch.Om.Simple (class DecodeResponse, decodeResponse, FetchError, FetchResponse, get, getWithHeaders, delete, deleteWithHeaders, delete_, post, postWithHeaders, post_, put, putWithHeaders, put_, patch, patchWithHeaders, patch_)
3535import Yoga.Fetch.Om.StreamDecode (class StreamDecode, decodeStream)
@@ -40,7 +40,7 @@ import Yoga.Fetch.Om.ParseResponse (class ParseResponse, parseResponse)
4040import Yoga.Fetch.Om.SplitResponses (class SplitResponses)
4141import Yoga.Fetch.Om.Variant (class VariantOrValue, variantOrValue)
4242import Yoga.HTTP.API.Path (Path, Root, Lit, Capture, PathCons, Param, QueryParams, Required, type (/), type (:), type (:?), class PathPattern)
43−import Yoga.HTTP.API.Route (Route(..), GET, POST, PUT, DELETE, PATCH, Response(..), JSON, FormData, PlainText, Streaming, NoBody)
43+import Yoga.HTTP.API.Route (Route(..), GET, POST, PUT, DELETE, PATCH, Response(..), JSON, FormData, PlainText, Streaming, NoBody, class HeaderValue, printHeader)
4444import Yoga.HTTP.API.Route.Handler (class SegmentPathParams, class SegmentQueryParams)
4545import Yoga.Om (class ToOm, Om, toOm)
4646
@@ -57,8 +57,19 @@ class ToHeaders headersRL headers where
5757instance ToHeaders RL.Nil headers where
5858 toHeaders _ _ = Headers.empty
5959
60−else instance Homogeneous headers String => ToHeaders headersRL headers where
61− toHeaders _ = Headers.fromRecord
60+else instance
61+ ( IsSymbol name
62+ , HeaderValue ty
63+ , Row.Cons name ty tailRow headers
64+ , Row.Lacks name tailRow
65+ , ToHeaders tail tailRow
66+ ) =>
67+ ToHeaders (RL.Cons name ty tail) headers where
68+ toHeaders _ headers = Headers.fromFoldable ([ headerName /\ headerValue ] <> Headers.toArray rest)
69+ where
70+ headerName = reflectSymbol (Proxy :: Proxy name)
71+ headerValue = printHeader (Record.get (Proxy :: Proxy name) headers)
72+ rest = toHeaders (Proxy :: Proxy tail) (unsafeCoerce headers :: Record tailRow)
6273
6374class DeriveClientFn :: forall k1. Type -> k1 -> Type -> Row Type -> Row Type -> Row Type -> Type -> Type -> Constraint
6475class
@@ -114,23 +125,20 @@ class DeriveClient routesRow clientsRow | routesRow -> clientsRow where
114125-- | type UserAPI = { getUser :: Route ... }
115126-- | api = client @UserAPI "https://api.example.com"
116127-- | ```
117−client :: forall @routes routesRow clientsRow polyClientsRow. RecordRow routes routesRow => DeriveClient routesRow clientsRow => String -> Record polyClientsRow
118−client baseUrl = polymorphic (deriveClientImpl baseUrl (Proxy :: _ { | routesRow }) :: Record clientsRow)
128+client :: forall @routes routesRow clientsRow. RecordRow routes routesRow => DeriveClient routesRow clientsRow => String -> Record clientsRow
129+client baseUrl = deriveClientImpl baseUrl (Proxy :: _ { | routesRow })
119130
120131-- | Deprecated: Use `client` with VTA instead
121132-- |
122133-- | ```purescript
123134-- | api = deriveClient @UserAPI "https://api.example.com"
124135-- | ```
125−deriveClient :: forall @routesRow clientsRow polyClientsRow. DeriveClient routesRow clientsRow => String -> Record polyClientsRow
126−deriveClient baseUrl = polymorphic (deriveClientImpl baseUrl (Proxy :: _ { | routesRow }) :: Record clientsRow)
136+deriveClient :: forall @routesRow clientsRow. DeriveClient routesRow clientsRow => String -> Record clientsRow
137+deriveClient baseUrl = deriveClientImpl baseUrl (Proxy :: _ { | routesRow })
127138
128139plainText :: PlainText -> String
129140plainText = unsafeCoerce
130141
131−polymorphic :: forall a b. a -> b
132−polymorphic = unsafeCoerce
133−
134142instance
135143 ( RowToList routesRow rl
136144 , DeriveClientRL rl () clientsRow
src/Yoga/Fetch/Om/Simple.purs−6
@@ -102,13 +102,7 @@ simpleFetch method headers url maybeBody = do
102102 { method
103103 , headers: allHeaders
104104 , body: reqBody
105− , credentials: Credentials.SameOrigin
106− , mode: Mode.Cors
107− , referrer: Referrer.ReferrerNone
108− , referrerPolicy: ReferrerPolicy.NoReferrer
109− , integrity: Integrity ""
110105 , duplex: Duplex.Half
111− , cache: Cache.Default
112106 }
113107
114108get
test/Complete.Example.purs+11−16
@@ -6,7 +6,8 @@ import Data.Array (length)
66import Data.Maybe (Maybe(..))
77import Effect.Class.Console (log)
88import Yoga.Fetch.Om (GET, POST, PUT, DELETE, Route, JSON, Path, type (/), type (:), type (:?), client)
9−import Yoga.Om (Om, handleErrors)
9+import Yoga.HTTP.API.Route (BearerToken(..))
10+import Yoga.Om (Om, expand, handleErrors)
1011
1112type User =
1213 { id :: Int
@@ -67,32 +68,23 @@ type UserAPI =
6768 , createUserAuth ::
6869 Route POST
6970 (Path "users")
70− { headers :: Record (authorization :: String), body :: JSON CreateUserRequest }
71+ { headers :: Record (authorization :: BearerToken), body :: JSON CreateUserRequest }
7172 ( created :: { body :: User }
7273 , badRequest :: { body :: ErrorMessage }
7374 )
7475 }
7576
76−-- Generated client with polymorphic type annotation
77−api
78− :: forall ctx err
79− . { createUser :: CreateUserRequest -> Om ctx (badRequest :: ErrorMessage | err) User
80− , createUserAuth :: { authorization :: String } -> CreateUserRequest -> Om ctx (badRequest :: ErrorMessage | err) User
81− , deleteUser :: { id :: Int } -> Om ctx (notFound :: ErrorMessage | err) {}
82− , getUser :: { id :: Int } -> Om ctx (notFound :: ErrorMessage | err) 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
85− }
8677api = client @UserAPI "https://api.example.com"
8778
8879exampleGetUser :: Om {} (notFound :: ErrorMessage) Unit
8980exampleGetUser = do
90− user <- api.getUser { id: 42 }
81+ user <- api.getUser { id: 42 } # expand
9182 log $ "Found user: " <> user.name <> " (" <> user.email <> ")"
9283
9384exampleGetUserHandled :: Om {} () Unit
9485exampleGetUserHandled = do
9586 user <- api.getUser { id: 42 }
87+ # expand
9688 # handleErrors
9789 { notFound: \err -> do
9890 log $ "User not found: " <> err.error
@@ -102,7 +94,7 @@ exampleGetUserHandled = do
10294
10395exampleListUsers :: Om {} () Unit
10496exampleListUsers = do
105− users <- api.listUsers { limit: Just 10, offset: Just 0 }
97+ users <- api.listUsers { limit: Just 10, offset: Just 0 } # expand
10698 log $ "Found " <> show (length users) <> " users"
10799
108100exampleCreateUser :: Om {} (badRequest :: ErrorMessage) Unit
@@ -111,6 +103,7 @@ exampleCreateUser = do
111103 { name: "Alice"
112104 , email: "alice@example.com"
113105 }
106+ # expand
114107 log $ "Created user with ID: " <> show user.id
115108
116109exampleUpdateUser :: Om {} (notFound :: ErrorMessage, badRequest :: ErrorMessage) Unit
@@ -119,17 +112,19 @@ exampleUpdateUser = do
119112 { name: "Alice Updated"
120113 , email: "alice.new@example.com"
121114 }
115+ # expand
122116 log $ "Updated user: " <> user.name
123117
124118exampleDeleteUser :: Om {} (notFound :: ErrorMessage) Unit
125119exampleDeleteUser = do
126− _ <- api.deleteUser { id: 42 }
120+ _ <- api.deleteUser { id: 42 } # expand
127121 log "User deleted successfully"
128122
129123exampleCreateUserAuth :: Om {} (badRequest :: ErrorMessage) Unit
130124exampleCreateUserAuth = do
131− user <- api.createUserAuth { authorization: "Bearer abc123" }
125+ user <- api.createUserAuth { authorization: BearerToken "abc123" }
132126 { name: "Alice"
133127 , email: "alice@example.com"
134128 }
129+ # expand
135130 log $ "Created user with ID: " <> show user.id
test/Example.purs+2−2
@@ -1,6 +1,6 @@
11module Example where
22
3−import Yoga.HTTP.API.Route (GET, POST, DELETE, Route)
3+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
@@ -39,7 +39,7 @@ type UserAPI =
3939 , createUserAuth ::
4040 Route POST
4141 (Path "users")
42− { headers :: Record (authorization :: String), body :: JSON CreateUserRequest }
42+ { headers :: Record (authorization :: BearerToken), body :: JSON CreateUserRequest }
4343 ( created :: { body :: User }
4444 , badRequest :: { body :: ErrorMessage }
4545 )
test/MakeRequest.Spec.purs+5
@@ -11,6 +11,7 @@ import Test.Spec.Assertions (shouldEqual, shouldSatisfy)
1111import Type.Proxy (Proxy(..))
1212import Yoga.Fetch.Om (toHeaders)
1313import Yoga.Fetch.Om.MakeRequest (httpMethod)
14+import Yoga.HTTP.API.Route (BearerToken(..))
1415import Yoga.HTTP.API.Route.Method as Route
1516
1617spec :: Spec Unit
@@ -37,3 +38,7 @@ spec = do
3738 arr `shouldSatisfy` \a ->
3839 a == [ "authorization" /\ "Bearer abc123", "x-custom" /\ "value" ]
3940 || a == [ "x-custom" /\ "value", "authorization" /\ "Bearer abc123" ]
41+
42+ it "typed header values are rendered with HeaderValue" do
43+ let hdrs = toHeaders (Proxy :: _ (RL.Cons "authorization" BearerToken RL.Nil)) { authorization: BearerToken "abc123" }
44+ Headers.toArray hdrs `shouldEqual` [ "authorization" /\ "Bearer abc123" ]
test/Polymorphic.Spec.purs+5−6
@@ -12,7 +12,7 @@ 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 (Om, ask, expand, runOm)
1616
1717type User = { id :: Int, name :: String }
1818
@@ -22,7 +22,6 @@ type TestAPI =
2222 { getUser :: GetUserRoute
2323 }
2424
25−api :: forall ctx err. { getUser :: { id :: Int } -> Om ctx err User }
2625api = client @TestAPI "http://localhost:44931"
2726
2827spec :: Spec Unit
@@ -30,23 +29,23 @@ spec = around withServer $ describe "polymorphic client" do
3029 it "fetches user with { token :: String } context" \_ ->
3130 runOm { token: "secret123" } { exception: \_ -> pure unit } do
3231 ctx <- ask
33− user <- api.getUser { id: 42 }
32+ user <- api.getUser { id: 42 } # expand
3433 liftAff $ (ctx.token <> ":" <> user.name) `shouldEqual` "secret123:User 42"
3534
3635 it "fetches user with { apiKey :: String, userId :: Int } context" \_ ->
3736 runOm { apiKey: "mykey", userId: 7 } { exception: \_ -> pure unit } do
3837 ctx <- ask
39− user <- api.getUser { id: ctx.userId }
38+ user <- api.getUser { id: ctx.userId } # expand
4039 liftAff $ (ctx.apiKey <> ":" <> user.name) `shouldEqual` "mykey:User 7"
4140
4241 it "same api.getUser works in two different contexts" \_ -> do
4342 runOm { token: "tok1" } { exception: \_ -> pure unit } do
4443 ctx <- ask
45− user <- api.getUser { id: 1 }
44+ user <- api.getUser { id: 1 } # expand
4645 liftAff $ (ctx.token <> ":" <> user.name) `shouldEqual` "tok1:User 1"
4746 runOm { endpoint: "prod", region: "eu" } { exception: \_ -> pure unit } do
4847 ctx <- ask
49− user <- api.getUser { id: 2 }
48+ user <- api.getUser { id: 2 } # expand
5049 liftAff $ (ctx.endpoint <> "-" <> ctx.region <> ":" <> user.name) `shouldEqual` "prod-eu:User 2"
5150
5251withServer :: forall a. (Unit -> _ a) -> _ a