ini 0.4.2 → 0.5.1
raw patch · 5 files changed
+238/−91 lines, 5 filesdep +QuickChecknew-uploaderPVP ok
version bump matches the API change (PVP)
Dependencies added: QuickCheck
API changes (from Hackage documentation)
+ Data.Ini: instance GHC.Enum.Bounded Data.Ini.KeySeparator
+ Data.Ini: instance GHC.Enum.Enum Data.Ini.KeySeparator
Files
- CHANGELOG.md +24/−0
- README.md +1/−1
- ini.cabal +21/−20
- src/Data/Ini.hs +11/−6
- test/Main.hs +181/−64
CHANGELOG.md view
@@ -1,3 +1,27 @@+## 0.5.1++_2025-12-20, Jan Hrček_++- Fixed a parser bug where parsing would fail in presence of comments at the end of file+[#12](https://github.com/jhrcek/ini/pull/12)++## 0.5.0++_2023-02-08, Chris Martin_++The behavior of `(<>)` for the `Ini` type has changed+[#2](https://github.com/andreasabel/ini/issues/2)++- `<>` previously discarded all `iniGlobals`. Now it concatenates+ the globals from the two `Ini` values.++- When two `Ini` values contained `iniSections` with the same name,+ `<>` previously returned the section from the left value and+ discarded the section of the same name from the right value.+ Now it concatenates the sections of the same name.++Tested with GHC 7.0 - ghc-9.6.0.20230128.+ ## 0.4.2 _2022-07-26, Andreas Abel_
README.md view
@@ -1,7 +1,7 @@ [](https://hackage.haskell.org/package/ini) [](https://stackage.org/nightly/package/ini) [](https://www.stackage.org/package/ini)-[](https://github.com/andreasabel/ini/actions/workflows/haskell.yml)+[](https://github.com/jhrcek/ini/actions/workflows/haskell-ci.yml) ini ===
ini.cabal view
@@ -1,34 +1,32 @@ cabal-version: >= 1.10 name: ini-version: 0.4.2+version: 0.5.1 synopsis: Configuration files in the INI format. description: Quick and easy configuration files in the INI format. license: BSD3 license-file: LICENSE author: Chris Done-maintainer: Andreas Abel-homepage: https://github.com/andreasabel/ini-bug-reports: https://github.com/andreasabel/ini/issues+maintainer: Jan Hrček+homepage: https://github.com/jhrcek/ini+bug-reports: https://github.com/jhrcek/ini/issues copyright: 2013 Chris Done category: Data, Configuration build-type: Simple tested-with:- GHC == 9.4.1- GHC == 9.2.3- GHC == 9.0.2- GHC == 8.10.7- GHC == 8.8.4- GHC == 8.6.5- GHC == 8.4.4- GHC == 8.2.2- GHC == 8.0.2- GHC == 7.10.3- GHC == 7.8.4- GHC == 7.6.3- GHC == 7.4.2- GHC == 7.2.2- GHC == 7.0.4+ GHC == 9.12.2+ || == 9.10.3+ || == 9.8.4+ || == 9.6.7+ || == 9.4.8+ || == 9.2.8+ || == 9.0.2+ || == 8.10.7+ || == 8.8.4+ || == 8.6.5+ || == 8.4.4+ || == 8.2.2+ || == 8.0.2 extra-source-files: CHANGELOG.md@@ -56,13 +54,16 @@ type: exitcode-stdio-1.0 hs-source-dirs: test main-is: Main.hs+ ghc-options: -Wall build-depends: base >= 4 && <5+ , QuickCheck , ini , hspec+ , text , unordered-containers default-language: Haskell98 source-repository head type: git- location: https://github.com/andreasabel/ini.git+ location: https://github.com/jhrcek/ini.git
src/Data/Ini.hs view
@@ -1,5 +1,4 @@-{-# LANGUAGE GeneralizedNewtypeDeriving #-}-{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE OverloadedStrings #-} -- | Clean configuration files in the INI format. --@@ -89,8 +88,13 @@ } deriving (Show, Eq) +-- | '<>' concatenates the lists of entries within each section (since @ini-0.5.0@) instance Semigroup Ini where- x <> y = Ini {iniGlobals = mempty, iniSections = iniSections x <> iniSections y}+ x <> y =+ Ini+ { iniGlobals = iniGlobals x ++ iniGlobals y+ , iniSections = M.unionWith (++) (iniSections x) (iniSections y)+ } instance Monoid Ini where mempty = Ini {iniGlobals = mempty, iniSections = mempty}@@ -209,7 +213,7 @@ data KeySeparator = ColonKeySeparator | EqualsKeySeparator- deriving (Eq, Show)+ deriving (Bounded, Enum, Eq, Show) -- | Settings determining how an INI file is written. data WriteIniSettings = WriteIniSettings@@ -229,8 +233,8 @@ -- | Print an INI config. printIniWith :: WriteIniSettings -> Ini -> Text printIniWith wis i =- T.concat $ (map buildPair (iniGlobals i)) ++- (map buildSection (M.toList (iniSections i)))+ T.concat $ map buildPair (iniGlobals i) +++ map buildSection (M.toList (iniSections i)) where buildSection (name,pairs) = "[" <> name <> "]\n" <> T.concat (map buildPair pairs)@@ -246,6 +250,7 @@ (\kv secs -> Ini {iniSections = M.fromList secs, iniGlobals = kv}) <$> many keyValueParser <*> many sectionParser <*+ skipComments <* (endOfInput <|> (fail . T.unpack =<< takeWhile (not . isControl))) -- | A section. Format: @[foo]@. Conventionally, @[FOO]@.
test/Main.hs view
@@ -1,74 +1,191 @@ {-# LANGUAGE OverloadedStrings #-}+{-# OPTIONS_GHC -Wno-orphans #-} -- | Simple test suite. module Main where +import Data.Char (isControl, isSpace) import qualified Data.HashMap.Strict as HM-import Data.Ini-import Test.Hspec+import Data.Ini (Ini (..), KeySeparator (..), WriteIniSettings (..), parseIni, printIniWith)+import Data.Text (Text)+import qualified Data.Text as T+import Test.Hspec (describe, hspec, it, shouldBe)+import Test.QuickCheck main :: IO () main =- hspec- (do describe- "Regular files"- (do it- "Multi-section file with comments"- (shouldBe- (parseIni- "# Some comment.\n\- \[SERVER]\n\- \port=6667\n\- \hostname=localhost\n\- \[AUTH]\n\- \user=hello\n\- \pass=world\n\- \# Salt can be an empty string.\n\- \salt=")- (Right- (Ini- { iniSections =- HM.fromList- [ ( "AUTH"- , [ ("user", "hello")- , ("pass", "world")- , ("salt", "")- ])- , ( "SERVER"- , [("port", "6667"), ("hostname", "localhost")])- ]- , iniGlobals = []- })))- it- "File with globals"- (shouldBe- (parseIni- "# Some comment.\n\- \port=6667\n\- \hostname=localhost\n\- \[AUTH]\n\- \user=hello\n\- \pass=world\n\- \# Salt can be an empty string.\n\- \salt=")- (Right- (Ini- { iniSections =- HM.fromList- [ ( "AUTH"- , [ ("user", "hello")- , ("pass", "world")- , ("salt", "")- ])- ]- , iniGlobals =- [("port", "6667"), ("hostname", "localhost")]- })))- it- "File with invalid keys"- (shouldBe- (parseIni- "Name=Foo\n\- \Name[en_GB]=Fubar")- (Left "Failed reading: Name[en_GB]=Fubar"))))+ hspec $ do+ describe "parseIni" $ do+ it "parses multi-section file with comments" $+ parseIni+ "# Some comment.\n\+ \[SERVER]\n\+ \port=6667\n\+ \hostname=localhost\n\+ \[AUTH]\n\+ \user=hello\n\+ \pass=world\n\+ \# Salt can be an empty string.\n\+ \salt="+ `shouldBe` Right+ ( Ini+ { iniSections =+ HM.fromList+ [+ ( "AUTH"+ ,+ [ ("user", "hello")+ , ("pass", "world")+ , ("salt", "")+ ]+ )+ ,+ ( "SERVER"+ , [("port", "6667"), ("hostname", "localhost")]+ )+ ]+ , iniGlobals = []+ }+ )++ it "parses file with globals" $+ parseIni+ "# Some comment.\n\+ \port=6667\n\+ \hostname=localhost\n\+ \[AUTH]\n\+ \user=hello\n\+ \pass=world\n\+ \# Salt can be an empty string.\n\+ \salt="+ `shouldBe` Right+ ( Ini+ { iniSections =+ HM.fromList+ [+ ( "AUTH"+ ,+ [ ("user", "hello")+ , ("pass", "world")+ , ("salt", "")+ ]+ )+ ]+ , iniGlobals =+ [("port", "6667"), ("hostname", "localhost")]+ }+ )++ it "fails to parse file with invalid keys" $+ parseIni+ "Name=Foo\n\+ \Name[en_GB]=Fubar"+ `shouldBe` Left "Failed reading: Name[en_GB]=Fubar"++ it "parses file ending with comments" $+ parseIni+ "[default]\n\+ \a = 1\n\+ \\n\+ \#[staging-PI]\n\+ \#a = 2\n"+ `shouldBe` Right+ ( Ini+ { iniSections = HM.fromList [("default", [("a", "1")])]+ , iniGlobals = []+ }+ )++ it "parses file with globals only" $+ parseIni+ "global1 = hello\n\+ \global2 = 123\n\+ \# An end of file comment here"+ `shouldBe` Right+ ( Ini+ { iniSections = HM.empty+ , iniGlobals = [("global1", "hello"), ("global2", "123")]+ }+ )++ it "parses empty file" $+ parseIni "" `shouldBe` Right mempty++ it "roundtrips with printIniWith" $+ property $ \ini settings ->+ let printed = printIniWith settings ini+ parsed = parseIni printed+ in counterexample (T.unpack printed) $+ parsed === Right ini++genKey :: Gen Text+genKey = do+ firstChar <- arbitrary `suchThat` isValidFirstKeyChar+ restChars <- listOf $ arbitrary `suchThat` isValidKeyChar+ pure $ T.pack (firstChar : restChars)+ where+ isValidKeyChar c = not (c == '=' || c == ':' || c == '[' || c == ']' || isControl c || isSpace c)+ isValidFirstKeyChar c = isValidKeyChar c && c /= ';' && c /= '#'++genSectionName :: Gen Text+genSectionName = do+ chars <- listOf1 $ arbitrary `suchThat` isValidSectionChar+ let name = T.strip $ T.pack chars+ if T.null name+ then genSectionName -- retry if stripping results in empty+ else pure name+ where+ isValidSectionChar c = c /= '[' && c /= ']' && not (isControl c)++genValue :: Gen Text+genValue = do+ chars <- listOf $ arbitrary `suchThat` (not . isControl)+ pure $ T.strip $ T.pack chars++genKeyValue :: Gen (Text, Text)+genKeyValue = (,) <$> genKey <*> genValue++instance Arbitrary KeySeparator where+ arbitrary = arbitraryBoundedEnum+ shrink ColonKeySeparator = []+ shrink EqualsKeySeparator = [ColonKeySeparator]++instance Arbitrary Ini where+ arbitrary = do+ numSections <- choose (0, 2)+ sections <- vectorOf numSections $ do+ name <- genSectionName+ numPairs <- choose (0, 2)+ pairs <- vectorOf numPairs genKeyValue+ pure (name, pairs)+ numGlobals <- choose (0, 3)+ globals <- vectorOf numGlobals genKeyValue+ pure $+ Ini+ { iniSections = HM.fromList sections+ , iniGlobals = globals+ }+ shrink ini =+ -- Shrink by removing sections+ [ ini{iniSections = HM.fromList secs'}+ | secs' <- shrinkList (const []) (HM.toList (iniSections ini))+ ]+ +++ -- Shrink by removing key-value pairs from sections+ [ ini{iniSections = HM.fromList secs'}+ | secs' <- shrinkOne shrinkSection (HM.toList (iniSections ini))+ ]+ +++ -- Shrink by removing globals+ [ ini{iniGlobals = globals'}+ | globals' <- shrinkList (const []) (iniGlobals ini)+ ]+ where+ shrinkSection (name, pairs) = [(name, pairs') | pairs' <- shrinkList (const []) pairs]+ shrinkOne _ [] = []+ shrinkOne f (x : xs) = [x' : xs | x' <- f x] ++ [x : xs' | xs' <- shrinkOne f xs]++instance Arbitrary WriteIniSettings where+ arbitrary = WriteIniSettings <$> arbitrary+ shrink (WriteIniSettings sep) = WriteIniSettings <$> shrink sep