packages feed

hq-json-0.1.1: test/Test/HQ.hs

{-# LANGUAGE ImportQualifiedPost #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE NoImplicitPrelude #-}

-- | Shared helpers for the @hq@ test suite.
module Test.HQ
  ( decodeChunks,
    runStreaming,
    drainCollect,
    splits,
    validPullCorpus,
    malformedPullCorpus,
    decodeShown,
  )
where

import Data.Text qualified as T
import HQ.Error (HQError (..), renderHQError)
import HQ.JSON.Decoder (DecodeError (..), DecoderResult (..), DecoderState (..), decodeTexts, finish, step)
import HQ.JSON.Event (JSONEvent)
import Relude hiding (Compose, id)

-- | Stream text chunks through the decode function.
decodeChunks :: [Text] -> Either DecodeError [JSONEvent]
decodeChunks = decodeTexts

-- | Helper to run a streaming decode and check the result.
runStreaming :: [Text] -> [JSONEvent]
runStreaming chunks = case decodeChunks chunks of
  Left _err -> []
  Right events -> events

-- | Drain events from a decoder result by repeatedly calling step,
-- then finalize with finish. Steps while input remains, and only calls
-- finish on a drained decoder: finish discards decoderInput, and
-- stepping an empty-input decoder that returns NeedInput would loop
-- forever.
drainCollect :: DecoderResult -> Either DecodeError [JSONEvent]
drainCollect (Emit event next) = case step next of
  Left err -> Left err
  Right result -> fmap (event :) (drainCollect result)
drainCollect (Done _) = Right []
drainCollect (NeedInput next)
  -- There is still unconsumed input; step it rather than calling finish
  -- (finish discards decoderInput, which would lose surrogate pairs,
  -- keywords, and numbers that span across the last feed boundary).
  | not (T.null (decoderInput next)) = case step next of
      Left err -> Left err
      Right result -> drainCollect result
  | otherwise = case finish next of
      Right (Emit event next') -> case step next' of
        Left err' -> Left err'
        Right result' -> fmap (event :) (drainCollect result')
      Right (Done _) -> Right []
      Right (NeedInput _) -> Left UnexpectedEnd
      Left err -> Left err

-- | Chunkings under which an input is decoded: whole, every two-way
-- split, and one-character chunks for short inputs.
splits :: Text -> [[Text]]
splits input
  | T.null input = [[input]]
  | otherwise =
      [input]
        : [[T.take n input, T.drop n input] | n <- [1 .. T.length input - 1]]
          ++ [[T.singleton c | c <- toString input] | T.length input <= 24]

-- | Streaming decode with errors shown, for differential testing.
-- Errors are rendered for the CLI boundary ('renderHQError') so both
-- drives compare the messages users actually see.
decodeShown :: [Text] -> Either Text [JSONEvent]
decodeShown chunks = case decodeChunks chunks of
  Left err -> Left (renderHQError (HQDecodeError err))
  Right events -> Right events

--------------------------------------------------------------------------------
-- Shared corpus
--------------------------------------------------------------------------------

validPullCorpus :: [Text]
validPullCorpus =
  [ "42",
    "-42",
    "0",
    "3.14",
    "-3.14",
    "0.15",
    "1e10",
    "1E10",
    "1e-2",
    "1e+2",
    "1.5e2",
    "-1.5e-6",
    "-1e5",
    "null",
    "true",
    "false",
    "\"\"",
    "\"hello\"",
    "\"a\\\"b\\\\c\\/\\b\\f\\n\\r\\t\"",
    "\"\\u00e9\"",
    "\"\\ud83d\\ude00\"",
    "[\"\\ud83d\\ude00\"]",
    "{\"u\":\"\\ud83d\\ude00\"}",
    "[]",
    "[1,2,3]",
    "[1,\"a\",true,null]",
    "{}",
    "{\"a\":1,\"b\":[2,3]}",
    " {\"a\" : [1, 2] } ",
    "",
    "   "
  ]

malformedPullCorpus :: [Text]
malformedPullCorpus =
  [ "\"abc",
    "{\"a\":1",
    "[1,2",
    "12a",
    "\"\\x\"",
    "\"\\u00z1\"",
    "\"\\ud83d\"",
    "\"\\ud83dX\"",
    "01",
    "1e",
    "1e+",
    "{\"a\" 1}",
    "[1,,2]",
    "[1 2]",
    "{,}",
    "nul",
    "-",
    "--1"
  ]