Skip to content

Provide HasField instances for Entity for OverloadedRecordDot#1381

Merged
parsonsmatt merged 9 commits intomasterfrom
matt/hasfield-entity
Apr 12, 2022
Merged

Provide HasField instances for Entity for OverloadedRecordDot#1381
parsonsmatt merged 9 commits intomasterfrom
matt/hasfield-entity

Conversation

@parsonsmatt
Copy link
Collaborator

OK, so this is a fun PR!

We can provide:

instance (SymbolToField sym rec typ, PersistEntity rec) => HasField sym (Entity rec) typ where
  getField =
    view (fieldLens (symbolToField @sym @rec @typ))

which uses similar machinery to this esqueleto PR.

It uses the non-prefixed versions of the fields, which means that we're also polymorphic over what this means.

mkPersist sqlSettings [persistLowerCase|
User
    name Text
    age Int
|]

main = do
    let user = Entity (toSqlKey 1) User { userName = "Matt", userAge = 33 }
    print user.name
    print user.age

This is a polymorphic entity use:

wow :: (PersistEntity rec, SymbolToField "hello" (Entity rec) a, Show a) => Entity rec -> IO ()
wow rec = print rec.hello 

Which does mimic the polymorphism in HasField generally.


Before submitting your PR, check that you've:

  • Documented new APIs with Haddock markup
  • Added @since declarations to the Haddock
  • Ran stylish-haskell on any changed files.
  • Adhered to the code style (see the .editorconfig file for details)

After submitting your PR:

  • Update the Changelog.md file with a link to your PR
  • Bumped the version number if there isn't an (unreleased) on the Changelog
  • Check that CI passes (or if it fails, for reasons unrelated to your change, like CI timeouts)

@parsonsmatt parsonsmatt added this to the 2.14 milestone Apr 8, 2022
@parsonsmatt parsonsmatt merged commit 6c8afab into master Apr 12, 2022
@hw202207
Copy link
Contributor

Notes:
This implementation had been removed at later version: #1401

@jaredramirez
Copy link

jaredramirez commented Nov 7, 2024

If anyone comes across this issue in the future wanting HasField instances for their entities, here's some TH code that I wrote to generate instances:

(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 -> t

You can call deriveHasFieldInstances with an entity list from mkEntityDefList or discoverEntities.

Unfortunately, you'll need to have the call to deriveHasFieldInstances in a different module than where you define your entities, for the same problem described here: #1318

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants