/
Server.purs
252 lines (220 loc) · 8.46 KB
/
Server.purs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
module Payload.Server
( launch
, start
, start_
, startGuarded
, startGuarded_
, Options
, defaultOpts
, LogLevel(..)
, Server
, close
) where
import Prelude
import Data.Array as Array
import Data.Either (Either(..))
import Data.List (List(..), (:))
import Data.List as List
import Data.Maybe (Maybe(..), fromMaybe, maybe)
import Data.Nullable (toMaybe)
import Data.String as String
import Data.Symbol (SProxy(..))
import Effect (Effect)
import Effect.Aff (Aff)
import Effect.Aff as Aff
import Effect.Class (liftEffect)
import Effect.Console (log)
import Effect.Exception (Error)
import Node.HTTP as HTTP
import Node.URL (URL)
import Node.URL as Url
import Payload.Internal.UrlParsing (Segment)
import Payload.ResponseTypes (ResponseBody(..))
import Payload.Server.Internal.Request (RequestUrl)
import Payload.Server.Internal.ServerResponse (writeResponse)
import Payload.Server.Internal.Trie (Trie)
import Payload.Server.Internal.Trie as Trie
import Payload.Server.Internal.UrlString (urlToSegments)
import Payload.Server.Response (internalError)
import Payload.Server.Response as Response
import Payload.Server.Routable (class Routable, HandlerEntry, Outcome(..), mkRouter)
import Payload.Spec (Spec(Spec))
import Record as Record
type Options =
{ backlog :: Maybe Int
, hostname :: String
, port :: Int
, logLevel :: LogLevel }
data LogLevel = LogSilent | LogError | LogNormal | LogDebug
instance eqLogLevel :: Eq LogLevel where
eq LogSilent LogSilent = true
eq LogError LogError = true
eq LogNormal LogNormal = true
eq LogDebug LogDebug = true
eq _ _ = false
instance ordLogLevel :: Ord LogLevel where
compare l1 l2 = rank l1 `compare` rank l2
where
rank :: LogLevel -> Int
rank LogSilent = 0
rank LogError = 1
rank LogNormal = 2
rank LogDebug = 3
defaultOpts :: Options
defaultOpts =
{ backlog: Nothing
, hostname: "localhost"
, port: 3000
, logLevel: LogNormal }
newtype Server = Server HTTP.Server
type Config =
{ logger :: Logger }
type Logger =
{ log :: String -> Effect Unit
, logDebug :: String -> Effect Unit
, logError :: String -> Effect Unit
}
-- | Start server with default options, ignoring unexpected startup errors.
launch
:: forall routesSpec handlers
. Routable routesSpec {} handlers {}
=> Spec routesSpec
-> handlers
-> Effect Unit
launch routeSpec handlers = Aff.launchAff_ (start_ routeSpec handlers)
-- | Start server with default options and given route spec and handlers (no guards).
start_
:: forall routesSpec handlers
. Routable routesSpec {} handlers {}
=> Spec routesSpec
-> handlers
-> Aff (Either String Server)
start_ = start defaultOpts
-- | Start server with given routes and handlers (no guards).
start
:: forall routesSpec handlers
. Routable routesSpec {} handlers {}
=> Options
-> Spec routesSpec
-> handlers
-> Aff (Either String Server)
start opts routeSpec handlers = startGuarded opts api { handlers, guards: {} }
where
api = Spec :: Spec { routes :: routesSpec, guards :: {} }
-- | Start server with default options and given spec, handlers, and guards.
startGuarded_
:: forall routesSpec guardsSpec handlers guards
. Routable routesSpec guardsSpec handlers guards
=> Spec { routes :: routesSpec, guards :: guardsSpec }
-> { handlers :: handlers, guards :: guards }
-> Aff (Either String Server)
startGuarded_ = startGuarded defaultOpts
-- | Start server with given spec, handlers, and guards.
startGuarded
:: forall routesSpec guardsSpec handlers guards
. Routable routesSpec guardsSpec handlers guards
=> Options
-> Spec { guards :: guardsSpec, routes :: routesSpec }
-> { handlers :: handlers, guards :: guards }
-> Aff (Either String Server)
startGuarded opts apiSpec api = do
let cfg = mkConfig opts
case mkRouter apiSpec api of
Right routerTrie -> do
server <- Server <$> (liftEffect $ HTTP.createServer (handleRequest cfg routerTrie))
let httpOpts = Record.delete (SProxy :: SProxy "logLevel") opts
listenResult <- listen cfg server httpOpts
pure (const server <$> listenResult)
Left err -> pure (Left err)
dumpRoutes :: Trie HandlerEntry -> Effect Unit
dumpRoutes = log <<< showRoutes
showRoutes :: Trie HandlerEntry -> String
showRoutes routerTrie = Trie.dumpEntries (_.route <$> routerTrie)
mkConfig :: Options -> Config
mkConfig { logLevel } = { logger: mkLogger logLevel }
mkLogger :: LogLevel -> Logger
mkLogger logLevel = { log: log_, logDebug, logError }
where
log_ :: String -> Effect Unit
log_ | logLevel >= LogNormal = log
log_ = const $ pure unit
logDebug :: String -> Effect Unit
logDebug | logLevel >= LogDebug = log
logDebug = const $ pure unit
logError :: String -> Effect Unit
logError | logLevel >= LogError = log
logError = const $ pure unit
handleRequest :: Config -> Trie HandlerEntry -> HTTP.Request -> HTTP.Response -> Effect Unit
handleRequest cfg@{ logger } routerTrie req res = do
let url = Url.parse (HTTP.requestURL req)
logger.logDebug (HTTP.requestMethod req <> " " <> show (url.path))
case requestUrl req of
Right reqUrl -> runHandlers cfg routerTrie reqUrl req res
Left err -> do
writeResponse res (internalError $ StringBody $ "Path could not be decoded: " <> show err)
runHandlers :: Config -> Trie HandlerEntry -> RequestUrl
-> HTTP.Request -> HTTP.Response -> Effect Unit
runHandlers { logger } routerTrie reqUrl req res = do
let (matches :: List HandlerEntry) = Trie.lookup (reqUrl.method : reqUrl.path) routerTrie
let matchesStr = String.joinWith "\n" (Array.fromFoldable $ (showRouteUrl <<< _.route) <$> matches)
logger.logDebug $ showUrl reqUrl <> " -> " <> show (List.length matches) <> " matches:\n" <> matchesStr
Aff.launchAff_ $ do
outcome <- handleNext Nothing matches
case outcome of
(Forward msg) -> do
liftEffect $ writeResponse res (Response.notFound (StringBody ""))
_ -> pure unit
where
handleNext :: Maybe Outcome -> List HandlerEntry -> Aff Outcome
handleNext Nothing ({ handler } : rest) = do
outcome <- handler reqUrl req res
handleNext (Just outcome) rest
handleNext (Just Success) _ = pure Success
handleNext (Just Failure) _ = pure Failure
handleNext (Just (Forward msg)) ({ handler } : rest) = do
liftEffect $ logger.logDebug $ "-> Forwarding to next route. Previous failure: " <> msg
outcome <- handler reqUrl req res
handleNext (Just outcome) rest
handleNext (Just (Forward msg)) Nil = do
liftEffect $ logger.logDebug $ "-> No more routes to try. Last failure: " <> msg
pure (Forward "No match could handle")
handleNext _ Nil = pure (Forward "No match could handle")
showMatches :: List HandlerEntry -> String
showMatches matches = " " <> String.joinWith "\n " (Array.fromFoldable $ showMatch <$> matches)
where
showMatch = showRouteUrl <<< _.route
showUrl :: RequestUrl -> String
showUrl { method, path, query } = method <> " " <> fullPath
where fullPath = String.joinWith "/" (Array.fromFoldable path)
showRouteUrl :: List Segment -> String
showRouteUrl (method : rest) = show method <> " /" <> String.joinWith "/" (Array.fromFoldable $ show <$> rest)
showRouteUrl Nil = ""
requestUrl :: HTTP.Request -> Either String RequestUrl
requestUrl req = do
let parsedUrl = Url.parse (HTTP.requestURL req)
path <- urlPath parsedUrl
let query = fromMaybe "" $ toMaybe parsedUrl.query
let pathSegments = urlToSegments path
pure { method, path: pathSegments, query }
where
method = HTTP.requestMethod req
urlPath :: URL -> Either String String
urlPath url = url.pathname
# toMaybe
# maybe (Left "No path") Right
urlQuery :: URL -> Maybe String
urlQuery url = url.query # toMaybe
foreign import onError :: HTTP.Server -> (Error -> Effect Unit) -> Effect Unit
listen :: Config -> Server -> HTTP.ListenOptions -> Aff (Either String Unit)
listen { logger } server@(Server httpServer) opts = Aff.makeAff $ \cb -> do
onError httpServer \error -> cb (Right (Left (show error)))
HTTP.listen httpServer opts (logger.log startedMsg *> cb (Right (Right unit)))
pure $ Aff.Canceler (\error -> liftEffect (logger.logError (errorMsg error)) *> close server)
where
startedMsg = "Server is running on http://" <> opts.hostname <> ":" <> show opts.port
errorMsg e = "Closing server due to error: " <> show e
-- | Stops a server
close :: Server -> Aff Unit
close (Server server) = Aff.makeAff $ \cb -> do
HTTP.close server (cb (Right unit))
pure Aff.nonCanceler