master-plan 0.3.0 → 0.3.1
raw patch · 12 files changed
+430/−500 lines, 12 filesdep −containersPVP: major bump suggested
API removals or changes: PVP suggests a major version bump
Dependencies removed: containers
API changes (from Hackage documentation)
- MasterPlan.Backend.Graph: [rootKey] :: RenderOptions -> ProjectKey
- MasterPlan.Backend.Graph: [whitelistedProps] :: RenderOptions -> [ProjAttribute]
- MasterPlan.Data: BindingAtomic :: ProjectProperties -> Cost -> Trust -> Progress -> Binding
- MasterPlan.Data: BindingExpr :: ProjectProperties -> ProjectExpr -> Binding
- MasterPlan.Data: ProjectKey :: String -> ProjectKey
- MasterPlan.Data: ProjectSystem :: Map ProjectKey Binding -> ProjectSystem
- MasterPlan.Data: Reference :: ProjectKey -> ProjectExpr
- MasterPlan.Data: [bindings] :: ProjectSystem -> Map ProjectKey Binding
- MasterPlan.Data: [getProjectKey] :: ProjectKey -> String
- MasterPlan.Data: bindingTitle :: Binding -> String
- MasterPlan.Data: data Binding
- MasterPlan.Data: data ProjectExpr
- MasterPlan.Data: defaultTaskProj :: ProjectProperties -> Binding
- MasterPlan.Data: instance Data.Data.Data MasterPlan.Data.Binding
- MasterPlan.Data: instance Data.Data.Data MasterPlan.Data.ProjectExpr
- MasterPlan.Data: instance Data.Data.Data MasterPlan.Data.ProjectKey
- MasterPlan.Data: instance Data.Data.Data MasterPlan.Data.ProjectSystem
- MasterPlan.Data: instance Data.String.IsString MasterPlan.Data.ProjectKey
- MasterPlan.Data: instance GHC.Classes.Eq MasterPlan.Data.Binding
- MasterPlan.Data: instance GHC.Classes.Eq MasterPlan.Data.ProjectExpr
- MasterPlan.Data: instance GHC.Classes.Eq MasterPlan.Data.ProjectKey
- MasterPlan.Data: instance GHC.Classes.Eq MasterPlan.Data.ProjectSystem
- MasterPlan.Data: instance GHC.Classes.Ord MasterPlan.Data.ProjectKey
- MasterPlan.Data: instance GHC.Show.Show MasterPlan.Data.Binding
- MasterPlan.Data: instance GHC.Show.Show MasterPlan.Data.ProjectExpr
- MasterPlan.Data: instance GHC.Show.Show MasterPlan.Data.ProjectKey
- MasterPlan.Data: instance GHC.Show.Show MasterPlan.Data.ProjectSystem
- MasterPlan.Data: newtype ProjectKey
- MasterPlan.Data: newtype ProjectSystem
- MasterPlan.Data: prioritizeProj :: ProjectSystem -> ProjectExpr -> ProjectExpr
- MasterPlan.Data: prioritizeSys :: ProjectSystem -> ProjectSystem
- MasterPlan.Data: simplifyProj :: ProjectExpr -> ProjectExpr
- MasterPlan.Internal.Debug: debugProj :: ProjectSystem -> ProjectExpr -> IO ()
- MasterPlan.Internal.Debug: debugSys :: ProjectSystem -> IO ()
+ MasterPlan.Backend.Graph: [whitelistedAttrs] :: RenderOptions -> [ProjAttribute]
+ MasterPlan.Data: Annotated :: e -> Project e
+ MasterPlan.Data: Atomic :: ProjectProperties -> Cost -> Trust -> Progress -> Project e
+ MasterPlan.Data: data Project e
+ MasterPlan.Data: defaultAtomic :: Project a
+ MasterPlan.Data: instance Data.Data.Data e => Data.Data.Data (MasterPlan.Data.Project e)
+ MasterPlan.Data: instance GHC.Classes.Eq e => GHC.Classes.Eq (MasterPlan.Data.Project e)
+ MasterPlan.Data: instance GHC.Show.Show e => GHC.Show.Show (MasterPlan.Data.Project e)
+ MasterPlan.Data: prioritize :: ProjectExpr -> ProjectExpr
+ MasterPlan.Data: properties :: Project a -> Maybe ProjectProperties
+ MasterPlan.Data: subprojects :: Project a -> [Project a]
+ MasterPlan.Data: type ProjectExpr = Project Void
+ MasterPlan.Data: type ProjectKey = String
+ MasterPlan.Internal.Debug: printProject :: ProjectExpr -> IO ()
+ MasterPlan.Parser: type ProjectKey = String
- MasterPlan.Backend.Graph: RenderOptions :: Bool -> Integer -> Integer -> ProjectKey -> [ProjAttribute] -> RenderOptions
+ MasterPlan.Backend.Graph: RenderOptions :: Bool -> Integer -> Integer -> [ProjAttribute] -> RenderOptions
- MasterPlan.Backend.Graph: render :: FilePath -> RenderOptions -> ProjectSystem -> IO ()
+ MasterPlan.Backend.Graph: render :: FilePath -> RenderOptions -> ProjectExpr -> IO ()
- MasterPlan.Backend.Identity: render :: ProjectSystem -> [ProjAttribute] -> Text
+ MasterPlan.Backend.Identity: render :: Project a -> [ProjAttribute] -> Text
- MasterPlan.Data: Product :: (NonEmpty ProjectExpr) -> ProjectExpr
+ MasterPlan.Data: Product :: ProjectProperties -> (NonEmpty (Project e)) -> Project e
- MasterPlan.Data: ProjectProperties :: String -> Maybe String -> Maybe String -> Maybe String -> ProjectProperties
+ MasterPlan.Data: ProjectProperties :: Maybe String -> Maybe String -> Maybe String -> Maybe String -> ProjectProperties
- MasterPlan.Data: Sequence :: (NonEmpty ProjectExpr) -> ProjectExpr
+ MasterPlan.Data: Sequence :: ProjectProperties -> (NonEmpty (Project e)) -> Project e
- MasterPlan.Data: Sum :: (NonEmpty ProjectExpr) -> ProjectExpr
+ MasterPlan.Data: Sum :: ProjectProperties -> (NonEmpty (Project e)) -> Project e
- MasterPlan.Data: [title] :: ProjectProperties -> String
+ MasterPlan.Data: [title] :: ProjectProperties -> Maybe String
- MasterPlan.Data: cost :: ProjectSystem -> ProjectExpr -> Cost
+ MasterPlan.Data: cost :: ProjectExpr -> Cost
- MasterPlan.Data: progress :: ProjectSystem -> ProjectExpr -> Progress
+ MasterPlan.Data: progress :: ProjectExpr -> Progress
- MasterPlan.Data: simplify :: ProjectSystem -> ProjectSystem
+ MasterPlan.Data: simplify :: Project a -> Project a
- MasterPlan.Data: trust :: ProjectSystem -> ProjectExpr -> Trust
+ MasterPlan.Data: trust :: ProjectExpr -> Trust
- MasterPlan.Parser: runParser :: FilePath -> Text -> Either String ProjectSystem
+ MasterPlan.Parser: runParser :: Bool -> FilePath -> Text -> ProjectKey -> Either String ProjectExpr
Files
- CHANGELOG.md +7/−0
- README.md +5/−4
- app/Main.hs +31/−31
- master-plan.cabal +1/−4
- src/MasterPlan/Backend/Graph.hs +48/−96
- src/MasterPlan/Backend/Identity.hs +77/−55
- src/MasterPlan/Data.hs +91/−112
- src/MasterPlan/Internal/Debug.hs +13/−23
- src/MasterPlan/Parser.hs +62/−47
- test/MasterPlan/Arbitrary.hs +17/−44
- test/MasterPlan/DataSpec.hs +50/−58
- test/MasterPlan/ParserSpec.hs +28/−26
CHANGELOG.md view
@@ -7,6 +7,13 @@ ## [Unreleased] +## [0.3.1] - 2017-08-18++ * New option flag: `--strict` that will instruct the parser to reject plans+ with undefined projects (_i.e._ used in expressions but never defined).+ * Fix: make cost and trust value rendering consistent for leaf and non-leaf+ projects - now they are partial everywhere.+ ## [0.3.0] - 2017-08-17 New option flag: `--render-parse-error` which will render the image with the
README.md view
@@ -48,9 +48,9 @@ ``` master-plan - project management tool for hackers -Usage: master-plan [FILENAME] [-o|--output FILENAME] [--progress-below N]- [--render-parse-error] [-c|--color] [-w|--width NUMBER]- [--height NUMBER] [-r|--root NAME]+Usage: master-plan [FILENAME] [-o|--output FILENAME] [-r|--root NAME]+ [--progress-below N] [--render-parse-error] [--strict]+ [-c|--color] [-w|--width NUMBER] [--height NUMBER] [--hide title|description|url|owner|cost|trust|progress] See documentation on how to write project plan files @@ -58,13 +58,14 @@ FILENAME plan file to read from (default from stdin) -o,--output FILENAME output file name (.png, .tif, .bmp, .jpg and .pdf supported)+ -r,--root NAME name of the root project definition (default: "root") --progress-below N only display projects which progress is < N% --render-parse-error instead of printing parsing errors, render as an image+ --strict strict parsing: every project has to be defined -c,--color color each project by progress -w,--width NUMBER width of the output image --height NUMBER height of the output image- -r,--root NAME name of the root project definition (default: "root") --hide title|description|url|owner|cost|trust|progress hide a particular property -h,--help Show this help text
app/Main.hs view
@@ -12,7 +12,6 @@ import Data.List (intercalate) import qualified Data.List.NonEmpty as NE-import qualified Data.Map as M import Data.Maybe (catMaybes, fromMaybe) import Data.Semigroup ((<>)) import qualified Data.Text.IO as TIO@@ -26,18 +25,16 @@ -- |Type output from the command line parser data Opts = Opts { inputPath :: Maybe FilePath , outputPath :: Maybe FilePath+ , rootKey :: ProjectKey -- ^ name of the root project , projFilter :: ProjFilter -- ^ filter to consider , renderParsingError :: Bool -- ^ will render the parsing error instead of printing+ , parseStrict :: Bool -- ^ every project has to be defined , renderOptions :: RenderOptions }- deriving (Show) -newtype ProjFilter = ProjFilter (ProjectSystem → ProjectExpr → Bool)+type ProjFilter = ProjectExpr → Bool noFilter ∷ ProjFilter-noFilter = ProjFilter $ const $ const True--instance Show ProjFilter where- show _ = "ProjFilter"+noFilter = const True readEnum ∷ [(String, a)] → ReadM a readEnum mapping = maybeReader $ flip lookup mapping@@ -50,9 +47,17 @@ <> short 'o' <> help "output file name (.png, .tif, .bmp, .jpg and .pdf supported)" <> metavar "FILENAME" ))+ <*> strOption ( long "root"+ <> short 'r'+ <> help "name of the root project definition"+ <> value "root"+ <> showDefault+ <> metavar "NAME") <*> (filterParser <|> pure noFilter) <*> switch ( long "render-parse-error" <> help "instead of printing parsing errors, render as an image")+ <*> switch ( long "strict"+ <> help "strict parsing: every project has to be defined") <*> renderOptionsParser where renderOptionsParser ∷ Parser RenderOptions@@ -68,12 +73,6 @@ <> help "height of the output image" <> value (-1) <> metavar "NUMBER")- <*> (ProjectKey <$> strOption ( long "root"- <> short 'r'- <> help "name of the root project definition"- <> value "root"- <> showDefault- <> metavar "NAME")) <*> (invertProps <$> many (option property ( long "hide" <> help "hide a particular property" <> metavar (intercalate "|" $ map fst propertyNames))))@@ -84,11 +83,11 @@ invertProps l = filter (`notElem` l) $ map snd propertyNames filterParser ∷ Parser ProjFilter- filterParser = (ProjFilter . mkProgressFilter . Progress) <$> option auto ( long "progress-below"+ filterParser = (mkProgressFilter . Progress) <$> option auto ( long "progress-below" <> help "only display projects which progress is < N%" <> metavar "N" ) where- mkProgressFilter n sys p = progress sys p * 100 < n+ mkProgressFilter n p = progress p * 100 < n main ∷ IO () main = masterPlan =<< execParser opts@@ -98,28 +97,29 @@ <> progDesc "See documentation on how to write project plan files" <> header "master-plan - project management tool for hackers" ) -filterBinding ∷ ProjectSystem → ProjFilter → Binding → Maybe Binding-filterBinding sys (ProjFilter f) (BindingExpr r e) = BindingExpr r <$> filterProj e- where- filterProj p@(Sum ps) = filterHelper p ps Sum- filterProj p@(Product ps) = filterHelper p ps Product- filterProj p@(Sequence ps) = filterHelper p ps Sequence- filterProj p = if f sys p then Just p else Nothing-- filterHelper p ps c = if f sys p then c <$> filterProjs ps else Nothing- filterProjs ps = NE.nonEmpty (catMaybes $ NE.toList $ filterProj <$> ps)+filterProj ∷ ProjFilter -> ProjectExpr → Maybe ProjectExpr+filterProj f p@(Sum r ps) = filterHelper p f ps (Sum r)+filterProj f p@(Product r ps) = filterHelper p f ps (Product r)+filterProj f p@(Sequence r ps) = filterHelper p f ps (Sequence r)+filterProj f p = if f p then Just p else Nothing -filterBinding _ _ b = Just b+filterHelper :: ProjectExpr+ -> ProjFilter+ -> NE.NonEmpty ProjectExpr+ -> (NE.NonEmpty ProjectExpr -> ProjectExpr)+ -> Maybe ProjectExpr+filterHelper p f ps c = if f p then c <$> filterProjs ps else Nothing+ where+ filterProjs ps' = NE.nonEmpty (catMaybes $ NE.toList $ filterProj f <$> ps') masterPlan ∷ Opts → IO () masterPlan opts = do contents <- maybe (TIO.hGetContents stdin) TIO.readFile $ inputPath opts let outfile = fromMaybe (fromMaybe "output" (outputPath opts) ++ ".pdf") $ outputPath opts- case P.runParser (fromMaybe "stdin" $ inputPath opts) contents of+ case P.runParser (parseStrict opts) (fromMaybe "stdin" $ inputPath opts) contents (rootKey opts) of Left e -> if renderParsingError opts then renderText outfile (renderOptions opts) (lines e) else die e- Right sys@(ProjectSystem b) ->- do let sys' = prioritizeSys $ ProjectSystem $ M.mapMaybe- (filterBinding sys $ projFilter opts) b- render outfile (renderOptions opts) sys'+ Right p ->+ do let p' = fromMaybe defaultAtomic $ prioritize <$> filterProj (projFilter opts) p+ render outfile (renderOptions opts) p'
master-plan.cabal view
@@ -1,5 +1,5 @@ name: master-plan-version: 0.3.0+version: 0.3.1 synopsis: The project management tool for hackers description: Master Plan is a tool that parses files that describes projects using a simple and powerful syntax in which@@ -36,7 +36,6 @@ ghc-options: -Wall default-extensions: UnicodeSyntax build-depends: base >= 4.5 && < 5- , containers , master-plan , optparse-applicative , text@@ -47,7 +46,6 @@ ghc-options: -Wall default-extensions: UnicodeSyntax build-depends: base >= 4.5 && < 5- , containers , diagrams , diagrams-lib , diagrams-rasterific@@ -69,7 +67,6 @@ default-language: Haskell2010 build-depends: base >= 4.5 && < 5 , QuickCheck- , containers , hspec , master-plan , mtl
src/MasterPlan/Backend/Graph.hs view
@@ -16,13 +16,9 @@ , renderText , RenderOptions(..)) where -import Control.Applicative ((<|>)) import Control.Monad.State import Data.List (intersperse, isSuffixOf)-import qualified Data.List.NonEmpty as NE-import qualified Data.Map as M import Data.Maybe-import Data.Tree import Diagrams.Backend.Rasterific import Diagrams.Prelude hiding (Product, Sum, render) import Diagrams.TwoD.Text@@ -83,77 +79,17 @@ -> QDiagram b V2 n Any textOverflow = textOverflow' FontSlantNormal FontWeightNormal --- * Types--data NodeType = SumNode | ProductNode | SequenceNode | AtomicNode---- |Data type used by the tree-data PNode = PNode (Maybe ProjectKey)- (Maybe ProjectProperties)- Cost- Trust- Progress- | NodeRef ProjectKey--type RenderModel = Tree (NodeType, PNode)--mkLeaf :: PNode -> RenderModel-mkLeaf a = Node (AtomicNode, a) []---- |Translates a ProjectSystem into a Tree PNode-toRenderModel :: ProjectSystem -> ProjectKey -> State [ProjectKey] (Maybe RenderModel)-toRenderModel sys rootK = case M.lookup rootK (bindings sys) of- Nothing -> pure Nothing- Just b -> Just <$> bindingToRM rootK b- where- bindingToRM :: ProjectKey -> Binding -> State [ProjectKey] RenderModel- bindingToRM key (BindingExpr prop p) = projToRM p (Just key) (Just prop)- bindingToRM key (BindingAtomic prop c t p) = pure $ mkLeaf $ PNode (Just key)- (Just prop)- c t p-- mkNode :: (PNode -> [RenderModel] -> RenderModel)- -> ProjectExpr- -> NE.NonEmpty ProjectExpr- -> Maybe ProjectKey- -> Maybe ProjectProperties- -> State [ProjectKey] RenderModel- mkNode f p ps key prop = f (PNode key prop- (cost sys p)- (trust sys p)- (progress sys p))- <$> mapM (\p' -> projToRM p' Nothing Nothing) (NE.toList ps)-- projToRM :: ProjectExpr -> Maybe ProjectKey -> Maybe ProjectProperties -> State [ProjectKey] RenderModel- projToRM p@(Sum ps) = mkNode (\x -> Node (SumNode, x)) p ps- projToRM p@(Sequence ps) = mkNode (\x -> Node (SequenceNode, x)) p ps- projToRM p@(Product ps) = mkNode (\x -> Node (ProductNode, x)) p ps- projToRM (Reference n) =- \k p -> case M.lookup n $ bindings sys of- Nothing -> pure $ Node (AtomicNode, PNode k (p <|> pure defaultProjectProps {title=getProjectKey n}) defaultCost defaultTrust defaultProgress) []- Just b -> do alreadyProcessed <- gets (n `elem`)- if alreadyProcessed- then pure $ Node (AtomicNode, NodeRef $ ProjectKey $ bindingTitle b) []- else modify (n:) >> bindingToRM n b---- |how many leaf nodes-leafCount :: Tree a -> Double-leafCount (Node _ []) = 1-leafCount (Node _ ts) = sum $ leafCount <$> ts- -- |Options for rendering data RenderOptions = RenderOptions { colorByProgress :: Bool -- ^Whether to color boxes depending on progress , renderWidth :: Integer -- ^The width of the output image , renderHeight :: Integer -- ^The height of the output image- , rootKey :: ProjectKey -- ^The name of the root project- , whitelistedProps :: [ProjAttribute] -- ^Properties that should be rendered+ , whitelistedAttrs :: [ProjAttribute] -- ^Attributes that should be rendered } deriving (Eq, Show) -- | The main rendering function-render ∷ FilePath -> RenderOptions-> ProjectSystem → IO ()-render fp (RenderOptions colorByP w h rootK props) sys =- let noRootEroor = texterific $ "no project named \"" ++ getProjectKey rootK ++ "\" found."- dia = fromMaybe noRootEroor $ renderTree colorByP props <$> evalState (toRenderModel sys rootK) []+render ∷ FilePath -> RenderOptions-> ProjectExpr → IO ()+render fp (RenderOptions colorByP w h attrs) proj =+ let dia = evalState (renderProject colorByP attrs proj) [] in renderRasterific fp (dims2D (fromInteger w) (fromInteger h)) $ bgFrame 1 white $ centerXY dia -- |Render a multi-line text to file@@ -162,38 +98,51 @@ let dia = multilineText (0.1 :: Float) ss in renderRasterific fp (dims2D (fromInteger w) (fromInteger h)) $ bgFrame 1 white $ centerXY dia -renderTree :: Bool -> [ProjAttribute] -> RenderModel -> QDiagram B V2 Double Any-renderTree colorByP props (Node (_, n) []) = alignL $ renderNode colorByP props n-renderTree colorByP props x@(Node (ty, n) ts@(t:_)) =- (strutY (12 * leafCount x) <> alignL (centerY $ renderNode colorByP props n))- ||| (translateX 2 typeSymbol # withEnvelope (mempty :: D V2 Double) <> hrule 4 # lwO 2)- ||| centerY (headBar === treeBar sizes)- ||| centerY (vcat $ map renderSubTree ts)- where- sizes = map ((* 6) . leafCount) ts- renderSubTree subtree = hrule 4 # lwO 2 ||| renderTree colorByP props subtree+-- |Monad that keep state of all projects rendered so far+type AvoidRedundancy = State [ProjectExpr] - headBar = strutY $ leafCount t * 6+renderProject :: Bool -> [ProjAttribute] -> ProjectExpr -> AvoidRedundancy (QDiagram B V2 Double Any)+renderProject _ _ (Annotated _) = undefined+renderProject colorByP attrs p@Atomic {} = pure $ alignL $ renderNode colorByP attrs p+renderProject colorByP attrs proj =+ do alreadyRendered <- gets (proj `elem`)+ case title =<< properties proj of+ Just n | alreadyRendered -> pure $ renderReference n+ _ -> do modify (proj:)+ subtrees <- mapM renderSubTree $ subprojects proj+ let sizesY = map (diameter unitY) subtrees+ let headBar = case sizesY of+ [] -> mempty+ s:_ -> strutY (s/2)+ pure $ (strutY (sum sizesY) <> alignL (centerY $ renderNode colorByP attrs proj))+ ||| (translateX 2 typeSymbol # withEnvelope (mempty :: D V2 Double) <> hrule 4 # lwO 2)+ ||| centerY (headBar === treeBar sizesY)+ ||| centerY (vcat subtrees)+ where+ renderSubTree subtree = (hrule 4 # lwO 2 |||) <$> renderProject colorByP attrs subtree+ renderReference refName = text refName <> roundedRect 30 2 0.5 # lwO 2 # fc white # dashingN [0.005, 0.005] 0 treeBar :: [Double] -> QDiagram B V2 Double Any- treeBar (s1:s2:ss) = vrule s1 # lwO 2 === vrule s2 # lwO 2 === treeBar (s2:ss)- treeBar [s1] = strutY s1+ treeBar (s1:s2:ss) = vrule (s1/2) # lwO 2 === vrule (s2/2) # lwO 2 === treeBar (s2:ss)+ treeBar [s1] = strutY (s1/2) treeBar _ = mempty typeSymbol =- let txt = case ty of- SumNode -> text "+"- ProductNode -> text "x"- SequenceNode -> text "->"- AtomicNode -> mempty- in txt # fontSizeL 2 # bold <> roundedRect 3 2 1 # fc white # lwO 1+ let txt = case proj of+ Sum {} -> text "+"+ Product {} -> text "x"+ Sequence {} -> text "->"+ _ -> mempty+ in txt # fontSizeL 2 # bold <> extrudeTop 2 (extrudeBottom 2 (roundedRect 3 2 1 # fc white # lwO 1)) -renderNode :: Bool -> [ProjAttribute] -> PNode -> QDiagram B V2 Double Any-renderNode _ _ (NodeRef (ProjectKey n)) =- text n <> roundedRect 30 12 0.5 # lwO 2 # fc white # dashingN [0.005, 0.005] 0-renderNode colorByP props (PNode _ prop c t p) =- centerY nodeDia <> strutY 12+renderNode :: Bool -> [ProjAttribute] -> ProjectExpr -> QDiagram B V2 Double Any+renderNode colorByP attrs proj =+ centerY $ extrudeTop 2 $ extrudeBottom 2 nodeDia where+ c = cost proj+ t = trust proj+ p = progress proj+ prop = properties proj nodeDia = let sections = if isJust titleHeader then catMaybes [ headerSection@@ -211,14 +160,17 @@ l -> Just $ strutY 2 <> strutX nodeW <> mconcat (catMaybes l) givenProp :: ProjAttribute -> Maybe a -> Maybe a- givenProp pro x = if pro `elem` props then x else Nothing+ givenProp pro x = if pro `elem` attrs then x else Nothing headerSection = case [progressHeader, titleHeader, costHeader] of [Nothing, Nothing, Nothing] -> Nothing l -> Just $ strutY 2 <> strutX nodeW <> mconcat (catMaybes l) progressHeader = givenProp PProgress $ Just $ displayProgress p # translateX (-nodeW/2 + 1)- titleHeader = givenProp PTitle $- (centerXY . textOverflow' FontSlantNormal FontWeightBold 1 30 0.1 . title) <$> prop++ titleHeader :: Maybe (QDiagram B V2 Double Any)+ titleHeader = givenProp PTitle $ prop+ >>= title+ >>= (pure . centerXY . textOverflow' FontSlantNormal FontWeightBold 1 30 0.1) costHeader = givenProp PCost $ Just $ displayCost c # translateX (nodeW/2 - 1) descriptionSection, urlSection, bottomSection :: Maybe (QDiagram B V2 Double Any)@@ -237,7 +189,7 @@ trustHeader = translateX (-nodeW/2+1) <$> trustHeader' leftText trustHeader' txt = case t of- _ | PTrust `notElem` props -> Nothing+ _ | PTrust `notElem` attrs -> Nothing t' | t' == defaultTrust -> Nothing t' | t' == 0 -> Just $ txt "impossible" _ -> Just $ txt ("trust = " ++ percentageText (getTrust t))
src/MasterPlan/Backend/Identity.hs view
@@ -12,81 +12,103 @@ module MasterPlan.Backend.Identity (render) where import Control.Monad (when)-import Control.Monad.RWS (RWS, evalRWS, gets, modify, tell)-import Data.Generics-import Data.List (nub)+import Control.Monad.RWS hiding (Product, Sum)+import Data.List (intersperse) import qualified Data.List.NonEmpty as NE-import qualified Data.Map as M-import Data.Maybe (isJust) import Data.Monoid ((<>)) import qualified Data.Text as T import MasterPlan.Data +type RenderMonad a = RWS [ProjAttribute] T.Text [(ProjectKey, Project a)]+ -- |Plain text renderer-render ∷ ProjectSystem → [ProjAttribute] -> T.Text-render (ProjectSystem bs) whitelist =- snd $ evalRWS (renderName "root" >> renderRest) whitelist bs+render ∷ Project a → [ProjAttribute] -> T.Text+render proj whitelist =+ snd $ evalRWS (renderDefinition "root" proj) whitelist [] where- renderRest = gets M.keys >>= mapM_ renderName+ renderDefinition key p =+ do tell $ T.pack key+ when (hasAttribute p) $ do+ tell " {\n"+ renderAttr p+ tell "}"+ case p of+ Atomic {} -> pure ()+ Annotated {} -> pure ()+ p' -> tell " " >> expression False p'+ tell "\n;"+ modify $ filter ((/= key) . fst)+ remainingBindings <- get+ case remainingBindings of+ [] -> pure ()+ (k, p'):_ -> renderDefinition k p' -type RenderMonad = RWS [ProjAttribute] T.Text (M.Map ProjectKey Binding)+ expression :: Bool -> Project a -> RenderMonad a ()+ expression parens p@(Product _ ps) = maybeBinding p $ combinedE parens "*" ps+ expression parens p@(Sequence _ ps) = maybeBinding p $ combinedE parens "->" ps+ expression parens p@(Sum _ ps) = maybeBinding p $ combinedE parens "+" ps+ expression _ p@Atomic {} = maybeBinding p $ tell $ T.pack $ mkKey p+ expression _ _ = pure () -renderName ∷ ProjectKey → RenderMonad ()-renderName projName =- do mb <- gets $ M.lookup projName- case mb of- Nothing -> pure ()- Just b -> do tell $ T.pack $ getProjectKey projName- when (hasAttribute b) $ do- tell " {\n"- renderAttr b- tell "}"- case b of- BindingExpr _ e -> tell $ " " <> expressionToStr False e <> ";\n"- _ -> tell ";\n"- modify $ M.delete projName- mapM_ renderName $ dependencies b- where- hasAttribute (BindingExpr props _) = hasProperty props- hasAttribute (BindingAtomic props c t p) = hasProperty props+ maybeBinding :: Project a -> RenderMonad a () -> RenderMonad a ()+ maybeBinding p action+ | hasAttribute p = let key = mkKey p+ in modify ((key, p):) >> tell (T.pack key)+ | otherwise = action++ mkKey :: Project a -> String+ mkKey (Annotated _) = "?"+ mkKey (Product props _) = maybe "?" toId $ title props+ mkKey (Sequence props _) = maybe "?" toId $ title props+ mkKey (Sum props _) = maybe "?" toId $ title props+ mkKey (Atomic props _ _ _) = maybe "?" toId $ title props++ toId :: String -> String+ toId = mconcat . words++ combinedE :: Bool -> T.Text -> NE.NonEmpty (Project a) -> RenderMonad a ()+ combinedE parens op ps = let sube = expression True <$> NE.toList ps+ s = sequence_ $ intersperse (tell $ " " <> op <> " ") sube+ in if parens && length ps > 1+ then tell "(" >> s >> tell ")"+ else s++ hasAttribute (Annotated _) = False+ hasAttribute (Product props _) = hasProperty props+ hasAttribute (Sequence props _) = hasProperty props+ hasAttribute (Sum props _) = hasProperty props+ hasAttribute (Atomic props c t p) = hasProperty props || c /= defaultCost || t /= defaultTrust || p /= defaultProgress- hasProperty props = title props /= getProjectKey projName- || isJust (description props)- || isJust (owner props)- || isJust (url props) + hasProperty props = isNonEmpty (title props)+ || isNonEmpty (description props)+ || isNonEmpty (owner props)+ || isNonEmpty (url props)++ isNonEmpty Nothing = False+ isNonEmpty (Just "") = False+ isNonEmpty (Just _) = True+ percentage n = T.pack $ show (n * 100) <> "%" - renderAttr (BindingExpr props _) = renderProps props- renderAttr (BindingAtomic props c t p) =+ renderAttr (Annotated _) = pure ()+ renderAttr (Product props _) = renderProps props+ renderAttr (Sequence props _) = renderProps props+ renderAttr (Sum props _) = renderProps props+ renderAttr (Atomic props c t p) = do renderProps props when (c /= defaultCost) $ tell $ "cost " <> T.pack (show $ getCost c) <> "\n" when (t /= defaultTrust) $ tell $ "trust " <> percentage (getTrust t) <> "\n" when (p /= defaultProgress) $ tell $ "progress " <> percentage (getProgress p) <> "\n" - renderProps :: ProjectProperties -> RenderMonad ()- renderProps p = do let maybeRender :: T.Text -> Maybe String -> RenderMonad ()- maybeRender n = maybe (pure ()) (\x -> tell $ n <> " " <> T.pack (show x) <> "\n")- when (title p /= getProjectKey projName) $- tell $ "title " <> T.pack (show $ title p) <> "\n"+ renderProps :: ProjectProperties -> RenderMonad a ()+ renderProps p = do let maybeRender :: T.Text -> Maybe String -> RenderMonad a ()+ maybeRender _ Nothing = pure ()+ maybeRender _ (Just "") = pure ()+ maybeRender n (Just x) = tell $ n <> " " <> T.pack (show x) <> "\n"+ maybeRender "title" (title p) maybeRender "description" (description p) maybeRender "url" (url p) maybeRender "owner" (owner p)-- combinedEToStr parens op ps = let sube = map (expressionToStr True) $ NE.toList ps- s = T.intercalate (" " <> op <> " ") sube- in if parens && length ps > 1 then "(" <> s <> ")" else s-- expressionToStr :: Bool -> ProjectExpr -> T.Text- expressionToStr _ (Reference (ProjectKey n)) = T.pack n- expressionToStr parens (Product ps) = combinedEToStr parens "*" ps- expressionToStr parens (Sequence ps) = combinedEToStr parens "->" ps- expressionToStr parens (Sum ps) = combinedEToStr parens "+" ps--dependencies ∷ Binding → [ProjectKey]-dependencies = nub . everything (++) ([] `mkQ` collectDep)- where- collectDep (Reference n) = [n]- collectDep _ = []
src/MasterPlan/Data.hs view
@@ -12,11 +12,10 @@ {-# LANGUAGE OverloadedLists #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE UnicodeSyntax #-}-module MasterPlan.Data ( ProjectExpr(..)+module MasterPlan.Data ( ProjectExpr+ , ProjectKey+ , Project(..) , ProjectProperties(..)- , ProjectSystem(..)- , Binding(..)- , ProjectKey(..) , ProjAttribute(..) , Trust(..) , Cost(..)@@ -25,24 +24,25 @@ , defaultCost , defaultTrust , defaultProgress- , defaultTaskProj- , bindingTitle+ , defaultAtomic+ , properties , cost , progress , trust , simplify- , simplifyProj- , prioritizeSys- , prioritizeProj ) where+ , subprojects+ , prioritize ) where import Data.Generics import Data.List.NonEmpty (NonEmpty ((:|))) import qualified Data.List.NonEmpty as NE-import qualified Data.Map as M-import Data.String (IsString)+import Data.Void (Void) -- * Types +-- |When using to reference projects by name+type ProjectKey = String+ newtype Trust = Trust { getTrust :: Float } deriving (Show, Eq, Data, Typeable, Ord, Num, Real, RealFrac, Fractional) newtype Cost = Cost { getCost :: Float }@@ -50,25 +50,18 @@ newtype Progress = Progress { getProgress :: Float } deriving (Show, Eq, Data, Typeable, Ord, Num, Real, RealFrac, Fractional) -newtype ProjectKey = ProjectKey { getProjectKey :: String }- deriving (Show, Eq, Data, Typeable, Ord, IsString)- -- |Structure of a project expression-data ProjectExpr = Sum (NE.NonEmpty ProjectExpr)- | Product (NE.NonEmpty ProjectExpr)- | Sequence (NE.NonEmpty ProjectExpr)- | Reference ProjectKey- deriving (Eq, Show, Data, Typeable)+data Project e = Sum ProjectProperties (NE.NonEmpty (Project e))+ | Product ProjectProperties (NE.NonEmpty (Project e))+ | Sequence ProjectProperties (NE.NonEmpty (Project e))+ | Atomic ProjectProperties Cost Trust Progress+ | Annotated e+ deriving (Eq, Show, Data, Typeable) --- |A binding of a name can refer to an expression. If there are no--- associated expressions (i.e. equation) then it can have task-level--- properties-data Binding = BindingAtomic ProjectProperties Cost Trust Progress- | BindingExpr ProjectProperties ProjectExpr- deriving (Eq, Show, Data, Typeable)+type ProjectExpr = Project Void -- |Any binding (with a name) may have associated properties-data ProjectProperties = ProjectProperties { title :: String+data ProjectProperties = ProjectProperties { title :: Maybe String , description :: Maybe String , url :: Maybe String , owner :: Maybe String@@ -86,13 +79,8 @@ show PTrust = "trust" show PProgress = "progress" --- |A project system defines the bindins (mapping from names to expressions or tasks)--- and properties, which can be associated to any binding-newtype ProjectSystem = ProjectSystem { bindings :: M.Map ProjectKey Binding }- deriving (Eq, Show, Data, Typeable)- defaultProjectProps ∷ ProjectProperties-defaultProjectProps = ProjectProperties { title = "?"+defaultProjectProps = ProjectProperties { title = Nothing , description = Nothing , url = Nothing , owner = Nothing }@@ -106,108 +94,99 @@ defaultProgress ∷ Progress defaultProgress = 0 -defaultTaskProj ∷ ProjectProperties → Binding-defaultTaskProj pr = BindingAtomic pr defaultCost defaultTrust defaultProgress--bindingTitle ∷ Binding → String-bindingTitle (BindingAtomic ProjectProperties { title=t} _ _ _) = t-bindingTitle (BindingExpr ProjectProperties { title=t} _) = t+defaultAtomic :: Project a+defaultAtomic = Atomic defaultProjectProps defaultCost defaultTrust defaultProgress -- | Expected cost-cost ∷ ProjectSystem → ProjectExpr → Cost-cost sys (Reference n) =- case M.lookup n (bindings sys) of- Just (BindingAtomic _ (Cost c) _ (Progress p)) -> Cost $ c * (1 - p) -- cost is weighted by remaining progress- Just (BindingExpr _ p) -> cost sys p -- TODO:0 avoid cyclic- Nothing -> defaultCost -- mentioned but no props neither task defined-cost sys (Sequence ps) = costConjunction sys ps-cost sys (Product ps) = costConjunction sys ps-cost sys (Sum ps) =+cost ∷ ProjectExpr → Cost+cost (Atomic _ (Cost c) _ (Progress p)) = Cost $ c * (1 - p) -- cost is weighted by remaining progress+cost (Sequence _ ps) = costConjunction ps+cost (Product _ ps) = costConjunction ps+cost (Sum _ ps) = Cost $ sum $ map (\x -> (1 - snd x) * fst x) $ zip costs accTrusts where- costs = NE.toList $ (getCost . cost sys) <$> ps- accTrusts = NE.toList $ NE.scanl (\a b -> a + b*(1-a)) 0 $ (getTrust . trust sys) <$> ps+ costs = NE.toList $ (getCost . cost) <$> ps+ accTrusts = NE.toList $ NE.scanl (\a b -> a + b*(1-a)) 0 $ (getTrust . trust) <$> ps+cost (Annotated _) = undefined -costConjunction ∷ ProjectSystem → NE.NonEmpty ProjectExpr → Cost-costConjunction sys ps =+costConjunction ∷ NE.NonEmpty ProjectExpr → Cost+costConjunction ps = Cost $ sum $ zipWith (*) costs accTrusts where- costs = NE.toList $ (getCost . cost sys) <$> ps- accTrusts = NE.toList $ product <$> NE.inits ((getTrust . trust sys) <$> ps)+ costs = NE.toList $ (getCost . cost) <$> ps+ accTrusts = NE.toList $ product <$> NE.inits ((getTrust . trust) <$> ps) -- | Expected probability of succeeding-trust ∷ ProjectSystem → ProjectExpr → Trust-trust sys (Reference n) =- case M.lookup n (bindings sys) of- Just (BindingAtomic _ _ (Trust t) (Progress p)) -> Trust $ p + t * (1-p)- Just (BindingExpr _ p) -> trust sys p -- TODO:10 avoid cyclic- Nothing -> defaultTrust -- mentioned but no props neither task defined-trust sys (Sequence ps) = trustConjunction sys ps-trust sys (Product ps) = trustConjunction sys ps-trust sys (Sum ps) =- Trust $ foldl (\a b -> a + b*(1-a)) 0 $ (getTrust . trust sys) <$> ps+trust ∷ ProjectExpr → Trust+trust (Atomic _ _ (Trust t) (Progress p)) = Trust $ p + t * (1-p) -- reduced by progress+trust (Sequence _ ps) = trustConjunction ps+trust (Product _ ps) = trustConjunction ps+trust (Sum _ ps) =+ Trust $ foldl (\a b -> a + b*(1-a)) 0 $ (getTrust . trust) <$> ps+trust (Annotated _) = undefined -trustConjunction ∷ ProjectSystem → NE.NonEmpty ProjectExpr → Trust-trustConjunction sys ps = Trust $ product $ (getTrust . trust sys) <$> ps+trustConjunction ∷ NE.NonEmpty ProjectExpr → Trust+trustConjunction ps = Trust $ product $ (getTrust . trust) <$> ps -progress ∷ ProjectSystem → ProjectExpr → Progress-progress sys (Reference n) =- case M.lookup n (bindings sys) of- Just (BindingAtomic _ _ _ p) -> p- Just (BindingExpr _ p) -> progress sys p -- TODO:20 avoid cyclic- Nothing -> defaultProgress -- mentioned but no props neither task defined-progress sys (Sequence ps) = progressConjunction sys ps-progress sys (Product ps) = progressConjunction sys ps-progress sys (Sum ps) = maximum $ progress sys <$> ps+subprojects :: Project a -> [Project a]+subprojects (Sequence _ ps) = NE.toList ps+subprojects (Product _ ps) = NE.toList ps+subprojects (Sum _ ps) = NE.toList ps+subprojects _ = [] -progressConjunction ∷ ProjectSystem → NE.NonEmpty ProjectExpr → Progress-progressConjunction sys ps = sum (progress sys <$> ps) / fromIntegral (length ps) --- |Simplify a project binding structure-simplify ∷ ProjectSystem → ProjectSystem-simplify = everywhere (mkT simplifyProj)+properties :: Project a -> Maybe ProjectProperties+properties (Annotated _) = Nothing+properties (Product props _) = Just props+properties (Sequence props _) = Just props+properties (Sum props _) = Just props+properties (Atomic props _ _ _) = Just props +progress ∷ ProjectExpr → Progress+progress (Atomic _ _ _ p) = p+progress (Sequence _ ps) = progressConjunction ps+progress (Product _ ps) = progressConjunction ps+progress (Sum _ ps) = maximum $ progress <$> ps+progress (Annotated _) = undefined++progressConjunction ∷ NE.NonEmpty ProjectExpr → Progress+progressConjunction ps = sum (progress <$> ps) / fromIntegral (length ps)+ -- |Simplify a project expression structure -- 1) transform singleton collections into it's only child -- 2) flatten same constructor of the collection-simplifyProj ∷ ProjectExpr → ProjectExpr-simplifyProj (Sum (p :| [])) = simplifyProj p-simplifyProj (Product (p :| [])) = simplifyProj p-simplifyProj (Sequence (p :| [])) = simplifyProj p-simplifyProj (Sum ps) =- Sum $ (reduce . simplifyProj) =<< ps+simplify ∷ Project a → Project a+simplify (Sum _ (p :| [])) = simplify p+simplify (Product _ (p :| [])) = simplify p+simplify (Sequence _ (p :| [])) = simplify p+simplify (Sum r ps) =+ Sum r $ (reduce . simplify) =<< ps where- reduce (Sum ps') = reduce =<< ps'- reduce p = [simplifyProj p]-simplifyProj (Product ps) =- Product $ (reduce . simplifyProj) =<< ps+ reduce (Sum _ ps') = reduce =<< ps'+ reduce p = [simplify p]+simplify (Product r ps) =+ Product r $ (reduce . simplify) =<< ps where- reduce (Product ps') = reduce =<< ps'- reduce p = [simplifyProj p]-simplifyProj (Sequence ps) =- Sequence $ (reduce . simplifyProj) =<< ps+ reduce (Product _ ps') = reduce =<< ps'+ reduce p = [simplify p]+simplify (Sequence r ps) =+ Sequence r $ (reduce . simplify) =<< ps where- reduce (Sequence ps') = reduce =<< ps'- reduce p = [simplifyProj p]-simplifyProj p@Reference {} = p---- |Sort projects in the system order that minimizes cost-prioritizeSys ∷ ProjectSystem → ProjectSystem-prioritizeSys sys = everywhere (mkT $ prioritizeProj sys) sys+ reduce (Sequence _ ps') = reduce =<< ps'+ reduce p = [simplify p]+simplify p = p -- |Sort project in order that minimizes cost-prioritizeProj ∷ ProjectSystem → ProjectExpr → ProjectExpr-prioritizeProj sys (Sum ps) =- let f p = getCost (cost sys' p) / getTrust (trust sys' p)- sys' = prioritizeSys sys- in Sum $ NE.sortWith (nanToInf . f) $ prioritizeProj sys' <$> ps-prioritizeProj sys (Product ps) =- let f p = getCost (cost sys' p) / (1 - getTrust (trust sys' p))- sys' = prioritizeSys sys- in Product $ NE.sortWith (nanToInf . f) $ prioritizeProj sys' <$> ps-prioritizeProj sys (Sequence ps) =- Sequence $ prioritizeProj sys <$> ps-prioritizeProj _ p = p+prioritize ∷ ProjectExpr → ProjectExpr+prioritize (Sum r ps) =+ let f p = getCost (cost p) / getTrust (trust p)+ in Sum r $ NE.sortWith (nanToInf . f) $ prioritize <$> ps+prioritize (Product r ps) =+ let f p = getCost (cost p) / (1 - getTrust (trust p))+ in Product r $ NE.sortWith (nanToInf . f) $ prioritize <$> ps+prioritize (Sequence r ps) =+ Sequence r $ prioritize <$> ps+prioritize p = p -- |Helper function to transform any Nan (not a number) to positive infinity nanToInf :: RealFloat a => a -> a
src/MasterPlan/Internal/Debug.hs view
@@ -8,41 +8,31 @@ Portability : POSIX -} {-# LANGUAGE UnicodeSyntax #-}-module MasterPlan.Internal.Debug ( debugSys , debugProj) where+module MasterPlan.Internal.Debug (printProject) where -import Control.Monad (forM_, replicateM_, void)-import qualified Data.Map as M+import Control.Monad (forM_, replicateM_)+import Data.Maybe (fromMaybe) import MasterPlan.Data import Text.Printf (printf) -- * Debugging --- |Print a ProjectSystem to standard output-debugSys ∷ ProjectSystem → IO ()-debugSys sys@(ProjectSystem bs) = void $ M.traverseWithKey printBinding bs- where- printBinding key b = do putStrLn "-------------------"- putStr $ getProjectKey key ++ " = "- case b of- BindingExpr _ e -> putStr "\n" >> debugProj sys e- BindingAtomic _ (Cost c) (Trust t) (Progress p) ->- putStrLn $ printf "(c:%.2f,t:%.2f,p:%2.f)" c t p- -- |Print a Project Expression in a Project System to standard output. -- The expression is printed in a tree like fashion.-debugProj ∷ ProjectSystem → ProjectExpr → IO ()-debugProj sys = print' 0+printProject ∷ ProjectExpr → IO ()+printProject = print' 0 where ident ∷ Int → IO () ident il = replicateM_ il $ putStr " |" print' ∷ Int → ProjectExpr → IO ()- print' il p@(Reference (ProjectKey n)) = ident il >> putStr ("-" ++ n ++ " ") >> ctp p- print' il p@(Sum ps) = ident il >> putStr "-+ " >> ctp p >> forM_ ps (print' $ il+1)- print' il p@(Sequence ps) = ident il >> putStr "-> " >> ctp p >> forM_ ps (print' $ il+1)- print' il p@(Product ps) = ident il >> putStr "-* " >> ctp p >> forM_ ps (print' $ il+1)+ print' il p@(Atomic r _ _ _) = ident il >> putStr ("-" ++ fromMaybe "?" (title r) ++ " ") >> ctp p+ print' il p@(Sum _ ps) = ident il >> putStr "-+ " >> ctp p >> forM_ ps (print' $ il+1)+ print' il p@(Sequence _ ps) = ident il >> putStr "-> " >> ctp p >> forM_ ps (print' $ il+1)+ print' il p@(Product _ ps) = ident il >> putStr "-* " >> ctp p >> forM_ ps (print' $ il+1)+ print' _ (Annotated _ ) = undefined ctp p = putStrLn $ printf " c=%.2f t=%.2f p=%.2f"- (getCost $ cost sys p)- (getTrust $ trust sys p)- (getProgress $ progress sys p)+ (getCost $ cost p)+ (getTrust $ trust p)+ (getProgress $ progress p)
src/MasterPlan/Parser.hs view
@@ -10,13 +10,10 @@ {-# LANGUAGE OverloadedLists #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE UnicodeSyntax #-}-module MasterPlan.Parser (runParser) where+module MasterPlan.Parser (ProjectKey, runParser) where import Control.Monad.State-import Data.Generics-import Data.List (nub) import qualified Data.List.NonEmpty as NE-import qualified Data.Map as M import Data.Maybe (fromMaybe, isJust) import qualified Data.Text as T import Data.Void@@ -50,8 +47,8 @@ identifier ∷ Parser String identifier = (lexeme . try) $ (:) <$> letterChar <*> many alphaNumChar -projectKey :: Parser ProjectKey-projectKey = ProjectKey <$> (identifier >>= check) <?> "project key"+projectKey :: Parser String+projectKey = (identifier >>= check) <?> "project key" where check x | x `elem` rws = fail $ "keyword " ++ show x ++ " cannot be an identifier"@@ -69,31 +66,31 @@ nonNegativeNumber :: Parser Float nonNegativeNumber = L.float -expression ∷ Parser ProjectExpr-expression =- simplifyProj <$> makeExprParser term table <?> "expression"+-- |Parses the part of right-hand-side after the optional properties+-- (literal string title or properties between curly brackets)+expression ∷ ProjectProperties -> Parser (Project ProjectKey)+expression props =+ simplify <$> makeExprParser term table <?> "expression" where- term = parens expression <|> (Reference <$> projectKey)+ term = parens (expression defaultProjectProps) <|> (Annotated <$> projectKey) table = [[binary "*" (combineWith Product)] ,[binary "->" (combineWith Sequence)] ,[binary "+" (combineWith Sum)]]- binary op f = InfixL (f <$ symbol op)-- combineWith :: (NE.NonEmpty ProjectExpr -> ProjectExpr) -> ProjectExpr -> ProjectExpr -> ProjectExpr- combineWith c p1 p2 = c $ p1 NE.<| [p2]+ binary op f = InfixL (f <$ symbol op) + combineWith c p1 p2 = c props $ p1 NE.<| [p2] -binding :: ProjectKey -> Parser Binding---binding key = do (props, mc, mt, mp) <- bracketAttributes+-- |Parses the entire right-hand-side of definitions+binding :: ProjectKey -> Parser (Project ProjectKey) binding key = do (props, mc, mt, mp) <- try simpleTitle <|> try bracketAttributes <|> noAttributes case (mc, mt, mp) of (Nothing, Nothing, Nothing) ->- try (BindingExpr props <$> (sc *> optional (symbol "=") *> expression)) <|>- pure (BindingAtomic props defaultCost defaultTrust defaultProgress)- (mc', mt', mp') -> pure $ BindingAtomic props- (fromMaybe defaultCost mc')- (fromMaybe defaultTrust mt')- (fromMaybe defaultProgress mp')+ try (sc *> optional (symbol "=") *> expression props) <|>+ pure (Atomic props defaultCost defaultTrust defaultProgress)+ (mc', mt', mp') -> pure $ Atomic props+ (fromMaybe defaultCost mc')+ (fromMaybe defaultTrust mt')+ (fromMaybe defaultProgress mp') where attrKey :: Parser ProjAttribute attrKey = do n <- identifier <?> "attribute name"@@ -103,11 +100,11 @@ simpleTitle, bracketAttributes, noAttributes :: Parser (ProjectProperties, Maybe Cost, Maybe Trust, Maybe Progress) simpleTitle = do s <- stringLiteral <?> "title"- pure (defaultProjectProps {title=s}, Nothing, Nothing, Nothing)+ pure (defaultProjectProps {title= Just s}, Nothing, Nothing, Nothing) - bracketAttributes = symbol "{" *> attributes (defaultProjectProps {title=getProjectKey key}) Nothing Nothing Nothing+ bracketAttributes = symbol "{" *> attributes (defaultProjectProps {title=Just key}) Nothing Nothing Nothing - noAttributes = pure (defaultProjectProps {title=getProjectKey key}, Nothing, Nothing, Nothing)+ noAttributes = pure (defaultProjectProps {title=Just key}, Nothing, Nothing, Nothing) attributes :: ProjectProperties -> Maybe Cost -> Maybe Trust -> Maybe Progress -> Parser (ProjectProperties, Maybe Cost, Maybe Trust, Maybe Progress)@@ -116,7 +113,7 @@ do attr <- sc *> attrKey case attr of PTitle -> do s <- stringLiteral <?> "title"- attributes (props {title=s}) mc mt mp+ attributes (props {title=Just s}) mc mt mp PDescription -> do when (isJust $ description props) $ fail "redefinition of description" s <- stringLiteral <?> "description" attributes (props {description=Just s}) mc mt mp@@ -137,31 +134,49 @@ attributes props mc mt (Just p) --- find out all the names that a particular binding references-dependencies ∷ ProjectSystem -> Binding → [ProjectKey]-dependencies sys = everything (++) ([] `mkQ` collectDep)- where- collectDep (Reference n) = nub $ n : everything (++) ([] `mkQ` collectDep) (M.lookup n $ bindings sys)- collectDep _ = []--projectSystem :: Parser ProjectSystem-projectSystem =- mkProjSystem <$> definitions []+-- |Parses the entire plan file, given the name of the "root" project.+-- returns this project+plan :: Bool -- ^ strict mode: fails if a reference has no definition+ -> ProjectKey -- ^ the name of the root project+ -> Parser ProjectExpr+plan strictMode root =+ do bindings <- definitions []+ case lookup root bindings of+ Nothing -> fail $ "root project \"" ++ root ++ "\" is undefined"+ Just p -> resolveReferences bindings [root] p where- mkProjSystem = ProjectSystem . M.fromList + -- definitions will parse into a list of tuples: (key, Project ProjectKey)+ definitions :: [(ProjectKey, Project ProjectKey)] -> Parser [(ProjectKey, Project ProjectKey)] definitions ds = do key <- sc *> projectKey- when (key `elem` map fst ds) $ fail $ "redefinition of \"" ++ getProjectKey key ++ "\""+ when (key `elem` map fst ds) $ fail $ "redefinition of \"" ++ key ++ "\"" b <- binding key <* symbol ";"-- -- check if it's recursive- let deps = dependencies (mkProjSystem ds) b- when (key `elem` deps) $ fail $ "definition of \"" ++ getProjectKey key ++ "\" is recursive"- let ds' = (key,b):ds (try eof *> pure ds') <|> definitions ds' -runParser :: FilePath -> T.Text -> Either String ProjectSystem-runParser filename contents = case parse projectSystem filename contents of- Left e -> Left $ parseErrorPretty' contents e- Right v -> Right v+ resolveReferences :: [(ProjectKey, Project ProjectKey)]+ -> [ProjectKey]+ -> Project ProjectKey+ -> Parser ProjectExpr+ resolveReferences bs ks (Sum r ps) = Sum r <$> mapM (resolveReferences bs ks) ps+ resolveReferences bs ks (Sequence r ps) = Sequence r <$> mapM (resolveReferences bs ks) ps+ resolveReferences bs ks (Product r ps) = Product r <$> mapM (resolveReferences bs ks) ps+ resolveReferences bs ks (Annotated k)+ | k `elem` ks = fail $ "definition of \"" ++ k ++ "\" is recursive"+ | otherwise = case lookup k bs of+ Nothing+ | strictMode -> fail $ "project \"" ++ k ++ "\" is undefined"+ | otherwise -> pure $ Atomic defaultProjectProps {title=Just k}+ defaultCost defaultTrust defaultProgress+ Just p -> resolveReferences bs (k:ks) p+ resolveReferences _ _ (Atomic r c t p) = pure $ Atomic r c t p++runParser :: Bool -- ^ strict mode: fails if a reference has no definition+ -> FilePath -- ^ the file name to use in error messages+ -> T.Text -- ^ contents of the plan to parse+ -> ProjectKey -- ^ the name of the root project+ -> Either String ProjectExpr+runParser strictMode filename contents root =+ case parse (plan strictMode root) filename contents of+ Left e -> Left $ parseErrorPretty' contents e+ Right v -> Right v
test/MasterPlan/Arbitrary.hs view
@@ -2,9 +2,7 @@ {-# LANGUAGE UnicodeSyntax #-} module MasterPlan.Arbitrary () where -import Control.Monad (replicateM) import qualified Data.List.NonEmpty as NE-import qualified Data.Map as M import Data.Maybe (isNothing) import MasterPlan.Data import Test.QuickCheck@@ -14,54 +12,29 @@ arbitrary = let s = getASCIIString <$> arbitrary- os = oneof [pure Nothing, Just <$> s]- in ProjectProperties <$> s <*> os <*> os <*> os+ maybeStr x = if null x then Nothing else Just x+ os = oneof [pure Nothing, maybeStr <$> s]+ titleG = Just <$> elements [[x] | x <- ['a'..'z']]+ in ProjectProperties <$> titleG <*> os <*> os <*> os shrink p = (if isNothing (description p) then [] else [p { description = Nothing }]) ++ (if isNothing (url p) then [] else [p { url = Nothing }]) ++ (if isNothing (owner p) then [] else [p { owner = Nothing }]) -testingKeys ∷ [ProjectKey]-testingKeys = ["a", "b", "c", "d"]--instance Arbitrary ProjectSystem where-- arbitrary = do bs <- replicateM (length testingKeys) arbitrary- rootB <- BindingExpr <$> arbitrary <*> arbitrary- pure $ ProjectSystem $ M.insert "root" rootB $ M.fromList $ zip testingKeys bs-- shrink (ProjectSystem bs) =- map ProjectSystem $ concatMap shrinkOne testingKeys- where- shrinkOne ∷ ProjectKey → [M.Map ProjectKey Binding]- shrinkOne k = case M.lookup k bs of- Nothing -> []- Just b -> map (\s -> M.adjust (const s) k bs) $ shrink b--instance Arbitrary Binding where-- -- NOTE: Binding arbitrary are always tasks (no expression)- -- to avoid generating cycles- arbitrary =- let unitGen = elements [0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0]- in BindingAtomic <$> arbitrary- <*> (Cost <$> elements [0, 1 .. 100])- <*> (Trust <$> unitGen)- <*> (Progress <$> unitGen)-- shrink (BindingExpr pr e) = map (BindingExpr pr) $ shrink e- shrink _ = []--instance Arbitrary ProjectExpr where+instance Arbitrary (Project a) where arbitrary = let shrinkFactor n = 3 * n `quot` 5- in frequency [ (1, Sum <$> scale shrinkFactor arbitrary)- , (1, Product <$> scale shrinkFactor arbitrary)- , (1, Sequence <$> scale shrinkFactor arbitrary)- , (2, Reference <$> elements testingKeys) ]+ unitGen = elements [0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0]+ in frequency [ (1, Sum defaultProjectProps <$> scale shrinkFactor arbitrary)+ , (1, Product defaultProjectProps <$> scale shrinkFactor arbitrary)+ , (1, Sequence defaultProjectProps <$> scale shrinkFactor arbitrary)+ , (2, Atomic <$> arbitrary+ <*> (Cost <$> elements [0, 1 .. 100])+ <*> (Trust <$> unitGen)+ <*> (Progress <$> unitGen)) ] - shrink (Sum ps) = NE.toList ps- shrink (Product ps) = NE.toList ps- shrink (Sequence ps) = NE.toList ps- shrink (Reference _) = []+ shrink (Sum _ ps) = NE.toList ps+ shrink (Product _ ps) = NE.toList ps+ shrink (Sequence _ ps) = NE.toList ps+ shrink _ = []
test/MasterPlan/DataSpec.hs view
@@ -5,8 +5,6 @@ import Control.Monad.State import Data.Bool (bool) import qualified Data.List.NonEmpty as NE-import qualified Data.Map as M-import Data.Maybe (fromJust) import MasterPlan.Arbitrary () import MasterPlan.Data import System.Random@@ -19,26 +17,23 @@ -- It's a stateful computation with the random generator, which computes -- a 2-tuple with a Boolean: whether the execution was successful (A Bernoulli -- sample from trust), and the total actual cost incurred.-simulate ∷ RandomGen g ⇒ ProjectSystem → ProjectExpr → State g (Bool, Cost)-simulate sys (Reference n) =- case M.lookup n (bindings sys) of- Just (BindingAtomic _ (Cost c) (Trust t) (Progress p)) ->- do r <- state $ randomR (0, 1)- let remainingProgress = 1 - p- effectiveTrust = p + t * remainingProgress- effectiveCost = c * remainingProgress- pure (effectiveTrust > r, Cost effectiveCost)- Just (BindingExpr _ p) -> simulate sys p -- TODO:30 avoid cyclic- Nothing -> pure (True, defaultCost)+simulate ∷ RandomGen g ⇒ Project a → State g (Bool, Cost)+simulate (Atomic _ (Cost c) (Trust t) (Progress p)) =+ do r <- state $ randomR (0, 1)+ let remainingProgress = 1 - p+ effectiveTrust = p + t * remainingProgress+ effectiveCost = c * remainingProgress+ pure (effectiveTrust > r, Cost effectiveCost) -simulate sys (Sequence ps) = simulateConjunction sys $ NE.toList ps-simulate sys (Product ps) = simulateConjunction sys $ NE.toList ps-simulate sys (Sum ps) =+simulate (Annotated _) = pure (True, 0)+simulate (Sequence _ ps) = simulateConjunction $ NE.toList ps+simulate (Product _ ps) = simulateConjunction $ NE.toList ps+simulate (Sum _ ps) = simulate' $ NE.toList ps where- simulate' ∷ RandomGen g ⇒ [ProjectExpr] → State g (Bool, Cost)+ simulate' ∷ RandomGen g ⇒ [Project a] → State g (Bool, Cost) simulate' [] = pure (False, 0)- simulate' (p:rest) = do (success, c) <- simulate sys p+ simulate' (p:rest) = do (success, c) <- simulate p if success then pure (True, c) else@@ -48,23 +43,23 @@ -- |Helper function that samples from a sequence of projects to be executed in -- order, and which all must be successful for the end result to be succesful. -- This is the case for sequences, and products (in a particular permutation).-simulateConjunction ∷ RandomGen g ⇒ ProjectSystem → [ProjectExpr] → State g (Bool, Cost)-simulateConjunction _ [] = pure (True, 0)-simulateConjunction sys (p:rest) = do (success, c) <- simulate sys p- if success then do- (success', c') <- simulateConjunction sys rest- pure (success', c + c')- else- pure (False, c)+simulateConjunction ∷ RandomGen g ⇒ [Project a] → State g (Bool, Cost)+simulateConjunction [] = pure (True, 0)+simulateConjunction (p:rest) = do (success, c) <- simulate p+ if success then do+ (success', c') <- simulateConjunction rest+ pure (success', c + c')+ else+ pure (False, c) -- |Compute a project's trust and cost via a Monte Carlo method of computing -- the average of a handful of samples.-monteCarloTrustAndCost ∷ RandomGen g ⇒ Int → ProjectSystem → ProjectExpr → State g (Trust, Cost)-monteCarloTrustAndCost n sys p = do results <- replicateM n $ simulate sys p- let trusts = map (bool 0 1 . fst) results- costs = map snd results- pure (sum trusts / fromIntegral n,- sum costs / fromIntegral n)+monteCarloTrustAndCost ∷ RandomGen g ⇒ Int → ProjectExpr → State g (Trust, Cost)+monteCarloTrustAndCost n p = do results <- replicateM n $ simulate p+ let trusts = map (bool 0 1 . fst) results+ costs = map snd results+ pure (sum trusts / fromIntegral n,+ sum costs / fromIntegral n) aproximatelyEqual ∷ (Show a, Real a, Fractional a) => a -> a -> a → a -> Property aproximatelyEqual alpha beta x y =@@ -83,13 +78,12 @@ eq = aproximatelyEqual 0.05 0.05 prop "monte-carlo and analytical implementations should agree" $ do- let p = Reference "root"- monteCarloAndAnalyticalMustAgree ∷ ProjectSystem -> Property- monteCarloAndAnalyticalMustAgree sys =- counterexample "disagree on cost" (cost' `eq` cost sys p) .&&.- counterexample "disagree on trust" (trust' `eq` trust sys p)+ let monteCarloAndAnalyticalMustAgree ∷ ProjectExpr -> Property+ monteCarloAndAnalyticalMustAgree p =+ counterexample "disagree on cost" (cost' `eq` cost p) .&&.+ counterexample "disagree on trust" (trust' `eq` trust p) where- (trust', cost') = evalState (monteCarloTrustAndCost 50000 sys p) g+ (trust', cost') = evalState (monteCarloTrustAndCost 50000 p) g monteCarloAndAnalyticalMustAgree describe "simplification" $ do@@ -100,31 +94,30 @@ prop "is irreductible" $ do let simplificationIsIrreductible :: ProjectExpr -> Property simplificationIsIrreductible p =- let p' = simplifyProj p- p'' = simplifyProj p'+ let p' = simplify p+ p'' = simplify p' in p /= p' ==> p' == p'' simplificationIsIrreductible prop "is stable" $ do- let simplifyIsStable :: ProjectSystem -> Property- simplifyIsStable sys =- let sys' = simplify sys- p = Reference "root"- in cost sys p `eq` cost sys' p .&&. trust sys p `eq` trust sys' p+ let simplifyIsStable :: ProjectExpr -> Property+ simplifyIsStable p =+ let p' = simplify p+ in cost p `eq` cost p' .&&. trust p `eq` trust p' simplifyIsStable describe "prioritization" $ do - let shuffleProjs :: NE.NonEmpty ProjectExpr -> IO (NE.NonEmpty ProjectExpr)+ let shuffleProjs :: NE.NonEmpty (Project a) -> IO (NE.NonEmpty (Project a)) shuffleProjs ps = do ps' <- NE.toList <$> mapM shuffleProj ps g <- newStdGen pure $ NE.fromList $ shuffle' ps' (length ps') g - shuffleProj :: ProjectExpr -> IO ProjectExpr- shuffleProj (Sum ps) = Sum <$> shuffleProjs ps- shuffleProj (Product ps) = Product <$> shuffleProjs ps- shuffleProj p = pure p+ shuffleProj :: Project a -> IO (Project a)+ shuffleProj (Sum r ps) = Sum r <$> shuffleProjs ps+ shuffleProj (Product r ps) = Product r <$> shuffleProjs ps+ shuffleProj p = pure p prop "minimize cost and keep trust stable" $ do -- This test verifies that for any arbitrary project tree, the@@ -132,15 +125,14 @@ let eq = aproximatelyEqual 0.005 0.005 - let prioritizeMinimizesCost :: ProjectSystem -> Property- prioritizeMinimizesCost sys =- let (BindingExpr _ p) = fromJust $ M.lookup "root" $ bindings sys- op = prioritizeProj sys p- ocost = cost sys op- otrust = trust sys op+ let prioritizeMinimizesCost :: ProjectExpr -> Property+ prioritizeMinimizesCost p =+ let op = prioritize p+ ocost = cost op+ otrust = trust op costIsLessOrEqual p' =- counterexample ("variation has smaller cost: " ++ show p') $ ocost <= cost sys p'- trustIsSame p' = otrust `eq` trust sys p'+ counterexample ("variation has smaller cost: " ++ show p') $ ocost <= cost p'+ trustIsSame p' = otrust `eq` trust p' in ioProperty $ do variations <- replicateM 10 (shuffleProj p) return $ conjoin (map costIsLessOrEqual variations) .&. conjoin (map trustIsSame variations)
test/MasterPlan/ParserSpec.hs view
@@ -3,8 +3,6 @@ module MasterPlan.ParserSpec (spec) where import Data.Either (isRight)-import qualified Data.Map as M-import Data.Maybe (fromJust) import Data.Monoid ((<>)) import qualified Data.Text as T import MasterPlan.Arbitrary ()@@ -21,44 +19,48 @@ let allProps = [minBound :: ProjAttribute ..] + let asRoot (Atomic r c t p) = Atomic r {title=Just "root"} c t p+ asRoot (Sum r ps) = Sum r {title=Just "root"} ps+ asRoot (Sequence r ps) = Sequence r {title=Just "root"} ps+ asRoot (Product r ps) = Product r {title=Just "root"} ps+ asRoot p = p+ prop "rendered should be parseable" $ do- let renderedIsParseable ∷ ProjectSystem → Property- renderedIsParseable sys =- let rendered = render sys allProps- in counterexample (T.unpack rendered) $ isRight (runParser "test1" rendered)+ let renderedIsParseable ∷ ProjectExpr → Property+ renderedIsParseable p =+ let p' = simplify p+ rendered = render p' allProps+ in counterexample (T.unpack rendered) $ isRight (runParser False "test1" rendered "root") withMaxSuccess 50 renderedIsParseable prop "identity backend output should parse into the same input" $ do - let propertyParseAndOutputIdentity ∷ ProjectSystem → Property- propertyParseAndOutputIdentity sys =- let sys' = simplify sys- parsed = runParser "test2" (render sys' allProps)- in isRight parsed ==> parsed === Right sys'+ let propertyParseAndOutputIdentity ∷ ProjectExpr → Property+ propertyParseAndOutputIdentity p =+ let p' = asRoot $ simplify p+ parsed = runParser False "test2" (render p' allProps) "root"+ in isRight parsed ==> parsed === Right p' withMaxSuccess 50 propertyParseAndOutputIdentity it "should parse without prioritization" $ do - let input = "root = a + b;\+ let input = "main = a + b;\ \a = x + y;\ \b { cost 9 };\ \x { cost 10 };\ \y { cost 5 trust 90% };" - let (Right sys) = runParser "test" input-- let (BindingExpr _ root) = fromJust $ M.lookup "root" (bindings sys)+ let (Right p) = runParser True "test" input "main" - cost sys root `shouldBe` 10.0+ cost p `shouldBe` 10.0 -- now prioritize... a little out of scope for this test, but fine: - let sys' = prioritizeSys sys- let (BindingExpr _ root') = fromJust $ M.lookup "root" (bindings sys')+ let p' = prioritize p - cost sys' root' `shouldBe` 6.0+ cost p' `shouldBe` 6.0 it "should reject recursive equations" $ do @@ -72,22 +74,22 @@ -- obvious let program1 = wrap ["root = a + b + root"] - runParser "recursive1" program1 `shouldSatisfy` expectedError "root"+ runParser False "recursive1" program1 "root" `shouldSatisfy` expectedError "root" let program2 = wrap [ "root = a + b" , "a = x * root" ] - runParser "recursive2" program2 `shouldSatisfy` expectedError "a"+ runParser False "recursive2" program2 "root" `shouldSatisfy` expectedError "root" - let program3 = wrap [ "root = x + y"+ let program3 = wrap [ "xxx = x + a" , "a = b * c" , "c = d -> a" ] - runParser "recursive3" program3 `shouldSatisfy` expectedError "c"+ runParser False "recursive3" program3 "xxx" `shouldSatisfy` expectedError "a" - let program4 = wrap [ "root = a + y"- , "d = x + root"+ let program4 = wrap [ "yyy = a + y"+ , "d = x + yyy" , "a = b * c" , "c = d -> e" ] - runParser "recursive4" program4 `shouldSatisfy` expectedError "c"+ runParser False "recursive4" program4 "yyy" `shouldSatisfy` expectedError "yyy"