servant-router 0.7.1 → 0.8.0
raw patch · 4 files changed
+166/−8 lines, 4 filesdep +blaze-htmldep +servant-blazedep +servant-serverdep ~basedep ~mtlPVP ok
version bump matches the API change (PVP)
Dependencies added: blaze-html, servant-blaze, servant-server, warp
Dependency ranges changed: base, mtl
API changes (from Hackage documentation)
- Servant.Router: RCapture :: (x -> Router m a) -> Router m a
- Servant.Router: RChoice :: Router m a -> Router m a -> Router m a
- Servant.Router: RPage :: m a -> Router m a
- Servant.Router: RPath :: Proxy sym -> Router m a -> Router m a
- Servant.Router: RQueryFlag :: Proxy sym -> (Bool -> Router m a) -> Router m a
- Servant.Router: RQueryParam :: Proxy sym -> (Maybe x -> Router m a) -> Router m a
- Servant.Router: RQueryParams :: Proxy sym -> ([x] -> Router m a) -> Router m a
- Servant.Router: instance forall (k :: BOX) (path :: GHC.TypeLits.Symbol) (sublayout :: k). (Servant.Router.HasRouter sublayout, GHC.TypeLits.KnownSymbol path) => Servant.Router.HasRouter (path Servant.API.Sub.:> sublayout)
- Servant.Router: instance forall (k :: BOX) (sym :: GHC.TypeLits.Symbol) (sublayout :: k). (Servant.Router.HasRouter sublayout, GHC.TypeLits.KnownSymbol sym) => Servant.Router.HasRouter (Servant.API.QueryParam.QueryFlag sym Servant.API.Sub.:> sublayout)
- Servant.Router: instance forall (k :: BOX) (sym :: GHC.TypeLits.Symbol) x (sublayout :: k). (Servant.Router.HasRouter sublayout, Web.HttpApiData.Internal.FromHttpApiData x) => Servant.Router.HasRouter (Servant.API.Capture.Capture sym x Servant.API.Sub.:> sublayout)
- Servant.Router: instance forall (k :: BOX) (sym :: GHC.TypeLits.Symbol) x (sublayout :: k). (Servant.Router.HasRouter sublayout, Web.HttpApiData.Internal.FromHttpApiData x, GHC.TypeLits.KnownSymbol sym) => Servant.Router.HasRouter (Servant.API.QueryParam.QueryParam sym x Servant.API.Sub.:> sublayout)
- Servant.Router: instance forall (k :: BOX) (sym :: GHC.TypeLits.Symbol) x (sublayout :: k). (Servant.Router.HasRouter sublayout, Web.HttpApiData.Internal.FromHttpApiData x, GHC.TypeLits.KnownSymbol sym) => Servant.Router.HasRouter (Servant.API.QueryParam.QueryParams sym x Servant.API.Sub.:> sublayout)
+ Servant.Router: [RCapture] :: FromHttpApiData x => (x -> Router m a) -> Router m a
+ Servant.Router: [RChoice] :: Router m a -> Router m a -> Router m a
+ Servant.Router: [RPage] :: m a -> Router m a
+ Servant.Router: [RPath] :: KnownSymbol sym => Proxy sym -> Router m a -> Router m a
+ Servant.Router: [RQueryFlag] :: KnownSymbol sym => Proxy sym -> (Bool -> Router m a) -> Router m a
+ Servant.Router: [RQueryParam] :: (FromHttpApiData x, KnownSymbol sym) => Proxy sym -> (Maybe x -> Router m a) -> Router m a
+ Servant.Router: [RQueryParams] :: (FromHttpApiData x, KnownSymbol sym) => Proxy sym -> ([x] -> Router m a) -> Router m a
+ Servant.Router: constHandler :: (HasRouter layout, Monad m) => Proxy layout -> Proxy m -> a -> RouteT layout m a
+ Servant.Router: instance forall k (sublayout :: k) (path :: GHC.Types.Symbol). (Servant.Router.HasRouter sublayout, GHC.TypeLits.KnownSymbol path) => Servant.Router.HasRouter (path Servant.API.Sub.:> sublayout)
+ Servant.Router: instance forall k (sublayout :: k) (sym :: GHC.Types.Symbol). (Servant.Router.HasRouter sublayout, GHC.TypeLits.KnownSymbol sym) => Servant.Router.HasRouter (Servant.API.QueryParam.QueryFlag sym Servant.API.Sub.:> sublayout)
+ Servant.Router: instance forall k (sublayout :: k) x (sym :: GHC.Types.Symbol). (Servant.Router.HasRouter sublayout, Web.HttpApiData.Internal.FromHttpApiData x) => Servant.Router.HasRouter (Servant.API.Capture.Capture sym x Servant.API.Sub.:> sublayout)
+ Servant.Router: instance forall k (sublayout :: k) x (sym :: GHC.Types.Symbol). (Servant.Router.HasRouter sublayout, Web.HttpApiData.Internal.FromHttpApiData x, GHC.TypeLits.KnownSymbol sym) => Servant.Router.HasRouter (Servant.API.QueryParam.QueryParam sym x Servant.API.Sub.:> sublayout)
+ Servant.Router: instance forall k (sublayout :: k) x (sym :: GHC.Types.Symbol). (Servant.Router.HasRouter sublayout, Web.HttpApiData.Internal.FromHttpApiData x, GHC.TypeLits.KnownSymbol sym) => Servant.Router.HasRouter (Servant.API.QueryParam.QueryParams sym x Servant.API.Sub.:> sublayout)
+ Servant.Router: routeConst :: (HasRouter layout, Monad m) => Proxy layout -> Proxy m -> a -> Router m a
+ Servant.Router: type family RouteT layout (m :: * -> *) a :: *;
+ Servant.Router: }
- Servant.Router: class HasRouter layout where type family RouteT layout (m :: * -> *) a :: *
+ Servant.Router: class HasRouter layout where type RouteT layout (m :: * -> *) a :: * routeConst l m a = route l m (Proxy :: Proxy a) (constHandler l m a) where {
- Servant.Router: runRoute :: (HasRouter layout, MonadError RoutingError m) => String -> Proxy layout -> RouteT layout m a -> m a
+ Servant.Router: runRoute :: forall layout m a. (HasRouter layout, MonadError RoutingError m) => String -> Proxy layout -> RouteT layout m a -> m a
Files
- README.md +88/−0
- servant-router.cabal +17/−3
- src/Servant/Router.hs +26/−5
- test/Server.hs +35/−0
+ README.md view
@@ -0,0 +1,88 @@+servant-router+---++`servant-router` routes a URI given a Servant API and an appropriate handler.+In web applications,+this is used to make single page applications (SPAs) with front-end routing,+letting you share portions of your Servant APIs between the client and server.++`servant-router` does not depend on `reflex` or any GHCJS packages.+It's intended to be a general purpose URI router on any platform.+Combined with `reflex-dom`, `servant-reflex`, and `reflex-dom-contrib`,+this makes for a very satisfactory front-end Haskell experience.++```haskell+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE TypeOperators #-}++module Main where++import Control.Monad.Except+import Data.Proxy+import Reflex.Dom+import Reflex.Dom.Contrib.Router+import Servant.API+import Servant.Router++type MyApi = "books" :> Capture "id" Int :> View+ :<|> "search" :> QueryParam "keywords" String :> View+myApi :: Proxy MyApi+myApi = Proxy++main :: IO ()+-- reflex-dom-contrib provides a 'routeSite' function+-- for routing single page applications with reflex.+-- servant-router uses the provided uri String to perform the routing.+main = routeSite $ \uri -> do+ let handler = books :<|> search+ books i = do+ -- Here, you would get and display a book.+ -- Return a Reflex event for changing the browser location.+ return never+ search Nothing = do+ -- Here, you would display a search bar.+ return never+ search (Just keywords) = do+ -- Here you would display the search bar plus results.+ return never+ -- With the handler constructed, run the router with the uri.+ result <- runExceptT $ runRoute uri myApi handler+ case result of+ -- If 'Left', there was no correct route for the uri.+ Left _ -> do+ el "div" $ text "No such page"+ return never+ -- If 'Right', the result of the route is returned.+ Right e -> return e+```++Serving+---++When using `servant-router` on the front-end in Single Page Applications (SPAs),+you still need to serve the application from the back-end.+To share the routing layout between the front-end and back-end,+the `View` endpoints need to be converted to a verb like `Get`.+Plus, with an SPA,+all the end-points should serve the same HTML on the back-end.+To do this, use the `ViewTransform` type family, and `constHandler` function.++```haskell+type Views = "books" :> View+ :<|> "search" :> QueryParam "query" String :> View++-- Equivalent to:+--+-- type ViewsServer = "books" :> Get '[HTML] Blaze.Html+-- :<|> "search" :> QueryParam "query" String :> Get '[HTML] Blaze.Html+type ViewsServer = ViewTransform Views (Get '[HTML] Blaze.Html)++viewsServer :: Server ViewsServer+viewsServer = constHandler+ (Proxy :: Proxy ViewsServer)+ (Proxy :: Proxy Handler) $+ docTypeHtml $ do+ H.head $ return ()+ body $+ script ! src "app.js" $ return ()+```
servant-router.cabal view
@@ -1,5 +1,5 @@ name: servant-router-version: 0.7.1+version: 0.8.0 synopsis: Servant router for non-server applications. description: Write Servant APIs to be routed without a server. homepage: https://github.com/ElvishJerricco/servant-router@@ -10,7 +10,7 @@ copyright: 2016 Will Fancher category: Web build-type: Simple--- extra-source-files:+extra-source-files: README.md cabal-version: >=1.10 library@@ -35,7 +35,21 @@ , servant-router , servant , mtl- ghc-options: -threaded -rtsopts -with-rtsopts=-N+ ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N+ default-language: Haskell2010++test-suite server-test+ type: exitcode-stdio-1.0+ hs-source-dirs: test+ main-is: Server.hs+ build-depends: base+ , servant-router+ , servant-server+ , servant-blaze+ , mtl+ , warp+ , blaze-html+ ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N default-language: Haskell2010 source-repository head
src/Servant/Router.hs view
@@ -54,16 +54,31 @@ RPath :: KnownSymbol sym => Proxy sym -> Router m a -> Router m a RPage :: m a -> Router m a +-- | Transform a layout by replacing 'View' with another type+type family ViewTransform layout view where+ ViewTransform (a :<|> b) view = ViewTransform a view :<|> ViewTransform b view+ ViewTransform (a :> b) view = a :> ViewTransform b view+ ViewTransform View view = view+ -- | This is similar to the @HasServer@ class from @servant-server@. -- It is the class responsible for making API combinators routable. -- 'RuoteT' is used to build up the handler types. -- 'Router' is returned, to be interpretted by 'routeLoc'. class HasRouter layout where+ -- | A route handler. type RouteT layout (m :: * -> *) a :: *+ -- | Create a constant route handler that returns @a@+ constHandler :: Monad m => Proxy layout -> Proxy m -> a -> RouteT layout m a+ -- | Transform a route handler into a 'Router'. route :: Proxy layout -> Proxy m -> Proxy a -> RouteT layout m a -> Router m a+ -- | Create a 'Router' from a constant.+ routeConst :: Monad m => Proxy layout -> Proxy m -> a -> Router m a+ routeConst l m a = route l m (Proxy :: Proxy a) (constHandler l m a) instance (HasRouter x, HasRouter y) => HasRouter (x :<|> y) where type RouteT (x :<|> y) m a = RouteT x m a :<|> RouteT y m a+ constHandler _ m a = constHandler (Proxy :: Proxy x) m a+ :<|> constHandler (Proxy :: Proxy y) m a route _ (m :: Proxy m)@@ -72,41 +87,47 @@ = RChoice (route (Proxy :: Proxy x) m a x) (route (Proxy :: Proxy y) m a y) instance (HasRouter sublayout, FromHttpApiData x)- => HasRouter (Capture sym x :> sublayout) where+ => HasRouter (Capture sym x :> sublayout) where type RouteT (Capture sym x :> sublayout) m a = x -> RouteT sublayout m a+ constHandler _ m a _ = constHandler (Proxy :: Proxy sublayout) m a route _ m a f = RCapture (route (Proxy :: Proxy sublayout) m a . f) instance (HasRouter sublayout, FromHttpApiData x, KnownSymbol sym)- => HasRouter (QueryParam sym x :> sublayout) where+ => HasRouter (QueryParam sym x :> sublayout) where type RouteT (QueryParam sym x :> sublayout) m a = Maybe x -> RouteT sublayout m a+ constHandler _ m a _ = constHandler (Proxy :: Proxy sublayout) m a route _ m a f = RQueryParam (Proxy :: Proxy sym) (route (Proxy :: Proxy sublayout) m a . f) instance (HasRouter sublayout, FromHttpApiData x, KnownSymbol sym)- => HasRouter (QueryParams sym x :> sublayout) where+ => HasRouter (QueryParams sym x :> sublayout) where type RouteT (QueryParams sym x :> sublayout) m a = [x] -> RouteT sublayout m a+ constHandler _ m a _ = constHandler (Proxy :: Proxy sublayout) m a route _ m a f = RQueryParams (Proxy :: Proxy sym) (route (Proxy :: Proxy sublayout) m a . f) instance (HasRouter sublayout, KnownSymbol sym)- => HasRouter (QueryFlag sym :> sublayout) where+ => HasRouter (QueryFlag sym :> sublayout) where type RouteT (QueryFlag sym :> sublayout) m a = Bool -> RouteT sublayout m a+ constHandler _ m a _ = constHandler (Proxy :: Proxy sublayout) m a route _ m a f = RQueryFlag (Proxy :: Proxy sym) (route (Proxy :: Proxy sublayout) m a . f) instance (HasRouter sublayout, KnownSymbol path)- => HasRouter (path :> sublayout) where+ => HasRouter (path :> sublayout) where type RouteT (path :> sublayout) m a = RouteT sublayout m a+ constHandler _ = constHandler (Proxy :: Proxy sublayout) route _ m a page = RPath (Proxy :: Proxy path) (route (Proxy :: Proxy sublayout) m a page) instance HasRouter View where type RouteT View m a = m a+ constHandler _ _ = return route _ _ _ = RPage -- | Use a handler to route a location, represented as a 'String'.
+ test/Server.hs view
@@ -0,0 +1,35 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeOperators #-}++import Data.Proxy+import Network.Wai.Handler.Warp+import Servant+import Servant.HTML.Blaze+import Servant.Router+import Text.Blaze.Html5 as H hiding (main)+import Text.Blaze.Html5.Attributes++type Views = "books" :> View+ :<|> "search" :> QueryParam "query" String :> View+views :: Proxy Views+views = Proxy++type Api = "api" :> "books" :> Get '[JSON] [String]+api :: Proxy Api+api = Proxy++type WholeServer = ViewTransform Views (Get '[HTML] Html)+ :<|> Api+wholeServer :: Proxy WholeServer+wholeServer = Proxy++server :: Server WholeServer+server = viewServer :<|> apiServer where+ apiServer = return ["Book Title!"]+ viewServer = constHandler views (Proxy :: Proxy Handler) $ docTypeHtml $ do+ H.head $ return ()+ body $ script ! src "app.js" $ return ()++main :: IO ()+main = run 8080 $ serve wholeServer server