{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE TemplateHaskell #-} module Erd.Config ( Config(..) , configIO , defaultConfig , defaultConfigFile ) where import Control.Exception (tryJust) import Control.Monad (guard) import qualified Data.ByteString.Char8 as B import Data.Char (isSpace) import qualified Data.GraphViz.Attributes.Complete as A import qualified Data.GraphViz.Commands as G import Data.List (dropWhileEnd, intercalate) import qualified Data.Map as M import Data.Maybe (isNothing) import Data.Version (showVersion) import Data.Yaml (FromJSON (..), (.:)) import qualified Data.Yaml as Y import Development.GitRev (gitHash) import Paths_erd (version) import qualified System.Console.GetOpt as O import System.Directory (getHomeDirectory) import System.Environment (getArgs) import System.Exit (exitFailure, exitSuccess) import System.FilePath (()) import System.IO (Handle, IOMode (..), openFile, stderr, stdin, stdout) import System.IO.Error (isDoesNotExistError) import Text.Printf (HPrintfType, hPrintf, printf) import Text.RawString.QQ -- | Config represents all information from command line flags. data Config = Config { cin :: (String, Handle) , cout :: (String, Handle) , outfmt :: Maybe G.GraphvizOutput , edgeType :: Maybe A.EdgeType , configFile :: Maybe FilePath , dotentity :: Bool } -- | Represents fields that are stored in the configuration file. data ConfigFile = ConfigFile { cFmtOut :: String , cEdgeType :: String , cDotEntity :: Bool } deriving Show instance FromJSON ConfigFile where parseJSON (Y.Object v) = ConfigFile <$> v .: "output-format" <*> v .: "edge-style" <*> v .: "dot-entity" parseJSON _ = fail "Incorrect configuration file." defaultConfig :: Config defaultConfig = Config { cin = ("", stdin) , cout = ("", stdout) , outfmt = Nothing , edgeType = Just A.SplineEdges , configFile = Nothing , dotentity = False } defaultConfigFile :: B.ByteString defaultConfigFile = B.unlines [[r|# Erd (~/.erd.yaml) default configuration file.|], B.append [r|output-format: pdf # Supported formats: |] (defVals fmts), B.append [r|edge-style: spline # Supported values : |] (defVals edges), B.append [r|dot-entity: false # Supported values : |] (defVals valBool) ] where defVals = B.pack . unwords . M.keys -- | Creates a new Config value from command line options. -- If an output path is given and `--fmt` is omitted, then a format -- will be inferred from the output path extension. -- Failing all of that, PDF is used by default. configIO :: IO Config configIO = do args <- getArgs case O.getOpt O.Permute opts args of (flags, [], []) -> do conf <- foldl (\c app -> app c) (return defaultConfig) flags let outpath = fst (cout conf) return $ if isNothing (outfmt conf) && outpath /= "" then conf { outfmt = toGraphFmt $ takeExtension outpath } else conf (_, _, errs@(_:_)) -> do ef "Error(s) parsing flags:\n\t%s\n" $ intercalate "\n\t" $ map strip errs exitFailure (_, _, []) -> do ef "erd does not have any positional arguments.\n\n" usageExit -- | Order of processing command-line options is important to keep priority of -- configuration sources, in terms of increasing precedence. -- 1. Command-line options -- 2. Configuration file of arbitrary path, e.g. '-c"/tmp/myconfig.yaml"' -- 3. Configuration file: ~/.erd.yaml opts :: [O.OptDescr (IO Config -> IO Config)] opts = [ O.Option "c" ["config"] (O.OptArg (\mf cIO -> cIO >>= \c -> do globConfFile <- readGlobalConfigFile f <- readConfigFile mf case (f, globConfFile) of (Nothing, Nothing) -> -- Config-file is desired, but unavailable. B.putStr defaultConfigFile >> return c (Nothing, Just x) -> -- Use global config-file from ~/.erd.yaml . return $ toConfig x (Just x, _) -> -- Use user specified config-file. return $ toConfig x ) "FILE") "Configuration file." , O.Option "i" ["input"] (O.ReqArg (\fpath cIO -> do c <- cIO i <- openFile fpath ReadMode return $ c {cin = (fpath, i)} ) "FILE") ("When set, input will be read from the given file.\n" ++ "Otherwise, stdin will be used.") , O.Option "o" ["output"] (O.ReqArg (\fpath cIO -> do c <- cIO o <- openFile fpath WriteMode return $ c {cout = (fpath, o)} ) "FILE") ("When set, output will be written to the given file.\n" ++ "Otherwise, stdout will be used.\n" ++ "If given and if --fmt is omitted, then the format will be\n" ++ "guessed from the file extension.") , O.Option "h" ["help"] (O.NoArg $ const usageExit) "Show this usage message." , O.Option "f" ["fmt"] (O.ReqArg (\fmt cIO -> do c <- cIO let mfmt = toGraphFmt fmt case mfmt of Nothing -> do ef "'%s' is not a valid output format." fmt exitFailure Just gfmt -> return c {outfmt = Just gfmt} ) "FMT") (printf "Force the output format to one of:\n%s" (intercalate ", " $ M.keys fmts)) , O.Option "e" ["edge"] (O.ReqArg (\edge cIO -> do c <- cIO let edgeG = toEdgeG edge case edgeG of Nothing -> do ef "'%s' is not a valid type of edge." edge exitFailure Just x -> return c {edgeType = Just x} ) "EDGE") (printf "Select one type of edge:\n%s" (intercalate ", " $ M.keys edges)) , O.Option "d" ["dot-entity"] (O.NoArg (\cIO -> do c <- cIO return $ c { dotentity = True } )) ("When set, output will consist of regular dot tables instead of HTML tables.\n" ++ "Formatting will be disabled. Use only for further manual configuration.") , O.Option "v" ["version"] (O.NoArg $ const erdVersion) "Shows version of application and revision code.." ] toConfig :: ConfigFile -> Config toConfig c = defaultConfig {outfmt = toGraphFmt $ cFmtOut c, edgeType = toEdgeG $ cEdgeType c, dotentity = cDotEntity c } -- | Reads and parses configuration file at default location: ~/.erd.yaml readGlobalConfigFile :: IO (Maybe ConfigFile) readGlobalConfigFile = do mHome <- tryJust (guard . isDoesNotExistError) getHomeDirectory case mHome of Left _ -> return Nothing Right home -> readConfigFile $ Just (home ".erd.yaml") -- | Reads and parses a configuration file, exceptions may come via -- AesonException. readConfigFile :: Maybe FilePath -> IO (Maybe ConfigFile) readConfigFile Nothing = return Nothing readConfigFile (Just f) = do mHome <- tryJust (guard . isDoesNotExistError) $ B.readFile f case mHome of Left _ -> return Nothing Right home -> Y.decodeThrow home -- | A subset of formats supported from GraphViz. fmts :: M.Map String (Maybe G.GraphvizOutput) fmts = M.fromList [ ("pdf", Just G.Pdf) , ("svg", Just G.Svg) , ("eps", Just G.Eps) , ("bmp", Just G.Bmp) , ("jpg", Just G.Jpeg) , ("png", Just G.Png) , ("gif", Just G.Gif) , ("tiff", Just G.Tiff) , ("dot", Just G.Canon) , ("ps", Just G.Ps) , ("ps2", Just G.Ps2) , ("plain", Just G.Plain) ] edges :: M.Map String (Maybe A.EdgeType) edges = M.fromList [ ("spline", Just A.SplineEdges) , ("ortho", Just A.Ortho) , ("noedge", Just A.NoEdges) , ("poly", Just A.PolyLine) , ("compound", Just A.CompoundEdge) ] valBool :: M.Map String Bool valBool = M.fromList [ ("true", True) , ("false", False) ] -- | takeExtension returns the last extension from a file path, or the -- empty string if no extension was found. e.g., the extension of -- "wat.pdf" is "pdf". takeExtension :: String -> String takeExtension s = if null rest then "" else reverse ext where (ext, rest) = span (/= '.') $ reverse s toGraphFmt :: String -> Maybe G.GraphvizOutput toGraphFmt ext = M.findWithDefault Nothing ext fmts toEdgeG :: String -> Maybe A.EdgeType toEdgeG edge = M.findWithDefault Nothing edge edges usageExit :: IO a usageExit = usage >> exitFailure usage :: IO () usage = hPrintf stderr "%s\n" $ O.usageInfo "Usage: erd [flags]" opts erdVersion :: IO a erdVersion = do hPrintf stdout "erd-%s %s\n" (showVersion version) ($(gitHash) :: String) exitSuccess ef :: HPrintfType r => String -> r ef = hPrintf stderr strip :: String -> String strip = dropWhile isSpace . dropWhileEnd isSpace