{-# 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 import qualified Data.Aeson as Aeson (Value (..), eitherDecode) import Data.String (fromString) 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 ] 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) 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 (fromString s) toAeson (JSArray xs) = Aeson.Array $ V.fromList [ toAeson x | x <- xs ] toAeson (JSObject xs) = Aeson.Object $ HM.fromList [ (fromString k, toAeson v) | (k, v) <- xs ] instance Arbitrary JSValue where arbitrary = sized $ \sz -> frequency [ (1, pure JSNull) , (1, JSBool <$> arbitrary) , (2, JSNum <$> arbitrary) , (2, JSString . getASCIIString <$> arbitrary) , (3, JSArray <$> resize (sz `div` 2) arbitrary) , (3, JSObject . mapFirst getASCIIString . noDupFields <$> resize (sz `div` 2) arbitrary) ] where noDupFields = nubBy (\(x,_) (y,_) -> x==y) mapFirst f = map (\(x, y) -> (f 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