packages feed

kdl-hs-1.0.0: 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 (..),
  runDecodeM,
  decodeThrow,
  failM,
  makeFatal,
  makeNonFatal,
  addContext,
) where

import Control.Applicative (Alternative (..))
import Data.Text (Text)
import KDL.Decoder.Internal.Error

-- | The monad that returns either a 'DecodeError' or a result of type @a@.
--
-- To a first approximation, this monad is equivalent to the @Either DecodeError@
-- monad, with the following changes:
--
--   * Uses continuation-passing style for performance
--   * Has two error channels, one for fatal errors and one for non-fatal errors (see 'makeFatal')
--   * Collects as many errors as possible, within an Applicative context
data DecodeM a
  = DecodeM
      ( forall r.
        (DecodeError -> r) -> -- fatal error, not handled by <|>
        (DecodeError -> r) -> -- non-fatal error, handled by <|>
        (a -> r) ->
        r
      )

instance Functor DecodeM where
  fmap f (DecodeM k) = DecodeM $ \onFatal onFail onSuccess -> k onFatal onFail (onSuccess . f)
instance Applicative DecodeM where
  pure x = DecodeM $ \_ _ onSuccess -> onSuccess x
  DecodeM kf <*> DecodeM ka = DecodeM $ \onFatal onFail onSuccess ->
    -- Collect all errors
    kf
      (\e1 -> ka (\e2 -> onFatal $ e1 <> e2) (\e2 -> onFatal $ e1 <> e2) (\_ -> onFatal e1))
      (\e1 -> ka (\e2 -> onFatal $ e1 <> e2) (\e2 -> onFail $ e1 <> e2) (\_ -> onFail e1))
      (\f -> ka onFatal onFail (onSuccess . f))
instance Monad DecodeM where
  (>>) = (*>)
  DecodeM ka >>= k = DecodeM $ \onFatal onFail onSuccess ->
    ka onFatal onFail $ \a -> let DecodeM kb = k a in kb onFatal onFail onSuccess
instance Alternative DecodeM where
  empty = DecodeM $ \_ onFail _ -> onFail mempty
  DecodeM k1 <|> DecodeM k2 = DecodeM $ \onFatal onFail onSuccess ->
    k1
      onFatal
      (\e1 -> k2 onFatal (\e2 -> onFail $ e1 <> e2) onSuccess)
      onSuccess

-- | Run a 'DecodeM' action and return the result or the error.
runDecodeM :: DecodeM a -> Either DecodeError a
runDecodeM (DecodeM f) = f Left Left Right

-- | Throw an error.
--
-- This error is non-fatal and can be handled by '<|>'. See 'makeFatal'
-- for more information.
decodeThrow :: BaseDecodeError -> DecodeM a
decodeThrow e = DecodeM $ \_ onFail _ -> onFail $ DecodeError Nothing [([], e)]

-- | Throw a 'DecodeError_Custom' error.
failM :: Text -> DecodeM a
failM = decodeThrow . DecodeError_Custom

-- | Make all errors in the given action fatal errors.
--
-- A la standard parsing libraries like megaparsec, errors should be
-- considered fatal when decoding has started consuming something.
makeFatal :: DecodeM a -> DecodeM a
makeFatal (DecodeM f) = DecodeM $ \onFatal _ onSuccess -> f onFatal onFatal onSuccess

-- | Make all errors non-fatal errors.
makeNonFatal :: DecodeM a -> DecodeM a
makeNonFatal (DecodeM f) = DecodeM $ \_ onFail onSuccess -> f onFail onFail onSuccess

-- | Add context to all errors that occur in the given action.
addContext :: ContextItem -> DecodeM a -> DecodeM a
addContext ctxItem (DecodeM f) = DecodeM $ \onFatal onFail onSuccess -> f (onFatal . addCtx) (onFail . addCtx) onSuccess
 where
  addCtx e = e{errors = [(ctxItem : ctx, msg) | (ctx, msg) <- e.errors]}