Skip to content

Commit 5996d4f

Browse files
committed
Merge PR #167 (Parameterized record parsers)
(also tweak changelog & since annotations)
2 parents 0e78553 + 2113cfa commit 5996d4f

File tree

7 files changed

+110
-22
lines changed

7 files changed

+110
-22
lines changed

CHANGES.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
## Version 0.5.2.0
22

33
* Add `FromField`/`ToField` instances for `Identity` and `Const` (#158)
4-
4+
* New `typeclass`-less decoding functions `decodeWithP` and `decodeByNameWithP` (#67,#167)
55
* Support for final phase of MFP / base-4.13
66

77
## Version 0.5.1.0

Setup.hs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,8 @@
11
import Distribution.Simple
22
main = defaultMain
3+
4+
5+
6+
7+
8+
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{-# LANGUAGE OverloadedStrings #-}
2+
3+
import qualified Data.ByteString.Lazy as BL
4+
import Data.Csv
5+
import qualified Data.Vector as V
6+
7+
data Person = Person
8+
{ name :: String
9+
, salary :: Int
10+
}
11+
12+
valueParse :: NamedRecord -> Parser Person
13+
valueParse r = Person <$> r .: "name" <*> r .: "salary"
14+
15+
main :: IO ()
16+
main = do
17+
csvData <- BL.readFile "salaries.csv"
18+
case decodeByNameWithP valueParse defaultDecodeOptions csvData of
19+
Left err -> putStrLn err
20+
Right (_, v) -> V.forM_ v $ \ p ->
21+
putStrLn $ name p ++ " earns " ++ show (salary p) ++ " dollars"

examples/cassava-examples.cabal

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,16 @@ executable NamedBasedDecode
4242
vector
4343
default-language: Haskell2010
4444

45+
executable NamedBasedExplicitDecode
46+
main-is: NamedBasedExplicitDecode.hs
47+
build-depends:
48+
base,
49+
bytestring,
50+
cassava,
51+
vector
52+
default-language: Haskell2010
53+
54+
4555
executable NamedBasedGeneric
4656
main-is: NamedBasedGeneric.hs
4757
build-depends:

src/Data/Csv.hs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,9 @@ module Data.Csv
4848
, DecodeOptions(..)
4949
, defaultDecodeOptions
5050
, decodeWith
51+
, decodeWithP
5152
, decodeByNameWith
53+
, decodeByNameWithP
5254
, EncodeOptions(..)
5355
, Quoting(..)
5456
, defaultEncodeOptions

src/Data/Csv/Encoding.hs

Lines changed: 39 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@ module Data.Csv.Encoding
2424
, DecodeOptions(..)
2525
, defaultDecodeOptions
2626
, decodeWith
27+
, decodeWithP
2728
, decodeByNameWith
29+
, decodeByNameWithP
2830
, EncodeOptions(..)
2931
, defaultEncodeOptions
3032
, encodeWith
@@ -117,7 +119,7 @@ decodeWith :: FromRecord a
117119
-- skipped
118120
-> L.ByteString -- ^ CSV data
119121
-> Either String (Vector a)
120-
decodeWith = decodeWithC csv
122+
decodeWith = decodeWithC (csv parseRecord)
121123
{-# INLINE [1] decodeWith #-}
122124

123125
{-# RULES
@@ -130,12 +132,25 @@ idDecodeWith :: DecodeOptions -> HasHeader -> L.ByteString
130132
-> Either String (Vector (Vector B.ByteString))
131133
idDecodeWith = decodeWithC Parser.csv
132134

135+
-- | Like 'decodeWith'', but lets you specify a parser function.
136+
--
137+
-- @since 0.5.2.0
138+
decodeWithP :: (Record -> Conversion.Parser a)
139+
-- ^ Custom parser function
140+
-> DecodeOptions -- ^ Decoding options
141+
-> HasHeader -- ^ Data contains header that should be
142+
-- skipped
143+
-> L.ByteString -- ^ CSV data
144+
-> Either String (Vector a)
145+
decodeWithP _parseRecord = decodeWithC (csv _parseRecord)
146+
{-# INLINE [1] decodeWithP #-}
147+
133148
-- | Decode CSV data using the provided parser, skipping a leading
134149
-- header if 'hasHeader' is 'HasHeader'. Returns 'Left' @errMsg@ on
135150
-- failure.
136151
decodeWithC :: (DecodeOptions -> AL.Parser a) -> DecodeOptions -> HasHeader
137152
-> BL8.ByteString -> Either String a
138-
decodeWithC p !opts hasHeader = decodeWithP parser
153+
decodeWithC p !opts hasHeader = decodeWithP' parser
139154
where parser = case hasHeader of
140155
HasHeader -> header (decDelimiter opts) *> p opts
141156
NoHeader -> p opts
@@ -147,7 +162,18 @@ decodeByNameWith :: FromNamedRecord a
147162
=> DecodeOptions -- ^ Decoding options
148163
-> L.ByteString -- ^ CSV data
149164
-> Either String (Header, Vector a)
150-
decodeByNameWith !opts = decodeWithP (csvWithHeader opts)
165+
decodeByNameWith !opts = decodeWithP' (csvWithHeader parseNamedRecord opts)
166+
167+
-- | Like 'decodeByNameWith', but lets you specify a parser function.
168+
--
169+
-- @since 0.5.2.0
170+
decodeByNameWithP :: (NamedRecord -> Conversion.Parser a)
171+
-- ^ Custom parser function
172+
-> DecodeOptions -- ^ Decoding options
173+
-> L.ByteString -- ^ CSV data
174+
-> Either String (Header, Vector a)
175+
decodeByNameWithP _parseNamedRecord !opts =
176+
decodeWithP' (csvWithHeader _parseNamedRecord opts)
151177

152178
-- | Should quoting be applied to fields, and at which level?
153179
data Quoting
@@ -328,8 +354,8 @@ prependToAll :: Builder -> [Builder] -> [Builder]
328354
prependToAll _ [] = []
329355
prependToAll sep (x:xs) = sep <> x : prependToAll sep xs
330356

331-
decodeWithP :: AL.Parser a -> L.ByteString -> Either String a
332-
decodeWithP p s =
357+
decodeWithP' :: AL.Parser a -> L.ByteString -> Either String a
358+
decodeWithP' p s =
333359
case AL.parse p s of
334360
AL.Done _ v -> Right v
335361
AL.Fail left _ msg -> Left errMsg
@@ -338,7 +364,7 @@ decodeWithP p s =
338364
(if BL8.length left > 100
339365
then (take 100 $ BL8.unpack left) ++ " (truncated)"
340366
else show (BL8.unpack left))
341-
{-# INLINE decodeWithP #-}
367+
{-# INLINE decodeWithP' #-}
342368

343369
-- These alternative implementation of the 'csv' and 'csvWithHeader'
344370
-- parsers from the 'Parser' module performs the
@@ -351,26 +377,27 @@ decodeWithP p s =
351377
-- "parse error: conversion error: ...".
352378

353379
-- | Parse a CSV file that does not include a header.
354-
csv :: FromRecord a => DecodeOptions -> AL.Parser (V.Vector a)
355-
csv !opts = do
380+
csv :: (Record -> Conversion.Parser a) -> DecodeOptions
381+
-> AL.Parser (V.Vector a)
382+
csv _parseRecord !opts = do
356383
vals <- records
357384
return $! V.fromList vals
358385
where
359386
records = do
360387
!r <- record (decDelimiter opts)
361388
if blankLine r
362389
then (endOfInput *> pure []) <|> (endOfLine *> records)
363-
else case runParser (parseRecord r) of
390+
else case runParser (_parseRecord r) of
364391
Left msg -> fail $ "conversion error: " ++ msg
365392
Right val -> do
366393
!vals <- (endOfInput *> AP.pure []) <|> (endOfLine *> records)
367394
return (val : vals)
368395
{-# INLINE csv #-}
369396

370397
-- | Parse a CSV file that includes a header.
371-
csvWithHeader :: FromNamedRecord a => DecodeOptions
398+
csvWithHeader :: (NamedRecord -> Conversion.Parser a) -> DecodeOptions
372399
-> AL.Parser (Header, V.Vector a)
373-
csvWithHeader !opts = do
400+
csvWithHeader _parseNamedRecord !opts = do
374401
!hdr <- header (decDelimiter opts)
375402
vals <- records hdr
376403
let !v = V.fromList vals
@@ -386,4 +413,4 @@ csvWithHeader !opts = do
386413
!vals <- (endOfInput *> pure []) <|> (endOfLine *> records hdr)
387414
return (val : vals)
388415

389-
convert hdr = parseNamedRecord . Types.toNamedRecord hdr
416+
convert hdr = _parseNamedRecord . Types.toNamedRecord hdr

src/Data/Csv/Incremental.hs

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -55,11 +55,13 @@ module Data.Csv.Incremental
5555
, HasHeader(..)
5656
, decode
5757
, decodeWith
58+
, decodeWithP
5859

5960
-- ** Name-based record conversion
6061
-- $namebased
6162
, decodeByName
6263
, decodeByNameWith
64+
, decodeByNameWithP
6365

6466
-- * Encoding
6567
-- ** Index-based record conversion
@@ -247,12 +249,23 @@ decodeWith :: FromRecord a
247249
-> HasHeader -- ^ Data contains header that should be
248250
-- skipped
249251
-> Parser a
250-
decodeWith !opts hasHeader = case hasHeader of
252+
decodeWith !opts hasHeader = decodeWithP parseRecord opts hasHeader
253+
254+
-- | Like 'decodeWith', but lets you pass an explicit parser value instead of
255+
-- using a typeclass
256+
--
257+
-- @since 0.5.2.0
258+
decodeWithP :: (Record -> Conversion.Parser a)
259+
-> DecodeOptions -- ^ Decoding options
260+
-> HasHeader -- ^ Data contains header that should be
261+
-- skipped
262+
-> Parser a
263+
decodeWithP p !opts hasHeader = case hasHeader of
251264
HasHeader -> go (decodeHeaderWith opts)
252-
NoHeader -> Many [] $ \ s -> decodeWithP parseRecord opts s
265+
NoHeader -> Many [] $ \ s -> decodeWithP' p opts s
253266
where go (FailH rest msg) = Fail rest msg
254267
go (PartialH k) = Many [] $ \ s' -> go (k s')
255-
go (DoneH _ rest) = decodeWithP parseRecord opts rest
268+
go (DoneH _ rest) = decodeWithP' p opts rest
256269

257270
------------------------------------------------------------------------
258271

@@ -269,22 +282,31 @@ decodeByName = decodeByNameWith defaultDecodeOptions
269282
decodeByNameWith :: FromNamedRecord a
270283
=> DecodeOptions -- ^ Decoding options
271284
-> HeaderParser (Parser a)
272-
decodeByNameWith !opts = go (decodeHeaderWith opts)
285+
decodeByNameWith !opts = decodeByNameWithP parseNamedRecord opts
286+
287+
-- | Like 'decodeByNameWith', but lets you pass an explicit parser value instead
288+
-- of using a typeclass
289+
--
290+
-- @since 0.5.2.0
291+
decodeByNameWithP :: (NamedRecord -> Conversion.Parser a)
292+
-> DecodeOptions -- ^ Decoding options
293+
-> HeaderParser (Parser a)
294+
decodeByNameWithP p !opts = go (decodeHeaderWith opts)
273295
where
274296
go (FailH rest msg) = FailH rest msg
275297
go (PartialH k) = PartialH $ \ s -> go (k s)
276298
go (DoneH hdr rest) =
277-
DoneH hdr (decodeWithP (parseNamedRecord . toNamedRecord hdr) opts rest)
299+
DoneH hdr (decodeWithP' (p . toNamedRecord hdr) opts rest)
278300

279301
------------------------------------------------------------------------
280302

281303
-- TODO: 'decodeWithP' should probably not take an initial
282304
-- 'B.ByteString' input.
283305

284306
-- | Like 'decode', but lets you customize how the CSV data is parsed.
285-
decodeWithP :: (Record -> Conversion.Parser a) -> DecodeOptions -> B.ByteString
307+
decodeWithP' :: (Record -> Conversion.Parser a) -> DecodeOptions -> B.ByteString
286308
-> Parser a
287-
decodeWithP p !opts = go Incomplete [] . parser
309+
decodeWithP' p !opts = go Incomplete [] . parser
288310
where
289311
go !_ !acc (A.Fail rest _ msg)
290312
| null acc = Fail rest err
@@ -294,7 +316,7 @@ decodeWithP p !opts = go Incomplete [] . parser
294316
where cont s = go m [] (k s)
295317
where m | B.null s = Complete
296318
| otherwise = Incomplete
297-
go Complete _ (A.Partial _) = moduleError "decodeWithP" msg
319+
go Complete _ (A.Partial _) = moduleError "decodeWithP'" msg
298320
where msg = "attoparsec should never return Partial in this case"
299321
go m acc (A.Done rest r)
300322
| B.null rest = case m of
@@ -309,7 +331,7 @@ decodeWithP p !opts = go Incomplete [] . parser
309331

310332
parser = A.parse (record (decDelimiter opts) <* (endOfLine <|> endOfInput))
311333
convert = runParser . p
312-
{-# INLINE decodeWithP #-}
334+
{-# INLINE decodeWithP' #-}
313335

314336
blankLine :: V.Vector B.ByteString -> Bool
315337
blankLine v = V.length v == 1 && (B.null (V.head v))

0 commit comments

Comments
 (0)