packages feed

borsh-0.1.0: src/Codec/Borsh/Encoding.hs

module Codec.Borsh.Encoding (
    -- * Encoder definition
    Encoder (..)
    -- * Encoders for non-composite types mandated by the Borsh spec
  , encodeU8
  , encodeU16
  , encodeU32
  , encodeU64
  , encodeU128
  , encodeI8
  , encodeI16
  , encodeI32
  , encodeI64
  , encodeI128
  , encodeF32
  , encodeF64
  , encodeString
  -- * Encoders for composite types mandated by the Borsh spec
  , encodeArray
  , encodeVec
  , encodeOption
  , encodeHashSet
  , encodeHashMap
  , encodeStruct
  , encodeEnum
  -- * Encoders for Haskell types not mandated by the Borsh spec
  , encodeLazyByteString
  , encodeStrictByteString
  , encodeChar
  , encodeBool
  ) where

import Data.Char (ord)
import Data.ByteString.Builder (Builder)
import Data.Foldable (toList)
import Data.Functor.Contravariant
import Data.Int
import Data.Map (Map)
import Data.Set (Set)
import Data.SOP
import Data.Text (Text)
import Data.Word

import qualified Data.ByteString         as S
import qualified Data.ByteString.Builder as B
import qualified Data.ByteString.Lazy    as L
import qualified Data.Map                as Map
import qualified Data.Set                as Set
import qualified Data.Text.Encoding      as Text

import Data.FixedSizeArray (FixedSizeArray)
import Codec.Borsh.Internal.Util.ByteString
import Codec.Borsh.Internal.Util.SOP (indices)
import Data.Word128
import Data.Int128

{-------------------------------------------------------------------------------
  Encoder definition
-------------------------------------------------------------------------------}

-- | Encoder
--
-- An encoder describes how to serialise a given value in BORSH format.
newtype Encoder a = Encoder {
      runEncoder :: a -> Builder
    }

instance Contravariant Encoder where
  contramap f (Encoder e) = Encoder (e . f)

liftEncoder :: Encoder a -> (I -.-> K Builder) a
liftEncoder (Encoder e) = fn $ K . e . unI

{-------------------------------------------------------------------------------
  Encoders for non-composite types mandated by the Borsh spec
-------------------------------------------------------------------------------}

encodeU8   :: Encoder Word8
encodeU16  :: Encoder Word16
encodeU32  :: Encoder Word32
encodeU64  :: Encoder Word64
encodeI8   :: Encoder Int8
encodeI16  :: Encoder Int16
encodeI32  :: Encoder Int32
encodeI64  :: Encoder Int64
encodeF32  :: Encoder Float
encodeF64  :: Encoder Double

encodeU8  = Encoder B.word8
encodeU16 = Encoder B.word16LE
encodeU32 = Encoder B.word32LE
encodeU64 = Encoder B.word64LE
encodeI8  = Encoder B.int8
encodeI16 = Encoder B.int16LE
encodeI32 = Encoder B.int32LE
encodeI64 = Encoder B.int64LE
encodeF32 = Encoder B.floatLE
encodeF64 = Encoder B.doubleLE

encodeU128 :: Encoder Word128
encodeU128 = Encoder $
    \w128 -> B.word64LE (word128LS64 w128) <> B.word64LE (word128MS64 w128)

encodeI128 :: Encoder Int128
encodeI128 = Encoder $
    \i128 -> B.word64LE (int128LS64 i128) <> B.word64LE (int128MS64 i128)

-- Encoding 'Text'
--
-- Borsh requires the length of the utf8-encoded string before the string, but
-- unfortunately we have no easy way to compute this without encoding the entire
-- string. This means that we are not streaming here: the entire utf8 encoding
-- is constructed in memory.
--
-- With text version 2.0 we can use @lengthWord8@ but that is not available most
-- of the time.
encodeString :: Encoder Text
encodeString = Encoder $ \txt ->
    B.word32LE (lengthLazy $ utf8 txt) <> B.lazyByteString (utf8 txt)
  where
    utf8 :: Text -> L.ByteString
    utf8 txt = B.toLazyByteString $ Text.encodeUtf8Builder txt

{-------------------------------------------------------------------------------
  Encoders for composite types mandated by the Borsh spec
-------------------------------------------------------------------------------}

encodeArray :: Encoder a -> Encoder (FixedSizeArray n a)
encodeArray e = Encoder $ mconcat . map (runEncoder e) . toList

encodeVec :: Encoder a -> Encoder [a]
encodeVec e = Encoder $ \xs -> mconcat $
      runEncoder encodeU32 (fromIntegral $ length xs)
    : map (runEncoder e) xs

encodeOption :: Encoder a -> Encoder (Maybe a)
encodeOption e = Encoder $ \case
    Nothing -> runEncoder encodeU8 0
    Just x  -> runEncoder encodeU8 1 <> runEncoder e x

encodeHashSet :: Encoder a -> Encoder (Set a)
encodeHashSet e = Encoder $ \xs -> mconcat $
      runEncoder encodeU32 (fromIntegral $ Set.size xs)
    : (map (runEncoder e) $ Set.toList xs)

encodeHashMap :: Encoder k -> Encoder a -> Encoder (Map k a)
encodeHashMap ek ev = Encoder $ \xs -> mconcat $
      runEncoder encodeU32 (fromIntegral $ Map.size xs)
    : (map ePair $ Map.toList xs)
  where
    ePair (k,v) =  runEncoder ek k <> runEncoder ev v

encodeStruct :: SListI xs => NP Encoder xs -> Encoder (NP I xs)
encodeStruct es = Encoder $
      mconcat
    . hcollapse
    . hap (hliftA liftEncoder es)

encodeEnum :: All SListI xss => POP Encoder xss -> Encoder (SOP I xss)
encodeEnum  es = Encoder $
      hcollapse
    . hczipWith (Proxy @SListI) aux indices
    . unSOP
    . hap (hliftA liftEncoder es)
  where
    aux :: SListI xs => K Word8 xs -> NP (K Builder) xs -> K Builder xs
    aux (K ix) xs = K $ runEncoder encodeU8 ix <> mconcat (hcollapse xs)

{-------------------------------------------------------------------------------
  Encoders for Haskell types not mandated by the Borsh spec
-------------------------------------------------------------------------------}

-- ByteStrings

encodeLazyByteString :: Encoder L.ByteString
encodeLazyByteString = Encoder $ \bs ->
       runEncoder encodeU32 (fromIntegral $ L.length bs)
    <> B.lazyByteString bs

encodeStrictByteString :: Encoder S.ByteString
encodeStrictByteString = Encoder $ \bs ->
       runEncoder encodeU32 (fromIntegral $ S.length bs)
    <> B.byteString bs

-- Char, Bool

encodeChar :: Encoder Char
encodeChar = Encoder $ runEncoder encodeU32 . fromIntegral . ord

encodeBool :: Encoder Bool
encodeBool = Encoder $ runEncoder encodeU8 . fromIntegral . fromEnum