pipes-aeson 0.2.1 → 0.3.0
raw patch · 7 files changed
+283/−180 lines, 7 filesdep +pipes-bytestringdep ~pipesdep ~pipes-attoparsecdep ~pipes-parse
Dependencies added: pipes-bytestring
Dependency ranges changed: pipes, pipes-attoparsec, pipes-parse
Files
- PEOPLE +1/−0
- changelog +7/−0
- pipes-aeson.cabal +6/−6
- src/Pipes/Aeson.hs +108/−46
- src/Pipes/Aeson/Internal.hs +51/−68
- src/Pipes/Aeson/Unchecked.hs +110/−0
- src/Pipes/Aeson/Unsafe.hs +0/−60
PEOPLE view
@@ -5,3 +5,4 @@ Renzo Carbonara Michael Thompson David Flemström+Gabriel Gonzalez
changelog view
@@ -1,3 +1,10 @@+# 0.3.0++* API revamped to be compatible with `pipes-parse-3.0.*`.++* Renamed `Pipes.Aeson.Unsafe` module to `Pipes.Aeson.Unchecked`.++ # 0.2.1 * Generalize `encode` from `Producer` to `Producer'`.
pipes-aeson.cabal view
@@ -1,5 +1,5 @@ name: pipes-aeson-version: 0.2.1+version: 0.3.0 license: BSD3 license-file: LICENSE copyright: Copyright (c) Renzo Carbonara 2013-2014@@ -27,16 +27,16 @@ library hs-source-dirs: src exposed-modules: Pipes.Aeson- Pipes.Aeson.Unsafe+ Pipes.Aeson.Unchecked other-modules: Pipes.Aeson.Internal build-depends: aeson (>=0.6.1 && <0.8) , attoparsec (>=0.10 && <0.12) , base (>=4.5 && <5.0)- , pipes (>=4.0 && <4.1)- , pipes-attoparsec (>=0.3.1 && <0.4)- , pipes-parse (>=2.0 && <2.1)+ , pipes (>=4.1 && <4.2)+ , pipes-attoparsec (>=0.4 && <0.5)+ , pipes-bytestring (>=2.0 && <2.1)+ , pipes-parse (>=3.0.1 && <3.1) , bytestring (>=0.9.2.1) , transformers (>=0.2 && <0.4) ghc-options: -Wall -O2- ghc-prof-options: -fprof-auto
src/Pipes/Aeson.hs view
@@ -4,26 +4,39 @@ -- through Pipes streams. -- -- This module builds on top of the @aeson@, @pipes@ and @pipes-parse@--- libraries, and assumes you know how to use them.+-- libraries, and assumes you know how to use them. Please read the examples+-- in "Pipes.Parse.Tutorial" to understand how to use these functions.+--+-- In this module, the following type synonym compatible with the @lens@,+-- @lens-family@ and @lens-family-core@ libraries is used but not exported:+--+-- @+-- type Lens' s a = forall f . 'Functor' f => (a -> f a) -> (s -> f s)+-- @ module Pipes.Aeson ( -- * Encoding encode+ -- * Decoding -- $decoding , decode- , decodeMany+ , decoded+ -- ** Including lengths+ , decodeL+ , decodedL+ -- * Types , I.DecodingError(..) ) where +import qualified Data.Aeson as Ae+import qualified Data.ByteString.Char8 as B import Pipes-import qualified Pipes.Aeson.Internal as I-import qualified Pipes.Aeson.Unsafe as U-import qualified Pipes.Attoparsec as PA-import qualified Control.Monad.Trans.State.Strict as S-import qualified Data.Aeson as Ae-import qualified Data.ByteString.Char8 as B+import qualified Pipes.Aeson.Internal as I+import qualified Pipes.Aeson.Unchecked as U+import qualified Pipes.Attoparsec as PA+import qualified Pipes.Parse as Pipes -------------------------------------------------------------------------------- @@ -33,7 +46,7 @@ -- /Note:/ The JSON RFC-4627 standard only allows arrays or objects as top-level -- entities, which is why this function restricts its input to them. If you -- prefer to ignore the standard and encode any 'Ae.Value', then use 'U.encode'--- from the "Pipes.Aeson.Unsafe" module.+-- from the "Pipes.Aeson.Unchecked" module. -- -- /Hint:/ You can easily turn this 'Producer'' into a 'Pipe' that encodes -- 'Ae.Array' or 'Ae.Object' values as JSON as they flow downstream using:@@ -42,13 +55,17 @@ -- 'for' 'cat' 'encode' :: ('Monad' m) => 'Pipe' ('Either' 'Ae.Object' 'Ae.Array') 'B.ByteString' m r -- @ encode :: Monad m => Either Ae.Object Ae.Array -> Producer' B.ByteString m ()-encode = either U.encode U.encode+encode (Left x) = U.encode x+encode (Right x) = U.encode x {-# INLINABLE encode #-}+{-# RULES "p >-> for cat encode" forall p .+ p >-> for cat encode = for p (\a -> encode a)+ #-} -------------------------------------------------------------------------------- -- $decoding ----- Decoding JSON as a Haskell value in involves two different steps:+-- Decoding JSON as a Haskell value involves two different steps: -- -- * Parsing a raw JSON 'B.ByteString' into an 'Ae.Object' or an 'Ae.Array'. --@@ -56,55 +73,100 @@ -- 'Ae.FromJSON' instance. -- -- Any of those steps can fail, in which case a 'I.DecodingError' will report--- the precise error and at which the step it appened.+-- the precise error and at which the step it happened. -- | Decodes an 'Ae.Object' or 'Ae.Array' JSON value from the underlying state. ----- Returns either the decoded entitiy and the number of decoded bytes,--- or a 'I.DecodingError' in case of failures.+-- Returns either the decoded entitiy, or a 'I.DecodingError' in case of error. -- -- /Do not/ use this function if the underlying 'Producer' has leading empty -- chunks or whitespace, otherwise you may get unexpected parsing errors. -- -- /Note:/ The JSON RFC-4627 standard only allows arrays or objects as top-level--- entities, which is why this 'Producer' restricts its output to them. If you--- prefer to ignore the standard and decode any 'Ae.Value', then use 'U.decode'--- from the "Pipes.Aeson.Unsafe" module.+-- entities, which is why this 'Pipes.Parser' restricts its output to them. If+-- you prefer to ignore the standard and decode any 'Ae.Value', then use+-- 'U.decode' from the "Pipes.Aeson.Unchecked" module. decode- :: (Monad m, Ae.FromJSON b)- => S.StateT (Producer B.ByteString m r) m (Either I.DecodingError (Int, b))+ :: (Monad m, Ae.FromJSON a)+ => Pipes.Parser B.ByteString m (Either I.DecodingError a) decode = do- ev <- PA.parse Ae.json'- return $ do- case ev of- Left e -> Left (I.ParserError e)- Right (len, v) -> do- case Ae.fromJSON v of- Ae.Error e -> Left (I.ValueError e)- Ae.Success b -> Right (len, b)+ x <- decodeL+ return (case x of+ Left e -> Left e+ Right (_, a) -> Right a) {-# INLINABLE decode #-} --- | Continuously 'decode' the JSON output from the given 'Producer', sending--- downstream pairs of each successfully decoded entity together with the number--- of bytes consumed in order to produce it. Whitespace in between JSON content--- is ignored.+-- | Like 'decode', except it also returns 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.+decodeL+ :: (Monad m, Ae.FromJSON a)+ => Pipes.Parser B.ByteString m (Either I.DecodingError (Int, a))+decodeL = do+ ev <- PA.parseL Ae.json'+ return (case ev of+ Left e -> Left (I.AttoparsecError e)+ Right (n, v) -> case Ae.fromJSON v of+ Ae.Error e -> Left (I.FromJSONError e)+ Ae.Success a -> Right (n, a))+{-# INLINABLE decodeL #-}+++-- | /Improper lens/ that turns a stream of raw JSON input into a stream of+-- 'Ae.FromJSON' and back. ----- This 'Producer' runs until it either runs out of input or until a decoding--- failure occurs, in which case it returns 'Left' with a 'I.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' monad--- transformer.+-- By /improper lens/ we mean that in practice you can't expect the+-- /Monad Morphism Laws/ to be true when using 'decoded' with+-- 'Control.Lens.zoom'. --+-- @+-- 'Control.Lens.zoom' 'decoded' ('return' r) /= 'return' r+-- 'Control.Lens.zoom' 'decoded' (m >>= k) /= 'Control.Lens.zoom' m >>= 'Control.Lens.zoom' . f+-- @+-- -- /Note:/ The JSON RFC-4627 standard only allows arrays or objects as top-level--- entities, which is why this 'Producer' restricts its output to them. If you--- prefer to ignore the standard and decode any 'Ae.Value', then use--- 'U.decodeMany' from the "Pipes.Aeson.Unsafe" module.-decodeMany- :: (Monad m, Ae.FromJSON b)- => Producer B.ByteString m r -- ^Producer from which to draw JSON.- -> Producer (Int, b) m- (Either (I.DecodingError, Producer B.ByteString m r) r)-decodeMany = I.consecutively decode-{-# INLINABLE decodeMany #-}+-- entities, which is why this function restricts its stream values to them. If+-- you prefer to ignore the standard and encode or decode any 'Ae.Value', then+-- use 'U.decoded' from the "Pipes.Aeson.Unchecked" module.+decoded+ :: (Monad m, Ae.FromJSON a, Ae.ToJSON a)+ => (Ae.Value -> Either Ae.Object Ae.Array)+ -- ^ A witness that @a@ can be represented either as an 'Ae.Object' or as+ -- an 'Ae.Array'. The passed in 'Ae.Value' is @'Ae.toJSON' a@+ -> Lens' (Producer B.ByteString m r)+ (Producer a m (Either (I.DecodingError, Producer B.ByteString m r) r))+decoded f k p0 = fmap _encode (k (I.consecutively decode p0))+ where+ _encode = \p -> do+ er <- for p (\a -> encode (f (Ae.toJSON a)))+ case er of+ Left (_, p') -> p'+ Right r -> return r+{-# INLINABLE decoded #-}++-- | Like 'decoded', except it also tags each decoded entity with the length of+-- JSON input that was consumed in order to obtain the value, not including the+-- length of whitespace between each parsed JSON input.+decodedL+ :: (Monad m, Ae.FromJSON a, Ae.ToJSON a)+ => (Ae.Value -> Either Ae.Object Ae.Array)+ -- ^ A witness that @a@ can be represented either as an 'Ae.Object' or as+ -- an 'Ae.Array'. The passed in 'Ae.Value' is @'Ae.toJSON' a@+ -> Lens' (Producer B.ByteString m r)+ (Producer (Int, a) m (Either (I.DecodingError, Producer B.ByteString m r) r))+decodedL f k p0 = fmap _encode (k (I.consecutively decode p0))+ where+ _encode = \p -> do+ er <- for p (\(_, a) -> encode (f (Ae.toJSON a)))+ case er of+ Left (_, p') -> p'+ Right r -> return r+{-# INLINABLE decodedL #-}+++--------------------------------------------------------------------------------+-- Internal tools --------------------------------------------------------------++type Lens' s a = forall f . Functor f => (a -> f a) -> (s -> f s)
src/Pipes/Aeson/Internal.hs view
@@ -1,7 +1,6 @@ {-# LANGUAGE DeriveDataTypeable #-}-{-# LANGUAGE RankNTypes #-}-{-# LANGUAGE FlexibleInstances #-}-{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE RankNTypes #-} -- | This module provides internal utilities and it is likely -- to be modified in backwards-incompatible ways in the future.@@ -10,95 +9,79 @@ module Pipes.Aeson.Internal ( DecodingError(..) , consecutively- , skipSpace- , fromLazy ) where import Control.Exception (Exception)-import qualified Control.Monad.Trans.Error as E+import Control.Monad.Trans.Error (Error) import qualified Control.Monad.Trans.State.Strict as S-import qualified Data.Aeson as Ae-import qualified Data.ByteString.Char8 as B-import qualified Data.ByteString.Lazy.Internal as BLI-import qualified Data.Char as Char+import qualified Data.ByteString as B+import qualified Data.ByteString.Internal as B (isSpaceWord8) import Data.Data (Data, Typeable) import Pipes-import qualified Pipes.Attoparsec as PA-import qualified Pipes.Parse as Pp-import qualified Pipes.Lift as P+import Pipes.Attoparsec (ParsingError)+import qualified Pipes.ByteString as PB+import qualified Pipes.Parse as Pipes -------------------------------------------------------------------------------- -- | An error while decoding a JSON value. data DecodingError- = ParserError PA.ParsingError- -- ^An Attoparsec error that happened while parsing the raw JSON string.- | ValueError String- -- ^An Aeson error that happened while trying to convert a- -- 'Data.Aeson.Value' to an 'A.FromJSON' instance, as reported by+ = AttoparsecError 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 -- 'Data.Aeson.Error'. deriving (Show, Eq, Data, Typeable) instance Exception DecodingError-instance E.Error DecodingError-instance Monad m => E.Error (DecodingError, Producer a m r)+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 'b' elements from the given 'Producer' using the given+-- | Consecutively parse 'a' elements from the given 'Producer' using the given -- parser (such as 'Pipes.Aeson.decode' or 'Pipes.Aeson.parseValue'), skipping -- any leading whitespace each time. -- -- This 'Producer' runs until it either runs out of input or until a decoding--- failure occurs, in which case it returns 'Left' with a 'I.DecodingError' and--- a 'Producer' with any leftovers. You can use 'P.errorP' to turn the 'Either'--- return value into an 'Control.Monad.Trans.Error.ErrorT' monad transformer.+-- 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'+-- monad transformer. consecutively- :: (Monad m, Ae.FromJSON b)- => S.StateT (Producer B.ByteString m r) m (Either DecodingError (Int, b))- -> Producer B.ByteString m r -- ^Producer from which to draw JSON.- -> Producer (Int, b) m- (Either (DecodingError, Producer B.ByteString m r) r)-consecutively parser = \src -> do- (er, src') <- P.runStateP src prod- return $ case er of- Left e -> Left (e, src')- Right r -> Right r- where- prod = do- eof <- lift (skipSpace >> PA.isEndOfParserInput)- if eof- then do- ra <- lift Pp.draw- case ra of- Left r -> return (Right r)- Right _ -> error "Pipes.Aeson.parseMany: impossible!!"- else do- eb <- lift parser- case eb of- Left e -> return (Left e)- Right b -> yield b >> prod+ :: (Monad m)+ => Pipes.Parser B.ByteString m (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)+consecutively parser = step where+ step p0 = do+ (mr, p1) <- lift $+ S.runStateT atEndOfBytes (p0 >-> PB.dropWhile B.isSpaceWord8)+ case mr of+ Just r -> return (Right r)+ Nothing -> do+ (ea, p2) <- lift (S.runStateT parser p1)+ case ea of+ Left e -> return (Left (e, p2))+ Right a -> yield a >> step p2 {-# INLINABLE consecutively #-} ------------------------------------------------------------------------------------ XXX we define the following proxies here until 'pipes-bytestring' is released---- | Consumes and discards leading 'I.ParserInput' characters from upstream as--- long as the given predicate holds 'True'.-skipSpace :: Monad m => S.StateT (Producer B.ByteString m r) m ()-skipSpace = do- ma <- Pp.draw- case ma of- Left _ -> return ()- Right a -> do- let a' = B.dropWhile Char.isSpace a- if B.null a'- then skipSpace- else Pp.unDraw a'-{-# INLINABLE skipSpace #-}+-- Internal tools -------------------------------------------------------------- --- Sends each of the 'BLI.ByteString''s strict chunks downstream.-fromLazy :: Monad m => BLI.ByteString -> Producer' B.ByteString m ()-fromLazy = BLI.foldrChunks (\e a -> yield e >> a) (return ())-{-# INLINABLE fromLazy #-}+-- | Returns @'Just' r@ if the producer has reached end of input, otherwise+-- 'Nothing'.+atEndOfBytes :: Monad m => S.StateT (Producer B.ByteString m r) m (Maybe r)+atEndOfBytes = step =<< S.get where+ step p0 = do+ x <- lift (next p0)+ case x of+ Left r -> S.put (return r) >> return (Just r)+ Right (a,p1)+ | B.null a -> step p1+ | otherwise -> S.put (yield a >> p1) >> return Nothing+{-# INLINABLE atEndOfBytes #-}
+ src/Pipes/Aeson/Unchecked.hs view
@@ -0,0 +1,110 @@+{-# LANGUAGE RankNTypes #-}++-- | This module exports facilities similar to those exported by the+-- "Pipes.Aeson" module, except they do not restrict the 'Ae.Value's+-- that might be encoded or decoded to be just valid top-level values. That is,+-- not only 'Ae.Object's or 'Ae.Array's, according to to the RFC-4627 JSON+-- standard.++module Pipes.Aeson.Unchecked+ ( -- * Encoding+ encode+ -- * Decoding+ , decode+ , decoded+ -- ** Including lenghts+ , decodeL+ , decodedL+ ) where++import Pipes+import qualified Pipes.Parse as Pipes+import qualified Pipes.Aeson.Internal as I+import qualified Pipes.Attoparsec as PA+import qualified Pipes.ByteString as PB+import qualified Data.Aeson as Ae+import qualified Data.Aeson.Parser as Ae (value')+import qualified Data.ByteString as B++--------------------------------------------------------------------------------++-- | 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 = \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))+ #-}++--------------------------------------------------------------------------------++-- | Like 'Pipes.Aeson.decode', except it will decode any 'Ae.FromJSON'+-- instance, not just 'Ae.Array' or 'Ae.Object'.+decode+ :: (Monad m, Ae.FromJSON a)+ => Pipes.Parser B.ByteString m (Either I.DecodingError a) -- ^+decode = do+ x <- decodeL+ return (case x of+ Left e -> Left e+ Right (_, a) -> Right a)+{-# INLINABLE decode #-}+++-- | Like 'decode', except it also returns the length of JSON input that was+-- consumed in order to obtain the value, not including the length of whitespace+-- between each parsed JSON input.+decodeL+ :: (Monad m, Ae.FromJSON a)+ => Pipes.Parser B.ByteString m (Either I.DecodingError (Int, a)) -- ^+decodeL = do+ ev <- PA.parseL Ae.value'+ return (case ev of+ Left e -> Left (I.AttoparsecError e)+ Right (n, v) -> case Ae.fromJSON v of+ Ae.Error e -> Left (I.FromJSONError e)+ Ae.Success a -> Right (n, a))+{-# INLINABLE decodeL #-}++-- | Like 'Pipes.Aeson.decoded', except it will decode and decode any+-- 'Ae.FromJSON' and 'Ae.ToJSON' instance, not just 'Ae.Array' or 'Ae.Object'.+decoded+ :: (Monad m, Ae.FromJSON a, Ae.ToJSON a)+ => Lens' (Producer B.ByteString m r)+ (Producer a m (Either (I.DecodingError, Producer B.ByteString m r) r))+ -- ^+decoded k p = fmap _encode (k (I.consecutively decode p))+ where+ _encode = \p0 -> do+ er <- for p0 (\a -> encode a)+ case er of+ Left (_, p1) -> p1+ Right r -> return r+ {-# INLINE _encode #-}+{-# INLINABLE decoded #-}+++-- | Like 'decoded', except it also tags each decoded entity with the length of+-- JSON input that was consumed in order to obtain the value, not including the+-- length of whitespace between each parsed JSON input.+decodedL+ :: (Monad m, Ae.FromJSON a, Ae.ToJSON a)+ => Lens' (Producer B.ByteString m r)+ (Producer (Int, a) m (Either (I.DecodingError, Producer B.ByteString m r) r))+ -- ^+decodedL k p = fmap _encode (k (I.consecutively decodeL p))+ where+ _encode = \p0 -> do+ er <- for p0 (\(_, a) -> encode a)+ case er of+ Left (_, p1) -> p1+ Right r -> return r+ {-# INLINE _encode #-}+{-# INLINABLE decodedL #-}+++--------------------------------------------------------------------------------+-- Internal tools --------------------------------------------------------------++type Lens' s a = forall f . Functor f => (a -> f a) -> (s -> f s)
− src/Pipes/Aeson/Unsafe.hs
@@ -1,60 +0,0 @@-{-# LANGUAGE RankNTypes #-}---- | This module exports facilities similar to those exported by the--- "Pipes.Aeson" module, except they do not restrict the 'Ae.Value's--- that might be encoded or decoded to be just valid top-level values. That is,--- not only 'Ae.Object's or 'Ae.Array's, according to to the RFC-4627 JSON--- standard.--module Pipes.Aeson.Unsafe- ( -- * Encoding- encode- -- * Decoding- , decode- , decodeMany- ) where--import Pipes-import qualified Pipes.Aeson.Internal as I-import qualified Pipes.Attoparsec as PA-import qualified Control.Monad.Trans.State.Strict as S-import qualified Data.Aeson as Ae-import qualified Data.Aeson.Parser as Ae (value')-import qualified Data.ByteString as B-------------------------------------------------------------------------------------- | 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 = I.fromLazy . Ae.encode-{-# INLINABLE encode #-}-------------------------------------------------------------------------------------- | Like 'Pipes.Aeson.decode', except it will decode any 'Ae.ToJSON' instance,--- not just 'Ae.Array' or 'Ae.Object'.-decode- :: (Monad m, Ae.FromJSON b)- => S.StateT (Producer B.ByteString m r) m (Either I.DecodingError (Int, b))-decode = do- ev <- PA.parse Ae.value'- return $ do- case ev of- Left e -> Left (I.ParserError e)- Right (len, v) -> do- case Ae.fromJSON v of- Ae.Error e -> Left (I.ValueError e)- Ae.Success b -> Right (len, b)-{-# INLINABLE decode #-}---- | Like 'Pipes.Aeson.decodeMany', except it will decode any 'Ae.ToJSON'--- instance, not just 'Ae.Array' or 'Ae.Object'.-decodeMany- :: (Monad m, Ae.FromJSON b)- => Producer B.ByteString m r -- ^Producer from which to draw JSON.- -> Producer (Int, b) m- (Either (I.DecodingError, Producer B.ByteString m r) r)-decodeMany = I.consecutively decode-{-# INLINABLE decodeMany #-}-