rowrecord (empty) → 0.1
raw patch · 5 files changed
+264/−0 lines, 5 filesdep +basedep +containersdep +template-haskellsetup-changed
Dependencies added: base, containers, template-haskell
Files
- LICENSE +26/−0
- Setup.hs +4/−0
- Text/RowRecord.hs +131/−0
- Text/RowRecord/TH.hs +68/−0
- rowrecord.cabal +35/−0
+ LICENSE view
@@ -0,0 +1,26 @@+Copyright (c) Keegan McAllister 2010++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions+are met:+1. Redistributions of source code must retain the above copyright+ notice, this list of conditions and the following disclaimer.+2. Redistributions in binary form must reproduce the above copyright+ notice, this list of conditions and the following disclaimer in the+ documentation and/or other materials provided with the distribution.+3. Neither the name of the author nor the names of his contributors+ may be used to endorse or promote products derived from this software+ without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS''+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE+DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE FOR ANY+DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ Setup.hs view
@@ -0,0 +1,4 @@+#! /usr/bin/runhaskell++import Distribution.Simple+main = defaultMain
+ Text/RowRecord.hs view
@@ -0,0 +1,131 @@+{-# LANGUAGE+ ViewPatterns+ , TypeSynonymInstances #-}++-- | Convert lists of strings to records.++module Text.RowRecord+ ( -- * Tables+ Column+ , Row+ , Table+ , fromStrings++ -- * Parsing fields+ , Result (..)+ , RowError(..)+ , Field (..)+ , require+ , safeRead+ , getField++ -- * Parsing tables+ , ParseRow(..)+ , parseTable+ ) where++import Control.Applicative+import Control.Monad++import qualified Data.Map as M++-- | Identifies a column.+type Column = String++-- | A row of @'String'@ data.+type Row = M.Map Column String++-- | A table.+type Table = [Row]++-- | Convert a list of @'String'@ rows into a @'Table'@.+-- Uses the first row as column names.+fromStrings :: [[String]] -> Result Table+fromStrings [] = Failure $ MissingField "header"+fromStrings (h:xs) = Success $ map (M.fromList . zip h) xs++-- | A parse result.+data Result a+ = Success a+ | Failure RowError+ deriving (Show)++-- | Possible errors from parsing a row.+data RowError+ = MissingField Column+ | NoParse Column String+ deriving (Show)++instance Functor Result where+ fmap f (Success v) = Success $ f v+ fmap _ (Failure e) = Failure e++instance Monad Result where+ return = Success+ Success x >>= f = f x+ Failure e >>= _ = Failure e++instance Applicative Result where+ pure = return+ (<*>) = ap++-- | Class of field types which can be decoded from @'String'@.+--+-- The input can be @'Nothing'@ to represent a missing field.+-- The instance @Field a => Field (Maybe a)@ models optional fields.+--+-- If your record contains custom types, you must create a @'Field'@+-- instance for each. If you have base types but need different +-- parsing behavior, you can use a @newtype@ wrapper.+class Field a where+ decode :: Maybe String -> Result a++-- | @'read'@ in @'Maybe'@.+safeRead :: (Read a) => String -> Maybe a+safeRead (reads -> [(v,"")]) = Just v+safeRead _ = Nothing++-- | Implement @'decode'@ for a required field.+require :: (String -> Maybe a) -> Maybe String -> Result a+require _ Nothing = Failure $ MissingField ""+require f (Just (f -> Just v)) = Success v+require _ (Just xs) = Failure $ NoParse "" xs++instance Field Bool where decode = require safeRead+instance Field Int where decode = require safeRead+instance Field Integer where decode = require safeRead+instance Field Float where decode = require safeRead+instance Field Double where decode = require safeRead+instance Field String where decode = require Just++instance Field Char where+ decode = require f where+ f [x] = Just x+ f _ = Nothing++instance (Field a) => Field (Maybe a) where+ decode Nothing = Success Nothing+ decode (Just "") = Success Nothing+ decode (decode -> r) = Just <$> r++-- | Decode a field by column name.+--+-- Called from TH-generated code, but may be+-- useful independently.+getField :: (Field a) => Column -> Row -> Result a+getField k m = case decode $ M.lookup k m of+ v@(Success _) -> v+ Failure e -> Failure $ case e of+ MissingField _ -> MissingField k+ (NoParse _ s) -> NoParse k s++-- | Class of types which can be parsed from a @'Row'@.+-- These types are typically single-constructor records.+--+-- Instances may be generated using @Text.RowRecord.TH@.+class ParseRow a where+ parseRow :: Row -> Result a++-- | Parse a whole table.+parseTable :: (ParseRow a) => Table -> Result [a]+parseTable = mapM parseRow
+ Text/RowRecord/TH.hs view
@@ -0,0 +1,68 @@+{-# LANGUAGE+ TemplateHaskell+ , ViewPatterns+ , FlexibleInstances #-}++-- | Generate instances for converting lists of strings to records.++module Text.RowRecord.TH+ ( rowRecords+ ) where++import Text.RowRecord++import Control.Applicative++import Language.Haskell.TH+import Language.Haskell.TH.Syntax++-- Turn a record label name into a column name.+-- Drop everything through the first '_'.+getColumn :: Name -> Column+getColumn = f . nameBase where+ f (break (=='_') -> (_, ('_':xs))) = xs+ f n = error ("RowRecord: bad label: " ++ n)++-- | Generate a @'ParseRow'@ instance for each of the named types.+--+-- Each type must have exactly one constructor, in record style.+--+-- Column names are derived from the record field names by dropping the first+-- @'_'@-separated component. This allows for a prefix to disambiguate record+-- labels between types.+--+-- For example, with+--+-- > data Foo = Foo+-- > { f_bar :: String+-- > , f_baz :: Int }+-- > $(rowRecords [''Foo])+--+-- we can parse files of the form+-- +-- > bar,baz+-- > abc,3+-- > def,5+--+-- assuming an appropriate CSV parser.++rowRecords :: [Name] -> Q [Dec]+rowRecords ns = concat <$> mapM rowRecord ns++rowRecord :: Name -> Q [Dec]+rowRecord n = do+ i <- reify n+ case i of+ TyConI (DataD _ _ _ [c] _) -> fromCon n c+ TyConI (NewtypeD _ _ _ c _) -> fromCon n c+ _ -> error ("RowRecord: not a data type: " ++ show n)++fromCon :: Name -> Con -> Q [Dec]+fromCon ty (RecC ctor fields) =+ let gets m = [ [| getField $(lift $ getColumn n) $(varE m) |]+ | (n,_,_) <- fields ]+ splat a b = [| $a <*> $b |]+ bdy = [| \m -> $(foldl1 splat ([| pure $(conE ctor) |] : gets 'm)) |]+ in [d| instance ParseRow $(conT ty) where { parseRow = $bdy } |]++fromCon _ c = error ("RowRecord: not a record: " ++ show c)
+ rowrecord.cabal view
@@ -0,0 +1,35 @@+name: rowrecord+version: 0.1+license: BSD3+license-file: LICENSE+synopsis: Build records from lists of strings, as from CSV files.+category: Data, Text+author: Keegan McAllister <mcallister.keegan@gmail.com>+maintainer: Keegan McAllister <mcallister.keegan@gmail.com>+build-type: Simple+cabal-version: >=1.2+description:+ Given rows of @String@ data with column headings, this library will create+ values of user-defined record types. Records can contain mandatory or+ optional fields of any type, subject to a class constraint.+ Heading names and and record construction code are derived using+ Template Haskell.+ .+ One use case for this library is parsing records from a CSV file. A+ parser from CSV to @[[String]]@ is not included, but there are several+ suitable packages on Hackage.+ .+ The emphasis of this library is on simplicity of use rather than performance.+ It is likely to be suitable for a hundred thousand rows, but not many+ millions. A future version may support packed input formats like+ @ByteString@ or @Text@. Suggestions and patches are welcome.++library+ exposed-modules:+ Text.RowRecord+ , Text.RowRecord.TH+ ghc-options: -Wall+ build-depends:+ base >= 3 && < 5+ , template-haskell >= 2.4+ , containers >= 0.3