packages feed

wireform-proto-0.2.0.0: src/Proto/Internal/SizedBuilder.hs

{-# LANGUAGE BangPatterns #-}

{- | Church-encoded sized builder: fuses size calculation with serialization.

A 'SizedBuilder' carries both the computed byte size and the builder
in a single value. This avoids the need for a separate size-computation
pass when encoding submessages — the size is accumulated alongside
the builder as fields are appended.

'toByteString' allocates a single strict ByteString of exactly the
right size and fills it in one pass — no intermediate lazy chunks
or recopying.
-}
module Proto.Internal.SizedBuilder (
  -- * Core type
  SizedBuilder,

  -- * Construction
  empty,
  sized,

  -- * Running (strict ByteString — single allocation)
  toByteString,
  toByteStringFromBuilder,
  toBuilder,
  size,
  toLazyByteString,

  -- * Combinators
  withSubMessage,
) where

import Data.ByteString (ByteString)
import Data.ByteString.Lazy qualified as BL
import Proto.Internal.Wire.Encode (putVarint, varintSize)
import Wireform.Builder qualified as B


-- | A builder that tracks its own byte size.
data SizedBuilder = SizedBuilder
  { sbSize :: {-# UNPACK #-} !Int
  , sbBuilder :: !B.Builder
  }


instance Semigroup SizedBuilder where
  SizedBuilder s1 b1 <> SizedBuilder s2 b2 =
    SizedBuilder (s1 + s2) (b1 <> b2)
  {-# INLINE (<>) #-}


instance Monoid SizedBuilder where
  mempty = SizedBuilder 0 mempty
  {-# INLINE mempty #-}


-- | The empty 'SizedBuilder' (zero size, empty builder).
empty :: SizedBuilder
empty = mempty
{-# INLINE empty #-}


-- | Construct a 'SizedBuilder' from a known byte size and a builder.
sized :: Int -> B.Builder -> SizedBuilder
sized = SizedBuilder
{-# INLINE sized #-}


-- | Extract the underlying builder, discarding the size.
toBuilder :: SizedBuilder -> B.Builder
toBuilder = sbBuilder
{-# INLINE toBuilder #-}


-- | Get the precomputed byte size.
size :: SizedBuilder -> Int
size = sbSize
{-# INLINE size #-}


{- | Produce a strict ByteString. Pre-allocates a buffer of exactly
the right size (from 'sbSize') — no reallocation, no IORef.
-}
toByteString :: SizedBuilder -> ByteString
toByteString (SizedBuilder sz bld) = B.toStrictByteStringExact sz bld
{-# INLINE toByteString #-}


{- | Produce a strict ByteString from a Builder when the exact size
is known. Pre-allocates a buffer of @sz@ bytes — no reallocation,
no IORef.
-}
toByteStringFromBuilder :: Int -> B.Builder -> ByteString
toByteStringFromBuilder = B.toStrictByteStringExact
{-# INLINE toByteStringFromBuilder #-}


-- | Produce a lazy 'BL.ByteString' from the builder.
toLazyByteString :: SizedBuilder -> BL.ByteString
toLazyByteString (SizedBuilder _sz bld) = B.toLazyByteString bld
{-# INLINE toLazyByteString #-}


-- | Wrap a 'SizedBuilder' as a length-delimited submessage (prepends the varint length prefix).
withSubMessage :: SizedBuilder -> SizedBuilder
withSubMessage sb =
  let !payloadSize = sbSize sb
      !lenPrefixBuilder = putVarint (fromIntegral payloadSize)
  in SizedBuilder
       { sbSize = varintSize (fromIntegral payloadSize) + payloadSize
       , sbBuilder = lenPrefixBuilder <> sbBuilder sb
       }
{-# INLINE withSubMessage #-}