jsonlogic-hs-0.1.0.0: test/ValueSpec.hs
{-# LANGUAGE OverloadedStrings #-}
module ValueSpec (spec) where
import qualified Data.Aeson as Aeson
import qualified Data.Aeson.KeyMap as KeyMap
import Data.Text (Text)
import qualified Data.Text as Text
import Test.Hspec
import Test.QuickCheck (Gen, arbitrary, choose, elements, forAll, listOf, oneof, (==>))
import qualified Data.Vector as Vector
import JsonLogic.Core.Number (maximumJs, minimumJs, numberToString, remainderJs)
import JsonLogic.Core.Value
arr :: [Val] -> Val
arr = VArr . Vector.fromList
nan :: Double
nan = 0 / 0
infinity :: Double
infinity = 1 / 0
genScalar :: Gen Val
genScalar =
oneof
[ pure VNull
, pure VUndefined
, VBool <$> arbitrary
, VNum <$> oneof [elements [0, -0.0, 1, -1, 0.5, infinity, negate infinity, nan], arbitrary]
, VStr . Text.pack <$> listOf (elements "ab01 .-eE")
]
genVal :: Gen Val
genVal = oneof [genScalar, arr <$> listOf genScalar, pure (VObj KeyMap.empty)]
numberCases :: [(Double, Text)]
numberCases =
[ (1, "1")
, (-1, "-1")
, (1.5, "1.5")
, (100, "100")
, (0.5, "0.5")
, (0.000001, "0.000001")
, (1e-7, "1e-7")
, (1e21, "1e+21")
, (1e20, "100000000000000000000")
, (123.456, "123.456")
, (0.1 + 0.2, "0.30000000000000004")
, (-0.0, "0")
, (1.2345e25, "1.2345e+25")
, (5e-324, "5e-324")
, (2.2531e21, "2.2531e+21")
, (-2.2531e21, "-2.2531e+21")
, (9007199254740992, "9007199254740992")
, (nan, "NaN")
, (infinity, "Infinity")
, (negate infinity, "-Infinity")
]
toNumberCases :: [(Val, Double)]
toNumberCases =
[ (VStr "", 0)
, (VStr " 12 ", 12)
, (VStr "0x10", 16)
, (VStr "0b11", 3)
, (VStr "1e3", 1000)
, (VStr ".5", 0.5)
, (VStr "5.", 5)
, (VStr "-2.5", -2.5)
, (VNull, 0)
, (VBool True, 1)
, (arr [], 0)
, (arr [VNum 7], 7)
, (VStr "Infinity", infinity)
, (VStr "-Infinity", negate infinity)
, (VStr "-2.2531e+21", -2.2531e21)
, (VStr "2.2531e21", 2.2531e21)
, (VStr "1e400", infinity)
, (VStr "1e-400", 0)
, (VStr "5e-324", 5e-324)
, (VStr "123456789012345678901234567890", 1.2345678901234568e29)
]
parseFloatCases :: [(Val, Double)]
parseFloatCases =
[ (VStr "3.14abc", 3.14)
, (VStr " 7", 7)
, (VStr "1e3x", 1000)
, (VStr ".5", 0.5)
, (VStr "-12.5e1", -125)
, (arr [VNum 5], 5)
, (VNum (-0.0), 0)
, (VStr "Infinityx", infinity)
]
looseCases :: [(Val, Val, Bool)]
looseCases =
[ (VNum 1, VStr "1", True)
, (VNum 0, VStr "", True)
, (VNull, VNull, True)
, (VNull, VNum 0, False)
, (VNull, VBool False, False)
, (VBool True, VNum 1, True)
, (VBool False, VStr "0", True)
, (VStr "a", VStr "a", True)
, (VStr "abc", VNum 0, False)
, (arr [VNum 1], VStr "1", True)
, (arr [], VNum 0, True)
, (arr [], VBool False, True)
, (arr [], arr [], False)
, (VNum nan, VNum nan, False)
, (VUndefined, VNull, True)
, (VUndefined, VUndefined, True)
, (VUndefined, VNum 0, False)
, (VUndefined, VBool False, False)
]
lessCases :: [(Val, Val, Bool)]
lessCases =
[ (VNum 1, VNum 2, True)
, (VStr "10", VStr "9", True)
, (VStr "2", VNum 10, True)
, (VNull, VNum 1, True)
, (VNum nan, VNum 1, False)
, (VStr "a", VNum 1, False)
, (arr [VNum 1], arr [VNum 2], True)
]
remainderCases :: [(Double, Double, Double)]
remainderCases =
[ (5, 3, 2)
, (-5, 3, -2)
, (5, -3, 2)
, (5.5, 2, 1.5)
, (5, infinity, 5)
]
-- | Doubles across the whole range, including the large ones where digit
-- generation has edge cases.
genWideDouble :: Gen Double
genWideDouble = do
mantissa <- choose (-1000000000000000, 1000000000000000) :: Gen Integer
power <- choose (-300, 290) :: Gen Int
pure (fromInteger mantissa * 10 ^^ power)
roundTripsThroughJson :: Double -> Bool
roundTripsThroughJson d = fromValue (toValue (VNum d)) == VNum d
spec :: Spec
spec = describe "JsonLogic.Core.Value" $ do
describe "truthy" $ do
it "is false for exactly the reference's falsy values" $
map truthy [VNull, VUndefined, VBool False, VNum 0, VNum nan, VStr "", arr []]
`shouldBe` replicate 7 False
it "is true for everything else, including \"0\" and empty objects" $
map truthy [VBool True, VNum 1, VNum (-1), VStr "0", VStr "a", arr [VNull], VObj KeyMap.empty]
`shouldBe` replicate 7 True
describe "numberToString"
$ it "follows the ECMAScript Number-to-string rules"
$ map (numberToString . fst) numberCases `shouldBe` map snd numberCases
describe "toNumber" $ do
it "converts the way Number() does" $
map (toNumber . fst) toNumberCases `shouldBe` map snd toNumberCases
it "gives NaN for malformed input" $
map
(isNaN . toNumber)
[ VStr "abc"
, VStr "1 2"
, VStr "0x"
, VStr "--1"
, VStr "1e"
, VStr "+-1"
, VStr "infinity"
, VObj KeyMap.empty
, arr [VNum 1, VNum 2]
]
`shouldBe` replicate 9 True
describe "parseFloat" $ do
it "reads the longest decimal prefix" $
map (parseFloat . fst) parseFloatCases `shouldBe` map snd parseFloatCases
it "gives NaN when there is no number to read" $
map (isNaN . parseFloat) [VStr "abc", VStr "", VStr "x1", VNull, VBool True]
`shouldBe` replicate 5 True
describe "equality" $ do
it "looseEquals matches JavaScript's == on a table of cases" $
map (\(x, y, _) -> looseEquals x y) looseCases
`shouldBe` map (\(_, _, expected) -> expected) looseCases
it "strictEquals never converts types" $
[ strictEquals (VNum 1) (VStr "1")
, strictEquals (VNum nan) (VNum nan)
, strictEquals (VStr "a") (VStr "a")
, strictEquals (arr []) (arr [])
]
`shouldBe` [False, False, True, False]
describe "ordering" $ do
it "lessThan matches JavaScript's < on a table of cases" $
map (\(x, y, _) -> lessThan x y) lessCases
`shouldBe` map (\(_, _, expected) -> expected) lessCases
it "lessOrEqual compares strings lexicographically" $
[lessOrEqual (VNum 1) (VNum 1), lessOrEqual (VStr "b") (VStr "a")]
`shouldBe` [True, False]
describe "arithmetic helpers" $ do
it "remainderJs takes the sign of the dividend" $
map (\(x, y, _) -> remainderJs x y) remainderCases
`shouldBe` map (\(_, _, expected) -> expected) remainderCases
it "remainderJs gives NaN where JavaScript does" $
map (isNaN . uncurry remainderJs) [(5, 0), (infinity, 3), (nan, 1)]
`shouldBe` replicate 3 True
it "maximumJs and minimumJs follow Math.max and Math.min" $ do
(maximumJs [1, 3, 2], maximumJs [], minimumJs []) `shouldBe` (3, negate infinity, infinity)
isNaN (maximumJs [1, nan]) `shouldBe` True
describe "JSON conversion" $ do
it "turns NaN and infinities into null" $
map (toValue . VNum) [nan, infinity, negate infinity]
`shouldBe` replicate 3 Aeson.Null
it "keeps ordinary numbers and strings" $ do
toValue (VNum 2) `shouldBe` Aeson.Number 2
fromValue (Aeson.String "x") `shouldBe` VStr "x"
describe "laws" $ do
it "looseEquals is symmetric" $
forAll genVal $
\x -> forAll genVal $ \y -> looseEquals x y == looseEquals y x
it "strictEquals implies looseEquals" $
forAll genVal $
\x -> forAll genVal $ \y -> not (strictEquals x y) || looseEquals x y
it "lessThan is asymmetric" $
forAll genVal $
\x -> forAll genVal $ \y -> not (lessThan x y && lessThan y x)
it "numberToString round-trips through toNumber" $
forAll (arbitrary :: Gen Double) $ \d ->
not (isNaN d || isInfinite d) ==> toNumber (VStr (numberToString d)) == d
it "writing a double as JSON and reading it back is the identity" $
forAll genWideDouble roundTripsThroughJson
it "numberToString round-trips for large and tiny doubles too" $
forAll genWideDouble $
\d -> toNumber (VStr (numberToString d)) == d