composite-opaleye-0.5.4.0: src/Composite/Opaleye/TH.hs
{-# LANGUAGE CPP #-}
module Composite.Opaleye.TH where
import Composite.Opaleye.Util (constantColumnUsing)
import Control.Lens ((<&>))
import qualified Data.ByteString.Char8 as BSC8
import Data.Maybe (fromMaybe)
import Data.Monoid ((<>))
import Data.Profunctor.Product.Default (Default, def)
import Data.Traversable (for)
import Database.PostgreSQL.Simple (ResultError(ConversionFailed, Incompatible, UnexpectedNull))
import Database.PostgreSQL.Simple.FromField (FromField, fromField, typename, returnError)
import Language.Haskell.TH
( Q, Name, mkName, nameBase, newName, pprint, reify
, Info(TyConI), Dec(DataD), Con(NormalC)
, conT
, dataD, instanceD
, lamE, varE, caseE, conE
, conP, varP, wildP, litP, stringL
, caseE, match
, funD, clause
, normalB, normalGE, guardedB
, cxt
)
import Language.Haskell.TH.Syntax (lift)
import Opaleye
( Column, Constant, QueryRunnerColumnDefault, PGText, fieldQueryRunnerColumn, queryRunnerColumnDefault
)
-- |Derive the various instances required to make a Haskell enumeration map to a PostgreSQL @enum@ type.
--
-- In @deriveOpaleyeEnum ''HaskellType "sqltype" hsConToSqlValue@, @''HaskellType@ is the sum type (data declaration) to make instances for, @"sqltype"@ is
-- the PostgreSQL type name, and @hsConToSqlValue@ is a function to map names of constructors to SQL values.
--
-- The function @hsConToSqlValue@ is of the type @String -> Maybe String@ in order to make using 'stripPrefix' convenient. The function is applied to each
-- constructor name and for @Just value@ that value is used, otherwise for @Nothing@ the constructor name is used.
--
-- For example, given the Haskell type:
--
-- @
-- data MyEnum = MyFoo | MyBar
-- @
--
-- And PostgreSQL type:
--
-- @
-- CREATE TYPE myenum AS ENUM('foo', 'bar');
-- @
--
-- The splice:
--
-- @
-- deriveOpaleyeEnum ''MyEnum "myenum" ('stripPrefix' "my" . 'map' 'toLower')
-- @
--
-- Will create @PGMyEnum@ and instances required to use @MyEnum@ / @Column MyEnum@ in Opaleye.
--
-- The Haskell generated by this splice for the example is something like:
--
-- @
-- data PGMyEnum
--
-- instance 'FromField' MyEnum where
-- 'fromField' f mbs = do
-- tname <- 'typename' f
-- case mbs of
-- _ | tname /= "myenum" -> 'returnError' 'Incompatible' f ""
-- Just "foo" -> pure MyFoo
-- Just "bar" -> pure MyBar
-- Just other -> 'returnError' 'ConversionFailed' f ("Unexpected myenum value: " <> 'BSC8.unpack' other)
-- Nothing -> 'returnError' 'UnexpectedNull' f ""
--
-- instance 'QueryRunnerColumnDefault' PGMyEnum MyEnum where
-- queryRunnerColumnDefault = 'fieldQueryRunnerColumn'
--
-- instance 'Default' 'Constant' MyEnum ('Column' PGMyEnum) where
-- def = 'constantColumnUsing' (def :: 'Constant' String ('Column' 'PGText')) $ \ case
-- MyFoo -> "foo"
-- MyBar -> "bar"
-- @
deriveOpaleyeEnum :: Name -> String -> (String -> Maybe String) -> Q [Dec]
deriveOpaleyeEnum hsName sqlName hsConToSqlValue = do
let sqlTypeName = mkName $ "PG" ++ nameBase hsName
sqlType = conT sqlTypeName
hsType = conT hsName
rawCons <- reify hsName >>= \ case
TyConI (DataD _cxt _name _tvVarBndrs _maybeKind cons _derivingCxt) ->
pure cons
other ->
fail $ "expected " <> show hsName <> " to name a data declaration, not:\n" <> pprint other
nullaryCons <- for rawCons $ \ case
NormalC conName [] ->
pure conName
other ->
fail $ "expected every constructor of " <> show hsName <> " to be a regular nullary constructor, not:\n" <> pprint other
let conPairs = nullaryCons <&> \ conName ->
(conName, fromMaybe (nameBase conName) (hsConToSqlValue (nameBase conName)))
sqlTypeDecl <-
dataD
(cxt [])
sqlTypeName
[]
Nothing
[]
#if MIN_VERSION_template_haskell(2,12,0)
[]
#else
(cxt [])
#endif
fromFieldInst <- instanceD (cxt []) [t| FromField $hsType |] . (:[]) $ do
field <- newName "field"
mbs <- newName "mbs"
tname <- newName "tname"
other <- newName "other"
let bodyCase = caseE (varE mbs) $
[ match
wildP
(guardedB [ normalGE [| $(varE tname) /= $(lift sqlName) |]
[| returnError Incompatible $(varE field) "" |] ])
[]
] ++
(
conPairs <&> \ (conName, value) ->
match
[p| Just $(litP $ stringL value) |]
(normalB [| pure $(conE conName) |])
[]
) ++
[ match
[p| Just $(varP other) |]
(normalB [| returnError ConversionFailed $(varE field) ("Unexpected " <> $(lift sqlName) <> " value: " <> BSC8.unpack $(varE other)) |])
[]
, match
[p| Nothing |]
(normalB [| returnError UnexpectedNull $(varE field) "" |])
[]
]
funD 'fromField
[ clause
[varP field, varP mbs]
(normalB [|
do
$(varP tname) <- typename $(varE field)
$bodyCase
|])
[]
]
queryRunnerColumnDefaultInst <- instanceD (cxt []) [t| QueryRunnerColumnDefault $sqlType $hsType |] . (:[]) $
funD 'queryRunnerColumnDefault
[ clause
[]
(normalB [| fieldQueryRunnerColumn |])
[]
]
defaultInst <- instanceD (cxt []) [t| Default Constant $hsType (Column $sqlType) |] . (:[]) $ do
s <- newName "s"
let body = lamE [varP s] $
caseE (varE s) $
conPairs <&> \ (conName, value) ->
match
(conP conName [])
(normalB $ lift value)
[]
funD 'def
[ clause
[]
(normalB [| constantColumnUsing (def :: Constant String (Column PGText)) $body |])
[]
]
pure [sqlTypeDecl, fromFieldInst, queryRunnerColumnDefaultInst, defaultInst]