packages feed

spire-protobuf-0.1.0.0: src/Spire/Protobuf/Message.hs

{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE DefaultSignatures #-}
{-# OPTIONS_GHC -Wno-orphans #-}

-- | GHC Generics integration for automatic protobuf encoding/decoding.
--
-- Define your message as a Haskell record with 'Field' wrappers,
-- derive 'Generic', and write an empty 'ProtoMessage' instance:
--
-- @
-- data User = User
--   { name  :: Field 1 Text
--   , age   :: Field 2 Int32
--   , email :: Field 3 (Maybe Text)
--   } deriving (Generic)
--
-- instance ProtoMessage User
-- @
--
-- Encoding and decoding are then available via 'encode' and 'decode'.
module Spire.Protobuf.Message
  ( ProtoMessage (..)
  , encode
  , decode
  , GProtoEncode (..)
  , GProtoDecode (..)
  ) where

import Data.ByteString (ByteString)
import qualified Data.Map.Strict as Map
import GHC.Generics
import GHC.TypeLits (KnownNat, natVal)
import Data.Proxy (Proxy(..))

import Spire.Protobuf.Wire
import Spire.Protobuf.Field
import Spire.Protobuf.Encode
import Spire.Protobuf.Decode


-- ===================================================================
-- ProtoMessage class
-- ===================================================================

-- | Class for protobuf messages that can be encoded and decoded.
--
-- The default implementations use GHC Generics to walk the record
-- structure. Each field must be wrapped in 'Field' with a unique
-- field number.
class ProtoMessage a where
  protoEncode :: a -> ProtoBuilder
  default protoEncode :: (Generic a, GProtoEncode (Rep a)) => a -> ProtoBuilder
  protoEncode = gProtoEncode . from

  protoDecode :: ByteString -> Either DecodeError a
  default protoDecode :: (Generic a, GProtoDecode (Rep a)) => ByteString -> Either DecodeError a
  protoDecode bs = do
    rawFields <- parseFields bs
    to <$> gProtoDecode rawFields

-- | Encode a message to a strict 'ByteString'.
encode :: ProtoMessage a => a -> ByteString
encode = runProtoBuilder . protoEncode
{-# INLINE encode #-}

-- | Decode a message from a strict 'ByteString'.
decode :: ProtoMessage a => ByteString -> Either DecodeError a
decode = protoDecode
{-# INLINE decode #-}

-- ProtoMessage instance for submessage encoding
instance {-# OVERLAPPABLE #-} ProtoMessage a => ProtoEncode a where
  protoWireType = WireLengthDelimited
  protoEncodeValue a = encodeSubmessage (protoEncode a)
  {-# INLINE protoEncodeValue #-}

-- ProtoMessage instance for submessage decoding
instance {-# OVERLAPPABLE #-} ProtoMessage a => ProtoDecode a where
  protoDecodeValue (RawBytes bs) = protoDecode bs
  protoDecodeValue _             = Left (UnexpectedWireType 0 WireLengthDelimited WireVarint)
  {-# INLINE protoDecodeValue #-}


-- ===================================================================
-- GProtoEncode: Generic encoding
-- ===================================================================

-- | Generic encoding class. Walks the 'Rep' of a type and encodes
-- each 'Field n a' it encounters.
class GProtoEncode f where
  gProtoEncode :: f p -> ProtoBuilder

-- M1: metadata wrappers (datatype, constructor, selector) — pass through
instance GProtoEncode f => GProtoEncode (M1 i c f) where
  gProtoEncode (M1 x) = gProtoEncode x
  {-# INLINE gProtoEncode #-}

-- :*: product — encode both sides
instance (GProtoEncode f, GProtoEncode g) => GProtoEncode (f :*: g) where
  gProtoEncode (f :*: g) = gProtoEncode f <> gProtoEncode g
  {-# INLINE gProtoEncode #-}

-- U1: unit (empty record)
instance GProtoEncode U1 where
  gProtoEncode U1 = mempty
  {-# INLINE gProtoEncode #-}

-- K1: a field of type Field n a — encode it
instance (KnownNat n, ProtoEncode a, Eq a, ProtoDefault a)
  => GProtoEncode (K1 i (Field n a)) where
  gProtoEncode (K1 fld) = encodeField fld
  {-# INLINE gProtoEncode #-}

-- K1: optional field — Field n (Maybe a)
instance {-# OVERLAPPING #-} (KnownNat n, ProtoEncode a)
  => GProtoEncode (K1 i (Field n (Maybe a))) where
  gProtoEncode (K1 fld) = encodeOptionalField fld
  {-# INLINE gProtoEncode #-}

-- K1: repeated field — Field n [a]
instance {-# OVERLAPPING #-} (KnownNat n, ProtoEncode a)
  => GProtoEncode (K1 i (Field n [a])) where
  gProtoEncode (K1 fld) = encodeRepeatedField fld
  {-# INLINE gProtoEncode #-}


-- ===================================================================
-- GProtoDecode: Generic decoding
-- ===================================================================

-- | Generic decoding class. Constructs the 'Rep' of a type from a
-- list of raw (field_number, value) pairs.
--
-- The approach: parse all wire fields into @[(Int, RawField)]@ up front,
-- then for each record field, look up its field number in the list.
-- Unknown fields are silently ignored (proto3 forward-compatibility).
-- Last-wins semantics for duplicate field numbers.
class GProtoDecode f where
  gProtoDecode :: [(Int, RawField)] -> Either DecodeError (f p)

-- M1: metadata wrappers — pass through
instance GProtoDecode f => GProtoDecode (M1 i c f) where
  gProtoDecode rfs = M1 <$> gProtoDecode rfs
  {-# INLINE gProtoDecode #-}

-- :*: product — decode both sides from the same field list
instance (GProtoDecode f, GProtoDecode g) => GProtoDecode (f :*: g) where
  gProtoDecode rfs = do
    l <- gProtoDecode rfs
    r <- gProtoDecode rfs
    Right (l :*: r)
  {-# INLINE gProtoDecode #-}

-- U1: unit
instance GProtoDecode U1 where
  gProtoDecode _ = Right U1
  {-# INLINE gProtoDecode #-}

-- K1: required field — Field n a. Look up field number n; if absent, use default.
instance (KnownNat n, ProtoDecode a, ProtoDefault a)
  => GProtoDecode (K1 i (Field n a)) where
  gProtoDecode rfs =
    let fn = fromIntegral (natVal (Proxy @n))
    in case lookupLast fn rfs of
         Nothing  -> Right (K1 (Field protoDefault))
         Just raw -> K1 . Field <$> protoDecodeValue raw
  {-# INLINE gProtoDecode #-}

-- K1: optional field — Field n (Maybe a). Absent means Nothing.
instance {-# OVERLAPPING #-} (KnownNat n, ProtoDecode a)
  => GProtoDecode (K1 i (Field n (Maybe a))) where
  gProtoDecode rfs =
    let fn = fromIntegral (natVal (Proxy @n))
    in case lookupLast fn rfs of
         Nothing  -> Right (K1 (Field Nothing))
         Just raw -> K1 . Field . Just <$> protoDecodeValue raw
  {-# INLINE gProtoDecode #-}

-- K1: repeated field — Field n [a]. Collect all occurrences.
-- Handles both unpacked (each element tagged separately) and packed
-- (all elements in a single length-delimited blob) wire encodings.
-- For packable scalar types (Int32, Int64, Word32, Word64, Bool, Float,
-- Double, SInt32, SInt64), a RawBytes entry is automatically unpacked
-- into multiple values. For non-packable types (Text, ByteString,
-- submessages), each RawBytes is decoded as a single value.
instance {-# OVERLAPPING #-} (KnownNat n, DecodeRepeatedEntry a)
  => GProtoDecode (K1 i (Field n [a])) where
  gProtoDecode rfs =
    let fn = fromIntegral (natVal (Proxy @n))
        raws = [raw | (n', raw) <- rfs, n' == fn]
    in K1 . Field . concat <$> traverse decodeRepeatedEntry raws
  {-# INLINE gProtoDecode #-}


-- ===================================================================
-- Helpers
-- ===================================================================

-- | Look up the last occurrence of a field number (last-wins proto3 semantics).
lookupLast :: Int -> [(Int, RawField)] -> Maybe RawField
lookupLast fn = go Nothing
  where
    go acc [] = acc
    go acc ((n, v) : rest)
      | n == fn   = go (Just v) rest
      | otherwise = go acc rest
{-# INLINE lookupLast #-}


-- ===================================================================
-- ProtoMap: Proto3 map field support
-- ===================================================================

-- | Proto3 default for ProtoMap: empty map.
instance ProtoDefault (ProtoMap k v) where
  protoDefault = ProtoMap Map.empty

-- | Encode a ProtoMap as repeated length-delimited entries.
-- Each map entry is a submessage with field 1 = key, field 2 = value.
instance (Ord k, ProtoEncode k, ProtoEncode v) => ProtoEncode (ProtoMap k v) where
  protoWireType = WireLengthDelimited
  protoEncodeValue (ProtoMap m) =
    let encodeEntry k v =
          let keyEnc   = pbTag 1 (protoWireType @k) <> protoEncodeValue k
              valEnc   = pbTag 2 (protoWireType @v) <> protoEncodeValue v
              entryBody = keyEnc <> valEnc
          in encodeSubmessage entryBody
    in Map.foldlWithKey' (\acc k v -> acc <> encodeEntry k v) mempty m
  {-# INLINE protoEncodeValue #-}

-- | Decode a ProtoMap entry from a single RawBytes (one key-value pair submessage).
-- The caller (repeated field decoder) will collect all entries.
instance (Ord k, ProtoDecode k, ProtoDefault k, ProtoDecode v, ProtoDefault v)
  => ProtoDecode (ProtoMap k v) where
  protoDecodeValue (RawBytes bs) = do
    -- Parse the entry submessage
    rfs <- parseFields bs
    -- Extract key (field 1) and value (field 2), using defaults if absent
    k <- case lookupLast 1 rfs of
           Nothing  -> Right protoDefault
           Just raw -> protoDecodeValue raw
    v <- case lookupLast 2 rfs of
           Nothing  -> Right protoDefault
           Just raw -> protoDecodeValue raw
    Right (ProtoMap (Map.singleton k v))
  protoDecodeValue _ = Left (UnexpectedWireType 0 WireLengthDelimited WireVarint)
  {-# INLINE protoDecodeValue #-}

-- | DecodeRepeatedEntry for ProtoMap: each RawBytes is a single entry.
instance (Ord k, ProtoDecode k, ProtoDefault k, ProtoDecode v, ProtoDefault v)
  => DecodeRepeatedEntry (ProtoMap k v) where
  decodeRepeatedEntry raw = (: []) <$> protoDecodeValue raw
  {-# INLINE decodeRepeatedEntry #-}


-- ===================================================================
-- ProtoMap Generics encoding/decoding
-- ===================================================================

-- K1: ProtoMap field — Field n (ProtoMap k v). Encode all entries as
-- repeated submessages with the same field number.
instance {-# OVERLAPPING #-}
  (KnownNat n, Ord k, ProtoEncode k, ProtoEncode v)
  => GProtoEncode (K1 i (Field n (ProtoMap k v))) where
  gProtoEncode (K1 (Field (ProtoMap m)))
    | Map.null m = mempty
    | otherwise =
      let fn = fromIntegral (natVal (Proxy @n)) :: Int
          tag = pbTag fn WireLengthDelimited
          encodeEntry k v =
            let keyEnc   = pbTag 1 (protoWireType @k) <> protoEncodeValue k
                valEnc   = pbTag 2 (protoWireType @v) <> protoEncodeValue v
                entryBody = keyEnc <> valEnc
            in tag <> encodeSubmessage entryBody
      in Map.foldlWithKey' (\acc k v -> acc <> encodeEntry k v) mempty m
  {-# INLINE gProtoEncode #-}

-- K1: ProtoMap field — Field n (ProtoMap k v). Decode by collecting all
-- repeated submessage entries for the field number and merging them.
instance {-# OVERLAPPING #-}
  (KnownNat n, Ord k, ProtoDecode k, ProtoDefault k, ProtoDecode v, ProtoDefault v)
  => GProtoDecode (K1 i (Field n (ProtoMap k v))) where
  gProtoDecode rfs =
    let fn = fromIntegral (natVal (Proxy @n))
        raws = [raw | (n', raw) <- rfs, n' == fn]
    in do
      entries <- traverse decodeMapEntry raws
      Right (K1 (Field (ProtoMap (Map.unions entries))))
  {-# INLINE gProtoDecode #-}

-- | Decode a single map entry submessage into a singleton Map.
decodeMapEntry :: (Ord k, ProtoDecode k, ProtoDefault k, ProtoDecode v, ProtoDefault v)
               => RawField -> Either DecodeError (Map.Map k v)
decodeMapEntry (RawBytes bs) = do
  rfs <- parseFields bs
  k <- case lookupLast 1 rfs of
         Nothing  -> Right protoDefault
         Just raw -> protoDecodeValue raw
  v <- case lookupLast 2 rfs of
         Nothing  -> Right protoDefault
         Just raw -> protoDecodeValue raw
  Right (Map.singleton k v)
decodeMapEntry _ = Left (UnexpectedWireType 0 WireLengthDelimited WireVarint)