{-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TemplateHaskell #-} -- | This module defines how report input is formed. module OrgStat.Scope ( AstPath (..) , isSubPath , existsPath , ScopeModifier (..) , applyModifiers ) where import qualified Base as Base import Control.Lens (to) import Control.Monad.Except (throwError) import qualified Data.Text as T import Universum import OrgStat.Ast (Org, atDepth, orgClocks, orgSubtrees, orgTitle, traverseTree) -- | Path in org AST is just a list of paths, head ~ closer to tree -- root. newtype AstPath = AstPath { getAstPath :: [Text] } deriving (Eq, Ord) instance Base.Show AstPath where show (AstPath path) | null path = "" | otherwise = intercalate "/" (map T.unpack path) isSubPath :: AstPath -> AstPath -> Bool isSubPath (AstPath l1) (AstPath l2) = l1 `isPrefixOf` l2 -- | Lens to a org node at path. atPath :: AstPath -> Lens' Org (Maybe Org) atPath (AstPath []) f o = fromMaybe o <$> f (Just o) atPath (AstPath p) f o = atPathDo p o where atPathDo [] org = f Nothing $> org atPathDo (x:xs) org = let match = find ((== x) . view orgTitle) $ org ^. orgSubtrees modified foo = org & orgSubtrees %~ foo . filter ((/= match) . Just) fmapFoo Nothing = modified identity fmapFoo (Just o') = modified (o' :) in case (xs,match) of (_,Nothing) -> f Nothing $> org ([],_) -> fmap fmapFoo $ f match (cont,Just m) -> fmap (\new -> modified (new:)) $ atPathDo cont m -- | Checks if something is on that path in given 'Org'. existsPath :: AstPath -> Org -> Bool existsPath p o = o ^. atPath p . to isJust -- | Modificicators of org tree. They remove some subtrees data ScopeModifier = ModPruneSubtree AstPath Int -- ^ Turns all subtrees starting with @path@ and then on depth @d@ into leaves. | ModFilterTag Text -- ^ Given text tag name, it leaves only those subtrees that -- have this tag (tags inherit). | ModSquash AstPath -- ^ Starting at node on path A and depth n, turn A into set of -- nodes A/a1/a2/.../an. Doesn't work/make sense for empty path. | ModSelectSubtree AstPath -- ^ Leaves only node at @path@, deletes all other subtrees. deriving (Show,Eq,Ord) -- | Errors related to modifiers application data ModifierError = MEConflicting ScopeModifier ScopeModifier Text -- ^ Modifiers can't be applied together (del/sel) | MEWrongParam ScopeModifier Text -- ^ Modifier doesn't support this parameter deriving (Show,Typeable) instance Exception ModifierError -- | Applies modifier to org tree applyModifier :: ScopeModifier -> Org -> Either ModifierError Org applyModifier m@(ModPruneSubtree path depth) org = do unless (depth >= 0) $ throwError $ MEWrongParam m "Depth should be >= 0" unless (existsPath path org) $ throwError $ MEWrongParam m $ "Path " <> show path <> " doesn't exist" let subclocks o' = o' & orgClocks .~ (concatMap (view orgClocks) $ o' ^.. traverseTree) & orgSubtrees .~ [] let pruneChildren o = o & atDepth depth %~ subclocks pure $ org & atPath path %~ (\x -> maybe x (Just . pruneChildren) x) applyModifier m@(ModSelectSubtree path) org = do unless (existsPath path org) $ throwError $ MEWrongParam m $ "Path " <> show path <> " doesn't exist" pure $ fromMaybe (error "applyModifier@ModSelectSubtree is broken") $ org ^. atPath path applyModifier _ org = pure org -- TODO -- | Generates an org to be processed by report generators from 'Scope'. applyModifiers :: Org -> [ScopeModifier] -> Either ModifierError Org applyModifiers org s = do -- whenList addDelConflicts $ \(m1,m2) -> -- throwError $ MEConflicting m1 m2 "Path of first modifier is subpath of second one" foldrM applyModifier org mods where -- whenList ls foo = case ls of -- [] -> pass -- (h:_) -> foo h -- addDelConflicts = -- let addDelConflict (ModPruneSubtree a _) (ModSelectSubtree b) = a `isSubPath` b -- addDelConflict _ _ = False -- in filter (uncurry addDelConflict) modsPairs -- modsPairs = [(a,b) | a <- mods, b <- mods, a < b] mods = sort s