packages feed

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

{-# LANGUAGE DeriveTraversable #-}

-- |
-- Module      : JsonLogic.Core.Rule
--
-- The rule tree: one layer of it ('RuleF', the base functor), and the
-- recursive tree whose every node records where in the rule's JSON it came
-- from ('Rule').
--
-- Recursion over the tree is never written by hand: the parser builds it
-- with an unfold ('Data.Fix.unfoldFixM') and the evaluator consumes it with a
-- fold ('Data.Fix.foldFix'), both from @data-fix@.
--
-- The builtin operators are a closed sum, matched exhaustively, so an
-- operator cannot be half-implemented. Operators supplied by the caller are
-- the one open extension point, 'CustomOp', whose behaviour is looked up when
-- the rule is compiled.
--
-- Constructors are shaped so that, where an operator takes a fixed number
-- of operands or a small range of them, a wrong count cannot be built
-- ('Lt' takes two operands and an optional third, for instance). The
-- operators that take any number of operands hold a list.
module JsonLogic.Core.Rule
  ( RuleF (..)
  , Node (..)
  , Rule
  , Term
  , stripLocations
  ) where

import Data.Aeson (Value)
import Data.Fix (Fix (..), foldFix)
import Data.Foldable (toList)
import Data.Functor (void)
import Data.Functor.Classes (Eq1 (..), Show1 (..))

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

-- | One layer of a JsonLogic rule, with its sub-rules at type @a@. Each
-- constructor's comment states what the reference implementation does, which
-- is what the evaluator implements.
data RuleF a
  = -- | A JSON scalar, or an object that is not an operator call (it has zero
    -- or several keys). Evaluates to itself.
    Lit Value
  | -- | A JSON array. The reference evaluates an array element by element,
    -- each element being a rule.
    Arr [a]
  | -- | @{"var": [path, default]}@. The path is a sub-rule, evaluated and
    -- then read as a dotted path or an index; the default, if given, is
    -- used when the lookup finds nothing.
    Var a (Maybe a)
  | -- | @{"missing": [k1, k2, ...]}@: the keys whose values are absent, or
    -- empty strings. If the first key evaluates to an array, that array is
    -- the key list.
    Missing [a]
  | -- | @{"missing_some": [need, keys]}@: nothing, if at least @need@ of the
    -- keys are present; otherwise the missing ones.
    MissingSome a a
  | -- | @{"if": [c1, t1, c2, t2, ..., else]}@ (also written @?:@): the
    -- consequent of the first truthy condition, else the final operand if the
    -- count is odd, else @null@. Only what is needed is evaluated.
    If [a]
  | -- | The first falsy operand, or the last operand; JavaScript @undefined@
    -- when there are none. Stops at the first falsy one.
    And [a]
  | -- | The first truthy operand, or the last operand; JavaScript
    -- @undefined@ when there are none. Stops at the first truthy one.
    Or [a]
  | -- | @!@: the negation of the operand's truthiness.
    Not a
  | -- | @!!@: the operand's truthiness.
    DoubleNot a
  | -- | @==@, JavaScript's loose equality.
    LooseEq a a
  | -- | @===@, JavaScript's strict equality.
    StrictEq a a
  | -- | @!=@.
    LooseNeq a a
  | -- | @!==@.
    StrictNeq a a
  | -- | @<@, or with a third operand the chained @a < b < c@ ("between").
    Lt a a (Maybe a)
  | -- | @<=@, or with a third operand the chained @a <= b <= c@.
    Lte a a (Maybe a)
  | -- | @>@, strictly binary.
    Gt a a
  | -- | @>=@, strictly binary.
    Gte a a
  | -- | @Math.max@ of the operands: @-Infinity@ (written @null@) for none.
    Max [a]
  | -- | @Math.min@ of the operands: @Infinity@ (written @null@) for none.
    Min [a]
  | -- | The sum of the operands read with @parseFloat@; @0@ for none.
    Add [a]
  | -- | @{"-": [x]}@ negates; @{"-": [x, y]}@ subtracts.
    Sub a (Maybe a)
  | -- | The product. The reference folds without a seed: one operand is
    -- returned as it is, and none at all throws (@InvalidOperand@
    -- when reached; strict mode refuses to read it).
    Mul [a]
  | -- | @/@, strictly binary.
    Div a a
  | -- | @%@, JavaScript's truncating remainder, strictly binary.
    Mod a a
  | -- | @{"map": [array, body]}@: the body evaluated against each element.
    Map a a
  | -- | @{"filter": [array, body]}@: the elements for which the body is
    -- truthy.
    Filter a a
  | -- | @{"reduce": [array, body, initial]}@: the body evaluated against
    -- @{"current": element, "accumulator": so far}@ for each element.
    Reduce a a a
  | -- | Whether the body is truthy for every element (false for none).
    All a a
  | -- | Whether the body is truthy for no element (true for none).
    None a a
  | -- | Whether the body is truthy for some element (false for none).
    Some a a
  | -- | The operands concatenated, arrays flattened by one level.
    Merge [a]
  | -- | @{"in": [needle, haystack]}@: substring of a string, or strict-equal
    -- element of an array.
    In a a
  | -- | The operands joined as strings.
    Cat [a]
  | -- | @{"substr": [string, start, length]}@, the length optional.
    Substr a a (Maybe a)
  | -- | @log@: its operand, unchanged. The reference also prints it; this
    -- library does no I/O.
    Log a
  | -- | @Discard inner extras@: the value of @inner@, after evaluating each
    -- of @extras@ for its failures only. Built in lenient arity mode only,
    -- for the surplus operands of an operator the reference evaluates in
    -- full before applying it, so that a surplus operand that fails still
    -- fails the rule, as in the reference.
    Discard a [a]
  | -- | An operator outside the builtin set, applied to its operands. Its
    -- behaviour is supplied by the caller.
    CustomOp OperatorName [a]
  deriving (Eq, Show, Functor, Foldable, Traversable)

-- | Two layers are equal when they have the same shape (constructor, fields
-- other than sub-rules, and number of sub-rules) and pairwise-equal
-- sub-rules. Written with 'Foldable' instead of derived by Template Haskell:
-- 'void' keeps the shape, 'toList' gives the sub-rules in order.
instance Eq1 RuleF where
  liftEq eq x y = void x == void y && and (zipWith eq (toList x) (toList y))

-- | The derived 'Show', with each sub-rule shown by the given function.
instance Show1 RuleF where
  liftShowsPrec showsChild _ d = showsPrec d . fmap (Shown . flip showsChild)

-- | A layer of the tree and where it came from.
data Node a = Node
  { nodeLocation :: !Location
  -- ^ The JSON Pointer of this node in the rule.
  , nodeRule :: !(RuleF a)
  -- ^ The node itself.
  }
  deriving (Eq, Show, Functor, Foldable, Traversable)

instance Eq1 Node where
  liftEq eq (Node a x) (Node b y) = a == b && liftEq eq x y

instance Show1 Node where
  liftShowsPrec showsChild _ d = showsPrec d . fmap (Shown . flip showsChild)

-- | A rule as read from JSON: every node carries its 'Location'.
type Rule = Fix Node

-- | A rule tree without locations, for comparing shapes.
type Term = Fix RuleF

-- | Forget where every node came from.
stripLocations :: Rule -> Term
stripLocations = foldFix (Fix . nodeRule)

-- | A value shown by a function, so that a derived 'Show' can be reused to
-- write 'Show1' instances.
newtype Shown = Shown (Int -> ShowS)

instance Show Shown where
  showsPrec d (Shown shows') = shows' d