1 {-# LANGUAGE DataKinds #-}
2 {-# LANGUAGE DeriveFunctor #-}
3 {-# LANGUAGE GeneralizedNewtypeDeriving #-}
4 {-# LANGUAGE InstanceSigs #-}
5 {-# LANGUAGE OverloadedStrings #-}
6 {-# LANGUAGE TypeFamilies #-}
7 module Symantic.HTTP.Router where
9 import Control.Arrow (first)
10 import Control.Monad (Monad(..), unless, sequence)
11 import Control.Monad.Trans.Class (MonadTrans(..))
12 import Control.Monad.Trans.Except (ExceptT(..), runExceptT)
14 import Data.Either (Either(..))
15 import Data.Eq (Eq(..))
16 import Data.Function (($), (.), id)
17 import Data.Functor (Functor, (<$>))
19 import Data.Maybe (Maybe(..), fromMaybe, catMaybes)
20 import Data.Proxy (Proxy(..))
21 import Data.Semigroup (Semigroup(..))
22 import Data.String (String, IsString(..))
23 import Data.Text (Text)
26 import Text.Show (Show(..))
27 import qualified Control.Monad.Classes as MC
28 import qualified Control.Monad.Trans.State as S
29 import qualified Data.ByteString.Lazy as BSL
30 import qualified Data.List as List
31 import qualified Data.Text.Encoding as Text
32 import qualified Network.HTTP.Media as Media
33 import qualified Network.HTTP.Types as HTTP
34 import qualified Network.Wai as Wai
35 import qualified Web.HttpApiData as Web
37 import Symantic.HTTP.API
38 import Symantic.HTTP.Media
39 import Symantic.HTTP.Mime
43 debug msg x = trace (msg<>": "<>show x) x
46 -- | Convenient alias.
47 liftIO :: MC.MonadExec IO m => IO a -> m a
51 -- | @RouterAPI f k@ is a recipe to produce an 'Wai.Application'
52 -- from handlers 'f' (one per number of alternative routes).
54 -- 'RouterAPI' is analogous to a scanf using a format customized for HTTP routing.
56 -- The multiple monad transformers are there to prioritize the errors
57 -- according to the type of check raising them,
58 -- instead of the order of the combinators within an actual API specification.
59 newtype RouterAPI f k = RouterAPI { unRouterAPI ::
61 (RouterCheckT [RouterErrorBody] -- 8th check, 400 error
62 (RouterCheckT [RouterErrorHeader] -- 7th check, 400 error
63 (RouterCheckT [RouterErrorQuery] -- 6th check, 400 error
64 (RouterCheckT [RouterErrorContentType] -- 5th check, 415 error
65 (RouterCheckT [RouterErrorAccept] -- 4th check, 406 error
66 (-- TODO: RouterCheckT [RouterErrorAuth] -- 3rd check, 401 error
67 (RouterCheckT [RouterErrorMethod] -- 2nd check, 405 error
68 (RouterCheckT [RouterErrorPath] -- 1st check, 404 error
70 deriving (Functor {-, Applicative, Alternative, Monad, MonadPlus, P.MonadParsec RouteError RouteTokens-})
74 S.StateT RouterState (ExceptT e1 (ExceptT e2 (ExceptT e3 (ExceptT e4 (ExceptT e5 (ExceptT e6 (ExceptT e7 IO))))))) a ->
75 RouterState -> IO (Either e7 (Either e6 (Either e5 (Either e4 (Either e3 (Either e2 (Either e1 (a, RouterState))))))))
86 -- ** Type 'RouterCheckT'
87 type RouterCheckT e = ExceptT (Fail e)
89 -- *** Type 'RouteResult'
90 type RouteResult e = Either (Fail e)
94 = Fail RouterState e -- ^ Keep trying other paths. 404, 405 or 406.
95 | FailFatal !RouterState !e -- ^ Don't try other paths.
97 failState :: Fail e -> RouterState
98 failState (Fail st _) = st
99 failState (FailFatal st _) = st
100 instance Semigroup e => Semigroup (Fail e) where
101 Fail _ x <> Fail st y = Fail st (x<>y)
102 FailFatal _ x <> Fail st y = FailFatal st (x<>y)
103 Fail _ x <> FailFatal st y = FailFatal st (x<>y)
104 FailFatal _ x <> FailFatal st y = FailFatal st (x<>y)
106 -- ** Type 'RouterState'
107 data RouterState = RouterState
108 { routerState_offset :: Offset
109 , routerState_request :: Wai.Request
111 instance Show RouterState where
112 show _ = "RouterState"
113 instance Cat RouterAPI where
117 repr a b -> repr b c -> repr a c
118 -- NOTE: if x fails, run y to see if it fails on a more prioritized check.
119 -- And if so, fail with y instead of x.
121 -- This long spaghetti code may probably be avoided
122 -- with a more sophisticated RouterAPI using a binary tree
123 -- instead of nested Either, so that its Monad instance
124 -- would do the right thing, but to my mind,
125 -- with the very few priorities of checks currently needed,
126 -- this is currently not worth the cognitive pain to design it.
127 -- A copy/paste/modify will do for now.
128 RouterAPI x <.> RouterAPI y = RouterAPI $
130 xPath <- liftIO $ runRouterAPI x st
132 Left xe -> MC.throw xe
136 yPath <- liftIO $ runRouterAPI y (failState xe)
138 Left ye -> MC.throw ye
139 Right _yMethod -> MC.throw xe
143 yPath <- liftIO $ runRouterAPI y (failState xe)
145 Left ye -> MC.throw ye
148 Left ye -> MC.throw ye
149 Right _yAccept -> MC.throw xe
150 Right xContentType ->
153 yPath <- liftIO $ runRouterAPI y (failState xe)
155 Left ye -> MC.throw ye
158 Left ye -> MC.throw ye
161 Left ye -> MC.throw ye
162 Right _yQuery -> MC.throw xe
166 yPath <- liftIO $ runRouterAPI y (failState xe)
168 Left ye -> MC.throw ye
171 Left ye -> MC.throw ye
174 Left ye -> MC.throw ye
177 Left ye -> MC.throw ye
178 Right _yHeader -> MC.throw xe
182 yPath <- liftIO $ runRouterAPI y (failState xe)
184 Left ye -> MC.throw ye
187 Left ye -> MC.throw ye
190 Left ye -> MC.throw ye
193 Left ye -> MC.throw ye
196 Left ye -> MC.throw ye
197 Right _yBody -> MC.throw xe
201 yPath <- liftIO $ runRouterAPI y (failState xe)
203 Left ye -> MC.throw ye
206 Left ye -> MC.throw ye
209 Left ye -> MC.throw ye
212 Left ye -> MC.throw ye
215 Left ye -> MC.throw ye
216 Right _yBody -> MC.throw xe
218 (first (. a2b)) <$> S.runStateT y st'
219 instance Alt RouterAPI where
220 RouterAPI x <!> RouterAPI y = RouterAPI $
222 xPath <- liftIO $ runRouterAPI x st
223 yPath <- liftIO $ runRouterAPI y st
224 let fy = (first (\b2k (_a:!:b) -> b2k b) <$>)
226 Left xe | FailFatal{} <- xe -> MC.throw xe
229 Left ye -> MC.throw (xe<>ye)
231 fy $ ExceptT $ ExceptT $ ExceptT $ ExceptT $ ExceptT $ ExceptT $ ExceptT $
232 return $ Right yMethod
235 Left xe | FailFatal{} <- xe -> MC.throw xe
238 Left _ye -> MC.throw xe
241 Left ye -> MC.throw (xe<>ye)
243 fy $ ExceptT $ ExceptT $ ExceptT $ ExceptT $ ExceptT $ ExceptT $
244 return $ Right $ yAccept
247 Left xe | FailFatal{} <- xe -> MC.throw xe
250 Left _ye -> MC.throw xe
253 Left _ye -> MC.throw xe
256 Left ye -> MC.throw (xe<>ye)
257 Right yContentType ->
258 fy $ ExceptT $ ExceptT $ ExceptT $ ExceptT $ ExceptT $
259 return $ Right yContentType
260 Right xContentType ->
262 Left xe | FailFatal{} <- xe -> MC.throw xe
265 Left _ye -> MC.throw xe
268 Left _ye -> MC.throw xe
271 Left _ye -> MC.throw xe
272 Right yContentType ->
274 Left ye -> MC.throw (xe<>ye)
276 fy $ ExceptT $ ExceptT $ ExceptT $ ExceptT $
277 return $ Right yQuery
280 Left xe | FailFatal{} <- xe -> MC.throw xe
283 Left _ye -> MC.throw xe
286 Left _ye -> MC.throw xe
289 Left _ye -> MC.throw xe
290 Right yContentType ->
292 Left _ye -> MC.throw xe
295 Left ye -> MC.throw (xe<>ye)
297 fy $ ExceptT $ ExceptT $ ExceptT $
298 return $ Right yHeader
301 Left xe | FailFatal{} <- xe -> MC.throw xe
304 Left _ye -> MC.throw xe
307 Left _ye -> MC.throw xe
310 Left _ye -> MC.throw xe
311 Right yContentType ->
313 Left _ye -> MC.throw xe
316 Left _ye -> MC.throw xe
319 Left ye -> MC.throw (xe<>ye)
321 fy $ ExceptT $ ExceptT $
325 Left xe | FailFatal{} <- xe -> MC.throw xe
328 Left _ye -> MC.throw xe
331 Left _ye -> MC.throw xe
334 Left _ye -> MC.throw xe
335 Right yContentType ->
337 Left _ye -> MC.throw xe
340 Left _ye -> MC.throw xe
343 Left _ye -> MC.throw xe
346 Left ye -> MC.throw (xe<>ye)
351 return $ first (\a2k (a:!:_b) -> a2k a) xr
353 instance Pro RouterAPI where
354 dimap a2b _b2a (RouterAPI r) = RouterAPI $ (\k b2k -> k (b2k . a2b)) <$> r
356 -- | @'routerAPI' rt api@ returns a 'Wai.Application'
357 -- ready to be given to @Warp.run 80@.
359 RouterAPI handlers RouterResponse ->
362 routerAPI (RouterAPI api) handlers rq re = do
363 lrPath <- liftIO $ runRouterAPI api (RouterState 0 rq)
365 Left err -> respondError status404 err
368 Left err -> respondError status405 err
371 Left err -> respondError status406 err
372 Right lrContentType ->
373 case lrContentType of
374 Left err -> respondError status415 err
377 Left err -> respondError status400 err
380 Left err -> respondError status400 err
383 Left err -> respondError status400 err
385 let RouterResponse app = a2k handlers in app rq re
387 respondError :: Show err => HTTP.Status -> err -> IO Wai.ResponseReceived
388 respondError st err =
389 re $ Wai.responseLBS st
390 [(HTTP.hContentType, Media.renderHeader $ mediaType mimePlainText)]
391 (fromString $ show err)
393 -- ** Type 'RouterErrorPath'
394 data RouterErrorPath = RouterErrorPath Offset Text
396 instance HTTP_Path RouterAPI where
397 segment expSegment = RouterAPI $ do
399 { routerState_offset = o
400 , routerState_request = req
402 case Wai.pathInfo req of
403 [] -> MC.throw $ Fail st [RouterErrorPath o "segment: empty"]
404 [""] -> MC.throw $ Fail st [RouterErrorPath o "trailing slash"]
406 | curr /= expSegment ->
407 MC.throw $ Fail st [RouterErrorPath o $ "expected: "<>expSegment<>" but got: "<>curr]
410 { routerState_offset = o+1
411 , routerState_request = req{ Wai.pathInfo = next }
414 capture' :: forall a k.
415 Web.FromHttpApiData a =>
416 Web.ToHttpApiData a =>
417 Name -> RouterAPI (a -> k) k
418 capture' name = RouterAPI $ do
420 { routerState_offset = o
421 , routerState_request = req
423 case Wai.pathInfo req of
424 [] -> MC.throw $ Fail st [RouterErrorPath o "empty"]
425 [""] -> MC.throw $ Fail st [RouterErrorPath o "trailing slash"]
427 case Web.parseUrlPiece curr of
428 Left err -> MC.throw $ Fail st [RouterErrorPath o $ "capture: "<>fromString name<>": "<>err]
431 { routerState_offset = o+1
432 , routerState_request = req{ Wai.pathInfo = next }
435 captureAll = RouterAPI $ do
436 req <- S.gets routerState_request
437 return ($ Wai.pathInfo req)
439 -- ** Type 'RouterErrorMethod'
440 data RouterErrorMethod = RouterErrorMethod
442 instance HTTP_Method RouterAPI where
443 method exp = RouterAPI $ do
445 let got = Wai.requestMethod $ routerState_request st
447 || got == HTTP.methodHead
448 && exp == HTTP.methodGet
450 else MC.throw $ Fail st [RouterErrorMethod]
452 -- | TODO: add its own error?
453 instance HTTP_Version RouterAPI where
454 version exp = RouterAPI $ do
456 let got = Wai.httpVersion $ routerState_request st
459 else MC.throw $ Fail st [RouterErrorMethod] -- FIXME: RouterErrorVersion
461 -- ** Type 'RouterErrorAccept'
462 data RouterErrorAccept = RouterErrorAccept
464 instance HTTP_Accept RouterAPI where
465 accept exp = RouterAPI $ do
467 let hs = Wai.requestHeaders $ routerState_request st
468 case List.lookup HTTP.hAccept hs of
469 Nothing -> MC.throw $ Fail st [RouterErrorAccept]
471 case Media.parseAccept h of
472 Nothing -> MC.throw $ Fail st [RouterErrorAccept]
473 Just got | mediaType exp`Media.matches`got -> return id
474 | otherwise -> MC.throw $ Fail st [RouterErrorAccept]
476 -- ** Type 'RouterErrorContentType'
477 data RouterErrorContentType = RouterErrorContentType
479 instance HTTP_ContentType RouterAPI where
480 contentType exp = RouterAPI $ do
482 let hs = Wai.requestHeaders $ routerState_request st
484 -- DOC: http://www.w3.org/Protocols/rfc2616/rfc2616-sec7.html#sec7.2.1
485 -- DOC: http://www.w3.org/2001/tag/2002/0129-mime
486 fromMaybe "application/octet-stream" $
487 List.lookup HTTP.hContentType hs
488 case Media.mapContentMedia [(mediaType exp, ())] got of
489 Nothing -> MC.throw $ Fail st [RouterErrorContentType]
490 Just () -> return id -- TODO: mimeUnserialize
492 -- ** Type 'RouterErrorQuery'
493 newtype RouterErrorQuery = RouterErrorQuery Text
495 instance HTTP_Query RouterAPI where
496 queryParams' name = RouterAPI $ do
498 lift $ ExceptT $ ExceptT $ ExceptT $ return $
499 let qs = Wai.queryString $ routerState_request st in
500 let vals = catMaybes $ (<$> qs) $ \(n,v) ->
502 then Web.parseQueryParam . Text.decodeUtf8 <$> v
504 case sequence vals of
505 Left err -> Left $ Fail st [RouterErrorQuery err]
506 Right vs -> Right $ Right $ Right ($ vs)
508 -- ** Type 'RouterErrorHeader'
509 data RouterErrorHeader = RouterErrorHeader
511 instance HTTP_Header RouterAPI where
512 header n = RouterAPI $ do
514 lift $ ExceptT $ ExceptT $ return $
515 let hs = Wai.requestHeaders $ routerState_request st in
516 case List.lookup n hs of
517 Nothing -> Left $ Fail st [RouterErrorHeader]
518 Just v -> Right $ Right ($ v)
520 -- ** Type 'RouterErrorBody'
521 newtype RouterErrorBody = RouterErrorBody String
523 -- *** Type 'RouterBodyArg'
524 newtype RouterBodyArg mt a = RouterBodyArg a
526 instance HTTP_Body RouterAPI where
527 type BodyArg RouterAPI = RouterBodyArg
530 MimeUnserialize mt a =>
531 MimeSerialize mt a =>
533 repr (BodyArg repr mt a -> k) k
534 body'= RouterAPI $ do
536 lift $ ExceptT $ ExceptT $ ExceptT $ ExceptT $ do
537 let hs = Wai.requestHeaders $ routerState_request st
538 let expContentType = (Proxy::Proxy mt)
540 -- DOC: http://www.w3.org/Protocols/rfc2616/rfc2616-sec7.html#sec7.2.1
541 -- DOC: http://www.w3.org/2001/tag/2002/0129-mime
542 fromMaybe "application/octet-stream" $
543 List.lookup HTTP.hContentType hs
544 case Media.mapContentMedia
545 [ ( mediaType expContentType
546 , mimeUnserialize expContentType )
548 Nothing -> return $ Left $ Fail st [RouterErrorContentType]
549 Just unSerialize -> do
550 bodyBS <- liftIO $ Wai.requestBody $ routerState_request st
551 return $ Right $ Right $ Right $
552 -- NOTE: delay unSerialize after all checks
553 case unSerialize $ BSL.fromStrict bodyBS of
554 Left err -> Left $ Fail st [RouterErrorBody err]
555 Right a -> Right ($ RouterBodyArg a)
557 -- ** Type 'RouterResponse'
558 newtype RouterResponse = RouterResponse
559 ( -- the request made to the router
561 -- the continuation for the router to respond
562 (Wai.Response -> IO Wai.ResponseReceived) ->
563 IO Wai.ResponseReceived
565 instance Show RouterResponse where
566 show _ = "RouterResponse"
568 -- *** Type 'RouterResponseArg'
569 newtype RouterResponseArg mt a = RouterResponseArg
571 HTTP.ResponseHeaders ->
574 instance HTTP_Response RouterAPI where
575 type Response RouterAPI = RouterResponse
576 type ResponseArg RouterAPI = RouterResponseArg
579 MimeUnserialize mt a =>
580 MimeSerialize mt a =>
584 repr (ResponseArg repr mt a -> k) k
585 response' expMethod = RouterAPI $ do
587 let reqMethod = Wai.requestMethod $ routerState_request st
588 unless (reqMethod == expMethod
589 || reqMethod == HTTP.methodHead
590 && expMethod == HTTP.methodGet) $
591 MC.throw $ Fail st [RouterErrorMethod]
593 let reqHeaders = Wai.requestHeaders $ routerState_request st
594 let expAccept = (Proxy::Proxy mt)
596 case List.lookup HTTP.hAccept reqHeaders of
597 Nothing -> MC.throw $ Fail st [RouterErrorAccept]
599 case Media.parseAccept h of
600 Nothing -> MC.throw $ Fail st [RouterErrorAccept]
601 Just got | mediaType expAccept`Media.matches`got ->
602 return expAccept -- FIXME: return got, maybe with GADTs
603 | otherwise -> MC.throw $ Fail st [RouterErrorAccept]
605 return ($ RouterResponseArg $ \s hs a ->
607 ((HTTP.hContentType, Media.renderHeader $ mediaType reqAccept):hs)
608 (if reqMethod == HTTP.methodHead
610 else mimeSerialize reqAccept a))