packages feed

hq-json-0.1.1: test/HQ/Transformation/ParserSpec.hs

{-# LANGUAGE ImportQualifiedPost #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE NoImplicitPrelude #-}

module HQ.Transformation.ParserSpec (spec) where

import Data.Aeson (Value (..))
import Data.Aeson.KeyMap qualified as KeyMap
import HQ.Transformation
import HQ.Transformation.Parser (parseTransformation)
import Relude hiding (Compose, Const, many, some)
import Test.Syd

spec :: Spec
spec = describe "HQ.Transformation.Parser" $ do
  it "parses arithmetic" $ forM_ arithmeticCases $ \(input, expected) ->
    parseTransformation input `shouldBe` Right expected

  it "parses strings and arrays" $ forM_ stringArrayCases $ \(input, expected) ->
    parseTransformation input `shouldBe` Right expected

  it "parses affixes and predicates" $ forM_ affixCases $ \(input, expected) ->
    parseTransformation input `shouldBe` Right expected

  it "parses constants" $ forM_ constantCases $ \(input, expected) ->
    parseTransformation input `shouldBe` Right expected

  it "parses booleans" $ forM_ booleanCases $ \(input, expected) ->
    parseTransformation input `shouldBe` Right expected

  it "parses composition and precedence" $ forM_ compositionCases $ \(input, expected) ->
    parseTransformation input `shouldBe` Right expected

  it "rejects invalid expressions" $ forM_ errorCases $ \input ->
    parseTransformation input `shouldSatisfy` isLeft

arithmeticCases :: [(Text, Transformation)]
arithmeticCases =
  [ ("+1", Add 1),
    ("*2", Multiply 2),
    ("-5", Subtract 5),
    ("/4", Divide 4),
    ("+0.5", Add 0.5),
    ("<5", Lt 5),
    ("<=5", Lte 5),
    (">5", Compose Not (Lte 5)),
    (">=5", Compose Not (Lt 5)),
    ("*1e10", Multiply 1e10)
  ]

stringArrayCases :: [(Text, Transformation)]
stringArrayCases =
  [ ("++ \" world\"", ConcatString " world"),
    ("concat [1,2]", ConcatArray [Number 1, Number 2]),
    ("trim", Trim),
    ("replace \"ab\" \"bc\"", Replace "ab" "bc"),
    ("concat[1,2]", ConcatArray [Number 1, Number 2])
  ]

affixCases :: [(Text, Transformation)]
affixCases =
  [ ("stripPrefix \"pre\"", StripPrefix "pre"),
    ("stripSuffix \"suf\"", StripSuffix "suf"),
    ("isPrefixOf \"pre\"", IsPrefixOf "pre"),
    ("isSuffixOf \"suf\"", IsSuffixOf "suf"),
    ("isInfixOf \"fix\"", IsInfixOf "fix"),
    ("isEmpty", IsEmpty),
    ("length", ArrayLength),
    ("reverse", ArrayReverse),
    ("unique", ArrayUnique),
    ("sort", ArraySort),
    ("merge {\"b\": 1}", Merge (KeyMap.fromList [("b", Number 1)])),
    ("deepMerge {\"a\": {\"y\": 2}}", DeepMerge (KeyMap.fromList [("a", Object (KeyMap.fromList [("y", Number 2)]))])),
    ("stripPrefix \"a\" . trim", Compose (StripPrefix "a") Trim)
  ]

constantCases :: [(Text, Transformation)]
constantCases =
  [ ("const 3", Const (Number 3)),
    ("const \"x\"", Const (String "x")),
    ("const 3 . +1", Compose (Const (Number 3)) (Add 1))
  ]

booleanCases :: [(Text, Transformation)]
booleanCases =
  [ ("== 3", Equal (Number 3)),
    ("= \"x\"", Equal (String "x")),
    ("not", Not),
    ("== 1 . not", Compose (Equal (Number 1)) Not),
    ("not . == 1", Compose Not (Equal (Number 1))),
    ("== 1 or == 2", Or (Equal (Number 1)) (Equal (Number 2))),
    ("== 1 || == 2", Or (Equal (Number 1)) (Equal (Number 2))),
    ("== 1 and == 1", And (Equal (Number 1)) (Equal (Number 1))),
    ("== 1 && == 1", And (Equal (Number 1)) (Equal (Number 1))),
    ("== 1 xor == 2", Xor (Equal (Number 1)) (Equal (Number 2))),
    ("== 1 ^^ == 2", Xor (Equal (Number 1)) (Equal (Number 2))),
    ("== 1 and == 1 or == 3", Or (And (Equal (Number 1)) (Equal (Number 1))) (Equal (Number 3))),
    ("== 1 or == 2 xor == 2", Or (Equal (Number 1)) (Xor (Equal (Number 2)) (Equal (Number 2)))),
    ("== 1 xor == 2 and == 2", Xor (Equal (Number 1)) (And (Equal (Number 2)) (Equal (Number 2))))
  ]

compositionCases :: [(Text, Transformation)]
compositionCases =
  [ ("+1 . trim", Compose (Add 1) Trim),
    ("+1 . == 3", Compose (Add 1) (Equal (Number 3))),
    ("+1 . == 3 or == 4", Or (Compose (Add 1) (Equal (Number 3))) (Equal (Number 4))),
    ("not . == 1 . == 3", Compose (Compose Not (Equal (Number 1))) (Equal (Number 3))),
    ("(== 1 or == 2) . == 3", Compose (Or (Equal (Number 1)) (Equal (Number 2))) (Equal (Number 3))),
    ("not . (== 1 or == 2)", Compose Not (Or (Equal (Number 1)) (Equal (Number 2))))
  ]

errorCases :: [Text]
errorCases =
  [ "merge [1]",
    "",
    "5",
    "+",
    "blah",
    "+ \"x\"",
    "replace 1 2",
    "stripPrefix 1",
    "isEmpty \"x\"",
    "concat 3",
    "trim xyz",
    "not == 1",
    "!",
    "const1",
    "sortOn",
    "android",
    "== 1 orange == 2"
  ]