Skip to content

Commit 6e39818

Browse files
committed
Support for monad transformers; resolves #7.
1 parent 15b4954 commit 6e39818

File tree

11 files changed

+292
-99
lines changed

11 files changed

+292
-99
lines changed

examples/transformer/Main.purs

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
module Payload.Examples.Transformer.Main where
2+
3+
import Prelude
4+
5+
import Control.Monad.Reader.Trans (ReaderT, ask, asks, runReaderT)
6+
import Data.Either (Either, note)
7+
import Data.Foldable (find)
8+
import Effect (Effect)
9+
import Effect.Aff (Aff, launchAff_)
10+
import Effect.Aff.Class (liftAff)
11+
import Node.HTTP as HTTP
12+
import Payload.Headers as Headers
13+
import Payload.ResponseTypes (Response)
14+
import Payload.Server as Payload
15+
import Payload.Server.Guards as Guards
16+
import Payload.Server.Response as Resp
17+
import Payload.Spec (type (:), Nil, GET, Guards, Spec(Spec))
18+
19+
type Env =
20+
{ adminKey :: String
21+
, dbConnectionString :: String
22+
}
23+
24+
spec ::Spec
25+
{ guards ::
26+
{ adminKeyMatch :: Unit
27+
}
28+
, routes ::
29+
{ env :: GET "/env" { guards :: Guards ("adminKeyMatch" : Nil), response :: Env }
30+
}
31+
}
32+
spec = Spec
33+
34+
guards ::
35+
{ adminKeyMatch ::
36+
HTTP.Request -> ReaderT Env Aff (Either (Response String) Unit)
37+
}
38+
guards =
39+
{ adminKeyMatch: \request -> do
40+
expected <- asks _.adminKey
41+
headers <- liftAff $ Guards.headers request
42+
let provided = find (\x -> x == expected) $ Headers.lookup "x-admin-key" headers
43+
pure $ void $ note (Resp.unauthorized $ "\"x-admin-key\" header must match configured secret key (\"" <> expected <> "\")") provided
44+
}
45+
46+
handlers ::
47+
{ env ::
48+
{ guards :: { adminKeyMatch :: Unit } } -> ReaderT Env Aff Env
49+
}
50+
handlers = { env: const ask }
51+
52+
main :: Effect Unit
53+
main =
54+
launchAff_ $
55+
Payload.startGuarded'
56+
(flip runReaderT { adminKey: "secret", dbConnectionString: "postgresql://postgres@localhost/postgres" })
57+
Payload.defaultOpts
58+
spec
59+
{ guards, handlers }

examples/transformer/Test.purs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
module Payload.Examples.Transformer.Test where
2+
3+
import Prelude
4+
5+
import Control.Monad.Reader.Trans (runReaderT)
6+
import Payload.Examples.Transformer.Main (guards, handlers, spec)
7+
import Payload.Headers (empty, set) as Headers
8+
import Payload.Test.Helpers (respMatches, withServer')
9+
import Payload.Test.Helpers as Helpers
10+
import Simple.JSON (writeJSON)
11+
import Test.Unit (TestSuite, suite, test)
12+
import Test.Unit.Assert as Assert
13+
14+
env :: { adminKey :: String, dbConnectionString :: String }
15+
env =
16+
{ adminKey: "foo"
17+
, dbConnectionString: "bar"
18+
}
19+
20+
tests :: TestSuite
21+
tests = do
22+
let withApi = withServer' (flip runReaderT env) spec { guards, handlers }
23+
suite "Transformer" do
24+
let host = "http://localhost:3000"
25+
path = "/env"
26+
test ("GET " <> path <> " with admin key succeeds") $ withApi do
27+
res <- Helpers.get_
28+
host
29+
path
30+
$ Headers.empty # Headers.set "x-admin-key" "foo"
31+
respMatches
32+
{ status: 200
33+
, body: writeJSON env
34+
}
35+
res
36+
test ("GET " <> path <> " with invalid x-admin-key header fails with 401 Unauthorized") $ withApi do
37+
res <- Helpers.get_
38+
host
39+
path
40+
$ Headers.empty # Headers.set "x-admin-key" "xx"
41+
Assert.equal 401 res.status
42+
test ("GET " <> path <> " without x-admin-key header fails with 401 Unauthorized") $ withApi do
43+
res <- (Helpers.request host).get path
44+
Assert.equal 401 res.status

src/Payload/ResponseTypes.purs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,5 +58,5 @@ instance showResponseBody :: Show ResponseBody where
5858
show (StreamBody _) = "StreamBody"
5959

6060
-- | Internally handlers and guards all de-sugar into this type.
61-
type Result a = ExceptT Failure Aff a
61+
type Result m = ExceptT Failure m
6262

src/Payload/Server.purs

Lines changed: 64 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ module Payload.Server
44
, start_
55
, startGuarded
66
, startGuarded_
7+
, startGuarded'
78
, Options
89
, defaultOpts
910
, LogLevel(..)
@@ -23,9 +24,9 @@ import Data.String as String
2324
import Effect (Effect)
2425
import Effect.Aff (Aff)
2526
import Effect.Aff as Aff
26-
import Effect.Class (liftEffect)
27+
import Effect.Class (class MonadEffect, liftEffect)
2728
import Effect.Console (log)
28-
import Effect.Exception (Error)
29+
import Effect.Exception (Error, message)
2930
import Node.HTTP as HTTP
3031
import Node.URL (URL)
3132
import Node.URL as Url
@@ -88,7 +89,7 @@ type Logger =
8889
-- | Start server with default options, ignoring unexpected startup errors.
8990
launch
9091
:: forall routesSpec handlers
91-
. Routable routesSpec {} handlers {}
92+
. Routable routesSpec {} handlers {} Aff
9293
=> Spec routesSpec
9394
-> handlers
9495
-> Effect Unit
@@ -97,7 +98,7 @@ launch routeSpec handlers = Aff.launchAff_ (start_ routeSpec handlers)
9798
-- | Start server with default options and given route spec and handlers (no guards).
9899
start_
99100
:: forall routesSpec handlers
100-
. Routable routesSpec {} handlers {}
101+
. Routable routesSpec {} handlers {} Aff
101102
=> Spec routesSpec
102103
-> handlers
103104
-> Aff (Either String Server)
@@ -106,7 +107,7 @@ start_ = start defaultOpts
106107
-- | Start server with given routes and handlers (no guards).
107108
start
108109
:: forall routesSpec handlers
109-
. Routable routesSpec {} handlers {}
110+
. Routable routesSpec {} handlers {} Aff
110111
=> Options
111112
-> Spec routesSpec
112113
-> handlers
@@ -118,7 +119,7 @@ start opts routeSpec handlers = startGuarded opts api { handlers, guards: {} }
118119
-- | Start server with default options and given spec, handlers, and guards.
119120
startGuarded_
120121
:: forall routesSpec guardsSpec handlers guards
121-
. Routable routesSpec guardsSpec handlers guards
122+
. Routable routesSpec guardsSpec handlers guards Aff
122123
=> Spec { routes :: routesSpec, guards :: guardsSpec }
123124
-> { handlers :: handlers, guards :: guards }
124125
-> Aff (Either String Server)
@@ -127,25 +128,37 @@ startGuarded_ = startGuarded defaultOpts
127128
-- | Start server with given spec, handlers, and guards.
128129
startGuarded
129130
:: forall routesSpec guardsSpec handlers guards
130-
. Routable routesSpec guardsSpec handlers guards
131+
. Routable routesSpec guardsSpec handlers guards Aff
131132
=> Options
132133
-> Spec { guards :: guardsSpec, routes :: routesSpec }
133134
-> { handlers :: handlers, guards :: guards }
134135
-> Aff (Either String Server)
135-
startGuarded opts apiSpec api = do
136+
startGuarded = startGuarded' identity
137+
138+
-- | Start server with given monad transformation, spec, handlers, and guards.
139+
startGuarded'
140+
:: forall routesSpec guardsSpec handlers guards m
141+
. MonadEffect m
142+
=> Routable routesSpec guardsSpec handlers guards m
143+
=> (m ~> Aff)
144+
-> Options
145+
-> Spec { guards :: guardsSpec, routes :: routesSpec }
146+
-> { handlers :: handlers, guards :: guards }
147+
-> Aff (Either String Server)
148+
startGuarded' runM opts apiSpec api = do
136149
let cfg = mkConfig opts
137150
case mkRouter apiSpec api of
138151
Right routerTrie -> do
139-
server <- Server <$> (liftEffect $ HTTP.createServer (handleRequest cfg routerTrie))
152+
server <- Server <$> (liftEffect $ HTTP.createServer (handleRequest runM cfg routerTrie))
140153
let httpOpts = Record.delete (Proxy :: Proxy "logLevel") opts
141154
listenResult <- listen cfg server httpOpts
142155
pure (const server <$> listenResult)
143156
Left err -> pure (Left err)
144157

145-
dumpRoutes :: Trie HandlerEntry -> Effect Unit
158+
dumpRoutes :: forall m. Trie (HandlerEntry m) -> Effect Unit
146159
dumpRoutes = log <<< showRoutes
147160

148-
showRoutes :: Trie HandlerEntry -> String
161+
showRoutes :: forall m. Trie (HandlerEntry m) -> String
149162
showRoutes routerTrie = Trie.dumpEntries (_.route <$> routerTrie)
150163

151164
mkConfig :: Options -> Config
@@ -166,29 +179,54 @@ mkLogger logLevel = { log: log_, logDebug, logError }
166179
logError | logLevel >= LogError = log
167180
logError = const $ pure unit
168181

169-
handleRequest :: Config -> Trie HandlerEntry -> HTTP.Request -> HTTP.Response -> Effect Unit
170-
handleRequest cfg@{ logger } routerTrie req res = do
182+
handleRequest
183+
:: forall m
184+
. MonadEffect m
185+
=> (m ~> Aff)
186+
-> Config
187+
-> Trie (HandlerEntry m)
188+
-> HTTP.Request
189+
-> HTTP.Response
190+
-> Effect Unit
191+
handleRequest runM cfg@{ logger } routerTrie req res = do
171192
let url = Url.parse (HTTP.requestURL req)
172193
logger.logDebug (HTTP.requestMethod req <> " " <> show (url.path))
173194
case requestUrl req of
174-
Right reqUrl -> runHandlers cfg routerTrie reqUrl req res
195+
Right reqUrl -> runHandlers runM cfg routerTrie reqUrl req res
175196
Left err -> do
176197
writeResponse res (internalError $ StringBody $ "Path could not be decoded: " <> show err)
177198

178-
runHandlers :: Config -> Trie HandlerEntry -> RequestUrl
179-
-> HTTP.Request -> HTTP.Response -> Effect Unit
180-
runHandlers { logger } routerTrie reqUrl req res = do
181-
let (matches :: List HandlerEntry) = Trie.lookup (reqUrl.method : reqUrl.path) routerTrie
199+
runHandlers
200+
:: forall m
201+
. MonadEffect m
202+
=> (m ~> Aff)
203+
-> Config
204+
-> Trie (HandlerEntry m)
205+
-> RequestUrl
206+
-> HTTP.Request
207+
-> HTTP.Response
208+
-> Effect Unit
209+
runHandlers runM { logger } routerTrie reqUrl req res = do
210+
let (matches :: List (HandlerEntry m)) = Trie.lookup (reqUrl.method : reqUrl.path) routerTrie
182211
let matchesStr = String.joinWith "\n" (Array.fromFoldable $ (showRouteUrl <<< _.route) <$> matches)
183212
logger.logDebug $ showUrl reqUrl <> " -> " <> show (List.length matches) <> " matches:\n" <> matchesStr
184-
Aff.launchAff_ $ do
185-
outcome <- handleNext Nothing matches
186-
case outcome of
187-
(Forward msg) -> do
188-
liftEffect $ writeResponse res (Response.notFound (StringBody ""))
189-
_ -> pure unit
213+
Aff.runAff_
214+
(
215+
case _ of
216+
Left e -> liftEffect do
217+
logger.logError $ message e
218+
writeResponse res (internalError (StringBody ""))
219+
_ ->
220+
pure unit
221+
)
222+
$ runM do
223+
outcome <- handleNext Nothing matches
224+
case outcome of
225+
(Forward msg) -> do
226+
liftEffect $ writeResponse res (Response.notFound (StringBody ""))
227+
_ -> pure unit
190228
where
191-
handleNext :: Maybe Outcome -> List HandlerEntry -> Aff Outcome
229+
handleNext :: Maybe Outcome -> List (HandlerEntry m) -> m Outcome
192230
handleNext Nothing ({ handler } : rest) = do
193231
outcome <- handler reqUrl req res
194232
handleNext (Just outcome) rest
@@ -203,7 +241,7 @@ runHandlers { logger } routerTrie reqUrl req res = do
203241
pure (Forward "No match could handle")
204242
handleNext _ Nil = pure (Forward "No match could handle")
205243

206-
showMatches :: List HandlerEntry -> String
244+
showMatches :: forall m. List (HandlerEntry m) -> String
207245
showMatches matches = " " <> String.joinWith "\n " (Array.fromFoldable $ showMatch <$> matches)
208246
where
209247
showMatch = showRouteUrl <<< _.route

src/Payload/Server/Guards.purs

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import Data.Map (Map)
1818
import Data.Symbol (class IsSymbol)
1919
import Data.Tuple (Tuple)
2020
import Effect.Aff (Aff)
21+
import Effect.Aff.Class (class MonadAff)
2122
import Foreign.Object as Object
2223
import Node.HTTP as HTTP
2324
import Payload.Headers (Headers)
@@ -38,7 +39,7 @@ import Type.Proxy (Proxy(..))
3839
-- | Guards can also fail and return a response directly, by returning
3940
-- | Either.
4041
class ToGuardVal a b where
41-
toGuardVal :: a -> Result b
42+
toGuardVal :: forall m. MonadAff m => a -> Result m b
4243

4344
instance toGuardValEitherFailureVal
4445
:: ToGuardVal (Either Failure a) a where
@@ -76,35 +77,37 @@ rawRequest req = pure req
7677
cookies :: HTTP.Request -> Aff (Map String String)
7778
cookies req = pure (Cookies.requestCookies req)
7879

79-
type GuardFn a = HTTP.Request -> Aff a
80+
type GuardFn m a = HTTP.Request -> m a
8081

8182
class RunGuards
8283
(guardNames :: GuardList)
8384
(guardsSpec :: Row Type)
8485
(allGuards :: Row Type)
8586
(results :: Row Type)
86-
(routeGuardSpec :: Row Type) | guardNames guardsSpec allGuards -> routeGuardSpec where
87-
runGuards :: Guards guardNames
87+
(routeGuardSpec :: Row Type)
88+
m | guardNames guardsSpec allGuards -> routeGuardSpec where
89+
runGuards :: Guards guardNames
8890
-> GuardTypes (Record guardsSpec)
8991
-> Record allGuards
9092
-> Record results
9193
-> HTTP.Request
92-
-> Result (Record routeGuardSpec)
94+
-> Result m (Record routeGuardSpec)
9395

94-
instance runGuardsNil :: RunGuards GNil guardsSpec allGuards routeGuardSpec routeGuardSpec where
96+
instance runGuardsNil :: Monad m => RunGuards GNil guardsSpec allGuards routeGuardSpec routeGuardSpec m where
9597
runGuards _ _ allGuards results req = pure results
9698

9799
instance runGuardsCons ::
98100
( IsSymbol name
99101
, Row.Cons name guardVal guardsSpec' guardsSpec
100-
, Row.Cons name (GuardFn guardRes) allGuards' allGuards
102+
, Row.Cons name (GuardFn m guardRes) allGuards' allGuards
101103
, Row.Cons name guardVal results newResults
102104
, Row.Lacks name results
103105
, ToGuardVal guardRes guardVal
104-
, RunGuards rest guardsSpec allGuards newResults routeGuardSpec
105-
) => RunGuards (GCons name rest) guardsSpec allGuards results routeGuardSpec where
106+
, RunGuards rest guardsSpec allGuards newResults routeGuardSpec m
107+
, MonadAff m
108+
) => RunGuards (GCons name rest) guardsSpec allGuards results routeGuardSpec m where
106109
runGuards _ _ allGuards results req = do
107-
let (guardHandler :: GuardFn guardRes) = Record.get (Proxy :: Proxy name) (to allGuards)
110+
let (guardHandler :: GuardFn m guardRes) = Record.get (Proxy :: Proxy name) (to allGuards)
108111
(guardHandlerResult :: guardRes) <- lift $ guardHandler req
109112
(guardResult :: guardVal) <- toGuardVal guardHandlerResult
110113
let newResults = Record.insert (Proxy :: Proxy name) guardResult results

0 commit comments

Comments
 (0)