packages feed

config-parser (empty) → 0.1.0.0

raw patch · 9 files changed

+1091/−0 lines, 9 filesdep +basedep +hspecdep +lenssetup-changed

Dependencies added: base, hspec, lens, parsec, text

Files

+ ChangeLog.md view
@@ -0,0 +1,5 @@+# Revision history for config-parser++## 0.1.0.0  -- YYYY-mm-dd++* First version. Released on an unsuspecting world.
+ LICENSE view
@@ -0,0 +1,20 @@+Copyright (c) 2017 Ben Hamlin++Permission is hereby granted, free of charge, to any person obtaining+a copy of this software and associated documentation files (the+"Software"), to deal in the Software without restriction, including+without limitation the rights to use, copy, modify, merge, publish,+distribute, sublicense, and/or sell copies of the Software, and to+permit persons to whom the Software is furnished to do so, subject to+the following conditions:++The above copyright notice and this permission notice shall be included+in all copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ Text/ConfigParser.hs view
@@ -0,0 +1,81 @@+{-|+Module      : Text.ConfigParser+Description : A simple library to parse config files using parsec that+              generates parse errors on invalid keys+Copyright   : (c) Ben Hamlin, 2017+License     : MIT+Maintainer  : protob3n@gmail.com+Stability   : experimental+Portability : POSIX++This is yet another entry in Haskell's enourmous collection of config-file+parsing libraries. It lacks many of the bells and whistles of other config-file+parsing libraries, such as hierarchical sections and on-the-fly reloading. On+the other hand, it has a combination of features I was unable to find in other+libraries:+* Keys and values are parsed with configurable parsec parsers, resulting in+  flexible syntax and pretty error messages.+* Custom parsers can be created with parsec to handle values of any type.+* Keys that aren't explicitly handled result in parse errors.++If you don't need all of these features, there are probably better libraries out+there for you. If you're free to use its idiosyncratic file format, the+config-value library, in particular, is excelent.++By default, this library parses flat config like the following:+@+a_string = "blah, blah, blah\nmore blah"+a_number = 9001+a_list   = [1,2,3,4,5]+# This is a comment+@++If you wanted to parse the above file, saved as @./config.txt@, you might do so+as follows:+@+import Text.ConfigParser++cp :: ConfigParser (Maybe String, Maybe Integer, [Integer])+cp = configParser (Nothing, Nothing, [])+    [ ConfigOption+        { key    = "a_string"+        , parser = string+        , action = \s (_,n,ns) -> (Just s, n, ns)+        }+    , ConfigOption+        { key    = "a_number"+        , parser = integer+        , action = \n (s,_,ns) -> (s, Just n, ns)+        }+    , ConfigOption+        { key    = "a_list"+        , parser = list integer+        , action = \ns (s,n,_) -> (s, n, ns)+        }+    ]++main :: IO ()+main = parseFromFile cp "./config.txt" >>= print+@+-}+module Text.ConfigParser+    ( module Types+    , module Parser+    ) where++import Text.ConfigParser.Types as Types+    ( Key+    , ConfigOption+    , ConfigParser+    , configParser_+    , configParser+    , defaultKeyValue+    , defaultLineCommentInit+    )+import Text.ConfigParser.Parser as Parser+    ( config+    , string+    , integer+    , bool+    , list+    )
+ Text/ConfigParser/Parser.hs view
@@ -0,0 +1,121 @@+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE ScopedTypeVariables #-}+module Text.ConfigParser.Parser where++import Control.Monad (void)+import Data.String (IsString(..))+import Text.Parsec (SourceName, ParseError, State(..))+import Text.Parsec (getParserState, setParserState, unexpected, newline)+import Text.Parsec (manyTill, char, choice, digit, sepBy, many, many1, try)+import Text.Parsec (spaces, eof, parse, (<|>), (<?>))+import Text.Parsec.Char (noneOf, oneOf, anyChar)+import Text.Parsec.Text (Parser)+import qualified Text.Parsec as P (string)++import Text.ConfigParser.Util+import Text.ConfigParser.Types++-- | Parse a string surrounded by quotes. Quotes within the string must be+-- escaped with backslashes.+string :: IsString s => Parser s+string = char '"' *> fmap fromString (many stringChar) <* char '"'+     <?> "string in quotes"+    where+    stringChar = noneOf "\"\n\\" <|> char '\\' *> escapeSeq+    escapeSeq  = char '"' <|> char '\\' <|> '\n' <$ char 'n'++-- | Parse an integer.+integer :: Parser Integer+integer = read .: (++) <$> sign <*> many1 digit <?> "integer"+    where+    sign = P.string "-" <|> P.string ""++-- | Parse a bounded integer. Fail to parse with a descriptive message if the+-- value is out of bounds.+boundedIntegral :: forall n. (Show n, Bounded n, Integral n) => Parser n+boundedIntegral = bound =<< integer+              <?> "integer between " ++ show intMin ++ " and " ++ show intMax+    where+    intMin  = minBound :: n+    intMax  = maxBound :: n+    bound n | n > fromIntegral intMax = unexpected $ "integer above " ++ show intMax+            | n < fromIntegral intMin = unexpected $ "integer below " ++ show intMin++            | otherwise    = return $ fromIntegral n++-- | Parse a boolean. Valid synonyms for @True@ are @true@, @yes@, @Yes@, @on@,+-- and @On@. Valid synonyms for @False@ are @false@, @no@, @No@, @off@, and+-- @Off@.+bool :: Parser Bool+bool = True <$ try truthy <|> False <$ try falsey+    where+    truthy = choice $ fmap P.string ["true", "True", "yes", "Yes", "on", "On"]+    falsey = choice $ fmap P.string ["false", "False", "no", "No", "off", "Off"]++-- | Parse a list of values surrounded by @[@ and @]@, and separated by commas.+-- The list can contain whitespace and newlines.+list :: (Parser a) -> Parser [a]+list p = initial *> (p `sepBy` separator) <* terminator <?> "list in brackets"+    where+    initial    = try $ char '[' <* spaces+    separator  = try $ spaces *> char ',' <* spaces+    terminator = try $ spaces *> char ']'++-- | Ignore zero or more spaces, tabs, or vertical tabs.+whitespace :: Parser ()+whitespace = () <$ many (oneOf " \t\v\r") <?> "whitespace"++-- | Extract a parser for a transformation on c's from a 'ConfigOption'.+actionParser :: ConfigParser c -> ConfigOption c -> Parser (c -> c)+actionParser c ConfigOption {..} =+    whitespace *> keyValue c key (action <$> parser)++-- Parse a string and replace the input of the parser with the result.+replaceParserInput :: Parser String -> Parser ()+replaceParserInput p = do+    s <- getParserState+    i <- p+    void $ setParserState s {stateInput = fromString i}++-- Replace each line comment with a single newline.+removeLineComments :: ConfigParser c -> Parser ()+removeLineComments ConfigParser {..} = replaceParserInput $+    mconcat <$> many (escapedComment <|> comment <|> content)+    where+    startComment   = choice $ try . P.string <$> lineCommentInit+    terminator     = void newline <|> eof+    comment        = '\n':[] <$ startComment <* anyChar `manyTill` terminator+    escapedComment = try $ char '\\' *> startComment+    content        = (:[]) <$> anyChar++-- Remove spaces from the start and end of each line, at the start of the+-- input, and at the end of the input.+removeExtraSpaces :: Parser ()+removeExtraSpaces = replaceParserInput $+    whitespace *> contentChar `manyTill` try (whitespace *> eof)+    where+    contentChar = try strippedNL <|> anyChar+    strippedNL  = whitespace *> newline <* whitespace++-- Replace sequences of multiple newlines with a single newline.+removeExtraLines :: Parser ()+removeExtraLines = replaceParserInput $+    optionalNLs *> contentChar `manyTill` try (optionalNLs *> eof)+    where+    contentChar = combinedNLs <|> anyChar+    optionalNLs = ()   <$ many newline+    combinedNLs = '\n' <$ many1 newline++-- Parse a config file as specified by a 'ConfigParser'.+config :: ConfigParser c -> Parser c+config p = foldr ($) (defaults p) <$> do+    removeLineComments p+    removeExtraSpaces+    removeExtraLines+    optionParsers `sepBy` newline <* eof+    where+    optionParsers = choice $ try . actionParser p <$> options p++-- Parse a config file from disk.+parseFromFile :: ConfigParser c -> SourceName -> IO (Either ParseError c)+parseFromFile p f = parse (config p) f . fromString <$> readFile f
+ Text/ConfigParser/Types.hs view
@@ -0,0 +1,61 @@+{-# LANGUAGE ExistentialQuantification #-}+{-# LANGUAGE RankNTypes #-}+module Text.ConfigParser.Types where++import Data.List (nub)+import Text.Parsec (string, spaces, char, (<?>))+import Text.Parsec.Text (Parser)++type Key = String++-- Key-value pair to parse from a config file.+data ConfigOption c = forall a. ConfigOption+    { key    :: Key         -- ^ Key name.+    , parser :: Parser a    -- ^ Parser for the given value type.+    , action :: a -> c -> c -- ^ How the value should change the state @c@.+    }++-- | Parameters for a parser that takes a config file and produces a @c@. Use+-- the 'configParser_' constructor if you want to specify your own 'lineParser'+-- or 'commentStart'. Otherwise, use the 'configParser' smart constructor.+data ConfigParser c = ConfigParser+    { keyValue :: forall a. Key -> Parser a -> Parser a+      -- ^ Specifies how a key and a value parser should be represented in the+      -- config file, e.g., @key = value@, or @key: value@.+    , lineCommentInit :: [String]+      -- Strings to start a line comment, such as @#@, @--@, or @//@. All+      -- characters following this string up to the following newline or EOF+      -- will be removed. You can use the string without starting a comment by+      -- escaping it with a backslash, e.g. @\#@ or @\--@.+    , defaults :: c+      -- Initial @c@ to fold each 'ConfigOption's action over.+    , options :: [ConfigOption c]+      -- List of key-value pairs to parse from the config file. Any key in the+      -- config file that doesn't appear here will result in parse error.+    }++-- | Smart constructor to check that 'options' doesn't contain any duplicate+-- keys before creating a 'ConfigParser'.+configParser_ :: (forall a. Key -> Parser a -> Parser a)+              -> [String] -> c -> [ConfigOption c] -> ConfigParser c+configParser_ kv cs ds os' = ConfigParser kv cs ds os+    where+    os = if length (nub $ fmap key os') == length os'+            then os'+            else error "duplicate option keys in ConfigParser"++-- | Smart constructor for a 'ConfigParser' that uses a default syntax like+-- @key = value@ and line comments starting with @#@.+configParser :: c -> [ConfigOption c] -> ConfigParser c+configParser = configParser_ defaultKeyValue defaultLineCommentInit++-- | Default syntax like @key = value@.+defaultKeyValue :: Key -> Parser a -> Parser a+defaultKeyValue k p = keyParser *> separator *> p+    where+    keyParser = string k <?> "option key"+    separator = spaces *> char '=' <* spaces++-- | Default line comment like @# comment text@.+defaultLineCommentInit :: [String]+defaultLineCommentInit = ["#"]
+ Text/ConfigParser/Util.hs view
@@ -0,0 +1,4 @@+module Text.ConfigParser.Util where++(.:) :: (c -> d) -> (a -> b -> c) -> a -> b -> d+(.:) = (.).(.)
+ config-parser.cabal view
@@ -0,0 +1,61 @@+name:                config-parser+version:             0.1.0.0+synopsis:            Parse config files using parsec that generates parse errors+                     on invalid keys+description:         This is yet another entry in Haskell's enourmous collection+                     of config-file parsing libraries. It lacks many of the+                     bells and whistles of other config-file parsing libraries,+                     such as hierarchical sections and on-the-fly reloading. On+                     the other hand, it has a combination of features I was+                     unable to find in other libraries:+                     * Keys and values are parsed with configurable parsec+                       parsers, resulting in flexible syntax and pretty error+                       messages.+                     * Custom parsers can be created with parsec to handle+                       values of any type.+                     * Keys that aren't explicitly handled result in parse+                       errors.++                     If you don't need all of these features, there are+                     probably better libraries out there for you. If you're free+                     to use its idiosyncratic file format, the config-value+                     library, in particular, is excelent.+homepage:            github.com/protoben/config-parser+license:             MIT+license-file:        LICENSE+author:              Ben Hamlin+maintainer:          protob3n@gmail.com+-- copyright:           +category:            Text+build-type:          Simple+extra-source-files:  ChangeLog.md+cabal-version:       >=1.10++library+  ghc-options:         -Wall+  exposed-modules:     Text.ConfigParser+  other-modules:       Text.ConfigParser.Util,+                       Text.ConfigParser.Types,+                       Text.ConfigParser.Parser+  other-extensions:    ExistentialQuantification,+                       RankNTypes,+                       RecordWildCards+  build-depends:       base   >=4.9 && <4.10,+                       parsec >=3.1 && <3.2+  -- hs-source-dirs:      +  default-language:    Haskell2010++test-suite parsing+  ghc-options:         -Wall+  other-modules:       Text.ConfigParser.Util,+                       Text.ConfigParser.Types,+                       Text.ConfigParser.Parser,+                       Text.ConfigParser+  type:                exitcode-stdio-1.0+  main-is:             tests/Parsing.hs+  default-language:    Haskell2010+  build-depends:       base   >=4.9  && <4.10,+                       hspec  >=2.4  && <2.5,+                       lens   >=4.15 && <4.16,+                       parsec >=3.1  && <3.2,+                       text   >=1.2  && <1.3
+ tests/Parsing.hs view
@@ -0,0 +1,736 @@+{-# LANGUAGE NegativeLiterals #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TemplateHaskell #-}+module Main where++import Prelude hiding (unlines)++import Control.Lens+import Control.Monad (void)+import Data.Int (Int8)+import Data.Text (unlines)+import Data.Word (Word8)+import Test.Hspec+import Text.Parsec (ParseError)+import Text.Parsec (parse, errorPos, sourceLine, eof, getInput, spaces)+import Text.Parsec.Text (Parser)+import qualified Text.Parsec as P (string)++import Text.ConfigParser.Types+import Text.ConfigParser.Parser++data C = C+    { _f1 :: String+    , _f2 :: Integer+    , _f3 :: Bool+    , _f4 :: [String]+    , _f5 :: [Integer]+    , _f6 :: [Bool]+    , _f7 :: [[Integer]]+    } deriving (Show, Eq)++makeLenses ''C++defC :: C+defC = C+    { _f1 = "def"+    , _f2 = 0+    , _f3 = False+    , _f4 = []+    , _f5 = []+    , _f6 = []+    , _f7 = []+    }++co1,co2,co3,co4,co5,co6,co7 :: ConfigOption C+co1 = ConfigOption+  { key    = "f1"+  , action = set' f1+  , parser = string+  }+co2 = ConfigOption+  { key    = "f2"+  , action = set' f2+  , parser = integer+  }+co3 = ConfigOption+  { key    = "f3"+  , action = set' f3+  , parser = bool+  }+co4 = ConfigOption+  { key    = "f4"+  , action = set' f4+  , parser = list string+  }+co5 = ConfigOption+  { key    = "f5"+  , action = set' f5+  , parser = list integer+  }+co6 = ConfigOption+  { key    = "f6"+  , action = set' f6+  , parser = list bool+  }+co7 = ConfigOption+  { key    = "f7"+  , action = set' f7+  , parser = list (list integer)+  }++cp, cp' :: ConfigParser C+cp  = configParser defC [co1,co2,co3,co4,co5,co6,co7]+cp' = configParser_ lp lcs defC [co1,co2,co3,co4,co5,co6,co7]+    where+    lp :: Key -> Parser a -> Parser a+    lp k q = q <* spaces <* P.string "->" <* spaces <* P.string k+    lcs    = ["--","//"]++p, p' :: Parser C+p  = config cp+p' = config cp'++shouldFailOnLine :: Show a => Either ParseError a -> Int -> Expectation+shouldFailOnLine e n = case e of+    Left err -> sourceLine (errorPos err) `shouldBe` n+    _        -> expectationFailure $ show e++main :: IO ()+main = mapM_ hspec+    [ testString+    , testInteger+    , testBoundedIntegral+    , testBool+    , testList+    , testDefaultKeyValue+    , testActionParser+    , testRemoveLineComments+    , testRemoveExtraSpaces+    , testRemoveExtraLines+    , testConfig+    ]++testString :: Spec+testString = describe "string" $ do+    it "parses a string" $+        parse string' "test" "\"foo\"" `shouldBe` Right "foo"+    it "parses a string with a literal 'n' in it" $+        parse string' "test" "\"foonbar\"" `shouldBe` Right "foonbar"+    it "parses a string with a quote at the start" $+        parse string' "test" "\"\\\"bar\"" `shouldBe` Right "\"bar"+    it "parses a string with a quote in the middle" $+        parse string' "test" "\"foo\\\"bar\"" `shouldBe` Right "foo\"bar"+    it "parses a string with a quote at the end" $+        parse string' "test" "\"foo\\\"\"" `shouldBe` Right "foo\""+    it "parses a string with a newline at the start" $+        parse string' "test" "\"\\nbar\"" `shouldBe` Right "\nbar"+    it "parses a string with a newline in the middle" $+        parse string' "test" "\"foo\\nbar\"" `shouldBe` Right "foo\nbar"+    it "parses a string with a newline at the end" $+        parse string' "test" "\"foo\\n\"" `shouldBe` Right "foo\n"+    it "parses a string with a backslash at the start" $+        parse string' "test" "\"\\\\bar\"" `shouldBe` Right "\\bar"+    it "parses a string with a backslash in the middle" $+        parse string' "test" "\"foo\\\\bar\"" `shouldBe` Right "foo\\bar"+    it "parses a string with a backslash in front of a quote" $+        parse string' "test" "\"foo\\\\\\\"bar\"" `shouldBe` Right "foo\\\"bar"+    it "parses a string with a literal backslash-n" $+        parse string' "test" "\"foo\\\\nbar\"" `shouldBe` Right "foo\\nbar"+    it "parses a string with a backslash at the end" $+        parse string' "test" "\"foo\\\\\"" `shouldBe` Right "foo\\"+    it "ends a string at the first quote" $+        parse (string' <* eof) "test" "\"foo\"bar\"" `shouldFailOnLine` 1+    it "errors on an unterminated string" $+        parse string' "test" "\"foo" `shouldFailOnLine` 1+    it "errors on a newline inside a string" $+        parse string' "test" "\"foo\nbar\"" `shouldFailOnLine` 1+    where+    string' = string :: Parser String++testInteger :: Spec+testInteger = describe "integer" $ do+    it "parses a positive integer" $+        parse integer "test" "42" `shouldBe` Right 42+    it "parses a negative integer" $+        parse integer "test" "-42" `shouldBe` Right (-42)+    it "parses a zero" $+        parse integer "test" "0" `shouldBe` Right 0+    it "ignores initial zeroes" $+        parse integer "test" "000042" `shouldBe` Right 42++testBoundedIntegral :: Spec+testBoundedIntegral = describe "boundedIntegral" $ do+    it "parses negative values in range" $+        parse int8 "test" "-128" `shouldBe` Right (-128)+    it "parses positive values in range" $+        parse int8 "test" "127" `shouldBe` Right 127+    it "parses unsigned values in range" $+        parse word8 "test" "0" `shouldBe` Right 0+    it "bounds signed values below" $+        parse int8 "test" "-129" `shouldFailOnLine` 1+    it "bounds signed values above" $+        parse int8 "test" "128" `shouldFailOnLine` 1+    it "bounds unsigned values below" $+        parse word8 "test" "-1" `shouldFailOnLine` 1+    it "bounds unsigned values above" $+        parse word8 "test" "256" `shouldFailOnLine` 1+    where+    int8  = boundedIntegral :: Parser Int8+    word8 = boundedIntegral :: Parser Word8++testBool :: Spec+testBool = describe "bool" $ do+    it "parses \"true\"" $+        parse bool "test" "true" `shouldBe` Right True+    it "parses \"True\"" $+        parse bool "test" "True" `shouldBe` Right True+    it "parses \"on\"" $+        parse bool "test" "on" `shouldBe` Right True+    it "parses \"On\"" $+        parse bool "test" "On" `shouldBe` Right True+    it "parses \"yes\"" $+        parse bool "test" "yes" `shouldBe` Right True+    it "parses \"Yes\"" $+        parse bool "test" "Yes" `shouldBe` Right True+    it "parses \"false\"" $+        parse bool "test" "false" `shouldBe` Right False+    it "parses \"False\"" $+        parse bool "test" "False" `shouldBe` Right False+    it "parses \"off\"" $+        parse bool "test" "off" `shouldBe` Right False+    it "parses \"Off\"" $+        parse bool "test" "Off" `shouldBe` Right False+    it "parses \"no\"" $+        parse bool "test" "no" `shouldBe` Right False+    it "parses \"No\"" $+        parse bool "test" "No" `shouldBe` Right False++testList :: Spec+testList = describe "list" $ do+    it "parses an empty list" $+        parse (list integer) "test" "[]" `shouldBe` Right []+    it "parses an empty list with spaces in it" $+        parse (list integer) "test" "[  ]" `shouldBe` Right []+    it "parses an empty list with a newline in it" $+        parse (list integer) "test" "[\n]" `shouldBe` Right []+    it "parses a singleton" $+        parse (list integer) "test" "[42]" `shouldBe` Right [42]+    it "parses a singleton with spaces in it" $+        parse (list integer) "test" "[  42  ]" `shouldBe` Right [42]+    it "parses a singleton with newlines in it" $+        parse (list integer) "test" "[\n42\n]" `shouldBe` Right [42]+    it "parses a list with multiple elements" $+        parse (list integer) "test" "[42,43,44]" `shouldBe` Right [42,43,44]+    it "parses a list with multiple elements with spaces in it" $+        parse (list integer) "test" "[  42  ,  43  ,  44  ]"+            `shouldBe` Right [42,43,44]+    it "parses a list with multiple elements with newlines in it" $+        parse (list integer) "test" "[\n42\n,\n43\n,\n44\n]"+            `shouldBe` Right [42,43,44]+    it "parses a nested list" $+        parse (list (list integer)) "test" "[[],[42],[42,43,44]]"+            `shouldBe` Right [[],[42],[42,43,44]]+    it "fails on an unterminated list with no elements" $+        parse (list integer) "test" "[" `shouldFailOnLine` 1+    it "fails on an unterminated list with elements" $+        parse (list integer) "test" "[42,43" `shouldFailOnLine` 1+    it "fails on an unterminated list after a comma" $+        parse (list integer) "test" "[42," `shouldFailOnLine` 1++testWhitespace :: Spec+testWhitespace = describe "whitespace" $ do+    it "parses spaces" $+        parse whitespace "test" "       " `shouldBe` Right ()+    it "parses tabs" $+        parse whitespace "test" "\t\t\t\t" `shouldBe` Right ()+    it "parses a mix of spaces and tabs" $+        parse whitespace "test" "  \t \t  \t\t " `shouldBe` Right ()+    it "fails on newlines" $+        parse (whitespace <* eof) "test" "\n" `shouldFailOnLine` 1+    it "fails on carriage return" $+        parse (whitespace <* eof) "test" "\r" `shouldFailOnLine` 1++testDefaultKeyValue :: Spec+testDefaultKeyValue = describe "defaultKeyValue" $ do+    it "parses \"<key>=<value>\"" $+        parse (defaultKeyValue "foo" integer) "test" "foo=42"+            `shouldBe` Right 42+    it "parses \"<key> = <value>\"" $+        parse (defaultKeyValue "foo" integer) "test" "foo = 42"+            `shouldBe` Right 42+    it "parses \"<key>\\t=\\t<value>\"" $+        parse (defaultKeyValue "foo" integer) "test" "foo\t=\t42"+            `shouldBe` Right 42+    it "parses \"<key>\\n=\\n<value>\"" $+        parse (defaultKeyValue "foo" integer) "test" "foo\n=\n42"+            `shouldBe` Right 42+    it "parses \"<key>\\n  =\\n  <value>\"" $+        parse (defaultKeyValue "foo" integer) "test" "foo\n  =\n  42"+            `shouldBe` Right 42+    it "parses \"<key>  \\n=  \\n<value>\"" $+        parse (defaultKeyValue "foo" integer) "test" "foo  \n=  \n42"+            `shouldBe` Right 42+    it "fails on incorrect key" $+        parse (defaultKeyValue "foo" integer) "test" "bar = 42"+            `shouldFailOnLine` 1++testActionParser :: Spec+testActionParser = describe "actionParser" $ do+    it "doesn't require whitespace at the start of a line" $+        parse (actionParser' co2) "test" "f2 = 42" `shouldBe` Right ()+    it "allows whitespace at the start of a line" $+        parse (actionParser' co2) "test" "   f2 = 42" `shouldBe` Right ()+    where+    actionParser' = void . actionParser cp++testRemoveLineComments :: Spec+testRemoveLineComments = describe "removeLineComments" $ do+    it "preserves an empty file" $+        parse removeLineComments' "test" "" `shouldBe` Right ""+    it "preserves a file with one simple line" $+        parse removeLineComments' "test" "foobaz" `shouldBe` Right "foobaz"+    it "preserves spaces in the middle of a simple line" $+        parse removeLineComments' "test" "foo   baz"+            `shouldBe` Right "foo   baz"+    it "preserves a simple multiline file" $+        parse removeLineComments' "test" "foo\nbar\nzap"+            `shouldBe` Right "foo\nbar\nzap"+    it "preserves a multiline file with spaces" $+        parse removeLineComments' "test" "foo\nwiz   woz\nzap"+            `shouldBe` Right "foo\nwiz   woz\nzap"+    it "removes a lone comment with no spaces" $+        parse removeLineComments' "test" "#foo" `shouldBe` Right "\n"+    it "removes a lone comment preceded by spaces" $+        parse removeLineComments' "test" "   #foo" `shouldBe` Right "   \n"+    it "removes a lone comment followed by spaces" $+        parse removeLineComments' "test" "#foo   " `shouldBe` Right "\n"+    it "removes a lone comment starting with spaces" $+        parse removeLineComments' "test" "#   foo" `shouldBe` Right "\n"+    it "removes a lone comment with spaces in the middle" $+        parse removeLineComments' "test" "#foo   bar" `shouldBe` Right "\n"+    it "removes a comment before a single input line" $+        parse removeLineComments' "test" "#foobar\nbaz"+            `shouldBe` Right "\nbaz"+    it "removes a comment after a single input line" $+        parse removeLineComments' "test" "baz\n#foobar"+            `shouldBe` Right "baz\n\n"+    it "removes a comment after a single input line without spaces" $+        parse removeLineComments' "test" "baz#foobar" `shouldBe` Right "baz\n"+    it "removes a comment after a single input line with spaces" $+        parse removeLineComments' "test" "baz   #foobar"+            `shouldBe` Right "baz   \n"+    it "removes a comment at the start of a multi-line file" $+        parse removeLineComments' "test" "#foobar\nfoo\nwiz woz\nzap"+            `shouldBe` Right "\nfoo\nwiz woz\nzap"+    it "removes a full-line comment at the end of a multi-line file" $+        parse removeLineComments' "test" "foo\nwiz woz\nzap\n#foobar"+            `shouldBe` Right "foo\nwiz woz\nzap\n\n"+    it "removes a line-end comment at the end of a multi-line file" $+        parse removeLineComments' "test" "foo\nwiz woz\nzap #foobar"+            `shouldBe` Right "foo\nwiz woz\nzap \n"+    it "removes a full-line comment in the middle of a multi-line file" $+        parse removeLineComments' "test" "foo\nwiz woz\n#foobar\nzap"+            `shouldBe` Right "foo\nwiz woz\n\nzap"+    it "removes a line-end comment in the middle of a multi-line file" $+        parse removeLineComments' "test" "foo\nwiz woz #foobar\nzap #foobar"+            `shouldBe` Right "foo\nwiz woz \nzap \n"+    it "resolves an escaped `#` at the start of a line" $+        parse removeLineComments' "test" "\\#bar" `shouldBe` Right "#bar"+    it "resolves an escaped `#` at the end of a line" $+        parse removeLineComments' "test" "foo\\#" `shouldBe` Right "foo#"+    it "resolves an escaped `#` in the middle of a line" $+        parse removeLineComments' "test" "foo\\#bar" `shouldBe` Right "foo#bar"+    it "allows an unescaped `\\`" $+        parse removeLineComments' "test" "\\" `shouldBe` Right "\\"+    it "permits a literal `\\#` represented as `\\\\#'" $+        parse removeLineComments' "test" "\\\\#" `shouldBe` Right "\\#"+    it "permits an escaped `#` in a comment" $+        parse removeLineComments' "test" "#foobaz\\#asdf " `shouldBe` Right "\n"+    where+    removeLineComments' = removeLineComments cp >> getInput++testRemoveExtraSpaces :: Spec+testRemoveExtraSpaces = describe "removeExtraSpaces" $ do+    it "preserves an empty file" $+        parse removeExtraSpaces' "test" "" `shouldBe` Right ""+    it "preserves a file with one simple line" $+        parse removeExtraSpaces' "test" "foobaz" `shouldBe` Right "foobaz"+    it "preserves spaces in the middle of a simple line" $+        parse removeExtraSpaces' "test" "foo   baz" `shouldBe` Right "foo   baz"+    it "preserves a simple multiline file" $+        parse removeExtraSpaces' "test" "foo\nbar\nzap"+            `shouldBe` Right "foo\nbar\nzap"+    it "preserves a multiline file with spaces" $+        parse removeExtraSpaces' "test" "foo\nwiz   woz\nzap"+            `shouldBe` Right "foo\nwiz   woz\nzap"+    it "removes space alternatives" $+        parse removeExtraSpaces' "test" " \t\r\vbaz" `shouldBe` Right "baz"+    it "removes spaces at the start of a simple line" $+        parse removeExtraSpaces' "test" "   baz" `shouldBe` Right "baz"+    it "removes spaces at the end of a simple line" $+        parse removeExtraSpaces' "test" "foo   " `shouldBe` Right "foo"+    it "removes spaces at the start of a multi-line file" $+        parse removeExtraSpaces' "test" "   foo\nwiz woz\nzap"+            `shouldBe` Right "foo\nwiz woz\nzap"+    it "removes spaces at the end of a multi-line file" $+        parse removeExtraSpaces' "test" "foo\nwiz woz\nzap   "+            `shouldBe` Right "foo\nwiz woz\nzap"+    it "removes spaces at the start of a line in a multi-line file" $+        parse removeExtraSpaces' "test" "foo\n   wiz woz\nzap"+            `shouldBe` Right "foo\nwiz woz\nzap"+    it "removes spaces at the end of a line in a multi-line file" $+        parse removeExtraSpaces' "test" "foo\nwiz woz   \nzap"+            `shouldBe` Right "foo\nwiz woz\nzap"+    where+    removeExtraSpaces' = removeExtraSpaces >> getInput++testRemoveExtraLines :: Spec+testRemoveExtraLines = describe "removeExtraLines" $ do+    it "preserves an empty file" $+        parse removeExtraLines' "test" "" `shouldBe` Right ""+    it "preserves a file with one simple line" $+        parse removeExtraLines' "test" "foobaz" `shouldBe` Right "foobaz"+    it "preserves spaces in the middle of a simple line" $+        parse removeExtraLines' "test" "foo   baz" `shouldBe` Right "foo   baz"+    it "preserves a simple multiline file" $+        parse removeExtraLines' "test" "foo\nbar\nzap"+            `shouldBe` Right "foo\nbar\nzap"+    it "preserves a multiline file with spaces" $+        parse removeExtraLines' "test" "foo\nwiz   woz\nzap"+            `shouldBe` Right "foo\nwiz   woz\nzap"+    it "removes newlines at the start of a multi-line file" $+        parse removeExtraLines' "test" "\n\n\nfoo\nwiz woz\nzap"+            `shouldBe` Right "foo\nwiz woz\nzap"+    it "removes newlines at the end of a multi-line file" $+        parse removeExtraLines' "test" "foo\nwiz woz\nzap\n\n\n"+            `shouldBe` Right "foo\nwiz woz\nzap"+    it "removes newlines in the middle of a multi-line file" $+        parse removeExtraLines' "test" "foo\nwiz woz\n\n\nzap"+            `shouldBe` Right "foo\nwiz woz\nzap"+    where+    removeExtraLines' = removeExtraLines >> getInput++testConfig :: Spec+testConfig = describe "config" $ do+    it "parses an empty config file" $+        parse p "test" "" `shouldBe` Right defC+    it "parses a config file with just empty lines" $+        parse p "test" (unlines+            [ ""+            , "\t"+            , " "+            ]) `shouldBe` Right defC+    it "parses a config file with just a line comment" $ do+        parse p "test" "# foo" `shouldBe` Right defC+    it "parses a config file with just one line" $+        parse p "test" "f1 = \"foo\"" `shouldBe` Right defC {_f1 = "foo"}+    it "parses a string with escape sequences in it" $+        parse p "test" "f1 = \"foo\\n\\\"\\\\\""+            `shouldBe` Right defC {_f1 = "foo\n\"\\"}+    it "parses multiple in-order options from a config file" $+        parse p "test" (unlines+            [ "f1 = \"foo\""+            , "f2 = 9001"+            , "f3 = True"+            , "f4 = [\"foo\",\"bar\"]"+            , "f5 = [1,2,3,4,5]"+            ]) `shouldBe` Right defC+                { _f1 = "foo"+                , _f2 = 9001+                , _f3 = True+                , _f4 = ["foo","bar"]+                , _f5 = [1,2,3,4,5]+                }+    it "parses multiple out-of-order options from a config file" $+        parse p "test" (unlines+            [ "f4 = [\"foo\",\"bar\"]"+            , "f2 = 9001"+            , "f1 = \"foo\""+            , "f3 = True"+            , "f5 = [1,2,3,4,5]"+            ]) `shouldBe` Right defC+                { _f1 = "foo"+                , _f2 = 9001+                , _f3 = True+                , _f4 = ["foo","bar"]+                , _f5 = [1,2,3,4,5]+                }+    it "allows a comment at the top of a config file" $+        parse p "test" (unlines+            [ "# I'm a comment"+            , "f1 = \"foo\""+            , "f2 = 9001"+            , "f3 = True"+            , "f4 = [\"foo\",\"bar\"]"+            , "f5 = [1,2,3,4,5]"+            ]) `shouldBe` Right defC+                { _f1 = "foo"+                , _f2 = 9001+                , _f3 = True+                , _f4 = ["foo","bar"]+                , _f5 = [1,2,3,4,5]+                }+    it "allows a comment in the middle of a config file" $+        parse p "test" (unlines+            [ "f1 = \"foo\""+            , "f2 = 9001"+            , "f3 = True"+            , "# I'm a comment"+            , "f4 = [\"foo\",\"bar\"]"+            , "f5 = [1,2,3,4,5]"+            ]) `shouldBe` Right defC+                { _f1 = "foo"+                , _f2 = 9001+                , _f3 = True+                , _f4 = ["foo","bar"]+                , _f5 = [1,2,3,4,5]+                }+    it "allows a comment at the end of a config file" $+        parse p "test" (unlines+            [ "f1 = \"foo\""+            , "f2 = 9001"+            , "f3 = True"+            , "f4 = [\"foo\",\"bar\"]"+            , "f5 = [1,2,3,4,5]"+            , "# I'm a comment"+            ]) `shouldBe` Right defC+                { _f1 = "foo"+                , _f2 = 9001+                , _f3 = True+                , _f4 = ["foo","bar"]+                , _f5 = [1,2,3,4,5]+                }+    it "allows a comment at the end of a line" $+        parse p "test" (unlines+            [ "f1 = \"foo\""+            , "f2 = 9001"+            , "f3 = True # I'm a comment"+            , "f4 = [\"foo\",\"bar\"]"+            , "f5 = [1,2,3,4,5]"+            ]) `shouldBe` Right defC+                { _f1 = "foo"+                , _f2 = 9001+                , _f3 = True+                , _f4 = ["foo","bar"]+                , _f5 = [1,2,3,4,5]+                }+    it "allows a comment at the end of a line and file" $+        parse p "test" (unlines+            [ "f1 = \"foo\""+            , "f2 = 9001"+            , "f3 = True"+            , "f4 = [\"foo\",\"bar\"]"+            , "f5 = [1,2,3,4,5] # I'm a comment"+            ]) `shouldBe` Right defC+                { _f1 = "foo"+                , _f2 = 9001+                , _f3 = True+                , _f4 = ["foo","bar"]+                , _f5 = [1,2,3,4,5]+                }+    it "allows a newline at the top of a config file" $+        parse p "test" (unlines+            [ ""+            , "f1 = \"foo\""+            , "f2 = 9001"+            , "f3 = True"+            , "f4 = [\"foo\",\"bar\"]"+            , "f5 = [1,2,3,4,5]"+            ]) `shouldBe` Right defC+                { _f1 = "foo"+                , _f2 = 9001+                , _f3 = True+                , _f4 = ["foo","bar"]+                , _f5 = [1,2,3,4,5]+                }+    it "allows a newline in the middle of a config file" $+        parse p "test" (unlines+            [ "f1 = \"foo\""+            , "f2 = 9001"+            , "f3 = True"+            , ""+            , "f4 = [\"foo\",\"bar\"]"+            , "f5 = [1,2,3,4,5]"+            ]) `shouldBe` Right defC+                { _f1 = "foo"+                , _f2 = 9001+                , _f3 = True+                , _f4 = ["foo","bar"]+                , _f5 = [1,2,3,4,5]+                }+    it "allows a newline at the end of a config file" $+        parse p "test" (unlines+            [ "f1 = \"foo\""+            , "f2 = 9001"+            , "f3 = True"+            , "f4 = [\"foo\",\"bar\"]"+            , "f5 = [1,2,3,4,5]"+            , ""+            ]) `shouldBe` Right defC+                { _f1 = "foo"+                , _f2 = 9001+                , _f3 = True+                , _f4 = ["foo","bar"]+                , _f5 = [1,2,3,4,5]+                }+    it "allows whitespace at the start of a line" $+        parse p "test" (unlines+            [ "f1 = \"foo\""+            , "f2 = 9001"+            , "   \r\v\tf3 = True"+            , "f4 = [\"foo\",\"bar\"]"+            , "f5 = [1,2,3,4,5]"+            ]) `shouldBe` Right defC+                { _f1 = "foo"+                , _f2 = 9001+                , _f3 = True+                , _f4 = ["foo","bar"]+                , _f5 = [1,2,3,4,5]+                }+    it "allows whitespace at the start of a line and file" $+        parse p "test" (unlines+            [ "   \r\v\tf1 = \"foo\""+            , "f2 = 9001"+            , "f3 = True"+            , "f4 = [\"foo\",\"bar\"]"+            , "f5 = [1,2,3,4,5]"+            ]) `shouldBe` Right defC+                { _f1 = "foo"+                , _f2 = 9001+                , _f3 = True+                , _f4 = ["foo","bar"]+                , _f5 = [1,2,3,4,5]+                }+    it "allows whitespace at the end of a line" $+        parse p "test" (unlines+            [ "f1 = \"foo\""+            , "f2 = 9001"+            , "f3 = True   \r\v\t"+            , "f4 = [\"foo\",\"bar\"]"+            , "f5 = [1,2,3,4,5]"+            ]) `shouldBe` Right defC+                { _f1 = "foo"+                , _f2 = 9001+                , _f3 = True+                , _f4 = ["foo","bar"]+                , _f5 = [1,2,3,4,5]+                }+    it "allows whitespace at the end of a line and file" $+        parse p "test" (unlines+            [ "f1 = \"foo\""+            , "f2 = 9001"+            , "f3 = True"+            , "f4 = [\"foo\",\"bar\"]"+            , "f5 = [1,2,3,4,5]   \r\v\t"+            ]) `shouldBe` Right defC+                { _f1 = "foo"+                , _f2 = 9001+                , _f3 = True+                , _f4 = ["foo","bar"]+                , _f5 = [1,2,3,4,5]+                }+    it "allows empty lines at the start of a file" $+        parse p "test" (unlines+            [ "   \r\v\t"+            , ""+            , ""+            , "f1 = \"foo\""+            , "f2 = 9001"+            , "f3 = True"+            , "f4 = [\"foo\",\"bar\"]"+            , "f5 = [1,2,3,4,5]"+            ]) `shouldBe` Right defC+                { _f1 = "foo"+                , _f2 = 9001+                , _f3 = True+                , _f4 = ["foo","bar"]+                , _f5 = [1,2,3,4,5]+                }+    it "allows empty lines in the middle of a file" $+        parse p "test" (unlines+            [ "f1 = \"foo\""+            , "f2 = 9001"+            , "   \r\v\t"+            , ""+            , ""+            , "f3 = True"+            , "f4 = [\"foo\",\"bar\"]"+            , "f5 = [1,2,3,4,5]"+            ]) `shouldBe` Right defC+                { _f1 = "foo"+                , _f2 = 9001+                , _f3 = True+                , _f4 = ["foo","bar"]+                , _f5 = [1,2,3,4,5]+                }+    it "allows empty lines at the end of a file" $+        parse p "test" (unlines+            [ "f1 = \"foo\""+            , "f2 = 9001"+            , "f3 = True"+            , "f4 = [\"foo\",\"bar\"]"+            , "f5 = [1,2,3,4,5]"+            , ""+            , ""+            , "   \r\v\t"+            ]) `shouldBe` Right defC+                { _f1 = "foo"+                , _f2 = 9001+                , _f3 = True+                , _f4 = ["foo","bar"]+                , _f5 = [1,2,3,4,5]+                }+    it "parses an alternative key-value syntax" $+        parse p' "test" (unlines+            [ "\"foo\"           -> f1"+            , "9001              -> f2"+            , "True              -> f3"+            , "[\"foo\",\"bar\"] -> f4"+            , "[1,2,3,4,5]       -> f5"+            ]) `shouldBe` Right defC+                { _f1 = "foo"+                , _f2 = 9001+                , _f3 = True+                , _f4 = ["foo","bar"]+                , _f5 = [1,2,3,4,5]+                }+    it "parses an alternative comment syntax" $+        parse p' "test" (unlines+            [ "\"foo\"           -> f1"+            , "9001              -> f2"+            , "True              -> f3"+            , "-- This is a comment"+            , "[\"foo\",\"bar\"] -> f4"+            , "[1,2,3,4,5]       -> f5"+            ]) `shouldBe` Right defC+                { _f1 = "foo"+                , _f2 = 9001+                , _f3 = True+                , _f4 = ["foo","bar"]+                , _f5 = [1,2,3,4,5]+                }+    it "parses an multiple comment syntaxes" $+        parse p' "test" (unlines+            [ "\"foo\"           -> f1"+            , "9001              -> f2"+            , "True              -> f3"+            , "// This is a comment"+            , "[\"foo\",\"bar\"] -> f4"+            , "[1,2,3,4,5]       -> f5"+            ]) `shouldBe` Right defC+                { _f1 = "foo"+                , _f2 = 9001+                , _f3 = True+                , _f4 = ["foo","bar"]+                , _f5 = [1,2,3,4,5]+                }+    it "errors on non-existent keys" $+        parse p "test" (unlines+            [ "f1 = \"foo\""+            , "f2 = 9001"+            , "f3 = True   "+            , "f4 = [\"foo\",\"bar\"]"+            , "f5 = [1,2,3,4,5]"+            , "badkey = 9999"+            ]) `shouldFailOnLine` 6