packages feed

hq-json-0.1.1: src/HQ/JSON/Decoder/Core.hs

{-# LANGUAGE ImportQualifiedPost #-}
{-# LANGUAGE StrictData #-}
{-# LANGUAGE NoImplicitPrelude #-}

-- | Shared vocabulary of the streaming JSON decoder.
module HQ.JSON.Decoder.Core where

import Data.Char (digitToInt, isHexDigit)
import Data.Text qualified as T
import HQ.JSON.Decoder.Error (DecodeError)
import HQ.JSON.Event (JSONEvent (..))
import Relude hiding (Compose, id, many, some, state)
import Streaming (Of, Stream)

-- | Buffered string fragments (reversed for O(1) prepend).
newtype StringBuffer = StringBuffer [Text] deriving (Eq)

emptyStringBuffer :: StringBuffer
emptyStringBuffer = StringBuffer []

appendStringBuffer :: Text -> StringBuffer -> StringBuffer
appendStringBuffer t (StringBuffer ts)
  | T.null t = StringBuffer ts
  | otherwise = StringBuffer (t : ts)

appendCharStringBuffer :: Char -> StringBuffer -> StringBuffer
appendCharStringBuffer c = appendStringBuffer (T.singleton c)

finishStringBuffer :: StringBuffer -> Text
finishStringBuffer (StringBuffer ts) = T.concat (reverse ts)

-- | Nesting depth: the number of enclosing containers.
newtype NestDepth = NestDepth Int deriving (Eq, Ord)

initialDepth :: NestDepth
initialDepth = NestDepth 0

deeper :: NestDepth -> NestDepth
deeper (NestDepth n) = NestDepth (n + 1)

shallower :: NestDepth -> NestDepth
shallower (NestDepth n) = NestDepth (n - 1)

data DecoderState = DecoderState
  { decoderInput :: Text,
    decoderStack :: [DecodeContext],
    decoderNestDepth :: NestDepth,
    decoderPhase :: DecoderPhase
  }

data DecoderPhase
  = DecoderPhaseValue
  | DecoderPhaseObjectKey
  | DecoderPhaseObjectColon
  | DecoderPhaseObjectComma
  | DecoderPhaseArrayComma
  | DecoderPhaseString StringState
  | DecoderPhaseNumber NumberState
  | DecoderPhaseKeyword KeywordState
  | DecoderPhaseFinished
  deriving (Eq)

data DecodeContext = DecodeArray | DecodeObject

data NumberState = NumberState
  { numberNegative :: Bool,
    numberCoeff :: Integer,
    -- | Exponent digits accumulated so far.
    numberExp :: Integer,
    -- | Fraction digits folded into 'numberCoeff'.
    numberFracDigits :: Int,
    -- | '-' seen after 'e'/'E'.
    numberExpNeg :: Bool,
    numberPhase :: NumberPhase,
    -- Error payloads only: retained, never decoded, on the success path.
    numberFrags :: [Text]
  }
  deriving (Eq)

-- | Tracks how much of a JSON number has been consumed.
data NumberPhase
  = -- | Saw leading '-'
    NumberSign
  | -- | Saw '0' (must not be followed by more digits)
    NumberZero
  | -- | Saw [1-9] and possibly more digits
    NumberNonZero
  | -- | Saw '.' after integer part (need fractional digits)
    NumberDot
  | -- | Saw '.' + at least one digit
    NumberFraction
  | -- | Saw 'e'/'E' (waiting for optional sign or digit)
    NumberExpSign
  | -- | Saw 'e'/'E' then '+'/'-' (need digits)
    NumberExpAfterSign
  | -- | Saw exponent digits
    NumberExpDigit
  deriving (Eq)

data NumberStep
  = NumberStep NumberPhase
  | NumberEnd
  | NumberError

data StringTarget = StringKey | StringValue
  deriving (Eq)

data BufferedStringTarget = BufferedStringTarget StringTarget StringBuffer
  deriving (Eq)

data Unicode = Unicode {unicodeValue :: Int, unicodeDigits :: Int}
  deriving (Eq)

data StringState
  = InString BufferedStringTarget
  | AfterEscape BufferedStringTarget
  | InUnicodeEscape BufferedStringTarget Unicode
  | AfterHighSurrogate BufferedStringTarget Int
  | -- Mid low-surrogate digits; separate so split escapes resume in the low half.
    InLowSurrogateEscape BufferedStringTarget Int Unicode
  | -- Low escape's backslash at a chunk end; next chunk must start with @u@.
    AfterLowBackslash BufferedStringTarget Int
  deriving (Eq)

data KeywordState
  = KeywordNull Int
  | KeywordTrue Int
  | KeywordFalse Int
  deriving (Eq)

data DecoderResult
  = Emit JSONEvent DecoderState
  | NeedInput DecoderState
  | Done DecoderState

-- | Emit one event, continuing after the value (container ends pop
-- and mark the parent via 'finishValue'). Single policy for scalars
-- and container ends. Inlined into the call sites.
{-# INLINE emitEvent #-}
emitEvent :: JSONEvent -> Text -> DecoderState -> Either DecodeError DecoderResult
emitEvent event remaining decoder =
  let newDec = decoder {decoderInput = remaining}
   in Right $ Emit event $ finishValue newDec

finishValue :: DecoderState -> DecoderState
finishValue decoder = case decoderStack decoder of
  [] -> decoder {decoderPhase = DecoderPhaseFinished}
  DecodeArray : _ -> decoder {decoderPhase = DecoderPhaseArrayComma}
  DecodeObject : _ -> decoder {decoderPhase = DecoderPhaseObjectComma}

isWhitespace :: Char -> Bool
isWhitespace c = c `elem` [' ', '\t', '\n', '\r']

isJsonDelimiter :: Char -> Bool
isJsonDelimiter c = isWhitespace c || c `elem` [',', ']', '}']

isHighSurrogate :: Int -> Bool
isHighSurrogate x = x >= 0xD800 && x <= 0xDBFF

isLowSurrogate :: Int -> Bool
isLowSurrogate x = x >= 0xDC00 && x <= 0xDFFF

-- | Verbatim string bytes; shared so decoding, skipping and collection
-- split identically.
isStringChar :: Char -> Bool
isStringChar c = c /= '"' && c /= '\\' && ord c >= 0x20

-- | Simple (non-\u) escapes in the JSON string table.
isSimpleEscape :: Char -> Bool
isSimpleEscape c = c `elem` ['"', '\\', '/', 'b', 'f', 'n', 'r', 't']

-- | Decode a simple escape to its character. Caller must have checked
-- 'isSimpleEscape' (except 'u', handled separately).
decodeSimpleEscape :: Char -> Char
decodeSimpleEscape '"' = '"'
decodeSimpleEscape '\\' = '\\'
decodeSimpleEscape '/' = '/'
decodeSimpleEscape 'b' = '\b'
decodeSimpleEscape 'f' = '\f'
decodeSimpleEscape 'n' = '\n'
decodeSimpleEscape 'r' = '\r'
decodeSimpleEscape 't' = '\t'
decodeSimpleEscape c = c

-- | Accumulate up to @needed@ hex digits from the head of @input@.
-- Returns @(value', digits', rest)@ where @value'@ folds the new digits
-- into @value@ and @rest@ is the unconsumed remainder.
accumulateHex :: Int -> Int -> Text -> (Int, Int, Text)
accumulateHex value digits input =
  let needed = 4 - digits
      hex = T.take needed (T.takeWhile isHexDigit input)
      rest = T.drop (T.length hex) input
      value' = T.foldl' (\v c -> v * 16 + digitToInt c) value hex
      digits' = digits + T.length hex
   in (value', digits', rest)

data Next = EndOfInput | NextEvent NextEmitted

-- | One decoded event with the decoder and text positioned after it.
data NextEmitted = NextEmitted
  { nextEvent :: JSONEvent,
    nextDecoder :: DecoderState,
    nextText :: StreamIO Text ()
  }

-- | Stream over 'IO'. Failures abort via 'HQ.Early' exceptions.
type StreamIO s = Stream (Of s) IO