packages feed

persistent-template-classy-0.2.0: src/Database/Persist/TH/Classy.hs

{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TemplateHaskellQuotes #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE OverloadedStrings #-}

module Database.Persist.TH.Classy where

import Control.Lens (Lens')
import qualified Data.Char as Char
import qualified Data.Text as T
import Data.Traversable (forM)
import Database.Persist
import Language.Haskell.TH.Lib
import Language.Haskell.TH.Quote
import Language.Haskell.TH.Syntax

-- | Generate something like:
--
--     class HasName ev a | ev -> a where
--       name :: Lens' ev a
mkClassyClass ::
     String -- ^ like "name"
  -> Q [Dec]
mkClassyClass name
      -- | like "Name"
 =
  let nameCapitalized =
        case name of
          "" -> ""
          (x:xs) -> Char.toUpper x : xs
      -- | like "HasName"
      hasName = "Has" <> nameCapitalized
   in return
        [ ClassD
            []
            (mkName hasName)
            [PlainTV (mkName "ev"), PlainTV (mkName "a")]
            [FunDep [mkName "ev"] [mkName "a"]]
            [ SigD
                (mkName name)
                (AppT
                   (AppT (ConT (mkName "Lens'")) (VarT (mkName "ev")))
                   (VarT (mkName "a")))
            ]
        ]

-- | Generate something like:
--
-- instance HasName Person String where
--   name = (lens personName) (\ x y -> x {personName = y})
mkClassyInstances :: [EntityDef] -> Q [Dec]
mkClassyInstances defs = do
  concat <$> mapM mkClassyInstance defs

mkClassyInstance :: EntityDef -> Q [Dec]
mkClassyInstance EntityDef {..} = do
  forM entityFields $ \FieldDef {..} ->
    case fieldType of
      FTTypeCon tmodule tname -> do
        let _unused = ()
              -- | like "Person"
            instanceTypeName =
              ConT (mkName (T.unpack (unHaskellName entityHaskell)))
              -- | like "person"
            instanceTypeNameLowerFirstChar =
              case T.unpack (unHaskellName entityHaskell) of
                "" -> ""
                (x:xs) -> Char.toLower x : xs
              -- | like "Name"
            fieldUpperFirstChar =
              case T.unpack (unHaskellName fieldHaskell) of
                "" -> ""
                (x:xs) -> Char.toUpper x : xs
              -- | like "HasName"
            instanceHasName = mkName ("Has" <> fieldUpperFirstChar)
              -- | like "personName"
            fieldLongName =
              mkName (instanceTypeNameLowerFirstChar ++ fieldUpperFirstChar)
            fieldClause =
              Clause
                []
                (NormalB
                   (AppE
                      (AppE (VarE (mkName "lens")) (VarE fieldLongName))
                      (LamE
                         [VarP (mkName "x"), VarP (mkName "y")]
                         (RecUpdE
                            (VarE (mkName "x"))
                            [(fieldLongName, VarE (mkName "y"))]))))
                []
            field =
              FunD
                (mkName (T.unpack (unHaskellName fieldHaskell)))
                [fieldClause]
            fieldTName =
              let tnameAndModule = case tmodule of
                    Nothing -> tname
                    Just t -> t <> "." <> tname
                  nonMaybe = ConT (mkName (T.unpack tnameAndModule))
               in case FieldAttrMaybe `elem` fieldAttrs of
                    False -> nonMaybe
                    True -> AppT (ConT (mkName "Maybe")) nonMaybe
        return $
          InstanceD
            Nothing
            []
            ((AppT (AppT (ConT instanceHasName) instanceTypeName) fieldTName))
            [field]