hq-json-0.1.1: src/HQ/Runner/Take.hs
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE ImportQualifiedPost #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE StrictData #-}
{-# LANGUAGE NoImplicitPrelude #-}
module HQ.Runner.Take where
import Data.Text qualified as T
import HQ.Early (Early, leave)
import HQ.JSON.Decoder
import HQ.JSON.Decoder qualified as Decoder
import HQ.JSON.Event (JSONEvent (..), matchingClose)
import HQ.Runner.Cursor
import HQ.Runner.Error (RunnerError (..))
import Relude hiding (Compose, id, many, some, state)
import Streaming (Of, Stream)
import Streaming.Prelude qualified as S
-- | Pull a single event, with the advanced cursor.
pullOne :: Early HQError -> Cursor -> IO Step
pullOne early =
pullCursor early
>=> maybe (leave early $ HQRunnerError UnexpectedEndOfInput) pure
-- | True when the decoder still holds pending input that must be
-- stepped before pulling more text. Single name for the 4x
-- @not (T.null (decoderInput ...))@ guard.
hasPending :: DecoderState -> Bool
hasPending dec = not (T.null (decoderInput dec))
-- | Shared 'finish' decision for take loops: top-level exhaustion
-- means a truncated take here (contrast 'atEnd'/'finishEnd', where it
-- ends cleanly). Decided by 'classifyTerminal', like both drivers.
finishTakeEvent :: Early HQError -> DecoderState -> IO (JSONEvent, DecoderState)
finishTakeEvent early dec = case classifyTerminal dec of
TerminalEnd -> leave early $ HQRunnerError UnexpectedEndOfInput
TerminalError err -> leave early (HQDecodeError err)
TerminalEmit (TerminalEmitted event dec') -> pure (event, dec')
-- | Stream the events of exactly one complete JSON value. Lazy: stops
-- pulling once the value is complete.
takeValue :: Early HQError -> Continuation
takeValue early cursor = do
Step event cursor' <- lift (pullOne early cursor)
S.yield event
case matchingClose event of
Just closing -> takeContainerFrom early closing cursor'
Nothing -> case event of
JSONEndArray -> lift (leave early $ HQRunnerError UnexpectedEndOfArray)
JSONEndObject -> lift (leave early $ HQRunnerError UnexpectedEndOfObject)
JSONObjectKey _ -> lift (leave early $ HQRunnerError UnexpectedObjectKey)
_ -> pure cursor'
takeFirstValue :: EventStream r -> EventStream ()
takeFirstValue = go (0 :: Int)
where
go depth stream = do
result <- lift (S.next stream)
case result of
Left _ -> pure ()
Right (event, rest) -> do
S.yield event
case event of
JSONBeginArray -> go (depth + 1) rest
JSONBeginObject -> go (depth + 1) rest
JSONEndArray
| depth == 1 -> pure ()
| otherwise -> go (depth - 1) rest
JSONEndObject
| depth == 1 -> pure ()
| otherwise -> go (depth - 1) rest
_
| depth == 0 -> pure ()
| otherwise -> go depth rest
-- | Take a container body event by event, driving the decoder directly.
-- Thin wrapper over 'takeContainerGeneric' with 'S.yield' as the output
-- action and no extra state.
takeContainerFrom :: Early HQError -> JSONEvent -> Cursor -> EventStream Cursor
takeContainerFrom early closing cursor = do
(cur, _) <- takeContainerGeneric early closing cursor () outputOne
pure cur
where
outputOne :: JSONEvent -> () -> EventStream ()
outputOne event () = S.yield event
{-# INLINE takeContainerFrom #-}
-- | Single event-by-event container loop shared by the read side
-- ('takeContainerFrom', output = 'S.yield') and the rewrite side
-- ('takeContainerChunks' in "HQ.Runner.Rewrite.Emit", output =
-- 'emitChunk'): same decoder driving, same errors, same termination;
-- only the per-event output action and the threaded state differ. Both
-- callers inline it, exposing the concrete output action (verified by
-- benchmark; no 'SPECIALIZE' pragma, which is illegal here).
{-# INLINE takeContainerGeneric #-}
takeContainerGeneric ::
forall e s.
Early HQError ->
JSONEvent ->
Cursor ->
s ->
(JSONEvent -> s -> Stream (Of e) IO s) ->
Stream (Of e) IO (Cursor, s)
takeContainerGeneric early closing cursor0 st0 outputOne = go cursor0 st0
where
go :: Cursor -> s -> Stream (Of e) IO (Cursor, s)
go (Cursor buf dec txt) st = case buf of
event : rest -> emit event (Cursor rest dec txt) st
[] -> stepMore dec txt st
-- Step while the decoder holds pending input: mid-value pauses
-- (e.g. a high surrogate awaiting its low half) must be stepped,
-- not finished. Only a truly drained decoder may pull more text.
-- Mirrors 'pullMore' in "HQ.Runner.Cursor".
stepMore :: DecoderState -> StreamIO Text () -> s -> Stream (Of e) IO (Cursor, s)
stepMore dec txt st = case Decoder.step dec of
Left err -> lift (leave early $ HQDecodeError err)
Right (Emit event dec') -> emit event (Cursor [] dec' txt) st
Right (NeedInput dec')
| hasPending dec' -> stepMore dec' txt st
| otherwise -> pullMore dec' txt st
Right (Done _) -> lift (leave early $ HQRunnerError UnexpectedEndOfInput)
pullMore :: DecoderState -> StreamIO Text () -> s -> Stream (Of e) IO (Cursor, s)
pullMore dec txt st = do
result <- lift (S.next txt)
case result of
Left () -> finishTake dec st
Right (chunk, rest) -> case Decoder.feed chunk dec of
Left err -> lift (leave early (HQDecodeError err))
Right (Emit event dec') -> emit event (Cursor [] dec' rest) st
Right (NeedInput dec') -> stepMore dec' rest st
Right (Done _) -> lift (leave early $ HQRunnerError UnexpectedEndOfInput)
-- Mirror terminal 'finish' policy: a value completed exactly at end
-- of input still yields its final event; anything else ends the
-- take the same way the event-stream takes did.
finishTake :: DecoderState -> s -> Stream (Of e) IO (Cursor, s)
finishTake dec st = do
(event, dec') <- lift (finishTakeEvent early dec)
emit event (Cursor [] dec' (pure ())) st
emit :: JSONEvent -> Cursor -> s -> Stream (Of e) IO (Cursor, s)
emit !event cur st = do
st' <- outputOne event st
if event == closing
then pure (cur, st')
else case matchingClose event of
Just end -> nested end cur st'
Nothing -> go cur st'
nested :: JSONEvent -> Cursor -> s -> Stream (Of e) IO (Cursor, s)
nested end cur st = do
(cur', st') <- takeContainerGeneric early end cur st outputOne
go cur' st'