jsonlogic-hs-0.1.0.0: test/LocationSpec.hs
{-# LANGUAGE OverloadedStrings #-}
-- | Error locations are JSON Pointers into the rule: every one of them must
-- name a value that exists in the rule's JSON, and that value must be the
-- operator call (or literal) the error is about.
module LocationSpec (spec) where
import Data.Aeson (Value (..))
import qualified Data.Aeson.Key as Key
import qualified Data.Aeson.KeyMap as KeyMap
import Data.Foldable (toList)
import Data.Maybe (isJust)
import Test.Hspec
import Test.QuickCheck
import Gen (genData, genRule)
import JsonLogic
import Support (at, json)
-- | The value a pointer names, if it names one (RFC 6901 resolution, on
-- already-decoded segments).
resolve :: Location -> Value -> Maybe Value
resolve location = go (locationSegments location)
where
go [] v = Just v
go (Key k : rest) (Object fields) = KeyMap.lookup (Key.fromText k) fields >>= go rest
go (Index i : rest) (Array items) = case drop i (toList items) of
item : _ | i >= 0 -> go rest item
_ -> Nothing
go _ _ = Nothing
-- | For an operator error, the operator call the pointer names.
operatorAt :: Location -> Value -> Maybe OperatorName
operatorAt location rule = case resolve location rule of
Just (Object fields) | [(key, _)] <- KeyMap.toList fields -> Just (OperatorName (Key.toText key))
_ -> Nothing
-- | The operator an error is about, when it is about one.
operatorOf :: Error -> Maybe OperatorName
operatorOf e = case e of
ParseFailure _ (ArityMismatch name _ _) -> Just name
EvalFailure _ (UnknownOperator name) -> Just name
EvalFailure _ (CustomOperatorFailed name _) -> Just name
EvalFailure _ (InvalidOperand name) -> Just name
_ -> Nothing
modes :: [Config]
modes = [defaultConfig, defaultConfig{arityMode = Lenient}, defaultConfig{maxSteps = 25}, defaultConfig{maxDepth = 3}]
spec :: Spec
spec = describe "error locations" $ do
it "always name a value that exists in the rule" $
forAll (genRule 6) $ \rule -> forAll (genData 3) $ \dat -> forAll (elements modes) $ \config ->
case applyWithConfig config noOperators rule dat of
Left e -> counterexample (show e) (isJust (resolve (errorLocation e) rule))
Right _ -> property True
it "name the very operator an operator error is about" $
forAll (genRule 6) $ \rule -> forAll (genData 3) $ \dat -> forAll (elements modes) $ \config ->
case applyWithConfig config noOperators rule dat of
Left e | Just name <- operatorOf e -> counterexample (show e) (operatorAt (errorLocation e) rule === Just name)
_ -> property True
it "follow bare operands, which have no index" $
apply (json "{\"!\":{\"!\":{\"nope\":1}}}") Null
`shouldBe` Left (EvalFailure (at [Left "!", Left "!"]) (UnknownOperator (OperatorName "nope")))
it "point into literals' own arrays" $
apply (json "{\"merge\":[[{\"nope\":[]}]]}") Null
`shouldBe` Left (EvalFailure (at [Left "merge", Right 0, Right 0]) (UnknownOperator (OperatorName "nope")))
it "put an operand the rule leaves out at the operator call, which exists" $
-- five steps: the and, its true, the ==, its 1, and the undefined that
-- stands in for the missing second operand, which runs out
applyWithConfig defaultConfig{arityMode = Lenient, maxSteps = 4} noOperators (json "{\"and\":[true,{\"==\":[1]}]}") Null
`shouldBe` Left (EvalFailure (at [Left "and", Right 1]) (LimitExceeded (StepLimit 4)))