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 #-}