cassava 0.5.0.0 → 0.5.1.0
raw patch · 9 files changed
+552/−114 lines, 9 filesdep +natsdep +quickcheck-instancesdep +scientificdep ~attoparsecdep ~containersPVP: major bump suggested
API removals or changes: PVP suggests a major version bump
Dependencies added: nats, quickcheck-instances, scientific
Dependency ranges changed: attoparsec, containers
API changes (from Hackage documentation)
+ Data.Csv: class GFromNamedRecord f
+ Data.Csv: class GFromRecord f
+ Data.Csv: class GToNamedRecordHeader a
+ Data.Csv: class GToRecord a f
+ Data.Csv: data Options
+ Data.Csv: defaultOptions :: Options
+ Data.Csv: fieldLabelModifier :: Options -> String -> String
+ Data.Csv: genericHeaderOrder :: (Generic a, GToNamedRecordHeader (Rep a)) => Options -> a -> Header
+ Data.Csv: genericParseNamedRecord :: (Generic a, GFromNamedRecord (Rep a)) => Options -> NamedRecord -> Parser a
+ Data.Csv: genericParseRecord :: (Generic a, GFromRecord (Rep a)) => Options -> Record -> Parser a
+ Data.Csv: genericToNamedRecord :: (Generic a, GToRecord (Rep a) (ByteString, ByteString)) => Options -> a -> NamedRecord
+ Data.Csv: genericToRecord :: (Generic a, GToRecord (Rep a) Field) => Options -> a -> Record
- Data.Csv: class DefaultOrdered a where headerOrder = fromList . gtoNamedRecordHeader . from
+ Data.Csv: class DefaultOrdered a
- Data.Csv: class FromNamedRecord a where parseNamedRecord r = to <$> gparseNamedRecord r
+ Data.Csv: class FromNamedRecord a
- Data.Csv: class FromRecord a where parseRecord r = to <$> gparseRecord r
+ Data.Csv: class FromRecord a
- Data.Csv: class ToNamedRecord a where toNamedRecord = namedRecord . gtoRecord . from
+ Data.Csv: class ToNamedRecord a
- Data.Csv: class ToRecord a where toRecord = fromList . gtoRecord . from
+ Data.Csv: class ToRecord a
- Data.Csv: newtype Only a :: * -> *
+ Data.Csv: newtype Only a
Files
- CHANGES.md +10/−0
- Data/Csv.hs +106/−27
- Data/Csv/Builder.hs +1/−1
- Data/Csv/Conversion.hs +183/−45
- Data/Csv/Conversion/Internal.hs +7/−0
- Data/Csv/Incremental.hs +2/−0
- README.md +98/−0
- cassava.cabal +53/−10
- tests/UnitTests.hs +92/−31
CHANGES.md view
@@ -1,3 +1,13 @@+## Version 0.5.1.0++ * Add `FromField`/`ToField` instance for `Natural` (#141,#142)+ * Add `FromField`/`ToField` instances for `Scientific` (#143,#144)+ * Add support for modifying Generics-based instances (adding+ `Options`, `defaultOptions`, `fieldLabelModifier`,+ `genericParseRecord`, `genericToRecord`, `genericToNamedRecord`,+ `genericHeaderOrder`) (#139,#140)+ * Documentation improvements+ ## Version 0.5.0.0 ### Semantic changes
Data/Csv.hs view
@@ -1,6 +1,7 @@--- | This module implements encoding and decoding of CSV data. The--- implementation is RFC 4180 compliant, with the following--- extensions:+-- | This module implements encoding and decoding+-- of [comma-separated values (CSV)](https://en.wikipedia.org/wiki/Comma-separated_values)+-- data. The implementation is [RFC 4180](https://tools.ietf.org/html/rfc4180)+-- compliant, with the following extensions: -- -- * Empty lines are ignored. --@@ -93,6 +94,26 @@ -- $fieldconversion , FromField(..) , ToField(..)++ -- ** 'Generic' record conversion+ -- $genericconversion+ , genericParseRecord+ , genericToRecord+ , genericParseNamedRecord+ , genericToNamedRecord+ , genericHeaderOrder++ -- *** 'Generic' type conversion options+ , Options+ , defaultOptions+ , fieldLabelModifier++ -- *** 'Generic' type conversion class name+ -- $genericconversionclass+ , GFromRecord+ , GToRecord+ , GFromNamedRecord+ , GToNamedRecordHeader ) where import Prelude hiding (lookup)@@ -105,19 +126,23 @@ -- -- Encoding standard Haskell types: ----- > >>> encode [("John" :: Text, 27), ("Jane", 28)]--- > "John,27\r\nJane,28\r\n"+-- >>> :set -XOverloadedStrings+-- >>> import Data.Text (Text)+-- >>> encode [("John" :: Text, 27 :: Int), ("Jane", 28)]+-- "John,27\r\nJane,28\r\n" ----- Since string literals are overloaded we have to supply a type+-- Since we enabled the [-XOverloadedStrings extension](https://downloads.haskell.org/~ghc/8.2.1/docs/html/users_guide/glasgow_exts.html#overloaded-string-literals),+-- string literals are polymorphic and we have to supply a type -- signature as the compiler couldn't deduce which string type (i.e.--- 'String' or 'Text') we want to use. In most cases type inference--- will infer the type from the context and you can omit type--- signatures.+-- 'String', 'Data.Text.Short.ShortText', or 'Data.Text.Text') we want to use. In most cases+-- type inference will infer the type from the context and you can+-- omit type signatures. -- -- Decoding standard Haskell types: ----- > >>> decode NoHeader "John,27\r\nJane,28\r\n" :: Either String (Vector (Text, Int))--- > Right [("John",27),("Jane",28)]+-- >>> import Data.Vector (Vector)+-- >>> decode NoHeader "John,27\r\nJane,28\r\n" :: Either String (Vector (Text, Int))+-- Right [("John",27),("Jane",28)] -- -- We pass 'NoHeader' as the first argument to indicate that the CSV -- input data isn't preceded by a header.@@ -137,50 +162,59 @@ -- $example-indexed-instance ----- Derived:+-- "GHC.Generics"-derived: -- -- > {-# LANGUAGE DeriveGeneric #-} -- >+-- > import Data.Text (Text)+-- > import GHC.Generics (Generic)+-- > -- > data Person = Person { name :: !Text , salary :: !Int }--- > deriving Generic+-- > deriving (Generic, Show) -- > -- > instance FromRecord Person -- > instance ToRecord Person -- -- Manually defined: --+-- > import Control.Monad (mzero)+-- > -- > data Person = Person { name :: !Text , salary :: !Int }+-- > deriving (Show) -- > -- > instance FromRecord Person where -- > parseRecord v -- > | length v == 2 = Person <$> v .! 0 <*> v .! 1 -- > | otherwise = mzero -- > instance ToRecord Person where--- > toRecord (Person name age) = record [--- > toField name, toField age]+-- > toRecord (Person name' age') = record [+-- > toField name', toField age'] -- -- We can now use e.g. 'encode' and 'decode' to encode and decode our -- data type. -- -- Encoding: ----- > >>> encode [Person ("John" :: Text) 27]--- > "John,27\r\n"+-- >>> encode [Person ("John" :: Text) 27]+-- "John,27\r\n" -- -- Decoding: ----- > >>> decode NoHeader "John,27\r\n" :: Either String (Vector Person)--- > Right [Person {name = "John", salary = 27}]+-- >>> decode NoHeader "John,27\r\n" :: Either String (Vector Person)+-- Right [Person {name = "John", salary = 27}] -- -- $example-named-instance ----- Derived:+-- "GHC.Generics"-derived: -- -- > {-# LANGUAGE DeriveGeneric #-} -- >+-- > import Data.Text (Text)+-- > import GHC.Generics (Generic)+-- > -- > data Person = Person { name :: !Text , salary :: !Int }--- > deriving Generic+-- > deriving (Generic, Show) -- > -- > instance FromNamedRecord Person -- > instance ToNamedRecord Person@@ -189,6 +223,7 @@ -- Manually defined: -- -- > data Person = Person { name :: !Text , salary :: !Int }+-- > deriving (Show) -- > -- > instance FromNamedRecord Person where -- > parseNamedRecord m = Person <$> m .: "name" <*> m .: "salary"@@ -204,13 +239,13 @@ -- -- Encoding: ----- > >>> encodeDefaultOrderedByName [Person ("John" :: Text) 27]--- > "name,salary\r\nJohn,27\r\n"+-- >>> encodeDefaultOrderedByName [Person ("John" :: Text) 27]+-- "name,salary\r\nJohn,27\r\n" -- -- Decoding: ----- > >>> decodeByName "name,salary\r\nJohn,27\r\n" :: Either String (Header, Vector Person)--- > Right (["name","salary"],[Person {name = "John", salary = 27}])+-- >>> decodeByName "name,salary\r\nJohn,27\r\n" :: Either String (Header, Vector Person)+-- Right (["name","salary"],[Person {name = "John", salary = 27}]) -- -- $generic-processing@@ -221,8 +256,9 @@ -- parse a CSV file to a generic representation, just convert each -- record to a @'Vector' 'ByteString'@ value, like so: ----- > >>> decode NoHeader "John,27\r\nJane,28\r\n" :: Either String (Vector (Vector ByteString))--- > Right [["John","27"],["Jane","28"]]+-- >>> import Data.ByteString (ByteString)+-- >>> decode NoHeader "John,27\r\nJane,28\r\n" :: Either String (Vector (Vector ByteString))+-- Right [["John","27"],["Jane","28"]] -- -- As the example output above shows, all the fields are returned as -- uninterpreted 'ByteString' values.@@ -325,3 +361,46 @@ -- 'Field's and values you care about (e.g. 'Int's). Most of the time -- you don't need to write your own instances as the standard ones -- cover most use cases.++-- $genericconversion+--+-- There may be times that you do not want to manually write out class+-- instances for record conversion, but you can't rely upon the+-- default instances (e.g. you can't create field names that match the+-- actual column names in expected data).+--+-- For example, consider you have a type @MyType@ where you have+-- prefixed certain columns with an underscore, but in the actual data+-- they're not. You can then write:+--+-- > myOptions :: Options+-- > myOptions = defaultOptions { fieldLabelmodifier = rmUnderscore }+-- > where+-- > rmUnderscore ('_':str) = str+-- > rmUnderscore str = str+-- >+-- > instance ToNamedRecord MyType where+-- > toNamedRecord = genericToNamedRecord myOptions+-- >+-- > instance FromNamedRecord MyType where+-- > parseNamedRecord = genericParseNamedRecord myOptions+-- >+-- > instance DefaultOrdered MyType where+-- > headerOrder = genericHeaderOrder myOptions++-- $genericconversionclass+--+-- __NOTE__: Only the class /names/ are exposed in order to make it possible to write type signatures referring to these classes++-- $setup+-- >>> :set -XOverloadedStrings -XDeriveGeneric+-- >>> import Data.Text (Text)+-- >>> import Data.Vector (Vector)+-- >>> import GHC.Generics (Generic)+-- >>>+-- >>> data Person = Person { name :: !Text, salary :: !Int } deriving (Generic, Show)+-- >>> instance FromRecord Person+-- >>> instance ToRecord Person+-- >>> instance FromNamedRecord Person+-- >>> instance ToNamedRecord Person+-- >>> instance DefaultOrdered Person
Data/Csv/Builder.hs view
@@ -1,7 +1,7 @@ {-# LANGUAGE ScopedTypeVariables #-} -- | Low-level bytestring builders. Most users want to use the more--- type-safe 'Data.Csv.Incremental' module instead.+-- type-safe "Data.Csv.Incremental" module instead. module Data.Csv.Builder ( -- * Encoding single records and headers
Data/Csv/Conversion.hs view
@@ -38,6 +38,24 @@ , ToRecord(..) , ToField(..) + -- ** Generic type conversion+ , genericParseRecord+ , genericToRecord+ , genericParseNamedRecord+ , genericToNamedRecord+ , genericHeaderOrder++ -- *** Generic type conversion options+ , Options+ , defaultOptions+ , fieldLabelModifier++ -- *** Generic type conversion class names+ , GFromRecord+ , GToRecord+ , GFromNamedRecord+ , GToNamedRecordHeader+ -- * Parser , Parser , runParser@@ -66,11 +84,13 @@ #if MIN_VERSION_bytestring(0,10,4) import qualified Data.ByteString.Short as SBS #endif+import Data.List (intercalate) import Data.Hashable (Hashable) import qualified Data.HashMap.Lazy as HM import Data.Int (Int8, Int16, Int32, Int64) import qualified Data.IntMap as IM import qualified Data.Map as M+import Data.Scientific (Scientific) import Data.Semigroup (Semigroup, (<>)) import qualified Data.Text as T import qualified Data.Text.Encoding as T@@ -86,6 +106,7 @@ import Data.Word (Word8, Word16, Word32, Word64) import GHC.Float (double2Float) import GHC.Generics+import Numeric.Natural import Prelude hiding (lookup, takeWhile) import Data.Csv.Conversion.Internal@@ -119,6 +140,41 @@ ------------------------------------------------------------------------ -- Index-based conversion +-- | Options to customise how to generically encode\/decode your+-- datatype to\/from CSV.+--+-- @since 0.5.1.0+newtype Options = Options+ { fieldLabelModifier :: String -> String+ -- ^ How to convert Haskell field labels to CSV fields.+ --+ -- @since 0.5.1.0+ }++instance Show Options where+ show (Options fld) =+ "Options {"+ ++ intercalate ","+ [ "fieldLabelModifier =~ " ++ show sampleField ++ " -> " ++ show (fld sampleField)+ ]+ ++ "}"+ where+ sampleField = "_column_A"++-- | Default conversion options.+--+-- @+-- Options+-- { 'fieldLabelModifier' = id+-- }+-- @+--+-- @since 0.5.1.0+defaultOptions :: Options+defaultOptions = Options+ { fieldLabelModifier = id+ }+ -- | A type that can be converted from a single CSV record, with the -- possibility of failure. --@@ -145,8 +201,16 @@ parseRecord :: Record -> Parser a default parseRecord :: (Generic a, GFromRecord (Rep a)) => Record -> Parser a- parseRecord r = to <$> gparseRecord r+ parseRecord = genericParseRecord defaultOptions +-- | A configurable CSV record parser. This function applied to+-- 'defaultOptions' is used as the default for 'parseRecord' when the+-- type is an instance of 'Generic'.+--+-- @since 0.5.1.0+genericParseRecord :: (Generic a, GFromRecord (Rep a)) => Options -> Record -> Parser a+genericParseRecord opts r = to <$> gparseRecord opts r+ -- | A type that can be converted to a single CSV record. -- -- An example type and instance:@@ -166,8 +230,16 @@ toRecord :: a -> Record default toRecord :: (Generic a, GToRecord (Rep a) Field) => a -> Record- toRecord = V.fromList . gtoRecord . from+ toRecord = genericToRecord defaultOptions +-- | A configurable CSV record creator. This function applied to+-- 'defaultOptions' is used as the default for 'toRecord' when the+-- type is an instance of 'Generic'.+--+-- @since 0.5.1.0+genericToRecord :: (Generic a, GToRecord (Rep a) Field) => Options -> a -> Record+genericToRecord opts = V.fromList . gtoRecord opts . from+ instance FromField a => FromRecord (Only a) where parseRecord v | n == 1 = Only <$> unsafeIndex v 0@@ -563,8 +635,16 @@ parseNamedRecord :: NamedRecord -> Parser a default parseNamedRecord :: (Generic a, GFromNamedRecord (Rep a)) => NamedRecord -> Parser a- parseNamedRecord r = to <$> gparseNamedRecord r+ parseNamedRecord = genericParseNamedRecord defaultOptions +-- | A configurable CSV named record parser. This function applied to+-- 'defaultOptions' is used as the default for 'parseNamedRecord'+-- when the type is an instance of 'Generic'.+--+-- @since 0.5.1.0+genericParseNamedRecord :: (Generic a, GFromNamedRecord (Rep a)) => Options -> NamedRecord -> Parser a+genericParseNamedRecord opts r = to <$> gparseNamedRecord opts r+ -- | A type that can be converted to a single CSV record. -- -- An example type and instance:@@ -581,8 +661,17 @@ default toNamedRecord :: (Generic a, GToRecord (Rep a) (B.ByteString, B.ByteString)) => a -> NamedRecord- toNamedRecord = namedRecord . gtoRecord . from+ toNamedRecord = genericToNamedRecord defaultOptions +-- | A configurable CSV named record creator. This function applied+-- to 'defaultOptions' is used as the default for 'toNamedRecord' when+-- the type is an instance of 'Generic'.+--+-- @since 0.5.1.0+genericToNamedRecord :: (Generic a, GToRecord (Rep a) (B.ByteString, B.ByteString))+ => Options -> a -> NamedRecord+genericToNamedRecord opts = namedRecord . gtoRecord opts . from+ -- | A type that has a default field order when converted to CSV. This -- class lets you specify how to get the headers to use for a record -- type that's an instance of 'ToNamedRecord'.@@ -617,8 +706,17 @@ default headerOrder :: (Generic a, GToNamedRecordHeader (Rep a)) => a -> Header- headerOrder = V.fromList. gtoNamedRecordHeader . from+ headerOrder = genericHeaderOrder defaultOptions +-- | A configurable CSV header record generator. This function+-- applied to 'defaultOptions' is used as the default for+-- 'headerOrder' when the type is an instance of 'Generic'.+--+-- @since 0.5.1.0+genericHeaderOrder :: (Generic a, GToNamedRecordHeader (Rep a))+ => Options -> a -> Header+genericHeaderOrder opts = V.fromList. gtoNamedRecordHeader opts . from+ instance (FromField a, FromField b, Ord a) => FromNamedRecord (M.Map a b) where parseNamedRecord m = M.fromList <$> (traverse parseBoth $ HM.toList m)@@ -715,6 +813,22 @@ toField = toField . T.encodeUtf8 . T.singleton {-# INLINE toField #-} +-- | Accepts the same syntax as 'rational'. Ignores whitespace.+--+-- @since 0.5.1.0+instance FromField Scientific where+ parseField s = case parseOnly (ws *> A8.scientific <* ws) s of+ Left err -> typeError "Scientific" s (Just err)+ Right n -> pure n+ {-# INLINE parseField #-}++-- | Uses decimal notation or scientific notation, depending on the number.+--+-- @since 0.5.1.0+instance ToField Scientific where+ toField = scientific+ {-# INLINE toField #-}+ -- | Accepts same syntax as 'rational'. Ignores whitespace. instance FromField Double where parseField = parseDouble@@ -814,6 +928,20 @@ {-# INLINE toField #-} -- | Accepts an unsigned decimal number. Ignores whitespace.+--+-- @since 0.5.1.0+instance FromField Natural where+ parseField = parseUnsigned "Natural"+ {-# INLINE parseField #-}++-- | Uses decimal encoding.+--+-- @since 0.5.1.0+instance ToField Natural where+ toField = decimal+ {-# INLINE toField #-}++-- | Accepts an unsigned decimal number. Ignores whitespace. instance FromField Word8 where parseField = parseUnsigned "Word8" {-# INLINE parseField #-}@@ -880,10 +1008,16 @@ #endif #if MIN_VERSION_text_short(0,1,0)+-- | Assumes UTF-8 encoding. Fails on invalid byte sequences.+--+-- @since 0.5.0.0 instance FromField T.S.ShortText where parseField = maybe (fail "Invalid UTF-8 stream") pure . T.S.fromByteString {-# INLINE parseField #-} +-- | Uses UTF-8 encoding.+--+-- @since 0.5.0.0 instance ToField T.S.ShortText where toField = T.S.toByteString {-# INLINE toField #-}@@ -1057,6 +1191,7 @@ fail = Fail.fail {-# INLINE fail #-} +-- | @since 0.5.0.0 instance Fail.MonadFail Parser where fail msg = Parser $ \kf _ks -> kf msg {-# INLINE fail #-}@@ -1085,6 +1220,7 @@ in unParser a kf' ks {-# INLINE mplus #-} +-- | @since 0.5.0.0 instance Semigroup (Parser a) where (<>) = mplus {-# INLINE (<>) #-}@@ -1119,116 +1255,118 @@ -- Generics class GFromRecord f where- gparseRecord :: Record -> Parser (f p)+ gparseRecord :: Options -> Record -> Parser (f p) instance GFromRecordSum f Record => GFromRecord (M1 i n f) where- gparseRecord v =- case (IM.lookup n gparseRecordSum) of+ gparseRecord opts v =+ case IM.lookup n (gparseRecordSum opts) of Nothing -> lengthMismatch n v Just p -> M1 <$> p v where n = V.length v class GFromNamedRecord f where- gparseNamedRecord :: NamedRecord -> Parser (f p)+ gparseNamedRecord :: Options -> 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)+ gparseNamedRecord opts v =+ foldr (\f p -> p <|> M1 <$> f v) empty (IM.elems (gparseRecordSum opts)) class GFromRecordSum f r where- gparseRecordSum :: IM.IntMap (r -> Parser (f p))+ gparseRecordSum :: Options -> IM.IntMap (r -> Parser (f p)) instance (GFromRecordSum a r, GFromRecordSum b r) => GFromRecordSum (a :+: b) r where- gparseRecordSum =+ gparseRecordSum opts = IM.unionWith (\a b r -> a r <|> b r)- (fmap (L1 <$>) <$> gparseRecordSum)- (fmap (R1 <$>) <$> gparseRecordSum)+ (fmap (L1 <$>) <$> gparseRecordSum opts)+ (fmap (R1 <$>) <$> gparseRecordSum opts) instance GFromRecordProd f r => GFromRecordSum (M1 i n f) r where- gparseRecordSum = IM.singleton n (fmap (M1 <$>) f)+ gparseRecordSum opts = IM.singleton n (fmap (M1 <$>) f) where- (n, f) = gparseRecordProd 0+ (n, f) = gparseRecordProd opts 0 class GFromRecordProd f r where- gparseRecordProd :: Int -> (Int, r -> Parser (f p))+ gparseRecordProd :: Options -> Int -> (Int, r -> Parser (f p)) instance GFromRecordProd U1 r where- gparseRecordProd n = (n, const (pure U1))+ gparseRecordProd _ n = (n, const (pure U1)) instance (GFromRecordProd a r, GFromRecordProd b r) => GFromRecordProd (a :*: b) r where- gparseRecordProd n0 = (n2, f)+ gparseRecordProd opts n0 = (n2, f) where f r = (:*:) <$> fa r <*> fb r- (n1, fa) = gparseRecordProd n0- (n2, fb) = gparseRecordProd n1+ (n1, fa) = gparseRecordProd opts n0+ (n2, fb) = gparseRecordProd opts n1 instance GFromRecordProd f Record => GFromRecordProd (M1 i n f) Record where- gparseRecordProd n = fmap (M1 <$>) <$> gparseRecordProd n+ gparseRecordProd opts n = fmap (M1 <$>) <$> gparseRecordProd opts n instance FromField a => GFromRecordProd (K1 i a) Record where- gparseRecordProd n = (n + 1, \v -> K1 <$> parseField (V.unsafeIndex v n))+ 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)+ gparseRecordProd opts n = (n + 1, \v -> (M1 . K1) <$> v .: name) where- name = T.encodeUtf8 (T.pack (selName (Proxy :: Proxy s f a)))+ name = T.encodeUtf8 (T.pack (fieldLabelModifier opts (selName (Proxy :: Proxy s f a)))) class GToRecord a f where- gtoRecord :: a p -> [f]+ gtoRecord :: Options -> a p -> [f] instance GToRecord U1 f where- gtoRecord U1 = []+ gtoRecord _ U1 = [] instance (GToRecord a f, GToRecord b f) => GToRecord (a :*: b) f where- gtoRecord (a :*: b) = gtoRecord a ++ gtoRecord b+ gtoRecord opts (a :*: b) = gtoRecord opts a ++ gtoRecord opts b instance (GToRecord a f, GToRecord b f) => GToRecord (a :+: b) f where- gtoRecord (L1 a) = gtoRecord a- gtoRecord (R1 b) = gtoRecord b+ gtoRecord opts (L1 a) = gtoRecord opts a+ gtoRecord opts (R1 b) = gtoRecord opts b instance GToRecord a f => GToRecord (M1 D c a) f where- gtoRecord (M1 a) = gtoRecord a+ gtoRecord opts (M1 a) = gtoRecord opts a instance GToRecord a f => GToRecord (M1 C c a) f where- gtoRecord (M1 a) = gtoRecord a+ gtoRecord opts (M1 a) = gtoRecord opts a instance GToRecord a Field => GToRecord (M1 S c a) Field where- gtoRecord (M1 a) = gtoRecord a+ gtoRecord opts (M1 a) = gtoRecord opts a instance ToField a => GToRecord (K1 i a) Field where- gtoRecord (K1 a) = [toField a]+ 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]+ gtoRecord opts m@(M1 (K1 a)) = [name .= toField a]+ where+ name = T.encodeUtf8 (T.pack (fieldLabelModifier opts (selName m))) -- We statically fail on sum types and product types without selectors -- (field names). class GToNamedRecordHeader a where- gtoNamedRecordHeader :: a p -> [Name]+ gtoNamedRecordHeader :: Options -> a p -> [Name] instance GToNamedRecordHeader U1 where- gtoNamedRecordHeader _ = []+ gtoNamedRecordHeader _ _ = [] instance (GToNamedRecordHeader a, GToNamedRecordHeader b) => GToNamedRecordHeader (a :*: b) where- gtoNamedRecordHeader _ = gtoNamedRecordHeader (undefined :: a p) ++- gtoNamedRecordHeader (undefined :: b p)+ gtoNamedRecordHeader opts _ = gtoNamedRecordHeader opts (undefined :: a p) +++ gtoNamedRecordHeader opts (undefined :: b p) instance GToNamedRecordHeader a => GToNamedRecordHeader (M1 D c a) where- gtoNamedRecordHeader _ = gtoNamedRecordHeader (undefined :: a p)+ gtoNamedRecordHeader opts _ = gtoNamedRecordHeader opts (undefined :: a p) instance GToNamedRecordHeader a => GToNamedRecordHeader (M1 C c a) where- gtoNamedRecordHeader _ = gtoNamedRecordHeader (undefined :: a p)+ gtoNamedRecordHeader opts _ = gtoNamedRecordHeader opts (undefined :: a p) -- | Instance to ensure that you cannot derive DefaultOrdered for -- constructors without selectors.@@ -1239,12 +1377,12 @@ instance DefaultOrdered (M1 S NoSelector a ()) => GToNamedRecordHeader (M1 S NoSelector a) #endif where- gtoNamedRecordHeader _ =+ gtoNamedRecordHeader _ _ = error "You cannot derive DefaultOrdered for constructors without selectors." instance Selector s => GToNamedRecordHeader (M1 S s a) where- gtoNamedRecordHeader m+ gtoNamedRecordHeader opts m | null name = error "Cannot derive DefaultOrdered for constructors without selectors"- | otherwise = [B8.pack (selName m)]+ | otherwise = [B8.pack (fieldLabelModifier opts (selName m))] where name = selName m
Data/Csv/Conversion/Internal.hs view
@@ -1,17 +1,20 @@ module Data.Csv.Conversion.Internal ( decimal+ , scientific , realFloat ) where import Data.ByteString.Builder (Builder, toLazyByteString, word8, char8, string8, byteString) import qualified Data.ByteString.Builder.Prim as BP+import Data.ByteString.Builder.Scientific (scientificBuilder) import Data.Array.Base (unsafeAt) import Data.Array.IArray import qualified Data.ByteString as B import Data.Char (ord) import Data.Int import Data.Monoid+import Data.Scientific (Scientific) import Data.Word import Data.Csv.Util (toStrict)@@ -98,6 +101,10 @@ ------------------------------------------------------------------------ -- Floating point numbers++scientific :: Scientific -> B.ByteString+scientific = toStrict . toLazyByteString . scientificBuilder+{-# INLINE scientific #-} realFloat :: RealFloat a => a -> B.ByteString {-# SPECIALIZE realFloat :: Float -> B.ByteString #-}
Data/Csv/Incremental.hs view
@@ -344,6 +344,7 @@ runBuilder :: Quoting -> Word8 -> Bool -> Builder.Builder } +-- | @since 0.5.0.0 instance Semigroup (Builder a) where Builder f <> Builder g = Builder $ \ qtng delim useCrlf ->@@ -420,6 +421,7 @@ runNamedBuilder :: Header -> Quoting -> Word8 -> Bool -> Builder.Builder } +-- | @since 0.5.0.0 instance Semigroup (NamedBuilder a) where NamedBuilder f <> NamedBuilder g = NamedBuilder $ \ hdr qtng delim useCrlf ->
+ README.md view
@@ -0,0 +1,98 @@+# `cassava`: A CSV parsing and encoding library [](https://hackage.haskell.org/package/cassava) [](https://travis-ci.org/hvr/cassava)++**Please refer to the [package description](https://hackage.haskell.org/package/cassava#description) for an overview of `cassava`.**++## Usage example++Here's the two second crash course in using the library. Given a CSV file with this content:++```csv+John Doe,50000+Jane Doe,60000+```++here's how you'd process it record-by-record:++```haskell+{-# LANGUAGE ScopedTypeVariables #-}++import qualified Data.ByteString.Lazy as BL+import Data.Csv+import qualified Data.Vector as V++main :: IO ()+main = do+ csvData <- BL.readFile "salaries.csv"+ case decode NoHeader csvData of+ Left err -> putStrLn err+ Right v -> V.forM_ v $ \ (name, salary :: Int) ->+ putStrLn $ name ++ " earns " ++ show salary ++ " dollars"+```++If you want to parse a file that includes a header, like this one++```csv+name,salary+John Doe,50000+Jane Doe,60000+```++use [`decodeByName`](https://hackage.haskell.org/package/cassava/docs/Data-Csv.html#v:decodeByName):++```haskell+{-# LANGUAGE OverloadedStrings #-}++import Control.Applicative+import qualified Data.ByteString.Lazy as BL+import Data.Csv+import qualified Data.Vector as V++data Person = Person+ { name :: !String+ , salary :: !Int+ }++instance FromNamedRecord Person where+ parseNamedRecord r = Person <$> r .: "name" <*> r .: "salary"++main :: IO ()+main = do+ 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"+```++You can find more code examples in the [`examples/` folder](https://github.com/hvr/cassava/tree/master/examples) as well as smaller usage examples in the [`Data.Csv` module documentation](https://hackage.haskell.org/package/cassava/docs/Data-Csv.html).++## Project Goals for `cassava`++There's no end to what people consider CSV data. Most programs don't+follow [RFC4180](https://tools.ietf.org/html/rfc4180) so one has to+make a judgment call which contributions to accept. Consequently, not+everything gets accepted, because then we'd end up with a (slow)+general purpose parsing library. There are plenty of those. The goal+is to roughly accept what the Python+[`csv`](https://docs.python.org/3/library/csv.html) module accepts.++The Python `csv` module (which is implemented in C) is also considered+the base-line for performance. Adding options (e.g. the above+mentioned parsing "flexibility") will have to be a trade off against+performance. There's been complaints about performance in the past,+therefore, if in doubt performance wins over features.++Last but not least, it's important to keep the dependency footprint+light, as each additional dependency incurs costs and risks in terms+of additional maintenance overhead and loss of flexibility. So adding+a new package dependency should only be done if that dependency is+known to be a reliable package and there's a clear benefit which+outweights the cost.++## Further reading++The primary API documentation for `cassava` is its Haddock documentation which can be found at http://hackage.haskell.org/package/cassava/docs/Data-Csv.html ++Below are listed additional recommended third-party blogposts and tutorials++ - [CSV encoding and decoding in Haskell with Cassava](https://www.stackbuilders.com/tutorials/haskell/csv-encoding-decoding/)
cassava.cabal view
@@ -1,9 +1,33 @@+cabal-version: 1.12 Name: cassava-Version: 0.5.0.0+Version: 0.5.1.0 Synopsis: A CSV parsing and encoding library-Description:- A CSV parsing and encoding library optimized for ease of use and high- performance.+Description: {++@cassava@ is a library for parsing and encoding [RFC 4180](https://tools.ietf.org/html/rfc4180)+compliant [comma-separated values (CSV)](https://en.wikipedia.org/wiki/Comma-separated_values) data,+which is a textual line-oriented format commonly used for exchanging tabular data.+.+@cassava@'s API includes support for+.+- Index-based record-conversion+- Name-based record-conversion+- Typeclass directed conversion of fields and records+- Built-in field-conversion instances for standard types+- Customizable record-conversion instance derivation via GHC generics+- Low-level [bytestring](https://hackage.haskell.org/package/bytestring) builders (see "Data.Csv.Builder")+- Incremental decoding and encoding API (see "Data.Csv.Incremental")+- Streaming API for constant-space decoding (see "Data.Csv.Streaming")+.+Moreover, this library is designed to be easy to use; for instance, here's a+very simple example of encoding CSV data:+.+>>> Data.Csv.encode [("John",27),("Jane",28)]+"John,27\r\nJane,28\r\n"+.+Please refer to the documentation in "Data.Csv" and the included [README](#readme) for more usage examples.++} Homepage: https://github.com/hvr/cassava License: BSD3 License-file: LICENSE@@ -15,16 +39,20 @@ Maintainer: hvr@gnu.org Category: Text, Web, CSV Build-type: Simple-Cabal-version: >=1.10 Extra-source-files: examples/*.hs,- CHANGES.md+ CHANGES.md,+ README.md Tested-with: GHC ==8.2.1, GHC ==8.0.2, GHC ==8.0.1, GHC ==7.10.3, GHC ==7.8.4, GHC ==7.6.3, GHC ==7.4.2 ---------------------------------------------------------------------------- -flag pre-bytestring-0-10-4- description: bytestring < 0.10.4+source-repository head+ type: git+ location: https://github.com/hvr/cassava.git +flag bytestring--LT-0_10_4+ description: [bytestring](https://hackage.haskell.org/haskell/package/bytestring) < 0.10.4+ Library default-language: Haskell2010 other-extensions:@@ -65,18 +93,19 @@ Build-depends: array >= 0.4 && < 0.6,- attoparsec >= 0.10.2 && < 0.14,+ attoparsec >= 0.11.3.0 && < 0.14, base >= 4.5 && < 5, bytestring >= 0.9.2 && < 0.11, containers >= 0.4.2 && < 0.6, deepseq >= 1.1 && < 1.5, hashable < 1.3,+ scientific >= 0.3.4.7 && < 0.4, text < 1.3, unordered-containers < 0.3, vector >= 0.8 && < 0.13, Only >= 0.1 && < 0.1.1 - if flag(pre-bytestring-0-10-4)+ if flag(bytestring--LT-0_10_4) build-depends: bytestring < 0.10.4 , bytestring-builder >= 0.10.8 && < 0.11 else@@ -87,6 +116,10 @@ if impl(ghc < 7.6) build-depends: ghc-prim == 0.2.* + -- For Numeric.Natural+ if impl(ghc < 7.10)+ build-depends: nats >= 1 && < 1.2+ -- https://ghc.haskell.org/trac/ghc/wiki/Migration/8.0#Recommendationsforforward-compatibility if impl(ghc >= 8.0) ghc-options: -Wcompat -Wnoncanonical-monad-instances -Wnoncanonical-monadfail-instances@@ -112,17 +145,27 @@ , bytestring , cassava , hashable+ , scientific , text , unordered-containers , vector -- extra dependencies not already used by lib:cassava build-depends: HUnit < 1.7 , QuickCheck == 2.10.*+ , quickcheck-instances >= 0.3.12 && < 0.4 , test-framework == 0.8.* , test-framework-hunit == 0.3.* , test-framework-quickcheck2 == 0.3.* hs-source-dirs: tests++ -- GHC.Generics lived in `ghc-prim` for GHC 7.2 & GHC 7.4 only+ if impl(ghc < 7.6)+ build-depends: ghc-prim == 0.2.*++ -- For Numeric.Natural+ if impl(ghc < 7.10)+ build-depends: nats >= 1 && < 1.2 -- https://ghc.haskell.org/trac/ghc/wiki/Migration/8.0#Recommendationsforforward-compatibility if impl(ghc >= 8.0)
tests/UnitTests.hs view
@@ -1,6 +1,11 @@-{-# LANGUAGE OverloadedStrings, ScopedTypeVariables #-}-{-# OPTIONS_GHC -fno-warn-orphans #-}+{-# LANGUAGE CPP, DeriveGeneric, OverloadedStrings, ScopedTypeVariables #-} +#if __GLASGOW_HASKELL__ >= 801+{-# OPTIONS_GHC -Wno-orphans -Wno-unused-top-binds #-}+#else+{-# OPTIONS_GHC -fno-warn-orphans -fno-warn-unused-binds #-}+#endif+ module Main ( main ) where@@ -10,20 +15,28 @@ import qualified Data.ByteString.Lazy.Char8 as BL8 import qualified Data.HashMap.Strict as HM import Data.Int+import Data.Scientific (Scientific) import qualified Data.Text as T import qualified Data.Text.Lazy as LT import qualified Data.Vector as V import qualified Data.Foldable as F import Data.Word+import Numeric.Natural+import GHC.Generics (Generic) import Test.HUnit import Test.Framework as TF import Test.Framework.Providers.HUnit as TF import Test.QuickCheck+import Test.QuickCheck.Instances () import Test.Framework.Providers.QuickCheck2 as TF import Data.Csv hiding (record) import qualified Data.Csv.Streaming as S +#if !MIN_VERSION_base(4,8,0)+import Control.Applicative ((<$>), (<*>))+#endif+ ------------------------------------------------------------------------ -- Parse tests @@ -240,18 +253,6 @@ ------------------------------------------------------------------------ -- Conversion tests -instance Arbitrary B.ByteString where- arbitrary = B.pack `fmap` arbitrary--instance Arbitrary BL.ByteString where- arbitrary = BL.fromChunks `fmap` arbitrary--instance Arbitrary T.Text where- arbitrary = T.pack `fmap` arbitrary--instance Arbitrary LT.Text where- arbitrary = LT.fromChunks `fmap` arbitrary- -- A single column with an empty string is indistinguishable from an -- empty line (which we will ignore.) We therefore encode at least two -- columns.@@ -291,11 +292,13 @@ , testProperty "Int16" (roundTrip :: Int16 -> Bool) , testProperty "Int32" (roundTrip :: Int32 -> Bool) , testProperty "Int64" (roundTrip :: Int64 -> Bool)+ , testProperty "Natural" (roundTrip :: Natural -> Bool) , testProperty "Word" (roundTrip :: Word -> Bool) , testProperty "Word8" (roundTrip :: Word8 -> Bool) , testProperty "Word16" (roundTrip :: Word16 -> Bool) , testProperty "Word32" (roundTrip :: Word32 -> Bool) , testProperty "Word64" (roundTrip :: Word64 -> Bool)+ , testProperty "Scientific" (roundTrip :: Scientific -> Bool) , testProperty "lazy ByteString" (roundTrip :: BL.ByteString -> Bool) , testProperty "Text" (roundTrip :: T.Text -> Bool)@@ -321,25 +324,35 @@ "Cam yiyebilirim, bana zararı dokunmaz.") ] , testGroup "Partial Decodes"- [ testCase "Int" (partialDecode- (parseField "12.7" :: Parser Int))- , testCase "Word" (partialDecode- (parseField "12.7" :: Parser Word))- , testCase "Double" (partialDecode- (parseField "1.0+" :: Parser Double))- , testCase "Integer" (partialDecode- (parseField "1e6" :: Parser Integer))+ [ testCase "Int" (partialDecode+ (parseField "12.7" :: Parser Int))+ , testCase "Natural" (partialDecode+ (parseField "12.7" :: Parser Natural))+ , testCase "Word" (partialDecode+ (parseField "12.7" :: Parser Word))+ , testCase "Scientific" (partialDecode+ (parseField "1.0+" :: Parser Scientific))+ , testCase "Double" (partialDecode+ (parseField "1.0+" :: Parser Double))+ , testCase "Integer" (partialDecode+ (parseField "1e6" :: Parser Integer)) ] , testGroup "Space trimming"- [ testCase "_Int" (expect (parseField " 12" :: Parser Int) 12)- , testCase "Int_" (expect (parseField "12 " :: Parser Int) 12)- , testCase "_Int_" (expect (parseField " 12 " :: Parser Int) 12)- , testCase "_Word" (expect (parseField " 12" :: Parser Word) 12)- , testCase "Word_" (expect (parseField "12 " :: Parser Word) 12)- , testCase "_Word_" (expect (parseField " 12 " :: Parser Word) 12)- , testCase "_Double" (expect (parseField " 1.2e1" :: Parser Double) 12)- , testCase "Double_" (expect (parseField "1.2e1 " :: Parser Double) 12)- , testCase "_Double_" (expect (parseField " 1.2e1 " :: Parser Double) 12)+ [ testCase "_Int" (expect (parseField " 12" :: Parser Int) 12)+ , testCase "Int_" (expect (parseField "12 " :: Parser Int) 12)+ , testCase "_Int_" (expect (parseField " 12 " :: Parser Int) 12)+ , testCase "_Natural" (expect (parseField " 12" :: Parser Natural) 12)+ , testCase "Natural_" (expect (parseField "12 " :: Parser Natural) 12)+ , testCase "_Natural_" (expect (parseField " 12 " :: Parser Natural) 12)+ , testCase "_Word" (expect (parseField " 12" :: Parser Word) 12)+ , testCase "Word_" (expect (parseField "12 " :: Parser Word) 12)+ , testCase "_Word_" (expect (parseField " 12 " :: Parser Word) 12)+ , testCase "_Scientific" (expect (parseField " 1.2e1" :: Parser Scientific) 12)+ , testCase "Scientific_" (expect (parseField "1.2e1 " :: Parser Scientific) 12)+ , testCase "_Scientific_" (expect (parseField " 1.2e1 " :: Parser Scientific) 12)+ , testCase "_Double" (expect (parseField " 1.2e1" :: Parser Double) 12)+ , testCase "Double_" (expect (parseField "1.2e1 " :: Parser Double) 12)+ , testCase "_Double_" (expect (parseField " 1.2e1 " :: Parser Double) 12) ] ] @@ -379,6 +392,53 @@ expected = ["a" :: String] ------------------------------------------------------------------------+-- Custom conversion option tests++genericConversionTests :: [TF.Test]+genericConversionTests =+ [ testCase "headerOrder" (header ["column1", "column2", "column_3"] @=? hdrs)+ , testCase "encode" (encodeDefaultOrderedByName sampleValues @?= sampleEncoding)+ , testCase "decode" (Right (hdrs, V.fromList sampleValues) @=? decodeByName sampleEncoding)+ , testProperty "roundTrip" rtProp+ ]+ where+ hdrs = headerOrder (undefined :: SampleType)++ sampleValues = [ SampleType "" 1 Nothing+ , SampleType "field" 99999 (Just 1.234)+ ]++ sampleEncoding = "column1,column2,column_3\r\n,1,\r\nfield,99999,1.234\r\n"++ rtProp :: [SampleType] -> Bool+ rtProp vs = Right (hdrs, V.fromList vs)+ == decodeByName (encodeDefaultOrderedByName vs)++data SampleType = SampleType+ { _column1 :: !T.Text+ , column2 :: !Int+ , _column_3 :: !(Maybe Double)+ } deriving (Eq, Show, Read, Generic)++sampleOptions :: Options+sampleOptions = defaultOptions { fieldLabelModifier = rmUnderscore }+ where+ rmUnderscore ('_':str) = str+ rmUnderscore str = str++instance ToNamedRecord SampleType where+ toNamedRecord = genericToNamedRecord sampleOptions++instance FromNamedRecord SampleType where+ parseNamedRecord = genericParseNamedRecord sampleOptions++instance DefaultOrdered SampleType where+ headerOrder = genericHeaderOrder sampleOptions++instance Arbitrary SampleType where+ arbitrary = SampleType <$> arbitrary <*> arbitrary <*> arbitrary++------------------------------------------------------------------------ -- Test harness allTests :: [TF.Test]@@ -387,6 +447,7 @@ , testGroup "conversion" conversionTests , testGroup "custom-options" customOptionsTests , testGroup "instances" instanceTests+ , testGroup "generic-conversions" genericConversionTests ] main :: IO ()