git.iko.soy/purestack-dev/todomvc
on 077efee help sign in
12 files changed+91−714
since splitsnapshots
unifiedsplit
.gitignore−4 flake.lock+3−73 flake.nix+4−14 spago.dhall−1 src/Backend.purs+1−52 src/Frontend.purs+2−51 src/Plum.purs+16−51 src/Plum/View.purs+2−26 src/ProjectM36.purs+7−17 src/PureStack/Client.purs−282 src/PureStack/Route.purs−122 src/PureStack/Server.purs+56−21
.gitignore−4
@@ -11,7 +11,3 @@
1111backend.js
1212frontend.js
1313result
14−index.js
15−m36
16−backend.js.map
17−frontend.js.map
flake.lock+3−73
@@ -34,23 +34,6 @@
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− },
5437 "nixpkgs": {
5538 "locked": {
5639 "lastModified": 1750436177,
@@ -82,28 +65,6 @@
8265 "type": "github"
8366 }
8467 },
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": 1751636615,
95− "narHash": "sha256-8svgD2LdlamTqKqCkdf3ROQLckvnXP+vx3M5a57Kl5U=",
96− "owner": "purestack-dev",
97− "repo": "project-m36-rest",
98− "rev": "c3a0331d62f7f5c9d90012d533e5713095a5fb4e",
99− "type": "github"
100− },
101− "original": {
102− "owner": "purestack-dev",
103− "repo": "project-m36-rest",
104− "type": "github"
105− }
106− },
10768 "purescript-overlay": {
10869 "inputs": {
10970 "flake-compat": "flake-compat",
@@ -146,7 +107,6 @@
146107 "inputs": {
147108 "flake-utils": "flake-utils",
148109 "nixpkgs": "nixpkgs",
149− "project-m36-rest": "project-m36-rest",
150110 "purescript-overlay": "purescript-overlay",
151111 "spago-nix": "spago-nix"
152112 }
@@ -178,11 +138,11 @@
178138 "registry": "registry"
179139 },
180140 "locked": {
181− "lastModified": 1750748667,
182− "narHash": "sha256-7RYmZu/q2zfGuYT0O57wXfS9/PDaHF86Kh7BDURDAZ8=",
141+ "lastModified": 1750694836,
142+ "narHash": "sha256-QxWjqgH4NapAYOCf7DG1t4G0aNO6sYgXZj+r8RAaIPA=",
183143 "owner": "purestack-dev",
184144 "repo": "spago-nix",
185− "rev": "cf33c40bf9174c4ce58637f5d882eef23ac3d3f5",
145+ "rev": "e0c303a269fee603809f458dfb62db974d573968",
186146 "type": "github"
187147 },
188148 "original": {
@@ -191,21 +151,6 @@
191151 "type": "github"
192152 }
193153 },
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− },
209154 "systems": {
210155 "locked": {
211156 "lastModified": 1681028828,
@@ -220,21 +165,6 @@
220165 "repo": "default",
221166 "type": "github"
222167 }
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− }
238168 }
239169 },
240170 "root": "root",
flake.nix+4−14
@@ -7,13 +7,8 @@
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− };
1410 };
15− outputs = { self, nixpkgs, flake-utils, purescript-overlay, spago-nix
16− , project-m36-rest }:
11+ outputs = { self, nixpkgs, flake-utils, purescript-overlay, spago-nix }:
1712 let supportedSystems = flake-utils.lib.defaultSystems;
1813 in flake-utils.lib.eachSystem supportedSystems (system:
1914 let
@@ -26,20 +21,15 @@
2621 processes = {
2722 spago-frontend = {
2823 command =
29− "${pkgs.spago}/bin/spago bundle-app --watch --main Frontend --source-maps --to frontend.js";
24+ "${pkgs.spago}/bin/spago bundle-app --watch --main Frontend --to frontend.js";
3025 is_tty = true;
3126 };
3227 spago-backend = {
3328 command =
34− "${pkgs.spago}/bin/spago bundle-app --watch --main Backend --source-maps --to backend.js";
29+ "${pkgs.spago}/bin/spago bundle-app --watch --main Backend --to backend.js";
3530 is_tty = true;
3631 };
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";
41− proxy.command =
42− "${pkgs.reproxy}/bin/reproxy -l=localhost:3001 --static.enabled --static.rule='*,^/api/(.*),http://localhost:3000/$$1' --assets.location=. --assets.spa --logger.stdout";
32+ bun = { command = "${pkgs.bun}/bin/bun run --watch backend.js"; };
4333 };
4434 });
4535 in {
spago.dhall−1
@@ -15,7 +15,6 @@
1515 , "effect"
1616 , "either"
1717 , "enums"
18− , "exceptions"
1918 , "ezfetch"
2019 , "foldable-traversable"
2120 , "foreign"
src/Backend.purs+1−52
@@ -1,62 +1,11 @@
11module Backend where
22
33import Prelude
4−import PureStack.Route
54
6−import Bun.Server as Bun
7−import Data.Argonaut (class DecodeJson, class EncodeJson)
8−import Data.Maybe (Maybe(..))
9−import Data.Newtype (class Newtype, unwrap, wrap)
10−import Data.URL as URL
115import Effect (Effect)
12−import Effect.Aff (launchAff_)
13−import Effect.Aff.Class (liftAff)
14−import Effect.Class (liftEffect)
156import Effect.Console (log)
16−import Effect.Exception (throw)
17−import ProjectM36 (class RelVar, define, insert, lit, query, rel, relVar, withTransaction)
18−import ProjectM36 as M36
19−import PureStack.Client (class ToRequest)
20−import PureStack.Server (class FromRequest)
21−import PureStack.Server as Server
22−
23−newtype TodoItem = TodoItem { text :: String, done :: Boolean }
24−
25−derive instance Newtype TodoItem _
26−derive newtype instance EncodeJson TodoItem
27−derive newtype instance DecodeJson TodoItem
28−derive newtype instance ToRequest TodoItem
29−derive newtype instance FromRequest TodoItem
30−
31−instance RelVar TodoItem where
32− relVar = "todo-item"
33−
34−type API =
35− ( getTodos :: "todos" / GET Unit (Array TodoItem)
36− , createTodo :: "todos" / POST TodoItem Unit
37− )
387
398main :: Effect Unit
409main = do
41− m36Connection <- case URL.fromString "http://localhost:6543" of
42− Just url -> pure $ M36.Connection url
43− Nothing -> throw "Invalid m36 URL"
44−
45− launchAff_ do
46− liftEffect $ log "Creating relational variables"
47− withTransaction m36Connection do
48− define @TodoItem
49− liftEffect $ log "Done"
10+ log "backend!!!"
5011
51− liftEffect $ Bun.serve $ Server.run @API identity
52− { handlers:
53− { getTodos: do
54− liftAff $ withTransaction m36Connection $ do
55− (map <<< map) wrap <$> query $ rel @TodoItem
56− , createTodo: \todo -> do
57− liftAff $ withTransaction m36Connection $ do
58− insert @TodoItem $ lit [ todo ]
59− pure unit
60− }
61− , middlewares: {}
62− }
src/Frontend.purs+2−51
@@ -1,60 +1,11 @@
11module Frontend where
22
3−import Backend
4−import Plum.View
53import Prelude
6−import Prim
74
8−import Data.Array ((:))
9−import Data.Foldable (for_)
10−import Data.URL as URL
115import Effect (Effect)
12−import Plum as Plum
13−import PureStack.Client (client)
14−
15−type Model = { newTodo :: String, todos :: Array TodoItem }
16−
17−data Msg
18− = UpdateNewTodo String
19− | SetTodos (Array TodoItem)
20− | AddNewTodo
21− | TodoAdded
6+import Effect.Console (log)
227
238main :: Effect Unit
249main = do
25− origin <- Plum.currentOrigin
26− let api = getApi (origin `URL.addSegment` "api")
27− Plum.run @Model @Msg "plum"
28− { init: \runMsg -> do
29− runMsg $ SetTodos <$> api.getTodos
30− pure { newTodo: "", todos: [] }
31− , update: \runMsg msg model -> case msg of
32− UpdateNewTodo newTodo -> pure model { newTodo = newTodo }
33− AddNewTodo -> do
34− runMsg $ api.createTodo (TodoItem { text: model.newTodo, done: false }) $> TodoAdded
35− pure model
36− TodoAdded -> do
37− runMsg $ SetTodos <$> api.getTodos
38− pure model
39− SetTodos todos -> pure model { todos = todos }
40− , view: \model -> column do
41− font "sans-serif"
42−
43− column $ height (Px 30)
44− text "todos" do
45− fontSize 80
46− fontColor (rgb255 184 63 69)
47− column $ do
48− width (Px 550)
49−
50− textInput { onChange: UpdateNewTodo, value: model.newTodo } do
51− fontSize 24
52−
53− onKey "Enter" AddNewTodo
54−
55− for_ model.todos $ \(TodoItem todo) -> do
56− text todo.text mempty
57− }
58−
59−getApi = client @API
10+ log "frontend"
6011
src/Plum.purs+16−51
@@ -1,71 +1,52 @@
1−module Plum (Plum, run, currentOrigin) where
1+module Plum (Plum, run) where
22
33import Prelude
44
55import Data.Array as Array
66import Data.Maybe (Maybe(..))
7−import Data.URL (URL)
8−import Data.URL as URL
97import Effect (Effect)
10−import Effect.Aff (Aff, launchAff_)
11−import Effect.Class (liftEffect)
128import Effect.Console (log)
13−import Effect.Exception (throw)
149import Effect.Ref as Ref
1510import Foreign.Object as Object
1611import Literals.Undefined (undefined)
1712import Plum.View (GenericUI(..), UI, grow)
1813import Snabbdom as Snabbdom
19−import Unsafe.Coerce (unsafeCoerce)
2014import Untagged.Union (asOneOf)
2115import Web.DOM.NonElementParentNode (getElementById) as Web
2216import Web.HTML (window) as Web
23−import Web.HTML.Event.EventTypes (invalid)
2417import Web.HTML.HTMLDocument (toNonElementParentNode) as Web
25−import Web.HTML.Location as Location
26−import Web.HTML.Window (document, location) as Web
18+import Web.HTML.Window (document) as Web
2719
2820type Plum msg model =
29− { init :: (Aff msg -> Effect Unit) -> Effect model
30− , update :: (Aff msg -> Effect Unit) -> msg -> model -> Effect model
21+ { init :: Effect model
22+ , update :: msg -> model -> Effect model
3123 , view :: model -> UI msg Unit
3224 }
3325
34−run :: forall @model @msg. String -> Plum msg model -> Effect Unit
26+run :: forall msg model. String -> Plum msg model -> Effect Unit
3527run id plum = do
3628 Web.window >>= Web.document >>= Web.getElementById id <<< Web.toNonElementParentNode >>= case _ of
3729 Just element -> do
3830 let node = Snabbdom.toVNode element
39− modelRef <- Ref.new { model: unsafeCoerce undefined, lastNode: node }
4031 patch <- Snabbdom.init
32+ initModel <- plum.init
33+ modelRef <- Ref.new { model: initModel, lastNode: node }
4134 let
42− fire =
43− ( \msg -> do
44− log "in"
45− { model: m, lastNode } <- Ref.read modelRef
46− m_ <- plum.update
47− ( \aff ->
48− launchAff_
49− ( do
50− msg' <- aff
51− liftEffect $ fire msg'
52− pure unit
53− )
54− )
55− msg
56− m
57− node' <- view m_
58− newNode <- patch lastNode node'
59− Ref.write { model: m_, lastNode: newNode } modelRef
60− pure unit
61− )
6235 view model = do
6336 let UI { children } _ = plum.view model
6437 case Array.uncons children of
6538 Just { head: child } -> do
6639 let
6740 { node, style } = grow
68− fire
41+ ( \msg -> do
42+ log "in"
43+ { model: m, lastNode } <- Ref.read modelRef
44+ m_ <- plum.update msg m
45+ node <- view m_
46+ newNode <- patch lastNode node
47+ Ref.write { model: m_, lastNode: newNode } modelRef
48+ pure unit
49+ )
6950 child
7051 pure $
7152 Snabbdom.h "div"
@@ -94,15 +75,6 @@ run id plum = do
9475 }
9576 []
9677 (asOneOf undefined)
97− initModel <- plum.init
98− ( \aff ->
99− launchAff_
100− ( do
101− msg' <- aff
102− liftEffect $ fire msg'
103− pure unit
104− )
105− )
10678 newNode <- view initModel
10779 newNode_ <- patch node newNode
10880 log "patched"
@@ -112,10 +84,3 @@ run id plum = do
11284
11385outerStyle :: String
11486outerStyle = "html,body{height:100%;padding:0;margin:0;}"
115−
116−currentOrigin :: Effect URL
117−currentOrigin = do
118− s <- Web.window >>= Web.location >>= Location.origin
119− case URL.fromString s of
120− Nothing -> throw "window.location.origin returned an invalid url"
121− Just url -> pure url
src/Plum/View.purs+2−26
@@ -54,12 +54,9 @@ module Plum.View
5454 , onHover
5555 , onMouseDown
5656 , font
57− , fontColor
5857 , fontSize
5958 , fontWeight
6059 , onClick
61− , onPress
62− , onKey
6360 ) where
6461
6562import Prelude
@@ -91,10 +88,8 @@ import Web.DOM.Element as Element
9188import Web.DOM.ParentNode (firstElementChild)
9289import Web.Event.Event (Event)
9390import Web.Event.Event as Event
94−import Web.HTML.HTMLInputElement as HTMLInputElement
9591import Web.UIEvent.InputEvent as InputEvent
96−import Web.UIEvent.KeyboardEvent (KeyboardEvent)
97−import Web.UIEvent.KeyboardEvent as KeyboardEvent
92+import Web.HTML.HTMLInputElement as HTMLInputElement
9893
9994type GenericUIView children msg =
10095 { nerves :: Array (Nerve msg)
@@ -254,13 +249,6 @@ n nerve = UI (mempty :: GenericUIView ch msg) { nerves = [ nerve ] } mempty
254249onClick :: forall ch msg. Monoid ch => msg -> GenericUI ch msg Unit
255250onClick msg = n $ OnClick msg
256251
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−
264252m :: forall ch msg. Monoid ch => Meat -> GenericUI ch msg Unit
265253m meat = UI (mempty :: GenericUIView ch msg) { meat = [ meat ] } mempty
266254
@@ -333,9 +321,6 @@ innerShadow cfg = m $ InnerShadow $ ShadowCfg cfg
333321font :: forall ch msg. Monoid ch => String -> GenericUI ch msg Unit
334322font f = m $ Font f
335323
336−fontColor :: forall ch msg. Monoid ch => Color -> GenericUI ch msg Unit
337−fontColor c = m $ FontColor c
338−
339324fontSize :: forall ch msg. Monoid ch => Int -> GenericUI ch msg Unit
340325fontSize f = m $ FontSize f
341326
@@ -416,10 +401,7 @@ instance Renderable Side where
416401 Bottom -> "bottom"
417402 Left -> "left"
418403
419−data Nerve msg
420− = OnClick msg
421− | OnPress (KeyboardEvent -> msg)
422− | OnKey String msg
404+data Nerve msg = OnClick msg
423405
424406newtype WiredNerves = WiredNerves (Object (Event -> Effect Unit))
425407
@@ -433,12 +415,6 @@ instance Monoid WiredNerves where
433415wireNerve :: forall msg. (msg -> Effect Unit) -> Nerve msg -> WiredNerves
434416wireNerve fire nerve = WiredNerves $ case nerve of
435417 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
442418
443419type View msg =
444420 { nerves :: Array (Nerve msg)
src/ProjectM36.purs+7−17
@@ -65,13 +65,11 @@ import Data.Set as Set
6565import Data.Traversable (traverse)
6666import Data.Tuple.Nested (type (/\), (/\))
6767import Data.URL (URL, addSegment)
68−import Debug (traceM)
6968import Effect.Aff (Aff, bracket, error, throwError)
7069import Effect.Aff.HTTP (Method(..), fetch)
7170import Effect.Aff.HTTP.Request as Request
7271import Effect.Aff.HTTP.Response (Response)
7372import Effect.Aff.HTTP.Response as Response
74−import Effect.Class.Console (log)
7573import Foreign (Foreign, unsafeFromForeign, unsafeToForeign)
7674import Foreign.Object (Object)
7775import Foreign.Object as Object
@@ -94,7 +92,7 @@ data Expr
9492
9593instance EncodeJson Expr where
9694 encodeJson = case _ of
97− Lit l -> encodeJson l
95+ Lit l -> l
9896 Join x y -> encodeJson { join: [ x, y ] }
9997 Union x y -> encodeJson { union: [ x, y ] }
10098 Project proj expr -> encodeJson { proj: { proj: proj, expr: expr } }
@@ -118,14 +116,8 @@ class LiteralRelation r fields | r -> fields where
118116instance (RowList.RowToList fields fields', Relatable fields fields') => LiteralRelation (Array (Record fields)) fields where
119117 lit r = Rel $ Lit $ encodeJson { lit: { attrs: attributeTypes @fields @fields', vals: encodeRelation @fields @fields' <$> r } }
120118
121−else instance
122− ( Newtype r (Record fields)
123− , LiteralRelation (Array (Record field)) fields
124− , RowToList fields fields'
125− , Relatable fields fields'
126− ) =>
127− LiteralRelation (Array r) fields where
128− lit = map unwrap >>> lit
119+else instance (LiteralRelation (Record fields) fields, Newtype r (Record fields)) => LiteralRelation r fields where
120+ lit = unwrap >>> lit
129121
130122rel :: forall @rel fields. Newtype rel (Record fields) => RelVar rel => Rel fields
131123rel = Rel $ RelationVariable (relVar @rel)
@@ -482,7 +474,7 @@ define
482474 => Transaction Unit
483475define = Transaction $ ReaderT $ \{ conn: Connection c, sess } -> do
484476 fetch POST (c `addSegment` "transaction" `addSegment` sessionString sess)
485− { body: Request.json $ jsonToForeign $ encodeJson { define: { name: relVar @rel, attrs: attributeTypes @fields @fields' } } }
477+ { body: Request.json { define: { name: relVar @rel, attrs: Object.fromFoldable (attributeTypes @fields @fields') } } }
486478 >>= Response.guardStatusOk
487479 pure unit
488480
@@ -493,9 +485,8 @@ insert
493485 => Rel fields
494486 -> Transaction Unit
495487insert (Rel r) = Transaction $ ReaderT $ \{ conn: Connection c, sess } -> do
496− traceM $ jsonToForeign $ encodeJson { insert: { name: relVar @rel, rel: r } }
497488 fetch POST (c `addSegment` "transaction" `addSegment` sessionString sess)
498− { body: Request.json $ jsonToForeign $ encodeJson { insert: { name: relVar @rel, rel: r } } }
489+ { body: Request.json { insert: { name: relVar @rel, rel: jsonToForeign (encodeJson r) } } }
499490 >>= Response.guardStatusOk
500491 pure unit
501492
@@ -507,7 +498,7 @@ query
507498 -> Transaction (Array (Record fields))
508499query (Rel r) = Transaction $ ReaderT $ \{ conn: Connection c, sess } -> do
509500 resp <- fetch POST (c `addSegment` "transaction" `addSegment` sessionString sess)
510− { body: Request.json $ jsonToForeign $ encodeJson { query: r } }
501+ { body: Request.json { query: jsonToForeign (encodeJson r) } }
511502 Response.guardStatusOk resp
512503 j <- json resp
513504 case decodeRelation @fields @fields' j of
@@ -519,8 +510,7 @@ decodeRelation
519510 . BuildRelation fields fields'
520511 => Json
521512 -> Either JsonDecodeError (Array (Record fields))
522−decodeRelation j = do
523− decodeRelationJson j >>= traverse (decodeRelationFromObject @fields @fields')
513+decodeRelation j = decodeRelationJson j >>= traverse (decodeRelationFromObject @fields @fields')
524514
525515jsonToForeign :: Json -> Foreign
526516jsonToForeign = unsafeCoerce
src/PureStack/Client.pursdeleted−282
@@ -1,282 +0,0 @@
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, JsonDecodeError, 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 FromResponse Json where
229− fromResponse resp = map (Just <<< foreignToJson) (Response.json resp)
230−
231−instance DecodeJson x => FromResponse (Array x) where
232− fromResponse resp = do
233− fromResponse resp <#> case _ of
234− Nothing -> Nothing
235− Just json ->
236− case decodeJson json of
237− Right x -> Just x
238− Left _ -> Nothing
239−
240−instance DecodeJson (Record row) => FromResponse (Record row) where
241− fromResponse resp = do
242− fromResponse resp <#> case _ of
243− Nothing -> Nothing
244− Just json ->
245− case decodeJson json of
246− Right x -> Just x
247− Left _ -> Nothing
248−
249−instance FromResponse Unit where
250− fromResponse _ = pure $ Just unit
251−
252−instance FromResponse String where
253− fromResponse = map Just <<< Response.text
254−
255−instance FromResponse ArrayBuffer where
256− fromResponse = map Just <<< Response.arrayBuffer
257−
258−class EzFetchMethod :: HttpMethod -> Constraint
259−class EzFetchMethod meth where
260− ezFetchMethod :: Method
261−
262−instance EzFetchMethod (GET req resp) where
263− ezFetchMethod = GET
264−
265−instance EzFetchMethod (POST req resp) where
266− ezFetchMethod = POST
267−
268−instance EzFetchMethod (PATCH req resp) where
269− ezFetchMethod = PATCH
270−
271−instance EzFetchMethod (PUT req resp) where
272− ezFetchMethod = PUT
273−
274−instance EzFetchMethod (DELETE req resp) where
275− ezFetchMethod = DELETE
276−
277−jsonToForeign :: Json -> Foreign
278−jsonToForeign = unsafeCoerce
279−
280−foreignToJson :: Foreign -> Json
281−foreignToJson = unsafeCoerce
282−
src/PureStack/Route.purs−122
@@ -10,35 +10,9 @@ 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
2113 , Middleware
2214 ) where
2315
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−
4216data HttpMethod
4317
4418-- | The `req` can only ever be `Unit`
@@ -78,100 +52,4 @@ infixr 1 type Slash as /
7852
7953data Headers headers r = Headers headers r
8054
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
17755data Middleware tag
src/PureStack/Server.purs+56−21
@@ -20,7 +20,10 @@ module PureStack.Server
2020 , serveQuery
2121 , class ParsePathPiece
2222 , parsePathPiece
23− , module PureStack.Route
23+ , class FromHeaders
24+ , fromHeaders
25+ , class FromHeader
26+ , fromHeader
2427 ) where
2528
2629import Prelude
@@ -140,7 +143,7 @@ else instance (ToResponse resp, MethodName (meth Unit resp)) => ServeRoute (meth
140143 pure $ \(Nt nt) handler _middlewares _req -> do
141144 resp <- nt handler
142145 pure $ toResponse resp
143−else instance (FromRequest req, MethodName (meth resp Unit)) => ServeRoute (meth req Unit) (req -> m Unit) middlewares m where
146+else instance (FromRequest req, MethodName (meth resp Unit)) => ServeRoute (meth resp Unit) (req -> m Unit) middlewares m where
144147 serveRoute { path, verb } = do
145148 assert $ verb == methodName @(meth resp Unit)
146149 assert $ path == []
@@ -236,29 +239,11 @@ class ToResponse r where
236239 toResponse :: r -> Response
237240
238241instance EncodeJson (Record row) => ToResponse (Record row) where
239− toResponse rec = toResponse $ encodeJson rec
240−
241−instance EncodeJson t => ToResponse (Array t) where
242− toResponse = encodeJson >>> toResponse
242+ toResponse rec = Response { body: JsonResponseBody $ encodeJson rec, status: 200, statusText: "OK", headers: Object.empty }
243243
244244instance ToResponse Response where
245245 toResponse resp = resp
246246
247−instance ToResponse Json where
248− toResponse body = Response { body: JsonResponseBody body, status: 200, statusText: "OK", headers: Object.empty }
249−
250−instance
251− ( ToResponse resp
252− , ToHeaders headers headersList
253− , RowToList headers headersList
254− ) =>
255− ToResponse (Headers (Record headers) resp) where
256− toResponse (Headers headers resp) =
257− let
258− Response initResp = toResponse resp
259− in
260− Response initResp { headers = Object.union (toHeaders @headers @headersList headers) initResp.headers }
261−
262247class FromRequest r where
263248 fromRequest :: Request -> Aff (Maybe r)
264249
@@ -298,6 +283,56 @@ instance
298283 r' <- r
299284 pure $ Headers (Builder.buildFromScratch headers) r'
300285
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+
301336class ParsePathPiece t where
302337 parsePathPiece :: String -> Maybe t
303338