packages feed

jsonlogic-hs-0.1.0.0: src/JsonLogic/Core/Errors.hs

{-# LANGUAGE OverloadedStrings #-}

-- |
-- Module      : JsonLogic.Core.Errors
--
-- The closed error vocabulary, layered by stage: reading a rule can fail
-- ('ParseError'), evaluating one can fail ('EvalError'), and 'Error' is
-- either, together with the 'Location' in the rule where it happened.
--
-- Callers match on the constructors, never on rendered text. 'renderError'
-- produces a message for people and logs; its wording may change in any
-- release.
module JsonLogic.Core.Errors
  ( Arity (..)
  , Error (..)
  , EvalError (..)
  , LimitKind (..)
  , OperatorsError (..)
  , ParseError (..)
  , errorLocation
  , renderError
  , renderEvalError
  , renderParseError
  ) where

import Data.Text (Text)
import qualified Data.Text as Text

import JsonLogic.Core.Location (Location, renderLocation)
import JsonLogic.Core.Operator (OperatorName (..))

-- | How many arguments an operator accepts.
data Arity
  = Exactly Int
  | AtLeast Int
  | Between Int Int
  deriving (Eq, Show)

-- | Why a JSON value could not be read as a rule.
data ParseError
  = -- | The rule, or a literal inside it, nests deeper than the configured
    -- limit (carried here). Raised before evaluation ever sees the rule.
    DepthExceeded Int
  | -- | An operator was given a number of arguments it does not take:
    -- the operator, what it accepts, and how many it was given. Raised in
    -- strict arity mode only.
    ArityMismatch OperatorName Arity Int
  deriving (Eq, Show)

-- | Which resource limit stopped an evaluation, with the configured
-- limit. The limits bound work and memory, not wall-clock time.
data LimitKind
  = -- | More evaluation steps than allowed (see @maxSteps@ in "JsonLogic.Core.Config"
    -- for what a step is).
    StepLimit Int
  | -- | A string result longer than allowed, in UTF-16 code units.
    StringLimit Int
  | -- | An array result, or an array to iterate, longer than allowed.
    ArrayLimit Int
  | -- | A value a custom operator returned nests deeper than allowed.
    ValueDepthLimit Int
  deriving (Eq, Show)

-- | Why a well-formed rule could not be evaluated. JavaScript semantics
-- turn most odd inputs into @NaN@ or @null@ rather than a failure, so only
-- an operator nobody supplied, the few operands the reference itself throws
-- on, a custom operator's own failure, and an exhausted limit can go wrong.
data EvalError
  = -- | The rule reached an operator that is neither builtin nor supplied.
    UnknownOperator OperatorName
  | -- | A supplied operator reported failure, with its reason.
    CustomOperatorFailed OperatorName Text
  | -- | An operand the reference implementation throws a @TypeError@ on:
    -- @missing_some@ with a @null@ or missing option list, and @*@ with no
    -- operands at all (reached in lenient arity mode only; strict mode
    -- rejects it as an 'ArityMismatch' before evaluating).
    InvalidOperand OperatorName
  | -- | A configured resource limit was exceeded.
    LimitExceeded LimitKind
  deriving (Eq, Show)

-- | Why a registry of custom operators was refused. Raised when the
-- registry is built, before any rule is read.
data OperatorsError
  = -- | The name belongs to a builtin operator, which would shadow it.
    ReservedOperatorName OperatorName
  | -- | The name was given more than once.
    DuplicateOperatorName OperatorName
  deriving (Eq, Show)

-- | A failure of either stage, and where in the rule it happened.
data Error
  = -- | The rule could not be read. The location is the node that could not
    -- be read: the operator with the wrong argument count, or the first
    -- value that nests too deeply.
    ParseFailure Location ParseError
  | -- | The rule could not be evaluated. The location is the node being
    -- evaluated when it failed: the unknown or failing operator, the
    -- operator given an invalid operand, or the node that ran out of a
    -- resource.
    EvalFailure Location EvalError
  deriving (Eq, Show)

-- | Where an error happened.
errorLocation :: Error -> Location
errorLocation (ParseFailure location _) = location
errorLocation (EvalFailure location _) = location

-- | A one-line description of an error and its location, for logs and
-- messages.
--
-- > at /if/1: operator "*" takes at least 1 argument, but was given 0
renderError :: Error -> Text
renderError e = case e of
  ParseFailure location problem -> at location (renderParseError problem)
  EvalFailure location problem -> at location (renderEvalError problem)
  where
    at location message = case renderLocation location of
      "" -> "at the root of the rule: " <> message
      pointer -> "at " <> pointer <> ": " <> message

-- | A one-line description of a parse error, without its location.
renderParseError :: ParseError -> Text
renderParseError (DepthExceeded limit) =
  "the rule nests deeper than the limit of " <> shown limit
renderParseError (ArityMismatch (OperatorName name) accepted given) =
  "operator " <> quoted name <> " takes " <> renderArity accepted <> ", but was given " <> shown given

-- | A one-line description of an evaluation error, without its location.
renderEvalError :: EvalError -> Text
renderEvalError (UnknownOperator (OperatorName name)) =
  "unknown operator " <> quoted name
renderEvalError (CustomOperatorFailed (OperatorName name) reason) =
  "custom operator " <> quoted name <> " failed: " <> reason
renderEvalError (InvalidOperand (OperatorName name)) =
  "operator " <> quoted name <> " was given an operand it cannot work with"
renderEvalError (LimitExceeded kind) = case kind of
  StepLimit n -> "evaluation exceeded the limit of " <> shown n <> " steps"
  StringLimit n -> "a string result exceeded the limit of " <> shown n <> " code units"
  ArrayLimit n -> "an array exceeded the limit of " <> shown n <> " elements"
  ValueDepthLimit n -> "a value nests deeper than the limit of " <> shown n

renderArity :: Arity -> Text
renderArity (Exactly n) = "exactly " <> shown n <> plural n
renderArity (AtLeast n) = "at least " <> shown n <> plural n
renderArity (Between low high) = "between " <> shown low <> " and " <> shown high <> " arguments"

plural :: Int -> Text
plural 1 = " argument"
plural _ = " arguments"

shown :: Int -> Text
shown = Text.pack . show

quoted :: Text -> Text
quoted name = "\"" <> name <> "\""