packages feed

wireform-arrow-0.1.0.0: src/Arrow/Record/Generic.hs

{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE DefaultSignatures #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeOperators #-}

{- | @GHC.Generics@-backed 'Table' derivation for records whose
fields all have primitive Arrow encoders.

This module bridges "Arrow.Record"'s combinator API with
Haskell's 'Generic' machinery so that a record with
@deriving stock Generic@ can get an Arrow 'Table' instance in
one line:

@
data Trade = Trade { sym :: Text, qty :: Int32, note :: Maybe Text }
  deriving stock ('Generic')

tradeTable :: 'Table' Trade
tradeTable = 'genericTable'
@

Under the hood, 'genericTable' walks the 'Rep' to collect the
selector name + the 'HasEncoder' / 'HasDecoder' instance for
each field. Users who want a non-default representation for a
field type (e.g. a newtype wrapping @Int64@ that should
serialise as its underlying type) write a one-line
'HasEncoder' / 'HasDecoder' instance via
'Data.Functor.Contravariant.contramap' + 'fmap':

@
newtype UserId = UserId { unUserId :: Int64 }

instance 'HasEncoder' UserId where
  hasEncoder = 'contramap' unUserId 'int64E'
instance 'HasDecoder' UserId where
  hasDecoder = UserId \<$\> 'int64D'
@

For records that don't want to depend on 'Generic' (or that
want explicit column-name control), see "Arrow.Record.TH".
-}
module Arrow.Record.Generic (
  -- * Type classes for Generic deriving
  HasEncoder (..),
  HasDecoder (..),

  -- * Row-level (nested-record) classes
  HasRowEncoder (..),
  HasRowDecoder (..),

  -- * Deriver
  genericTable,
  genericRowEncoder,
  genericRowDecoder,

  -- * Re-exports for convenience
  Generic,
) where

import Arrow.Record (
  Decoder,
  Encoder,
  RowDecoder,
  RowEncoder,
  Table (..),
  binaryD,
  binaryE,
  boolD,
  boolE,
  columnD,
  doubleD,
  doubleE,
  fieldE,
  floatD,
  floatE,
  int16D,
  int16E,
  int32D,
  int32E,
  int64D,
  int64E,
  int8D,
  int8E,
  nullable,
  nullableD,
  utf8D,
  utf8E,
  word16D,
  word16E,
  word32D,
  word32E,
  word64D,
  word64E,
  word8D,
  word8E,
 )
import Data.ByteString (ByteString)
import Data.Functor.Contravariant (Contravariant (contramap))
import Data.Int (Int16, Int32, Int64, Int8)
import Data.Kind (Type)
import Data.Text (Text)
import Data.Text qualified as T
import Data.Word (Word16, Word32, Word64, Word8)
import GHC.Generics


-- ============================================================
-- HasEncoder / HasDecoder
-- ============================================================

{- | Bridge class between Haskell primitive types and
"Arrow.Record" 'Encoder' values. Only needed when deriving a
'Table' with 'genericTable'; hand-written 'RowEncoder's don't
touch this class.
-}
class HasEncoder a where
  hasEncoder :: Encoder a


-- | Mirror of 'HasEncoder' on the decoding side.
class HasDecoder a where
  hasDecoder :: Decoder a


{- | Per-record class used when a nested record needs to be
embedded as an Arrow @ColStruct@ column.

Default implementation uses 'genericRowEncoder', so any
'Generic' record whose fields all have 'HasEncoder' or
'HasEncoder (Maybe a)' instances picks up the instance for
free:

@
data Address = Address { city :: Text, zip :: Text }
  deriving stock ('Generic')

instance 'HasRowEncoder' Address
instance 'HasRowDecoder' Address
@

Once the inner record has these, lift it into a parent
'RowEncoder' / 'RowDecoder' with @'structE' "addr" addr
'hasRowEncoder'@ / @'structD' "addr" 'hasRowDecoder'@.
-}
class HasRowEncoder a where
  hasRowEncoder :: RowEncoder a
  default hasRowEncoder
    :: (Generic a, GRowEncoder (Rep a)) => RowEncoder a
  hasRowEncoder = genericRowEncoder


-- | Mirror of 'HasRowEncoder' on the decoding side.
class HasRowDecoder a where
  hasRowDecoder :: RowDecoder a
  default hasRowDecoder
    :: (Generic a, GRowDecoder (Rep a)) => RowDecoder a
  hasRowDecoder = genericRowDecoder


-- Primitive instances just delegate to the corresponding
-- combinator from "Arrow.Record".

instance HasEncoder Int8 where hasEncoder = int8E


instance HasEncoder Int16 where hasEncoder = int16E


instance HasEncoder Int32 where hasEncoder = int32E


instance HasEncoder Int64 where hasEncoder = int64E


instance HasEncoder Word8 where hasEncoder = word8E


instance HasEncoder Word16 where hasEncoder = word16E


instance HasEncoder Word32 where hasEncoder = word32E


instance HasEncoder Word64 where hasEncoder = word64E


instance HasEncoder Float where hasEncoder = floatE


instance HasEncoder Double where hasEncoder = doubleE


instance HasEncoder Bool where hasEncoder = boolE


instance HasEncoder Text where hasEncoder = utf8E


instance HasEncoder ByteString where hasEncoder = binaryE


instance HasDecoder Int8 where hasDecoder = int8D


instance HasDecoder Int16 where hasDecoder = int16D


instance HasDecoder Int32 where hasDecoder = int32D


instance HasDecoder Int64 where hasDecoder = int64D


instance HasDecoder Word8 where hasDecoder = word8D


instance HasDecoder Word16 where hasDecoder = word16D


instance HasDecoder Word32 where hasDecoder = word32D


instance HasDecoder Word64 where hasDecoder = word64D


instance HasDecoder Float where hasDecoder = floatD


instance HasDecoder Double where hasDecoder = doubleD


instance HasDecoder Bool where hasDecoder = boolD


instance HasDecoder Text where hasDecoder = utf8D


instance HasDecoder ByteString where hasDecoder = binaryD


{- | @'Maybe' a@ lifts through 'nullable' / 'nullableD'. The
@{\-# OVERLAPPING #-\}@ is needed because the instance head
'HasEncoder (Maybe a)' overlaps with the base instance when
GHC considers the final record field type.
-}
instance {-# OVERLAPPING #-} HasEncoder a => HasEncoder (Maybe a) where
  hasEncoder = nullable hasEncoder
  {-# INLINE hasEncoder #-}


instance {-# OVERLAPPING #-} HasDecoder a => HasDecoder (Maybe a) where
  hasDecoder = nullableD hasDecoder
  {-# INLINE hasDecoder #-}


-- ============================================================
-- Generic derivation
-- ============================================================

{- | Derive a 'Table' for any record whose field types all have
'HasEncoder' + 'HasDecoder' instances. Column names come from
the record selector names.

@
data Trade = Trade { sym :: Text, qty :: Int32 } deriving (Generic)
tradeTable :: 'Table' Trade
tradeTable = 'genericTable'
@
-}
genericTable
  :: forall r
   . ( Generic r
     , GRowEncoder (Rep r)
     , GRowDecoder (Rep r)
     )
  => Table r
genericTable =
  Table
    { tableEncode = genericRowEncoder @r
    , tableDecode = genericRowDecoder @r
    }
{-# INLINE genericTable #-}


genericRowEncoder
  :: forall r
   . (Generic r, GRowEncoder (Rep r))
  => RowEncoder r
genericRowEncoder =
  contramap from (gRowEncoder :: RowEncoder (Rep r ()))
{-# INLINE genericRowEncoder #-}


genericRowDecoder
  :: forall r
   . (Generic r, GRowDecoder (Rep r))
  => RowDecoder r
genericRowDecoder =
  to <$> (gRowDecoder :: RowDecoder (Rep r ()))
{-# INLINE genericRowDecoder #-}


-- ============================================================
-- Generic Rep walkers
-- ============================================================

class GRowEncoder (f :: Type -> Type) where
  gRowEncoder :: RowEncoder (f p)


class GRowDecoder (f :: Type -> Type) where
  gRowDecoder :: RowDecoder (f p)


-- Datatype wrapper: transparent.
instance GRowEncoder f => GRowEncoder (M1 D m f) where
  gRowEncoder = unM1 `contramap` gRowEncoder
  {-# INLINE gRowEncoder #-}


instance GRowDecoder f => GRowDecoder (M1 D m f) where
  gRowDecoder = M1 <$> gRowDecoder
  {-# INLINE gRowDecoder #-}


-- Constructor wrapper: transparent.
instance GRowEncoder f => GRowEncoder (M1 C m f) where
  gRowEncoder = unM1 `contramap` gRowEncoder
  {-# INLINE gRowEncoder #-}


instance GRowDecoder f => GRowDecoder (M1 C m f) where
  gRowDecoder = M1 <$> gRowDecoder
  {-# INLINE gRowDecoder #-}


-- Product: concatenate via RowEncoder's Semigroup.
instance (GRowEncoder f, GRowEncoder g) => GRowEncoder (f :*: g) where
  gRowEncoder =
    ((\(l :*: _) -> l) `contramap` gRowEncoder)
      <> ((\(_ :*: r) -> r) `contramap` gRowEncoder)
  {-# INLINE gRowEncoder #-}


instance (GRowDecoder f, GRowDecoder g) => GRowDecoder (f :*: g) where
  gRowDecoder = (:*:) <$> gRowDecoder <*> gRowDecoder
  {-# INLINE gRowDecoder #-}


-- Record selector: pull the selector name via 'Selector' and
-- pair it with the field type's HasEncoder / HasDecoder instance.
instance (Selector s, HasEncoder a) => GRowEncoder (S1 s (Rec0 a)) where
  gRowEncoder =
    let selectorName = T.pack (selName (undefined :: S1 s (Rec0 a) p))
    in (unK1 . unM1)
         `contramap` fieldE selectorName id (hasEncoder :: Encoder a)
  {-# INLINE gRowEncoder #-}


instance (Selector s, HasDecoder a) => GRowDecoder (S1 s (Rec0 a)) where
  gRowDecoder =
    let selectorName = T.pack (selName (undefined :: S1 s (Rec0 a) p))
    in (M1 . K1) <$> columnD selectorName (hasDecoder :: Decoder a)
  {-# INLINE gRowDecoder #-}