dojang-0.1.0: src/Dojang/Types/FileRoute.hs
{-# LANGUAGE OverloadedRecordDot #-}
{-# LANGUAGE OverloadedStrings #-}
module Dojang.Types.FileRoute
( FileRoute (..)
, RouteWarning (..)
, fileRoute
, fileRoute'
, dispatch
, routePath
) where
import Data.List (sortOn)
import Data.Ord (Down (Down))
import Prelude hiding (lookup)
import Data.HashMap.Strict (lookup)
import Data.HashSet (HashSet, singleton, union, unions)
import Data.Text (unpack)
import System.OsPath (OsPath)
import System.OsString (OsString)
import Dojang.MonadFileSystem (FileType, MonadFileSystem (..))
import Dojang.Types.Environment (Environment)
import Dojang.Types.EnvironmentPredicate (EnvironmentPredicate (..))
import Dojang.Types.EnvironmentPredicate.Evaluate (EvaluationWarning, evaluate')
import Dojang.Types.EnvironmentPredicate.Specificity (Specificity, specificity)
import Dojang.Types.FilePathExpression (EnvironmentVariable, FilePathExpression)
import Dojang.Types.FilePathExpression.Expansion
( ExpansionWarning
, expandFilePath
)
import Dojang.Types.MonikerMap (MonikerMap, MonikerResolver)
import Dojang.Types.MonikerName (MonikerName)
-- | A route that maps 'EnvironmentPredicate's to 'FilePathExpression's.
--
-- Each 'EnvironmentPredicate' represents a condition that must be met for the
-- 'FilePathExpression' to be dispatched. The 'FilePathExpression' is
-- the path to the directory that should be dispatched to. If it is 'Nothing',
-- then it is considered a "null route" and will be ignored.
--
-- Note that a null route will still be considered a match, and will be
-- dispatched as 'Nothing'.
data FileRoute = FileRoute
{ monikerResolver :: MonikerResolver
-- ^ The 'MonikerResolver' used to resolve 'MonikerName's.
, predicates :: [(EnvironmentPredicate, Maybe FilePathExpression)]
-- ^ The pairs of 'EnvironmentPredicate's and 'FilePathExpression's that make
-- up the 'FileRoute'. If the file path expression is 'Nothing', then it is
-- considered a "null route" and will be ignored.
, fileType :: FileType
-- ^ Whether the 'FileRoute' represents a file or a directory.
}
instance Eq FileRoute where
a == b =
(a.fileType == b.fileType)
&& (a.predicates == b.predicates)
&& all (\m -> a.monikerResolver m == b.monikerResolver m) allMonikerRefs
where
monikerRefs :: EnvironmentPredicate -> HashSet MonikerName
monikerRefs predicates' =
case predicates' of
Moniker monikerName -> singleton monikerName
Or ps -> foldMap monikerRefs ps
And ps -> foldMap monikerRefs ps
Not p -> monikerRefs p
OperatingSystem _ -> mempty
Architecture _ -> mempty
Always -> mempty
KernelName _ -> mempty
KernelRelease _ -> mempty
KernelReleasePrefix _ -> mempty
KernelReleaseSuffix _ -> mempty
allMonikerRefs :: HashSet MonikerName
allMonikerRefs =
unions (monikerRefs . fst <$> a.predicates)
`union` unions (monikerRefs . fst <$> b.predicates)
-- | Creates a 'FileRoute' from a 'MonikerMap' and pairs of
-- 'MonikerName's and 'FilePathExpression's.
fileRoute
:: MonikerMap
-- ^ The 'MonikerMap' to resolve 'MonikerName's against.
-> [(MonikerName, Maybe FilePathExpression)]
-- ^ The pairs of 'MonikerName's and 'FilePathExpression's that make up the
-- 'FileRoute'. If the file path expression is 'Nothing', then it is
-- considered a "null route" and will be ignored.
-> FileType
-- ^ Whether the 'FileRoute' represents a file or a directory.
-> FileRoute
-- ^ The resulting 'FileRoute'.
fileRoute monikerMap predicates' =
fileRoute'
(`lookup` monikerMap)
[(Moniker name, pathExpr) | (name, pathExpr) <- predicates']
-- | Similar to 'FileRoute', but it takes more raw arguments: a 'MonikerResolver'
-- (instead of a 'MonikerMap'), and a pairs of 'EnvironmentPredicate' (instead
-- of 'MonikerName') and 'FilePathExpression'.
fileRoute'
:: MonikerResolver
-- ^ A function that resolves a 'MonikerName' to an 'EnvironmentPredicate'.
-> [(EnvironmentPredicate, Maybe FilePathExpression)]
-- ^ The pairs of 'EnvironmentPredicate's and 'FilePathExpression's that make
-- up the 'FileRoute'. If the file path expression is 'Nothing', then it is
-- considered a "null route" and will be ignored.
-> FileType
-- ^ Whether the 'FileRoute' represents a file or a directory.
-> FileRoute
-- ^ The resulting 'FileRoute'.
fileRoute' resolver predicates' =
FileRoute resolver $ sortOn sortKey predicates'
where
sortKey
:: (EnvironmentPredicate, Maybe FilePathExpression)
-> Down (Specificity, String)
sortKey (pred', _) = Down (specificity resolver pred', show pred')
-- | Dispatches the given 'FileRoute' against the given 'Environment'.
dispatch
:: Environment
-- ^ The 'Environment' to dispatch against.
-> FileRoute
-- ^ The 'FileRoute' to dispatch.
-> ([Maybe FilePathExpression], [EvaluationWarning])
-- ^ The list of 'Maybe' 'FilePathExpression's that matched
-- the given 'Environment' and 'MonikerMap'. Even if matches are made,
-- their values may be 'Nothing' if the 'FilePathExpression' was
-- a "null route".
-- The 'snd' of the result is a list of warnings that occurred
-- during evaluation whether or not any 'FilePathExpression' matched.
dispatch environment (FileRoute resolver route _) =
(matched, warnings)
where
eval :: EnvironmentPredicate -> (Bool, [EvaluationWarning])
eval = evaluate' environment resolver
evaluated :: [((Bool, [EvaluationWarning]), Maybe FilePathExpression)]
evaluated = [(eval predicate, expr) | (predicate, expr) <- route]
warnings :: [EvaluationWarning]
warnings = concatMap (snd . fst) evaluated
matched :: [Maybe FilePathExpression]
matched = [expr | ((True, _), expr) <- evaluated]
-- | A warning that can occur during path routing.
data RouteWarning
= -- | A warning that can occur during environment predicate evaluation.
EnvironmentPredicateWarning EvaluationWarning
| -- | A warning that can occur during file path expression expansion.
FilePathExpressionWarning ExpansionWarning
deriving (Eq, Show)
-- | Routes the given 'FileRoute' against the given 'Environment', and expands
-- the resulting 'FilePathExpression' into an 'OsPath', if any. For null
-- routes, returns 'Nothing'.
routePath
:: (MonadFileSystem m)
=> FileRoute
-- ^ The 'FileRoute' to route and expand.
-> Environment
-- ^ The environment to use when evaluating environment predicates.
-> (EnvironmentVariable -> m (Maybe OsString))
-- ^ A function that can look up an environment variable.
-> m (Maybe OsPath, [RouteWarning])
-- ^ The resulting 'OsPath', if any, and a list of warnings that occurred
-- during evaluation and expansion. For null routes, returns 'Nothing',
-- but still returns any warnings that occurred (if any).
routePath route env lookupEnvVar = do
let (matched, warnings) = dispatch env route
let predWarnings = map EnvironmentPredicateWarning warnings
case matched of
[] -> return (Nothing, predWarnings)
(Nothing : _) -> return (Nothing, predWarnings)
(Just pathExpr : _) -> do
(path, exprWarnings) <-
expandFilePath pathExpr lookupEnvVar (encodePath . unpack)
return (Just path, predWarnings ++ map FilePathExpressionWarning exprWarnings)
instance Show FileRoute where
show (FileRoute _ route fileType') =
"FileRoute " ++ show route ++ " " ++ show fileType'