pipes-aeson 0.4.1.8 → 0.4.2
raw patch · 6 files changed
+183/−27 lines, 6 filesdep +attoparsec-aesonsetup-changedPVP: major bump suggested
API removals or changes: PVP suggests a major version bump
Dependencies added: attoparsec-aeson
API changes (from Hackage documentation)
+ Pipes.Aeson: loop :: (Monad m, FromJSON a) => (Producer ByteString m r -> Producer ByteString m r) -> Producer ByteString m r -> Producer' (Either DecodingError a) m r
+ Pipes.Aeson: loopL :: (Monad m, FromJSON a) => (Producer ByteString m r -> Producer ByteString m r) -> Producer ByteString m r -> Proxy x' x () (Either DecodingError (Int, a)) m r
+ Pipes.Aeson.Unchecked: loop :: (Monad m, FromJSON a) => (Producer ByteString m r -> Producer ByteString m r) -> Producer ByteString m r -> Producer' (Either DecodingError a) m r
+ Pipes.Aeson.Unchecked: loopL :: (Monad m, FromJSON a) => (Producer ByteString m r -> Producer ByteString m r) -> Producer ByteString m r -> Proxy x' x () (Either DecodingError (Int, a)) m r
- Pipes.Aeson: FromJSONError :: String -> DecodingError
+ Pipes.Aeson: FromJSONError :: Value -> String -> DecodingError
- Pipes.Aeson: encodeArray :: Monad m => Array -> Producer' ByteString m ()
+ Pipes.Aeson: encodeArray :: Monad m => Array -> Proxy x' x () ByteString m ()
- Pipes.Aeson: encodeObject :: Monad m => Object -> Producer' ByteString m ()
+ Pipes.Aeson: encodeObject :: Monad m => Object -> Proxy x' x () ByteString m ()
- Pipes.Aeson.Unchecked: encode :: (Monad m, ToJSON a) => a -> Producer' ByteString m ()
+ Pipes.Aeson.Unchecked: encode :: (Monad m, ToJSON a) => a -> Proxy x' x () ByteString m ()
Files
- Setup.hs +0/−2
- changelog.md +13/−0
- pipes-aeson.cabal +7/−6
- src/Pipes/Aeson.hs +61/−4
- src/Pipes/Aeson/Internal.hs +46/−13
- src/Pipes/Aeson/Unchecked.hs +56/−2
− Setup.hs
@@ -1,2 +0,0 @@-import Distribution.Simple-main = defaultMain
changelog.md view
@@ -1,3 +1,16 @@+# 0.4.2++* BREAKING CHANGE: `FromJSONError` now carries the failing `Value` as payload.++* BREAKING CHANGE: Removed support for `ErrorT`.++* Added `loop` and `loopL`.++* GHC >= 9.0 compatibility.++* Add dependency on `attoparsec-aeson`.++ # 0.4.1.8 * Remove upper bound dependencies on everything but `base`.
pipes-aeson.cabal view
@@ -1,17 +1,16 @@+cabal-version: 2.4 name: pipes-aeson-version: 0.4.1.8-license: BSD3+version: 0.4.2+license: BSD-3-Clause license-file: LICENSE-copyright: Copyright (c) Renzo Carbonara 2013-2017+copyright: Copyright (c) Renzo Carbonara 2013 author: Renzo Carbonara-maintainer: renzocarbonaraλgmail.com+maintainer: renλren.zone stability: Experimental-tested-with: GHC ==8.0.2 homepage: https://github.com/k0001/pipes-aeson bug-reports: https://github.com/k0001/pipes-aeson/issues category: Pipes, Parser build-type: Simple-cabal-version: >=1.8 synopsis: Encode and decode JSON streams using Aeson and Pipes. extra-source-files: README.md PEOPLE changelog.md description:@@ -25,6 +24,7 @@ location: git://github.com/k0001/pipes-aeson.git library+ default-language: Haskell2010 hs-source-dirs: src exposed-modules: Pipes.Aeson Pipes.Aeson.Unchecked@@ -32,6 +32,7 @@ build-depends: aeson (>=0.6.1) , attoparsec (>=0.10)+ , attoparsec-aeson (>=2.2) , base (>=4.5 && <5.0) , pipes (>=4.1) , pipes-attoparsec (>=0.5)
src/Pipes/Aeson.hs view
@@ -24,16 +24,18 @@ -- $decoding , decode , decoded+ , loop -- ** Including lengths , decodeL , decodedL+ , loopL -- * Types , I.DecodingError(..) ) where -import Control.Monad (liftM) import qualified Data.Aeson as Ae+import qualified Data.Aeson.Parser as Ae import qualified Data.ByteString.Char8 as B import Pipes import qualified Pipes.Aeson.Internal as I@@ -60,7 +62,7 @@ -- @ -- 'for' 'cat' 'encodeObject' :: 'Monad' m => 'Pipe' 'Ae.Object' 'B.ByteString' m r -- @-encodeObject :: Monad m => Ae.Object -> Producer' B.ByteString m ()+encodeObject :: Monad m => Ae.Object -> Proxy x' x () B.ByteString m () encodeObject = U.encode {-# INLINABLE encodeObject #-} {-# RULES "p >-> for cat encodeObject" forall p .@@ -75,7 +77,7 @@ -- @ -- 'for' 'cat' 'encodeArray' :: 'Monad' m => 'Pipe' 'Ae.Array' 'B.ByteString' m r -- @-encodeArray :: Monad m => Ae.Array -> Producer' B.ByteString m ()+encodeArray :: Monad m => Ae.Array -> Proxy x' x () B.ByteString m () encodeArray = U.encode {-# INLINABLE encodeArray #-} {-# RULES "p >-> for cat encodeArray" forall p .@@ -108,7 +110,7 @@ decode :: (Monad m, Ae.FromJSON a) => Pipes.Parser B.ByteString m (Maybe (Either I.DecodingError a))-decode = fmap (fmap snd) `liftM` decodeL+decode = fmap (fmap (fmap snd)) decodeL {-# INLINABLE decode #-} -- | Like 'decode', except it also returns the length of JSON input that was@@ -172,6 +174,61 @@ Right r -> return r {-# INLINABLE decodedL #-} +-- | Repeteadly try to parse raw JSON bytes into @a@ values, reporting any+-- 'I.DecodingError's downstream as they happen.+--+-- /Note:/ The JSON RFC-4627 standard only allows arrays or objects as top-level+-- entities, which is why these functions restrict their input to them. If you+-- prefer to ignore the standard and encode any 'Ae.Value', then use 'U.encode'+-- from the "Pipes.Aeson.Unchecked" module.+loop+ :: (Monad m, Ae.FromJSON a)+ => (Pipes.Producer B.ByteString m r -> Pipes.Producer B.ByteString m r)+ -- ^ In case of 'I.AttoparsecError', this function will be called to modify+ -- the leftovers 'Pipes.Producer' before using it.+ --+ -- Ideally you will want to drop everything until the beginning of the next+ -- JSON element. This is easy to accomplish if there is a clear whitespace+ -- delimiter between the JSON elements, such as a newline (i.e.,+ -- @'Pipes.ByteString.drop' 1 . 'Pipes.ByteString.dropWhile' (/= 0xA)@).+ -- However, it can be hard to do correctly is there is no such delimiter.+ -- Skipping the first character (i.e., @'Pipes.ByteString.drop' 1@) should be+ -- sufficient in most cases, but not when parsing recursive data structures+ -- because you can accidentally parse a child in its parent's stead.+ --+ -- Notice that unless you advance the 'Pipes.Producer' somehow, 'loop'+ -- will never terminate.+ -> Pipes.Producer B.ByteString m r+ -- ^ Raw JSON input.+ -> Pipes.Producer' (Either I.DecodingError a) m r+{-# INLINABLE loop #-}+loop fp p0 = Pipes.for (loopL fp p0) (Pipes.yield . fmap snd)++-- | Like 'loop', except it also outputs the length of JSON input that was+-- consumed in order to obtain the value, not including the length of whitespace+-- before nor after the parsed JSON input.+loopL+ :: (Monad m, Ae.FromJSON a)+ => (Pipes.Producer B.ByteString m r -> Pipes.Producer B.ByteString m r)+ -- ^ In case of 'I.AttoparsecError', this function will be called to modify+ -- the leftovers 'Pipes.Producer' before using it.+ --+ -- Ideally you will want to drop everything until the beginning of the next+ -- JSON element. This is easy to accomplish if there is a clear whitespace+ -- delimiter between the JSON elements, such as a newline (i.e.,+ -- @'Pipes.ByteString.drop' 1 . 'Pipes.ByteString.dropWhile' (/= 0xA)@).+ -- However, it can be hard to do correctly is there is no such delimiter.+ -- Skipping the first character (i.e., @'Pipes.ByteString.drop' 1@) should be+ -- sufficient in most cases, but not when parsing recursive data structures+ -- because you can accidentally parse a child in its parent's stead.+ --+ -- Notice that unless you advance the 'Pipes.Producer' somehow, 'loopL'+ -- will never terminate.+ -> Pipes.Producer B.ByteString m r+ -- ^ Raw JSON input.+ -> Pipes.Proxy x' x () (Either I.DecodingError (Int, a)) m r+{-# INLINABLE loopL #-}+loopL = I.loopL Ae.json' -------------------------------------------------------------------------------- -- Internal tools --------------------------------------------------------------
src/Pipes/Aeson/Internal.hs view
@@ -1,5 +1,6 @@ {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE LambdaCase #-} {-# LANGUAGE RankNTypes #-} -- | This module provides internal utilities and it is likely@@ -10,15 +11,16 @@ ( DecodingError(..) , consecutively , decodeL+ , loopL ) where import Control.Exception (Exception)-import Control.Monad.Trans.Error (Error) import qualified Control.Monad.Trans.State.Strict as S import qualified Data.Aeson as Ae import qualified Data.Attoparsec.Types as Attoparsec import qualified Data.ByteString as B import qualified Data.ByteString.Internal as B (isSpaceWord8) import Data.Data (Data, Typeable)+import Data.Function (fix) import Pipes import qualified Pipes.Attoparsec as PA import qualified Pipes.Parse as Pipes@@ -29,19 +31,14 @@ data DecodingError = AttoparsecError PA.ParsingError -- ^An @attoparsec@ error that happened while parsing the raw JSON string.- | FromJSONError String- -- ^An @aeson@ error that happened while trying to convert a- -- 'Data.Aeson.Value' to an 'A.FromJSON' instance, as reported by+ | FromJSONError Ae.Value String+ -- ^An @aeson@ error that happened while trying to convert the given+ -- 'Data.Aeson.Value' to an 'Ae.FromJSON' instance, as reported by -- 'Data.Aeson.Error'. deriving (Show, Eq, Data, Typeable) instance Exception DecodingError-instance Error DecodingError --- | This instance allows using 'Pipes.Lift.errorP' with 'Pipes.Aeson.decoded'--- and 'Pipes.Aeson.decodedL'-instance Error (DecodingError, Producer a m r)- -------------------------------------------------------------------------------- -- | Consecutively parse 'a' elements from the given 'Producer' using the given@@ -50,11 +47,11 @@ -- -- This 'Producer' runs until it either runs out of input or until a decoding -- failure occurs, in which case it returns 'Left' with a 'DecodingError' and--- a 'Producer' with any leftovers. You can use 'Pipes.Lift.errorP' to turn the--- 'Either' return value into an 'Control.Monad.Trans.Error.ErrorT'+-- a 'Producer' with any leftovers. You can use 'Pipes.Lift.exceptP' to turn the+-- 'Either' return value into an 'Control.Monad.Trans.ExceptT' -- monad transformer. consecutively- :: (Monad m)+ :: Monad m => Pipes.Parser B.ByteString m (Maybe (Either e a)) -> Producer B.ByteString m r -- ^Producer from which to draw raw input. -> Producer a m (Either (e, Producer B.ByteString m r) r)@@ -87,10 +84,46 @@ Nothing -> Nothing Just (Left l) -> Just (Left (AttoparsecError l)) Just (Right (n, v)) -> case Ae.fromJSON v of- Ae.Error e -> Just (Left (FromJSONError e))+ Ae.Error e -> Just (Left (FromJSONError v e)) Ae.Success a -> Just (Right (n, a)) {-# INLINABLE decodeL #-} +--------------------------------------------------------------------------------++-- | Repeteadly try to parse raw JSON bytes into @a@ values, reporting any+-- 'I.DecodingError's downstream as they happen.+loopL+ :: (Monad m, Ae.FromJSON a)+ => Attoparsec.Parser B.ByteString Ae.Value+ -> (Pipes.Producer B.ByteString m r -> Pipes.Producer B.ByteString m r)+ -- ^ In case of 'AttoparsecError', this function will be called to modify+ -- the leftovers 'Pipes.Producer' before using it.+ --+ -- Ideally you will want to drop everything until the beginning of the next+ -- JSON element. This is easy to accomplish if there is a clear whitespace+ -- delimiter between the JSON elements, such as a newline (i.e.,+ -- @'Pipes.ByteString.drop' 1 . 'Pipes.ByteString.dropWhile' (/= 0xA)@).+ -- However, it can be hard to do correctly is there is no such delimiter.+ -- Skipping the first character (i.e., @'Pipes.ByteString.drop' 1@) should be+ -- sufficient in most cases, but not when parsing recursive data structures+ -- because you can accidentally parse a child in its parent's stead.+ --+ -- Notice that unless you advance the 'Pipes.Producer' somehow, 'loopL' will+ -- never terminate.+ -> Pipes.Producer B.ByteString m r+ -- ^ Raw JSON input.+ -> Pipes.Proxy x' x () (Either DecodingError (Int, a)) m r+{-# INLINABLE loopL #-}+loopL parser fp = fix $ \k p0 -> do+ (ye, p1) <- lift (S.runStateT (decodeL parser) p0)+ case ye of+ Just (Left e@AttoparsecError{}) -> do+ Pipes.yield (Left e)+ k (fp p1)+ Just ea -> Pipes.yield ea >> k p1+ Nothing -> lift (Pipes.next p1) >>= \case+ Left r -> pure r+ Right _ -> error "Pipes.Aeson.Internal.loopL: impossible" -------------------------------------------------------------------------------- -- Internal stuff
src/Pipes/Aeson/Unchecked.hs view
@@ -12,9 +12,11 @@ -- * Decoding , decode , decoded+ , loop -- ** Including lenghts , decodeL , decodedL+ , loopL ) where import Control.Monad (liftM)@@ -30,8 +32,9 @@ -- | Like 'Pipes.Aeson.encode', except it accepts any 'Ae.ToJSON' instance, -- not just 'Ae.Array' or 'Ae.Object'.-encode :: (Monad m, Ae.ToJSON a) => a -> Producer' B.ByteString m ()-encode = PB.fromLazy . Ae.encode+-- encode :: (Monad m, Ae.ToJSON a) => a -> Producer' B.ByteString m ()+encode :: (Monad m, Ae.ToJSON a) => a -> Proxy x' x () PB.ByteString m ()+encode a = PB.fromLazy (Ae.encode a) {-# INLINABLE encode #-} {-# RULES "p >-> for cat encode" forall p . p >-> for cat encode = for p (\a -> PB.fromLazy (Ae.encode a))@@ -93,6 +96,57 @@ {-# INLINE _encode #-} {-# INLINABLE decodedL #-} ++-- | Repeteadly try to parse raw JSON bytes into @a@ values, reporting any+-- 'I.DecodingError's downstream as they happen.+loop+ :: (Monad m, Ae.FromJSON a)+ => (Pipes.Producer B.ByteString m r -> Pipes.Producer B.ByteString m r)+ -- ^ In case of 'I.AttoparsecError', this function will be called to modify+ -- the leftovers 'Pipes.Producer' before using it.+ --+ -- Ideally you will want to drop everything until the beginning of the next+ -- JSON element. This is easy to accomplish if there is a clear whitespace+ -- delimiter between the JSON elements, such as a newline (i.e.,+ -- @'Pipes.ByteString.drop' 1 . 'Pipes.ByteString.dropWhile' (/= 0xA)@).+ -- However, it can be hard to do correctly is there is no such delimiter.+ -- Skipping the first character (i.e., @'Pipes.ByteString.drop' 1@) should be+ -- sufficient in most cases, but not when parsing recursive data structures+ -- because you can accidentally parse a child in its parent's stead.+ --+ -- Notice that unless you advance the 'Pipes.Producer' somehow, 'loop'+ -- will never terminate.+ -> Pipes.Producer B.ByteString m r+ -- ^ Raw JSON input.+ -> Pipes.Producer' (Either I.DecodingError a) m r+{-# INLINABLE loop #-}+loop fp p0 = Pipes.for (loopL fp p0) (Pipes.yield . fmap snd)++-- | Like 'loop', except it also outputs the length of JSON input that was+-- consumed in order to obtain the value, not including the length of whitespace+-- before nor after the parsed JSON input.+loopL+ :: (Monad m, Ae.FromJSON a)+ => (Pipes.Producer B.ByteString m r -> Pipes.Producer B.ByteString m r)+ -- ^ In case of 'I.AttoparsecError', this function will be called to modify+ -- the leftovers 'Pipes.Producer' before using it.+ --+ -- Ideally you will want to drop everything until the beginning of the next+ -- JSON element. This is easy to accomplish if there is a clear whitespace+ -- delimiter between the JSON elements, such as a newline (i.e.,+ -- @'Pipes.ByteString.drop' 1 . 'Pipes.ByteString.dropWhile' (/= 0xA)@).+ -- However, it can be hard to do correctly is there is no such delimiter.+ -- Skipping the first character (i.e., @'Pipes.ByteString.drop' 1@) should be+ -- sufficient in most cases, but not when parsing recursive data structures+ -- because you can accidentally parse a child in its parent's stead.+ --+ -- Notice that unless you advance the 'Pipes.Producer' somehow, 'loopL'+ -- will never terminate.+ -> Pipes.Producer B.ByteString m r+ -- ^ Raw JSON input.+ -> Pipes.Proxy x' x () (Either I.DecodingError (Int, a)) m r+{-# INLINABLE loopL #-}+loopL = I.loopL Ae.value' -------------------------------------------------------------------------------- -- Internal tools --------------------------------------------------------------