kdl-hs-1.2.1: src/KDL/Decoder/Internal/DecodeM.hs
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedRecordDot #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE NoFieldSelectors #-}
module KDL.Decoder.Internal.DecodeM (
-- * Decoding errors
module KDL.Decoder.Internal.Error,
-- * DecodeM monad
DecodeM (..),
DecodeHints,
runDecodeM,
decodeThrow,
failM,
mapErrors,
addContext,
discardHints,
) where
import Control.Applicative (Alternative (..))
import Data.Bifunctor (first)
import Data.Default (def)
import Data.List.NonEmpty (NonEmpty (..))
import Data.List.NonEmpty qualified as NonEmpty
import Data.Text (Text)
import KDL.Decoder.Internal.Error
import KDL.Decoder.Internal.Error qualified as Error
import KDL.Types (Span)
import Prelude hiding (span)
-- | The monad that returns either a 'DecodeError' or a result of type @a@.
data DecodeM a
= DecodeM_Found a DecodeHints
| DecodeM_Fail (NonEmpty BaseDecodeError)
-- | Hints to provide additional context in a future error after a successful
-- branch.
--
-- Take this motivating example: a node takes an arbitrary number of string
-- args. If you pass some strings then a number, it'll successfully parse up to
-- the number and return success, only for the node to fail later with
-- "unexpected argument: 123". But the true error was
-- "unexpected number, expected string".
type DecodeHints = [BaseDecodeError]
instance Functor DecodeM where
fmap f = \case
DecodeM_Found a es -> DecodeM_Found (f a) es
DecodeM_Fail es -> DecodeM_Fail es
instance Applicative DecodeM where
pure x = DecodeM_Found x mempty
l <*> r =
case (l, r) of
(DecodeM_Found f es1, DecodeM_Found a es2) -> DecodeM_Found (f a) (es1 <> es2)
(DecodeM_Found _ es1, DecodeM_Fail es2) -> DecodeM_Fail (mergeHintsL es1 es2)
(DecodeM_Fail es1, DecodeM_Found _ es2) -> DecodeM_Fail (mergeHintsR es1 es2)
(DecodeM_Fail es1, DecodeM_Fail es2) -> DecodeM_Fail (mergeErrors es1 es2)
instance Monad DecodeM where
(>>) = (*>)
m >>= k =
case m of
DecodeM_Fail es1 -> DecodeM_Fail es1
DecodeM_Found a es1 ->
case k a of
DecodeM_Found b es2 -> DecodeM_Found b (es1 <> es2)
DecodeM_Fail es2 -> DecodeM_Fail (mergeHintsL es1 es2)
instance Alternative DecodeM where
empty = failM "<empty>"
l <|> r =
case l of
DecodeM_Found a es1 -> DecodeM_Found a es1
DecodeM_Fail es1 ->
case r of
DecodeM_Found a es2 -> DecodeM_Found a (NonEmpty.toList es1 <> es2)
DecodeM_Fail es2 -> DecodeM_Fail (mergeErrors es1 es2)
-- | Run a 'DecodeM' action and return the result or the deepest error found.
runDecodeM :: DecodeM a -> Either DecodeError a
runDecodeM = \case
DecodeM_Found a _ -> Right a
DecodeM_Fail errors -> Left DecodeError{filepath = Nothing, errors}
{----- mergeErrors -----}
mergeErrors ::
NonEmpty BaseDecodeError ->
NonEmpty BaseDecodeError ->
NonEmpty BaseDecodeError
mergeErrors es1 es2 =
case compare (key es1) (key es2) of
LT -> es2
EQ -> es1 <> es2
GT -> es1
where
key = length . (.path) . fst . NonEmpty.head
mergeHintsL ::
DecodeHints ->
NonEmpty BaseDecodeError ->
NonEmpty BaseDecodeError
mergeHintsL l r = maybe r (\l' -> mergeErrors l' r) (NonEmpty.nonEmpty l)
mergeHintsR ::
NonEmpty BaseDecodeError ->
DecodeHints ->
NonEmpty BaseDecodeError
mergeHintsR l r = maybe l (\r' -> mergeErrors l r') (NonEmpty.nonEmpty r)
{----- DecodeM operations -----}
mapErrors :: (BaseDecodeError -> BaseDecodeError) -> DecodeM a -> DecodeM a
mapErrors f = \case
DecodeM_Found a es -> DecodeM_Found a (map f es)
DecodeM_Fail es -> DecodeM_Fail (fmap f es)
mapErrorContext :: (Error.Context -> Error.Context) -> DecodeM a -> DecodeM a
mapErrorContext f = mapErrors (first f)
-- | Throw an error.
decodeThrow :: DecodeErrorKind -> DecodeM a
decodeThrow e = DecodeM_Fail . NonEmpty.singleton $ (def, e)
-- | Throw a 'DecodeError_Custom' error.
failM :: Text -> DecodeM a
failM = decodeThrow . DecodeError_Custom
-- | Add context to all errors that occur in the given action.
addContext :: Span -> ContextItem -> DecodeM a -> DecodeM a
addContext span ctxItem = mapErrorContext $ \ctx ->
ctx
{ path = ctxItem : ctx.path
, -- Span should only be attached to the nearest context; i.e. the first
-- addContext that runs
span = ctx.span <|> span'
}
where
-- Ignore span if it's empty
span' = if span == def then Nothing else Just span
-- | Discard hints after validating that an error context is successful.
discardHints :: DecodeM a -> DecodeM a
discardHints = \case
DecodeM_Found a _ -> DecodeM_Found a mempty
DecodeM_Fail es -> DecodeM_Fail es