gdritter repos config-ini / 844e2db
config-ini library, working with some basic tests Getty Ritter 7 years ago
13 changed file(s) with 747 addition(s) and 0 deletion(s). Collapse all Expand all
1 *~
2 .cabal-sandbox
3 dist
4 dist-newstyle
5 cabal.sandbox.config
1 Copyright (c) 2016, Getty Ritter
2 All rights reserved.
3
4 Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
5
6 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
7
8 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.
9
10 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.
11
12 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.
1 name: config-ini
2 version: 0.1.0.0
3 synopsis: A library for simple INI-based configuration files.
4 homepage: https://github.com/aisamanra/config-ini
5 description: The @config-ini@ library is a small monadic language
6 for writing simple configuration languages with convenient,
7 human-readable error messages.
8 .
9 > parseConfig :: IniParser (Text, Int, Bool)
10 > parseConfig = section "NETWORK" $ do
11 > user <- field "user"
12 > port <- fieldOf "port" number
13 > enc <- fieldFlagDef "encryption" True
14 > return (user, port, enc)
15
16 license: BSD3
17 license-file: LICENSE
18 author: Getty Ritter <gettyritter@gmail.com>
19 maintainer: Getty Ritter <gettyritter@gmail.com>
20 copyright: ©2016 Getty Ritter
21 category: Configuration
22 build-type: Simple
23 cabal-version: >= 1.14
24
25 flag build-examples
26 description: Build example applications
27 default: False
28
29 library
30 hs-source-dirs: src
31 exposed-modules: Data.Ini.Config
32 , Data.Ini.Raw
33 ghc-options: -Wall
34 build-depends: base >=4.7 && <4.10
35 , text
36 , unordered-containers
37 , transformers
38 , megaparsec
39 default-language: Haskell2010
40
41 executable basic-example
42 if !flag(build-examples)
43 buildable: False
44 hs-source-dirs: basic-example
45 main-is: Main.hs
46 ghc-options: -Wall
47 build-depends: base >=4.7 && <4.10
48 , text
49 , config-ini
50 default-language: Haskell2010
51
52 executable config-example
53 if !flag(build-examples)
54 buildable: False
55 hs-source-dirs: config-example
56 main-is: Main.hs
57 ghc-options: -Wall
58 build-depends: base >=4.7 && <4.10
59 , text
60 , config-ini
61 default-language: Haskell2010
62
63 test-suite test-ini-compat
64 type: exitcode-stdio-1.0
65 ghc-options: -Wall
66 default-language: Haskell2010
67 hs-source-dirs: test/ini-compat
68 main-is: Main.hs
69 build-depends: base
70 , ini
71 , config-ini
72 , QuickCheck
73 , unordered-containers
74 , text
75
76 test-suite test-suite
77 type: exitcode-stdio-1.0
78 ghc-options: -Wall
79 default-language: Haskell2010
80 hs-source-dirs: test/general
81 main-is: Main.hs
82 build-depends: base
83 , config-ini
84 , unordered-containers
85 , text
86 , directory
1 {-# LANGUAGE OverloadedStrings #-}
2
3 module Main where
4
5 import Data.Ini.Config
6 import Data.Text (Text)
7
8 data Config = Config
9 { confUsername :: Text
10 , confPort :: Int
11 , confUseEncryption :: Bool
12 } deriving (Eq, Show)
13
14 parseConfig :: IniParser Config
15 parseConfig = section "network" $ do
16 user <- field "user"
17 port <- fieldOf "port" number
18 enc <- fieldFlagDef "encryption" True
19 return (Config user port enc)
20
21 example :: Text
22 example = "[NETWORK]\n\
23 \user = gdritter\n\
24 \port = 8888\n"
25
26 main :: IO ()
27 main = do
28 print (parseIniFile example parseConfig)
1 {-# LANGUAGE OverloadedStrings #-}
2
3 module Main where
4
5 import Data.Ini.Config
6 import Data.Text (Text)
7 import qualified Data.Text.IO as T
8
9 data Config = Config
10 { cfNetwork :: NetworkConfig, cfLocal :: Maybe LocalConfig }
11 deriving (Eq, Show)
12
13 data NetworkConfig = NetworkConfig
14 { netHost :: String, netPort :: Int }
15 deriving (Eq, Show)
16
17 data LocalConfig = LocalConfig
18 { localUser :: Text }
19 deriving (Eq, Show)
20
21 configParser :: IniParser Config
22 configParser = do
23 netCf <- section "NETWORK" $ do
24 host <- fieldOf "host" string
25 port <- fieldOf "port" number
26 return NetworkConfig { netHost = host, netPort = port }
27 locCf <- sectionMb "LOCAL" $
28 LocalConfig <$> field "user"
29 return Config { cfNetwork = netCf, cfLocal = locCf }
30
31 main :: IO ()
32 main = do
33 rs <- T.getContents
34 print (parseIniFile rs configParser)
1 {-# LANGUAGE OverloadedStrings #-}
2 {-# LANGUAGE ScopedTypeVariables #-}
3 {-# LANGUAGE GeneralizedNewtypeDeriving #-}
4 {-# OPTIONS_GHC -fno-warn-redundant-constraints #-}
5
6 module Data.Ini.Config
7 (
8 -- $main
9 -- * Running Parsers
10 parseIniFile
11 -- * Parser Types
12 , IniParser
13 , SectionParser
14 -- * Section-Level Parsing
15 , section
16 , sectionMb
17 , sectionDef
18 -- * Field-Level Parsing
19 , field
20 , fieldOf
21 , fieldMb
22 , fieldMbOf
23 , fieldDef
24 , fieldDefOf
25 , fieldFlag
26 , fieldFlagDef
27 -- * Reader Functions
28 , readable
29 , number
30 , string
31 , flag
32 ) where
33
34 import Control.Monad.Trans.Except
35 import qualified Data.HashMap.Strict as HM
36 import Data.Ini.Raw
37 import Data.String (IsString(..))
38 import Data.Text (Text)
39 import qualified Data.Text as T
40 import Data.Typeable (Typeable, Proxy(..), typeRep)
41 import Text.Read (readMaybe)
42
43 addLineInformation :: Int -> Text -> StParser s a -> StParser s a
44 addLineInformation lineNo sec = withExceptT go
45 where go e = "Line " ++ show lineNo ++
46 ", in section " ++ show sec ++
47 ": " ++ e
48
49 type StParser s a = ExceptT String ((->) s) a
50
51 -- | An 'IniParser' value represents a computation for parsing entire
52 -- INI-format files.
53 newtype IniParser a = IniParser (StParser Ini a)
54 deriving (Functor, Applicative, Monad)
55
56 -- | A 'SectionParser' value represents a computation for parsing a single
57 -- section of an INI-format file.
58 newtype SectionParser a = SectionParser (StParser IniSection a)
59 deriving (Functor, Applicative, Monad)
60
61 -- | Parse a 'Text' value as an INI file and run an 'IniParser' over it
62 parseIniFile :: Text -> IniParser a -> Either String a
63 parseIniFile text (IniParser mote) = do
64 ini <- parseIni text
65 runExceptT mote ini
66
67 -- | Find a named section in the INI file and parse it with the provided
68 -- section parser, failing if the section does not exist.
69 --
70 -- >>> parseIniFile "[ONE]\nx = hello\n" $ section "ONE" (field "x")
71 -- Right "hello"
72 -- >>> parseIniFile "[ONE]\nx = hello\n" $ section "TWO" (field "y")
73 -- Left "No top-level section named \"TWO\""
74 section :: Text -> SectionParser a -> IniParser a
75 section name (SectionParser thunk) = IniParser $ ExceptT $ \(Ini ini) ->
76 case HM.lookup (T.toLower name) ini of
77 Nothing -> Left ("No top-level section named " ++ show name)
78 Just sec -> runExceptT thunk sec
79
80 -- | Find a named section in the INI file and parse it with the provided
81 -- section parser, returning 'Nothing' if the section does not exist.
82 --
83 -- >>> parseIniFile "[ONE]\nx = hello\n" $ sectionMb "ONE" (field "x")
84 -- Right (Just "hello")
85 -- >>> parseIniFile "[ONE]\nx = hello\n" $ sectionMb "TWO" (field "y")
86 -- Right Nothing
87 sectionMb :: Text -> SectionParser a -> IniParser (Maybe a)
88 sectionMb name (SectionParser thunk) = IniParser $ ExceptT $ \(Ini ini) ->
89 case HM.lookup (T.toLower name) ini of
90 Nothing -> return Nothing
91 Just sec -> Just `fmap` runExceptT thunk sec
92
93 -- | Find a named section in the INI file and parse it with the provided
94 -- section parser, returning a default value if the section does not exist.
95 --
96 -- >>> parseIniFile "[ONE]\nx = hello\n" $ sectionDef "ONE" "def" (field "x")
97 -- Right "hello"
98 -- >>> parseIniFile "[ONE]\nx = hello\n" $ sectionDef "TWO" "def" (field "y")
99 -- Right "def"
100 sectionDef :: Text -> a -> SectionParser a -> IniParser a
101 sectionDef name def (SectionParser thunk) = IniParser $ ExceptT $ \(Ini ini) ->
102 case HM.lookup (T.toLower name) ini of
103 Nothing -> return def
104 Just sec -> runExceptT thunk sec
105
106 ---
107
108 throw :: String -> StParser s a
109 throw msg = ExceptT $ (\ _ -> Left msg)
110
111 getSectionName :: StParser IniSection Text
112 getSectionName = ExceptT $ (\ m -> return (isName m))
113
114 rawFieldMb :: Text -> StParser IniSection (Maybe IniValue)
115 rawFieldMb name = ExceptT $ \m ->
116 return (HM.lookup name (isVals m))
117
118 rawField :: Text -> StParser IniSection IniValue
119 rawField name = do
120 sec <- getSectionName
121 valMb <- rawFieldMb name
122 case valMb of
123 Nothing -> throw ("Missing field " ++ show name ++
124 " in section " ++ show sec)
125 Just x -> return x
126
127 -- | Retrieve a field, failing if it doesn't exist, and return its raw value.
128 --
129 -- >>> parseIniFile "[MAIN]\nx = hello\n" $ section "MAIN" (field "x")
130 -- Right "hello"
131 -- >>> parseIniFile "[MAIN]\nx = hello\n" $ section "MAIN" (field "y")
132 -- Left "Missing field \"y\" in section \"main\""
133 field :: Text -> SectionParser Text
134 field name = SectionParser $ vValue `fmap` rawField name
135
136 -- | Retrieve a field and use the supplied parser to parse it as a value,
137 -- failing if the field does not exist, or if the parser fails to
138 -- produce a value.
139 --
140 -- >>> parseIniFile "[MAIN]\nx = 72\n" $ section "MAIN" (fieldOf "x" number)
141 -- Right 72
142 -- >>> parseIniFile "[MAIN]\nx = hello\n" $ section "MAIN" (fieldOf "x" number)
143 -- Left "Line 2, in section \"main\": Unable to parse \"hello\" as a value of type Integer"
144 -- >>> parseIniFile "[MAIN]\nx = 72\n" $ section "MAIN" (fieldOf "y" number)
145 -- Left "Missing field \"y\" in section \"main\""
146 fieldOf :: Text -> (Text -> Either String a) -> SectionParser a
147 fieldOf name parse = SectionParser $ do
148 sec <- getSectionName
149 val <- rawField name
150 case parse (vValue val) of
151 Left err -> addLineInformation (vLineNo val) sec (throw err)
152 Right x -> return x
153
154 -- | Retrieve a field, returning a @Nothing@ value if it does not exist.
155 --
156 -- >>> parseIniFile "[MAIN]\nx = hello\n" $ section "MAIN" (fieldMb "x")
157 -- Right (Just "hello")
158 -- >>> parseIniFile "[MAIN]\nx = hello\n" $ section "MAIN" (fieldMb "y")
159 -- Right Nothing
160 fieldMb :: Text -> SectionParser (Maybe Text)
161 fieldMb name = SectionParser $ fmap vValue `fmap` rawFieldMb name
162
163 -- | Retrieve a field and parse it according to the given parser, returning
164 -- @Nothing@ if it does not exist. If the parser fails, then this will
165 -- fail.
166 --
167 -- >>> parseIniFile "[MAIN]\nx = 72\n" $ section "MAIN" (fieldMbOf "x" number)
168 -- Right 72
169 -- >>> parseIniFile "[MAIN]\nx = hello\n" $ section "MAIN" (fieldMbOf "x" number)
170 -- Left "Line 2, in section \"main\": Unable to parse \"hello\" as a value of type Integer"
171 -- >>> parseIniFile "[MAIN]\nx = 72\n" $ section "MAIN" (fieldMbOf "y" number)
172 -- Right Nothing
173 fieldMbOf :: Text -> (Text -> Either String a) -> SectionParser (Maybe a)
174 fieldMbOf name parse = SectionParser $ do
175 sec <- getSectionName
176 mb <- rawFieldMb name
177 case mb of
178 Nothing -> return Nothing
179 Just v -> case parse (vValue v) of
180 Left err -> addLineInformation (vLineNo v) sec (throw err)
181 Right x -> return (Just x)
182
183 -- | Retrieve a field and supply a default value for if it doesn't exist.
184 --
185 -- >>> parseIniFile "[MAIN]\nx = hello\n" $ section "MAIN" (fieldDef "x" "def")
186 -- Right "hello"
187 -- >>> parseIniFile "[MAIN]\nx = hello\n" $ section "MAIN" (fieldDef "y" "def")
188 -- Right "def"
189 fieldDef :: Text -> Text -> SectionParser Text
190 fieldDef name def = SectionParser $ ExceptT $ \m ->
191 case HM.lookup name (isVals m) of
192 Nothing -> return def
193 Just x -> return (vValue x)
194
195 -- | Retrieve a field, parsing it according to the given parser, and returning
196 -- a default value if it does not exist. If the parser fails, then this will
197 -- fail.
198 --
199 -- >>> parseIniFile "[MAIN]\nx = 72\n" $ section "MAIN" (fieldDefOf "x" number 99)
200 -- Right 72
201 -- >>> parseIniFile "[MAIN]\nx = hello\n" $ section "MAIN" (fieldDefOf "x" number 99)
202 -- Left "Line 2, in section \"main\": Unable to parse \"hello\" as a value of type Integer"
203 -- >>> parseIniFile "[MAIN]\nx = 72\n" $ section "MAIN" (fieldDefOf "y" number 99)
204 -- Right 99
205 fieldDefOf :: Text -> (Text -> Either String a) -> a -> SectionParser a
206 fieldDefOf name parse def = SectionParser $ do
207 sec <- getSectionName
208 mb <- rawFieldMb name
209 case mb of
210 Nothing -> return def
211 Just v -> case parse (vValue v) of
212 Left err -> addLineInformation (vLineNo v) sec (throw err)
213 Right x -> return x
214
215 -- | Retrieve a field and treat it as a boolean, failing if it
216 -- does not exist.
217 --
218 -- >>> parseIniFile "[MAIN]\nx = yes\n" $ section "MAIN" (fieldFlag "x")
219 -- Right True
220 -- >>> parseIniFile "[MAIN]\nx = yes\n" $ section "MAIN" (fieldFlag "y")
221 -- Left "Missing field \"y\" in section \"main\""
222 fieldFlag :: Text -> SectionParser Bool
223 fieldFlag name = fieldOf name flag
224
225 -- | Retrieve a field and treat it as a boolean, subsituting
226 -- a default value if it doesn't exist.
227 --
228 -- >>> parseIniFile "[MAIN]\nx = yes\n" $ section "MAIN" (fieldFlagDef "x" False)
229 -- Right True
230 -- >>> parseIniFile "[MAIN]\nx = hello\n" $ section "MAIN" (fieldFlagDef "x" False)
231 -- Left "Line 2, in section \"main\": Unable to parse \"hello\" as a boolean"
232 -- >>> parseIniFile "[MAIN]\nx = yes\n" $ section "MAIN" (fieldFlagDef "y" False)
233 -- Right False
234 fieldFlagDef :: Text -> Bool -> SectionParser Bool
235 fieldFlagDef name def = fieldDefOf name flag def
236
237 ---
238
239 -- | Try to use the "Read" instance for a type to parse a value, failing
240 -- with a human-readable error message if reading fails.
241 --
242 -- >>> readable "(5, 7)" :: Either String (Int, Int)
243 -- Right (5, 7)
244 -- >>> readable "hello" :: Either String (Int, Int)
245 -- Left "Unable to parse \"hello\" as a value of type (Int,Int)"
246 readable :: forall a. (Read a, Typeable a) => Text -> Either String a
247 readable t = case readMaybe str of
248 Just v -> Right v
249 Nothing -> Left ("Unable to parse " ++ show str ++
250 " as a value of type " ++ show typ)
251 where str = T.unpack t
252 typ = typeRep prx
253 prx :: Proxy a
254 prx = Proxy
255
256 -- | Try to use the "Read" instance for a numeric type to parse a value,
257 -- failing with a human-readable error message if reading fails.
258 --
259 -- >>> number "5" :: Either String Int
260 -- Right 5
261 -- >>> number "hello" :: Either String Int
262 -- Left "Unable to parse \"hello\" as a value of type Int"
263 number :: (Num a, Read a, Typeable a) => Text -> Either String a
264 number = readable
265
266 -- | Convert a textua value to the appropriate string type. This will
267 -- never fail.
268 --
269 -- >>> string "foo" :: Either String String
270 -- Right "foo"
271 string :: (IsString a) => Text -> Either String a
272 string = return . fromString . T.unpack
273
274 -- | Convert a string that represents a boolean to a proper boolean. This
275 -- is case-insensitive, and matches the words @true@, @false@, @yes@,
276 -- @no@, as well as single-letter abbreviations for all of the above.
277 -- If the input does not match, then this will fail with a human-readable
278 -- error message.
279 --
280 -- >>> flag "TRUE"
281 -- Right True
282 -- >>> flag "y"
283 -- Right True
284 -- >>> flag "no"
285 -- Right False
286 -- >>> flag "F"
287 -- Right False
288 -- >>> flag "That's a secret!"
289 -- Left "Unable to parse \"that's a secret!\" as a boolean"
290 flag :: Text -> Either String Bool
291 flag s = case T.toLower s of
292 "true" -> Right True
293 "yes" -> Right True
294 "t" -> Right True
295 "y" -> Right True
296 "false" -> Right False
297 "no" -> Right False
298 "f" -> Right False
299 "n" -> Right False
300 _ -> Left ("Unable to parse " ++ show s ++ " as a boolean")
301
302
303 -- $main
304 -- The 'config-ini' library exports some simple monadic functions to
305 -- make parsing INI-like configuration easier. INI files have a
306 -- two-level structure: the top-level named chunks of configuration,
307 -- and the individual key-value pairs contained within those chunks.
308 -- For example, the following INI file has two sections, @NETWORK@
309 -- and @LOCAL@, and each contains its own key-value pairs. Comments,
310 -- which begin with @#@ or @;@, are ignored:
311 --
312 -- > [NETWORK]
313 -- > host = example.com
314 -- > port = 7878
315 -- >
316 -- > # here is a comment
317 -- > [LOCAL]
318 -- > user = terry
319 --
320 -- The combinators provided here are designed to write quick and
321 -- idiomatic parsers for files of this form. Sections are parsed by
322 -- 'IniParser' computations, like 'section' and its variations,
323 -- while the fields within sections are parsed by 'SectionParser'
324 -- computations, like 'field' and its variations. If we want to
325 -- parse an INI file like the one above, treating the entire
326 -- `LOCAL` section as optional, we can write it like this:
327 --
328 -- > data Config = Config
329 -- > { cfNetwork :: NetworkConfig, cfLocal :: Maybe LocalConfig }
330 -- > deriving (Eq, Show)
331 -- >
332 -- > data NetworkConfig = NetworkConfig
333 -- > { netHost :: String, netPort :: Int }
334 -- > deriving (Eq, Show)
335 -- >
336 -- > data LocalConfig = LocalConfig
337 -- > { localUser :: Text }
338 -- > deriving (Eq, Show)
339 -- >
340 -- > configParser :: IniParser Config
341 -- > configParser = do
342 -- > netCf <- section "NETWORK" $ do
343 -- > host <- fieldOf "host" string
344 -- > port <- fieldOf "port" number
345 -- > return NetworkConfig { netHost = host, netPort = port }
346 -- > locCf <- sectionMb "LOCAL" $
347 -- > LocalConfig <$> field "user"
348 -- > return Config { cfNetwork = netCf, cfLocal = locCf }
349 --
350 -- We can run our computation with 'parseIniFile', which,
351 -- when run on our example file above, would produce the
352 -- following:
353 --
354 -- >>> parseIniFile example configParser
355 -- Right (Config {cfNetwork = NetworkConfig {netHost = "example.com", netPort = 7878}, cfLocal = Just (LocalConfig {localUser = "terry"})})
1 module Data.Ini.Raw
2 ( Ini(..)
3 , IniSection(..)
4 , IniValue(..)
5 , parseIni
6 ) where
7
8 import Control.Monad (void)
9 import Data.HashMap.Strict (HashMap)
10 import qualified Data.HashMap.Strict as HM
11 import Data.Text (Text)
12 import qualified Data.Text as T
13 import Text.Megaparsec
14 import Text.Megaparsec.Text
15
16 -- | An 'Ini' value is a mapping from section names to
17 -- 'IniSection' values.
18 newtype Ini
19 = Ini { fromIni :: HashMap Text IniSection }
20 deriving (Eq, Show)
21
22 -- | An 'IniSection' consists of a name, a mapping of key-value pairs,
23 -- and metadata about where the section starts and ends in the file.
24 data IniSection = IniSection
25 { isName :: Text
26 , isVals :: HashMap Text IniValue
27 , isStartLine :: Int
28 , isEndLine :: Int
29 } deriving (Eq, Show)
30
31 -- | An 'IniValue' represents a key-value mapping, and also stores the
32 -- line number where it appears.
33 data IniValue = IniValue
34 { vLineNo :: Int
35 , vName :: Text
36 , vValue :: Text
37 } deriving (Eq, Show)
38
39 -- | Parse a 'Text' value into an 'Ini' value.
40 parseIni :: Text -> Either String Ini
41 parseIni t = case runParser pIni "ini file" t of
42 Left err -> Left (parseErrorPretty err)
43 Right v -> Right v
44
45 pIni :: Parser Ini
46 pIni = sBlanks *> (go `fmap` (many (pSection <?> "section") <* eof))
47 where go vs = Ini (HM.fromList [ (isName v, v) | v <- vs ])
48
49 sBlanks :: Parser ()
50 sBlanks = skipMany (void eol <|> sComment)
51
52 sComment :: Parser ()
53 sComment = do
54 void (oneOf ";#")
55 void (manyTill anyChar eol)
56
57 pSection :: Parser IniSection
58 pSection = do
59 start <- getCurrentLine
60 void (char '[')
61 name <- T.pack `fmap` some (noneOf "[]")
62 void (char ']')
63 sBlanks
64 vals <- many (pPair <?> "key-value pair")
65 end <- getCurrentLine
66 sBlanks
67 return IniSection
68 { isName = T.toLower (T.strip name)
69 , isVals = HM.fromList [ (vName v, v) | v <- vals ]
70 , isStartLine = start
71 , isEndLine = end
72 }
73
74 pPair :: Parser IniValue
75 pPair = do
76 pos <- getCurrentLine
77 key <- T.pack `fmap` some (noneOf "[]=:")
78 void (oneOf ":=")
79 val <- T.pack `fmap` manyTill anyChar eol
80 sBlanks
81 return IniValue
82 { vLineNo = pos
83 , vName = T.strip key
84 , vValue = T.strip val
85 }
86
87 getCurrentLine :: Parser Int
88 getCurrentLine = (fromIntegral . unPos . sourceLine) `fmap` getPosition
1 module Main where
2
3 import Data.List
4 import Data.Ini.Raw
5 import Data.HashMap.Strict (HashMap)
6 import Data.Text (Text)
7 import qualified Data.Text.IO as T
8 import System.Directory
9 import System.Exit
10
11 dir :: FilePath
12 dir = "test/general/cases"
13
14 main :: IO ()
15 main = do
16 files <- getDirectoryContents dir
17 let inis = [ f | f <- files
18 , ".ini" `isSuffixOf` f
19 ]
20 mapM_ runTest inis
21
22 toMaps :: Ini -> HashMap Text (HashMap Text Text)
23 toMaps (Ini m) = fmap (fmap vValue . isVals) m
24
25 runTest :: FilePath -> IO ()
26 runTest iniF = do
27 let hsF = take (length iniF - 4) iniF ++ ".hs"
28 ini <- T.readFile (dir ++ "/" ++ iniF)
29 hs <- readFile (dir ++ "/" ++ hsF)
30 case parseIni ini of
31 Left err -> do
32 putStrLn ("Error parsing " ++ iniF)
33 putStrLn err
34 exitFailure
35 Right x
36 | toMaps x == read hs -> do
37 putStrLn ("Passed: " ++ iniF)
38 | otherwise -> do
39 putStrLn ("Parses do not match for " ++ iniF)
40 putStrLn ("Expected: " ++ hs)
41 putStrLn ("Actual: " ++ show (toMaps x))
42 exitFailure
1 fromList
2 [ ( "s1"
3 , fromList
4 [ ( "foo", "bar" )
5 , ( "baz", "quux" )
6 ]
7 )
8 , ( "s2"
9 , fromList [ ( "argl", "bargl" ) ]
10 )
11 ]
1 # a thorough test
2 # leading comments
3 [S1]
4 # test with equals
5 foo = bar
6 # test with colon
7 baz : quux
8
9 [S2]
10 ; comments with semicolons
11 argl = bargl
12 ; trailing comments
1 fromList
2 [ ( "中文"
3 , fromList
4 [ ("鸡丁", "宫保")
5 , ("豆腐", "麻婆")
6 ]
7 )
8 , ( "русский"
9 , fromList [ ( "хорошо", "очень" ) ]
10 )
11 , ( "العَرَبِيَّة‎‎"
12 , fromList
13 [ ("واحد", "١")
14 , ("اثنان", "٢")
15 , ("ثلاثة", "٣")
16 ]
17 )
18 ]
1 # some unicode tests, for good measure
2
3 [中文]
4 # 也有漢字在這個注释
5 鸡丁 = 宫保
6 豆腐 : 麻婆
7
8 [русский]
9 ; и это комментарии
10 хорошо = очень
11
12 [العَرَبِيَّة‎‎]
13 واحد = ١
14 اثنان = ٢
15 ثلاثة = ٣
1 module Main where
2
3 import Data.Char
4 import Data.HashMap.Strict (HashMap)
5 import qualified Data.HashMap.Strict as HM
6 import qualified Data.Ini as I1
7 import qualified Data.Ini.Raw as I2
8 import Data.Text (Text)
9 import qualified Data.Text as T
10
11 import Test.QuickCheck
12
13 iniEquiv :: I1.Ini -> Bool
14 iniEquiv raw = case (i1, i2) of
15 (Right i1', Right i2') ->
16 let i1'' = lower i1'
17 i2'' = toMaps i2'
18 in i1'' == i2''
19 _ -> False
20 where pr = I1.printIniWith I1.defaultWriteIniSettings raw
21 i2 = I2.parseIni pr
22 i1 = I1.parseIni pr
23
24 lower :: I1.Ini -> HashMap Text (HashMap Text Text)
25 lower (I1.Ini hm) =
26 HM.fromList [ (T.toLower k, v) | (k, v) <- HM.toList hm ]
27
28 toMaps :: I2.Ini -> HashMap Text (HashMap Text Text)
29 toMaps (I2.Ini m) = fmap (fmap I2.vValue . I2.isVals) m
30
31 instance Arbitrary I1.Ini where
32 arbitrary = (I1.Ini . HM.fromList) <$> listOf sections
33 where sections = (,) <$> str <*> section
34 str = (T.pack <$> arbitrary) `suchThat` (\ t ->
35 T.all (\ c -> isAlphaNum c || c == ' ')
36 t && not (T.null t))
37 section = HM.fromList <$> listOf kv
38 kv = (,) <$> str <*> str
39
40 main :: IO ()
41 main = quickCheck iniEquiv