tapioca is a package that builds on cassava, to provide a simpler, more succinct method of encoding and decoding CSV's with headers.
Let's say we have a list of data MyRecord which we want to encode and decode to and from a CSV file:
dataMyRecord=MyRecord{field1::Int
, field2::String}myRecords:: [a]
myRecords =..Here is how it might be done in cassava:
importData.CsvinstanceToNamedRecordMyRecordwhere
toNamedRecord (MyRecord field1 field2)= namedRecord
[ "Header for Field 1".= field1
, "Header for Field 2".= field2
]
instanceDefaultOrderedMyRecordwhere
headerOrder _ =
[ "Header for Field 1"
, "Header for Field 2"
]
instanceFromNamedRecordMyRecordwhere
parseNamedRecord m =MyRecord<$> m .:"Header for Field 1"<*> m .:"Header for Field 2"-- Example usagetoCSV::ByteString
toCSV = encodeDefaultOrderedByName myRecords
fromCSV::ByteString->EitherString (VectorMyRecord)
fromCSV = (snd<$>) . decodeByNameWhile serviceable, the need to define headers twice is less than ideal, resulting in code that is bulkier and more fragile.
Here's how we do it in tapioca:
importData.TapiocainstanceCsvMappedMyRecordwhere
csvMap =CsvMap$"Header for Field 1".->#field1
:|"Header for Field 2".->#field2
-- Example usagemyCSV::ByteString
myCSV = encode WithHeader myRecords
fromCSV::ByteString->EitherString (VectorMyRecord)
fromCSV = decode WithHeaderWe see here that tapioca provides us with a more succinct definition for defining bidrectional CSV mappings, avoiding any unnecessary duplication, and keeping the entire definition within a single typeclass.
As seen earlier, the key part of using Tapioca to create a bidirectional mapping is to define an instance of CsvMapped EncodeDecode for your type, using the mkCsvMap function. This instance will only be allowed if no encode or decode-only functions are used in your mapping.
instanceCsvMappedEncodeDecodeMyRecordwhere
csvMap = mkCsvMap
$"Header for Field 1".->#field1
:|"Header for Field 2".->#field2
:|"Header for Field 3".->#field3
Encode-only mappings allow for more encoding options than bidrectional mappings, as there is no concern for parsing a csv back into the record.
instanceCsvMappedEncodeMyRecordwhere
csvMap = mkCsvMap
$"Header for Field 1".->#field1
:|"Header for Field 2".->#field2
:|"Header for Field 3".-> to (\record -> foo record)Fields can be mapped on top of cassava's FromField and ToField instances on a per-field basis.
If you wish to map a bidirectional field, use codec together with encoding and decoding mapping functions.
If you wish to map an encode-only field, use encoder together with an encoding function.
If you wish to map a decode-only field, use decoder together with a decoding function.
instanceCsvMappedEncodeMyRecordwhere
csvMap = mkCsvMap
$"Header for Field 1".-> codec toOrdinal fromOrdinal #field1
:|"Header for Field 2".-> encoder toOrdinal #field2 -- Can no longer have a bidirectional mappingRefer to the EncodeOnly and DecodeWith examples to see this in practice.
Occasionally you may want to nest a record within another record. Provided that both your records implement CsvMapped, this can be done by using the Nest constructor:
dataNestingRecord=NestingRecord{exampleRecord::ExampleRecord
, other::Int}deriving (Show, Generic)
instanceCsvMappedEncodeDecodeNestingRecordwhere
csvMap = mkCsvMap
$ nest #exampleRecord
:|"Other".->#other
Then in this example, for each row, the fields of ExampleRecord will precede the "Other" column field. Note that when decoding a spliced CSV with Headers, order of each field of ExampleRecord within the row is inferred from the order of the CSV headers. It is not required that the CSV's ExampleRecord columns are contiguous.
Refer to the NestedEncode and NestedDecode examples to see this in practice.