packages feed

nested-routes 6.1.0 → 7.0.0

raw patch · 11 files changed

+588/−1087 lines, 11 filesdep +exceptionsdep +hashabledep +unordered-containersdep −base64-bytestringdep −blaze-builderdep −containersdep ~basedep ~composition-extradep ~nested-routessetup-changedPVP ok

version bump matches the API change (PVP)

Dependencies added: exceptions, hashable, unordered-containers

Dependencies removed: base64-bytestring, blaze-builder, containers, cookie, cryptonite, data-default, iso8601-time, lucid, memory, stm, time, utf8-string, wai-extra, wai-session

Dependency ranges changed: base, composition-extra, nested-routes, poly-arity, pred-trie, wai-middleware-content-type, wai-middleware-verbs, wai-transformers

API changes (from Hackage documentation)

- Web.Routes.Nested: HandlerT :: StateT (Tries x sec err) m a -> HandlerT x sec err aux m a
- Web.Routes.Nested: [runHandlerT] :: HandlerT x sec err aux m a -> StateT (Tries x sec err) m a
- Web.Routes.Nested: action :: MonadIO m => ActionT e u m () -> MiddlewareT m
- Web.Routes.Nested: actionToMiddleware :: MonadIO m => RoutableActionT sec e u ue m () -> RoutableT sec e u ue m ()
- Web.Routes.Nested: execHandlerT :: Monad m => HandlerT x sec err aux m a -> m (Tries x sec err)
- Web.Routes.Nested: extractContent :: (Functor m, Monad m, MonadIO m) => HandlerT (MiddlewareT m) sec err aux m a -> MiddlewareT m
- Web.Routes.Nested: extractNotFound :: (Functor m, Monad m, MonadIO m) => HandlerT (MiddlewareT m) sec err aux m a -> MiddlewareT m
- Web.Routes.Nested: handle :: (Monad m, Functor m, HasResult childContent (MiddlewareT m), HasResult err (e -> MiddlewareT m), Singleton (UrlChunks xs) childContent (RootedPredTrie Text resultContent), cleanxs ~ CatMaybes xs, ArityTypeListIso childContent cleanxs resultContent) => UrlChunks xs -> childContent -> HandlerT resultContent sec err (e, u, ue) m ()
- Web.Routes.Nested: handleAction :: (Monad m, Functor m, HasResult childContent (ActionT ue u m ()), HasResult err (e -> ActionT ue u m ()), Singleton (UrlChunks xs) childContent (RootedPredTrie Text resultContent), cleanxs ~ CatMaybes xs, ArityTypeListIso childContent cleanxs resultContent) => UrlChunks xs -> childContent -> HandlerT resultContent sec err (e, u, ue) m ()
- Web.Routes.Nested: handleAny :: (Monad m, Functor m, HasResult content (MiddlewareT m), HasResult err (e -> MiddlewareT m)) => content -> HandlerT content sec err (e, u, ue) m ()
- Web.Routes.Nested: handleAnyAction :: (Monad m, Functor m, HasResult content (ActionT ue u m ()), HasResult err (e -> ActionT ue u m ())) => content -> HandlerT content sec err (e, u, ue) m ()
- Web.Routes.Nested: here :: (Monad m, Functor m, HasResult content (MiddlewareT m), HasResult err (e -> MiddlewareT m)) => content -> HandlerT content sec err (e, u, ue) m ()
- Web.Routes.Nested: hereAction :: (Monad m, Functor m, HasResult content (ActionT ue u m ()), HasResult err (e -> ActionT ue u m ())) => content -> HandlerT content sec err (e, u, ue) m ()
- Web.Routes.Nested: instance forall (k :: BOX) x sec err (aux :: k) (m :: * -> *). Control.Monad.IO.Class.MonadIO m => Control.Monad.IO.Class.MonadIO (Web.Routes.Nested.HandlerT x sec err aux m)
- Web.Routes.Nested: instance forall (k :: BOX) x sec err (aux :: k) (m :: * -> *). GHC.Base.Functor m => GHC.Base.Functor (Web.Routes.Nested.HandlerT x sec err aux m)
- Web.Routes.Nested: instance forall (k :: BOX) x sec err (aux :: k) (m :: * -> *). GHC.Base.Monad m => Control.Monad.State.Class.MonadState (Web.Routes.Nested.Tries x sec err) (Web.Routes.Nested.HandlerT x sec err aux m)
- Web.Routes.Nested: instance forall (k :: BOX) x sec err (aux :: k) (m :: * -> *). GHC.Base.Monad m => GHC.Base.Applicative (Web.Routes.Nested.HandlerT x sec err aux m)
- Web.Routes.Nested: instance forall (k :: BOX) x sec err (aux :: k) (m :: * -> *). GHC.Base.Monad m => GHC.Base.Monad (Web.Routes.Nested.HandlerT x sec err aux m)
- Web.Routes.Nested: instance forall (k :: BOX) x sec err (aux :: k). Control.Monad.Trans.Class.MonadTrans (Web.Routes.Nested.HandlerT x sec err aux)
- Web.Routes.Nested: newtype HandlerT x sec err aux m a
- Web.Routes.Nested: notFound :: (Monad m, Functor m, HasResult content (MiddlewareT m), HasResult err (e -> MiddlewareT m)) => content -> HandlerT content sec err (e, u, ue) m ()
- Web.Routes.Nested: notFoundAction :: (Monad m, Functor m, HasResult content (ActionT ue u m ()), HasResult err (e -> ActionT ue u m ())) => content -> HandlerT content sec err (e, u, ue) m ()
- Web.Routes.Nested: parent :: (Monad m, Functor m, cleanxs ~ CatMaybes xs, ExtrudeSoundly cleanxs xs childContent resultContent, ExtrudeSoundly cleanxs xs childSec resultSec, ExtrudeSoundly cleanxs xs childErr resultErr) => UrlChunks xs -> HandlerT childContent childSec childErr aux m () -> HandlerT resultContent resultSec resultErr aux m ()
- Web.Routes.Nested: routeAction :: (Functor m, Monad m, MonadIO m) => RoutableActionT sec e u ue m () -> MiddlewareT m
- Web.Routes.Nested: routeActionAuth :: (Functor m, Monad m, MonadIO m) => (Request -> [sec] -> m (Response -> Response, Maybe e)) -> RoutableActionT sec e u ue m () -> MiddlewareT m
- Web.Routes.Nested: type ActionT e u m a = VerbListenerT (FileExtListenerT (MiddlewareT m) m a) e u m a
- Web.Routes.Nested: type ExtrudeSoundly cleanxs xs c r = (cleanxs ~ CatMaybes xs, ArityTypeListIso c cleanxs r, Extrude (UrlChunks xs) (RootedPredTrie Text c) (RootedPredTrie Text r))
- Web.Routes.Nested: type RoutableActionT s e u ue m a = HandlerT (ActionT ue u m ()) (s, AuthScope) (e -> ActionT ue u m ()) (e, u, ue) m a
- Web.Routes.Nested: type RoutableT s e u ue m a = HandlerT (MiddlewareT m) (s, AuthScope) (e -> MiddlewareT m) (e, u, ue) m a
- Web.Routes.Nested: type Tries x s e = (RootedPredTrie Text x, RootedPredTrie Text x, RootedPredTrie Text s, RootedPredTrie Text e)
- Web.Routes.Nested.Types: class Extend eitherUrlChunk child result | eitherUrlChunk child -> result
- Web.Routes.Nested.Types: class Extrude chunks start result | chunks start -> result
- Web.Routes.Nested.Types: class Singleton chunks a trie | chunks a -> trie
- Web.Routes.Nested.Types: extend :: Extend eitherUrlChunk child result => eitherUrlChunk -> child -> result
- Web.Routes.Nested.Types: extrude :: Extrude chunks start result => chunks -> start -> result
- Web.Routes.Nested.Types: instance (Web.Routes.Nested.Types.Extrude (Web.Routes.Nested.Types.UrlChunks.UrlChunks xs) trie0 trie1, Web.Routes.Nested.Types.Extend (Web.Routes.Nested.Types.UrlChunks.EitherUrlChunk x) trie1 trie2) => Web.Routes.Nested.Types.Extrude (Web.Routes.Nested.Types.UrlChunks.UrlChunks (x : xs)) trie0 trie2
- Web.Routes.Nested.Types: instance (Web.Routes.Nested.Types.Singleton (Web.Routes.Nested.Types.UrlChunks.UrlChunks xs) a trie0, Web.Routes.Nested.Types.Extend (Web.Routes.Nested.Types.UrlChunks.EitherUrlChunk x) trie0 trie1) => Web.Routes.Nested.Types.Singleton (Web.Routes.Nested.Types.UrlChunks.UrlChunks (x : xs)) a trie1
- Web.Routes.Nested.Types: instance Web.Routes.Nested.Types.Extend (Web.Routes.Nested.Types.UrlChunks.EitherUrlChunk 'GHC.Base.Nothing) (Data.Trie.Pred.RootedPredTrie Data.Text.Internal.Text a) (Data.Trie.Pred.RootedPredTrie Data.Text.Internal.Text a)
- Web.Routes.Nested.Types: instance Web.Routes.Nested.Types.Extend (Web.Routes.Nested.Types.UrlChunks.EitherUrlChunk ('GHC.Base.Just r)) (Data.Trie.Pred.RootedPredTrie Data.Text.Internal.Text (r -> a)) (Data.Trie.Pred.RootedPredTrie Data.Text.Internal.Text a)
- Web.Routes.Nested.Types: instance Web.Routes.Nested.Types.Extrude (Web.Routes.Nested.Types.UrlChunks.UrlChunks '[]) (Data.Trie.Pred.RootedPredTrie Data.Text.Internal.Text a) (Data.Trie.Pred.RootedPredTrie Data.Text.Internal.Text a)
- Web.Routes.Nested.Types: instance Web.Routes.Nested.Types.Singleton (Web.Routes.Nested.Types.UrlChunks.UrlChunks '[]) a (Data.Trie.Pred.RootedPredTrie Data.Text.Internal.Text a)
- Web.Routes.Nested.Types: singleton :: Singleton chunks a trie => chunks -> a -> trie
- Web.Routes.Nested.Types.UrlChunks: (:=) :: Text -> EitherUrlChunk Nothing
- Web.Routes.Nested.Types.UrlChunks: (:~) :: (Text, Text -> Maybe r) -> EitherUrlChunk (Just r)
- Web.Routes.Nested.Types.UrlChunks: (</>) :: EitherUrlChunk mx -> UrlChunks xs -> UrlChunks (mx : xs)
- Web.Routes.Nested.Types.UrlChunks: Cons :: EitherUrlChunk mx -> UrlChunks xs -> UrlChunks (mx : xs)
- Web.Routes.Nested.Types.UrlChunks: Root :: UrlChunks '[]
- Web.Routes.Nested.Types.UrlChunks: data EitherUrlChunk (x :: Maybe *)
- Web.Routes.Nested.Types.UrlChunks: data UrlChunks (xs :: [Maybe *])
- Web.Routes.Nested.Types.UrlChunks: instance (x ~ 'GHC.Base.Nothing) => Data.String.IsString (Web.Routes.Nested.Types.UrlChunks.EitherUrlChunk x)
- Web.Routes.Nested.Types.UrlChunks: l_ :: Text -> EitherUrlChunk Nothing
- Web.Routes.Nested.Types.UrlChunks: literal_ :: Text -> EitherUrlChunk Nothing
- Web.Routes.Nested.Types.UrlChunks: o_ :: UrlChunks '[]
- Web.Routes.Nested.Types.UrlChunks: origin_ :: UrlChunks '[]
- Web.Routes.Nested.Types.UrlChunks: p_ :: (Text, Parser r) -> EitherUrlChunk (Just r)
- Web.Routes.Nested.Types.UrlChunks: parse_ :: (Text, Parser r) -> EitherUrlChunk (Just r)
- Web.Routes.Nested.Types.UrlChunks: pred_ :: (Text, Text -> Maybe r) -> EitherUrlChunk (Just r)
- Web.Routes.Nested.Types.UrlChunks: r_ :: (Text, Regex) -> EitherUrlChunk (Just [String])
- Web.Routes.Nested.Types.UrlChunks: regex_ :: (Text, Regex) -> EitherUrlChunk (Just [String])
+ Web.Routes.Nested: SecurityToken :: !s -> !AuthScope -> SecurityToken s
+ Web.Routes.Nested: [securityScope] :: SecurityToken s -> !AuthScope
+ Web.Routes.Nested: [securityToken] :: SecurityToken s -> !s
+ Web.Routes.Nested: data SecurityToken s
+ Web.Routes.Nested: extractMatch :: (Monad m) => [Text] -> HandlerT r sec m a -> m (Maybe r)
+ Web.Routes.Nested: extractMatchAny :: (Monad m) => [Text] -> HandlerT r sec m a -> m (Maybe r)
+ Web.Routes.Nested: instance GHC.Show.Show s => GHC.Show.Show (Web.Routes.Nested.SecurityToken s)
+ Web.Routes.Nested: match :: (Monad m, Singleton (UrlChunks xs) childContent (RootedPredTrie Text resultContent), cleanxs ~ CatMaybes xs, ArityTypeListIso childContent cleanxs resultContent) => UrlChunks xs -> childContent -> HandlerT resultContent sec m ()
+ Web.Routes.Nested: matchAny :: (Monad m) => content -> HandlerT content sec m ()
+ Web.Routes.Nested: matchGroup :: (Monad m, cleanxs ~ CatMaybes xs, ExtrudeSoundly cleanxs xs childContent resultContent, ExtrudeSoundly cleanxs xs childSec resultSec) => UrlChunks xs -> HandlerT childContent childSec m () -> HandlerT resultContent resultSec m ()
+ Web.Routes.Nested: matchHere :: (Monad m) => content -> HandlerT content sec m ()
+ Web.Routes.Nested.Match: (</>) :: EitherUrlChunk mx -> UrlChunks xs -> UrlChunks (mx : xs)
+ Web.Routes.Nested.Match: f_ :: Text -> EitherUrlChunk (Just Text)
+ Web.Routes.Nested.Match: file_ :: Text -> EitherUrlChunk (Just Text)
+ Web.Routes.Nested.Match: l_ :: Text -> EitherUrlChunk Nothing
+ Web.Routes.Nested.Match: literal_ :: Text -> EitherUrlChunk Nothing
+ Web.Routes.Nested.Match: o_ :: UrlChunks '[]
+ Web.Routes.Nested.Match: origin_ :: UrlChunks '[]
+ Web.Routes.Nested.Match: p_ :: Text -> Parser r -> EitherUrlChunk (Just r)
+ Web.Routes.Nested.Match: parse_ :: Text -> Parser r -> EitherUrlChunk (Just r)
+ Web.Routes.Nested.Match: pred_ :: Text -> (Text -> Maybe r) -> EitherUrlChunk (Just r)
+ Web.Routes.Nested.Match: r_ :: Text -> Regex -> EitherUrlChunk (Just [String])
+ Web.Routes.Nested.Match: regex_ :: Text -> Regex -> EitherUrlChunk (Just [String])
+ Web.Routes.Nested.Match: type EitherUrlChunk = PathChunk Text
+ Web.Routes.Nested.Match: type UrlChunks = PathChunks Text
+ Web.Routes.Nested.Types: HandlerT :: StateT (Tries x sec) m a -> HandlerT x sec m a
+ Web.Routes.Nested.Types: Tries :: !(RootedPredTrie Text x) -> !(RootedPredTrie Text x) -> !(RootedPredTrie Text s) -> Tries x s
+ Web.Routes.Nested.Types: [runHandlerT] :: HandlerT x sec m a -> StateT (Tries x sec) m a
+ Web.Routes.Nested.Types: [trieCatchAll] :: Tries x s -> !(RootedPredTrie Text x)
+ Web.Routes.Nested.Types: [trieContent] :: Tries x s -> !(RootedPredTrie Text x)
+ Web.Routes.Nested.Types: [trieSecurity] :: Tries x s -> !(RootedPredTrie Text s)
+ Web.Routes.Nested.Types: action :: Monad m => ActionT m () -> MiddlewareT m
+ Web.Routes.Nested.Types: data Tries x s
+ Web.Routes.Nested.Types: execHandlerT :: Monad m => HandlerT x sec m a -> m (Tries x sec)
+ Web.Routes.Nested.Types: instance Control.Monad.IO.Class.MonadIO m => Control.Monad.IO.Class.MonadIO (Web.Routes.Nested.Types.HandlerT x sec m)
+ Web.Routes.Nested.Types: instance Control.Monad.Trans.Class.MonadTrans (Web.Routes.Nested.Types.HandlerT x sec)
+ Web.Routes.Nested.Types: instance GHC.Base.Functor m => GHC.Base.Functor (Web.Routes.Nested.Types.HandlerT x sec m)
+ Web.Routes.Nested.Types: instance GHC.Base.Monad m => Control.Monad.State.Class.MonadState (Web.Routes.Nested.Types.Tries x sec) (Web.Routes.Nested.Types.HandlerT x sec m)
+ Web.Routes.Nested.Types: instance GHC.Base.Monad m => GHC.Base.Applicative (Web.Routes.Nested.Types.HandlerT x sec m)
+ Web.Routes.Nested.Types: instance GHC.Base.Monad m => GHC.Base.Monad (Web.Routes.Nested.Types.HandlerT x sec m)
+ Web.Routes.Nested.Types: instance GHC.Base.Monoid (Web.Routes.Nested.Types.Tries x s)
+ Web.Routes.Nested.Types: newtype HandlerT x sec m a
+ Web.Routes.Nested.Types: type ActionT m a = VerbListenerT (FileExtListenerT m a) m a
+ Web.Routes.Nested.Types: type ExtrudeSoundly cleanxs xs c r = (cleanxs ~ CatMaybes xs, ArityTypeListIso c cleanxs r, Extrude (UrlChunks xs) (RootedPredTrie Text c) (RootedPredTrie Text r))
- Web.Routes.Nested: auth :: (Monad m, Functor m) => sec -> err -> AuthScope -> HandlerT content (sec, AuthScope) err aux m ()
+ Web.Routes.Nested: auth :: (Monad m) => sec -> AuthScope -> HandlerT content (SecurityToken sec) m ()
- Web.Routes.Nested: extractAuth :: (Functor m, Monad m, MonadIO m) => (Request -> [sec] -> m (Response -> Response, Maybe e)) -> HandlerT x (sec, AuthScope) (e -> MiddlewareT m) aux m a -> MiddlewareT m
+ Web.Routes.Nested: extractAuth :: (Monad m, MonadThrow m) => (Request -> [sec] -> m ()) -> Request -> HandlerT x (SecurityToken sec) m a -> m ()
- Web.Routes.Nested: extractAuthSym :: (Functor m, Monad m) => HandlerT x (sec, AuthScope) err aux m a -> Request -> m [sec]
+ Web.Routes.Nested: extractAuthSym :: (Monad m) => [Text] -> HandlerT x (SecurityToken sec) m a -> m [sec]
- Web.Routes.Nested: extractNearestVia :: (Functor m, Monad m, MonadIO m) => (HandlerT (MiddlewareT m) sec err aux m a -> m (RootedPredTrie Text (MiddlewareT m))) -> HandlerT (MiddlewareT m) sec err aux m a -> MiddlewareT m
+ Web.Routes.Nested: extractNearestVia :: (Monad m) => [Text] -> (HandlerT r sec m a -> m (RootedPredTrie Text r)) -> HandlerT r sec m a -> m (Maybe r)
- Web.Routes.Nested: route :: (Functor m, Monad m, MonadIO m) => HandlerT (MiddlewareT m) sec err aux m () -> MiddlewareT m
+ Web.Routes.Nested: route :: (Monad m) => HandlerT (MiddlewareT m) sec m a -> MiddlewareT m
- Web.Routes.Nested: routeAuth :: (Functor m, Monad m, MonadIO m) => (Request -> [sec] -> m (Response -> Response, Maybe e)) -> RoutableT sec e u ue m () -> MiddlewareT m
+ Web.Routes.Nested: routeAuth :: (Monad m, MonadThrow m) => (Request -> [sec] -> m ()) -> HandlerT (MiddlewareT m) (SecurityToken sec) m a -> MiddlewareT m

Files

+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
examples/Main.hs view
@@ -2,6 +2,8 @@     OverloadedStrings   , ScopedTypeVariables   , FlexibleContexts+  , DeriveDataTypeable+  , DataKinds   #-}  @@ -12,74 +14,136 @@ import Network.Wai.Handler.Warp import Network.HTTP.Types import           Text.Regex+import qualified Data.Text      as T import qualified Data.Text.Lazy as LT-import           Data.Attoparsec.Text+import           Data.Attoparsec.Text hiding (match) import Data.Monoid-import Control.Monad.Except+import Data.Typeable+import Control.Monad.IO.Class+import Control.Monad.Catch  -data AuthRole = AuthRole deriving (Show, Eq)-data AuthErr = NeedsAuth deriving (Show, Eq)+defApp :: Application+defApp _ respond = respond+                 $ textOnly "404 :(" status404 [] --- | If you fail here and throw an AuthErr, then the user was not authorized to--- under the conditions set by @ss :: [AuthRole]@, and based o_n the authentication--- o_f that user's session from the @Request@ o_bject. Note that we could have a--- shared cache o_f authenticated sessions, by adding more constraints o_n @m@ like--- @MonadIO@.--- For instance, even if there are [] auth roles, we could still include a header/timestamp--- pair to uniquely identify the guest. o_r, we could equally change @Checksum ~ Maybe Token@,--- so a guest just returns Nothing, and we could handle the case in @putAuth@ to--- not do anything.+main :: IO ()+main = run 3000 $+  (routeAuth authorize routes+  `catchMiddlewareT` handleUploadError+  `catchMiddlewareT` handleAuthError+  ) defApp+  where+    handleUploadError u = fileExtsToMiddleware $+      overFileExts [Text] (mapStatus $ const status400) $+        case u of+          NoChunkedBody  -> text "No chunked body allowed!"+          UploadTooLarge -> text "Upload too large"++    handleAuthError e = fileExtsToMiddleware $+      overFileExts [Text] (mapStatus $ const status400) $+        case e of+          NeedsAuth -> text "Authentication needed"++-- | A set of symbolic security roles+data AuthRole = AuthRole+  deriving (Show, Eq)++-- | The error thrown during authentication or authorization.+--+--   I would encourage other security-related error schemes:+--+--   - one data type for user authority errors - like when a logged-in user tries to access+--     the admin console+--   - one for types of failed authentication, during login (user doesn't exist,+--     incorrect password, etc.)+--   - one for malicious attacks - for instance if someone has tried (and failed) to login+--     5 times in the last minute, or multiple requests to the same resource+data AuthError = NeedsAuth+  deriving (Show, Eq, Typeable)++instance Exception AuthError++-- | Simple function that returns a response modification function - like+--   something that adds to the response headers - to maintain a session cookie,+--   for instance.+--+--   You can use 'Control.Monad.Catch.throwM' to throw arbitrary errors, but+--   you should also catch them with 'Network.Wai.Trans.catchMiddlewareT' to+--   avoid runtime exceptions. authorize :: ( Monad m-             ) => Request -> [AuthRole] -> m (Response -> Response, Maybe AuthErr)-authorize _ _ = return (id, Nothing) -- uncomment to force constant authorization--- authorize req ss | null ss   = return (id, Nothing)---                  | otherwise = return (id, Just NeedsAuth)+             , MonadThrow m+             ) => Request -> [AuthRole] -> m ()+authorize req ss | null ss   = return ()+                 | otherwise = throwM NeedsAuth -defApp :: Application-defApp _ respond = respond $ textOnlyStatus status404 "404 :("+-- | The error thrown during the uploading function+data UploadError = NoChunkedBody+                 | UploadTooLarge+  deriving (Show, Typeable) -main :: IO ()-main =-  let app = routeActionAuth authorize routes-      routes = do-        hereAction rootHandle-        parent ("foo" </> o_) $ do-          hereAction fooHandle-          auth AuthRole unauthHandle DontProtectHere-          handleAction ("bar" </> o_) barHandle-          handleAction (p_ ("double", double) </> o_) doubleHandle-        handleAction emailRoute emailHandle-        handleAction ("baz" </> o_) bazHandle-        notFoundAction notFoundHandle-  in run 3000 $ app defApp+instance Exception UploadError+++routes :: ( MonadIO m+          , MonadThrow m+          ) => HandlerT (MiddlewareT m) (SecurityToken AuthRole) m ()+routes = do+  matchHere (action rootHandle)+  matchGroup (l_ "foo" </> o_) $ do+    matchHere (action fooHandle)+    auth AuthRole DontProtectHere+    match (l_ "bar" </> o_) (action barHandle)+  match (p_ "double" double </> o_) doubleHandle+  match emailRoute emailHandle+  match (l_ "baz" </> o_) (action bazHandle)+  matchAny (action notFoundHandle)   where-    rootHandle = get $ text "Home"+    rootHandle :: MonadIO m => ActionT m ()+    rootHandle =+      get $ do+        text "Home"+        json ("Home" :: T.Text)      -- `/foo`+    fooHandle :: MonadIO m => ActionT m ()     fooHandle = get $ text "foo!"      -- `/foo/bar`-    barHandle = get $ do-      text "bar!"-      json ("json bar!" :: LT.Text)+    barHandle :: MonadIO m => ActionT m ()+    barHandle =+      get $ do+        text "bar!"+        json ("json bar!" :: T.Text)      -- `/foo/1234e12`, uses attoparsec-    doubleHandle d = get $ text $ LT.pack (show d) <> " foos"+    doubleHandle :: MonadIO m => Double-> MiddlewareT m+    doubleHandle d = action $ get $ text $ LT.pack (show d) <> " foos"      -- `/athan@foo.com`-    emailRoute = r_ ("email", mkRegex "(^[-a-zA-Z0-9_.]+@[-a-zA-Z0-9]+\\.[-a-zA-Z0-9.]+$)") </> o_-    emailHandle e = get $ text $ LT.pack (show e) <> " email"+    emailRoute :: UrlChunks '[ 'Just [String] ]+    emailRoute = r_ "email" (mkRegex "(^[-a-zA-Z0-9_.]+@[-a-zA-Z0-9]+\\.[-a-zA-Z0-9.]+$)") </> o_ -    -- `/baz`, uses regex-compat+    emailHandle :: MonadIO m => [String] -> MiddlewareT m+    emailHandle e = action $ get $ text $ LT.pack (show e) <> " email"++    -- `/baz+    bazHandle :: ( MonadIO m+                 , MonadThrow m+                 ) => ActionT m ()     bazHandle = do       get $ text "baz!"-      let uploader req = do liftIO $ print =<< strictRequestBody req-                            return ()-          uploadHandle (Left Nothing)  = text "Upload Failed"-          uploadHandle (Left (Just _)) = text "Impossible - no errors thrown in uploader"-          uploadHandle (Right ())      = text "Woah! Upload content!"-      post uploader uploadHandle+      post uploader $ text "uploaded!"+      where+        uploader :: (MonadIO m, MonadThrow m) => Request -> m ()+        uploader req =+          case requestBodyLength req of+                 ChunkedBody               -> throwM NoChunkedBody+                 KnownLength l | l > 1024  -> throwM UploadTooLarge+                               | otherwise -> liftIO $ print =<< strictRequestBody req -    unauthHandle NeedsAuth = get $ textStatus status401 "Unauthorized!"-    notFoundHandle = get $ textStatus status404 "Not Found :("++    notFoundHandle :: MonadIO m => ActionT m ()+    notFoundHandle = get $+      overFileExts [Text] (mapStatus $ const status400) $+        text "Not Found :("
− examples/STM.hs
@@ -1,137 +0,0 @@-{-# LANGUAGE-    OverloadedStrings-  , ScopedTypeVariables-  , FlexibleContexts-  #-}---module Main where--import STM.Auth-import STM.Templates--import Network.Wai.Trans-import Network.Wai.Handler.Warp-import Network.Wai.Parse-import Network.HTTP.Types-import Web.Routes.Nested-import Data.Attoparsec.Text-import Text.Regex-import Data.Monoid-import qualified Data.Text.Lazy as LT-import qualified Data.ByteString as BS-import qualified Data.IntMap as IntMap-import Data.ByteArray (convert)--import Control.Concurrent.STM-import Control.Error.Util-import Control.Monad.Except-import Control.Monad.Reader-import Control.Monad.Trans.Maybe-import Crypto.Random-import Crypto.Hash---data LoginError = InvalidLoginAttempt--defApp :: Application-defApp _ respond = respond $ textOnlyStatus status404 "404 :("--main :: IO ()-main = do-  cacheVar <- newTVarIO IntMap.empty-  uIdVar <- newTVarIO 0-  salt <- do (n :: BS.ByteString) <- liftIO $ getRandomBytes 256-             let n' = hash n :: Digest SHA512-                 n'' = convert n' :: BS.ByteString-             return n''-  let routedApp app req resp = runReaderT (routeActionAuth authenticate routes app req resp) $ AuthEnv cacheVar uIdVar salt-      routes = do-        hereAction rootHandle-        parent ("foo" </> o_) $ do-          hereAction fooHandle-          auth ShouldBeLoggedIn unauthHandle DontProtectHere-          handleAction ("bar" </> o_) barHandle-          handleAction doubleRoute doubleHandle-        handleAction emailRoute emailHandle-        handleAction ("baz" </> o_) bazHandle-        handleAction ("login" </> o_) loginHandle-        parent ("logout" </> o_) $ do-          hereAction logoutHandle-          auth ShouldBeLoggedIn unauthHandle ProtectHere-        notFoundAction notFoundHandle-  run 3000 $ routedApp $ liftApplication defApp-  where-    rootHandle = get $ text "Home"--    -- `/foo`-    fooHandle = get $ text "foo!"--    -- `/foo/bar`-    barHandle = get $ do-      text "bar!"-      json ("json bar!" :: LT.Text)--    -- `/foo/1234e12`-    doubleRoute = p_ ("double", double) </> o_-    doubleHandle d = get $ text $ LT.pack (show d) <> " foos"--    -- `/athan@foo.com`-    emailRoute = r_ ("email", mkRegex "(^[-a-zA-Z0-9_.]+@[-a-zA-Z0-9]+\\.[-a-zA-Z0-9.]+$)") </> o_-    emailHandle e = get $ text $ LT.pack (show e) <> " email"--    -- `/baz`-    bazHandle = do-      get $ text "baz!"-      let uploader req = do liftIO $ print =<< strictRequestBody req-                            return undefined-          uploadHandle (Left Nothing) = text "Upload Failed"-          uploadHandle (Left (Just _)) = text "Should never happen"-          uploadHandle (Right _) = text "Woah! Upload content!"-          -- Hidden flaw ^ UserSession & LoginError are only options-      post uploader uploadHandle--    loginHandle = do-      get $ lucid loginPage-      postReq (login 128) loginResp-        where-          loginResp _ (Left Nothing) = textStatus status500 "Server malfunction :E"-          loginResp _ (Left (Just InvalidLoginAttempt)) = textStatusWith clearSessionResponse status403 "Bad login"-          loginResp _ (Right u) = textWith (mapResponseHeaders (++ makeSessionCookies u)) "logged-in"--    login s req =-      case requestBodyLength req of-        KnownLength b | b <= s -> do-          (ps,_) <- liftIO $ parseRequestBody (const $ const $ const $ return ()) req-          if firstIs (\(x,y) -> BS.null x || BS.null y) ps || length ps < 2-          then throwError $ Just InvalidLoginAttempt-          else do-            mSuccess <- runMaybeT $ do-              user <- hoistMaybe $ lookup "user" ps-              pass <- hoistMaybe $ lookup "password" ps-              liftIO $ putStrLn $ "<~!~> Attempted Login: " ++ show user ++ ", " ++ show pass ++ "\n"-              nUserSess <- newUserSession-              lift $ writeUserSession nUserSess-              return nUserSess-            note' (Just InvalidLoginAttempt) mSuccess-        _ -> throwError $ Just InvalidLoginAttempt-      where-        firstIs _ [] = True-        firstIs f (x:_) = f x---    logoutHandle = getReq $ \req -> do-      resp <- case getUserSession $ requestHeaders req of-        Nothing -> return "no session :s"-        Just userSession -> do-          lift $ deleteUserSession (userSessID userSession)-          return "logged-out"-      textWith clearSessionResponse resp--    unauthHandle SessionNotInCache   = get $ textStatusWith clearSessionResponse status401 "Unauthorized! - session not in cache"-    unauthHandle SessionTimedOut     = get $ textStatusWith clearSessionResponse status401 "Unauthorized! - session timed out"-    unauthHandle SessionMismatch     = get $ textStatusWith clearSessionResponse status401 "Unauthorized! - session mismatch"-    unauthHandle InvalidRequestNonce = get $ textStatusWith clearSessionResponse status401 "Unauthorized! - you're doing something sneaky!"-    unauthHandle NoSessionHeaders    = get $ textStatusWith clearSessionResponse status401 "Unauthorized! - are you sure you're logged in?"-    notFoundHandle = get $ textStatus status404 "Not Found :("-
− examples/STM/Auth.hs
@@ -1,227 +0,0 @@-{-# LANGUAGE-    FlexibleContexts-  , OverloadedStrings-  #-}--module STM.Auth where--import Network.Wai.Trans-import Network.HTTP.Types-import Web.Cookie-import Data.Attoparsec.Text-import Data.Monoid-import qualified Data.Text as T-import qualified Data.Text.Encoding as T-import qualified Data.ByteString as BS-import qualified Data.ByteString.Base64 as BS-import Blaze.ByteString.Builder (toByteString)-import qualified Data.IntMap as IntMap-import Data.ByteString.UTF8 (fromString)-import Data.ByteArray (convert)-import Data.Time-import Data.Time.ISO8601-import Data.Maybe--import Control.Concurrent.STM-import Control.Error.Util-import Control.Monad.Except-import Control.Monad.Reader-import Control.Monad.State-import Crypto.Hash----- | @sec@-data SecurityLayer = ShouldBeLoggedIn-                   | ShouldBeModerator-                   | ShouldBeAdmin-  deriving (Show, Eq, Ord)--data AuthEnv = AuthEnv-  { authEnvCache   :: TVar (IntMap.IntMap (UTCTime, BS.ByteString))-  , authEnvFreshId :: TVar Int-  , authEnvSalt :: BS.ByteString-  }---sessionId :: BS.ByteString-sessionId = "security-id"--sessionNonce :: BS.ByteString-sessionNonce = "security-token"--sessionTime :: BS.ByteString-sessionTime = "security-token-time"--makeSessionCookies :: UserSession BS.ByteString -> RequestHeaders-makeSessionCookies (UserSession i t c) = repeat "Set-Cookie" `zip` cookies-  where-    cookies =-      [ toByteString $-        renderSetCookie $ def { setCookieName = sessionId-                              , setCookieMaxAge = Just 60-                              , setCookieValue = fromString $ show i }-      , toByteString $-        renderSetCookie $ def { setCookieName = sessionTime-                              , setCookieMaxAge = Just 60-                              , setCookieValue = T.encodeUtf8 $ T.pack $ formatISO8601 t }-      , toByteString $-        renderSetCookie $ def { setCookieName = sessionNonce-                              , setCookieMaxAge = Just 60-                              , setCookieValue = c }-      ]--clearSessionResponse :: Response -> Response-clearSessionResponse = mapResponseHeaders $-  filter $ \(a,b) ->-    let cookieName = setCookieName $ parseSetCookie b-    in not $ a == "Set-Cookie" && ( cookieName == sessionId-                                 || cookieName == sessionTime-                                 || cookieName == sessionNonce )--clearSessionRequest :: Request -> Request-clearSessionRequest req =-  req {requestHeaders = go $ requestHeaders req}-  where-    go = filter $ \(a,b) ->-      let cookies = parseCookies b-          hasSession (k,_) = k == sessionId || k == sessionTime || k == sessionNonce-      in not $ a == "Cookie" && any hasSession cookies--getUserSession :: RequestHeaders -> Maybe (UserSession BS.ByteString)-getUserSession hs =-  let (mi,mt,mc) = foldr go (Nothing,Nothing,Nothing) hs-  in UserSession <$> mi <*> mt <*> mc-  where-    go (k,c) (mi,mt,mc) | k == "Cookie" =-        let cs = parseCookies c-            go' (k',c') (mi',mt',mc')-              | k' == sessionId = (hush $ parseOnly integer $ T.decodeUtf8 c', mt', mc')-              | k' == sessionTime = (mi', parseISO8601 $ T.unpack $ T.decodeUtf8 c', mc')-              | k' == sessionNonce = (mi', mt', Just c')-              | otherwise = (mi',mt',mc')-        in foldr go' (mi,mt,mc) cs-      | otherwise = (mi,mt,mc)---integer :: Parser Int-integer = do-  xs <- double-  return $ floor xs----data UserSession a = UserSession-  { userSessID    :: Int-  , userSessTime  :: UTCTime-  , userSessNonce :: a-  } deriving (Show, Eq, Ord)--newUserSession :: ( MonadIO m-                  , MonadReader AuthEnv m-                  ) => m (UserSession BS.ByteString)-newUserSession = do-  salt <- authEnvSalt <$> ask-  uIdKey <- authEnvFreshId <$> ask-  uId <- liftIO $ atomically $ do-    i <- readTVar uIdKey-    modifyTVar' uIdKey (+1)-    return i-  now <- liftIO getCurrentTime-  return $ UserSession uId now $ BS.encode $ convert $ go now salt-  where-    go :: UTCTime -> BS.ByteString -> Digest SHA512-    go now salt = hash $ salt <> fromString (formatISO8601 now)---lookupUserSession :: ( MonadIO m-                     , MonadReader AuthEnv m-                     ) => Int -> m (Maybe (UserSession BS.ByteString))-lookupUserSession i = do-  cacheKey <- authEnvCache <$> ask-  liftIO $ atomically $ do-    cache <- readTVar cacheKey-    return $ do (t,c) <- IntMap.lookup i cache-                return $ UserSession i t c--deleteUserSession :: ( MonadIO m-                     , MonadReader AuthEnv m-                     ) => Int -> m ()-deleteUserSession i = do-  cacheKey <- authEnvCache <$> ask-  liftIO $ atomically $ modifyTVar' cacheKey (IntMap.delete i)---writeUserSession :: ( MonadIO m-                    , MonadReader AuthEnv m-                    ) => UserSession BS.ByteString -> m ()-writeUserSession (UserSession i t c) = do-  cacheKey <- authEnvCache <$> ask-  liftIO $ atomically $ modifyTVar' cacheKey $ IntMap.insert i (t, c)---checkUserSession :: MonadReader AuthEnv m =>-                    UserSession BS.ByteString-                 -> m Bool-checkUserSession (UserSession _ t c) = do-  salt <- authEnvSalt <$> ask-  let old = hash $ salt <> fromString (formatISO8601 t) :: Digest SHA512-      old' = BS.encode $ convert old :: BS.ByteString-  return $ c == old'---data AuthenticationError = NoSessionHeaders-                         | InvalidRequestNonce-                         | SessionNotInCache-                         | SessionMismatch-                         | SessionTimedOut---authenticate :: ( MonadIO m-                , MonadReader AuthEnv m-                ) => Request -> [SecurityLayer] -> m (Response -> Response, Maybe AuthenticationError)-authenticate _ [] = return (id, Nothing)-authenticate req _ = flip runStateT Nothing $ do-  liftIO $ putStrLn "<!> Auth Attempt <!>"-  liftIO $ putStrLn "--- Headers ----------- \n" >> print (requestHeaders req) >> putStrLn "\n"-  let mUserSession = getUserSession $ requestHeaders req-  put $ Just NoSessionHeaders <* mUserSession-  maybe (return id) hasUserSession mUserSession--hasUserSession userSession = do-  liftIO $ putStrLn "--- User Session ------ \n" >> print userSession >> putStrLn "\n"-  cacheKey <- authEnvCache <$> ask-  cache <- liftIO $ readTVarIO cacheKey-  liftIO $ putStrLn "--- Cache ------------- \n" >> print cache >> putStrLn "\n"-  mCachedSession <- lookupUserSession (userSessID userSession)-  put $ Just SessionNotInCache <* mCachedSession-  maybe (return id) (hasCachedSession userSession) mCachedSession--hasCachedSession userSession (UserSession i cachedTime cachedNonce) = do-  newUserSess' <- newUserSession-  let newUserSess = newUserSess' {userSessID = i}-  userSessionIsValid <- checkUserSession userSession-  case () of-    () | diffUTCTime (userSessTime newUserSess) cachedTime >= 60 -> do-           deleteUserSession i-           put $ Just SessionTimedOut-           return id-       | cachedNonce /= userSessNonce userSession -> do-           put $ Just SessionMismatch-           return id-       | not userSessionIsValid -> do-           put $ Just InvalidRequestNonce-           return id-       | otherwise -> do-           writeUserSession newUserSess-           return $ mapResponseHeaders (++ makeSessionCookies newUserSess)-                      . clearSessionResponse---note' :: MonadError e m => e -> Maybe a -> m a-note' e mx = fromMaybe (throwError e) $ pure <$> mx---insert :: Eq k => k -> a -> [(k,a)] -> [(k,a)]-insert k v [] = [(k,v)]-insert k v ((k',v'):xs) | k == k' = (k,v):xs-                        | otherwise = (k',v'): insert k v xs
nested-routes.cabal view
@@ -1,5 +1,5 @@ Name:                   nested-routes-Version:                6.1.0+Version:                7.0.0 Author:                 Athan Clark <athan.clark@gmail.com> Maintainer:             Athan Clark <athan.clark@gmail.com> License:                BSD3@@ -20,9 +20,7 @@   <https://hackage.haskell.org/package/attoparsec Attoparsec>   parsers and <https://hackage.haskell.org/package/regex-compat Regular Expressions>   /directly/ in our routes, with our handlers-  reflecting their results. You can find more information in the-  <https://www.fpcomplete.com/user/AthanClark/nested-routes demo>.-  and the examples.+  reflecting their results. Cabal-Version:          >= 1.10 Build-Type:             Simple @@ -31,34 +29,32 @@   Description:          Build the trivial example.   Default:              False -Flag Example-STM-  Description:          Build the Sha512 / STM nonce cache example.-  Default:              False-- Library   Default-Language:     Haskell2010   HS-Source-Dirs:       src   GHC-Options:          -Wall   Exposed-Modules:      Web.Routes.Nested+                        Web.Routes.Nested.Match                         Web.Routes.Nested.Types-                        Web.Routes.Nested.Types.UrlChunks-  Build-Depends:        base >= 4.6 && < 5+  Build-Depends:        base >= 4.8 && < 5                       , attoparsec                       , bytestring                       , composition-extra >= 2.0.0-                      , containers+                      , errors+                      , exceptions+                      , hashable                       , mtl                       , poly-arity >= 0.0.7-                      , pred-trie >= 0.3+                      , pred-trie >= 0.5.0                       , regex-compat                       , semigroups                       , text                       , transformers                       , tries-                      , wai-transformers >= 0.0.3-                      , wai-middleware-content-type >= 0.0.3.1-                      , wai-middleware-verbs >= 0.0.4+                      , unordered-containers+                      , wai-transformers >= 0.0.4+                      , wai-middleware-content-type >= 0.4.0+                      , wai-middleware-verbs >= 0.2.0   Test-Suite test@@ -73,13 +69,14 @@                         Web.Routes.NestedSpec.Basic                         Web.Routes.Nested                         Web.Routes.Nested.Types-                        Web.Routes.Nested.Types.UrlChunks   Build-Depends:        base                       , nested-routes                       , attoparsec                       , bytestring                       , composition-extra-                      , containers+                      , errors+                      , exceptions+                      , hashable                       , http-types                       , mtl                       , poly-arity@@ -89,6 +86,7 @@                       , text                       , transformers                       , tries+                      , unordered-containers                       , wai-transformers                       , wai-middleware-content-type                       , wai-middleware-verbs@@ -108,13 +106,14 @@   Main-Is:              Main.hs   Other-Modules:        Web.Routes.Nested                         Web.Routes.Nested.Types-                        Web.Routes.Nested.Types.UrlChunks   Build-Depends:        base-                      , nested-routes+                      , nested-routes >= 7                       , attoparsec                       , bytestring                       , composition-extra-                      , containers+                      , errors+                      , exceptions+                      , hashable                       , http-types                       , mtl                       , poly-arity@@ -124,60 +123,13 @@                       , text                       , transformers                       , tries+                      , unordered-containers                       , wai-transformers                       , wai-middleware-content-type                       , wai-middleware-verbs                       , warp  -Executable example-stm-  if flag(Example-STM)-    Buildable: True-  else-    Buildable: False-  Default-Language:     Haskell2010-  HS-Source-Dirs:       src-                      , examples-  GHC-Options:          -Wall-  Main-Is:              STM.hs-  Other-Modules:        STM.Auth-                        Web.Routes.Nested-                        Web.Routes.Nested.Types-                        Web.Routes.Nested.Types.UrlChunks-  Build-Depends:        base-                      , base64-bytestring-                      , blaze-builder-                      , nested-routes-                      , attoparsec-                      , bytestring-                      , composition-extra-                      , containers-                      , cookie-                      , cryptonite-                      , data-default-                      , errors-                      , http-types-                      , iso8601-time-                      , lucid-                      , memory-                      , mtl-                      , poly-arity-                      , pred-trie-                      , regex-compat-                      , semigroups-                      , stm-                      , text-                      , time-                      , transformers-                      , tries-                      , wai-extra-                      , wai-middleware-content-type-                      , wai-middleware-verbs-                      , wai-transformers-                      , wai-session-                      , warp-                      , utf8-string- Source-Repository head   Type:                 git-  Location:             git://github.com/athanclark/nested-routes.git+  Location:             https://github.com/athanclark/nested-routes.git
src/Web/Routes/Nested.hs view
@@ -2,7 +2,7 @@     GADTs   , PolyKinds   , TypeFamilies-  , DeriveFunctor+  , BangPatterns   , TypeOperators   , TupleSections   , DoAndIfThenElse@@ -10,486 +10,345 @@   , FlexibleContexts   , OverloadedStrings   , ScopedTypeVariables-  , GeneralizedNewtypeDeriving   #-} --- |--- Module      : Web.Routes.Nested--- Copyright   : (c) 2015 Athan Clark------ License     : BSD-style--- Maintainer  : athan.clark@gmail.com--- Stability   : experimental--- Portability : GHC------ This module exports most of what you'll need for sophisticated routing ---- all the tools from <https://hackage.haskell.org/package/wai-middleware-verbs wai-middleware-verbs>--- (routing for the incoming HTTP method) and--- <https://hackage.haskell.org/package/wai-middleware-content-type wai-middleware-content-type>--- (routing for the incoming Accept header, and implied file extension),--- <https://hackage.haskell.org/package/wai WAI> itself, and--- <https://hackage.haskell.org/package/wai-transformers wai-transformers> - some simple--- type aliases wrapped around WAI's @Application@ and @Middleware@ types, allowing us--- to embed monad transformer stacks for our applications.------ The routing system lets you embed these complicated HTTP verb / content-type--- sensative responses just as easily as a WAI @Middleware@. There is enough--- tooling provided to use one paradigm or the other. Note - nested-routes--- does not affect the @pathInfo@ of the incoming @Request@ in any way, but merely--- matches on it and passes control to the designated response.------ To match a route, you have a few options - you can match against a string literal,--- a regular expression (via <https://hackage.haskell.org/package/regex-compat regex-compat>),--- or an <https://hackage.haskell.org/package/attoparsec attoparsec> parser. This list--- will most likely grow in the future, depending on demand.------ There is also support for embedding security layers in your routes, in the same--- nested manner. By "tagging" a set of routes with an authorization role (with @auth@),--- you populate a list of roles breached during any request. In the authentication--- parameter in @routeAuth@ and @routeActionAuth@, the function--- keeps the session integrity in-place, while @auth@ lets you create your authorization--- boundaries. Both are symbiotic and neccessary for establishing security, and both allow--- you to tap into the monad transformer stack to do logging, STM, database queries,--- etc.------ To use your set of routes in a WAI application, you need to "extract" the--- functionality from your route set - using the @route@, @routeAuth@, @routeAction@,--- and @routeActionAuth@--- functions, you can create monolithic apps very easily.--- But, if you would like to extract the security middleware to place before--- say, a /static/ middleware you already have in place, use the @extractAuth@--- functions, and others for their respective purposes. This way, you can decompose--- the routing system into each subject matter, and re-compose (@.@) them in whichever--- order you like for your application.+{- |+Module      : Web.Routes.Nested+Copyright   : (c) 2015 Athan Clark +License     : BSD-style+Maintainer  : athan.clark@gmail.com+Stability   : experimental+Portability : GHC +This module exports most of what you'll need for sophisticated routing -+all the tools from <https://hackage.haskell.org/package/wai-middleware-verbs wai-middleware-verbs>+(routing for the incoming HTTP method) and+<https://hackage.haskell.org/package/wai-middleware-content-type wai-middleware-content-type>+(routing for the incoming Accept header, and implied file extension),+<https://hackage.haskell.org/package/wai WAI> itself, and+<https://hackage.haskell.org/package/wai-transformers wai-transformers> - some simple+type aliases wrapped around WAI's @Application@ and @Middleware@ types, allowing us+to embed monad transformer stacks for our applications. +To match a route, you have a few options - you can match against a string literal,+a regular expression (via <https://hackage.haskell.org/package/regex-compat regex-compat>),+or an <https://hackage.haskell.org/package/attoparsec attoparsec> parser. This list+will most likely grow in the future, depending on demand.++There is also support for embedding security layers in your routes, in the same+nested manner. By "tagging" a set of routes with an authorization role (with @auth@),+you populate a list of roles breached during any request. The function argument to+'routeAuth' guards a Request to pass or fail at the high level, while 'auth' lets+you create your authorization boundaries on a case-by-case basis. Both allow+you to tap into the monad transformer stack for logging, STM variables, database queries,+etc.+-}++ module Web.Routes.Nested-  ( module X-  -- * Types-  , Tries-  , HandlerT (..)-  , execHandlerT-  , ActionT-  , RoutableT-  , RoutableActionT-  , AuthScope (..)-  , ExtrudeSoundly-  -- * Combinators-  , handle-  , handleAction-  , here-  , hereAction-  , handleAny-  , handleAnyAction-  , parent+  ( -- * Combinators+    match+  , matchHere+  , matchAny+  , matchGroup   , auth-  , notFound-  , notFoundAction-  , action-  -- * Routing-  , route+  , -- * Routing+    route   , routeAuth-  , routeAction-  , routeActionAuth-  -- * Extraction-  , extractContent-  , extractNotFound+  , extractMatch+  , extractMatchAny   , extractAuthSym   , extractAuth   , extractNearestVia-  , actionToMiddleware+  , -- * Metadata+    SecurityToken (..)+  , AuthScope (..)+  , -- * Re-Exports+    module Web.Routes.Nested.Match+  , module Web.Routes.Nested.Types+  , module Network.Wai.Middleware.Verbs+  , module Network.Wai.Middleware.ContentType   ) where -import           Web.Routes.Nested.Types            as X-import           Network.Wai.Trans                  as X-import           Network.Wai.Middleware.Verbs       as X-import           Network.Wai.Middleware.ContentType as X+import           Web.Routes.Nested.Match+import           Web.Routes.Nested.Types+import           Network.Wai.Trans+import           Network.Wai.Middleware.Verbs+import           Network.Wai.Middleware.ContentType hiding (responseStatus, responseHeaders, responseData) -import           Data.Trie.Pred                     (RootedPredTrie (..), PredTrie (..))-import qualified Data.Trie.Pred                     as PT -- only using lookups-import           Data.Trie.Pred.Step                (PredStep (..), PredSteps (..))+import qualified Data.Trie.Pred.Base                as PT -- only using lookups+import           Data.Trie.Pred.Base                (RootedPredTrie (..), PredTrie (..))+import           Data.Trie.Pred.Base.Step           (PredStep (..), PredSteps (..))+import           Data.Trie.Pred.Interface.Types     (Singleton (..), Extrude (..), CatMaybes) import qualified Data.Trie.Class                    as TC-import           Data.Trie.Map                      (MapStep (..))-import qualified Data.Map                           as Map-import           Data.List                          (stripPrefix)+import           Data.Trie.HashMap                  (HashMapStep (..), HashMapChildren (..))+import qualified Data.HashMap.Lazy                  as HM import           Data.List.NonEmpty                 (NonEmpty (..)) import qualified Data.List.NonEmpty                 as NE import qualified Data.Text                          as T-import           Data.Maybe                         (fromMaybe)+import           Data.Hashable import           Data.Monoid-import           Data.Foldable import           Data.Functor.Syntax import           Data.Function.Poly  import           Control.Monad-import           Control.Monad.IO.Class-import           Control.Monad.Trans import qualified Control.Monad.State                as S+import           Control.Monad.Catch+import           Control.Monad.Trans  -type Tries x s e = ( RootedPredTrie T.Text x-                   , RootedPredTrie T.Text x-                   , RootedPredTrie T.Text s-                   , RootedPredTrie T.Text e-                   )--newtype HandlerT x sec err aux m a = HandlerT-  { runHandlerT :: S.StateT (Tries x sec err) m a }-  deriving ( Functor-           , Applicative-           , Monad-           , MonadIO-           , MonadTrans-           , S.MonadState (Tries x sec err)-           )--execHandlerT :: Monad m => HandlerT x sec err aux m a -> m (Tries x sec err)-execHandlerT hs = S.execStateT (runHandlerT hs) mempty--type ActionT e u m a = VerbListenerT (FileExtListenerT (MiddlewareT m) m a) e u m a---- | Turn an @ActionT@ into a @MiddlewareT@ - could be used to make middleware-based--- route sets cooperate with the content-type and verb combinators.-action :: MonadIO m => ActionT e u m () -> MiddlewareT m-action xs = verbsToMiddleware $ mapVerbs fileExtsToMiddleware xs+-- | Embed a 'Network.Wai.Trans.MiddlewareT' into a set of routes via a matching string. You should+--   expect the match to create /arity/ in your handler - the @childContent@ variable.+--   The arity of @childContent@ may grow or shrink, depending on the heterogeneous+--   list created from the list of parsers, regular expressions, or arbitrary predicates+--   /in the order written/ - so something like:+--+--   > match (p_ "double-parser" double </> o_)+--   >   handler+--+--   ...then @handler@ /must/ have arity @Double ->@. If this+--   route was at the top level, then the total arity __must__ be @Double -> MiddlewareT m@.+--+--   Generally, if the routes you are building get grouped+--   by a predicate with 'matchGroup',+--   then we would need another level of arity /before/ the @Double@.+match :: ( Monad m+         , Singleton (UrlChunks xs)+             childContent+             (RootedPredTrie T.Text resultContent)+         , cleanxs ~ CatMaybes xs+         , ArityTypeListIso childContent cleanxs resultContent+         ) => UrlChunks xs+           -> childContent+           -> HandlerT resultContent sec m ()+match !ts !vl =+  tell' $ Tries (singleton ts vl)+                mempty+                mempty  -type RoutableT s e u ue m a =-  HandlerT (MiddlewareT m) (s, AuthScope) (e -> MiddlewareT m) (e,u,ue) m a--type RoutableActionT s e u ue m a =-  HandlerT (ActionT ue u m ()) (s, AuthScope) (e -> ActionT ue u m ()) (e,u,ue) m a--type ExtrudeSoundly cleanxs xs c r =-  ( cleanxs ~ CatMaybes xs-  , ArityTypeListIso c cleanxs r-  , Extrude (UrlChunks xs)-      (RootedPredTrie T.Text c)-      (RootedPredTrie T.Text r)-  )+{-# INLINEABLE match #-} +-- | Create a handle for the /current/ route - an alias for @\h -> match o_ h@.+matchHere :: ( Monad m+             ) => content+               -> HandlerT content sec m ()+matchHere = match origin_ --- | Embed an @ActionT@ into a set of routes directly, without first converting--- it to a @MiddlewareT@.-handleAction :: ( Monad m-                , Functor m-                , HasResult childContent (ActionT ue u m ())-                , HasResult err          (e -> ActionT ue u m ())-                , Singleton (UrlChunks xs)-                    childContent-                    (RootedPredTrie T.Text resultContent)-                , cleanxs ~ CatMaybes xs-                , ArityTypeListIso childContent cleanxs resultContent-                ) => UrlChunks xs-                  -> childContent-                  -> HandlerT resultContent sec err (e,u,ue) m ()-handleAction ts vl = tell' (singleton ts vl, mempty, mempty, mempty)+{-# INLINEABLE matchHere #-}  --- | Embed a @MiddlewareT@ into a set of routes.-handle :: ( Monad m-          , Functor m-          , HasResult childContent (MiddlewareT m)-          , HasResult err     (e -> MiddlewareT m)-          , Singleton (UrlChunks xs)-              childContent-              (RootedPredTrie T.Text resultContent)-          , cleanxs ~ CatMaybes xs-          , ArityTypeListIso childContent cleanxs resultContent-          ) => UrlChunks xs-            -> childContent-            -> HandlerT resultContent sec err (e,u,ue) m ()-handle ts vl = tell' (singleton ts vl, mempty, mempty, mempty)+-- | Match against any route, as a last resort against all failing matches -+--   use this for a catch-all at some level in their routes, something+--   like a @not-found 404@ page is useful.+matchAny :: ( Monad m+            ) => content+              -> HandlerT content sec m ()+matchAny !vl =+  tell' $ Tries mempty+                (singleton origin_ vl)+                mempty  -hereAction :: ( Monad m-              , Functor m-              , HasResult content (ActionT ue u m ())-              , HasResult err     (e -> ActionT ue u m ())-              ) => content-                -> HandlerT content sec err (e,u,ue) m ()-hereAction = handleAction origin_---- | Create a handle for the present route - an alias for @\h -> handle o (Just h)@.-here :: ( Monad m-        , Functor m-        , HasResult content (MiddlewareT m)-        , HasResult err     (e -> MiddlewareT m)-        ) => content-          -> HandlerT content sec err (e,u,ue) m ()-here = handle origin_+{-# INLINEABLE matchAny #-}  -handleAnyAction :: ( Monad m-                   , Functor m-                   , HasResult content (ActionT ue u m ())-                   , HasResult err     (e -> ActionT ue u m ())-                   ) => content-                     -> HandlerT content sec err (e,u,ue) m ()-handleAnyAction vl = tell' (mempty, singleton origin_ vl, mempty, mempty)---- | Match against any route, as a last resort against all failing @handle@s.-handleAny :: ( Monad m-             , Functor m-             , HasResult content (MiddlewareT m)-             , HasResult err     (e -> MiddlewareT m)-             ) => content-               -> HandlerT content sec err (e,u,ue) m ()-handleAny vl = tell' (mempty, singleton origin_ vl, mempty, mempty)+-- | Prepends a common route to an existing set of routes. You should note that+--   doing this with a parser or regular expression will necessitate the existing+--   arity in the handlers before the progam can compile.+matchGroup :: ( Monad m+              , cleanxs ~ CatMaybes xs+              , ExtrudeSoundly cleanxs xs childContent resultContent+              , ExtrudeSoundly cleanxs xs childSec     resultSec+              ) => UrlChunks xs+                -> HandlerT childContent  childSec  m ()+                -> HandlerT resultContent resultSec m ()+matchGroup !ts cs = do+  (Tries trieContent trieNotFound trieSec) <- lift $ execHandlerT cs+  tell' $ Tries (extrude ts trieContent)+                (extrude ts trieNotFound)+                (extrude ts trieSec)  --- | Prepend a path to an existing set of routes.-parent :: ( Monad m-          , Functor m-          , cleanxs ~ CatMaybes xs-          , ExtrudeSoundly cleanxs xs childContent resultContent-          , ExtrudeSoundly cleanxs xs childSec     resultSec-          , ExtrudeSoundly cleanxs xs childErr     resultErr-          ) => UrlChunks xs-            -> HandlerT childContent  childSec  childErr  aux m ()-            -> HandlerT resultContent resultSec resultErr aux m ()-parent ts cs = do-  (trieContent,trieNotFound,trieSec,trieErr) <- lift $ execHandlerT cs-  tell' ( extrude ts trieContent-        , extrude ts trieNotFound-        , extrude ts trieSec-        , extrude ts trieErr-        )+{-# INLINEABLE matchGroup #-} +-- | Use a custom security token type and an 'AuthScope' to define+--   /where/ and /what kind/ of security should take place.+data SecurityToken s = SecurityToken+  { securityToken :: !s+  , securityScope :: !AuthScope+  } deriving (Show)  -- | Designate the scope of security to the set of routes - either only the adjacent -- routes, or the adjacent /and/ the parent container node (root node if not -- declared).-data AuthScope = ProtectHere | DontProtectHere+data AuthScope+  = ProtectHere+  | DontProtectHere   deriving (Show, Eq)  -- | Sets the security role and error handler for a set of routes, optionally -- including its parent route. auth :: ( Monad m-        , Functor m         ) => sec-          -> err           -> AuthScope-          -> HandlerT content (sec, AuthScope) err aux m ()-auth token handleFail scope =-  tell' ( mempty-        , mempty-        , RootedPredTrie (Just (token,scope)) mempty-        , RootedPredTrie (Just handleFail) mempty-        )+          -> HandlerT content (SecurityToken sec) m ()+auth !token !scope =+  tell' $ Tries mempty+                mempty+                (singleton origin_ $ SecurityToken token scope)  --- | Embed an @ActionT@ as a not-found handler into a set of routes, without first converting--- it to a @MiddlewareT@.-notFoundAction :: ( Monad m-                  , Functor m-                  , HasResult content (ActionT ue u m ())-                  , HasResult err     (e -> ActionT ue u m ())-                  ) => content-                    -> HandlerT content sec err (e,u,ue) m ()-notFoundAction = handleAnyAction---- | Embed a @MiddlewareT@ as a not-found handler into a set of routes.-notFound :: ( Monad m-            , Functor m-            , HasResult content (MiddlewareT m)-            , HasResult err     (e -> MiddlewareT m)-            ) => content-              -> HandlerT content sec err (e,u,ue) m ()-notFound = handleAny-+{-# INLINEABLE auth #-}   -- * Routing --------------------------------------- --- | Turns a @HandlerT@ containing @MiddlewareT@s into a @MiddlewareT@.-route :: ( Functor m-         , Monad m-         , MonadIO m-         ) => HandlerT (MiddlewareT m) sec err aux m () -- ^ Assembled @handle@ calls+route :: ( Monad m+         ) => HandlerT (MiddlewareT m) sec m a            -> MiddlewareT m-route hs = extractContent hs . extractNotFound hs-+route hs app req resp = do+  let path = pathInfo req+  mMatch <- extractMatch path hs+  case mMatch of+    Nothing -> do+      mMatch <- extractMatchAny path hs+      maybe+        (app req resp)+        (\mid -> mid app req resp)+        mMatch+    Just mid -> mid app req resp --- | Given a security verification function that returns a method to updating the session,--- turn a set of routes containing @MiddlewareT@s into a @MiddlewareT@, where a session--- is secured before responding.-routeAuth :: ( Functor m-             , Monad m-             , MonadIO m-             ) => (Request -> [sec] -> m (Response -> Response, Maybe e)) -- ^ authorize-               -> RoutableT sec e u ue m () -- ^ Assembled @handle@ calls+routeAuth :: ( Monad m+             , MonadThrow m+             ) => (Request -> [sec] -> m ())+               -> HandlerT (MiddlewareT m) (SecurityToken sec) m a                -> MiddlewareT m-routeAuth authorize hs = extractAuth authorize hs . route hs+routeAuth authorize hs app req resp = do+  extractAuth authorize req hs+  route hs app req resp --- | Exactly like @route@, except specialized to route sets that contain @ActionT@s ---- essentially @fmap@ing @action@ to each element.-routeAction :: ( Functor m-               , Monad m-               , MonadIO m-               ) => RoutableActionT sec e u ue m ()-                 -> MiddlewareT m-routeAction = route . actionToMiddleware+-- * Extraction ------------------------------- --- | Exactly like @routeAuth@, but specialized for @ActionT@.-routeActionAuth :: ( Functor m-                   , Monad m-                   , MonadIO m-                   ) => (Request -> [sec] -> m (Response -> Response, Maybe e)) -- ^ authorize-                     -> RoutableActionT sec e u ue m () -- ^ Assembled @handle@ calls-                     -> MiddlewareT m-routeActionAuth authorize = routeAuth authorize . actionToMiddleware+-- | Extracts only the normal 'match' and 'matchHere'+extractMatch :: ( Monad m+                ) => [T.Text]+                  -> HandlerT r sec m a+                  -> m (Maybe r)+extractMatch path !hs = do+  trie <- trieContent <$> execHandlerT hs+  case matchWithLRPT trimFileExt path trie of+    Nothing -> return $ do+      guard $ not (null path)+      guard $ trimFileExt (last path) == "index"+      TC.lookup (init path) trie+    Just (_,r) -> return (Just r) +{-# INLINEABLE extractMatch #-} --- | Turns a @HandlerT@ containing @ActionT@s into a @HandlerT@ containing @MiddlewareT@s.-actionToMiddleware :: MonadIO m =>-                      RoutableActionT sec e u ue m ()-                   -> RoutableT sec e u ue m ()-actionToMiddleware hs = do-  (rtrie,nftrie,strie,errtrie) <- lift $ execHandlerT hs-  tell' ( action <$> rtrie-        , action <$> nftrie-        , strie-        , (action .) <$> errtrie-        ) +-- | Extracts only the @notFound@ responses+extractMatchAny :: ( Monad m+                   ) => [T.Text]+                     -> HandlerT r sec m a+                     -> m (Maybe r)+extractMatchAny path = extractNearestVia path (\x -> trieCatchAll <$> execHandlerT x) --- * Extraction -------------------------------+{-# INLINEABLE extractMatchAny #-} --- | Extracts only the normal @handle@ (content) routes into--- a @MiddlewareT@, disregarding security and not-found responses.-extractContent :: ( Functor m-                  , Monad m-                  , MonadIO m-                  ) => HandlerT (MiddlewareT m) sec err aux m a-                    -> MiddlewareT m-extractContent hs app req respond = do-  (trie,_,_,_) <- execHandlerT hs-  case matchWithLRPT trimFileExt (pathInfo req) trie of-    Nothing -> fromMaybe (app req respond) $ do-      guard $ not . null $ pathInfo req-      guard $ trimFileExt (last $ pathInfo req) == "index"-      mid <- TC.lookup (init $ pathInfo req) trie-      Just $    mid app (adjustPathInfo (init $ pathInfo req) req) respond-    Just (prefix,mid) -> mid app (adjustPathInfo prefix req) respond   -- | Find the security tokens / authorization roles affiliated with--- a request for a set of routes.-extractAuthSym :: ( Functor m-                  , Monad m-                  ) => HandlerT x (sec, AuthScope) err aux m a-                    -> Request+--   a request for a set of routes.+extractAuthSym :: ( Monad m+                  ) => [T.Text]+                    -> HandlerT x (SecurityToken sec) m a                     -> m [sec]-extractAuthSym hs req = do-  (_,_,trie,_) <- execHandlerT hs-  return $ foldl go [] (PT.matchesRPT (pathInfo req) trie)+extractAuthSym path hs = do+  trie <- trieSecurity <$> execHandlerT hs+  return $! foldr go [] $ PT.matchesRPT path trie   where-    go ys (_,(_,DontProtectHere),[]) = ys-    go ys (_,(x,_              ),_ ) = ys ++ [x]+    go (_,SecurityToken _ DontProtectHere,[]) ys = ys+    go (_,SecurityToken x _              ,_ ) ys = x:ys --- | Extracts only the security handling logic into a @MiddlewareT@.-extractAuth :: ( Functor m-               , Monad m-               , MonadIO m-               ) => (Request -> [sec] -> m (Response -> Response, Maybe e)) -- authorization method-                 -> HandlerT x (sec, AuthScope) (e -> MiddlewareT m) aux m a-                 -> MiddlewareT m-extractAuth authorize hs app req respond = do-  (_,_,_,trie) <- execHandlerT hs-  ss <- extractAuthSym hs req-  (f,me) <- authorize req ss-  fromMaybe (app req (respond . f)) $ do-    e <- me-    (prefix,mid,_) <- PT.matchRPT (pathInfo req) trie-    return $ mid e app (adjustPathInfo prefix req) (respond . f)+{-# INLINEABLE extractAuthSym #-} +-- | Extracts only the security handling logic, and turns it into a guard+extractAuth :: ( Monad m+               , MonadThrow m+               ) => (Request -> [sec] -> m ()) -- authorization method+                 -> Request+                 -> HandlerT x (SecurityToken sec) m a+                 -> m ()+extractAuth authorize req hs = do+  ss <- extractAuthSym (pathInfo req) hs+  authorize req ss --- | Extracts only the @notFound@ responses into a @MiddlewareT@.-extractNotFound :: ( Functor m-                   , Monad m-                   , MonadIO m-                   ) => HandlerT (MiddlewareT m) sec err aux m a-                     -> MiddlewareT m-extractNotFound = extractNearestVia (execHandlerT >=> \(_,t,_,_) -> return t)+{-# INLINEABLE extractAuth #-}   -- | Given a way to draw out a special-purpose trie from our route set, route--- to the responses based on a /furthest-reached/ method.-extractNearestVia :: ( Functor m-                     , Monad m-                     , MonadIO m-                     ) => (HandlerT (MiddlewareT m) sec err aux m a -> m (RootedPredTrie T.Text (MiddlewareT m)))-                       -> HandlerT (MiddlewareT m) sec err aux m a-                       -> MiddlewareT m-extractNearestVia extr hs app req respond = do+--   to the responses based on a /furthest-reached/ method.+extractNearestVia :: ( Monad m+                     ) => [T.Text]+                       -> (HandlerT r sec m a -> m (RootedPredTrie T.Text r))+                       -> HandlerT r sec m a+                       -> m (Maybe r)+extractNearestVia path extr hs = do   trie <- extr hs-  maybe (app req respond)-        (\(prefix, mid,_) -> mid app (adjustPathInfo prefix req) respond)-      $ PT.matchRPT (pathInfo req) trie+  pure (mid <$> PT.matchRPT path trie)+  where+    mid (_,r,_) = r +{-# INLINEABLE extractNearestVia #-}  + -- * Pred-Trie related -----------------  -- | Removes @.txt@ from @foo.txt@ trimFileExt :: T.Text -> T.Text-trimFileExt s =-  let lastExt = getLastExt (T.unpack s)-  in if lastExt `elem` possibleExts-     then T.pack lastExt-     else s-  where-    possibleExts = [ ".html",".htm",".txt",".json",".lucid"-                   , ".julius",".css",".cassius",".lucius"-                   ]-    getLastExt ts = S.evalState (foldrM go [] ts) False-      where-        go c soFar = do-          sawPeriod <- S.get-          if sawPeriod-          then return soFar-          else if c == '.'-               then do S.put True-                       return ('.' : soFar)-               else    return (c : soFar)+trimFileExt !s = fst $! T.breakOn "." s +{-# INLINEABLE trimFileExt #-} + -- | A quirky function for processing the last element of a lookup path, only -- on /literal/ matches.-matchWithLPT :: Ord s => (s -> s) -> NonEmpty s -> PredTrie s a -> Maybe ([s], a)-matchWithLPT f (t:|ts) (PredTrie (MapStep ls) (PredSteps ps))-  | null ts   = getFirst $ First (goLit (f t) ls) <> foldMap (First . goPred) ps-  | otherwise = getFirst $ First (goLit    t  ls) <> foldMap (First . goPred) ps+matchWithLPT :: ( Hashable s+                , Eq s+                ) => (s -> s) -> NonEmpty s -> PredTrie s a -> Maybe ([s], a)+matchWithLPT f (t:|ts) (PredTrie (HashMapStep ls) (PredSteps ps))+  | null ts   = getFirst $ First ((goLit $! f t) ls) <> foldMap (First . goPred) ps+  | otherwise = getFirst $ First (goLit       t  ls) <> foldMap (First . goPred) ps   where     goLit t' xs = do-      (mx,mxs) <- Map.lookup t' xs+      (HashMapChildren mx mxs) <- HM.lookup t' xs       if null ts       then ([t],) <$> mx-      else (\(ts',x) -> (t:ts',x)) <$> (matchWithLPT f (NE.fromList ts) =<< mxs)---- TODO: Need `match` for Map? idk actually, `t` should be enough for this step.+      else fmap (\(ts',x) -> (t:ts',x)) $! matchWithLPT f (NE.fromList ts) =<< mxs      goPred (PredStep _ predicate mx xs) = do       d <- predicate t       if null ts       then ([t],) <$> (mx <$~> d)-      else (\(ts',x) -> (t:ts',x d)) <$> (matchWithLPT f (NE.fromList ts) xs)+      else fmap (\(ts',x) -> (t:ts',x d)) $! matchWithLPT f (NE.fromList ts) xs -matchWithLRPT :: Ord s => (s -> s) -> [s] -> RootedPredTrie s a -> Maybe ([s], a)+{-# INLINEABLE matchWithLPT #-}+++matchWithLRPT :: ( Hashable s+                 , Eq s+                 ) => (s -> s) -> [s] -> RootedPredTrie s a -> Maybe ([s], a) matchWithLRPT _ [] (RootedPredTrie mx _) = ([],) <$> mx matchWithLRPT f ts (RootedPredTrie _ xs) = matchWithLPT f (NE.fromList ts) xs +{-# INLINEABLE matchWithLRPT #-} + tell' :: (Monoid w, S.MonadState w m) => w -> m ()-tell' x = do-  xs <- S.get-  S.put $ xs <> x+tell' x = S.modify' (<> x) +{-# INLINEABLE tell' #-} -adjustPathInfo :: [T.Text] -> Request -> Request-adjustPathInfo matched req' =-  req' { pathInfo = fromMaybe (error "non-prefix on match") $-                      stripPrefix matched (pathInfo req')-       }
+ src/Web/Routes/Nested/Match.hs view
@@ -0,0 +1,100 @@+{-# LANGUAGE+    GADTs+  , DataKinds+  , RankNTypes+  , TypeOperators+  , OverloadedStrings+  #-}++{- |+Module      : Web.Routes.Nested.Match+Copyright   : (c) 2015 Athan Clark++License     : BSD-style+Maintainer  : athan.clark@gmail.com+Stability   : experimental+Portability : GHC+-}++module Web.Routes.Nested.Match+  ( -- * Path Combinators+    o_+  , origin_+  , l_+  , literal_+  , f_+  , file_+  , p_+  , parse_+  , r_+  , regex_+  , pred_+  , (</>)+  , -- ** Path Types+    EitherUrlChunk+  , UrlChunks+  ) where++import Prelude hiding (pred)+import Data.Attoparsec.Text+import Text.Regex+import qualified Data.Text as T+import Control.Monad+import Control.Error (hush)+import Data.Trie.Pred+++o_, origin_ :: UrlChunks '[]+o_ = origin_++-- | The /Origin/ chunk - the equivalent to @[]@+origin_ = nil+++l_, literal_ :: T.Text -> EitherUrlChunk 'Nothing+l_ = literal_++-- | Match against a /Literal/ chunk+literal_ = only++f_, file_ :: T.Text -> EitherUrlChunk ('Just T.Text)+f_ = file_++-- | Removes file extension from the matched route+file_ f = pred_ f (\t -> t <$ guard (fst (T.breakOn "." t) == f))+++p_, parse_ :: T.Text -> Parser r -> EitherUrlChunk ('Just r)+p_ = parse_++-- | Match against a /Parsed/ chunk, with <https://hackage.haskell.org/package/attoparsec attoparsec>.+parse_ i q = pred_ i (hush . parseOnly q)+++r_, regex_ :: T.Text -> Regex -> EitherUrlChunk ('Just [String])+r_ = regex_++-- | Match against a /Regular expression/ chunk, with <https://hackage.haskell.org/package/regex-compat regex-compat>.+regex_ i q = pred_ i (matchRegex q . T.unpack)++-- | Match with a predicate against the url chunk directly.+pred_ :: T.Text -> (T.Text -> Maybe r) -> EitherUrlChunk ('Just r)+pred_ = pred+++-- | Constrained to AttoParsec, Regex-Compat and T.Text+type EitherUrlChunk = PathChunk T.Text+++-- | Container when defining route paths+type UrlChunks = PathChunks T.Text++++-- | Prefix a routable path by more predicative lookup data.+(</>) :: EitherUrlChunk mx -> UrlChunks xs -> UrlChunks (mx ': xs)+(</>) = (./)++infixr 9 </>++
src/Web/Routes/Nested/Types.hs view
@@ -1,78 +1,75 @@ {-# LANGUAGE-    GADTs-  , TypeOperators-  , TypeFamilies-  , KindSignatures-  , DataKinds-  , RankNTypes-  , FlexibleInstances-  , UndecidableInstances-  , MultiParamTypeClasses-  , FunctionalDependencies+    DeriveFunctor   , ConstraintKinds+  , TypeFamilies+  , FlexibleContexts+  , GeneralizedNewtypeDeriving   #-} -module Web.Routes.Nested.Types-  ( Singleton (..)-  , Extend (..)-  , Extrude (..)-  , CatMaybes-  , module Web.Routes.Nested.Types.UrlChunks-  ) where--import           Web.Routes.Nested.Types.UrlChunks-import qualified Data.Text as T-import           Data.Trie.Pred-import           Data.Trie.Pred.Step-import qualified Data.Trie.Map as MT-import qualified Data.Map as Map---type family CatMaybes (xs :: [Maybe *]) :: [*] where-  CatMaybes '[] = '[]-  CatMaybes ('Nothing  ': xs) = CatMaybes xs-  CatMaybes (('Just x) ': xs) = x ': CatMaybes xs---- | Creates a string of nodes - a trie with a width of 1.-class Singleton chunks a trie | chunks a -> trie where-  singleton :: chunks -> a -> trie+module Web.Routes.Nested.Types where --- Basis-instance Singleton (UrlChunks '[]) a (RootedPredTrie T.Text a) where-  singleton Root r = RootedPredTrie (Just r) emptyPT+import           Web.Routes.Nested.Match+import           Network.Wai.Middleware.Verbs+import           Network.Wai.Middleware.ContentType+import           Network.Wai.Trans+import           Data.Trie.Pred.Base                (RootedPredTrie (..))+import           Data.Trie.Pred.Interface.Types     (Extrude (..), CatMaybes) --- Successor-instance ( Singleton (UrlChunks xs) a trie0-         , Extend (EitherUrlChunk x) trie0 trie1-         ) => Singleton (UrlChunks (x ': xs)) a trie1 where-  singleton (Cons u us) r = extend u (singleton us r)+import           Data.Monoid+import qualified Data.Text as T+import           Data.Function.Poly+import           Control.Monad.Trans+import qualified Control.Monad.State                as S  --- | Turn a list of tries (@Rooted@) into a node with those children-class Extend eitherUrlChunk child result | eitherUrlChunk child -> result where-  extend :: eitherUrlChunk -> child -> result+-- | The internal data structure built during route declaration.+data Tries x s = Tries+  { trieContent  :: !(RootedPredTrie T.Text x)+  , trieCatchAll :: !(RootedPredTrie T.Text x)+  , trieSecurity :: !(RootedPredTrie T.Text s)+  } --- | Literal case-instance Extend (EitherUrlChunk  'Nothing) (RootedPredTrie T.Text a) (RootedPredTrie T.Text a) where-  extend ((:=) t) (RootedPredTrie mx xs) = RootedPredTrie Nothing $-    PredTrie (MT.MapStep $ Map.singleton t (mx, Just xs)) mempty+instance Monoid (Tries x s) where+  mempty = Tries mempty mempty mempty+  mappend (Tries x1 x2 x3) (Tries y1 y2 y3) =+    ((Tries $! x1 <> y1)+            $! x2 <> y2)+            $! x3 <> y3 --- | Existentially quantified case-instance Extend (EitherUrlChunk ('Just r)) (RootedPredTrie T.Text (r -> a)) (RootedPredTrie T.Text a) where-  extend ((:~) (i,q)) (RootedPredTrie mx xs) = RootedPredTrie Nothing $-    PredTrie mempty (PredSteps [PredStep i q mx xs])+-- | The return type of a route building expression like `match` -+--   it should have a shape of @HandlerT (MiddlewareT m) (SecurityToken s) m a@+--   when used with `route`.+newtype HandlerT x sec m a = HandlerT+  { runHandlerT :: S.StateT (Tries x sec) m a+  } deriving ( Functor, Applicative, Monad, MonadIO, MonadTrans+             , S.MonadState (Tries x sec)) +-- | Run the monad, only getting the built state and throwing away @a@.+execHandlerT :: Monad m => HandlerT x sec m a -> m (Tries x sec)+execHandlerT hs = S.execStateT (runHandlerT hs) mempty --- | @FoldR Extend start chunks ~ result@-class Extrude chunks start result | chunks start -> result where-  extrude :: chunks -> start -> result+{-# INLINEABLE execHandlerT #-} --- Basis-instance Extrude (UrlChunks '[]) (RootedPredTrie T.Text a) (RootedPredTrie T.Text a) where-  extrude Root r = r+-- | Deductive proof that prepending a list of types to a function as arity+--   can be deconstructed.+type ExtrudeSoundly cleanxs xs c r =+  ( cleanxs ~ CatMaybes xs+  , ArityTypeListIso c cleanxs r+  , Extrude (UrlChunks xs)+      (RootedPredTrie T.Text c)+      (RootedPredTrie T.Text r)+  ) --- Successor-instance ( Extrude (UrlChunks xs) trie0 trie1-         , Extend (EitherUrlChunk x) trie1 trie2 ) => Extrude (UrlChunks (x ': xs)) trie0 trie2 where-  extrude (Cons u us) r = extend u (extrude us r)+-- | The type of content builders.+type ActionT m a = VerbListenerT (FileExtListenerT m a) m a +-- | Run the content builder into a middleware that responds when the content+--   is satisfiable (i.e. @Accept@ headers are O.K., etc.)+action :: Monad m => ActionT m () -> MiddlewareT m+action xs app req respond = do+  vmap <- execVerbListenerT (mapVerbs fileExtsToMiddleware xs)+  let v = getVerb req+  mMid <- lookupVerb req v vmap+  case mMid of+    Nothing  -> app req respond+    Just mid -> mid app req respond
− src/Web/Routes/Nested/Types/UrlChunks.hs
@@ -1,93 +0,0 @@-{-# LANGUAGE-    KindSignatures-  , GADTs-  , RankNTypes-  , TypeOperators-  , DataKinds-  #-}--module Web.Routes.Nested.Types.UrlChunks-  ( -- * Path Combinators-    o_-  , origin_-  , l_-  , literal_-  , p_-  , parse_-  , r_-  , regex_-  , pred_-  , (</>)-  , -- * Path Types-    EitherUrlChunk (..)-  , UrlChunks (..)-  ) where--import Data.Attoparsec.Text-import Text.Regex-import Data.String (IsString (..))-import qualified Data.Text as T----o_, origin_ :: UrlChunks '[]-o_ = origin_---- | The /Origin/ chunk - the equivalent to @[]@-origin_ = Root---l_, literal_ :: T.Text -> EitherUrlChunk 'Nothing-l_ = literal_---- | Match against a /Literal/ chunk-literal_ = (:=)---p_, parse_ :: (T.Text, Parser r) -> EitherUrlChunk ('Just r)-p_ = parse_---- | Match against a /Parsed/ chunk, with <https://hackage.haskell.org/package/attoparsec attoparsec>.-parse_ (i,q) = (:~) (i, eitherToMaybe . parseOnly q)---r_, regex_ :: (T.Text, Regex) -> EitherUrlChunk ('Just [String])-r_ = regex_---- | Match against a /Regular expression/ chunk, with <https://hackage.haskell.org/package/regex-compat regex-compat>.-regex_ (i,q) = (:~) (i, matchRegex q . T.unpack)---- | Match with a predicate against the url chunk directly.-pred_ :: (T.Text, T.Text -> Maybe r) -> EitherUrlChunk ('Just r)-pred_ = (:~)----- | Constrained to AttoParsec, Regex-Compat and T.Text-data EitherUrlChunk (x :: Maybe *) where-  (:=) :: T.Text                      -> EitherUrlChunk 'Nothing-  (:~) :: (T.Text, T.Text -> Maybe r) -> EitherUrlChunk ('Just r)---- | Use raw strings instead of prepending @l@-instance x ~ 'Nothing => IsString (EitherUrlChunk x) where-  fromString = literal_ . T.pack----- | Container when defining route paths-data UrlChunks (xs :: [Maybe *]) where-  Cons :: EitherUrlChunk mx -> UrlChunks xs -> UrlChunks (mx ': xs) -- stack is left-to-right-  Root :: UrlChunks '[]------ | Prefix a routable path by more predicative lookup data.-(</>) :: EitherUrlChunk mx -> UrlChunks xs -> UrlChunks (mx ': xs)-(</>) = Cons--infixr 9 </>----- Utils--eitherToMaybe :: Either String r -> Maybe r-eitherToMaybe (Right r') = Just r'-eitherToMaybe _         = Nothing
test/Web/Routes/NestedSpec.hs view
@@ -21,44 +21,34 @@         get "/" `shouldRespondWith`         200       describe "GET /foo" $-        it "should respond with 200 and 'foo!'" $+        it "should respond with 200" $         get "/foo" `shouldRespondWith`-        "" { matchStatus = 200-           , matchBody = Just "foo!"-           }+        200       describe "GET /baz" $-        it "should respond with 200 and 'baz!'" $+        it "should respond with 200" $         get "/baz" `shouldRespondWith`-        "" { matchStatus = 200-           , matchBody = Just "baz!"-           }+        200       describe "GET /borked" $         it "should respond with 404" $         get "/borked" `shouldRespondWith`         404+  describe "Attoparsec Routes" $+    with (return app) $ do+      describe "GET /12.34" $+        it "should respond with 200" $+        get "/12.34" `shouldRespondWith`+        200   describe "Regex Routes" $     with (return app) $ do       describe "GET /athan@emails.com" $-        it "should respond with 200 and '[\"athan@emails.com\"] email'" $+        it "should respond with 200" $         get "/athan@emails.com" `shouldRespondWith`-        "" { matchStatus = 200-           , matchBody = Just "[\"athan@emails.com\"] email"-           }-  describe "Upload Routes" $-    with (return app) $ do-      describe "POST /baz" $-        it "should respond with 200 and 'Woah! Upload content!'" $-        post "/baz" "anything" `shouldRespondWith`-        "" { matchStatus = 200-           , matchBody = Just "Woah! Upload content!"-           }+        200   describe "Secure Routes" $     with (return app) $ do       describe "GET /foo/bar" $-        it "should respond with 401 and 'Unauthorized!'" $+        it "should respond with 401" $         get "/foo/bar" `shouldRespondWith`-        "" { matchStatus = 401-           , matchBody = Just "Unauthorized!"-           }+        401  
test/Web/Routes/NestedSpec/Basic.hs view
@@ -2,6 +2,7 @@     OverloadedStrings   , ScopedTypeVariables   , FlexibleContexts+  , DeriveGeneric   #-}  @@ -12,77 +13,70 @@ import Network.HTTP.Types import           Text.Regex import qualified Data.Text.Lazy as LT-import           Data.Attoparsec.Text+import           Data.Attoparsec.Text hiding (match) import Data.Monoid-import Control.Monad.Except+import Control.Monad.Catch+import Control.Monad.IO.Class+import GHC.Generics   data AuthRole = AuthRole deriving (Show, Eq)-data AuthErr = NeedsAuth deriving (Show, Eq)+data AuthErr = NeedsAuth deriving (Show, Eq, Generic) +instance Exception AuthErr+ -- | If you fail here and throw an AuthErr, then the user was not authorized to--- under the conditions set by @ss :: [AuthRole]@, and based on the authentication--- of that user's session from the @Request@ object. Note that we could have a--- shared cache of authenticated sessions, by adding more constraints on @m@ like--- @MonadIO@.--- For instance, even if there are [] auth roles, we could still include a header/timestamp--- pair to uniquely identify the guest. Or, we could equally change @Checksum ~ Maybe Token@,--- so a guest just returns Nothing, and we could handle the case in @putAuth@ to--- not do anything.-authorize :: ( Monad m-             ) => Request -> [AuthRole] -> m (Response -> Response, Maybe AuthErr)+--   under the conditions set by @ss :: [AuthRole]@, and based on the authentication+--   of that user's session from the @Request@ object. Note that we could have a+--   shared cache of authenticated sessions, by adding more constraints on @m@ like+--   @MonadIO@.+--   For instance, even if there are [] auth roles, we could still include a header/timestamp+--   pair to uniquely identify the guest. Or, we could equally change @Checksum ~ Maybe Token@,+--   so a guest just returns Nothing, and we could handle the case in @putAuth@ to+--   not do anything.+authorize :: ( MonadThrow m+             ) => Request -> [AuthRole] -> m () -- authorize _ _ = return id -- uncomment to force constant authorization-authorize req ss | null ss   = return (id, Nothing)-                 | otherwise = return (id, Just NeedsAuth)+authorize req ss | null ss   = return ()+                 | otherwise = throwM NeedsAuth  defApp :: Application-defApp _ respond = respond $ textOnlyStatus status404 "404 :("+defApp _ respond = respond $+  textOnly "404 :(" status404 [] +successMiddleware :: Middleware+successMiddleware _ _ respond = respond $ textOnly "200!" status200 []+ app :: Application app =-  let yoDawgIHeardYouLikeYoDawgsYo = routeActionAuth authorize routes+  let yoDawgIHeardYouLikeYoDawgsYo =+        (routeAuth authorize routes) `catchMiddlewareT` unauthHandle       routes = do-        hereAction rootHandle-        parent fooRoute $ do-          hereAction fooHandle-          auth AuthRole unauthHandle DontProtectHere-          handleAction barRoute    barHandle-          handleAction doubleRoute doubleHandle-        handleAction emailRoute emailHandle-        handleAction bazRoute bazHandle-        notFoundAction notFoundHandle+        matchHere successMiddleware+        matchGroup fooRoute $ do+          matchHere successMiddleware+          auth AuthRole DontProtectHere+          match barRoute    successMiddleware+        match doubleRoute (\_ -> successMiddleware)+        match emailRoute (\_ -> successMiddleware)+        match bazRoute successMiddleware   in yoDawgIHeardYouLikeYoDawgsYo defApp   where-    rootHandle = get $ text "Home"-     -- `/foo`     fooRoute = l_ "foo" </> o_-    fooHandle = get $ text "foo!"      -- `/foo/bar`     barRoute = l_ "bar" </> o_-    barHandle = get $ do-      text "bar!"-      json ("json bar!" :: LT.Text)      -- `/foo/1234e12`, uses attoparsec-    doubleRoute = p_ ("double", double) </> o_-    doubleHandle d = get $ text $ LT.pack (show d) <> " foos"+    doubleRoute = p_ "double" double </> o_      -- `/athan@foo.com`-    emailRoute = r_ ("email", mkRegex "(^[-a-zA-Z0-9_.]+@[-a-zA-Z0-9]+\\.[-a-zA-Z0-9.]+$)") </> o_-    emailHandle e = get $ text $ LT.pack (show e) <> " email"+    emailRoute = r_ "email" (mkRegex "(^[-a-zA-Z0-9_.]+@[-a-zA-Z0-9]+\\.[-a-zA-Z0-9.]+$)") </> o_      -- `/baz`, uses regex-compat     bazRoute = l_ "baz" </> o_-    bazHandle = do-      get $ text "baz!"-      let uploader req = do liftIO $ print =<< strictRequestBody req-                            return ()-          uploadHandle (Left Nothing)  = text "Upload Failed"-          uploadHandle (Left (Just _)) = text "Impossible - no errors thrown in uploader"-          uploadHandle (Right ())      = text "Woah! Upload content!"-      post uploader uploadHandle -    unauthHandle NeedsAuth = get $ textStatus status401 "Unauthorized!"-    notFoundHandle = get $ textStatus status404 "Not Found :("+unauthHandle :: AuthErr -> Middleware+unauthHandle NeedsAuth _ _ respond = respond $+  textOnly "Unauthorized!" status401 []