Provide HasField instances for Entity for OverloadedRecordDot#1381
Merged
parsonsmatt merged 9 commits intomasterfrom Apr 12, 2022
Merged
Provide HasField instances for Entity for OverloadedRecordDot#1381parsonsmatt merged 9 commits intomasterfrom
HasField instances for Entity for OverloadedRecordDot#1381parsonsmatt merged 9 commits intomasterfrom
Conversation
Contributor
|
Notes: |
|
If anyone comes across this issue in the future wanting (Witten for persistent v2.13.3.5, very likely this would need to be tweaked slightly for newer versions) Code{-# LANGUAGE TemplateHaskellQuotes #-}
{-
This module containts template haskell code that generates HasField instances
for generated persistent entities. For each @EntityDef@ provided, it
generates:
instance HasField "fieldKey" TableType FieldValue
instance HasField "fieldKey" (Entity TableType) FieldValue
instance HasField "id" (Entity TableType) (Key TableType)
With these instances, you can access fields from persistent types much more
easily. For example:
longTableNameLongFieldName fetchEntity
Becomes
fetchEntity.longFieldName
-}
module Models.HasFieldTH (deriveHasFieldInstances) where
import Data.Char qualified as Char
import Data.Text qualified as Text
import Database.Persist qualified as Persist
import Database.Persist.Types qualified as PersistTypes
import GHC.Records (HasField (..))
import Language.Haskell.TH qualified as TH
import MoonBase
--------------------------------------------------------------------------------
-- Instances TH
-- | For a list of entities, generate Has field instances
deriveHasFieldInstances :: [Persist.EntityDef] -> TH.Q [TH.Dec]
deriveHasFieldInstances = fmap concat . traverse deriveHasFieldInstanceForEntity
-- | For an entity, generate HasField instances
deriveHasFieldInstanceForEntity :: Persist.EntityDef -> TH.Q [TH.Dec]
deriveHasFieldInstanceForEntity entityDef = do
-- Get basic metadata about the entity
let entityName = Persist.unEntityNameHS $ Persist.getEntityHaskellName entityDef
entityPrefix = lowercaseFirst entityName
entityFields = Persist.getEntityFields entityDef
-- Helper function to creat the @HasField@ instance
mkHasFieldInstanceType fieldKeyType modifyTableType fieldValType =
TH.ConT ''HasField
`TH.AppT` fieldKeyType
`TH.AppT` modifyTableType (TH.ConT . TH.mkName $ Text.unpack entityName)
`TH.AppT` fieldValType
-- For every field, generate `TableType` and `Entity TableType` HasField instances
fieldInstances <- forM entityFields $ \entityField -> do
-- Get field name, type and accessor metadata
let fieldName = Persist.unFieldNameHS $ Persist.fieldHaskell entityField
fieldIsMaybe = elem PersistTypes.FieldAttrMaybe $ Persist.fieldAttrs entityField
fieldType =
toFieldType (Persist.fieldType entityField)
& if fieldIsMaybe
then TH.AppT (TH.ConT $ TH.mkName "Maybe")
else id
recordAccessor = entityPrefix <> uppercaseFirst fieldName
recordAccessorVar = TH.VarE . TH.mkName $ Text.unpack recordAccessor
fieldNameType = TH.LitT (TH.StrTyLit $ Text.unpack fieldName)
-- Create the instance types
hasFieldTableType =
mkHasFieldInstanceType fieldNameType id fieldType
hasFieldEntityType =
mkHasFieldInstanceType fieldNameType (TH.ConT ''Persist.Entity `TH.AppT`) fieldType
-- Create the getter for the entit y
getEntityFieldDef <- [e|Persist.entityVal <&> $(pure recordAccessorVar)|]
pure
[ -- Translates to:
-- instance HasField "field" (Entity TableType) FieldType where
-- getField = fieldAccessor
TH.InstanceD
Nothing
[]
hasFieldEntityType
[ TH.FunD
(TH.mkName "getField")
[ TH.Clause [] (TH.NormalB getEntityFieldDef) []
]
]
, -- Translates to:
-- instance HasField "field" TableType FieldType where
-- getField = fieldAccessor
TH.InstanceD
Nothing
[]
hasFieldTableType
[ TH.FunD
(TH.mkName "getField")
[ TH.Clause [] (TH.NormalB recordAccessorVar) []
]
]
]
-- Create the HasField instance for the entity key
getEntityIdDef <- [e|Persist.entityKey|]
let hasFieldIdEntityType =
mkHasFieldInstanceType
(TH.LitT $ TH.StrTyLit "id")
(TH.ConT ''Persist.Entity `TH.AppT`)
(TH.ConT ''Persist.Key `TH.AppT` TH.ConT (TH.mkName $ Text.unpack entityName))
idInstance =
-- instance HasField "id" (Entity EntityType) FieldType where
-- getField = fieldAccessor
TH.InstanceD
Nothing
[]
hasFieldIdEntityType
[ TH.FunD
(TH.mkName "getField")
[ TH.Clause [] (TH.NormalB getEntityIdDef) []
]
]
pure $ idInstance : concat fieldInstances
where
-- Convert a persist type to a TH type
toFieldType :: PersistTypes.FieldType -> TH.Type
toFieldType = \case
PersistTypes.FTTypeCon mbModule typ ->
TH.ConT . TH.mkName . Text.unpack $ maybe "" (<> ".") mbModule <> typ
PersistTypes.FTTypePromoted typ ->
TH.PromotedT . TH.mkName . Text.unpack $ typ
PersistTypes.FTList typ ->
TH.AppT (TH.ConT $ TH.mkName "[]") (toFieldType typ)
PersistTypes.FTApp typA typB ->
TH.AppT (toFieldType typA) (toFieldType typB)
--------------------------------------------------------------------------------
-- Helpers
-- | Convert the first letter of the Text to lowercase
lowercaseFirst :: Text -> Text
lowercaseFirst t =
case Text.uncons t of
Just (firstLetter, rest) -> Text.cons (Char.toLower firstLetter) rest
Nothing -> t
-- | Convert the first letter of the Text to uppercase
uppercaseFirst :: Text -> Text
uppercaseFirst t =
case Text.uncons t of
Just (firstLetter, rest) -> Text.cons (Char.toUpper firstLetter) rest
Nothing -> tYou can call Unfortunately, you'll need to have the call to |
7 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
OK, so this is a fun PR!
We can provide:
which uses similar machinery to this
esqueletoPR.It uses the non-prefixed versions of the fields, which means that we're also polymorphic over what this means.
This is a polymorphic entity use:
Which does mimic the polymorphism in
HasFieldgenerally.Before submitting your PR, check that you've:
@sincedeclarations to the Haddockstylish-haskellon any changed files..editorconfigfile for details)After submitting your PR:
(unreleased)on the Changelog