packages feed

hw-kafka-streamly-0.2.0.0: src/Kafka/Streamly/Combinators.hs

{- |
Module      : Kafka.Streamly.Combinators
Description : Auxiliary combinators for Streamly–Kafka pipelines.

Auxiliary combinators that bridge consumer streams and producer folds:
size-bounded batching with an explicit flush token, and helpers for callers
who prefer exceptions over the @Either KafkaError a@ values that
'Kafka.Streamly.Stream' yields.

Re-exports t'Kafka.Types.BatchSize' from "Kafka.Types" for convenience.
-}
module Kafka.Streamly.Combinators (
    -- * Types
    BatchSize (..),

    -- * Batching combinators
    batchByOrFlush,
    batchByOrFlushEither,

    -- * Error handling
    throwLeft,
    throwLeftSatisfy,
) where

import Control.Monad.Catch (Exception, MonadThrow, throwM)
import Kafka.Types (BatchSize (..))
import Streamly.Data.Scanl qualified as Scanl
import Streamly.Data.Stream (Stream)
import Streamly.Data.Stream qualified as Stream

{- | Throws the left part of a value as an exception, passing right values
through.

@since 0.1.0.0
-}
throwLeft ::
    (MonadThrow m, Exception e) =>
    Stream m (Either e a) ->
    Stream m a
throwLeft = Stream.mapMaybeM $ \case
    Left e -> throwM e
    Right a -> pure (Just a)
{-# INLINE throwLeft #-}

{- | Throws the left part of a value as an exception if it satisfies the
predicate. Non-matching left values and all right values pass through
unchanged.

@since 0.1.0.0
-}
throwLeftSatisfy ::
    (MonadThrow m, Exception e) =>
    (e -> Bool) ->
    Stream m (Either e a) ->
    Stream m (Either e a)
throwLeftSatisfy p = Stream.mapMaybeM $ \case
    Left e | p e -> throwM e
    v -> pure (Just v)
{-# INLINE throwLeftSatisfy #-}

{- | Batch stream elements by size, flushing on 'Nothing'.

Elements wrapped in 'Just' accumulate into a batch. When the batch reaches
the specified size or a 'Nothing' is received, the current batch is emitted.
Empty batches are not emitted. A final incomplete batch is emitted when the
stream ends.

Raises an 'error' if the t'BatchSize' is non-positive — @BatchSize 0@ would
emit a singleton batch for every element, which is never what the caller
wants.

@since 0.1.0.0
-}
batchByOrFlush ::
    (Monad m) =>
    BatchSize ->
    Stream m (Maybe a) ->
    Stream m [a]
batchByOrFlush n input =
    batchInternal "batchByOrFlush" n (Stream.append input (Stream.fromPure Nothing))
{-# INLINE batchByOrFlush #-}

{- | Batch stream elements by size, flushing on 'Left'.

'Right' values accumulate into a batch. When the batch reaches
the specified size or a 'Left' is received, the current batch is emitted.
Empty batches are not emitted. A final incomplete batch is emitted when the
stream ends.

Raises an 'error' if the t'BatchSize' is non-positive.

@since 0.1.0.0
-}
batchByOrFlushEither ::
    (Monad m) =>
    BatchSize ->
    Stream m (Either e a) ->
    Stream m [a]
batchByOrFlushEither n input =
    batchInternal "batchByOrFlushEither" n $
        Stream.append (fmap eitherToMaybe input) (Stream.fromPure Nothing)
  where
    eitherToMaybe (Left _) = Nothing
    eitherToMaybe (Right a) = Just a
{-# INLINE batchByOrFlushEither #-}

-- Internal batching implementation shared by both combinators.
batchInternal ::
    (Monad m) =>
    String ->
    BatchSize ->
    Stream m (Maybe a) ->
    Stream m [a]
batchInternal caller (BatchSize n)
    | n <= 0 = error (caller <> ": BatchSize must be positive, got " <> show n)
    | otherwise =
        Stream.catMaybes
            . fmap extract
            . Stream.scanl (Scanl.mkScanl step initial)
  where
    initial :: (Int, [a], Maybe [a])
    initial = (0, [], Nothing)

    extract :: (Int, [a], Maybe [a]) -> Maybe [a]
    extract (_, _, out) = out

    step :: (Int, [a], Maybe [a]) -> Maybe a -> (Int, [a], Maybe [a])
    step (_, acc, _) Nothing =
        if null acc
            then (0, [], Nothing)
            else (0, [], Just (reverse acc))
    step (i, acc, _) (Just a) =
        let acc' = a : acc
         in if i + 1 >= n
                then (0, [], Just (reverse acc'))
                else (i + 1, acc', Nothing)