hermes-json-0.1.0.0: src/Data/Hermes/Decoder/Types.hs
{-# OPTIONS_HADDOCK show-extensions #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE ScopedTypeVariables #-}
-- | `Decoder` is the monad used for decoding JSON with simdjson via the FFI.
-- This module contains helpers for working with the `Decoder` context.
module Data.Hermes.Decoder.Types
( Decoder(runDecoder)
, HermesEnv(..)
, HermesException(..)
, DocumentError(path, errorMsg, docLocation, docDebug)
, allocaValue
, allocaArray
, allocaArrayIter
, allocaObject
, allocaObjectIter
, handleErrorCode
, typePrefix
, withDocumentPointer
, withParserPointer
, withHermesEnv
) where
import Control.Applicative (Alternative(..))
import Control.DeepSeq (NFData)
import Control.Monad.IO.Class (MonadIO, liftIO)
import Control.Monad.IO.Unlift (MonadUnliftIO)
import Control.Monad.Reader (MonadReader, asks)
import Control.Monad.Trans.Reader (ReaderT)
import Data.Text (Text)
import qualified Data.Text as T
import GHC.Generics (Generic)
import UnliftIO.Exception (Exception, bracket, catch, throwIO)
import UnliftIO.Foreign
( CInt
, ForeignPtr
, allocaBytes
, finalizeForeignPtr
, peekCString
, withForeignPtr
)
import Data.Hermes.SIMDJSON.Bindings (getErrorMessageImpl)
import Data.Hermes.SIMDJSON.Types
( Array(..)
, ArrayIter(..)
, Document(..)
, Object(..)
, ObjectIter(..)
, Parser(..)
, SIMDDocument
, SIMDErrorCode(..)
, SIMDParser
, Value(..)
)
import Data.Hermes.SIMDJSON.Wrapper
-- | A Decoder is some context around the IO needed by the C FFI to allocate local memory.
-- Users shouldn't need to deal with the underlying IO except in advanced use cases.
-- Using `Data.Hermes.decodeEither` discharges the IO and returns us to purity,
-- since we know decoding a document is referentially transparent.
newtype Decoder a = Decoder { runDecoder :: ReaderT HermesEnv IO a }
deriving newtype
( Functor
, Applicative
, Monad
, MonadIO
, MonadReader HermesEnv
, MonadUnliftIO
)
instance Alternative Decoder where
empty = fail "Unspecified error"
ad <|> bd = ad `catch` (\(_err :: HermesException) -> bd)
instance MonadFail Decoder where
{-# INLINE fail #-}
fail = throwHermes . T.pack
-- | Contains foreign references to the allocated simdjson::parser
-- and simdjson::document. Also maintains a path string that is updated
-- when an object field or array value is entered and which is displayed in errors.
data HermesEnv =
HermesEnv
{ hParser :: !(ForeignPtr SIMDParser)
, hDocument :: !(ForeignPtr SIMDDocument)
, hPath :: !Text
}
-- | Make a new HermesEnv. This allocates foreign references to
-- a simdjson::ondemand::parser and a simdjson::ondemand::document.
-- The optional capacity argument sets the max capacity in bytes for the
-- simdjson::ondemand::parser, which defaults to 4GB.
mkHermesEnv :: Maybe Int -> IO HermesEnv
mkHermesEnv mCapacity = do
parser <- mkSIMDParser mCapacity
document <- mkSIMDDocument
pure HermesEnv
{ hParser = parser
, hDocument = document
, hPath = ""
}
-- | Shortcut for constructing a default `HermesEnv`.
mkHermesEnv_ :: IO HermesEnv
mkHermesEnv_ = mkHermesEnv Nothing
-- | Internal finalizer for simdjson instances.
cleanupHermesEnv :: HermesEnv -> IO ()
cleanupHermesEnv hEnv = do
finalizeForeignPtr (hDocument hEnv)
finalizeForeignPtr (hParser hEnv)
-- | Run an action that is passed a `HermesEnv`.
-- The simdjson instances are created and destroyed using the `bracket` function.
withHermesEnv :: MonadUnliftIO m => (HermesEnv -> m a) -> m a
withHermesEnv = bracket acquire release
where
acquire = liftIO mkHermesEnv_
release = liftIO . cleanupHermesEnv
-- | The library can throw exceptions from simdjson in addition to
-- its own exceptions.
data HermesException =
SIMDException DocumentError
-- ^ An exception thrown from the simdjson library.
| InternalException DocumentError
-- ^ An exception thrown from an internal library function.
deriving stock (Eq, Show, Generic)
instance Exception HermesException
instance NFData HermesException
-- | Record containing all pertinent information for troubleshooting an exception.
data DocumentError =
DocumentError
{ path :: !Text
-- ^ The path to the current element determined by the decoder.
-- Formatted in the JSON Pointer standard per RFC 6901.
, errorMsg :: !Text
-- ^ An error message.
, docLocation :: !Text
-- ^ Truncated location of the simdjson document iterator.
, docDebug :: !Text
-- ^ Debug information from simdjson::document.
}
deriving stock (Eq, Show, Generic)
instance NFData DocumentError
mkDocumentError :: Text -> Text -> Text -> Text -> DocumentError
mkDocumentError pth msg locStr debugStr = DocumentError pth msg (T.take 20 locStr) debugStr
typePrefix :: Text -> Text
typePrefix typ = "Error while getting value of type " <> typ <> ". "
-- | Re-throw an exception caught from the simdjson library.
throwSIMD :: SIMDErrorCode -> Text -> Decoder a
throwSIMD errCode msg = do
pth <- asks hPath
if errCode `elem`
[ EMPTY
, INSUFFICIENT_PADDING
, SCALAR_DOCUMENT_AS_VALUE
, UTF8_ERROR
, UNCLOSED_STRING
, UNESCAPED_CHARS
]
then
liftIO . throwIO . SIMDException $
mkDocumentError pth msg "" ""
else do
withDocumentPointer $ \docPtr -> do
(locTxt, debugTxt) <- liftIO $ getDocumentInfo docPtr
liftIO . throwIO . SIMDException $
mkDocumentError pth msg locTxt debugTxt
-- | Throw an IO exception in the `Decoder` context.
throwHermes :: Text -> Decoder a
throwHermes msg = do
pth <- asks hPath
liftIO . throwIO . InternalException $
mkDocumentError pth msg "" ""
-- Foreign helpers
handleErrorCode :: Text -> CInt -> Decoder ()
handleErrorCode pre errInt = do
let errCode = toEnum $ fromIntegral errInt
if errCode == SUCCESS
then pure ()
else do
errStr <- peekCString =<< liftIO (getErrorMessageImpl errInt)
throwSIMD errCode $ pre <> T.pack errStr
{-# INLINE handleErrorCode #-}
withParserPointer :: (Parser -> Decoder a) -> Decoder a
withParserPointer f =
asks hParser >>= \parserFPtr -> withForeignPtr parserFPtr $ f . Parser
{-# INLINE withParserPointer #-}
withDocumentPointer :: (Document -> Decoder a) -> Decoder a
withDocumentPointer f =
asks hDocument >>= \docFPtr -> withForeignPtr docFPtr $ f . Document
{-# INLINE withDocumentPointer #-}
allocaValue :: (Value -> Decoder a) -> Decoder a
allocaValue f = allocaBytes 24 $ \val -> f (Value val)
{-# INLINE allocaValue #-}
allocaObject :: (Object -> Decoder a) -> Decoder a
allocaObject f = allocaBytes 24 $ \objPtr -> f (Object objPtr)
{-# INLINE allocaObject #-}
allocaArray :: (Array -> Decoder a) -> Decoder a
allocaArray f = allocaBytes 24 $ \arr -> f (Array arr)
{-# INLINE allocaArray #-}
allocaArrayIter :: (ArrayIter -> Decoder a) -> Decoder a
allocaArrayIter f = allocaBytes 24 $ \iter -> f (ArrayIter iter)
{-# INLINE allocaArrayIter #-}
allocaObjectIter :: (ObjectIter -> Decoder a) -> Decoder a
allocaObjectIter f = allocaBytes 24 $ \iter -> f (ObjectIter iter)
{-# INLINE allocaObjectIter #-}