packages feed

cassava 0.1.0.1 → 0.2.0.0

raw patch · 16 files changed

+942/−100 lines, 16 filesdep +ghc-primdep ~arraydep ~attoparsecdep ~basePVP ok

version bump matches the API change (PVP)

Dependencies added: ghc-prim

Dependency ranges changed: array, attoparsec, base, blaze-builder, text, vector

API changes (from Hackage documentation)

+ Data.Csv.Incremental: Done :: [Either String a] -> Parser a
+ Data.Csv.Incremental: DoneH :: !Header -> a -> HeaderParser a
+ Data.Csv.Incremental: Fail :: !ByteString -> String -> Parser a
+ Data.Csv.Incremental: FailH :: !ByteString -> String -> HeaderParser a
+ Data.Csv.Incremental: Partial :: (ByteString -> Parser a) -> Parser a
+ Data.Csv.Incremental: PartialH :: (ByteString -> HeaderParser a) -> HeaderParser a
+ Data.Csv.Incremental: Some :: [Either String a] -> (ByteString -> Parser a) -> Parser a
+ Data.Csv.Incremental: data HeaderParser a
+ Data.Csv.Incremental: data Parser a
+ Data.Csv.Incremental: decode :: FromRecord a => Bool -> Parser a
+ Data.Csv.Incremental: decodeByName :: FromNamedRecord a => HeaderParser (Parser a)
+ Data.Csv.Incremental: decodeByNameWith :: FromNamedRecord a => DecodeOptions -> HeaderParser (Parser a)
+ Data.Csv.Incremental: decodeHeader :: HeaderParser ByteString
+ Data.Csv.Incremental: decodeHeaderWith :: DecodeOptions -> HeaderParser ByteString
+ Data.Csv.Incremental: decodeWith :: FromRecord a => DecodeOptions -> Bool -> Parser a
+ Data.Csv.Incremental: feedChunk :: Parser a -> ByteString -> Parser a
+ Data.Csv.Incremental: feedChunkH :: HeaderParser a -> ByteString -> HeaderParser a
+ Data.Csv.Incremental: feedEndOfInput :: Parser a -> Parser a
+ Data.Csv.Incremental: feedEndOfInputH :: HeaderParser a -> HeaderParser a
+ Data.Csv.Incremental: instance Eq More
+ Data.Csv.Incremental: instance Functor HeaderParser
+ Data.Csv.Incremental: instance Functor Parser
+ Data.Csv.Incremental: instance Show More
+ Data.Csv.Incremental: instance Show a => Show (HeaderParser a)
+ Data.Csv.Incremental: instance Show a => Show (Parser a)
+ Data.Csv.Streaming: Cons :: (Either String a) -> (Records a) -> Records a
+ Data.Csv.Streaming: Nil :: (Maybe String) -> ByteString -> Records a
+ Data.Csv.Streaming: data Records a
+ Data.Csv.Streaming: decode :: FromRecord a => Bool -> ByteString -> Records a
+ Data.Csv.Streaming: decodeByName :: FromNamedRecord a => ByteString -> Either String (Header, Records a)
+ Data.Csv.Streaming: decodeByNameWith :: FromNamedRecord a => DecodeOptions -> ByteString -> Either String (Header, Records a)
+ Data.Csv.Streaming: decodeWith :: FromRecord a => DecodeOptions -> Bool -> ByteString -> Records a
+ Data.Csv.Streaming: instance Eq a => Eq (Records a)
+ Data.Csv.Streaming: instance Foldable Records
+ Data.Csv.Streaming: instance Functor Records
+ Data.Csv.Streaming: instance Show a => Show (Records a)
+ Data.Csv.Streaming: instance Traversable Records
- Data.Csv: class FromNamedRecord a
+ Data.Csv: class FromNamedRecord a where parseNamedRecord r = to <$> gparseNamedRecord r
- Data.Csv: class FromRecord a
+ Data.Csv: class FromRecord a where parseRecord r = to <$> gparseRecord r
- Data.Csv: class ToNamedRecord a
+ Data.Csv: class ToNamedRecord a where toNamedRecord = namedRecord . gtoRecord . from
- Data.Csv: class ToRecord a
+ Data.Csv: class ToRecord a where toRecord = fromList . gtoRecord . from
- Data.Csv: decode :: FromRecord a => ByteString -> Either String (Vector a)
+ Data.Csv: decode :: FromRecord a => Bool -> ByteString -> Either String (Vector a)
- Data.Csv: decodeWith :: FromRecord a => DecodeOptions -> ByteString -> Either String (Vector a)
+ Data.Csv: decodeWith :: FromRecord a => DecodeOptions -> Bool -> ByteString -> Either String (Vector a)
- Data.Csv.Parser: name :: Word8 -> Parser Field
+ Data.Csv.Parser: name :: Word8 -> Parser Name

Files

Data/Csv.hs view
@@ -14,6 +14,9 @@     -- * Usage example     -- $example +    -- * Treating CSV data as opaque byte strings+    -- $generic-processing+     -- * Encoding and decoding     -- $encoding       decode@@ -85,9 +88,30 @@ -- -- A short decoding usage example: ----- @ >>> 'decode' \"John,27\\r\\nJane,28\\r\\n\" :: Either String (Vector (Text, Int))+-- @ >>> 'decode' 'False' \"John,27\\r\\nJane,28\\r\\n\" :: Either String (Vector (Text, Int)) -- Right (fromList [(\"John\",27),(\"Jane\",28)]) -- @+--+-- We pass 'False' as the first argument to indicate that the CSV+-- input data isn't preceded by a header.+--+-- In practice, the return type of 'decode' rarely needs to be given,+-- as it can often be inferred from the context.++-- $generic-processing+--+-- Sometimes you might want to work with a CSV file which contents is+-- unknown to you. For example, you might want remove the second+-- column of a file without knowing anything about its content. To+-- parse a CSV file to a generic representation, just convert each+-- record to a @'Vector' 'ByteString'@ value, like so:+--+-- @ 'decode' 'False' \"John,27\\r\\nJane,28\\r\\n\" :: Either String (Vector (Vector ByteString))+-- Right (fromList [fromList [\"John\",\"27\"],fromList [\"Jane\",\"28\"]])+-- @+--+-- As the example output above shows, all the fields are returned as+-- uninterpreted 'ByteString' values.  -- $encoding --
+ Data/Csv/Compat/Monoid.hs view
@@ -0,0 +1,14 @@+{-# LANGUAGE CPP #-}+module Data.Csv.Compat.Monoid+    ( (<>)+    ) where++import Data.Monoid++#if !MIN_VERSION_base(4,5,0)+infixr 6 <>+-- | An infix synonym for 'mappend'.+(<>) :: Monoid m => m -> m -> m+(<>) = mappend+{-# INLINE (<>) #-}+#endif
Data/Csv/Conversion.hs view
@@ -1,4 +1,9 @@-{-# LANGUAGE FlexibleInstances, OverloadedStrings, Rank2Types #-}+{-# LANGUAGE BangPatterns, CPP, FlexibleInstances, OverloadedStrings,+             Rank2Types #-}+#ifdef GENERICS+{-# LANGUAGE DefaultSignatures, TypeOperators, KindSignatures, FlexibleContexts,+             MultiParamTypeClasses, UndecidableInstances, ScopedTypeVariables #-}+#endif module Data.Csv.Conversion     (     -- * Type conversion@@ -14,6 +19,7 @@     , Result(..)     , Parser     , parse+    , parseEither      -- * Accessors     , (.!)@@ -40,6 +46,7 @@ import Data.Traversable import Data.Vector (Vector, (!)) import qualified Data.Vector as V+import qualified Data.Vector.Unboxed as U import Data.Word import GHC.Float (double2Float) import Prelude hiding (takeWhile)@@ -47,6 +54,11 @@ import Data.Csv.Conversion.Internal import Data.Csv.Types +#ifdef GENERICS+import GHC.Generics+import qualified Data.IntMap as IM+#endif+ ------------------------------------------------------------------------ -- Type conversion @@ -78,6 +90,11 @@ -- @ class FromRecord a where     parseRecord :: Record -> Parser a+  +#ifdef GENERICS+    default parseRecord :: (Generic a, GFromRecord (Rep a)) => Record -> Parser a+    parseRecord r = to <$> gparseRecord r+#endif  -- | Haskell lacks a single-element tuple type, so if you CSV data -- with just one column you can use the 'Only' type to represent a@@ -104,6 +121,11 @@ class ToRecord a where     toRecord :: a -> Record +#ifdef GENERICS+    default toRecord :: (Generic a, GToRecord (Rep a) Field) => a -> Record+    toRecord = V.fromList . gtoRecord . from+#endif+ instance FromField a => FromRecord (Only a) where     parseRecord v         | n == 1    = Only <$> parseField (V.unsafeIndex v 0)@@ -237,6 +259,12 @@ instance ToField a => ToRecord (Vector a) where     toRecord = V.map toField +instance (FromField a, U.Unbox a) => FromRecord (U.Vector a) where+    parseRecord = fmap U.convert . traverse parseField++instance (ToField a, U.Unbox a) => ToRecord (U.Vector a) where+    toRecord = V.map toField . U.convert+ ------------------------------------------------------------------------ -- Name-based conversion @@ -270,6 +298,11 @@ class FromNamedRecord a where     parseNamedRecord :: NamedRecord -> Parser a +#ifdef GENERICS+    default parseNamedRecord :: (Generic a, GFromNamedRecord (Rep a)) => NamedRecord -> Parser a+    parseNamedRecord r = to <$> gparseNamedRecord r+#endif+ -- | A type that can be converted to a single CSV record. -- -- An example type and instance:@@ -283,6 +316,11 @@ class ToNamedRecord a where     toNamedRecord :: a -> NamedRecord +#ifdef GENERICS+    default toNamedRecord :: (Generic a, GToRecord (Rep a) (B.ByteString, B.ByteString)) => a -> NamedRecord+    toNamedRecord = namedRecord . gtoRecord . from+#endif+ instance FromField a => FromNamedRecord (M.Map B.ByteString a) where     parseNamedRecord m = M.fromList <$>                          (traverse parseSnd $ HM.toList m)@@ -339,6 +377,18 @@ class ToField a where     toField :: a -> Field +-- | 'Nothing' if the field is empty, 'Just' otherwise.+instance FromField a => FromField (Maybe a) where+    parseField s+        | B.null s  = pure Nothing+        | otherwise = Just <$> parseField s+    {-# INLINE parseField #-}++-- | 'Nothing' is encoded as an empty field.+instance ToField a => ToField (Maybe a) where+    toField = maybe B.empty toField+    {-# INLINE toField #-}+ instance FromField Char where     parseField s         | T.compareLength t 1 == EQ = pure (T.head t)@@ -460,29 +510,12 @@     toField = decimal     {-# INLINE toField #-} --- TODO: Optimize-escape :: B.ByteString -> B.ByteString-escape s-    | B.find (\ b -> b == dquote || b == comma || b == nl || b == cr ||-                     b == sp) s == Nothing = s-    | otherwise =-        B.concat ["\"",-                  B.concatMap-                  (\ b -> if b == dquote then "\"\"" else B.singleton b) s,-                  "\""]-  where-    dquote = 34-    comma  = 44-    nl     = 10-    cr     = 13-    sp     = 32- instance FromField B.ByteString where     parseField = pure     {-# INLINE parseField #-}  instance ToField B.ByteString where-    toField = escape+    toField = id     {-# INLINE toField #-}  instance FromField L.ByteString where@@ -678,7 +711,113 @@   return (b a) {-# INLINE apP #-} --- | Run a 'Parser'.+-- | Run a 'Parser'. Forces the value in the 'Success' or 'Error'+-- constructors to weak head normal form. parse :: Parser a -> Result a-parse p = runParser p Error Success+parse p = runParser p err success+  where+    err !errMsg = Error errMsg+    success !x = Success x {-# INLINE parse #-}++-- | Run a 'Parser', returning either @'Left' msg@ or @'Right'+-- result@. Forces the value in the 'Left' or 'Right' constructors to+-- weak head normal form.+parseEither :: Parser a -> Either String a+parseEither p = runParser p left right+  where+    left !errMsg = Left errMsg+    right !x = Right x+{-# INLINE parseEither #-}++#ifdef GENERICS++class GFromRecord f where+    gparseRecord :: Record -> Parser (f p)++instance GFromRecordSum f Record => GFromRecord (M1 i n f) where+    gparseRecord v = +        case (IM.lookup n gparseRecordSum) of+            Nothing -> lengthMismatch n v +            Just p -> M1 <$> p v+      where+        n = V.length v++class GFromNamedRecord f where+    gparseNamedRecord :: NamedRecord -> Parser (f p)++instance GFromRecordSum f NamedRecord => GFromNamedRecord (M1 i n f) where+    gparseNamedRecord v = +        foldr (\f p -> p <|> M1 <$> f v) empty (IM.elems gparseRecordSum)+++class GFromRecordSum f r where+    gparseRecordSum :: IM.IntMap (r -> Parser (f p))++instance (GFromRecordSum a r, GFromRecordSum b r) => GFromRecordSum (a :+: b) r where+    gparseRecordSum = +        IM.unionWith (\a b r -> a r <|> b r) +            (fmap (L1 <$>) <$> gparseRecordSum)+            (fmap (R1 <$>) <$> gparseRecordSum)++instance GFromRecordProd f r => GFromRecordSum (M1 i n f) r where+    gparseRecordSum = IM.singleton n (fmap (M1 <$>) f)+      where+        (n, f) = gparseRecordProd 0++class GFromRecordProd f r where+    gparseRecordProd :: Int -> (Int, r -> Parser (f p))++instance GFromRecordProd U1 r where+    gparseRecordProd n = (n, const (pure U1))++instance (GFromRecordProd a r, GFromRecordProd b r) => GFromRecordProd (a :*: b) r where+    gparseRecordProd n0 = (n2, f)+      where+        f r = (:*:) <$> fa r <*> fb r+        (n1, fa) = gparseRecordProd n0+        (n2, fb) = gparseRecordProd n1++instance GFromRecordProd f Record => GFromRecordProd (M1 i n f) Record where+    gparseRecordProd n = fmap (M1 <$>) <$> gparseRecordProd n++instance FromField a => GFromRecordProd (K1 i a) Record where+    gparseRecordProd n = (n + 1, \v -> K1 <$> parseField (V.unsafeIndex v n))++data Proxy s (f :: * -> *) a = Proxy++instance (FromField a, Selector s) => GFromRecordProd (M1 S s (K1 i a)) NamedRecord where+    gparseRecordProd n = (n + 1, \v -> (M1 . K1) <$> v .: name)+      where+        name = T.encodeUtf8 (T.pack (selName (Proxy :: Proxy s f a)))+++class GToRecord a f where+    gtoRecord :: a p -> [f]++instance GToRecord U1 f where+    gtoRecord U1 = []++instance (GToRecord a f, GToRecord b f) => GToRecord (a :*: b) f where+    gtoRecord (a :*: b) = gtoRecord a ++ gtoRecord b++instance (GToRecord a f, GToRecord b f) => GToRecord (a :+: b) f where+    gtoRecord (L1 a) = gtoRecord a+    gtoRecord (R1 b) = gtoRecord b++instance GToRecord a f => GToRecord (M1 D c a) f where+    gtoRecord (M1 a) = gtoRecord a++instance GToRecord a f => GToRecord (M1 C c a) f where+    gtoRecord (M1 a) = gtoRecord a++instance GToRecord a Field => GToRecord (M1 S c a) Field where+    gtoRecord (M1 a) = gtoRecord a++instance ToField a => GToRecord (K1 i a) Field where+    gtoRecord (K1 a) = [toField a]++instance (ToField a, Selector s) => GToRecord (M1 S s (K1 i a)) (B.ByteString, B.ByteString) where+    gtoRecord m@(M1 (K1 a)) = [T.encodeUtf8 (T.pack (selName m)) .= toField a]++#endif
Data/Csv/Conversion/Internal.hs view
@@ -10,8 +10,9 @@ import qualified Data.ByteString as B import Data.Char (ord) import Data.Int-import Data.Monoid import Data.Word++import Data.Csv.Compat.Monoid ((<>))  ------------------------------------------------------------------------ -- Integers
Data/Csv/Encoding.hs view
@@ -37,16 +37,17 @@ import qualified Data.ByteString.Lazy as L import qualified Data.ByteString.Lazy.Char8 as BL8 import qualified Data.HashMap.Strict as HM-import Data.Monoid-import Data.Traversable+import Data.Monoid (mconcat, mempty) import Data.Vector (Vector) import qualified Data.Vector as V import Data.Word (Word8) import Prelude hiding (unlines) +import Data.Csv.Compat.Monoid ((<>)) import Data.Csv.Conversion import Data.Csv.Parser import Data.Csv.Types+import Data.Csv.Util ((<$!>))  -- TODO: 'encode' isn't as efficient as it could be. @@ -56,7 +57,11 @@ -- | Efficiently deserialize CSV records from a lazy 'L.ByteString'. -- If this fails due to incomplete or invalid input, @'Left' msg@ is -- returned. Equivalent to @'decodeWith' 'defaultDecodeOptions'@.-decode :: FromRecord a => L.ByteString -> Either String (Vector a)+decode :: FromRecord a+       => Bool          -- ^ Data contains header that should be+                        -- skipped+       -> L.ByteString  -- ^ CSV data+       -> Either String (Vector a) decode = decodeWith defaultDecodeOptions {-# INLINE decode #-} @@ -64,47 +69,80 @@ -- If this fails due to incomplete or invalid input, @'Left' msg@ is -- returned. The data is assumed to be preceeded by a header. -- Equivalent to @'decodeByNameWith' 'defaultDecodeOptions'@.-decodeByName :: FromNamedRecord a => L.ByteString-                 -> Either String (Header, Vector a)+decodeByName :: FromNamedRecord a+             => L.ByteString  -- ^ CSV data+             -> Either String (Header, Vector a) decodeByName = decodeByNameWith defaultDecodeOptions {-# INLINE decodeByName #-}  -- | Efficiently serialize CVS records as a lazy 'L.ByteString'. encode :: ToRecord a => V.Vector a -> L.ByteString encode = encodeWith defaultEncodeOptions+{-# INLINE encode #-}  -- | Efficiently serialize CVS records as a lazy 'L.ByteString'. The -- header is written before any records and dictates the field order. encodeByName :: ToNamedRecord a => Header -> V.Vector a -> L.ByteString encodeByName = encodeByNameWith defaultEncodeOptions+{-# INLINE encodeByName #-}  ------------------------------------------------------------------------ -- ** Encoding and decoding options  -- | Like 'decode', but lets you customize how the CSV data is parsed.-decodeWith :: FromRecord a => DecodeOptions -> L.ByteString+decodeWith :: FromRecord a+           => DecodeOptions  -- ^ Decoding options+           -> Bool           -- ^ Data contains header that should be+                             -- skipped+           -> L.ByteString   -- ^ CSV data            -> Either String (Vector a)-decodeWith !opts = decodeWithP (csv opts) (parse . traverse parseRecord)+decodeWith = decodeWithC (parse . parseCsv) {-# INLINE [1] decodeWith #-} +parseCsv :: FromRecord a => Csv -> Parser (Vector a)+parseCsv xs = V.fromList <$!> mapM' parseRecord (V.toList xs)++mapM' :: Monad m => (a -> m b) -> [a] -> m [b]+mapM' f = go+  where+    go [] = return []+    go (x:xs) = do+        !y <- f x+        ys <- go xs+        return (y : ys)+{-# INLINE mapM' #-}+ {-# RULES     "idDecodeWith" decodeWith = idDecodeWith  #-}  -- | Same as 'decodeWith', but more efficient as no type -- conversion is performed.-idDecodeWith :: DecodeOptions -> L.ByteString+idDecodeWith :: DecodeOptions -> Bool -> L.ByteString              -> Either String (Vector (Vector B.ByteString))-idDecodeWith !opts = decodeWithP (csv opts) pure+idDecodeWith = decodeWithC pure +decodeWithC :: (Csv -> Result a) -> DecodeOptions -> Bool -> L.ByteString+            -> Either String a+decodeWithC convert !opts skipHeader = decodeWithP parser convert+  where parser+            | skipHeader = header (decDelimiter opts) *> csv opts+            | otherwise  = csv opts+{-# INLINE decodeWithC #-}+ -- | Like 'decodeByName', but lets you customize how the CSV data is -- parsed.-decodeByNameWith :: FromNamedRecord a => DecodeOptions -> L.ByteString-                     -> Either String (Header, Vector a)+decodeByNameWith :: FromNamedRecord a+                 => DecodeOptions  -- ^ Decoding options+                 -> L.ByteString   -- ^ CSV data+                 -> Either String (Header, Vector a) decodeByNameWith !opts =     decodeWithP (csvWithHeader opts)-    (\ (hdr, vs) -> (,) <$> pure hdr <*> (parse $ traverse parseNamedRecord vs))+    (\ (hdr, vs) -> (,) <$> pure hdr <*> (parse $ parseNamedCsv vs)) +parseNamedCsv :: FromNamedRecord a => Vector NamedRecord -> Parser (Vector a)+parseNamedCsv xs = V.fromList <$!> mapM' parseNamedRecord (V.toList xs)+ -- | Options that controls how data is encoded. These options can be -- used to e.g. encode data in a tab-separated format instead of in a -- comma-separated format.@@ -126,12 +164,30 @@                   . unlines                   . map (encodeRecord (encDelimiter opts) . toRecord)                   . V.toList+{-# INLINE encodeWith #-}  encodeRecord :: Word8 -> Record -> Builder encodeRecord delim = mconcat . intersperse (fromWord8 delim)-                     . map fromByteString . V.toList+                     . map fromByteString . map escape . V.toList {-# INLINE encodeRecord #-} +-- TODO: Optimize+escape :: B.ByteString -> B.ByteString+escape s+    | B.find (\ b -> b == dquote || b == comma || b == nl || b == cr ||+                     b == sp) s == Nothing = s+    | otherwise =+        B.concat ["\"",+                  B.concatMap+                  (\ b -> if b == dquote then "\"\"" else B.singleton b) s,+                  "\""]+  where+    dquote = 34+    comma  = 44+    nl     = 10+    cr     = 13+    sp     = 32+ -- | Like 'encodeByName', but lets you customize how the CSV data is -- encoded. encodeByNameWith :: ToNamedRecord a => EncodeOptions -> Header -> V.Vector a@@ -144,6 +200,7 @@               . map (encodeRecord (encDelimiter opts)                      . namedRecordToRecord hdr . toNamedRecord)               . V.toList $ v+{-# INLINE encodeByNameWith #-}   namedRecordToRecord :: Header -> NamedRecord -> Record
+ Data/Csv/Incremental.hs view
@@ -0,0 +1,304 @@+{-# LANGUAGE BangPatterns, DeriveFunctor #-}++-- | This module allows for incremental decoding of CSV data. This is+-- useful if you e.g. want to interleave I\/O with parsing or if you+-- want finer grained control over how you deal with type conversion+-- errors.+module Data.Csv.Incremental+    (+    -- * Decoding headers+      HeaderParser(..)+    , decodeHeader+    , decodeHeaderWith+    -- ** Providing input+    -- $feed-header+    , feedChunkH+    , feedEndOfInputH++    -- * Decoding records+    -- $typeconversion+    , Parser(..)++    -- ** Index-based record conversion+    -- $indexbased+    , decode+    , decodeWith++    -- ** Name-based record conversion+    -- $namebased+    , decodeByName+    , decodeByNameWith++    -- ** Providing input+    -- $feed-records+    , feedChunk+    , feedEndOfInput+    ) where++import Control.Applicative+import qualified Data.Attoparsec as A+import Data.Attoparsec.Char8 (endOfInput, endOfLine)+import qualified Data.ByteString as B+import qualified Data.HashMap.Strict as HM+import qualified Data.Vector as V++import Data.Csv.Conversion hiding (Parser, Result, record, toNamedRecord)+import qualified Data.Csv.Conversion as Conversion+import Data.Csv.Parser+import Data.Csv.Types++-- $feed-header+--+-- These functions are sometimes convenient when working with+-- 'HeaderParser', but don't let you do anything you couldn't already+-- do using the 'HeaderParser' constructors directly.++-- $indexbased+--+-- See documentation on index-based conversion in "Data.Csv" for more+-- information.++-- $namebased+--+-- See documentation on name-based conversion in "Data.Csv" for more+-- information.++-- $feed-records+--+-- These functions are sometimes convenient when working with+-- 'Parser', but don't let you do anything you couldn't already do+-- using the 'Parser' constructors directly.++------------------------------------------------------------------------+-- * Decoding headers++-- | An incremental parser that when fed data eventually returns a+-- parsed 'Header', or an error.+data HeaderParser a =+      -- | The input data was malformed. The first field contains any+      -- unconsumed input and second field contains information about+      -- the parse error.+      FailH !B.ByteString String++      -- | The parser needs more input data before it can produce a+      -- result. Use an 'B.empty' string to indicate that no more+      -- input data is available. If fed an 'B.empty string', the+      -- continuation is guaranteed to return either 'FailH' or+      -- 'DoneH'.+    | PartialH (B.ByteString -> HeaderParser a)++      -- | The parse succeeded and produced the given 'Header'.+    | DoneH !Header a+    deriving Functor++instance Show a => Show (HeaderParser a) where+    showsPrec d (FailH rest msg) = showParen (d > appPrec) showStr+      where+        showStr = showString "FailH " . showsPrec (appPrec+1) rest .+                  showString " " . showsPrec (appPrec+1) msg+    showsPrec _ (PartialH _) = showString "PartialH <function>"+    showsPrec d (DoneH hdr x) = showParen (d > appPrec) showStr+      where+        showStr = showString "DoneH " . showsPrec (appPrec+1) hdr .+                  showString " " . showsPrec (appPrec+1) x++-- Application has precedence one more than the most tightly-binding+-- operator+appPrec :: Int+appPrec = 10++-- | Feed a 'HeaderParser' with more input. If the 'HeaderParser' is+-- 'FailH' it will add the input to 'B.ByteString' of unconsumed+-- input. If the 'HeaderParser' is 'DoneH' it will drop the extra+-- input on the floor.+feedChunkH :: HeaderParser a -> B.ByteString -> HeaderParser a+feedChunkH (FailH rest err) s = FailH (B.append rest s) err+feedChunkH (PartialH k) s     = k s+feedChunkH d@(DoneH _ _) _s   = d++-- | Tell a 'HeaderParser' that there is no more input. This passes+-- 'B.empty' to a 'PartialH' parser, otherwise returns the parser+-- unchanged.+feedEndOfInputH :: HeaderParser a -> HeaderParser a+feedEndOfInputH (PartialH k) = k B.empty+feedEndOfInputH p            = p++-- | Parse a CSV header in an incremental fashion. When done, the+-- 'HeaderParser' returns any unconsumed input in its second field.+decodeHeader :: HeaderParser B.ByteString+decodeHeader = decodeHeaderWith defaultDecodeOptions++-- | Like 'decodeHeader', but lets you customize how the CSV data is+-- parsed.+decodeHeaderWith :: DecodeOptions -> HeaderParser B.ByteString+decodeHeaderWith !opts = PartialH (go . parser)+  where+    parser = A.parse (header $ decDelimiter opts)++    go (A.Fail rest _ msg) = FailH rest err+      where err = "parse error (" ++ msg ++ ")"+    -- TODO: Check empty and give attoparsec one last chance to return+    -- something:+    go (A.Partial k)       = PartialH $ \ s -> go (k s)+    go (A.Done rest r)     = DoneH r rest++------------------------------------------------------------------------+-- * Decoding records++-- $typeconversion+--+-- Just like in the case of non-incremental decoding, there are two+-- ways to convert CSV records to and from and user-defined data+-- types: index-based conversion and name-based conversion.++-- | An incremental parser that when fed data eventually produces some+-- parsed records, converted to the desired type, or an error in case+-- of malformed input data.+data Parser a =+      -- | The input data was malformed. The first field contains any+      -- unconsumed input and second field contains information about+      -- the parse error.+      Fail !B.ByteString String++      -- | The parser needs more input data before it can produce a+      -- result. Use an 'B.empty' string to indicate that no more+      -- input data is available. If fed an 'B.empty' string, the+      -- continuation is guaranteed to return either 'Fail' or 'Done'.+    | Partial (B.ByteString -> Parser a)++      -- | The parser parsed and converted some records. Any records+      -- that failed type conversion are returned as @'Left' errMsg@+      -- and the rest as @'Right' val@. Feed a 'B.ByteString' to the+      -- continuation to continue parsing. Use an 'B.empty' string to+      -- indicate that no more input data is available. If fed an+      -- 'B.empty' string, the continuation is guaranteed to return+      -- either 'Fail' or 'Done'.+    | Some [Either String a] (B.ByteString -> Parser a)++      -- | The parser parsed and converted some records. Any records+      -- that failed type conversion are returned as @'Left' errMsg@+      -- and the rest as @'Right' val@.+    | Done [Either String a]+    deriving Functor++instance Show a => Show (Parser a) where+    showsPrec d (Fail rest msg) = showParen (d > appPrec) showStr+      where+        showStr = showString "Fail " . showsPrec (appPrec+1) rest .+                  showString " " . showsPrec (appPrec+1) msg+    showsPrec _ (Partial _) = showString "Partial <function>"+    showsPrec d (Some rs _) = showParen (d > appPrec) showStr+      where+        showStr = showString "Some " . showsPrec (appPrec+1) rs .+                  showString " <function>"+    showsPrec d (Done rs) = showParen (d > appPrec) showStr+      where+        showStr = showString "Done " . showsPrec (appPrec+1) rs++-- | Feed a 'Parser' with more input. If the 'Parser' is 'Fail' it+-- will add the input to 'B.ByteString' of unconsumed input. If the+-- 'Parser' is 'Done' it will drop the extra input on the floor.+feedChunk :: Parser a -> B.ByteString -> Parser a+feedChunk (Fail rest err) s = Fail (B.append rest s) err+feedChunk (Partial k) s     = k s+feedChunk (Some xs k) s     = Some xs (\ s' -> k s `feedChunk` s')+feedChunk (Done xs) _s      = Done xs++-- | Tell a 'Parser' that there is no more input. This passes 'empty'+-- to a 'Partial' parser, otherwise returns the parser unchanged.+feedEndOfInput :: Parser a -> Parser a+feedEndOfInput (Partial k)     = k B.empty+feedEndOfInput p               = p++-- | Have we read all available input?+data More = Incomplete | Complete+          deriving (Eq, Show)++-- | Efficiently deserialize CSV in an incremental fashion. Equivalent+-- to @'decodeByNameWith' 'defaultDecodeOptions'@.+decode :: FromRecord a+       => Bool          -- ^ Data contains header that should be+                        -- skipped+       -> Parser a+decode = decodeWith defaultDecodeOptions++-- | Like 'decode', but lets you customize how the CSV data is parsed.+decodeWith :: FromRecord a+           => DecodeOptions  -- ^ Decoding options+           -> Bool           -- ^ Data contains header that should be+                             -- skipped+           -> Parser a+decodeWith !opts skipHeader+    | skipHeader = Partial $ \ s -> go (decodeHeaderWith opts `feedChunkH` s)+    | otherwise  = Partial (decodeWithP parseRecord opts)+  where go (FailH rest msg) = Fail rest msg+        go (PartialH k)     = Partial $ \ s' -> go (k s')+        go (DoneH _ rest)   = decodeWithP parseRecord opts rest++------------------------------------------------------------------------++-- | Efficiently deserialize CSV in an incremental fashion. The data+-- is assumed to be preceeded by a header. Returns a 'HeaderParser'+-- that when done produces a 'Parser' for parsing the actual records.+-- Equivalent to @'decodeByNameWith' 'defaultDecodeOptions'@.+decodeByName :: FromNamedRecord a+             => HeaderParser (Parser a)+decodeByName = decodeByNameWith defaultDecodeOptions++-- | Like 'decodeByName', but lets you customize how the CSV data is+-- parsed.+decodeByNameWith :: FromNamedRecord a+                 => DecodeOptions  -- ^ Decoding options+                 -> HeaderParser (Parser a)+decodeByNameWith !opts =+    PartialH (runParser . (decodeHeaderWith opts `feedChunkH`))+  where+    runParser (FailH rest msg) = FailH rest msg+    runParser (PartialH k)     = PartialH $ \ s -> runParser (k s)+    runParser (DoneH hdr rest) =+        DoneH hdr (decodeWithP (parseNamedRecord . toNamedRecord hdr) opts rest)++-- Copied from Data.Csv.Parser+toNamedRecord :: Header -> Record -> NamedRecord+toNamedRecord hdr v = HM.fromList . V.toList $ V.zip hdr v++------------------------------------------------------------------------++-- | Like 'decode', but lets you customize how the CSV data is parsed.+decodeWithP :: (Record -> Conversion.Parser a) -> DecodeOptions -> B.ByteString+            -> Parser a+decodeWithP p !opts = go Incomplete [] . parser+  where+    go !_ !acc (A.Fail rest _ msg)+        | null acc  = Fail rest err+        | otherwise = Some (reverse acc) (\ s -> Fail (rest `B.append` s) err)+      where err = "parse error (" ++ msg ++ ")"+    go Incomplete acc (A.Partial k)+        | null acc  = Partial cont+        | otherwise = Some (reverse acc) cont+      where cont s = go m [] (k s)+              where m | B.null s  = Complete+                      | otherwise = Incomplete+    go Complete _ (A.Partial _) = moduleError "decodeWithP" msg+        where msg = "attoparsec should never return Partial in this case"+    go m acc (A.Done rest r)+        | B.null rest = case m of+            Complete   -> Done (reverse acc')+            Incomplete -> Partial cont+        | otherwise   = go m acc' (parser rest)+      where cont s+                | B.null s  = Done (reverse acc')+                | otherwise = go Incomplete acc' (parser s)+            acc' | blankLine r = acc+                 | otherwise   = convert r : acc++    parser = A.parse (record (decDelimiter opts) <* (endOfLine <|> endOfInput))+    convert = parseEither . p+{-# INLINE decodeWithP #-}++blankLine :: V.Vector B.ByteString -> Bool+blankLine v = V.length v == 1 && (B.null (V.head v))++moduleError :: String -> String -> a+moduleError func msg = error $ "Data.Csv.Incremental." ++ func ++ ": " ++ msg+{-# NOINLINE moduleError #-}
Data/Csv/Parser.hs view
@@ -1,4 +1,4 @@-{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE BangPatterns, CPP #-}  -- | A CSV parser. The parser defined here is RFC 4180 compliant, with -- the following extensions:@@ -30,6 +30,7 @@ import Data.Attoparsec.Char8 hiding (Parser, Result, parse) import qualified Data.Attoparsec as A import qualified Data.Attoparsec.Lazy as AL+import Data.Attoparsec.Types (Parser) import qualified Data.Attoparsec.Zepto as Z import qualified Data.ByteString as S import qualified Data.ByteString.Unsafe as S@@ -39,6 +40,7 @@ import Data.Word  import Data.Csv.Types+import Data.Csv.Util ((<$!>))  -- | Options that controls how data is decoded. These options can be -- used to e.g. decode tab-separated data instead of comma-separated@@ -57,34 +59,52 @@ -- | Parse a CSV file that does not include a header. csv :: DecodeOptions -> AL.Parser Csv csv !opts = do-    vals <- record (decDelimiter opts) `sepBy1` endOfLine+    vals <- record (decDelimiter opts) `sepBy1'` endOfLine     _ <- optional endOfLine     endOfInput     let nonEmpty = removeBlankLines vals-    return (V.fromList nonEmpty)+    return $! V.fromList nonEmpty {-# INLINE csv #-} +-- | @sepBy1' p sep@ applies /one/ or more occurrences of @p@,+-- separated by @sep@. Returns a list of the values returned by @p@.+-- The value returned by @p@ is forced to WHNF.+--+-- > commaSep p  = p `sepBy1'` (symbol ",")+sepBy1' :: (Alternative f, Monad f) => f a -> f s -> f [a]+sepBy1' p s = go+  where+    go = do+        !a <- p+        as <- (s *> go) <|> pure []+        return (a : as)+#if __GLASGOW_HASKELL__ >= 700+{-# SPECIALIZE sepBy1' :: Parser S.ByteString a -> Parser S.ByteString s+                       -> Parser S.ByteString [a] #-}+#endif+ -- | Parse a CSV file that includes a header. csvWithHeader :: DecodeOptions -> AL.Parser (Header, V.Vector NamedRecord) csvWithHeader !opts = do-    hdr <- header (decDelimiter opts)+    !hdr <- header (decDelimiter opts)     vals <- map (toNamedRecord hdr) . removeBlankLines <$>-            (record (decDelimiter opts)) `sepBy1` endOfLine+            (record (decDelimiter opts)) `sepBy1'` endOfLine     _ <- optional endOfLine     endOfInput-    return (hdr, V.fromList vals)+    let !v = V.fromList vals+    return (hdr, v) -toNamedRecord :: V.Vector S.ByteString -> Record -> NamedRecord+toNamedRecord :: Header -> Record -> NamedRecord toNamedRecord hdr v = HM.fromList . V.toList $ V.zip hdr v  -- | Parse a header, including the terminating line separator. header :: Word8  -- ^ Field delimiter        -> AL.Parser Header-header !delim = V.fromList <$> name delim `sepBy1` (A.word8 delim) <* endOfLine+header !delim = V.fromList <$!> name delim `sepBy1'` (A.word8 delim) <* endOfLine  -- | Parse a header name. Header names have the same format as regular -- 'field's.-name :: Word8 -> AL.Parser Field  -- TODO: Create Name type alias+name :: Word8 -> AL.Parser Name name !delim = field delim  removeBlankLines :: [Record] -> [Record]@@ -98,7 +118,9 @@ -- this parser. record :: Word8  -- ^ Field delimiter        -> AL.Parser Record-record !delim = V.fromList <$> field delim `sepBy1` (A.word8 delim)+record !delim = do+    fs <- field delim `sepBy1'` (A.word8 delim)+    return $! V.fromList fs {-# INLINE record #-}  -- | Parse a field. The field may be in either the escaped or@@ -111,6 +133,7 @@     case mb of         Just b | b == doubleQuote -> escapedField         _                         -> unescapedField delim+{-# INLINE field #-}  escapedField :: AL.Parser S.ByteString escapedField = do@@ -137,7 +160,7 @@ dquote = char '"'  unescape :: Z.Parser S.ByteString-unescape = toByteString <$> go mempty where+unescape = toByteString <$!> go mempty where   go acc = do     h <- Z.takeWhile (/= doubleQuote)     let rest = do
+ Data/Csv/Streaming.hs view
@@ -0,0 +1,130 @@+{-# LANGUAGE BangPatterns, CPP, DeriveFunctor #-}++-- | This module allows for streaming decoding of CSV data. This is+-- useful if you need to parse large amounts of input in constant+-- space. The API also allows you to ignore type conversion errors on+-- a per-record basis.+module Data.Csv.Streaming+    ( Records(..)+    , decode+    , decodeWith+    , decodeByName+    , decodeByNameWith+    ) where++import Control.Applicative ((<$>), (<*>), pure)+import qualified Data.ByteString as B+import qualified Data.ByteString.Lazy as BL+import qualified Data.ByteString.Lazy.Char8 as BL8+import Data.Foldable (Foldable(..))+import Data.Traversable (Traversable(..))+import Prelude hiding (foldr)++import Data.Csv.Conversion+import Data.Csv.Incremental hiding (decode, decodeByName, decodeByNameWith,+                                    decodeWith)+import qualified Data.Csv.Incremental as I+import Data.Csv.Parser+import Data.Csv.Types++-- | A stream of parsed records. If type conversion failed for the+-- record, the error is returned as @'Left' errMsg@.+data Records a+    = -- | A record or an error message, followed by more records.+      Cons (Either String a) (Records a)++      -- | End of stream, potentially due to a parse error. If a parse+      -- error occured, the first field contains the error message.+      -- The second field contains any unconsumed input.+    | Nil (Maybe String) BL.ByteString+    deriving (Eq, Functor, Show)++instance Foldable Records where+    foldr = foldrRecords+#if MIN_VERSION_base(4,6,0)+    foldl' = foldlRecords'+#endif++foldrRecords :: (a -> b -> b) -> b -> Records a -> b+foldrRecords f = go+  where+    go z (Cons (Right x) rs) = f x (go z rs)+    go z _ = z+{-# INLINE foldrRecords #-}++#if MIN_VERSION_base(4,6,0)+foldlRecords' :: (a -> b -> a) -> a -> Records b -> a+foldlRecords' f = go+  where+    go z (Cons (Right x) rs) = let z' = f z x in z' `seq` go z' rs+    go z _ = z+{-# INLINE foldlRecords' #-}+#endif++instance Traversable Records where+    traverse _ (Nil merr rest) = pure $ Nil merr rest+    traverse f (Cons x xs)     = Cons <$> traverseElem x <*> traverse f xs+      where+        traverseElem (Left err) = pure $ Left err+        traverseElem (Right y)  = Right <$> f y++-- | Efficiently deserialize CSV records in a streaming fashion.+-- Equivalent to @'decodeWith' 'defaultDecodeOptions'@.+decode :: FromRecord a+       => Bool           -- ^ Data contains header that should be+                         -- skipped+       -> BL.ByteString  -- ^ CSV data+       -> Records a+decode = decodeWith defaultDecodeOptions++-- | Like 'decode', but lets you customize how the CSV data is parsed.+decodeWith :: FromRecord a+           => DecodeOptions  -- ^ Decoding options+           -> Bool           -- ^ Data contains header that should be+                             -- skipped+           -> BL.ByteString  -- ^ CSV data+           -> Records a+decodeWith !opts skipHeader s0 = case BL.toChunks s0 of+    []     -> go [] (feedEndOfInput $ I.decodeWith opts skipHeader)+    (s:ss) -> go ss (I.decodeWith opts skipHeader `feedChunk` s)+  where+    go ss (Done xs)       = foldr Cons (Nil Nothing (BL.fromChunks ss)) xs+    go ss (Fail rest err) = Nil (Just err) (BL.fromChunks (rest:ss))+    go [] (Partial k)     = go [] (k B.empty)+    go (s:ss) (Partial k) = go ss (k s)+    go [] (Some xs k)     = foldr Cons (go [] (k B.empty)) xs+    go (s:ss) (Some xs k) = foldr Cons (go ss (k s)) xs++-- | Efficiently deserialize CSV in a streaming fashion. The data is+-- assumed to be preceeded by a header. Returns @'Left' errMsg@ if+-- parsing the header fails. Equivalent to @'decodeByNameWith'+-- 'defaultDecodeOptions'@.+decodeByName :: FromNamedRecord a+             => BL.ByteString  -- ^ CSV data+             -> Either String (Header, Records a)+decodeByName = decodeByNameWith defaultDecodeOptions++-- TODO: Include something more in error messages?++-- | Like 'decodeByName', but lets you customize how the CSV data is+-- parsed.+decodeByNameWith :: FromNamedRecord a+                 => DecodeOptions  -- ^ Decoding options+                 -> BL.ByteString  -- ^ CSV data+                 -> Either String (Header, Records a)+decodeByNameWith !opts s0 = case BL.toChunks s0 of+    []     -> go [] (feedEndOfInputH $ I.decodeByNameWith opts)+    (s:ss) -> go ss (I.decodeByNameWith opts `feedChunkH` s)+  where+    go ss (DoneH hdr p)    = Right (hdr, go2 ss p)+    go ss (FailH rest err) = Left $ err ++ " at " +++                             show (BL8.unpack . BL.fromChunks $ rest : ss)+    go [] (PartialH k)     = go [] (k B.empty)+    go (s:ss) (PartialH k) = go ss (k s)++    go2 ss (Done xs)       = foldr Cons (Nil Nothing (BL.fromChunks ss)) xs+    go2 ss (Fail rest err) = Nil (Just err) (BL.fromChunks (rest:ss))+    go2 [] (Partial k)     = go2 [] (k B.empty)+    go2 (s:ss) (Partial k) = go2 ss (k s)+    go2 [] (Some xs k)     = foldr Cons (go2 [] (k B.empty)) xs+    go2 (s:ss) (Some xs k) = foldr Cons (go2 ss (k s)) xs
+ Data/Csv/Util.hs view
@@ -0,0 +1,10 @@+module Data.Csv.Util ((<$!>)) where++-- | A strict version of 'Data.Functor.<$>' for monads.+(<$!>) :: Monad m => (a -> b) -> m a -> m b+f <$!> m = do+    a <- m+    return $! f a+{-# INLINE (<$!>) #-}++infixl 4 <$!>
benchmarks/Benchmarks.hs view
@@ -71,13 +71,13 @@         ]   where     decodePresidents :: BL.ByteString -> Either String (Vector President)-    decodePresidents = decode+    decodePresidents = decode False      decodePresidentsN :: BL.ByteString -> Either String (Header, Vector President)     decodePresidentsN = decodeByName      idDecode :: BL.ByteString -> Either String (Vector (Vector B.ByteString))-    idDecode = decode+    idDecode = decode False      idDecodeN :: BL.ByteString -> Either String (Header, Vector (BSHashMap B.ByteString))     idDecodeN = decodeByName
cassava.cabal view
@@ -1,5 +1,5 @@ Name:                cassava-Version:             0.1.0.1+Version:             0.2.0.0 Synopsis:            A CSV parsing and encoding library Description:   A CSV parsing and encoding library optimized for ease of use and high@@ -21,25 +21,33 @@  Library   Exposed-modules:     Data.Csv+                       Data.Csv.Incremental                        Data.Csv.Parser+                       Data.Csv.Streaming -  Other-modules:       Data.Csv.Conversion+  Other-modules:       Data.Csv.Compat.Monoid+                       Data.Csv.Conversion                        Data.Csv.Conversion.Internal                        Data.Csv.Encoding                        Data.Csv.Types+                       Data.Csv.Util -  Build-depends:       array,-                       attoparsec >= 0.10.2,+  Build-depends:       array < 0.5,+                       attoparsec >= 0.10.2 && < 0.11,                        base < 5,-                       blaze-builder,-                       bytestring,-                       containers,-                       text,-                       unordered-containers,-                       vector+                       blaze-builder < 0.4,+                       bytestring < 0.11,+                       containers < 0.6,+                       text < 0.12,+                       unordered-containers < 0.3,+                       vector < 0.11    ghc-options:         -Wall -O2 +  if impl(ghc >= 7.2.1)+    cpp-options:       -DGENERICS+    build-depends:     ghc-prim >= 0.2 && < 0.4+   Test-suite unit-tests   Type: exitcode-stdio-1.0   Main-is: UnitTests.hs
+ examples/IncrementalIndexedBasedDecode.hs view
@@ -0,0 +1,24 @@+{-# LANGUAGE BangPatterns, ScopedTypeVariables #-}++import Control.Monad+import qualified Data.ByteString as B+import Data.Csv.Incremental+import System.Exit+import System.IO++main :: IO ()+main = withFile "salaries.csv" ReadMode $ \ csvFile -> do+    let loop !_ (Fail _ errMsg) = putStrLn errMsg >> exitFailure+        loop acc (Partial k)    = loop acc =<< feed k+        loop acc (Some rs k)    = loop (acc + sumSalaries rs) =<< feed k+        loop acc (Done rs)      = putStrLn $ "Total salaries: " +++                                  show (sumSalaries rs + acc)++        feed k = do+            isEof <- hIsEOF csvFile+            if isEof+                then return $ k B.empty+                else k `fmap` B.hGetSome csvFile 4096+    loop 0 (decode False)+  where+    sumSalaries rs = sum [salary | Right (_ :: String, salary :: Int) <- rs]
examples/IndexBasedDecode.hs view
@@ -7,7 +7,7 @@ main :: IO () main = do     csvData <- BL.readFile "salaries.csv"-    case decode csvData of+    case decode False csvData of         Left err -> putStrLn err         Right v -> V.forM_ v $ \ (name, salary :: Int) ->             putStrLn $ name ++ " earns " ++ show salary ++ " dollars"
+ examples/IndexBasedGeneric.hs view
@@ -0,0 +1,23 @@+{-# LANGUAGE ScopedTypeVariables, DeriveGeneric #-}++import qualified Data.ByteString.Lazy as BL+import Data.Csv+import qualified Data.Vector as V+import GHC.Generics++data Person = Person String Int deriving Generic++instance FromRecord Person+instance ToRecord Person++persons :: [Person]+persons = [Person "John" 50000, Person "Jane" 60000]++main :: IO ()+main = do+    BL.writeFile "salaries.csv" $ encode (V.fromList persons)+    csvData <- BL.readFile "salaries.csv"+    case decode False csvData of+        Left err -> putStrLn err+        Right v -> V.forM_ v $ \ (Person name salary) ->+            putStrLn $ name ++ " earns " ++ show salary ++ " dollars"
+ examples/NamedBasedGeneric.hs view
@@ -0,0 +1,27 @@+{-# LANGUAGE OverloadedStrings, DeriveGeneric #-}++import qualified Data.ByteString.Lazy as BL+import Data.Csv+import qualified Data.Vector as V+import GHC.Generics++data Person = Person+    { name   :: String+    , salary :: Int+    }+    deriving Generic++instance FromNamedRecord Person+instance ToNamedRecord Person++persons :: [Person]+persons = [Person "John" 50000, Person "Jane" 60000]++main :: IO ()+main = do+    BL.writeFile "salaries.csv" $ encodeByName (V.fromList ["name", "salary"]) (V.fromList persons)+    csvData <- BL.readFile "salaries.csv"+    case decodeByName csvData of+        Left err -> putStrLn err+        Right (_, v) -> V.forM_ v $ \ p ->+            putStrLn $ name p ++ " earns " ++ show (salary p) ++ " dollars"
tests/UnitTests.hs view
@@ -12,7 +12,6 @@ import Data.Int import qualified Data.Text as T import qualified Data.Text.Lazy as LT-import Data.Vector ((!)) import qualified Data.Vector as V import Data.Word import Test.HUnit@@ -21,17 +20,18 @@ import Test.QuickCheck import Test.Framework.Providers.QuickCheck2 as TF -import Data.Csv+import Data.Csv hiding (record)+import qualified Data.Csv.Streaming as S  ------------------------------------------------------------------------ -- Parse tests  decodesAs :: BL.ByteString -> [[B.ByteString]] -> Assertion-decodesAs input expected = assertResult input expected $ decode input+decodesAs input expected = assertResult input expected $ decode False input  decodesWithAs :: DecodeOptions -> BL.ByteString -> [[B.ByteString]] -> Assertion decodesWithAs opts input expected =-    assertResult input expected $ decodeWith opts input+    assertResult input expected $ decodeWith opts False input  assertResult :: BL.ByteString -> [[B.ByteString]]              -> Either String (V.Vector (V.Vector B.ByteString)) -> Assertion@@ -65,19 +65,39 @@   where     expected' = V.fromList $ map HM.fromList expected -testRfc4180 :: Assertion-testRfc4180 = (BL8.pack $-               "#field1,field2,field3\n" ++-               "\"aaa\",\"bb\n" ++-               "b\",\"ccc\"\n" ++-               "\"a,a\",\"b\"\"bb\",\"ccc\"\n" ++-               "zzz,yyy,xxx\n")-              `decodesAs`-              [["#field1", "field2", "field3"],-               ["aaa", "bb\nb", "ccc"],-               ["a,a", "b\"bb", "ccc"],-               ["zzz", "yyy", "xxx"]]+recordsToList :: S.Records a -> Either String [a]+recordsToList (S.Nil (Just err) _)  = Left err+recordsToList (S.Nil Nothing _)     = Right []+recordsToList (S.Cons (Left err) _) = Left err+recordsToList (S.Cons (Right x) rs) = case recordsToList rs of+    l@(Left _) -> l+    (Right xs) -> Right (x : xs) +decodesStreamingAs :: BL.ByteString -> [[B.ByteString]] -> Assertion+decodesStreamingAs input expected =+    assertResult input expected $ fmap (V.fromList . map V.fromList) $+    recordsToList $ S.decode False input++decodesWithStreamingAs :: DecodeOptions -> BL.ByteString -> [[B.ByteString]]+                       -> Assertion+decodesWithStreamingAs opts input expected =+    assertResult input expected $ fmap (V.fromList . map V.fromList) $+    recordsToList $ S.decodeWith opts False input++namedDecodesStreamingAs :: BL.ByteString -> [B.ByteString]+                        -> [[(B.ByteString, B.ByteString)]] -> Assertion+namedDecodesStreamingAs input ehdr expected = case S.decodeByName input of+    Right (hdr, rs) -> case recordsToList rs of+        Right xs -> (V.fromList ehdr, expected') @=? (hdr, xs)+        Left err -> assertFailure $+                    "           input: " ++ show (BL8.unpack input) ++ "\n" +++                    "conversion error: " ++ err+    Left err -> assertFailure $+                "      input: " ++ show (BL8.unpack input) ++ "\n" +++                "parse error: " ++ err+  where+    expected' = map HM.fromList expected+ positionalTests :: [TF.Test] positionalTests =     [ testGroup "encode" $ map encodeTest@@ -95,24 +115,47 @@       [ testCase "tab-delim" $ encodesWithAs (defEnc { encDelimiter = 9 })         [["1", "2"]] "1\t2\r\n"       ]-    , testGroup "decode" $ map decodeTest-      [ ("simple",       "a,b,c\n",        [["a", "b", "c"]])-      , ("crlf",         "a,b\r\nc,d\r\n", [["a", "b"], ["c", "d"]])-      , ("noEol",        "a,b,c",          [["a", "b", "c"]])-      , ("blankLine",    "a,b,c\n\nd,e,f\n\n",-         [["a", "b", "c"], ["d", "e", "f"]])-      , ("leadingSpace", " a,  b,   c\n",  [[" a", "  b", "   c"]])-      ] ++ [testCase "rfc4180" testRfc4180]-    , testGroup "decodeWith"-      [ testCase "tab-delim" $ decodesWithAs (defDec { decDelimiter = 9 })-        "1\t2" [["1", "2"]]+    , testGroup "decode" $ map decodeTest decodeTests+    , testGroup "decodeWith" $ map decodeWithTest decodeWithTests+    , testGroup "streaming"+      [ testGroup "decode" $ map streamingDecodeTest decodeTests+      , testGroup "decodeWith" $ map streamingDecodeWithTest decodeWithTests       ]     ]   where+    rfc4180Input = BL8.pack $+                   "#field1,field2,field3\n" +++                   "\"aaa\",\"bb\n" +++                   "b\",\"ccc\"\n" +++                   "\"a,a\",\"b\"\"bb\",\"ccc\"\n" +++                   "zzz,yyy,xxx\n"+    rfc4180Output = [["#field1", "field2", "field3"],+                     ["aaa", "bb\nb", "ccc"],+                     ["a,a", "b\"bb", "ccc"],+                     ["zzz", "yyy", "xxx"]]+    decodeTests =+        [ ("simple",       "a,b,c\n",        [["a", "b", "c"]])+        , ("crlf",         "a,b\r\nc,d\r\n", [["a", "b"], ["c", "d"]])+        , ("noEol",        "a,b,c",          [["a", "b", "c"]])+        , ("blankLine",    "a,b,c\n\nd,e,f\n\n",+           [["a", "b", "c"], ["d", "e", "f"]])+        , ("leadingSpace", " a,  b,   c\n",  [[" a", "  b", "   c"]])+        , ("rfc4180", rfc4180Input, rfc4180Output)+        ]+    decodeWithTests =+        [ ("tab-delim", defDec { decDelimiter = 9 }, "1\t2", [["1", "2"]])+        ]+     encodeTest (name, input, expected) =         testCase name $ input `encodesAs` expected     decodeTest (name, input, expected) =         testCase name $ input `decodesAs` expected+    decodeWithTest (name, opts, input, expected) =+        testCase name $ decodesWithAs opts input expected+    streamingDecodeTest (name, input, expected) =+        testCase name $ input `decodesStreamingAs` expected+    streamingDecodeWithTest (name, opts, input, expected) =+        testCase name $ decodesWithStreamingAs opts input expected     defEnc = defaultEncodeOptions     defDec = defaultDecodeOptions @@ -126,19 +169,26 @@       , ("twoRecords", ["field"], [[("field", "abc")], [("field", "def")]],          "field\r\nabc\r\ndef\r\n")       ]-    , testGroup "decode" $ map decodeTest-      [("simple", "field\r\nabc\r\n", ["field"], [[("field", "abc")]])-      , ("twoFields", "field1,field2\r\nabc,def\r\n", ["field1", "field2"],-         [[("field1", "abc"), ("field2", "def")]])-      , ("twoRecords", "field\r\nabc\r\ndef\r\n", ["field"],-         [[("field", "abc")], [("field", "def")]])+    , testGroup "decode" $ map decodeTest decodeTests+    , testGroup "streaming"+      [ testGroup "decode" $ map streamingDecodeTest decodeTests       ]     ]   where+    decodeTests =+        [ ("simple", "field\r\nabc\r\n", ["field"], [[("field", "abc")]])+        , ("twoFields", "field1,field2\r\nabc,def\r\n", ["field1", "field2"],+           [[("field1", "abc"), ("field2", "def")]])+        , ("twoRecords", "field\r\nabc\r\ndef\r\n", ["field"],+           [[("field", "abc")], [("field", "def")]])+        ]+     encodeTest (name, hdr, input, expected) =         testCase name $ namedEncodesAs hdr input expected     decodeTest (name, input, hdr, expected) =         testCase name $ namedDecodesAs input hdr expected+    streamingDecodeTest (name, input, hdr, expected) =+        testCase name $ namedDecodesStreamingAs input hdr expected  ------------------------------------------------------------------------ -- Conversion tests@@ -159,17 +209,18 @@ -- empty line (which we will ignore.) We therefore encode at least two -- columns. roundTrip :: (Eq a, FromField a, ToField a) => a -> Bool-roundTrip x = case decode (encode (V.singleton (x, dummy))) of-    Right v | V.length v == 1 -> let (y, _ :: Char) = v ! 0 in x == y-    _  -> False-  where dummy = 'a'+roundTrip x = Right record == decode False (encode record) +  where record = V.singleton (x, dummy)+        dummy = 'a' +roundTripUnicode :: T.Text -> Assertion+roundTripUnicode x = Right record @=? decode False (encode record)+  where record = V.singleton (x, dummy)+        dummy = 'a'+ boundary :: forall a. (Bounded a, Eq a, FromField a, ToField a) => a -> Bool boundary _dummy = roundTrip (minBound :: a) && roundTrip (maxBound :: a) --- TODO: Right now we only encode ASCII properly. Should we support--- UTF-8? Arbitrary byte strings?- conversionTests :: [TF.Test] conversionTests =     [ testGroup "roundTrip"@@ -202,6 +253,13 @@       , testProperty "Word16" (boundary (undefined :: Word16))       , testProperty "Word32" (boundary (undefined :: Word32))       , testProperty "Word64" (boundary (undefined :: Word64))+      ]+    , testGroup "Unicode"+      [ testCase "Chinese" (roundTripUnicode "我能吞下玻璃而不伤身体。")+      , testCase "Icelandic" (roundTripUnicode+                              "Sævör grét áðan því úlpan var ónýt.")+      , testCase "Turkish" (roundTripUnicode+                            "Cam yiyebilirim, bana zararı dokunmaz.")       ]     ]