sajson (empty) → 0.1.0.0
raw patch · 8 files changed
+605/−0 lines, 8 filesdep +QuickCheckdep +aesondep +basesetup-changed
Dependencies added: QuickCheck, aeson, base, bytestring, criterion, hspec, sajson, scientific, text, unordered-containers, vector
Files
- LICENSE +49/−0
- README.md +69/−0
- Setup.hs +2/−0
- bench/Bench.hs +34/−0
- cbits/sajson_wrapper.cpp +60/−0
- sajson.cabal +74/−0
- src/Data/Sajson.hs +235/−0
- test/Spec.hs +82/−0
+ LICENSE view
@@ -0,0 +1,49 @@+Written by Zhouyu Qian.+Copyright (c) 2017 Zhouyu Qian++Permission is hereby granted, free of charge, to any person+obtaining a copy of this software and associated documentation+files (the "Software"), to deal in the Software without+restriction, including without limitation the rights to use, copy,+modify, merge, publish, distribute, sublicense, and/or sell copies+of the Software, and to permit persons to whom the Software is+furnished to do so, subject to the following conditions:++The above copyright notice and this permission notice shall be+included in all copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS+BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN+ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE+SOFTWARE.++---++The software incorporates the sajson C++ library.++Written by Chad Austin+Copyright (c) 2012-2017 Chad Austin++Permission is hereby granted, free of charge, to any person+obtaining a copy of this software and associated documentation+files (the "Software"), to deal in the Software without+restriction, including without limitation the rights to use, copy,+modify, merge, publish, distribute, sublicense, and/or sell copies+of the Software, and to permit persons to whom the Software is+furnished to do so, subject to the following conditions:++The above copyright notice and this permission notice shall be+included in all copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS+BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN+ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE+SOFTWARE.
+ README.md view
@@ -0,0 +1,69 @@+# sajson++This is a Haskell package that is a thin wrapper over the wonderful+[sajson](https://github.com/chadaustin/sajson) library written by Chad Austin.+It provides a high performance JSON parser written in C++11. After parsing, a+Haskell `Data.Aeson.Value` is constructed.++## Who This Is For++This library is specifically designed for Haskell users who find the performance+of the pure-Haskell `Data.Aeson` parser inadequate. This library focuses on+performance, not portability or absolute correctness. You should *only* use this+library if all of the following is true:++* You are using a system that is 64-bit. This library assumes+ `sizeof(size_t) == 8`. If this is not the case, the behavior is+ undefined.++* You are using a system where the alignment of `double` is no stricter than+ that of `size_t`. Internally, the library casts a pointer to `size_t` into a+ pointer to `double` and expects this to be meaningful.++* You do not expect to parse floating point numbers beyond the range of+ `double`.++* You do not expect to parse integers that are longer than 53 bits.++* You do not expect parsing a JSON number will produce a Haskell `Scientific`+ value that exactly represents the mathematical value of the JSON number, or+ even the closest approximation in IEEE-754 `double`.++ For example if you parse numbers that would be denormal numbers+ in IEEE-754 double-precision numbers, you will very likely get back zero+ instead.++ As another example, if you parse `0.3`, which cannot be represented+ exactly in IEEE-754 `double`, you will get back 0.30000000000000004, instead+ of the closest number in IEEE-754 `double` which is 0.29999999999999999.++* You wish to sacrifice memory consumption for speed. The *intermediate* memory+ needed is 9 times the number of bytes of the input `ByteString` plus+ constant. This excludes the memory needed by the parsed `Value` or the memory+ of the input `ByteString`.++* You are parsing JSON that is known to be valid. As a library written in C++,+ it is inherently unsafe. Despite the use of address sanitizer and AFL which+ have+ [caught memory usage bugs](https://github.com/chadaustin/sajson/commit/0bd8a661421fc3e61262c283543689b0f8a88483),+ it is difficult to guarantee that creatively crafted input will not crash or+ cause arbitrary code execution. It is recommend that you use this library+ only for parsing known good JSON.++If you satisfy all of the above, then this library could work for you. Again,+please benchmark and show that JSON parsing is indeed a bottleneck before using+this library.++## Ideas and Future Plan++* Skip creating `Value`s when the you eventually want another Haskell type.+ Currently the `FromJSON` instance needs a `Value`, but perhaps we can devise+ another method to avoid the generation of these intermediate `Value`s.++* Provide a benchmark so that we can quantitatively argue this library parses+ JSON faster than `Data.Aeson`.++* Provide an alternative for parsing floating point numbers that better+ preserves the value.++* More complete tests, including QuickCheck-based tests.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ bench/Bench.hs view
@@ -0,0 +1,34 @@+module Main where++import Control.Monad+import Criterion.Main+import Data.Aeson as A+import qualified Data.ByteString as B+import Data.Sajson as S+import System.Environment++dataFiles :: [String]+dataFiles =+ [+ "apache_builds.min.json",+ "github_events.min.json",+ "instruments.min.json",+ "mesh.min.json",+ "svg_menu.min.json",+ "twitter.min.json",+ "update-center.min.json"+ ]++parseWithAeson, parseWithSajson :: B.ByteString -> Maybe Value+parseWithAeson = A.decodeStrict'+parseWithSajson = S.decodeStrict++main :: IO ()+main = do+ mdir <- lookupEnv "SAJSON_BENCHMARK_DATA_DIR"+ let dir = maybe "./" (++"/") mdir+ benches <- forM dataFiles $ \f -> do+ let path = dir ++ f+ contents <- B.readFile path+ pure $ bgroup f [ bench "aeson" $ nf parseWithAeson contents, bench "sajson" $ nf parseWithSajson contents ]+ defaultMain benches
+ cbits/sajson_wrapper.cpp view
@@ -0,0 +1,60 @@+#include "sajson_wrapper.h"+#include "sajson.hpp"+#include <stdlib.h>+#include <type_traits>++// never instantiated, only inherits so static_cast is legal+struct sajson_document: sajson::document {};+struct sajson_value: sajson::value {};++static inline sajson::document* unwrap(sajson_document* doc) {+ return static_cast<sajson::document*>(doc);+}++static inline sajson_document* wrap(sajson::document* doc) {+ return static_cast<sajson_document*>(doc);+}++size_t sajson_document_sizeof(void) {+ return sizeof(sajson::document);+}++sajson_document* sajson_parse_single_allocation(char* str, size_t length, size_t *buffer, char *rv) {+ auto doc = sajson::parse(sajson::single_allocation(buffer, length), sajson::mutable_string_view(length, str));+ return wrap(new(rv) sajson::document(std::move(doc)));+}++void sajson_free_document(sajson_document* doc) {+ static_assert(std::is_trivially_destructible<sajson::document>::value, "sajson::document needs to have a trivial destructor because we do not call it; we just deallocate its storage");+ // According to the C++ Standard (section 3.8), you can end an object's lifetime by deallocating or reusing its storage. There's no need to call a destructor that does nothing. In this case we statically verify that it is trivially destructible.+ // unwrap(doc)->~document();+}++int sajson_has_error(sajson_document* doc) {+ return !unwrap(doc)->is_valid();+}++size_t sajson_get_error_line(sajson_document* doc) {+ return unwrap(doc)->get_error_line();+}++size_t sajson_get_error_column(sajson_document* doc) {+ return unwrap(doc)->get_error_column();+}++const char* sajson_get_error_message(sajson_document* doc) {+ return unwrap(doc)->get_error_message_as_cstring();+}++uint8_t sajson_get_root_type(sajson_document* doc) {+ return unwrap(doc)->_internal_get_root_type();+}++const size_t* sajson_get_root(sajson_document* doc) {+ return unwrap(doc)->_internal_get_root();+}++const unsigned char* sajson_get_input(sajson_document* doc) {+ return reinterpret_cast<const unsigned char*>(+ unwrap(doc)->_internal_get_input().get_data());+}
+ sajson.cabal view
@@ -0,0 +1,74 @@+name: sajson+version: 0.1.0.0+cabal-version: >=1.10+build-type: Simple+license: MIT+license-file: LICENSE+copyright: (c) 2012-2017 Chad Austin+ (c) 2017 Zhouyu Qian+maintainer: qzy@qzy.io+stability: experimental+homepage: https://github.com/kccqzy/haskell-sajson#readme+synopsis: Fast JSON parsing powered by Chad Austin's sajson library+description:+ A fast JSON parsing library that is faster than aeson.+category: Web, Text, JSON+author: Chad Austin, Zhouyu Qian+tested-with: GHC ==8.0.2+extra-source-files:+ README.md++source-repository head+ type: git+ location: https://github.com/kccqzy/haskell-sajson++library+ exposed-modules:+ Data.Sajson+ build-depends:+ base >=4.7 && <5,+ bytestring >=0.10.8.1 && <0.11,+ text >=1.2.2.1 && <1.3,+ aeson >=1.0.2.1 && <1.1,+ vector >=0.11.0.0 && <0.12,+ scientific >=0.3.4.12 && <0.4,+ unordered-containers >=0.2.8.0 && <0.3+ cc-options: -Wall -O3 -march=native -std=c++11 -fomit-frame-pointer+ c-sources:+ cbits/sajson_wrapper.cpp+ default-language: Haskell2010+ extra-libraries:+ stdc+++ include-dirs: cbits+ hs-source-dirs: src+ ghc-options: -Wall -O2++executable sajson-bench+ main-is: Bench.hs+ build-depends:+ base >=4.9.1.0 && <5,+ sajson,+ criterion >=1.1.4.0 && <1.2,+ aeson >=1.0.2.1 && <1.1,+ bytestring >=0.10.8.1 && <0.11+ default-language: Haskell2010+ hs-source-dirs: bench+ ghc-options: -Wall -threaded -rtsopts++test-suite sajson-test+ type: exitcode-stdio-1.0+ main-is: Spec.hs+ build-depends:+ base >=4.9.1.0 && <5,+ sajson,+ QuickCheck >=2.9.2 && <2.10,+ aeson >=1.0.2.1 && <1.1,+ bytestring >=0.10.8.1 && <0.11,+ hspec >=2.4.3 && <2.5,+ scientific >=0.3.4.12 && <0.4,+ text >=1.2.2.1 && <1.3,+ unordered-containers >=0.2.8.0 && <0.3,+ vector >=0.11.0.0 && <0.12+ default-language: Haskell2010+ hs-source-dirs: test+ ghc-options: -Wall -threaded -rtsopts
+ src/Data/Sajson.hs view
@@ -0,0 +1,235 @@+{-# LANGUAGE ForeignFunctionInterface #-}++-- |+-- Module: Data.Sajson+-- Copyright: (c) 2017 Zhouyu Qian+-- (c) 2012-2017 Chad Austin+-- License: MIT+-- Maintainer: Zhouyu Qian+--+-- A more efficient C++-based JSON parser.++module Data.Sajson+ ( -- * Introduction+ -- $introduction++ -- * Native APIs+ -- $native+ sajsonParse+ , unsafeMutableByteStringSajsonParse+ , SajsonParseError(..)++ -- * Drop-in Replacements+ -- $replacements+ , eitherDecodeStrict+ , decodeStrict+ , eitherDecode+ , decode+ ) where++import Control.Exception+import Control.Monad+import Data.Aeson.Types (FromJSON (..), Value (..), parseEither, parseMaybe)+import Data.Bits+import qualified Data.ByteString as B+import qualified Data.ByteString.Lazy as BL+import qualified Data.ByteString.Unsafe as BU+import qualified Data.HashMap.Strict as HM+import Data.Scientific+import qualified Data.Text as T+import qualified Data.Text.Encoding as TE+import qualified Data.Vector as V+import Data.Word+import Foreign.C.String+import Foreign.C.Types+import Foreign.Marshal.Alloc+import Foreign.Ptr+import Foreign.Storable+import System.IO.Unsafe++-- $introduction+-- This library is a high-performance parser for JSON. It is an alternative+-- to the native parsers in 'Data.Aeson'.+--+-- Before using this library, please carefully consider whether you actually+-- should use this library. This library is specifically designed for+-- Haskell users who find the performance of the pure-Haskell 'Data.Aeson'+-- parser inadequate. This library focuses on performance, not portability+-- or absolute correctness. You should /only/ use this library if all of the+-- following is true:+--+-- * You are using a system that is 64-bit.+-- * You are using a system where the alignment of @double@ is no stricter than+-- that of @size_t@.+-- * You do not expect to parse floating point numbers beyond the range of+-- `double`.+-- * You do not expect to parse integers that are longer than 53 bits.+-- * You do not expect parsing a JSON number will produce a Haskell+-- 'Data.Scientific.Scientific' value that exactly represents the+-- mathematical value of the JSON number, or even the closest+-- approximation in IEEE-754 `double`.+-- * You wish to sacrifice memory consumption for speed.+-- * You are parsing JSON that is known to be valid.+--+-- When all of the above are true, this library could work for you. Again,+-- please benchmark and show that JSON parsing is indeed a bottleneck before+-- using this library.++-- $native+-- A single function 'sajsonParse' is provided to access the parsing+-- functionality directly. However, you might instead want to use one of the+-- drop-in replacements below.++-- | Phantom type for a pointer to sajson_document.+data SajsonDocument++-- | A structure containing information about parse failures. The error line,+-- column, and messages are from the @sajson@ C++ library.+data SajsonParseError+ = SajsonParseError+ { sajsonParseErrorLine :: Int -- ^ The line where the error occurred.+ , sajsonParseErrorColumn :: Int -- ^ The column where the error occurred.+ , sajsonParseErrorMessage :: String -- ^ The error message. This is a canned message; it is not generated from the input.+ } deriving (Show, Eq)++type SajsonValueType = Word8+type SajsonValuePayload = Ptr CSize+type SajsonValueInputMutableView = Ptr CUChar++foreign import ccall unsafe "sajson_wrapper.h sajson_document_sizeof"+ c_sajson_document_sizeof :: CSize++foreign import ccall unsafe "sajson_wrapper.h sajson_parse_single_allocation"+ c_sajson_parse_single_allocation :: Ptr CChar -> CSize -> Ptr CSize -> Ptr CChar -> IO (Ptr SajsonDocument)++foreign import ccall unsafe "sajson_wrapper.h sajson_has_error"+ c_sajson_has_error :: Ptr SajsonDocument -> IO CInt++foreign import ccall unsafe "sajson_wrapper.h sajson_get_error_line"+ c_sajson_get_error_line :: Ptr SajsonDocument -> IO CSize++foreign import ccall unsafe "sajson_wrapper.h sajson_get_error_column"+ c_sajson_get_error_column :: Ptr SajsonDocument -> IO CSize++foreign import ccall unsafe "sajson_wrapper.h sajson_get_error_message"+ c_sajson_get_error_message :: Ptr SajsonDocument -> IO CString++foreign import ccall unsafe "sajson_wrapper.h sajson_get_root_type"+ c_sajson_get_root_type :: Ptr SajsonDocument -> IO SajsonValueType++foreign import ccall unsafe "sajson_wrapper.h sajson_get_root"+ c_sajson_get_root :: Ptr SajsonDocument -> IO SajsonValuePayload++foreign import ccall unsafe "sajson_wrapper.h sajson_get_input"+ c_sajson_get_input :: Ptr SajsonDocument -> IO SajsonValueInputMutableView++-- | Parse a 'CStringLen' into a 'Value' with the possibility of failing with+-- 'SajsonParseError'.+doParse :: CStringLen -> IO (Either SajsonParseError Value)+doParse (ptr, size) =+ allocaBytes (fromIntegral c_sajson_document_sizeof) $ \rvbuf ->+ allocaBytes (8 * size) $ \buf -> do+ doc <- c_sajson_parse_single_allocation ptr (fromIntegral size) buf rvbuf+ hasError <- c_sajson_has_error doc+ case hasError of+ 0 -> Right <$> join (constructHaskellValue+ <$> c_sajson_get_root_type doc+ <*> c_sajson_get_root doc+ <*> c_sajson_get_input doc)+ _ -> Left <$> (SajsonParseError+ <$> (fromIntegral <$> c_sajson_get_error_line doc)+ <*> (fromIntegral <$> c_sajson_get_error_column doc)+ <*> (c_sajson_get_error_message doc >>= peekCString))+{-# INLINE doParse #-}++-- | Parse a 'B.ByteString' into a 'Value' with the possibility of failing with+-- 'SajsonParseError'.+sajsonParse :: B.ByteString -> IO (Either SajsonParseError Value)+sajsonParse bs = B.useAsCStringLen bs doParse++unsafeMutableByteStringSajsonParse :: B.ByteString -> IO (Either SajsonParseError Value)+unsafeMutableByteStringSajsonParse bs = BU.unsafeUseAsCStringLen bs doParse++makeString :: SajsonValuePayload -> Int -> SajsonValueInputMutableView -> IO T.Text+makeString payload index buf = do+ start <- peekElemOff payload index+ end <- peekElemOff payload (1 + index)+ bs <- B.packCStringLen (buf `plusPtr` fromIntegral start, fromIntegral (end - start))+ pure (TE.decodeUtf8 bs)+{-# INLINE makeString #-}++makeValue :: SajsonValuePayload -> Int -> SajsonValueInputMutableView -> IO Value+makeValue payload index buf = do+ element <- peekElemOff payload index+ let vt = fromIntegral (element .&. 7)+ vp = payload `plusPtr` fromIntegral (8 * (element `shiftR` 3)) -- where 8 == sizeof(size_t)+ constructHaskellValue vt vp buf++constructHaskellValue :: SajsonValueType -> SajsonValuePayload -> SajsonValueInputMutableView -> IO Value+constructHaskellValue 0 payload _ = do -- TYPE_INTEGER+ i <- peekElemOff (castPtr payload) 0+ pure (Number (fromIntegral (i :: CInt)))+constructHaskellValue 1 payload _ = do -- TYPE_DOUBLE+ d <- peekElemOff (castPtr payload) 0+ pure (Number (fromFloatDigits (d :: CDouble)))+constructHaskellValue 2 _ _ = -- TYPE_NULL+ pure Null+constructHaskellValue 3 _ _ = -- TYPE_FALSE+ pure (Bool False)+constructHaskellValue 4 _ _ = -- TYPE_TRUE+ pure (Bool True)+constructHaskellValue 5 payload buf = -- TYPE_STRING+ String <$> makeString payload 0 buf+constructHaskellValue 6 payload buf = do -- TYPE_ARRAY+ len <- peekElemOff payload 0+ (Array <$>) . V.generateM (fromIntegral len) $ \i -> makeValue payload (1 + i) buf+constructHaskellValue 7 payload buf = do -- TYPE_OBJECT+ len <- peekElemOff payload 0+ Object <$> V.foldM' go HM.empty (V.enumFromN 0 (fromIntegral len))+ where go hm i = do+ key <- makeString payload (1 + i * 3) buf+ val <- makeValue payload (3 + i * 3) buf+ pure $ HM.insert key val hm+constructHaskellValue vt _ _ = throwIO (ErrorCall $ "Data.Sajson internal error: unexpected sajson type tag: " ++ show vt)++-- $replacements+-- These functions are (almost) drop-in replacements for the equivalently named+-- functions in 'Data.Aeson', with the caveat that the outermost structure must+-- be either an 'Object' or an 'Array'.++toDataEither :: FromJSON a => Value -> Either String a+toDataEither = parseEither parseJSON++toData :: FromJSON a => Value -> Maybe a+toData = parseMaybe parseJSON++describeSajsonParseError :: SajsonParseError -> String+describeSajsonParseError (SajsonParseError line col err) = "Error in sajson parser: line " ++ show line ++ " column " ++ show col ++ ": " ++ err++-- | Efficiently deserialize a JSON value from a strict 'B.ByteString'. If this+-- fails, 'Left' is returned with a 'String' describing the issue.+eitherDecodeStrict :: FromJSON a => B.ByteString -> Either String a+eitherDecodeStrict bs = unsafePerformIO $ either (Left . describeSajsonParseError) toDataEither <$> sajsonParse bs++-- | Efficiently deserialize a JSON value from a strict 'B.ByteString'. If this+-- fails, 'Nothing' is returned.+decodeStrict :: FromJSON a => B.ByteString -> Maybe a+decodeStrict bs = unsafePerformIO $ either (pure Nothing) toData <$> sajsonParse bs++-- | Deserialize a JSON value from a lazy 'BL.ByteString'. If this fails, 'Left'+-- is returned with a 'String' describing the issue.+--+-- It should be noted that this function first converts the lazy 'BL.ByteString'+-- into a strict 'B.ByteString'. It does not allow streaming. Therefore it+-- requires at least twice the total size of the 'BL.ByteString' to be in memory.+eitherDecode :: FromJSON a => BL.ByteString -> Either String a+eitherDecode = eitherDecodeStrict . BL.toStrict++-- | Deserialize a JSON value from a lazy 'BL.ByteString'. If this+-- fails, 'Nothing' is returned.+--+-- It should be noted that this function first converts the lazy 'BL.ByteString'+-- into a strict 'B.ByteString'. It does not allow streaming. Therefore it+-- requires at least twice the total size of the 'BL.ByteString' to be in memory.+decode :: FromJSON a => BL.ByteString -> Maybe a+decode = decodeStrict . BL.toStrict
+ test/Spec.hs view
@@ -0,0 +1,82 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RankNTypes #-}++module Main where++import Data.Aeson as A+import qualified Data.ByteString.Lazy.Char8 as BL+import qualified Data.HashMap.Strict as HM+import Data.Sajson as S+import Data.Scientific+import Data.String+import qualified Data.Text as T+import qualified Data.Vector as V+import Test.Hspec+import Test.QuickCheck++newtype AcceptableValue = AcceptableValue { getAcceptableValue :: Value } deriving (Show, Eq)++instance Arbitrary AcceptableValue where+ arbitrary = AcceptableValue <$> oneof [Array <$> genArray, Object <$> genObject]+ where genText = T.pack <$> listOf (oneof [choose (' ', '~'), choose ('\256', '\383'), choose ('\19968', '\40959'), choose ('\128512', '\128591')]) -- latin text, CJK ideographs, and emoji+ genNumber = fromIntegral <$> choose (-9007199254740992, 9007199254740992 :: Int)+ genArray = V.fromList <$> reducedSize (listOf genValue)+ genObject = HM.fromList <$> reducedSize (listOf ((,) <$> genText <*> genValue))+ genValue = oneof [Number <$> genNumber, pure Null, Bool <$> arbitrary, String <$> genText, Array <$> genArray, Object <$> genObject]+ reducedSize = scale (`div` 2)++main :: IO ()+main = hspec $ do+ describe "sajsonParse" $ do+ it "returns proper error for truncated JSON document" $ do+ r <- sajsonParse "{\"abc\":123,\"def\":[1,2,3"+ r `shouldBe` Left SajsonParseError {sajsonParseErrorLine = 1, sajsonParseErrorColumn = 24, sajsonParseErrorMessage = "unexpected end of input"}+ describe "returns Right for correct JSON document" $ do+ it "object of null" $ do+ r <- sajsonParse "{\"abc\":null}"+ r `shouldBe` Right (object ["abc" .= Null])+ it "object of true" $ do+ r <- sajsonParse "{\"abc\":true}"+ r `shouldBe` Right (object ["abc" .= Bool True])+ it "object of false" $ do+ r <- sajsonParse "{\"abc\":false}"+ r `shouldBe` Right (object ["abc" .= Bool False])+ it "object of integer" $ do+ r <- sajsonParse "{\"abc\":2147483647}"+ r `shouldBe` Right (object ["abc" .= Number 2147483647])+ it "object of double" $ do+ r <- sajsonParse "{\"abc\":3.141592653589793}"+ r `shouldBe` Right (object ["abc" .= Number 3.141592653589793])+ it "object of big integer (exponential notation)" $ do+ r <- sajsonParse "{\"abc\":1e308}"+ r `shouldBe` Right (object ["abc" .= Number (scientific 1 308)])+ it "object of array of number" $ do+ r <- sajsonParse "{\"abc\":[1,2,3],\"def\":[0.1,0.2]}"+ r `shouldBe` Right (object ["abc" .= [1.0 :: Scientific, 2.0, 3.0], "def" .= [0.1 :: Scientific, 0.2]])+ it "round trips acceptable values with aeson" $ property $ \(AcceptableValue v) -> ioProperty $ do+ let encoded = encode v+ decoded <- sajsonParse (BL.toStrict encoded)+ decoded `shouldBe` Right v+ pure True++ let testEither :: IsString s => (forall a. FromJSON a => s -> Either String a) -> (forall a. FromJSON a => s -> Either String a) -> String -> Spec+ testEither f f' n = describe n $ do+ it "handles incorrect JSON" $+ f "[0" `shouldBe` (Left "Error in sajson parser: line 1 column 3: unexpected end of input" :: Either String [Int])+ it "handles correct JSON but incorrect schema" $ do+ let s = "{\"a\":42}"+ f s `shouldBe` (f' s :: Either String [Int])++ testMaybe :: IsString s => (forall a. FromJSON a => s -> Maybe a) -> String -> Spec+ testMaybe f n = describe n $ do+ it "handles incorrect JSON" $+ f "[0" `shouldBe` (Nothing :: Maybe [Int])+ it "handles correct JSON but incorrect schema" $ do+ let s = "{\"a\":42}"+ f s `shouldBe` (Nothing :: Maybe [Int])++ testEither S.eitherDecodeStrict A.eitherDecodeStrict "eitherDecodeStrict"+ testEither S.eitherDecode A.eitherDecode "eitherDecode"++ testMaybe S.decodeStrict "decodeStrict"+ testMaybe S.decode "decode"