config-ini (empty) → 0.1.0.0
raw patch · 9 files changed
+702/−0 lines, 9 filesdep +QuickCheckdep +basedep +config-inisetup-changed
Dependencies added: QuickCheck, base, config-ini, directory, ini, megaparsec, text, transformers, unordered-containers
Files
- LICENSE +12/−0
- Setup.hs +2/−0
- config-ini.cabal +90/−0
- examples/basic-example/Main.hs +28/−0
- examples/config-example/Main.hs +34/−0
- src/Data/Ini/Config.hs +363/−0
- src/Data/Ini/Config/Raw.hs +90/−0
- test/ini-compat/Main.hs +41/−0
- test/prewritten/Main.hs +42/−0
+ LICENSE view
@@ -0,0 +1,12 @@+Copyright (c) 2016, Getty Ritter+All rights reserved.++Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:++1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.++2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.++3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ config-ini.cabal view
@@ -0,0 +1,90 @@+name: config-ini+version: 0.1.0.0+synopsis: A library for simple INI-based configuration files.+homepage: https://github.com/aisamanra/config-ini+description: The @config-ini@ library is a small monadic language+ for writing simple configuration languages with convenient,+ human-readable error messages.+ .+ > parseConfig :: IniParser (Text, Int, Bool)+ > parseConfig = section "NETWORK" $ do+ > user <- field "user"+ > port <- fieldOf "port" number+ > enc <- fieldFlagDef "encryption" True+ > return (user, port, enc)++license: BSD3+license-file: LICENSE+author: Getty Ritter <gettyritter@gmail.com>+maintainer: Getty Ritter <gettyritter@gmail.com>+copyright: ©2016 Getty Ritter+category: Configuration+build-type: Simple+cabal-version: >= 1.14++source-repository head+ type: git+ location: git://github.com/aisamanra/config-ini.git++flag build-examples+ description: Build example applications+ default: False++library+ hs-source-dirs: src+ exposed-modules: Data.Ini.Config+ , Data.Ini.Config.Raw+ ghc-options: -Wall+ build-depends: base >=4.7 && <4.10+ , text >=1.2.2 && <1.3+ , unordered-containers >=0.2.7 && <0.3+ , transformers >=0.5.2 && <0.6+ , megaparsec >=5.1.2 && <5.2+ default-language: Haskell2010++executable basic-example+ if !flag(build-examples)+ buildable: False+ hs-source-dirs: examples/basic-example+ main-is: Main.hs+ ghc-options: -Wall+ build-depends: base >=4.7 && <4.10+ , text+ , config-ini+ default-language: Haskell2010++executable config-example+ if !flag(build-examples)+ buildable: False+ hs-source-dirs: examples/config-example+ main-is: Main.hs+ ghc-options: -Wall+ build-depends: base >=4.7 && <4.10+ , text+ , config-ini+ default-language: Haskell2010++test-suite test-ini-compat+ type: exitcode-stdio-1.0+ ghc-options: -Wall+ default-language: Haskell2010+ hs-source-dirs: test/ini-compat+ main-is: Main.hs+ build-depends: base+ , ini+ , config-ini+ , QuickCheck+ , unordered-containers+ , text++test-suite test-prewritten+ type: exitcode-stdio-1.0+ ghc-options: -Wall+ default-language: Haskell2010+ hs-source-dirs: test/prewritten+ main-is: Main.hs+ build-depends: base+ , config-ini+ , unordered-containers+ , text+ , directory
+ examples/basic-example/Main.hs view
@@ -0,0 +1,28 @@+{-# LANGUAGE OverloadedStrings #-}++module Main where++import Data.Ini.Config+import Data.Text (Text)++data Config = Config+ { confUsername :: Text+ , confPort :: Int+ , confUseEncryption :: Bool+ } deriving (Eq, Show)++parseConfig :: IniParser Config+parseConfig = section "network" $ do+ user <- field "user"+ port <- fieldOf "port" number+ enc <- fieldFlagDef "encryption" True+ return (Config user port enc)++example :: Text+example = "[NETWORK]\n\+ \user = gdritter\n\+ \port = 8888\n"++main :: IO ()+main = do+ print (parseIniFile example parseConfig)
+ examples/config-example/Main.hs view
@@ -0,0 +1,34 @@+{-# LANGUAGE OverloadedStrings #-}++module Main where++import Data.Ini.Config+import Data.Text (Text)+import qualified Data.Text.IO as T++data Config = Config+ { cfNetwork :: NetworkConfig, cfLocal :: Maybe LocalConfig }+ deriving (Eq, Show)++data NetworkConfig = NetworkConfig+ { netHost :: String, netPort :: Int }+ deriving (Eq, Show)++data LocalConfig = LocalConfig+ { localUser :: Text }+ deriving (Eq, Show)++configParser :: IniParser Config+configParser = do+ netCf <- section "NETWORK" $ do+ host <- fieldOf "host" string+ port <- fieldOf "port" number+ return NetworkConfig { netHost = host, netPort = port }+ locCf <- sectionMb "LOCAL" $+ LocalConfig <$> field "user"+ return Config { cfNetwork = netCf, cfLocal = locCf }++main :: IO ()+main = do+ rs <- T.getContents+ print (parseIniFile rs configParser)
+ src/Data/Ini/Config.hs view
@@ -0,0 +1,363 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# OPTIONS_GHC -fno-warn-redundant-constraints #-}++module Data.Ini.Config+(+-- $main+-- * Running Parsers+ parseIniFile+-- * Parser Types+, IniParser+, SectionParser+-- * Section-Level Parsing+, section+, sectionMb+, sectionDef+-- * Field-Level Parsing+, field+, fieldOf+, fieldMb+, fieldMbOf+, fieldDef+, fieldDefOf+, fieldFlag+, fieldFlagDef+-- * Reader Functions+, readable+, number+, string+, flag+) where++import Control.Monad.Trans.Except+import qualified Data.HashMap.Strict as HM+import Data.Ini.Config.Raw+import Data.String (IsString(..))+import Data.Text (Text)+import qualified Data.Text as T+import Data.Typeable (Typeable, Proxy(..), typeRep)+import Text.Read (readMaybe)++addLineInformation :: Int -> Text -> StParser s a -> StParser s a+addLineInformation lineNo sec = withExceptT go+ where go e = "Line " ++ show lineNo +++ ", in section " ++ show sec +++ ": " ++ e++type StParser s a = ExceptT String ((->) s) a++-- | An 'IniParser' value represents a computation for parsing entire+-- INI-format files.+newtype IniParser a = IniParser (StParser Ini a)+ deriving (Functor, Applicative, Monad)++-- | A 'SectionParser' value represents a computation for parsing a single+-- section of an INI-format file.+newtype SectionParser a = SectionParser (StParser IniSection a)+ deriving (Functor, Applicative, Monad)++-- | Parse a 'Text' value as an INI file and run an 'IniParser' over it+parseIniFile :: Text -> IniParser a -> Either String a+parseIniFile text (IniParser mote) = do+ ini <- parseIni text+ runExceptT mote ini++-- | Find a named section in the INI file and parse it with the provided+-- section parser, failing if the section does not exist. In order to+-- support classic INI files with capitalized section names, section+-- lookup is __case-insensitive__.+--+-- >>> parseIniFile "[ONE]\nx = hello\n" $ section "ONE" (field "x")+-- Right "hello"+-- >>> parseIniFile "[ONE]\nx = hello\n" $ section "TWO" (field "x")+-- Left "No top-level section named \"TWO\""+section :: Text -> SectionParser a -> IniParser a+section name (SectionParser thunk) = IniParser $ ExceptT $ \(Ini ini) ->+ case HM.lookup (T.toLower name) ini of+ Nothing -> Left ("No top-level section named " ++ show name)+ Just sec -> runExceptT thunk sec++-- | Find a named section in the INI file and parse it with the provided+-- section parser, returning 'Nothing' if the section does not exist.+-- In order to+-- support classic INI files with capitalized section names, section+-- lookup is __case-insensitive__.+--+-- >>> parseIniFile "[ONE]\nx = hello\n" $ sectionMb "ONE" (field "x")+-- Right (Just "hello")+-- >>> parseIniFile "[ONE]\nx = hello\n" $ sectionMb "TWO" (field "x")+-- Right Nothing+sectionMb :: Text -> SectionParser a -> IniParser (Maybe a)+sectionMb name (SectionParser thunk) = IniParser $ ExceptT $ \(Ini ini) ->+ case HM.lookup (T.toLower name) ini of+ Nothing -> return Nothing+ Just sec -> Just `fmap` runExceptT thunk sec++-- | Find a named section in the INI file and parse it with the provided+-- section parser, returning a default value if the section does not exist.+-- In order to+-- support classic INI files with capitalized section names, section+-- lookup is __case-insensitive__.+--+-- >>> parseIniFile "[ONE]\nx = hello\n" $ sectionDef "ONE" "def" (field "x")+-- Right "hello"+-- >>> parseIniFile "[ONE]\nx = hello\n" $ sectionDef "TWO" "def" (field "x")+-- Right "def"+sectionDef :: Text -> a -> SectionParser a -> IniParser a+sectionDef name def (SectionParser thunk) = IniParser $ ExceptT $ \(Ini ini) ->+ case HM.lookup (T.toLower name) ini of+ Nothing -> return def+ Just sec -> runExceptT thunk sec++---++throw :: String -> StParser s a+throw msg = ExceptT $ (\ _ -> Left msg)++getSectionName :: StParser IniSection Text+getSectionName = ExceptT $ (\ m -> return (isName m))++rawFieldMb :: Text -> StParser IniSection (Maybe IniValue)+rawFieldMb name = ExceptT $ \m ->+ return (HM.lookup name (isVals m))++rawField :: Text -> StParser IniSection IniValue+rawField name = do+ sec <- getSectionName+ valMb <- rawFieldMb name+ case valMb of+ Nothing -> throw ("Missing field " ++ show name +++ " in section " ++ show sec)+ Just x -> return x++-- | Retrieve a field, failing if it doesn't exist, and return its raw value.+--+-- >>> parseIniFile "[MAIN]\nx = hello\n" $ section "MAIN" (field "x")+-- Right "hello"+-- >>> parseIniFile "[MAIN]\nx = hello\n" $ section "MAIN" (field "y")+-- Left "Missing field \"y\" in section \"main\""+field :: Text -> SectionParser Text+field name = SectionParser $ vValue `fmap` rawField name++-- | Retrieve a field and use the supplied parser to parse it as a value,+-- failing if the field does not exist, or if the parser fails to+-- produce a value.+--+-- >>> parseIniFile "[MAIN]\nx = 72\n" $ section "MAIN" (fieldOf "x" number)+-- Right 72+-- >>> parseIniFile "[MAIN]\nx = hello\n" $ section "MAIN" (fieldOf "x" number)+-- Left "Line 2, in section \"MAIN\": Unable to parse \"hello\" as a value of type Integer"+-- >>> parseIniFile "[MAIN]\nx = 72\n" $ section "MAIN" (fieldOf "y" number)+-- Left "Missing field \"y\" in section \"MAIN\""+fieldOf :: Text -> (Text -> Either String a) -> SectionParser a+fieldOf name parse = SectionParser $ do+ sec <- getSectionName+ val <- rawField name+ case parse (vValue val) of+ Left err -> addLineInformation (vLineNo val) sec (throw err)+ Right x -> return x++-- | Retrieve a field, returning a @Nothing@ value if it does not exist.+--+-- >>> parseIniFile "[MAIN]\nx = hello\n" $ section "MAIN" (fieldMb "x")+-- Right (Just "hello")+-- >>> parseIniFile "[MAIN]\nx = hello\n" $ section "MAIN" (fieldMb "y")+-- Right Nothing+fieldMb :: Text -> SectionParser (Maybe Text)+fieldMb name = SectionParser $ fmap vValue `fmap` rawFieldMb name++-- | Retrieve a field and parse it according to the given parser, returning+-- @Nothing@ if it does not exist. If the parser fails, then this will+-- fail.+--+-- >>> parseIniFile "[MAIN]\nx = 72\n" $ section "MAIN" (fieldMbOf "x" number)+-- Right 72+-- >>> parseIniFile "[MAIN]\nx = hello\n" $ section "MAIN" (fieldMbOf "x" number)+-- Left "Line 2, in section \"MAIN\": Unable to parse \"hello\" as a value of type Integer"+-- >>> parseIniFile "[MAIN]\nx = 72\n" $ section "MAIN" (fieldMbOf "y" number)+-- Right Nothing+fieldMbOf :: Text -> (Text -> Either String a) -> SectionParser (Maybe a)+fieldMbOf name parse = SectionParser $ do+ sec <- getSectionName+ mb <- rawFieldMb name+ case mb of+ Nothing -> return Nothing+ Just v -> case parse (vValue v) of+ Left err -> addLineInformation (vLineNo v) sec (throw err)+ Right x -> return (Just x)++-- | Retrieve a field and supply a default value for if it doesn't exist.+--+-- >>> parseIniFile "[MAIN]\nx = hello\n" $ section "MAIN" (fieldDef "x" "def")+-- Right "hello"+-- >>> parseIniFile "[MAIN]\nx = hello\n" $ section "MAIN" (fieldDef "y" "def")+-- Right "def"+fieldDef :: Text -> Text -> SectionParser Text+fieldDef name def = SectionParser $ ExceptT $ \m ->+ case HM.lookup name (isVals m) of+ Nothing -> return def+ Just x -> return (vValue x)++-- | Retrieve a field, parsing it according to the given parser, and returning+-- a default value if it does not exist. If the parser fails, then this will+-- fail.+--+-- >>> parseIniFile "[MAIN]\nx = 72\n" $ section "MAIN" (fieldDefOf "x" number 99)+-- Right 72+-- >>> parseIniFile "[MAIN]\nx = hello\n" $ section "MAIN" (fieldDefOf "x" number 99)+-- Left "Line 2, in section \"MAIN\": Unable to parse \"hello\" as a value of type Integer"+-- >>> parseIniFile "[MAIN]\nx = 72\n" $ section "MAIN" (fieldDefOf "y" number 99)+-- Right 99+fieldDefOf :: Text -> (Text -> Either String a) -> a -> SectionParser a+fieldDefOf name parse def = SectionParser $ do+ sec <- getSectionName+ mb <- rawFieldMb name+ case mb of+ Nothing -> return def+ Just v -> case parse (vValue v) of+ Left err -> addLineInformation (vLineNo v) sec (throw err)+ Right x -> return x++-- | Retrieve a field and treat it as a boolean, failing if it+-- does not exist.+--+-- >>> parseIniFile "[MAIN]\nx = yes\n" $ section "MAIN" (fieldFlag "x")+-- Right True+-- >>> parseIniFile "[MAIN]\nx = yes\n" $ section "MAIN" (fieldFlag "y")+-- Left "Missing field \"y\" in section \"main\""+fieldFlag :: Text -> SectionParser Bool+fieldFlag name = fieldOf name flag++-- | Retrieve a field and treat it as a boolean, subsituting+-- a default value if it doesn't exist.+--+-- >>> parseIniFile "[MAIN]\nx = yes\n" $ section "MAIN" (fieldFlagDef "x" False)+-- Right True+-- >>> parseIniFile "[MAIN]\nx = hello\n" $ section "MAIN" (fieldFlagDef "x" False)+-- Left "Line 2, in section \"MAIN\": Unable to parse \"hello\" as a boolean"+-- >>> parseIniFile "[MAIN]\nx = yes\n" $ section "MAIN" (fieldFlagDef "y" False)+-- Right False+fieldFlagDef :: Text -> Bool -> SectionParser Bool+fieldFlagDef name def = fieldDefOf name flag def++---++-- | Try to use the "Read" instance for a type to parse a value, failing+-- with a human-readable error message if reading fails.+--+-- >>> readable "(5, 7)" :: Either String (Int, Int)+-- Right (5, 7)+-- >>> readable "hello" :: Either String (Int, Int)+-- Left "Unable to parse \"hello\" as a value of type (Int,Int)"+readable :: forall a. (Read a, Typeable a) => Text -> Either String a+readable t = case readMaybe str of+ Just v -> Right v+ Nothing -> Left ("Unable to parse " ++ show str +++ " as a value of type " ++ show typ)+ where str = T.unpack t+ typ = typeRep prx+ prx :: Proxy a+ prx = Proxy++-- | Try to use the "Read" instance for a numeric type to parse a value,+-- failing with a human-readable error message if reading fails.+--+-- >>> number "5" :: Either String Int+-- Right 5+-- >>> number "hello" :: Either String Int+-- Left "Unable to parse \"hello\" as a value of type Int"+number :: (Num a, Read a, Typeable a) => Text -> Either String a+number = readable++-- | Convert a textua value to the appropriate string type. This will+-- never fail.+--+-- >>> string "foo" :: Either String String+-- Right "foo"+string :: (IsString a) => Text -> Either String a+string = return . fromString . T.unpack++-- | Convert a string that represents a boolean to a proper boolean. This+-- is case-insensitive, and matches the words @true@, @false@, @yes@,+-- @no@, as well as single-letter abbreviations for all of the above.+-- If the input does not match, then this will fail with a human-readable+-- error message.+--+-- >>> flag "TRUE"+-- Right True+-- >>> flag "y"+-- Right True+-- >>> flag "no"+-- Right False+-- >>> flag "F"+-- Right False+-- >>> flag "That's a secret!"+-- Left "Unable to parse \"that's a secret!\" as a boolean"+flag :: Text -> Either String Bool+flag s = case T.toLower s of+ "true" -> Right True+ "yes" -> Right True+ "t" -> Right True+ "y" -> Right True+ "false" -> Right False+ "no" -> Right False+ "f" -> Right False+ "n" -> Right False+ _ -> Left ("Unable to parse " ++ show s ++ " as a boolean")+++-- $main+-- The 'config-ini' library exports some simple monadic functions to+-- make parsing INI-like configuration easier. INI files have a+-- two-level structure: the top-level named chunks of configuration,+-- and the individual key-value pairs contained within those chunks.+-- For example, the following INI file has two sections, @NETWORK@+-- and @LOCAL@, and each contains its own key-value pairs. Comments,+-- which begin with @#@ or @;@, are ignored:+--+-- > [NETWORK]+-- > host = example.com+-- > port = 7878+-- >+-- > # here is a comment+-- > [LOCAL]+-- > user = terry+--+-- The combinators provided here are designed to write quick and+-- idiomatic parsers for files of this form. Sections are parsed by+-- 'IniParser' computations, like 'section' and its variations,+-- while the fields within sections are parsed by 'SectionParser'+-- computations, like 'field' and its variations. If we want to+-- parse an INI file like the one above, treating the entire+-- @LOCAL@ section as optional, we can write it like this:+--+-- > data Config = Config+-- > { cfNetwork :: NetworkConfig, cfLocal :: Maybe LocalConfig }+-- > deriving (Eq, Show)+-- >+-- > data NetworkConfig = NetworkConfig+-- > { netHost :: String, netPort :: Int }+-- > deriving (Eq, Show)+-- >+-- > data LocalConfig = LocalConfig+-- > { localUser :: Text }+-- > deriving (Eq, Show)+-- >+-- > configParser :: IniParser Config+-- > configParser = do+-- > netCf <- section "NETWORK" $ do+-- > host <- fieldOf "host" string+-- > port <- fieldOf "port" number+-- > return NetworkConfig { netHost = host, netPort = port }+-- > locCf <- sectionMb "LOCAL" $+-- > LocalConfig <$> field "user"+-- > return Config { cfNetwork = netCf, cfLocal = locCf }+--+-- We can run our computation with 'parseIniFile', which,+-- when run on our example file above, would produce the+-- following:+--+-- >>> parseIniFile example configParser+-- Right (Config {cfNetwork = NetworkConfig {netHost = "example.com", netPort = 7878}, cfLocal = Just (LocalConfig {localUser = "terry"})})
+ src/Data/Ini/Config/Raw.hs view
@@ -0,0 +1,90 @@+module Data.Ini.Config.Raw+( Ini(..)+, IniSection(..)+, IniValue(..)+, parseIni+) where++import Control.Monad (void)+import Data.HashMap.Strict (HashMap)+import qualified Data.HashMap.Strict as HM+import Data.Text (Text)+import qualified Data.Text as T+import Text.Megaparsec+import Text.Megaparsec.Text++-- | An 'Ini' value is a mapping from section names to+-- 'IniSection' values.+newtype Ini+ = Ini { fromIni :: HashMap Text IniSection }+ deriving (Eq, Show)++-- | An 'IniSection' consists of a name, a mapping of key-value pairs,+-- and metadata about where the section starts and ends in the file.+data IniSection = IniSection+ { isName :: Text+ , isVals :: HashMap Text IniValue+ , isStartLine :: Int+ , isEndLine :: Int+ } deriving (Eq, Show)++-- | An 'IniValue' represents a key-value mapping, and also stores the+-- line number where it appears.+data IniValue = IniValue+ { vLineNo :: Int+ , vName :: Text+ , vValue :: Text+ } deriving (Eq, Show)++-- | Parse a 'Text' value into an 'Ini' value.+parseIni :: Text -> Either String Ini+parseIni t = case runParser pIni "ini file" t of+ Left err -> Left (parseErrorPretty err)+ Right v -> Right v++pIni :: Parser Ini+pIni = sBlanks *> (go `fmap` (many (pSection <?> "section") <* eof))+ where go vs = Ini $ HM.fromList [ (T.toLower (isName v), v)+ | v <- vs+ ]++sBlanks :: Parser ()+sBlanks = skipMany (void eol <|> sComment)++sComment :: Parser ()+sComment = do+ void (oneOf ";#")+ void (manyTill anyChar eol)++pSection :: Parser IniSection+pSection = do+ start <- getCurrentLine+ void (char '[')+ name <- T.pack `fmap` some (noneOf "[]")+ void (char ']')+ sBlanks+ vals <- many (pPair <?> "key-value pair")+ end <- getCurrentLine+ sBlanks+ return IniSection+ { isName = T.strip name+ , isVals = HM.fromList [ (vName v, v) | v <- vals ]+ , isStartLine = start+ , isEndLine = end+ }++pPair :: Parser IniValue+pPair = do+ pos <- getCurrentLine+ key <- T.pack `fmap` some (noneOf "[]=:")+ void (oneOf ":=")+ val <- T.pack `fmap` manyTill anyChar eol+ sBlanks+ return IniValue+ { vLineNo = pos+ , vName = T.strip key+ , vValue = T.strip val+ }++getCurrentLine :: Parser Int+getCurrentLine = (fromIntegral . unPos . sourceLine) `fmap` getPosition
+ test/ini-compat/Main.hs view
@@ -0,0 +1,41 @@+module Main where++import Data.Char+import Data.HashMap.Strict (HashMap)+import qualified Data.HashMap.Strict as HM+import qualified Data.Ini as I1+import qualified Data.Ini.Config.Raw as I2+import Data.Text (Text)+import qualified Data.Text as T++import Test.QuickCheck++iniEquiv :: I1.Ini -> Bool+iniEquiv raw = case (i1, i2) of+ (Right i1', Right i2') ->+ let i1'' = lower i1'+ i2'' = toMaps i2'+ in i1'' == i2''+ _ -> False+ where pr = I1.printIniWith I1.defaultWriteIniSettings raw+ i2 = I2.parseIni pr+ i1 = I1.parseIni pr++lower :: I1.Ini -> HashMap Text (HashMap Text Text)+lower (I1.Ini hm) =+ HM.fromList [ (T.toLower k, v) | (k, v) <- HM.toList hm ]++toMaps :: I2.Ini -> HashMap Text (HashMap Text Text)+toMaps (I2.Ini m) = fmap (fmap I2.vValue . I2.isVals) m++instance Arbitrary I1.Ini where+ arbitrary = (I1.Ini . HM.fromList) <$> listOf sections+ where sections = (,) <$> str <*> section+ str = (T.pack <$> arbitrary) `suchThat` (\ t ->+ T.all (\ c -> isAlphaNum c || c == ' ')+ t && not (T.null t))+ section = HM.fromList <$> listOf kv+ kv = (,) <$> str <*> str++main :: IO ()+main = quickCheck iniEquiv
+ test/prewritten/Main.hs view
@@ -0,0 +1,42 @@+module Main where++import Data.List+import Data.Ini.Config.Raw+import Data.HashMap.Strict (HashMap)+import Data.Text (Text)+import qualified Data.Text.IO as T+import System.Directory+import System.Exit++dir :: FilePath+dir = "test/prewritten/cases"++main :: IO ()+main = do+ files <- getDirectoryContents dir+ let inis = [ f | f <- files+ , ".ini" `isSuffixOf` f+ ]+ mapM_ runTest inis++toMaps :: Ini -> HashMap Text (HashMap Text Text)+toMaps (Ini m) = fmap (fmap vValue . isVals) m++runTest :: FilePath -> IO ()+runTest iniF = do+ let hsF = take (length iniF - 4) iniF ++ ".hs"+ ini <- T.readFile (dir ++ "/" ++ iniF)+ hs <- readFile (dir ++ "/" ++ hsF)+ case parseIni ini of+ Left err -> do+ putStrLn ("Error parsing " ++ iniF)+ putStrLn err+ exitFailure+ Right x+ | toMaps x == read hs -> do+ putStrLn ("Passed: " ++ iniF)+ | otherwise -> do+ putStrLn ("Parses do not match for " ++ iniF)+ putStrLn ("Expected: " ++ hs)+ putStrLn ("Actual: " ++ show (toMaps x))+ exitFailure