wireform-arrow-0.1.0.0: src/Arrow/Record/TH.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}
{- | Template Haskell deriver for 'Arrow.Record.Table'.
When a record type doesn't have a 'GHC.Generics.Generic'
instance, or callers want explicit control over column names
(e.g. @"user_id"@ for a selector named @userId@), use the
'deriveTable' splice instead of @'Arrow.Record.Generic.genericTable'@.
@
{\-# LANGUAGE TemplateHaskell #-\}
import Arrow.Record.TH
data Trade = Trade { sym :: Text, qty :: Int32, note :: Maybe Text }
tradeTable :: 'Table' Trade
tradeTable = $('deriveTable' ''Trade)
@
or with a custom column-name transformer (e.g. snake_case):
@
tradeTable :: 'Table' Trade
tradeTable = $('deriveTableWith' toSnakeCase ''Trade)
where
toSnakeCase = ... :: String -> String
@
The splice emits code equivalent to a hand-written
'Arrow.Record.table' expression: 'Arrow.Record.fieldE' for
every record selector, 'Arrow.Record.columnD' on the decoder
side. Field types must have 'HasEncoder' / 'HasDecoder'
instances from "Arrow.Record.Generic".
-}
module Arrow.Record.TH (
deriveTable,
deriveTableWith,
) where
import Arrow.Record (
columnD,
fieldE,
table,
)
import Arrow.Record.Generic (HasDecoder (hasDecoder), HasEncoder (hasEncoder))
import Data.Text qualified as T
import Language.Haskell.TH
{- | Derive a 'Table' for the named record type, using the
record selectors as column names verbatim.
-}
deriveTable :: Name -> Q Exp
deriveTable = deriveTableWith id
{- | Like 'deriveTable' but renames columns via the supplied
pure function. Common choices: @'map' 'Data.Char.toLower'@,
a snake-case converter, or a full alias map via
@\\n -> 'Data.Map.findWithDefault' n n aliasMap@.
-}
deriveTableWith :: (String -> String) -> Name -> Q Exp
deriveTableWith renameField tyName = do
info <- reify tyName
fields <- case info of
TyConI (DataD _ _ _ _ [RecC _ fs] _) -> pure fs
TyConI (NewtypeD _ _ _ _ (RecC _ fs) _) -> pure fs
_ ->
fail $
"Arrow.Record.TH.deriveTable: "
++ show tyName
++ " must be a single-constructor record type"
-- Build the RowEncoder: mconcat of fieldE "name" selector hasEncoder.
let mkEncoderPart (selName', _, _) = do
let colName = renameField (nameBase selName')
[|fieldE (T.pack colName) $(varE selName') hasEncoder|]
encoderParts <- mapM mkEncoderPart fields
let encoderExp = foldr1 (\a b -> [|$a <> $b|]) (map pure encoderParts)
-- Build the RowDecoder: Ctor <$> columnD "name1" hasDecoder <*> ...
let ctorName = case info of
TyConI (DataD _ _ _ _ [RecC c _] _) -> c
TyConI (NewtypeD _ _ _ _ (RecC c _) _) -> c
_ -> error "unreachable: validated above"
let mkDecoderPart (selName', _, _) =
let colName = renameField (nameBase selName')
in [|columnD (T.pack colName) hasDecoder|]
initial = [|pure $(conE ctorName)|]
combine acc step = [|$acc <*> $step|]
decoderExp <- foldl combine initial (map mkDecoderPart fields)
encoderExp' <- encoderExp
-- Build: table encoderExp decoderExp
[|table $(pure encoderExp') $(pure decoderExp)|]