git.iko.soy/rowtype-yoga/purescript-yoga-fetch-om
on main help sign in
Add Boolean/Number roundtrip tests, wire up local deps for integration

Tests verify Boolean and Number query params survive the full server↔client cycle: client serializes, HTTP transport, server parses. Both pass with the ParseParam fixes in yoga-http-api.

commitc6007af67a657016aa14b18bc28fed7c6a5bf499 authorMark <mark.eibes@gmail.com> dateSat 18 Apr 2026, 16:12 (5 months ago) parentad58708
3 files changed+171−1
src/Yoga/Fetch/Om.purs+12 src/Yoga/Fetch/Om/QueryDefaults.purs+135 test/Roundtrip.Spec.purs+24−1
src/Yoga/Fetch/Om.purs+12
@@ -13,6 +13,7 @@ module Yoga.Fetch.Om
1313 , module Yoga.HTTP.API.Route
1414 , module Yoga.HTTP.API.Path
1515 , plainText
16+ , opt
1617 , module Yoga.Fetch.Om.StreamDecode
1718 , module Yoga.Fetch.Om.Simple
1819 ) where
@@ -34,6 +35,7 @@ import Yoga.Fetch.Om.BuildUrl (class BuildUrl, buildUrl)
3435import Yoga.Fetch.Om.Simple (class DecodeResponse, decodeResponse, FetchError, FetchResponse, get, getWithHeaders, delete, deleteWithHeaders, delete_, post, postWithHeaders, post_, put, putWithHeaders, put_, patch, patchWithHeaders, patch_)
3536import Yoga.Fetch.Om.StreamDecode (class StreamDecode, decodeStream)
3637import Yoga.Fetch.Om.ClientFunction (class BuildClientFn, class CheckBodyIsUnit, buildClientFn)
38+import Yoga.Fetch.Om.QueryDefaults (class BuildQueryFromPartial, buildQueryFromPartial)
3739import Yoga.Fetch.Om.ExtractParams (class ExtractRequestBody, class ExtractBodyEncoding, class ExtractRequestHeaders)
3840import Yoga.Fetch.Om.MakeRequest (class MakeRequest, class BodyEncoding, makeRequest, encodingContentType, encodeBody)
3941import Yoga.Fetch.Om.ParseResponse (class ParseResponse, parseResponse)
@@ -144,6 +146,16 @@ deriveClient baseUrl = deriveClientImpl @ctx @extraErr baseUrl (Proxy :: _ { | r
144146plainText :: PlainText -> String
145147plainText = unsafeCoerce
146148
149+-- | Convert a partial record to a full query params record.
150+-- | Provided fields get `Just`, missing fields get `Nothing`.
151+-- |
152+-- | ```purescript
153+-- | api.listUsers $ opt { limit: 10 }
154+-- | -- instead of: api.listUsers { limit: Just 10, offset: Nothing }
155+-- | ```
156+opt :: forall @queryParams provided. BuildQueryFromPartial queryParams provided => Record provided -> Record queryParams
157+opt = buildQueryFromPartial
158+
147159instance
148160 ( RowToList routesRow rl
149161 , DeriveClientRL ctx extraErr rl () clientsRow
src/Yoga/Fetch/Om/QueryDefaults.pursnew file+135
@@ -0,0 +1,135 @@
1+module Yoga.Fetch.Om.QueryDefaults
2+ ( class BuildMaybeQuery
3+ , buildMaybeQuery
4+ , class HasField
5+ , class BuildMaybeQueryField
6+ , buildMaybeQueryField
7+ , class SplitQueryRow
8+ , class SplitQueryRowRL
9+ , class BuildQueryFromPartial
10+ , buildQueryFromPartial
11+ , class BuildQueryFromPartialRL
12+ , buildQueryFromPartialRL
13+ ) where
14+
15+import Data.Maybe (Maybe(..))
16+import Data.Symbol (class IsSymbol)
17+import Prim.Boolean (True, False)
18+import Prim.Row as Row
19+import Prim.RowList (class RowToList, RowList)
20+import Prim.RowList as RL
21+import Record as Record
22+import Type.Proxy (Proxy(..))
23+
24+-- | Check whether a label exists in a RowList
25+class HasField (name :: Symbol) (rl :: RowList Type) (has :: Boolean) | name rl -> has
26+
27+instance HasField name RL.Nil False
28+instance HasField name (RL.Cons name ty tail) True
29+else instance HasField name tail has => HasField name (RL.Cons other ty tail) has
30+
31+-- | Get a Maybe-wrapped value from a record, based on whether the field exists
32+class BuildMaybeQueryField (has :: Boolean) (name :: Symbol) (ty :: Type) (provided :: Row Type) where
33+ buildMaybeQueryField :: Proxy has -> Proxy name -> Record provided -> Maybe ty
34+
35+instance (IsSymbol name, Row.Cons name ty rest provided) =>
36+ BuildMaybeQueryField True name ty provided where
37+ buildMaybeQueryField _ _ provided = Just (Record.get (Proxy :: _ name) provided)
38+
39+instance BuildMaybeQueryField False name ty provided where
40+ buildMaybeQueryField _ _ _ = Nothing
41+
42+-- | Build a full Maybe-wrapped query record from a partial provided record.
43+-- | Fields present in provided get Just, missing fields get Nothing.
44+class BuildMaybeQuery (rl :: RowList Type) (providedRL :: RowList Type) (provided :: Row Type) (queryParams :: Row Type) | rl -> queryParams where
45+ buildMaybeQuery :: Proxy rl -> Proxy providedRL -> Record provided -> Record queryParams
46+
47+instance BuildMaybeQuery RL.Nil providedRL provided () where
48+ buildMaybeQuery _ _ _ = {}
49+
50+instance
51+ ( IsSymbol name
52+ , HasField name providedRL has
53+ , BuildMaybeQueryField has name ty provided
54+ , BuildMaybeQuery tail providedRL provided tailQuery
55+ , Row.Cons name (Maybe ty) tailQuery queryParams
56+ , Row.Lacks name tailQuery
57+ ) =>
58+ BuildMaybeQuery (RL.Cons name ty tail) providedRL provided queryParams where
59+ buildMaybeQuery _ prl provided = Record.insert (Proxy :: _ name) val rest
60+ where
61+ val = buildMaybeQueryField (Proxy :: _ has) (Proxy :: _ name) provided
62+ rest = buildMaybeQuery (Proxy :: _ tail) prl provided
63+
64+-- | Split a query params row into optional raw types and required raw types.
65+-- | Maybe ty → optional with raw type ty
66+-- | ty (no Maybe) → required with raw type ty (from Required query params)
67+class SplitQueryRow (queryParams :: Row Type) (optionalRaw :: Row Type) (requiredRaw :: Row Type) | queryParams -> optionalRaw requiredRaw
68+
69+instance (RowToList queryParams rl, SplitQueryRowRL rl optionalRaw requiredRaw) => SplitQueryRow queryParams optionalRaw requiredRaw
70+
71+class SplitQueryRowRL (rl :: RowList Type) (optionalRaw :: Row Type) (requiredRaw :: Row Type) | rl -> optionalRaw requiredRaw
72+
73+instance SplitQueryRowRL RL.Nil () ()
74+
75+-- Maybe ty field → optional
76+instance
77+ ( SplitQueryRowRL tail optTail reqTail
78+ , Row.Cons name ty optTail optionalRaw
79+ , Row.Lacks name optTail
80+ ) =>
81+ SplitQueryRowRL (RL.Cons name (Maybe ty) tail) optionalRaw reqTail
82+
83+-- Non-Maybe field → required (from Required query params)
84+else instance
85+ ( SplitQueryRowRL tail optTail reqTail
86+ , Row.Cons name ty reqTail requiredRaw
87+ , Row.Lacks name reqTail
88+ ) =>
89+ SplitQueryRowRL (RL.Cons name ty tail) optTail requiredRaw
90+
91+-- | Build full queryParams from a partial record that contains path params,
92+-- | required query params, and optionally some optional query params.
93+-- | Walks the queryParams RowList:
94+-- | Maybe ty field → check if raw value provided, wrap in Just or Nothing
95+-- | bare ty field → copy from provided (required, always present)
96+class BuildQueryFromPartial (queryParams :: Row Type) (provided :: Row Type) where
97+ buildQueryFromPartial :: Record provided -> Record queryParams
98+
99+instance (RowToList queryParams rl, RowToList provided prl, BuildQueryFromPartialRL rl prl provided queryParams) => BuildQueryFromPartial queryParams provided where
100+ buildQueryFromPartial = buildQueryFromPartialRL (Proxy :: _ rl) (Proxy :: _ prl)
101+
102+class BuildQueryFromPartialRL (rl :: RowList Type) (providedRL :: RowList Type) (provided :: Row Type) (queryParams :: Row Type) | rl -> queryParams where
103+ buildQueryFromPartialRL :: Proxy rl -> Proxy providedRL -> Record provided -> Record queryParams
104+
105+instance BuildQueryFromPartialRL RL.Nil providedRL provided () where
106+ buildQueryFromPartialRL _ _ _ = {}
107+
108+-- Maybe ty: check if field is provided, wrap in Just or use Nothing
109+instance
110+ ( IsSymbol name
111+ , HasField name providedRL has
112+ , BuildMaybeQueryField has name ty provided
113+ , BuildQueryFromPartialRL tail providedRL provided tailQuery
114+ , Row.Cons name (Maybe ty) tailQuery queryParams
115+ , Row.Lacks name tailQuery
116+ ) =>
117+ BuildQueryFromPartialRL (RL.Cons name (Maybe ty) tail) providedRL provided queryParams where
118+ buildQueryFromPartialRL _ prl provided = Record.insert (Proxy :: _ name) val rest
119+ where
120+ val = buildMaybeQueryField (Proxy :: _ has) (Proxy :: _ name) provided
121+ rest = buildQueryFromPartialRL (Proxy :: _ tail) prl provided
122+
123+-- bare ty (Required): always present in provided, copy directly
124+else instance
125+ ( IsSymbol name
126+ , Row.Cons name ty rest provided
127+ , BuildQueryFromPartialRL tail providedRL provided tailQuery
128+ , Row.Cons name ty tailQuery queryParams
129+ , Row.Lacks name tailQuery
130+ ) =>
131+ BuildQueryFromPartialRL (RL.Cons name ty tail) providedRL provided queryParams where
132+ buildQueryFromPartialRL _ prl provided = Record.insert (Proxy :: _ name) val rest
133+ where
134+ val = Record.get (Proxy :: _ name) provided
135+ rest = buildQueryFromPartialRL (Proxy :: _ tail) prl provided
test/Roundtrip.Spec.purs+24−1
@@ -12,7 +12,7 @@ import Yoga.Fastify.Fastify as F
1212import Yoga.Fastify.Fastify (Port(..), Host(..))
1313import Yoga.Fastify.Om.API (registerAPI)
1414import Yoga.Fastify.Om.Route (Handler, handle, respond, reject, BearerToken(..))
15−import Yoga.Fetch.Om (GET, POST, PUT, DELETE, Route, JSON, type (/), type (:), type (:?), client)
15+import Yoga.Fetch.Om (GET, POST, PUT, DELETE, Route, JSON, type (/), type (:), type (:?), client, opt)
1616import Yoga.Om (Om, ask, handleErrors, runOm)
1717
1818-- Types
@@ -242,6 +242,29 @@ spec = around withServer $ describe "server ↔ client roundtrip" do
242242 result.active `shouldEqual` false
243243 result.minScore `shouldEqual` 0.0
244244
245+ describe "opt helper for partial query params" do
246+ it "passes subset of query params via opt" \_ ->
247+ runOm {} { exception: \_ -> pure unit } do
248+ users <- api.listUsers $ opt { limit: 1 }
249+ liftAff $ (map _.name users) `shouldEqual` ["Alice"]
250+
251+ it "passes all query params via opt" \_ ->
252+ runOm {} { exception: \_ -> pure unit } do
253+ users <- api.listUsers $ opt { limit: 2, offset: 0 }
254+ liftAff $ (map _.name users) `shouldEqual` ["Alice", "Bob"]
255+
256+ it "passes empty record via opt" \_ ->
257+ runOm {} { exception: \_ -> pure unit } do
258+ users <- api.listUsers $ opt {}
259+ liftAff $ (map _.name users) `shouldEqual` ["Alice", "Bob", "Charlie"]
260+
261+ it "passes Boolean via opt" \_ ->
262+ runOm {} { exception: \_ -> pure unit } do
263+ result <- api.search $ opt { active: true }
264+ liftAff do
265+ result.active `shouldEqual` true
266+ result.minScore `shouldEqual` 0.0
267+
245268-- Server setup
246269
247270withServer :: forall a. (Unit -> _ a) -> _ a