/
Routable.purs
259 lines (231 loc) · 9.27 KB
/
Routable.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
253
254
255
256
257
258
259
module Payload.Routable
( class Routable
, mkRouter
, class RoutableList
, mkRouterList
, DefaultParentRoute
, HandlerEntry
, Outcome(Success, Failure, Forward)
) where
import Prelude
import Control.Monad.Except (runExceptT)
import Data.Bifunctor (lmap)
import Data.Either (Either(..))
import Data.List (List(..), (:))
import Data.Symbol (class IsSymbol, SProxy(..), reflectSymbol)
import Effect.Aff (Aff)
import Effect.Aff as Aff
import Effect.Class (liftEffect)
import Effect.Console (errorShow)
import Node.HTTP as HTTP
import Payload.Handleable (class Handleable, MethodHandler, handle)
import Payload.Internal.GuardParsing (GuardTypes(GuardTypes))
import Payload.Internal.GuardParsing as GuardParsing
import Payload.Internal.Request (RequestUrl)
import Payload.Internal.ServerResponse (sendResponse)
import Payload.Internal.Trie (Trie)
import Payload.Internal.Trie as Trie
import Payload.Internal.Url as PayloadUrl
import Payload.Internal.UrlParsing (class ParseUrl, class ToSegments, Segment(..))
import Payload.Internal.UrlParsing as UrlParsing
import Payload.Response (RawResponse)
import Payload.Response as Resp
import Payload.Route (DefaultRequest)
import Payload.Spec (kind GuardList, Spec, GNil, Guards(Guards), Route(Route), Routes(..))
import Prim.Row as Row
import Prim.RowList (class RowToList, kind RowList)
import Prim.RowList as RowList
import Prim.Symbol as Symbol
import Record (get)
import Record as Record
import Type.Data.RowList (RLProxy(..))
import Type.Equality (class TypeEquals)
import Type.Proxy (Proxy(..))
type RoutingTrie = Trie HandlerEntry
type HandlerEntry =
{ handler :: RequestUrl -> HTTP.Request -> HTTP.Response -> Aff Outcome
, route :: List Segment }
type RawHandler = RequestUrl -> HTTP.Request -> HTTP.Response -> Aff Outcome
data Outcome = Success | Failure | Forward String
type DefaultParentRoute = ( params :: {}, guards :: Guards GNil )
defaultParent :: Record DefaultParentRoute
defaultParent =
{ params: {}, guards: Guards :: _ GNil }
class Routable routesSpec guardsSpec handlers guards |
routesSpec guardsSpec -> handlers,
guardsSpec -> guards where
mkRouter :: Spec { routes :: routesSpec, guards :: guardsSpec }
-> { handlers :: handlers, guards :: guards }
-> Either String RoutingTrie
instance routableRecord ::
( RowToList routesSpec routesSpecList
, RoutableList routesSpecList "" () GNil guardsSpec (Record handlers) (Record guards)
) => Routable (Record routesSpec) (Record guardsSpec) (Record handlers) (Record guards) where
mkRouter _ { handlers, guards } =
mkRouterList
(RLProxy :: RLProxy routesSpecList)
(SProxy :: _ "")
(Proxy :: _ {})
(Guards :: _ GNil)
(Proxy :: _ (Record guardsSpec))
handlers
guards
Trie.empty
class RoutableList
(routesSpecList :: RowList)
(basePath :: Symbol)
(baseParams :: # Type)
(baseGuards :: GuardList)
(guardsSpec :: # Type)
handlers
guards
| routesSpecList guardsSpec -> handlers
, guardsSpec -> guards where
mkRouterList ::
RLProxy routesSpecList
-> SProxy basePath
-> Proxy (Record baseParams)
-> Guards baseGuards
-> Proxy (Record guardsSpec)
-> handlers
-> guards
-> RoutingTrie
-> Either String RoutingTrie
instance routableListNil :: RoutableList RowList.Nil basePath baseParams baseGuards guardsSpec handlers guards where
mkRouterList _ _ _ _ _ _ _ trie = Right trie
instance routableListCons ::
( IsSymbol routeName
, IsSymbol path
, IsSymbol method
, Row.Union spec DefaultRequest mergedSpec
, Row.Nub mergedSpec specWithDefaults
, Handleable (Route method path (Record specWithDefaults)) handler basePath baseParams baseGuards guardsSpec (Record guards)
, RoutableList remRoutes basePath baseParams baseGuards guardsSpec (Record handlers) (Record guards)
, Row.Cons routeName handler h' handlers
, Symbol.Append basePath path fullPath
, ParseUrl fullPath urlParts
, ToSegments urlParts
) => RoutableList (RowList.Cons routeName (Route method path (Record spec)) remRoutes)
basePath
baseParams
baseGuards
guardsSpec
(Record handlers)
(Record guards)
where
mkRouterList _ basePath baseParams baseGuards guardsSpec handlers guards trie = do
newTrie <- insertRoute (Lit method : routePath) handler trie
trieWithRest <- mkRouterList (RLProxy :: RLProxy remRoutes)
basePath
baseParams
baseGuards
guardsSpec
handlers
guards
newTrie
case method of
"GET" -> orElse (const trieWithRest) $ insertRoute (Lit "HEAD" : routePath) headHandler trieWithRest
_ -> pure trieWithRest
where
method :: String
method = reflectSymbol (SProxy :: SProxy method)
routePath :: List Segment
routePath = UrlParsing.asSegments (SProxy :: SProxy fullPath)
handler :: RawHandler
handler url req res =
methodHandler url req res
# executeHandler res
headHandler :: RawHandler
headHandler url req res =
methodHandler url req res
<#> Resp.setBody Resp.EmptyBody
# executeHandler res
executeHandler :: HTTP.Response -> Resp.Result RawResponse -> Aff Outcome
executeHandler res mHandler = do
result <- Aff.attempt $ runExceptT mHandler
case result of
Right (Right rawResponse) -> do
liftEffect $ sendResponse res (Right rawResponse)
pure Success
Right (Left (Resp.Error error)) -> do
liftEffect $ sendResponse res (Left error)
pure Failure
Right (Left (Resp.Forward error)) -> pure (Forward error)
Left error -> do
liftEffect $ errorShow error
liftEffect $ sendResponse res (Left (Resp.internalError (Resp.StringBody "Internal error")))
pure Failure
methodHandler :: MethodHandler
methodHandler = handle
(SProxy :: _ basePath)
baseParams
baseGuards
(GuardTypes :: _ (Record guardsSpec))
(Route :: Route method path (Record specWithDefaults))
payloadHandler
guards
payloadHandler :: handler
payloadHandler = get (SProxy :: SProxy routeName) handlers
instance routableListConsRoutes ::
( IsSymbol parentName
, IsSymbol basePath
, IsSymbol path
-- Parse out child routes from parent params
, Row.Union parentSpec DefaultParentRoute mergedSpec
, Row.Nub mergedSpec parentSpecWithDefaults
, TypeEquals
(Record parentSpecWithDefaults)
{params :: Record parentParams, guards :: Guards parentGuards | childRoutes}
, Row.Union baseParams parentParams childParams
, GuardParsing.Append baseGuards parentGuards childGuards
-- Extra check: fail here already if they don't match
, PayloadUrl.DecodeUrl path parentParams
, Row.Cons parentName (Record childHandlers) handlers' handlers
-- Recurse through child routes
, RowToList childRoutes childRoutesList
, Symbol.Append basePath path childBasePath
, RoutableList childRoutesList childBasePath childParams childGuards guardsSpec (Record childHandlers) (Record guards)
-- Iterate through rest of list routes
, RoutableList remRoutes basePath baseParams baseGuards guardsSpec (Record handlers) (Record guards)
) => RoutableList (RowList.Cons parentName (Routes path (Record parentSpec)) remRoutes)
basePath
baseParams
baseGuards
guardsSpec
(Record handlers)
(Record guards) where
mkRouterList _ basePath baseParams baseGuards guardsSpec handlers guards trie =
case trieWithChildRoutes of
Right newTrie -> mkRouterList (RLProxy :: RLProxy remRoutes)
basePath
baseParams
baseGuards
guardsSpec
handlers
guards
newTrie
Left e -> Left $ "Could not insert child routes for path '"
<> reflectSymbol (SProxy :: SProxy path)
<> "': " <> e
where
childHandlers = Record.get (SProxy :: _ parentName) handlers
trieWithChildRoutes = mkRouterList
(RLProxy :: _ childRoutesList)
(SProxy :: _ childBasePath)
(Proxy :: _ (Record childParams))
(Guards :: _ childGuards)
guardsSpec
childHandlers
guards
trie
insertRoute :: List Segment -> RawHandler -> RoutingTrie -> Either String RoutingTrie
insertRoute route handler trie = lmap wrapError $ Trie.insert {route, handler} route trie
where
handlerEntry = { route, handler }
wrapError :: String -> String
wrapError e = "Could not insert route for path '" <>
show route <>
"' into routing trie"
orElse :: forall a b c. (a -> c) -> Either a c -> Either b c
orElse _ (Right v) = Right v
orElse f (Left v) = Right (f v)