packages feed

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"