jsonlogic-hs-0.1.0.0: src/JsonLogic/Core/Parse.hs
{-# LANGUAGE LambdaCase #-}
-- |
-- Module : JsonLogic.Core.Parse
--
-- Reads a JSON 'Value' into a 'Rule'. Reading is an unfold
-- ('unfoldFixM'): a seed is a piece of JSON still to read, with how deeply it
-- is nested and where it is, and each step produces one node whose children
-- are further seeds.
--
-- What counts as a rule follows the reference implementation: an object with
-- exactly one key is an operator call, an array is read element by element,
-- and anything else (including an object with zero or several keys) is a
-- literal.
--
-- __Arity.__ The reference ignores surplus operands and treats missing ones as
-- JavaScript @undefined@. In 'Strict' mode (the default) a wrong count is a
-- precise 'ArityMismatch' instead. In 'Lenient' mode the count is repaired
-- here, exactly as the reference behaves: missing operands become
-- @undefined@ (@null@ for the initial value of @reduce@), and surplus ones are
-- dropped, after being evaluated for their failures where the reference
-- evaluates them ('Discard'). How each builtin treats its operands is stated
-- once, in @shapeOf@.
module JsonLogic.Core.Parse
( parseRule
, parseRuleWith
) where
import Data.Aeson (Value (..))
import qualified Data.Aeson.Key as Key
import qualified Data.Aeson.KeyMap as KeyMap
import Data.Fix (unfoldFixM)
import Data.Foldable (toList)
import Data.Text (Text)
import JsonLogic.Core.Config (ArityMode (..), Config (..), defaultConfig)
import JsonLogic.Core.Errors (Arity (..), ParseError (..))
import JsonLogic.Core.Location (Location, Segment (..), child, root)
import JsonLogic.Core.Operator
( Builtin (..)
, OperatorName (..)
, builtinOperatorName
, lookupBuiltin
)
import JsonLogic.Core.Rule (Node (..), Rule, RuleF (..))
import JsonLogic.Core.Value (nestsDeeperThan)
-- | Read a rule with 'defaultConfig'.
parseRule :: Value -> Either (Location, ParseError) Rule
parseRule = parseRuleWith defaultConfig
-- | Read a rule with the configured depth limit and arity mode. A failure
-- says where in the rule it happened.
parseRuleWith :: Config -> Value -> Either (Location, ParseError) Rule
parseRuleWith config value = unfoldFixM (step config) (Seed 0 root (Json value))
-- | Something still to read: how many levels deep it is, where it is, and
-- what it is.
data Seed = Seed !Int !Location !Source
data Source
= -- | JSON from the rule.
Json Value
| -- | An operand the rule leaves out, which the reference reads as
-- @undefined@ or, in two places, @null@.
Absent Absence
| -- | An operator call whose operand count has already been repaired.
Repaired Builtin [Seed]
data Absence = AbsentUndefined | AbsentNull
-- | One layer of the unfold.
step :: Config -> Seed -> Either (Location, ParseError) (Node Seed)
step config (Seed depth location source)
| depth > limit = Left (location, DepthExceeded limit)
| otherwise = Node location <$> layer
where
limit = maxDepth config
layer = case source of
Json (Array items) ->
Right (Arr [Seed (depth + 1) (child location (Index i)) (Json item) | (i, item) <- zip [0 ..] (toList items)])
Json value@(Object fields) -> case KeyMap.toList fields of
[(key, operands)] -> operator config depth location (Key.toText key) operands
_ -> literal value
Json value -> literal value
-- @{"and": []}@ is how the reference's @undefined@ is written as a rule
Absent AbsentUndefined -> Right (And [])
Absent AbsentNull -> Right (Lit Null)
Repaired builtin operands -> build location builtin operands
-- A literal's own nesting counts towards the limit, so a deeply nested
-- object cannot be smuggled past it inside a rule.
literal value
| nestsDeeperThan (limit - depth) value = Left (location, DepthExceeded limit)
| otherwise = Right (Lit value)
-- | An operator call: a builtin, read to its shape, or a custom operator.
operator :: Config -> Int -> Location -> Text -> Value -> Either (Location, ParseError) (RuleF Seed)
operator config depth location name operands = case lookupBuiltin name of
Nothing -> Right (CustomOp (OperatorName name) seeds)
Just builtin -> case arityMode config of
Strict
| admits (shapeArity shape) (length seeds) -> build location builtin (padded depth location builtin seeds)
| otherwise -> Left (location, ArityMismatch (builtinOperatorName builtin) (shapeArity shape) (length seeds))
where
shape = shapeOf builtin
Lenient -> lenient depth location builtin seeds
where
seeds = case operands of
Array items ->
[Seed (depth + 1) (child here (Index i)) (Json item) | (i, item) <- zip [0 ..] (toList items)]
-- a bare operand is shorthand for a list of one
other -> [Seed (depth + 1) here (Json other)]
here = child location (Key name)
-- | Repair the operand count as the reference would, and build the call.
lenient :: Int -> Location -> Builtin -> [Seed] -> Either (Location, ParseError) (RuleF Seed)
lenient depth location builtin seeds = case maximumOf (shapeArity shape) of
Just most
| length full > most -> case shapeSurplus shape of
NeverEvaluated -> build location builtin (take most full)
Evaluated -> Right (Discard (Seed depth location (Repaired builtin (take most full))) (drop most full))
_ -> build location builtin full
where
shape = shapeOf builtin :: Shape Seed
full = padded depth location builtin seeds
-- | The operands, followed by the ones the reference reads by position but
-- the rule leaves out, as absent values. There is no JSON for those, so they
-- are given the location of the operator itself.
padded :: Int -> Location -> Builtin -> [Seed] -> [Seed]
padded depth location builtin seeds =
seeds
++ [ Seed (depth + 1) location (Absent (absence builtin i))
| i <- [length seeds .. shapePositional (shapeOf builtin :: Shape Seed) - 1]
]
-- | What the reference reads for a missing operand: @undefined@, except
-- that a missing @var@ path is the data itself (the classic suite requires
-- @{"var": []}@ to mean the data, as @{"var": null}@ does), and the missing
-- initial value of @reduce@ is @null@.
absence :: Builtin -> Int -> Absence
absence BVar 0 = AbsentNull
absence BReduce 2 = AbsentNull
absence _ _ = AbsentUndefined
-- | Build a call whose operand count the shape admits.
build :: Location -> Builtin -> [Seed] -> Either (Location, ParseError) (RuleF Seed)
build location builtin seeds = case shapeBuild shape seeds of
Just node -> Right node
Nothing -> Left (location, ArityMismatch (builtinOperatorName builtin) (shapeArity shape) (length seeds))
where
shape = shapeOf builtin
-- ---------------------------------------------------------------------
-- How each builtin takes its operands
-- ---------------------------------------------------------------------
-- | How an operator takes its operands, building nodes with children of
-- type @a@.
data Shape a = Shape
{ shapeArity :: Arity
-- ^ The operand counts strict mode accepts.
, shapePositional :: Int
-- ^ How many leading operands the reference reads by position, so that a
-- missing one is read as absent ('absence').
, shapeSurplus :: Surplus
-- ^ Whether the reference evaluates operands past the most it uses.
, shapeBuild :: [a] -> Maybe (RuleF a)
-- ^ The node, if the count is one the constructor can hold.
}
data Surplus
= -- | Every operand is evaluated before the operator runs.
Evaluated
| -- | The operator evaluates only the operands it uses (@map@ and the
-- other operators with a body).
NeverEvaluated
-- | The single statement of every builtin's operands. The evaluator follows
-- it; the outlier suite checks it against the reference in both modes.
shapeOf :: Builtin -> Shape a
shapeOf builtin = case builtin of
BVar -> Shape (Between 0 2) 1 Evaluated (oneOrTwo Var)
BMissing -> variadic Missing
BMissingSome -> exactlyTwo MissingSome
BIf -> variadic If
BTernary -> variadic If
BAnd -> variadic And
BOr -> variadic Or
BNot -> exactlyOne Not
BDoubleNot -> exactlyOne DoubleNot
BLooseEq -> exactlyTwo LooseEq
BStrictEq -> exactlyTwo StrictEq
BLooseNeq -> exactlyTwo LooseNeq
BStrictNeq -> exactlyTwo StrictNeq
BLt -> twoOrThree Lt
BLte -> twoOrThree Lte
BGt -> exactlyTwo Gt
BGte -> exactlyTwo Gte
BMax -> variadic Max
BMin -> variadic Min
BAdd -> variadic Add
BSub -> Shape (Between 1 2) 1 Evaluated (oneOrTwo Sub)
-- strict mode refuses an empty @*@; lenient mode keeps it, and it throws
-- if it is ever evaluated, as in the reference
BMul -> Shape (AtLeast 1) 0 Evaluated (Just . Mul)
BDiv -> exactlyTwo Div
BMod -> exactlyTwo Mod
BMap -> lazily (exactlyTwo Map)
BFilter -> lazily (exactlyTwo Filter)
BReduce -> lazily (exactlyThree Reduce)
BAll -> lazily (exactlyTwo All)
BNone -> lazily (exactlyTwo None)
BSome -> lazily (exactlyTwo Some)
BMerge -> variadic Merge
BIn -> exactlyTwo In
BCat -> variadic Cat
BSubstr -> twoOrThree Substr
BLog -> exactlyOne Log
where
variadic make = Shape (AtLeast 0) 0 Evaluated (Just . make)
exactlyOne make = Shape (Exactly 1) 1 Evaluated $ \case
[x] -> Just (make x)
_ -> Nothing
exactlyTwo make = Shape (Exactly 2) 2 Evaluated $ \case
[x, y] -> Just (make x y)
_ -> Nothing
exactlyThree make = Shape (Exactly 3) 3 Evaluated $ \case
[x, y, z] -> Just (make x y z)
_ -> Nothing
oneOrTwo make = \case
[x] -> Just (make x Nothing)
[x, y] -> Just (make x (Just y))
_ -> Nothing
twoOrThree make = Shape (Between 2 3) 2 Evaluated $ \case
[x, y] -> Just (make x y Nothing)
[x, y, z] -> Just (make x y (Just z))
_ -> Nothing
lazily shape = shape{shapeSurplus = NeverEvaluated}
admits :: Arity -> Int -> Bool
admits (Exactly n) given = given == n
admits (AtLeast n) given = given >= n
admits (Between low high) given = low <= given && given <= high
maximumOf :: Arity -> Maybe Int
maximumOf (Exactly n) = Just n
maximumOf (AtLeast _) = Nothing
maximumOf (Between _ high) = Just high