{-# 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))