pipes-aeson (empty) → 0.1.0.0
raw patch · 9 files changed
+535/−0 lines, 9 filesdep +aesondep +attoparsecdep +basesetup-changed
Dependencies added: aeson, attoparsec, base, bytestring, pipes, pipes-attoparsec, pipes-parse
Files
- LICENSE +30/−0
- NEWS +3/−0
- PEOPLE +5/−0
- README.md +13/−0
- Setup.hs +2/−0
- pipes-aeson.cabal +42/−0
- src/Control/Proxy/Aeson.hs +274/−0
- src/Control/Proxy/Aeson/Internal.hs +63/−0
- src/Control/Proxy/Aeson/Unsafe.hs +103/−0
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright (c) 2013, Renzo Carbonara++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++ * Redistributions of source code must retain the above copyright+ notice, this list of conditions and the following disclaimer.++ * Redistributions in binary form must reproduce the above+ copyright notice, this list of conditions and the following+ disclaimer in the documentation and/or other materials provided+ with the distribution.++ * Neither the name of Renzo Carbonara nor the names of other+ contributors may be used to endorse or promote products derived+ from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ NEWS view
@@ -0,0 +1,3 @@+# 0.1.0.0++* First version.
+ PEOPLE view
@@ -0,0 +1,5 @@+The following people have participated in creating this library, either+by directly contributing code or by providing thoughtful input in+discussions about the library design.++Renzo Carbonara
+ README.md view
@@ -0,0 +1,13 @@+# pipes-aeson++Utilities to encode and decode streams of **JSON** values using **Pipes** and+**Aeson**.++Check the source or rendered Haddocks for documentation.++This code is licensed under the terms of the so called **3-clause BSD+license**. Read the file named ``LICENSE`` found in this same directory+for details.++See the ``PEOPLE`` file to learn about the people involved in this+effort.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ pipes-aeson.cabal view
@@ -0,0 +1,42 @@+name: pipes-aeson+version: 0.1.0.0+license: BSD3+license-file: LICENSE+copyright: Copyright (c) Renzo Carbonara 2013+author: Renzo Carbonara+maintainer: renzocarbonaraλgmail.com+stability: Experimental+tested-with: GHC ==7.4.1+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 NEWS+description:+ Utilities to encode and decode JSON streams using @aeson@ and @pipes@,+ possibly interleaving other stream effects too.+ .+ See the @NEWS@ file in the source distribution to learn about any+ important changes between version.++source-repository head+ type: git+ location: git://github.com/k0001/pipes-aeson.git++library+ hs-source-dirs: src+ exposed-modules: Control.Proxy.Aeson+ Control.Proxy.Aeson.Unsafe+ other-modules: Control.Proxy.Aeson.Internal+ build-depends:+ aeson (>=0.6.1 && < 0.7)+ , attoparsec+ , base (>=4.5 && < 5.0)+ , pipes (>=3.3 && <3.4)+ , pipes-attoparsec (>=0.2 && <0.3)+ , pipes-parse (>=1.0 && <1.1)+ , bytestring (>=0.9.2.1)+ ghc-options: -Wall -O2+ ghc-prof-options: -fprof-auto
+ src/Control/Proxy/Aeson.hs view
@@ -0,0 +1,274 @@+-- | This module allows you to encode and decode JSON values flowing downstream+-- through Pipes streams, possibly interleaving other stream effects.+--+-- This module builds on top of the @pipes-parse@ and @pipes-attoparsec@+-- packages and assumes you have read "Control.Proxy.Parse.Tutorial".++module Control.Proxy.Aeson+ ( -- * Top level JSON values+ -- $top-level-value+ TopLevelValue(..)+ , toTopLevelValue+ -- * Encoding+ -- $encoding+ , encode+ , encodeD+ -- * Decoding+ -- $decoding+ , decode+ , decodeD+ -- ** Lower level parsing+ , parseValue+ , parseValueD+ , fromValue+ , fromValueD+ -- * Types+ , I.DecodingError(..)+ ) where+++import Control.Monad (unless)+import qualified Control.Proxy as P+import qualified Control.Proxy.Aeson.Internal as I+import qualified Control.Proxy.Aeson.Unsafe as U+import qualified Control.Proxy.Attoparsec as PA+import qualified Control.Proxy.Trans.Either as P+import qualified Control.Proxy.Trans.State as P+import qualified Data.Aeson as Ae+import qualified Data.ByteString.Char8 as B+import Data.Maybe (fromJust)++--------------------------------------------------------------------------------+-- $top-level-value+--+-- The JSON RFC-4627 standard only allows 'Ae.Array' or 'Ae.Object' values as+-- top-level. The 'TopLevelValue' type used throughout this module in+-- replacement of Aesons's 'Ae.Value' enforces that restricion in a type-safe+-- manner.+--+-- If you want to ignore the standard and encode or decode any 'Ae.Value', then+-- use the facilities exported by the "Control.Proxy.Aeson.Unsafe" module.+--+-- * You may use the 'toTopLevelValue' function to convert any 'Ae.ToJSON'+-- instance to a 'TopLevelValue', if possible. Remember that 'Ae.Value' is+-- one such instance.+--+-- * Use the 'Ae.toJSON' method on a 'TopLevelValue' to obtain its underlying+-- 'Ae.Value' representation.+++-- | A JSON top-level value must be an 'Ae.Object' or an 'Ae.Array', according+-- to RFC-4627.+data TopLevelValue+ = Object !Ae.Object+ | Array !Ae.Array+ deriving (Show, Eq)++instance Ae.ToJSON TopLevelValue where+ toJSON (Object o) = Ae.Object o+ toJSON (Array a) = Ae.Array a++instance Ae.FromJSON TopLevelValue where+ parseJSON (Ae.Object o) = return (Object o)+ parseJSON (Ae.Array a) = return (Array a)+ parseJSON _ = fail "Not a valid top-level value"++-- | Converts the given 'Ae.ToJSON' instance to a 'TopLevelValue' as long as its+-- 'Ae.Value' representation is one of 'Ae.Object' or 'Ae.Array', otherwise+-- 'Nothing'. Remember that 'Ae.Value' itself is a 'Ae.ToJSON' instance.+toTopLevelValue :: Ae.ToJSON a => a -> Maybe TopLevelValue+toTopLevelValue = \a ->+ case Ae.toJSON a of+ Ae.Object x -> Just (Object x)+ Ae.Array x -> Just (Array x)+ _ -> Nothing+{-# INLINABLE toTopLevelValue #-}++--------------------------------------------------------------------------------+-- $encoding+--+-- There are two different JSON encoding facilities exported by this module, and+-- choosing between them is easy: If you need to interleave JSON encoding+-- with other stream effects you must use 'encode', otherwise you may use the+-- simpler 'encodeD'.+--+-- Both encoding proxies enforce the JSON RFC-4627 requirement that top-level+-- values are either 'Ae.Array's or 'Ae.Object's, as witnessed by the+-- 'TopLevelValue' type. However, if you need to ignore this requirement you may+-- use the similar encoding proxies exported by the "Control.Proxy.Aeson.Unsafe"+-- module.++-- | Encodes the given 'TopLevelValue' as JSON and sends it downstream, possibly+-- in more than one 'BS.ByteString' chunk.+encode :: (P.Proxy p, Monad m) => TopLevelValue -> p x' x () B.ByteString m ()+encode = U.encode+{-# INLINABLE encode #-}++-- | Encodes 'TopLevelValue's flowing downstream as JSON, each in possibly more+-- than one 'BS.ByteString' chunk, and sends each chunk downstream.+encodeD :: (P.Proxy p, Monad m) => () -> P.Pipe p TopLevelValue B.ByteString m r+encodeD = U.encodeD+{-# INLINABLE encodeD #-}++--------------------------------------------------------------------------------+-- $decoding+--+-- Decoding a JSON value as a Haskell type in involves two different steps:+--+-- * Parsing a raw JSON 'B.ByteString' into a 'TopLevelValue'.+--+-- * Converting the obtained 'TopLevelValue' to the desired type, which must be+-- a 'Ae.FromJSON' instance.+--+-- Any of those steps can fail, and in case of errors, the 'I.DecodingError'+-- type explicitly states at which the step the error happened.+--+-- There are two different JSON decoding facilities exported by this module,+-- both perform those steps at once. Choosing between them is easy: If you+-- need to interleave JSON decoding with other stream effects you must use+-- 'decode', otherwise you may use the simpler 'decodeD'.+--+-- These proxies use the 'P.EitherP' proxy transformer to report decoding+-- errors, you might use any of the facilities exported by+-- "Control.Proxy.Trans.Either" to recover from them.+--+-- If you prefer to perform each of the decoding steps separately, you+-- could use instead the 'parseValue', 'parseValueD', 'fromValue' or+-- 'fromValueD' proxies.+++-- | Decodes one JSON value flowing downstream.+--+-- * In case of decoding errors, a 'I.DecodingError' exception is thrown in+-- the 'Pe.EitherP' proxy transformer.+--+-- * Requests more input from upstream using 'Pa.draw' when needed.+--+-- * /Do not/ use this proxy if your stream has leading empty chunks or+-- whitespace, otherwise you may get unexpected parsing errors.+--+-- Here is an example parsing loop that allows interleaving stream effects+-- together with 'decode':+--+-- @+-- loop = do+-- -- Skip any leading whitespace and check that we haven't reached EOF.+-- eof <- 'P.liftP' $ 'Control.Proxy.ByteString.dropWhile' 'Data.Char.isSpace' >> 'PA.isEndOfParserInput'+-- 'unless' eof $ do+-- -- 1. Possibly perform some stream effects here.+-- -- 2. Decode one JSON element from the stream.+-- exampleElement <- 'decode'+-- -- 3. Do something with exampleElement and possibly perform+-- -- some more stream effects.+-- -- 4. Start all over again.+-- loop+-- @+decode+ :: (Monad m, P.Proxy p, Ae.FromJSON r)+ => P.EitherP I.DecodingError (P.StateP [B.ByteString] p)+ () (Maybe B.ByteString) y' y m r+decode = do+ ev <- P.liftP . P.runEitherP $ PA.parse Ae.json'+ case ev of+ Left e -> P.throw (I.ParserError e)+ Right v ->+ case Ae.fromJSON v of+ Ae.Error e -> P.throw (I.ValueError e)+ Ae.Success r -> return r+{-# INLINABLE decode #-}+++-- | Decodes consecutive JSON values flowing downstream until end of input.+--+-- * In case of decoding errors, a 'I.DecodingError' exception is thrown in+-- the 'Pe.EitherP' proxy transformer.+--+-- * Requests more input from upstream using 'Pa.draw' when needed.+--+-- * Empty input chunks flowing downstream and whitespace in between JSON+-- values will be discarded.+decodeD+ :: (Monad m, P.Proxy p, Ae.FromJSON b)+ => ()+ -> P.Pipe (P.EitherP I.DecodingError (P.StateP [B.ByteString] p))+ (Maybe B.ByteString) b m ()+decodeD = \() -> loop where+ loop = do+ eof <- P.liftP $ I.skipSpace >> PA.isEndOfParserInput+ unless eof $ decode >>= P.respond >> loop+{-# INLINABLE decodeD #-}++--------------------------------------------------------------------------------++-- | Parses a JSON value flowing downstream into a 'TopLevelValue'.+--+-- * In case of parsing errors, a 'PA.ParsingError' exception is thrown in+-- the 'Pe.EitherP' proxy transformer.+--+-- * Requests more input from upstream using 'Pa.draw' when needed.+--+-- * /Do not/ use this proxy if your stream has leading empty chunks or+-- whitespace, otherwise you may get unexpected parsing errors.+--+-- See the documentation of 'decode' for an example of how to interleave+-- other stream effects together with this proxy.+parseValue+ :: (Monad m, P.Proxy p)+ => P.EitherP PA.ParsingError (P.StateP [B.ByteString] p)+ () (Maybe B.ByteString) y' y m TopLevelValue+parseValue = return . fromJust . toTopLevelValue =<< PA.parse Ae.json'+{-# INLINABLE parseValue #-}+++-- | Parses consecutive JSON values flowing downstream as 'TopLevelValue's,+-- until end of input.+--+-- * In case of parsing errors, a 'I.DecodingError' exception is thrown in+-- the 'Pe.EitherP' proxy transformer.+--+-- * Requests more input from upstream using 'Pa.draw' when needed.+--+-- * Empty input chunks flowing downstream and whitespace in between JSON+-- values will be discarded.+parseValueD+ :: (Monad m, P.Proxy p)+ => ()+ -> P.Pipe (P.EitherP PA.ParsingError (P.StateP [B.ByteString] p))+ (Maybe B.ByteString) TopLevelValue m ()+parseValueD = \() -> loop where+ loop = do+ eof <- P.liftP $ I.skipSpace >> PA.isEndOfParserInput+ unless eof $ parseValue >>= P.respond >> loop+{-# INLINABLE parseValueD #-}++--------------------------------------------------------------------------------++-- | Converts any 'Ae.Value' flowing downstream to a 'Ae.FromJSON' instance.+--+-- * In case of parsing errors, a 'String' exception holding the value provided+-- by Aeson's 'Ae.Error' is thrown in the 'Pe.EitherP' proxy transformer.+--+-- See the documentation of 'decode' for an example of how to interleave+-- other stream effects together with this proxy.+fromValue+ :: (Monad m, P.Proxy p, Ae.FromJSON r)+ => x -> P.EitherP String p x Ae.Value y' y m r+fromValue = \x -> do+ v <- P.request x+ case Ae.fromJSON v of+ Ae.Error e -> P.throw e+ Ae.Success r -> return r+{-# INLINABLE fromValue #-}+++-- | Converts any 'Ae.Value's flowing downstream to 'Ae.FromJSON' instances and+-- forwards them downstream.+--+-- * In case of parsing errors, a 'String' exception holding the value provided+-- by Aeson's 'Ae.Error' is thrown in the 'Pe.EitherP' proxy transformer.+fromValueD+ :: (Monad m, P.Proxy p, Ae.FromJSON b)+ => x -> P.EitherP String p x Ae.Value x b m r+fromValueD = fromValue P.\>\ P.pull+{-# INLINABLE fromValueD #-}+
+ src/Control/Proxy/Aeson/Internal.hs view
@@ -0,0 +1,63 @@+{-# LANGUAGE DeriveDataTypeable #-}++-- | This module provides internal utilities and it is likely+-- to be modified in backwards-incompatible ways in the future.+--+-- Use the stable API exported by the "Control.Proxy.Aeson" module instead.+module Control.Proxy.Aeson.Internal+ ( DecodingError(..)+ , skipSpace+ , fromLazy+ ) where++import qualified Control.Proxy.Attoparsec as PA+import qualified Control.Proxy.Trans.State as P+import Control.Exception (Exception)+import qualified Control.Proxy as P+import qualified Control.Proxy.Parse as Pp+import qualified Data.ByteString.Char8 as B+import qualified Data.ByteString.Lazy.Internal as BLI+import qualified Data.Char as Char+import Data.Data (Data, Typeable)+import Data.Function (fix)++--------------------------------------------------------------------------------++-- | 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+ -- 'Data.Aeson.Error'.+ deriving (Show, Eq, Data, Typeable)++instance Exception DecodingError++--------------------------------------------------------------------------------+-- 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, P.Proxy p)+ => P.StateP [B.ByteString] p () (Maybe B.ByteString) y' y m ()+skipSpace = fix $ \loop -> do+ ma <- Pp.draw+ case ma of+ Nothing -> return ()+ Just a -> do+ let a' = B.dropWhile Char.isSpace a+ if B.null a'+ then loop+ else Pp.unDraw a'+{-# INLINABLE skipSpace #-}++-- Sends each of the 'BLI.ByteString''s strict chunks downstream.+fromLazy+ :: (Monad m, P.Proxy p)+ => BLI.ByteString -> p x' x y' B.ByteString m ()+fromLazy =+ P.runIdentityP . BLI.foldrChunks (\e a -> P.respond e >> a) (return ())+{-# INLINABLE fromLazy #-}
+ src/Control/Proxy/Aeson/Unsafe.hs view
@@ -0,0 +1,103 @@+-- | This module exports facilities similar to those exported by the+-- "Control.Proxy.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 Control.Proxy.Aeson.Unsafe+ (-- * Encoding+ -- $encoding+ encode+ , encodeD+ -- * Decoding+ -- $decoding+ , decode+ , decodeD+ -- ** Lower level parsing+ , parseValue+ , parseValueD+ -- * Types+ , I.DecodingError(..)+ ) where++import Control.Monad (unless)+import qualified Control.Proxy as P+import qualified Control.Proxy.Aeson.Internal as I+import qualified Control.Proxy.Attoparsec as PA+import qualified Control.Proxy.Trans.Either as P+import qualified Control.Proxy.Trans.State as P+import qualified Data.Aeson as Ae+import qualified Data.Aeson.Parser as Ae (value')+import qualified Data.ByteString.Char8 as B++--------------------------------------------------------------------------------++-- | Like 'Control.Proxy.Aeson.encode', except it accepts any 'Ae.ToJSON'+-- instance.+encode+ :: (P.Proxy p, Monad m, Ae.ToJSON a) => a -> p x' x () B.ByteString m ()+encode = I.fromLazy . Ae.encode+{-# INLINABLE encode #-}++-- | Like 'Control.Proxy.Aeson.encodeD', except it accepts any 'Ae.ToJSON'+-- instance.+encodeD+ :: (P.Proxy p, Monad m, Ae.ToJSON a) => () -> P.Pipe p a B.ByteString m r+encodeD = P.pull P./>/ encode+{-# INLINABLE encodeD #-}++--------------------------------------------------------------------------------++-- | Like 'Control.Proxy.Aeson.decode', except it will decode any 'Ae.ToJSON'+-- instance.+decode+ :: (Monad m, P.Proxy p, Ae.FromJSON r)+ => P.EitherP I.DecodingError (P.StateP [B.ByteString] p)+ () (Maybe B.ByteString) y' y m r+decode = do+ ev <- P.liftP . P.runEitherP $ PA.parse Ae.value'+ case ev of+ Left e -> P.throw (I.ParserError e)+ Right v ->+ case Ae.fromJSON v of+ Ae.Error e -> P.throw (I.ValueError e)+ Ae.Success r -> return r+{-# INLINABLE decode #-}++-- | Like 'Control.Proxy.Aeson.decodeD', except it will decode any 'Ae.ToJSON'+-- instance.+decodeD+ :: (Monad m, P.Proxy p, Ae.FromJSON b)+ => ()+ -> P.Pipe (P.EitherP I.DecodingError (P.StateP [B.ByteString] p))+ (Maybe B.ByteString) b m ()+decodeD = \() -> loop where+ loop = do+ eof <- P.liftP $ I.skipSpace >> PA.isEndOfParserInput+ unless eof $ decode >>= P.respond >> loop+{-# INLINABLE decodeD #-}++--------------------------------------------------------------------------------++-- | Like 'Control.Proxy.Aeson.parseValue', except it will parse into any+-- 'Ae.Value'.+parseValue+ :: (Monad m, P.Proxy p)+ => P.EitherP PA.ParsingError (P.StateP [B.ByteString] p)+ () (Maybe B.ByteString) y' y m Ae.Value+parseValue = PA.parse Ae.value'+{-# INLINABLE parseValue #-}++-- | Like 'Control.Proxy.Aeson.parseValueD', except it will parse into any+-- 'Ae.Value'.+parseValueD+ :: (Monad m, P.Proxy p)+ => ()+ -> P.Pipe (P.EitherP PA.ParsingError (P.StateP [B.ByteString] p))+ (Maybe B.ByteString) Ae.Value m ()+parseValueD = \() -> loop where+ loop = do+ eof <- P.liftP $ I.skipSpace >> PA.isEndOfParserInput+ unless eof $ parseValue >>= P.respond >> loop+{-# INLINABLE parseValueD #-}+