pg-entity-0.0.2.0: src/Database/PostgreSQL/Entity/Types.hs
{-# LANGUAGE DefaultSignatures #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE UndecidableInstances #-}
-- |
-- Module : Database.PostgreSQL.Entity.Types
-- Copyright : © Clément Delafargue, 2018
-- Théophile Choutri, 2021
-- License : MIT
-- Maintainer : theophile@choutri.eu
-- Stability : stable
--
-- Types and classes
module Database.PostgreSQL.Entity.Types
( -- * The /Entity/ Typeclass
Entity (..)
-- * Associated Types
, Field
, field
, fieldName
, fieldType
, UpdateRow (..)
, SortKeyword (..)
-- * Generics
, Options (..)
, defaultEntityOptions
-- * DerivingVia Options
, GenericEntity (..)
, EntityOptions (..)
, PrimaryKey
, Schema
, TableName
, FieldModifiers
, TextModifier (..)
, StripPrefix
, CamelTo
, CamelToSnake
, CamelToKebab
)
where
import Data.Char
import Data.Kind
import Data.Maybe
import Data.Proxy
import Data.Text (Text)
import qualified Data.Text as T
import Data.Text.Display (Display, ShowInstance (..))
import qualified Data.Text.Manipulate as T
import Data.Vector (Vector)
import qualified Data.Vector as V
import qualified Data.Vector as Vector
import Database.PostgreSQL.Entity.Internal.QQ (field)
import Database.PostgreSQL.Entity.Internal.Unsafe (Field (Field))
import Database.PostgreSQL.Simple.ToRow (ToRow (..))
import GHC.Generics
import GHC.TypeLits
-- | An 'Entity' stores the following information about the structure of a database table:
--
-- * Its name
-- * Its primary key
-- * The fields it contains
--
-- == Example
--
-- > data ExampleEntity = E
-- > { key :: Key
-- > , field1 :: Int
-- > , field2 :: Bool
-- > }
-- > deriving stock (Eq, Show, Generic)
-- > deriving anyclass (FromRow, ToRow)
-- > deriving Entity
-- > via (GenericEntity '[TableName "entities"] ExampleEntity)
--
-- When using the functions provided by this library, you will sometimes need to be explicit about the Entity you are
-- referring to.
--
-- @since 0.0.1.0
class Entity e where
-- | The name of the table in the PostgreSQL database.
tableName :: Text
default tableName :: (GetTableName (Rep e)) => Text
tableName = getTableName @(Rep e) defaultEntityOptions
-- | The name of the schema; will be appended to the table name: schema."tablename"
schema :: Maybe Text
schema = Nothing
-- | The name of the primary key for the table.
primaryKey :: Field
default primaryKey :: (GetFields (Rep e)) => Field
primaryKey = newPrimaryKey
where
primMod = primaryKeyModifiers defaultEntityOptions
fs = getField @(Rep e) defaultEntityOptions
newPrimaryKey =
case Vector.find (\(Field name _type) -> name == primMod name) fs of
Nothing -> Field (primMod "") Nothing
Just f -> f
-- | The fields of the table.
fields :: Vector Field
default fields :: (GetFields (Rep e)) => Vector Field
fields = getField @(Rep e) defaultEntityOptions
-- The sub-class that fetches the table name
class GetTableName (e :: Type -> Type) where
getTableName :: Options -> Text
instance (TypeError ( 'Text "You can't derive Entity for a void type")) => GetTableName V1 where
getTableName _opts = error "You can't derive Entity for a void type"
instance (TypeError ( 'Text "You can't derive Entity for a unit type")) => GetTableName U1 where
getTableName _opts = error "You can't derive Entity for a unit type"
instance (TypeError ( 'Text "You can't derive Entity for a sum type")) => GetTableName (e :+: f) where
getTableName _opts = error "You can't derive Entity for a sum type"
instance (TypeError ( 'Text "You can't derive an Entity for a type constructor's field")) => GetTableName (K1 i c) where
getTableName _opts = error "You can't derive Entity for a type constructor's field"
instance (TypeError ( 'Text "You don't have to derive GetTableName for a product type")) => GetTableName (e :*: f) where
getTableName _opts = error "You don't have to derive GetTableName for a product type"
instance GetTableName e => GetTableName (M1 C _1 e) where
getTableName opts = getTableName @e opts
instance GetTableName e => GetTableName (M1 S _1 e) where
getTableName opts = getTableName @e opts
instance
(KnownSymbol name) =>
GetTableName (M1 D ( 'MetaData name _1 _2 _3) e)
where
getTableName Options{tableNameModifiers, fieldModifiers} = tableNameModifiers $ fieldModifiers $ T.pack $ symbolVal (Proxy :: Proxy name)
-- The sub-class that fetches the table fields
class GetFields (e :: Type -> Type) where
getField :: Options -> Vector Field
instance (TypeError ( 'Text "You can't derive Entity for a void type")) => GetFields V1 where
getField _opts = error "You can't derive Entity for a void type"
instance (TypeError ( 'Text "You can't derive Entity for a unit type")) => GetFields U1 where
getField _opts = error "You can't derive Entity for a unit type"
instance (TypeError ( 'Text "You can't derive Entity for a sum type")) => GetFields (e :+: f) where
getField _opts = error "You can't derive Entity for a sum type"
instance (TypeError ( 'Text "You can't derive Entity for a a type constructor's field")) => GetFields (K1 i c) where
getField _opts = error "You can't derive Entity for a type constructor's field"
instance (GetFields e, GetFields f) => GetFields (e :*: f) where
getField opts = getField @e opts <> getField @f opts
instance GetFields e => GetFields (M1 C _1 e) where
getField opts = getField @e opts
instance GetFields e => GetFields (M1 D ( 'MetaData _1 _2 _3 _4) e) where
getField opts = getField @e opts
instance (KnownSymbol name) => GetFields (M1 S ( 'MetaSel ( 'Just name) _1 _2 _3) _4) where
getField Options{fieldModifiers} = V.singleton $ Field fieldName' Nothing
where
fieldName' = fieldModifiers $ T.pack $ symbolVal (Proxy @name)
-- Deriving Via machinery
newtype GenericEntity t e = GenericEntity {getGenericEntity :: e}
instance (EntityOptions t, GetTableName (Rep e), GetFields (Rep e)) => Entity (GenericEntity t e) where
tableName = getTableName @(Rep e) (entityOptions @t)
schema = schemaModifier (entityOptions @t)
primaryKey = newPrimaryKey
where
primMod = primaryKeyModifiers (entityOptions @t)
fs = getField @(Rep e) (entityOptions @t)
newPrimaryKey =
case Vector.find (\(Field name _type) -> name == primMod name) fs of
Nothing -> Field (primMod "") Nothing
Just f -> f
fields = getField @(Rep e) (entityOptions @t)
-- | Term-level options
data Options = Options
{ tableNameModifiers :: Text -> Text
, schemaModifier :: Maybe Text
, primaryKeyModifiers :: Text -> Text
, fieldModifiers :: Text -> Text
}
defaultEntityOptions :: Options
defaultEntityOptions =
Options
{ tableNameModifiers = T.toSnake
, schemaModifier = Nothing
, primaryKeyModifiers = T.toSnake
, fieldModifiers = T.toSnake
}
-- | Type-level options for Deriving Via
class EntityOptions xs where
entityOptions :: Options
instance EntityOptions '[] where
entityOptions = defaultEntityOptions
instance (GetName name, EntityOptions xs) => EntityOptions (TableName name ': xs) where
entityOptions = (entityOptions @xs){tableNameModifiers = const (getName @name)}
instance (GetName name, EntityOptions xs) => EntityOptions (PrimaryKey name ': xs) where
entityOptions = (entityOptions @xs){primaryKeyModifiers = const (getName @name)}
instance (TextModifier mods, EntityOptions xs) => EntityOptions (FieldModifiers mods ': xs) where
entityOptions = (entityOptions @xs){fieldModifiers = getTextModifier @mods}
instance (GetName name, EntityOptions xs) => EntityOptions (Schema name ': xs) where
entityOptions = (entityOptions @xs){schemaModifier = Just $ getName @name}
data TableName (t :: Symbol)
data PrimaryKey (t :: Symbol)
data Schema (t :: Symbol)
-- | Contains a list of 'TextModifiers' modifiers
data FieldModifiers ms
-- | 'TextModifier' to remove a certain prefix from the fields
data StripPrefix (prefix :: Symbol)
-- | 'FieldModifier' taking a separator Char when transforming from CamelCase.
data CamelTo (separator :: Symbol)
-- | CamelCase to snake_case
type CamelToSnake = CamelTo "_"
-- | CamelCase to kebab-case
type CamelToKebab = CamelTo "-"
-- | The modifiers that you can apply to the fields:
--
-- * 'StripPrefix'
-- * 'CamelTo', and its variations
-- * 'CamelToSnake'
-- * 'CamelToKebab'
class TextModifier t where
getTextModifier :: Text -> Text
-- No modifier
instance TextModifier '[] where
getTextModifier = id
-- How we can have multiple modifiers chained
instance (TextModifier x, TextModifier xs) => TextModifier (x ': xs) where
getTextModifier = getTextModifier @xs . getTextModifier @x
instance (KnownSymbol prefix) => TextModifier (StripPrefix prefix) where
getTextModifier fld = fromMaybe fld (T.stripPrefix prefixToStrip fld)
where
prefixToStrip = T.pack $ symbolVal (Proxy @prefix)
instance (KnownSymbol separator, NonEmptyText separator) => TextModifier (CamelTo separator) where
getTextModifier fld = T.pack $ camelTo2 char (T.unpack fld)
where
char :: Char
char = head $ symbolVal (Proxy @separator)
camelTo2 :: Char -> String -> String
camelTo2 c text = map toLower . go2 $ go1 text
where
go1 "" = ""
go1 (x : u : l : xs) | isUpper u && isLower l = x : c : u : l : go1 xs
go1 (x : xs) = x : go1 xs
go2 "" = ""
go2 (l : u : xs) | isLower l && isUpper u = l : c : u : go2 xs
go2 (x : xs) = x : go2 xs
class GetName name where
getName :: Text
instance (KnownSymbol name, NonEmptyText name) => GetName name where
getName = T.pack (symbolVal (Proxy @name))
type family NonEmptyText (xs :: Symbol) :: Constraint where
NonEmptyText "" = TypeError ( 'Text "User-provided string cannot be empty!")
NonEmptyText _ = ()
-- | Get the name of a field.
--
-- @since 0.1.0.0
fieldName :: Field -> Text
fieldName (Field name _) = name
-- | Get the type of a field, if any.
--
-- @since 0.1.0.0
fieldType :: Field -> Maybe Text
fieldType (Field _ typ) = typ
-- | Wrapper used by the update function in order to have the primary key as the last parameter passed,
-- since it appears in the WHERE clause.
--
-- @since 0.0.1.0
newtype UpdateRow a = UpdateRow {getUpdate :: a}
deriving stock (Eq, Show)
deriving newtype (Entity)
instance ToRow a => ToRow (UpdateRow a) where
toRow = (drop <> take) 1 . toRow . getUpdate
-- |
-- @since 0.0.2.0
data SortKeyword = ASC | DESC
deriving stock (Eq, Show)
deriving
(Display)
via ShowInstance SortKeyword