toml-parser-2.0.0.0: src/Toml/Semantics.hs
{-# OPTIONS_GHC -Wno-unrecognised-pragmas #-}
{-# HLINT ignore "Use section" #-}
{-|
Module : Toml.Semantics
Description : Semantic interpretation of raw TOML expressions
Copyright : (c) Eric Mertens, 2023
License : ISC
Maintainer : emertens@gmail.com
This module extracts a nested Map representation of a TOML
file. It detects invalid key assignments and resolves dotted
key assignments.
-}
module Toml.Semantics (
-- * Types
Value, Value'(..),
Table, Table'(..),
-- * Validation
semantics,
SemanticError(..), SemanticErrorKind(..),
-- * Annotations
forgetTableAnns,
forgetValueAnns,
valueAnn,
valueType,
) where
import Control.Monad (foldM)
import Data.List.NonEmpty (NonEmpty((:|)))
import Data.List.NonEmpty qualified as NonEmpty
import Data.Map (Map)
import Data.Map qualified as Map
import Data.Text (Text)
import Toml.Syntax.Types (SectionKind(..), Key, Val(..), Expr(..))
import Toml.Semantics.Types
-- | This type represents errors generated when resolving keys in a TOML
-- document.
--
-- @since 1.3.0.0
data SemanticError a = SemanticError {
errorAnn :: a, -- ^ Annotation associated with offending key
errorKey :: Text,
errorKind :: SemanticErrorKind
} deriving (
Read {- ^ Default instance -},
Show {- ^ Default instance -},
Eq {- ^ Default instance -},
Ord {- ^ Default instance -},
Functor, Foldable, Traversable)
-- | Enumeration of the kinds of conflicts a key can generate.
--
-- @since 1.3.0.0
data SemanticErrorKind
= AlreadyAssigned -- ^ Attempted to assign to a key that was already assigned
| ClosedTable -- ^ Attempted to open a table already closed
| ImplicitlyTable -- ^ Attempted to open a tables as an array of tables that was implicitly defined to be a table
deriving (
Read {- ^ Default instance -},
Show {- ^ Default instance -},
Eq {- ^ Default instance -},
Ord {- ^ Default instance -})
-- | Extracts a semantic value from a sequence of raw TOML expressions,
-- or reports a semantic error if one occurs.
semantics :: [Expr a] -> Either (SemanticError a) (Table' a)
semantics exprs =
do f <- foldM processExpr (flip assignKeyVals Map.empty) exprs
framesToTable <$> f []
where
processExpr f = \case
KeyValExpr k v -> Right (f . ((k,v):))
TableExpr k -> processSection TableKind k
ArrayTableExpr k -> processSection ArrayTableKind k
where
processSection kind k = flip (addSection kind k) <$> f []
-- | A top-level table used to distinguish top-level defined arrays
-- and tables from inline values.
type FrameTable a = Map Text (a, Frame a)
-- | M is the error-handling monad used through this module for
-- propagating semantic errors through the 'semantics' function.
type M a = Either (SemanticError a)
-- | Frames are the top-level skeleton of the TOML file that mirror the
-- subset of values that can be constructed with with top-level syntax.
-- TOML syntax makes a distinction between tables and arrays that are
-- defined at the top-level and those defined with inline syntax. This
-- separate type keeps these syntactic differences separate while table
-- and array resolution is still happening. Frames can keep track of which
-- tables finished and which are eligible for extension.
data Frame a
= FrameTable a FrameKind (FrameTable a)
| FrameArray (NonEmpty (a, FrameTable a)) -- stored in reverse order for easy "append"
| FrameValue (Value' a)
deriving Show
-- | Top-level tables can be in various states of completeness. This type
-- keeps track of the current state of a top-level defined table.
data FrameKind
= Open -- ^ table implicitly defined as super-table of [x.y.z]
| Dotted -- ^ table implicitly defined using dotted key assignment
| Closed -- ^ table closed to further extension
deriving Show
-- | Convert a top-level table "frame" representation into the plain Value
-- representation once the distinction is no longer needed.
framesToTable :: FrameTable a -> Table' a
framesToTable = fmap MkTable $ fmap $ fmap
\case
FrameTable a _kind t -> Table' a (framesToTable t)
FrameArray (NonEmpty.reverse -> t :| ts) ->
-- the array itself is attributed to the first table defined
List' (fst t) [Table' a (framesToTable x) | (a, x) <- t : ts]
FrameValue v -> v
-- | Attempts to insert the key-value pairs given into a new section
-- located at the given key-path in a frame map.
addSection ::
SectionKind {- ^ section kind -} ->
Key a {- ^ section key -} ->
[(Key a, Val a)] {- ^ values to install -} ->
FrameTable a {- ^ local frame map -} ->
M a (FrameTable a) {- ^ error message or updated local frame table -}
addSection kind (k :| []) kvs =
alterFrame k
-- defining a new table
(case kind of
TableKind -> FrameTable (fst k) Closed <$> go mempty
ArrayTableKind -> FrameArray . (:| []) . (,) (fst k) <$> go mempty)
\case
-- defining a super table of a previously defined sub-table
FrameTable _ Open t ->
case kind of
-- the annotation of the open table changes from the first mention closing key
TableKind -> FrameTable (fst k) Closed <$> go t
ArrayTableKind -> invalidKey k ImplicitlyTable
-- Add a new array element to an existing table array
FrameArray (t :| ts) ->
case kind of
TableKind -> invalidKey k ClosedTable
ArrayTableKind -> FrameArray . (:| t : ts) . (,) (fst k) <$> go mempty
-- failure cases
FrameTable _ Closed _ -> invalidKey k ClosedTable
FrameTable _ Dotted _ -> error "addSection: dotted table left unclosed"
FrameValue {} -> invalidKey k AlreadyAssigned
where
go = assignKeyVals kvs
addSection kind (k1 :| k2 : ks) kvs =
alterFrame k1
(FrameTable (fst k1) Open <$> go mempty)
\case
FrameTable a tk t -> FrameTable a tk <$> go t
FrameArray (t :| ts) -> FrameArray . (:| ts) <$> traverse go t
FrameValue _ -> invalidKey k1 AlreadyAssigned
where
go = addSection kind (k2 :| ks) kvs
-- | Close all of the tables that were implicitly defined with
-- dotted prefixes. These tables are only eligible for extension
-- within the @[table]@ section in which they were introduced.
closeDots :: FrameTable a -> FrameTable a
closeDots =
fmap $ fmap \case
FrameTable a Dotted t -> FrameTable a Closed (closeDots t)
frame -> frame
-- | Extend the given frame table with a list of key-value pairs.
-- Any tables created through dotted keys will be closed after
-- all of the key-value pairs are processed.
assignKeyVals :: [(Key a, Val a)] -> FrameTable a -> M a (FrameTable a)
assignKeyVals kvs t = closeDots <$> foldM f t kvs
where
f m (k,v) = assign k v m
-- | Assign a single dotted key in a frame. Any open table traversed
-- by a dotted key will be marked as dotted so that it will become
-- closed at the end of the current call to 'assignKeyVals'.
assign :: Key a -> Val a -> FrameTable a -> M a (FrameTable a)
assign (key :| []) val =
alterFrame key
(FrameValue <$> valToValue val)
(\_ -> invalidKey key AlreadyAssigned)
assign (key :| k1 : keys) val =
alterFrame key (go (fst key) mempty)
\case
FrameTable a Open t -> go a t
FrameTable a Dotted t -> go a t
FrameTable _ Closed _ -> invalidKey key ClosedTable
FrameArray _ -> invalidKey key ClosedTable
FrameValue _ -> invalidKey key AlreadyAssigned
where
go a t = FrameTable a Dotted <$> assign (k1 :| keys) val t
-- | Convert 'Val' to 'Value' potentially raising an error if
-- it contains inline tables with key-conflicts.
valToValue :: Val a -> M a (Value' a)
valToValue =
\case
ValInteger a x -> Right (Integer' a x)
ValFloat a x -> Right (Double' a x)
ValBool a x -> Right (Bool' a x)
ValString a x -> Right (Text' a x)
ValTimeOfDay a x -> Right (TimeOfDay' a x)
ValZonedTime a x -> Right (ZonedTime' a x)
ValLocalTime a x -> Right (LocalTime' a x)
ValDay a x -> Right (Day' a x)
ValArray a xs -> List' a <$> traverse valToValue xs
ValTable a kvs -> Table' a . framesToTable <$> assignKeyVals kvs mempty
-- | Abort validation by reporting an error about the given key.
invalidKey ::
(a, Text) {- ^ sub-key -} ->
SemanticErrorKind {- ^ error kind -} ->
M a b
invalidKey (a, key) kind = Left (SemanticError a key kind)
-- | Specialization of 'Map.alterF' used to adjust a location in a 'FrameTable'
alterFrame ::
(a, Text) {- ^ annotated key -} ->
M a (Frame a) {- ^ new value case -} ->
(Frame a -> M a (Frame a)) {- ^ update value case -} ->
FrameTable a -> M a (FrameTable a)
alterFrame (a, k) create update = Map.alterF g k
where
-- insert a new value
g Nothing =
do lf <- create
pure (Just (a, lf))
-- update an existing value and preserve its annotation
g (Just (op, ov)) =
do lf <- update ov
pure (Just (op, lf))