{-# LANGUAGE DefaultSignatures #-} module Database.GP.Entity ( Entity (..), columnNameFor, fieldTypeFor, maybeFieldTypeFor, toString, evidence, evidenceFrom, ResolutionCache, EntityId, Ctx (..), GP, ) where import Data.Char (toLower) import Data.Data import Database.HDBC (SqlValue, fromSql, ConnWrapper) import Database.GP.RecordtypeReflection (gFromRow, gToRow) import Database.GP.TypeInfo import Data.Dynamic import RIO {-- This is the Entity class. It is a type class that is used to define the mapping between a Haskell product type in record notation and a database table. The class has a default implementation for all methods. The default implementation uses the type information to determine a simple 1:1 mapping. That means that - the type name is used as the table name and the - field names are used as the column names. - A field named 'ID' is used as the primary key field. The default implementation can be overridden by defining a custom instance for a type. Please note the following constraints, which apply to all valid Entity type, but that are not explicitely encoded in the type class definition: - The type must be a product type in record notation. - The type must have exactly one constructor. - There must be single primary key field, compund primary keys are not supported. --} class (Data a) => Entity a where -- | Converts a database row to a value of type 'a'. fromRow :: [SqlValue] -> GP a -- | Converts a value of type 'a' to a database row. toRow :: a -> GP [SqlValue] -- | Returns the name of the primary key field for a type 'a'. idField :: a -> String -- | Returns a list of tuples that map field names to column names for a type 'a'. fieldsToColumns :: a -> [(String, String)] -- | Returns the name of the table for a type 'a'. tableName :: a -> String -- | generic default implementation default fromRow :: [SqlValue] -> GP a fromRow = pure . gFromRow -- | generic default implementation default toRow :: a -> GP [SqlValue] toRow = pure . gToRow -- | default implementation: the ID field is the field with the same name -- as the type name in lower case and appended with "ID", e.g. "bookID" default idField :: a -> String idField = idFieldName . typeInfo where idFieldName :: TypeInfo a -> String idFieldName ti = map toLower (typeName ti) ++ "ID" -- | default implementation: the field names are used as column names default fieldsToColumns :: a -> [(String, String)] fieldsToColumns x = zip (fieldNames (typeInfo x)) (fieldNames (typeInfo x)) -- | default implementation: the type name is used as table name default tableName :: a -> String tableName = typeName . typeInfo -- | type Ctx defines the context in which the persistence operations are executed. -- It contains a connection to the database and a resolution cache for circular lookups. data Ctx = Ctx {connection :: ConnWrapper, cache :: ResolutionCache } type GP = RIO Ctx -- | The EntityId is a tuple of the TypeRep and the primary key value of an Entity. -- It is used as a key in the resolution cache. type EntityId = (TypeRep, SqlValue) -- | The resolution cache maps an EntityId to a Dynamic value (representing an Entity). -- It is used to resolve circular references during loading and storing of Entities. type ResolutionCache = [(EntityId, Dynamic)] -- | A convenience function: returns the name of the column for a field of a type 'a'. columnNameFor :: Entity a => a -> String -> String columnNameFor x fieldName = case maybeColumnNameFor x fieldName of Just columnName -> columnName Nothing -> error ("columnNameFor: " ++ toString x ++ " has no column mapping for " ++ fieldName) where maybeColumnNameFor :: Entity a => a -> String -> Maybe String maybeColumnNameFor a field = lookup field (fieldsToColumns a) -- | A convenience function: returns the TypeRep of a field of a type 'a'. fieldTypeFor :: Entity a => a -> String -> TypeRep fieldTypeFor x fieldName = case maybeFieldTypeFor x fieldName of Just tyRep -> tyRep Nothing -> error ("fieldTypeFor: " ++ toString x ++ " has no field " ++ fieldName) maybeFieldTypeFor :: Entity a => a -> String -> Maybe TypeRep maybeFieldTypeFor a field = lookup field (fieldsAndTypes (typeInfo a)) where fieldsAndTypes :: TypeInfo a -> [(String, TypeRep)] fieldsAndTypes ti = zip (fieldNames ti) (fieldTypes ti) -- | Returns a string representation of a value of type 'a'. toString :: (Entity a) => a -> String toString x = typeName (typeInfo x) ++ " " ++ unwords mappedRow where mappedRow = map fromSql (gToRow x) -- | A convenience function: returns an evidence instance of type 'a'. -- This is useful for type inference where no instance is available. evidence :: forall a. (Entity a) => a evidence = evidenceFrom ti where ti = typeInfoFromContext :: TypeInfo a evidenceFrom :: forall a. (Entity a) => TypeInfo a -> a evidenceFrom = fromConstr . typeConstructor