├── .ghci ├── .gitignore ├── .travis.yml ├── .travis_long.sh ├── CHANGES.md ├── LICENSE ├── README.md ├── Setup.lhs ├── benchmarks ├── Benchmarks.hs ├── example.toml ├── repeated.toml └── results │ ├── attoparsec-less-manys.html │ ├── attoparsec-more-manys.html │ └── parsec-0.html ├── htoml.cabal ├── src └── Text │ ├── Toml.hs │ └── Toml │ ├── Parser.hs │ └── Types.hs └── test ├── BurntSushi.hs ├── BurntSushi ├── fetch-toml-tests.sh ├── invalid │ ├── array-mixed-types-arrays-and-ints.toml │ ├── array-mixed-types-ints-and-floats.toml │ ├── array-mixed-types-strings-and-ints.toml │ ├── datetime-malformed-no-leads.toml │ ├── datetime-malformed-no-secs.toml │ ├── datetime-malformed-no-t.toml │ ├── datetime-malformed-no-z.toml │ ├── datetime-malformed-with-milli.toml │ ├── duplicate-key-table.toml │ ├── duplicate-keys.toml │ ├── duplicate-tables.toml │ ├── empty-implicit-table.toml │ ├── empty-table.toml │ ├── float-no-leading-zero.toml │ ├── float-no-trailing-digits.toml │ ├── key-empty.toml │ ├── key-hash.toml │ ├── key-newline.toml │ ├── key-open-bracket.toml │ ├── key-single-open-bracket.toml │ ├── key-space.toml │ ├── key-start-bracket.toml │ ├── key-two-equals.toml │ ├── string-bad-byte-escape.toml │ ├── string-bad-escape.toml │ ├── string-byte-escapes.toml │ ├── string-no-close.toml │ ├── table-array-implicit.toml │ ├── table-array-malformed-bracket.toml │ ├── table-array-malformed-empty.toml │ ├── table-empty.toml │ ├── table-nested-brackets-close.toml │ ├── table-nested-brackets-open.toml │ ├── table-whitespace.toml │ ├── table-with-pound.toml │ ├── text-after-array-entries.toml │ ├── text-after-integer.toml │ ├── text-after-string.toml │ ├── text-after-table.toml │ ├── text-before-array-separator.toml │ └── text-in-array.toml └── valid │ ├── array-empty.json │ ├── array-empty.toml │ ├── array-nospaces.json │ ├── array-nospaces.toml │ ├── arrays-hetergeneous.json │ ├── arrays-hetergeneous.toml │ ├── arrays-nested.json │ ├── arrays-nested.toml │ ├── arrays.json │ ├── arrays.toml │ ├── bool.json │ ├── bool.toml │ ├── comments-everywhere.json │ ├── comments-everywhere.toml │ ├── datetime.json │ ├── datetime.toml │ ├── empty.json │ ├── empty.toml │ ├── example.json │ ├── example.toml │ ├── float.json │ ├── float.toml │ ├── implicit-and-explicit-after.json │ ├── implicit-and-explicit-after.toml │ ├── implicit-and-explicit-before.json │ ├── implicit-and-explicit-before.toml │ ├── implicit-groups.json │ ├── implicit-groups.toml │ ├── integer.json │ ├── integer.toml │ ├── key-equals-nospace.json │ ├── key-equals-nospace.toml │ ├── key-space.json │ ├── key-space.toml │ ├── key-special-chars.json │ ├── key-special-chars.toml │ ├── long-float.json │ ├── long-float.toml │ ├── long-integer.json │ ├── long-integer.toml │ ├── multiline-string.json │ ├── multiline-string.toml │ ├── raw-multiline-string.json │ ├── raw-multiline-string.toml │ ├── raw-string.json │ ├── raw-string.toml │ ├── string-empty.json │ ├── string-empty.toml │ ├── string-escapes.json │ ├── string-escapes.toml │ ├── string-simple.json │ ├── string-simple.toml │ ├── string-with-pound.json │ ├── string-with-pound.toml │ ├── table-array-implicit.json │ ├── table-array-implicit.toml │ ├── table-array-many.json │ ├── table-array-many.toml │ ├── table-array-nest.json │ ├── table-array-nest.toml │ ├── table-array-one.json │ ├── table-array-one.toml │ ├── table-empty.json │ ├── table-empty.toml │ ├── table-sub-empty.json │ ├── table-sub-empty.toml │ ├── table-whitespace.json │ ├── table-whitespace.toml │ ├── table-with-pound.json │ ├── table-with-pound.toml │ ├── unicode-escape.json │ ├── unicode-escape.toml │ ├── unicode-literal.json │ └── unicode-literal.toml ├── Test.hs └── Text └── Toml └── Parser └── Spec.hs /.ghci: -------------------------------------------------------------------------------- 1 | import Prelude hiding (readFile) 2 | import Control.Applicative hiding (many, (<|>)) 3 | import Text.Parsec 4 | import Text.Parsec.Text 5 | import Data.HashMap.Strict 6 | import Data.Text 7 | import Data.Text.IO (readFile) 8 | import Data.Aeson.Types 9 | import Data.Aeson.Encode 10 | 11 | :seti -XOverloadedStrings 12 | :set prompt "> " 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | cabal-dev 3 | *.o 4 | *.hi 5 | *.chi 6 | *.chs.h 7 | .virthualenv 8 | .cabal-sandbox 9 | cabal.sandbox.config 10 | .*.sw[po] 11 | .stack-work 12 | 13 | # just create your own with `stack init` to best fit your setup 14 | stack.yaml 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # Use new container infrastructure to enable caching 2 | sudo: false 3 | 4 | # Choose a lightweight base image; we provide our own build tools. 5 | # NB: don't set `language: haskell` here 6 | language: c 7 | 8 | # GHC depends on GMP. You can add other dependencies here as well. 9 | addons: 10 | apt: 11 | packages: 12 | - libgmp-dev 13 | 14 | env: 15 | - ARGS="" 16 | - ARGS="--resolver lts-2" 17 | - ARGS="--resolver lts-3" 18 | - ARGS="--resolver lts-4" 19 | - ARGS="--resolver lts-5" 20 | - ARGS="--resolver lts" 21 | - ARGS="--resolver nightly" 22 | 23 | before_install: 24 | # Download and unpack the stack executable 25 | - mkdir -p ~/.local/bin 26 | - export PATH=$HOME/.local/bin:$PATH 27 | - travis_retry curl -L https://www.stackage.org/stack/linux-x86_64 | tar xz --wildcards --strip-components=1 -C ~/.local/bin '*/stack' 28 | 29 | # This line does all of the work: installs GHC if necessary, build the library, 30 | # executables, and test suites, and runs the test suites. --no-terminal works 31 | # around some quirks in Travis's terminal implementation. 32 | script: 33 | # `--solver` needs `cabal` on the PATH 34 | - ./.travis_long.sh stack $ARGS --no-terminal --install-ghc setup 35 | - ./.travis_long.sh stack $ARGS --no-terminal --install-ghc install cabal-install 36 | # `--force` so it overwrites stack.yaml after the first run, `--solver` so it picks `extra-deps` 37 | - ./.travis_long.sh stack $ARGS --no-terminal --install-ghc init --force --solver 38 | # for debug purpose, know what's going on 39 | - cat stack.yaml 40 | # build and run the test 41 | - ./.travis_long.sh stack $ARGS --no-terminal --install-ghc test 42 | # build the docs 43 | - ./.travis_long.sh stack $ARGS --no-terminal --install-ghc install hscolour 44 | - ./.travis_long.sh stack $ARGS --no-terminal --install-ghc haddock --no-haddock-deps 45 | # build and run the benchmarks 46 | - ./.travis_long.sh stack $ARGS --no-terminal --install-ghc build :benchmarks 47 | 48 | # Caching so the next build will be fast. 49 | cache: 50 | directories: 51 | - $HOME/.stack 52 | -------------------------------------------------------------------------------- /.travis_long.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | $* & 4 | pidA=$! 5 | minutes=0 6 | 7 | while true; do sleep 60; ((minutes++)); echo -e "\033[0;32m$minutes minute(s) elapsed.\033[0m"; done & 8 | pidB=$! 9 | 10 | wait $pidA 11 | 12 | echo -e "\033[0;32m$* finished.\033[0m" 13 | 14 | kill -9 $pidB -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | Change log 2 | ========== 3 | 4 | #### dev 5 | * ... 6 | 7 | ### 1.0.0.1 8 | * Improve docs 9 | 10 | ### 1.0.0.0 11 | * Use `Vector` over `List` internally, as per discussion in [issue 13](https://github.com/cies/htoml/issues/13) 12 | 13 | ### 0.2.0.1 14 | * Expose `ToJSON` implementation 15 | * Remove unused .cabal dependency (thanks @tmcgilchrist) 16 | 17 | #### 0.2.0.0 18 | * Compatible with TOML 0.4.0 19 | * Improve test suite (all test now pass -- thanks @HuwCampbell) 20 | * Slight API breakage (therefore major version bump) 21 | * Use Parsec's parser state to track explicitness of table definitions (thanks @HuwCampbell) 22 | * Clean up docs and code 23 | 24 | #### 0.1.0.3 25 | * GHC 7.10 compatibility fix (thanks @erebe) 26 | * Allow time >= 1.5.0, by using some CPP trickery 27 | * Improve README based on 28 | [feedback on Reddit](http://www.reddit.com/r/haskell/comments/2s376c/show_rhaskell_htoml_a_parser_for_toml_files) 29 | 30 | #### 0.1.0.2 31 | * Update the REAMDE 32 | * Add/relax dependency version contraints where applicable 33 | * Fix all warnings 34 | * Add `CHANGES.md` 35 | 36 | #### 0.1.0.1 37 | * Fix `cabal configure` error in cabal file 38 | 39 | #### 0.1.0.0 40 | * Initial upload to Hackage 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013-2014, Spiros Eliopoulos 2 | 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions 7 | are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright 10 | notice, this list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright 13 | notice, this list of conditions and the following disclaimer in the 14 | documentation and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the author nor the names of his contributors 17 | may be used to endorse or promote products derived from this software 18 | without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE CONTRIBUTORS ``AS IS'' AND ANY EXPRESS 21 | OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 22 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE FOR 24 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 26 | OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 27 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 28 | STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 29 | ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 30 | POSSIBILITY OF SUCH DAMAGE. 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | htoml 2 | ===== 3 | 4 | [![Build Status](https://travis-ci.org/cies/htoml.svg?branch=master)](https://travis-ci.org/cies/htoml) 5 | [![Latest version on Hackage](https://img.shields.io/hackage/v/htoml.svg)](https://hackage.haskell.org/package/htoml) 6 | [![Dependencies of latest version on Hackage](https://img.shields.io/hackage-deps/v/htoml.svg)](https://hackage.haskell.org/package/htoml) 7 | 8 | [![htoml on Stackage LTS 5](http://stackage.org/package/htoml/badge/lts-5)](http://stackage.org/lts-5/package/htoml) 9 | [![htoml on Stackage LTS 6](http://stackage.org/package/htoml/badge/lts-6)](http://stackage.org/lts-6/package/htoml) 10 | [![htoml on Stackage Nightly](http://stackage.org/package/htoml/badge/nightly)](http://stackage.org/nightly/package/htoml) 11 | 12 | 13 | A [TOML](https://github.com/mojombo/toml) parser library in 14 | [Haskell](http://haskell-lang.org). 15 | 16 | TOML is the obvious, minimal configuration language by 17 | [Tom Preston-Werner](https://github.com/mojombo). 18 | It is an alternative to the [XML](http://www.w3.org/TR/REC-xml/), 19 | [YAML](http://www.yaml.org/spec/1.2/spec.html) and 20 | [INI](http://en.wikipedia.org/wiki/INI_file) formats mainly for the purpose of 21 | configuration files. Many will find that XML and YAML are too heavy for 22 | the purpose of configuration files prupose while INI is underspecified. 23 | TOML is to configuration files, like what Markdown is for rich-text. 24 | 25 | This library aims to be compatible with the latest version of the 26 | [TOML spec](https://github.com/mojombo/toml). 27 | Compatibility between `htoml` version and TOML (as proven by 28 | [BurntSushi's language agnostic TOML test suite](https://github.com/BurntSushi/toml-test)) 29 | is as follows: 30 | 31 | * [TOML v0.4.0](https://github.com/toml-lang/toml/releases/tag/v0.4.0) 32 | is implemented by `htoml >= 1.0.0.0` 33 | * *(currently only one item in this mapping, more will follow)* 34 | 35 | 36 | ### Documentation 37 | 38 | Apart from this README, documentation for this package may 39 | (or may not) be found on [Hackage](https://hackage.haskell.org/package/htoml). 40 | 41 | 42 | ### Quick start 43 | 44 | Installing `htoml` is easy. Either by using 45 | [Stack](http://haskellstack.org) (recommended): 46 | 47 | stack install htoml 48 | 49 | Or by using Cabal: 50 | 51 | cabal install htoml 52 | 53 | In order to make your project depend on it you can add it as a 54 | dependency in your project's `.cabal` file, and since it is not 55 | yet on [Stackage](https://www.stackage.org/) you will also have 56 | to add it to the `extra-deps` section of your `stack.yaml` file 57 | when using Stack. 58 | 59 | To quickly show some features of `htoml` we use Stack to start a 60 | GHCi-based REPL. It picks up configuration from the `.ghci` file 61 | in the root of the repository. 62 | 63 | git clone https://github.com/cies/htoml.git 64 | cd htoml 65 | stack init 66 | stack --install-ghc ghci 67 | 68 | Add a `--resolver` flag to the `stack init` command to specify 69 | a specific package snapshot, e.g.: `--resolver lts-4.1`. 70 | 71 | In case you have missing dependencies (possibly `file-embed`), 72 | they can be added to the `extra-deps` in `stack.yaml` 73 | automatically with: 74 | 75 | stack solver --update-config 76 | 77 | We can now start exploring `htoml` from a GHCi REPL. From the 78 | root of this repository run: 79 | 80 | stack ghci 81 | 82 | Now read a `.toml` file from the benchmark suite, with: 83 | 84 | ```haskell 85 | txt <- readFile "benchmarks/example.toml" 86 | let r = parseTomlDoc "" txt 87 | r 88 | ``` 89 | 90 | ...which prints: 91 | 92 | Right (fromList [("database",VTable (fromList [("enabled",VBoolean True),("po [...] 93 | 94 | Then convert it to [Aeson](https://hackage.haskell.org/package/aeson) (JSON), with: 95 | 96 | ```haskell 97 | let Right toml = r 98 | toJSON toml 99 | ``` 100 | 101 | ...which prints: 102 | 103 | Object (fromList [("database",Object (fromList [("enabled",Bool True),("po [...] 104 | 105 | Finally trigger a parse error, with: 106 | 107 | ```haskell 108 | let Left err = parseTomlDoc "" "== invalid toml ==" 109 | err 110 | ``` 111 | 112 | ...it errors out (as it should), showing: 113 | 114 | (line 1, column 1): 115 | unexpected '=' 116 | expecting "#", "\n", "\r\n", letter or digit, "_", "-", "\"", "'", "[" or end of input 117 | 118 | **Note:** Some of the above outputs are truncated, indicated by `[...]`. 119 | 120 | 121 | ### How to pull data from a TOML file after parsing it 122 | 123 | Once you have sucessfully parsed a TOML file you most likely want to pull 124 | some piecces of data out of the resulting data structure. 125 | 126 | To do so you have two main options. The first is to use pattern matching. 127 | For example let's consider the following `parseResult`: 128 | 129 | ```haskell 130 | Right (fromList [("server",VTable (fromList [("enabled",VBoolean True)] ) )] ) 131 | ``` 132 | 133 | Which could be pattern matched with: 134 | 135 | ```haskell 136 | case parseResult of 137 | Left _ -> "Could not parse file" 138 | Right m -> case m ! "server" of 139 | VTable mm -> case mm ! "enabled" of 140 | VBoolean b -> "Server is " ++ (if b then "enabled" else "disabled") 141 | _ -> "Could not parse server status (Boolean)" 142 | _ -> "TOML file does not contain the 'server' key" 143 | ``` 144 | 145 | The second main option is to use the `toJSON` function to transform the data 146 | to an [Aeson](https://hackage.haskell.org/package/aeson) data structure, 147 | after which you can use your Aeson toolbelt to tackle the problem. Since 148 | TOML is intended to be a close cousin of JSON this is a very practical 149 | approach. 150 | 151 | Other ways to pull data from a parsed TOML document will most likely 152 | exist; possible using the `lens` library as 153 | [documented here](https://github.com/cies/htoml/issues/8). 154 | 155 | 156 | ### Compatibility 157 | 158 | Currently we are testing against several versions of GHC with 159 | [Travis CI](https://travis-ci.org/cies/htoml) as defined in the `env` section of our 160 | [`.travis.yml`](https://github.com/cies/htoml/blob/master/.travis.yml). 161 | `lts-2` implies GHC 7.8.4, `lts-3` implies GHC 7.10.2, `lts-4`/`lts-5` 162 | imply GHC 7.10.3, and `nightly` is build with a regularly updated version of GHC. 163 | 164 | 165 | ### Version contraints of `htoml`'s dependencies 166 | 167 | If you encounter any problems because `htoml`'s dependecies are 168 | constrained either too much or too little, please 169 | [file a issue](https://github.com/cies/htoml/issues) for that. 170 | Or off even better submit a PR. 171 | 172 | 173 | ### Tests and benchmarks 174 | 175 | Tests are build and run with: 176 | 177 | stack test 178 | 179 | [BurntSushi's language agnostic test suite](https://github.com/BurntSushi/toml-test) 180 | is embedded in the test suite executable. Using a shell script (that 181 | lives in `test/BurntSushi`) the latest tests can be fetched from 182 | its Github repository. 183 | 184 | The benchmarks, that use the amazing [`criterion`](http://www.serpentine.com/criterion) 185 | library, are build and run with: 186 | 187 | stack build :benchmarks 188 | 189 | 190 | ### Contributions 191 | 192 | Most welcome! Please raise issues, start discussions, give comments or 193 | submit pull-requests. 194 | This is one of the first Haskell libraries I wrote, feedback is 195 | much appreciated. 196 | 197 | 198 | ### Features 199 | 200 | * Compatibility to the TOML spec is proven by an extensive test suite 201 | * Incorporates [BurntSushi's language agnostic test suite](https://github.com/BurntSushi/toml-test) 202 | * Has an internal representation that easily maps to JSON 203 | * Provides an [Aeson](https://hackage.haskell.org/package/aeson)-style JSON interface (suggested by Greg Weber) 204 | * Useful error messages (thanks to using Parsec over Attoparsec) 205 | * Understands arrays as described in [this issue](https://github.com/toml-lang/toml/issues/254) 206 | * Fails on mix-type arrays (as per spec) 207 | * Comes with a benchmark suite to make performance gains/regressions measurable 208 | * Tries to be well documented (please raise an issue if you find documentation lacking) 209 | * Available on [Stackage](http://stackage.org) (see top of this README for badges 210 | indicating TOMLs *inclusion in Stackage status*) 211 | 212 | 213 | ### Todo 214 | 215 | * More documentation and start to use the proper Haddock idioms 216 | * Add property tests with QuickCheck (the internet says it's possible for parsers) 217 | * Extensively test error cases (probably improving error reporting along the way) 218 | * See how lenses may (or may not) fit into this package, or an additional package 219 | * Consider moving to [one of the more modern parser combinators](https://www.reddit.com/r/haskell/comments/46u45o/what_is_the_current_state_of_parser_libraries_in) 220 | in Haskell (`megaparsec` maybe?) -- possibly wait until a clear winner shows 221 | 222 | Do you see todo that looks like fun thing to implement and you can spare the time? 223 | Please knoe that PRs are welcome :) 224 | 225 | 226 | ### Acknowledgements 227 | 228 | Originally this project started off by improving the `toml` package by 229 | Spiros Eliopoulos. 230 | 231 | [HuwCampbell](https://github.com/HuwCampbell) helped a lot by making tests 232 | pass and implementing "explicitness tracking" in Parsec's parser state. 233 | 234 | 235 | ### Copyright and licensing 236 | 237 | This package includes BurntSushi's language agnostic 238 | [TOML tests](https://github.com/BurntSushi/toml-test), which are WTFPL 239 | licensed. 240 | 241 | The TOML examples that are used as part of the benchmarks are copied 242 | from Tom Preston-Werner's TOML spec which is MIT licensed. 243 | 244 | For all other files in this project the copyrights are specified in the 245 | `htoml.cabal` file, they are distributed under the BSD3 license as found 246 | in the `LICENSE` file. 247 | -------------------------------------------------------------------------------- /Setup.lhs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env runghc 2 | 3 | > module Main where 4 | 5 | > import Distribution.Simple 6 | 7 | > main :: IO () 8 | > main = defaultMain 9 | -------------------------------------------------------------------------------- /benchmarks/Benchmarks.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | 3 | import Prelude hiding (readFile) 4 | 5 | import Criterion.Main 6 | import Data.Text.IO (readFile) 7 | 8 | import Text.Toml 9 | 10 | 11 | main :: IO () 12 | main = do 13 | exampleToml <- readFile "./benchmarks/example.toml" 14 | repeatedToml <- readFile "./benchmarks/repeated.toml" 15 | defaultMain 16 | 17 | [ bgroup "string" 18 | [ bench "assignment" $ whnf (parseTomlDoc "") "q=42\nqa=[4,2,]\nqb=true\nqf=23.23\n\ 19 | \qd=1979-05-27T07:32:00Z\nqs='forty-two'" 20 | , bench "headers" $ whnf (parseTomlDoc "") "[Q]\n[A]\n[[QA]]\n[[QA]]\n[[QA]]\n[[QA]]" 21 | , bench "mixed" $ whnf (parseTomlDoc "") "q=42\nqa=[4,2,]\n[Q]qq=42\n[[QA]]\n[[QA]]" 22 | ] 23 | 24 | , bgroup "file" 25 | [ bench "example" $ whnf (parseTomlDoc "") exampleToml 26 | , bench "repeated-4x" $ whnf (parseTomlDoc "") repeatedToml 27 | ] 28 | 29 | ] 30 | -------------------------------------------------------------------------------- /benchmarks/example.toml: -------------------------------------------------------------------------------- 1 | # This is a TOML document. Boom. 2 | 3 | title = "TOML Example" 4 | 5 | [owner] 6 | name = "Tom Preston-Werner" 7 | organization = "GitHub" 8 | bio = "GitHub Cofounder & CEO\nLikes tater tots and beer." 9 | dob = 1979-05-27T07:32:00Z # First class dates? Why not? 10 | 11 | [database] 12 | server = "192.168.1.1" 13 | ports = [ 8001, 8001, 8002 ] 14 | connection_max = 5000 15 | enabled = true 16 | 17 | [servers1] 18 | 19 | # You can indent as you please. Tabs or spaces. TOML don't care. 20 | [servers.alpha] 21 | ip = "10.0.0.1" 22 | dc = "eqdc10" 23 | 24 | [servers.beta] 25 | ip = "10.0.0.2" 26 | dc = "eqdc10" 27 | country = "中国" # This should be parsed as UTF-8 28 | 29 | [clients] 30 | data = [ ["gamma", "delta"], [1, 2] ] # just an update to make sure parsers support it 31 | 32 | 33 | # Line breaks are OK when inside arrays 34 | hosts = [ 35 | "alpha", 36 | "omega" 37 | ] 38 | 39 | # Products 40 | 41 | [[products]] 42 | name = "Hammer" 43 | sku = 738594937 44 | 45 | [[products]] 46 | name = "Nail" 47 | sku = 284758393 48 | color = "gray" 49 | -------------------------------------------------------------------------------- /benchmarks/repeated.toml: -------------------------------------------------------------------------------- 1 | # This is a TOML document. Boom. 2 | 3 | title = "TOML Example" 4 | 5 | [owner] 6 | name = "Tom Preston-Werner" 7 | organization = "GitHub" 8 | bio = "GitHub Cofounder & CEO\nLikes tater tots and beer." 9 | dob = 1979-05-27T07:32:00Z # First class dates? Why not? 10 | 11 | [database] 12 | server = "192.168.1.1" 13 | ports = [ 8001, 8001, 8002 ] 14 | connection_max = 5000 15 | enabled = true 16 | 17 | [servers1] 18 | 19 | # You can indent as you please. Tabs or spaces. TOML don't care. 20 | [servers.alpha] 21 | ip = "10.0.0.1" 22 | dc = "eqdc10" 23 | 24 | [servers.beta] 25 | ip = "10.0.0.2" 26 | dc = "eqdc10" 27 | country = "中国" # This should be parsed as UTF-8 28 | 29 | [clients] 30 | data = [ ["gamma", "delta"], [1, 2] ] # just an update to make sure parsers support it 31 | 32 | 33 | # Line breaks are OK when inside arrays 34 | hosts = [ 35 | "alpha", 36 | "omega" 37 | ] 38 | 39 | # Products 40 | 41 | [[products]] 42 | name = "Hammer" 43 | sku = 738594937 44 | 45 | [[products]] 46 | name = "Nail" 47 | sku = 284758393 48 | color = "gray" 49 | 50 | 51 | # This is a TOML document. Boom. 52 | 53 | [owner2] 54 | name = "Tom Preston-Werner" 55 | organization = "GitHub" 56 | bio = "GitHub Cofounder & CEO\nLikes tater tots and beer." 57 | dob = 1979-05-27T07:32:00Z # First class dates? Why not? 58 | 59 | [database2] 60 | server = "192.168.1.1" 61 | ports = [ 8001, 8001, 8002 ] 62 | connection_max = 5000 63 | enabled = true 64 | 65 | [servers2] 66 | 67 | # You can indent as you please. Tabs or spaces. TOML don't care. 68 | [servers.alpha2] 69 | ip = "10.0.0.1" 70 | dc = "eqdc10" 71 | 72 | [servers.beta2] 73 | ip = "10.0.0.2" 74 | dc = "eqdc10" 75 | country = "中国" # This should be parsed as UTF-8 76 | 77 | [clients2] 78 | data = [ ["gamma", "delta"], [1, 2] ] # just an update to make sure parsers support it 79 | 80 | 81 | # Line breaks are OK when inside arrays 82 | hosts = [ 83 | "alpha", 84 | "omega" 85 | ] 86 | 87 | # Products 88 | 89 | [[products2]] 90 | name = "Hammer" 91 | sku = 738594937 92 | 93 | [[products2]] 94 | name = "Nail" 95 | sku = 284758393 96 | color = "gray" 97 | 98 | 99 | 100 | # This is a TOML document. Boom. 101 | 102 | [owner3] 103 | name = "Tom Preston-Werner" 104 | organization = "GitHub" 105 | bio = "GitHub Cofounder & CEO\nLikes tater tots and beer." 106 | dob = 1979-05-27T07:32:00Z # First class dates? Why not? 107 | 108 | [database3] 109 | server = "192.168.1.1" 110 | ports = [ 8001, 8001, 8002 ] 111 | connection_max = 5000 112 | enabled = true 113 | 114 | [servers3] 115 | 116 | # You can indent as you please. Tabs or spaces. TOML don't care. 117 | [servers.alpha3] 118 | ip = "10.0.0.1" 119 | dc = "eqdc10" 120 | 121 | [servers.beta3] 122 | ip = "10.0.0.2" 123 | dc = "eqdc10" 124 | country = "中国" # This should be parsed as UTF-8 125 | 126 | [clients3] 127 | data = [ ["gamma", "delta"], [1, 3] ] # just an update to make sure parsers support it 128 | 129 | 130 | # Line breaks are OK when inside arrays 131 | hosts = [ 132 | "alpha", 133 | "omega" 134 | ] 135 | 136 | # Products 137 | 138 | [[products3]] 139 | name = "Hammer" 140 | sku = 738594937 141 | 142 | [[products3]] 143 | name = "Nail" 144 | sku = 284758393 145 | color = "gray" 146 | 147 | 148 | 149 | # This is a TOML document. Boom. 150 | 151 | [owner4] 152 | name = "Tom Preston-Werner" 153 | organization = "GitHub" 154 | bio = "GitHub Cofounder & CEO\nLikes tater tots and beer." 155 | dob = 1979-05-27T07:32:00Z # First class dates? Why not? 156 | 157 | [database4] 158 | server = "192.168.1.1" 159 | ports = [ 8001, 8001, 8002 ] 160 | connection_max = 5000 161 | enabled = true 162 | 163 | [servers4] 164 | 165 | # You can indent as you please. Tabs or spaces. TOML don't care. 166 | [servers.alpha4] 167 | ip = "10.0.0.1" 168 | dc = "eqdc10" 169 | 170 | [servers.beta4] 171 | ip = "10.0.0.2" 172 | dc = "eqdc10" 173 | country = "中国" # This should be parsed as UTF-8 174 | 175 | [clients4] 176 | data = [ ["gamma", "delta"], [1, 4] ] # just an update to make sure parsers support it 177 | 178 | 179 | # Line breaks are OK when inside arrays 180 | hosts = [ 181 | "alpha", 182 | "omega" 183 | ] 184 | 185 | # Products 186 | 187 | [[products4]] 188 | name = "Hammer" 189 | sku = 738594937 190 | 191 | [[products4]] 192 | name = "Nail" 193 | sku = 284758393 194 | color = "gray" 195 | -------------------------------------------------------------------------------- /htoml.cabal: -------------------------------------------------------------------------------- 1 | name: htoml 2 | version: 1.0.0.3 3 | synopsis: Parser for TOML files 4 | description: TOML is an obvious and minimal format for config files. 5 | . 6 | This package provides a TOML parser, 7 | build with the Parsec library. It exposes a JSON 8 | interface using the Aeson library. 9 | homepage: https://github.com/cies/htoml 10 | bug-reports: https://github.com/cies/htoml/issues 11 | license: BSD3 12 | license-file: LICENSE 13 | copyright: (c) 2013-2016 Cies Breijs 14 | author: Cies Breijs 15 | maintainer: Cies Breijs 16 | category: Data, Text, Parser, Configuration, JSON, Language 17 | build-type: Simple 18 | cabal-version: >= 1.10 19 | extra-source-files: README.md 20 | , CHANGES.md 21 | , test/BurntSushi/fetch-toml-tests.sh 22 | , test/BurntSushi/valid/*.toml 23 | , test/BurntSushi/valid/*.json 24 | , test/BurntSushi/invalid/*.toml 25 | , benchmarks/example.toml 26 | , benchmarks/repeated.toml 27 | 28 | source-repository head 29 | type: git 30 | location: https://github.com/cies/htoml.git 31 | 32 | library 33 | exposed-modules: Text.Toml 34 | , Text.Toml.Parser 35 | , Text.Toml.Types 36 | ghc-options: -Wall 37 | hs-source-dirs: src 38 | default-language: Haskell2010 39 | build-depends: base >= 4.3 && < 5 40 | , parsec >= 3.1.2 && < 4 41 | , containers >= 0.5 42 | , unordered-containers >= 0.2 43 | , vector >= 0.10 44 | , aeson >= 0.8 45 | , text >= 1.0 && < 2 46 | , time -any 47 | , old-locale -any 48 | 49 | test-suite htoml-test 50 | hs-source-dirs: test 51 | ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N 52 | main-is: Test.hs 53 | other-modules: BurntSushi 54 | , Text.Toml.Parser.Spec 55 | type: exitcode-stdio-1.0 56 | default-language: Haskell2010 57 | build-depends: base 58 | , parsec 59 | , containers 60 | , unordered-containers 61 | , vector 62 | , aeson 63 | , text 64 | , time 65 | -- from here non-lib deps 66 | , htoml 67 | , bytestring 68 | , file-embed 69 | , tasty 70 | , tasty-hspec 71 | , tasty-hunit 72 | 73 | benchmark benchmarks 74 | hs-source-dirs: benchmarks . 75 | ghc-options: -O2 -Wall -threaded -rtsopts -with-rtsopts=-N 76 | main-is: Benchmarks.hs 77 | type: exitcode-stdio-1.0 78 | default-language: Haskell2010 79 | build-depends: base 80 | , parsec 81 | , containers 82 | , unordered-containers 83 | , vector 84 | , aeson 85 | , text 86 | , time 87 | -- from here non-lib deps 88 | , htoml 89 | , criterion 90 | -------------------------------------------------------------------------------- /src/Text/Toml.hs: -------------------------------------------------------------------------------- 1 | module Text.Toml where 2 | 3 | import Data.Text (Text) 4 | import Data.Set (empty) 5 | import Text.Parsec 6 | 7 | import Text.Toml.Parser 8 | 9 | 10 | -- | Parse a 'Text' that results in 'Either' a 'String' 11 | -- containing the error message, or an internal representation 12 | -- of the document as a 'Table'. 13 | parseTomlDoc :: String -> Text -> Either ParseError Table 14 | parseTomlDoc inputName input = runParser tomlDoc empty inputName input 15 | -------------------------------------------------------------------------------- /src/Text/Toml/Parser.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE FlexibleContexts #-} 2 | {-# LANGUAGE OverloadedStrings #-} 3 | {-# LANGUAGE RankNTypes #-} 4 | {-# LANGUAGE CPP #-} 5 | 6 | module Text.Toml.Parser 7 | ( module Text.Toml.Parser 8 | , module Text.Toml.Types 9 | ) where 10 | 11 | import Control.Applicative hiding (many, optional, (<|>)) 12 | import Control.Monad 13 | 14 | import qualified Data.HashMap.Strict as M 15 | import qualified Data.List as L 16 | import qualified Data.Set as S 17 | import Data.Text (Text, pack, unpack) 18 | import qualified Data.Vector as V 19 | 20 | #if MIN_VERSION_time(1,5,0) 21 | import Data.Time.Format (defaultTimeLocale, iso8601DateFormat, 22 | parseTimeM) 23 | #else 24 | import Data.Time.Format (parseTime) 25 | import System.Locale (defaultTimeLocale, iso8601DateFormat) 26 | #endif 27 | 28 | import Numeric (readHex) 29 | import Text.Parsec 30 | 31 | import Text.Toml.Types 32 | 33 | -- Imported as last to fix redundancy warning 34 | import Prelude hiding (concat, takeWhile) 35 | 36 | 37 | -- | Our very own Parser type. 38 | type Parser a = forall s. Parsec Text s a 39 | 40 | 41 | -- | Convenience function for the test suite and GHCI. 42 | parseOnly :: Parsec Text (S.Set [Text]) a -> Text -> Either ParseError a 43 | parseOnly p str = runParser (p <* eof) S.empty "test" str 44 | 45 | 46 | -- | Parses a complete document formatted according to the TOML spec. 47 | tomlDoc :: Parsec Text (S.Set [Text]) Table 48 | tomlDoc = do 49 | skipBlanks 50 | topTable <- table 51 | namedSections <- many namedSection 52 | -- Ensure the input is completely consumed 53 | eof 54 | -- Load each named section into the top table 55 | foldM (flip (insert Explicit)) topTable namedSections 56 | 57 | -- | Parses a table of key-value pairs. 58 | table :: Parser Table 59 | table = do 60 | pairs <- try (many (assignment <* skipBlanks)) <|> (try skipBlanks >> return []) 61 | case maybeDupe (map fst pairs) of 62 | Just k -> fail $ "Cannot redefine key " ++ (unpack k) 63 | Nothing -> return $ M.fromList pairs 64 | 65 | -- | Parses an inline table of key-value pairs. 66 | inlineTable :: Parser Node 67 | inlineTable = do 68 | pairs <- between (char '{') (char '}') (skipSpaces *> separatedValues <* skipSpaces) 69 | case maybeDupe (map fst pairs) of 70 | Just k -> fail $ "Cannot redefine key " ++ (unpack k) 71 | Nothing -> return $ VTable $ M.fromList pairs 72 | where 73 | skipSpaces = many (satisfy isSpc) 74 | separatedValues = sepBy (skipSpaces *> assignment <* skipSpaces) comma 75 | comma = skipSpaces >> char ',' >> skipSpaces 76 | 77 | -- | Find dupes, if any. 78 | maybeDupe :: Ord a => [a] -> Maybe a 79 | maybeDupe xx = dup xx S.empty 80 | where 81 | dup [] _ = Nothing 82 | dup (x:xs) s = if S.member x s then Just x else dup xs (S.insert x s) 83 | 84 | 85 | -- | Parses a 'Table' or 'TableArray' with its header. 86 | -- The resulting tuple has the header's value in the first position, and the 87 | -- 'NTable' or 'NTArray' in the second. 88 | namedSection :: Parser ([Text], Node) 89 | namedSection = do 90 | eitherHdr <- try (Left <$> tableHeader) <|> try (Right <$> tableArrayHeader) 91 | skipBlanks 92 | tbl <- table 93 | skipBlanks 94 | return $ case eitherHdr of Left ns -> (ns, VTable tbl ) 95 | Right ns -> (ns, VTArray $ V.singleton tbl) 96 | 97 | 98 | -- | Parses a table header. 99 | tableHeader :: Parser [Text] 100 | tableHeader = between (char '[') (char ']') headerValue 101 | 102 | 103 | -- | Parses a table array header. 104 | tableArrayHeader :: Parser [Text] 105 | tableArrayHeader = between (twoChar '[') (twoChar ']') headerValue 106 | where 107 | twoChar c = count 2 (char c) 108 | 109 | 110 | -- | Parses the value of any header (names separated by dots), into a list of 'Text'. 111 | headerValue :: Parser [Text] 112 | headerValue = ((pack <$> many1 keyChar) <|> anyStr') `sepBy1` (char '.') 113 | where 114 | keyChar = alphaNum <|> char '_' <|> char '-' 115 | 116 | -- | Parses a key-value assignment. 117 | assignment :: Parser (Text, Node) 118 | assignment = do 119 | k <- (pack <$> many1 keyChar) <|> anyStr' 120 | many (satisfy isSpc) >> char '=' >> skipBlanks 121 | v <- value 122 | return (k, v) 123 | where 124 | -- TODO: Follow the spec, e.g.: only first char cannot be '['. 125 | keyChar = alphaNum <|> char '_' <|> char '-' 126 | 127 | 128 | -- | Parses a value. 129 | value :: Parser Node 130 | value = (try array "array") 131 | <|> (try boolean "boolean") 132 | <|> (try anyStr "string") 133 | <|> (try datetime "datetime") 134 | <|> (try float "float") 135 | <|> (try integer "integer") 136 | <|> (try inlineTable "inline table") 137 | 138 | 139 | -- 140 | -- | * Toml value parsers 141 | -- 142 | 143 | array :: Parser Node 144 | array = (try (arrayOf array) "array of arrays") 145 | <|> (try (arrayOf boolean) "array of booleans") 146 | <|> (try (arrayOf anyStr) "array of strings") 147 | <|> (try (arrayOf datetime) "array of datetimes") 148 | <|> (try (arrayOf float) "array of floats") 149 | <|> (try (arrayOf integer) "array of integers") 150 | 151 | 152 | boolean :: Parser Node 153 | boolean = VBoolean <$> ( (try . string $ "true") *> return True <|> 154 | (try . string $ "false") *> return False ) 155 | 156 | 157 | anyStr :: Parser Node 158 | anyStr = VString <$> anyStr' 159 | 160 | anyStr' :: Parser Text 161 | anyStr' = try multiBasicStr <|> try basicStr <|> try multiLiteralStr <|> try literalStr 162 | 163 | 164 | basicStr :: Parser Text 165 | basicStr = between dQuote dQuote (fmap pack $ many strChar) 166 | where 167 | strChar = try escSeq <|> try (satisfy (\c -> c /= '"' && c /= '\\')) 168 | dQuote = char '\"' 169 | 170 | 171 | multiBasicStr :: Parser Text 172 | multiBasicStr = (openDQuote3 *> escWhiteSpc *> (pack <$> manyTill strChar (try dQuote3))) 173 | where 174 | -- | Parse the a tripple-double quote, with possibly a newline attached 175 | openDQuote3 = try (dQuote3 <* char '\n') <|> try dQuote3 176 | -- | Parse tripple-double quotes 177 | dQuote3 = count 3 $ char '"' 178 | -- | Parse a string char, accepting escaped codes, ignoring escaped white space 179 | strChar = (escSeq <|> (satisfy (/= '\\'))) <* escWhiteSpc 180 | -- | Parse escaped white space, if any 181 | escWhiteSpc = many $ char '\\' >> char '\n' >> (many $ satisfy (\c -> isSpc c || c == '\n')) 182 | 183 | 184 | literalStr :: Parser Text 185 | literalStr = between sQuote sQuote (pack <$> many (satisfy (/= '\''))) 186 | where 187 | sQuote = char '\'' 188 | 189 | 190 | multiLiteralStr :: Parser Text 191 | multiLiteralStr = (openSQuote3 *> (fmap pack $ manyTill anyChar sQuote3)) 192 | where 193 | -- | Parse the a tripple-single quote, with possibly a newline attached 194 | openSQuote3 = try (sQuote3 <* char '\n') <|> try sQuote3 195 | -- | Parse tripple-single quotes 196 | sQuote3 = try . count 3 . char $ '\'' 197 | 198 | 199 | datetime :: Parser Node 200 | datetime = do 201 | d <- try $ manyTill anyChar (char 'Z') 202 | #if MIN_VERSION_time(1,5,0) 203 | let mt = parseTimeM True defaultTimeLocale (iso8601DateFormat $ Just "%X") d 204 | #else 205 | let mt = parseTime defaultTimeLocale (iso8601DateFormat $ Just "%X") d 206 | #endif 207 | case mt of Just t -> return $ VDatetime t 208 | Nothing -> fail "parsing datetime failed" 209 | 210 | 211 | -- | Attoparsec 'double' parses scientific "e" notation; reimplement according to Toml spec. 212 | float :: Parser Node 213 | float = VFloat <$> do 214 | n <- intStr <* lookAhead (satisfy (\c -> c == '.' || c == 'e' || c == 'E')) 215 | d <- try (satisfy (== '.') *> uintStr) <|> return "0" 216 | e <- try (satisfy (\c -> c == 'e' || c == 'E') *> intStr) <|> return "0" 217 | return . read . L.concat $ [n, ".", d, "e", e] 218 | where 219 | sign = try (string "-") <|> (try (char '+') >> return "") <|> return "" 220 | uintStr = (:) <$> digit <*> many (optional (char '_') *> digit) 221 | intStr = do s <- sign 222 | u <- uintStr 223 | return . L.concat $ [s, u] 224 | 225 | 226 | integer :: Parser Node 227 | integer = VInteger <$> (signed $ read <$> uintStr) 228 | where 229 | uintStr :: Parser [Char] 230 | uintStr = (:) <$> digit <*> many (optional (char '_') *> digit) 231 | 232 | -- 233 | -- * Utility functions 234 | -- 235 | 236 | -- | Parses the elements of an array, while restricting them to a certain type. 237 | arrayOf :: Parser Node -> Parser Node 238 | arrayOf p = (VArray . V.fromList) <$> 239 | between (char '[') (char ']') (skipBlanks *> separatedValues) 240 | where 241 | separatedValues = sepEndBy (skipBlanks *> try p <* skipBlanks) comma <* skipBlanks 242 | comma = skipBlanks >> char ',' >> skipBlanks 243 | 244 | 245 | -- | Parser for escape sequences. 246 | escSeq :: Parser Char 247 | escSeq = char '\\' *> escSeqChar 248 | where 249 | escSeqChar = try (char '"') *> return '"' 250 | <|> try (char '\\') *> return '\\' 251 | <|> try (char '/') *> return '/' 252 | <|> try (char 'b') *> return '\b' 253 | <|> try (char 't') *> return '\t' 254 | <|> try (char 'n') *> return '\n' 255 | <|> try (char 'f') *> return '\f' 256 | <|> try (char 'r') *> return '\r' 257 | <|> try (char 'u') *> unicodeHex 4 258 | <|> try (char 'U') *> unicodeHex 8 259 | "escape character" 260 | 261 | 262 | -- | Parser for unicode hexadecimal values of representation length 'n'. 263 | unicodeHex :: Int -> Parser Char 264 | unicodeHex n = do 265 | h <- count n (satisfy isHex) 266 | let v = fst . head . readHex $ h 267 | return $ if v <= maxChar then toEnum v else '_' 268 | where 269 | isHex c = (c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') 270 | maxChar = fromEnum (maxBound :: Char) 271 | 272 | 273 | -- | Parser for signs (a plus or a minus). 274 | signed :: Num a => Parser a -> Parser a 275 | signed p = try (negate <$> (char '-' *> p)) 276 | <|> try (char '+' *> p) 277 | <|> try p 278 | 279 | 280 | -- | Parses the (rest of the) line including an EOF, whitespace and comments. 281 | skipBlanks :: Parser () 282 | skipBlanks = skipMany blank 283 | where 284 | blank = try ((many1 $ satisfy isSpc) >> return ()) <|> try comment <|> try eol 285 | comment = char '#' >> (many $ satisfy (/= '\n')) >> return () 286 | 287 | 288 | -- | Results in 'True' for whitespace chars, tab or space, according to spec. 289 | isSpc :: Char -> Bool 290 | isSpc c = c == ' ' || c == '\t' 291 | 292 | 293 | -- | Parse an EOL, as per TOML spec this is 0x0A a.k.a. '\n' or 0x0D a.k.a. '\r'. 294 | eol :: Parser () 295 | eol = (string "\n" <|> string "\r\n") >> return () 296 | -------------------------------------------------------------------------------- /src/Text/Toml/Types.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE FlexibleInstances #-} 2 | {-# LANGUAGE OverloadedStrings #-} 3 | 4 | module Text.Toml.Types 5 | ( Table 6 | , emptyTable 7 | , VTArray 8 | , VArray 9 | , Node (..) 10 | , Explicitness (..) 11 | , isExplicit 12 | , insert 13 | , ToJSON (..) 14 | , ToBsJSON (..) 15 | ) where 16 | 17 | import Control.Monad (when) 18 | import Text.Parsec 19 | import Data.Aeson.Types 20 | import Data.HashMap.Strict (HashMap) 21 | import qualified Data.HashMap.Strict as M 22 | import Data.Int (Int64) 23 | import Data.List (intersect) 24 | import Data.Set (Set) 25 | import qualified Data.Set as S 26 | import Data.Text (Text) 27 | import qualified Data.Text as T 28 | import Data.Time.Clock (UTCTime) 29 | import Data.Time.Format () 30 | import Data.Vector (Vector) 31 | import qualified Data.Vector as V 32 | 33 | 34 | -- | The TOML 'Table' is a mapping ('HashMap') of 'Text' keys to 'Node' values. 35 | type Table = HashMap Text Node 36 | 37 | -- | Contruct an empty 'Table'. 38 | emptyTable :: Table 39 | emptyTable = M.empty 40 | 41 | -- | An array of 'Table's, implemented using a 'Vector'. 42 | type VTArray = Vector Table 43 | 44 | -- | A \"value\" array that may contain zero or more 'Node's, implemented using a 'Vector'. 45 | type VArray = Vector Node 46 | 47 | -- | A 'Node' may contain any type of value that may be put in a 'VArray'. 48 | data Node = VTable !Table 49 | | VTArray !VTArray 50 | | VString !Text 51 | | VInteger !Int64 52 | | VFloat !Double 53 | | VBoolean !Bool 54 | | VDatetime !UTCTime 55 | | VArray !VArray 56 | deriving (Eq, Show) 57 | 58 | -- | To mark whether or not a 'Table' has been explicitly defined. 59 | -- See: https://github.com/toml-lang/toml/issues/376 60 | data Explicitness = Explicit | Implicit 61 | deriving (Eq, Show) 62 | 63 | -- | Convenience function to get a boolean value. 64 | isExplicit :: Explicitness -> Bool 65 | isExplicit Explicit = True 66 | isExplicit Implicit = False 67 | 68 | 69 | -- | Inserts a table, 'Table', with the namespaced name, '[Text]', (which 70 | -- may be part of a table array) into a 'Table'. 71 | -- It may result in an error in the 'ParsecT' monad for redefinitions. 72 | insert :: Explicitness -> ([Text], Node) -> Table -> Parsec Text (Set [Text]) Table 73 | insert _ ([], _) _ = parserFail "FATAL: Cannot call 'insert' without a name." 74 | insert ex ([name], node) ttbl = 75 | -- In case 'name' is final (a top-level name) 76 | case M.lookup name ttbl of 77 | Nothing -> do when (isExplicit ex) $ updateExState [name] node 78 | return $ M.insert name node ttbl 79 | Just (VTable t) -> case node of 80 | (VTable nt) -> case merge t nt of 81 | Left ds -> nameInsertError ds name 82 | Right r -> do when (isExplicit ex) $ 83 | updateExStateOrError [name] node 84 | return $ M.insert name (VTable r) ttbl 85 | _ -> commonInsertError node [name] 86 | Just (VTArray a) -> case node of 87 | (VTArray na) -> return $ M.insert name (VTArray $ a V.++ na) ttbl 88 | _ -> commonInsertError node [name] 89 | Just _ -> commonInsertError node [name] 90 | insert ex (fullName@(name:ns), node) ttbl = 91 | -- In case 'name' is not final (not a top-level name) 92 | case M.lookup name ttbl of 93 | Nothing -> do 94 | r <- insert Implicit (ns, node) emptyTable 95 | when (isExplicit ex) $ updateExState fullName node 96 | return $ M.insert name (VTable r) ttbl 97 | Just (VTable t) -> do 98 | r <- insert Implicit (ns, node) t 99 | when (isExplicit ex) $ updateExStateOrError fullName node 100 | return $ M.insert name (VTable r) ttbl 101 | Just (VTArray a) -> 102 | if V.null a 103 | then parserFail "FATAL: Call to 'insert' found impossibly empty VArray." 104 | else do r <- insert Implicit (ns, node) (V.last a) 105 | return $ M.insert name (VTArray $ (V.init a) `V.snoc` r) ttbl 106 | Just _ -> commonInsertError node fullName 107 | 108 | 109 | -- | Merge two tables, resulting in an error when overlapping keys are 110 | -- found ('Left' will contain those keys). When no overlapping keys are 111 | -- found the result will contain the union of both tables in a 'Right'. 112 | merge :: Table -> Table -> Either [Text] Table 113 | merge existing new = case M.keys existing `intersect` M.keys new of 114 | [] -> Right $ M.union existing new 115 | ds -> Left $ ds 116 | 117 | -- TOML tables maybe redefined when first definition was implicit. 118 | -- For instance a top-level table `a` can implicitly defined by defining a non top-level 119 | -- table `b` under it (namely with `[a.b]`). Once the table `a` is subsequently defined 120 | -- explicitly (namely with `[a]`), it is then not possible to (re-)define it again. 121 | -- A parser state of all explicitly defined tables is maintained, which allows 122 | -- raising errors for illegal redefinitions of such. 123 | updateExStateOrError :: [Text] -> Node -> Parsec Text (Set [Text]) () 124 | updateExStateOrError name node@(VTable _) = do 125 | explicitlyDefinedNames <- getState 126 | when (S.member name explicitlyDefinedNames) $ tableClashError name 127 | updateExState name node 128 | updateExStateOrError _ _ = return () 129 | 130 | -- | Like 'updateExStateOrError' but does not raise errors. Only use this when sure 131 | -- that redefinitions cannot occur. 132 | updateExState :: [Text] -> Node -> Parsec Text (S.Set [Text]) () 133 | updateExState name (VTable _) = modifyState $ S.insert name 134 | updateExState _ _ = return () 135 | 136 | 137 | -- * Parse errors resulting from invalid TOML 138 | 139 | -- | Key(s) redefintion error. 140 | nameInsertError :: [Text] -> Text -> Parsec Text (Set [Text]) a 141 | nameInsertError ns name = parserFail . T.unpack $ T.concat 142 | [ "Cannot redefine key(s) (", T.intercalate ", " ns 143 | , "), from table named '", name, "'." ] 144 | 145 | -- | Table redefinition error. 146 | tableClashError :: [Text] -> Parsec Text (Set [Text]) a 147 | tableClashError name = parserFail . T.unpack $ T.concat 148 | [ "Cannot redefine table named: '", T.intercalate "." name, "'." ] 149 | 150 | -- | Common redefinition error. 151 | commonInsertError :: Node -> [Text] -> Parsec Text (Set [Text]) a 152 | commonInsertError what name = parserFail . concat $ 153 | [ "Cannot insert ", w, " as '", n, "' since key already exists." ] 154 | where 155 | n = T.unpack $ T.intercalate "." name 156 | w = case what of (VTable _) -> "tables" 157 | _ -> "array of tables" 158 | 159 | 160 | -- * Regular ToJSON instances 161 | 162 | -- | 'ToJSON' instances for the 'Node' type that produce Aeson (JSON) 163 | -- in line with the TOML specification. 164 | instance ToJSON Node where 165 | toJSON (VTable v) = toJSON v 166 | toJSON (VTArray v) = toJSON v 167 | toJSON (VString v) = toJSON v 168 | toJSON (VInteger v) = toJSON v 169 | toJSON (VFloat v) = toJSON v 170 | toJSON (VBoolean v) = toJSON v 171 | toJSON (VDatetime v) = toJSON v 172 | toJSON (VArray v) = toJSON v 173 | 174 | 175 | 176 | -- * Special BurntSushi ToJSON type class and instances 177 | 178 | -- | Type class for conversion to BurntSushi-style JSON. 179 | -- 180 | -- BurntSushi has made a language agnostic test suite available that 181 | -- this library uses. This test suit expects that values are encoded 182 | -- as JSON objects with a 'type' and a 'value' member. 183 | class ToBsJSON a where 184 | toBsJSON :: a -> Value 185 | 186 | -- | Provide a 'toBsJSON' instance to the 'VTArray'. 187 | instance (ToBsJSON a) => ToBsJSON (Vector a) where 188 | toBsJSON = Array . V.map toBsJSON 189 | {-# INLINE toBsJSON #-} 190 | 191 | -- | Provide a 'toBsJSON' instance to the 'NTable'. 192 | instance (ToBsJSON v) => ToBsJSON (M.HashMap Text v) where 193 | toBsJSON = Object . M.map toBsJSON 194 | {-# INLINE toBsJSON #-} 195 | 196 | -- | 'ToBsJSON' instances for the 'TValue' type that produce Aeson (JSON) 197 | -- in line with BurntSushi's language agnostic TOML test suite. 198 | -- 199 | -- As seen in this function, BurntSushi's JSON encoding explicitly 200 | -- specifies the types of the values. 201 | instance ToBsJSON Node where 202 | toBsJSON (VTable v) = toBsJSON v 203 | toBsJSON (VTArray v) = toBsJSON v 204 | toBsJSON (VString v) = object [ "type" .= toJSON ("string" :: String) 205 | , "value" .= toJSON v ] 206 | toBsJSON (VInteger v) = object [ "type" .= toJSON ("integer" :: String) 207 | , "value" .= toJSON (show v) ] 208 | toBsJSON (VFloat v) = object [ "type" .= toJSON ("float" :: String) 209 | , "value" .= toJSON (show v) ] 210 | toBsJSON (VBoolean v) = object [ "type" .= toJSON ("bool" :: String) 211 | , "value" .= toJSON (if v then "true" else "false" :: String) ] 212 | toBsJSON (VDatetime v) = object [ "type" .= toJSON ("datetime" :: String) 213 | , "value" .= toJSON (let s = show v 214 | z = take (length s - 4) s ++ "Z" 215 | d = take (length z - 10) z 216 | t = drop (length z - 9) z 217 | in d ++ "T" ++ t) ] 218 | toBsJSON (VArray v) = object [ "type" .= toJSON ("array" :: String) 219 | , "value" .= toBsJSON v ] 220 | -------------------------------------------------------------------------------- /test/BurntSushi.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | {-# LANGUAGE TemplateHaskell #-} 3 | 4 | module BurntSushi (tests) where 5 | 6 | import Test.Tasty (TestTree, testGroup) 7 | import Test.Tasty.HUnit 8 | 9 | import Data.Aeson 10 | import qualified Data.ByteString as B 11 | import Data.ByteString.Lazy (fromStrict) 12 | import Data.FileEmbed 13 | import Data.List (isPrefixOf, isSuffixOf) 14 | import Data.Text.Encoding (decodeUtf8) 15 | 16 | import Text.Toml 17 | import Text.Toml.Types 18 | 19 | 20 | allFiles :: [(FilePath, B.ByteString)] 21 | allFiles = $(makeRelativeToProject "test/BurntSushi" >>= embedDir) 22 | 23 | 24 | validPairs :: [(String, (B.ByteString, B.ByteString))] 25 | validPairs = 26 | map (\(tFP, tBS) -> (stripExt tFP, (tBS, jsonCounterpart tFP))) tomlFiles 27 | where 28 | validFiles = filter (\(f, _) -> "valid" `isPrefixOf` f) allFiles 29 | filterOnSuffix sfx = filter (\(f, _) -> sfx `isSuffixOf` f) 30 | tomlFiles = filterOnSuffix ".toml" validFiles 31 | jsonFiles = filterOnSuffix ".json" validFiles 32 | stripExt fp = take (length fp - 5) fp 33 | jsonCounterpart tFP = 34 | case filter (\(f, _) -> f == stripExt tFP ++ ".json") jsonFiles of 35 | [] -> error $ "Could not find a JSON counterpart for: " ++ tFP 36 | [(_, j)] -> j 37 | _ -> error $ "Expected one, but found several \ 38 | \JSON counterparts for: " ++ tFP 39 | 40 | 41 | invalidTomlFiles :: [(FilePath, B.ByteString)] 42 | invalidTomlFiles = filter (\(f, _) -> "invalid" `isPrefixOf` f) allFiles 43 | 44 | 45 | tests :: IO TestTree 46 | tests = return $ testGroup "BurntSushi's test suite" 47 | [ testGroup "test equality of resulting JSON (valid)" $ 48 | map (\(fp, (tBS, jBS)) -> testCase fp $ assertIsValid fp tBS jBS) validPairs 49 | , testGroup "test parse failures of malformed TOML files (invalid)" $ 50 | map (\(fp, tBS) -> testCase fp $ assertParseFailure fp tBS) invalidTomlFiles 51 | ] 52 | where 53 | assertIsValid f tomlBS jsonBS = 54 | case parseTomlDoc "test" (decodeUtf8 tomlBS) of 55 | Left e -> assertFailure $ "Could not parse TOML file: " ++ f ++ ".toml\n" ++ (show e) 56 | Right tomlTry -> case eitherDecode (fromStrict jsonBS) of 57 | Left _ -> assertFailure $ "Could not parse JSON file: " ++ f ++ ".json" 58 | Right jsonCorrect -> assertEqual "" jsonCorrect (toBsJSON tomlTry) 59 | assertParseFailure f tomlBS = 60 | case parseTomlDoc "test" (decodeUtf8 tomlBS) of 61 | Left _ -> return () 62 | Right _ -> assertFailure $ "Parser accepted invalid TOML file: " ++ f 63 | -------------------------------------------------------------------------------- /test/BurntSushi/fetch-toml-tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | rm valid/* 4 | rmdir valid 5 | rm invalid/* 6 | rmdir invalid 7 | 8 | git clone https://github.com/BurntSushi/toml-test 9 | 10 | mv toml-test/tests/valid . 11 | mv toml-test/tests/invalid . 12 | 13 | rm -rf toml-test 14 | -------------------------------------------------------------------------------- /test/BurntSushi/invalid/array-mixed-types-arrays-and-ints.toml: -------------------------------------------------------------------------------- 1 | arrays-and-ints = [1, ["Arrays are not integers."]] 2 | -------------------------------------------------------------------------------- /test/BurntSushi/invalid/array-mixed-types-ints-and-floats.toml: -------------------------------------------------------------------------------- 1 | ints-and-floats = [1, 1.1] 2 | -------------------------------------------------------------------------------- /test/BurntSushi/invalid/array-mixed-types-strings-and-ints.toml: -------------------------------------------------------------------------------- 1 | strings-and-ints = ["hi", 42] 2 | -------------------------------------------------------------------------------- /test/BurntSushi/invalid/datetime-malformed-no-leads.toml: -------------------------------------------------------------------------------- 1 | no-leads = 1987-7-05T17:45:00Z 2 | -------------------------------------------------------------------------------- /test/BurntSushi/invalid/datetime-malformed-no-secs.toml: -------------------------------------------------------------------------------- 1 | no-secs = 1987-07-05T17:45Z 2 | -------------------------------------------------------------------------------- /test/BurntSushi/invalid/datetime-malformed-no-t.toml: -------------------------------------------------------------------------------- 1 | no-t = 1987-07-0517:45:00Z 2 | -------------------------------------------------------------------------------- /test/BurntSushi/invalid/datetime-malformed-no-z.toml: -------------------------------------------------------------------------------- 1 | no-z = 1987-07-05T17:45:00 2 | -------------------------------------------------------------------------------- /test/BurntSushi/invalid/datetime-malformed-with-milli.toml: -------------------------------------------------------------------------------- 1 | with-milli = 1987-07-5T17:45:00.12Z 2 | -------------------------------------------------------------------------------- /test/BurntSushi/invalid/duplicate-key-table.toml: -------------------------------------------------------------------------------- 1 | [fruit] 2 | type = "apple" 3 | 4 | [fruit.type] 5 | apple = "yes" 6 | -------------------------------------------------------------------------------- /test/BurntSushi/invalid/duplicate-keys.toml: -------------------------------------------------------------------------------- 1 | dupe = false 2 | dupe = true 3 | -------------------------------------------------------------------------------- /test/BurntSushi/invalid/duplicate-tables.toml: -------------------------------------------------------------------------------- 1 | [a] 2 | [a] 3 | -------------------------------------------------------------------------------- /test/BurntSushi/invalid/empty-implicit-table.toml: -------------------------------------------------------------------------------- 1 | [naughty..naughty] 2 | -------------------------------------------------------------------------------- /test/BurntSushi/invalid/empty-table.toml: -------------------------------------------------------------------------------- 1 | [] 2 | -------------------------------------------------------------------------------- /test/BurntSushi/invalid/float-no-leading-zero.toml: -------------------------------------------------------------------------------- 1 | answer = .12345 2 | neganswer = -.12345 3 | -------------------------------------------------------------------------------- /test/BurntSushi/invalid/float-no-trailing-digits.toml: -------------------------------------------------------------------------------- 1 | answer = 1. 2 | neganswer = -1. 3 | -------------------------------------------------------------------------------- /test/BurntSushi/invalid/key-empty.toml: -------------------------------------------------------------------------------- 1 | = 1 2 | -------------------------------------------------------------------------------- /test/BurntSushi/invalid/key-hash.toml: -------------------------------------------------------------------------------- 1 | a# = 1 2 | -------------------------------------------------------------------------------- /test/BurntSushi/invalid/key-newline.toml: -------------------------------------------------------------------------------- 1 | a 2 | = 1 3 | -------------------------------------------------------------------------------- /test/BurntSushi/invalid/key-open-bracket.toml: -------------------------------------------------------------------------------- 1 | [abc = 1 2 | -------------------------------------------------------------------------------- /test/BurntSushi/invalid/key-single-open-bracket.toml: -------------------------------------------------------------------------------- 1 | [ -------------------------------------------------------------------------------- /test/BurntSushi/invalid/key-space.toml: -------------------------------------------------------------------------------- 1 | a b = 1 -------------------------------------------------------------------------------- /test/BurntSushi/invalid/key-start-bracket.toml: -------------------------------------------------------------------------------- 1 | [a] 2 | [xyz = 5 3 | [b] 4 | -------------------------------------------------------------------------------- /test/BurntSushi/invalid/key-two-equals.toml: -------------------------------------------------------------------------------- 1 | key= = 1 2 | -------------------------------------------------------------------------------- /test/BurntSushi/invalid/string-bad-byte-escape.toml: -------------------------------------------------------------------------------- 1 | naughty = "\xAg" 2 | -------------------------------------------------------------------------------- /test/BurntSushi/invalid/string-bad-escape.toml: -------------------------------------------------------------------------------- 1 | invalid-escape = "This string has a bad \a escape character." 2 | -------------------------------------------------------------------------------- /test/BurntSushi/invalid/string-byte-escapes.toml: -------------------------------------------------------------------------------- 1 | answer = "\x33" 2 | -------------------------------------------------------------------------------- /test/BurntSushi/invalid/string-no-close.toml: -------------------------------------------------------------------------------- 1 | no-ending-quote = "One time, at band camp 2 | -------------------------------------------------------------------------------- /test/BurntSushi/invalid/table-array-implicit.toml: -------------------------------------------------------------------------------- 1 | # This test is a bit tricky. It should fail because the first use of 2 | # `[[albums.songs]]` without first declaring `albums` implies that `albums` 3 | # must be a table. The alternative would be quite weird. Namely, it wouldn't 4 | # comply with the TOML spec: "Each double-bracketed sub-table will belong to 5 | # the most *recently* defined table element *above* it." 6 | # 7 | # This is in contrast to the *valid* test, table-array-implicit where 8 | # `[[albums.songs]]` works by itself, so long as `[[albums]]` isn't declared 9 | # later. (Although, `[albums]` could be.) 10 | [[albums.songs]] 11 | name = "Glory Days" 12 | 13 | [[albums]] 14 | name = "Born in the USA" 15 | -------------------------------------------------------------------------------- /test/BurntSushi/invalid/table-array-malformed-bracket.toml: -------------------------------------------------------------------------------- 1 | [[albums] 2 | name = "Born to Run" 3 | -------------------------------------------------------------------------------- /test/BurntSushi/invalid/table-array-malformed-empty.toml: -------------------------------------------------------------------------------- 1 | [[]] 2 | name = "Born to Run" 3 | -------------------------------------------------------------------------------- /test/BurntSushi/invalid/table-empty.toml: -------------------------------------------------------------------------------- 1 | [] 2 | -------------------------------------------------------------------------------- /test/BurntSushi/invalid/table-nested-brackets-close.toml: -------------------------------------------------------------------------------- 1 | [a]b] 2 | zyx = 42 3 | -------------------------------------------------------------------------------- /test/BurntSushi/invalid/table-nested-brackets-open.toml: -------------------------------------------------------------------------------- 1 | [a[b] 2 | zyx = 42 3 | -------------------------------------------------------------------------------- /test/BurntSushi/invalid/table-whitespace.toml: -------------------------------------------------------------------------------- 1 | [invalid key] -------------------------------------------------------------------------------- /test/BurntSushi/invalid/table-with-pound.toml: -------------------------------------------------------------------------------- 1 | [key#group] 2 | answer = 42 -------------------------------------------------------------------------------- /test/BurntSushi/invalid/text-after-array-entries.toml: -------------------------------------------------------------------------------- 1 | array = [ 2 | "Is there life after an array separator?", No 3 | "Entry" 4 | ] 5 | -------------------------------------------------------------------------------- /test/BurntSushi/invalid/text-after-integer.toml: -------------------------------------------------------------------------------- 1 | answer = 42 the ultimate answer? 2 | -------------------------------------------------------------------------------- /test/BurntSushi/invalid/text-after-string.toml: -------------------------------------------------------------------------------- 1 | string = "Is there life after strings?" No. 2 | -------------------------------------------------------------------------------- /test/BurntSushi/invalid/text-after-table.toml: -------------------------------------------------------------------------------- 1 | [error] this shouldn't be here 2 | -------------------------------------------------------------------------------- /test/BurntSushi/invalid/text-before-array-separator.toml: -------------------------------------------------------------------------------- 1 | array = [ 2 | "Is there life before an array separator?" No, 3 | "Entry" 4 | ] 5 | -------------------------------------------------------------------------------- /test/BurntSushi/invalid/text-in-array.toml: -------------------------------------------------------------------------------- 1 | array = [ 2 | "Entry 1", 3 | I don't belong, 4 | "Entry 2", 5 | ] 6 | -------------------------------------------------------------------------------- /test/BurntSushi/valid/array-empty.json: -------------------------------------------------------------------------------- 1 | { 2 | "thevoid": { "type": "array", "value": [ 3 | {"type": "array", "value": [ 4 | {"type": "array", "value": [ 5 | {"type": "array", "value": [ 6 | {"type": "array", "value": []} 7 | ]} 8 | ]} 9 | ]} 10 | ]} 11 | } 12 | -------------------------------------------------------------------------------- /test/BurntSushi/valid/array-empty.toml: -------------------------------------------------------------------------------- 1 | thevoid = [[[[[]]]]] 2 | -------------------------------------------------------------------------------- /test/BurntSushi/valid/array-nospaces.json: -------------------------------------------------------------------------------- 1 | { 2 | "ints": { 3 | "type": "array", 4 | "value": [ 5 | {"type": "integer", "value": "1"}, 6 | {"type": "integer", "value": "2"}, 7 | {"type": "integer", "value": "3"} 8 | ] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/BurntSushi/valid/array-nospaces.toml: -------------------------------------------------------------------------------- 1 | ints = [1,2,3] 2 | -------------------------------------------------------------------------------- /test/BurntSushi/valid/arrays-hetergeneous.json: -------------------------------------------------------------------------------- 1 | { 2 | "mixed": { 3 | "type": "array", 4 | "value": [ 5 | {"type": "array", "value": [ 6 | {"type": "integer", "value": "1"}, 7 | {"type": "integer", "value": "2"} 8 | ]}, 9 | {"type": "array", "value": [ 10 | {"type": "string", "value": "a"}, 11 | {"type": "string", "value": "b"} 12 | ]}, 13 | {"type": "array", "value": [ 14 | {"type": "float", "value": "1.1"}, 15 | {"type": "float", "value": "2.1"} 16 | ]} 17 | ] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /test/BurntSushi/valid/arrays-hetergeneous.toml: -------------------------------------------------------------------------------- 1 | mixed = [[1, 2], ["a", "b"], [1.1, 2.1]] 2 | -------------------------------------------------------------------------------- /test/BurntSushi/valid/arrays-nested.json: -------------------------------------------------------------------------------- 1 | { 2 | "nest": { 3 | "type": "array", 4 | "value": [ 5 | {"type": "array", "value": [ 6 | {"type": "string", "value": "a"} 7 | ]}, 8 | {"type": "array", "value": [ 9 | {"type": "string", "value": "b"} 10 | ]} 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test/BurntSushi/valid/arrays-nested.toml: -------------------------------------------------------------------------------- 1 | nest = [["a"], ["b"]] 2 | -------------------------------------------------------------------------------- /test/BurntSushi/valid/arrays.json: -------------------------------------------------------------------------------- 1 | { 2 | "ints": { 3 | "type": "array", 4 | "value": [ 5 | {"type": "integer", "value": "1"}, 6 | {"type": "integer", "value": "2"}, 7 | {"type": "integer", "value": "3"} 8 | ] 9 | }, 10 | "floats": { 11 | "type": "array", 12 | "value": [ 13 | {"type": "float", "value": "1.1"}, 14 | {"type": "float", "value": "2.1"}, 15 | {"type": "float", "value": "3.1"} 16 | ] 17 | }, 18 | "strings": { 19 | "type": "array", 20 | "value": [ 21 | {"type": "string", "value": "a"}, 22 | {"type": "string", "value": "b"}, 23 | {"type": "string", "value": "c"} 24 | ] 25 | }, 26 | "dates": { 27 | "type": "array", 28 | "value": [ 29 | {"type": "datetime", "value": "1987-07-05T17:45:00Z"}, 30 | {"type": "datetime", "value": "1979-05-27T07:32:00Z"}, 31 | {"type": "datetime", "value": "2006-06-01T11:00:00Z"} 32 | ] 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /test/BurntSushi/valid/arrays.toml: -------------------------------------------------------------------------------- 1 | ints = [1, 2, 3] 2 | floats = [1.1, 2.1, 3.1] 3 | strings = ["a", "b", "c"] 4 | dates = [ 5 | 1987-07-05T17:45:00Z, 6 | 1979-05-27T07:32:00Z, 7 | 2006-06-01T11:00:00Z, 8 | ] 9 | -------------------------------------------------------------------------------- /test/BurntSushi/valid/bool.json: -------------------------------------------------------------------------------- 1 | { 2 | "f": {"type": "bool", "value": "false"}, 3 | "t": {"type": "bool", "value": "true"} 4 | } 5 | -------------------------------------------------------------------------------- /test/BurntSushi/valid/bool.toml: -------------------------------------------------------------------------------- 1 | t = true 2 | f = false 3 | -------------------------------------------------------------------------------- /test/BurntSushi/valid/comments-everywhere.json: -------------------------------------------------------------------------------- 1 | { 2 | "group": { 3 | "answer": {"type": "integer", "value": "42"}, 4 | "more": { 5 | "type": "array", 6 | "value": [ 7 | {"type": "integer", "value": "42"}, 8 | {"type": "integer", "value": "42"} 9 | ] 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/BurntSushi/valid/comments-everywhere.toml: -------------------------------------------------------------------------------- 1 | # Top comment. 2 | # Top comment. 3 | # Top comment. 4 | 5 | # [no-extraneous-groups-please] 6 | 7 | [group] # Comment 8 | answer = 42 # Comment 9 | # no-extraneous-keys-please = 999 10 | # Inbetween comment. 11 | more = [ # Comment 12 | # What about multiple # comments? 13 | # Can you handle it? 14 | # 15 | # Evil. 16 | # Evil. 17 | 42, 42, # Comments within arrays are fun. 18 | # What about multiple # comments? 19 | # Can you handle it? 20 | # 21 | # Evil. 22 | # Evil. 23 | # ] Did I fool you? 24 | ] # Hopefully not. 25 | -------------------------------------------------------------------------------- /test/BurntSushi/valid/datetime.json: -------------------------------------------------------------------------------- 1 | { 2 | "bestdayever": {"type": "datetime", "value": "1987-07-05T17:45:00Z"} 3 | } 4 | -------------------------------------------------------------------------------- /test/BurntSushi/valid/datetime.toml: -------------------------------------------------------------------------------- 1 | bestdayever = 1987-07-05T17:45:00Z 2 | -------------------------------------------------------------------------------- /test/BurntSushi/valid/empty.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /test/BurntSushi/valid/empty.toml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cies/htoml/f776a75eda018b6885bfc802757cd3ea3d26c7d7/test/BurntSushi/valid/empty.toml -------------------------------------------------------------------------------- /test/BurntSushi/valid/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "best-day-ever": {"type": "datetime", "value": "1987-07-05T17:45:00Z"}, 3 | "numtheory": { 4 | "boring": {"type": "bool", "value": "false"}, 5 | "perfection": { 6 | "type": "array", 7 | "value": [ 8 | {"type": "integer", "value": "6"}, 9 | {"type": "integer", "value": "28"}, 10 | {"type": "integer", "value": "496"} 11 | ] 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test/BurntSushi/valid/example.toml: -------------------------------------------------------------------------------- 1 | best-day-ever = 1987-07-05T17:45:00Z 2 | 3 | [numtheory] 4 | boring = false 5 | perfection = [6, 28, 496] 6 | -------------------------------------------------------------------------------- /test/BurntSushi/valid/float.json: -------------------------------------------------------------------------------- 1 | { 2 | "pi": {"type": "float", "value": "3.14"}, 3 | "negpi": {"type": "float", "value": "-3.14"} 4 | } 5 | -------------------------------------------------------------------------------- /test/BurntSushi/valid/float.toml: -------------------------------------------------------------------------------- 1 | pi = 3.14 2 | negpi = -3.14 3 | -------------------------------------------------------------------------------- /test/BurntSushi/valid/implicit-and-explicit-after.json: -------------------------------------------------------------------------------- 1 | { 2 | "a": { 3 | "better": {"type": "integer", "value": "43"}, 4 | "b": { 5 | "c": { 6 | "answer": {"type": "integer", "value": "42"} 7 | } 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/BurntSushi/valid/implicit-and-explicit-after.toml: -------------------------------------------------------------------------------- 1 | [a.b.c] 2 | answer = 42 3 | 4 | [a] 5 | better = 43 6 | -------------------------------------------------------------------------------- /test/BurntSushi/valid/implicit-and-explicit-before.json: -------------------------------------------------------------------------------- 1 | { 2 | "a": { 3 | "better": {"type": "integer", "value": "43"}, 4 | "b": { 5 | "c": { 6 | "answer": {"type": "integer", "value": "42"} 7 | } 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/BurntSushi/valid/implicit-and-explicit-before.toml: -------------------------------------------------------------------------------- 1 | [a] 2 | better = 43 3 | 4 | [a.b.c] 5 | answer = 42 6 | -------------------------------------------------------------------------------- /test/BurntSushi/valid/implicit-groups.json: -------------------------------------------------------------------------------- 1 | { 2 | "a": { 3 | "b": { 4 | "c": { 5 | "answer": {"type": "integer", "value": "42"} 6 | } 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/BurntSushi/valid/implicit-groups.toml: -------------------------------------------------------------------------------- 1 | [a.b.c] 2 | answer = 42 3 | -------------------------------------------------------------------------------- /test/BurntSushi/valid/integer.json: -------------------------------------------------------------------------------- 1 | { 2 | "answer": {"type": "integer", "value": "42"}, 3 | "neganswer": {"type": "integer", "value": "-42"} 4 | } 5 | -------------------------------------------------------------------------------- /test/BurntSushi/valid/integer.toml: -------------------------------------------------------------------------------- 1 | answer = 42 2 | neganswer = -42 3 | -------------------------------------------------------------------------------- /test/BurntSushi/valid/key-equals-nospace.json: -------------------------------------------------------------------------------- 1 | { 2 | "answer": {"type": "integer", "value": "42"} 3 | } 4 | -------------------------------------------------------------------------------- /test/BurntSushi/valid/key-equals-nospace.toml: -------------------------------------------------------------------------------- 1 | answer=42 2 | -------------------------------------------------------------------------------- /test/BurntSushi/valid/key-space.json: -------------------------------------------------------------------------------- 1 | { 2 | "a b": {"type": "integer", "value": "1"} 3 | } 4 | -------------------------------------------------------------------------------- /test/BurntSushi/valid/key-space.toml: -------------------------------------------------------------------------------- 1 | "a b" = 1 2 | -------------------------------------------------------------------------------- /test/BurntSushi/valid/key-special-chars.json: -------------------------------------------------------------------------------- 1 | { 2 | "~!@$^&*()_+-`1234567890[]|/?><.,;:'": { 3 | "type": "integer", "value": "1" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/BurntSushi/valid/key-special-chars.toml: -------------------------------------------------------------------------------- 1 | "~!@$^&*()_+-`1234567890[]|/?><.,;:'" = 1 2 | -------------------------------------------------------------------------------- /test/BurntSushi/valid/long-float.json: -------------------------------------------------------------------------------- 1 | { 2 | "longpi": {"type": "float", "value": "3.141592653589793"}, 3 | "neglongpi": {"type": "float", "value": "-3.141592653589793"} 4 | } 5 | -------------------------------------------------------------------------------- /test/BurntSushi/valid/long-float.toml: -------------------------------------------------------------------------------- 1 | longpi = 3.141592653589793 2 | neglongpi = -3.141592653589793 3 | -------------------------------------------------------------------------------- /test/BurntSushi/valid/long-integer.json: -------------------------------------------------------------------------------- 1 | { 2 | "answer": {"type": "integer", "value": "9223372036854775807"}, 3 | "neganswer": {"type": "integer", "value": "-9223372036854775808"} 4 | } 5 | -------------------------------------------------------------------------------- /test/BurntSushi/valid/long-integer.toml: -------------------------------------------------------------------------------- 1 | answer = 9223372036854775807 2 | neganswer = -9223372036854775808 3 | -------------------------------------------------------------------------------- /test/BurntSushi/valid/multiline-string.json: -------------------------------------------------------------------------------- 1 | { 2 | "multiline_empty_one": { 3 | "type": "string", 4 | "value": "" 5 | }, 6 | "multiline_empty_two": { 7 | "type": "string", 8 | "value": "" 9 | }, 10 | "multiline_empty_three": { 11 | "type": "string", 12 | "value": "" 13 | }, 14 | "multiline_empty_four": { 15 | "type": "string", 16 | "value": "" 17 | }, 18 | "equivalent_one": { 19 | "type": "string", 20 | "value": "The quick brown fox jumps over the lazy dog." 21 | }, 22 | "equivalent_two": { 23 | "type": "string", 24 | "value": "The quick brown fox jumps over the lazy dog." 25 | }, 26 | "equivalent_three": { 27 | "type": "string", 28 | "value": "The quick brown fox jumps over the lazy dog." 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /test/BurntSushi/valid/multiline-string.toml: -------------------------------------------------------------------------------- 1 | multiline_empty_one = """""" 2 | multiline_empty_two = """ 3 | """ 4 | multiline_empty_three = """\ 5 | """ 6 | multiline_empty_four = """\ 7 | \ 8 | \ 9 | """ 10 | 11 | equivalent_one = "The quick brown fox jumps over the lazy dog." 12 | equivalent_two = """ 13 | The quick brown \ 14 | 15 | 16 | fox jumps over \ 17 | the lazy dog.""" 18 | 19 | equivalent_three = """\ 20 | The quick brown \ 21 | fox jumps over \ 22 | the lazy dog.\ 23 | """ 24 | -------------------------------------------------------------------------------- /test/BurntSushi/valid/raw-multiline-string.json: -------------------------------------------------------------------------------- 1 | { 2 | "oneline": { 3 | "type": "string", 4 | "value": "This string has a ' quote character." 5 | }, 6 | "firstnl": { 7 | "type": "string", 8 | "value": "This string has a ' quote character." 9 | }, 10 | "multiline": { 11 | "type": "string", 12 | "value": "This string\nhas ' a quote character\nand more than\none newline\nin it." 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test/BurntSushi/valid/raw-multiline-string.toml: -------------------------------------------------------------------------------- 1 | oneline = '''This string has a ' quote character.''' 2 | firstnl = ''' 3 | This string has a ' quote character.''' 4 | multiline = ''' 5 | This string 6 | has ' a quote character 7 | and more than 8 | one newline 9 | in it.''' 10 | -------------------------------------------------------------------------------- /test/BurntSushi/valid/raw-string.json: -------------------------------------------------------------------------------- 1 | { 2 | "backspace": { 3 | "type": "string", 4 | "value": "This string has a \\b backspace character." 5 | }, 6 | "tab": { 7 | "type": "string", 8 | "value": "This string has a \\t tab character." 9 | }, 10 | "newline": { 11 | "type": "string", 12 | "value": "This string has a \\n new line character." 13 | }, 14 | "formfeed": { 15 | "type": "string", 16 | "value": "This string has a \\f form feed character." 17 | }, 18 | "carriage": { 19 | "type": "string", 20 | "value": "This string has a \\r carriage return character." 21 | }, 22 | "slash": { 23 | "type": "string", 24 | "value": "This string has a \\/ slash character." 25 | }, 26 | "backslash": { 27 | "type": "string", 28 | "value": "This string has a \\\\ backslash character." 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /test/BurntSushi/valid/raw-string.toml: -------------------------------------------------------------------------------- 1 | backspace = 'This string has a \b backspace character.' 2 | tab = 'This string has a \t tab character.' 3 | newline = 'This string has a \n new line character.' 4 | formfeed = 'This string has a \f form feed character.' 5 | carriage = 'This string has a \r carriage return character.' 6 | slash = 'This string has a \/ slash character.' 7 | backslash = 'This string has a \\ backslash character.' 8 | -------------------------------------------------------------------------------- /test/BurntSushi/valid/string-empty.json: -------------------------------------------------------------------------------- 1 | { 2 | "answer": { 3 | "type": "string", 4 | "value": "" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/BurntSushi/valid/string-empty.toml: -------------------------------------------------------------------------------- 1 | answer = "" 2 | -------------------------------------------------------------------------------- /test/BurntSushi/valid/string-escapes.json: -------------------------------------------------------------------------------- 1 | { 2 | "backspace": { 3 | "type": "string", 4 | "value": "This string has a \u0008 backspace character." 5 | }, 6 | "tab": { 7 | "type": "string", 8 | "value": "This string has a \u0009 tab character." 9 | }, 10 | "newline": { 11 | "type": "string", 12 | "value": "This string has a \u000A new line character." 13 | }, 14 | "formfeed": { 15 | "type": "string", 16 | "value": "This string has a \u000C form feed character." 17 | }, 18 | "carriage": { 19 | "type": "string", 20 | "value": "This string has a \u000D carriage return character." 21 | }, 22 | "quote": { 23 | "type": "string", 24 | "value": "This string has a \u0022 quote character." 25 | }, 26 | "backslash": { 27 | "type": "string", 28 | "value": "This string has a \u005C backslash character." 29 | }, 30 | "notunicode1": { 31 | "type": "string", 32 | "value": "This string does not have a unicode \\u escape." 33 | }, 34 | "notunicode2": { 35 | "type": "string", 36 | "value": "This string does not have a unicode \u005Cu escape." 37 | }, 38 | "notunicode3": { 39 | "type": "string", 40 | "value": "This string does not have a unicode \\u0075 escape." 41 | }, 42 | "notunicode4": { 43 | "type": "string", 44 | "value": "This string does not have a unicode \\\u0075 escape." 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /test/BurntSushi/valid/string-escapes.toml: -------------------------------------------------------------------------------- 1 | backspace = "This string has a \b backspace character." 2 | tab = "This string has a \t tab character." 3 | newline = "This string has a \n new line character." 4 | formfeed = "This string has a \f form feed character." 5 | carriage = "This string has a \r carriage return character." 6 | quote = "This string has a \" quote character." 7 | backslash = "This string has a \\ backslash character." 8 | notunicode1 = "This string does not have a unicode \\u escape." 9 | notunicode2 = "This string does not have a unicode \u005Cu escape." 10 | notunicode3 = "This string does not have a unicode \\u0075 escape." 11 | notunicode4 = "This string does not have a unicode \\\u0075 escape." 12 | -------------------------------------------------------------------------------- /test/BurntSushi/valid/string-simple.json: -------------------------------------------------------------------------------- 1 | { 2 | "answer": { 3 | "type": "string", 4 | "value": "You are not drinking enough whisky." 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/BurntSushi/valid/string-simple.toml: -------------------------------------------------------------------------------- 1 | answer = "You are not drinking enough whisky." 2 | -------------------------------------------------------------------------------- /test/BurntSushi/valid/string-with-pound.json: -------------------------------------------------------------------------------- 1 | { 2 | "pound": {"type": "string", "value": "We see no # comments here."}, 3 | "poundcomment": { 4 | "type": "string", 5 | "value": "But there are # some comments here." 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/BurntSushi/valid/string-with-pound.toml: -------------------------------------------------------------------------------- 1 | pound = "We see no # comments here." 2 | poundcomment = "But there are # some comments here." # Did I # mess you up? 3 | -------------------------------------------------------------------------------- /test/BurntSushi/valid/table-array-implicit.json: -------------------------------------------------------------------------------- 1 | { 2 | "albums": { 3 | "songs": [ 4 | {"name": {"type": "string", "value": "Glory Days"}} 5 | ] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/BurntSushi/valid/table-array-implicit.toml: -------------------------------------------------------------------------------- 1 | [[albums.songs]] 2 | name = "Glory Days" 3 | -------------------------------------------------------------------------------- /test/BurntSushi/valid/table-array-many.json: -------------------------------------------------------------------------------- 1 | { 2 | "people": [ 3 | { 4 | "first_name": {"type": "string", "value": "Bruce"}, 5 | "last_name": {"type": "string", "value": "Springsteen"} 6 | }, 7 | { 8 | "first_name": {"type": "string", "value": "Eric"}, 9 | "last_name": {"type": "string", "value": "Clapton"} 10 | }, 11 | { 12 | "first_name": {"type": "string", "value": "Bob"}, 13 | "last_name": {"type": "string", "value": "Seger"} 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /test/BurntSushi/valid/table-array-many.toml: -------------------------------------------------------------------------------- 1 | [[people]] 2 | first_name = "Bruce" 3 | last_name = "Springsteen" 4 | 5 | [[people]] 6 | first_name = "Eric" 7 | last_name = "Clapton" 8 | 9 | [[people]] 10 | first_name = "Bob" 11 | last_name = "Seger" 12 | -------------------------------------------------------------------------------- /test/BurntSushi/valid/table-array-nest.json: -------------------------------------------------------------------------------- 1 | { 2 | "albums": [ 3 | { 4 | "name": {"type": "string", "value": "Born to Run"}, 5 | "songs": [ 6 | {"name": {"type": "string", "value": "Jungleland"}}, 7 | {"name": {"type": "string", "value": "Meeting Across the River"}} 8 | ] 9 | }, 10 | { 11 | "name": {"type": "string", "value": "Born in the USA"}, 12 | "songs": [ 13 | {"name": {"type": "string", "value": "Glory Days"}}, 14 | {"name": {"type": "string", "value": "Dancing in the Dark"}} 15 | ] 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /test/BurntSushi/valid/table-array-nest.toml: -------------------------------------------------------------------------------- 1 | [[albums]] 2 | name = "Born to Run" 3 | 4 | [[albums.songs]] 5 | name = "Jungleland" 6 | 7 | [[albums.songs]] 8 | name = "Meeting Across the River" 9 | 10 | [[albums]] 11 | name = "Born in the USA" 12 | 13 | [[albums.songs]] 14 | name = "Glory Days" 15 | 16 | [[albums.songs]] 17 | name = "Dancing in the Dark" 18 | -------------------------------------------------------------------------------- /test/BurntSushi/valid/table-array-one.json: -------------------------------------------------------------------------------- 1 | { 2 | "people": [ 3 | { 4 | "first_name": {"type": "string", "value": "Bruce"}, 5 | "last_name": {"type": "string", "value": "Springsteen"} 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /test/BurntSushi/valid/table-array-one.toml: -------------------------------------------------------------------------------- 1 | [[people]] 2 | first_name = "Bruce" 3 | last_name = "Springsteen" 4 | -------------------------------------------------------------------------------- /test/BurntSushi/valid/table-empty.json: -------------------------------------------------------------------------------- 1 | { 2 | "a": {} 3 | } 4 | -------------------------------------------------------------------------------- /test/BurntSushi/valid/table-empty.toml: -------------------------------------------------------------------------------- 1 | [a] 2 | -------------------------------------------------------------------------------- /test/BurntSushi/valid/table-sub-empty.json: -------------------------------------------------------------------------------- 1 | { 2 | "a": { "b": {} } 3 | } 4 | -------------------------------------------------------------------------------- /test/BurntSushi/valid/table-sub-empty.toml: -------------------------------------------------------------------------------- 1 | [a] 2 | [a.b] 3 | -------------------------------------------------------------------------------- /test/BurntSushi/valid/table-whitespace.json: -------------------------------------------------------------------------------- 1 | { 2 | "valid key": {} 3 | } 4 | -------------------------------------------------------------------------------- /test/BurntSushi/valid/table-whitespace.toml: -------------------------------------------------------------------------------- 1 | ["valid key"] 2 | -------------------------------------------------------------------------------- /test/BurntSushi/valid/table-with-pound.json: -------------------------------------------------------------------------------- 1 | { 2 | "key#group": { 3 | "answer": {"type": "integer", "value": "42"} 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/BurntSushi/valid/table-with-pound.toml: -------------------------------------------------------------------------------- 1 | ["key#group"] 2 | answer = 42 3 | -------------------------------------------------------------------------------- /test/BurntSushi/valid/unicode-escape.json: -------------------------------------------------------------------------------- 1 | { 2 | "answer4": {"type": "string", "value": "\u03B4"}, 3 | "answer8": {"type": "string", "value": "\u03B4"} 4 | } 5 | -------------------------------------------------------------------------------- /test/BurntSushi/valid/unicode-escape.toml: -------------------------------------------------------------------------------- 1 | answer4 = "\u03B4" 2 | answer8 = "\U000003B4" 3 | -------------------------------------------------------------------------------- /test/BurntSushi/valid/unicode-literal.json: -------------------------------------------------------------------------------- 1 | { 2 | "answer": {"type": "string", "value": "δ"} 3 | } 4 | -------------------------------------------------------------------------------- /test/BurntSushi/valid/unicode-literal.toml: -------------------------------------------------------------------------------- 1 | answer = "δ" 2 | -------------------------------------------------------------------------------- /test/Test.hs: -------------------------------------------------------------------------------- 1 | module Main where 2 | 3 | import Prelude hiding (readFile) -- needed for GHCi, see `.ghci` 4 | import Test.Tasty (defaultMain, testGroup) 5 | 6 | import qualified BurntSushi 7 | import Text.Toml.Parser.Spec 8 | 9 | 10 | main :: IO () 11 | main = do 12 | parserSpec <- tomlParserSpec 13 | bsTests <- BurntSushi.tests 14 | 15 | defaultMain $ testGroup "" $ 16 | [ parserSpec 17 | , bsTests 18 | --, quickCheckSuite 19 | -- A QuickCheck suite for a parser is possible. The internet knows. 20 | ] 21 | -------------------------------------------------------------------------------- /test/Text/Toml/Parser/Spec.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | 3 | module Text.Toml.Parser.Spec where 4 | 5 | import Test.Tasty (TestTree) 6 | import Test.Tasty.Hspec 7 | 8 | import Data.HashMap.Strict (fromList) 9 | import Data.Time.Calendar (Day (..)) 10 | import Data.Time.Clock (UTCTime (..)) 11 | import qualified Data.Vector as V 12 | 13 | import Text.Toml.Parser 14 | 15 | 16 | mkVTArray :: [Table] -> Node 17 | mkVTArray = VTArray . V.fromList 18 | 19 | mkVArray :: [Node] -> Node 20 | mkVArray = VArray . V.fromList 21 | 22 | 23 | tomlParserSpec :: IO TestTree 24 | tomlParserSpec = testSpec "Parser Hspec suite" $ do 25 | 26 | describe "Parser.tomlDoc generic" $ do 27 | 28 | it "should parse empty input" $ 29 | testParser tomlDoc "" $ fromList [] 30 | 31 | it "should parse non-empty tomlDocs that do not end with a newline" $ 32 | testParser tomlDoc "number = 123" $ 33 | fromList [("number", VInteger 123)] 34 | 35 | it "should parse when tomlDoc ends in a comment" $ 36 | testParser tomlDoc "q = 42 # understood?" $ 37 | fromList [("q", VInteger 42)] 38 | 39 | it "should not parse re-assignment of key" $ 40 | testParserFails tomlDoc "q=42\nq=42" 41 | 42 | it "should not parse rubbish" $ 43 | testParserFails tomlDoc "{" 44 | 45 | 46 | describe "Parser.tomlDoc (named tables)" $ do 47 | 48 | it "should parse simple named table" $ 49 | testParser tomlDoc "[a]\naa = 108" $ 50 | fromList [("a", VTable (fromList [("aa", VInteger 108)] ))] 51 | 52 | it "should not parse redefined table header (key already exists at scope)" $ 53 | testParserFails tomlDoc "[a]\n[a]" 54 | 55 | it "should parse redefinition of implicit key" $ 56 | testParser tomlDoc "[a.b]\n[a]" $ 57 | fromList [("a", VTable (fromList [("b", VTable emptyTable)] ))] 58 | 59 | it "should parse redefinition of implicit key, with table contents" $ 60 | testParser tomlDoc "[a.b]\nb=3\n[a]\na=4" $ 61 | fromList [("a", VTable (fromList [("b", VTable (fromList [("b", VInteger 3)])), 62 | ("a", VInteger 4)]))] 63 | 64 | it "should parse redefinition by implicit table header" $ 65 | testParser tomlDoc "[a]\n[a.b]" $ 66 | fromList [("a", VTable (fromList [("b", VTable emptyTable)] ))] 67 | 68 | it "should parse inline tables the same as normal tables" $ 69 | testParser tomlDoc "[a]\nb={}" $ 70 | fromList [("a", VTable (fromList [("b", VTable emptyTable)] ))] 71 | 72 | it "should not parse redefinition key" $ 73 | testParserFails tomlDoc "[a]\nb=1\n[a.b]" 74 | 75 | 76 | describe "Parser.tomlDoc (tables arrays)" $ do 77 | 78 | it "should parse a simple empty table array" $ 79 | testParser tomlDoc "[[a]]\n[[a]]" $ 80 | fromList [("a", mkVTArray [ fromList [] 81 | , fromList [] ] )] 82 | 83 | it "should parse a simple table array with content" $ 84 | testParser tomlDoc "[[a]]\na1=1\n[[a]]\na2=2" $ 85 | fromList [("a", mkVTArray [ fromList [("a1", VInteger 1)] 86 | , fromList [("a2", VInteger 2)] ] )] 87 | 88 | it "should not allow a simple table array to be inserted into a non table array" $ 89 | testParserFails tomlDoc "a = [1,2,3]\n[[a]]" 90 | 91 | it "should parse a simple empty nested table array" $ 92 | testParser tomlDoc "[[a.b]]\n[[a.b]]" $ 93 | fromList [("a", VTable (fromList [("b", mkVTArray [ emptyTable 94 | , emptyTable ] )] ) ) ] 95 | 96 | it "should parse a simple non empty table array" $ 97 | testParser tomlDoc "[[a.b]]\na1=1\n[[a.b]]\na2=2" $ 98 | fromList [("a", VTable (fromList [("b", mkVTArray [ fromList [("a1", VInteger 1)] 99 | , fromList [("a2", VInteger 2)] 100 | ] )] ) )] 101 | 102 | it "should parse redefined implicit table header" $ 103 | testParserFails tomlDoc "[[a.b]]\n[[a]]" 104 | 105 | it "should parse redefinition by implicit table header" $ 106 | testParser tomlDoc "[[a]]\n[[a.b]]" $ 107 | fromList [("a", mkVTArray [ fromList [("b", mkVTArray [ fromList [] ])] ] )] 108 | 109 | 110 | describe "Parser.tomlDoc (mixed named tables and tables arrays)" $ do 111 | 112 | it "should not parse redefinition of key by table header (table array by table)" $ 113 | testParserFails tomlDoc "[[a]]\n[a]" 114 | 115 | it "should not parse redefinition of key by table header (table by table array)" $ 116 | testParserFails tomlDoc "[a]\n[[a]]" 117 | 118 | it "should not parse redefinition implicit table header (table by array)" $ 119 | testParserFails tomlDoc "[a.b]\n[[a]]" 120 | 121 | it "should parse redefined implicit table header (array by table)" $ 122 | testParser tomlDoc "[[a.b]]\n[a]" $ 123 | fromList [("a", VTable (fromList [("b", mkVTArray [ fromList [] ])] ) )] 124 | 125 | it "should not parse redefined implicit table header (array by table), when keys collide" $ 126 | testParserFails tomlDoc "[[a.b]]\n[a]\nb=1" 127 | 128 | it "should insert sub-key of regular table in most recently defined table array" $ 129 | testParser tomlDoc "[[a]]\ni=0\n[[a]]\ni=1\n[a.b]" $ 130 | fromList [("a", mkVTArray [ fromList [ ("i", VInteger 0) ] 131 | , fromList [ ("b", VTable $ fromList [] ) 132 | , ("i", VInteger 1) ] 133 | ] )] 134 | 135 | it "should insert sub-key of table array" $ 136 | testParser tomlDoc "[a]\n[[a.b]]" $ 137 | fromList [("a", VTable (fromList [("b", mkVTArray [fromList []])] ) )] 138 | 139 | it "should insert sub-key (with content) of table array" $ 140 | testParser tomlDoc "[a]\nq=42\n[[a.b]]\ni=0" $ 141 | fromList [("a", VTable (fromList [ ("q", VInteger 42), 142 | ("b", mkVTArray [ 143 | fromList [("i", VInteger 0)] 144 | ]) ]) )] 145 | 146 | describe "Parser.headerValue" $ do 147 | 148 | it "should parse simple table header" $ 149 | testParser headerValue "table" ["table"] 150 | 151 | it "should parse simple nested table header" $ 152 | testParser headerValue "main.sub" ["main", "sub"] 153 | 154 | it "should not parse just a dot (separator)" $ 155 | testParserFails headerValue "." 156 | 157 | it "should not parse an empty most right name" $ 158 | testParserFails headerValue "first." 159 | 160 | it "should not parse an empty most left name" $ 161 | testParserFails headerValue ".second" 162 | 163 | it "should not parse an empty middle name" $ 164 | testParserFails headerValue "first..second" 165 | 166 | 167 | describe "Parser.tableHeader" $ do 168 | 169 | it "should not parse an empty table header" $ 170 | testParserFails tableHeader "[]" 171 | 172 | it "should parse simple table header" $ 173 | testParser tableHeader "[item]" ["item"] 174 | 175 | it "should parse simple nested table header" $ 176 | testParser tableHeader "[main.sub]" ["main", "sub"] 177 | 178 | 179 | describe "Parser.tableArrayHeader" $ do 180 | 181 | it "should not parse an empty table header" $ 182 | testParserFails tableArrayHeader "[[]]" 183 | 184 | it "should parse simple table array header" $ 185 | testParser tableArrayHeader "[[item]]" ["item"] 186 | 187 | it "should parse simple nested table array header" $ 188 | testParser tableArrayHeader "[[main.sub]]" ["main", "sub"] 189 | 190 | 191 | describe "Parser.assignment" $ do 192 | 193 | it "should parse simple example" $ 194 | testParser assignment "country = \"\"" ("country", VString "") 195 | 196 | it "should parse without spacing around the assignment operator" $ 197 | testParser assignment "a=108" ("a", VInteger 108) 198 | 199 | it "should parse when value on next line" $ 200 | testParser assignment "a =\n108" ("a", VInteger 108) 201 | 202 | 203 | describe "Parser.boolean" $ do 204 | 205 | it "should parse true" $ 206 | testParser boolean "true" $ VBoolean True 207 | 208 | it "should parse false" $ 209 | testParser boolean "false" $ VBoolean False 210 | 211 | it "should not parse capitalized variant" $ 212 | testParserFails boolean "False" 213 | 214 | 215 | describe "Parser.basicStr" $ do 216 | 217 | it "should parse the common escape sequences in basic strings" $ 218 | testParser basicStr "\"123\\b\\t\\n\\f\\r\\\"\\/\\\\\"" $ "123\b\t\n\f\r\"/\\" 219 | 220 | it "should parse the simple unicode value from the example" $ 221 | testParser basicStr "\"中国\"" $ "中国" 222 | 223 | it "should parse escaped 4 digit unicode values" $ 224 | testParser assignment "special_k = \"\\u0416\"" ("special_k", VString "Ж") 225 | 226 | it "should parse escaped 8 digit unicode values" $ 227 | testParser assignment "g_clef = \"\\U0001D11e\"" ("g_clef", VString "𝄞") 228 | 229 | it "should not parse escaped unicode values with missing digits" $ 230 | testParserFails assignment "g_clef = \"\\U1D11e\"" 231 | 232 | 233 | describe "Parser.multiBasicStr" $ do 234 | 235 | it "should parse simple example" $ 236 | testParser multiBasicStr "\"\"\"thorrough\"\"\"" $ "thorrough" 237 | 238 | it "should parse text containing a quote" $ 239 | testParser multiBasicStr "\"\"\"is \"it\" complete\"\"\"" $ "is \"it\" complete" 240 | 241 | it "should parse with newlines" $ 242 | testParser multiBasicStr "\"\"\"One\nTwo\"\"\"" $ "One\nTwo" 243 | 244 | it "should parse with escaped newlines" $ 245 | testParser multiBasicStr "\"\"\"One\\\nTwo\"\"\"" $ "OneTwo" 246 | 247 | it "should parse newlines, ignoring 1 leading newline" $ 248 | testParser multiBasicStr "\"\"\"\nOne\\\nTwo\"\"\"" $ "OneTwo" 249 | 250 | it "should parse with espaced whitespace" $ 251 | testParser multiBasicStr "\"\"\"\\\n\ 252 | \Quick \\\n\ 253 | \\\\n\ 254 | \Jumped \\\n\ 255 | \Lazy\\\n\ 256 | \ \"\"\"" $ "Quick Jumped Lazy" 257 | 258 | it "should parse espaced on first" $ 259 | testParser multiBasicStr "\"\"\"\\\nQuick \\\n\\\nJumped \\\nLazy\\\n\"\"\"" $ "Quick Jumped Lazy" 260 | 261 | 262 | describe "Parser.literalStr" $ do 263 | 264 | it "should parse literally" $ 265 | testParser literalStr "'\"Your\" folder: \\\\User\\new\\tmp\\'" $ 266 | "\"Your\" folder: \\\\User\\new\\tmp\\" 267 | 268 | it "has no notion of 'escaped single quotes'" $ 269 | testParserFails tomlDoc "q = 'I don\\'t know.'" -- string terminates before the "t" 270 | 271 | 272 | describe "Parser.multiLiteralStr" $ do 273 | 274 | it "should parse literally" $ 275 | testParser multiLiteralStr 276 | "'''\nFirst newline is dropped.\n Other whitespace,\n is preserved -- isn't it?'''" 277 | $ "First newline is dropped.\n Other whitespace,\n is preserved -- isn't it?" 278 | 279 | 280 | describe "Parser.datetime" $ do 281 | 282 | it "should parse a JSON formatted datetime string in zulu timezone" $ 283 | testParser datetime "1979-05-27T07:32:00Z" $ 284 | VDatetime $ UTCTime (ModifiedJulianDay 44020) 27120 285 | 286 | it "should not parse only dates" $ 287 | testParserFails datetime "1979-05-27" 288 | 289 | it "should not parse without the Z" $ 290 | testParserFails datetime "1979-05-27T07:32:00" 291 | 292 | 293 | describe "Parser.float" $ do 294 | 295 | it "should parse positive floats" $ 296 | testParser float "3.14" $ VFloat 3.14 297 | 298 | it "should parse positive floats with plus sign" $ 299 | testParser float "+3.14" $ VFloat 3.14 300 | 301 | it "should parse negative floats" $ 302 | testParser float "-0.1" $ VFloat (-0.1) 303 | 304 | it "should parse more or less zero float" $ 305 | testParser float "0.0" $ VFloat 0.0 306 | 307 | it "should parse 'scientific notation' ('e'-notation)" $ 308 | testParser float "1.5e6" $ VFloat 1500000.0 309 | 310 | it "should parse 'scientific notation' ('e'-notation) with upper case E" $ 311 | testParser float "1E0" $ VFloat 1.0 312 | 313 | it "should not accept floats starting with a dot" $ 314 | testParserFails float ".5" 315 | 316 | it "should not accept floats without any decimals" $ 317 | testParserFails float "5." 318 | 319 | it "should accept floats which contain underscores" $ 320 | testParser float "5_3.4_5" $ VFloat 53.45 321 | 322 | it "should not accept floats which end with underscores" $ 323 | testParserFails float "5_3.4_5_" 324 | 325 | it "should not accept floats which start with underscores" $ 326 | testParserFails float "_5_3.4_5" 327 | 328 | it "should not accept floats which have two consecutive underscores" $ 329 | testParserFails float "5__3.4__5" 330 | 331 | describe "Parser.integer" $ do 332 | 333 | it "should parse positive integers" $ 334 | testParser integer "108" $ VInteger 108 335 | 336 | it "should parse negative integers" $ 337 | testParser integer "-1" $ VInteger (-1) 338 | 339 | it "should parse zero" $ 340 | testParser integer "0" $ VInteger 0 341 | 342 | it "should parse integers prefixed with a plus" $ 343 | testParser integer "+42" $ VInteger 42 344 | 345 | it "should accept integers which contain underscores" $ 346 | testParser integer "4_2" $ VInteger 42 347 | 348 | it "should not accept integers which end with underscores" $ 349 | testParserFails integer "4_2_" 350 | 351 | it "should not accept integers which start with underscores" $ 352 | testParserFails integer "_4_2" 353 | 354 | it "should not accept integers which have two consecutive underscores" $ 355 | testParserFails integer "4__2" 356 | 357 | describe "Parser.tomlDoc arrays" $ do 358 | 359 | it "should parse an empty array" $ 360 | testParser array "[]" $ mkVArray [] 361 | 362 | it "should parse an empty array with whitespace" $ 363 | testParser array "[ ]" $ mkVArray [] 364 | 365 | it "should not parse an empty array with only a terminating comma" $ 366 | testParserFails array "[,]" 367 | 368 | it "should parse an empty array of empty arrays" $ 369 | testParser array "[[],[]]" $ mkVArray [ mkVArray [], mkVArray [] ] 370 | 371 | it "should parse an empty array of empty arrays with whitespace" $ 372 | testParser array "[ \n[ ]\n ,\n [ \n ] ,\n ]" $ mkVArray [ mkVArray [], mkVArray [] ] 373 | 374 | it "should parse nested arrays" $ 375 | testParser assignment "d = [ ['gamma', 'delta'], [1, 2] ]" 376 | $ ("d", mkVArray [ mkVArray [ VString "gamma" 377 | , VString "delta" ] 378 | , mkVArray [ VInteger 1 379 | , VInteger 2 ] ]) 380 | 381 | it "should allow linebreaks in an array" $ 382 | testParser assignment "hosts = [\n'alpha',\n'omega'\n]" 383 | $ ("hosts", mkVArray [VString "alpha", VString "omega"]) 384 | 385 | it "should allow some linebreaks in an array" $ 386 | testParser assignment "hosts = ['alpha' ,\n'omega']" 387 | $ ("hosts", mkVArray [VString "alpha", VString "omega"]) 388 | 389 | it "should allow linebreaks in an array, with comments" $ 390 | testParser assignment "hosts = [\n\ 391 | \'alpha', # the first\n\ 392 | \'omega' # the last\n\ 393 | \]" 394 | $ ("hosts", mkVArray [VString "alpha", VString "omega"]) 395 | 396 | it "should allow linebreaks in an array, with comments, and terminating comma" $ 397 | testParser assignment "hosts = [\n\ 398 | \'alpha', # the first\n\ 399 | \'omega', # the last\n\ 400 | \]" 401 | $ ("hosts", mkVArray [VString "alpha", VString "omega"]) 402 | 403 | it "inside an array, all element should be of the same type" $ 404 | testParserFails array "[1, 2.0]" 405 | 406 | it "inside an array of arrays, this inner arrays may contain values of different types" $ 407 | testParser array "[[1], [2.0], ['a']]" $ 408 | mkVArray [ mkVArray [VInteger 1], mkVArray [VFloat 2.0], mkVArray [VString "a"] ] 409 | 410 | it "all string variants are of the same type of the same type" $ 411 | testParser assignment "data = [\"a\", \"\"\"b\"\"\", 'c', '''d''']" $ 412 | ("data", mkVArray [ VString "a", VString "b", 413 | VString "c", VString "d" ]) 414 | 415 | it "should parse terminating commas in arrays" $ 416 | testParser array "[1, 2, ]" $ mkVArray [ VInteger 1, VInteger 2 ] 417 | 418 | it "should parse terminating commas in arrays(2)" $ 419 | testParser array "[1,2,]" $ mkVArray [ VInteger 1, VInteger 2 ] 420 | 421 | describe "Parser.tomlDoc inline tables" $ do 422 | 423 | it "should parse an empty inline table" $ 424 | testParser inlineTable "{}" $ VTable (fromList []) 425 | 426 | it "should parse simple inline tables" $ 427 | testParser inlineTable "{ a = 8 , b = \"things\" }" $ 428 | VTable (fromList [ ("a" , VInteger 8) , ("b", VString "things") ]) 429 | 430 | it "should not parse simple inline tables with newline " $ 431 | testParserFails inlineTable "{ a = 8 , \n b = \"things\" }" 432 | 433 | where 434 | testParser p str success = case parseOnly p str of Left _ -> False 435 | Right x -> x == success 436 | testParserFails p str = case parseOnly p str of Left _ -> True 437 | Right _ -> False 438 | --------------------------------------------------------------------------------