{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE InstanceSigs #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
module Shpadoinkle.Disembodied (
Site(..)
, Disembodied(..)
, writeSite
, writeSiteMap
) where
import Control.Monad (join, void)
import Data.Kind (Type)
import Data.Proxy (Proxy (..))
import Data.Text.Lazy (isSuffixOf, pack, unpack, fromStrict)
import Data.Text.Lazy.IO as LT (writeFile)
import Servant.API
import System.Directory (createDirectoryIfMissing)
import System.FilePath ((<.>), (</>))
import UnliftIO.Async (concurrently, forConcurrently_)
import GHC.TypeLits (KnownSymbol, symbolVal)
import Shpadoinkle (Html)
import Shpadoinkle.Backend.Static (renderStatic)
import Shpadoinkle.Router (HTML, View)
data Site where
SPath
:: String
-> Site
-> Site
SIndex
:: forall m a
. Html m a
-> Site
SCapture
:: (FromHttpApiData x, ToHttpApiData x, Bounded x, Enum x)
=> (x -> Site)
-> Site
SChoice :: Site -> Site -> Site
class Disembodied a where
type SiteSpec a :: Type
buildSite :: SiteSpec a -> Site
instance (Disembodied x, Disembodied y)
=> Disembodied (x :<|> y) where
type SiteSpec (x :<|> y) = SiteSpec x :<|> SiteSpec y
buildSite :: SiteSpec x :<|> SiteSpec y -> Site
buildSite (x :<|> y) = SChoice (buildSite @x x) (buildSite @y y)
{-# INLINABLE buildSite #-}
instance (Disembodied sub, KnownSymbol path)
=> Disembodied (path :> sub) where
type SiteSpec (path :> sub) = SiteSpec sub
buildSite :: SiteSpec sub -> Site
buildSite = SPath (symbolVal (Proxy @path)) . buildSite @sub
{-# INLINABLE buildSite #-}
instance (Disembodied sub, FromHttpApiData x, ToHttpApiData x, Bounded x, Enum x)
=> Disembodied (Capture sym x :> sub) where
type SiteSpec (Capture sym x :> sub) = x -> SiteSpec sub
buildSite :: (x -> SiteSpec sub) -> Site
buildSite = SCapture . (buildSite @sub .)
{-# INLINABLE buildSite #-}
instance Disembodied sub
=> Disembodied (QueryParam sym x :> sub) where
type SiteSpec (QueryParam sym x :> sub) = Maybe x -> SiteSpec sub
buildSite :: (Maybe x -> SiteSpec sub) -> Site
buildSite f = buildSite @sub $ f Nothing
{-# INLINABLE buildSite #-}
instance Disembodied sub
=> Disembodied (QueryParam' ms sym x :> sub) where
type SiteSpec (QueryParam' ms sym x :> sub) = Maybe x -> SiteSpec sub
buildSite :: (Maybe x -> SiteSpec sub) -> Site
buildSite f = buildSite @sub $ f Nothing
{-# INLINABLE buildSite #-}
instance Disembodied sub
=> Disembodied (QueryParams sym x :> sub) where
type SiteSpec (QueryParams sym x :> sub) = [x] -> SiteSpec sub
buildSite :: ([x] -> SiteSpec sub) -> Site
buildSite f = buildSite @sub $ f []
{-# INLINABLE buildSite #-}
instance Disembodied sub
=> Disembodied (QueryFlag sym :> sub) where
type SiteSpec (QueryFlag sym :> sub) = Bool -> SiteSpec sub
buildSite :: (Bool -> SiteSpec sub) -> Site
buildSite f = buildSite @sub $ f False
{-# INLINABLE buildSite #-}
instance Disembodied (f '[HTML] (Html m a)) where
type SiteSpec (f '[HTML] (Html m a)) = Html m a
buildSite :: Html m a -> Site
buildSite = SIndex
{-# INLINABLE buildSite #-}
instance Disembodied (View m a) where
type SiteSpec (View m a) = Html m a
buildSite :: Html m a -> Site
buildSite = SIndex
{-# INLINABLE buildSite #-}
writeSiteMap
:: forall layout. Disembodied layout
=> String
-> FilePath
-> SiteSpec layout
-> IO ()
writeSiteMap base fs layout
= LT.writeFile (fs </> "sitemap" <.> "xml")
. buildSiteMapXML . go mempty $ buildSite @layout layout
where
go (x,xs) (SIndex _) = x:xs
go curr (SChoice x y) = go curr x <> go curr y
go curr (SCapture f) = join $ traverse (
\c -> go curr $ SPath (unpack . fromStrict $ toUrlPiece c) $ f c
) [ minBound .. maxBound ]
go (x,xs) (SPath path (SIndex _)) | ".html" `isSuffixOf` pack path =
(x <> "/" <> path):xs
go (x,xs) (SPath path site) = go (x <> "/" <> path, xs) site
buildSiteMapXML xs = pack . unlines $
[ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
, "<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">"
] <>
(xs >>= (\url ->
[ " <url>"
, " <loc>" <> base <> url <> "</loc>"
, " </url>"
])) <>
[ "</urlset>"
]
writeSite
:: forall layout. Disembodied layout
=> FilePath
-> SiteSpec layout
-> IO ()
writeSite fs layout = go fs $ buildSite @layout layout where
go :: FilePath -> Site -> IO ()
go curr (SIndex page) = LT.writeFile (curr </> "index" <.> "html") $ renderStatic page
go curr (SChoice x y) = void $ go curr x `concurrently` go curr y
go curr (SCapture f) = forConcurrently_ [ minBound .. maxBound ] $
\c -> go curr $ SPath (unpack . fromStrict $ toUrlPiece c) $ f c
go curr (SPath path (SIndex page)) | ".html" `isSuffixOf` pack path = do
createDirectoryIfMissing False curr
LT.writeFile (curr </> path) $ renderStatic page
go curr (SPath path site) = do
createDirectoryIfMissing False (curr </> path)
go (curr </> path) site