diff --git a/aeson.cabal b/aeson.cabal index 2552899ce..cef407f1f 100644 --- a/aeson.cabal +++ b/aeson.cabal @@ -1,5 +1,5 @@ name: aeson -version: 1.5.6.0 +version: 2.0.0.0 license: BSD3 license-file: LICENSE category: Text, Web, JSON @@ -68,6 +68,7 @@ library Data.Aeson.Internal Data.Aeson.Internal.Time Data.Aeson.Parser.Internal + Data.Aeson.KeyMap -- Deprecated modules exposed-modules: diff --git a/benchmarks/aeson-benchmarks.cabal b/benchmarks/aeson-benchmarks.cabal index d49726927..eb2ecc3dc 100644 --- a/benchmarks/aeson-benchmarks.cabal +++ b/benchmarks/aeson-benchmarks.cabal @@ -76,6 +76,7 @@ library Data.Aeson.Internal Data.Aeson.Internal.Functions Data.Aeson.Internal.Time + Data.Aeson.KeyMap Data.Aeson.Parser Data.Aeson.Parser.Internal Data.Aeson.Parser.Time diff --git a/changelog.md b/changelog.md index 682d19153..141f2ffe9 100644 --- a/changelog.md +++ b/changelog.md @@ -1,6 +1,6 @@ For the latest version of this document, please see [https://github.com/haskell/aeson/blob/master/changelog.md](https://github.com/haskell/aeson/blob/master/changelog.md). -### 1.6.0.0 +### 2.0.0.0 * Remove forced `-O2` and then unneeded `fast` flag. Also remove most of `INLINE` pragmas. @@ -8,6 +8,9 @@ For the latest version of this document, please see [https://github.com/haskell/ To get `fast` compilation effect cabal-install users may specify `optimization: False`. +* Make map type used by Object abstract so the underlying implementation can + be modified, thanks to Callan McGill + ### 1.5.6.0 * Make `Show Value` instance print object keys in lexicographic order. diff --git a/src/Data/Aeson/Encoding/Builder.hs b/src/Data/Aeson/Encoding/Builder.hs index e0fce8552..8e1c6e6a3 100644 --- a/src/Data/Aeson/Encoding/Builder.hs +++ b/src/Data/Aeson/Encoding/Builder.hs @@ -42,6 +42,7 @@ import Prelude.Compat import Data.Aeson.Internal.Time import Data.Aeson.Types.Internal (Value (..)) +import qualified Data.Aeson.KeyMap as KM import Data.ByteString.Builder as B import Data.ByteString.Builder.Prim as BP import Data.ByteString.Builder.Scientific (scientificBuilder) @@ -54,7 +55,6 @@ import Data.Time.Calendar.Month.Compat (Month, toYearMonth) import Data.Time.Calendar.Quarter.Compat (Quarter, toYearQuarter, QuarterOfYear (..)) import Data.Time.LocalTime import Data.Word (Word8) -import qualified Data.HashMap.Strict as HMS import qualified Data.Text as T import qualified Data.Vector as V @@ -90,8 +90,8 @@ array v withComma a z = B.char8 ',' <> encodeToBuilder a <> z -- Encode a JSON object. -object :: HMS.HashMap T.Text Value -> Builder -object m = case HMS.toList m of +object :: KM.KeyMap Value -> Builder +object m = case KM.toList m of (x:xs) -> B.char8 '{' <> one x <> foldr withComma (B.char8 '}') xs _ -> emptyObject_ where diff --git a/src/Data/Aeson/Internal/Functions.hs b/src/Data/Aeson/Internal/Functions.hs index c81951ed0..77bb6a86f 100644 --- a/src/Data/Aeson/Internal/Functions.hs +++ b/src/Data/Aeson/Internal/Functions.hs @@ -9,8 +9,7 @@ -- Portability: portable module Data.Aeson.Internal.Functions - ( - mapHashKeyVal + ( mapTextKeyVal , mapKeyVal , mapKey ) where @@ -18,14 +17,16 @@ module Data.Aeson.Internal.Functions import Prelude.Compat import Data.Hashable (Hashable) +import qualified Data.Aeson.KeyMap as KM import qualified Data.HashMap.Strict as H import qualified Data.Map as M +import qualified Data.Text as T --- | Transform a 'M.Map' into a 'H.HashMap' while transforming the keys. -mapHashKeyVal :: (Eq k2, Hashable k2) => (k1 -> k2) -> (v1 -> v2) - -> M.Map k1 v1 -> H.HashMap k2 v2 -mapHashKeyVal fk kv = M.foldrWithKey (\k v -> H.insert (fk k) (kv v)) H.empty -{-# INLINE mapHashKeyVal #-} +-- | Transform a 'M.Map' into a 'KM.KeyMap' while transforming the keys. +mapTextKeyVal :: (k -> T.Text) -> (v1 -> v2) + -> M.Map k v1 -> KM.KeyMap v2 +mapTextKeyVal fk kv = M.foldrWithKey (\k v -> KM.insert (fk k) (kv v)) KM.empty +{-# INLINE mapTextKeyVal #-} -- | Transform the keys and values of a 'H.HashMap'. mapKeyVal :: (Eq k2, Hashable k2) => (k1 -> k2) -> (v1 -> v2) @@ -37,3 +38,4 @@ mapKeyVal fk kv = H.foldrWithKey (\k v -> H.insert (fk k) (kv v)) H.empty mapKey :: (Eq k2, Hashable k2) => (k1 -> k2) -> H.HashMap k1 v -> H.HashMap k2 v mapKey fk = mapKeyVal fk id {-# INLINE mapKey #-} + diff --git a/src/Data/Aeson/KeyMap.hs b/src/Data/Aeson/KeyMap.hs new file mode 100644 index 000000000..6cba98acc --- /dev/null +++ b/src/Data/Aeson/KeyMap.hs @@ -0,0 +1,230 @@ +{-# LANGUAGE CPP #-} +{-# LANGUAGE DeriveDataTypeable #-} +{-# LANGUAGE DeriveFunctor #-} +{-# LANGUAGE TemplateHaskell #-} + +-- | +-- An abstract interface for maps from Textual keys to values. + +module Data.Aeson.KeyMap ( + -- * Map Type + KeyMap, + + -- * Query + lookup, + size, + member, + + -- * Construction + empty, + singleton, + + -- ** Insertion + insert, + + -- * Combine + difference, + + -- * Lists + fromList, + fromListWith, + toList, + toAscList, + + -- * HashMaps + fromHashMap, + toHashMap, + + -- * Traversal + -- ** Map + mapKeyVal, + traverseWithKey, + + -- * Folds + foldrWithKey, + + -- * Conversions + keys, +) where + +#if 1 +import Control.DeepSeq (NFData(..)) +import Data.Data (Data) +import Data.Hashable (Hashable(..)) +import Data.HashMap.Strict (HashMap) +import Data.List (sortBy) +import Data.Ord (comparing) +import Data.Text (Text, unpack, pack) +import Data.Typeable (Typeable) +import Prelude hiding (lookup) +import Control.Arrow (first) +import Data.Foldable hiding (toList) +import Text.Read +#if __GLASGOW_HASKELL__ < 710 +import Data.Monoid (Monoid(mempty, mappend)) +import Data.Traversable (Traversable(..)) +import Control.Applicative (Applicative) +#endif +#if __GLASGOW_HASKELL__ >= 711 +import Data.Semigroup (Semigroup((<>))) +#endif + +import qualified Data.HashMap.Strict as H +import qualified Language.Haskell.TH.Syntax as TH + +newtype KeyMap v = KeyMap { unKeyMap :: HashMap Text v } + deriving (Eq, Ord, Typeable, Data, Functor) + +instance Read v => Read (KeyMap v) where + readPrec = parens $ prec 10 $ do + Ident "fromList" <- lexP + xs <- readPrec + return (fromList xs) + + readListPrec = readListPrecDefault + +instance Show v => Show (KeyMap v) where + showsPrec d m = showParen (d > 10) $ + showString "fromList " . shows (toAscList m) + + +#if __GLASGOW_HASKELL__ >= 711 +instance Semigroup (KeyMap v) where + (KeyMap m1) <> (KeyMap m2) = KeyMap (m1 `H.union` m2) + {-# INLINE (<>) #-} +#endif +instance Monoid (KeyMap v) where + mempty = empty + {-# INLINE mempty #-} +#if __GLASGOW_HASKELL__ >= 711 + mappend = (<>) +#else + mappend (KeyMap m1) (KeyMap m2) = KeyMap (m1 `H.union` m2) +#endif + {-# INLINE mappend #-} + +instance Hashable v => Hashable (KeyMap v) where + hashWithSalt salt (KeyMap hm) = hashWithSalt salt hm + +instance NFData v => NFData (KeyMap v) where + rnf (KeyMap hm) = rnf hm + +instance Foldable KeyMap where + foldMap f (KeyMap tm) = H.foldMapWithKey (\ _k v -> f v) tm + {-# INLINE foldMap #-} + foldr f z (KeyMap tm) = H.foldr f z tm + {-# INLINE foldr #-} + foldl f z (KeyMap tm) = H.foldl f z tm + {-# INLINE foldl #-} + foldr' f z (KeyMap tm) = H.foldr' f z tm + {-# INLINE foldr' #-} + foldl' f z (KeyMap tm) = H.foldl' f z tm + {-# INLINE foldl' #-} +#if MIN_VERSION_base(4,8,0) + null = H.null . unKeyMap + {-# INLINE null #-} + length = size + {-# INLINE length #-} +#endif + +instance Traversable KeyMap where + traverse f = traverseWithKey (const f) + {-# INLINABLE traverse #-} + + +instance TH.Lift v => TH.Lift (KeyMap v) where + lift (KeyMap m) = [| KeyMap (H.fromList . map (first pack) $ m') |] + where + m' = map (first unpack) . H.toList $ m + +#if MIN_VERSION_template_haskell(2,17,0) + liftTyped = TH.unsafeCodeCoerce . TH.lift +#elif MIN_VERSION_template_haskell(2,16,0) + liftTyped = TH.unsafeTExpCoerce . TH.lift +#endif + +-- | +-- Construct an empty map. +empty :: KeyMap v +empty = KeyMap H.empty + +-- | +-- Return the number of key-value mappings in this map. +size :: KeyMap v -> Int +size = H.size . unKeyMap + +-- | +-- Construct a map with a single element. +singleton :: Text -> v -> KeyMap v +singleton k v = KeyMap (H.singleton k v) + +member :: Text -> KeyMap a -> Bool +member t (KeyMap m) = H.member t m + +-- | Return the value to which the specified key is mapped, +-- or Nothing if this map contains no mapping for the key. +lookup :: Text -> KeyMap v -> Maybe v +lookup t tm = H.lookup t (unKeyMap tm) + +-- | Associate the specified value with the specified key +-- in this map. If this map previously contained a mapping +-- for the key, the old value is replaced. +insert :: Text -> v -> KeyMap v -> KeyMap v +insert k v tm = KeyMap (H.insert k v (unKeyMap tm)) + +-- | Reduce this map by applying a binary operator to all +-- elements, using the given starting value (typically the +-- right-identity of the operator). +foldrWithKey :: (Text -> v -> a -> a) -> a -> KeyMap v -> a +foldrWithKey f a = H.foldrWithKey f a . unKeyMap + +-- | Perform an Applicative action for each key-value pair +-- in a 'KeyMap' and produce a 'KeyMap' of all the results. +traverseWithKey :: Applicative f => (Text -> v1 -> f v2) -> KeyMap v1 -> f (KeyMap v2) +traverseWithKey f = fmap KeyMap . H.traverseWithKey f . unKeyMap + +-- | Construct a map from a list of elements. Uses the +-- provided function, f, to merge duplicate entries with +-- (f newVal oldVal). +fromListWith :: (v -> v -> v) -> [(Text, v)] -> KeyMap v +fromListWith op = KeyMap . H.fromListWith op + +-- | Construct a map with the supplied mappings. If the +-- list contains duplicate mappings, the later mappings take +-- precedence. +fromList :: [(Text, v)] -> KeyMap v +fromList = KeyMap . H.fromList + +-- | Return a list of this map's elements. +toList :: KeyMap v -> [(Text, v)] +toList = H.toList . unKeyMap + +-- | Return a list of this map's elements in ascending order +-- based of the textual key. +toAscList :: KeyMap v -> [(Text, v)] +toAscList = sortBy (comparing fst) . toList + +-- | Difference of two maps. Return elements of the first +-- map not existing in the second. +difference :: KeyMap v -> KeyMap v' -> KeyMap v +difference tm1 tm2 = KeyMap (H.difference (unKeyMap tm1) (unKeyMap tm2)) + +-- | Return a list of this map's keys. +keys :: KeyMap v -> [Text] +keys = H.keys . unKeyMap + +-- | Convert a 'KeyMap' to a 'HashMap'. +toHashMap :: KeyMap v -> HashMap Text v +toHashMap = unKeyMap + +-- | Convert a 'HashMap' to a 'KeyMap'. +fromHashMap :: HashMap Text v -> KeyMap v +fromHashMap = KeyMap + +-- | Transform the keys and values of a 'KeyMap'. +mapKeyVal :: (Text -> Text) -> (v1 -> v2) + -> KeyMap v1 -> KeyMap v2 +mapKeyVal fk kv = foldrWithKey (\k v -> insert (fk k) (kv v)) empty +{-# INLINE mapKeyVal #-} + +#endif diff --git a/src/Data/Aeson/Parser/Internal.hs b/src/Data/Aeson/Parser/Internal.hs index 84d08e533..533e5bb47 100644 --- a/src/Data/Aeson/Parser/Internal.hs +++ b/src/Data/Aeson/Parser/Internal.hs @@ -2,7 +2,7 @@ {-# LANGUAGE CPP #-} {-# LANGUAGE NoImplicitPrelude #-} {-# LANGUAGE OverloadedStrings #-} -#if __GLASGOW_HASKELL__ <= 710 && __GLASGOW_HASKELL__ >= 706 +#if __GLASGOW_HASKELL__ <= 800 && __GLASGOW_HASKELL__ >= 706 -- Work around a compiler bug {-# OPTIONS_GHC -fsimpl-tick-factor=300 #-} #endif @@ -52,6 +52,7 @@ import Prelude.Compat import Control.Applicative ((<|>)) import Control.Monad (void, when) import Data.Aeson.Types.Internal (IResult(..), JSONPath, Object, Result(..), Value(..)) +import qualified Data.Aeson.KeyMap as KM import Data.Attoparsec.ByteString.Char8 (Parser, char, decimal, endOfInput, isDigit_w8, signed, string) import Data.Function (fix) import Data.Functor.Compat (($>)) @@ -68,7 +69,6 @@ import qualified Data.ByteString.Lazy as L import qualified Data.ByteString.Lazy as BSL import qualified Data.ByteString.Lazy.Char8 as C import qualified Data.ByteString.Builder as B -import qualified Data.HashMap.Strict as H import qualified Data.Scientific as Sci import Data.Aeson.Parser.Unescape (unescapeText) @@ -146,16 +146,18 @@ object_' mkObject val' = {-# SCC "object_'" #-} do {-# INLINE object_' #-} objectValues :: ([(Text, Value)] -> Either String Object) - -> Parser Text -> Parser Value -> Parser (H.HashMap Text Value) + -> Parser Text -> Parser Value -> Parser (KM.KeyMap Value) objectValues mkObject str val = do skipSpace w <- A.peekWord8' if w == CLOSE_CURLY - then A.anyWord8 >> return H.empty + then A.anyWord8 >> return KM.empty else loop [] where - -- Why use acc pattern here, you may ask? because 'H.fromList' use 'unsafeInsert' - -- and it's much faster because it's doing in place update to the 'HashMap'! + -- Why use acc pattern here, you may ask? because then the underlying 'KM.fromList' + -- implementation can make use of mutation when constructing a map. For example, + -- 'HashMap` uses 'unsafeInsert' and it's much faster because it's doing in place + -- update to the 'HashMap'! loop acc = do k <- (str A. "object key") <* skipSpace <* (char ':' A. "':'") v <- (val A. "object value") <* skipSpace @@ -196,7 +198,7 @@ arrayValues val = do -- | Parse any JSON value. Synonym of 'json'. value :: Parser Value -value = jsonWith (pure . H.fromList) +value = jsonWith (pure . KM.fromList) -- | Parse any JSON value. -- @@ -206,7 +208,7 @@ value = jsonWith (pure . H.fromList) -- -- ==== __Examples__ -- --- 'json' keeps only the first occurence of each key, using 'HashMap.Lazy.fromList'. +-- 'json' keeps only the first occurence of each key, using 'Data.Aeson.KeyMap.fromList'. -- -- @ -- 'json' = 'jsonWith' ('Right' '.' 'H.fromList') @@ -249,7 +251,7 @@ jsonWith mkObject = fix $ \value_ -> do -- | Variant of 'json' which keeps only the last occurence of every key. jsonLast :: Parser Value -jsonLast = jsonWith (Right . H.fromListWith (const id)) +jsonLast = jsonWith (Right . KM.fromListWith (const id)) -- | Variant of 'json' wrapping all object mappings in 'Array' to preserve -- key-value pairs with the same keys. @@ -267,19 +269,20 @@ jsonNoDup = jsonWith parseListNoDup -- fromList [("apple",Array [Bool False,Bool True]),("orange",Array [Bool False])] fromListAccum :: [(Text, Value)] -> Object fromListAccum = - fmap (Array . Vector.fromList . ($ [])) . H.fromListWith (.) . (fmap . fmap) (:) + fmap (Array . Vector.fromList . ($ [])) . KM.fromListWith (.) . (fmap . fmap) (:) -- | @'fromListNoDup' kvs@ fails if @kvs@ contains duplicate keys. parseListNoDup :: [(Text, Value)] -> Either String Object parseListNoDup = - H.traverseWithKey unwrap . H.fromListWith (\_ _ -> Nothing) . (fmap . fmap) Just + KM.traverseWithKey unwrap . KM.fromListWith (\_ _ -> Nothing) . (fmap . fmap) Just where + unwrap k Nothing = Left $ "found duplicate key: " ++ show k unwrap _ (Just v) = Right v -- | Strict version of 'value'. Synonym of 'json''. value' :: Parser Value -value' = jsonWith' (pure . H.fromList) +value' = jsonWith' (pure . KM.fromList) -- | Strict version of 'jsonWith'. jsonWith' :: ([(Text, Value)] -> Either String Object) -> Parser Value @@ -304,7 +307,7 @@ jsonWith' mkObject = fix $ \value_ -> do -- | Variant of 'json'' which keeps only the last occurence of every key. jsonLast' :: Parser Value -jsonLast' = jsonWith' (pure . H.fromListWith (const id)) +jsonLast' = jsonWith' (pure . KM.fromListWith (const id)) -- | Variant of 'json'' wrapping all object mappings in 'Array' to preserve -- key-value pairs with the same keys. diff --git a/src/Data/Aeson/TH.hs b/src/Data/Aeson/TH.hs index e8e2e5db8..6dd04518e 100644 --- a/src/Data/Aeson/TH.hs +++ b/src/Data/Aeson/TH.hs @@ -128,6 +128,7 @@ import Data.Aeson.Types (Options(..), Parser, SumEncoding(..), Value(..), defaul import Data.Aeson.Types.Internal ((), JSONPathElement(Key)) import Data.Aeson.Types.FromJSON (parseOptionalFieldWith) import Data.Aeson.Types.ToJSON (fromPairs, pair) +import qualified Data.Aeson.KeyMap as KM import Control.Monad (liftM2, unless, when) import Data.Foldable (foldr') #if MIN_VERSION_template_haskell(2,8,0) && !MIN_VERSION_template_haskell(2,10,0) @@ -147,7 +148,6 @@ import Language.Haskell.TH.Syntax (mkNameG_tc) import Text.Printf (printf) import qualified Data.Aeson.Encoding.Internal as E import qualified Data.Foldable as F (all) -import qualified Data.HashMap.Strict as H (difference, fromList, keys, lookup, toList) import qualified Data.List.NonEmpty as NE (length, reverse) import qualified Data.Map as M (fromList, keys, lookup , singleton, size) #if !MIN_VERSION_base(4,16,0) @@ -849,7 +849,7 @@ consFromJSON jc tName opts instTys cons = do parseObjectWithSingleField tvMap obj = do conKey <- newName "conKey" conVal <- newName "conVal" - caseE ([e|H.toList|] `appE` varE obj) + caseE ([e|KM.toList|] `appE` varE obj) [ match (listP [tupP [varP conKey, varP conVal]]) (normalB $ parseContents tvMap conKey (Right conVal) 'conNotFoundFailObjectSingleField) [] @@ -947,11 +947,11 @@ parseRecord jc tvMap argTys opts tName conName fields obj inTaggedObject = where tagFieldNameAppender = if inTaggedObject then (tagFieldName (sumEncoding opts) :) else id - knownFields = appE [|H.fromList|] $ listE $ + knownFields = appE [|KM.fromList|] $ listE $ map (\knownName -> tupE [appE [|T.pack|] $ litE $ stringL knownName, [|()|]]) $ tagFieldNameAppender $ map (fieldLabel opts) fields checkUnknownRecords = - caseE (appE [|H.keys|] $ infixApp (varE obj) [|H.difference|] knownFields) + caseE (appE [|KM.keys|] $ infixApp (varE obj) [|KM.difference|] knownFields) [ match (listP []) (normalB [|return ()|]) [] , newName "unknownFields" >>= \unknownFields -> match (varP unknownFields) @@ -1149,7 +1149,7 @@ instance INCOHERENT_ LookupField (Semigroup.Option a) where lookupFieldWith :: (Value -> Parser a) -> String -> String -> Object -> T.Text -> Parser a lookupFieldWith pj tName rec obj key = - case H.lookup key obj of + case KM.lookup key obj of Nothing -> unknownFieldFail tName rec (T.unpack key) Just v -> pj v Key key diff --git a/src/Data/Aeson/Text.hs b/src/Data/Aeson/Text.hs index 90ef95f4f..493eb1c81 100644 --- a/src/Data/Aeson/Text.hs +++ b/src/Data/Aeson/Text.hs @@ -26,11 +26,11 @@ import Prelude.Compat import Data.Aeson.Types (Value(..), ToJSON(..)) import Data.Aeson.Encoding (encodingToLazyByteString) +import qualified Data.Aeson.KeyMap as KM import Data.Scientific (FPFormat(..), Scientific, base10Exponent) import Data.Text.Lazy.Builder import Data.Text.Lazy.Builder.Scientific (formatScientificBuilder) import Numeric (showHex) -import qualified Data.HashMap.Strict as H import qualified Data.Text as T import qualified Data.Text.Lazy as LT import qualified Data.Text.Lazy.Encoding as LT @@ -66,7 +66,7 @@ encodeToTextBuilder = V.foldr f (singleton ']') (V.unsafeTail v) where f a z = singleton ',' <> go a <> z go (Object m) = {-# SCC "go/Object" #-} - case H.toList m of + case KM.toList m of (x:xs) -> singleton '{' <> one x <> foldr f (singleton '}') xs _ -> "{}" where f a z = singleton ',' <> one a <> z diff --git a/src/Data/Aeson/Types/FromJSON.hs b/src/Data/Aeson/Types/FromJSON.hs index 68e5bda51..f5c2aa903 100644 --- a/src/Data/Aeson/Types/FromJSON.hs +++ b/src/Data/Aeson/Types/FromJSON.hs @@ -122,6 +122,7 @@ import Numeric.Natural (Natural) import Text.ParserCombinators.ReadP (readP_to_S) import Unsafe.Coerce (unsafeCoerce) import qualified Data.Aeson.Parser.Time as Time +import qualified Data.Aeson.KeyMap as KM import qualified Data.Attoparsec.ByteString.Char8 as A (endOfInput, parseOnly, scientific) import qualified Data.ByteString.Lazy as L import qualified Data.DList as DList @@ -225,7 +226,7 @@ parseBoundedIntegralText name t = parseOptionalFieldWith :: (Value -> Parser (Maybe a)) -> Object -> Text -> Parser (Maybe a) parseOptionalFieldWith pj obj key = - case H.lookup key obj of + case KM.lookup key obj of Nothing -> pure Nothing Just v -> pj v Key key @@ -821,19 +822,19 @@ parseFieldMaybe' = (.:!) -- -- E.g. @'explicitParseField' 'parseJSON1' :: ('FromJSON1' f, 'FromJSON' a) -> 'Object' -> 'Text' -> 'Parser' (f a)@ explicitParseField :: (Value -> Parser a) -> Object -> Text -> Parser a -explicitParseField p obj key = case H.lookup key obj of +explicitParseField p obj key = case KM.lookup key obj of Nothing -> fail $ "key " ++ show key ++ " not found" Just v -> p v Key key -- | Variant of '.:?' with explicit parser function. explicitParseFieldMaybe :: (Value -> Parser a) -> Object -> Text -> Parser (Maybe a) -explicitParseFieldMaybe p obj key = case H.lookup key obj of +explicitParseFieldMaybe p obj key = case KM.lookup key obj of Nothing -> pure Nothing Just v -> liftParseJSON p (listParser p) v Key key -- listParser isn't used by maybe instance. -- | Variant of '.:!' with explicit parser function. explicitParseFieldMaybe' :: (Value -> Parser a) -> Object -> Text -> Parser (Maybe a) -explicitParseFieldMaybe' p obj key = case H.lookup key obj of +explicitParseFieldMaybe' p obj key = case KM.lookup key obj of Nothing -> pure Nothing Just v -> Just <$> p v Key key @@ -1104,12 +1105,12 @@ parseNonAllNullarySum p@(tname :* opts :* _) = cnames_ = unTagged2 (constructorTags (constructorTagModifier opts) :: Tagged2 f [String]) ObjectWithSingleField -> - withObject tname $ \obj -> case H.toList obj of + withObject tname $ \obj -> case KM.toList obj of [(tag, v)] -> maybe (badTag tag) ( Key tag) $ parsePair (tag :* p) v _ -> contextType tname . fail $ "expected an Object with a single pair, but found " ++ - show (H.size obj) ++ " pairs" + show (KM.size obj) ++ " pairs" where badTag tag = failWith_ $ \cnames -> "expected an Object with a single pair where the tag is one of " ++ @@ -1281,15 +1282,15 @@ instance ( FieldNames f recordParseJSON (fromTaggedSum :* p@(cname :* tname :* opts :* _)) = \obj -> checkUnknown obj >> recordParseJSON' p obj where - knownFields :: H.HashMap Text () - knownFields = H.fromList $ map ((,()) . pack) $ + knownFields :: KM.KeyMap () + knownFields = KM.fromList $ map ((,()) . pack) $ [tagFieldName (sumEncoding opts) | fromTaggedSum] <> (fieldLabelModifier opts <$> fieldNames (undefined :: f a) []) checkUnknown = if not (rejectUnknownFields opts) then \_ -> return () - else \obj -> case H.keys (H.difference obj knownFields) of + else \obj -> case KM.keys (KM.difference obj knownFields) of [] -> return () unknownFields -> contextCons cname tname $ fail ("unknown fields: " ++ show unknownFields) @@ -1482,7 +1483,7 @@ instance (FromJSON a) => FromJSON (Maybe a) where instance FromJSON2 Either where - liftParseJSON2 pA _ pB _ (Object (H.toList -> [(key, value)])) + liftParseJSON2 pA _ pB _ (Object (KM.toList -> [(key, value)])) | key == left = Left <$> pA value Key left | key == right = Right <$> pB value Key right where @@ -1799,7 +1800,7 @@ instance (FromJSON1 f, FromJSON1 g, FromJSON a) => FromJSON (Product f g a) wher instance (FromJSON1 f, FromJSON1 g) => FromJSON1 (Sum f g) where - liftParseJSON p pl (Object (H.toList -> [(key, value)])) + liftParseJSON p pl (Object (KM.toList -> [(key, value)])) | key == inl = InL <$> liftParseJSON p pl value Key inl | key == inr = InR <$> liftParseJSON p pl value Key inl where @@ -1849,11 +1850,11 @@ instance FromJSON a => FromJSON (IntMap.IntMap a) where instance (FromJSONKey k, Ord k) => FromJSON1 (M.Map k) where liftParseJSON p _ = case fromJSONKey of FromJSONKeyCoerce -> withObject "Map" $ - fmap (H.foldrWithKey (M.insert . unsafeCoerce) M.empty) . H.traverseWithKey (\k v -> p v Key k) + fmap (KM.foldrWithKey (M.insert . unsafeCoerce) M.empty) . KM.traverseWithKey (\k v -> p v Key k) FromJSONKeyText f -> withObject "Map" $ - fmap (H.foldrWithKey (M.insert . f) M.empty) . H.traverseWithKey (\k v -> p v Key k) + fmap (KM.foldrWithKey (M.insert . f) M.empty) . KM.traverseWithKey (\k v -> p v Key k) FromJSONKeyTextParser f -> withObject "Map" $ - H.foldrWithKey (\k v m -> M.insert <$> f k Key k <*> p v Key k <*> m) (pure M.empty) + KM.foldrWithKey (\k v m -> M.insert <$> f k Key k <*> p v Key k <*> m) (pure M.empty) FromJSONKeyValue f -> withArray "Map" $ \arr -> fmap M.fromList . Tr.sequence . zipWith (parseIndexedJSONPair f p) [0..] . V.toList $ arr @@ -1920,11 +1921,13 @@ instance (Eq a, Hashable a, FromJSON a) => FromJSON (HashSet.HashSet a) where instance (FromJSONKey k, Eq k, Hashable k) => FromJSON1 (H.HashMap k) where liftParseJSON p _ = case fromJSONKey of FromJSONKeyCoerce -> withObject "HashMap ~Text" $ - uc . H.traverseWithKey (\k v -> p v Key k) + uc . H.traverseWithKey (\k v -> p v Key k) . KM.toHashMap FromJSONKeyText f -> withObject "HashMap" $ - fmap (mapKey f) . H.traverseWithKey (\k v -> p v Key k) + fmap (mapKey f) . H.traverseWithKey (\k v -> p v Key k) . KM.toHashMap FromJSONKeyTextParser f -> withObject "HashMap" $ - H.foldrWithKey (\k v m -> H.insert <$> f k Key k <*> p v Key k <*> m) (pure H.empty) + H.foldrWithKey + (\k v m -> H.insert <$> f k Key k <*> p v Key k <*> m) (pure H.empty) + . KM.toHashMap FromJSONKeyValue f -> withArray "Map" $ \arr -> fmap H.fromList . Tr.sequence . zipWith (parseIndexedJSONPair f p) [0..] . V.toList $ arr @@ -2274,7 +2277,7 @@ instance FromJSONKey b => FromJSONKey (Tagged a b) where -- | @since 1.5.1.0 instance (FromJSON a, FromJSON b) => FromJSON (These a b) where - parseJSON = withObject "These a b" (p . H.toList) + parseJSON = withObject "These a b" (p . KM.toList) where p [("This", a), ("That", b)] = These <$> parseJSON a <*> parseJSON b p [("That", b), ("This", a)] = These <$> parseJSON a <*> parseJSON b @@ -2284,7 +2287,7 @@ instance (FromJSON a, FromJSON b) => FromJSON (These a b) where -- | @since 1.5.1.0 instance FromJSON a => FromJSON1 (These a) where - liftParseJSON pb _ = withObject "These a b" (p . H.toList) + liftParseJSON pb _ = withObject "These a b" (p . KM.toList) where p [("This", a), ("That", b)] = These <$> parseJSON a <*> pb b p [("That", b), ("This", a)] = These <$> parseJSON a <*> pb b @@ -2294,7 +2297,7 @@ instance FromJSON a => FromJSON1 (These a) where -- | @since 1.5.1.0 instance FromJSON2 These where - liftParseJSON2 pa _ pb _ = withObject "These a b" (p . H.toList) + liftParseJSON2 pa _ pb _ = withObject "These a b" (p . KM.toList) where p [("This", a), ("That", b)] = These <$> pa a <*> pb b p [("That", b), ("This", a)] = These <$> pa a <*> pb b @@ -2304,7 +2307,7 @@ instance FromJSON2 These where -- | @since 1.5.1.0 instance (FromJSON1 f, FromJSON1 g) => FromJSON1 (These1 f g) where - liftParseJSON px pl = withObject "These1" (p . H.toList) + liftParseJSON px pl = withObject "These1" (p . KM.toList) where p [("This", a), ("That", b)] = These1 <$> liftParseJSON px pl a <*> liftParseJSON px pl b p [("That", b), ("This", a)] = These1 <$> liftParseJSON px pl a <*> liftParseJSON px pl b diff --git a/src/Data/Aeson/Types/Internal.hs b/src/Data/Aeson/Types/Internal.hs index 24f7df492..a126b1714 100644 --- a/src/Data/Aeson/Types/Internal.hs +++ b/src/Data/Aeson/Types/Internal.hs @@ -86,16 +86,13 @@ module Data.Aeson.Types.Internal import Prelude.Compat import Control.Applicative (Alternative(..)) -import Control.Arrow (first) import Control.DeepSeq (NFData(..)) import Control.Monad (MonadPlus(..), ap) import Data.Char (isLower, isUpper, toLower, isAlpha, isAlphaNum) import Data.Data (Data) import Data.Foldable (foldl') -import Data.HashMap.Strict (HashMap) import Data.Hashable (Hashable(..)) -import Data.List (intercalate, sortBy) -import Data.Ord (comparing) +import Data.List (intercalate) import Data.Scientific (Scientific) import Data.String (IsString(..)) import Data.Text (Text, pack, unpack) @@ -104,12 +101,13 @@ import Data.Time.Format (FormatTime) import Data.Typeable (Typeable) import Data.Vector (Vector) import GHC.Generics (Generic) +import Data.Aeson.KeyMap (KeyMap) import qualified Control.Monad as Monad import qualified Control.Monad.Fail as Fail -import qualified Data.HashMap.Strict as H import qualified Data.Scientific as S import qualified Data.Vector as V import qualified Language.Haskell.TH.Syntax as TH +import qualified Data.Aeson.KeyMap as KM -- | Elements of a JSON path used to describe the location of an -- error. @@ -351,7 +349,7 @@ apP d e = do {-# INLINE apP #-} -- | A JSON \"object\" (key\/value map). -type Object = HashMap Text Value +type Object = KeyMap Value -- | A JSON \"array\" (sequence). type Array = Vector Value @@ -385,7 +383,7 @@ instance Show Value where $ showString "Array " . showsPrec 11 xs showsPrec d (Object xs) = showParen (d > 10) $ showString "Object (fromList " - . showsPrec 11 (sortBy (comparing fst) (H.toList xs)) + . showsPrec 11 (KM.toAscList xs) . showChar ')' -- | @@ -445,8 +443,7 @@ instance TH.Lift Value where where s = unpack t lift (Array a) = [| Array (V.fromList a') |] where a' = V.toList a - lift (Object o) = [| Object (H.fromList . map (first pack) $ o') |] - where o' = map (first unpack) . H.toList $ o + lift (Object o) = [| Object o |] #if MIN_VERSION_template_haskell(2,17,0) liftTyped = TH.unsafeCodeCoerce . TH.lift @@ -466,7 +463,7 @@ isEmptyArray _ = False -- | The empty object. emptyObject :: Value -emptyObject = Object H.empty +emptyObject = Object KM.empty -- | Run a 'Parser'. parse :: (a -> Parser b) -> a -> Result b @@ -534,7 +531,7 @@ type Pair = (Text, Value) -- | Create a 'Value' from a list of name\/value 'Pair's. If duplicate -- keys arise, later keys and their associated values win. object :: [Pair] -> Value -object = Object . H.fromList +object = Object . KM.fromList {-# INLINE object #-} -- | Add JSON Path context to a parser diff --git a/src/Data/Aeson/Types/ToJSON.hs b/src/Data/Aeson/Types/ToJSON.hs index 76c9feb06..b14d693ee 100644 --- a/src/Data/Aeson/Types/ToJSON.hs +++ b/src/Data/Aeson/Types/ToJSON.hs @@ -63,9 +63,10 @@ import Control.Applicative (Const(..)) import Control.Monad.ST (ST) import Data.Aeson.Encoding (Encoding, Encoding', Series, dict, emptyArray_) import Data.Aeson.Encoding.Internal ((>*<)) -import Data.Aeson.Internal.Functions (mapHashKeyVal, mapKeyVal) +import Data.Aeson.Internal.Functions (mapTextKeyVal, mapKeyVal) import Data.Aeson.Types.Generic (AllNullary, False, IsRecord, One, ProductSize, Tagged2(..), True, Zero, productSize) import Data.Aeson.Types.Internal +import qualified Data.Aeson.KeyMap as TM import Data.Attoparsec.Number (Number(..)) import Data.Bits (unsafeShiftR) import Data.DList (DList) @@ -335,11 +336,11 @@ instance KeyValue Pair where name .= value = (name, toJSON value) {-# INLINE (.=) #-} --- | Constructs a singleton 'H.HashMap'. For calling functions that +-- | Constructs a singleton 'TM.KeyMap'. For calling functions that -- demand an 'Object' for constructing objects. To be used in -- conjunction with 'mconcat'. Prefer to use 'object' where possible. instance KeyValue Object where - name .= value = H.singleton name (toJSON value) + name .= value = TM.singleton name (toJSON value) {-# INLINE (.=) #-} ------------------------------------------------------------------------------- @@ -1254,8 +1255,8 @@ instance (ToJSON a) => ToJSON (Maybe a) where instance ToJSON2 Either where - liftToJSON2 toA _ _toB _ (Left a) = Object $ H.singleton "Left" (toA a) - liftToJSON2 _toA _ toB _ (Right b) = Object $ H.singleton "Right" (toB b) + liftToJSON2 toA _ _toB _ (Left a) = Object $ TM.singleton "Left" (toA a) + liftToJSON2 _toA _ toB _ (Right b) = Object $ TM.singleton "Right" (toB b) liftToEncoding2 toA _ _toB _ (Left a) = E.pairs $ E.pair "Left" $ toA a liftToEncoding2 _toA _ toB _ (Right b) = E.pairs $ E.pair "Right" $ toB b @@ -1583,8 +1584,8 @@ instance (ToJSON1 f, ToJSON1 g, ToJSON a) => ToJSON (Product f g a) where toEncoding = toEncoding1 instance (ToJSON1 f, ToJSON1 g) => ToJSON1 (Sum f g) where - liftToJSON tv tvl (InL x) = Object $ H.singleton "InL" (liftToJSON tv tvl x) - liftToJSON tv tvl (InR y) = Object $ H.singleton "InR" (liftToJSON tv tvl y) + liftToJSON tv tvl (InL x) = Object $ TM.singleton "InL" (liftToJSON tv tvl x) + liftToJSON tv tvl (InR y) = Object $ TM.singleton "InR" (liftToJSON tv tvl y) liftToEncoding te tel (InL x) = E.pairs $ E.pair "InL" $ liftToEncoding te tel x liftToEncoding te tel (InR y) = E.pairs $ E.pair "InR" $ liftToEncoding te tel y @@ -1638,7 +1639,7 @@ instance ToJSON a => ToJSON (IntMap.IntMap a) where instance ToJSONKey k => ToJSON1 (M.Map k) where liftToJSON g _ = case toJSONKey of - ToJSONKeyText f _ -> Object . mapHashKeyVal f g + ToJSONKeyText f _ -> Object . mapTextKeyVal f g ToJSONKeyValue f _ -> Array . V.fromList . map (toJSONPair f g) . M.toList liftToEncoding g _ = case toJSONKey of @@ -1737,10 +1738,11 @@ instance (ToJSON a) => ToJSON (HashSet.HashSet a) where instance ToJSONKey k => ToJSON1 (H.HashMap k) where liftToJSON g _ = case toJSONKey of - ToJSONKeyText f _ -> Object . mapKeyVal f g - ToJSONKeyValue f _ -> Array . V.fromList . map (toJSONPair f g) . H.toList + ToJSONKeyText f _ -> Object . TM.fromHashMap . mapKeyVal f g + ToJSONKeyValue f _ + -> Array . V.fromList . map (toJSONPair f g) . H.toList - -- liftToEncoding :: forall a. (a -> Encoding) -> ([a] -> Encoding) -> H.HashMap k a -> Encoding + -- liftToEncoding :: forall a. (a -> Encoding) -> ([a] -> Encoding) -> TM.HashMap k a -> Encoding liftToEncoding g _ = case toJSONKey of ToJSONKeyText _ f -> dict f g H.foldrWithKey ToJSONKeyValue _ f -> listEncoding (pairEncoding f) . H.toList @@ -1748,6 +1750,19 @@ instance ToJSONKey k => ToJSON1 (H.HashMap k) where pairEncoding f (a, b) = E.list id [f a, g b] instance (ToJSON v, ToJSONKey k) => ToJSON (H.HashMap k v) where + toJSON = toJSON1 + toEncoding = toEncoding1 + +------------------------------------------------------------------------------- +-- Data.Aeson.KeyMap +------------------------------------------------------------------------------- + +instance ToJSON1 TM.KeyMap where + liftToJSON g _ = Object . fmap g + + liftToEncoding g _ = dict E.text g TM.foldrWithKey + +instance (ToJSON v) => ToJSON (TM.KeyMap v) where {-# SPECIALIZE instance ToJSON Object #-} toJSON = toJSON1 diff --git a/tests/Instances.hs b/tests/Instances.hs index 21a0355d9..f683b7f36 100644 --- a/tests/Instances.hs +++ b/tests/Instances.hs @@ -13,6 +13,7 @@ import Prelude.Compat import Control.Applicative (empty) import Control.Monad import Data.Aeson.Types +import qualified Data.Aeson.KeyMap as KM import Data.Function (on) import Data.Time (ZonedTime(..), TimeZone(..)) import Data.Time.Clock (UTCTime(..)) @@ -23,6 +24,7 @@ import qualified Data.DList as DList import qualified Data.Vector as V import qualified Data.HashMap.Strict as HM + import Data.Orphans () import Test.QuickCheck.Instances () @@ -185,7 +187,7 @@ instance Arbitrary Value where obj n = do pars <- arbPartition (n - 1) - fmap (Object . HM.fromList) (traverse pair pars) + fmap (Object . KM.fromList) (traverse pair pars) pair n = do k <- arbitrary diff --git a/tests/PropUtils.hs b/tests/PropUtils.hs index de997bedf..ac7325931 100644 --- a/tests/PropUtils.hs +++ b/tests/PropUtils.hs @@ -12,6 +12,7 @@ import Data.Aeson.Internal (IResult(..), formatError, ifromJSON, iparse) import qualified Data.Aeson.Internal as I import Data.Aeson.Parser (value) import Data.Aeson.Types +import qualified Data.Aeson.KeyMap as KM import Data.HashMap.Strict (HashMap) import Data.Hashable (Hashable) import Data.Int (Int8) @@ -178,8 +179,8 @@ is2ElemArray (Array v) = V.length v == 2 && isString (V.head v) is2ElemArray _ = False isTaggedObjectValue :: Value -> Bool -isTaggedObjectValue (Object obj) = "tag" `H.member` obj && - "contents" `H.member` obj +isTaggedObjectValue (Object obj) = "tag" `KM.member` obj && + "contents" `KM.member` obj isTaggedObjectValue _ = False isNullaryTaggedObject :: Value -> Bool @@ -189,11 +190,11 @@ isTaggedObject :: Value -> Property isTaggedObject = checkValue isTaggedObject' isTaggedObject' :: Value -> Bool -isTaggedObject' (Object obj) = "tag" `H.member` obj +isTaggedObject' (Object obj) = "tag" `KM.member` obj isTaggedObject' _ = False isObjectWithSingleField :: Value -> Bool -isObjectWithSingleField (Object obj) = H.size obj == 1 +isObjectWithSingleField (Object obj) = KM.size obj == 1 isObjectWithSingleField _ = False -- | is untaggedValue of EitherTextInt diff --git a/tests/UnitTests.hs b/tests/UnitTests.hs index e5c73f469..f0b237b30 100644 --- a/tests/UnitTests.hs +++ b/tests/UnitTests.hs @@ -38,6 +38,7 @@ import Data.Aeson.Types ( Options(..), Result(Success, Error), ToJSON(..) , Value(Array, Bool, Null, Number, Object, String), camelTo, camelTo2 , defaultOptions, formatPath, formatRelativePath, omitNothingFields, parse) +import qualified Data.Aeson.KeyMap as KM import Data.Attoparsec.ByteString (Parser, parseOnly) import Data.Char (toUpper) import Data.Either.Compat (isLeft, isRight) @@ -63,7 +64,6 @@ import qualified Data.ByteString as S import qualified Data.ByteString.Base16.Lazy as LBase16 import qualified Data.ByteString.Lazy.Char8 as L import qualified Data.HashSet as HashSet -import qualified Data.HashMap.Lazy as HashMap import qualified Data.Text.Lazy as LT import qualified Data.Text.Lazy.Builder as TLB import qualified Data.Text.Lazy.Encoding as LT @@ -624,26 +624,26 @@ keyOrdering :: [TestTree] keyOrdering = [ testParser "json" json "{\"k\":true,\"k\":false}" $ - Right (Object (HashMap.fromList [("k", Bool True)])) + Right (Object (KM.fromList [("k", Bool True)])) , testParser "jsonLast" jsonLast "{\"k\":true,\"k\":false}" $ - Right (Object (HashMap.fromList [("k", Bool False)])) + Right (Object (KM.fromList [("k", Bool False)])) , testParser "jsonAccum" jsonAccum "{\"k\":true,\"k\":false}" $ - Right (Object (HashMap.fromList [("k", Array (Vector.fromList [Bool True, Bool False]))])) + Right (Object (KM.fromList [("k", Array (Vector.fromList [Bool True, Bool False]))])) , testParser "jsonNoDup" jsonNoDup "{\"k\":true,\"k\":false}" $ Left "Failed reading: found duplicate key: \"k\"" , testParser "json'" json' "{\"k\":true,\"k\":false}" $ - Right (Object (HashMap.fromList [("k", Bool True)])) + Right (Object (KM.fromList [("k", Bool True)])) , testParser "jsonLast'" jsonLast' "{\"k\":true,\"k\":false}" $ - Right (Object (HashMap.fromList [("k", Bool False)])) + Right (Object (KM.fromList [("k", Bool False)])) , testParser "jsonAccum'" jsonAccum' "{\"k\":true,\"k\":false}" $ - Right (Object (HashMap.fromList [("k", Array (Vector.fromList [Bool True, Bool False]))])) + Right (Object (KM.fromList [("k", Array (Vector.fromList [Bool True, Bool False]))])) , testParser "jsonNoDup'" jsonNoDup' "{\"k\":true,\"k\":false}" $ Left "Failed reading: found duplicate key: \"k\""