ipldm-1.0.0.0: dagcbor/IPLD/DagCBOR/Encoder.hs
-- | Module : IPLD.DagCBOR.Encoder
-- Description : Implements DM.Encoder for DAG-CBOR
-- Copyright : Zoey McBride (c) 2026
-- License : AGPL-3.0-or-later
-- Maintainer : zoeymcbride@mailbox.org
-- Stability : experimental
module IPLD.DagCBOR.Encoder
( -- * Encoder output
DagCBOR,
-- * Implements `DM.Encoder` class
Encoder,
-- * List CBOR encoding
encodeListKind,
majorList,
-- * Key-value pair CBOR encoding
encodeMapKind,
majorMap,
-- * Encoder utilities
toByteString,
toDigits,
toHex,
-- * Implementing methods
module IPLD.DM.Encoder,
)
where
import Control.Applicative (Alternative (..))
import Data.BaseSystems.Lazy (BaseSystem, base16lower)
import Data.BaseSystems.Lazy qualified as Bases
import Data.ByteString.Builder (Builder)
import Data.ByteString.Builder qualified as BSB
import Data.ByteString.Lazy (LazyByteString)
import Data.Text (Text)
import IPLD.DM (Kind (..), ListData, MapData)
import IPLD.DM.Encoder hiding (Encoder)
import IPLD.DM.Encoder qualified as DM
import IPLD.DagCBOR.Encoder.Arguments
import IPLD.DagCBOR.Encoder.CID
import IPLD.DagCBOR.Encoder.Errors
import IPLD.DagCBOR.Encoder.Floats
import IPLD.DagCBOR.Encoder.Ints
import IPLD.DagCBOR.Encoder.SimpleTypes
import IPLD.DagCBOR.Encoder.Strings
-- | Method results from `DM.Encoder`.
type DagCBOR = Encoder Builder
toByteString :: DagCBOR -> Okay LazyByteString
toByteString dagcbor =
case dagcbor of
EncoderError err -> Left err
EncoderValue result -> Right (BSB.toLazyByteString result)
-- | Encodes DagCBOR to a digit string in the provided `BaseSystem`.
toDigits :: (BaseSystem system) => system -> DagCBOR -> Okay Text
toDigits base dagcbor =
case conv <$> dagcbor of
EncoderError err -> Left err
EncoderValue result -> Right result
where
conv = Bases.encoder base . BSB.toLazyByteString
-- | Encodes DagCBOR to hexadecimal string.
toHex :: DagCBOR -> Okay Text
toHex = toDigits base16lower
-- | Provides a Encoder implementation for DAG-CBOR.
data Encoder encoded
= -- | An encoded value result.
EncoderValue encoded
| -- | For the `empty` object in the `Alternative` class.
EncoderError Error
deriving (Show, Eq)
-- | Pattern synonym for the `Alternative` `empty` object.
pattern EncoderEmpty :: forall encoded. Encoder encoded
pattern EncoderEmpty = EncoderError EmptyAlternative
-- | Converts `Okay` functor into `Encoder` functor.
{-# INLINE okay #-}
okay :: Okay t -> Encoder t
okay = either EncoderError EncoderValue
-- | Encodes the given IPLD node into DAG-CBOR binary format.
-- https://ipld.io/specs/codecs/dag-cbor/spec/
instance DM.Encoder Encoder where
intKind (IntKind int) = okay (encodeIntKind int)
intKind _ = empty
bytesKind (BytesKind bytes) = okay (encodeBytesKind bytes)
bytesKind _ = empty
stringKind (StringKind string) = okay (encodeStringKind string)
stringKind _ = empty
listKind (ListKind list) = encodeListKind list
listKind _ = empty
mapKind (MapKind dict) = encodeMapKind dict
mapKind _ = empty
resourceKind (ResourceKind cid) = okay (encodeResourceKind cid)
resourceKind _ = empty
floatKind (FloatKind float) = pure (encodeFloatKind float)
floatKind _ = empty
boolKind (BoolKind bool) = pure (encodeBoolKind bool)
boolKind _ = empty
nullKind NullKind = pure encodeNullKind
nullKind _ = empty
-- | Implements function injection (fmap) into some encoder process.
instance Functor Encoder where
-- Applies `f` to the encoded value if it exists
{-# INLINE fmap #-}
f `fmap` EncoderValue value = pure (f value)
_ `fmap` EncoderError err = EncoderError err
-- | Implements value injection in encoder context (pure) and applying some
-- process derived from encoding (<*>).
instance Applicative Encoder where
-- Wraps a Haskell type into an EncoderValue.
{-# INLINE pure #-}
pure = EncoderValue
-- Resolve an encoded function and encoded value and apply the function to the
-- value.
{-# INLINE (<*>) #-}
-- Extract the function from its context and apply it in value's context
EncoderValue func <*> value = func <$> value
EncoderError err <*> _ = EncoderError err
-- | Implements chaining encoders together in steps using binds (>>=).
instance Monad Encoder where
{-# INLINE (>>=) #-}
(EncoderValue value) >>= next = next value
(EncoderError err) >>= _ = EncoderError err
-- | Defines alternative code paths (<|>) and default failure element (empty).
instance Alternative Encoder where
-- For inputs that don't have the expected node type.
empty = EncoderEmpty
-- Tries the second code branch if the first fails
{-# INLINE (<|>) #-}
-- If the first branch is Just, return it
EncoderValue ok1 <|> _ = pure ok1
-- If the first branch is empty, return the second branch
EncoderEmpty <|> EncoderValue ok2 = pure ok2
-- If both branches are Nothing, return empty
EncoderEmpty <|> EncoderEmpty = empty
-- Otherwise, report the error that happened.
_ <|> EncoderError err = EncoderError err
EncoderError err <|> _ = EncoderError err
-- | Implements joining the results of two Encoders via semigroup adding (<>),
-- for Builders this would be concatinating the two bytestring contents.
instance (Semigroup s) => Semigroup (Encoder s) where
-- This should only return an empty value when both the results are empty.
{-# INLINE (<>) #-}
-- Concat the values within DagCBOR context
EncoderValue v1 <> EncoderValue v2 = pure (v1 <> v2)
EncoderValue v1 <> EncoderEmpty = pure v1
EncoderEmpty <> EncoderValue v2 = pure v2
EncoderEmpty <> EncoderEmpty = empty
-- Otherwise, report the error that happened
_ <> EncoderError err = EncoderError err
EncoderError err <> _ = EncoderError err
-- | Provides zero element `mempty` for encoder concatination. Applying (<>) to
-- this value will result an identity function.
instance (Semigroup s) => Monoid (Encoder s) where
mempty = empty
-- | Major id for CBOR lists.
majorList :: MajorArg
majorList = 4
-- | Recursively encodes the list of CBOR objects.
{-# INLINE encodeListKind #-}
encodeListKind :: ListData Kind -> Encoder Builder
encodeListKind listdata =
let -- Get the #elements in the list
len = length listdata
-- Encode the argument with the with the list's length
arg = encodeIntArgument majorList len
-- Initializes a Builder in the Encoder Monoid
initobj = pure mempty
-- Recursively encode the elements of the arguments
objs = foldr (\item acc -> DM.encoder item <> acc) initobj listdata
in -- Concat arg as DAG-CBOR data and the CBOR list objects
okay arg <> objs
-- | Major id for CBOR maps.
majorMap :: MajorArg
majorMap = 5
-- | Recursively encodes the key-value pairs of CBOR objects.
{-# INLINE encodeMapKind #-}
encodeMapKind :: MapData Kind Kind -> Encoder Builder
encodeMapKind mapdata =
let -- Get the #keys in the map
len = length mapdata
-- Encode the argument with only the #keys (#values is implied)
arg = encodeIntArgument majorMap len
-- Initializes a Builder in the Encoder Monoid
initobj = pure mempty
-- Recursively encode each key/value pair in the map.
objs = foldr (\pair acc -> encodeMapPair pair <> acc) initobj mapdata
in -- Concat the arg as DAG-CBOR and concat the encoded map.
okay arg <<>> objs
where
-- Encodes a single map pair from tuple into Builder.
{-# INLINE encodeMapPair #-}
encodeMapPair (key, val) = DM.encoder key <> DM.encoder val