content: {-# LANGUAGE KindSignatures #-}
  {-# LANGUAGE LambdaCase #-}
  {-# LANGUAGE MultiWayIf #-}
  {-# LANGUAGE OverloadedStrings #-}
  {-# LANGUAGE QuasiQuotes #-}
  {-# LANGUAGE ScopedTypeVariables #-}
  
  -- | Type-safe routes for static sites.
  module Rib.Route
    ( -- * Defining routes
      IsRoute (..),
  
      -- * Rendering routes
      routeUrl,
      routeUrlRel,
  
      -- * Writing routes
      writeRoute,
    )
  where
  
  import Control.Monad.Catch
  import Data.Kind
  import qualified Data.Text as T
  import Development.Shake (Action)
  import Relude
  import Rib.Shake (writeFileCached)
  import System.FilePath
  
  -- | A route is a GADT which represents the individual routes in a static site.
  --
  -- `r` represents the data used to render that particular route.
  class IsRoute (r :: Type -> Type) where
    -- | Return the filepath (relative to `Rib.Shake.ribInputDir`) where the
    -- generated content for this route should be written.
    routeFile :: MonadThrow m => r a -> m (FilePath)
  
  data UrlType = Relative | Absolute
  
  path2Url :: FilePath -> UrlType -> Text
  path2Url fp = toText . \case
    Relative ->
      fp
    Absolute ->
      "/" </> fp
  
  -- | The absolute URL to this route (relative to site root)
  routeUrl :: IsRoute r => r a -> Text
  routeUrl = routeUrl' Absolute
  
  -- | The relative URL to this route
  routeUrlRel :: IsRoute r => r a -> Text
  routeUrlRel = routeUrl' Relative
  
  -- | Get the URL to a route
  routeUrl' :: IsRoute r => UrlType -> r a -> Text
  routeUrl' urlType = stripIndexHtml . flip path2Url urlType . either (error . toText . displayException) id . routeFile
    where
      stripIndexHtml s =
        -- Because path2Url can return relative URL, we must account for there
        -- not being a / at the beginning.
        if  | "/index.html" `T.isSuffixOf` s ->
              T.dropEnd (T.length "index.html") s
            | s == "index.html" ->
              "."
            | otherwise ->
              s
  
  -- | Write the content `s` to the file corresponding to the given route.
  --
  -- This is similar to `Rib.Shake.writeFileCached`, but takes a route instead of
  -- a filepath as its argument.
  writeRoute :: (IsRoute r, ToString s) => r a -> s -> Action ()
  writeRoute r content = do
    fp <- liftIO $ routeFile r
    writeFileCached fp $ toString $ content
  