pg-entity-0.0.4.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