{-# 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.moduleData.Csv.Incremental(-- * Decoding headersHeaderParser(..),decodeHeader,decodeHeaderWith-- * Decoding records-- $typeconversion,Parser(..)-- ** Index-based record conversion-- $indexbased,HasHeader(..),decode,decodeWith-- ** Name-based record conversion-- $namebased,decodeByName,decodeByNameWith)whereimportControl.Applicative((<*),(<|>))importqualifiedData.AttoparsecasAimportData.Attoparsec.Char8(endOfInput)importqualifiedData.ByteStringasBimportqualifiedData.VectorasVimportData.Csv.Conversionhiding(Parser,record,toNamedRecord)importqualifiedData.Csv.ConversionasConversionimportData.Csv.ParserimportData.Csv.TypesimportData.Csv.Util(endOfLine)-- $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.dataHeaderParsera=-- | The input data was malformed. The first field contains any-- unconsumed input and second field contains information about-- the parse error.FailH!B.ByteStringString-- | 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->HeaderParsera)-- | The parse succeeded and produced the given 'Header'.|DoneH!HeaderaderivingFunctorinstanceShowa=>Show(HeaderParsera)whereshowsPrecd(FailHrestmsg)=showParen(d>appPrec)showStrwhereshowStr=showString"FailH ".showsPrec(appPrec+1)rest.showString" ".showsPrec(appPrec+1)msgshowsPrec_(PartialH_)=showString"PartialH <function>"showsPrecd(DoneHhdrx)=showParen(d>appPrec)showStrwhereshowStr=showString"DoneH ".showsPrec(appPrec+1)hdr.showString" ".showsPrec(appPrec+1)x-- Application has precedence one more than the most tightly-binding-- operatorappPrec::IntappPrec=10-- | Parse a CSV header in an incremental fashion. When done, the-- 'HeaderParser' returns any unconsumed input in the second field of-- the 'DoneH' constructor.decodeHeader::HeaderParserB.ByteStringdecodeHeader=decodeHeaderWithdefaultDecodeOptions-- | Like 'decodeHeader', but lets you customize how the CSV data is-- parsed.decodeHeaderWith::DecodeOptions->HeaderParserB.ByteStringdecodeHeaderWith!opts=PartialH(go.parser)whereparser=A.parse(header$decDelimiteropts)go(A.Failrest_msg)=FailHresterrwhereerr="parse error ("++msg++")"-- TODO: Check empty and give attoparsec one last chance to return-- something:go(A.Partialk)=PartialH$\s->go(ks)go(A.Donerestr)=DoneHrrest-------------------------------------------------------------------------- * 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.dataParsera=-- | The input data was malformed. The first field contains any-- unconsumed input and second field contains information about-- the parse error.Fail!B.ByteStringString-- | The parser parsed and converted zero or more 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'.|Many[EitherStringa](B.ByteString->Parsera)-- | 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[EitherStringa]derivingFunctorinstanceShowa=>Show(Parsera)whereshowsPrecd(Failrestmsg)=showParen(d>appPrec)showStrwhereshowStr=showString"Fail ".showsPrec(appPrec+1)rest.showString" ".showsPrec(appPrec+1)msgshowsPrecd(Manyrs_)=showParen(d>appPrec)showStrwhereshowStr=showString"Many ".showsPrec(appPrec+1)rs.showString" <function>"showsPrecd(Doners)=showParen(d>appPrec)showStrwhereshowStr=showString"Done ".showsPrec(appPrec+1)rs-- | Have we read all available input?dataMore=Incomplete|Completederiving(Eq,Show)-- | Efficiently deserialize CSV in an incremental fashion. Equivalent-- to @'decodeWith' 'defaultDecodeOptions'@.decode::FromRecorda=>HasHeader-- ^ Data contains header that should be-- skipped->Parseradecode=decodeWithdefaultDecodeOptions-- | Like 'decode', but lets you customize how the CSV data is parsed.decodeWith::FromRecorda=>DecodeOptions-- ^ Decoding options->HasHeader-- ^ Data contains header that should be-- skipped->ParseradecodeWith!optshasHeader=casehasHeaderofHasHeader->go(decodeHeaderWithopts)NoHeader->Many[]$\s->decodeWithPparseRecordoptsswherego(FailHrestmsg)=Failrestmsggo(PartialHk)=Many[]$\s'->go(ks')go(DoneH_rest)=decodeWithPparseRecordoptsrest-------------------------------------------------------------------------- | 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::FromNamedRecorda=>HeaderParser(Parsera)decodeByName=decodeByNameWithdefaultDecodeOptions-- | Like 'decodeByName', but lets you customize how the CSV data is-- parsed.decodeByNameWith::FromNamedRecorda=>DecodeOptions-- ^ Decoding options->HeaderParser(Parsera)decodeByNameWith!opts=go(decodeHeaderWithopts)wherego(FailHrestmsg)=FailHrestmsggo(PartialHk)=PartialH$\s->go(ks)go(DoneHhdrrest)=DoneHhdr(decodeWithP(parseNamedRecord.toNamedRecordhdr)optsrest)-------------------------------------------------------------------------- TODO: 'decodeWithP' should probably not take an initial-- 'B.ByteString' input.-- | Like 'decode', but lets you customize how the CSV data is parsed.decodeWithP::(Record->Conversion.Parsera)->DecodeOptions->B.ByteString->ParseradecodeWithPp!opts=goIncomplete[].parserwherego!_!acc(A.Failrest_msg)|nullacc=Failresterr|otherwise=Many(reverseacc)(\s->Fail(rest`B.append`s)err)whereerr="parse error ("++msg++")"goIncompleteacc(A.Partialk)=Many(reverseacc)contwhereconts=gom[](ks)wherem|B.nulls=Complete|otherwise=IncompletegoComplete_(A.Partial_)=moduleError"decodeWithP"msgwheremsg="attoparsec should never return Partial in this case"gomacc(A.Donerestr)|B.nullrest=casemofComplete->Done(reverseacc')Incomplete->Many(reverseacc')(cont[])|otherwise=gomacc'(parserrest)wherecontacc''s|B.nulls=Done(reverseacc'')|otherwise=goIncompleteacc''(parsers)acc'|blankLiner=acc|otherwise=let!r'=convertrinr':accparser=A.parse(record(decDelimiteropts)<*(endOfLine<|>endOfInput))convert=runParser.p{-# INLINE decodeWithP #-}blankLine::V.VectorB.ByteString->BoolblankLinev=V.lengthv==1&&(B.null(V.headv))moduleError::String->String->amoduleErrorfuncmsg=error$"Data.Csv.Incremental."++func++": "++msg{-# NOINLINE moduleError #-}