packages feed

opaleye-0.10.3.0: src/Opaleye/Experimental/Enum.hs

{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE RankNTypes #-}

module Opaleye.Experimental.Enum
  (
    enumMapper,
    EnumMapper,
    enumMapperWithSchema,
    enumFromField,
    enumToFields,
    enumShowSqlType,
  ) where

import           Opaleye.Field (Field)
import qualified Opaleye as O
import qualified Opaleye.Internal.PGTypes as IPT
import qualified Opaleye.Internal.RunQuery as RQ

import           Data.ByteString.Char8 (unpack)
import Text.PrettyPrint.HughesPJ (doubleQuotes, render, text)
import Prelude hiding ((<>))

data EnumMapper sqlEnum haskellSum = EnumMapper {
    enumFromField :: RQ.FromField sqlEnum haskellSum
  , enumToFields :: O.ToFields haskellSum (Field sqlEnum)
  , enumShowSqlType :: forall proxy. proxy sqlEnum -> String
  }

-- | Create a mapping between a Postgres @ENUM@ type and a Haskell
-- type.  Also works for @DOMAIN@ types. For example, if you have the
-- following @ENUM@
--
-- @
-- CREATE TYPE public.mpaa_rating AS ENUM (
--    \'G\',
--    \'PG\',
--    \'PG-13\',
--    \'R\',
--    \'NC-17\'
-- );
-- @
--
-- then you can define data types to represent the enum on the SQL
-- side and Haskell side respectively
--
-- @
-- data SqlRating
-- data Rating = G | PG | PG13 | R | NC17 deriving Show
-- @
--
-- and functions to map between them
--
-- @
-- toSqlRatingString :: Rating -> String
-- toSqlRatingString r = case r of
--     G    -> \"G\"
--     PG   -> \"PG\"
--     PG13 -> \"PG-13\"
--     R    -> \"R\"
--     NC17 -> \"NC-17\"
--
-- fromSqlRatingString :: String -> Maybe Rating
-- fromSqlRatingString s = case s of
--     \"G\"     -> Just G
--     \"PG\"    -> Just PG
--     \"PG-13\" -> Just PG13
--     \"R\"     -> Just R
--     \"NC-17\" -> Just NC17
--     _       -> Nothing
-- @
--
-- Then you can use the mappings as follows
--
-- @
-- import qualified Opaleye as O
-- import qualified Data.Profunctor.Product.Default as D
--
-- sqlRatingMapper :: EnumMapper SqlRating Rating
-- sqlRatingMapper = enumMapper "mpaa_rating" fromSqlRatingString toSqlRatingString
--
-- instance O.DefaultFromField SqlRating Rating where
--   defaultFromField = enumFromField sqlRatingMapper
--
-- instance rating ~ Rating
--   => D.Default (Inferrable O.FromField) SqlRating rating where
--   def = Inferrable D.def
--
-- instance D.Default O.ToFields Rating (O.Field SqlRating) where
--   def = enumToFields sqlRatingMapper
--
-- instance IsSqlType SqlRating where
--   showSqlType = enumShowSqlType sqlRatingMapper
-- @
enumMapper :: String
           -- ^ The name of the @ENUM@ type
           -> (String -> Maybe haskellSum)
           -- ^ A function which converts from the string
           -- representation of the ENUM field
           -> (haskellSum -> String)
           -- ^ A function which converts to the string representation
           -- of the ENUM field
           -> EnumMapper sqlEnum haskellSum
           -- ^ The @sqlEnum@ type variable is phantom. To protect
           -- yourself against type mismatches you should set it to
           -- the Haskell type that you use to represent the @ENUM@.
enumMapper type_ = enumMapper' (render (doubleQuotes (text type_)))

enumMapperWithSchema :: String
           -- ^ The schema of the @ENUM@ type
           -> String
           -- ^ The name of the @ENUM@ type
           -> (String -> Maybe haskellSum)
           -- ^ A function which converts from the string
           -- representation of the ENUM field
           -> (haskellSum -> String)
           -- ^ A function which converts to the string representation
           -- of the ENUM field
           -> EnumMapper sqlEnum haskellSum
           -- ^ The @sqlEnum@ type variable is phantom. To protect
           -- yourself against type mismatches you should set it to
           -- the Haskell type that you use to represent the @ENUM@.
enumMapperWithSchema schema type_ =
  enumMapper'
    (IPT.sqlTypeWithSchema schema type_)

enumMapper' :: String
           -- ^ The name of the @ENUM@ type
           -> (String -> Maybe haskellSum)
           -- ^ A function which converts from the string
           -- representation of the ENUM field
           -> (haskellSum -> String)
           -- ^ A function which converts to the string representation
           -- of the ENUM field
           -> EnumMapper sqlEnum haskellSum
           -- ^ The @sqlEnum@ type variable is phantom. To protect
           -- yourself against type mismatches you should set it to
           -- the Haskell type that you use to represent the @ENUM@.
enumMapper' type_ from to_ = EnumMapper {
    enumFromField = fromFieldEnum
  , enumToFields = toFieldsEnum
  , enumShowSqlType = \_ -> type_
  }
   where
     toFieldsEnum = O.toToFields (O.unsafeCast type_ . O.sqlString . to_)
     fromFieldEnum = flip fmap RQ.unsafeFromFieldRaw $ \(_, mdata) -> case mdata of
       Nothing -> error "Unexpected NULL"
       Just s -> case from (unpack s) of
         Just r -> r
         Nothing -> error ("Unexpected: " ++ unpack s)