packages feed

canonical-json 0.5.0.1 → 0.6.0.0

raw patch · 9 files changed

+287/−55 lines, 9 filesdep +criteriondep +deepseqdep ~basedep ~bytestringdep ~containersPVP ok

version bump matches the API change (PVP)

Dependencies added: criterion, deepseq

Dependency ranges changed: base, bytestring, containers

API changes (from Hackage documentation)

+ Text.JSON.Canonical: data JSString
+ Text.JSON.Canonical: fromJSString :: JSString -> String
+ Text.JSON.Canonical: toJSString :: String -> JSString
+ Text.JSON.Canonical.Class: instance GHC.Base.Monad m => Text.JSON.Canonical.Class.FromObjectKey m Text.JSON.Canonical.Types.JSString
+ Text.JSON.Canonical.Class: instance GHC.Base.Monad m => Text.JSON.Canonical.Class.ToJSON m Text.JSON.Canonical.Types.JSString
+ Text.JSON.Canonical.Class: instance GHC.Base.Monad m => Text.JSON.Canonical.Class.ToObjectKey m Text.JSON.Canonical.Types.JSString
+ Text.JSON.Canonical.Class: instance Text.JSON.Canonical.Class.ReportSchemaErrors m => Text.JSON.Canonical.Class.FromJSON m Text.JSON.Canonical.Types.JSString
+ Text.JSON.Canonical.Types: data JSString
+ Text.JSON.Canonical.Types: fromJSString :: JSString -> String
+ Text.JSON.Canonical.Types: instance Control.DeepSeq.NFData Text.JSON.Canonical.Types.JSString
+ Text.JSON.Canonical.Types: instance Control.DeepSeq.NFData Text.JSON.Canonical.Types.JSValue
+ Text.JSON.Canonical.Types: instance Data.String.IsString Text.JSON.Canonical.Types.JSString
+ Text.JSON.Canonical.Types: instance GHC.Base.Monoid Text.JSON.Canonical.Types.JSString
+ Text.JSON.Canonical.Types: instance GHC.Base.Semigroup Text.JSON.Canonical.Types.JSString
+ Text.JSON.Canonical.Types: instance GHC.Classes.Eq Text.JSON.Canonical.Types.JSString
+ Text.JSON.Canonical.Types: instance GHC.Classes.Ord Text.JSON.Canonical.Types.JSString
+ Text.JSON.Canonical.Types: instance GHC.Read.Read Text.JSON.Canonical.Types.JSString
+ Text.JSON.Canonical.Types: instance GHC.Show.Show Text.JSON.Canonical.Types.JSString
+ Text.JSON.Canonical.Types: instance Text.Printf.PrintfArg Text.JSON.Canonical.Types.JSString
+ Text.JSON.Canonical.Types: toJSString :: String -> JSString
- Text.JSON.Canonical: JSObject :: [(String, JSValue)] -> JSValue
+ Text.JSON.Canonical: JSObject :: [(JSString, JSValue)] -> JSValue
- Text.JSON.Canonical: JSString :: String -> JSValue
+ Text.JSON.Canonical: JSString :: !JSString -> JSValue
- Text.JSON.Canonical: fromJSField :: (ReportSchemaErrors m, FromJSON m a) => JSValue -> String -> m a
+ Text.JSON.Canonical: fromJSField :: (ReportSchemaErrors m, FromJSON m a) => JSValue -> JSString -> m a
- Text.JSON.Canonical: fromJSObject :: ReportSchemaErrors m => JSValue -> m [(String, JSValue)]
+ Text.JSON.Canonical: fromJSObject :: ReportSchemaErrors m => JSValue -> m [(JSString, JSValue)]
- Text.JSON.Canonical: fromJSOptField :: (ReportSchemaErrors m, FromJSON m a) => JSValue -> String -> m (Maybe a)
+ Text.JSON.Canonical: fromJSOptField :: (ReportSchemaErrors m, FromJSON m a) => JSValue -> JSString -> m (Maybe a)
- Text.JSON.Canonical: fromObjectKey :: FromObjectKey m a => String -> m (Maybe a)
+ Text.JSON.Canonical: fromObjectKey :: FromObjectKey m a => JSString -> m (Maybe a)
- Text.JSON.Canonical: mkObject :: forall m. Monad m => [(String, m JSValue)] -> m JSValue
+ Text.JSON.Canonical: mkObject :: forall m. Monad m => [(JSString, m JSValue)] -> m JSValue
- Text.JSON.Canonical: toObjectKey :: ToObjectKey m a => a -> m String
+ Text.JSON.Canonical: toObjectKey :: ToObjectKey m a => a -> m JSString
- Text.JSON.Canonical.Class: fromJSField :: (ReportSchemaErrors m, FromJSON m a) => JSValue -> String -> m a
+ Text.JSON.Canonical.Class: fromJSField :: (ReportSchemaErrors m, FromJSON m a) => JSValue -> JSString -> m a
- Text.JSON.Canonical.Class: fromJSObject :: ReportSchemaErrors m => JSValue -> m [(String, JSValue)]
+ Text.JSON.Canonical.Class: fromJSObject :: ReportSchemaErrors m => JSValue -> m [(JSString, JSValue)]
- Text.JSON.Canonical.Class: fromJSOptField :: (ReportSchemaErrors m, FromJSON m a) => JSValue -> String -> m (Maybe a)
+ Text.JSON.Canonical.Class: fromJSOptField :: (ReportSchemaErrors m, FromJSON m a) => JSValue -> JSString -> m (Maybe a)
- Text.JSON.Canonical.Class: fromObjectKey :: FromObjectKey m a => String -> m (Maybe a)
+ Text.JSON.Canonical.Class: fromObjectKey :: FromObjectKey m a => JSString -> m (Maybe a)
- Text.JSON.Canonical.Class: mkObject :: forall m. Monad m => [(String, m JSValue)] -> m JSValue
+ Text.JSON.Canonical.Class: mkObject :: forall m. Monad m => [(JSString, m JSValue)] -> m JSValue
- Text.JSON.Canonical.Class: toObjectKey :: ToObjectKey m a => a -> m String
+ Text.JSON.Canonical.Class: toObjectKey :: ToObjectKey m a => a -> m JSString
- Text.JSON.Canonical.Types: JSObject :: [(String, JSValue)] -> JSValue
+ Text.JSON.Canonical.Types: JSObject :: [(JSString, JSValue)] -> JSValue
- Text.JSON.Canonical.Types: JSString :: String -> JSValue
+ Text.JSON.Canonical.Types: JSString :: !JSString -> JSValue

Files

ChangeLog.md view
@@ -1,5 +1,15 @@ # Revision history for canonical-json +## 0.6.0.0 2019-07-31++* Introduced JSString type rather than using String, for improved memory use.++* Improved parser performance++* Reduce stack space usage and introduce stack use tests++* Added benchmarks+ ## 0.5.0.1 2018-10-26  * ghc-8.4 compatibility.
+ README.md view
@@ -0,0 +1,45 @@+An implementation of [Canonical JSON].++The "canonical JSON" format is designed to provide repeatable hashes of+JSON values. It is designed for applications that need to hash, sign+or authenitcate JSON data structures., including embedded signatures.++Canonical JSON is parsable with any full JSON parser, and it allows white+space for pretty-printed human readable presentation, but it can be put into+a canonical form which then has a stable serialised representation and thus a+stable hash.++The basic concept is that a file in the canonical JSON format can be read+using `parseCanonicalJSON`. Note that this input file does *not* itself need+to be in canonical form, it just needs to be in the canonical JSON format.+Then the `renderCanonicalJSON` function is used to render into the canonical+form. This is then the form that can be hashed or signed etc.++The `prettyCanonicalJSON` is for convenience to render in a human readable+style, since the canoncal form eliminates unnecessary white space which+makes the output hard to read. This style is again suitable to read using+'parseCanonicalJSON'. So this is suitable to use for producing output that+has to be later hashed or otherwise checked.++See the [API docs] on Hackage.++This package has been extracted from the [hackage-security] package where+canonical JSON is used for all the signed TUF files, such as the+[root keys file], etc. As you can see from that, canoncal JSON allows keeping+JSON files in a human readable pretty-printed form, and still allows verifying+signatures. In particular this demonstrates the use of embedded signatures,+where the `root.json` both contains a body value and multiple signatures of+that body all within the same file. This is because canoncal JSON is about+hashes for *JSON values, not serialised JSON text*.++[Canonical JSON]: http://wiki.laptop.org/go/Canonical_JSON+[API docs]: https://hackage.haskell.org/package/canonical-json+[hackage-security]: https://hackage.haskell.org/package/hackage-security+[root keys file]: https://hackage.haskell.org/root.json+++Known bugs limitations+-----------------------++ * Decoding/encoding Unicode code-points beyond `U+00ff` is currently broken+
Text/JSON/Canonical.hs view
@@ -39,6 +39,7 @@     -- * Types     JSValue(..)   , Int54+  , JSString     -- * Parsing and printing   , parseCanonicalJSON   , renderCanonicalJSON@@ -53,6 +54,8 @@   , Got   , expectedButGotValue     -- * Utility+  , toJSString+  , fromJSString   , fromJSObject   , fromJSField   , fromJSOptField
Text/JSON/Canonical/Class.hs view
@@ -1,4 +1,5 @@ {-# LANGUAGE CPP #-}+{-# LANGUAGE BangPatterns #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE ScopedTypeVariables #-}@@ -33,7 +34,7 @@  import Text.JSON.Canonical.Types -import Control.Monad (liftM)+import Control.Monad (foldM, liftM) import Data.Maybe (catMaybes) import Data.Map (Map) import qualified Data.Map as Map@@ -59,11 +60,11 @@  -- | Used in the 'ToJSON' instance for 'Map' class ToObjectKey m a where-  toObjectKey :: a -> m String+  toObjectKey :: a -> m JSString  -- | Used in the 'FromJSON' instance for 'Map' class FromObjectKey m a where-  fromObjectKey :: String -> m (Maybe a)+  fromObjectKey :: JSString -> m (Maybe a)  -- | Monads in which we can report schema errors class (Applicative m, Monad m) => ReportSchemaErrors m where@@ -83,19 +84,25 @@     describeValue (JSArray  _) = "array"     describeValue (JSObject _) = "object" -unknownField :: ReportSchemaErrors m => String -> m a+unknownField :: ReportSchemaErrors m => JSString -> m a unknownField field = expected ("field " ++ show field) Nothing  {-------------------------------------------------------------------------------   ToObjectKey and FromObjectKey instances -------------------------------------------------------------------------------} -instance Monad m => ToObjectKey m String where+instance Monad m => ToObjectKey m JSString where   toObjectKey = return -instance Monad m => FromObjectKey m String where+instance Monad m => FromObjectKey m JSString where   fromObjectKey = return . Just +instance Monad m => ToObjectKey m String where+  toObjectKey = return . toJSString++instance Monad m => FromObjectKey m String where+  fromObjectKey = return . Just . fromJSString+ {-------------------------------------------------------------------------------   ToJSON and FromJSON instances -------------------------------------------------------------------------------}@@ -106,13 +113,20 @@ instance Monad m => FromJSON m JSValue where   fromJSON = return -instance Monad m => ToJSON m String where+instance Monad m => ToJSON m JSString where   toJSON = return . JSString -instance ReportSchemaErrors m => FromJSON m String where+instance ReportSchemaErrors m => FromJSON m JSString where   fromJSON (JSString str) = return str   fromJSON val            = expectedButGotValue "string" val +instance Monad m => ToJSON m String where+  toJSON = return . JSString . toJSString++instance ReportSchemaErrors m => FromJSON m String where+  fromJSON (JSString str) = return (fromJSString str)+  fromJSON val            = expectedButGotValue "string" val+ instance Monad m => ToJSON m Int54 where   toJSON = return . JSNum @@ -125,14 +139,14 @@   {-# OVERLAPPABLE #-} #endif     (Monad m, ToJSON m a) => ToJSON m [a] where-  toJSON = liftM JSArray . mapM toJSON+  toJSON = liftM JSArray . mapM' toJSON  instance #if __GLASGOW_HASKELL__ >= 710   {-# OVERLAPPABLE #-} #endif     (ReportSchemaErrors m, FromJSON m a) => FromJSON m [a] where-  fromJSON (JSArray as) = mapM fromJSON as+  fromJSON (JSArray as) = mapM' fromJSON as   fromJSON val          = expectedButGotValue "array" val  @@ -140,10 +154,10 @@          , ToObjectKey m k          , ToJSON m a          ) => ToJSON m (Map k a) where-  toJSON = liftM JSObject . mapM aux . Map.toList+  toJSON = liftM JSObject . mapM' aux . Map.toList     where-      aux :: (k, a) -> m (String, JSValue)-      aux (k, a) = do k' <- toObjectKey k; a' <- toJSON a; return (k', a')+      aux :: (k, a) -> m (JSString, JSValue)+      aux (k, a) = (,) <$> toObjectKey k <*> toJSON a  instance ( ReportSchemaErrors m          , Ord k@@ -152,26 +166,25 @@          ) => FromJSON m (Map k a) where   fromJSON enc = do       obj <- fromJSObject enc-      Map.fromList . catMaybes <$> mapM aux obj+      Map.fromList . catMaybes <$> mapM_reverse aux obj     where-      aux :: (String, JSValue) -> m (Maybe (k, a))+      aux :: (JSString, JSValue) -> m (Maybe (k, a))       aux (k, a) = knownKeys <$> fromObjectKey k <*> fromJSON a       knownKeys :: Maybe k -> a -> Maybe (k, a)       knownKeys Nothing  _ = Nothing       knownKeys (Just k) a = Just (k, a) - {-------------------------------------------------------------------------------   Utility -------------------------------------------------------------------------------} -fromJSObject :: ReportSchemaErrors m => JSValue -> m [(String, JSValue)]+fromJSObject :: ReportSchemaErrors m => JSValue -> m [(JSString, JSValue)] fromJSObject (JSObject obj) = return obj fromJSObject val            = expectedButGotValue "object" val  -- | Extract a field from a JSON object fromJSField :: (ReportSchemaErrors m, FromJSON m a)-            => JSValue -> String -> m a+            => JSValue -> JSString -> m a fromJSField val nm = do     obj <- fromJSObject val     case lookup nm obj of@@ -179,18 +192,27 @@       Nothing  -> unknownField nm  fromJSOptField :: (ReportSchemaErrors m, FromJSON m a)-               => JSValue -> String -> m (Maybe a)+               => JSValue -> JSString -> m (Maybe a) fromJSOptField val nm = do     obj <- fromJSObject val     case lookup nm obj of       Just fld -> Just <$> fromJSON fld       Nothing  -> return Nothing -mkObject :: forall m. Monad m => [(String, m JSValue)] -> m JSValue+mkObject :: forall m. Monad m => [(JSString, m JSValue)] -> m JSValue mkObject = liftM JSObject . sequenceFields   where-    sequenceFields :: [(String, m JSValue)] -> m [(String, JSValue)]+    sequenceFields :: [(JSString, m JSValue)] -> m [(JSString, JSValue)]     sequenceFields []               = return []     sequenceFields ((fld,val):flds) = do val' <- val                                          flds' <- sequenceFields flds                                          return ((fld,val'):flds')++-- Avoid stack overflow on large lists+mapM' :: Monad m => (a -> m b) -> [a] -> m [b]+mapM' f = fmap reverse . mapM_reverse f++-- For when we don't care about order, can avoid the reverse+mapM_reverse :: Monad m => (a -> m b) -> [a] -> m [b]+mapM_reverse f = foldM (\xs a -> fmap (:xs) (f a)) []+
Text/JSON/Canonical/Parse.hs view
@@ -21,10 +21,12 @@  import Text.JSON.Canonical.Types -import Text.ParserCombinators.Parsec-         ( CharParser, (<|>), (<?>), many, between, sepBy+import Text.Parsec+         ( (<|>), (<?>), many, between, sepBy          , satisfy, char, string, digit, spaces          , parse )+import Text.Parsec.ByteString.Lazy+         ( Parser ) import Text.PrettyPrint hiding (char) import qualified Text.PrettyPrint as Doc #if !(MIN_VERSION_base(4,8,0))@@ -77,8 +79,8 @@ s_value (JSArray vs)   = s_array  vs s_value (JSObject fs)  = s_object (sortBy (compare `on` fst) fs) -s_string :: String -> ShowS-s_string s = showChar '"' . showl s+s_string :: JSString -> ShowS+s_string s = showChar '"' . showl (fromJSString s)   where showl []     = showChar '"'         showl (c:cs) = s_char c . showl cs @@ -92,7 +94,7 @@   where showl []     = showChar ']'         showl (v:vs) = showChar ',' . s_value v . showl vs -s_object :: [(String, JSValue)] -> ShowS+s_object :: [(JSString, JSValue)] -> ShowS s_object []               = showString "{}" s_object ((k0,v0):kvs0)   = showChar '{' . s_string k0                           . showChar ':' . s_value v0@@ -115,12 +117,11 @@ parseCanonicalJSON :: BS.ByteString -> Either String JSValue parseCanonicalJSON = either (Left . show) Right                    . parse p_value ""-                   . BS.unpack -p_value :: CharParser () JSValue+p_value :: Parser JSValue p_value = spaces *> p_jvalue -tok              :: CharParser () a -> CharParser () a+tok              :: Parser a -> Parser a tok p             = p <* spaces  {-@@ -133,7 +134,7 @@    false    null -}-p_jvalue         :: CharParser () JSValue+p_jvalue         :: Parser JSValue p_jvalue          =  (JSNull      <$  p_null)                  <|> (JSBool      <$> p_boolean)                  <|> (JSArray     <$> p_array)@@ -142,10 +143,10 @@                  <|> (JSNum       <$> p_number)                  <?> "JSON value" -p_null           :: CharParser () ()+p_null           :: Parser () p_null            = tok (string "null") >> return () -p_boolean        :: CharParser () Bool+p_boolean        :: Parser Bool p_boolean         = tok                       (  (True  <$ string "true")                      <|> (False <$ string "false")@@ -158,7 +159,7 @@    value    value , elements -}-p_array          :: CharParser () [JSValue]+p_array          :: Parser [JSValue] p_array           = between (tok (char '[')) (tok (char ']'))                   $ p_jvalue `sepBy` tok (char ',') @@ -174,8 +175,9 @@    \\    \" -}-p_string         :: CharParser () String-p_string          = between (char '"') (tok (char '"')) (many p_char)+p_string         :: Parser JSString+p_string          = between (char '"') (tok (char '"'))+                            (many p_char >>= \str -> return $! toJSString str)   where p_char    =  (char '\\' >> p_esc)                  <|> (satisfy (\x -> x /= '"' && x /= '\\')) @@ -192,7 +194,7 @@ pair:    string : value -}-p_object         :: CharParser () [(String,JSValue)]+p_object         :: Parser [(JSString, JSValue)] p_object          = between (tok (char '{')) (tok (char '}'))                   $ p_field `sepBy` tok (char ',')   where p_field   = (,) <$> (p_string <* tok (char ':')) <*> p_jvalue@@ -214,7 +216,7 @@ -- -- TODO: Currently this allows for a maximum of 15 digits (i.e. a maximum value -- of @999,999,999,999,999@) as a crude approximation of the 'Int54' range.-p_number         :: CharParser () Int54+p_number         :: Parser Int54 p_number          = tok                       (  (char '-' *> (negate <$> pnat))                      <|> pnat@@ -228,7 +230,7 @@ digitToInt54 :: Char -> Int54 digitToInt54 = fromIntegral . digitToInt -manyN :: Int -> CharParser () a -> CharParser () [a]+manyN :: Int -> Parser a -> Parser [a] manyN 0 _ =  pure [] manyN n p =  ((:) <$> p <*> manyN (n-1) p)          <|> pure []@@ -256,8 +258,8 @@ jvalue (JSArray vs)   = jarray  vs jvalue (JSObject fs)  = jobject fs -jstring :: String -> Doc-jstring = doubleQuotes . hcat . map jchar+jstring :: JSString -> Doc+jstring = doubleQuotes . hcat . map jchar . fromJSString  jchar :: Char -> Doc jchar '"'   = Doc.char '\\' <> Doc.char '"'@@ -268,7 +270,7 @@ jarray = sep . punctuate' lbrack comma rbrack        . map jvalue -jobject :: [(String, JSValue)] -> Doc+jobject :: [(JSString, JSValue)] -> Doc jobject = sep . punctuate' lbrace comma rbrace         . map (\(k,v) -> sep [jstring k <> colon, nest 2 (jvalue v)]) 
Text/JSON/Canonical/Types.hs view
@@ -9,9 +9,13 @@ module Text.JSON.Canonical.Types   ( JSValue(..)   , Int54(..)+  , JSString+  , toJSString+  , fromJSString   ) where  import Control.Arrow (first)+import Control.DeepSeq (NFData(..)) import Data.Bits (Bits) #if MIN_VERSION_base(4,7,0) import Data.Bits (FiniteBits)@@ -19,19 +23,72 @@ import Data.Data (Data) import Data.Int (Int64) import Data.Ix (Ix)+#if !MIN_VERSION_base(4,8,0)+import Data.Monoid (Monoid)+#endif+#if MIN_VERSION_base(4,9,0)+import Data.Semigroup (Semigroup)+#endif import Data.Typeable (Typeable) import Foreign.Storable (Storable)+#if MIN_VERSION_base(4,7,0)+import Text.Printf (PrintfArg(..))+import qualified Text.Printf as Printf+#else import Text.Printf (PrintfArg)+#endif+import Data.String (IsString)+import Data.ByteString.Short (ShortByteString)+import qualified Data.ByteString.Short as BS   data JSValue     = JSNull     | JSBool     !Bool     | JSNum      !Int54-    | JSString   String+    | JSString   !JSString     | JSArray    [JSValue]-    | JSObject   [(String, JSValue)]+    | JSObject   [(JSString, JSValue)]     deriving (Show, Read, Eq, Ord)++instance NFData JSValue where+  rnf JSNull        = ()+  rnf JSBool{}      = ()+  rnf JSNum{}       = ()+  rnf JSString{}    = ()+  rnf (JSArray  vs) = rnf vs+  rnf (JSObject vs) = rnf vs++-- | Canonical JSON strings are in fact just bytes.+--+newtype JSString = JStr ShortByteString+    deriving (Eq, Ord, IsString,+#if MIN_VERSION_base(4,9,0)+              Semigroup,+#endif+              Monoid)++instance NFData JSString where+  rnf JStr{} = ()++instance Show JSString where+  showsPrec n (JStr bs) = showsPrec n bs++instance Read JSString where+  readsPrec p = map (first JStr) . readsPrec p++#if MIN_VERSION_base(4,7,0)+instance PrintfArg JSString where+    formatArg = Printf.formatString . fromJSString+#endif++toJSString :: String -> JSString+toJSString str+  | all (<= '\255') str = JStr . BS.pack . map (fromIntegral . fromEnum) $ str+  | otherwise           = error "toJSString: cannot use non-ASCII chars"++fromJSString :: JSString -> String+fromJSString = map (toEnum . fromIntegral) . BS.unpack . (\(JStr bs) -> bs)  -- | 54-bit integer values --
+ benchmark/Parse.hs view
@@ -0,0 +1,15 @@++import Text.JSON.Canonical.Parse++import qualified Data.ByteString.Lazy as BS++import Criterion.Main++main :: IO ()+main = defaultMain [+  env loadData $ \jsondata ->+  bench "parse" $ nf parseCanonicalJSON jsondata+  ]++loadData :: IO BS.ByteString+loadData = BS.readFile "benchmark/cardano-genesis.json"
canonical-json.cabal view
@@ -1,5 +1,5 @@ name:                canonical-json-version:             0.5.0.1+version:             0.6.0.0 synopsis:            Canonical JSON for signing and hashing JSON values description:         An implementation of Canonical JSON.                      .@@ -19,13 +19,15 @@ license-file:        LICENSE author:              Duncan Coutts, Edsko de Vries maintainer:          duncan@well-typed.com, edsko@well-typed.com-copyright:           Copyright 2015-2017 Well-Typed LLP+copyright:           Copyright 2015-2018 Well-Typed LLP homepage:            https://github.com/well-typed/canonical-json category:            Text, JSON build-type:          Simple extra-source-files:  ChangeLog.md cabal-version:       >=1.10 +extra-source-files:  README.md+ source-repository head   type:     git   location: https://github.com/well-typed/canonical-json.git@@ -39,8 +41,9 @@                        MultiParamTypeClasses, FlexibleInstances,                        ScopedTypeVariables, OverlappingInstances   build-depends:       base              >= 4.5     && < 5,-                       bytestring        >= 0.9     && < 0.11,-                       containers        >= 0.4     && < 0.6,+                       bytestring        >= 0.10.4  && < 0.11,+                       containers        >= 0.4     && < 0.7,+                       deepseq           >= 1.2     && < 1.5,                        parsec            >= 3.1     && < 3.2,                        pretty            >= 1.0     && < 1.2   default-language:    Haskell2010@@ -53,6 +56,7 @@   build-depends:       base,                        bytestring,                        canonical-json,+                       containers,                        aeson             == 1.4.*,                        vector,                        unordered-containers,@@ -60,4 +64,17 @@                        tasty,                        tasty-quickcheck   default-language:    Haskell2010-  ghc-options:         -Wall+  -- -K100k to check for stack overflow:+  ghc-options:         -Wall -with-rtsopts=-K100k++benchmark parse-bench+  type:                exitcode-stdio-1.0+  main-is:             Parse.hs+  hs-source-dirs:      benchmark+  build-depends:       base,+                       bytestring,+                       canonical-json,+                       containers,+                       criterion >= 1.1+  default-language:    Haskell2010+  ghc-options:         -Wall -rtsopts
tests/TestSuite.hs view
@@ -1,3 +1,5 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# OPTIONS_GHC -fno-warn-orphans #-} module Main (main) where @@ -7,8 +9,13 @@ 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 (fromString)+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) @@ -26,6 +33,16 @@         , 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+          ]         ]  @@ -48,6 +65,43 @@ 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@@ -61,11 +115,14 @@ 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 (JSString s)  = Aeson.String (toAesonStr s) toAeson (JSArray xs)  = Aeson.Array  $ V.fromList  [ toAeson x | x <- xs ]-toAeson (JSObject xs) = Aeson.Object $ HM.fromList [ (fromString k, toAeson v)+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 ->@@ -73,13 +130,13 @@       [ (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)+      , (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)-      mapFirst f = map (\(x, y) -> (f x, y))    shrink JSNull        = []   shrink (JSBool    _) = []@@ -101,4 +158,8 @@       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) ]