git.iko.soy/typeable/cassava-streams
on trunk help sign in
The decoding functions now expose bad records

* All of the input streams now yield `Either String a` * New stream transformer `onlyValidRecords` which throws an exception on bad CSV type conversions.

commit4cd1ab8bbb520d177395a0f2678b6cf9ef84e2c4 authorPeter Jones <pjones@pmade.com> dateTue 22 Apr 2014, 16:50 (12 years ago) parentf4f63fc
3 files changed+72−29
src/System/IO/Streams/Csv.hs+10 src/System/IO/Streams/Csv/Decode.hs+52−25 src/System/IO/Streams/Csv/Tutorial.hs+10−4
src/System/IO/Streams/Csv.hs+10
@@ -24,6 +24,16 @@ module System.IO.Streams.Csv
2424 -- | These functions convert an io-streams @InputStream
2525 -- ByteString@ stream into one that decodes CSV records and
2626 -- produces these decoded records.
27+ --
28+ -- Each of the decoding functions produce an @InputStream@
29+ -- which yields an @Either@ value. @Left String@ represents
30+ -- a record which failed type conversion. @Right a@ is a
31+ -- successfully decoded record.
32+ --
33+ -- See the tutorial in "System.IO.Streams.Csv.Tutorial" for
34+ -- details on how to use the 'onlyValidRecords' function to
35+ -- transform the decoding streams so that they only produce
36+ -- valid records and throw exceptions for bad records.
2737 module System.IO.Streams.Csv.Decode
2838
2939 -- * Encoding CSV
src/System/IO/Streams/Csv/Decode.hs+52−25
@@ -16,6 +16,7 @@ module System.IO.Streams.Csv.Decode
1616 , decodeStreamWith
1717 , decodeStreamByName
1818 , decodeStreamByNameWith
19+ , onlyValidRecords
1920 ) where
2021
2122--------------------------------------------------------------------------------
@@ -23,9 +24,7 @@ import Data.ByteString (ByteString)
2324import qualified Data.ByteString as BS
2425import Data.Csv hiding (Parser, decodeWith, decodeByNameWith)
2526import Data.Csv.Incremental
26−import Data.Either (lefts, rights)
2727import Data.IORef
28−import Data.List (intercalate)
2928import System.IO.Streams (InputStream, makeInputStream)
3029import qualified System.IO.Streams as Streams
3130
@@ -35,19 +34,26 @@ import qualified System.IO.Streams as Streams
3534--
3635-- Equivalent to @decodeStreamWith defaultDecodeOptions@.
3736decodeStream :: (FromRecord a)
38− => HasHeader -- ^ Whether to skip a header or not.
39− -> InputStream ByteString -- ^ Upstream.
40− -> IO (InputStream a) -- ^ An @InputStream@ which produces records.
37+ => HasHeader
38+ -- ^ Whether to skip a header or not.
39+ -> InputStream ByteString
40+ -- ^ Upstream.
41+ -> IO (InputStream (Either String a))
42+ -- ^ An @InputStream@ which produces records.
4143decodeStream = decodeStreamWith defaultDecodeOptions
4244
4345--------------------------------------------------------------------------------
4446-- | Create an @InputStream@ which decodes CSV records from the given
4547-- upstream data source.
4648decodeStreamWith :: (FromRecord a)
47− => DecodeOptions -- ^ CSV decoding options.
48− -> HasHeader -- ^ Whether to skip a header or not.
49− -> InputStream ByteString -- ^ Upstream.
50− -> IO (InputStream a) -- ^ An @InputStream@ which produces records.
49+ => DecodeOptions
50+ -- ^ CSV decoding options.
51+ -> HasHeader
52+ -- ^ Whether to skip a header or not.
53+ -> InputStream ByteString
54+ -- ^ Upstream.
55+ -> IO (InputStream (Either String a))
56+ -- ^ An @InputStream@ which produces records.
5157decodeStreamWith ops hdr input = do
5258 queue <- newIORef []
5359 parser <- newIORef $ Just (decodeWith ops hdr)
@@ -59,17 +65,22 @@ decodeStreamWith ops hdr input = do
5965--
6066-- Equivalent to @decodeStreamByNameWith defaultDecodeOptions@.
6167decodeStreamByName :: (FromNamedRecord a)
62− => InputStream ByteString -- ^ Upstream.
63− -> IO (InputStream a) -- ^ An @InputStream@ which produces records.
68+ => InputStream ByteString
69+ -- ^ Upstream.
70+ -> IO (InputStream (Either String a))
71+ -- ^ An @InputStream@ which produces records.
6472decodeStreamByName = decodeStreamByNameWith defaultDecodeOptions
6573
6674--------------------------------------------------------------------------------
6775-- | Create an @InputStream@ which decodes CSV records from the given
6876-- upstream data source. Data should be preceded by a header.
6977decodeStreamByNameWith :: (FromNamedRecord a)
70− => DecodeOptions -- ^ CSV decoding options.
71− -> InputStream ByteString -- ^ Upstream.
72− -> IO (InputStream a) -- ^ An @InputStream@ which produces records.
78+ => DecodeOptions
79+ -- ^ CSV decoding options.
80+ -> InputStream ByteString
81+ -- ^ Upstream.
82+ -> IO (InputStream (Either String a))
83+ -- ^ An @InputStream@ which produces records.
7384decodeStreamByNameWith ops input = go (decodeByNameWith ops) where
7485 -- Dispatch on the HeaderParser type.
7586 go (FailH _ e) = bomb e
@@ -79,12 +90,32 @@ decodeStreamByNameWith ops input = go (decodeByNameWith ops) where
7990 parser <- newIORef (Just p)
8091 makeInputStream (dispatch queue parser input)
8192
93+--------------------------------------------------------------------------------
94+-- | Creates a new @InputStream@ which only sends valid CSV records
95+-- downstream. The first invalid record will throw an exception.
96+onlyValidRecords :: InputStream (Either String a)
97+ -- ^ Upstream.
98+ -> IO (InputStream a)
99+ -- ^ An @InputStream@ which only produces valid
100+ -- records.
101+onlyValidRecords input = makeInputStream $ do
102+ upstream <- Streams.read input
103+
104+ case upstream of
105+ Nothing -> return Nothing
106+ Just (Left err) -> bomb err -- FIXME: replace with throwIO
107+ Just (Right x) -> return (Just x)
108+
82109--------------------------------------------------------------------------------
83110-- | Internal function which feeds data to the CSV parser.
84−dispatch :: IORef [a] -- ^ List of queued CSV records.
85− -> IORef (Maybe (Parser a)) -- ^ Current CSV parser state.
86− -> InputStream ByteString -- ^ Upstream.
87− -> IO (Maybe a) -- ^ Data feed downstream.
111+dispatch :: IORef [Either String a]
112+ -- ^ List of queued CSV records.
113+ -> IORef (Maybe (Parser a))
114+ -- ^ Current CSV parser state.
115+ -> InputStream ByteString
116+ -- ^ Upstream.
117+ -> IO (Maybe (Either String a))
118+ -- ^ Data feed downstream.
88119dispatch queueRef parserRef input = do
89120 queue <- readIORef queueRef
90121
@@ -107,13 +138,9 @@ dispatch queueRef parserRef input = do
107138 more f = Streams.read input >>=
108139 writeIORef parserRef . Just . maybe (f BS.empty) f
109140
110− -- Feed more data downstream or fail if some records didn't parse
111− -- correctly. The elements are wrapped in an @Either@ which
112− -- indicates decoding failures.
113− feed xs = if not $ null (lefts xs)
114− then bomb $ intercalate "\n" (lefts xs)
115− else writeIORef queueRef (rights xs) >>
116− dispatch queueRef parserRef input
141+ -- Feed records downstream.
142+ feed xs = writeIORef queueRef xs >>
143+ dispatch queueRef parserRef input
117144
118145--------------------------------------------------------------------------------
119146-- | FIXME: use a proper exception.
src/System/IO/Streams/Csv/Tutorial.hs+10−4
@@ -85,6 +85,12 @@ instance ToField TState where
8585-- 'decodeStreamByName' which converts the @InputStream ByteString@
8686-- stream into an @InputStream Item@ stream.
8787--
88+-- Notice that the cassava-streams function 'onlyValidRecords' is used
89+-- to transform the decoding stream into one that only produces valid
90+-- records. Any records which fail type conversion (via
91+-- @FromNamedRecord@ or @FromRecord@) will not escape from
92+-- 'onlyValidRecords' but instead will throw an exception.
93+--
8894-- Finally the io-streams @filter@ function is used to filter the
8995-- input stream so that it only produces to-do items which haven't
9096-- been completed.
@@ -93,8 +99,8 @@ onlyTodo :: Handle -- ^ Input handle where CSV data can be read.
9399 -> IO ()
94100onlyTodo inH outH = do
95101 -- A stream which produces items which are not 'Done'.
96− input <- Streams.handleToInputStream inH >>=
97− decodeStreamByName >>=
102+ input <- Streams.handleToInputStream inH >>=
103+ decodeStreamByName >>= onlyValidRecords >>=
98104 Streams.filter (\item -> state item /= Done)
99105
100106 -- A stream to write items into. They will be converted to CSV.
@@ -120,8 +126,8 @@ markDone titleOfItem inH outH = do
120126
121127 -- A stream which produces items and converts matching items to the
122128 -- 'Done' state.
123− input <- Streams.handleToInputStream inH >>=
124− decodeStreamByName >>=
129+ input <- Streams.handleToInputStream inH >>=
130+ decodeStreamByName >>= onlyValidRecords >>=
125131 Streams.map markDone'
126132
127133 -- A stream to write items into. They will be converted to CSV.