module Data.Ini.List
(
Section,
Config,
OptionName,
SectionName,
Option,
SectionItem,
ConfigItem,
UpdateValue,
UpdateOption,
Value(value, getValue),
config,
setDefault,
(<+), (+>),
toList,
fromList,
get,
getDefault,
getSection,
getSections,
getSectionsBy,
updateValues,
updateDefaultValues,
updateSectionValues,
updateOptions,
updateDefaultOptions,
updateSectionOptions,
formatConfig,
writeConfig,
writeConfigFile,
hWriteConfig,
parseConfig,
parseFile,
parseFileEx
) where
import Control.Applicative ((<$>), (<|>), (<*>), (<*), (*>), Applicative,
many, optional, pure, some)
import qualified Data.AList as AL
import Data.Bifunctor (second)
import Data.Char (isPrint, isSpace, toLower)
import Data.Maybe (fromMaybe)
import Data.List (isPrefixOf, intercalate)
import Data.Monoid (Monoid, mempty, mappend, (<>))
import Safe (readMay, headMay)
import System.IO
import Text.Trifecta ((<?>), char, CharParsing, eof, manyTill, newline, oneOf,
option, parseFromFile, parseFromFileEx, parseString,
Result, runUnlined, satisfy, sepEndBy, sepEndBy1,
TokenParsing, try, whiteSpace)
data Config = Config Section (AL.AList SectionName Section) deriving (Show)
type OptionName = String
type SectionName = String
type Option = String
type Section = AL.AList OptionName Option
type SectionItem = (OptionName, Option)
type ConfigItem = (SectionName, Section)
type UpdateValue = SectionName -> OptionName -> Option -> Maybe Option
type UpdateOption = SectionName -> OptionName -> Option -> Maybe SectionItem
class Value a where
getValue :: OptionName -> Section -> Maybe a
getValue o s = value <$> getOption o s
value :: Option -> a
instance Value String where
value v = v
instance Value Bool where
value v = value' $ map toLower v
where value' v' | elem v' ["1", "yes", "on", "enabled", "true"] = True
| elem v' ["0", "no", "off", "disabled", "false"] = False
value' v' = error $ "couldn't parse '" ++ v' ++ "' as Bool"
instance Value t => Value [t] where
getValue o s = Just . map value $ getOptions o s
value o = [value o]
instance Read t => Value t where
value v = fromMaybe (error $ "couldn't parse '" ++ v ++ "'") $ readMay v
class Cons container item | container -> item where
fromList :: [item] -> container
toList :: container -> [item]
cons :: item -> container -> container
(+>) :: item -> container -> container
(+>) = cons
(<+) :: container -> item -> container
(<+) = flip (+>)
infixl 7 <+
infixr 7 +>
instance Cons Section SectionItem where
fromList = AL.fromList
toList = AL.toList
cons i os = os <> AL.fromList [i]
instance Cons Config ConfigItem where
fromList l = Config (AL.fromList []) (AL.fromList l)
toList (Config _ sl) = AL.toList sl
cons i (Config d sl)= Config d $ sl <> AL.fromList [i]
instance Monoid Config where
mempty = Config (AL.fromList []) $ AL.fromList []
mappend (Config ad al) (Config bd bl) = Config (ad <> bd) $ al <> bl
config :: Section -> [ConfigItem] -> Config
config s l = Config s $ AL.fromList l
setDefault :: Config -> Section -> Config
setDefault (Config _ sl) s = Config s sl
getDefault :: Config -> Section
getDefault (Config d _) = d
getSections :: Config -> SectionName -> [Section]
getSections (Config _ sl) n = AL.lookupAll n sl
getSection :: Config -> SectionName -> Maybe Section
getSection c n = headMay $ getSections c n
getSectionsBy :: Config -> (SectionName -> Bool) -> [ConfigItem]
getSectionsBy (Config _ sl) sel = filter (\(n, _) -> sel n) $ AL.toList sl
get :: Value a => Config -> Maybe SectionName -> OptionName -> Maybe a
get c s o = get' s >>= getValue o where
get' (Just n) = getSection c n
get' Nothing = Just $ getDefault c
updateOptions :: UpdateOption -> Config -> Config
updateOptions f (Config d sl) =
Config d . AL.fromList . map (\(n, s) -> (n, updateSectionOptions (f n) s))
$ AL.toList sl
updateValues :: UpdateValue -> Config -> Config
updateValues f (Config d sl) =
Config d . AL.fromList . map (\(n, s) -> (n, updateSectionValues (f n) s))
$ AL.toList sl
updateDefaultOptions :: (OptionName -> Option -> Maybe SectionItem)
-> Config
-> Config
updateDefaultOptions f (Config d sl) = Config (updateSectionOptions f d) sl
updateDefaultValues :: (OptionName -> Option -> Maybe Option) -> Config -> Config
updateDefaultValues f (Config d sl) = Config (updateSectionValues f d) sl
updateSectionOptions :: (OptionName -> Option -> Maybe SectionItem)
-> Section
-> Section
updateSectionOptions f os = AL.fromList . map (updateOption f) $ AL.toList os where
updateOption f o@(n, v) = fromMaybe o $ f n v
updateSectionValues :: (OptionName -> Option -> Maybe Option) -> Section -> Section
updateSectionValues f s = updateSectionOptions (updateValue f) s where
updateValue f n o = (n ,) <$> f n o
getOptions :: OptionName -> Section -> [Option]
getOptions k os = map snd . filter (\(n, _) -> n == k) $ AL.toList os
getOption :: OptionName -> Section -> Maybe Option
getOption os n = AL.lookupFirst os n
formatOption :: SectionItem -> String
formatOption (name, val) = name ++ "=" ++ val
formatSection :: ConfigItem -> String
formatSection (name, section) =
unlines $ ("[" ++ name ++ "]") : (map formatOption $ AL.toList section)
formatConfig :: Config -> String
formatConfig (Config options sections) =
intercalate "\n" $
(unlines . map formatOption $ AL.toList options)
:(map formatSection $ AL.toList sections)
writeConfigFile :: FilePath -> Config -> IO ()
writeConfigFile f c = writeFile f $ formatConfig c
hWriteConfig :: Handle -> Config -> IO ()
hWriteConfig h c = hPutStr h $ formatConfig c
writeConfig :: Config -> IO ()
writeConfig = hWriteConfig stdout
parseConfig :: String -> Result Config
parseConfig = parseString parser mempty
parseFile :: FilePath -> IO (Maybe Config)
parseFile = parseFromFile parser
parseFileEx :: FilePath -> IO (Result Config)
parseFileEx = parseFromFileEx parser
parser :: (Monad m, TokenParsing m) => m Config
parser = runUnlined
$ optional lineSep *>
(Config <$> (AL.fromList <$> sepEndBy (try optionLine) lineSep)
<*> (AL.fromList <$> many sectionLine))
<* many (char '\0')
<* eof
<?> "config"
sectionLine :: (Monad m, TokenParsing m) => m ConfigItem
sectionLine = (,) <$> sectionName <* lineSep
<*> (AL.fromList <$> sepEndBy1 (try optionLine) lineSep)
<?> "section"
sectionName :: (Monad m, TokenParsing m) => m String
sectionName = whiteSpace
*> char '['
*> ((:) <$> satisfy (\c -> c /= ']' && isPrint c)
<*> manyTill printing (char ']')
<?> "section name")
<* many anyChar
<?> "section start line"
optionLine :: (Monad m, TokenParsing m) => m SectionItem
optionLine = (,) <$> (strip <$> optionName)
<*> (unComment <$> many printing <?> "option value")
<?> "option line"
where
strip = reverse . dropWhile isSpace . reverse
unComment = dropWhile isSpace . reverse . dropWhile isSpace . unComment' ""
unComment' r v | null v = r
| isPrefixOf " ;" v = r
| otherwise = unComment' (head v:r) (tail v)
optionName :: (Monad m, TokenParsing m) => m OptionName
optionName = whiteSpace *> ((:) <$> satisfy (\c -> isPrint c && c /= '[')
<*> manyTill printing (oneOf ":="))
<?> "option name"
lineSep :: (Monad m, TokenParsing m) => m ()
lineSep = some (whiteSpace <* optional (oneOf ";#" <* many anyChar) <* newline)
*> pure ()
<?> "line separator"
printing, anyChar :: CharParsing m => m Char
printing = satisfy isPrint
anyChar = satisfy (/= '\n')