herringbone-0.1.1: Web/Herringbone/Internal/Types.hs
module Web.Herringbone.Internal.Types where
import Control.Monad.Reader
import Control.Applicative
import Data.Maybe
import Data.Map (Map)
import qualified Data.Map as M
import Data.Text (Text)
import qualified Data.Text as T
import Data.Time.Clock
import Data.Time.Format
import System.Locale
import System.IO hiding (FilePath)
import qualified Data.ByteString as B
import qualified Filesystem.Path.CurrentOS as F
import qualified Filesystem as F
import Filesystem.Path.CurrentOS (FilePath)
import Prelude hiding (FilePath)
-- | A value describing an error that occurred while trying to produce an
-- 'Asset'.
data AssetError = AssetNotFound
| AssetCompileError CompileError
| AmbiguousSources [FilePath]
deriving (Show, Eq)
-- | Data which is given to preprocessors on the off-chance that they need it
-- (eg, Fay)
data PPReader = PPReader
{ ppReaderHb :: Herringbone
-- ^ The Herringbone which was used to build the asset
, ppReaderSourcePath :: FilePath
-- ^ The file path to the source file
, ppReaderPPs :: [PP]
-- ^ Preprocessors being invoked. Currently this will only ever have zero
-- or one elements; in the future, Herringbone may be able to run multiple
-- preprocessors on a single file.
}
-- | A monad in which preprocessor actions happen.
newtype PPM a = PPM { unPPM :: ReaderT PPReader IO a }
deriving (Functor, Applicative, Monad, MonadIO, (MonadReader PPReader))
runPPM :: PPM a -> PPReader -> IO a
runPPM comp = runReaderT (unPPM comp)
-- | A preprocessor something which is run on the asset before it is served.
-- Preprocessors are run when a file matches its rule. For example, if you
-- have a preprocessor which takes \"coffee\" files and emits \"js\" files,
-- there is a file named \"application.coffee\", and you request
-- \"application.js\", Herringbone will run the coffee preprocessor on
-- \"application.coffee\" and serve you the result.
data PP = PP
{ ppName :: Text
-- ^ Identifies a preprocessor. Mainly useful for debugging compile errors.
, ppConsumes :: Text
-- ^ Extension for files this preprocessor consumes.
, ppProduces :: Text
-- ^ Extension for files this preprocessor produces.
, ppAction :: PPAction
-- ^ Performs the compilation.
}
instance Show PP where
show (PP name consumes produces _) = concat
[ "<PP ", T.unpack name,
" (", T.unpack consumes, " -> ", T.unpack produces, ")>"
]
-- | A function which performs the compilation.
type PPAction =
B.ByteString
-- ^ Input file contents
-> PPM (Either CompileError B.ByteString)
-- ^ Output file contents, or a compile error.
-- | A string which should contain information about why an asset failed to
-- compile.
type CompileError = B.ByteString
-- | A collection of preprocessors. This can store many preprocessors which
-- produce files with the same extension, but may not store more than one
-- preprocessor which consumes files of a particular extension.
newtype PPs = PPs { unPPs :: Map Text PP }
deriving (Show)
emptyPPs :: PPs
emptyPPs = PPs M.empty
-- | Given a file extension, find the preprocessor (if any) which consumes it.
lookupPP :: Text -> PPs -> Maybe PP
lookupPP ext pps = M.lookup ext (unPPs pps)
-- | Inserts a preprocessor into a PPs. If a preprocessor already exists with
-- the given extension, it is discarded.
insertPP :: PP -> PPs -> PPs
insertPP pp pps = PPs $ M.insert (ppConsumes pp) pp (unPPs pps)
-- | Turn a list of PPs into a proper 'PPs'.
fromList :: [PP] -> PPs
fromList = foldr insertPP emptyPPs
-- | A BuildSpec specifies how an asset should be built.
data BuildSpec = BuildSpec
{ bsSourcePath :: FilePath -- ^ Source path (relative)
, bsDestPath :: FilePath -- ^ Destination path (again, relative)
, bsPP :: (Maybe PP) -- ^ Preprocessor to run (if any)
}
deriving (Show)
-- | A BuildMapping contains the information to build all of the assets
-- Herringbone is aware of.
type BuildMapping = [BuildSpec]
-- | The \'main\' datatype in this library. All of the important functions
-- will take a 'Herringbone' as their first argument.
data Herringbone = Herringbone
{ herringboneSettings :: HerringboneSettings
, herringboneStartTime :: UTCTime
}
-- | Contains configuration.
data HerringboneSettings = HerringboneSettings
{ settingsSourceDir :: FilePath
-- ^ The directory to take asset sources from.
, settingsDestDir :: FilePath
-- ^ Where to copy assets to after they've been compiled.
, settingsPPs :: PPs
-- ^ Preprocessors
, settingsVerbose :: Bool
-- ^ Dump debugging data to stdout on every request.
}
deriving (Show)
type ConfigBuilder = HerringboneSettings -> HerringboneSettings
-- | The directory where Herringbone will look when searching for assets.
hbSourceDir :: Herringbone -> FilePath
hbSourceDir = settingsSourceDir . herringboneSettings
-- | The directory to place assets in after compilation.
hbDestDir :: Herringbone -> FilePath
hbDestDir = settingsDestDir . herringboneSettings
-- | The collection of preprocessors that will be used when preprocessing
-- assets.
hbPPs :: Herringbone -> PPs
hbPPs = settingsPPs . herringboneSettings
-- | True iff the 'Herringbone' has the verbose setting enabled.
hbVerbose :: Herringbone -> Bool
hbVerbose = settingsVerbose . herringboneSettings
-- | Log a message to stdout if hbVerbose is enabled.
verbosePut :: Herringbone -> String -> IO ()
verbosePut hb msg = when (hbVerbose hb) $ do
putStrLn ("herringbone: " ++ msg)
hFlush stdout
-- | All assets in Herringbone are referenced by their logical path. This is
-- the path to an asset, relative to the source directory.
newtype LogicalPath = LogicalPath { fromLogicalPath :: [Text] }
deriving (Show, Eq)
-- | Create a LogicalPath from a list of path segments. For example,
-- @[\"data\", \"dogs.txt\"]@ would map to data/dogs.txt (relative to the
-- source directory). This returns Nothing if the path would be unsafe (that
-- is, if it contains \"..\"), to prevent directory traversal attacks.
makeLogicalPath :: [Text] -> Maybe LogicalPath
makeLogicalPath xs = if safe xs then Just $ LogicalPath xs else Nothing
where
safe = all (not . (==) "..")
-- | Create a LogicalPath without checking any of the values.
unsafeMakeLogicalPath :: [Text] -> LogicalPath
unsafeMakeLogicalPath = LogicalPath
toFilePath :: LogicalPath -> FilePath
toFilePath = F.concat . map F.fromText . fromLogicalPath
-- | A preprocessed asset. Any function that returns this will already have
-- done the preprocessing (if necessary).
data Asset = Asset
{ assetSize :: Integer
-- ^ Size of the asset in bytes.
, assetSourcePath :: FilePath
-- ^ Path to the asset's source file on disk.
, assetFilePath :: FilePath
-- ^ Path to the preprocessed asset on disk. Note that assets which do not
-- require preprocessing will still be copied to the destination directory.
, assetModifiedTime :: UTCTime
-- ^ Modification time of the asset's source file.
}
assetContent :: Asset -> IO B.ByteString
assetContent = F.readFile . assetFilePath
instance Show Asset where
show (Asset size sourcePath filePath modifiedTime) =
"Asset { " ++
"assetSize = " ++ show size ++ ", " ++
"assetSourcePath = " ++ show sourcePath ++ ", " ++
"assetFilePath = " ++ show filePath ++ ", " ++
"assetModifiedTime = " ++ showTime modifiedTime ++ " }"
where
showTime = formatTime defaultTimeLocale (dateTimeFmt defaultTimeLocale)