{-# language AllowAmbiguousTypes #-} {-# language DataKinds #-} {-# language FlexibleContexts #-} {-# language FlexibleInstances #-} {-# language LambdaCase #-} {-# language RankNTypes #-} {-# language ScopedTypeVariables #-} {-# language StandaloneKindSignatures #-} {-# language TypeApplications #-} {-# language TypeFamilies #-} {-# language TypeOperators #-} {-# language UndecidableInstances #-} module Rel8.Type.Enum ( Enum( Enum ) , DBEnum( enumValue, enumTypeName ) , Enumable ) where -- base import Control.Applicative ( (<|>) ) import Control.Arrow ( (&&&) ) import Data.Kind ( Constraint, Type ) import Data.Proxy ( Proxy( Proxy ) ) import GHC.Generics ( Generic, Rep, from, to , (:+:)( L1, R1 ), M1( M1 ), U1( U1 ) , D, C, Meta( MetaCons ) ) import GHC.TypeLits ( KnownSymbol, symbolVal ) import Prelude hiding ( Enum ) -- hasql import qualified Hasql.Decoders as Hasql -- opaleye import qualified Opaleye.Internal.HaskellDB.PrimQuery as Opaleye -- rel8 import Rel8.Type ( DBType, typeInformation ) import Rel8.Type.Eq ( DBEq ) import Rel8.Type.Information ( TypeInformation(..) ) import Rel8.Type.Ord ( DBOrd, DBMax, DBMin ) -- text import Data.Text ( pack ) -- | A deriving-via helper type for column types that store an \"enum\" type -- (in Haskell terms, a sum type where all constructors are nullary) using a -- Postgres @enum@ type. -- -- Note that this should map to a specific type in your database's schema -- (explicitly created with @CREATE TYPE ... AS ENUM@). Use 'DBEnum' to -- specify the name of this Postgres type and the names of the individual -- values. If left unspecified, the names of the values of the Postgres -- @enum@ are assumed to match exactly exactly the names of the constructors -- of the Haskell type (up to and including case sensitivity). type Enum :: Type -> Type newtype Enum a = Enum { unEnum :: a } instance DBEnum a => DBType (Enum a) where typeInformation = TypeInformation { decode = Hasql.enum $ flip lookup $ map ((pack . enumValue &&& Enum) . to) $ genumerate @(Rep a) , encode = Opaleye.ConstExpr . Opaleye.StringLit . enumValue @a . unEnum , typeName = enumTypeName @a } instance DBEnum a => DBEq (Enum a) instance DBEnum a => DBOrd (Enum a) instance DBEnum a => DBMax (Enum a) instance DBEnum a => DBMin (Enum a) -- | @DBEnum@ contains the necessary metadata to describe a PostgreSQL @enum@ type. type DBEnum :: Type -> Constraint class (DBType a, Enumable a) => DBEnum a where -- | Map Haskell values to the corresponding element of the @enum@ type. The -- default implementation of this method will use the exact name of the -- Haskell constructors. enumValue :: a -> String enumValue = gshow @(Rep a) . from -- | The name of the PostgreSQL @enum@ type that @a@ maps to. enumTypeName :: String -- | Types that are sum types, where each constructor is unary (that is, has no -- fields). class (Generic a, GEnumable (Rep a)) => Enumable a instance (Generic a, GEnumable (Rep a)) => Enumable a type GEnumable :: (Type -> Type) -> Constraint class GEnumable rep where genumerate :: [rep x] gshow :: rep x -> String instance GEnumable rep => GEnumable (M1 D meta rep) where genumerate = M1 <$> genumerate gshow (M1 rep) = gshow rep instance (GEnumable a, GEnumable b) => GEnumable (a :+: b) where genumerate = L1 <$> genumerate <|> R1 <$> genumerate gshow = \case L1 a -> gshow a R1 a -> gshow a instance ( meta ~ 'MetaCons name _fixity _isRecord , KnownSymbol name ) => GEnumable (M1 C meta U1) where genumerate = [M1 U1] gshow (M1 U1) = symbolVal (Proxy @name)