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
c6007afMark
src/Yoga/Fetch/Om.purs+12
@@ -13,6 +13,7 @@ module Yoga.Fetch.Om
13 , module Yoga.HTTP.API.Route
13 , module Yoga.HTTP.API.Route
14 , module Yoga.HTTP.API.Path
14 , module Yoga.HTTP.API.Path
15 , plainText
15 , plainText
16+ , opt
16 , module Yoga.Fetch.Om.StreamDecode
17 , module Yoga.Fetch.Om.StreamDecode
17 , module Yoga.Fetch.Om.Simple
18 , module Yoga.Fetch.Om.Simple
18 ) where
19 ) where
@@ -34,6 +35,7 @@ import Yoga.Fetch.Om.BuildUrl (class BuildUrl, buildUrl)
34import Yoga.Fetch.Om.Simple (class DecodeResponse, decodeResponse, FetchError, FetchResponse, get, getWithHeaders, delete, deleteWithHeaders, delete_, post, postWithHeaders, post_, put, putWithHeaders, put_, patch, patchWithHeaders, patch_)
35import Yoga.Fetch.Om.Simple (class DecodeResponse, decodeResponse, FetchError, FetchResponse, get, getWithHeaders, delete, deleteWithHeaders, delete_, post, postWithHeaders, post_, put, putWithHeaders, put_, patch, patchWithHeaders, patch_)
35import Yoga.Fetch.Om.StreamDecode (class StreamDecode, decodeStream)
36import Yoga.Fetch.Om.StreamDecode (class StreamDecode, decodeStream)
36import Yoga.Fetch.Om.ClientFunction (class BuildClientFn, class CheckBodyIsUnit, buildClientFn)
37import Yoga.Fetch.Om.ClientFunction (class BuildClientFn, class CheckBodyIsUnit, buildClientFn)
38+import Yoga.Fetch.Om.QueryDefaults (class BuildQueryFromPartial, buildQueryFromPartial)
37import Yoga.Fetch.Om.ExtractParams (class ExtractRequestBody, class ExtractBodyEncoding, class ExtractRequestHeaders)
39import Yoga.Fetch.Om.ExtractParams (class ExtractRequestBody, class ExtractBodyEncoding, class ExtractRequestHeaders)
38import Yoga.Fetch.Om.MakeRequest (class MakeRequest, class BodyEncoding, makeRequest, encodingContentType, encodeBody)
40import Yoga.Fetch.Om.MakeRequest (class MakeRequest, class BodyEncoding, makeRequest, encodingContentType, encodeBody)
39import Yoga.Fetch.Om.ParseResponse (class ParseResponse, parseResponse)
41import Yoga.Fetch.Om.ParseResponse (class ParseResponse, parseResponse)
@@ -144,6 +146,16 @@ deriveClient baseUrl = deriveClientImpl @ctx @extraErr baseUrl (Proxy :: _ { | r
144plainText :: PlainText -> String
146plainText :: PlainText -> String
145plainText = unsafeCoerce
147plainText = unsafeCoerce
146
148
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+
147instance
159instance
148 ( RowToList routesRow rl
160 ( RowToList routesRow rl
149 , DeriveClientRL ctx extraErr rl () clientsRow
161 , 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
12import Yoga.Fastify.Fastify (Port(..), Host(..))
12import Yoga.Fastify.Fastify (Port(..), Host(..))
13import Yoga.Fastify.Om.API (registerAPI)
13import Yoga.Fastify.Om.API (registerAPI)
14import Yoga.Fastify.Om.Route (Handler, handle, respond, reject, BearerToken(..))
14import 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)
16import Yoga.Om (Om, ask, handleErrors, runOm)
16import Yoga.Om (Om, ask, handleErrors, runOm)
17
17
18-- Types
18-- Types
@@ -242,6 +242,29 @@ spec = around withServer $ describe "server ↔ client roundtrip" do
242 result.active `shouldEqual` false
242 result.active `shouldEqual` false
243 result.minScore `shouldEqual` 0.0
243 result.minScore `shouldEqual` 0.0
244
244
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+
245-- Server setup
268-- Server setup
246
269
247withServer :: forall a. (Unit -> _ a) -> _ a
270withServer :: forall a. (Unit -> _ a) -> _ a