packages feed

hslua-aeson-2.3.1: test/test-hslua-aeson.hs

{-# OPTIONS_GHC -fno-warn-orphans #-}
{-|
Copyright   :  © 2017-2024 Albert Krewinkel
License     :  MIT

Tests for Aeson–Lua glue.
-}
import Control.Monad (when)
import Data.Aeson (ToJSON, object, (.=))
import Data.Scientific (Scientific, fromFloatDigits, toRealFloat)
import Data.Text (Text)
import HsLua.Core as Lua
import HsLua.Marshalling
import HsLua.Aeson
import Test.QuickCheck.Monadic (assert)
import Test.Tasty (TestTree, defaultMain, testGroup)
import Test.Tasty.HUnit ((@?=), testCase)
import Test.Tasty.QuickCheck
import Test.QuickCheck.Instances ()

import qualified Data.Aeson as Aeson
import qualified Data.Vector as Vector
import qualified Test.QuickCheck.Monadic as QC

#if MIN_VERSION_aeson(2,0,0)
import qualified Data.Aeson.KeyMap as KeyMap
#if !MIN_VERSION_aeson(2,0,3)
import Data.Aeson.Key (Key, fromText)
#endif
#else
import qualified Data.HashMap.Strict as KeyMap
#endif


-- | Run this spec.
main :: IO ()
main = defaultMain tests

-- | Aeson tests
tests :: TestTree
tests = testGroup "hslua-aeson"
  [ testGroup "Value"
    [ testProperty "can be round-tripped through the stack" $
      assertRoundtripEqual pushValue peekValue . numbersToDoubles
    , testProperty "can roundtrip a bool nested in 50 layers of arrays" $
      \b -> QC.monadicIO $ do
        let go _ x = Aeson.Array $ Vector.fromList [x]
            mkValue a = foldr go (Aeson.Bool a) [(1::Int) .. 50]
        x <- QC.run . run @Lua.Exception $ do
          pushValue $ mkValue b
          forcePeek $ peekValue top
        return (x === mkValue b)
    , testProperty "can roundtrip a bool nested in 50 layers of objects" $
      \b -> QC.monadicIO $ do
        let go _ x = Aeson.Object $ KeyMap.fromList [("x", x)]
            mkValue a = foldr go (Aeson.Bool a) [(1::Int) .. 50]
        x <- QC.run . run @Lua.Exception $ do
          pushValue $ mkValue b
          forcePeek $ peekValue top
        return (x === mkValue b)
    , testProperty "can roundtrip a null nested in 50 layers of objects" $
      \() -> QC.monadicIO $ do
        let go _ x = Aeson.Object $ KeyMap.fromList [("x", x)]
            mkValue = foldr go Aeson.Null [(1::Int) .. 50]
        x <- QC.run . run @Lua.Exception $ do
          pushValue mkValue
          forcePeek $ peekValue top
        return (x === mkValue)
    ]

  , testGroup "via JSON"
    [ testProperty "can roundtrip 'Maybe Text' via JSON" $
      assertRoundtripEqual @(Maybe Int) pushViaJSON peekViaJSON
    , testProperty "can roundtrip '(Int, Float)' via JSON" $
      assertRoundtripEqual @(Int, Float) pushViaJSON peekViaJSON
    , testProperty "can roundtrip 'Either Bool Text' via JSON" $
      assertRoundtripEqual @(Either Bool Text) pushViaJSON peekViaJSON
    ]

  , testGroup "special encodings"
    [ testGroup "__toaeson"
      [ testCase "respect __toaeson metamethod" . run @Lua.Exception $ do
          pushTwentyThree TwentyThree
          val <- forcePeek $ peekValue top
          liftIO $ object [ "title" .= (23 :: Int) ] @?= val
      ]
    , testGroup "__tojson"
      [ testCase "respect __tojson metamethod" . run @Lua.Exception $ do
          newtable -- object

          newtable -- metatable
          pushHaskellFunction (1 <$ pushText "{\"answer\": 42}")
          setfield (nth 2) "__tojson"

          setmetatable (nth 2)
          val <- forcePeek $ peekValue top
          liftIO $ object [ "answer" .= (42 :: Int) ] @?= val
      ]
    ]
  ]

assertRoundtripEqual :: Eq a
                     => Pusher Lua.Exception a -> Peeker Lua.Exception a
                     -> a -> Property
assertRoundtripEqual pushX peekX x = QC.monadicIO $ do
  y <- QC.run $ roundtrip pushX peekX x
  assert (x == y)

roundtrip :: Pusher Lua.Exception a -> Peeker Lua.Exception a -> a -> IO a
roundtrip pushX peekX x = run $ do
  pushX x
  size <- gettop
  when (size /= 1) $
    failLua $ "not the right amount of elements on the stack: " ++ show size
  result <- forcePeek $ peekX top
  afterPeekSize <- gettop
  when (afterPeekSize /= 1) $
    failLua $ "peeking modified the stack: " ++ show afterPeekSize
  return result


-- | Ensure that numbers are representable as Doubles.
numbersToDoubles :: Aeson.Value -> Aeson.Value
numbersToDoubles (Aeson.Number x) = Aeson.Number . luaNumberToScientific $
                                    toRealFloat x
numbersToDoubles (Aeson.Object x) = Aeson.Object $ fmap numbersToDoubles x
numbersToDoubles (Aeson.Array x) = Aeson.Array $ fmap numbersToDoubles x
numbersToDoubles x = x

-- | Convert a Lua number to a scientific number, which are the basis for JSON
-- numbers.
luaNumberToScientific :: Lua.Number -> Scientific
luaNumberToScientific = fromFloatDigits . (realToFrac :: Lua.Number -> Double)

-- aeson defines instances for Arbitrary since 2.0.3.0
#if !MIN_VERSION_aeson(2,0,3)
instance Arbitrary Aeson.Value where
  arbitrary = arbitraryValue 9

#if MIN_VERSION_aeson(2,0,0)
instance Arbitrary Key where
  arbitrary = fmap fromText arbitrary

instance Arbitrary a => Arbitrary (KeyMap.KeyMap a) where
  arbitrary = fmap KeyMap.fromList arbitrary
#endif

arbitraryValue :: Int -> Gen Aeson.Value
arbitraryValue size = frequency
    [ (1, return Aeson.Null)
    , (4, Aeson.Bool <$> arbitrary)
    -- Note: we don't draw numbers from the whole possible range, but
    -- only from the range of numbers that Lua can handle without
    -- rounding errors. This is ok, as JSON doesn't define a required
    -- precision, and (usually) matches the behavior of JavaScript.
    , (4, Aeson.Number . luaNumberToScientific . Lua.Number <$> arbitrary)
    , (4, Aeson.String <$> arbitrary)
    , (2, resize (size - 1) $ Aeson.Array <$> arbitrary)
    , (2, resize (size - 1) $ Aeson.Object <$> arbitrary)
    ]
#endif

--
-- Type for __toaeson tests
--

-- | Example type with custom JSON encoding.
data TwentyThree = TwentyThree

instance ToJSON TwentyThree where
  toJSON _ = object
    [ "title" .= (23 :: Int)
    ]

peekTwentyThree :: Peeker e TwentyThree
peekTwentyThree =
  reportValueOnFailure "TwentyThree" (`Lua.fromuserdata` "TwentyThree")

pushTwentyThree :: LuaError e => Pusher e TwentyThree
pushTwentyThree _ = do
  Lua.newhsuserdatauv TwentyThree 0
  created <- Lua.newudmetatable "TwentyThree"
  when created $ do
    pushToAeson (fmap Aeson.toJSON . peekTwentyThree)
    setfield (nth 2) "__toaeson"
  setmetatable (nth 2)