packages feed

polysemy-process-0.14.0.0: lib/Polysemy/Process/Interpreter/ProcessOutput.hs

{-# options_haddock prune #-}

-- | Description: ProcessOutput Interpreters, Internal
module Polysemy.Process.Interpreter.ProcessOutput where

import qualified Data.ByteString as ByteString

import Polysemy.Process.Data.ProcessOutputParseResult (ProcessOutputParseResult (Done, Fail, Partial))
import Polysemy.Process.Effect.ProcessOutput (ProcessOutput (Chunk))
import qualified Polysemy.Process.Effect.ProcessOutput as ProcessOutput

-- | Interpret 'ProcessOutput' by discarding any output.
interpretProcessOutputIgnore ::
  ∀ p a r .
  InterpreterFor (ProcessOutput p a) r
interpretProcessOutputIgnore =
  interpret \case
    Chunk _ _ ->
      pure ([], "")
{-# inline interpretProcessOutputIgnore #-}

-- | Interpret 'ProcessOutput' by immediately emitting raw 'ByteString's without accumulation.
interpretProcessOutputId ::
  ∀ p r .
  InterpreterFor (ProcessOutput p ByteString) r
interpretProcessOutputId =
  interpret \case
    Chunk buffer new ->
      pure ([buffer <> new], "")
{-# inline interpretProcessOutputId #-}

-- | Transformer for 'ProcessOutput' that lifts results into 'Left', creating 'ProcessOutput p (Either a b)' from
-- 'ProcessOutput p a'.
interpretProcessOutputLeft ::
  ∀ p a b r .
  Member (ProcessOutput p a) r =>
  InterpreterFor (ProcessOutput p (Either a b)) r
interpretProcessOutputLeft =
  interpret \case
    Chunk buf new ->
      first (fmap Left) <$> ProcessOutput.chunk @p buf new

-- | Transformer for 'ProcessOutput' that lifts results into 'Right', creating 'ProcessOutput p (Either a b)' from
-- 'ProcessOutput p b'.
interpretProcessOutputRight ::
  ∀ p a b r .
  Member (ProcessOutput p b) r =>
  InterpreterFor (ProcessOutput p (Either a b)) r
interpretProcessOutputRight =
  interpret \case
    Chunk buf new ->
      first (fmap Right) <$> ProcessOutput.chunk @p buf new

splitLines :: ByteString -> ByteString -> ([ByteString], ByteString)
splitLines buffer new =
  second fold (foldr' folder ([], Nothing) parts)
  where
    parts =
      ByteString.split 10 (buffer <> new)
    folder a (z, Nothing) =
      (z, Just a)
    folder a (z, Just r) =
      (a : z, Just r)

-- | Interpret 'ProcessOutput' by emitting individual 'ByteString' lines of output.
interpretProcessOutputLines ::
  ∀ p r .
  InterpreterFor (ProcessOutput p ByteString) r
interpretProcessOutputLines =
  interpret \case
    Chunk buffer new ->
      pure (splitLines buffer new)
{-# inline interpretProcessOutputLines #-}

-- | Interpret 'ProcessOutput' by immediately emitting 'Text' without accumulation.
interpretProcessOutputText ::
  ∀ p r .
  InterpreterFor (ProcessOutput p Text) r
interpretProcessOutputText =
  interpret \case
    Chunk buffer new ->
      pure ([decodeUtf8 (buffer <> new)], "")
{-# inline interpretProcessOutputText #-}

-- | Interpret 'ProcessOutput' by emitting individual 'Text' lines of output.
interpretProcessOutputTextLines ::
  ∀ p r .
  InterpreterFor (ProcessOutput p Text) r
interpretProcessOutputTextLines =
  interpret \case
    Chunk buffer new ->
      pure (first (fmap decodeUtf8) (splitLines buffer new))
{-# inline interpretProcessOutputTextLines #-}

type Parser a =
  ByteString -> ProcessOutputParseResult a

-- | Internal helper for 'interpretProcessOutputIncremental' that repeatedly parses elements from a chunk until the
-- parser returns a failure or a partial result.
parseMany ::
  Parser a ->
  Maybe (Parser a) ->
  ByteString ->
  (Maybe (Parser a), ([Either Text a], ByteString))
parseMany parse =
  spin id
  where
    spin cons cont = \case
      "" ->
        (cont, (cons [], ""))
      chunk ->
        case fromMaybe parse cont chunk of
          Fail e ->
            (Nothing, (cons [Left e], ""))
          Partial c ->
            (Just c, (cons [], ""))
          Done a rest ->
            spin (cons . (Right a :)) Nothing rest

-- | Whenever a chunk of output arrives, call the supplied incremental parser whose result must be converted to
-- 'ProcessOutputParseResult'.
-- If a partial parse result is produced, it is stored in the state and resumed when the next chunk is available.
-- If parsing an @a@ succeeds, the parser recurses until it fails.
interpretProcessOutputIncremental ::
  ∀ p a r .
  (ByteString -> ProcessOutputParseResult a) ->
  InterpreterFor (ProcessOutput p (Either Text a)) r
interpretProcessOutputIncremental parse =
  evalState (Nothing :: Maybe (ByteString -> ProcessOutputParseResult a)) .
  atomicStateToState @(Maybe (ByteString -> ProcessOutputParseResult a)) .
  interpret \case
    Chunk buffer new -> atomicState (flip (parseMany parse) (buffer <> new))
  . raiseUnder2