dataframe-persistent-0.1.0.0: src/DataFrame/IO/Persistent/TH.hs
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeApplications #-}
module DataFrame.IO.Persistent.TH (
-- * Template Haskell derivation
deriveEntityToDataFrame,
deriveDataFrameToEntity,
derivePersistentDataFrame,
) where
import Control.Monad (forM, when)
import Data.List (foldl')
import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Vector as V
import DataFrame.IO.Persistent
import qualified DataFrame.Internal.Column as DFCol
import Database.Persist
import Database.Persist.Sql (fromSqlKey)
import Database.Persist.TH
import Language.Haskell.TH
import Language.Haskell.TH.Syntax (Lift, lift)
-- | Derive EntityToDataFrame instance using Template Haskell
deriveEntityToDataFrame :: Name -> Q [Dec]
deriveEntityToDataFrame entityName = do
info <- reify entityName
case info of
TyConI (DataD _ _ _ _ [RecC conName fields] _) -> do
-- Extract field names and remove entity prefix
let entityNameStr = nameBase entityName
entityPrefix = camelToSnake entityNameStr ++ "_"
fieldNames = [nameBase fname | (fname, _, _) <- fields]
-- Remove the entity prefix from field names
cleanFieldNames = map (removePrefix entityPrefix . camelToSnake) fieldNames
textFieldNames = map T.pack cleanFieldNames
-- Generate entityColumnNames implementation
colNamesImpl <- [|$(lift textFieldNames)|]
let colNamesMethod =
FunD
'entityColumnNames
[Clause [WildP] (NormalB colNamesImpl) []]
-- Generate entityToColumnData implementation
entityDataLambda <- generateEntityToColumnData conName fields cleanFieldNames
let entityVar = mkName "entity"
entityDataImpl = AppE entityDataLambda (VarE entityVar)
entityDataMethod =
FunD
'entityToColumnData
[Clause [VarP entityVar] (NormalB entityDataImpl) []]
-- Create instance
let instanceDec =
InstanceD
Nothing
[]
(AppT (ConT ''EntityToDataFrame) (ConT entityName))
[colNamesMethod, entityDataMethod]
return [instanceDec]
_ ->
fail $
"deriveEntityToDataFrame: " ++ show entityName ++ " must be a record type"
-- | Remove prefix from a string
removePrefix :: String -> String -> String
removePrefix prefix str
| take (length prefix) str == prefix = drop (length prefix) str
| otherwise = str
-- | Generate the entityToColumnData expression
generateEntityToColumnData :: Name -> [(Name, Bang, Type)] -> [String] -> Q Exp
generateEntityToColumnData conName fields cleanFieldNames = do
entityVar <- newName "entity"
keyVar <- newName "key"
valVar <- newName "val"
-- Pattern to destructure Entity
let entityPat = ConP 'Entity [] [VarP keyVar, VarP valVar]
-- Generate field extractors
fieldExprs <-
mapM (generateFieldExtractor valVar) (zip3 fields cleanFieldNames [0 ..])
-- Build the final list expression with ID column if needed
let idExpr =
TupE
[ Just (LitE (StringL "id"))
, Just
( AppE
(ConE 'SomeColumn)
( AppE
(VarE 'V.singleton)
(VarE keyVar)
)
)
]
allExprs = idExpr : fieldExprs
-- Return lambda that pattern matches on Entity
return $ LamE [entityPat] (ListE allExprs)
-- | Generate field extractor for a single field
generateFieldExtractor :: Name -> ((Name, Bang, Type), String, Int) -> Q Exp
generateFieldExtractor valVar ((fieldName, _, fieldType), cleanName, idx) = do
let accessor = mkName (nameBase fieldName)
value = AppE (VarE accessor) (VarE valVar)
-- Wrap the value in SomeColumn after converting to vector
return $
TupE
[ Just (LitE (StringL cleanName))
, Just
( AppE
(ConE 'SomeColumn)
(AppE (VarE 'V.singleton) value)
)
]
-- | Derive DataFrameToEntity instance
deriveDataFrameToEntity :: Name -> Q [Dec]
deriveDataFrameToEntity entityName = do
info <- reify entityName
case info of
TyConI (DataD _ _ _ _ [RecC conName fields] _) -> do
-- Generate rowToEntity implementation
rowToEntityImpl <- generateRowToEntity conName fields
let rowToEntityMethod =
FunD
'rowToEntity
[ Clause
[VarP (mkName "idx"), VarP (mkName "df")]
(NormalB rowToEntityImpl)
[]
]
-- Create instance
let instanceDec =
InstanceD
Nothing
[]
(AppT (ConT ''DataFrameToEntity) (ConT entityName))
[rowToEntityMethod]
return [instanceDec]
_ ->
fail $
"deriveDataFrameToEntity: " ++ show entityName ++ " must be a record type"
-- | Generate the rowToEntity expression
generateRowToEntity :: Name -> [(Name, Bang, Type)] -> Q Exp
generateRowToEntity conName fields = do
-- This is a simplified implementation
-- In practice, you'd need to extract values from the DataFrame
[|Left "Auto-generated rowToEntity not fully implemented"|]
-- | Derive both instances at once
derivePersistentDataFrame :: Name -> Q [Dec]
derivePersistentDataFrame name = do
entityDecs <- deriveEntityToDataFrame name
dfDecs <- deriveDataFrameToEntity name
return (entityDecs ++ dfDecs)
-- | Convert camelCase to snake_case
camelToSnake :: String -> String
camelToSnake [] = []
camelToSnake (c : cs) = toLower c : go cs
where
go [] = []
go (c : cs)
| isUpper c = '_' : toLower c : go cs
| otherwise = c : go cs
isUpper c = c >= 'A' && c <= 'Z'
toLower c = if isUpper c then toEnum (fromEnum c + 32) else c