packages feed

hasql-auto-0.1.0.0: lib/Hasql/Auto/Decoders.hs

-- | Obtain row decoders automatically via either constructors or @ generics-sop @
module Hasql.Auto.Decoders (
  -- * Rows
  defaultRowFor,
  defaultRowForProductType,
  defaultColumn,

  -- * Composites
  defaultCompositeFor,
  defaultCompositeForProductType,
  defaultCompositeField,

  -- * Value decoding
  defaultNullableOrNotValue,
  DefaultValue (..),

  -- * Types
  LiftDefaultRow,
  DefaultRowFor,
  DefaultCompositeFor,
  DefaultNullableOrNotValue,
)
where

import Data.Aeson (FromJSON (..))
import Data.Aeson qualified as JSON
import Data.ByteString (ByteString)
import Data.Functor ((<&>))
import Data.Int (Int16, Int32, Int64)
import Data.Proxy (Proxy (..))
import Data.Scientific (Scientific)
import Data.String (fromString)
import Data.Text (Text)
import Data.Time (Day, DiffTime, LocalTime, TimeOfDay, TimeZone, UTCTime)
import Data.Vector (Vector)
import Generics.SOP qualified as SOP
import Hasql.Auto.Types
import Hasql.Decoders qualified as D
import PostgreSQL.Binary.Range (Multirange, Range (..))

--------------------------------------------------------------------------------
-- Rows
--------------------------------------------------------------------------------

-- | Create a 'D.Row' decoder for a given constructor function. Each argument of
-- the constructor function corresponds to a column to be decoded in the row,
-- whose decoder is provided by 'DefaultNullableOrNotValue', which determines
-- the use of 'D.nullable' or 'D.nonNullable', and is an abstraction over
-- the user-extensible 'DefaultValue' typeclass.
{-# INLINE defaultRowFor #-}
defaultRowFor
  :: forall a f
   . (DefaultRowFor a f)
  => f
  -> D.Row a
defaultRowFor f = liftDefaultRow @(GetConstructorShape f) (pure f)

-- | Create a 'D.Row' decoder for any product type with a 'SOP.Generic' instance
{-# INLINE defaultRowForProductType #-}
defaultRowForProductType :: (SOP.IsProductType a xs, SOP.All DefaultNullableOrNotValue xs) => D.Row a
defaultRowForProductType =
  SOP.productTypeTo
    <$> SOP.hsequence (SOP.hcpure (Proxy :: Proxy DefaultNullableOrNotValue) defaultColumn)

type DefaultRowFor a f = LiftDefaultRow (GetConstructorShape f) a f

class LiftDefaultRow (shape :: ConstructorShape) a f where
  liftDefaultRow :: D.Row f -> D.Row a

instance
  ( DefaultNullableOrNotValue x
  , LiftDefaultRow next r' r
  )
  => LiftDefaultRow (AddColumn next) r' (x -> r)
  where
  {-# INLINE liftDefaultRow #-}
  liftDefaultRow r = liftDefaultRow @next (r <*> defaultColumn)

instance (r ~ r') => LiftDefaultRow DecoderDone r r' where
  {-# INLINE liftDefaultRow #-}
  liftDefaultRow r = r

defaultColumn :: (DefaultNullableOrNotValue a) => D.Row a
defaultColumn = D.column defaultNullableOrNotValue

--------------------------------------------------------------------------------
-- Composite s
--------------------------------------------------------------------------------

type DefaultCompositeFor a f = LiftDefaultComposite (GetConstructorShape f) a f

-- | Create a 'D.Composite' decoder for a given constructor function, a la
-- 'defaultRowFor'.
{-# INLINE defaultCompositeFor #-}
defaultCompositeFor
  :: forall a f
   . (DefaultCompositeFor a f)
  => f
  -> D.Composite a
defaultCompositeFor f =
  liftDefaultComposite @(GetConstructorShape f) (pure f)

-- | Create a 'D.Composite' decoder for any product type with a 'SOP.Generic' instance
{-# INLINE defaultCompositeForProductType #-}
defaultCompositeForProductType :: (SOP.IsProductType a xs, SOP.All DefaultNullableOrNotValue xs) => D.Composite a
defaultCompositeForProductType =
  SOP.productTypeTo
    <$> SOP.hsequence (SOP.hcpure (Proxy :: Proxy DefaultNullableOrNotValue) defaultCompositeField)

class LiftDefaultComposite (shape :: ConstructorShape) a f where
  liftDefaultComposite :: D.Composite f -> D.Composite a

instance
  ( DefaultNullableOrNotValue x
  , LiftDefaultComposite next r' r
  )
  => LiftDefaultComposite (AddColumn next) r' (x -> r)
  where
  {-# INLINE liftDefaultComposite #-}
  liftDefaultComposite r = liftDefaultComposite @next (r <*> defaultCompositeField)

instance (r ~ r') => LiftDefaultComposite DecoderDone r r' where
  {-# INLINE liftDefaultComposite #-}
  liftDefaultComposite r = r

defaultCompositeField :: (DefaultNullableOrNotValue a) => D.Composite a
defaultCompositeField = D.field defaultNullableOrNotValue

--------------------------------------------------------------------------------
-- Value s
--------------------------------------------------------------------------------

defaultNullableOrNotValue
  :: forall a
   . (DefaultNullableOrNotValue a)
  => D.NullableOrNot D.Value a
defaultNullableOrNotValue = defaultNullableOrNotValue_

class DefaultValue a where
  defaultValue :: D.Value a

class DefaultNullableOrNotValue a where
  defaultNullableOrNotValue_ :: D.NullableOrNot D.Value a

-- | When both instances match, this instance takes precedence
instance {-# OVERLAPPING #-} (DefaultValue a) => DefaultNullableOrNotValue (Maybe a) where
  defaultNullableOrNotValue_ = D.nullable defaultValue

instance (DefaultValue a) => DefaultNullableOrNotValue a where
  defaultNullableOrNotValue_ = D.nonNullable defaultValue

--------------------------------------------------------------------------------
-- Instances for PostgreSQL types

instance DefaultValue Bool where
  {-# INLINE defaultValue #-}
  defaultValue = D.bool

instance DefaultValue Int16 where
  {-# INLINE defaultValue #-}
  defaultValue = D.int2

instance DefaultValue Int32 where
  {-# INLINE defaultValue #-}
  defaultValue = D.int4

instance DefaultValue Int64 where
  {-# INLINE defaultValue #-}
  defaultValue = D.int8

instance DefaultValue Float where
  {-# INLINE defaultValue #-}
  defaultValue = D.float4

instance DefaultValue Double where
  {-# INLINE defaultValue #-}
  defaultValue = D.float8

instance DefaultValue Scientific where
  {-# INLINE defaultValue #-}
  defaultValue = D.numeric

instance DefaultValue Char where
  {-# INLINE defaultValue #-}
  defaultValue = D.char

instance DefaultValue Text where
  {-# INLINE defaultValue #-}
  defaultValue = D.text

instance DefaultValue ByteString where
  {-# INLINE defaultValue #-}
  defaultValue = D.bytea

instance DefaultValue Day where
  {-# INLINE defaultValue #-}
  defaultValue = D.date

instance DefaultValue LocalTime where
  {-# INLINE defaultValue #-}
  defaultValue = D.timestamp

instance DefaultValue UTCTime where
  {-# INLINE defaultValue #-}
  defaultValue = D.timestamptz

instance DefaultValue TimeOfDay where
  {-# INLINE defaultValue #-}
  defaultValue = D.time

instance DefaultValue (TimeOfDay, TimeZone) where
  {-# INLINE defaultValue #-}
  defaultValue = D.timetz

instance DefaultValue DiffTime where
  {-# INLINE defaultValue #-}
  defaultValue = D.interval

instance DefaultValue JSON.Value where
  {-# INLINE defaultValue #-}
  defaultValue = D.jsonb

instance (DefaultValue a, DefaultNullableOrNotValue a) => DefaultValue (Vector a) where
  {-# INLINE defaultValue #-}
  defaultValue = D.vectorArray defaultNullableOrNotValue

instance (DefaultValue a, DefaultNullableOrNotValue a) => DefaultValue [a] where
  {-# INLINE defaultValue #-}
  defaultValue = D.listArray defaultNullableOrNotValue

instance DefaultValue (Range Int32) where
  {-# INLINE defaultValue #-}
  defaultValue = D.int4range

instance DefaultValue (Range Int64) where
  {-# INLINE defaultValue #-}
  defaultValue = D.int8range

instance DefaultValue (Range Scientific) where
  {-# INLINE defaultValue #-}
  defaultValue = D.numrange

instance DefaultValue (Range LocalTime) where
  {-# INLINE defaultValue #-}
  defaultValue = D.tsrange

instance DefaultValue (Range UTCTime) where
  {-# INLINE defaultValue #-}
  defaultValue = D.tstzrange

instance DefaultValue (Range Day) where
  {-# INLINE defaultValue #-}
  defaultValue = D.daterange

instance DefaultValue (Multirange Int32) where
  {-# INLINE defaultValue #-}
  defaultValue = D.int4multirange

instance DefaultValue (Multirange Int64) where
  {-# INLINE defaultValue #-}
  defaultValue = D.int8multirange

instance DefaultValue (Multirange Scientific) where
  {-# INLINE defaultValue #-}
  defaultValue = D.nummultirange

instance DefaultValue (Multirange LocalTime) where
  {-# INLINE defaultValue #-}
  defaultValue = D.tsmultirange

instance DefaultValue (Multirange UTCTime) where
  {-# INLINE defaultValue #-}
  defaultValue = D.tstzmultirange

instance DefaultValue (Multirange Day) where
  {-# INLINE defaultValue #-}
  defaultValue = D.datemultirange

--------------------------------------------------------------------------------
-- Extra helper types

instance (FromJSON a) => DefaultValue (ViaJSONB a) where
  {-# INLINE defaultValue #-}
  defaultValue =
    ViaJSONB
      <$> D.jsonbBytes (JSON.eitherDecodeStrict <&> either (Left . fromString) Right)

instance (FromJSON a) => DefaultValue (ViaJSON a) where
  {-# INLINE defaultValue #-}
  defaultValue =
    ViaJSON
      <$> D.jsonBytes (JSON.eitherDecodeStrict <&> either (Left . fromString) Right)

--------------------------------------------------------------------------------
-- Helper types

data ConstructorShape
  = DecoderDone
  | AddColumn ConstructorShape

type family GetConstructorShape a where
  GetConstructorShape (a -> b) = AddColumn (GetConstructorShape b)
  GetConstructorShape _ = DecoderDone