{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeApplications #-} {-# OPTIONS_GHC -Wno-orphans #-} -- | Configuration file types, together with json instances. module OrgStat.Config ( ConfigException (..) , ConfDate(..) , ConfRange(..) , ConfReportType(..) , ConfScope (..) , ConfReport (..) , OrgStatConfig (..) ) where import Data.Aeson (FromJSON (..), Value (Object, String), (.!=), (.:), (.:?)) import Data.Aeson.Types (typeMismatch) import Data.Default (def) import Data.List.NonEmpty (NonEmpty) import qualified Data.Text as T import Data.Time (LocalTime) import Data.Time.Format (defaultTimeLocale, parseTimeM) import Universum import OrgStat.Report.Timeline (TimelineParams, tpBackground, tpColumnHeight, tpColumnWidth, tpLegend, tpTopDay) import OrgStat.Scope (AstPath (..), ScopeModifier (..)) import OrgStat.Util (parseColour, (??~)) -- | Exception type for everything bad that happens with config, -- starting from parsing to logic errors. data ConfigException = ConfigParseException Text | ConfigLogicException Text deriving (Show, Typeable) instance Exception ConfigException data ConfDate = ConfNow | ConfLocal !LocalTime deriving (Show) data ConfRange = ConfFromTo !ConfDate !ConfDate | ConfBlockWeek !Integer | ConfBlockDay !Integer | ConfBlockMonth !Integer deriving (Show) data ConfReportType = Timeline { timelineRange :: !ConfRange , timelineScope :: !Text , timelineParams :: !TimelineParams } deriving (Show) data ConfScope = ConfScope { csName :: !Text -- default is "default" , csPaths :: !(NonEmpty FilePath) } deriving (Show) data ConfReport = ConfReport { crType :: !ConfReportType -- includes config , crName :: !Text , crModifiers :: ![ScopeModifier] } deriving (Show) data OrgStatConfig = OrgStatConfig { confScopes :: ![ConfScope] , confReports :: ![ConfReport] , confBaseTimelineParams :: !TimelineParams , confTodoKeywords :: ![Text] , confOutputDir :: !FilePath -- default is "./orgstat" , confColorSalt :: !Int } deriving (Show) instance FromJSON AstPath where parseJSON (String s) = pure $ AstPath $ filter (not . T.null) $ T.splitOn "/" s parseJSON invalid = typeMismatch "AstPath" invalid instance FromJSON ScopeModifier where parseJSON (Object v) = do v .: "type" >>= \case (String "prune") -> ModPruneSubtree <$> v .: "path" <*> v .:? "depth" .!= 0 (String "select") -> ModSelectSubtree <$> v .: "path" other -> fail $ "Unsupported scope modifier type: " ++ show other parseJSON invalid = typeMismatch "ScopeModifier" invalid instance FromJSON ConfDate where parseJSON (String "now") = pure $ ConfNow parseJSON (String s) = case parseTimeM True defaultTimeLocale "%Y-%m-%d %H:%M" (T.unpack s) of Nothing -> fail $ "Couldn't read date " <> show s <> ". Correct format is 2016-01-01 23:59" Just ut -> pure $ ConfLocal ut parseJSON invalid = typeMismatch "ConfDate" invalid instance FromJSON ConfRange where parseJSON (String s) | any (`T.isPrefixOf` s) ["day", "week", "month"] = do let splitted = T.splitOn "-" $ if "-" `T.isInfixOf` s then s else s <> "-" [range, number] = splitted constructor :: (Monad m, MonadFail m) => Integer -> m ConfRange constructor i = case range of "day" -> pure $ ConfBlockDay i "week" -> pure $ ConfBlockWeek i "month" -> pure $ ConfBlockMonth i t -> fail $ "ConfRange@parseJSON can't parse " <> T.unpack t <> " should be [day|week|month]" numberParsed | number == "" = pure 0 | otherwise = case readMaybe (T.unpack number) of Nothing -> fail $ "Couldn't parse number modifier of " <> T.unpack s Just x -> pure x when (length splitted /= 2) $ fail $ "Couldn't parse range " <> T.unpack s <> ", splitted is " <> show splitted constructor =<< numberParsed parseJSON (Object v) = ConfFromTo <$> v .: "from" <*> v .: "to" parseJSON invalid = typeMismatch "ConfRange" invalid instance FromJSON TimelineParams where parseJSON (Object v) = do legend <- v .:? "legend" topDay <- v .:? "topDay" colWidth <- v .:? "colWidth" colHeight <- v .:? "colHeight" bgColorRaw <- v .:? "background" pure $ def & tpLegend ??~ legend & tpTopDay ??~ topDay & tpColumnWidth ??~ colWidth & tpColumnHeight ??~ colHeight & tpBackground ??~ (T.strip <$> bgColorRaw >>= parseColour @Text) parseJSON invalid = typeMismatch "TimelineParams" invalid instance FromJSON ConfReportType where parseJSON o@(Object v) = do v .: "type" >>= \case (String "timeline") -> Timeline <$> v .: "range" <*> v .:? "scope" .!= "default" <*> parseJSON o other -> fail $ "Unsupported scope modifier type: " ++ show other parseJSON invalid = typeMismatch "ConfReportType" invalid instance FromJSON ConfScope where parseJSON (Object v) = do ConfScope <$> v .:? "name" .!= "default" <*> v .: "paths" parseJSON invalid = typeMismatch "ConfScope" invalid instance FromJSON ConfReport where parseJSON (Object v) = do ConfReport <$> v .: "type" <*> v .:? "name" .!= "default" <*> v .:? "modifiers" .!= [] parseJSON invalid = typeMismatch "ConfReport" invalid instance FromJSON OrgStatConfig where parseJSON (Object v) = do OrgStatConfig <$> v .: "scopes" <*> v .: "reports" <*> v .:? "timelineDefault" .!= def <*> v .:? "todoKeywords" .!= [] <*> v .:? "output" .!= "./orgstat" <*> v .:? "colorSalt" .!= 0 parseJSON invalid = typeMismatch "ConfReport" invalid