packages feed

jordan-0.2.0.0: lib/Jordan/FromJSON/UnboxedReporting.hs

{-# LANGUAGE DerivingVia #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE NoMonomorphismRestriction #-}

-- | Attempts to parse to either a result type or a direct report, by using a custom parser.
-- This parser uses Haskell primops to try to avoid allocations.
-- At the end of the day, it's not as fast as Attoparsec, but it's pretty dang fast.
--
-- We could not use Attoparsec directly due to the need for differnet error handling.
-- Other libraries with correct error handling behavior do exist, but in order to keep the dependency footprint low,
-- we rolled our own.
module Jordan.FromJSON.UnboxedReporting (parseOrReportWith, parseOrReport) where

import Control.Applicative (Alternative (empty, (<|>)))
import Control.Applicative.Combinators (sepBy)
import Control.Monad (when)
import Data.Bifunctor
import qualified Data.ByteString as BS
import Data.ByteString.Unsafe as BS
import Data.Char (chr, isControl, ord)
import Data.Functor (void, ($>))
import qualified Data.Map.Strict as Map
import Data.Maybe (isJust)
import Data.Monoid (Alt (..))
import Data.Scientific (Scientific)
import qualified Data.Scientific as Scientific
import qualified Data.Text as T
import Data.Text.Encoding (decodeUtf8', encodeUtf8)
import Data.Word (Word8)
import Jordan.FromJSON.Class
import Jordan.FromJSON.Internal.Attoparsec (bsToInteger)
import Jordan.FromJSON.Internal.Permutation
import Jordan.FromJSON.Internal.UnboxedParser as UP hiding (AccumE (..), AccumEL, AccumER)
import Jordan.FromJSON.Internal.UnboxedReporting
import Jordan.Types.Internal.AccumE (AccumE (AccumE))
import Jordan.Types.JSONError
  ( JSONArrayError (..),
    JSONError
      ( ErrorBadArray,
        ErrorBadObject,
        ErrorBadTextConstant,
        ErrorBadType,
        ErrorInvalidJSON,
        ErrorMesage,
        ErrorNoValue
      ),
    JSONObjectError (..),
  )
import Jordan.Types.JSONType (JSONType (..))
import Numeric (showHex)

newtype ReportingParser a = ReportingParser {runReportingParser :: UP.Parser JSONError a}
  deriving (Functor) via (UP.Parser JSONError)
  deriving (Semigroup) via (Alt (UP.Parser JSONError) a)

newtype ReportingObjectParser a = ReportingObjectParser
  {runReportingObjectParser :: Permutation (UP.Parser JSONObjectError) a}
  deriving (Functor, Applicative) via (Permutation (UP.Parser JSONObjectError))

newtype ReportingTupleParser a = ReportingTupleParser
  {runReportingTupleParser :: Integer -> (Integer, UP.Parser JSONArrayError a)}

instance Functor ReportingTupleParser where
  fmap f (ReportingTupleParser cb) =
    ReportingTupleParser $ \index -> second (f <$>) $ cb index

instance Applicative ReportingTupleParser where
  pure a = ReportingTupleParser (,pure a)
  (ReportingTupleParser f) <*> (ReportingTupleParser a) =
    ReportingTupleParser $ \index ->
      let (index', fp) = f index
          (index'', ap) = a index
       in ( index'',
            do
              f' <- fp
              when (index /= index' && index /= index'') comma
              f' <$> ap
          )

toObjectParser :: T.Text -> Parser JSONError a -> ReportingObjectParser a
toObjectParser field itemParser =
  ReportingObjectParser $
    asPermutationWithFailing parseKV failNoValue
  where
    failNoValue = do
      r <- UP.peekRest
      UP.failWith $
        MkJSONObjectError $ Map.singleton field ErrorNoValue
    parseKV = first (MkJSONObjectError . Map.singleton field) $ parseObjectKV field itemParser
{-# INLINE toObjectParser #-}

toObjectParserDef field itemParser def =
  ReportingObjectParser $
    asPermutationWithDefault parseKV def
  where
    parseKV = first (MkJSONObjectError . Map.singleton field) $ do
      parseObjectKV field itemParser

parseArrayInner :: UP.Parser JSONError a -> Integer -> UP.Parser JSONArrayError [a]
parseArrayInner parse index =
  ((:) <$> parseElem <*> ((comma *> parseArrayInner parse (index + 1)) <|> pure []))
    <|> pure []
  where
    parseElem = first (MkJSONArrayError . Map.singleton index) parse
{-# INLINE parseArrayInner #-}

parseDictKey :: UP.Parser JSONError a -> UP.Parser JSONObjectError (T.Text, a)
parseDictKey parseVal = do
  key <- textParser
  kvSep
  val <- first (MkJSONObjectError . Map.singleton key) parseVal
  pure (key, val)

instance JSONTupleParser ReportingTupleParser where
  consumeItemWith = \(ReportingParser itemParser) ->
    ReportingTupleParser $
      \index ->
        (index + 1, first (MkJSONArrayError . Map.singleton index) itemParser)

instance JSONObjectParser ReportingObjectParser where
  parseFieldWith field = \(ReportingParser itemParser) ->
    toObjectParser field itemParser
  parseFieldWithDefault field = \(ReportingParser itemParser) def ->
    toObjectParserDef field itemParser def

instance JSONParser ReportingParser where
  parseTuple (ReportingTupleParser tp) =
    ReportingParser $ do
      jt <- peekJSONType
      case jt of
        JSONTypeArray -> tuple
        other -> skipWithFailure $ ErrorBadType JSONTypeArray other
    where
      tuple = do
        startArray
        let (_, arrayParse) = tp 0
        arr <- first ErrorBadArray arrayParse
        endArray
        pure arr
  {-# INLINE parseTuple #-}
  parseTextConstant tc =
    ReportingParser $ do
      jt <- peekJSONType
      case jt of
        JSONTypeText -> textConstant
        other -> skipWithFailure $ ErrorBadType JSONTypeText other
    where
      textConstant = do
        r <- UP.specificWord 34
        void (parseSpecificKeyAfterQuote tc) <|> do
          r <- parseAfterQuote
          UP.failWith (ErrorBadTextConstant tc r)
  {-# INLINE parseTextConstant #-}
  parseArrayWith (ReportingParser rp) =
    ReportingParser $
      array
        <|> skipNullExpecting JSONTypeArray
        <|> skipBoolExpecting JSONTypeArray
        <|> skipTextExpecting JSONTypeArray
        <|> skipNumberExpecting JSONTypeArray
    where
      array = do
        startArray
        arr <- first ErrorBadArray $ parseArrayInner rp 0
        endArray
        pure arr
  {-# INLINE parseDictionary #-}
  parseDictionary (ReportingParser dict) =
    ReportingParser $ do
      jt <- peekJSONType
      case jt of
        JSONTypeObject -> parseDict
        other -> UP.asFailure $ skipAnything $> ErrorBadType JSONTypeObject other
    where
      parseDict = do
        startBracket
        r <- first ErrorBadObject $ parseDictKey dict `sepBy` comma
        endBracket
        pure r
  parseObject (ReportingObjectParser permute) = ReportingParser $ do
    r <- peekJSONType
    case r of
      JSONTypeObject -> po
      other -> UP.asFailure $ skipAnything $> ErrorBadType JSONTypeObject other
    where
      po = first ErrorBadObject $ do
        startBracket
        a <-
          wrapEffect
            skipAnyKV
            comma
            permute
        rest <- peekRest
        endBracket <|> do
          comma
          skipAnyKV `sepByVoid` comma
          endBracket
        pure a
  {-# INLINE parseObject #-}
  parseNull =
    ReportingParser $ do
      jt <- peekJSONType
      case jt of
        JSONTypeNull -> nullParser
        other -> skipWithFailure $ ErrorBadType JSONTypeNull other
  {-# INLINE parseNull #-}
  parseBool = ReportingParser $ do
    jt <- peekJSONType
    case jt of
      JSONTypeBool -> boolParser
      other -> skipWithFailure $ ErrorBadType JSONTypeBool other
  {-# INLINE parseBool #-}
  parseText = ReportingParser $ do
    jt <- peekJSONType
    case jt of
      JSONTypeText -> textParser
      other -> skipWithFailure $ ErrorBadType JSONTypeText other
  {-# INLINE parseText #-}
  parseNumber =
    ReportingParser $ do
      r <- peekJSONType
      case r of
        JSONTypeNumber -> scientific
        other -> UP.asFailure $ skipAnything $> ErrorBadType JSONTypeNumber other
  {-# INLINE parseNumber #-}
  validateJSON (ReportingParser rp) =
    ReportingParser $
      lowerErr (fmap (first ErrorMesage) rp)
  {-# INLINE validateJSON #-}

parseOrReportWith ::
  (forall parser. JSONParser parser => parser a) ->
  BS.ByteString ->
  Either JSONError a
parseOrReportWith (ReportingParser rp) bs =
  case UP.parseBS (UP.skipWhitespace *> rp) bs of
    Nothing -> Left ErrorInvalidJSON
    Just (AccumE r) -> r
{-# INLINE parseOrReportWith #-}

parseOrReport :: (FromJSON a) => BS.ByteString -> Either JSONError a
parseOrReport = parseOrReportWith fromJSON
{-# INLINE parseOrReport #-}