hq-json-0.1.1: src/HQ/JSON/Decoder.hs
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE ImportQualifiedPost #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE StrictData #-}
{-# LANGUAGE NoImplicitPrelude #-}
-- | Streaming JSON decoder over text chunks.
module HQ.JSON.Decoder
( module HQ.Error,
module HQ.JSON.Decoder.Core,
module HQ.JSON.Decoder.Error,
initialDecoder,
feed,
finish,
step,
decodeTexts,
pullEvent,
Terminal (..),
TerminalEmitted (..),
classifyTerminal,
)
where
import Data.Text qualified as T
import HQ.Early (Early, leave)
import HQ.Error
import HQ.JSON.Decoder.Core
import HQ.JSON.Decoder.Error
import HQ.JSON.Decoder.Keyword
import HQ.JSON.Decoder.Number
import HQ.JSON.Decoder.String
import HQ.JSON.Event (JSONEvent (..))
import Relude hiding (Compose, id, many, some, state)
import Streaming.Prelude qualified as S
initialDecoder :: DecoderState
initialDecoder =
DecoderState
{ decoderInput = mempty,
decoderStack = [],
decoderNestDepth = initialDepth,
decoderPhase = DecoderPhaseValue
}
feed :: Text -> DecoderState -> Either DecodeError DecoderResult
feed input decoder = step decoder {decoderInput = decoderInput decoder <> input}
finish :: DecoderState -> Either DecodeError DecoderResult
finish decoder = case decoderPhase decoder of
DecoderPhaseString (AfterHighSurrogate _ _) -> Left InvalidSurrogatePair
DecoderPhaseString (AfterLowBackslash _ _) -> Left InvalidSurrogatePair
DecoderPhaseString (InLowSurrogateEscape {}) -> Left InvalidSurrogatePair
DecoderPhaseString _ -> Left UnexpectedEnd
DecoderPhaseNumber numState
| isValidNumberFinal (numberPhase numState) -> finalizeNumber decoder
| otherwise -> Left $ InvalidNumber (numberErrorText numState)
DecoderPhaseKeyword _ -> finalizeKeyword decoder
DecoderPhaseFinished -> case decoderStack decoder of
[] ->
let isEmpty = T.null (decoderInput decoder)
in if isEmpty then Right (Done decoder) else Left TrailingInput
_ -> Left UnexpectedEnd
_ -> Left UnexpectedEnd
step :: DecoderState -> Either DecodeError DecoderResult
step !decoder = case decoderPhase decoder of
DecoderPhaseString state -> stepString decoder state
DecoderPhaseNumber numState -> stepNumber decoder numState
DecoderPhaseKeyword state -> stepKeyword decoder state
_ -> stepStructural decoder
stepStructural :: DecoderState -> Either DecodeError DecoderResult
stepStructural !decoder = case T.uncons input of
Nothing -> case decoderPhase decoder of
DecoderPhaseFinished ->
let newDecoder = decoder {decoderInput = mempty}
in Right
$ if null (decoderStack decoder)
then Done newDecoder
else NeedInput newDecoder
_ -> Right (NeedInput decoder {decoderInput = mempty})
Just (c, rest) -> parseStructuralChar c rest decoder
where
input = T.dropWhile isWhitespace (decoderInput decoder)
parseStructuralChar ::
Char -> Text -> DecoderState -> Either DecodeError DecoderResult
parseStructuralChar !c !rest !decoder = case decoderPhase decoder of
DecoderPhaseValue -> parseValueChar c rest decoder
DecoderPhaseObjectKey
| c == '}' -> case decoderStack decoder of
(_ : contexts) ->
let newDecoder = decoder {decoderStack = contexts}
in emitEvent JSONEndObject rest newDecoder
[] -> Left ExpectedObjectKey
| c == '"' -> startString StringKey rest decoder
| otherwise -> Left ExpectedObjectKey
DecoderPhaseObjectColon
| c == ':' ->
step decoder {decoderInput = rest, decoderPhase = DecoderPhaseValue}
| otherwise -> Left ExpectedColon
DecoderPhaseObjectComma
| c == ',' ->
step
decoder {decoderInput = rest, decoderPhase = DecoderPhaseObjectKey}
| c == '}' -> case decoderStack decoder of
(_ : contexts) ->
let newDecoder = decoder {decoderStack = contexts}
in emitEvent JSONEndObject rest newDecoder
[] -> Left ExpectedCommaOrEnd
| otherwise -> Left ExpectedCommaOrEnd
DecoderPhaseArrayComma
| c == ',' ->
step decoder {decoderInput = rest, decoderPhase = DecoderPhaseValue}
| c == ']' -> case decoderStack decoder of
(_ : contexts) ->
emitEvent JSONEndArray rest decoder {decoderStack = contexts}
[] -> Left ExpectedCommaOrEnd
| otherwise -> Left ExpectedCommaOrEnd
DecoderPhaseFinished -> Left TrailingInput
DecoderPhaseString _ -> Left (UnexpectedChar c)
DecoderPhaseNumber _ -> Left (UnexpectedChar c)
DecoderPhaseKeyword _ -> Left (UnexpectedChar c)
parseValueChar :: Char -> Text -> DecoderState -> Either DecodeError DecoderResult
parseValueChar !c !rest !decoder = case decoderStack decoder of
DecodeArray : contexts
| c == ']' ->
emitEvent JSONEndArray rest decoder {decoderStack = contexts}
_ -> startValue c rest decoder
startValue :: Char -> Text -> DecoderState -> Either DecodeError DecoderResult
startValue !c !rest !decoder = case c of
'{' ->
Right
$ Emit
JSONBeginObject
decoder
{ decoderInput = rest,
decoderStack = DecodeObject : decoderStack decoder,
decoderPhase = DecoderPhaseObjectKey
}
'[' ->
Right
$ Emit
JSONBeginArray
decoder
{ decoderInput = rest,
decoderStack = DecodeArray : decoderStack decoder,
decoderPhase = DecoderPhaseValue
}
'"' -> startString StringValue rest decoder
'n' -> startKeyword decoder rest "null" 1
't' -> startKeyword decoder rest "true" 1
'f' -> startKeyword decoder rest "false" 1
_
| isNumberStart c ->
let numState = startNumberState c
in stepNumber
decoder {decoderInput = rest, decoderPhase = DecoderPhaseNumber numState}
numState
| otherwise -> Left (UnexpectedChar c)
-- Root value complete: empty stack + finished state.
isRootDone :: DecoderState -> Bool
isRootDone decoder =
decoderPhase decoder == DecoderPhaseFinished && null (decoderStack decoder)
isEmptyDecoder :: DecoderState -> Bool
isEmptyDecoder dec = nullInputPred && nullStackPred && depthPred && phasePred
where
nullInputPred = T.null (decoderInput dec)
nullStackPred = null (decoderStack dec)
depthPred = decoderNestDepth dec == initialDepth
phasePred = decoderPhase dec == DecoderPhaseValue
-- | Terminal reading of a finished decoder; keep 'decodeTexts',
-- 'pullEvent' and Take's finisher in sync.
data Terminal
= TerminalEnd
| TerminalError DecodeError
| TerminalEmit TerminalEmitted
-- | Payload of a 'TerminalEmit': one trailing event with the decoder after it.
data TerminalEmitted = TerminalEmitted
{ terminalEvent :: JSONEvent,
terminalDecoder :: DecoderState
}
classifyTerminal :: DecoderState -> Terminal
classifyTerminal dec = case finish dec of
Left UnexpectedEnd
| decoderPhase dec == DecoderPhaseValue && null (decoderStack dec) -> TerminalEnd
Left err -> TerminalError err
Right (Done _) -> TerminalEnd
Right (NeedInput _) -> TerminalError UnexpectedEnd
Right (Emit event dec') -> TerminalEmit (TerminalEmitted event dec')
-- | Decode complete text chunks to events (single-shot driver for literals and tests).
decodeTexts :: [Text] -> Either DecodeError [JSONEvent]
decodeTexts = go initialDecoder
where
go dec [] = atEnd dec
go dec (chunk : rest) = case feed chunk dec of
Left err -> Left err
Right res -> drain res rest
drain (Emit event dec') rest = (event :) <$> stepped dec' rest
drain (NeedInput dec') rest
| not (T.null (decoderInput dec')) = stepped dec' rest
| isRootDone dec' = trailing dec' rest
| otherwise = go dec' rest
drain (Done dec') rest = trailing dec' rest
stepped dec' rest = case step dec' of
Left err -> Left err
Right res -> drain res rest
trailing dec' [] = atEnd dec'
trailing dec' (chunk : rest)
| isRootDone dec' =
if T.all isWhitespace chunk
then trailing dec' rest
else Left TrailingInput
| otherwise = Left TrailingInput
atEnd dec' = case classifyTerminal dec' of
TerminalEnd -> Right []
TerminalError err -> Left err
TerminalEmit (TerminalEmitted event dec'') -> (event :) <$> atEnd dec''
-- | Pull a single event; bulk loops drive 'step' directly.
pullEvent :: Early HQError -> DecoderState -> StreamIO Text () -> IO Next
pullEvent early decoder txtStream = case step decoder of
Left err -> leave early (HQDecodeError err)
Right (Emit event dec') -> pure $ NextEvent (NextEmitted event dec' txtStream)
Right (NeedInput dec') -> pullMore dec' txtStream
Right (Done dec') -> endCheck dec' txtStream
where
pullMore :: DecoderState -> StreamIO Text () -> IO Next
pullMore dec txt
-- Pending input takes precedence over pulling more text; only a drained decoder may end.
| not (T.null (decoderInput dec)) = case step dec of
Left err -> leave early (HQDecodeError err)
Right (Emit event dec') -> pure (NextEvent (NextEmitted event dec' txt))
Right (NeedInput dec') -> pullMore dec' txt
Right (Done dec') -> endCheck dec' txt
| otherwise = do
result <- S.next txt
case result of
Left ()
| isRootDone dec || isEmptyDecoder dec -> pure EndOfInput
| otherwise -> finishEnd dec
Right (chunk, rest) -> case feed chunk dec of
Left err -> leave early (HQDecodeError err)
Right (Emit event dec') -> pure (NextEvent (NextEmitted event dec' rest))
Right (NeedInput dec') -> pullMore dec' rest
Right (Done dec') -> endCheck dec' rest
endCheck :: DecoderState -> StreamIO Text () -> IO Next
endCheck dec txt = do
result <- S.next txt
case result of
Left () -> finishEnd dec
Right (chunk, rest)
| isRootDone dec ->
if T.all isWhitespace chunk
then endCheck dec rest
else leave early (HQDecodeError TrailingInput)
| otherwise -> leave early (HQDecodeError TrailingInput)
finishEnd :: DecoderState -> IO Next
finishEnd dec = case classifyTerminal dec of
TerminalEnd -> pure EndOfInput
TerminalError err -> leave early (HQDecodeError err)
TerminalEmit (TerminalEmitted event dec') -> pure $ NextEvent (NextEmitted event dec' (pure ()))