ipldm-1.0.0.0: dagcbor/IPLD/DagCBOR/Decoder.hs
-- | Module : IPLD.DagCBOR.Decoder
-- Description : Implements DM.Decoder for DAG-CBOR
-- Copyright : Zoey McBride (c) 2026
-- License : AGPL-3.0-or-later
-- Maintainer : zoeymcbride@mailbox.org
-- Stability : experimental
module IPLD.DagCBOR.Decoder
( -- * Implements `DM.Decoder` for DAG-CBOR.
DagCBOR (decoderMethod),
-- * Evaluates the major parts of CBOR data.
evalMajorType,
evalMajorId,
-- * Evaulating DAG-CBOR binaries into `DM.Node`.
evalNode,
-- * Implements each `DM.Kind` for the decoder.
evalNullKind,
evalBoolKind,
evalIntKind,
evalFloatKind,
evalBytesKind,
evalStringKind,
evalResourceKind,
evalListKind,
evalMapKind,
)
where
import Control.Applicative (Alternative (..))
import Control.Monad ((>=>))
import Data.Bifunctor (Bifunctor (first))
import Data.XCodec.BinaryTranscoder (BinaryTranscoder)
import IPLD.DM qualified as DM
import IPLD.DM.Decoder qualified as DM
import IPLD.DagCBOR.Decoder.Errors
import IPLD.DagCBOR.Decoder.MajorTypes
import IPLD.DagCBOR.Decoder.Objects
-- | Implements `DM.Decoder` for DAG-CBOR binaries.
newtype DagCBOR bxc item = DagCBOR
{ -- | Provides the `DecoderMethod` for the `item` type.
decoderMethod :: DecoderMethod item bxc
}
-- | Evaluates the first byte's major-type in the given transcoder.
evalMajorType :: (BinaryTranscoder bxc) => MajorId -> DagCBOR bxc MajorType
evalMajorType typeid = DagCBOR decodeMajorType >>= matchTypeId
where
-- Fails with `EmptyAlternative` if the `MajorType` doesn't have the correct
-- `MajorId`.
matchTypeId majortype
| majorTypeId majortype /= typeid = pure majortype
| otherwise = empty
-- | Evaluates a well-formed CBOR binary object into the IPLD data model and
-- removes it from the transcoder.
evalNode :: (BinaryTranscoder bxc) => DagCBOR bxc DM.Node
evalNode = DagCBOR decodeObject
-- | Evaluates a `DM.Node` from DAG-CBOR when the major ID in the transcoder
-- matches the given param, and then removes the object from the binary.
-- Otherwise it fails with `EmptyAlternative`.
evalMajorId :: (BinaryTranscoder bxc) => MajorId -> DagCBOR bxc DM.Node
evalMajorId idtype = evalMajorType idtype >> evalNode
-- | Evaluates a `DM.IntKind` from DAG-CBOR. Fails with `EmptyAlternative`
-- when the binary doesn't contain the correct `DM.Kind`.
evalIntKind :: (BinaryTranscoder bxc) => DagCBOR bxc DM.Node
evalIntKind = evalMajorId idPositive <|> evalMajorId idNegative
-- | Evaluates a `DM.BytesKind` from DAG-CBOR. Fails with `EmptyAlternative`
-- when the binary doesn't contain the correct `DM.Kind`.
evalBytesKind :: (BinaryTranscoder bxc) => DagCBOR bxc DM.Node
evalBytesKind = evalMajorId idBytes
-- | Evaluates a `DM.StringKind` from DAG-CBOR. Fails with `EmptyAlternative`
-- when the binary doesn't contain the correct `DM.Kind`.
evalStringKind :: (BinaryTranscoder bxc) => DagCBOR bxc DM.Node
evalStringKind = evalMajorId idUTF8
-- | Evaluates a `DM.ListKind` from DAG-CBOR. Fails with `EmptyAlternative`
-- when the binary doesn't contain the correct `DM.Kind`.
evalListKind :: (BinaryTranscoder bxc) => DagCBOR bxc DM.Node
evalListKind = evalMajorId idList
-- | Evaluates a `DM.MapKind` from DAG-CBOR. Fails with `EmptyAlternative`
-- when the binary doesn't contain the correct `DM.Kind`.
evalMapKind :: (BinaryTranscoder bxc) => DagCBOR bxc DM.Node
evalMapKind = evalMajorId idMap
-- | Evaluates a `DM.ResourceKind` from DAG-CBOR. Fails with `EmptyAlternative`
-- when the binary doesn't contain the correct `DM.Kind`.
evalResourceKind :: (BinaryTranscoder bxc) => DagCBOR bxc DM.Node
evalResourceKind = evalMajorId idResource
-- | Evaluates a `DM.FloatKind` from DAG-CBOR. Fails with `EmptyAlternative`
-- when the binary doesn't contain the correct `DM.Kind`.
evalFloatKind :: (BinaryTranscoder bxc) => DagCBOR bxc DM.Node
evalFloatKind = evalMajorId idFloat >>= matchFloatKind
where
-- Fails with `EmptyAlternative` if the node isn't `DM.FloatKind`.
matchFloatKind node@(DM.FloatKind _) = pure node
matchFloatKind _ = empty
-- | Evaluates a `DM.BoolKind` from a DAG-CBOR. Fails with `EmptyAlternative`
-- when the binary doesn't contain the correct `DM.Kind`.
evalBoolKind :: (BinaryTranscoder bxc) => DagCBOR bxc DM.Node
evalBoolKind = evalMajorId idBool >>= matchBoolKind
where
-- Fails with `EmptyAlternative` if the node isn't `DM.BoolKind`.
matchBoolKind node@(DM.BoolKind _) = pure node
matchBoolKind _ = empty
-- | Evaluates a `DM.NullKind` from a DAG-CBOR. Fails with `EmptyAlternative`
-- when the binary doesn't contain the correct `DM.Kind`.
evalNullKind :: (BinaryTranscoder bxc) => DagCBOR bxc DM.Node
evalNullKind = evalMajorId idNull >>= matchNullKind
where
-- Fails with `EmptyAlternative` if the node isn't `DM.NullKind`.
matchNullKind node@DM.NullKind = pure node
matchNullKind _ = empty
-- | Parses the DM from some DAG-CBOR encoded bytestring.
-- https://ipld.io/specs/codecs/dag-cbor/spec/
instance (BinaryTranscoder bxc) => DM.Decoder (DagCBOR bxc) where
intKind = evalIntKind
bytesKind = evalBytesKind
stringKind = evalStringKind
listKind = evalListKind
mapKind = evalMapKind
resourceKind = evalResourceKind
floatKind = evalFloatKind
boolKind = evalBoolKind
nullKind = evalNullKind
-- | Implements function injection (fmap) into some decoder process.
instance (BinaryTranscoder bxc) => Functor (DagCBOR bxc) where
-- Decode the result and apply f to the first item in the struct
fmap f (DagCBOR decoder) = DagCBOR $ decoder >=> pure . first f
-- | Implements value injection in decoder context (pure) and applying some
-- process derived from parsing (<*>).
instance (BinaryTranscoder bxc) => Applicative (DagCBOR bxc) where
-- Push the value into the functor, keeping the original bytestring
pure value = DagCBOR $ pure . Result value
-- Composing functor wrapped functions
(DagCBOR step1) <*> (DagCBOR step2) =
DagCBOR $ \bytes -> do
-- Perform the 2 steps on the data
Result funcpart inner <- step1 bytes
Result datapart rest <- step2 inner
-- Apply the function and return the values from step2
Right $ Result (funcpart datapart) rest
-- | Binds next operation (>>=) in the DagCBOR context.
instance (BinaryTranscoder bxc) => Monad (DagCBOR bxc) where
(DagCBOR step1) >>= next =
DagCBOR $ \bytes -> do
Result input rest <- step1 bytes
let (DagCBOR step2) = next input
step2 rest
-- | Defines alternative code paths (<|>) and default failure element (empty).
instance (BinaryTranscoder bxc) => Alternative (DagCBOR bxc) where
-- Returns the special value marking a functor as an invalid code path
empty = DagCBOR $ const (Left EmptyAlternative)
-- Implements code branch selection
(DagCBOR step1) <|> (DagCBOR step2) =
DagCBOR $ \bytes ->
case step1 bytes of
-- Try the other branch if empty is called
Left EmptyAlternative -> step2 bytes
-- For other errors, return them to the caller
Left err -> Left err
-- Return the result if the first code branch works
Right result -> Right result