cassava-records (empty) → 0.1.0.0
raw patch · 9 files changed
+823/−0 lines, 9 filesdep +HUnitdep +QuickCheckdep +attoparsecsetup-changed
Dependencies added: HUnit, QuickCheck, attoparsec, base, bytestring, cassava, cassava-records, containers, foldl, lens, pptable, tasty, tasty-hunit, tasty-quickcheck, template-haskell, text, unordered-containers, vector
Files
- ChangeLog.md +3/−0
- LICENSE +22/−0
- README.md +275/−0
- Setup.hs +2/−0
- app/Main.hs +42/−0
- cassava-records.cabal +98/−0
- src/Data/Cassava/Internal/RecordBuilder.hs +175/−0
- src/Data/Cassava/Records.hs +141/−0
- test/Spec.hs +65/−0
+ ChangeLog.md view
@@ -0,0 +1,3 @@+# Changelog for cassava-records++## Unreleased changes
+ LICENSE view
@@ -0,0 +1,22 @@+MIT License++Copyright (c) 2017 Guru Devanla++Permission is hereby granted, free of charge, to any person obtaining a copy+of this software and associated documentation files (the "Software"), to deal+in the Software without restriction, including without limitation the rights+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell+copies of the Software, and to permit persons to whom the Software is+furnished to do so, subject to the following conditions:++The above copyright notice and this permission notice shall be included in all+copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE+SOFTWARE.+
+ README.md view
@@ -0,0 +1,275 @@+# cassava-records++A library extension for Cassava (Haskell CSV parser library) that+automatically creates a Record given the csv file.++# What is this tool for?++Say you are working on a project that involves processing a number of+comma separated or tab serparated files. Assuming, you are using+cassava for loading the input files, here is a typical workflow you+would follow++a. Inspect the file+b. Create a ```Record``` data type to reflect the columns and types+found in the file+c. Create instances of the Record type that may be required to load+the files with Cassava.++Now, imagine this file you are inspecting to contains tens or hundreds+of columns. Now, as a good Haskeller you will want to automate steps+(a) and (b) to the extend possible. That is precisely, what this+library does.++Cassava-records performs the following tasks. Given, a input file+(command or tab-seperated for example), it reads the whole file,+infers some basic data types for each column and automatically created+a ```Record``` data type using ```Template Haskell```.+++# Quick Start++## Example 1 :++Using data/salaries_simple.csv++```+emp_no,name,salary,status,years+1,John Doe,100.0,True,1+2,Jill Doe, 200.10,False,2+3,John Doe Sr,101.0,T,3+4,Jill Doe Sr, 10101.10,f,4.2+5,John Doe Jr,1010101.0,true,5.1+6,Jill Doe Jr, 10101.10,false,6++```++``` haskell++{-#LANGUAGE TemplateHaskell#-}+{-#LANGUAGE DeriveGeneric #-}++import Data.Cassava.Records+import Data.Csv+import qualified Data.ByteString.Lazy as BL+import Data.Vector as V+import Data.Text as DT++$(makeCsvRecord "Salaries" "data/salaries_simple.csv" "_" commaOptions)++```++The ```makeCsvRecord``` needs take 4 arguments,++1. ```"Salaries"``` : A ```String``` that will be used as name for+ the ```Record```.+2. ```data/salaries_simple.csv```: path to the input file+3. ```"_"```: string to prefix each field. Useful, if we need to build lens+for this record+4. ```commaOptions```: ```defaultDecodeOptions``` defined+ in ```cassava``` library++If you load this code in ```GHCi```, we will see++``` haskell+1 >:info Salaries+data Salaries+ = Salaries {+ _emp_no:: Integer,+ _name :: Text,+ _salary :: Double,+ _status :: Bool,+ _years:: Double}++```+Note that all column names are in lower case and "_" has been prefixed+to the column names.++To be consistent, ```cassava-record``` converts all column headers to+lower-case before created corresponding field names for each column+header. Therefore, if column headers were all upper-case, we need to+provide a field modifier while creating ```ToNamedRecord```+and ```FromNamedRecord``` instances for ```cassava```.++Note, that if the column headers are mixed case, it become+tricky. Current version of the library does not work very well with+mixed case column headers.++There is a convenience method called ```makeInstances``` that can create+the instances required for ```cassava```.The instances created use the+default ```fieldModifieroptions``` settings shown below.++``` haskell++fieldModifierOptions :: Options+fieldModifierOptions = defaultOptions { fieldLabelModifier = rmUnderscore }+ where+ rmUnderscore ('_':str) = DT.unpack . DT.toUpper . DT.pack $ str+ rmUnderscore str = str++-- the ToNamedRecord and FromNamedRecord are needed by Cassava since+-- we prefix+instance ToNamedRecord Salaries where+ toNamedRecord = genericToNamedRecord fieldModifierOptions++instance FromNamedRecord Salaries where+ parseNamedRecord = genericParseNamedRecord fieldModifierOptions++main :: IO ()+main = do+ v <- loadData "data/salaries_simple.csv":: IO (V.Vector Salaries)+ putStrLn . show $ v+```++In ```GHCi``` we see (formatted for clarity)++``` haskell+2 >loadData "data/salaries.csv"+[Salaries {_emp_no = 1, _name = "John Doe", _salary = 100.0, _status = False, _years = 1.0},+ Salaries {_emp_no = 2, _name = "Jill Doe", _salary = 200.1, _status = False, _years = 2.0},+ Salaries {_emp_no = 3, _name = "John Doe Sr", _salary = 101.0, _status = True, _years = 3.0},+ Salaries {_emp_no = 4, _name = "Jill Doe Sr", _salary = 10101.1, _status = False, _years = 4.2},+ Salaries {_emp_no = 5, _name = "John Doe Jr", _salary = 1010101.0, _status = False, _years = 5.1},+ Salaries {_emp_no = 6, _name = "Jill Doe Jr", _salary = 10101.1, _status = False, _years = 6.0}]+```++Note, the type inference in the above example is as follows:++1. If a column has values from the set {```true```, ```t```, ```false```, ```f```}+ (ignoring case) then the inferred type is ```Bool```.+2. If a column has values that are all numeric, then an ```Integer```+ type is attempted, or else a ```Double``` is infered. For example+ for ```emp_no``` the infered type is a ```Integer``` whereas for ```years``` the type is ```Double```.+3. For all other cases, a ```Text``` type is inferred.++# Example 2 (Missing Values)++The library also supports type inference when values are missing. For example in, data/salaries_mixed_input.csv++```+emp_no,name,salary,status,years+1,John Doe,100.0,True,1+2,Jill Doe, 200.10,False,2+3,John Doe Sr,101.0,T,+4,Jill Doe Sr,10101.10,,4.2+5,John Doe Jr,,true,5.1+6,, 10101.10,false,6+```++the ```status``` for Jill Doe Jr is missing and the ```salary``` for+John Doe Sr is missing. In this case, the type as wrapped in a ```Maybe``` type.++In that case, the record instance we get will be as follows:++``` haskell+3 >:info Salaries+data Salaries+ = Salaries {+ _emp_no:: Integer,+ _name :: Maybe Text,+ _salary :: Maybe Double,+ _status :: Maybe Bool,+ _years:: Maybe Double}+```++Loading this data, would produce the following output++``` haskell+{-#LANGUAGE TemplateHaskell#-}+{-#LANGUAGE DeriveGeneric #-}++import Data.Cassava.Records+import Data.Csv+import qualified Data.ByteString.Lazy as BL+import Data.Vector as V+import Data.Text as DT++$(makeCsvRecord "SalariesMixed" "data/salaries_mixed_input.csv" "_" commaOptions)+$(makeInstance "SalariesMixed")+-- ^ note that we can use this function instead of manually defining+-- all instances required by Cassava++main :: IO ()+main = do+ v <- loadData "data/salaries_mixed_input.csv":: IO (V.Vector SalariesMixed)+ putStrLn . show $ v++```++The output will be as follows:++```+[SalariesMixed {_emp_no = 1, _name = Just "John Doe", _salary = Just 100.0, _status = Just False, _years = Just 1.0},+ SalariesMixed {_emp_no = 2, _name = Just "Jill Doe", _salary = Just 200.1, _status = Just False, _years = Just 2.0},+ SalariesMixed {_emp_no = 3, _name = Just "John Doe Sr", _salary = Just 101.0, _status = Just True, _years = Nothing},+ SalariesMixed {_emp_no = 4, _name = Just "Jill Doe Sr", _salary = Just 10101.1, _status = Nothing, _years = Just 4.2},+ SalariesMixed {_emp_no = 5, _name = Just "John Doe Jr", _salary = Nothing, _status = Just False, _years = Just 5.1},+ SalariesMixed {_emp_no = 6, _name = Nothing, _salary = Just 10101.1, _status = Just False, _years = Just 6.0}]+```++Here is a full working code that uses both the examples:++``` haskell+{-#LANGUAGE TemplateHaskell#-}+{-#LANGUAGE DeriveGeneric #-}+{-#LANGUAGE ScopedTypeVariables #-}+{-#LANGUAGE DuplicateRecordFields #-}+{-# LANGUAGE DeriveDataTypeable #-}++module Main where++import Data.Cassava.Records+import Data.Csv+import qualified Data.ByteString.Lazy as BL+import Data.Vector as V+import Data.Text as DT+import qualified Text.PrettyPrint.Tabulate as T+import Language.Haskell.TH+-- import Control.Lens hiding (element)++$(makeCsvRecord "Salaries" "data/salaries_simple.csv" "_" commaOptions)+-- $(makeInstance "Salaries")++$(makeCsvRecord "SalariesMixed" "data/salaries_mixed_input.csv" "_" commaOptions)+$(makeInstance "SalariesMixed")++-- the following instance is not required, if $(makeInstance Salaries) statement+-- is spliced in (currently commented in the example)+myOptions :: Options+myOptions = defaultOptions { fieldLabelModifier = rmUnderscore }+ where+ rmUnderscore ('_':str) = DT.unpack . DT.toUpper . DT.pack $ str+ rmUnderscore str = str++instance ToNamedRecord Salaries where+ toNamedRecord = genericToNamedRecord myOptions++instance FromNamedRecord Salaries where+ parseNamedRecord = genericParseNamedRecord myOptions++main :: IO ()+main = do+ v <- loadData "data/salaries_simple.csv" :: IO (V.Vector Salaries)+ v1 <- loadData "data/salaries_mixed_input.csv" :: IO (V.Vector SalariesMixed)+ putStrLn . show $ v+ putStrLn . show $ v1++```+++# Caveats (Or list of future enhancements)++1. The columns names along with prefix should be valid Haskell field+ names. For example, column names cannot have spaces or other+ characters not supported by ```field``` names are not supported.+2. The library loads the whole file during compilation to infer+ types. Given the size of the file, this will increase the compile+ time. Alternative workflows, like stripping the file or dumping the+ created slice into a file is recommended. In the future, the+ makeCsvRecord function can take a parameter to specify the minimum+ number of rows that can be used to infer the types.+3. Supported types are limited. Text, Bool, Integer, Double and the MayBe+ variants of those.+4. Mixed case column headers not automatically supported. A more+ complex form of ```fieldOptionModifiers``` needs to be provided.+5. Currently no options to provide custom types.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ app/Main.hs view
@@ -0,0 +1,42 @@+{-#LANGUAGE TemplateHaskell#-}+{-#LANGUAGE DeriveGeneric #-}+{-#LANGUAGE ScopedTypeVariables #-}+{-#LANGUAGE DuplicateRecordFields #-}+{-# LANGUAGE DeriveDataTypeable #-}++module Main where++import Data.Cassava.Records+import Data.Csv+import qualified Data.ByteString.Lazy as BL+import Data.Vector as V+import Data.Text as DT+import qualified Text.PrettyPrint.Tabulate as T+import Language.Haskell.TH+-- import Control.Lens hiding (element)++$(makeCsvRecord "Salaries" "data/salaries_simple.csv" "_" commaOptions)+-- $(makeInstance "Salaries")++$(makeCsvRecord "SalariesMixed" "data/salaries_mixed_input.csv" "_" commaOptions)+$(makeInstance "SalariesMixed")++-- the following instance is not required, if $(makeInstance) call is spliced in+myOptions :: Options+myOptions = defaultOptions { fieldLabelModifier = rmUnderscore }+ where+ rmUnderscore ('_':str) = DT.unpack . DT.toUpper . DT.pack $ str+ rmUnderscore str = str++instance ToNamedRecord Salaries where+ toNamedRecord = genericToNamedRecord myOptions++instance FromNamedRecord Salaries where+ parseNamedRecord = genericParseNamedRecord myOptions++main :: IO ()+main = do+ v <- loadData "data/salaries_simple.csv" :: IO (V.Vector Salaries)+ v1 <- loadData "data/salaries_mixed_input.csv" :: IO (V.Vector SalariesMixed)+ putStrLn . show $ v+ putStrLn . show $ v1
+ cassava-records.cabal view
@@ -0,0 +1,98 @@+-- This file has been generated from package.yaml by hpack version 0.20.0.+--+-- see: https://github.com/sol/hpack+--+-- hash: f08ce1b824d1734099491f8c04b5ca3c74288ce2eacefe0c7cbd17028152ea09++name: cassava-records+version: 0.1.0.0+synopsis: Auto-generation of records data type.+description: cassava-records library helps in auto-creating record data types using Template Haskell by inferring types from the columns of a csv or compatible input file. The record and type classes instances generated can be seamlessly used with cassava(the haskell csv reader library) to load the data into these record types without dealing with any other level of abstraction.+ Please see README on Github at <https://github.com/gdevanla/cassava-records#readme>+category: Text, Web, CSV+homepage: https://github.com/gdevanla/cassava-records#readme+bug-reports: https://github.com/gdevanla/cassava-records/issues+author: Guru Devanla+maintainer: grdvnl@gmail.com+copyright: 2017 Author name here+license: BSD3+license-file: LICENSE+build-type: Simple+cabal-version: >= 1.10++extra-source-files:+ ChangeLog.md+ README.md++source-repository head+ type: git+ location: https://github.com/gdevanla/cassava-records++library+ hs-source-dirs:+ src+ build-depends:+ attoparsec+ , base >=4.7 && <5+ , bytestring+ , cassava+ , foldl+ , template-haskell+ , text+ , unordered-containers+ , vector+ exposed-modules:+ Data.Cassava.Internal.RecordBuilder+ Data.Cassava.Records+ other-modules:+ Paths_cassava_records+ default-language: Haskell2010++executable cassava-records-exe+ main-is: Main.hs+ hs-source-dirs:+ app+ ghc-options: -threaded -rtsopts -with-rtsopts=-N+ build-depends:+ attoparsec+ , base >=4.7 && <5+ , bytestring+ , cassava+ , cassava-records+ , foldl+ , lens+ , pptable+ , template-haskell+ , text+ , unordered-containers+ , vector+ other-modules:+ Paths_cassava_records+ default-language: Haskell2010++test-suite cassava-records-test+ type: exitcode-stdio-1.0+ main-is: Spec.hs+ hs-source-dirs:+ test+ ghc-options: -threaded -rtsopts -with-rtsopts=-N+ build-depends:+ HUnit+ , QuickCheck+ , attoparsec+ , base >=4.7 && <5+ , bytestring+ , cassava+ , cassava-records+ , containers+ , foldl+ , tasty+ , tasty-hunit+ , tasty-quickcheck+ , template-haskell+ , text+ , unordered-containers+ , vector+ other-modules:+ Paths_cassava_records+ default-language: Haskell2010
+ src/Data/Cassava/Internal/RecordBuilder.hs view
@@ -0,0 +1,175 @@+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE OverloadedStrings #-}++{-|+Module : RecordBuilder.hs+Description : Using Template Haskell this module auto create Record+ types by inferring types from the provided csv or tab separated file.+Copyright : (c) Guru Devanla 2018+License : MIT+Maintainer : grdvnl@gmail.com+Stability : experimental+++This module provides an easy way to explore input files that may have numerous columns+by helping create a Record types by guessing the types. That information can be used+as is or persisted to a file so that other customizations can be performed.+-}++module Data.Cassava.Internal.RecordBuilder where++import Control.Monad+import Language.Haskell.TH+import Language.Haskell.TH.Syntax+import qualified Data.ByteString as BL+import qualified Data.ByteString.Lazy as BLZ+import qualified Data.ByteString.Char8 as BC+import Data.Csv.Parser as CP+import qualified Data.Csv as Csv+import qualified Data.Vector as V+import Data.List as L+import Data.HashMap.Strict as H+import Data.Csv hiding(Name)+import Data.Attoparsec.ByteString as P+import Data.Attoparsec.Text as AT+import Data.String+import Text.Read+import qualified Data.Char as DC+import GHC.Generics (Generic)+import Data.Text as DT+import qualified Data.Text.Encoding as DTE+import Data.Data++{-| Create a field name and type tuple that will be used with RecC to+create a Record.+-}+makeField:: BC.ByteString -> Type -> String -> (Name, Bang, Type)+makeField fname ftype prefix = (+ mkName fname', defaultBang , ftype)+ where+ defaultBang = Bang NoSourceUnpackedness NoSourceStrictness+ fname' = prefix ++ DT.unpack (DT.toLower $ DTE.decodeUtf8 fname)++-- Create the list of fields that will form a Record+makeFields:: V.Vector (BC.ByteString, Type) -> String -> V.Vector (Name, Bang, Type)+makeFields fnames_types prefix = V.map makeField' fnames_types+ where+ makeField' (f, t) = makeField f t prefix++-- Return the expression that contains the Record declaration+makeRecord :: String -> V.Vector (Name, Bang, Type) -> DecsQ+makeRecord record_name fields = do+ let record_name' = mkName record_name+ recc = RecC record_name' $ V.toList fields+ deriv = [DerivClause Nothing [ConT ''Show, ConT ''Generic, ConT ''Data]]+ r = DataD [] record_name' [] Nothing [recc] deriv+ return [r]++-- Parses the file and returns the Header and Data from he input file+createRecords ::+ BC.ByteString -> DecodeOptions -> (Header, V.Vector NamedRecord)+createRecords csvData options =+ let p = CP.csvWithHeader options+ e = P.parseOnly p csvData in+ case e of+ Right f -> f+ Left f -> fail $ "unable to parse" ++ f++-- Infer the type for the given column+inferColumnType :: BL.ByteString -> V.Vector BC.ByteString -> (BC.ByteString, Type)+inferColumnType header column = (header, inferMajorityType column)++-- Check to see if that numeric value can be an Integer+isInteger s = case reads s :: [(Integer, String)] of+ [(_, "")] -> True+ _ -> False++-- Check to see if the value can be a Double+isDouble s = case reads s :: [(Double, String)] of+ [(_, "")] -> True+ _ -> False++-- Check to see if value is Numeric+isNumeric :: String -> Bool+isNumeric s = isInteger s || isDouble s++-- Check to see if value can be a Bool+isBool :: String -> Bool+isBool c = let x = fmap DC.toLower c+ in+ x == "t" || x == "f" || x == "true" || x == "false"++{-|+Instances to support conversion to Bool type. Cassava currently does not+provide an instance for Bool.+-}+instance ToField Bool where+ toField True = "True"+ toField False = "False"++{-|+Instances to support creation of Bool type fields. Cassava currently does not+provide an instance for Bool.+-}+instance FromField Bool where+ parseField field = do+ let s' = DT.toLower . DTE.decodeUtf8 $ field+ if s' == "t" || s' == "True" then return True+ else return False++data Empty = Empty++maybeType ftype = AppT (ConT ''Maybe) (ConT ftype)++inferMajorityType :: V.Vector BC.ByteString -> Type+inferMajorityType column =+ majority_types types'+ where+ types = V.map find_types column+ types' = V.filter (\t -> t /= ''Empty) types+ non_types' = V.filter (\t -> t == ''Empty) types+ find_types c+ | isInteger (DT.unpack . DTE.decodeUtf8 $ c) = ''Integer+ | isDouble (DT.unpack . DTE.decodeUtf8 $ c) = ''Double+ | isBool (DT.unpack . DTE.decodeUtf8 $ c) = ''Bool+ | c == "" = ''Empty+ | otherwise = ''Text+ doubleOrInteger t = t == ''Double || t == ''Integer+ majority_types t1+ | V.all (\t -> t == ''Integer) t1 && V.length non_types' > 0 = maybeType ''Integer+ | V.all (\t -> t == ''Integer) t1 = ConT ''Integer+ | V.all doubleOrInteger t1 && V.length non_types' > 0 = maybeType ''Double+ | V.all doubleOrInteger t1 = ConT ''Double+ | V.all (\t -> t == ''Bool) t1 && V.length non_types' > 0 = maybeType ''Bool+ | V.all (\t -> t == ''Bool) t1 = ConT ''Bool+ | V.length non_types' > 0 = maybeType ''Text+ | otherwise = ConT ''Text+++collectColumns :: BL.ByteString -> V.Vector NamedRecord -> V.Vector BC.ByteString+collectColumns header = V.map (! header)++inferTypes :: Header -> V.Vector NamedRecord -> String -> V.Vector (Name, Bang, Type)+inferTypes headers named_records suffix =+ let columns = V.map (`collectColumns` named_records) headers+ fieldnames_types = makeFields (V.zipWith inferColumnType headers columns) suffix+ in+ fieldnames_types+++-- defaultFieldNameOptions :: Options+-- defaultFieldNameOptions = defaultOptions { fieldLabelModifier = rmUnderscore }+-- where+-- rmUnderscore ('_':str) = DT.unpack . DT.pack $ str+-- rmUnderscore str = str+++-- makeInstance :: String -> DecsQ+-- makeInstance recordName = [d|+-- instance ToNamedRecord $(conT (mkName recordName)) where+-- toNamedRecord = genericToNamedRecord $ defaultFieldNameOptions+-- instance FromNamedRecord $(conT (mkName recordName)) where+-- parseNamedRecord = genericParseNamedRecord $ defaultFieldNameOptions+-- instance DefaultOrdered $(conT (mkName recordName)) where+-- headerOrder = genericHeaderOrder $ defaultFieldNameOptions+-- |]
+ src/Data/Cassava/Records.hs view
@@ -0,0 +1,141 @@+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE OverloadedStrings #-}++{-|+Module : Records.hs+Description : Using Template Haskell this module auto create Record+ types by inferring types from the provided csv or tab separated file.+Copyright : (c) Guru Devanla 2018+License : MIT+Maintainer : grdvnl@gmail.com+Stability : experimental+++This module provides an easy way to explore input files that may have numerous columns+by helping create a Record types by guessing the types. That information can be used+as is or persisted to a file so that other customizations can be performed.+-}++module Data.Cassava.Records+ (+ -- ** Creating Record types+ -- $makeCsvRecord+ makeCsvRecord+ -- $commaOptions+ , commaOptions+ -- $tabOptions+ , tabOptions+ -- $makeInstance+ , makeInstance+ -- $loadData+ , loadData+ )+where++import Control.Monad+import Language.Haskell.TH+import Language.Haskell.TH.Syntax+import qualified Data.ByteString as BL+import qualified Data.ByteString.Lazy as BLZ+import qualified Data.ByteString.Char8 as BC+import Data.Csv.Parser as CP+import qualified Data.Csv as Csv+import qualified Data.Vector as V+import Data.List as L+import Data.HashMap.Strict as H+import Data.Csv hiding(Name)+import Data.Attoparsec.ByteString as P+import Data.Attoparsec.Text as AT+import Data.String+import Text.Read+import qualified Data.Char as DC+import GHC.Generics (Generic)+import Data.Text as DT+import qualified Data.Text.Encoding as DTE+import Data.Data++import Data.Cassava.Internal.RecordBuilder+++defaultFieldNameOptions :: Options+defaultFieldNameOptions = defaultOptions { fieldLabelModifier = rmUnderscore }+ where+ rmUnderscore ('_':str) = DT.unpack . DT.pack $ str+ rmUnderscore str = str++{-| Convinience method that creates the default instances required by+Cassava. The generated methods assumed fields are prefixed with "_".++For example, if the column header in the input file have upper case or mixed case+the names will not directly match with field names in the record. In that case+explicity instances have to be provided manually and the field modifiers provided accordingly.++For example, if the columns in the input file have all headers listed in upper case,+since the field names are all lower case, the defaultFieldNameOptions function would look like+this++@++defaultFieldNameOptions :: Options+defaultFieldNameOptions = defaultOptions { fieldLabelModifier = rmUnderscore }+ where+ rmUnderscore ('_':str) = DT.unpack . DT.toUpper . DT.pack $ str+ rmUnderscore str = str+@++Note the DT.toUpper call to convert the field names to upper case before comparing to+'NamedRecords'++-}+makeInstance :: String -- ^ name of record for which the instance needs to be created+ -> DecsQ+makeInstance recordName = [d|+ instance ToNamedRecord $(conT (mkName recordName)) where+ toNamedRecord = genericToNamedRecord defaultFieldNameOptions+ instance FromNamedRecord $(conT (mkName recordName)) where+ parseNamedRecord = genericParseNamedRecord defaultFieldNameOptions+ instance DefaultOrdered $(conT (mkName recordName)) where+ headerOrder = genericHeaderOrder defaultFieldNameOptions+ |]+++-- $tabOptions+{-| Provides a default 'DecodeOptions' for tab separated input files+-}+tabOptions :: DecodeOptions+tabOptions = defaultDecodeOptions {+ decDelimiter = fromIntegral (DC.ord '\t')+ }++-- $commaOptions+{-| Provides a default 'DecodeOptions' for comma separated input files+-}+commaOptions :: DecodeOptions+commaOptions = defaultDecodeOptions++-- $makeCsvRecord+{-|+Makes the Record that reflects the types inferred from the input file.+-}+makeCsvRecord :: String -- ^ Name to use for the Record type being created+ -> FilePath -- ^ File path of input file+ -> String -- ^ Prefix to be used to field names. Recommended to use "_" to work well with Lens+ -> DecodeOptions -- ^ 'DecodeOptions' as required by Cassava to read the input file+ -> DecsQ+makeCsvRecord recordName fileName prefix decodeOptions = do+ csvData <- runIO $ BL.readFile fileName+ let (headers, named_records) = createRecords csvData decodeOptions+ makeRecord recordName (inferTypes headers named_records prefix)++-- $loadData+{-|+Helper function to load the data from the the provided file path+-}+loadData :: (FromNamedRecord a)+ => FilePath -- ^ Path of the file to be loaded+ -> IO (V.Vector a) -- ^ a will be of a Record type+loadData file_path = do+ csvData <- BLZ.readFile file_path+ case decodeByName csvData of+ Left err -> fail ("Faled to load" Prelude.++ err)+ Right (_, v) -> return v
+ test/Spec.hs view
@@ -0,0 +1,65 @@+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE OverloadedStrings #-}++import Data.Map as M+import Control.Monad+import Test.Tasty+import Test.Tasty.HUnit+import qualified Data.Vector as V+import qualified Data.ByteString.Lazy as BLZ+import Language.Haskell.TH+import Data.Text as DT++import Data.Cassava.Records+import Data.Cassava.Internal.RecordBuilder+++-- expectedRecord record_name name_type = RecC (record_name') fields++-- expectedDeriv = [DerivClause Nothing [ConT ''Show, ConT ''Generic, ConT ''Data]]++-- expectedData record_name name_type =+-- DataD [] record_name [] Nothing expectedRecord name_type++testSimple = testCase "testSimpleInput"+ (+ do+ let expected_types = V.fromList [+ ("emp_no", ConT ''Integer),+ ("name", ConT ''DT.Text),+ ("salary", ConT ''Double),+ ("status", ConT ''Bool),+ ("years", ConT ''Double)]+ let record_name = "TestSimpleR"+ let expected = makeRecord record_name $ makeFields expected_types "_"+ let qdec = makeCsvRecord record_name "test/data/salaries_simple.csv" "_" commaOptions+ dec <- runQ qdec+ expected_dec <- (runQ expected)+ assertEqual "Test Simple" (Prelude.head dec) (Prelude.head expected_dec)+ )+++testMixedInput = testCase "testMixedInput"+ (+ do+ let expected_types = V.fromList [+ ("emp_no", ConT ''Integer),+ ("name", maybeType ''DT.Text),+ ("salary", maybeType ''Double),+ ("status", maybeType ''Bool),+ ("years", maybeType ''Double)]+ let record_name = "TestMixedR"+ let expected = makeRecord record_name $ makeFields expected_types "_"+ let qdec = makeCsvRecord record_name "test/data/salaries_mixed_input.csv" "_" commaOptions+ dec <- runQ qdec+ expected_dec <- (runQ expected)+ assertEqual "Test Maybe Columns" (Prelude.head dec) (Prelude.head expected_dec)+ )++tests :: TestTree+tests = testGroup "Tests" [+ testSimple,+ testMixedInput+ ]++main = defaultMain tests