pipes-attoparsec 0.0.2 → 0.1.0.1
raw patch · 11 files changed
+785/−68 lines, 11 filesdep +HUnitdep +QuickCheckdep +bytestringdep −pipes-coredep ~attoparsecnew-uploaderPVP ok
version bump matches the API change (PVP)
Dependencies added: HUnit, QuickCheck, bytestring, pipes, pipes-attoparsec, test-framework, test-framework-hunit, test-framework-quickcheck2, text
Dependencies removed: pipes-core
Dependency ranges changed: attoparsec
API changes (from Hackage documentation)
- Control.Pipe.Attoparsec: DivergentParser :: ParseError
- Control.Pipe.Attoparsec: ParseError :: [String] -> String -> ParseError
- Control.Pipe.Attoparsec: data ParseError
- Control.Pipe.Attoparsec: errorContexts :: ParseError -> [String]
- Control.Pipe.Attoparsec: errorMessage :: ParseError -> String
- Control.Pipe.Attoparsec: instance Exception ParseError
- Control.Pipe.Attoparsec: instance Show ParseError
- Control.Pipe.Attoparsec: instance Typeable ParseError
- Control.Pipe.Attoparsec: pipeParser :: (Monoid a, Monad m) => (a -> IResult a r) -> Pipe a x m (a, Either ParseError r)
+ Control.Proxy.Attoparsec: parserD :: (Proxy p, Monad m, AttoparsecInput a) => Parser a b -> () -> p (ParserStatus a) (ParserSupply a) () b m r
+ Control.Proxy.Attoparsec: parserInputD :: (Monad m, Proxy p) => ParserStatus a -> p () a (ParserStatus a) (ParserSupply a) m r
+ Control.Proxy.Attoparsec.Control: limitInputLength :: (Monad m, Proxy p, AttoparsecInput a) => Int -> ParserStatus a -> EitherP BadInput p (ParserStatus a) (ParserSupply a) (ParserStatus a) (ParserSupply a) m r
+ Control.Proxy.Attoparsec.Control: retryLeftovers :: (Monad m, Proxy p, AttoparsecInput a) => ParserStatus a -> p (ParserStatus a) (ParserSupply a) (ParserStatus a) (ParserSupply a) m r
+ Control.Proxy.Attoparsec.Control: skipMalformedChunks :: (Monad m, Proxy p, AttoparsecInput a) => ParserStatus a -> p (ParserStatus a) (ParserSupply a) (ParserStatus a) (ParserSupply a) m r
+ Control.Proxy.Attoparsec.Control: throwParsingErrors :: (Monad m, Proxy p, AttoparsecInput a) => ParserStatus a -> EitherP BadInput p (ParserStatus a) (ParserSupply a) (ParserStatus a) (ParserSupply a) m r
+ Control.Proxy.Attoparsec.Tutorial: Name :: Text -> Name
+ Control.Proxy.Attoparsec.Tutorial: data Name
+ Control.Proxy.Attoparsec.Tutorial: hello :: Parser Name
+ Control.Proxy.Attoparsec.Tutorial: helloPipe1 :: (Proxy p, Monad m) => () -> Pipe p Text Name m r
+ Control.Proxy.Attoparsec.Tutorial: helloPipe2 :: (Proxy p, Monad m) => () -> Pipe p Text Name m r
+ Control.Proxy.Attoparsec.Tutorial: helloPipe3 :: (Proxy p, Monad m) => () -> Pipe (EitherP BadInput p) Text Name m r
+ Control.Proxy.Attoparsec.Tutorial: helloPipe4 :: (Proxy p, Monad m) => () -> Pipe (EitherP BadInput p) Text Name m r
+ Control.Proxy.Attoparsec.Tutorial: helloPipe5 :: (Proxy p, Monad m) => () -> Pipe (EitherP BadInput p) Text Name m r
+ Control.Proxy.Attoparsec.Tutorial: helloPipe6 :: (Proxy p, Monad m) => () -> Pipe p Text Name m r
+ Control.Proxy.Attoparsec.Tutorial: input1 :: [Text]
+ Control.Proxy.Attoparsec.Tutorial: input2 :: [Text]
+ Control.Proxy.Attoparsec.Tutorial: instance Eq Name
+ Control.Proxy.Attoparsec.Tutorial: instance Show Name
+ Control.Proxy.Attoparsec.Tutorial: skipPartialResults :: (Monad m, Proxy p, AttoparsecInput a) => ParserStatus a -> p (ParserStatus a) (ParserSupply a) (ParserStatus a) (ParserSupply a) m r
+ Control.Proxy.Attoparsec.Types: Failed :: a -> ParserError -> ParserStatus a
+ Control.Proxy.Attoparsec.Types: InputTooLong :: Int -> BadInput
+ Control.Proxy.Attoparsec.Types: MalformedInput :: ParserError -> BadInput
+ Control.Proxy.Attoparsec.Types: ParserError :: [String] -> String -> ParserError
+ Control.Proxy.Attoparsec.Types: Parsing :: Int -> ParserStatus a
+ Control.Proxy.Attoparsec.Types: Resume :: SupplyUse
+ Control.Proxy.Attoparsec.Types: Start :: SupplyUse
+ Control.Proxy.Attoparsec.Types: class Eq a => AttoparsecInput a
+ Control.Proxy.Attoparsec.Types: data BadInput
+ Control.Proxy.Attoparsec.Types: data ParserError
+ Control.Proxy.Attoparsec.Types: data ParserStatus a
+ Control.Proxy.Attoparsec.Types: data SupplyUse
+ Control.Proxy.Attoparsec.Types: drop :: AttoparsecInput a => Int -> a -> a
+ Control.Proxy.Attoparsec.Types: errorContexts :: ParserError -> [String]
+ Control.Proxy.Attoparsec.Types: errorMessage :: ParserError -> String
+ Control.Proxy.Attoparsec.Types: instance AttoparsecInput ByteString
+ Control.Proxy.Attoparsec.Types: instance AttoparsecInput Text
+ Control.Proxy.Attoparsec.Types: instance Eq BadInput
+ Control.Proxy.Attoparsec.Types: instance Eq ParserError
+ Control.Proxy.Attoparsec.Types: instance Eq SupplyUse
+ Control.Proxy.Attoparsec.Types: instance Eq a => Eq (ParserStatus a)
+ Control.Proxy.Attoparsec.Types: instance Show BadInput
+ Control.Proxy.Attoparsec.Types: instance Show ParserError
+ Control.Proxy.Attoparsec.Types: instance Show SupplyUse
+ Control.Proxy.Attoparsec.Types: instance Show a => Show (ParserStatus a)
+ Control.Proxy.Attoparsec.Types: itlLength :: BadInput -> Int
+ Control.Proxy.Attoparsec.Types: length :: AttoparsecInput a => a -> Int
+ Control.Proxy.Attoparsec.Types: miParserErrror :: BadInput -> ParserError
+ Control.Proxy.Attoparsec.Types: null :: AttoparsecInput a => a -> Bool
+ Control.Proxy.Attoparsec.Types: parse :: AttoparsecInput a => Parser a b -> a -> IResult a b
+ Control.Proxy.Attoparsec.Types: psError :: ParserStatus a -> ParserError
+ Control.Proxy.Attoparsec.Types: psLeftover :: ParserStatus a -> a
+ Control.Proxy.Attoparsec.Types: psLength :: ParserStatus a -> Int
+ Control.Proxy.Attoparsec.Types: type ParserSupply a = (SupplyUse, a)
Files
- Control/Pipe/Attoparsec.hs +0/−45
- LICENSE +3/−2
- README.md +5/−0
- pipes-attoparsec.cabal +64/−21
- src/Control/Proxy/Attoparsec.hs +94/−0
- src/Control/Proxy/Attoparsec/Control.hs +80/−0
- src/Control/Proxy/Attoparsec/Tutorial.lhs +344/−0
- src/Control/Proxy/Attoparsec/Types.hs +101/−0
- tests/Main.hs +21/−0
- tests/Test/Attoparsec.hs +7/−0
- tests/Test/Tutorial.hs +66/−0
− Control/Pipe/Attoparsec.hs
@@ -1,45 +0,0 @@-{-# LANGUAGE DeriveDataTypeable #-}-module Control.Pipe.Attoparsec (- ParseError(..),- pipeParser,- ) where--import qualified Control.Exception as E-import Control.Pipe-import Control.Pipe.Combinators-import Control.Pipe.Exception-import Data.Attoparsec.Types-import Data.Maybe-import Data.Monoid-import Data.Typeable---- | A parse error as returned by Attoparsec.-data ParseError- = ParseError {- errorContexts :: [String], -- ^ Contexts where the error occurred.- errorMessage :: String -- ^ Error message.- }- | DivergentParser -- ^ Returned if a parser does not terminate- -- when its input is exhausted.- deriving (Show, Typeable)--instance E.Exception ParseError---- | Convert a parser continuation into a Pipe.------ To get a parser continuation from a 'Parser', use the @parse@ function of the--- appropriate Attoparsec module.-pipeParser :: (Monoid a, Monad m) => (a -> IResult a r) -> Pipe a x m (a, Either ParseError r)-pipeParser p = go p- where- go p = do- chunk <- tryAwait- case p (maybe mempty id chunk) of- Fail leftover contexts msg ->- return (leftover, Left $ ParseError contexts msg)- Partial p' ->- if isNothing chunk- then return (mempty, Left DivergentParser)- else go p'- Done leftover result ->- return (leftover, Right result)
LICENSE view
@@ -1,4 +1,5 @@-Copyright (c)2012, Paolo Capriotti+Copyright (c) 2012-2012, Paolo Capriotti+Copyright (c) 2012-, Renzo Carbonara All rights reserved. @@ -13,7 +14,7 @@ disclaimer in the documentation and/or other materials provided with the distribution. - * Neither the name of Paolo Capriotti nor the names of other+ * 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.
+ README.md view
@@ -0,0 +1,5 @@+# pipes-attoparsec++Utilities to convert an Attoparsec Parser into a Pipe.++Version 0.1.\* is not backwards compatible with previous versions.
pipes-attoparsec.cabal view
@@ -1,24 +1,67 @@-Name: pipes-attoparsec-Version: 0.0.2-License: BSD3-License-file: LICENSE-Author: Paolo Capriotti-Maintainer: p.capriotti@gmail.com-Stability: Experimental-Homepage: https://github.com/pcapriotti/pipes-extra-Category: Text-Build-type: Simple-Synopsis: Utilities to convert a parser into a pipe.-Description: Utilities to convert a parser into a pipe.-Cabal-version: >=1.8+name: pipes-attoparsec+version: 0.1.0.1+license: BSD3+license-file: LICENSE+copyright: Copyright (c) Paolo Capriotti 2012-2012,+ Renzo Carbonara 2012-+author: Paolo Capriotti, Renzo Carbonara+maintainer: renzocarbonaraλgmail.com+stability: Experimental+tested-with: GHC ==7.4.1, GHC ==7.6.1+homepage: https://github.com/k0001/pipes-attoparsec+bug-reports: https://github.com/k0001/pipes-attoparsec+category: Pipes, Parser+build-type: Simple+cabal-version: >=1.8+synopsis: Utilities to convert an Attoparsec parser into a pipe Pipe.+description:+ Utilities to convert an Attoparsec 'Data.Attoparsec.Types.Parser' into+ a 'Control.Proxy.Synonym.Pipe'.+ .+ See "Control.Proxy.Attoparsec.Tutorial" for an extensive introduction with+ examples.+ .+ Version 0.1.* is not backwards compatible with previous versions.+extra-source-files:+ README.md -Source-Repository head- Type: git- Location: https://github.com/pcapriotti/pipes-extra+source-repository head+ type: git+ location: git://github.com/k0001/pipes-attoparsec.git -Library- Exposed-modules: Control.Pipe.Attoparsec- Build-depends:+library+ hs-source-dirs: src+ exposed-modules: Control.Proxy.Attoparsec+ Control.Proxy.Attoparsec.Control+ Control.Proxy.Attoparsec.Tutorial+ Control.Proxy.Attoparsec.Types+ build-depends:+ base (==4.*),+ pipes (>=3.0),+ attoparsec (>=0.10),+ bytestring,+ text++ ghc-options:+ -Wall -fno-warn-missing-signatures++test-suite tests+ type: exitcode-stdio-1.0+ hs-source-dirs: tests+ main-is: Main.hs+ other-modules: Test.Attoparsec+ Test.Tutorial+ ghc-options:+ -Wall -rtsopts -fno-warn-missing-signatures++ build-depends: base (== 4.*),- pipes-core (== 0.1.*),- attoparsec (== 0.10.*)+ pipes (>= 3.0),+ attoparsec (>= 0.10),+ text,+ pipes-attoparsec,+ QuickCheck (== 2.*),+ HUnit (== 1.*),+ test-framework (>= 0.6),+ test-framework-quickcheck2 (>= 0.2),+ test-framework-hunit (>= 0.2)
+ src/Control/Proxy/Attoparsec.hs view
@@ -0,0 +1,94 @@+-- | This module provides utilities to turn your Attoparsec @'Parser' a+-- b@ into a 'Pipe' that parses @a@ values into @b@ values as they flow+-- downstream.+--+-- See "Control.Proxy.Attoparsec.Tutorial" for an extensive introduction+-- with examples.++module Control.Proxy.Attoparsec+ ( -- * Parsing Pipe+ -- $pieces+ parserInputD+ , parserD+ -- * Modules+ , module Control.Proxy.Attoparsec.Types+ , module Control.Proxy.Attoparsec.Control+ ) where++import Control.Proxy+import Control.Proxy.Attoparsec.Control+import Control.Proxy.Attoparsec.Types+import Data.Attoparsec.Types (IResult (..), Parser)+import Prelude hiding (length, null)+++-- $pieces+--+-- A 'Pipe' that parses @a@ input flowing downstream into @b@ values is+-- made of at least two smaller cooperating 'Proxy's:+--+-- 1. 'parserInputD': Prepares @a@ input received from upstream to be+-- consumed by a downstream parsing 'Proxy'.+--+-- 2. 'parserD': Repeatedly runs a given @'Parser' a b@ on input 'a'+-- from upstream, and sends 'b' values downstream.+--+-- Given a @'Parser' a b@ named @myParser@, the simplest way to use+-- these 'Proxy's together is:+--+-- > myParsingPipe :: (Proxy p, Monad m, AttoparsecInput a) => Pipe a b m r+-- > myParsingPipe = parserInputD >-> parserD myParser+--+-- In between these two 'Proxy's, you can place other 'Proxy's to handle+-- extraordinary situations like parsing failures or limiting input+-- length. "Control.Proxy.Attoparsec.Control" exports some useful+-- 'Proxy's that fit this place. If you skip using any of those, the+-- default behavior is that of+-- 'Control.Proxy.Attoparsec.Control.skipMalformedChunks': to simply+-- ignore all parsing errors and malformed input, and start parsing new+-- input as soon as it's available.++++-- | 'Proxy' turning 'a' values flowing downstream into+-- @'ParserSupply' a@ values to be consumed by a downstream parsing+-- 'Proxy'.+--+-- This 'Proxy' responds with @'Resume' a@ to any 'ParserStatus' value+-- received from downstream.+parserInputD :: (Monad m, Proxy p)+ => ParserStatus a -> p () a (ParserStatus a) (ParserSupply a) m r+parserInputD _ = runIdentityP . forever $ request () >>= respond . (,) Resume+++-- | 'Proxy' using the given @'Parser' a b@ to repeatedly parse pieces+-- of @'ParserSupply' a@ values flowing downstream into @b@ values.+--+-- When more input is needed, a @'ParserStatus' a@ value reporting the+-- current parsing status is sent upstream, and in exchange a+-- @'ParserSupply' a@ value is expected, containing more input to be+-- parsed and directives on how to use it (see 'ParserSupply'+-- documentation).+parserD :: (Proxy p, Monad m, AttoparsecInput a)+ => Parser a b+ -> () -> p (ParserStatus a) (ParserSupply a) () b m r+parserD parser () = runIdentityP . forever $ ask k0 0 (Parsing 0)+ where+ k0 = parse parser++ requestNonEmpty status = go where+ go = do+ ps@(_, chunk) <- request status+ if null chunk then go else return ps++ ask k len status = do+ (su, chunk) <- requestNonEmpty status+ case su of+ Start -> use (k0 chunk) 0+ Resume -> use (k chunk) (len + length chunk)++ use (Partial k) len = ask k len $ Parsing len+ use (Fail rest ctx msg) _ = ask k0 0 $ Failed rest (ParserError ctx msg)+ use (Done rest result) _ = respond result >> use (k0 rest) 0++
+ src/Control/Proxy/Attoparsec/Control.hs view
@@ -0,0 +1,80 @@+-- | This module exports some useful 'Proxy's that act upon+-- 'ParserStatus' values received from downstream.++module Control.Proxy.Attoparsec.Control+ ( skipMalformedChunks+ , retryLeftovers+ , throwParsingErrors+ , limitInputLength+ ) where++import Control.Proxy+import Control.Proxy.Attoparsec.Types+import qualified Control.Proxy.Trans.Either as PE+import Prelude hiding (drop, null)++-- | If a downstream parsing 'Proxy' reports a parser failure, skip the+-- whole input /chunk/ being processed, including any left-overs, and+-- start processing new input as soon as it's available.+--+-- Useful when the input found in a single @'ParserSupply' a@ is+-- supposed to be well-formed, so it can be safely skipped if it is not.+skipMalformedChunks+ :: (Monad m, Proxy p, AttoparsecInput a)+ => ParserStatus a+ -> p (ParserStatus a) (ParserSupply a) (ParserStatus a) (ParserSupply a) m r+skipMalformedChunks = runIdentityK . foreverK $ go+ where go x@(Failed _ _) = request x >>= \(_,a) -> respond (Start, a)+ go x = request x >>= respond+++-- | If a downstream parsing 'Proxy' reports a parsing failure, then+-- keep retrying with any left-over input, skipping individual bits each+-- time. If there are no left-overs, then more input is requestsd form+-- upstream.+retryLeftovers+ :: (Monad m, Proxy p, AttoparsecInput a)+ => ParserStatus a+ -> p (ParserStatus a) (ParserSupply a) (ParserStatus a) (ParserSupply a) m r+retryLeftovers = runIdentityK . foreverK $ go+ where+ go s@(Failed _ _) = retry s >>= moreSupply >>= respond+ go s = request s >>= respond++ retry s@(Failed rest _) = do+ s' <- respond (Start, rest)+ if s' == s then return s+ else retry s'+ retry s = return s++ moreSupply s@(Failed rest _) = do+ let rest' = drop 1 rest+ if null rest'+ then request s+ else return (Start, rest')+ moreSupply s = request s+++-- | If a downstream parsing 'Proxy' reports a parser failure, then+-- throw a 'MalformedInput' error in the 'EitherP' proxy transformer.+throwParsingErrors+ :: (Monad m, Proxy p, AttoparsecInput a)+ => ParserStatus a+ -> PE.EitherP BadInput p (ParserStatus a) (ParserSupply a) (ParserStatus a) (ParserSupply a) m r+throwParsingErrors = foreverK go+ where go (Failed _ e) = PE.throw $ MalformedInput e+ go x = request x >>= respond+++-- | If a downstream parsing 'Proxy' doesn't produce a value after+-- having consumed input of at least the given length, then throw an+-- 'InputTooLong' error in the 'EitherP' proxy transformer.+limitInputLength+ :: (Monad m, Proxy p, AttoparsecInput a)+ => Int+ -> ParserStatus a+ -> PE.EitherP BadInput p (ParserStatus a) (ParserSupply a) (ParserStatus a) (ParserSupply a) m r+limitInputLength n = foreverK go+ where go (Parsing m) | m >= n = PE.throw $ InputTooLong m+ go x = request x >>= respond+
+ src/Control/Proxy/Attoparsec/Tutorial.lhs view
@@ -0,0 +1,344 @@+> {-# LANGUAGE OverloadedStrings #-}++| In this tutorial you will learn how to use this library. The /Simple example/+section should be enough to get you going, but you can keep reading if you want+to better understand how to deal with complex parsing scenarios.++You may import this module and try the subsequent examples as you go.+++> module Control.Proxy.Attoparsec.Tutorial+> (-- * Simple example+> -- $example-simple+>+> -- * Parsing control proxies+> -- ** Handling parser errors+> -- $example-errors+>+> -- ** Composing+> -- $example-compose-control+>+> -- ** Custom behavior+> -- $example-control-custom+>+> -- * Try for yourself+> -- $example-try+> Name(..)+> , hello+> , input1+> , input2+> , helloPipe1+> , helloPipe2+> , helloPipe3+> , helloPipe4+> , helloPipe5+> , helloPipe6+> , skipPartialResults+> ) where+>+> import Control.Proxy+> import Control.Proxy.Attoparsec+> import Control.Proxy.Trans.Either+> import Data.Attoparsec.Text+> import Data.Text+>+> data Name = Name Text+> deriving (Show, Eq)+>+> hello :: Parser Name+> hello = fmap Name $ "Hello " .*> takeWhile1 (/='.') <*. "."+>+> input1 :: [Text]+> input1 =+> [ "Hello Kate."+> , "Hello Mary.Hello Jef"+> , "f."+> , "Hel"+> , "lo Tom."+> ]+>+> input2 :: [Text]+> input2 =+> [ "Hello Amy."+> , "Hello, Hello Tim."+> , "Hello Bob."+> , "Hello James"+> , "Hello"+> , "Hello World."+> , "HexHello Jon."+> , "H"+> , "ello Ann"+> , "."+> , "Hello Jean-Luc."+> ]+>+> helloPipe1 :: (Proxy p, Monad m) => () -> Pipe p Text Name m r+> helloPipe1 = parserInputD >-> parserD hello+>+> helloPipe2 :: (Proxy p, Monad m) => () -> Pipe p Text Name m r+> helloPipe2 = parserInputD >-> retryLeftovers >-> parserD hello+>+> helloPipe3 :: (Proxy p, Monad m) => () -> Pipe (EitherP BadInput p) Text Name m r+> helloPipe3 = parserInputD >-> throwParsingErrors >-> parserD hello+>+> helloPipe4 :: (Proxy p, Monad m) => () -> Pipe (EitherP BadInput p) Text Name m r+> helloPipe4 = parserInputD >-> limitInputLength 10 >-> parserD hello+>+> helloPipe5 :: (Proxy p, Monad m) => () -> Pipe (EitherP BadInput p) Text Name m r+> helloPipe5 = parserInputD >-> limitInputLength 10 >-> retryLeftovers >-> parserD hello+>+> helloPipe6 :: (Proxy p, Monad m) => () -> Pipe p Text Name m r+> helloPipe6 = parserInputD >-> skipPartialResults >-> parserD hello+>+> skipPartialResults+> :: (Monad m, Proxy p, AttoparsecInput a)+> => ParserStatus a+> -> p (ParserStatus a) (ParserSupply a) (ParserStatus a) (ParserSupply a) m r+> skipPartialResults = runIdentityK . foreverK $ go+> where go x@(Parsing _) = request x >>= \(_, a) -> respond (Start, a)+> go x = request x >>= respond+++$example-simple++We'll write a simple 'Parser' that turns 'Text' like /“Hello John Doe.”/+into @'Name' \"John Doe\"@, and then make a 'Pipe' that turns+those 'Text' values flowing downstream into 'Name' values flowing+downstream using that 'Parser'.++In this example we are using 'Text', but we may as well use+'Data.ByteString.ByteString'. Also, the 'OverloadedStrings' language+extension lets us write our parser easily.++ > {-# LANGUAGE OverloadedStrings #-}+ >+ > import Control.Proxy+ > import Control.Proxy.Attoparsec+ > import Control.Proxy.Trans.Either+ > import Data.Attoparsec.Text+ > import Data.Text+ >+ > data Name = Name Text+ > deriving (Show)+ >+ > hello :: Parser Name+ > hello = fmap Name $ "Hello " .*> takeWhile1 (/='.') <*. "."++We are done with our parser, now lets make a simple parsing 'Pipe' with it.++ > helloPipe1 :: (Proxy p, Monad m) => () -> Pipe p Text Name m r+ > helloPipe1 = parserInputD >-> parserD hello++As the type indicates, this 'Pipe' receives 'Text' values from upstream and+sends 'Name' values downstream. This 'Pipe' is made of two smaller+two smaller cooperating 'Proxy's:++ 1. 'parserInputD': Prepares @a@ input received from upstream to be+ consumed by a downstream parsing 'Proxy'.++ 2. 'parserD': Repeatedly runs a given @'Parser' a b@ on input 'a' from+ upstream, and sends 'b' values downstream.++We need some sample input to test our simple 'helloPipe1'.++ > input1 :: [Text]+ > input1 =+ > [ "Hello Kate."+ > , "Hello Mary.Hello Jef"+ > , "f."+ > , "Hel"+ > , "lo Tom."+ > ]++We'll use @'fromListS' input1@ as our input source, which sends+downstream one element from the list at a time. We'll call each of these+elements a /chunk/. So, @'fromListS' input1@ sends 5 /chunks/ of+'Text' downstream.++Notice how some of our /chunks/ are not, by themselves, complete inputs+for our 'hello' 'Parser'. This is fine; we want to be able to feed the+'Parser' with either partial or complete input as soon as it's+received from upstream. More input will be requested when needed.+Attoparsec's 'Parser' handles partial parsing just fine.++ >>> runProxy $ fromListS input1 >-> helloPipe1 >-> printD+ Name "Kate"+ Name "Mary"+ Name "Jeff"+ Name "Tom"++We have accomplished our simple goal: We've made a 'Pipe' that parses+downstream flowing input using our 'Parser' 'hello'.+++$example-errors++Let's try with some more complex input.++ > input2 :: [Text]+ > input2 =+ > [ "Hello Amy."+ > , "Hello, Hello Tim."+ > , "Hello Bob."+ > , "Hello James"+ > , "Hello"+ > , "Hello World."+ > , "HexHello Jon."+ > , "H"+ > , "ello Ann"+ > , "."+ > , "Hello Jean-Luc."+ > ]++ >>> runProxy $ fromListS input2 >-> helloPipe1 >-> printD+ Name "Amy"+ Name "Bob"+ Name "JamesHelloHello World"+ Name "Ann"+ Name "Jean-Luc"++The simple @helloPipe1@ we built skips /chunks/ of input that fail to be+parsed, and then continues parsing new input. That approach might be+enough if you are certain your input is always well-formed, but+sometimes you may prefer to act differently on these extraordinary+situations.++Instead of just using 'parserInputD' and 'parserD' to build our+'helloPipe1', we could have used an additional 'Proxy' in between them to+handle these situations. The module "Control.Proxy.Attoparsec.Control"+exports some useful 'Proxy's that serve this purpose. The default+behavior just mentioned resembles the one provided by+'skipMalformedChunks'.++Here are some other examples:++['retryLeftovers']+ On parsing failures, keep retrying with any left-over input, skipping+ individual bits each time. If there are no left-overs, then more+ input is requests form upstream.++ > helloPipe2 :: (Proxy p, Monad m) => () -> Pipe p Text Name m r+ > helloPipe2 = parserInputD >-> retryLeftovers >-> parserD hello++ >>> runProxy $ fromListS input2 >-> helloPipe2 >-> printD+ Name "Amy"+ Name "Tim"+ Name "Bob"+ Name "JamesHelloHello World"+ Name "Jon"+ Name "Ann"+ Name "Jean-Luc"++['throwParsingErrors']+ When a parsing error arises, aborts execution by throwing+ 'MalformedInput' in the 'EitherP' proxy transformer.++ > helloPipe3 :: (Proxy p, Monad m) => () -> Pipe (EitherP BadInput p) Text Name m r+ > helloPipe3 = parserInputD >-> throwParsingErrors >-> parserD hello++ >>> runProxy . runEitherK $ fromListS input2 >-> helloPipe3 >-> printD+ Name "Amy"+ Left (MalformedInput {miParserErrror = ParserError {errorContexts = [], errorMessage = "Failed reading: takeWith"}})++[@'limitInputLength' n@]+ If a @'Parser' a b@ has consumed input @a@ of length longer than+ @n@ without producing a @b@ value, and it's still requesting more+ input, then throw 'InputTooLong' in the 'EitherP' proxy transformer.++ > helloPipe4 :: (Proxy p, Monad m) => () -> Pipe (EitherP BadInput p) Text Name m r+ > helloPipe4 = parserInputD >-> limitInputLength 10 >-> parserD hello++ >>> runProxy . runEitherK $ fromListS input2 >-> helloPipe4 >-> printD+ Name "Amy"+ Name "Bob"+ Left (InputTooLong {itlLenght = 11})++ Notice that by default, as mentioned earlier, parsing errors are+ ignored by skipping the malformed /chunk/. That's why we didn't get any+ complaint about the malformed input between /“Amy”/ and /“Bob”/.+++$example-compose-control++These 'Proxy's that control the parsing behavior can be easily plugged+together with @('>->')@ to achieve a combined functionality. Keep in+mind that the order in which these 'Proxy's are used is important.++Suppose you don't want to parse inputs of length longer than 10, and on+parsing failures, you want to retry feeding the parser with any+left-overs. You could achieve that behaviour like this:++ > helloPipe5 :: (Proxy p, Monad m) => () -> Pipe (EitherP BadInput p) Text Name m r+ > helloPipe5 = parserInputD >-> limitInputLength 10 >-> retryLeftovers >-> parserD hello++ >>> runProxy . runEitherK $ fromListS input2 >-> helloPipe5 >-> printD+ Name "Amy"+ Name "Tim"+ Name "Bob"+ Left (InputTooLong {itlLenght = 11})+++$example-control-custom++In case the 'Proxy's provided by "Control.Proxy.Attoparsec.Control" are not+enough for your needs, you can create your custom parsing control 'Proxy'.++Through a parsing control 'Proxy', @'ParserStatus' a@ values flow+upstream and @'ParserSupply' a@ values flow downstream. A parsing+control 'Proxy' may use, replace, or alter the flow of these values to+achieve its purpose. A @'ParserStatus' a@ value received from downstream+reports the status of a 'parserD' parsing 'Proxy', and in exchange,+downstream expects a @'ParserSupply' a@ value.++@'ParserSupply' a@ values carry raw input to be parsed and directives on how it+should be used. @'ParserSupply' a@ is just a type synonym for+@('SupplyUse', a)@. The @a@ value is the input chunk, and the 'SupplyUse' value+could be either 'Resume' if the parser currently waiting for input should be+fed, or 'Start', if a new parser should be started and fed the input,+effectively aborting any parsing activity currently waiting for more input.++See the documentation about 'ParserStatus' and 'ParserSupply' for more details.++Suppose you want to write a parsing control 'Proxy' that never provides+additional input to partial parsing results. Let's first take a look at the+type of this 'Proxy':++ > skipPartialResults+ > :: (Monad m, Proxy p, AttoparsecInput a)+ > => ParserStatus a+ > -> p (ParserStatus a) (ParserSupply a) (ParserStatus a) (ParserSupply a) m r++Just like we said, @'ParserStatus' a@ values flow upstream and @'ParserSupply'+a@ values flow downstream.++Now to a simple implementation: If we receive @'Parsing' n@ from downstream,+then we know there is a partial parsing result waiting for more input. If we+were to respond to this request with @('Resume', a)@, then the partial parser+would continue consuming input, but if we change our response to @('Start', a)@,+then the partial parser would be aborted and a new parser would start+consuming the given input. A simple implementation is quite straightforward:++ > skipPartialResults = runIdentityK . foreverK $ go+ > where go x@(Parsing _) = request x >>= \(_, a) -> respond (Start, a)+ > go x = request x >>= respond++We forward upstream the requests we get from downstream. However, in the case+of a @'Parsing' n@ status, we replace with 'Start' the first value in the+@'ParserSupply' a@ pair we receive from upstream before responding.++Now we can use this parsing control 'Proxy' with some simple input and see it+working.++ > helloPipe6 :: (Proxy p, Monad m) => () -> Pipe p Text Name m r+ > helloPipe6 = parserInputD >-> skipPartialResults >-> parserD hello++ >>> runProxy $ fromListS input1 >-> helloPipe6 >-> printD+ Name "Kate"+ Name "Mary"+++$example-try++This module exports the following previous examples so that you can try+them.+
+ src/Control/Proxy/Attoparsec/Types.hs view
@@ -0,0 +1,101 @@+{-# LANGUAGE DeriveFunctor #-}+{-# LANGUAGE KindSignatures #-}++-- | This module exports common types used throughout+-- @"Control.Proxy.Attoparsec".*@++module Control.Proxy.Attoparsec.Types+ ( -- * Proxy control+ ParserStatus(..)+ , SupplyUse(..)+ , ParserSupply+ -- * Error types+ , BadInput(..)+ -- * Attoparsec integration+ , AttoparsecInput(..)+ , ParserError(..)+ ) where++import qualified Data.Attoparsec.ByteString as ABS+import qualified Data.Attoparsec.Text as AT+import Data.Attoparsec.Types+import qualified Data.ByteString as BS+import qualified Data.Text as T++++-- | Status of a parsing 'Proxy'.+data ParserStatus a+ -- | There is a 'Parser' running and waiting for more @a@ input.+ --+ -- Receiving @('Resume', a)@ from upstream would feed input @a@ to the 'Parser'+ -- waiting for more input.+ = Parsing+ { psLength :: Int -- ^Length of input consumed so far.+ }+ -- | A 'Parser' has failed parsing.+ --+ -- Receiving @('Resume', a)@ from upstream would feed input @a@ to a newly+ -- started 'Parser'.+ | Failed+ { psLeftover :: a -- ^Input not yet consumed when the failure occurred.+ , psError :: ParserError -- ^Error found while parsing.+ }+ deriving (Show, Eq)+++data ParserError = ParserError+ { errorContexts :: [String] -- ^ Contexts where the error occurred.+ , errorMessage :: String -- ^ Error message.+ } deriving (Show, Eq)+++-- | Indicates how should a parsing 'Proxy' use new input.+data SupplyUse+ -- | Start a new parser and fed any new input to it.+ = Start+ -- | Resume feeding any new input to the current parser waiting for input.+ | Resume+ deriving (Eq, Show)+++-- | Input supplied to a parsing 'Proxy'.+--+-- The 'SupplyUse' value indicates how the @a@ input chunk should be used.+type ParserSupply a = (SupplyUse, a)+++-- | Reasons for an input value to be unnaceptable.+data BadInput+ = InputTooLong+ { itlLength :: Int -- ^ Length of the input.+ }+ | MalformedInput+ { miParserErrror :: ParserError -- ^ Error found while parsing.+ }+ deriving (Show, Eq)+++-- | A class for valid Attoparsec input types.+class Eq a => AttoparsecInput a where+ -- | Run a 'Parser' with input @a@.+ parse :: Parser a b -> a -> IResult a b+ -- | Tests whether @a@ is empty.+ null :: a -> Bool+ -- | Skips the first @n@ elements from @a@ and returns the rest.+ drop :: Int -> a -> a+ -- | Number of elements in @a@.+ length :: a -> Int++instance AttoparsecInput BS.ByteString where+ parse = ABS.parse+ null = BS.null+ drop = BS.drop+ length = BS.length++instance AttoparsecInput T.Text where+ parse = AT.parse+ null = T.null+ drop = T.drop+ length = T.length+
+ tests/Main.hs view
@@ -0,0 +1,21 @@+module Main where++import Test.Framework (defaultMain, testGroup)+import Test.Framework.Providers.QuickCheck2 (testProperty)+import Test.Framework.Providers.HUnit (testCase)+import Test.HUnit++import qualified Test.Tutorial+import qualified Test.Attoparsec++main = defaultMain tests++tests =+ [ testGroup "Sample" sampleTests+ , testGroup "Tutorial" Test.Tutorial.tests+ , testGroup "Attoparsec" Test.Attoparsec.tests+ ]++sampleTests = [ testProperty "QuickCheck" $ \x -> const True (x :: Int) == True+ , testCase "HUnit" $ True @?= True+ ]
+ tests/Test/Attoparsec.hs view
@@ -0,0 +1,7 @@+{-# LANGUAGE OverloadedStrings #-}++module Test.Attoparsec (tests) where++-- TODO++tests = []
+ tests/Test/Tutorial.hs view
@@ -0,0 +1,66 @@+{-# LANGUAGE OverloadedStrings #-}++module Test.Tutorial where++import Control.Proxy+import Control.Proxy.Attoparsec+import Control.Proxy.Attoparsec.Tutorial+import Control.Proxy.Trans.Either+import Data.Maybe+import Test.Framework.Providers.HUnit (testCase)+import Test.HUnit hiding (test)+++testPipe pipe input test = fromJust $ do+ let proxy = fromListS input >-> pipe >-> toListD+ output <- runWriterT . runProxy . runEitherK $ proxy+ return . assert $ test output+++test_helloPipe1_input1 = testPipe helloPipe1 input1 test+ where test (Right _, x) = x == fmap Name ["Kate" , "Mary", "Jeff", "Tom"]+ test _ = False++test_helloPipe1_input2 = testPipe helloPipe1 input2 test+ where test (Right _, x) = x == fmap Name [ "Amy", "Bob"+ , "JamesHelloHello World" , "Ann"+ , "Jean-Luc" ]+ test _ = False+++test_helloPipe2_input2 = testPipe helloPipe2 input2 test+ where test (Right _, x) = x == fmap Name [ "Amy" ,"Tim", "Bob"+ , "JamesHelloHello World", "Jon"+ , "Ann" , "Jean-Luc" ]+ test _ = False++test_helloPipe3_input2 = testPipe helloPipe3 input2 test+ where test (Left (MalformedInput _), [Name "Amy"]) = True+ test _ = False+++test_helloPipe4_input2 = testPipe helloPipe4 input2 test+ where test (Left (InputTooLong 11), [Name "Amy", Name "Bob"]) = True+ test _ = False+++test_helloPipe5_input2 = testPipe helloPipe5 input2 test+ where test (Left (InputTooLong 11),+ [Name "Amy", Name "Tim", Name "Bob"]) = True+ test _ = False++test_helloPipe6_input1 = testPipe helloPipe6 input1 test+ where test (Right _, [Name "Kate", Name "Mary"]) = True+ test _ = False+++tests =+ [ testCase "helloPipe1 + input1" test_helloPipe1_input1+ , testCase "helloPipe1 + input2" test_helloPipe1_input2+ , testCase "helloPipe2 + input2" test_helloPipe2_input2+ , testCase "helloPipe3 + input2" test_helloPipe3_input2+ , testCase "helloPipe4 + input2" test_helloPipe4_input2+ , testCase "helloPipe5 + input2" test_helloPipe5_input2+ , testCase "helloPipe6 + input1" test_helloPipe6_input1+ ]+