on master help sign in
Merge pull request #714 from Lysxia/dupkeys
8df0981Adam Bergmark
Data/Aeson/Parser.hs+11−1
@@ -35,10 +35,20 @@ module Data.Aeson.Parser
35 , value
35 , value
36 , jstring
36 , jstring
37 , scientific
37 , scientific
38+ -- ** Handling objects with duplicate keys
39+ , jsonWith
40+ , jsonLast
41+ , jsonAccum
42+ , jsonNoDup
38 -- * Strict parsers
43 -- * Strict parsers
39 -- $strict
44 -- $strict
40 , json'
45 , json'
41 , value'
46 , value'
47+ -- ** Handling objects with duplicate keys
48+ , jsonWith'
49+ , jsonLast'
50+ , jsonAccum'
51+ , jsonNoDup'
42 -- * Decoding without FromJSON instances
52 -- * Decoding without FromJSON instances
43 , decodeWith
53 , decodeWith
44 , decodeStrictWith
54 , decodeStrictWith
@@ -47,7 +57,7 @@ module Data.Aeson.Parser
47 ) where
57 ) where
48
58
49
59
50−import Data.Aeson.Parser.Internal (decodeStrictWith, decodeWith, eitherDecodeStrictWith, eitherDecodeWith, json, json', jstring, scientific, value, value')
60+import Data.Aeson.Parser.Internal
51
61
52-- $lazy
62-- $lazy
53--
63--
Data/Aeson/Parser/Internal.hs+149−34
@@ -5,6 +5,10 @@
5#if MIN_VERSION_ghc_prim(0,3,1)
5#if MIN_VERSION_ghc_prim(0,3,1)
6{-# LANGUAGE MagicHash #-}
6{-# LANGUAGE MagicHash #-}
7#endif
7#endif
8+#if __GLASGOW_HASKELL__ <= 710 && __GLASGOW_HASKELL__ >= 706
9+-- Work around a compiler bug
10+{-# OPTIONS_GHC -fsimpl-tick-factor=200 #-}
11+#endif
8-- |
12-- |
9-- Module: Data.Aeson.Parser.Internal
13-- Module: Data.Aeson.Parser.Internal
10-- Copyright: (c) 2011-2016 Bryan O'Sullivan
14-- Copyright: (c) 2011-2016 Bryan O'Sullivan
@@ -21,30 +25,43 @@ module Data.Aeson.Parser.Internal
21 (
25 (
22 -- * Lazy parsers
26 -- * Lazy parsers
23 json, jsonEOF
27 json, jsonEOF
28+ , jsonWith
29+ , jsonLast
30+ , jsonAccum
31+ , jsonNoDup
24 , value
32 , value
25 , jstring
33 , jstring
26 , jstring_
34 , jstring_
27 , scientific
35 , scientific
28 -- * Strict parsers
36 -- * Strict parsers
29 , json', jsonEOF'
37 , json', jsonEOF'
38+ , jsonWith'
39+ , jsonLast'
40+ , jsonAccum'
41+ , jsonNoDup'
30 , value'
42 , value'
31 -- * Helpers
43 -- * Helpers
32 , decodeWith
44 , decodeWith
33 , decodeStrictWith
45 , decodeStrictWith
34 , eitherDecodeWith
46 , eitherDecodeWith
35 , eitherDecodeStrictWith
47 , eitherDecodeStrictWith
48+ -- ** Handling objects with duplicate keys
49+ , fromListAccum
50+ , parseListNoDup
36 ) where
51 ) where
37
52
38import Prelude.Compat
53import Prelude.Compat
39
54
40import Control.Applicative ((<|>))
55import Control.Applicative ((<|>))
41import Control.Monad (void, when)
56import Control.Monad (void, when)
42−import Data.Aeson.Types.Internal (IResult(..), JSONPath, Result(..), Value(..))
57+import Data.Aeson.Types.Internal (IResult(..), JSONPath, Object, Result(..), Value(..))
43import Data.Attoparsec.ByteString.Char8 (Parser, char, decimal, endOfInput, isDigit_w8, signed, string)
58import Data.Attoparsec.ByteString.Char8 (Parser, char, decimal, endOfInput, isDigit_w8, signed, string)
59+import Data.Function (fix)
44import Data.Functor.Compat (($>))
60import Data.Functor.Compat (($>))
45import Data.Scientific (Scientific)
61import Data.Scientific (Scientific)
46import Data.Text (Text)
62import Data.Text (Text)
63+import Data.Vector (Vector)
47−import Data.Vector as Vector (Vector, empty, fromListN, reverse)
64+import qualified Data.Vector as Vector (empty, fromList, fromListN, reverse)
48import qualified Data.Attoparsec.ByteString as A
65import qualified Data.Attoparsec.ByteString as A
49import qualified Data.Attoparsec.Lazy as L
66import qualified Data.Attoparsec.Lazy as L
50import qualified Data.ByteString as B
67import qualified Data.ByteString as B
@@ -76,7 +93,7 @@ import qualified Data.Text.Encoding as TE
76#define C_n 110
93#define C_n 110
77#define C_t 116
94#define C_t 116
78
95
79−-- | Parse a top-level JSON value.
96+-- | Parse any JSON value.
80--
97--
81-- The conversion of a parsed value to a Haskell value is deferred
98-- The conversion of a parsed value to a Haskell value is deferred
82-- until the Haskell value is needed. This may improve performance if
99-- until the Haskell value is needed. This may improve performance if
@@ -86,10 +103,15 @@ import qualified Data.Text.Encoding as TE
86-- This function is an alias for 'value'. In aeson 0.8 and earlier, it
103-- This function is an alias for 'value'. In aeson 0.8 and earlier, it
87-- parsed only object or array types, in conformance with the
104-- parsed only object or array types, in conformance with the
88-- now-obsolete RFC 4627.
105-- now-obsolete RFC 4627.
106+--
107+-- ==== Warning
108+--
109+-- If an object contains duplicate keys, only the first one will be kept.
110+-- For a more flexible alternative, see 'jsonWith'.
89json :: Parser Value
111json :: Parser Value
90json = value
112json = value
91
113
92−-- | Parse a top-level JSON value.
114+-- | Parse any JSON value.
93--
115--
94-- This is a strict version of 'json' which avoids building up thunks
116-- This is a strict version of 'json' which avoids building up thunks
95-- during parsing; it performs all conversions immediately. Prefer
117-- during parsing; it performs all conversions immediately. Prefer
@@ -98,23 +120,38 @@ json = value
98-- This function is an alias for 'value''. In aeson 0.8 and earlier, it
120-- This function is an alias for 'value''. In aeson 0.8 and earlier, it
99-- parsed only object or array types, in conformance with the
121-- parsed only object or array types, in conformance with the
100-- now-obsolete RFC 4627.
122-- now-obsolete RFC 4627.
123+--
124+-- ==== Warning
125+--
126+-- If an object contains duplicate keys, only the first one will be kept.
127+-- For a more flexible alternative, see 'jsonWith''.
101json' :: Parser Value
128json' :: Parser Value
102json' = value'
129json' = value'
103
130
131+-- Open recursion: object_, object_', array_, array_' are parameterized by the
132+-- toplevel Value parser to be called recursively, to keep the parameter
133+-- hFromList outside of the recursive loop for proper inlining.
134+
104−object_ :: Parser Value
135+object_ :: HFromList -> Parser Value -> Parser Value
105−object_ = {-# SCC "object_" #-} Object <$> objectValues jstring value
136+object_ hFromList val = {-# SCC "object_" #-} Object <$> objectValues hFromList jstring val
137+{-# INLINE object_ #-}
106
138
107−object_' :: Parser Value
139+object_' :: HFromList -> Parser Value -> Parser Value
108−object_' = {-# SCC "object_'" #-} do
140+object_' hFromList val' = {-# SCC "object_'" #-} do
109− !vals <- objectValues jstring' value'
141+ !vals <- objectValues hFromList jstring' val'
110 return (Object vals)
142 return (Object vals)
111 where
143 where
112 jstring' = do
144 jstring' = do
113 !s <- jstring
145 !s <- jstring
114 return s
146 return s
147+{-# INLINE object_' #-}
115
148
149+-- The object parser 'objectValues' is parameterized by the object constructor.
150+-- See also 'jsonWith'.
151+type HFromList = [(Text, Value)] -> Parser Object
152+
116−objectValues :: Parser Text -> Parser Value -> Parser (H.HashMap Text Value)
153+objectValues :: HFromList -> Parser Text -> Parser Value -> Parser (H.HashMap Text Value)
117−objectValues str val = do
154+objectValues hFromList str val = do
118 skipSpace
155 skipSpace
119 w <- A.peekWord8'
156 w <- A.peekWord8'
120 if w == CLOSE_CURLY
157 if w == CLOSE_CURLY
@@ -130,16 +167,18 @@ objectValues str val = do
130 let acc' = (k, v) : acc
167 let acc' = (k, v) : acc
131 if ch == COMMA
168 if ch == COMMA
132 then skipSpace >> loop acc'
169 then skipSpace >> loop acc'
133− else return (H.fromList acc')
170+ else hFromList acc'
134{-# INLINE objectValues #-}
171{-# INLINE objectValues #-}
135
172
136−array_ :: Parser Value
173+array_ :: Parser Value -> Parser Value
137−array_ = {-# SCC "array_" #-} Array <$> arrayValues value
174+array_ val = {-# SCC "array_" #-} Array <$> arrayValues val
175+{-# INLINE array_ #-}
138
176
139−array_' :: Parser Value
177+array_' :: Parser Value -> Parser Value
140−array_' = {-# SCC "array_'" #-} do
178+array_' val = {-# SCC "array_'" #-} do
141− !vals <- arrayValues value'
179+ !vals <- arrayValues val
142 return (Array vals)
180 return (Array vals)
181+{-# INLINE array_' #-}
143
182
144arrayValues :: Parser Value -> Parser (Vector Value)
183arrayValues :: Parser Value -> Parser (Vector Value)
145arrayValues val = do
184arrayValues val = do
@@ -157,42 +196,104 @@ arrayValues val = do
157 else return (Vector.reverse (Vector.fromListN len (v:acc)))
196 else return (Vector.reverse (Vector.fromListN len (v:acc)))
158{-# INLINE arrayValues #-}
197{-# INLINE arrayValues #-}
159
198
160−-- | Parse any JSON value. You should usually 'json' in preference to
199+-- | Parse any JSON value. Synonym of 'json'.
161−-- this function, as this function relaxes the object-or-array
162−-- requirement of RFC 4627.
163−--
164−-- In particular, be careful in using this function if you think your
165−-- code might interoperate with Javascript. A na&#xef;ve Javascript
166−-- library that parses JSON data using @eval@ is vulnerable to attack
167−-- unless the encoded data represents an object or an array. JSON
168−-- implementations in other languages conform to that same restriction
169−-- to preserve interoperability and security.
170value :: Parser Value
200value :: Parser Value
171−value = do
201+value = jsonWith (pure . H.fromList)
202+
203+-- | Parse any JSON value.
204+--
205+-- This parser is parameterized by a function to construct an 'Object'
206+-- from a raw list of key-value pairs, where duplicates are preserved.
207+-- The pairs appear in __reverse order__ from the source.
208+--
209+-- ==== __Examples__
210+--
211+-- 'json' keeps only the first occurence of each key, using 'HashMap.Lazy.fromList'.
212+--
213+-- @
214+-- 'json' = 'jsonWith' ('pure' '.' 'H.fromList')
215+-- @
216+--
217+-- 'jsonLast' keeps the last occurence of each key, using
218+-- @'HashMap.Lazy.fromListWith' ('const' 'id')@.
219+--
220+-- @
221+-- 'jsonLast' = 'jsonWith' ('pure' '.' 'HashMap.Lazy.fromListWith' ('const' 'id'))
222+-- @
223+--
224+-- 'jsonAccum' keeps wraps all values in arrays to keep duplicates, using
225+-- 'fromListAccum'.
226+--
227+-- @
228+-- 'jsonAccum' = 'jsonWith' ('pure' . 'fromListAccum')
229+-- @
230+--
231+-- 'jsonNoDup' fails if any object contains duplicate keys, using 'parseListNoDup'.
232+--
233+-- @
234+-- 'jsonNoDup' = 'jsonWith' 'parseListNoDup'
235+-- @
236+jsonWith :: ([(Text, Value)] -> Parser Object) -> Parser Value
237+jsonWith hFromList = fix $ \value_ -> do
172 skipSpace
238 skipSpace
173 w <- A.peekWord8'
239 w <- A.peekWord8'
174 case w of
240 case w of
175 DOUBLE_QUOTE -> A.anyWord8 *> (String <$> jstring_)
241 DOUBLE_QUOTE -> A.anyWord8 *> (String <$> jstring_)
176− OPEN_CURLY -> A.anyWord8 *> object_
242+ OPEN_CURLY -> A.anyWord8 *> object_ hFromList value_
177− OPEN_SQUARE -> A.anyWord8 *> array_
243+ OPEN_SQUARE -> A.anyWord8 *> array_ value_
178 C_f -> string "false" $> Bool False
244 C_f -> string "false" $> Bool False
179 C_t -> string "true" $> Bool True
245 C_t -> string "true" $> Bool True
180 C_n -> string "null" $> Null
246 C_n -> string "null" $> Null
181 _ | w >= 48 && w <= 57 || w == 45
247 _ | w >= 48 && w <= 57 || w == 45
182 -> Number <$> scientific
248 -> Number <$> scientific
183 | otherwise -> fail "not a valid json value"
249 | otherwise -> fail "not a valid json value"
250+{-# INLINE jsonWith #-}
251+
252+-- | Variant of 'json' which keeps only the last occurence of every key.
253+jsonLast :: Parser Value
254+jsonLast = jsonWith (pure . H.fromListWith (const id))
255+
256+-- | Variant of 'json' wrapping all object mappings in 'Array' to preserve
257+-- key-value pairs with the same keys.
258+jsonAccum :: Parser Value
259+jsonAccum = jsonWith (pure . fromListAccum)
260+
261+-- | Variant of 'json' which fails if any object contains duplicate keys.
262+jsonNoDup :: Parser Value
263+jsonNoDup = jsonWith parseListNoDup
184
264
265+-- | @'fromListAccum' kvs@ is an object mapping keys to arrays containing all
266+-- associated values from the original list @kvs@.
267+--
268+-- >>> fromListAccum [("apple", Bool True), ("apple", Bool False), ("orange", Bool False)]
269+-- fromList [("apple", [Bool False, Bool True]), ("orange", [Bool False])]
270+fromListAccum :: [(Text, Value)] -> Object
271+fromListAccum =
272+ fmap (Array . Vector.fromList . ($ [])) . H.fromListWith (.) . (fmap . fmap) (:)
273+
274+-- | @'fromListNoDup' kvs@ fails if @kvs@ contains duplicate keys.
275+parseListNoDup :: [(Text, Value)] -> Parser Object
276+parseListNoDup =
277+ H.traverseWithKey unwrap . H.fromListWith (\_ _ -> Nothing) . (fmap . fmap) Just
278+ where
279+ unwrap k Nothing = fail $ "found duplicate key: " ++ show k
280+ unwrap _ (Just v) = pure v
281+
185−-- | Strict version of 'value'. See also 'json''.
282+-- | Strict version of 'value'. Synonym of 'json''.
186value' :: Parser Value
283value' :: Parser Value
187−value' = do
284+value' = jsonWith' (pure . H.fromList)
285+
286+-- | Strict version of 'jsonWith'.
287+jsonWith' :: ([(Text, Value)] -> Parser Object) -> Parser Value
288+jsonWith' hFromList = fix $ \value_ -> do
188 skipSpace
289 skipSpace
189 w <- A.peekWord8'
290 w <- A.peekWord8'
190 case w of
291 case w of
191 DOUBLE_QUOTE -> do
292 DOUBLE_QUOTE -> do
192 !s <- A.anyWord8 *> jstring_
293 !s <- A.anyWord8 *> jstring_
193 return (String s)
294 return (String s)
194− OPEN_CURLY -> A.anyWord8 *> object_'
295+ OPEN_CURLY -> A.anyWord8 *> object_' hFromList value_
195− OPEN_SQUARE -> A.anyWord8 *> array_'
296+ OPEN_SQUARE -> A.anyWord8 *> array_' value_
196 C_f -> string "false" $> Bool False
297 C_f -> string "false" $> Bool False
197 C_t -> string "true" $> Bool True
298 C_t -> string "true" $> Bool True
198 C_n -> string "null" $> Null
299 C_n -> string "null" $> Null
@@ -201,6 +302,20 @@ value' = do
201 !n <- scientific
302 !n <- scientific
202 return (Number n)
303 return (Number n)
203 | otherwise -> fail "not a valid json value"
304 | otherwise -> fail "not a valid json value"
305+{-# INLINE jsonWith' #-}
306+
307+-- | Variant of 'json'' which keeps only the last occurence of every key.
308+jsonLast' :: Parser Value
309+jsonLast' = jsonWith' (pure . H.fromListWith (const id))
310+
311+-- | Variant of 'json'' wrapping all object mappings in 'Array' to preserve
312+-- key-value pairs with the same keys.
313+jsonAccum' :: Parser Value
314+jsonAccum' = jsonWith' (pure . fromListAccum)
315+
316+-- | Variant of 'json'' which fails if any object contains duplicate keys.
317+jsonNoDup' :: Parser Value
318+jsonNoDup' = jsonWith' parseListNoDup
204
319
205-- | Parse a quoted JSON string.
320-- | Parse a quoted JSON string.
206jstring :: Parser Text
321jstring :: Parser Text
tests/UnitTests.hs+46−2
@@ -29,7 +29,13 @@ import Data.Aeson.Internal (JSONPathElement(..), formatError)
29import Data.Aeson.QQ.Simple (aesonQQ)
29import Data.Aeson.QQ.Simple (aesonQQ)
30import Data.Aeson.TH (deriveJSON, deriveToJSON, deriveToJSON1)
30import Data.Aeson.TH (deriveJSON, deriveToJSON, deriveToJSON1)
31import Data.Aeson.Text (encodeToTextBuilder)
31import Data.Aeson.Text (encodeToTextBuilder)
32+import Data.Aeson.Parser
33+ ( json, jsonLast, jsonAccum, jsonNoDup
34+ , json', jsonLast', jsonAccum', jsonNoDup')
35+import Data.Aeson.Types
36+ ( Options(..), Result(Success), ToJSON(..), Value(Array, Bool, Null, Object)
32−import Data.Aeson.Types (Options(..), Result(Success), ToJSON(..), Value(Null, Object), camelTo, camelTo2, defaultOptions, omitNothingFields, parse)
37+ , camelTo, camelTo2, defaultOptions, omitNothingFields, parse)
38+import Data.Attoparsec.ByteString (Parser, parseOnly)
33import Data.Char (toUpper)
39import Data.Char (toUpper)
34import Data.Either.Compat (isLeft, isRight)
40import Data.Either.Compat (isLeft, isRight)
35import Data.Hashable (hash)
41import Data.Hashable (hash)
@@ -47,16 +53,19 @@ import Numeric.Natural (Natural)
47import System.Directory (getDirectoryContents)
53import System.Directory (getDirectoryContents)
48import System.FilePath ((</>), takeExtension, takeFileName)
54import System.FilePath ((</>), takeExtension, takeFileName)
49import Test.Tasty (TestTree, testGroup)
55import Test.Tasty (TestTree, testGroup)
50−import Test.Tasty.HUnit (Assertion, assertBool, assertFailure, assertEqual, testCase)
56+import Test.Tasty.HUnit (Assertion, assertBool, assertFailure, assertEqual, testCase, (@?=))
51import Text.Printf (printf)
57import Text.Printf (printf)
52import UnitTests.NullaryConstructors (nullaryConstructors)
58import UnitTests.NullaryConstructors (nullaryConstructors)
59+import qualified Data.ByteString as S
53import qualified Data.ByteString.Base16.Lazy as LBase16
60import qualified Data.ByteString.Base16.Lazy as LBase16
54import qualified Data.ByteString.Lazy.Char8 as L
61import qualified Data.ByteString.Lazy.Char8 as L
55import qualified Data.HashSet as HashSet
62import qualified Data.HashSet as HashSet
63+import qualified Data.HashMap.Lazy as HashMap
56import qualified Data.Text.Lazy as LT
64import qualified Data.Text.Lazy as LT
57import qualified Data.Text.Lazy.Builder as TLB
65import qualified Data.Text.Lazy.Builder as TLB
58import qualified Data.Text.Lazy.Encoding as LT
66import qualified Data.Text.Lazy.Encoding as LT
59import qualified Data.Text.Lazy.Encoding as TLE
67import qualified Data.Text.Lazy.Encoding as TLE
68+import qualified Data.Vector as Vector
60import qualified ErrorMessages
69import qualified ErrorMessages
61import qualified SerializationFormatSpec
70import qualified SerializationFormatSpec
62
71
@@ -102,6 +111,7 @@ tests = testGroup "unit" [
102 , testGroup "SingleMaybeField" singleMaybeField
111 , testGroup "SingleMaybeField" singleMaybeField
103 , testCase "withEmbeddedJSON" withEmbeddedJSONTest
112 , testCase "withEmbeddedJSON" withEmbeddedJSONTest
104 , testCase "SingleFieldCon" singleFieldCon
113 , testCase "SingleFieldCon" singleFieldCon
114+ , testGroup "Ordering of object keys" keyOrdering
105 , testCase "Ratio with denominator 0" ratioDenominator0
115 , testCase "Ratio with denominator 0" ratioDenominator0
106 , testCase "Big scientific exponent" bigScientificExponent
116 , testCase "Big scientific exponent" bigScientificExponent
107 , testCase "Big integer decoding" bigIntegerDecoding
117 , testCase "Big integer decoding" bigIntegerDecoding
@@ -557,6 +567,40 @@ singleFieldCon :: Assertion
557singleFieldCon =
567singleFieldCon =
558 assertEqual "fromJSON" (Right (SingleFieldCon 0)) (eitherDecode "0")
568 assertEqual "fromJSON" (Right (SingleFieldCon 0)) (eitherDecode "0")
559
569
570+testParser :: (Eq a, Show a)
571+ => String -> Parser a -> S.ByteString -> Either String a -> TestTree
572+testParser name json_ s expected =
573+ testCase name (parseOnly json_ s @?= expected)
574+
575+keyOrdering :: [TestTree]
576+keyOrdering =
577+ [ testParser "json" json
578+ "{\"k\":true,\"k\":false}" $
579+ Right (Object (HashMap.fromList [("k", Bool True)]))
580+ , testParser "jsonLast" jsonLast
581+ "{\"k\":true,\"k\":false}" $
582+ Right (Object (HashMap.fromList [("k", Bool False)]))
583+ , testParser "jsonAccum" jsonAccum
584+ "{\"k\":true,\"k\":false}" $
585+ Right (Object (HashMap.fromList [("k", Array (Vector.fromList [Bool True, Bool False]))]))
586+ , testParser "jsonNoDup" jsonNoDup
587+ "{\"k\":true,\"k\":false}" $
588+ Left "Failed reading: found duplicate key: \"k\""
589+
590+ , testParser "json'" json'
591+ "{\"k\":true,\"k\":false}" $
592+ Right (Object (HashMap.fromList [("k", Bool True)]))
593+ , testParser "jsonLast'" jsonLast'
594+ "{\"k\":true,\"k\":false}" $
595+ Right (Object (HashMap.fromList [("k", Bool False)]))
596+ , testParser "jsonAccum'" jsonAccum'
597+ "{\"k\":true,\"k\":false}" $
598+ Right (Object (HashMap.fromList [("k", Array (Vector.fromList [Bool True, Bool False]))]))
599+ , testParser "jsonNoDup'" jsonNoDup'
600+ "{\"k\":true,\"k\":false}" $
601+ Left "Failed reading: found duplicate key: \"k\""
602+ ]
603+
560ratioDenominator0 :: Assertion
604ratioDenominator0 :: Assertion
561ratioDenominator0 =
605ratioDenominator0 =
562 assertEqual "Ratio with denominator 0"
606 assertEqual "Ratio with denominator 0"