packages feed

protobuf-builder-0.1.0.0: src/Protobuf/Builder.hs

{-# language DataKinds #-}
{-# language RankNTypes #-}
{-# language MagicHash #-}
{-# language TypeApplications #-}
{-# language KindSignatures #-}
{-# language ScopedTypeVariables #-}
{-# language PatternSynonyms #-}
{-# language GADTs #-}

-- | Encode data with protobuf. Under the hood, this uses catenable byte
-- sequences, meaning that a large number of small @ByteArray@s are allocated
-- during encoding. That makes this a simple but low-performance solution.
-- To prevent incorrect use, @Builder@ is parameterized by @Value@. This
-- distinguishes encodings of primitives from encodings of sequences of
-- key-value pairs. The functions 'message' and 'pair' are used to move
-- between these two domains.
module Protobuf.Builder
  ( Builder(..)
  , WireType(..)
  , Value(..)
  , run
    -- * Variable Length
  , variableWord8
  , variableWord16
  , variableWord32
  , variableWord64
  , sint64
  , int64
  , sint32
  , int32
    -- * Fixed Length 32
  , fixed32
    -- * Fixed Length 64
  , fixed64
  , double
    -- * Messages
  , message
  , pair
    -- * Length-Delimited 
  , shortText
  , shortByteString
  ) where

import Control.Monad.ST.Run (runByteArrayST)
import Data.Word (Word8,Word16,Word32,Word64)
import Data.Int (Int32,Int64)
import GHC.Exts (Proxy#,proxy#)
import Data.Bits ((.|.),unsafeShiftL)
import Data.Builder.Catenable.Bytes (pattern (:<))
import Data.Bytes (Bytes)
import Data.Word.Zigzag (toZigzag32,toZigzag64)
import Data.Text.Short (ShortText)
import Data.ByteString.Short (ShortByteString)

import qualified Data.Bytes as Bytes
import qualified Data.Bytes.Chunks as Chunks
import qualified Data.Bytes.Builder.Bounded as Bounded
import qualified Data.Builder.Catenable.Bytes as Builder
import qualified Data.Primitive as PM
import qualified Data.Kind as GHC
import qualified Arithmetic.Nat as Nat
import qualified Data.ByteString.Short as SBS
import qualified Data.Text.Short as TS

-- | A protobuf object builder. The data constructor is exposed, but it is
-- unsafe to use it.
--
-- Note that @Builder 'Pairs@ has @Semigroup@ and @Monoid@ instances.
-- This is an important part of the interface.
data Builder :: Value -> GHC.Type where
  Builder ::
       !Int -- length of builder
    -> !Builder.Builder -- builder
    -> Builder v

run :: Builder v -> Bytes
run (Builder _ b) = Chunks.concat (Builder.run b)

-- | Protobuf\'s four wire types.
data WireType
  = BitsFixed32
  | BitsFixed64
  | BitsVariable
  | Bytes

-- | Either a primitive type (a wire type) or a collector of encoded
-- key-value pairs.
data Value
  = Primitive WireType
  | Pairs

sint32 ::Int32 -> Builder ('Primitive 'BitsVariable)
sint32 w = variableWord32 (toZigzag32 w)

int32 ::Int32 -> Builder ('Primitive 'BitsVariable)
int32 w = variableWord32 (fromIntegral w)

sint64 ::Int64 -> Builder ('Primitive 'BitsVariable)
sint64 w = variableWord64 (toZigzag64 w)

int64 ::Int64 -> Builder ('Primitive 'BitsVariable)
int64 w = variableWord64 (fromIntegral w)

variableWord8 :: Word8 -> Builder ('Primitive 'BitsVariable)
variableWord8 w = variableWord64 (fromIntegral w)

variableWord16 :: Word16 -> Builder ('Primitive 'BitsVariable)
variableWord16 w = variableWord64 (fromIntegral w)

variableWord32 :: Word32 -> Builder ('Primitive 'BitsVariable)
variableWord32 w = variableWord64 (fromIntegral w)

variableWord64 :: Word64 -> Builder ('Primitive 'BitsVariable)
variableWord64 w =
  let b = Bounded.run Nat.constant (Bounded.word64LEB128 w)
   in Builder (PM.sizeofByteArray b) (Bytes.fromByteArray b :< Builder.Empty)

fixed32 :: Word32 -> Builder ('Primitive 'BitsFixed32)
fixed32 w =
  let b = Bounded.run Nat.constant (Bounded.word32LE w)
   in Builder (PM.sizeofByteArray b) (Bytes.fromByteArray b :< Builder.Empty)

fixed64 :: Word64 -> Builder ('Primitive 'BitsFixed64)
fixed64 w =
  let b = Bounded.run Nat.constant (Bounded.word64LE w)
   in Builder (PM.sizeofByteArray b) (Bytes.fromByteArray b :< Builder.Empty)

double :: Double -> Builder ('Primitive 'BitsFixed64)
double w =
  let b = runByteArrayST $ do
            dst <- PM.newByteArray 8
            PM.writeByteArray dst 0 w
            PM.unsafeFreezeByteArray dst
   in Builder 8 (Bytes.fromByteArray b :< Builder.Empty)

pair :: forall (ty :: WireType). HasWireTypeNumber ty
  => Word32 -- ^ key
  -> Builder ('Primitive ty) -- ^ value
  -> Builder 'Pairs
{-# inline pair #-}
pair k (Builder valLen valBuilder) =
  let fullKey = unsafeShiftL (fromIntegral @Word32 @Word64 k) 3 .|. fromIntegral @Word8 @Word64 (wireTypeNumber (proxy# @ty))
      keyBytes = Bounded.run Nat.constant (Bounded.word64LEB128 fullKey) 
   in Builder (valLen + PM.sizeofByteArray keyBytes) (Builder.Cons (Bytes.fromByteArray keyBytes) valBuilder)

message :: Builder 'Pairs -> Builder ('Primitive 'Bytes)
message (Builder len b) =
  let lenBytes = Bounded.run Nat.constant (Bounded.word64LEB128 (fromIntegral len))
   in Builder (len + PM.sizeofByteArray lenBytes) (Builder.Cons (Bytes.fromByteArray lenBytes) b)

shortText :: ShortText -> Builder ('Primitive 'Bytes)
shortText t = shortByteString (TS.toShortByteString t)

shortByteString :: ShortByteString -> Builder ('Primitive 'Bytes)
shortByteString sbs =
  let len = SBS.length sbs
      lenBytes = Bounded.run Nat.constant (Bounded.word64LEB128 (fromIntegral len))
   in Builder (len + PM.sizeofByteArray lenBytes) (Bytes.fromByteArray lenBytes :< Bytes.fromShortByteString sbs :< Builder.Empty)

instance (v ~ 'Pairs) => Monoid (Builder v) where
  mempty = Builder 0 Builder.Empty

instance (v ~ 'Pairs) => Semigroup (Builder v) where
  Builder xlen x <> Builder ylen y = Builder (xlen + ylen) (x <> y)

class HasWireTypeNumber (t :: WireType) where
  wireTypeNumber :: Proxy# t -> Word8

instance HasWireTypeNumber 'BitsFixed32 where
  wireTypeNumber _ = 5

instance HasWireTypeNumber 'BitsFixed64 where
  wireTypeNumber _ = 1

instance HasWireTypeNumber 'BitsVariable where
  wireTypeNumber _ = 0

instance HasWireTypeNumber 'Bytes where
  wireTypeNumber _ = 2