git.iko.soy/purestack-dev/todomvc
on master help sign in
Added client request generation
commit3a2d08217a1bfce29aff31b53daca4f40c47c99b authoriko <mail@iko.soy> dateSun 29 Jun 2025, 15:15 (1 year ago) parent4abeb71
10 files changed+581−63
.gitignore+1 flake.lock+70 flake.nix+10−2 src/Backend.purs+24−2 src/Frontend.purs+46−2 src/Plum.purs+1−1 src/Plum/View.purs+26−2 src/PureStack/Client.purs+268 src/PureStack/Route.purs+122 src/PureStack/Server.purs+13−54
.gitignore+1
@@ -12,3 +12,4 @@ backend.js
1212frontend.js
1313result
1414index.js
15+m36
flake.lock+70
@@ -34,6 +34,23 @@
3434 "type": "github"
3535 }
3636 },
37+ "flake-utils_2": {
38+ "inputs": {
39+ "systems": "systems_2"
40+ },
41+ "locked": {
42+ "lastModified": 1731533236,
43+ "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
44+ "owner": "numtide",
45+ "repo": "flake-utils",
46+ "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
47+ "type": "github"
48+ },
49+ "original": {
50+ "id": "flake-utils",
51+ "type": "indirect"
52+ }
53+ },
3754 "nixpkgs": {
3855 "locked": {
3956 "lastModified": 1750436177,
@@ -65,6 +82,28 @@
6582 "type": "github"
6683 }
6784 },
85+ "project-m36-rest": {
86+ "inputs": {
87+ "flake-utils": "flake-utils_2",
88+ "nixpkgs": [
89+ "nixpkgs"
90+ ],
91+ "stacklock2nix": "stacklock2nix"
92+ },
93+ "locked": {
94+ "lastModified": 1750766736,
95+ "narHash": "sha256-xSWxfDWjVhRNNyAg7iqv5yEZ+Ofad3YM9gneFO1M0Xg=",
96+ "owner": "purestack-dev",
97+ "repo": "project-m36-rest",
98+ "rev": "b137dc5eb0ebfe7071e758b0011cddc18a2eb087",
99+ "type": "github"
100+ },
101+ "original": {
102+ "owner": "purestack-dev",
103+ "repo": "project-m36-rest",
104+ "type": "github"
105+ }
106+ },
68107 "purescript-overlay": {
69108 "inputs": {
70109 "flake-compat": "flake-compat",
@@ -107,6 +146,7 @@
107146 "inputs": {
108147 "flake-utils": "flake-utils",
109148 "nixpkgs": "nixpkgs",
149+ "project-m36-rest": "project-m36-rest",
110150 "purescript-overlay": "purescript-overlay",
111151 "spago-nix": "spago-nix"
112152 }
@@ -151,6 +191,21 @@
151191 "type": "github"
152192 }
153193 },
194+ "stacklock2nix": {
195+ "locked": {
196+ "lastModified": 1743151034,
197+ "narHash": "sha256-2QgpxnOyQg475wd0f/qzgG4fNqce1UearDjzZyrmMG8=",
198+ "owner": "cdepillabout",
199+ "repo": "stacklock2nix",
200+ "rev": "108f6ab6d0208e3842443bea26dfd72b37d82f28",
201+ "type": "github"
202+ },
203+ "original": {
204+ "owner": "cdepillabout",
205+ "repo": "stacklock2nix",
206+ "type": "github"
207+ }
208+ },
154209 "systems": {
155210 "locked": {
156211 "lastModified": 1681028828,
@@ -165,6 +220,21 @@
165220 "repo": "default",
166221 "type": "github"
167222 }
223+ },
224+ "systems_2": {
225+ "locked": {
226+ "lastModified": 1681028828,
227+ "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
228+ "owner": "nix-systems",
229+ "repo": "default",
230+ "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
231+ "type": "github"
232+ },
233+ "original": {
234+ "owner": "nix-systems",
235+ "repo": "default",
236+ "type": "github"
237+ }
168238 }
169239 },
170240 "root": "root",
flake.nix+10−2
@@ -7,8 +7,13 @@
77 inputs.nixpkgs.follows = "nixpkgs";
88 };
99 spago-nix.url = "github:purestack-dev/spago-nix";
10+ project-m36-rest = {
11+ url = "github:purestack-dev/project-m36-rest";
12+ inputs.nixpkgs.follows = "nixpkgs";
13+ };
1014 };
11− outputs = { self, nixpkgs, flake-utils, purescript-overlay, spago-nix }:
15+ outputs = { self, nixpkgs, flake-utils, purescript-overlay, spago-nix
16+ , project-m36-rest }:
1217 let supportedSystems = flake-utils.lib.defaultSystems;
1318 in flake-utils.lib.eachSystem supportedSystems (system:
1419 let
@@ -29,7 +34,10 @@
2934 "${pkgs.spago}/bin/spago bundle-app --watch --main Backend --to backend.js";
3035 is_tty = true;
3136 };
32− bun = { command = "${pkgs.bun}/bin/bun run --watch backend.js"; };
37+ bun.command = "${pkgs.bun}/bin/bun run --watch backend.js";
38+ project-m36.command = "${
39+ project-m36-rest.packages.${system}.default
40+ }/bin/project-m36-rest";
3341 };
3442 });
3543 in {
src/Backend.purs+24−2
@@ -1,11 +1,33 @@
11module Backend where
22
33import Prelude
4+import PureStack.Route
45
6+import Bun.Server as Bun
7+import Data.Maybe (Maybe)
58import Effect (Effect)
69import Effect.Console (log)
10+import PureStack.Server as Server
11+
12+type Foo = { a :: String, b :: Maybe Int, c :: Array Number }
13+
14+type API =
15+ ( foo :: "foo" / "bar" / Foo / GET Unit { a :: Int, b :: Number, c :: Foo }
16+ , bar :: "foo" / Middleware "auth" / "qux" / POST (Headers { foo :: String } Unit) { a :: Int, s :: String }
17+ , deep ::
18+ "deep" /
19+ ( something :: "something" / GET Unit (Headers { foo :: String } { a :: String })
20+ )
21+ )
722
823main :: Effect Unit
924main = do
10− log "backend!!!"
11−
25+ log "🍝"
26+ Bun.serve $ Server.run @API identity
27+ { handlers:
28+ { foo: \x -> pure { a: 1, b: 1.0, c: x }
29+ , bar: \i (Headers x _) -> pure { a: 8, s: x.foo }
30+ , deep: { something: pure $ Headers { foo: "foo" } { a: "something" } }
31+ }
32+ , middlewares: { auth: \req -> pure 1 }
33+ }
src/Frontend.purs+46−2
@@ -1,11 +1,55 @@
11module Frontend where
22
3+import Backend
4+import Plum.View
35import Prelude
6+import Prim
47
8+import Data.Array ((:))
9+import Data.Foldable (for_)
10+import Data.Maybe (Maybe(..))
11+import Data.URL as URL
512import Effect (Effect)
6−import Effect.Console (log)
13+import Plum as Plum
14+import PureStack.Client (client)
15+import Unsafe.Coerce (unsafeCoerce)
16+
17+type Model = { newTodo :: String, todos :: Array String }
18+
19+data Msg
20+ = UpdateNewTodo String
21+ | AddNewTodo
722
823main :: Effect Unit
924main = do
10− log "frontend"
25+ Plum.run @Model @Msg "plum"
26+ { init: pure { newTodo: "", todos: [] }
27+ , update: \msg model -> case msg of
28+ UpdateNewTodo newTodo -> pure model { newTodo = newTodo }
29+ AddNewTodo -> pure model { newTodo = "", todos = model.newTodo : model.todos }
30+ , view: \model -> column do
31+ font "sans-serif"
32+
33+ column $ height (Px 30)
34+ text "todos" do
35+ fontSize 80
36+ fontColor (rgb255 184 63 69)
37+ column $ do
38+ width (Px 550)
39+
40+ textInput { onChange: UpdateNewTodo, value: model.newTodo } do
41+ fontSize 24
42+
43+ onKey "Enter" AddNewTodo
44+
45+ for_ model.todos $ \todo -> do
46+ text todo mempty
47+ }
48+
49+url = case URL.fromString "localhost" of
50+ Just x -> x
51+ Nothing -> unsafeCoerce unit
52+
53+x = client @API url
1154
55+foo = x.deep.something
src/Plum.purs+1−1
@@ -23,7 +23,7 @@ type Plum msg model =
2323 , view :: model -> UI msg Unit
2424 }
2525
26−run :: forall msg model. String -> Plum msg model -> Effect Unit
26+run :: forall @model @msg. String -> Plum msg model -> Effect Unit
2727run id plum = do
2828 Web.window >>= Web.document >>= Web.getElementById id <<< Web.toNonElementParentNode >>= case _ of
2929 Just element -> do
src/Plum/View.purs+26−2
@@ -54,9 +54,12 @@ module Plum.View
5454 , onHover
5555 , onMouseDown
5656 , font
57+ , fontColor
5758 , fontSize
5859 , fontWeight
5960 , onClick
61+ , onPress
62+ , onKey
6063 ) where
6164
6265import Prelude
@@ -88,8 +91,10 @@ import Web.DOM.Element as Element
8891import Web.DOM.ParentNode (firstElementChild)
8992import Web.Event.Event (Event)
9093import Web.Event.Event as Event
91−import Web.UIEvent.InputEvent as InputEvent
9294import Web.HTML.HTMLInputElement as HTMLInputElement
95+import Web.UIEvent.InputEvent as InputEvent
96+import Web.UIEvent.KeyboardEvent (KeyboardEvent)
97+import Web.UIEvent.KeyboardEvent as KeyboardEvent
9398
9499type GenericUIView children msg =
95100 { nerves :: Array (Nerve msg)
@@ -249,6 +254,13 @@ n nerve = UI (mempty :: GenericUIView ch msg) { nerves = [ nerve ] } mempty
249254onClick :: forall ch msg. Monoid ch => msg -> GenericUI ch msg Unit
250255onClick msg = n $ OnClick msg
251256
257+onPress :: forall ch msg. Monoid ch => (KeyboardEvent -> msg) -> GenericUI ch msg Unit
258+onPress msg = n $ OnPress msg
259+
260+-- | https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_code_values
261+onKey :: forall ch msg. Monoid ch => String -> msg -> GenericUI ch msg Unit
262+onKey key msg = n $ OnKey key msg
263+
252264m :: forall ch msg. Monoid ch => Meat -> GenericUI ch msg Unit
253265m meat = UI (mempty :: GenericUIView ch msg) { meat = [ meat ] } mempty
254266
@@ -321,6 +333,9 @@ innerShadow cfg = m $ InnerShadow $ ShadowCfg cfg
321333font :: forall ch msg. Monoid ch => String -> GenericUI ch msg Unit
322334font f = m $ Font f
323335
336+fontColor :: forall ch msg. Monoid ch => Color -> GenericUI ch msg Unit
337+fontColor c = m $ FontColor c
338+
324339fontSize :: forall ch msg. Monoid ch => Int -> GenericUI ch msg Unit
325340fontSize f = m $ FontSize f
326341
@@ -401,7 +416,10 @@ instance Renderable Side where
401416 Bottom -> "bottom"
402417 Left -> "left"
403418
404−data Nerve msg = OnClick msg
419+data Nerve msg
420+ = OnClick msg
421+ | OnPress (KeyboardEvent -> msg)
422+ | OnKey String msg
405423
406424newtype WiredNerves = WiredNerves (Object (Event -> Effect Unit))
407425
@@ -415,6 +433,12 @@ instance Monoid WiredNerves where
415433wireNerve :: forall msg. (msg -> Effect Unit) -> Nerve msg -> WiredNerves
416434wireNerve fire nerve = WiredNerves $ case nerve of
417435 OnClick msg -> Object.singleton "click" $ \_ev -> fire msg
436+ OnPress msg -> Object.singleton "keypress" $ \ev -> case KeyboardEvent.fromEvent ev of
437+ Nothing -> pure unit
438+ Just keyboardEvent -> fire $ msg keyboardEvent
439+ OnKey key msg -> Object.singleton "keypress" $ \ev -> case KeyboardEvent.fromEvent ev of
440+ Just keyboardEvent | KeyboardEvent.code keyboardEvent == key -> fire msg
441+ _ -> pure unit
418442
419443type View msg =
420444 { nerves :: Array (Nerve msg)
src/PureStack/Client.pursnew file+268
@@ -0,0 +1,268 @@
1+module PureStack.Client
2+ ( module PureStack.Route
3+ , client
4+ -------------------------------------------------------------------------------------------------
5+ , class ClientAPI
6+ , clientAPI
7+ , class ClientRoute
8+ , clientRoute
9+ , class EzFetchMethod
10+ , ezFetchMethod
11+ , class ToRequest
12+ , toRequest
13+ , class FromResponse
14+ , fromResponse
15+ , class ToPathPiece
16+ , toPathPiece
17+ , class ClientQuery
18+ , clientQuery
19+ ) where
20+
21+import Prelude
22+import PureStack.Route
23+
24+import Control.Alternative (class Alternative, empty)
25+import Data.Argonaut (class DecodeJson, class EncodeJson, Json, decodeJson, encodeJson)
26+import Data.ArrayBuffer.Types (ArrayBuffer)
27+import Data.Either (Either(..))
28+import Data.Map as Map
29+import Data.Maybe (Maybe(..))
30+import Data.Symbol (class IsSymbol, reflectSymbol)
31+import Data.Tuple.Nested ((/\))
32+import Data.URL (URL, addQuery)
33+import Data.URL as URL
34+import Effect.Aff (Aff)
35+import Effect.Aff.Class (class MonadAff, liftAff)
36+import Effect.Aff.HTTP (Method(..), OptionalFields, fetch)
37+import Effect.Aff.HTTP.Header as Response
38+import Effect.Aff.HTTP.Request (Body(..), Credentials(..))
39+import Effect.Aff.HTTP.Request as HTTP
40+import Effect.Aff.HTTP.Response (Response)
41+import Effect.Aff.HTTP.Response as Response
42+import Foreign (Foreign)
43+import Prim.Row as Row
44+import Prim.RowList (class RowToList, RowList)
45+import Prim.RowList as RowList
46+import Record as Record
47+import Record.Builder (Builder)
48+import Record.Builder as Builder
49+import Type.Prelude (Proxy(..))
50+import Unsafe.Coerce (unsafeCoerce)
51+
52+class ClientAPI :: forall k. Row k -> RowList k -> Row Type -> Constraint
53+class ClientAPI row list req | row -> req, list -> row where
54+ clientAPI :: URL -> Builder (Record ()) (Record req)
55+
56+instance ClientAPI () RowList.Nil () where
57+ clientAPI _ = identity
58+
59+instance
60+ ( ClientRoute route req
61+ , Row.Cons field route tail row
62+ , Row.Cons field req reqTail reqs
63+ , ClientAPI tail rest reqTail
64+ , Row.Lacks field reqTail
65+ , IsSymbol field
66+ ) =>
67+ ClientAPI row (RowList.Cons field route rest) reqs where
68+ clientAPI baseUrl =
69+ clientAPI @tail @rest baseUrl >>> Builder.insert (Proxy @field) (clientRoute @route @req baseUrl)
70+
71+client :: forall @row rowList reqs. RowToList row rowList => ClientAPI row rowList reqs => URL -> Record reqs
72+client baseUrl = Builder.buildFromScratch (clientAPI @row @rowList baseUrl)
73+
74+class ClientRoute :: forall k. k -> Type -> Constraint
75+class ClientRoute route request | route -> request where
76+ clientRoute :: URL -> request
77+
78+instance (IsSymbol path, ClientRoute rest req) => ClientRoute (path / rest) req where
79+ clientRoute = \baseUrl -> clientRoute @rest (baseUrl `URL.addSegment` reflectSymbol (Proxy @path))
80+
81+else instance (ClientRoute rest req, ClientQuery row rowList, RowToList row rowList) => ClientRoute (Record row / rest) (Record row -> req) where
82+ clientRoute = \baseUrl rec -> clientRoute @rest (clientQuery @row @rowList rec baseUrl)
83+
84+else instance ClientRoute rest req => ClientRoute (Middleware middleware / rest) req where
85+ clientRoute = clientRoute @rest
86+
87+else instance (ToPathPiece t, ClientRoute rest req) => ClientRoute (t / rest) (t -> req) where
88+ clientRoute = \baseUrl t -> clientRoute @rest (baseUrl `URL.addSegment` toPathPiece t)
89+
90+else instance
91+ ( MethodName (meth req resp)
92+ , EzFetchMethod (meth req resp)
93+ , FromResponse resp
94+ ) =>
95+ ClientRoute (meth Unit (Headers headers resp)) (Aff resp) where
96+ clientRoute = \baseUrl -> do
97+ fetch (ezFetchMethod @(meth req resp)) baseUrl (toRequest unit)
98+ >>= (liftAff <<< fromResponse)
99+ >>= case _ of
100+ Nothing -> empty
101+ Just x -> pure x
102+
103+else instance
104+ ( MethodName (meth req resp)
105+ , EzFetchMethod (meth req resp)
106+ , ToRequest req
107+ , FromResponse resp
108+ ) =>
109+ ClientRoute (meth req (Headers headers resp)) (req -> Aff resp) where
110+ clientRoute = \baseUrl req -> do
111+ fetch (ezFetchMethod @(meth req resp)) baseUrl (toRequest req)
112+ >>= (liftAff <<< fromResponse)
113+ >>= case _ of
114+ Nothing -> empty
115+ Just x -> pure x
116+
117+else instance
118+ ( MethodName (meth req resp)
119+ , EzFetchMethod (meth req resp)
120+ , FromResponse resp
121+ ) =>
122+ ClientRoute (meth Unit resp) (Aff resp) where
123+ clientRoute = \baseUrl -> do
124+ fetch (ezFetchMethod @(meth req resp)) baseUrl (toRequest unit)
125+ >>= (liftAff <<< fromResponse)
126+ >>= case _ of
127+ Nothing -> empty
128+ Just x -> pure x
129+
130+else instance
131+ ( MethodName (meth req resp)
132+ , EzFetchMethod (meth req resp)
133+ , ToRequest req
134+ , FromResponse resp
135+ ) =>
136+ ClientRoute (meth req resp) (req -> Aff resp) where
137+ clientRoute = \baseUrl req -> do
138+ fetch (ezFetchMethod @(meth req resp)) baseUrl (toRequest req)
139+ >>= (liftAff <<< fromResponse)
140+ >>= case _ of
141+ Nothing -> empty
142+ Just x -> pure x
143+else instance (ClientAPI routes routesList req, RowToList routes routesList) => ClientRoute routes (Record req) where
144+ clientRoute baseUrl = Builder.buildFromScratch $ clientAPI @routes @routesList baseUrl
145+
146+class ClientQuery :: Row Type -> RowList Type -> Constraint
147+class ClientQuery row list where
148+ clientQuery :: Record row -> URL -> URL
149+
150+instance ClientQuery row RowList.Nil where
151+ clientQuery _ url = url
152+
153+instance
154+ ( ClientQuery row rest
155+ , ToPathPiece t
156+ , IsSymbol field
157+ , Row.Cons field (Array t) tail row
158+ ) =>
159+ ClientQuery row (RowList.Cons field (Array t) rest) where
160+ clientQuery rec =
161+ flip addQuery ((reflectSymbol (Proxy @field)) /\ map toPathPiece (Record.get (Proxy @field) rec))
162+ <<< clientQuery @row @rest rec
163+else instance
164+ ( ClientQuery row rest
165+ , ToPathPiece t
166+ , IsSymbol field
167+ , Row.Cons field (Maybe t) tail row
168+ ) =>
169+ ClientQuery row (RowList.Cons field (Maybe t) rest) where
170+ clientQuery rec = case (Record.get (Proxy @field) rec) of
171+ Just x ->
172+ flip addQuery ((reflectSymbol (Proxy @field)) /\ toPathPiece x)
173+ <<< clientQuery @row @rest rec
174+ Nothing -> identity
175+else instance
176+ ( ClientQuery row rest
177+ , ToPathPiece t
178+ , IsSymbol field
179+ , Row.Cons field t tail row
180+ ) =>
181+ ClientQuery row (RowList.Cons field t rest) where
182+ clientQuery rec =
183+ flip addQuery ((reflectSymbol (Proxy @field)) /\ toPathPiece (Record.get (Proxy @field) rec))
184+ <<< clientQuery @row @rest rec
185+
186+class ToPathPiece t where
187+ toPathPiece :: t -> String
188+
189+instance ToPathPiece String where
190+ toPathPiece s = s
191+
192+instance ToPathPiece Int where
193+ toPathPiece = show
194+
195+instance ToPathPiece Number where
196+ toPathPiece = show
197+
198+instance ToPathPiece Boolean where
199+ toPathPiece = show
200+
201+class ToRequest r where
202+ toRequest :: r -> Record OptionalFields
203+
204+instance ToRequest Body where
205+ toRequest body = { body, credentials: IncludeCredentials, headers: mempty }
206+
207+instance EncodeJson (Record row) => ToRequest (Record row) where
208+ toRequest body = { body: HTTP.json (jsonToForeign $ encodeJson body), credentials: IncludeCredentials, headers: mempty }
209+
210+instance ToRequest Unit where
211+ toRequest _ = { body: BodyEmpty, credentials: IncludeCredentials, headers: mempty }
212+
213+instance (ToRequest req, RowToList headers headersList, ToHeaders headers headersList) => ToRequest (Headers (Record headers) req) where
214+ toRequest (Headers headers req) =
215+ let
216+ request = toRequest req
217+ in
218+ request
219+ { headers =
220+ Response.Headers (Map.fromFoldableWithIndex $ toHeaders @headers @headersList headers)
221+ <> request.headers
222+
223+ }
224+
225+class FromResponse r where
226+ fromResponse :: Response -> Aff (Maybe r)
227+
228+instance DecodeJson (Record row) => FromResponse (Record row) where
229+ fromResponse resp = do
230+ json <- map foreignToJson (Response.json resp)
231+ pure $ case decodeJson json of
232+ Right x -> Just x
233+ Left _ -> Nothing
234+
235+instance FromResponse Unit where
236+ fromResponse _ = pure $ Just unit
237+
238+instance FromResponse String where
239+ fromResponse = map Just <<< Response.text
240+
241+instance FromResponse ArrayBuffer where
242+ fromResponse = map Just <<< Response.arrayBuffer
243+
244+class EzFetchMethod :: HttpMethod -> Constraint
245+class EzFetchMethod meth where
246+ ezFetchMethod :: Method
247+
248+instance EzFetchMethod (GET req resp) where
249+ ezFetchMethod = GET
250+
251+instance EzFetchMethod (POST req resp) where
252+ ezFetchMethod = POST
253+
254+instance EzFetchMethod (PATCH req resp) where
255+ ezFetchMethod = PATCH
256+
257+instance EzFetchMethod (PUT req resp) where
258+ ezFetchMethod = PUT
259+
260+instance EzFetchMethod (DELETE req resp) where
261+ ezFetchMethod = DELETE
262+
263+jsonToForeign :: Json -> Foreign
264+jsonToForeign = unsafeCoerce
265+
266+foreignToJson :: Foreign -> Json
267+foreignToJson = unsafeCoerce
268+
src/PureStack/Route.purs+122
@@ -10,9 +10,35 @@ module PureStack.Route
1010 , DELETE
1111 , PATCH
1212 , Headers(..)
13+ , class ToHeaders
14+ , toHeaders
15+ , class ToHeader
16+ , toHeader
17+ , class FromHeaders
18+ , fromHeaders
19+ , class FromHeader
20+ , fromHeader
1321 , Middleware
1422 ) where
1523
24+import Prelude
25+
26+import Data.Either (Either(..))
27+import Data.Int as Int
28+import Data.Maybe (Maybe(..))
29+import Data.Symbol (class IsSymbol, reflectSymbol)
30+import Foreign.Object (Object)
31+import Foreign.Object as Object
32+import Prim.Row as Row
33+import Prim.RowList (RowList)
34+import Prim.RowList as RowList
35+import PureStack.Cookie (Cookie)
36+import PureStack.Cookie as Cookie
37+import Record as Record
38+import Record.Builder (Builder)
39+import Record.Builder as Builder
40+import Type.Prelude (Proxy(..))
41+
1642data HttpMethod
1743
1844-- | The `req` can only ever be `Unit`
@@ -52,4 +78,100 @@ infixr 1 type Slash as /
5278
5379data Headers headers r = Headers headers r
5480
81+class FromHeader t where
82+ fromHeader :: String -> Maybe t
83+
84+instance FromHeader Cookie where
85+ fromHeader = Cookie.parse >>> case _ of
86+ Left _ -> Nothing
87+ Right x -> Just x
88+
89+instance FromHeader String where
90+ fromHeader = Just
91+
92+instance FromHeader Int where
93+ fromHeader = Int.fromString
94+
95+class FromHeaders :: Row Type -> RowList Type -> Constraint
96+class FromHeaders row list | list -> row where
97+ fromHeaders :: Object String -> Maybe (Builder {} (Record row))
98+
99+instance FromHeaders () RowList.Nil where
100+ fromHeaders _ = Just $ identity
101+
102+instance
103+ ( FromHeader t
104+ , IsSymbol field
105+ , FromHeaders tail rest
106+ , Row.Cons field (Maybe t) tail row
107+ , Row.Lacks field tail
108+ ) =>
109+ FromHeaders row (RowList.Cons field (Maybe t) rest) where
110+ fromHeaders headers = do
111+ r <- fromHeaders @tail @rest headers
112+ case Object.lookup (reflectSymbol @field Proxy) headers of
113+ Nothing -> pure $ Builder.insert (Proxy @field) Nothing <<< r
114+ Just t -> do
115+ v <- fromHeader t
116+ pure $ Builder.insert (Proxy @field) (Just v) <<< r
117+
118+else instance
119+ ( FromHeader t
120+ , IsSymbol field
121+ , FromHeaders tail rest
122+ , Row.Cons field t tail row
123+ , Row.Lacks field tail
124+ ) =>
125+ FromHeaders row (RowList.Cons field t rest) where
126+ fromHeaders headers = do
127+ t <- Object.lookup (reflectSymbol @field Proxy) headers >>= fromHeader
128+ r <- fromHeaders @tail @rest headers
129+ pure $ Builder.insert (Proxy @field) t <<< r
130+
131+class ToHeader t where
132+ toHeader :: t -> String
133+
134+instance ToHeader Cookie where
135+ toHeader = Cookie.stringify
136+
137+instance ToHeader String where
138+ toHeader s = s
139+
140+instance ToHeader Int where
141+ toHeader = show
142+
143+class ToHeaders :: Row Type -> RowList Type -> Constraint
144+class ToHeaders row list where
145+ toHeaders :: Record row -> Object String
146+
147+instance ToHeaders row RowList.Nil where
148+ toHeaders _ = Object.empty
149+
150+instance
151+ ( ToHeaders row rest
152+ , Row.Cons field (Maybe t) tail row
153+ , IsSymbol field
154+ , ToHeader t
155+ ) =>
156+ ToHeaders row (RowList.Cons field (Maybe t) rest) where
157+ toHeaders headers =
158+ let
159+ rest = toHeaders @row @rest headers
160+ in
161+ case Record.get (Proxy @field) headers of
162+ Nothing -> rest
163+ Just val -> Object.insert (reflectSymbol @field Proxy) (toHeader val) rest
164+
165+else instance
166+ ( ToHeaders row rest
167+ , Row.Cons field t tail row
168+ , IsSymbol field
169+ , ToHeader t
170+ ) =>
171+ ToHeaders row (RowList.Cons field t rest) where
172+ toHeaders headers =
173+ Object.insert (reflectSymbol @field Proxy) (toHeader (Record.get (Proxy @field) headers))
174+ (toHeaders @row @rest headers)
175+
176+data Middleware :: Symbol -> Type
55177data Middleware tag
src/PureStack/Server.purs+13−54
@@ -20,10 +20,7 @@ module PureStack.Server
2020 , serveQuery
2121 , class ParsePathPiece
2222 , parsePathPiece
23− , class FromHeaders
24− , fromHeaders
25− , class FromHeader
26− , fromHeader
23+ , module PureStack.Route
2724 ) where
2825
2926import Prelude
@@ -244,6 +241,18 @@ instance EncodeJson (Record row) => ToResponse (Record row) where
244241instance ToResponse Response where
245242 toResponse resp = resp
246243
244+instance
245+ ( ToResponse resp
246+ , ToHeaders headers headersList
247+ , RowToList headers headersList
248+ ) =>
249+ ToResponse (Headers (Record headers) resp) where
250+ toResponse (Headers headers resp) =
251+ let
252+ Response initResp = toResponse resp
253+ in
254+ Response initResp { headers = Object.union (toHeaders @headers @headersList headers) initResp.headers }
255+
247256class FromRequest r where
248257 fromRequest :: Request -> Aff (Maybe r)
249258
@@ -283,56 +292,6 @@ instance
283292 r' <- r
284293 pure $ Headers (Builder.buildFromScratch headers) r'
285294
286−class FromHeader t where
287− fromHeader :: String -> Maybe t
288−
289−instance FromHeader Cookie where
290− fromHeader = Cookie.parse >>> case _ of
291− Left _ -> Nothing
292− Right x -> Just x
293−
294−instance FromHeader String where
295− fromHeader = Just
296−
297−instance FromHeader Int where
298− fromHeader = Int.fromString
299−
300−class FromHeaders :: Row Type -> RowList Type -> Constraint
301−class FromHeaders row list | list -> row where
302− fromHeaders :: Object String -> Maybe (Builder {} (Record row))
303−
304−instance FromHeaders () RowList.Nil where
305− fromHeaders _ = Just $ identity
306−
307−instance
308− ( FromHeader t
309− , IsSymbol field
310− , FromHeaders tail rest
311− , Row.Cons field (Maybe t) tail row
312− , Row.Lacks field tail
313− ) =>
314− FromHeaders row (RowList.Cons field (Maybe t) rest) where
315− fromHeaders headers = do
316− r <- fromHeaders @tail @rest headers
317− case Object.lookup (reflectSymbol @field Proxy) headers of
318− Nothing -> pure $ Builder.insert (Proxy @field) Nothing <<< r
319− Just t -> do
320− v <- fromHeader t
321− pure $ Builder.insert (Proxy @field) (Just v) <<< r
322−
323−else instance
324− ( FromHeader t
325− , IsSymbol field
326− , FromHeaders tail rest
327− , Row.Cons field t tail row
328− , Row.Lacks field tail
329− ) =>
330− FromHeaders row (RowList.Cons field t rest) where
331− fromHeaders headers = do
332− t <- Object.lookup (reflectSymbol @field Proxy) headers >>= fromHeader
333− r <- fromHeaders @tail @rest headers
334− pure $ Builder.insert (Proxy @field) t <<< r
335−
336295class ParsePathPiece t where
337296 parsePathPiece :: String -> Maybe t
338297