Frames-streamly-0.3.1.0: src/Frames/Streamly/Internal/CSV.hs
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveLift #-}
{-# LANGUAGE FlexibleContexts #-}
{-# Language GADTs #-}
{-# LANGUAGE TypeFamilies #-}
{-|
Module : Frames.Streamly.Internal.CSV
Description : Internal module to make the HeaderList opaque
Copyright : (c) Adam Conner-Sax 2021
License : BSD-3-Clause
Maintainer : adam_conner_sax@yahoo.com
Stability : experimental
-}
module Frames.Streamly.Internal.CSV
(
-- * Types
-- ** Text Wrappers
HeaderText(..)
, ColTypeName(..)
-- ** Type-Level representation for Column Id
, ColumnState(..)
, ColumnId(..)
, ColumnIdType
-- ** For RowGen
, RowGenColumnSelector(..)
, OrMissingWhen(..)
, MissingRequiredIdsF
-- ** For ParserOptions
, ParseColumnSelector(..)
-- * Combinators
-- ** Building RowGen Column Selectors
, modifyColumnSelector
-- ** Building ParserOptions from RowGen and File
, includedHeaders
, includedColTypeNames
, includedColTypeInfo
, colStatesAndHeadersToParseColHandler
-- * Exceptions
, FramesCSVException(..)
)
where
import Language.Haskell.TH.Syntax (Lift)
-- | Unite the various exceptions which might arise during TH inference of
-- column types or parsing of the file into a Frame.
data FramesCSVException =
EmptyStreamException
| MissingHeadersException [HeaderText]
| BadHeaderException Text
| WrongNumberColumnsException Text deriving Show
instance Exception FramesCSVException
-- | Type to control how column type inference is handled,
-- particularly in the presence of missing data
data OrMissingWhen = NeverMissing -- ^ Infer column type solely from non-missing data,
| AlwaysPossible -- ^ Infer column type as @Maybe a@ where @a@ is inferred from non-missing data.
| IfSomeMissing -- ^ Infer column type as @a@ if none is missing and @Maybe a@ otherwise.
deriving (Show, Eq, Lift)
-- | Wrapper for the text value of the column header to match on in column selection
newtype HeaderText = HeaderText { headerText :: Text } deriving (Show, Eq, Ord, Lift)
-- | Wrapper for the text of the type name we are generating for a column.
-- NB: This might be almost the same as 'HeaderText' (up to capitalization, etc.)
-- But it might be different:
-- 1. Due to prefixing or renaming.
-- 2. If we ignore the header or don't have one, then this name comes from the
-- column position rather than the header text.
newtype ColTypeName = ColTypeName { colTypeName :: Text } deriving (Show, Eq, Ord, Lift)
-- | Per-column indicator of exclusion or type-name to generate when included.
-- Isomorphic to @Maybe ColTypeName@ but clearer in use.
data ColumnState = Exclude | Include (ColTypeName, OrMissingWhen) deriving (Eq, Show, Lift)
-- | Type to index column selection and naming behavior
data ColumnId = ColumnByName | ColumnByPosition
-- | Map from 'ColumnId' type to the inhabited type used for
-- column inclusion/exclusion and type-naming.
type family ColumnIdType (a :: ColumnId) :: Type where
ColumnIdType 'ColumnByName = HeaderText
ColumnIdType 'ColumnByPosition = Int
type MissingRequiredIdsF (a :: ColumnId) = [ColumnIdType a] -> [ColumnIdType a]
-- For RowGen
-- | Type to specify how columns are selected when types are generated
-- by tableTypes. Types can be generated from header text or column position.
-- This type is parameterized by that choice.
data RowGenColumnSelector (a :: ColumnId) where
GenUsingHeader :: (HeaderText -> ColumnState) -> MissingRequiredIdsF 'ColumnByName -> RowGenColumnSelector 'ColumnByName
GenIgnoringHeader :: (Int -> ColumnState) -> MissingRequiredIdsF 'ColumnByPosition -> RowGenColumnSelector 'ColumnByPosition
GenWithoutHeader :: (Int -> ColumnState) -> MissingRequiredIdsF 'ColumnByPosition -> RowGenColumnSelector 'ColumnByPosition
-- | combinator to update or switch out the column selection function of a RowGenColumnSelector
modifyColumnSelector :: RowGenColumnSelector a
-> ((ColumnIdType a -> ColumnState) -> (ColumnIdType a -> ColumnState))
-> (MissingRequiredIdsF a -> MissingRequiredIdsF a)
-> RowGenColumnSelector a
modifyColumnSelector (GenUsingHeader f mr) g h = GenUsingHeader (g f) (h mr)
modifyColumnSelector (GenIgnoringHeader f mr) g h = GenIgnoringHeader (g f) (h mr)
modifyColumnSelector (GenWithoutHeader f mr) g h = GenWithoutHeader (g f) (h mr)
-- | Type to control how the csv parsers include/exclude columns
data ParseColumnSelector =
ParseAll Bool -- ^ True if there's a header and false if not
| ParseUsingHeader [HeaderText]
| ParseIgnoringHeader [ColumnState]
| ParseWithoutHeader [ColumnState] deriving (Lift, Show)
-- Helpers for generating the Correct ParseColumnSelector
includedHeaders :: [ColumnState] -> [HeaderText] -> [HeaderText]
includedHeaders cs hs = catMaybes $ fmap f $ zip hs cs where
f (_, Exclude) = Nothing
f (x, Include _) = Just x
{-# INLINEABLE includedHeaders #-}
includedColTypeNames :: [ColumnState] -> [ColTypeName]
includedColTypeNames = mapMaybe f where
f Exclude = Nothing
f (Include (x, _)) = Just x
{-# INLINEABLE includedColTypeNames #-}
includedColTypeInfo :: [ColumnState] -> [(ColTypeName, OrMissingWhen)]
includedColTypeInfo = mapMaybe f where
f Exclude = Nothing
f (Include x) = Just x
{-# INLINEABLE includedColTypeInfo #-}
colStatesAndHeadersToParseColHandler :: [ColumnState] -> [HeaderText] -> ParseColumnSelector
colStatesAndHeadersToParseColHandler cs hs = ParseUsingHeader $ includedHeaders cs hs
{-# INLINEABLE colStatesAndHeadersToParseColHandler #-}