htoml-megaparsec-2.1.0.3: internal/Text/Toml/Types.hs
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
module Text.Toml.Types
( Table
, emptyTable
, VTArray
, VArray
, Node (..)
, Explicitness (..)
, isExplicit
, insert
, throwParser
, Toml
, TomlM
, Parser
) where
import Control.Applicative (Alternative)
import Control.DeepSeq (NFData)
import Control.Monad (MonadPlus, join, when)
import Control.Monad.State (State)
import Control.Monad.State.Class (MonadState, get, modify)
import Control.Monad.Trans (lift)
import qualified Data.HashMap.Lazy as HM
import Data.Int (Int64)
import Data.List (intersect)
import Data.Semigroup
import qualified Data.Set as S
import qualified Data.Text as T
import Data.Time.Clock (UTCTime)
import Data.Time.Format ()
import Data.Vector (Vector)
import qualified Data.Vector as V
import Data.Void (Void)
import GHC.Generics (Generic)
import Text.Megaparsec hiding (State)
type Parser m a = (MonadState (S.Set [T.Text]) m) => ParsecT Void T.Text m a
type TomlM m = (MonadState (S.Set [T.Text]) m)
type Toml = State (S.Set [T.Text])
-- | The TOML 'Table' is a mapping ('HashMap') of 'Text' keys to 'Node' values.
type Table = HM.HashMap T.Text Node
-- | Contruct an empty 'Table'.
emptyTable :: Table
emptyTable = HM.empty
-- | An array of 'Table's, implemented using a 'Vector'.
type VTArray = Vector Table
-- | A \"value\" array that may contain zero or more 'Node's, implemented using a 'Vector'.
type VArray = Vector Node
-- | A 'Node' may contain any type of value that may be put in a 'VArray'.
data Node = VTable Table
| VTArray VTArray
| VString T.Text
| VInteger Int64
| VFloat Double
| VBoolean Bool
| VDatetime UTCTime
| VArray VArray
deriving (Show, Eq, Generic, NFData)
-- | To mark whether or not a 'Table' has been explicitly defined.
-- See: https://github.com/toml-lang/toml/issues/376
data Explicitness = Explicit | Implicit
-- | Convenience function to get a boolean value.
isExplicit :: Explicitness -> Bool
isExplicit Explicit = True
isExplicit Implicit = False
throwParser :: (MonadPlus m, Alternative m, Ord e, MonadParsec e s m) => String -> m a
throwParser x = fancyFailure $ S.fromList [ErrorFail x]
-- | Inserts a table, 'Table', with the namespaced name, '[Text]', (which
-- may be part of a table array) into a 'Table'.
-- It may result in an error in the 'ParsecT' monad for redefinitions.
insert :: (TomlM m) => Explicitness -> ([T.Text], Node) -> Table -> Parser m Table
insert _ ([], _) _ = throwParser "FATAL: Cannot call 'insert' without a name."
insert ex ([name], node) ttbl =
-- In case 'name' is final (a top-level name)
case HM.lookup name ttbl of
Nothing -> do when (isExplicit ex) $ updateExStateOrError [name] node
pure $ HM.insert name node ttbl
Just (VTable t) -> case node of
(VTable nt) -> case merge t nt of
Left ds -> nameInsertError ds name
Right r -> do when (isExplicit ex) $
updateExStateOrError [name] node
pure $ HM.insert name (VTable r) ttbl
_ -> commonInsertError node [name]
Just (VTArray a) -> case node of
(VTArray na) -> pure $ HM.insert name (VTArray $ a <> na) ttbl
_ -> commonInsertError node [name]
Just _ -> commonInsertError node [name]
insert ex (fullName@(name:ns), node) ttbl =
-- In case 'name' is not final (not a top-level name)
case HM.lookup name ttbl of
Nothing -> do
r <- insert Implicit (ns, node) emptyTable
when (isExplicit ex) $ updateExStateOrError fullName node
pure $ HM.insert name (VTable r) ttbl
Just (VTable t) -> do
r <- insert Implicit (ns, node) t
when (isExplicit ex) $ updateExStateOrError fullName node
pure $ HM.insert name (VTable r) ttbl
Just (VTArray a) ->
if V.null a
then throwParser "FATAL: Call to 'insert' found impossibly empty VArray."
else do r <- insert Implicit (ns, node) (V.last a)
pure $ HM.insert name (VTArray $ V.init a `V.snoc` r) ttbl
Just _ -> commonInsertError node fullName
-- FIXME use a Set here (?)
-- | Merge two tables, resulting in an error when overlapping keys are
-- found ('Left' will contain those keys). When no overlapping keys are
-- found the result will contain the union of both tables in a 'Right'.
merge :: Table -> Table -> Either [T.Text] Table
merge existing new = case HM.keys existing `intersect` HM.keys new of
[] -> Right $ HM.union existing new
ds -> Left ds
-- TOML tables maybe redefined when first definition was implicit.
-- For instance a top-level table `a` can implicitly defined by defining a non top-level
-- table `b` under it (namely with `[a.b]`). Once the table `a` is subsequently defined
-- explicitly (namely with `[a]`), it is then not possible to (re-)define it again.
-- A parser state of all explicitly defined tables is maintained, which allows
-- raising errors for illegal redefinitions of such.
updateExStateOrError :: (TomlM m) => [T.Text] -> Node -> Parser m ()
updateExStateOrError name node@(VTable _) = do
explicitlyDefinedNames <- lift get
let ns = explicitlyDefinedNames
when (S.member name ns) $ tableClashError name
updateExState name node
updateExStateOrError _ _ = pure ()
-- | Like 'updateExStateOrError' but does not raise errors. Only use this when sure
-- that redefinitions cannot occur.
updateExState :: (TomlM m) => [T.Text] -> Node -> Parser m ()
updateExState name (VTable _) = lift $ modify (S.insert name)
updateExState _ _ = pure ()
-- * Parse errors resulting from invalid TOML
-- | Key(s) redefintion error.
nameInsertError :: (TomlM m) => [T.Text] -> T.Text -> Parser m a
nameInsertError ns name = throwParser . T.unpack $ T.concat
[ "Cannot redefine key(s) (", T.intercalate ", " ns
, "), from table named '", name, "'." ]
-- | Table redefinition error.
tableClashError :: (TomlM m) => [T.Text] -> Parser m a
tableClashError name = throwParser . T.unpack $ T.concat
[ "Cannot redefine table named: '", T.intercalate "." name, "'." ]
-- | Common redefinition error.
commonInsertError :: (TomlM m) => Node -> [T.Text] -> Parser m a
commonInsertError what name = throwParser . join $
[ "Cannot insert ", w, " as '", n, "' since key already exists." ]
where
n = T.unpack $ T.intercalate "." name
w = case what of (VTable _) -> "tables"
_ -> "array of tables"