packages feed

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

-- |
-- Module      : JsonLogic.Core.Custom
--
-- Operators supplied by the caller: the one open extension point of the
-- rule language. A registry is built once, checked when it is built, and
-- handed to 'JsonLogic.compile', which resolves every custom operator in the
-- rule against it.
module JsonLogic.Core.Custom
  ( CustomOperator
  , Operators
  , noOperators
  , operatorsFromList
  , lookupOperator
  ) where

import Control.Monad (foldM)
import qualified Data.Aeson as Aeson
import qualified Data.Map.Strict as Map
import Data.Maybe (isJust)
import Data.Text (Text)

import JsonLogic.Core.Errors (OperatorsError (..))
import JsonLogic.Core.Operator (OperatorName (..), lookupBuiltin)

-- | A caller-supplied operator: given the data being evaluated against and
-- its already-evaluated arguments, produce a result or a reason it failed.
--
-- The contract, in full:
--
-- * All of the operator's operands are evaluated, left to right, before it
--   runs, as for the reference's own ordinary operators. An operator that
--   nobody supplied is therefore reported only after its operands have run,
--   and only if the rule reaches it.
-- * The data argument is the datum in scope: inside @map@, @filter@ and the
--   other iterating operators, the current element. It is converted to JSON
--   lazily, so an operator that ignores it pays nothing for it.
-- * Arguments and result are JSON. JavaScript values with no JSON form
--   cross the boundary as @null@: @NaN@, the infinities and @undefined@.
-- * The number of arguments is not checked by the library; the operator
--   checks its own.
-- * @Left reason@ stops the evaluation with
--   'JsonLogic.Core.Errors.CustomOperatorFailed'.
-- * The result is held to the configured depth and step limits; the
--   operator's own work is not (the library cannot see inside it).
type CustomOperator = Aeson.Value -> [Aeson.Value] -> Either Text Aeson.Value

-- | A registry of custom operators.
newtype Operators = Operators (Map.Map OperatorName CustomOperator)

-- | Registries combine left-biased: where both define a name, the left one
-- wins. Neither can hold a builtin name, so neither can the combination.
instance Semigroup Operators where
  Operators left <> Operators right = Operators (Map.union left right)

instance Monoid Operators where
  mempty = noOperators

-- | No custom operators: any operator outside the builtin set is an
-- 'JsonLogic.Core.Errors.UnknownOperator' when it is reached.
noOperators :: Operators
noOperators = Operators Map.empty

-- | Custom operators from an association list. A name that is a builtin
-- operator, or that appears twice, is refused rather than silently ignored.
operatorsFromList :: [(OperatorName, CustomOperator)] -> Either OperatorsError Operators
operatorsFromList entries = Operators <$> foldM add Map.empty entries
  where
    add table (name@(OperatorName text), implementation)
      | isJust (lookupBuiltin text) = Left (ReservedOperatorName name)
      | Map.member name table = Left (DuplicateOperatorName name)
      | otherwise = Right (Map.insert name implementation table)

-- | The operator registered under a name.
lookupOperator :: Operators -> OperatorName -> Maybe CustomOperator
lookupOperator (Operators table) name = Map.lookup name table