on master help sign in
Merge
commit65ec21a18b67c35798657c2b46a33b4f6e4e44d2 authorBryan O'Sullivan <bos@serpentine.com> dateWed 8 Jan 2014, 15:11 (13 years ago) parentdeb40c65e6c915
7 files changed+255−48
Data/Aeson/Encode.hs+56−37 Data/Aeson/Encode/ByteString.hs+116 Data/Aeson/Types/Internal.hs+1−1 aeson.cabal+14−2 benchmarks/CompareWithJSON.hs+16−2 benchmarks/aeson-benchmarks.cabal+2−1 tests/Properties.hs+50−5
Data/Aeson/Encode.hs+56−37
@@ -1,4 +1,4 @@
1−{-# LANGUAGE BangPatterns, OverloadedStrings #-}
1+{-# LANGUAGE CPP, BangPatterns, OverloadedStrings #-}
22
33-- |
44-- Module: Data.Aeson.Encode
@@ -14,49 +14,74 @@
1414-- Most frequently, you'll probably want to encode straight to UTF-8
1515-- (the standard JSON encoding) using 'encode'.
1616--
17−-- You can convert a 'Builder' (as returned by 'fromValue') to a
18−-- string using e.g. 'toLazyText'.
19−
17+-- You can use the conversions to 'Builder's when embedding JSON messages as
18+-- parts of a protocol.
2019module Data.Aeson.Encode
21− (
22− fromValue
23− , encode
20+ ( encode
21+
22+#if MIN_VERSION_bytestring(0,10,4)
23+ -- * Encoding to Builders
24+ , encodeToByteStringBuilder
25+ , encodeToTextBuilder
26+#else
27+ -- * Encoding to Text Builders
28+ , encodeToTextBuilder
29+#endif
30+
31+ -- * Deprecated
32+ , fromValue
2433 ) where
2534
26−import Data.Aeson.Types (ToJSON(..), Value(..))
35+import Data.Aeson.Types (Value(..))
2736import Data.Monoid (mappend)
2837import Data.Scientific (Scientific, coefficient, base10Exponent, scientificBuilder)
2938import Data.Text.Lazy.Builder
3039import Data.Text.Lazy.Builder.Int (decimal)
31−import Data.Text.Lazy.Encoding (encodeUtf8)
3240import Numeric (showHex)
33−import qualified Data.ByteString.Lazy as L
3441import qualified Data.HashMap.Strict as H
3542import qualified Data.Text as T
3643import qualified Data.Vector as V
3744
38−-- | Encode a JSON value to a 'Builder'. You can convert this to a
39−-- string using e.g. 'toLazyText', or encode straight to UTF-8 (the
40−-- standard JSON encoding) using 'encode'.
45+#if MIN_VERSION_bytestring(0,10,4)
46+import Data.Aeson.Encode.ByteString (encode, encodeToByteStringBuilder)
47+#else
48+import Data.Aeson.Types (ToJSON(toJSON))
49+import qualified Data.ByteString.Lazy as BL
50+import qualified Data.Text.Lazy.Builder as TLB
51+import qualified Data.Text.Lazy.Encoding as TLE
52+
53+-- | Encode a JSON 'Value' as a UTF-8 encoded 'BL.ByteString'.
54+encode :: ToJSON a => a -> BL.ByteString
55+encode = TLE.encodeUtf8 . TLB.toLazyText . encodeToTextBuilder . toJSON
56+#endif
57+
58+-- | Encode a JSON 'Value' to a 'Builder', which can be embedded efficiently
59+-- in a text-based protocol.
60+encodeToTextBuilder :: Value -> Builder
61+encodeToTextBuilder =
62+ go
63+ where
64+ go Null = {-# SCC "go/Null" #-} "null"
65+ go (Bool b) = {-# SCC "go/Bool" #-} if b then "true" else "false"
66+ go (Number s) = {-# SCC "go/Number" #-} fromScientific s
67+ go (String s) = {-# SCC "go/String" #-} string s
68+ go (Array v)
69+ | V.null v = {-# SCC "go/Array" #-} "[]"
70+ | otherwise = {-# SCC "go/Array" #-}
71+ singleton '[' <>
72+ go (V.unsafeHead v) <>
73+ V.foldr f (singleton ']') (V.unsafeTail v)
74+ where f a z = singleton ',' <> go a <> z
75+ go (Object m) = {-# SCC "go/Object" #-}
76+ case H.toList m of
77+ (x:xs) -> singleton '{' <> one x <> foldr f (singleton '}') xs
78+ _ -> "{}"
79+ where f a z = singleton ',' <> one a <> z
80+ one (k,v) = string k <> singleton ':' <> go v
81+
82+{-# DEPRECATED fromValue "Use 'encodeToTextBuilder' instead" #-}
4183fromValue :: Value -> Builder
42−fromValue Null = {-# SCC "fromValue/Null" #-} "null"
43−fromValue (Bool b) = {-# SCC "fromValue/Bool" #-}
44− if b then "true" else "false"
45−fromValue (Number s) = {-# SCC "fromValue/Number" #-} fromScientific s
46−fromValue (String s) = {-# SCC "fromValue/String" #-} string s
47−fromValue (Array v)
48− | V.null v = {-# SCC "fromValue/Array" #-} "[]"
49− | otherwise = {-# SCC "fromValue/Array" #-}
50− singleton '[' <>
51− fromValue (V.unsafeHead v) <>
52− V.foldr f (singleton ']') (V.unsafeTail v)
53− where f a z = singleton ',' <> fromValue a <> z
54−fromValue (Object m) = {-# SCC "fromValue/Object" #-}
55− case H.toList m of
56− (x:xs) -> singleton '{' <> one x <> foldr f (singleton '}') xs
57− _ -> "{}"
58− where f a z = singleton ',' <> one a <> z
59− one (k,v) = string k <> singleton ':' <> fromValue v
84+fromValue = encodeToTextBuilder
6085
6186string :: T.Text -> Builder
6287string s = {-# SCC "string" #-} singleton '"' <> quote s <> singleton '"'
@@ -93,12 +118,6 @@ fromScientific s
93118 where
94119 e = base10Exponent s
95120
96−-- | Efficiently serialize a JSON value as a lazy 'L.ByteString'.
97−encode :: ToJSON a => a -> L.ByteString
98−encode = {-# SCC "encode" #-} encodeUtf8 . toLazyText . fromValue .
99− {-# SCC "toJSON" #-} toJSON
100−{-# INLINE encode #-}
101−
102121(<>) :: Builder -> Builder -> Builder
103122(<>) = mappend
104123{-# INLINE (<>) #-}
Data/Aeson/Encode/ByteString.hsnew file+116
@@ -0,0 +1,116 @@
1+{-# LANGUAGE BangPatterns, OverloadedStrings #-}
2+
3+-- |
4+-- Module: Data.Aeson.EncodeUtf8
5+-- Copyright: (c) 2011 MailRank, Inc.
6+-- (c) 2013 Simon Meier <iridcode@gmail.com>
7+-- License: Apache
8+-- Maintainer: Bryan O'Sullivan <bos@serpentine.com>
9+-- Stability: experimental
10+-- Portability: portable
11+--
12+-- Efficiently serialize a JSON value using the UTF-8 encoding.
13+
14+module Data.Aeson.Encode.ByteString
15+ ( encode
16+ , encodeToByteStringBuilder
17+ ) where
18+
19+import Prelude hiding (null)
20+import Data.Aeson.Types (ToJSON(..), Value(..))
21+import Data.Char (ord)
22+import Data.Scientific (Scientific, coefficient, base10Exponent, formatScientific, FPFormat(Generic))
23+import Data.Word (Word8)
24+import Data.Monoid (mappend)
25+import Data.ByteString.Builder as B
26+import Data.ByteString.Builder.Prim as BP
27+import qualified Data.ByteString.Lazy as L
28+import qualified Data.HashMap.Strict as HMS
29+import qualified Data.Text as T
30+import qualified Data.Text.Encoding as TE
31+import qualified Data.Vector as V
32+
33+(<>) :: Builder -> Builder -> Builder
34+(<>) = mappend
35+{-# INLINE (<>) #-}
36+infixr 6 <>
37+
38+-- | Efficiently serialize a JSON value as a lazy 'L.ByteString'.
39+encode :: ToJSON a => a -> L.ByteString
40+encode = B.toLazyByteString . encodeToByteStringBuilder . toJSON
41+
42+-- | Encode a JSON value to a ByteString 'B.Builder'. Use this function if you
43+-- must prepend or append further bytes to the encoded JSON value.
44+encodeToByteStringBuilder :: Value -> Builder
45+encodeToByteStringBuilder Null = null
46+encodeToByteStringBuilder (Bool b) = bool b
47+encodeToByteStringBuilder (Number n) = number n
48+encodeToByteStringBuilder (String s) = string s
49+encodeToByteStringBuilder (Array v) = array v
50+encodeToByteStringBuilder (Object m) = object m
51+
52+null :: Builder
53+null = BP.primBounded (ascii4 ('n',('u',('l','l')))) ()
54+
55+bool :: Bool -> Builder
56+bool = BP.primBounded (BP.condB id (ascii4 ('t',('r',('u','e'))))
57+ (ascii5 ('f',('a',('l',('s','e'))))))
58+
59+array :: V.Vector Value -> Builder
60+array v
61+ | V.null v = B.char8 '[' <> B.char8 ']'
62+ | otherwise = B.char8 '[' <>
63+ encodeToByteStringBuilder (V.unsafeHead v) <>
64+ V.foldr withComma (B.char8 ']') (V.unsafeTail v)
65+ where
66+ withComma a z = B.char8 ',' <> encodeToByteStringBuilder a <> z
67+
68+object :: HMS.HashMap T.Text Value -> Builder
69+object m = case HMS.toList m of
70+ (x:xs) -> B.char8 '{' <> one x <> foldr withComma (B.char8 '}') xs
71+ _ -> B.char8 '{' <> B.char8 '}'
72+ where
73+ withComma a z = B.char8 ',' <> one a <> z
74+ one (k,v) = string k <> B.char8 ':' <> encodeToByteStringBuilder v
75+
76+string :: T.Text -> B.Builder
77+string t =
78+ B.char8 '"' <> TE.encodeUtf8BuilderEscaped escapeAscii t <> B.char8 '"'
79+ where
80+ escapeAscii :: BP.BoundedPrim Word8
81+ escapeAscii =
82+ BP.condB (== c2w '\\' ) (ascii2 ('\\','\\')) $
83+ BP.condB (== c2w '\"' ) (ascii2 ('\\','"' )) $
84+ BP.condB (>= c2w '\x20') (BP.liftFixedToBounded BP.word8) $
85+ BP.condB (== c2w '\n' ) (ascii2 ('\\','n' )) $
86+ BP.condB (== c2w '\r' ) (ascii2 ('\\','r' )) $
87+ BP.condB (== c2w '\t' ) (ascii2 ('\\','t' )) $
88+ (BP.liftFixedToBounded hexEscape) -- fallback for chars < 0x20
89+
90+ c2w = fromIntegral . ord
91+
92+ hexEscape :: BP.FixedPrim Word8
93+ hexEscape = (\c -> ('\\', ('u', fromIntegral c))) BP.>$<
94+ BP.char8 BP.>*< BP.char8 BP.>*< BP.word16HexFixed
95+
96+number :: Scientific -> Builder
97+number s
98+ | e < 0 = B.string8 $ formatScientific Generic Nothing s
99+ | otherwise = B.integerDec (coefficient s * 10 ^ e)
100+ where
101+ e = base10Exponent s
102+
103+
104+{-# INLINE ascii2 #-}
105+ascii2 :: (Char, Char) -> BP.BoundedPrim a
106+ascii2 cs = BP.liftFixedToBounded $ (const cs) BP.>$< BP.char7 BP.>*< BP.char7
107+
108+{-# INLINE ascii4 #-}
109+ascii4 :: (Char, (Char, (Char, Char))) -> BP.BoundedPrim a
110+ascii4 cs = BP.liftFixedToBounded $ (const cs) >$<
111+ BP.char7 BP.>*< BP.char7 BP.>*< BP.char7 BP.>*< BP.char7
112+
113+{-# INLINE ascii5 #-}
114+ascii5 :: (Char, (Char, (Char, (Char, Char)))) -> BP.BoundedPrim a
115+ascii5 cs = BP.liftFixedToBounded $ (const cs) >$<
116+ BP.char7 BP.>*< BP.char7 BP.>*< BP.char7 BP.>*< BP.char7 BP.>*< BP.char7
Data/Aeson/Types/Internal.hs+1−1
@@ -180,7 +180,7 @@ data Value = Object !Object
180180 | Number !Scientific
181181 | Bool !Bool
182182 | Null
183− deriving (Eq, Show, Typeable, Data)
183+ deriving (Eq, Show, Typeable)
184184
185185-- | A newtype wrapper for 'UTCTime' that uses the same non-standard
186186-- serialization format as Microsoft .NET, whose @System.DateTime@
aeson.cabal+14−2
@@ -92,6 +92,10 @@ flag blaze-builder
9292 description: Use blaze-builder instead of bytestring >= 0.10
9393 default: False
9494
95+flag new-bytestring-builder
96+ description: Use the new bytestring builder available in bytestring >= 0.10.4.0
97+ default: False
98+
9599library
96100 exposed-modules:
97101 Data.Aeson
@@ -108,6 +112,14 @@ library
108112 Data.Aeson.Types.Instances
109113 Data.Aeson.Types.Internal
110114
115+ if flag(new-bytestring-builder)
116+ other-modules: Data.Aeson.Encode.ByteString
117+ build-depends: bytestring >= 0.10.4.0,
118+ text >= 1.1.0.0
119+ else
120+ build-depends: bytestring < 0.10.4.0,
121+ text >= 0.11.1.0
122+
111123 if impl(ghc >= 7.2.1)
112124 cpp-options: -DGENERICS
113125 build-depends: ghc-prim >= 0.2, dlist >= 0.2
@@ -117,7 +129,6 @@ library
117129 build-depends:
118130 attoparsec >= 0.11.1.0,
119131 base == 4.*,
120− bytestring,
121132 containers,
122133 deepseq,
123134 hashable >= 1.1.2.0,
@@ -125,7 +136,6 @@ library
125136 old-locale,
126137 syb,
127138 template-haskell >= 2.4,
128− text >= 0.11.1.0,
129139 time,
130140 unordered-containers >= 0.1.3.0,
131141 vector >= 0.7.1,
@@ -172,6 +182,8 @@ test-suite tests
172182 template-haskell,
173183 test-framework,
174184 test-framework-quickcheck2,
185+ test-framework-hunit,
186+ HUnit,
175187 text,
176188 time,
177189 unordered-containers,
benchmarks/CompareWithJSON.hs+16−2
@@ -5,9 +5,13 @@ import Blaze.ByteString.Builder (toLazyByteString)
55import Blaze.ByteString.Builder.Char.Utf8 (fromString)
66import Control.DeepSeq (NFData(rnf))
77import Criterion.Main
8+import qualified Data.Aeson.Encode as A
89import qualified Data.Aeson as A
910import qualified Data.ByteString.Lazy as BL
1011import qualified Text.JSON as J
12+import qualified Data.Text.Lazy as TL
13+import qualified Data.Text.Lazy.Builder as TLB
14+import qualified Data.Text.Lazy.Encoding as TLE
1115
1216#if !MIN_VERSION_bytestring(0,10,0)
1317import qualified Data.ByteString.Lazy.Internal as BL
@@ -42,6 +46,12 @@ decodeA s = case A.decode s of
4246encodeJ :: J.JSValue -> BL.ByteString
4347encodeJ = toLazyByteString . fromString . J.encode
4448
49+encodeToText :: A.Value -> TL.Text
50+encodeToText = TLB.toLazyText . A.encodeToTextBuilder . A.toJSON
51+
52+encodeViaText :: A.Value -> BL.ByteString
53+encodeViaText = TLE.encodeUtf8 . encodeToText
54+
4555main :: IO ()
4656main = do
4757 let enFile = "json-data/twitter100.json"
@@ -63,11 +73,15 @@ main = do
6373 ]
6474 , bgroup "encode" [
6575 bgroup "en" [
66− bench "aeson" $ nf A.encode (decodeA enA)
76+ bench "aeson-to-bytestring" $ nf A.encode (decodeA enA)
77+ , bench "aeson-via-text-to-bytestring" $ nf encodeViaText (decodeA enA)
78+ , bench "aeson-to-text" $ nf encodeToText (decodeA enA)
6779 , bench "json" $ nf encodeJ (decodeJ enJ)
6880 ]
6981 , bgroup "jp" [
70− bench "aeson" $ nf A.encode (decodeA jpA)
82+ bench "aeson-to-bytestring" $ nf A.encode (decodeA jpA)
83+ , bench "aeson-via-text-to-bytestring" $ nf encodeViaText (decodeA jpA)
84+ , bench "aeson-to-text" $ nf encodeToText (decodeA jpA)
7185 , bench "json" $ nf encodeJ (decodeJ jpJ)
7286 ]
7387 ]
benchmarks/aeson-benchmarks.cabal+2−1
@@ -13,7 +13,8 @@ executable aeson-benchmark-compare-with-json
1313 blaze-builder,
1414 bytestring,
1515 criterion,
16− json
16+ json,
17+ text
1718
1819executable aeson-benchmark-aeson-encode
1920 main-is: AesonEncode.hs
tests/Properties.hs+50−5
@@ -1,15 +1,21 @@
11{-# LANGUAGE CPP, OverloadedStrings, RecordWildCards, ScopedTypeVariables #-}
22
3+import Control.Monad (forM)
4+import Data.Aeson (eitherDecode)
35import Data.Aeson.Encode
46import Data.Aeson.Parser (value)
57import Data.Aeson.Types
68import Test.Framework (Test, defaultMain, testGroup)
79import Test.Framework.Providers.QuickCheck2 (testProperty)
10+import Test.Framework.Providers.HUnit (testCase)
11+import Test.HUnit (assertFailure, assertEqual)
812import Test.QuickCheck (Arbitrary(..))
913import qualified Data.Vector as V
1014import qualified Data.Attoparsec.Lazy as L
1115import qualified Data.ByteString.Lazy.Char8 as L
1216import qualified Data.Text as T
17+import qualified Data.Text.Lazy.Builder as TLB
18+import qualified Data.Text.Lazy.Encoding as TLE
1319import qualified Data.HashMap.Strict as H
1420import Data.Time.Clock (UTCTime(..))
1521import Data.Time (ZonedTime(..))
@@ -22,11 +28,13 @@ import Data.Int
2228import qualified Data.Map as Map
2329#endif
2430
31+{-
2532roundTripCaml :: String -> Bool
2633roundTripCaml s = s == (camlFrom '_' $ camlTo '_' s)
2734 where
2835 camlFrom :: Char -> String -> String
2936 camlFrom c = concatMap capitalize $ split c
37+-}
3038
3139encodeDouble :: Double -> Double -> Bool
3240encodeDouble num denom
@@ -66,7 +74,9 @@ modifyFailureProp orig added =
6674 result = parse parser ()
6775
6876main :: IO ()
69−main = defaultMain tests
77+main = do
78+ comparisonTest <- encoderComparisonTests
79+ defaultMain (comparisonTest : tests)
7080
7181#ifdef GHC_GENERICS
7282type P6 = Product6 Int Bool String (Approx Double) (Int, Approx Double) ()
@@ -106,10 +116,10 @@ tests = [
106116 testProperty "encodeDouble" encodeDouble
107117 , testProperty "encodeInteger" encodeInteger
108118 ],
109− testGroup "camlCase" [
110− testProperty "camlTo" $ roundTripCaml "AnApiMethod"
111− , testProperty "camlTo" $ roundTripCaml "anotherMethodType"
112− ],
119+ -- testGroup "camlCase" [
120+ -- testProperty "camlTo" $ roundTripCaml "AnApiMethod"
121+ -- , testProperty "camlTo" $ roundTripCaml "anotherMethodType"
122+ -- ],
113123 testGroup "roundTrip" [
114124 testProperty "Bool" $ roundTripEq True
115125 , testProperty "Double" $ roundTripEq (1 :: Approx Double)
@@ -206,3 +216,38 @@ tests = [
206216 ]
207217#endif
208218 ]
219+
220+
221+------------------------------------------------------------------------------
222+-- Comparison between bytestring and text encoders
223+------------------------------------------------------------------------------
224+
225+encoderComparisonTests :: IO Test
226+encoderComparisonTests = do
227+ encoderTests <- forM testFiles $ \file0 -> do
228+ let file = "benchmarks/json-data/" ++ file0
229+ return $ testCase file $ do
230+ inp <- L.readFile file
231+ case eitherDecode inp of
232+ Left err -> assertFailure $ "Decoding failure: " ++ err
233+ Right val -> assertEqual "" (encode val) (encodeViaText val)
234+ return $ testGroup "Compare bytestring and text encoders" encoderTests
235+ where
236+ encodeViaText :: Value -> L.ByteString
237+ encodeViaText =
238+ TLE.encodeUtf8 . TLB.toLazyText . encodeToTextBuilder . toJSON
239+
240+ testFiles =
241+ [ "example.json"
242+ , "integers.json"
243+ , "jp100.json"
244+ , "numbers.json"
245+ , "twitter10.json"
246+ , "twitter20.json"
247+ , "geometry.json"
248+ , "jp10.json"
249+ , "jp50.json"
250+ , "twitter1.json"
251+ , "twitter100.json"
252+ , "twitter50.json"
253+ ]