{-# LANGUAGE CPP #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}
module Main (main) where
import Data.Int
import Data.List (sortBy, nubBy)
import Data.Function (on)
import qualified Data.ByteString.Lazy.Char8 as BS
import Text.JSON.Canonical
#if !(MIN_VERSION_base(4,8,0))
import Control.Applicative (Applicative(..), (<$>))
#endif
import qualified Data.Aeson as Aeson (Value (..), eitherDecode)
import Data.String (IsString, fromString)
import qualified Data.Map as Map
import qualified Data.Vector as V (fromList)
import qualified Data.HashMap.Strict as HM (fromList)
import Test.QuickCheck
import Test.Tasty.QuickCheck (testProperty)
import Test.Tasty (TestTree, defaultMain, testGroup)
main :: IO ()
main = defaultMain tests
tests :: TestTree
tests = testGroup "Canonical JSON" [
testProperty "prop_roundtrip_canonical" prop_roundtrip_canonical
, testProperty "prop_roundtrip_pretty" prop_roundtrip_pretty
, testProperty "prop_canonical_pretty" prop_canonical_pretty
, testProperty "prop_aeson_canonical" prop_aeson_canonical
, testGroup "prop_toJSON_fromJSON" [
testProperty "String" prop_toJSON_fromJSON_String
, testProperty "Int54" prop_toJSON_fromJSON_Int54
, testProperty "[]" prop_toJSON_fromJSON_List
, testProperty "Map" prop_toJSON_fromJSON_Map
]
, testGroup "no stack overflow" [
testProperty "[]" unit_large_List
, testProperty "Map" unit_large_Map
]
]
prop_roundtrip_canonical,
prop_roundtrip_pretty,
prop_canonical_pretty,
prop_aeson_canonical
:: JSValue -> Bool
prop_roundtrip_canonical jsval =
parseCanonicalJSON (renderCanonicalJSON jsval) == Right (canonicalise jsval)
prop_roundtrip_pretty jsval =
parseCanonicalJSON (BS.pack (prettyCanonicalJSON jsval)) == Right jsval
prop_canonical_pretty jsval =
parseCanonicalJSON (renderCanonicalJSON jsval) ==
fmap canonicalise (parseCanonicalJSON (BS.pack (prettyCanonicalJSON jsval)))
prop_aeson_canonical jsval =
Aeson.eitherDecode (renderCanonicalJSON jsval) == Right (toAeson jsval)
prop_toJSON_fromJSON :: (Monad m, ToJSON m a, FromJSON m a, Eq a) => a -> m Bool
prop_toJSON_fromJSON x =
toJSON x >>= fromJSON >>= \x' -> return (x' == x)
prop_toJSON_fromJSON_String :: JSString -> Property
prop_toJSON_fromJSON_String = runM . prop_toJSON_fromJSON
prop_toJSON_fromJSON_Int54 :: Int54 -> Property
prop_toJSON_fromJSON_Int54 = runM . prop_toJSON_fromJSON
prop_toJSON_fromJSON_List :: [JSString] -> Property
prop_toJSON_fromJSON_List = runM . prop_toJSON_fromJSON
prop_toJSON_fromJSON_Map :: Map.Map JSString JSString -> Property
prop_toJSON_fromJSON_Map = runM . prop_toJSON_fromJSON
unit_large_List :: Property
unit_large_List = runM (prop_toJSON_fromJSON large)
where
large = replicate 10000 (42 :: Int54)
unit_large_Map :: Property
unit_large_Map = runM (prop_toJSON_fromJSON large)
where
large = Map.fromList [ (show n, 42 :: Int54) | n <- [0.. 10000 :: Int] ]
newtype M a = M (Maybe a)
deriving (Functor, Applicative, Monad)
runM :: Testable prop => M prop -> Property
runM (M Nothing) = property False
runM (M (Just prop)) = property prop
instance ReportSchemaErrors M where
expected _ _ = M Nothing
canonicalise :: JSValue -> JSValue
canonicalise v@JSNull = v
canonicalise v@(JSBool _) = v
canonicalise v@(JSNum _) = v
canonicalise v@(JSString _) = v
canonicalise (JSArray vs) = JSArray [ canonicalise v | v <- vs]
canonicalise (JSObject vs) = JSObject [ (k, canonicalise v)
| (k,v) <- sortBy (compare `on` fst) vs ]
toAeson :: JSValue -> Aeson.Value
toAeson JSNull = Aeson.Null
toAeson (JSBool b) = Aeson.Bool b
toAeson (JSNum n) = Aeson.Number (fromIntegral n)
toAeson (JSString s) = Aeson.String (toAesonStr s)
toAeson (JSArray xs) = Aeson.Array $ V.fromList [ toAeson x | x <- xs ]
toAeson (JSObject xs) = Aeson.Object $ HM.fromList [ (toAesonStr k, toAeson v)
| (k, v) <- xs ]
toAesonStr :: IsString s => JSString -> s
toAesonStr = fromString . fromJSString
instance Arbitrary JSValue where
arbitrary =
sized $ \sz ->
frequency
[ (1, pure JSNull)
, (1, JSBool <$> arbitrary)
, (2, JSNum <$> arbitrary)
, (2, JSString <$> arbitrary)
, (3, JSArray <$> resize (sz `div` 2) arbitrary)
, (3, JSObject . noDupFields
<$> resize (sz `div` 2) arbitrary)
]
where
noDupFields = nubBy (\(x,_) (y,_) -> x==y)
shrink JSNull = []
shrink (JSBool _) = []
shrink (JSNum n) = [ JSNum n' | n' <- shrink n ]
shrink (JSString s) = [ JSString s' | s' <- shrink s ]
shrink (JSArray vs) = [ JSArray vs' | vs' <- shrink vs ]
shrink (JSObject vs) = [ JSObject vs' | vs' <- shrinkList shrinkSnd vs ]
where
shrinkSnd (a,b) = [ (a,b') | b' <- shrink b ]
instance Arbitrary Int54 where
arbitrary = fromIntegral <$>
frequency [ (1, pure lowerbound)
, (1, pure upperbound)
, (8, choose (lowerbound, upperbound))
]
where
upperbound, lowerbound :: Int64
upperbound = 999999999999999 -- 15 decimal digits
lowerbound = (-999999999999999)
shrink = shrinkIntegral
instance Arbitrary JSString where
arbitrary = toJSString . getASCIIString <$> arbitrary
shrink s = [ toJSString s' | s' <- shrink (fromJSString s) ]