packages feed

wai-make-assets-0.2: src/Network/Wai/MakeAssets.hs

{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TupleSections #-}

module Network.Wai.MakeAssets (
  serveAssets,
  Options(..),

  -- * re-exports
  Default(..),
) where

import           Control.Concurrent
import           Control.Exception
import           Control.Monad
import           Data.Default
import           Data.List (intercalate)
import           Data.Monoid
import           Data.String.Conversions
import           Development.Shake (cmd, Exit(..), Stderr(..), CmdOption(..))
import           Network.HTTP.Types.Status
import           Network.Wai
import           Network.Wai.Application.Static
import           System.Directory
import           System.Exit
import           System.FilePath

data Options
  = Options {
    clientDir :: FilePath
  }

instance Default Options where
  def = Options {
    clientDir = "client"
  }

-- | 'serveAssets' will create a wai 'Application' that serves files from the
-- "assets" directory.
--
-- The workflow that 'serveAssets' allows is similar to working on files (for
-- web-sites) that don't need compilation or generation, e.g. html, css, php or
-- javascript. You edit the file in an editor, save it, switch to a browser and
-- hit reload. 'serveAssets' makes sure your browser will be sent up-to-date
-- files.
--
-- To accomplish this, 'serveAssets' assumes that there's a "Makefile" in the
-- directory pointed to by 'clientDir' (default: "client"). This "Makefile" is
-- supposed to put compilation results into the "assets" directory. On __every__
-- request, 'serveAssets' will execute that "Makefile" and only start serving
-- files once the "Makefile" is done. ('serveAssets' makes sure not to run your
-- "Makefile" concurrently.)
serveAssets :: Options -> IO Application
serveAssets options = do
  startupChecks options
  let fileApp = staticApp $ defaultFileServerSettings "assets/"
  mvar <- newMVar ()
  return $ \ request respond -> do
    (Exit exitCode, Stderr errs) <- synchronize mvar $
      cmd (Cwd (clientDir options)) "make"
    case exitCode of
      ExitSuccess -> fileApp request respond
      ExitFailure _ -> respond $ responseLBS internalServerError500 [] $
        cs "make error:\n" <> errs

synchronize :: MVar () -> IO a -> IO a
synchronize mvar action = modifyMVar mvar $ \ () -> ((), ) <$> action

startupChecks :: Options -> IO ()
startupChecks options = do
  checkExists Dir (clientDir options) $
    "You should put sources for assets in there."
  checkExists File (clientDir options </> "Makefile") $ unwords $
    "Which will be invoked to build the assets." :
    "It should put compiled assets into 'assets/'." :
    []
  checkExists Dir "assets" $
    "All files in 'assets/' will be served."

data FileType
  = File
  | Dir

checkExists :: FileType -> FilePath -> String -> IO ()
checkExists typ path hint = do
  exists <- (isFile doesFileExist doesDirectoryExist) path
  when (not exists) $ do
    throwIO $ ErrorCall $ intercalate "\n" $
      ("missing " ++ isFile "file" "directory" ++ ": '" ++ showPath path ++ "'") :
      ("Please create '" ++ showPath path ++ "'.") :
      ("(" ++ hint ++ ")") :
      []
  where
    isFile :: a -> a -> a
    isFile a b = case typ of
      File -> a
      Dir -> b

    showPath :: FilePath -> String
    showPath = case typ of
      File -> id
      Dir -> (++ "/")