packages feed

toml-parser 1.0.1.0 → 1.1.0.0

raw patch · 21 files changed

+1391/−1030 lines, 21 filesPVP ok

version bump matches the API change (PVP)

API changes (from Hackage documentation)

- Toml.Lexer.Utils: alexGetByte :: AlexInput -> Maybe (Int, AlexInput)
- Toml.Lexer.Utils: type AlexInput = Located String
+ Toml.FromValue: instance GHC.Base.Alternative Toml.FromValue.ParseTable
+ Toml.FromValue: instance GHC.Base.MonadPlus Toml.FromValue.ParseTable
+ Toml.FromValue.Generic: class GParseTable f
+ Toml.FromValue.Generic: gParseTable :: GParseTable f => (f a -> ParseTable b) -> ParseTable b
+ Toml.FromValue.Generic: genericFromTable :: (Generic a, GParseTable (Rep a)) => Table -> Matcher a
+ Toml.FromValue.Generic: instance (GHC.Generics.Selector s, Toml.FromValue.FromValue a) => Toml.FromValue.Generic.GParseTable (GHC.Generics.S1 s (GHC.Generics.K1 i (GHC.Maybe.Maybe a)))
+ Toml.FromValue.Generic: instance (GHC.Generics.Selector s, Toml.FromValue.FromValue a) => Toml.FromValue.Generic.GParseTable (GHC.Generics.S1 s (GHC.Generics.K1 i a))
+ Toml.FromValue.Generic: instance (Toml.FromValue.Generic.GParseTable f, Toml.FromValue.Generic.GParseTable g) => Toml.FromValue.Generic.GParseTable (f GHC.Generics.:*: g)
+ Toml.FromValue.Generic: instance Toml.FromValue.Generic.GParseTable GHC.Generics.U1
+ Toml.FromValue.Generic: instance Toml.FromValue.Generic.GParseTable f => Toml.FromValue.Generic.GParseTable (GHC.Generics.C1 c f)
+ Toml.FromValue.Generic: instance Toml.FromValue.Generic.GParseTable f => Toml.FromValue.Generic.GParseTable (GHC.Generics.D1 c f)
+ Toml.FromValue.Matcher: instance GHC.Base.Alternative Toml.FromValue.Matcher.Matcher
+ Toml.FromValue.Matcher: instance GHC.Base.MonadPlus Toml.FromValue.Matcher.Matcher
+ Toml.FromValue.Matcher: instance GHC.Base.Monoid Toml.FromValue.Matcher.Strings
+ Toml.FromValue.Matcher: instance GHC.Base.Semigroup Toml.FromValue.Matcher.Strings
+ Toml.Lexer.Utils: locatedUncons :: Located String -> Maybe (Int, Located String)
+ Toml.ToValue.Generic: class GToTable f
+ Toml.ToValue.Generic: gToTable :: GToTable f => f a -> Table
+ Toml.ToValue.Generic: genericToTable :: (Generic a, GToTable (Rep a)) => a -> Table
+ Toml.ToValue.Generic: instance (GHC.Generics.Selector s, Toml.ToValue.ToValue a) => Toml.ToValue.Generic.GToTable (GHC.Generics.S1 s (GHC.Generics.K1 i (GHC.Maybe.Maybe a)))
+ Toml.ToValue.Generic: instance (GHC.Generics.Selector s, Toml.ToValue.ToValue a) => Toml.ToValue.Generic.GToTable (GHC.Generics.S1 s (GHC.Generics.K1 i a))
+ Toml.ToValue.Generic: instance (Toml.ToValue.Generic.GToTable f, Toml.ToValue.Generic.GToTable g) => Toml.ToValue.Generic.GToTable (f GHC.Generics.:*: g)
+ Toml.ToValue.Generic: instance Toml.ToValue.Generic.GToTable GHC.Generics.U1
+ Toml.ToValue.Generic: instance Toml.ToValue.Generic.GToTable GHC.Generics.V1
+ Toml.ToValue.Generic: instance Toml.ToValue.Generic.GToTable f => Toml.ToValue.Generic.GToTable (GHC.Generics.C1 c f)
+ Toml.ToValue.Generic: instance Toml.ToValue.Generic.GToTable f => Toml.ToValue.Generic.GToTable (GHC.Generics.D1 c f)
+ Toml.Value: instance Data.Data.Data Toml.Value.Value
+ Toml.Value: instance GHC.Generics.Generic Toml.Value.Value
- Toml: Failure :: String -> Result a
+ Toml: Failure :: [String] -> Result a
- Toml.FromValue: Failure :: String -> Result a
+ Toml.FromValue: Failure :: [String] -> Result a
- Toml.FromValue.Matcher: Failure :: String -> Result a
+ Toml.FromValue.Matcher: Failure :: [String] -> Result a

Files

ChangeLog.md view
@@ -1,6 +1,12 @@ # Revision history for toml-parser -## 1.0.1.0  --+## 1.1.0.0  --  2023-07-03++* Add Toml.FromValue.Generic and Toml.ToValue.Generic+* Add Alternative instance to Matcher and support multiple error messages in Result+* Add Data and Generic instances for Value++## 1.0.1.0  -- 2023-07-01  * Add ToTable and ToValue instances for Map * Refine error messages
README.md view
@@ -127,3 +127,22 @@     Fruit "apple" (Just (Physical "red" "round")) [Variety "red delicious", Variety "granny smith"],     Fruit "banana" Nothing [Variety "plantain"]]) ```++## Generics++Code for generating and matching tables to records can be derived+using GHC.Generics. This will generate tables using the field names+as table keys.++```haskell+data ExampleRecord = ExampleRecord {+  exString :: String,+  exList   :: [Int],+  exOpt    :: Maybe Bool}+  deriving (Show, Generic, Eq)++instance FromTable ExampleRecord where fromTable = genericFromTable+instance FromValue ExampleRecord where fromValue = defaultTableFromValue+instance ToTable   ExampleRecord where toTable   = genericToTable+instance ToValue   ExampleRecord where toValue   = defaultTableToValue+```
src/Toml.hs view
@@ -1,11 +1,13 @@ {-| Module      : Toml-Description : TOML parser+Description : TOML parsing, printing, and codecs Copyright   : (c) Eric Mertens, 2023 License     : ISC Maintainer  : emertens@gmail.com -This module parses TOML into semantically meaningful values.+This is the high-level interface to the toml-parser library.+It enables parsing, printing, and coversion into and out of+application-specific representations.  This parser implements TOML 1.0.0 <https://toml.io/en/v1.0.0> as carefully as possible.@@ -13,14 +15,14 @@ -} module Toml ( -    -- * types+    -- * Types     Table,     Value(..), -    -- * parsing+    -- * Parsing     parse, -    -- * printing+    -- * Printing     prettyToml,     DocClass(..), @@ -53,7 +55,7 @@  -- | Use the 'FromTable' instance to decode a value from a TOML string. decode :: FromTable a => String -> Result a-decode = either Failure (runMatcher . fromTable) . parse+decode = either (Failure . pure) (runMatcher . fromTable) . parse  -- | Use the 'ToTable' instance to encode a value to a TOML string. encode :: ToTable a => a -> TomlDoc
src/Toml/FromValue.hs view
@@ -20,37 +20,39 @@ problematic decodings or keys that might be unused now but were perhaps meaningful in an old version of a configuration file. +"Toml.FromValue.Generic" can be used to derive instances of 'FromTable'+automatically for record types.+ -} module Toml.FromValue (-    -- * deserialization classes+    -- * Deserialization classes     FromValue(..),     FromTable(..),     defaultTableFromValue, -    -- * matcher+    -- * Matcher     Matcher,+    Result(..),     runMatcher,     withScope,     warning, -    -- * results-    Result(..),--    -- * table matching+    -- * Table matching     ParseTable,     runParseTable,     optKey,     reqKey,     warnTable, -    -- * table matching primitives+    -- * Table matching primitives     getTable,     setTable,     ) where -import Control.Monad (zipWithM)+import Control.Applicative (Alternative)+import Control.Monad (MonadPlus, zipWithM) import Control.Monad.Trans.Class (lift)-import Control.Monad.Trans.State.Strict (StateT(..), evalStateT, put, get)+import Control.Monad.Trans.State.Strict (StateT(..), put, get) import Data.Int (Int8, Int16, Int32, Int64) import Data.List (intercalate) import Data.Map (Map)@@ -63,7 +65,6 @@ import Toml.Pretty (prettySimpleKey, prettyValue) import Toml.Value (Value(..), Table) - -- | Class for types that can be decoded from a TOML value. class FromValue a where     -- | Convert a 'Value' or report an error message@@ -96,10 +97,12 @@ typeError :: String {- ^ expected type -} -> Value {- ^ actual value -} -> Matcher a typeError wanted got = fail ("Type error. wanted: " ++ wanted ++ " got: " ++ show (prettyValue got)) +-- | Matches integer values instance FromValue Integer where     fromValue (Integer x) = pure x     fromValue v = typeError "integer" v +-- | Matches non-negative integer values instance FromValue Natural where     fromValue v =      do i <- fromValue v@@ -127,6 +130,8 @@ instance FromValue Word32 where fromValue = fromValueSized "Word32" instance FromValue Word64 where fromValue = fromValueSized "Word64" +-- | Matches single-character strings with 'fromValue' and arbitrary+-- strings with 'listFromValue' to support 'Prelude.String' instance FromValue Char where     fromValue (String [c]) = pure c     fromValue v = typeError "character" v@@ -134,39 +139,48 @@     listFromValue (String xs) = pure xs     listFromValue v = typeError "string" v +-- | Matches floating-point and integer values instance FromValue Double where     fromValue (Float x) = pure x     fromValue (Integer x) = pure (fromInteger x)     fromValue v = typeError "float" v +-- | Matches floating-point and integer values instance FromValue Float where     fromValue (Float x) = pure (realToFrac x)     fromValue (Integer x) = pure (fromInteger x)     fromValue v = typeError "float" v +-- | Matches @true@ and @false@ instance FromValue Bool where     fromValue (Bool x) = pure x     fromValue v = typeError "boolean" v +-- | Implemented in terms of 'listFromValue' instance FromValue a => FromValue [a] where     fromValue = listFromValue +-- | Matches local date literals instance FromValue Day where     fromValue (Day x) = pure x     fromValue v = typeError "local date" v +-- | Matches local time literals instance FromValue TimeOfDay where     fromValue (TimeOfDay x) = pure x     fromValue v = typeError "local time" v +-- | Matches offset date-time literals instance FromValue ZonedTime where     fromValue (ZonedTime x) = pure x     fromValue v = typeError "offset date-time" v +-- | Matches local date-time literals instance FromValue LocalTime where     fromValue (LocalTime x) = pure x     fromValue v = typeError "local date-time" v +-- | Matches all values, used for pass-through instance FromValue Value where     fromValue = pure @@ -175,7 +189,7 @@ -- -- Use 'optKey', 'reqKey', 'rej newtype ParseTable a = ParseTable (StateT Table Matcher a)-    deriving (Functor, Applicative, Monad)+    deriving (Functor, Applicative, Monad, Alternative, MonadPlus)  instance MonadFail ParseTable where     fail = ParseTable . fail
+ src/Toml/FromValue/Generic.hs view
@@ -0,0 +1,69 @@+{-|+Module      : Toml.FromValue.Generic+Description : GHC.Generics derived table parsing+Copyright   : (c) Eric Mertens, 2023+License     : ISC+Maintainer  : emertens@gmail.com++Use 'genericFromTable' to derive an instance of 'Toml.FromValue.FromTable'+using the field names of a record.++-}+module Toml.FromValue.Generic (+    GParseTable(..),+    genericFromTable,+    ) where++import GHC.Generics+import Toml.FromValue (FromValue(..), ParseTable, optKey, reqKey, runParseTable)+import Toml.FromValue.Matcher (Matcher)+import Toml.Value (Table)++-- | Match a 'Table' using the field names in a record.+--+-- @since 1.0.2.0+genericFromTable :: (Generic a, GParseTable (Rep a)) => Table -> Matcher a+genericFromTable = runParseTable (gParseTable (pure . to))+{-# INLINE genericFromTable #-}++-- gParseTable is written in continuation passing style because+-- it allows all the GHC.Generics constructors to inline into+-- a single location which allows the optimizer to optimize them+-- complete away.++-- | Supports conversion of product types with field selector names to+-- TOML values.+--+-- @since 1.0.2.0+class GParseTable f where+    -- | Convert a value and apply the continuation to the result.+    gParseTable :: (f a -> ParseTable b) -> ParseTable b++-- | Ignores type constructor name+instance GParseTable f => GParseTable (D1 c f) where+    gParseTable f = gParseTable (f . M1)+    {-# INLINE gParseTable #-}++-- | Ignores value constructor name+instance GParseTable f => GParseTable (C1 c f) where+    gParseTable f = gParseTable (f . M1)+    {-# INLINE gParseTable #-}++instance (GParseTable f, GParseTable g) => GParseTable (f :*: g) where+    gParseTable f = gParseTable \x -> gParseTable \y -> f (x :*: y)+    {-# INLINE gParseTable #-}++-- | Omits the key from the table on nothing, includes it on just+instance {-# OVERLAPS #-} (Selector s, FromValue a) => GParseTable (S1 s (K1 i (Maybe a))) where+    gParseTable f = f . M1 . K1 =<< optKey (selName (M1 [] :: S1 s [] ()))+    {-# INLINE gParseTable #-}++-- | Uses record selector name as table key+instance (Selector s, FromValue a) => GParseTable (S1 s (K1 i a)) where+    gParseTable f = f . M1 . K1 =<< reqKey (selName (M1 [] :: S1 s [] ()))+    {-# INLINE gParseTable #-}++-- | Emits empty table+instance GParseTable U1 where+    gParseTable f = f U1+    {-# INLINE gParseTable #-}
src/Toml/FromValue/Matcher.hs view
@@ -5,43 +5,67 @@ License     : ISC Maintainer  : emertens@gmail.com +This type helps to build up computations that can validate a TOML+value and compute some application-specific representation.++It supports warning messages which can be used to deprecate old+configuration options and to detect unused table keys.++It supports tracking multiple error messages when you have more+than one decoding option and all of them have failed.+ -} module Toml.FromValue.Matcher (      Matcher,+    Result(..),     runMatcher,     withScope,     getScope,     warning,--    Result(..),     ) where  import Control.Monad.Trans.Class (lift) import Control.Monad.Trans.Reader (asks, local, ReaderT(..))+import Control.Monad.Trans.Except (Except, runExcept, throwE) import Control.Monad.Trans.Writer.CPS (runWriterT, tell, WriterT)+import Data.Monoid (Endo(..))+import Control.Applicative (Alternative(..))+import Control.Monad (MonadPlus)  -- | Computations that result in a 'Result' and which track a list -- of nested contexts to assist in generating warnings and error -- messages. -- -- Use 'withScope' to run a 'Matcher' in a new, nested scope.-newtype Matcher a = Matcher (ReaderT [String] (WriterT (DList String) (Either String)) a)-    deriving (Functor, Applicative, Monad)+newtype Matcher a = Matcher (ReaderT [String] (WriterT Strings (Except Strings)) a)+    deriving (Functor, Applicative, Monad, Alternative, MonadPlus) -type DList a = [a] -> [a]+-- | List of strings that supports efficient left- and right-biased append+newtype Strings = Strings (Endo [String])+    deriving (Semigroup, Monoid) --- | Computation outcome with error and warning messages.+-- | Create a singleton list of strings+string :: String -> Strings+string x = Strings (Endo (x:))++-- | Extract the list of strings+runStrings :: Strings -> [String]+runStrings (Strings s) = s `appEndo` []++-- | Computation outcome with error and warning messages. Multiple error+-- messages can occur when multiple alternatives all fail. Resolving any+-- one of the error messages could allow the computation to succeed. data Result a-    = Failure String -- error message+    = Failure [String]   -- error messages     | Success [String] a -- warnings and result     deriving (Read, Show, Eq, Ord)  -- | Run a 'Matcher' with an empty scope. runMatcher :: Matcher a -> Result a runMatcher (Matcher m) =-    case runWriterT (runReaderT m []) of-        Left e -> Failure e-        Right (x,w) -> Success (w []) x+    case runExcept (runWriterT (runReaderT m [])) of+        Left e      -> Failure (runStrings e)+        Right (x,w) -> Success (runStrings w) x  -- | Run a 'Matcher' with a locally extended scope. withScope :: String -> Matcher a -> Matcher a@@ -55,10 +79,10 @@ warning :: String -> Matcher () warning w =  do loc <- getScope-    Matcher (lift (tell ((w ++ " in top" ++ concat loc):)))+    Matcher (lift (tell (string (w ++ " in top" ++ concat loc))))  -- | Fail with an error message annotated to the current location. instance MonadFail Matcher where     fail e =      do loc <- getScope-        Matcher (lift (lift (Left (e ++ " in top" ++ concat loc))))+        Matcher (lift (lift (throwE (string (e ++ " in top" ++ concat loc)))))
src/Toml/Lexer.x view
@@ -166,6 +166,11 @@  { +type AlexInput = Located String++alexGetByte :: AlexInput -> Maybe (Int, AlexInput)+alexGetByte = locatedUncons+ -- | Generate a lazy-list of tokens from the input string. -- The token stream is guaranteed to be terminated either with -- 'TokEOF' or 'TokError'.
src/Toml/Lexer/Token.hs view
@@ -5,33 +5,34 @@ License     : ISC Maintainer  : emertens@gmail.com -This module provides the datatype for the lexical-syntax of TOML files. These tokens will drive the-parser in the "Parser" module.+This module provides the datatype for the lexical syntax of TOML files.+These tokens are generated by "Toml.Lexer" and consumed in "Toml.Parser".  -} module Toml.Lexer.Token (+    -- * Types     Token(..), +    -- * String literals     mkLiteralString,     mkMlLiteralString, -    -- * integer literals+    -- * Integer literals     mkBinInteger,     mkDecInteger,     mkOctInteger,     mkHexInteger, -    -- * float literals+    -- * Float literals     mkFloat, -    -- * date and time patterns+    -- * Date and time patterns     localDatePatterns,     localTimePatterns,     localDateTimePatterns,     offsetDateTimePatterns, -    -- * errors+    -- * Errors     mkError,     ) where 
src/Toml/Lexer/Utils.hs view
@@ -9,6 +9,9 @@ lexer. This lexer drive provides nested states, unicode support, and file location tracking. +The various states of this module are needed to deal with the varying+lexing rules while lexing values, keys, and string-literals.+ -} module Toml.Lexer.Utils ( @@ -16,6 +19,9 @@     Action,     Context(..), +    -- * Input processing+    locatedUncons,+     -- * Actions     value,     value_,@@ -29,19 +35,14 @@      equals,     timeValue,+    eofToken, +    -- * String literals     strFrag,     startMlStr,     startStr,     endStr,     unicodeEscape,--    eofToken,--    -- * Alex extension points-    AlexInput,-    alexGetByte,-     ) where  import Control.Monad.Trans.State.Strict (State, state)@@ -54,38 +55,45 @@ import Toml.Position (move, Position) import Toml.Lexer.Token (Token(..)) +-- | Type of actions associated with lexer patterns type Action = Located String -> State [Context] [Located Token] +-- | Representation of the current lexer state. data Context   = ListContext Position -- ^ processing an inline list, lex values   | TableContext Position -- ^ processing an inline table, don't lex values   | ValueContext -- ^ processing after an equals, lex one value-  | MlStrContext Position [String]-  | StrContext   Position [String]+  | MlStrContext Position [String] -- ^ position of opening delimiter and list of fragments+  | StrContext   Position [String] -- ^ position of opening delimiter and list of fragments   deriving Show +-- | Add a literal fragment of a string to the current string state. strFrag :: Action strFrag s = state \case   StrContext   p acc : st -> ([], StrContext   p (locThing s : acc) : st)   MlStrContext p acc : st -> ([], MlStrContext p (locThing s : acc) : st)   _                       -> error "strFrag: panic" +-- | End the current string state and emit the string literal token. endStr :: Action endStr x = state \case     StrContext   p acc : st -> ([Located p (TokString   (concat (reverse (locThing x : acc))))], st)     MlStrContext p acc : st -> ([Located p (TokMlString (concat (reverse (locThing x : acc))))], st)     _                       -> error "endStr: panic" +-- | Start a basic string literal startStr :: Action startStr t = state \case   ValueContext : st -> ([], StrContext (locPosition t) [] : st)   st                -> ([], StrContext (locPosition t) [] : st) +-- | Start a multi-line basic string literal startMlStr :: Action startMlStr t = state \case   ValueContext : st -> ([], MlStrContext (locPosition t) [] : st)   st                -> ([], MlStrContext (locPosition t) [] : st) +-- | Resolve a unicode escape sequence and add it to the current string literal unicodeEscape :: Action unicodeEscape (Located p lexeme) =   case readHex (drop 2 lexeme) of@@ -94,60 +102,74 @@       | otherwise                     -> strFrag (Located p [chr n])     _                                 -> error "unicodeEscape: panic" +-- | Record an @=@ token and update the state equals :: Action equals t = state \case   st -> ([TokEquals <$ t], ValueContext : st) +-- | Record an opening square bracket and update the state squareO :: Action squareO t = state \case   ValueContext  : st -> ([TokSquareO <$ t], ListContext (locPosition t) : st)   ListContext p : st -> ([TokSquareO <$ t], ListContext (locPosition t): ListContext p : st)   st                 -> ([TokSquareO <$ t], st) +-- | Record a closing square bracket and update the state squareC :: Action squareC t = state \case   ListContext _ : st -> ([TokSquareC <$ t], st)   st                 -> ([TokSquareC <$ t], st) +-- | Record an opening curly bracket and update the state curlyO :: Action curlyO t = state \case   ValueContext  : st -> ([TokCurlyO <$ t], TableContext (locPosition t) : st)   ListContext p : st -> ([TokCurlyO <$ t], TableContext (locPosition t) : ListContext p : st)   st                 -> ([TokCurlyO <$ t], st) +-- | Record a closing curly bracket and update the state curlyC :: Action curlyC t = state \case   TableContext _ : st -> ([TokCurlyC <$ t], st)   st                  -> ([TokCurlyC <$ t], st) +-- | Emit a token ignoring the current lexeme token_ :: Token -> Action token_ t x = pure [t <$ x] +-- | Emit a token using the current lexeme token :: (String -> Token) -> Action token f x = pure [f <$> x] +-- | Emit a value token and update the current state value_ :: Token -> Action-value_ t x = emitValue (t <$ x)+value_ t = value (const t) +-- | Emit a value token using the current lexeme and update the current state value :: (String -> Token) -> Action-value f x = emitValue (f <$> x)--emitValue :: Located Token -> State [Context] [Located Token]-emitValue v = state \st ->+value f x = state \st ->   case st of-    ValueContext : st' -> ([v], st')-    _                  -> ([v], st )+    ValueContext : st' -> ([f <$> x], st')+    _                  -> ([f <$> x], st ) -timeValue :: ParseTime a => String -> [String] -> (a -> Token) -> Action+-- | Attempt to parse the current lexeme as a date-time token.+timeValue ::+  ParseTime a =>+  String       {- ^ description for error messages -} ->+  [String]     {- ^ possible valid patterns        -} ->+  (a -> Token) {- ^ token constructor              -} ->+  Action timeValue description patterns constructor = value \str ->-  case asum [parseTimeM False defaultTimeLocale pattern str | pattern <- patterns] of+  case asum [parseTimeM False defaultTimeLocale pat str | pat <- patterns] of     Nothing -> TokError ("malformed " ++ description)     Just t  -> constructor t -type AlexInput = Located String--alexGetByte :: AlexInput -> Maybe (Int, AlexInput)-alexGetByte Located { locPosition = p, locThing = str } =+-- | Pop the first character off a located string if it's not empty.+-- The resulting 'Int' will either be the ASCII value of the character+-- or @1@ for non-ASCII Unicode values. To avoid a clash, @\x1@ is+-- remapped to @0@.+locatedUncons :: Located String -> Maybe (Int, Located String)+locatedUncons Located { locPosition = p, locThing = str } =   case str of     "" -> Nothing     x:xs@@ -157,6 +179,7 @@       where         rest = Located { locPosition = move x p, locThing = xs } +-- | Generate the correct terminating token given the current lexer state. eofToken :: [Context] -> Located String -> Located Token eofToken (MlStrContext p _ : _) _ = Located p (TokError "unterminated multi-line string literal") eofToken (StrContext   p _ : _) _ = Located p (TokError "unterminated string literal")
src/Toml/Parser.y view
@@ -11,13 +11,13 @@  -} module Toml.Parser (-  -- * types+  -- * Types   Expr(..),   SectionKind(..),   Val(..),   Key, -  -- * parser+  -- * Parser   parseRawToml,   ) where @@ -125,6 +125,12 @@   | sepBy1_(p,q) q p  { NonEmpty.cons $3 $1   }  {++-- | Parse a list of tokens either returning the first unexpected+-- token or a list of the TOML statements in the file to be+-- processed by "Toml.Semantics".+parseRawToml :: [Located Token] -> Either (Located Token) [Expr]+-- implementation generated by happy  errorP :: [Located Token] -> Either (Located Token) a errorP (t:_) = Left t
src/Toml/Pretty.hs view
@@ -9,6 +9,11 @@ This module provides human-readable renderers for types used in this package to assist error message production. +The generated 'Doc' values are annotated with 'DocClass' values+to assist in producing syntax-highlighted outputs.++To extract a plain String representation, use 'show'.+ -} module Toml.Pretty (     -- * Types
src/Toml/ToValue.hs view
@@ -1,10 +1,20 @@-{-# LANGUAGE TypeOperators, TypeFamilies #-}+{-# LANGUAGE TypeFamilies #-} {-| Module      : Toml.ToValue Description : Automation for converting application values to TOML. Copyright   : (c) Eric Mertens, 2023 License     : ISC Maintainer  : emertens@gmail.com++The 'ToValue' class provides a conversion function from+application-specific to TOML values.++Because the top-level TOML document is always a table,+the 'ToTable' class is for types that specifically support+conversion from a 'Table'.++"Toml.ToValue.Generic" can be used to derive instances of 'ToTable'+automatically for record types.  -} module Toml.ToValue (
+ src/Toml/ToValue/Generic.hs view
@@ -0,0 +1,68 @@+{-|+Module      : Toml.ToValue.Matcher+Description : GHC.Generics derived table generation+Copyright   : (c) Eric Mertens, 2023+License     : ISC+Maintainer  : emertens@gmail.com++Use 'genericToTable' to derive an instance of 'Toml.ToValue.ToTable'+using the field names of a record.++-}+module Toml.ToValue.Generic (+    GToTable(..),+    genericToTable,+    ) where++import Data.Map qualified as Map+import GHC.Generics+import Toml.Value (Table)+import Toml.ToValue (ToValue(..))++-- | Use a record's field names to generate a 'Table'+--+-- @since 1.0.2.0+genericToTable :: (Generic a, GToTable (Rep a)) => a -> Table+genericToTable = gToTable . from+{-# INLINE genericToTable #-}++-- | Supports conversion of product types with field selector names+-- to TOML values.+--+-- @since 1.0.2.0+class GToTable f where+    gToTable :: f a -> Table++-- | Ignores type constructor names+instance GToTable f => GToTable (D1 c f) where+    gToTable (M1 x) = gToTable x+    {-# INLINE gToTable #-}++-- | Ignores value constructor names+instance GToTable f => GToTable (C1 c f) where+    gToTable (M1 x) = gToTable x+    {-# INLINE gToTable #-}++instance (GToTable f, GToTable g) => GToTable (f :*: g) where+    gToTable (x :*: y) = gToTable x <> gToTable y+    {-# INLINE gToTable #-}++-- | Omits the key from the table on nothing, includes it on just+instance {-# OVERLAPS #-} (Selector s, ToValue a) => GToTable (S1 s (K1 i (Maybe a))) where+    gToTable (M1 (K1 Nothing)) = Map.empty+    gToTable s@(M1 (K1 (Just x))) = Map.singleton (selName s) (toValue x)+    {-# INLINE gToTable #-}++-- | Uses record selector name as table key+instance (Selector s, ToValue a) => GToTable (S1 s (K1 i a)) where+    gToTable s@(M1 (K1 x)) = Map.singleton (selName s) (toValue x)+    {-# INLINE gToTable #-}++-- | Emits empty table+instance GToTable U1 where+    gToTable _ = Map.empty+    {-# INLINE gToTable #-}++instance GToTable V1 where+    gToTable v = case v of {}+    {-# INLINE gToTable #-}
src/Toml/Value.hs view
@@ -15,8 +15,10 @@     Table,     ) where +import Data.Data (Data) import Data.Map (Map) import Data.Time (Day, LocalTime, TimeOfDay, ZonedTime(zonedTimeToLocalTime, zonedTimeZone), timeZoneMinutes)+import GHC.Generics (Generic)  -- | Representation of a TOML key-value table. type Table = Map String Value@@ -33,7 +35,7 @@     | ZonedTime ZonedTime     | LocalTime LocalTime     | Day       Day-    deriving (Show, Read)+    deriving (Show, Read, Data, Generic)  instance Eq Value where     Integer   x == Integer   y = x == y
+ test/DecodeSpec.hs view
@@ -0,0 +1,128 @@+{-# Language DuplicateRecordFields #-}+module DecodeSpec (spec) where++import Data.Map qualified as Map+import Data.Maybe (fromMaybe)+import GHC.Generics (Generic)+import QuoteStr (quoteStr )+import Test.Hspec (it, shouldBe, Spec)+import Toml (decode, Result(Success), encode)+import Toml.FromValue (FromTable(..), FromValue(..), defaultTableFromValue, runParseTable, reqKey, optKey)+import Toml.FromValue.Generic (genericFromTable)+import Toml.ToValue+import Toml.ToValue.Generic (genericToTable)++newtype Fruits = Fruits { fruits :: [Fruit] }+    deriving (Eq, Show, Generic)++data Fruit = Fruit {+    name :: String,+    physical :: Maybe Physical,+    varieties :: [Variety]+    } deriving (Eq, Show, Generic)++data Physical = Physical {+    color :: String,+    shape :: String+    } deriving (Eq, Show, Generic)++newtype Variety = Variety {+    name :: String+    } deriving (Eq, Show, Generic)++instance FromTable Fruits   where fromTable = genericFromTable+instance FromTable Physical where fromTable = genericFromTable+instance FromTable Variety  where fromTable = genericFromTable++instance FromValue Fruits   where fromValue = defaultTableFromValue+instance FromValue Fruit    where fromValue = defaultTableFromValue+instance FromValue Physical where fromValue = defaultTableFromValue+instance FromValue Variety  where fromValue = defaultTableFromValue++instance ToTable Fruits   where toTable = genericToTable+instance ToTable Physical where toTable = genericToTable+instance ToTable Variety  where toTable = genericToTable++instance ToValue Fruits   where toValue = defaultTableToValue+instance ToValue Fruit    where toValue = defaultTableToValue+instance ToValue Physical where toValue = defaultTableToValue+instance ToValue Variety  where toValue = defaultTableToValue++instance FromTable Fruit where+    fromTable = runParseTable (Fruit+        <$> reqKey "name"+        <*> optKey "physical"+        <*> (fromMaybe [] <$> optKey "varieties"))++instance ToTable Fruit where+    toTable (Fruit n mbp vs) = Map.fromList $+        ["varieties" .= vs | not (null vs)] +++        ["physical"  .= p | Just p <- [mbp]] +++        ["name"      .= n]++spec :: Spec+spec =+ do let expect = Fruits [+            Fruit "apple" (Just (Physical "red" "round")) [Variety "red delicious", Variety "granny smith"],+            Fruit "banana" Nothing [Variety "plantain"]]++    it "handles fruit example" $+        decode [quoteStr|+            [[fruits]]+            name = "apple"++            [fruits.physical]  # subtable+            color = "red"+            shape = "round"++            [[fruits.varieties]]  # nested array of tables+            name = "red delicious"++            [[fruits.varieties]]+            name = "granny smith"++            [[fruits]]+            name = "banana"++            [[fruits.varieties]]+            name = "plantain"|]+        `shouldBe`+        Success mempty expect++    it "encodes correctly" $+        show (encode expect)+        `shouldBe`+        [quoteStr|+            [[fruits]]+            name = "apple"++            [fruits.physical]+            color = "red"+            shape = "round"++            [[fruits.varieties]]+            name = "red delicious"++            [[fruits.varieties]]+            name = "granny smith"++            [[fruits]]+            name = "banana"++            [[fruits.varieties]]+            name = "plantain"|]++    it "generates warnings for unused keys" $+        decode [quoteStr|+            [[fruits]]+            name = "peach"+            taste = "sweet"+            count = 5+            [[fruits]]+            name = "pineapple"+            color = "yellow"|]+        `shouldBe`+        Success [+            "Unexpected keys: count, taste in top.fruits[0]",+            "Unexpected key: color in top.fruits[1]"]+            (Fruits [Fruit "peach" Nothing [], Fruit "pineapple" Nothing []])
+ test/LexerSpec.hs view
@@ -0,0 +1,68 @@+module LexerSpec (spec) where++import Data.Map qualified as Map+import Test.Hspec (it, shouldBe, Spec)+import Toml (parse, Value(Integer))++spec :: Spec+spec =+ do it "handles special cased control character" $+        parse "x = '\SOH'"+        `shouldBe`+        Left "1:6: lexical error: unexpected '\\SOH'"++    -- These seem boring, but they provide test coverage of an error case in the state machine+    it "handles unexpected '}'" $+        parse "}"+        `shouldBe`+        Left "1:1: parse error: unexpected '}'"++    it "handles unexpected '{'" $+        parse "{"+        `shouldBe`+        Left "1:1: parse error: unexpected '{'"++    it "accepts tabs" $+        parse "x\t=\t1"+        `shouldBe`+        Right (Map.singleton "x" (Integer 1))++    it "computes columns correctly with tabs" $+        parse "x\t=\t="+        `shouldBe`+        Left "1:17: parse error: unexpected '='"++    it "detects non-scalars in strings" $+        parse "x = \"\\udfff\""+        `shouldBe`+        Left "1:6: lexical error: non-scalar unicode escape"++    it "catches unclosed [" $+        parse "x = [1,2,3"+        `shouldBe`+        Left "1:5: lexical error: unterminated '['"++    it "catches unclosed {" $+        parse "x = { y"+        `shouldBe`+        Left "1:5: lexical error: unterminated '{'"++    it "catches unclosed \"" $+        parse "x = \"abc"+        `shouldBe`+        Left "1:5: lexical error: unterminated string literal"++    it "catches unclosed \"\"\"" $+        parse "x = \"\"\"test"+        `shouldBe`+        Left "1:5: lexical error: unterminated multi-line string literal"++    it "handles escapes at the end of input" $+        parse "x = \"\\"+        `shouldBe`+        Left "1:7: lexical error: unexpected end-of-input"++    it "handles invalid escapes" $+        parse "x = \"\\p\""+        `shouldBe`+        Left "1:7: lexical error: unexpected 'p'"
test/Main.hs view
@@ -1,966 +1,1 @@-{-# Language QuasiQuotes #-}-{-|-Module      : Main-Description : Unit tests-Copyright   : (c) Eric Mertens, 2023-License     : ISC-Maintainer  : emertens@gmail.com--TOML parser and validator unit tests (primarily drawn from the-specification document).---}-module Main (main) where--import Data.Map (Map)-import Data.Map qualified as Map-import Data.Time (Day, TimeOfDay, LocalTime, ZonedTime)-import QuoteStr (quoteStr)-import Test.Hspec (hspec, describe, it, shouldBe, shouldSatisfy, Spec)-import Toml (Value(..), parse, decode, encode, Result(Success), prettyToml, Table)-import Toml.FromValue (FromValue(..), defaultTableFromValue, reqKey, optKey, runParseTable, ParseTable, FromTable (fromTable))-import Toml.ToValue (table, (.=), toValue)--main :: IO ()-main = hspec do--  describe "lexer"-   do it "handles special cased control character" $-        parse "x = '\SOH'"-        `shouldBe`-        Left "1:6: lexical error: unexpected '\\SOH'"--      -- These seem boring, but they provide test coverage of an error case in the state machine-      it "handles unexpected '}'" $-        parse "}"-        `shouldBe`-        Left "1:1: parse error: unexpected '}'"--      it "handles unexpected '{'" $-        parse "{"-        `shouldBe`-        Left "1:1: parse error: unexpected '{'"--      it "accepts tabs" $-        parse "x\t=\t1"-        `shouldBe`-        Right (Map.singleton "x" (Integer 1))--      it "computes columns correctly with tabs" $-        parse "x\t=\t="-        `shouldBe`-        Left "1:17: parse error: unexpected '='"--      it "detects non-scalars in strings" $-        parse "x = \"\\udfff\""-        `shouldBe`-        Left "1:6: lexical error: non-scalar unicode escape"--      it "catches unclosed [" $-        parse "x = [1,2,3"-        `shouldBe`-        Left "1:5: lexical error: unterminated '['"--      it "catches unclosed {" $-        parse "x = { y"-        `shouldBe`-        Left "1:5: lexical error: unterminated '{'"--      it "catches unclosed \"" $-        parse "x = \"abc"-        `shouldBe`-        Left "1:5: lexical error: unterminated string literal"--      it "catches unclosed \"\"\"" $-        parse "x = \"\"\"test"-        `shouldBe`-        Left "1:5: lexical error: unterminated multi-line string literal"-      -      it "handles escapes at the end of input" $-        parse "x = \"\\"-        `shouldBe`-        Left "1:7: lexical error: unexpected end-of-input"--      it "handles invalid escapes" $-        parse "x = \"\\p\""-        `shouldBe`-        Left "1:7: lexical error: unexpected 'p'"--  describe "ToValue"-   do-    it "converts characters as singleton strings" $-      toValue '!' `shouldBe` String "!"--  describe "parse" do-    describe "comment"-     do it "ignores comments" $-          parse [quoteStr|-            # This is a full-line comment-            key = "value"  # This is a comment at the end of a line-            another = "# This is not a comment"|]-          `shouldBe`-          Right (Map.fromList [("another",String "# This is not a comment"),("key",String "value")])--    describe "key/value pair"-     do it "supports the most basic assignments" $-          parse "key = \"value\"" `shouldBe` Right (Map.singleton "key" (String "value"))--        it "requires a value after equals" $-          parse "key = # INVALID"-          `shouldBe`-          Left "1:16: parse error: unexpected end-of-input"--        it "requires newlines between assignments" $-          parse "first = \"Tom\" last = \"Preston-Werner\" # INVALID"-          `shouldBe`-          Left "1:15: parse error: unexpected bare key"--    describe "keys"-     do it "allows bare keys" $-          parse [quoteStr|-            key = "value"-            bare_key = "value"-            bare-key = "value"-            1234 = "value"|]-          `shouldBe`-          Right (Map.fromList [-            "1234"     .= "value",-            "bare-key" .= "value",-            "bare_key" .= "value",-            "key"      .= "value"])--        it "allows quoted keys" $-          parse [quoteStr|-            "127.0.0.1" = "value"-            "character encoding" = "value"-            "ʎǝʞ" = "value"-            'key2' = "value"-            'quoted "value"' = "value"|]-          `shouldBe`-          Right (Map.fromList [-            "127.0.0.1"          .= "value",-            "character encoding" .= "value",-            "key2"               .= "value",-            "quoted \"value\""   .= "value",-            "ʎǝʞ"                .= "value"])--        it "allows dotted keys" $-          parse [quoteStr|-            name = "Orange"-            physical.color = "orange"-            physical.shape = "round"-            site."google.com" = true|]-          `shouldBe`-          Right (Map.fromList [-            "name"     .= "Orange",-            "physical" .= table ["color" .= "orange", "shape" .= "round"],-            "site"     .= table ["google.com" .= True]])--        it "prevents duplicate keys" $-          parse [quoteStr|-            name = "Tom"-            name = "Pradyun"|]-          `shouldBe` Left "2:1: key error: name is already assigned"--        it "prevents duplicate keys even between bare and quoted" $-          parse [quoteStr|-            spelling = "favorite"-            "spelling" = "favourite"|]-          `shouldBe` Left "2:1: key error: spelling is already assigned"--        it "allows out of order definitions" $-          parse [quoteStr|-            apple.type = "fruit"-            orange.type = "fruit"--            apple.skin = "thin"-            orange.skin = "thick"--            apple.color = "red"-            orange.color = "orange"|]-          `shouldBe`-          Right (Map.fromList [-            "apple" .= table [-                "color" .= "red",-                "skin"  .= "thin",-                "type"  .= "fruit"],-            "orange" .= table [-                "color" .= "orange",-                "skin"  .= "thick",-                "type"  .= "fruit"]])--        it "allows numeric bare keys" $-          parse "3.14159 = 'pi'" `shouldBe` Right (Map.singleton "3" (table [("14159", String "pi")]))--        it "allows keys that look like other values" $-          parse [quoteStr|-            true = true-            false = false-            1900-01-01 = 1900-01-01-            1_2 = 2_3|]-          `shouldBe`-          Right (Map.fromList [-            "1900-01-01" .= (read "1900-01-01" :: Day),-            "1_2"        .= (23::Int),-            "false"      .= False,-            "true"       .= True])--    describe "string"-     do it "parses escapes" $-          parse [quoteStr|-            str = "I'm a string. \"You can quote me\". Name\tJos\u00E9\nLocation\tSF."|]-          `shouldBe`-          Right (Map.singleton "str" (String "I'm a string. \"You can quote me\". Name\tJos\xe9\nLocation\tSF."))--        it "strips the initial newline from multiline strings" $-          parse [quoteStr|-            str1 = """-            Roses are red-            Violets are blue"""|]-          `shouldBe` Right (Map.singleton "str1" (String "Roses are red\nViolets are blue"))--        it "strips whitespace with a trailing escape" $-          parse [quoteStr|-            # The following strings are byte-for-byte equivalent:-            str1 = "The quick brown fox jumps over the lazy dog."--            str2 = """-            The quick brown \---            fox jumps over \-                the lazy dog."""--            str3 = """\-                The quick brown \-                fox jumps over \-                the lazy dog.\-                """|]-          `shouldBe`-          Right (Map.fromList [-            "str1" .= "The quick brown fox jumps over the lazy dog.",-            "str2" .= "The quick brown fox jumps over the lazy dog.",-            "str3" .= "The quick brown fox jumps over the lazy dog."])--        it "allows quotes inside multiline quoted strings" $-          parse [quoteStr|-            str4 = """Here are two quotation marks: "". Simple enough."""-            str5 = """Here are three quotation marks: ""\"."""-            str6 = """Here are fifteen quotation marks: ""\"""\"""\"""\"""\"."""--            # "This," she said, "is just a pointless statement."-            str7 = """"This," she said, "is just a pointless statement.""""|]-          `shouldBe`-          Right (Map.fromList [-            "str4" .= "Here are two quotation marks: \"\". Simple enough.",-            "str5" .= "Here are three quotation marks: \"\"\".",-            "str6" .= "Here are fifteen quotation marks: \"\"\"\"\"\"\"\"\"\"\"\"\"\"\".",-            "str7" .= "\"This,\" she said, \"is just a pointless statement.\""])--        it "disallows triple quotes inside a multiline string" $-          parse [quoteStr|-            str5 = """Here are three quotation marks: """."""  # INVALID|]-          `shouldBe` Left "1:46: parse error: unexpected '.'"--        it "ignores escapes in literal strings" $-          parse [quoteStr|-            # What you see is what you get.-            winpath  = 'C:\Users\nodejs\templates'-            winpath2 = '\\ServerX\admin$\system32\'-            quoted   = 'Tom "Dubs" Preston-Werner'-            regex    = '<\i\c*\s*>'|]-          `shouldBe`-          Right (Map.fromList [-            "quoted"   .= "Tom \"Dubs\" Preston-Werner",-            "regex"    .= "<\\i\\c*\\s*>",-            "winpath"  .= "C:\\Users\\nodejs\\templates",-            "winpath2" .= "\\\\ServerX\\admin$\\system32\\"])--        it "handles multiline literal strings" $-          parse [quoteStr|-            regex2 = '''I [dw]on't need \d{2} apples'''-            lines  = '''-            The first newline is-            trimmed in raw strings.-            All other whitespace-            is preserved.-            '''|]-          `shouldBe`-          Right (Map.fromList [-            "lines"  .= "The first newline is\ntrimmed in raw strings.\nAll other whitespace\nis preserved.\n",-            "regex2" .= "I [dw]on't need \\d{2} apples"])--        it "parses all the other escapes" $-          parse [quoteStr|-            x = "\\\b\f\r\U0010abcd"-            y = """\\\b\f\r\u7bca\U0010abcd\n\r\t"""|]-          `shouldBe`-          Right (Map.fromList [-            "x" .= "\\\b\f\r\x0010abcd",-            "y" .= "\\\b\f\r\x7bca\x0010abcd\n\r\t"])--        it "rejects out of range unicode escapes" $-          parse [quoteStr|-            x = "\U11111111"|]-          `shouldBe` Left "1:6: lexical error: unicode escape too large"--    describe "integer"-     do it "parses literals correctly" $-          parse [quoteStr|-            int1 = +99-            int2 = 42-            int3 = 0-            int4 = -17-            int5 = 1_000-            int6 = 5_349_221-            int7 = 53_49_221  # Indian number system grouping-            int8 = 1_2_3_4_5  # VALID but discouraged-            # hexadecimal with prefix `0x`-            hex1 = 0xDEADBEEF-            hex2 = 0xdeadbeef-            hex3 = 0xdead_beef--            # octal with prefix `0o`-            oct1 = 0o01234567-            oct2 = 0o755 # useful for Unix file permissions--            # binary with prefix `0b`-            bin1 = 0b11010110|]-          `shouldBe` Right-          (Map.fromList [-              "bin1" .= Integer 214,-              "hex1" .= Integer 0xDEADBEEF,-              "hex2" .= Integer 0xDEADBEEF,-              "hex3" .= Integer 0xDEADBEEF,-              "int1" .= Integer 99,-              "int2" .= Integer 42,-              "int3" .= Integer 0,-              "int4" .= Integer (-17),-              "int5" .= Integer 1000,-              "int6" .= Integer 5349221,-              "int7" .= Integer 5349221,-              "int8" .= Integer 12345,-              "oct1" .= Integer 0o01234567,-              "oct2" .= Integer 0o755])--    describe "float"-     do it "parses floats" $-          parse [quoteStr|-            # fractional-            flt1 = +1.0-            flt2 = 3.1415-            flt3 = -0.01--            # exponent-            flt4 = 5e+22-            flt5 = 1e06-            flt6 = -2E-2--            # both-            flt7 = 6.626e-34-            flt8 = 224_617.445_991_228-            # infinity-            sf1 = inf  # positive infinity-            sf2 = +inf # positive infinity-            sf3 = -inf # negative infinity|]-          `shouldBe`-          Right (Map.fromList [-            "flt1" .= Float 1.0,-            "flt2" .= Float 3.1415,-            "flt3" .= Float (-1.0e-2),-            "flt4" .= Float 4.9999999999999996e22,-            "flt5" .= Float 1000000.0,-            "flt6" .= Float (-2.0e-2),-            "flt7" .= Float 6.626e-34,-            "flt8" .= Float 224617.445991228,-            "sf1"  .= Float (1/0),-            "sf2"  .= Float (1/0),-            "sf3"  .= Float (-1/0)])--        it "parses nan correctly" $-          let checkNaN (Float x) = isNaN x-              checkNaN _         = False-          in-          parse [quoteStr|-            # not a number-            sf4 = nan  # actual sNaN/qNaN encoding is implementation-specific-            sf5 = +nan # same as `nan`-            sf6 = -nan # valid, actual encoding is implementation-specific|]-          `shouldSatisfy` \case-            Left{} -> False-            Right x -> all checkNaN x--    describe "boolean"-     do it "parses boolean literals" $-          parse [quoteStr|-            bool1 = true-            bool2 = false|]-          `shouldBe`-          Right (Map.fromList [-            "bool1" .= True,-            "bool2" .= False])--    describe "offset date-time"-     do it "parses offset date times" $-          parse [quoteStr|-            odt1 = 1979-05-27T07:32:00Z-            odt2 = 1979-05-27T00:32:00-07:00-            odt3 = 1979-05-27T00:32:00.999999-07:00-            odt4 = 1979-05-27 07:32:00Z|]-          `shouldBe`-          Right (Map.fromList [-            "odt1" .= ZonedTime (read "1979-05-27 07:32:00 +0000"),-            "odt2" .= ZonedTime (read "1979-05-27 00:32:00 -0700"),-            "odt3" .= ZonedTime (read "1979-05-27 00:32:00.999999 -0700"),-            "odt4" .= ZonedTime (read "1979-05-27 07:32:00 +0000")])--    describe "local date-time"-     do it "parses local date-times" $-          parse [quoteStr|-            ldt1 = 1979-05-27T07:32:00-            ldt2 = 1979-05-27T00:32:00.999999-            ldt3 = 1979-05-28 00:32:00.999999|]-          `shouldBe`-          Right (Map.fromList [-            "ldt1" .= LocalTime (read "1979-05-27 07:32:00"),-            "ldt2" .= LocalTime (read "1979-05-27 00:32:00.999999"),-            "ldt3" .= LocalTime (read "1979-05-28 00:32:00.999999")])--        it "catches invalid date-times" $-          parse [quoteStr|-            ldt = 9999-99-99T99:99:99|]-          `shouldBe`-          Left "1:7: lexical error: malformed local date-time"--    describe "local date"-     do it "parses dates" $-          parse [quoteStr|-            ld1 = 1979-05-27|]-          `shouldBe`-          Right (Map.singleton "ld1" (Day (read "1979-05-27")))--    describe "local time"-     do it "parses times" $-          parse [quoteStr|-            lt1 = 07:32:00-            lt2 = 00:32:00.999999|]-          `shouldBe`-          Right (Map.fromList [-            "lt1" .= TimeOfDay (read "07:32:00"),-            "lt2" .= TimeOfDay (read "00:32:00.999999")])--    describe "array"-     do it "parses array examples" $-          parse [quoteStr|-            integers = [ 1, 2, 3 ]-            colors = [ "red", "yellow", "green" ]-            nested_arrays_of_ints = [ [ 1, 2 ], [3, 4, 5] ]-            nested_mixed_array = [ [ 1, 2 ], ["a", "b", "c"] ]-            string_array = [ "all", 'strings', """are the same""", '''type''' ]--            # Mixed-type arrays are allowed-            numbers = [ 0.1, 0.2, 0.5, 1, 2, 5 ]-            contributors = [-            "Foo Bar <foo@example.com>",-            { name = "Baz Qux", email = "bazqux@example.com", url = "https://example.com/bazqux" }-            ]|]-            `shouldBe`-            Right (Map.fromList [-                "colors" .= ["red", "yellow", "green"],-                "contributors" .= [-                    String "Foo Bar <foo@example.com>",-                    table [-                        "email" .= "bazqux@example.com",-                        "name" .= "Baz Qux",-                        "url" .= "https://example.com/bazqux"]],-                "integers" .= [1, 2, 3 :: Integer],-                "nested_arrays_of_ints" .= [[1, 2], [3, 4, 5 :: Integer]],-                "nested_mixed_array" .= [[Integer 1, Integer 2], [String "a", String "b", String "c"]],-                "numbers" .= [Float 0.1, Float 0.2, Float 0.5, Integer 1, Integer 2, Integer 5],-                "string_array" .= ["all", "strings", "are the same", "type"]])--        it "handles newlines and comments" $-          parse [quoteStr|-            integers2 = [-            1, 2, 3-            ]--            integers3 = [-            1,-            2, # this is ok-            ]|]-            `shouldBe`-            Right (Map.fromList [-                "integers2" .= [1, 2, 3 :: Int],-                "integers3" .= [1, 2 :: Int]])--        it "disambiguates double brackets from array tables" $-          parse "x = [[1]]" `shouldBe` Right (Map.singleton "x" (Array [Array [Integer 1]]))--    describe "table"-     do it "allows empty tables" $-          parse "[table]" `shouldBe` Right (Map.singleton "table" (table []))--        it "parses simple tables" $-          parse [quoteStr|-            [table-1]-            key1 = "some string"-            key2 = 123--            [table-2]-            key1 = "another string"-            key2 = 456|]-          `shouldBe`-          Right (Map.fromList [-            "table-1" .= table [-                "key1" .= "some string",-                "key2" .= Integer 123],-            "table-2" .= table [-                "key1" .= "another string",-                "key2" .= Integer 456]])--        it "allows quoted keys" $-          parse [quoteStr|-            [dog."tater.man"]-            type.name = "pug"|]-          `shouldBe`-          Right (Map.fromList [("dog", table [("tater.man", table [("type", table [("name",String "pug")])])])])--        it "allows whitespace around keys" $-          parse [quoteStr|-            [a.b.c]            # this is best practice-            [ d.e.f ]          # same as [d.e.f]-            [ g .  h  . i ]    # same as [g.h.i]-            [ j . "ʞ" . 'l' ]  # same as [j."ʞ".'l']|]-          `shouldBe`-          Right (Map.fromList [-            "a" .= table ["b" .= table ["c" .= table []]],-            "d" .= table ["e" .= table ["f" .= table []]],-            "g" .= table ["h" .= table ["i" .= table []]],-            "j" .= table ["ʞ" .= table ["l" .= table []]]])--        it "allows supertables to be defined after subtables" $-          parse [quoteStr|-            # [x] you-            # [x.y] don't-            # [x.y.z] need these-            [x.y.z.w] # for this to work--            [x] # defining a super-table afterward is ok-            q=1|]-          `shouldBe`-          Right (Map.fromList [-            "x" .= table [-                "q" .= Integer 1,-                "y" .= table [-                    "z" .= table [-                        "w" .= table []]]]])--        it "prevents using a [table] to open a table defined with dotted keys" $-          parse [quoteStr|-            [fruit]-            apple.color = 'red'-            apple.taste.sweet = true-            [fruit.apple]|]-          `shouldBe` Left "4:8: key error: apple is a closed table"--        it "can add subtables" $-          parse [quoteStr|-            [fruit]-            apple.color = "red"-            apple.taste.sweet = true-            [fruit.apple.texture]  # you can add sub-tables-            smooth = true|]-          `shouldBe`-          Right (Map.fromList [-            "fruit" .= table [-                "apple" .= table [-                    "color" .= "red",-                    "taste" .= table [-                        "sweet" .= True],-                        "texture" .= table [-                            "smooth" .= True]]]])--    describe "inline table"-     do it "parses inline tables" $-          parse [quoteStr|-            name = { first = "Tom", last = "Preston-Werner" }-            point = { x = 1, y = 2 }-            animal = { type.name = "pug" }|]-          `shouldBe`-          Right (Map.fromList [-            "animal" .= table ["type" .= table ["name" .= "pug"]],-            "name"   .= table ["first" .= "Tom", "last" .= "Preston-Werner"],-            "point"  .= table ["x" .= Integer 1, "y" .= Integer 2]])--        it "prevents altering inline tables with dotted keys" $-          parse [quoteStr|-            [product]-            type = { name = "Nail" }-            type.edible = false  # INVALID|]-          `shouldBe` Left "3:1: key error: type is already assigned"--        it "prevents using inline tables to add keys to existing tables" $-          parse [quoteStr|-            [product]-            type.name = "Nail"-            type = { edible = false }  # INVALID|]-          `shouldBe` Left "3:1: key error: type is already assigned"--    describe "array of tables"-     do it "supports array of tables syntax" $-          decode [quoteStr|-            [[products]]-            name = "Hammer"-            sku = 738594937--            [[products]]  # empty table within the array--            [[products]]-            name = "Nail"-            sku = 284758393--            color = "gray"|]-          `shouldBe`-          Success mempty (Map.singleton "products" [-            Map.fromList [-              "name" .= "Hammer",-              "sku"  .= Integer 738594937],-            Map.empty,-            Map.fromList [-                "color" .= "gray",-                "name"  .= "Nail",-                "sku"   .= Integer 284758393]])--        it "handles subtables under array of tables" $-          parse [quoteStr|-            [[fruits]]-            name = "apple"--            [fruits.physical]  # subtable-            color = "red"-            shape = "round"--            [[fruits.varieties]]  # nested array of tables-            name = "red delicious"--            [[fruits.varieties]]-            name = "granny smith"---            [[fruits]]-            name = "banana"--            [[fruits.varieties]]-            name = "plantain"|]-          `shouldBe`-          Right (Map.fromList [-            "fruits" .= [-                table [-                    "name" .= "apple",-                    "physical" .= table [-                        "color" .= "red",-                        "shape" .= "round"],-                    "varieties" .= [-                        table ["name" .= "red delicious"],-                        table ["name" .= "granny smith"]]],-                table [-                    "name" .= "banana",-                    "varieties" .= [-                        table ["name" .= "plantain"]]]]])--        it "prevents redefining a supertable with an array of tables" $-          parse [quoteStr|-            # INVALID TOML DOC-            [fruit.physical]  # subtable, but to which parent element should it belong?-            color = "red"-            shape = "round"--            [[fruit]]  # parser must throw an error upon discovering that "fruit" is-                    # an array rather than a table-            name = "apple"|]-            `shouldBe` Left "6:3: key error: fruit is already a table"--        it "prevents redefining an inline array" $-          parse [quoteStr|-            # INVALID TOML DOC-            fruits = []--            [[fruits]] # Not allowed|]-          `shouldBe` Left "4:3: key error: fruits is already assigned"--    -- these cases are needed to complete coverage checking on Semantics module-    describe "corner cases"-     do it "stays open" $-          parse [quoteStr|-            [x.y.z]-            [x]-            [x.y]|]-          `shouldBe`-          parse "x.y.z={}"--        it "stays closed" $-          parse [quoteStr|-            [x.y]-            [x]-            [x.y]|] `shouldBe` Left "3:4: key error: y is a closed table"--        it "super tables of array tables preserve array tables" $-          parse [quoteStr|-            [[x.y]]-            [x]-            [[x.y]]|]-          `shouldBe`-          parse "x.y=[{},{}]"--        it "super tables of array tables preserve array tables" $-          parse [quoteStr|-            [[x.y]]-            [x]-            [x.y.z]|]-          `shouldBe`-          parse "x.y=[{z={}}]"--        it "detects conflicting inline keys" $-          parse [quoteStr|-            x = { y = 1, y.z = 2}|]-          `shouldBe` Left "1:14: key error: y is already assigned"--        it "handles merging dotted inline table keys" $-          parse [quoteStr|-            t = { a.x.y = 1, a.x.z = 2, a.q = 3}|]-          `shouldBe`-          Right (Map.fromList [-            ("t", table [-                ("a", table [-                    ("q",Integer 3),-                    ("x", table [-                        ("y",Integer 1),-                        ("z",Integer 2)])])])])--        it "disallows overwriting assignments with tables" $-          parse [quoteStr|-            x = 1-            [x.y]|]-          `shouldBe` Left "2:2: key error: x is already assigned"--        it "handles super super tables" $-          parse [quoteStr|-            [x.y.z]-            [x.y]-            [x]|]-          `shouldBe`-          parse "x.y.z={}"--        it "You can dot into open supertables" $-          parse [quoteStr|-            [x.y.z]-            [x]-            y.q = 1|]-          `shouldBe`-          parse "x.y={z={},q=1}"--        it "dotted tables close previously open tables" $-          parse [quoteStr|-            [x.y.z]-            [x]-            y.q = 1-            [x.y]|]-          `shouldBe` Left "4:4: key error: y is a closed table"--        it "dotted tables can't assign through closed tables!" $-          parse [quoteStr|-            [x.y]-            [x]-            y.z.w = 1|]-          `shouldBe` Left "3:1: key error: y is a closed table"--        it "super tables can't add new subtables to array tables via dotted keys" $-          parse [quoteStr|-            [[x.y]]-            [x]-            y.z.a = 1-            y.z.b = 2|]-          `shouldBe` Left "3:1: key error: y is a closed table"--        it "the previous example preserves closeness" $-          parse [quoteStr|-            [[x.y]]-            [x]-            y.z.a = 1-            y.w = 2|]-          `shouldBe` Left "3:1: key error: y is a closed table"--        it "defining a supertable closes the supertable" $-          parse [quoteStr|-            [x.y]-            [x]-            [x]|]-          `shouldBe` Left "3:2: key error: x is a closed table"--        it "prevents redefining an array of tables" $-          parse [quoteStr|-            [[x.y]]-            [x.y]|]-          `shouldBe` Left "2:4: key error: y is already an array of tables"--  describe "deserialization" deserializationTests-  describe "pretty-printing" prettyTests--tomlString :: Table -> String-tomlString = show . prettyToml--prettyTests :: Spec-prettyTests =- do it "renders example 1" $-      show (encode (Map.singleton "x" (1 :: Integer)))-        `shouldBe` [quoteStr|-        x = 1|]--    it "renders example 2" $-      fmap tomlString (parse "x=1\ny=2")-        `shouldBe` Right [quoteStr|-        x = 1-        y = 2|]--    it "renders example lists" $-      fmap tomlString (parse "x=[1,'two', [true]]")-        `shouldBe` Right [quoteStr|-        x = [1, "two", [true]]|]--    it "renders empty tables" $-      fmap tomlString (parse "x.y.z={}\nz.y.w=false")-        `shouldBe` Right [quoteStr|-        z.y.w = false--        [x.y.z]|]--    it "renders empty tables in array of tables" $-      fmap tomlString (parse "ex=[{},{},{a=9}]")-        `shouldBe` Right [quoteStr|-        [[ex]]--        [[ex]]--        [[ex]]-        a = 9|]--    it "renders multiple tables" $-      fmap tomlString (parse "a.x=1\nb.x=3\na.y=2\nb.y=4")-        `shouldBe` Right [quoteStr|-        [a]-        x = 1-        y = 2--        [b]-        x = 3-        y = 4|]--    it "renders escapes in strings" $-      fmap tomlString (parse "a=\"\\\\\\b\\t\\r\\n\\f\\\"\\u007f\\U0001000c\"")-        `shouldBe` Right [quoteStr|-        a = "\\\b\t\r\n\f\"\u007F\U0001000C"|]--    it "renders floats" $-      fmap tomlString (parse "a=0.0\nb=-0.1\nc=0.1\nd=3.141592653589793\ne=4e123")-        `shouldBe` Right [quoteStr|-        a = 0.0-        b = -0.1-        c = 0.1-        d = 3.141592653589793-        e = 4.0e123|]--    it "renders special floats" $-      fmap tomlString (parse "a=inf\nb=-inf\nc=nan")-        `shouldBe` Right [quoteStr|-        a = inf-        b = -inf-        c = nan|]--    it "renders empty documents" $-      fmap tomlString (parse "")-        `shouldBe` Right ""--    it "renders dates and time" $-      fmap tomlString (parse [quoteStr|-        a = 2020-05-07-        b = 15:16:17.990-        c = 2020-05-07T15:16:17.990-        d = 2020-05-07T15:16:17.990Z-        e = 2020-05-07T15:16:17-07:00-        f = 2021-09-06T14:15:19+08:00|])-        `shouldBe` Right [quoteStr|-        a = 2020-05-07-        b = 15:16:17.99-        c = 2020-05-07T15:16:17.99-        d = 2020-05-07T15:16:17.99Z-        e = 2020-05-07T15:16:17-07:00-        f = 2021-09-06T14:15:19+08:00|]--    it "renders quoted keys" $-      fmap tomlString (parse "''.'a b'.'\"' = 10")-        `shouldBe` Right [quoteStr|-        ""."a b"."\"" = 10|]--    it "renders inline tables" $-      fmap tomlString (parse [quoteStr|-        x = [[{a = 'this is a longer example', b = 'and it will linewrap'},{c = 'all on its own'}]]|])-        `shouldBe` Right [quoteStr|-          x = [ [ {a = "this is a longer example", b = "and it will linewrap"}-                , {c = "all on its own"} ] ]|]--newtype Fruits = Fruits [Fruit]-    deriving (Eq, Show)--data Fruit = Fruit String (Maybe Physical) [Variety]-    deriving (Eq, Show)--data Physical = Physical String String-    deriving (Eq, Show)--newtype Variety = Variety String-    deriving (Eq, Show)--instance FromTable Fruits where-    fromTable = runParseTable (Fruits <$> reqKey "fruits")--instance FromTable Fruit where-    fromTable = runParseTable (Fruit <$> reqKey "name" <*> optKey "physical" <*> reqKey "varieties")--instance FromTable Physical where-    fromTable = runParseTable (Physical <$> reqKey "color" <*> reqKey "shape")--instance FromTable Variety where-    fromTable = runParseTable (Variety <$> reqKey "name")--instance FromValue Fruits   where fromValue = defaultTableFromValue-instance FromValue Fruit    where fromValue = defaultTableFromValue-instance FromValue Physical where fromValue = defaultTableFromValue-instance FromValue Variety  where fromValue = defaultTableFromValue--deserializationTests :: Spec-deserializationTests =-     do it "handles fruit example" $-          decode [quoteStr|-              [[fruits]]-              name = "apple"--              [fruits.physical]  # subtable-              color = "red"-              shape = "round"--              [[fruits.varieties]]  # nested array of tables-              name = "red delicious"--              [[fruits.varieties]]-              name = "granny smith"--              [[fruits]]-              name = "banana"--              [[fruits.varieties]]-              name = "plantain"|]-          `shouldBe`-            Success mempty (Fruits [-                Fruit "apple" (Just (Physical "red" "round")) [Variety "red delicious", Variety "granny smith"],-                Fruit "banana" Nothing [Variety "plantain"]])+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
+ test/PrettySpec.hs view
@@ -0,0 +1,108 @@+module PrettySpec (spec) where++import Test.Hspec (it, shouldBe, Spec)+import QuoteStr (quoteStr)+import Toml (encode, parse, prettyToml, Table)+import Data.Map qualified as Map++tomlString :: Table -> String+tomlString = show . prettyToml++spec :: Spec+spec =+ do it "renders example 1" $+        show (encode (Map.singleton "x" (1 :: Integer)))+        `shouldBe` [quoteStr|+            x = 1|]++    it "renders example 2" $+        fmap tomlString (parse "x=1\ny=2")+        `shouldBe` Right [quoteStr|+            x = 1+            y = 2|]++    it "renders example lists" $+        fmap tomlString (parse "x=[1,'two', [true]]")+        `shouldBe` Right [quoteStr|+        x = [1, "two", [true]]|]++    it "renders empty tables" $+        fmap tomlString (parse "x.y.z={}\nz.y.w=false")+        `shouldBe` Right [quoteStr|+            z.y.w = false++            [x.y.z]|]++    it "renders empty tables in array of tables" $+        fmap tomlString (parse "ex=[{},{},{a=9}]")+        `shouldBe` Right [quoteStr|+            [[ex]]++            [[ex]]++            [[ex]]+            a = 9|]++    it "renders multiple tables" $+        fmap tomlString (parse "a.x=1\nb.x=3\na.y=2\nb.y=4")+        `shouldBe` Right [quoteStr|+            [a]+            x = 1+            y = 2++            [b]+            x = 3+            y = 4|]++    it "renders escapes in strings" $+        fmap tomlString (parse "a=\"\\\\\\b\\t\\r\\n\\f\\\"\\u007f\\U0001000c\"")+        `shouldBe` Right [quoteStr|+            a = "\\\b\t\r\n\f\"\u007F\U0001000C"|]++    it "renders floats" $+        fmap tomlString (parse "a=0.0\nb=-0.1\nc=0.1\nd=3.141592653589793\ne=4e123")+        `shouldBe` Right [quoteStr|+            a = 0.0+            b = -0.1+            c = 0.1+            d = 3.141592653589793+            e = 4.0e123|]++    it "renders special floats" $+        fmap tomlString (parse "a=inf\nb=-inf\nc=nan")+        `shouldBe` Right [quoteStr|+            a = inf+            b = -inf+            c = nan|]++    it "renders empty documents" $+        fmap tomlString (parse "")+        `shouldBe` Right ""++    it "renders dates and time" $+        fmap tomlString (parse [quoteStr|+            a = 2020-05-07+            b = 15:16:17.990+            c = 2020-05-07T15:16:17.990+            d = 2020-05-07T15:16:17.990Z+            e = 2020-05-07T15:16:17-07:00+            f = 2021-09-06T14:15:19+08:00|])+        `shouldBe` Right [quoteStr|+            a = 2020-05-07+            b = 15:16:17.99+            c = 2020-05-07T15:16:17.99+            d = 2020-05-07T15:16:17.99Z+            e = 2020-05-07T15:16:17-07:00+            f = 2021-09-06T14:15:19+08:00|]++    it "renders quoted keys" $+        fmap tomlString (parse "''.'a b'.'\"' = 10")+        `shouldBe` Right [quoteStr|+        ""."a b"."\"" = 10|]++    it "renders inline tables" $+        fmap tomlString (parse [quoteStr|+            x = [[{a = 'this is a longer example', b = 'and it will linewrap'},{c = 'all on its own'}]]|])+        `shouldBe` Right [quoteStr|+            x = [ [ {a = "this is a longer example", b = "and it will linewrap"}+                  , {c = "all on its own"} ] ]|]
+ test/ToValueSpec.hs view
@@ -0,0 +1,16 @@+module ToValueSpec where++import Test.Hspec (it, shouldBe, Spec)+import Toml (Value(..))+import Toml.ToValue (ToValue(toValue))++spec :: Spec+spec =+ do it "converts characters as singleton strings" $+        toValue '!' `shouldBe` String "!"+    +    it "converts strings normally" $+        toValue "demo" `shouldBe` String "demo"+    +    it "converts lists" $+        toValue [1,2,3::Int] `shouldBe` Array [Integer 1, Integer 2, Integer 3]
+ test/TomlSpec.hs view
@@ -0,0 +1,734 @@+{-# Language QuasiQuotes #-}+{-|+Module      : TomlSpec+Description : Unit tests+Copyright   : (c) Eric Mertens, 2023+License     : ISC+Maintainer  : emertens@gmail.com++TOML parser and validator unit tests (primarily drawn from the+specification document).++-}+module TomlSpec (spec) where++import Data.Map qualified as Map+import Data.Time (Day)+import QuoteStr (quoteStr)+import Test.Hspec (describe, it, shouldBe, shouldSatisfy, Spec)+import Toml (Value(..), parse, decode, Result(Success))+import Toml.ToValue (table, (.=))++spec :: Spec+spec =+ do describe "comment"+     do it "ignores comments" $+          parse [quoteStr|+            # This is a full-line comment+            key = "value"  # This is a comment at the end of a line+            another = "# This is not a comment"|]+          `shouldBe`+          Right (Map.fromList [("another",String "# This is not a comment"),("key",String "value")])++    describe "key/value pair"+     do it "supports the most basic assignments" $+          parse "key = \"value\"" `shouldBe` Right (Map.singleton "key" (String "value"))++        it "requires a value after equals" $+          parse "key = # INVALID"+          `shouldBe`+          Left "1:16: parse error: unexpected end-of-input"++        it "requires newlines between assignments" $+          parse "first = \"Tom\" last = \"Preston-Werner\" # INVALID"+          `shouldBe`+          Left "1:15: parse error: unexpected bare key"++    describe "keys"+     do it "allows bare keys" $+          parse [quoteStr|+            key = "value"+            bare_key = "value"+            bare-key = "value"+            1234 = "value"|]+          `shouldBe`+          Right (Map.fromList [+            "1234"     .= "value",+            "bare-key" .= "value",+            "bare_key" .= "value",+            "key"      .= "value"])++        it "allows quoted keys" $+          parse [quoteStr|+            "127.0.0.1" = "value"+            "character encoding" = "value"+            "ʎǝʞ" = "value"+            'key2' = "value"+            'quoted "value"' = "value"|]+          `shouldBe`+          Right (Map.fromList [+            "127.0.0.1"          .= "value",+            "character encoding" .= "value",+            "key2"               .= "value",+            "quoted \"value\""   .= "value",+            "ʎǝʞ"                .= "value"])++        it "allows dotted keys" $+          parse [quoteStr|+            name = "Orange"+            physical.color = "orange"+            physical.shape = "round"+            site."google.com" = true|]+          `shouldBe`+          Right (Map.fromList [+            "name"     .= "Orange",+            "physical" .= table ["color" .= "orange", "shape" .= "round"],+            "site"     .= table ["google.com" .= True]])++        it "prevents duplicate keys" $+          parse [quoteStr|+            name = "Tom"+            name = "Pradyun"|]+          `shouldBe` Left "2:1: key error: name is already assigned"++        it "prevents duplicate keys even between bare and quoted" $+          parse [quoteStr|+            spelling = "favorite"+            "spelling" = "favourite"|]+          `shouldBe` Left "2:1: key error: spelling is already assigned"++        it "allows out of order definitions" $+          parse [quoteStr|+            apple.type = "fruit"+            orange.type = "fruit"++            apple.skin = "thin"+            orange.skin = "thick"++            apple.color = "red"+            orange.color = "orange"|]+          `shouldBe`+          Right (Map.fromList [+            "apple" .= table [+                "color" .= "red",+                "skin"  .= "thin",+                "type"  .= "fruit"],+            "orange" .= table [+                "color" .= "orange",+                "skin"  .= "thick",+                "type"  .= "fruit"]])++        it "allows numeric bare keys" $+          parse "3.14159 = 'pi'" `shouldBe` Right (Map.singleton "3" (table [("14159", String "pi")]))++        it "allows keys that look like other values" $+          parse [quoteStr|+            true = true+            false = false+            1900-01-01 = 1900-01-01+            1_2 = 2_3|]+          `shouldBe`+          Right (Map.fromList [+            "1900-01-01" .= (read "1900-01-01" :: Day),+            "1_2"        .= (23::Int),+            "false"      .= False,+            "true"       .= True])++    describe "string"+     do it "parses escapes" $+          parse [quoteStr|+            str = "I'm a string. \"You can quote me\". Name\tJos\u00E9\nLocation\tSF."|]+          `shouldBe`+          Right (Map.singleton "str" (String "I'm a string. \"You can quote me\". Name\tJos\xe9\nLocation\tSF."))++        it "strips the initial newline from multiline strings" $+          parse [quoteStr|+            str1 = """+            Roses are red+            Violets are blue"""|]+          `shouldBe` Right (Map.singleton "str1" (String "Roses are red\nViolets are blue"))++        it "strips whitespace with a trailing escape" $+          parse [quoteStr|+            # The following strings are byte-for-byte equivalent:+            str1 = "The quick brown fox jumps over the lazy dog."++            str2 = """+            The quick brown \+++            fox jumps over \+                the lazy dog."""++            str3 = """\+                The quick brown \+                fox jumps over \+                the lazy dog.\+                """|]+          `shouldBe`+          Right (Map.fromList [+            "str1" .= "The quick brown fox jumps over the lazy dog.",+            "str2" .= "The quick brown fox jumps over the lazy dog.",+            "str3" .= "The quick brown fox jumps over the lazy dog."])++        it "allows quotes inside multiline quoted strings" $+          parse [quoteStr|+            str4 = """Here are two quotation marks: "". Simple enough."""+            str5 = """Here are three quotation marks: ""\"."""+            str6 = """Here are fifteen quotation marks: ""\"""\"""\"""\"""\"."""++            # "This," she said, "is just a pointless statement."+            str7 = """"This," she said, "is just a pointless statement.""""|]+          `shouldBe`+          Right (Map.fromList [+            "str4" .= "Here are two quotation marks: \"\". Simple enough.",+            "str5" .= "Here are three quotation marks: \"\"\".",+            "str6" .= "Here are fifteen quotation marks: \"\"\"\"\"\"\"\"\"\"\"\"\"\"\".",+            "str7" .= "\"This,\" she said, \"is just a pointless statement.\""])++        it "disallows triple quotes inside a multiline string" $+          parse [quoteStr|+            str5 = """Here are three quotation marks: """."""  # INVALID|]+          `shouldBe` Left "1:46: parse error: unexpected '.'"++        it "ignores escapes in literal strings" $+          parse [quoteStr|+            # What you see is what you get.+            winpath  = 'C:\Users\nodejs\templates'+            winpath2 = '\\ServerX\admin$\system32\'+            quoted   = 'Tom "Dubs" Preston-Werner'+            regex    = '<\i\c*\s*>'|]+          `shouldBe`+          Right (Map.fromList [+            "quoted"   .= "Tom \"Dubs\" Preston-Werner",+            "regex"    .= "<\\i\\c*\\s*>",+            "winpath"  .= "C:\\Users\\nodejs\\templates",+            "winpath2" .= "\\\\ServerX\\admin$\\system32\\"])++        it "handles multiline literal strings" $+          parse [quoteStr|+            regex2 = '''I [dw]on't need \d{2} apples'''+            lines  = '''+            The first newline is+            trimmed in raw strings.+            All other whitespace+            is preserved.+            '''|]+          `shouldBe`+          Right (Map.fromList [+            "lines"  .= "The first newline is\ntrimmed in raw strings.\nAll other whitespace\nis preserved.\n",+            "regex2" .= "I [dw]on't need \\d{2} apples"])++        it "parses all the other escapes" $+          parse [quoteStr|+            x = "\\\b\f\r\U0010abcd"+            y = """\\\b\f\r\u7bca\U0010abcd\n\r\t"""|]+          `shouldBe`+          Right (Map.fromList [+            "x" .= "\\\b\f\r\x0010abcd",+            "y" .= "\\\b\f\r\x7bca\x0010abcd\n\r\t"])++        it "rejects out of range unicode escapes" $+          parse [quoteStr|+            x = "\U11111111"|]+          `shouldBe` Left "1:6: lexical error: unicode escape too large"++    describe "integer"+     do it "parses literals correctly" $+          parse [quoteStr|+            int1 = +99+            int2 = 42+            int3 = 0+            int4 = -17+            int5 = 1_000+            int6 = 5_349_221+            int7 = 53_49_221  # Indian number system grouping+            int8 = 1_2_3_4_5  # VALID but discouraged+            # hexadecimal with prefix `0x`+            hex1 = 0xDEADBEEF+            hex2 = 0xdeadbeef+            hex3 = 0xdead_beef++            # octal with prefix `0o`+            oct1 = 0o01234567+            oct2 = 0o755 # useful for Unix file permissions++            # binary with prefix `0b`+            bin1 = 0b11010110|]+          `shouldBe` Right+          (Map.fromList [+              "bin1" .= Integer 214,+              "hex1" .= Integer 0xDEADBEEF,+              "hex2" .= Integer 0xDEADBEEF,+              "hex3" .= Integer 0xDEADBEEF,+              "int1" .= Integer 99,+              "int2" .= Integer 42,+              "int3" .= Integer 0,+              "int4" .= Integer (-17),+              "int5" .= Integer 1000,+              "int6" .= Integer 5349221,+              "int7" .= Integer 5349221,+              "int8" .= Integer 12345,+              "oct1" .= Integer 0o01234567,+              "oct2" .= Integer 0o755])++    describe "float"+     do it "parses floats" $+          parse [quoteStr|+            # fractional+            flt1 = +1.0+            flt2 = 3.1415+            flt3 = -0.01++            # exponent+            flt4 = 5e+22+            flt5 = 1e06+            flt6 = -2E-2++            # both+            flt7 = 6.626e-34+            flt8 = 224_617.445_991_228+            # infinity+            sf1 = inf  # positive infinity+            sf2 = +inf # positive infinity+            sf3 = -inf # negative infinity|]+          `shouldBe`+          Right (Map.fromList [+            "flt1" .= Float 1.0,+            "flt2" .= Float 3.1415,+            "flt3" .= Float (-1.0e-2),+            "flt4" .= Float 4.9999999999999996e22,+            "flt5" .= Float 1000000.0,+            "flt6" .= Float (-2.0e-2),+            "flt7" .= Float 6.626e-34,+            "flt8" .= Float 224617.445991228,+            "sf1"  .= Float (1/0),+            "sf2"  .= Float (1/0),+            "sf3"  .= Float (-1/0)])++        it "parses nan correctly" $+          let checkNaN (Float x) = isNaN x+              checkNaN _         = False+          in+          parse [quoteStr|+            # not a number+            sf4 = nan  # actual sNaN/qNaN encoding is implementation-specific+            sf5 = +nan # same as `nan`+            sf6 = -nan # valid, actual encoding is implementation-specific|]+          `shouldSatisfy` \case+            Left{} -> False+            Right x -> all checkNaN x++    describe "boolean"+     do it "parses boolean literals" $+          parse [quoteStr|+            bool1 = true+            bool2 = false|]+          `shouldBe`+          Right (Map.fromList [+            "bool1" .= True,+            "bool2" .= False])++    describe "offset date-time"+     do it "parses offset date times" $+          parse [quoteStr|+            odt1 = 1979-05-27T07:32:00Z+            odt2 = 1979-05-27T00:32:00-07:00+            odt3 = 1979-05-27T00:32:00.999999-07:00+            odt4 = 1979-05-27 07:32:00Z|]+          `shouldBe`+          Right (Map.fromList [+            "odt1" .= ZonedTime (read "1979-05-27 07:32:00 +0000"),+            "odt2" .= ZonedTime (read "1979-05-27 00:32:00 -0700"),+            "odt3" .= ZonedTime (read "1979-05-27 00:32:00.999999 -0700"),+            "odt4" .= ZonedTime (read "1979-05-27 07:32:00 +0000")])++    describe "local date-time"+     do it "parses local date-times" $+          parse [quoteStr|+            ldt1 = 1979-05-27T07:32:00+            ldt2 = 1979-05-27T00:32:00.999999+            ldt3 = 1979-05-28 00:32:00.999999|]+          `shouldBe`+          Right (Map.fromList [+            "ldt1" .= LocalTime (read "1979-05-27 07:32:00"),+            "ldt2" .= LocalTime (read "1979-05-27 00:32:00.999999"),+            "ldt3" .= LocalTime (read "1979-05-28 00:32:00.999999")])++        it "catches invalid date-times" $+          parse [quoteStr|+            ldt = 9999-99-99T99:99:99|]+          `shouldBe`+          Left "1:7: lexical error: malformed local date-time"++    describe "local date"+     do it "parses dates" $+          parse [quoteStr|+            ld1 = 1979-05-27|]+          `shouldBe`+          Right (Map.singleton "ld1" (Day (read "1979-05-27")))++    describe "local time"+     do it "parses times" $+          parse [quoteStr|+            lt1 = 07:32:00+            lt2 = 00:32:00.999999|]+          `shouldBe`+          Right (Map.fromList [+            "lt1" .= TimeOfDay (read "07:32:00"),+            "lt2" .= TimeOfDay (read "00:32:00.999999")])++    describe "array"+     do it "parses array examples" $+          parse [quoteStr|+            integers = [ 1, 2, 3 ]+            colors = [ "red", "yellow", "green" ]+            nested_arrays_of_ints = [ [ 1, 2 ], [3, 4, 5] ]+            nested_mixed_array = [ [ 1, 2 ], ["a", "b", "c"] ]+            string_array = [ "all", 'strings', """are the same""", '''type''' ]++            # Mixed-type arrays are allowed+            numbers = [ 0.1, 0.2, 0.5, 1, 2, 5 ]+            contributors = [+            "Foo Bar <foo@example.com>",+            { name = "Baz Qux", email = "bazqux@example.com", url = "https://example.com/bazqux" }+            ]|]+            `shouldBe`+            Right (Map.fromList [+                "colors" .= ["red", "yellow", "green"],+                "contributors" .= [+                    String "Foo Bar <foo@example.com>",+                    table [+                        "email" .= "bazqux@example.com",+                        "name" .= "Baz Qux",+                        "url" .= "https://example.com/bazqux"]],+                "integers" .= [1, 2, 3 :: Integer],+                "nested_arrays_of_ints" .= [[1, 2], [3, 4, 5 :: Integer]],+                "nested_mixed_array" .= [[Integer 1, Integer 2], [String "a", String "b", String "c"]],+                "numbers" .= [Float 0.1, Float 0.2, Float 0.5, Integer 1, Integer 2, Integer 5],+                "string_array" .= ["all", "strings", "are the same", "type"]])++        it "handles newlines and comments" $+          parse [quoteStr|+            integers2 = [+            1, 2, 3+            ]++            integers3 = [+            1,+            2, # this is ok+            ]|]+            `shouldBe`+            Right (Map.fromList [+                "integers2" .= [1, 2, 3 :: Int],+                "integers3" .= [1, 2 :: Int]])++        it "disambiguates double brackets from array tables" $+          parse "x = [[1]]" `shouldBe` Right (Map.singleton "x" (Array [Array [Integer 1]]))++    describe "table"+     do it "allows empty tables" $+          parse "[table]" `shouldBe` Right (Map.singleton "table" (table []))++        it "parses simple tables" $+          parse [quoteStr|+            [table-1]+            key1 = "some string"+            key2 = 123++            [table-2]+            key1 = "another string"+            key2 = 456|]+          `shouldBe`+          Right (Map.fromList [+            "table-1" .= table [+                "key1" .= "some string",+                "key2" .= Integer 123],+            "table-2" .= table [+                "key1" .= "another string",+                "key2" .= Integer 456]])++        it "allows quoted keys" $+          parse [quoteStr|+            [dog."tater.man"]+            type.name = "pug"|]+          `shouldBe`+          Right (Map.fromList [("dog", table [("tater.man", table [("type", table [("name",String "pug")])])])])++        it "allows whitespace around keys" $+          parse [quoteStr|+            [a.b.c]            # this is best practice+            [ d.e.f ]          # same as [d.e.f]+            [ g .  h  . i ]    # same as [g.h.i]+            [ j . "ʞ" . 'l' ]  # same as [j."ʞ".'l']|]+          `shouldBe`+          Right (Map.fromList [+            "a" .= table ["b" .= table ["c" .= table []]],+            "d" .= table ["e" .= table ["f" .= table []]],+            "g" .= table ["h" .= table ["i" .= table []]],+            "j" .= table ["ʞ" .= table ["l" .= table []]]])++        it "allows supertables to be defined after subtables" $+          parse [quoteStr|+            # [x] you+            # [x.y] don't+            # [x.y.z] need these+            [x.y.z.w] # for this to work++            [x] # defining a super-table afterward is ok+            q=1|]+          `shouldBe`+          Right (Map.fromList [+            "x" .= table [+                "q" .= Integer 1,+                "y" .= table [+                    "z" .= table [+                        "w" .= table []]]]])++        it "prevents using a [table] to open a table defined with dotted keys" $+          parse [quoteStr|+            [fruit]+            apple.color = 'red'+            apple.taste.sweet = true+            [fruit.apple]|]+          `shouldBe` Left "4:8: key error: apple is a closed table"++        it "can add subtables" $+          parse [quoteStr|+            [fruit]+            apple.color = "red"+            apple.taste.sweet = true+            [fruit.apple.texture]  # you can add sub-tables+            smooth = true|]+          `shouldBe`+          Right (Map.fromList [+            "fruit" .= table [+                "apple" .= table [+                    "color" .= "red",+                    "taste" .= table [+                        "sweet" .= True],+                        "texture" .= table [+                            "smooth" .= True]]]])++    describe "inline table"+     do it "parses inline tables" $+          parse [quoteStr|+            name = { first = "Tom", last = "Preston-Werner" }+            point = { x = 1, y = 2 }+            animal = { type.name = "pug" }|]+          `shouldBe`+          Right (Map.fromList [+            "animal" .= table ["type" .= table ["name" .= "pug"]],+            "name"   .= table ["first" .= "Tom", "last" .= "Preston-Werner"],+            "point"  .= table ["x" .= Integer 1, "y" .= Integer 2]])++        it "prevents altering inline tables with dotted keys" $+          parse [quoteStr|+            [product]+            type = { name = "Nail" }+            type.edible = false  # INVALID|]+          `shouldBe` Left "3:1: key error: type is already assigned"++        it "prevents using inline tables to add keys to existing tables" $+          parse [quoteStr|+            [product]+            type.name = "Nail"+            type = { edible = false }  # INVALID|]+          `shouldBe` Left "3:1: key error: type is already assigned"++    describe "array of tables"+     do it "supports array of tables syntax" $+          decode [quoteStr|+            [[products]]+            name = "Hammer"+            sku = 738594937++            [[products]]  # empty table within the array++            [[products]]+            name = "Nail"+            sku = 284758393++            color = "gray"|]+          `shouldBe`+          Success mempty (Map.singleton "products" [+            Map.fromList [+              "name" .= "Hammer",+              "sku"  .= Integer 738594937],+            Map.empty,+            Map.fromList [+                "color" .= "gray",+                "name"  .= "Nail",+                "sku"   .= Integer 284758393]])++        it "handles subtables under array of tables" $+          parse [quoteStr|+            [[fruits]]+            name = "apple"++            [fruits.physical]  # subtable+            color = "red"+            shape = "round"++            [[fruits.varieties]]  # nested array of tables+            name = "red delicious"++            [[fruits.varieties]]+            name = "granny smith"+++            [[fruits]]+            name = "banana"++            [[fruits.varieties]]+            name = "plantain"|]+          `shouldBe`+          Right (Map.fromList [+            "fruits" .= [+                table [+                    "name" .= "apple",+                    "physical" .= table [+                        "color" .= "red",+                        "shape" .= "round"],+                    "varieties" .= [+                        table ["name" .= "red delicious"],+                        table ["name" .= "granny smith"]]],+                table [+                    "name" .= "banana",+                    "varieties" .= [+                        table ["name" .= "plantain"]]]]])++        it "prevents redefining a supertable with an array of tables" $+          parse [quoteStr|+            # INVALID TOML DOC+            [fruit.physical]  # subtable, but to which parent element should it belong?+            color = "red"+            shape = "round"++            [[fruit]]  # parser must throw an error upon discovering that "fruit" is+                    # an array rather than a table+            name = "apple"|]+            `shouldBe` Left "6:3: key error: fruit is already a table"++        it "prevents redefining an inline array" $+          parse [quoteStr|+            # INVALID TOML DOC+            fruits = []++            [[fruits]] # Not allowed|]+          `shouldBe` Left "4:3: key error: fruits is already assigned"++    -- these cases are needed to complete coverage checking on Semantics module+    describe "corner cases"+     do it "stays open" $+          parse [quoteStr|+            [x.y.z]+            [x]+            [x.y]|]+          `shouldBe`+          parse "x.y.z={}"++        it "stays closed" $+          parse [quoteStr|+            [x.y]+            [x]+            [x.y]|] `shouldBe` Left "3:4: key error: y is a closed table"++        it "super tables of array tables preserve array tables" $+          parse [quoteStr|+            [[x.y]]+            [x]+            [[x.y]]|]+          `shouldBe`+          parse "x.y=[{},{}]"++        it "super tables of array tables preserve array tables" $+          parse [quoteStr|+            [[x.y]]+            [x]+            [x.y.z]|]+          `shouldBe`+          parse "x.y=[{z={}}]"++        it "detects conflicting inline keys" $+          parse [quoteStr|+            x = { y = 1, y.z = 2}|]+          `shouldBe` Left "1:14: key error: y is already assigned"++        it "handles merging dotted inline table keys" $+          parse [quoteStr|+            t = { a.x.y = 1, a.x.z = 2, a.q = 3}|]+          `shouldBe`+          Right (Map.fromList [+            ("t", table [+                ("a", table [+                    ("q",Integer 3),+                    ("x", table [+                        ("y",Integer 1),+                        ("z",Integer 2)])])])])++        it "disallows overwriting assignments with tables" $+          parse [quoteStr|+            x = 1+            [x.y]|]+          `shouldBe` Left "2:2: key error: x is already assigned"++        it "handles super super tables" $+          parse [quoteStr|+            [x.y.z]+            [x.y]+            [x]|]+          `shouldBe`+          parse "x.y.z={}"++        it "You can dot into open supertables" $+          parse [quoteStr|+            [x.y.z]+            [x]+            y.q = 1|]+          `shouldBe`+          parse "x.y={z={},q=1}"++        it "dotted tables close previously open tables" $+          parse [quoteStr|+            [x.y.z]+            [x]+            y.q = 1+            [x.y]|]+          `shouldBe` Left "4:4: key error: y is a closed table"++        it "dotted tables can't assign through closed tables!" $+          parse [quoteStr|+            [x.y]+            [x]+            y.z.w = 1|]+          `shouldBe` Left "3:1: key error: y is a closed table"++        it "super tables can't add new subtables to array tables via dotted keys" $+          parse [quoteStr|+            [[x.y]]+            [x]+            y.z.a = 1+            y.z.b = 2|]+          `shouldBe` Left "3:1: key error: y is a closed table"++        it "the previous example preserves closeness" $+          parse [quoteStr|+            [[x.y]]+            [x]+            y.z.a = 1+            y.w = 2|]+          `shouldBe` Left "3:1: key error: y is a closed table"++        it "defining a supertable closes the supertable" $+          parse [quoteStr|+            [x.y]+            [x]+            [x]|]+          `shouldBe` Left "3:2: key error: x is a closed table"++        it "prevents redefining an array of tables" $+          parse [quoteStr|+            [[x.y]]+            [x.y]|]+          `shouldBe` Left "2:4: key error: y is already an array of tables"
toml-parser.cabal view
@@ -1,6 +1,6 @@ cabal-version:      3.0 name:               toml-parser-version:            1.0.1.0+version:            1.1.0.0 synopsis:           TOML 1.0.0 parser description:     TOML parser using generated lexers and parsers with@@ -28,11 +28,18 @@     default-language:   Haskell2010     default-extensions:         BlockArguments+        DeriveDataTypeable+        DeriveGeneric         DeriveTraversable+        EmptyCase+        FlexibleContexts+        FlexibleInstances         GeneralizedNewtypeDeriving         ImportQualifiedPost         LambdaCase         ScopedTypeVariables+        TypeOperators+        TypeSynonymInstances         ViewPatterns  library@@ -42,6 +49,7 @@     exposed-modules:         Toml         Toml.FromValue+        Toml.FromValue.Generic         Toml.FromValue.Matcher         Toml.Lexer         Toml.Lexer.Token@@ -53,6 +61,7 @@         Toml.Pretty         Toml.Semantics         Toml.ToValue+        Toml.ToValue.Generic         Toml.Value     build-depends:         array           ^>= 0.5,@@ -70,6 +79,10 @@     type:               exitcode-stdio-1.0     hs-source-dirs:     test     main-is:            Main.hs+    default-extensions:+        QuasiQuotes+    build-tool-depends:+        hspec-discover:hspec-discover == 2.*     build-depends:         base,         containers,@@ -78,4 +91,9 @@         time,         toml-parser,     other-modules:+        DecodeSpec+        LexerSpec+        PrettySpec         QuoteStr+        TomlSpec+        ToValueSpec