htoml (empty) → 0.1.0.0
raw patch · 126 files changed
+2083/−0 lines, 126 filesdep +Cabaldep +aesondep +basesetup-changed
Dependencies added: Cabal, aeson, base, bytestring, containers, criterion, file-embed, htoml, old-locale, parsec, tasty, tasty-hspec, tasty-hunit, text, time, unordered-containers, vector
Files
- LICENSE +30/−0
- README.md +108/−0
- Setup.lhs +8/−0
- benchmarks/Benchmarks.hs +29/−0
- benchmarks/example.toml +48/−0
- benchmarks/repeated.toml +194/−0
- htoml.cabal +113/−0
- src/Text/Toml.hs +15/−0
- src/Text/Toml/Parser.hs +265/−0
- src/Text/Toml/Types.hs +178/−0
- test/BurntSushi.hs +62/−0
- test/BurntSushi/fetch-toml-tests.sh +13/−0
- test/BurntSushi/invalid/array-mixed-types-arrays-and-ints.toml +1/−0
- test/BurntSushi/invalid/array-mixed-types-ints-and-floats.toml +1/−0
- test/BurntSushi/invalid/array-mixed-types-strings-and-ints.toml +1/−0
- test/BurntSushi/invalid/datetime-malformed-no-leads.toml +1/−0
- test/BurntSushi/invalid/datetime-malformed-no-secs.toml +1/−0
- test/BurntSushi/invalid/datetime-malformed-no-t.toml +1/−0
- test/BurntSushi/invalid/datetime-malformed-no-z.toml +1/−0
- test/BurntSushi/invalid/datetime-malformed-with-milli.toml +1/−0
- test/BurntSushi/invalid/duplicate-key-table.toml +5/−0
- test/BurntSushi/invalid/duplicate-keys.toml +2/−0
- test/BurntSushi/invalid/duplicate-tables.toml +2/−0
- test/BurntSushi/invalid/empty-implicit-table.toml +1/−0
- test/BurntSushi/invalid/empty-table.toml +1/−0
- test/BurntSushi/invalid/float-no-leading-zero.toml +2/−0
- test/BurntSushi/invalid/float-no-trailing-digits.toml +2/−0
- test/BurntSushi/invalid/key-empty.toml +1/−0
- test/BurntSushi/invalid/key-hash.toml +1/−0
- test/BurntSushi/invalid/key-newline.toml +2/−0
- test/BurntSushi/invalid/key-open-bracket.toml +1/−0
- test/BurntSushi/invalid/key-single-open-bracket.toml +1/−0
- test/BurntSushi/invalid/key-start-bracket.toml +3/−0
- test/BurntSushi/invalid/key-two-equals.toml +1/−0
- test/BurntSushi/invalid/string-bad-byte-escape.toml +1/−0
- test/BurntSushi/invalid/string-bad-escape.toml +1/−0
- test/BurntSushi/invalid/string-byte-escapes.toml +1/−0
- test/BurntSushi/invalid/string-no-close.toml +1/−0
- test/BurntSushi/invalid/table-array-implicit.toml +14/−0
- test/BurntSushi/invalid/table-array-malformed-bracket.toml +2/−0
- test/BurntSushi/invalid/table-array-malformed-empty.toml +2/−0
- test/BurntSushi/invalid/table-empty.toml +1/−0
- test/BurntSushi/invalid/table-nested-brackets-close.toml +2/−0
- test/BurntSushi/invalid/table-nested-brackets-open.toml +2/−0
- test/BurntSushi/invalid/text-after-array-entries.toml +4/−0
- test/BurntSushi/invalid/text-after-integer.toml +1/−0
- test/BurntSushi/invalid/text-after-string.toml +1/−0
- test/BurntSushi/invalid/text-after-table.toml +1/−0
- test/BurntSushi/invalid/text-before-array-separator.toml +4/−0
- test/BurntSushi/invalid/text-in-array.toml +5/−0
- test/BurntSushi/valid/array-empty.json +11/−0
- test/BurntSushi/valid/array-empty.toml +1/−0
- test/BurntSushi/valid/array-nospaces.json +10/−0
- test/BurntSushi/valid/array-nospaces.toml +1/−0
- test/BurntSushi/valid/arrays-hetergeneous.json +19/−0
- test/BurntSushi/valid/arrays-hetergeneous.toml +1/−0
- test/BurntSushi/valid/arrays-nested.json +13/−0
- test/BurntSushi/valid/arrays-nested.toml +1/−0
- test/BurntSushi/valid/arrays.json +34/−0
- test/BurntSushi/valid/arrays.toml +8/−0
- test/BurntSushi/valid/bool.json +4/−0
- test/BurntSushi/valid/bool.toml +2/−0
- test/BurntSushi/valid/comments-everywhere.json +12/−0
- test/BurntSushi/valid/comments-everywhere.toml +24/−0
- test/BurntSushi/valid/datetime.json +3/−0
- test/BurntSushi/valid/datetime.toml +1/−0
- test/BurntSushi/valid/empty.json +1/−0
- test/BurntSushi/valid/empty.toml +0/−0
- test/BurntSushi/valid/example.json +14/−0
- test/BurntSushi/valid/example.toml +5/−0
- test/BurntSushi/valid/float.json +4/−0
- test/BurntSushi/valid/float.toml +2/−0
- test/BurntSushi/valid/implicit-and-explicit-after.json +10/−0
- test/BurntSushi/valid/implicit-and-explicit-after.toml +5/−0
- test/BurntSushi/valid/implicit-and-explicit-before.json +10/−0
- test/BurntSushi/valid/implicit-and-explicit-before.toml +5/−0
- test/BurntSushi/valid/implicit-groups.json +9/−0
- test/BurntSushi/valid/implicit-groups.toml +2/−0
- test/BurntSushi/valid/integer.json +4/−0
- test/BurntSushi/valid/integer.toml +2/−0
- test/BurntSushi/valid/key-equals-nospace.json +3/−0
- test/BurntSushi/valid/key-equals-nospace.toml +1/−0
- test/BurntSushi/valid/key-space.json +3/−0
- test/BurntSushi/valid/key-space.toml +1/−0
- test/BurntSushi/valid/key-special-chars.json +5/−0
- test/BurntSushi/valid/key-special-chars.toml +1/−0
- test/BurntSushi/valid/long-float.json +4/−0
- test/BurntSushi/valid/long-float.toml +2/−0
- test/BurntSushi/valid/long-integer.json +4/−0
- test/BurntSushi/valid/long-integer.toml +2/−0
- test/BurntSushi/valid/multiline-string.json +30/−0
- test/BurntSushi/valid/multiline-string.toml +23/−0
- test/BurntSushi/valid/raw-multiline-string.json +14/−0
- test/BurntSushi/valid/raw-multiline-string.toml +9/−0
- test/BurntSushi/valid/raw-string.json +30/−0
- test/BurntSushi/valid/raw-string.toml +7/−0
- test/BurntSushi/valid/string-empty.json +6/−0
- test/BurntSushi/valid/string-empty.toml +1/−0
- test/BurntSushi/valid/string-escapes.json +34/−0
- test/BurntSushi/valid/string-escapes.toml +8/−0
- test/BurntSushi/valid/string-simple.json +6/−0
- test/BurntSushi/valid/string-simple.toml +1/−0
- test/BurntSushi/valid/string-with-pound.json +7/−0
- test/BurntSushi/valid/string-with-pound.toml +2/−0
- test/BurntSushi/valid/table-array-implicit.json +7/−0
- test/BurntSushi/valid/table-array-implicit.toml +2/−0
- test/BurntSushi/valid/table-array-many.json +16/−0
- test/BurntSushi/valid/table-array-many.toml +11/−0
- test/BurntSushi/valid/table-array-nest.json +18/−0
- test/BurntSushi/valid/table-array-nest.toml +17/−0
- test/BurntSushi/valid/table-array-one.json +8/−0
- test/BurntSushi/valid/table-array-one.toml +3/−0
- test/BurntSushi/valid/table-empty.json +3/−0
- test/BurntSushi/valid/table-empty.toml +1/−0
- test/BurntSushi/valid/table-sub-empty.json +3/−0
- test/BurntSushi/valid/table-sub-empty.toml +2/−0
- test/BurntSushi/valid/table-whitespace.json +3/−0
- test/BurntSushi/valid/table-whitespace.toml +1/−0
- test/BurntSushi/valid/table-with-pound.json +5/−0
- test/BurntSushi/valid/table-with-pound.toml +2/−0
- test/BurntSushi/valid/unicode-escape.json +3/−0
- test/BurntSushi/valid/unicode-escape.toml +1/−0
- test/BurntSushi/valid/unicode-literal.json +3/−0
- test/BurntSushi/valid/unicode-literal.toml +1/−0
- test/Test.hs +20/−0
- test/Text/Toml/Parser/Spec.hs +392/−0
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright (c) 2013-2014, Spiros Eliopoulos++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 author nor the names of his contributors+ may be used to endorse or promote products derived from this software+ without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE 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 AUTHORS 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.
+ README.md view
@@ -0,0 +1,108 @@+htoml+=====++A [TOML](https://github.com/mojombo/toml) parser library in+[Haskell](http://haskell-lang.org).++TOML is the obvious, minimal configuration language by+[Tom Preston-Werner](https://github.com/mojombo).+It is an alternative to the [XML](http://www.w3.org/TR/REC-xml/),+[YAML](http://www.yaml.org/spec/1.2/spec.html) and+[INI](http://en.wikipedia.org/wiki/INI_file) formats for the purpose of+configuration files, as the first two are too heavy for that prupose,+and the latter is underspecified.+Toml is to configuration files, like what Markdown is for rich-text.++This library aims to be compatible with the latest version of the+[TOML spec](https://github.com/mojombo/toml), currently that is+[v0.3.1](https://github.com/toml-lang/toml/releases/tag/v0.3.1).++The documentation for this package may (or may not) be found on+[Hackage](https://hackage.haskell.org/package/htoml).+++### Quick start++To quickly show some features of `htoml` we use `GHCi`, from the+root of the repository run:++ cabal repl++It picks up configuration from the `.ghci` file, so we can immediately+start exploring:++ > txt <- readFile "benchmarks/example.toml"+ > let r = parseTomlDoc "" txt+ > r+ Right (fromList [("database",NTable (fromList [("enabled",NTValue (VBoolean True) [...]++ > let Right toml = r+ > toJSON toml+ Object (fromList [("database",Object (fromList [("enabled",Bool True) [...]++ > let Left error = parseTomlDoc "" "== invalid toml =="+ > error+ (line 1, column 1):+ unexpected '='+ expecting "#", "[" or end of input++First notice that some outputs are truncated.+++### Tests and benchmarks++The test suite is build by default, `cabal configure --disable-tests` disables them.+The benchmark suite is not run by default, `cabal configure --enable-benchmarks` enables them.++With `cabal build` both of these suites are build as executables and+put somewhere in `dist/`. Passing `--help` to them will reveal their+options.++[BurntSushi's language agnostic test suite](https://github.com/BurntSushi/toml-test)+is embedded in the test suite executable. Using a shell script (that+lives in `test/BurntSushi`) the latest tests can be fetched from+BurntSushi's repository.+++### Contributions++Most welcome! Please raise issues, start discussions, give comments or+submit pull-requests.+This is one of the first Haskell libraries I wrote, any feedback is+much appreciated.+++### Features++* Follows the latest version of the TOML spec, proven by an extensive test suite+* Incorporates [BurntSushi's language agnostic test suite](https://github.com/BurntSushi/toml-test)+* Has an internal representation that easily maps to JSON+* Provides a JSON interface (suggested by Greg Weber)+* Useful error messages (thanks to using Parsec over Attoparsec)+* Understands arrays as described in [this issue](https://github.com/toml-lang/toml/issues/254)+* Provides a benchmark suite+* Fails on mix-type arrays (as per spec)+* Haddock documentation+++### Todo++* Release a stable 1.0 release and submit it to [Stackage](http://stackage.org)+* More documentation+* Moke all tests pass (currently some more obscure corner cases don't pass)+* Add more tests (maybe find a more mature TOML parser and steal their tests)+* Add property tests with QuickCheck (the internet says it's possible for parsers)+* Extensively test error cases+* Try using Vector instead of List (measure performance increase with the benchmarks)+* See how lenses may (or may not) fit into this package+++### Acknoledgements++Originally this project started off by improving the `toml` package by+Spiros Eliopoulos.+++### License++BSD3 as found in the `LICENSE` file.
+ Setup.lhs view
@@ -0,0 +1,8 @@+#!/usr/bin/env runghc++> module Main where++> import Distribution.Simple++> main :: IO ()+> main = defaultMain
+ benchmarks/Benchmarks.hs view
@@ -0,0 +1,29 @@+{-# LANGUAGE OverloadedStrings #-}++import Prelude hiding (readFile)++import Criterion.Main+import Data.Text.IO (readFile)++import Text.Toml+++main :: IO ()+main = do+ exampleToml <- readFile "./benchmarks/example.toml"+ repeatedToml <- readFile "./benchmarks/repeated.toml"+ defaultMain++ [ bgroup "string"+ [ bench "assignment" $ whnf (parseTomlDoc "") "q=42\nqa=[4,2,]\nqb=true\nqf=23.23\n\+ \qd=1979-05-27T07:32:00Z\nqs='forty-two'"+ , bench "headers" $ whnf (parseTomlDoc "") "[Q]\n[A]\n[[QA]]\n[[QA]]\n[[QA]]\n[[QA]]"+ , bench "mixed" $ whnf (parseTomlDoc "") "q=42\nqa=[4,2,]\n[Q]qq=42\n[[QA]]\n[[QA]]"+ ]++ , bgroup "file"+ [ bench "example" $ whnf (parseTomlDoc "") exampleToml+ , bench "repeated-4x" $ whnf (parseTomlDoc "") repeatedToml+ ]++ ]
+ benchmarks/example.toml view
@@ -0,0 +1,48 @@+# This is a TOML document. Boom.++title = "TOML Example"++[owner]+name = "Tom Preston-Werner"+organization = "GitHub"+bio = "GitHub Cofounder & CEO\nLikes tater tots and beer."+dob = 1979-05-27T07:32:00Z # First class dates? Why not?++[database]+server = "192.168.1.1"+ports = [ 8001, 8001, 8002 ]+connection_max = 5000+enabled = true++[servers1]++ # You can indent as you please. Tabs or spaces. TOML don't care.+ [servers.alpha]+ ip = "10.0.0.1"+ dc = "eqdc10"++ [servers.beta]+ ip = "10.0.0.2"+ dc = "eqdc10"+ country = "中国" # This should be parsed as UTF-8++[clients]+data = [ ["gamma", "delta"], [1, 2] ] # just an update to make sure parsers support it+++# Line breaks are OK when inside arrays+hosts = [+ "alpha",+ "omega"+]++# Products++ [[products]]+ name = "Hammer"+ sku = 738594937++ [[products]]+ name = "Nail"+ sku = 284758393+ color = "gray"
+ benchmarks/repeated.toml view
@@ -0,0 +1,194 @@+# This is a TOML document. Boom.++title = "TOML Example"++[owner]+name = "Tom Preston-Werner"+organization = "GitHub"+bio = "GitHub Cofounder & CEO\nLikes tater tots and beer."+dob = 1979-05-27T07:32:00Z # First class dates? Why not?++[database]+server = "192.168.1.1"+ports = [ 8001, 8001, 8002 ]+connection_max = 5000+enabled = true++[servers1]++ # You can indent as you please. Tabs or spaces. TOML don't care.+ [servers.alpha]+ ip = "10.0.0.1"+ dc = "eqdc10"++ [servers.beta]+ ip = "10.0.0.2"+ dc = "eqdc10"+ country = "中国" # This should be parsed as UTF-8++[clients]+data = [ ["gamma", "delta"], [1, 2] ] # just an update to make sure parsers support it+++# Line breaks are OK when inside arrays+hosts = [+ "alpha",+ "omega"+]++# Products++ [[products]]+ name = "Hammer"+ sku = 738594937++ [[products]]+ name = "Nail"+ sku = 284758393+ color = "gray"+++# This is a TOML document. Boom.++[owner2]+name = "Tom Preston-Werner"+organization = "GitHub"+bio = "GitHub Cofounder & CEO\nLikes tater tots and beer."+dob = 1979-05-27T07:32:00Z # First class dates? Why not?++[database2]+server = "192.168.1.1"+ports = [ 8001, 8001, 8002 ]+connection_max = 5000+enabled = true++[servers2]++ # You can indent as you please. Tabs or spaces. TOML don't care.+ [servers.alpha2]+ ip = "10.0.0.1"+ dc = "eqdc10"++ [servers.beta2]+ ip = "10.0.0.2"+ dc = "eqdc10"+ country = "中国" # This should be parsed as UTF-8++[clients2]+data = [ ["gamma", "delta"], [1, 2] ] # just an update to make sure parsers support it+++# Line breaks are OK when inside arrays+hosts = [+ "alpha",+ "omega"+]++# Products++ [[products2]]+ name = "Hammer"+ sku = 738594937++ [[products2]]+ name = "Nail"+ sku = 284758393+ color = "gray"++++# This is a TOML document. Boom.++[owner3]+name = "Tom Preston-Werner"+organization = "GitHub"+bio = "GitHub Cofounder & CEO\nLikes tater tots and beer."+dob = 1979-05-27T07:32:00Z # First class dates? Why not?++[database3]+server = "192.168.1.1"+ports = [ 8001, 8001, 8002 ]+connection_max = 5000+enabled = true++[servers3]++ # You can indent as you please. Tabs or spaces. TOML don't care.+ [servers.alpha3]+ ip = "10.0.0.1"+ dc = "eqdc10"++ [servers.beta3]+ ip = "10.0.0.2"+ dc = "eqdc10"+ country = "中国" # This should be parsed as UTF-8++[clients3]+data = [ ["gamma", "delta"], [1, 3] ] # just an update to make sure parsers support it+++# Line breaks are OK when inside arrays+hosts = [+ "alpha",+ "omega"+]++# Products++ [[products3]]+ name = "Hammer"+ sku = 738594937++ [[products3]]+ name = "Nail"+ sku = 284758393+ color = "gray"++++# This is a TOML document. Boom.++[owner4]+name = "Tom Preston-Werner"+organization = "GitHub"+bio = "GitHub Cofounder & CEO\nLikes tater tots and beer."+dob = 1979-05-27T07:32:00Z # First class dates? Why not?++[database4]+server = "192.168.1.1"+ports = [ 8001, 8001, 8002 ]+connection_max = 5000+enabled = true++[servers4]++ # You can indent as you please. Tabs or spaces. TOML don't care.+ [servers.alpha4]+ ip = "10.0.0.1"+ dc = "eqdc10"++ [servers.beta4]+ ip = "10.0.0.2"+ dc = "eqdc10"+ country = "中国" # This should be parsed as UTF-8++[clients4]+data = [ ["gamma", "delta"], [1, 4] ] # just an update to make sure parsers support it+++# Line breaks are OK when inside arrays+hosts = [+ "alpha",+ "omega"+]++# Products++ [[products4]]+ name = "Hammer"+ sku = 738594937++ [[products4]]+ name = "Nail"+ sku = 284758393+ color = "gray"
+ htoml.cabal view
@@ -0,0 +1,113 @@+name: htoml+version: 0.1.0.0+synopsis: A parser for TOML files.+description: TOML is a obvious and minimal format for config files.+ .+ This package provides a TOML parser,+ build with the Parsec library of parser combinators.+ .+ It provides a JSON interface using the Aeson library.+homepage: https://github.com/cies/htoml+bug-reports: https://github.com/cies/htoml/issues+license: BSD3+license-file: LICENSE+author: Cies Breijs+maintainer: Cies Breijs+category: Text+build-type: Simple+cabal-version: >= 1.10+extra-source-files: README.md+ , test/BurntSushi/fetch-toml-tests.sh+ , test/BurntSushi/valid/*.toml+ , test/BurntSushi/valid/*.json+ , test/BurntSushi/invalid/*.toml+ , benchmarks/example.toml+ , benchmarks/repeated.toml+++source-repository head+ type: git+ location: https://github.com/cies/htoml.git++library+ exposed-modules: Text.Toml+ , Text.Toml.Parser+ , Text.Toml.Types+ ghc-options: -Wall+ hs-source-dirs: src+ default-language: Haskell2010+ build-depends: base >= 4.6 && < 5+ , parsec >= 3.1.5+ , containers >= 0.5 && < 0.6+ , unordered-containers+ , vector+ , aeson+ , text+ , time -any+ , old-locale -any++executable tests+ hs-source-dirs: test+ ghc-options: -Wall+ main-is: Test.hs+ other-modules: BurntSushi+ , BurntSushi+ , Text.Toml.Parser.Spec+ default-language: Haskell2010+ build-depends: base >= 4.6 && < 5+ , parsec >= 3.1.5+ , containers >= 0.5 && < 0.6+ , bytestring+ , file-embed+ , unordered-containers+ , vector+ , aeson+ , text+ , time -any+ , Cabal >= 1.16.0+ , htoml+ , tasty+ , tasty-hspec+ , tasty-hunit++test-suite Tests+ hs-source-dirs: test+ ghc-options: -Wall+ main-is: Test.hs+ other-modules: BurntSushi+ , BurntSushi+ , Text.Toml.Parser.Spec+ type: exitcode-stdio-1.0+ default-language: Haskell2010+ build-depends: base >= 4.6 && < 5+ , parsec >= 3.1.5+ , containers >= 0.5 && < 0.6+ , bytestring+ , file-embed+ , unordered-containers+ , vector+ , text+ , time -any+ , Cabal >= 1.16.0+ , htoml+ , tasty+ , tasty-hspec+ , tasty-hunit++benchmark benchmarks+ hs-source-dirs: benchmarks .+ ghc-options: -O2 -Wall -rtsopts+ main-is: Benchmarks.hs+ type: exitcode-stdio-1.0+ default-language: Haskell2010+ build-depends: base >= 4.6 && < 5+ , parsec >= 3.1.5+ , containers >= 0.5 && < 0.6+ , unordered-containers+ , vector+ , aeson+ , text+ , time -any+ , Cabal >= 1.16.0+ , htoml+ , criterion
+ src/Text/Toml.hs view
@@ -0,0 +1,15 @@+module Text.Toml where++import Prelude hiding (readFile)++import Data.Text (Text)+import Text.Parsec++import Text.Toml.Parser+++-- | Parse a 'Text' that results in 'Either' a 'String'+-- containing the error message, or an internal representation+-- of the document in the 'Toml' data type.+parseTomlDoc :: String -> Text -> Either ParseError Table+parseTomlDoc inputName input = parse tomlDoc inputName input
+ src/Text/Toml/Parser.hs view
@@ -0,0 +1,265 @@+{-# LANGUAGE OverloadedStrings #-}++module Text.Toml.Parser+ ( module Text.Toml.Parser+ , module Text.Toml.Types+ ) where+++import Prelude hiding (concat, takeWhile)++import Control.Applicative hiding (many, optional, (<|>))+import qualified Data.HashMap.Strict as M+import qualified Data.List as L+import qualified Data.Set as S+import Data.Text (Text, concat, pack, unpack)+import Data.Time.Format (parseTime)+import Numeric (readHex)+import System.Locale (defaultTimeLocale, iso8601DateFormat)+import Text.Parsec+import Text.Parsec.Text++import Text.Toml.Types++++-- | Convenience function for the test suite and GHCI.+parseOnly :: Parser a -> Text -> Either ParseError a+parseOnly p str = parse (p <* eof) "test" str+++-- | Parses a complete document formatted according to the TOML spec.+tomlDoc :: Parser Table+tomlDoc = do+ skipBlanks+ topTable <- table+ namedSections <- many namedSection+ eof -- ensures input is completely consumed+ case join topTable (reverse namedSections) of+ Left msg -> fail (unpack msg) -- TODO: allow Text in Parse Errors+ Right r -> return $ r+ where+ join tbl [] = Right tbl+ join tbl (x:xs) = case join tbl xs of Left msg -> Left msg+ Right r -> insert x r+++-- | Parses a table of key-value pairs.+table :: Parser Table+table = do+ pairs <- try (many (assignment <* skipBlanks)) <|> (try skipBlanks >> return [])+ case hasDup (map fst pairs) of+ Just k -> fail $ "Cannot redefine key " ++ (unpack k)+ Nothing -> return $ M.fromList (map (\(k, v) -> (k, NTValue v)) pairs)+ where+ hasDup :: Ord a => [a] -> Maybe a+ hasDup xs = dup' xs S.empty+ dup' [] _ = Nothing+ dup' (x:xs) s = if S.member x s then Just x else dup' xs (S.insert x s)+++-- | Parses a 'Table' or 'TableArray' with its header.+-- The resulting tuple has the header's value in the first position, and the+-- 'NTable' or 'NTArray' in the second.+namedSection :: Parser ([Text], Node)+namedSection = do+ eitherHdr <- try (Left <$> tableHeader) <|> try (Right <$> tableArrayHeader)+ skipBlanks+ tbl <- table+ skipBlanks+ return $ case eitherHdr of Left ns -> (ns, NTable tbl )+ Right ns -> (ns, NTArray [tbl])+++-- | Parses a table header.+tableHeader :: Parser [Text]+tableHeader = between (char '[') (char ']') headerValue+++-- | Parses a table array header.+tableArrayHeader :: Parser [Text]+tableArrayHeader = between (twoChar '[') (twoChar ']') headerValue+ where+ twoChar c = count 2 (char c)+++-- | Parses the value of any header (names separated by dots), into a list of 'Text'.+headerValue :: Parser [Text]+headerValue = (pack <$> many1 headerNameChar) `sepBy1` (char '.')+ where+ headerNameChar = satisfy (\c -> c /= ' ' && c /= '\t' && c /= '\n' &&+ c /= '[' && c /= ']' && c /= '.' && c /= '#')+++-- | Parses a key-value assignment.+assignment :: Parser (Text, TValue)+assignment = do+ k <- pack <$> many1 keyChar+ skipBlanks >> char '=' >> skipBlanks+ v <- value+ return (k, v)+ where+ -- TODO: Follow the spec, e.g.: only first char cannot be '['.+ keyChar = satisfy (\c -> c /= ' ' && c /= '\t' && c /= '\n' &&+ c /= '=' && c /= '#' && c /= '[')+++-- | Parses a value.+value :: Parser TValue+value = (try array <?> "array")+ <|> (try boolean <?> "boolean")+ <|> (try anyStr <?> "string")+ <|> (try datetime <?> "datetime")+ <|> (try float <?> "float")+ <|> (try integer <?> "integer")+++--+-- | * Toml value parsers+--++array :: Parser TValue+array = (try (arrayOf array) <?> "array of arrays")+ <|> (try (arrayOf boolean) <?> "array of booleans")+ <|> (try (arrayOf anyStr) <?> "array of strings")+ <|> (try (arrayOf datetime) <?> "array of datetimes")+ <|> (try (arrayOf float) <?> "array of floats")+ <|> (try (arrayOf integer) <?> "array of integers")+++boolean :: Parser TValue+boolean = VBoolean <$> ( (try . string $ "true") *> return True <|>+ (try . string $ "false") *> return False )+++anyStr :: Parser TValue+anyStr = try multiBasicStr <|> try basicStr <|> try multiLiteralStr <|> try literalStr+++basicStr :: Parser TValue+basicStr = VString <$> between dQuote dQuote (fmap pack $ many strChar)+ where+ strChar = try escSeq <|> try (satisfy (\c -> c /= '"' && c /= '\\'))+ dQuote = char '\"'+++multiBasicStr :: Parser TValue+multiBasicStr = VString <$> (openDQuote3 *> (fmap pack $ manyTill strChar dQuote3))+ where+ -- | Parse the a tripple-double quote, with possibly a newline attached+ openDQuote3 = try (dQuote3 <* char '\n') <|> try dQuote3+ -- | Parse tripple-double quotes+ dQuote3 = count 3 $ char '"'+ -- | Parse a string char, accepting escaped codes, ignoring escaped white space+ strChar = escWhiteSpc *> (escSeq <|> (satisfy (/= '\\'))) <* escWhiteSpc+ -- | Parse escaped white space, if any+ escWhiteSpc = many $ char '\\' >> char '\n' >> (many $ satisfy (\c -> isSpc c || c == '\n'))+++literalStr :: Parser TValue+literalStr = VString <$> between sQuote sQuote (pack <$> many (satisfy (/= '\'')))+ where+ sQuote = char '\''+++multiLiteralStr :: Parser TValue+multiLiteralStr = VString <$> (openSQuote3 *> (fmap pack $ manyTill anyChar sQuote3))+ where+ -- | Parse the a tripple-single quote, with possibly a newline attached+ openSQuote3 = try (sQuote3 <* char '\n') <|> try sQuote3+ -- | Parse tripple-single quotes+ sQuote3 = try . count 3 . char $ '\''+++datetime :: Parser TValue+datetime = do+ d <- manyTill anyChar (try $ char 'Z')+ let mt = parseTime defaultTimeLocale (iso8601DateFormat $ Just "%X") d+ case mt of Just t -> return $ VDatetime t+ Nothing -> fail "parsing datetime failed"+++-- | Attoparsec 'double' parses scientific "e" notation; reimplement according to Toml spec.+float :: Parser TValue+float = VFloat <$> do+ n <- intStr+ char '.'+ d <- uintStr+ e <- try (satisfy (\c -> c == 'e' || c == 'E') *> intStr) <|> return "0"+ return . read . L.concat $ [n, ".", d, "e", e]+ where+ sign = try (string "-") <|> (try (char '+') >> return "") <|> return ""+ uintStr = many1 digit+ intStr = do s <- sign+ u <- uintStr+ return . L.concat $ [s, u]+++integer :: Parser TValue+integer = VInteger <$> (signed $ read <$> (many1 digit))++++--+-- * Utility functions+--++-- | Parses the elements of an array, while restricting them to a certain type.+arrayOf :: Parser TValue -> Parser TValue+arrayOf p = VArray <$> between (char '[') (char ']') (skipBlanks *> separatedValues)+ where+ separatedValues = sepEndBy (skipBlanks *> p <* skipBlanks) comma <* skipBlanks+ comma = skipBlanks >> char ',' >> skipBlanks+++-- | Parser for escape sequences.+escSeq :: Parser Char+escSeq = char '\\' *> escSeqChar+ where+ escSeqChar = try (char '"') *> return '"'+ <|> try (char '\\') *> return '\\'+ <|> try (char '/') *> return '/'+ <|> try (char 'b') *> return '\b'+ <|> try (char 't') *> return '\t'+ <|> try (char 'n') *> return '\n'+ <|> try (char 'f') *> return '\f'+ <|> try (char 'r') *> return '\r'+ <|> try (char 'u') *> unicodeHex 4+ <|> try (char 'U') *> unicodeHex 8+ <?> "escape character"+++-- | Parser for unicode hexadecimal values of representation length 'n'.+unicodeHex :: Int -> Parser Char+unicodeHex n = do+ h <- count n (satisfy isHex)+ let v = fst . head . readHex $ h+ return $ if v <= maxChar then toEnum v else '_'+ where+ isHex c = (c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')+ maxChar = fromEnum (maxBound :: Char)+++-- | Parser for signs (a plus or a minus).+signed :: Num a => Parser a -> Parser a+signed p = try (negate <$> (char '-' *> p))+ <|> try (char '+' *> p)+ <|> try p+++-- | Parses the (rest of the) line including an EOF, whitespace and comments.+skipBlanks :: Parser ()+skipBlanks = skipMany blank+ where+ blank = try ((many1 $ satisfy isSpc) >> return ()) <|> try comment <|> try eol+ comment = char '#' >> (many $ satisfy (/= '\n')) >> return ()+++-- | Results in 'True' for whitespace chars, tab or space, according to spec.+isSpc :: Char -> Bool+isSpc c = c == ' ' || c == '\t'+++-- | Parse an EOL, as per TOML spec this is 0x0A a.k.a. '\n' or 0x0D a.k.a. '\r'.+eol :: Parser ()+eol = satisfy (\c -> c == '\n' || c == '\r') >> return ()
+ src/Text/Toml/Types.hs view
@@ -0,0 +1,178 @@+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE OverloadedStrings #-}++module Text.Toml.Types where++import Data.Aeson.Types+import qualified Data.HashMap.Strict as M+import Data.Int (Int64)+import Data.List (intersect)+import Data.Text (Text)+import qualified Data.Text as T+import Data.Time.Clock (UTCTime)+import Data.Time.Format ()+import qualified Data.Vector as V+++-- | The 'Table' is a mapping ('HashMap') of 'Text' keys to 'Node' values.+type Table = M.HashMap Text Node+++-- | A 'Node' may contain a 'TValue', a 'Table' or a table array '[Table]'.+data Node = NTValue TValue+ | NTable Table+ | NTArray [Table]+ deriving (Eq, Show)+++-- | A 'TValue' may contain any type of value that can put in a 'VArray'.+data TValue = VString Text+ | VInteger Int64+ | VFloat Double+ | VBoolean Bool+ | VDatetime UTCTime+ | VArray [TValue]+ deriving (Eq, Show)+++-- | Contruct an empty 'Table'.+emptyTable :: Table+emptyTable = M.empty+++-- | Contruct an empty 'NTable'.+emptyNTable :: Node+emptyNTable = NTable M.empty+++-- | Inserts a table ('Table') with name ('[Text]') which may be part of+-- a table array (when 'Bool' is 'True') into a 'Table'.+-- It may result in an error ('Text') on the 'Left' or a modified table+-- on the 'Right'.+insert :: ([Text], Node) -> Table -> Either Text Table+insert ([], _) _ = error "FATAL: Cannot call 'insert' without a name."+insert (_ , NTValue _) _ = error "FATAL: Cannot call 'insert' with a TValue."+insert ([name], node) ttbl =+ -- In case 'name' is final+ case M.lookup name ttbl of+ Nothing -> Right $ M.insert name node ttbl+ Just (NTable t) -> case node of+ (NTable nt) -> case merge t nt of+ Left ds -> Left $ T.concat [ "Cannot redefine key(s) (", (T.intercalate ", " ds)+ , "), from table named '", name, "'." ]+ Right r -> Right $ M.insert name (NTable r) ttbl+ (NTArray _) -> commonInsertError node [name]+ Just (NTArray a) -> case node of+ (NTable _) -> commonInsertError node [name]+ (NTArray na) -> Right $ M.insert name (NTArray $ a ++ na) ttbl+ Just _ -> commonInsertError node [name]+insert (fullName@(name:ns), node) ttbl =+ -- In case 'name' is not final, but a sub-name+ case M.lookup name ttbl of+ Nothing -> case insert (ns, node) emptyTable of+ Left msg -> Left msg+ Right r -> Right $ M.insert name (NTable r) ttbl+ Just (NTable t) -> case insert (ns, node) t of+ Left msg -> Left msg+ Right tt -> Right $ M.insert name (NTable tt) ttbl+ Just (NTArray []) -> error "FATAL: Call to 'insert' found impossibly empty NTArray."+ Just (NTArray a) -> case insert (ns, node) (last a) of+ Left msg -> Left msg+ Right t -> Right $ M.insert name (NTArray $ (init a) ++ [t]) ttbl+ Just _ -> commonInsertError node fullName+++-- | Merge two tables, resulting in an error when overlapping keys are+-- found ('Left' will contian those keys). When no overlapping keys are+-- found the result will contain the union of both tables in a 'Right'.+merge :: Table -> Table -> Either [Text] Table+merge existing new = case intersect (M.keys existing) (M.keys new) of+ [] -> Right $ M.union existing new+ ds -> Left $ ds+++-- | Convenience function to construct a common error message for the 'insert' function.+commonInsertError :: Node -> [Text] -> Either Text Table+commonInsertError what name =+ let w = case what of (NTable _) -> "tables"+ _ -> "array of tables"+ n = T.intercalate "." name+ in Left $ T.concat ["Cannot insert ", w, " '", n, "' as key already exists."]++++-- * Regular ToJSON instances++-- | 'ToJSON' instances for the 'Node' type that produce Aeson (JSON)+-- in line with the TOML specification.+instance ToJSON Node where+ toJSON (NTValue v) = toJSON v+ toJSON (NTable v) = toJSON v+ toJSON (NTArray v) = toJSON v+++-- | 'ToJSON' instances for the 'TValue' type that produce Aeson (JSON)+-- in line with the TOML specification.+instance ToJSON TValue where+ toJSON (VString v) = toJSON v+ toJSON (VInteger v) = toJSON v+ toJSON (VFloat v) = toJSON v+ toJSON (VBoolean v) = toJSON v+ toJSON (VDatetime v) = toJSON v+ toJSON (VArray v) = toJSON v++++-- * Special BurntSushi ToJSON type class and instances++-- | Type class for conversion to BurntSushi-style JSON.+--+-- BurntSushi has made a language agnostic test suite available that+-- this library uses. This test suit expects that values are encoded+-- as JSON objects with a 'type' and a 'value' member.+class ToBsJSON a where+ toBsJSON :: a -> Value+++-- | Provide a 'toBsJSON' instance to the 'NTArray'.+instance (ToBsJSON a) => ToBsJSON [a] where+ toBsJSON = Array . V.fromList . map toBsJSON+ {-# INLINE toBsJSON #-}+++-- | Provide a 'toBsJSON' instance to the 'NTable'.+instance (ToBsJSON v) => ToBsJSON (M.HashMap Text v) where+ toBsJSON = Object . M.map toBsJSON+ {-# INLINE toBsJSON #-}+++-- | 'ToBsJSON' instances for the 'Node' type that produce Aeson (JSON)+-- in line with BurntSushi's language agnostic TOML test suite.+instance ToBsJSON Node where+ toBsJSON (NTValue v) = toBsJSON v+ toBsJSON (NTable v) = toBsJSON v+ toBsJSON (NTArray v) = toBsJSON v+++-- | 'ToBsJSON' instances for the 'TValue' type that produce Aeson (JSON)+-- in line with BurntSushi's language agnostic TOML test suite.+--+-- As seen in this function, BurntSushi's JSON encoding explicitly+-- specifies the types of the values.+instance ToBsJSON TValue where+ toBsJSON (VString v) = object [ "type" .= toJSON ("string" :: String)+ , "value" .= toJSON v ]+ toBsJSON (VInteger v) = object [ "type" .= toJSON ("integer" :: String)+ , "value" .= toJSON (show v) ]+ toBsJSON (VFloat v) = object [ "type" .= toJSON ("float" :: String)+ , "value" .= toJSON (show v) ]+ toBsJSON (VBoolean v) = object [ "type" .= toJSON ("bool" :: String)+ , "value" .= toJSON (if v then "true" else "false" :: String) ]+ toBsJSON (VDatetime v) = object [ "type" .= toJSON ("datetime" :: String)+ , "value" .= toJSON (let s = show v+ z = take (length s - 4) s ++ "Z"+ d = take (length z - 10) z+ t = drop (length z - 9) z+ in d ++ "T" ++ t) ]+ toBsJSON (VArray v) = object [ "type" .= toJSON ("array" :: String)+ , "value" .= toBsJSON v ]
+ test/BurntSushi.hs view
@@ -0,0 +1,62 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TemplateHaskell #-}++module BurntSushi (tests) where++import Test.Tasty (TestTree, testGroup)+import Test.Tasty.HUnit++import Data.Aeson+import qualified Data.ByteString as B+import Data.ByteString.Lazy (fromStrict)+import Data.FileEmbed+import Data.List (isPrefixOf, isSuffixOf)+import Data.Text.Encoding (decodeUtf8)++import Text.Toml+import Text.Toml.Types+++allFiles :: [(FilePath, B.ByteString)]+allFiles = $(embedDir "test/BurntSushi")+++validPairs :: [(String, (B.ByteString, B.ByteString))]+validPairs =+ map (\(tFP, tBS) -> (stripExt tFP, (tBS, jsonCounterpart tFP))) tomlFiles+ where+ validFiles = filter (\(f, _) -> "valid" `isPrefixOf` f) allFiles+ filterOnSuffix sfx = filter (\(f, _) -> sfx `isSuffixOf` f)+ tomlFiles = filterOnSuffix ".toml" validFiles+ jsonFiles = filterOnSuffix ".json" validFiles+ stripExt fp = take (length fp - 5) fp+ jsonCounterpart tFP =+ case filter (\(f, _) -> f == stripExt tFP ++ ".json") jsonFiles of+ [] -> error $ "Could not find a JSON counterpart for: " ++ tFP+ [(_, j)] -> j+ _ -> error $ "Expected one, but found several \+ \JSON counterparts for: " ++ tFP+++invalidTomlFiles :: [(FilePath, B.ByteString)]+invalidTomlFiles = filter (\(f, _) -> "invalid" `isPrefixOf` f) allFiles+++tests :: IO TestTree+tests = return $ testGroup "BurntSushi's test suite"+ [ testGroup "test equality of resulting JSON (valid)" $+ map (\(fp, (tBS, jBS)) -> testCase fp $ assertIsValid fp tBS jBS) validPairs+ , testGroup "test parse failures of malformed TOML files (invalid)" $+ map (\(fp, tBS) -> testCase fp $ assertParseFailure fp tBS) invalidTomlFiles+ ]+ where+ assertIsValid f tomlBS jsonBS =+ case parseTomlDoc "test" (decodeUtf8 tomlBS) of+ Left e -> assertFailure $ "Could not parse TOML file: " ++ f ++ ".toml\n" ++ (show e)+ Right tomlTry -> case eitherDecode (fromStrict jsonBS) of+ Left _ -> assertFailure $ "Could not parse JSON file: " ++ f ++ ".json"+ Right jsonCorrect -> assertEqual "" jsonCorrect (toBsJSON tomlTry)+ assertParseFailure f tomlBS =+ case parseTomlDoc "test" (decodeUtf8 tomlBS) of+ Left _ -> return ()+ Right _ -> assertFailure $ "Parser accepted invalid TOML file: " ++ f ++ ".toml"
+ test/BurntSushi/fetch-toml-tests.sh view
@@ -0,0 +1,13 @@+#!/bin/sh++rm valid/*+rmdir valid+rm invalid/*+rmdir invalid++git clone https://github.com/BurntSushi/toml-test++mv toml-test/tests/valid .+mv toml-test/tests/invalid .++rm -rf toml-test
+ test/BurntSushi/invalid/array-mixed-types-arrays-and-ints.toml view
@@ -0,0 +1,1 @@+arrays-and-ints = [1, ["Arrays are not integers."]]
+ test/BurntSushi/invalid/array-mixed-types-ints-and-floats.toml view
@@ -0,0 +1,1 @@+ints-and-floats = [1, 1.1]
+ test/BurntSushi/invalid/array-mixed-types-strings-and-ints.toml view
@@ -0,0 +1,1 @@+strings-and-ints = ["hi", 42]
+ test/BurntSushi/invalid/datetime-malformed-no-leads.toml view
@@ -0,0 +1,1 @@+no-leads = 1987-7-05T17:45:00Z
+ test/BurntSushi/invalid/datetime-malformed-no-secs.toml view
@@ -0,0 +1,1 @@+no-secs = 1987-07-05T17:45Z
+ test/BurntSushi/invalid/datetime-malformed-no-t.toml view
@@ -0,0 +1,1 @@+no-t = 1987-07-0517:45:00Z
+ test/BurntSushi/invalid/datetime-malformed-no-z.toml view
@@ -0,0 +1,1 @@+no-z = 1987-07-05T17:45:00
+ test/BurntSushi/invalid/datetime-malformed-with-milli.toml view
@@ -0,0 +1,1 @@+with-milli = 1987-07-5T17:45:00.12Z
+ test/BurntSushi/invalid/duplicate-key-table.toml view
@@ -0,0 +1,5 @@+[fruit]+type = "apple"++[fruit.type]+apple = "yes"
+ test/BurntSushi/invalid/duplicate-keys.toml view
@@ -0,0 +1,2 @@+dupe = false+dupe = true
+ test/BurntSushi/invalid/duplicate-tables.toml view
@@ -0,0 +1,2 @@+[a]+[a]
+ test/BurntSushi/invalid/empty-implicit-table.toml view
@@ -0,0 +1,1 @@+[naughty..naughty]
+ test/BurntSushi/invalid/empty-table.toml view
@@ -0,0 +1,1 @@+[]
+ test/BurntSushi/invalid/float-no-leading-zero.toml view
@@ -0,0 +1,2 @@+answer = .12345+neganswer = -.12345
+ test/BurntSushi/invalid/float-no-trailing-digits.toml view
@@ -0,0 +1,2 @@+answer = 1.+neganswer = -1.
+ test/BurntSushi/invalid/key-empty.toml view
@@ -0,0 +1,1 @@+ = 1
+ test/BurntSushi/invalid/key-hash.toml view
@@ -0,0 +1,1 @@+a# = 1
+ test/BurntSushi/invalid/key-newline.toml view
@@ -0,0 +1,2 @@+a+= 1
+ test/BurntSushi/invalid/key-open-bracket.toml view
@@ -0,0 +1,1 @@+[abc = 1
+ test/BurntSushi/invalid/key-single-open-bracket.toml view
@@ -0,0 +1,1 @@+[
+ test/BurntSushi/invalid/key-start-bracket.toml view
@@ -0,0 +1,3 @@+[a]+[xyz = 5+[b]
+ test/BurntSushi/invalid/key-two-equals.toml view
@@ -0,0 +1,1 @@+key= = 1
+ test/BurntSushi/invalid/string-bad-byte-escape.toml view
@@ -0,0 +1,1 @@+naughty = "\xAg"
+ test/BurntSushi/invalid/string-bad-escape.toml view
@@ -0,0 +1,1 @@+invalid-escape = "This string has a bad \a escape character."
+ test/BurntSushi/invalid/string-byte-escapes.toml view
@@ -0,0 +1,1 @@+answer = "\x33"
+ test/BurntSushi/invalid/string-no-close.toml view
@@ -0,0 +1,1 @@+no-ending-quote = "One time, at band camp
+ test/BurntSushi/invalid/table-array-implicit.toml view
@@ -0,0 +1,14 @@+# This test is a bit tricky. It should fail because the first use of+# `[[albums.songs]]` without first declaring `albums` implies that `albums`+# must be a table. The alternative would be quite weird. Namely, it wouldn't+# comply with the TOML spec: "Each double-bracketed sub-table will belong to +# the most *recently* defined table element *above* it."+#+# This is in contrast to the *valid* test, table-array-implicit where+# `[[albums.songs]]` works by itself, so long as `[[albums]]` isn't declared+# later. (Although, `[albums]` could be.)+[[albums.songs]]+name = "Glory Days"++[[albums]]+name = "Born in the USA"
+ test/BurntSushi/invalid/table-array-malformed-bracket.toml view
@@ -0,0 +1,2 @@+[[albums]+name = "Born to Run"
+ test/BurntSushi/invalid/table-array-malformed-empty.toml view
@@ -0,0 +1,2 @@+[[]]+name = "Born to Run"
+ test/BurntSushi/invalid/table-empty.toml view
@@ -0,0 +1,1 @@+[]
+ test/BurntSushi/invalid/table-nested-brackets-close.toml view
@@ -0,0 +1,2 @@+[a]b]+zyx = 42
+ test/BurntSushi/invalid/table-nested-brackets-open.toml view
@@ -0,0 +1,2 @@+[a[b]+zyx = 42
+ test/BurntSushi/invalid/text-after-array-entries.toml view
@@ -0,0 +1,4 @@+array = [+ "Is there life after an array separator?", No+ "Entry"+]
+ test/BurntSushi/invalid/text-after-integer.toml view
@@ -0,0 +1,1 @@+answer = 42 the ultimate answer?
+ test/BurntSushi/invalid/text-after-string.toml view
@@ -0,0 +1,1 @@+string = "Is there life after strings?" No.
+ test/BurntSushi/invalid/text-after-table.toml view
@@ -0,0 +1,1 @@+[error] this shouldn't be here
+ test/BurntSushi/invalid/text-before-array-separator.toml view
@@ -0,0 +1,4 @@+array = [+ "Is there life before an array separator?" No,+ "Entry"+]
+ test/BurntSushi/invalid/text-in-array.toml view
@@ -0,0 +1,5 @@+array = [+ "Entry 1",+ I don't belong,+ "Entry 2",+]
+ test/BurntSushi/valid/array-empty.json view
@@ -0,0 +1,11 @@+{+ "thevoid": { "type": "array", "value": [+ {"type": "array", "value": [+ {"type": "array", "value": [+ {"type": "array", "value": [+ {"type": "array", "value": []}+ ]}+ ]}+ ]}+ ]}+}
+ test/BurntSushi/valid/array-empty.toml view
@@ -0,0 +1,1 @@+thevoid = [[[[[]]]]]
+ test/BurntSushi/valid/array-nospaces.json view
@@ -0,0 +1,10 @@+{+ "ints": {+ "type": "array",+ "value": [+ {"type": "integer", "value": "1"},+ {"type": "integer", "value": "2"},+ {"type": "integer", "value": "3"}+ ]+ }+}
+ test/BurntSushi/valid/array-nospaces.toml view
@@ -0,0 +1,1 @@+ints = [1,2,3]
+ test/BurntSushi/valid/arrays-hetergeneous.json view
@@ -0,0 +1,19 @@+{+ "mixed": {+ "type": "array",+ "value": [+ {"type": "array", "value": [+ {"type": "integer", "value": "1"},+ {"type": "integer", "value": "2"}+ ]},+ {"type": "array", "value": [+ {"type": "string", "value": "a"},+ {"type": "string", "value": "b"}+ ]},+ {"type": "array", "value": [+ {"type": "float", "value": "1.1"},+ {"type": "float", "value": "2.1"}+ ]}+ ]+ }+}
+ test/BurntSushi/valid/arrays-hetergeneous.toml view
@@ -0,0 +1,1 @@+mixed = [[1, 2], ["a", "b"], [1.1, 2.1]]
+ test/BurntSushi/valid/arrays-nested.json view
@@ -0,0 +1,13 @@+{+ "nest": {+ "type": "array",+ "value": [+ {"type": "array", "value": [+ {"type": "string", "value": "a"}+ ]},+ {"type": "array", "value": [+ {"type": "string", "value": "b"}+ ]}+ ]+ }+}
+ test/BurntSushi/valid/arrays-nested.toml view
@@ -0,0 +1,1 @@+nest = [["a"], ["b"]]
+ test/BurntSushi/valid/arrays.json view
@@ -0,0 +1,34 @@+{+ "ints": {+ "type": "array",+ "value": [+ {"type": "integer", "value": "1"},+ {"type": "integer", "value": "2"},+ {"type": "integer", "value": "3"}+ ]+ },+ "floats": {+ "type": "array",+ "value": [+ {"type": "float", "value": "1.1"},+ {"type": "float", "value": "2.1"},+ {"type": "float", "value": "3.1"}+ ]+ },+ "strings": {+ "type": "array",+ "value": [+ {"type": "string", "value": "a"},+ {"type": "string", "value": "b"},+ {"type": "string", "value": "c"}+ ]+ },+ "dates": {+ "type": "array",+ "value": [+ {"type": "datetime", "value": "1987-07-05T17:45:00Z"},+ {"type": "datetime", "value": "1979-05-27T07:32:00Z"},+ {"type": "datetime", "value": "2006-06-01T11:00:00Z"}+ ]+ }+}
+ test/BurntSushi/valid/arrays.toml view
@@ -0,0 +1,8 @@+ints = [1, 2, 3]+floats = [1.1, 2.1, 3.1]+strings = ["a", "b", "c"]+dates = [+ 1987-07-05T17:45:00Z,+ 1979-05-27T07:32:00Z,+ 2006-06-01T11:00:00Z,+]
+ test/BurntSushi/valid/bool.json view
@@ -0,0 +1,4 @@+{+ "f": {"type": "bool", "value": "false"},+ "t": {"type": "bool", "value": "true"}+}
+ test/BurntSushi/valid/bool.toml view
@@ -0,0 +1,2 @@+t = true+f = false
+ test/BurntSushi/valid/comments-everywhere.json view
@@ -0,0 +1,12 @@+{+ "group": {+ "answer": {"type": "integer", "value": "42"},+ "more": {+ "type": "array",+ "value": [+ {"type": "integer", "value": "42"},+ {"type": "integer", "value": "42"}+ ]+ }+ }+}
+ test/BurntSushi/valid/comments-everywhere.toml view
@@ -0,0 +1,24 @@+# Top comment.+ # Top comment.+# Top comment.++# [no-extraneous-groups-please]++[group] # Comment+answer = 42 # Comment+# no-extraneous-keys-please = 999+# Inbetween comment.+more = [ # Comment+ # What about multiple # comments?+ # Can you handle it?+ #+ # Evil.+# Evil.+ 42, 42, # Comments within arrays are fun.+ # What about multiple # comments?+ # Can you handle it?+ #+ # Evil.+# Evil.+# ] Did I fool you?+] # Hopefully not.
+ test/BurntSushi/valid/datetime.json view
@@ -0,0 +1,3 @@+{+ "bestdayever": {"type": "datetime", "value": "1987-07-05T17:45:00Z"}+}
+ test/BurntSushi/valid/datetime.toml view
@@ -0,0 +1,1 @@+bestdayever = 1987-07-05T17:45:00Z
+ test/BurntSushi/valid/empty.json view
@@ -0,0 +1,1 @@+{}
+ test/BurntSushi/valid/empty.toml view
+ test/BurntSushi/valid/example.json view
@@ -0,0 +1,14 @@+{+ "best-day-ever": {"type": "datetime", "value": "1987-07-05T17:45:00Z"},+ "numtheory": {+ "boring": {"type": "bool", "value": "false"},+ "perfection": {+ "type": "array",+ "value": [+ {"type": "integer", "value": "6"},+ {"type": "integer", "value": "28"},+ {"type": "integer", "value": "496"}+ ]+ }+ }+}
+ test/BurntSushi/valid/example.toml view
@@ -0,0 +1,5 @@+best-day-ever = 1987-07-05T17:45:00Z++[numtheory]+boring = false+perfection = [6, 28, 496]
+ test/BurntSushi/valid/float.json view
@@ -0,0 +1,4 @@+{+ "pi": {"type": "float", "value": "3.14"},+ "negpi": {"type": "float", "value": "-3.14"}+}
+ test/BurntSushi/valid/float.toml view
@@ -0,0 +1,2 @@+pi = 3.14+negpi = -3.14
+ test/BurntSushi/valid/implicit-and-explicit-after.json view
@@ -0,0 +1,10 @@+{+ "a": {+ "better": {"type": "integer", "value": "43"},+ "b": {+ "c": {+ "answer": {"type": "integer", "value": "42"}+ }+ }+ }+}
+ test/BurntSushi/valid/implicit-and-explicit-after.toml view
@@ -0,0 +1,5 @@+[a.b.c]+answer = 42++[a]+better = 43
+ test/BurntSushi/valid/implicit-and-explicit-before.json view
@@ -0,0 +1,10 @@+{+ "a": {+ "better": {"type": "integer", "value": "43"},+ "b": {+ "c": {+ "answer": {"type": "integer", "value": "42"}+ }+ }+ }+}
+ test/BurntSushi/valid/implicit-and-explicit-before.toml view
@@ -0,0 +1,5 @@+[a]+better = 43++[a.b.c]+answer = 42
+ test/BurntSushi/valid/implicit-groups.json view
@@ -0,0 +1,9 @@+{+ "a": {+ "b": {+ "c": {+ "answer": {"type": "integer", "value": "42"}+ }+ }+ }+}
+ test/BurntSushi/valid/implicit-groups.toml view
@@ -0,0 +1,2 @@+[a.b.c]+answer = 42
+ test/BurntSushi/valid/integer.json view
@@ -0,0 +1,4 @@+{+ "answer": {"type": "integer", "value": "42"},+ "neganswer": {"type": "integer", "value": "-42"}+}
+ test/BurntSushi/valid/integer.toml view
@@ -0,0 +1,2 @@+answer = 42+neganswer = -42
+ test/BurntSushi/valid/key-equals-nospace.json view
@@ -0,0 +1,3 @@+{+ "answer": {"type": "integer", "value": "42"}+}
+ test/BurntSushi/valid/key-equals-nospace.toml view
@@ -0,0 +1,1 @@+answer=42
+ test/BurntSushi/valid/key-space.json view
@@ -0,0 +1,3 @@+{+ "a b": {"type": "integer", "value": "1"}+}
+ test/BurntSushi/valid/key-space.toml view
@@ -0,0 +1,1 @@+ a b = 1
+ test/BurntSushi/valid/key-special-chars.json view
@@ -0,0 +1,5 @@+{+ "~!@$^&*()_+-`1234567890[]\\|/?><.,;:'": {+ "type": "integer", "value": "1"+ }+}
+ test/BurntSushi/valid/key-special-chars.toml view
@@ -0,0 +1,1 @@+~!@$^&*()_+-`1234567890[]\|/?><.,;:' = 1
+ test/BurntSushi/valid/long-float.json view
@@ -0,0 +1,4 @@+{+ "longpi": {"type": "float", "value": "3.141592653589793"},+ "neglongpi": {"type": "float", "value": "-3.141592653589793"}+}
+ test/BurntSushi/valid/long-float.toml view
@@ -0,0 +1,2 @@+longpi = 3.141592653589793+neglongpi = -3.141592653589793
+ test/BurntSushi/valid/long-integer.json view
@@ -0,0 +1,4 @@+{+ "answer": {"type": "integer", "value": "9223372036854776000"},+ "neganswer": {"type": "integer", "value": "-9223372036854776000"}+}
+ test/BurntSushi/valid/long-integer.toml view
@@ -0,0 +1,2 @@+answer = 9223372036854776000+neganswer = -9223372036854776000
+ test/BurntSushi/valid/multiline-string.json view
@@ -0,0 +1,30 @@+{+ "multiline empty one": {+ "type": "string",+ "value": ""+ },+ "multiline empty two": {+ "type": "string",+ "value": ""+ },+ "multiline empty three": {+ "type": "string",+ "value": ""+ },+ "multiline empty four": {+ "type": "string",+ "value": ""+ },+ "equivalent one": {+ "type": "string",+ "value": "The quick brown fox jumps over the lazy dog."+ },+ "equivalent two": {+ "type": "string",+ "value": "The quick brown fox jumps over the lazy dog."+ },+ "equivalent three": {+ "type": "string",+ "value": "The quick brown fox jumps over the lazy dog."+ }+}
+ test/BurntSushi/valid/multiline-string.toml view
@@ -0,0 +1,23 @@+multiline empty one = """"""+multiline empty two = """+"""+multiline empty three = """\+ """+multiline empty four = """\+ \+ \+ """++equivalent one = "The quick brown fox jumps over the lazy dog."+equivalent two = """+The quick brown \+++ fox jumps over \+ the lazy dog."""++equivalent three = """\+ The quick brown \+ fox jumps over \+ the lazy dog.\+ """
+ test/BurntSushi/valid/raw-multiline-string.json view
@@ -0,0 +1,14 @@+{+ "oneline": {+ "type": "string",+ "value": "This string has a ' quote character."+ },+ "firstnl": {+ "type": "string",+ "value": "This string has a ' quote character."+ },+ "multiline": {+ "type": "string",+ "value": "This string\nhas ' a quote character\nand more than\none newline\nin it."+ }+}
+ test/BurntSushi/valid/raw-multiline-string.toml view
@@ -0,0 +1,9 @@+oneline = '''This string has a ' quote character.'''+firstnl = '''+This string has a ' quote character.'''+multiline = '''+This string+has ' a quote character+and more than+one newline+in it.'''
+ test/BurntSushi/valid/raw-string.json view
@@ -0,0 +1,30 @@+{+ "backspace": {+ "type": "string",+ "value": "This string has a \\b backspace character."+ },+ "tab": {+ "type": "string",+ "value": "This string has a \\t tab character."+ },+ "newline": {+ "type": "string",+ "value": "This string has a \\n new line character."+ },+ "formfeed": {+ "type": "string",+ "value": "This string has a \\f form feed character."+ },+ "carriage": {+ "type": "string",+ "value": "This string has a \\r carriage return character."+ },+ "slash": {+ "type": "string",+ "value": "This string has a \\/ slash character."+ },+ "backslash": {+ "type": "string",+ "value": "This string has a \\\\ backslash character."+ }+}
+ test/BurntSushi/valid/raw-string.toml view
@@ -0,0 +1,7 @@+backspace = 'This string has a \b backspace character.'+tab = 'This string has a \t tab character.'+newline = 'This string has a \n new line character.'+formfeed = 'This string has a \f form feed character.'+carriage = 'This string has a \r carriage return character.'+slash = 'This string has a \/ slash character.'+backslash = 'This string has a \\ backslash character.'
+ test/BurntSushi/valid/string-empty.json view
@@ -0,0 +1,6 @@+{+ "answer": {+ "type": "string",+ "value": ""+ }+}
+ test/BurntSushi/valid/string-empty.toml view
@@ -0,0 +1,1 @@+answer = ""
+ test/BurntSushi/valid/string-escapes.json view
@@ -0,0 +1,34 @@+{+ "backspace": {+ "type": "string",+ "value": "This string has a \u0008 backspace character."+ },+ "tab": {+ "type": "string",+ "value": "This string has a \u0009 tab character."+ },+ "newline": {+ "type": "string",+ "value": "This string has a \u000A new line character."+ },+ "formfeed": {+ "type": "string",+ "value": "This string has a \u000C form feed character."+ },+ "carriage": {+ "type": "string",+ "value": "This string has a \u000D carriage return character."+ },+ "quote": {+ "type": "string",+ "value": "This string has a \u0022 quote character."+ },+ "slash": {+ "type": "string",+ "value": "This string has a \u002F slash character."+ },+ "backslash": {+ "type": "string",+ "value": "This string has a \u005C backslash character."+ }+}
+ test/BurntSushi/valid/string-escapes.toml view
@@ -0,0 +1,8 @@+backspace = "This string has a \b backspace character."+tab = "This string has a \t tab character."+newline = "This string has a \n new line character."+formfeed = "This string has a \f form feed character."+carriage = "This string has a \r carriage return character."+quote = "This string has a \" quote character."+slash = "This string has a \/ slash character."+backslash = "This string has a \\ backslash character."
+ test/BurntSushi/valid/string-simple.json view
@@ -0,0 +1,6 @@+{+ "answer": {+ "type": "string",+ "value": "You are not drinking enough whisky."+ }+}
+ test/BurntSushi/valid/string-simple.toml view
@@ -0,0 +1,1 @@+answer = "You are not drinking enough whisky."
+ test/BurntSushi/valid/string-with-pound.json view
@@ -0,0 +1,7 @@+{+ "pound": {"type": "string", "value": "We see no # comments here."},+ "poundcomment": {+ "type": "string",+ "value": "But there are # some comments here."+ }+}
+ test/BurntSushi/valid/string-with-pound.toml view
@@ -0,0 +1,2 @@+pound = "We see no # comments here."+poundcomment = "But there are # some comments here." # Did I # mess you up?
+ test/BurntSushi/valid/table-array-implicit.json view
@@ -0,0 +1,7 @@+{+ "albums": {+ "songs": [+ {"name": {"type": "string", "value": "Glory Days"}}+ ]+ }+}
+ test/BurntSushi/valid/table-array-implicit.toml view
@@ -0,0 +1,2 @@+[[albums.songs]]+name = "Glory Days"
+ test/BurntSushi/valid/table-array-many.json view
@@ -0,0 +1,16 @@+{+ "people": [+ {+ "first_name": {"type": "string", "value": "Bruce"},+ "last_name": {"type": "string", "value": "Springsteen"}+ },+ {+ "first_name": {"type": "string", "value": "Eric"},+ "last_name": {"type": "string", "value": "Clapton"}+ },+ {+ "first_name": {"type": "string", "value": "Bob"},+ "last_name": {"type": "string", "value": "Seger"}+ }+ ]+}
+ test/BurntSushi/valid/table-array-many.toml view
@@ -0,0 +1,11 @@+[[people]]+first_name = "Bruce"+last_name = "Springsteen"++[[people]]+first_name = "Eric"+last_name = "Clapton"++[[people]]+first_name = "Bob"+last_name = "Seger"
+ test/BurntSushi/valid/table-array-nest.json view
@@ -0,0 +1,18 @@+{+ "albums": [+ {+ "name": {"type": "string", "value": "Born to Run"},+ "songs": [+ {"name": {"type": "string", "value": "Jungleland"}},+ {"name": {"type": "string", "value": "Meeting Across the River"}}+ ]+ },+ {+ "name": {"type": "string", "value": "Born in the USA"},+ "songs": [+ {"name": {"type": "string", "value": "Glory Days"}},+ {"name": {"type": "string", "value": "Dancing in the Dark"}}+ ]+ }+ ]+}
+ test/BurntSushi/valid/table-array-nest.toml view
@@ -0,0 +1,17 @@+[[albums]]+name = "Born to Run"++ [[albums.songs]]+ name = "Jungleland"++ [[albums.songs]]+ name = "Meeting Across the River"++[[albums]]+name = "Born in the USA"+ + [[albums.songs]]+ name = "Glory Days"++ [[albums.songs]]+ name = "Dancing in the Dark"
+ test/BurntSushi/valid/table-array-one.json view
@@ -0,0 +1,8 @@+{+ "people": [+ {+ "first_name": {"type": "string", "value": "Bruce"},+ "last_name": {"type": "string", "value": "Springsteen"}+ }+ ]+}
+ test/BurntSushi/valid/table-array-one.toml view
@@ -0,0 +1,3 @@+[[people]]+first_name = "Bruce"+last_name = "Springsteen"
+ test/BurntSushi/valid/table-empty.json view
@@ -0,0 +1,3 @@+{+ "a": {}+}
+ test/BurntSushi/valid/table-empty.toml view
@@ -0,0 +1,1 @@+[a]
+ test/BurntSushi/valid/table-sub-empty.json view
@@ -0,0 +1,3 @@+{+ "a": { "b": {} }+}
+ test/BurntSushi/valid/table-sub-empty.toml view
@@ -0,0 +1,2 @@+[a]+[a.b]
+ test/BurntSushi/valid/table-whitespace.json view
@@ -0,0 +1,3 @@+{+ "valid key": {}+}
+ test/BurntSushi/valid/table-whitespace.toml view
@@ -0,0 +1,1 @@+[valid key]
+ test/BurntSushi/valid/table-with-pound.json view
@@ -0,0 +1,5 @@+{+ "key#group": {+ "answer": {"type": "integer", "value": "42"}+ }+}
+ test/BurntSushi/valid/table-with-pound.toml view
@@ -0,0 +1,2 @@+[key#group]+answer = 42
+ test/BurntSushi/valid/unicode-escape.json view
@@ -0,0 +1,3 @@+{+ "answer": {"type": "string", "value": "\u03B4"}+}
+ test/BurntSushi/valid/unicode-escape.toml view
@@ -0,0 +1,1 @@+answer = "\u03B4"
+ test/BurntSushi/valid/unicode-literal.json view
@@ -0,0 +1,3 @@+{+ "answer": {"type": "string", "value": "δ"}+}
+ test/BurntSushi/valid/unicode-literal.toml view
@@ -0,0 +1,1 @@+answer = "δ"
+ test/Test.hs view
@@ -0,0 +1,20 @@+module Main where+++import Test.Tasty (defaultMain, testGroup)++import qualified BurntSushi+import Text.Toml.Parser.Spec+++main :: IO ()+main = do+ parserSpec <- tomlParserSpec+ bsTests <- BurntSushi.tests++ defaultMain $ testGroup "" $+ [ parserSpec+ , bsTests+ --, quickCheckSuite+ -- A QuickCheck suite for a parser is possible. The internet knows.+ ]
+ test/Text/Toml/Parser/Spec.hs view
@@ -0,0 +1,392 @@+{-# LANGUAGE OverloadedStrings #-}++module Text.Toml.Parser.Spec where++import Test.Tasty (TestTree)+import Test.Tasty.Hspec++import Data.HashMap.Strict (fromList)+import Data.Time.Calendar (Day (..))+import Data.Time.Clock (UTCTime (..))++import Text.Toml.Parser+++tomlParserSpec :: IO TestTree+tomlParserSpec = testSpec "Parser Hspec suite" $ do++ describe "Parser.tomlDoc generic" $ do++ it "should parse empty input" $+ testParser tomlDoc "" $ fromList []++ it "should parse non-empty tomlDocs that do not end with a newline" $+ testParser tomlDoc "number = 123" $+ fromList [("number", NTValue $ VInteger 123)]++ it "should parse when tomlDoc ends in a comment" $+ testParser tomlDoc "q = 42 # understood?" $+ fromList [("q", NTValue $ VInteger 42)]++ it "should not parse re-assignment of key" $+ testParserFails tomlDoc "q=42\nq=42"++ it "should not parse rubbish" $+ testParserFails tomlDoc "{"+++ describe "Parser.tomlDoc (named tables)" $ do++ it "should parse simple named table" $+ testParser tomlDoc "[a]\naa = 108" $+ fromList [("a", NTable (fromList [("aa", NTValue $ VInteger 108)] ))]++ it "should not parse redefined table header (key already exists at scope)" $+ testParser tomlDoc "[a]\n[a]" $ fromList [("a", emptyNTable)]++ it "should parse redefinition of implicit key" $+ testParser tomlDoc "[a.b]\n[a]" $+ fromList [("a", NTable (fromList [("b", emptyNTable)] ))]++ it "should parse redefinition of implicit key, with table contents" $+ testParser tomlDoc "[a.b]\nb=3\n[a]\na=4" $+ fromList [("a", NTable (fromList [("b", NTable (fromList [("b", NTValue $ VInteger 3)])),+ ("a", NTValue $ VInteger 4)]))]++ it "should parse redefinition by implicit table header" $+ testParser tomlDoc "[a]\n[a.b]" $+ fromList [("a", NTable (fromList [("b", emptyNTable)] ))]++ it "should not parse redefinition key" $+ testParserFails tomlDoc "[a]\nb=1\n[a.b]"+++ describe "Parser.tomlDoc (tables arrays)" $ do++ it "should parse a simple empty table array" $+ testParser tomlDoc "[[a]]\n[[a]]" $+ fromList [("a", NTArray [ fromList []+ , fromList [] ] )]++ it "should parse a simple table array with content" $+ testParser tomlDoc "[[a]]\na1=1\n[[a]]\na2=2" $+ fromList [("a", NTArray [ fromList [("a1", NTValue $ VInteger 1)]+ , fromList [("a2", NTValue $ VInteger 2)] ] )]++ it "should not allow a simple table array to be inserted into a non table array" $+ testParserFails tomlDoc "a = [1,2,3]\n[[a]]"++ it "should parse a simple empty nested table array" $+ testParser tomlDoc "[[a.b]]\n[[a.b]]" $+ fromList [("a", NTable (fromList [("b", NTArray [ emptyTable+ , emptyTable ] )] ) )]++ it "should parse a simple non empty table array" $+ testParser tomlDoc "[[a.b]]\na1=1\n[[a.b]]\na2=2" $+ fromList [("a", NTable (fromList [("b", NTArray [ fromList [("a1", NTValue $ VInteger 1)]+ , fromList [("a2", NTValue $ VInteger 2)]+ ] )] ) )]++ it "should parse redefined implicit table header" $+ testParserFails tomlDoc "[[a.b]]\n[[a]]"++ it "should parse redefinition by implicit table header" $+ testParser tomlDoc "[[a]]\n[[a.b]]" $+ fromList [("a", NTArray [ fromList [("b", NTArray [ fromList [] ])] ] )]+++ describe "Parser.tomlDoc (mixed named tables and tables arrays)" $ do++ it "should not parse redefinition of key by table header (table array by table)" $+ testParserFails tomlDoc "[[a]]\n[a]"++ it "should not parse redefinition of key by table header (table by table array)" $+ testParserFails tomlDoc "[a]\n[[a]]"++ it "should not parse redefinition implicit table header (table by array)" $+ testParserFails tomlDoc "[a.b]\n[[a]]"++ it "should parse redefined implicit table header (array by table)" $+ testParser tomlDoc "[[a.b]]\n[a]" $+ fromList [("a", NTable (fromList [("b", NTArray [ fromList [] ])] ) )]++ it "should not parse redefined implicit table header (array by table), when keys collide" $+ testParserFails tomlDoc "[[a.b]]\n[a]\nb=1"++ it "should insert sub-key of regular table in most recently defined table array" $+ testParser tomlDoc "[[a]]\ni=0\n[[a]]\ni=1\n[a.b]" $+ fromList [("a", NTArray [ fromList [ ("i", NTValue $ VInteger 0) ]+ , fromList [ ("b", NTable $ fromList [] )+ , ("i", NTValue $ VInteger 1) ]+ ] )]++ it "should insert sub-key of table array" $+ testParser tomlDoc "[a]\n[[a.b]]" $+ fromList [("a", NTable (fromList [("b", NTArray [fromList []])] ) )]++ it "should insert sub-key (with content) of table array" $+ testParser tomlDoc "[a]\nq=42\n[[a.b]]\ni=0" $+ fromList [("a", NTable (fromList [ ("q", NTValue $ VInteger 42),+ ("b", NTArray [+ fromList [("i", NTValue $ VInteger 0)]+ ]) ]) )]++ describe "Parser.headerValue" $ do++ it "should parse simple table header" $+ testParser headerValue "table" ["table"]++ it "should parse simple nested table header" $+ testParser headerValue "main.sub" ["main", "sub"]++ it "should not parse just a dot (separator)" $+ testParserFails headerValue "."++ it "should not parse an empty most right name" $+ testParserFails headerValue "first."++ it "should not parse an empty most left name" $+ testParserFails headerValue ".second"++ it "should not parse an empty middle name" $+ testParserFails headerValue "first..second"+++ describe "Parser.tableHeader" $ do++ it "should not parse an empty table header" $+ testParserFails tableHeader "[]"++ it "should parse simple table header" $+ testParser tableHeader "[item]" ["item"]++ it "should parse simple nested table header" $+ testParser tableHeader "[main.sub]" ["main", "sub"]+++ describe "Parser.tableArrayHeader" $ do++ it "should not parse an empty table header" $+ testParserFails tableArrayHeader "[[]]"++ it "should parse simple table array header" $+ testParser tableArrayHeader "[[item]]" ["item"]++ it "should parse simple nested table array header" $+ testParser tableArrayHeader "[[main.sub]]" ["main", "sub"]+++ describe "Parser.assignment" $ do++ it "should parse simple example" $+ testParser assignment "country = \"\"" ("country", VString "")++ it "should parse without spacing around the assignment operator" $+ testParser assignment "a=108" ("a", VInteger 108)++ it "should parse when value on next line" $+ testParser assignment "a =\n108" ("a", VInteger 108)++ it "should parse when assignment operator and value are on the next line" $+ testParser assignment "a\n= 108" ("a", VInteger 108)++ it "should parse when key, value and assignment operator are on separate lines" $+ testParser assignment "a\n=\n108" ("a", VInteger 108)+++ describe "Parser.boolean" $ do++ it "should parse true" $+ testParser boolean "true" $ VBoolean True++ it "should parse false" $+ testParser boolean "false" $ VBoolean False++ it "should not parse capitalized variant" $+ testParserFails boolean "False"+++ describe "Parser.basicStr" $ do++ it "should parse the common escape sequences in basic strings" $+ testParser basicStr "\"123\\b\\t\\n\\f\\r\\\"\\/\\\\\"" $ VString "123\b\t\n\f\r\"/\\"++ it "should parse the simple unicode value from the example" $+ testParser basicStr "\"中国\"" $ VString "中国"++ it "should parse escaped 4 digit unicode values" $+ testParser assignment "special_k = \"\\u0416\"" ("special_k", VString "Ж")++ it "should parse escaped 8 digit unicode values" $+ testParser assignment "g_clef = \"\\U0001D11e\"" ("g_clef", VString "𝄞")++ it "should not parse escaped unicode values with missing digits" $+ testParserFails assignment "g_clef = \"\\U1D11e\""+++ describe "Parser.multiBasicStr" $ do++ it "should parse simple example" $+ testParser multiBasicStr "\"\"\"thorrough\"\"\"" $ VString "thorrough"++ it "should parse with newlines" $+ testParser multiBasicStr "\"\"\"One\nTwo\"\"\"" $ VString "One\nTwo"++ it "should parse with escaped newlines" $+ testParser multiBasicStr "\"\"\"One\\\nTwo\"\"\"" $ VString "OneTwo"++ it "should parse newlines, ignoring 1 leading newline" $+ testParser multiBasicStr "\"\"\"\nOne\\\nTwo\"\"\"" $ VString "OneTwo"++ it "should parse with espaced whitespace" $+ testParser multiBasicStr "\"\"\"\\\n\+ \Quick \\\n\+ \\\\n\+ \Jumped \\\n\+ \Lazy\\\n\+ \ \"\"\"" $ VString "Quick Jumped Lazy"+++ describe "Parser.literalStr" $ do++ it "should parse literally" $+ testParser literalStr "'\"Your\" folder: \\\\User\\new\\tmp\\'" $+ VString "\"Your\" folder: \\\\User\\new\\tmp\\"++ it "has no notion of 'escaped single quotes'" $+ testParserFails tomlDoc "q = 'I don\\'t know.'" -- string terminates before the "t"+++ describe "Parser.multiLiteralStr" $ do++ it "should parse literally" $+ testParser multiLiteralStr+ "'''\nFirst newline is dropped.\n Other whitespace,\n is preserved -- isn't it?'''"+ $ VString "First newline is dropped.\n Other whitespace,\n is preserved -- isn't it?"+++ describe "Parser.datetime" $ do++ it "should parse a JSON formatted datetime string in zulu timezone" $+ testParser datetime "1979-05-27T07:32:00Z" $+ VDatetime $ UTCTime (ModifiedJulianDay 44020) 27120++ it "should not parse only dates" $+ testParserFails datetime "1979-05-27"++ it "should not parse without the Z" $+ testParserFails datetime "1979-05-27T07:32:00"+++ describe "Parser.float" $ do++ it "should parse positive floats" $+ testParser float "3.14" $ VFloat 3.14++ it "should parse positive floats with plus sign" $+ testParser float "+3.14" $ VFloat 3.14++ it "should parse negative floats" $+ testParser float "-0.1" $ VFloat (-0.1)++ it "should parse more or less zero float" $+ testParser float "0.0" $ VFloat 0.0++ it "should parse 'scientific notation' ('e'-notation)" $+ testParser float "1.5e6" $ VFloat 1500000.0++ it "should parse 'scientific notation' ('e'-notation) with upper case E" $+ testParser float "1E0" $ VFloat 1.0++ it "should not accept floats starting with a dot" $+ testParserFails float ".5"++ it "should not accept floats without any decimals" $+ testParserFails float "5."+++ describe "Parser.integer" $ do++ it "should parse positive integers" $+ testParser integer "108" $ VInteger 108++ it "should parse negative integers" $+ testParser integer "-1" $ VInteger (-1)++ it "should parse zero" $+ testParser integer "0" $ VInteger 0++ it "should parse integers prefixed with a plus" $+ testParser integer "+42" $ VInteger 42+++ describe "Parser.tomlDoc arrays" $ do++ it "should parse an empty array" $+ testParser array "[]" $ VArray []++ it "should parse an empty array with whitespace" $+ testParser array "[ ]" $ VArray []++ it "should not parse an empty array with only a terminating comma" $+ testParserFails array "[,]"++ it "should parse an empty array of empty arrays" $+ testParser array "[[],[]]" $ VArray [ VArray [], VArray [] ]++ it "should parse an empty array of empty arrays with whitespace" $+ testParser array "[ \n[ ]\n ,\n [ \n ] ,\n ]" $ VArray [ VArray [], VArray [] ]++ it "should parse nested arrays" $+ testParser assignment "d = [ ['gamma', 'delta'], [1, 2] ]"+ $ ("d", VArray [ VArray [ VString "gamma"+ , VString "delta" ]+ , VArray [ VInteger 1+ , VInteger 2 ] ])++ it "should allow linebreaks in an array" $+ testParser assignment "hosts = [\n'alpha',\n'omega'\n]"+ $ ("hosts", VArray [VString "alpha", VString "omega"])++ it "should allow some linebreaks in an array" $+ testParser assignment "hosts = ['alpha' ,\n'omega']"+ $ ("hosts", VArray [VString "alpha", VString "omega"])++ it "should allow linebreaks in an array, with comments" $+ testParser assignment "hosts = [\n\+ \'alpha', # the first\n\+ \'omega' # the last\n\+ \]"+ $ ("hosts", VArray [VString "alpha", VString "omega"])++ it "should allow linebreaks in an array, with comments, and terminating comma" $+ testParser assignment "hosts = [\n\+ \'alpha', # the first\n\+ \'omega', # the last\n\+ \]"+ $ ("hosts", VArray [VString "alpha", VString "omega"])++ it "inside an array, all element should be of the same type" $+ testParserFails array "[1, 2.0]"++ it "inside an array of arrays, this inner arrays may contain values of different types" $+ testParser array "[[1], [2.0], ['a']]" $+ VArray [ VArray [VInteger 1], VArray [VFloat 2.0], VArray [VString "a"] ]++ it "all string variants are of the same type of the same type" $+ testParser assignment "data = [\"a\", \"\"\"b\"\"\", 'c', '''d''']" $+ ("data", VArray [ VString "a", VString "b",+ VString "c", VString "d" ])++ it "should parse terminating commas in arrays" $+ testParser array "[1, 2, ]" $ VArray [ VInteger 1, VInteger 2 ]++ it "should parse terminating commas in arrays(2)" $+ testParser array "[1,2,]" $ VArray [ VInteger 1, VInteger 2 ]+++ where+ testParser p str success = case parseOnly p str of Left _ -> False+ Right x -> x == success+ testParserFails p str = case parseOnly p str of Left _ -> True+ Right _ -> False