{-# LANGUAGE CPP, DataKinds, GADTs, KindSignatures, OverloadedStrings,
QuasiQuotes, RecordWildCards, RoleAnnotations,
ScopedTypeVariables, TemplateHaskell, TupleSections,
TypeApplications, TypeOperators #-}
module Frames.TH where
import Control.Arrow (second)
import Data.Char (toLower)
import Data.Maybe (fromMaybe)
#if __GLASGOW_HASKELL__ < 804
import Data.Semigroup ((<>))
#endif
import Data.Proxy (Proxy(..))
import qualified Data.Text as T
import Data.Vinyl
import Data.Vinyl.TypeLevel (RIndex)
import Frames.Col ((:->))
import Frames.ColumnTypeable
import Frames.ColumnUniverse
import Frames.CSV
import Frames.Rec(Record)
import Frames.Utils
import qualified GHC.Types as GHC
import Language.Haskell.TH
import Language.Haskell.TH.Syntax
import qualified Pipes as P
import qualified Pipes.Prelude as P
import qualified Pipes.Safe as P
recDec :: [Type] -> Type
recDec = AppT (ConT ''Record) . go
where go [] = PromotedNilT
go (t:cs) = AppT (AppT PromotedConsT t) (go cs)
mkColSynDec :: TypeQ -> Name -> DecQ
mkColSynDec colTypeQ colTName = tySynD colTName [] colTypeQ
mkColLensDec :: Name -> Type -> T.Text -> DecsQ
mkColLensDec colTName colTy colPName = sequenceA [tySig, val, tySig', val']
where nm = mkName $ T.unpack colPName
nm' = mkName $ T.unpack colPName <> "'"
tySig = sigD nm [t|forall f rs.
(Functor f,
RElem $(conT colTName) rs (RIndex $(conT colTName) rs))
=> ($(pure colTy) -> f $(pure colTy))
-> Record rs
-> f (Record rs)
|]
tySig' = sigD nm' [t|forall f g rs.
(Functor f,
RElem $(conT colTName) rs (RIndex $(conT colTName) rs))
=> (g $(conT colTName) -> f (g $(conT colTName)))
-> Rec g rs
-> f (Rec g rs)
|]
val = valD (varP nm)
(normalB [e|rlens @($(conT colTName)) . rfield |])
[]
val' = valD (varP nm')
(normalB [e|rlens' @($(conT colTName))|])
[]
lowerHead :: T.Text -> Maybe T.Text
lowerHead = fmap aux . T.uncons
where aux (c,t) = T.cons (toLower c) t
colDec :: T.Text -> String -> T.Text
-> (Either (String -> Q [Dec]) Type)
-> Q (Type, [Dec])
colDec prefix rowName colName colTypeGen = do
(colTy, extraDecs) <- either colDecsHelper (pure . (,[])) colTypeGen
let colTypeQ = [t|$(litT . strTyLit $ T.unpack colName) :-> $(return colTy)|]
syn <- mkColSynDec colTypeQ colTName'
lenses <- mkColLensDec colTName' colTy colPName
return (ConT colTName', syn : extraDecs ++ lenses)
where colTName = sanitizeTypeName (prefix <> capitalize1 colName)
colPName = fromMaybe "colDec impossible" (lowerHead colTName)
colTName' = mkName $ T.unpack colTName
colDecsHelper f =
let qualName = rowName ++ T.unpack (capitalize1 colName)
in (ConT (mkName qualName),) <$> f qualName
declareColumn :: T.Text -> Name -> DecsQ
declareColumn = flip declarePrefixedColumn T.empty
declarePrefixedColumn :: T.Text -> T.Text -> Name -> DecsQ
declarePrefixedColumn colName prefix colTypeName =
(:) <$> mkColSynDec colTypeQ colTName'
<*> mkColLensDec colTName' colTy colPName
where prefix' = capitalize1 prefix
colTName = sanitizeTypeName (prefix' <> capitalize1 colName)
colPName = fromMaybe "colDec impossible" (lowerHead colTName)
colTName' = mkName $ T.unpack colTName
colTy = ConT colTypeName
colTypeQ = [t|$(litT . strTyLit $ T.unpack colName) :-> $(return colTy)|]
data RowGen (a :: [GHC.Type]) =
RowGen { columnNames :: [String]
, tablePrefix :: String
, separator :: Separator
, rowTypeName :: String
, columnUniverse :: Proxy a
, lineReader :: Separator -> P.Producer [T.Text] (P.SafeT IO) ()
}
rowGen :: FilePath -> RowGen CommonColumns
rowGen = RowGen [] "" defaultSep "Row" Proxy . produceTokens
rowGenCat :: FilePath -> RowGen CommonColumnsCat
rowGenCat = RowGen [] "" defaultSep "Row" Proxy . produceTokens
tableTypes :: String -> FilePath -> DecsQ
tableTypes n fp = tableTypes' (rowGen fp) { rowTypeName = n }
prefixSize :: Int
prefixSize = 1000
colNamesP :: Monad m => P.Producer [T.Text] m () -> m [T.Text]
colNamesP src = either (const []) fst <$> P.next src
tableTypesText' :: forall a c.
(c ~ CoRec ColInfo a, ColumnTypeable c, Monoid c)
=> RowGen a -> DecsQ
tableTypesText' (RowGen {..}) =
do colNames <- runIO . P.runSafeT $
maybe (colNamesP (lineReader separator))
pure
(headerOverride opts)
let headers = zip colNames (repeat (ConT ''T.Text))
(colTypes, colDecs) <- (second concat . unzip)
<$> mapM (uncurry mkColDecs) headers
let recTy = TySynD (mkName rowTypeName) [] (recDec colTypes)
optsName = case rowTypeName of
[] -> error "Row type name shouldn't be empty"
h:t -> mkName $ toLower h : t ++ "Parser"
optsTy <- sigD optsName [t|ParserOptions|]
optsDec <- valD (varP optsName) (normalB $ lift opts) []
return (recTy : optsTy : optsDec : colDecs)
where colNames' | null columnNames = Nothing
| otherwise = Just (map T.pack columnNames)
opts = ParserOptions colNames' separator (RFC4180Quoting '\"')
mkColDecs colNm colTy = do
let safeName = T.unpack (sanitizeTypeName colNm)
mColNm <- lookupTypeName (tablePrefix ++ safeName)
case mColNm of
Just n -> pure (ConT n, [])
Nothing -> colDec (T.pack tablePrefix) rowTypeName colNm (Right colTy)
tableTypes' :: forall a c. (c ~ CoRec ColInfo a, ColumnTypeable c, Monoid c)
=> RowGen a -> DecsQ
tableTypes' (RowGen {..}) =
do headers <- runIO . P.runSafeT
$ readColHeaders opts lineSource :: Q [(T.Text, c)]
(colTypes, colDecs) <- (second concat . unzip)
<$> mapM (uncurry mkColDecs)
(map (second colType) headers)
let recTy = TySynD (mkName rowTypeName) [] (recDec colTypes)
optsName = case rowTypeName of
[] -> error "Row type name shouldn't be empty"
h:t -> mkName $ toLower h : t ++ "Parser"
optsTy <- sigD optsName [t|ParserOptions|]
optsDec <- valD (varP optsName) (normalB $ lift opts) []
return (recTy : optsTy : optsDec : colDecs)
where colNames' | null columnNames = Nothing
| otherwise = Just (map T.pack columnNames)
opts = ParserOptions colNames' separator (RFC4180Quoting '\"')
lineSource = lineReader separator P.>-> P.take prefixSize
mkColDecs :: T.Text -> Either (String -> Q [Dec]) Type -> Q (Type, [Dec])
mkColDecs colNm colTy = do
let safeName = tablePrefix ++ (T.unpack . sanitizeTypeName $ colNm)
mColNm <- lookupTypeName safeName
case mColNm of
Just n -> pure (ConT n, [])
Nothing -> colDec (T.pack tablePrefix) rowTypeName colNm colTy