This repository has been archived by the owner on May 20, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Config.hs
139 lines (112 loc) · 5.05 KB
/
Config.hs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
{-|
Module : Omnifmt.Config
Description : Configuration data structures.
Copyright : (c) Henry J. Wylde, 2015
License : BSD3
Maintainer : public@hjwylde.com
Configuration data structures.
-}
{-# LANGUAGE OverloadedStrings #-}
module Omnifmt.Config (
-- * Config
Config(..),
emptyConfig, readConfig, nearestConfigFile, defaultFileName, programFor, unsafeProgramFor,
supported,
-- * Program
Program(..),
emptyProgram, substitute, usesInputVariable, usesOutputVariable, inputVariableName,
outputVariableName,
) where
import Control.Arrow (second)
import Control.Monad.Except
import Control.Monad.Extra
import Control.Monad.Logger
import Data.Aeson.Types
import Data.HashMap.Lazy (toList)
import Data.List (find)
import Data.Maybe (fromJust, isJust)
import Data.Text (Text, cons, isInfixOf, pack, replace, snoc)
import Data.Yaml (prettyPrintParseException)
import Data.Yaml.Include (decodeFileEither)
import System.Directory.Extra
import System.FilePath
-- | A collection of 'Program's.
-- Optionally may include a source attribute of the file the config was created from.
data Config = Config {
source :: Maybe FilePath,
programs :: [Program]
}
deriving (Eq, Show)
instance FromJSON Config where
parseJSON (Object obj) = Config Nothing <$> mapM (\(key, value) ->
parseJSON value >>= \program -> return program { name = key }
) (toList obj)
parseJSON value = typeMismatch "Config" value
-- | An empty config (no source or programs).
emptyConfig :: Config
emptyConfig = Config Nothing []
-- | Reads a config from the given file path if possible.
-- If an error occurs it is logged using 'logDebugN'.
readConfig :: (MonadIO m, MonadLogger m) => FilePath -> m (Maybe Config)
readConfig filePath = liftIO (decodeFileEither filePath) >>= \ethr -> case ethr of
Left error -> do
logDebugN . pack $ filePath ++ ": error\n" ++ prettyPrintParseException error
return Nothing
Right config -> return $ Just config { source = Just filePath }
-- | Finds the nearest config file by searching from the given directory upwards.
--
-- TODO (hjw): fix the bug where it won't search the root directory.
nearestConfigFile :: MonadIO m => FilePath -> m (Maybe FilePath)
nearestConfigFile dir = findM (liftIO . doesFileExist) $ map (</> defaultFileName) parents
where
parents = takeWhile (\dir -> dir /= takeDrive dir) (iterate takeDirectory dir)
-- | The file name of the default config, '.omnifmt.yaml'.
defaultFileName :: FilePath
defaultFileName = ".omnifmt.yaml"
-- | Attempts to find a 'Program' for the given extension.
-- Programs are searched in order as provided by the 'Config' and the first match will be
-- returned.
programFor :: Config -> Text -> Maybe Program
programFor config ext = find (\program -> ext `elem` extensions program) (programs config)
-- | @fromJust . programFor@
unsafeProgramFor :: Config -> Text -> Program
unsafeProgramFor config = fromJust . programFor config
-- | Checks if the given extension is supported (i.e., there is a 'Program' for it).
supported :: Config -> Text -> Bool
supported config = isJust . programFor config
-- | A program has a semantic name, associated extensions and formatting command.
-- The command string may contain variables, denoted by strings surrounded with '{{..}}'.
-- The command should return a 0 exit code for success, or a non-0 exit code for failure.
data Program = Program {
name :: Text, -- ^ A semantic name (has no impact on formatting).
extensions :: [Text], -- ^ A list of extensions, without a period prefix.
command :: Text -- ^ A command to run in a shell that prettifies an input file and
-- writes to an output file.
}
deriving (Eq, Show)
instance FromJSON Program where
parseJSON (Object obj) = Program "" <$> obj .: "extensions" <*> obj .: "command"
parseJSON value = typeMismatch "Program" value
-- | The empty program (no extensions and the command always fails).
emptyProgram :: Program
emptyProgram = Program "" [] "false"
-- | Substitutes the mapping throughout the command.
-- The mapping is a tuple of @(variable, value)@.
-- Values given are quoted and have any backslashes and double quotaiton marks escaped.
substitute :: Text -> [(Text, Text)] -> Text
substitute = foldr (uncurry replace . second (quote . escape))
where
quote = cons '"' . (`snoc` '"')
escape = replace (pack "\"") (pack "\\\"") . replace (pack "\\") (pack "\\\\")
-- | Checks whether the text uses the input variable ('inputVariableName').
usesInputVariable :: Text -> Bool
usesInputVariable = isInfixOf inputVariableName
-- | Checks whether the text uses the output variable ('outputVariableName').
usesOutputVariable :: Text -> Bool
usesOutputVariable = isInfixOf outputVariableName
-- | The input variable name, '{{input}}'.
inputVariableName :: Text
inputVariableName = "{{input}}"
-- | The output variable name, '{{output}}'.
outputVariableName :: Text
outputVariableName = "{{output}}"