Shpadoinkle-router-0.2.0.1: Shpadoinkle/Router/Server.hs
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE CPP #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE InstanceSigs #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-}
-- | Since the single page application URIs are specified with Servant, we can automate much
-- of the process of serving the application with server-side rendering. This module provides
-- the basic infrastructure for serving rendered HTML using the same code that would be used
-- to render the same route on the client-side, ensuring a consistent rendering, whether a
-- URI is accessed via a client-side popstate event or via the initial page load.
module Shpadoinkle.Router.Server where
#ifndef ghcjs_HOST_OS
import Data.ByteString.Lazy as BS (ByteString, fromStrict,
length)
import Data.Text.Encoding (encodeUtf8)
import GHC.TypeLits (Symbol)
import Network.Wai (responseLBS)
import Network.Wai.Application.Static (StaticSettings (ssLookupFile, ssMaxAge),
defaultWebAppSettings,
staticApp)
import Servant.API
import Servant.Server (HasServer (ServerT), Server,
Tagged (Tagged))
import Servant.Server.StaticFiles (serveDirectoryWith)
import WaiAppStatic.Types (File (..),
LookupResult (LRFile, LRNotFound),
MaxAge (MaxAgeSeconds), Piece,
toPieces)
import Shpadoinkle (Html)
import Shpadoinkle.Backend.Static (renderStatic)
import Shpadoinkle.Router (HasRouter (..), View)
-- | Helper to serve a 'ByteString' as a file from the web application interface.
toFile :: Piece -> ByteString -> File
toFile p bs = File
{ fileGetSize = fromIntegral $ BS.length bs
, fileToResponse = \status headers -> responseLBS status headers bs
, fileName = p
, fileGetHash = pure Nothing
, fileGetModified = Nothing
}
-- | Serve index.html generated from a Shpadoinkle view using the static backend, otherwise serve out of a directory.
defaultSPAServerSettings
:: FilePath
-- ^ Directory to try files
-> IO (Html m a)
-- ^ Get the index.html page
-> StaticSettings
defaultSPAServerSettings root mhtml = settings { ssLookupFile = orIndex, ssMaxAge = MaxAgeSeconds 0 }
where
settings = defaultWebAppSettings root
orIndex ps = do
let file ps' = toFile ps' . BS.fromStrict . encodeUtf8 . renderStatic
res <- ssLookupFile settings ps
html <- mhtml
return $ case (res, toPieces ["index.html"]) of
(LRNotFound, Just [ps']) -> LRFile $ file ps' html
(_, Just [ps']) | [ps'] == ps || Prelude.null ps -> LRFile $ file ps' html
_ -> res
-- | Serve the UI by generating a Servant Server from the SPA URIs
class ServeRouter layout route m a where
serveUI
:: FilePath
-- ^ Where should we look for static assets?
-> (route -> IO (Html m a))
-- ^ How shall we get the page based on the requested route?
-> layout :>> route
-- ^ What is the relationship between URIs and routes?
-> Server layout
instance (ServeRouter x r m a, ServeRouter y r m a)
=> ServeRouter (x :<|> y) r m a where
serveUI :: FilePath -> (r -> IO (Html m a)) -> (x :<|> y) :>> r -> Server (x :<|> y)
serveUI root view (x :<|> y) = serveUI @x root view x :<|> serveUI @y root view y
{-# INLINABLE serveUI #-}
instance ServeRouter sub r m a
=> ServeRouter (Capture sym x :> sub) r m a where
serveUI :: FilePath -> (r -> IO (Html m a)) -> (x -> sub :>> r) -> Server (Capture sym x :> sub)
serveUI root view = (serveUI @sub root view .)
{-# INLINABLE serveUI #-}
instance ServeRouter sub r m a
=> ServeRouter (QueryParam sym x :> sub) r m a where
serveUI :: FilePath -> (r -> IO (Html m a)) -> (Maybe x -> sub :>> r) -> Server (QueryParam sym x :> sub)
serveUI root view = (serveUI @sub root view .)
{-# INLINABLE serveUI #-}
instance ServeRouter sub r m a
=> ServeRouter (QueryParams sym x :> sub) r m a where
serveUI :: FilePath -> (r -> IO (Html m a)) -> ([x] -> sub :>> r) -> Server (QueryParams sym x :> sub)
serveUI root view = (serveUI @sub root view .)
{-# INLINABLE serveUI #-}
instance ServeRouter sub r m a
=> ServeRouter (QueryFlag sym :> sub) r m a where
serveUI :: FilePath -> (r -> IO (Html m a)) -> (Bool -> sub :>> r) -> Server (QueryFlag sym :> sub)
serveUI root view = (serveUI @sub root view .)
{-# INLINABLE serveUI #-}
instance ServeRouter sub r m a
=> ServeRouter ((path :: Symbol) :> sub) r m a where
serveUI :: FilePath -> (r -> IO (Html m a)) -> (path :> sub) :>> r -> Server (path :> sub)
serveUI = serveUI @sub
{-# INLINABLE serveUI #-}
instance ServeRouter Raw r m a where
serveUI :: FilePath -> (r -> IO (Html m a)) -> Raw :>> r -> Server Raw
serveUI root view = serveDirectoryWith . defaultSPAServerSettings root . view
{-# INLINABLE serveUI #-}
instance ServeRouter (View n b) r m a where
serveUI :: FilePath -> (r -> IO (Html m a)) -> View n b :>> r -> Server (View n b)
serveUI root view = serveDirectoryWithSpa . defaultSPAServerSettings root . view
{-# INLINABLE serveUI #-}
serveDirectoryWithSpa :: StaticSettings -> ServerT (View m a) n
serveDirectoryWithSpa = Tagged . staticApp
#endif