hermes-json-0.1.0.0: src/Data/Hermes/Decoder/Value.hs
{-# OPTIONS_HADDOCK show-extensions #-}
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE MultiWayIf #-}
module Data.Hermes.Decoder.Value
( atKey
, atKeyOptional
, atKeyStrict
, atPointer
, bool
, char
, double
, int
, list
, nullable
, objectAsKeyValues
, scientific
, string
, text
, listOfDouble
, listOfInt
, isNull
, withArray
, withBool
, withDocumentValue
, withDouble
, withInt
, withObject
, withRawByteString
, withString
, withText
) where
import Control.Monad ((>=>))
import Control.Monad.IO.Class (liftIO)
import Control.Monad.IO.Unlift (withRunInIO)
import qualified Data.Attoparsec.ByteString as A
import qualified Data.Attoparsec.ByteString.Char8 as AC (scientific)
import qualified Data.ByteString as BS
import qualified Data.ByteString.Char8 as BSC
import qualified Data.ByteString.Unsafe as Unsafe
import qualified Data.DList as DList
import qualified Data.Scientific as Sci
import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.Encoding as T
import UnliftIO.Foreign
( CStringLen
, alloca
, peek
, peekArray
, peekCStringLen
, toBool
)
import qualified UnliftIO.Foreign as Foreign
import Data.Hermes.Decoder.Path
import Data.Hermes.Decoder.Types
import Data.Hermes.SIMDJSON
-- | Parse the given input into a document iterator, get its
-- Value, which is either a JSON object or an array, and run the given
-- action on that Value.
withDocumentValue :: (Value -> Decoder a) -> InputBuffer -> Decoder a
withDocumentValue f inputPtr =
allocaValue $ \valPtr ->
withParserPointer $ \parserPtr ->
withDocumentPointer $ \docPtr -> do
err <- liftIO $ getDocumentValueImpl parserPtr inputPtr docPtr valPtr
handleErrorCode "" err
f valPtr
-- | Decode a value at the particular JSON pointer following RFC 6901.
-- Be careful where you use this because it rewinds the document on each
-- successive call.
atPointer :: Text -> (Value -> Decoder a) -> Decoder a
atPointer jptr f =
withDocumentPointer $ \docPtr ->
withRunInIO $ \run ->
Unsafe.unsafeUseAsCStringLen (T.encodeUtf8 jptr) $ \(cstr, len) -> run $
allocaValue $ \vPtr -> withPath jptr $ do
err <- liftIO $ atPointerImpl cstr len docPtr vPtr
handleErrorCode "" err
f vPtr
-- | Helper to work with an Object parsed from a Value.
withObject :: (Object -> Decoder a) -> Value -> Decoder a
withObject f valPtr = allocaObject $ \oPtr -> do
err <- liftIO $ getObjectFromValueImpl valPtr oPtr
handleErrorCode (typePrefix "object") err
f oPtr
-- | Helper to work with an ObjectIter started from a Value assumed to be an Object.
withObjectIter :: (ObjectIter -> Decoder a) -> Value -> Decoder a
withObjectIter f valPtr = allocaObjectIter $ \iterPtr -> do
err <- liftIO $ getObjectIterFromValueImpl valPtr iterPtr
handleErrorCode "" err
f iterPtr
{-# INLINE withObjectIter #-}
-- | Execute a function on each Field in an ObjectIter and
-- accumulate key-value tuples into a list.
iterateOverFields :: (Text -> Decoder a) -> (Value -> Decoder b) -> ObjectIter -> Decoder [(a, b)]
iterateOverFields fk fv iterPtr = withRunInIO $ \runInIO ->
alloca $ \lenPtr ->
alloca $ \keyPtr -> runInIO $
allocaValue $ \valPtr ->
go DList.empty keyPtr lenPtr valPtr
where
{-# INLINE go #-}
go !acc keyPtr lenPtr valPtr = do
isOver <- fmap toBool . liftIO $ objectIterIsDoneImpl iterPtr
if not isOver
then do
err <- liftIO $ objectIterGetCurrentImpl iterPtr keyPtr lenPtr valPtr
handleErrorCode "" err
kLen <- fmap fromIntegral . liftIO $ peek lenPtr
kStr <- liftIO $ peek keyPtr
keyTxt <- parseTextFromCStrLen (kStr, kLen)
withPath (dot keyTxt) $ do
k <- fk keyTxt
v <- fv valPtr
liftIO $ objectIterMoveNextImpl iterPtr
removePath (dot keyTxt) $
go (acc <> DList.singleton (k, v)) keyPtr lenPtr valPtr
else
pure $ DList.toList acc
withUnorderedField :: (Value -> Decoder a) -> Object -> Text -> Decoder a
withUnorderedField f objPtr key = withRunInIO $ \run ->
Unsafe.unsafeUseAsCStringLen (T.encodeUtf8 key) $ \(cstr, len) -> run $
allocaValue $ \vPtr -> withPath (dot key) $ do
err <- liftIO $ findFieldUnorderedImpl objPtr cstr len vPtr
handleErrorCode "" err
f vPtr
{-# INLINE withUnorderedField #-}
withUnorderedOptionalField :: (Value -> Decoder a) -> Object -> Text -> Decoder (Maybe a)
withUnorderedOptionalField f objPtr key = withRunInIO $ \run ->
Unsafe.unsafeUseAsCStringLen (T.encodeUtf8 key) $ \(cstr, len) -> run $
allocaValue $ \vPtr -> withPath (dot key) $ do
err <- liftIO $ findFieldUnorderedImpl objPtr cstr len vPtr
let errCode = toEnum $ fromIntegral err
if | errCode == SUCCESS -> Just <$> f vPtr
| errCode == NO_SUCH_FIELD -> pure Nothing
| otherwise -> Nothing <$ handleErrorCode "" err
{-# INLINE withUnorderedOptionalField #-}
withField :: (Value -> Decoder a) -> Object -> Text -> Decoder a
withField f objPtr key = withRunInIO $ \run ->
Unsafe.unsafeUseAsCStringLen (T.encodeUtf8 key) $ \(cstr, len) -> run $
allocaValue $ \vPtr -> withPath (dot key) $ do
err <- liftIO $ findFieldImpl objPtr cstr len vPtr
handleErrorCode "" err
f vPtr
{-# INLINE withField #-}
getInt :: Value -> Decoder Int
getInt valPtr = withRunInIO $ \run ->
alloca $ \ptr -> run $ do
err <- liftIO $ getIntImpl valPtr ptr
handleErrorCode (typePrefix "int") err
liftIO $ peek ptr
{-# INLINE getInt #-}
-- | Helper to work with an Int parsed from a Value.
withInt :: (Int -> Decoder a) -> Value -> Decoder a
withInt f = getInt >=> f
getDouble :: Value -> Decoder Double
getDouble valPtr = withRunInIO $ \run ->
alloca $ \ptr -> run $ do
err <- liftIO $ getDoubleImpl valPtr ptr
handleErrorCode (typePrefix "double") err
liftIO $ peek ptr
{-# INLINE getDouble #-}
-- | Helper to work with a Double parsed from a Value.
withDouble :: (Double -> Decoder a) -> Value -> Decoder a
withDouble f = getDouble >=> f
-- | Parse a Scientific using attoparsec's ByteString.Char8 parser.
parseScientific :: BS.ByteString -> Decoder Sci.Scientific
parseScientific
= either (\err -> fail $ "Failed to parse Scientific: " <> err) pure
. A.parseOnly (AC.scientific <* A.endOfInput)
. BSC.strip
{-# INLINE parseScientific #-}
getBool :: Value -> Decoder Bool
getBool valPtr = withRunInIO $ \run ->
alloca $ \ptr -> run $ do
err <- liftIO $ getBoolImpl valPtr ptr
handleErrorCode (typePrefix "bool") err
fmap toBool . liftIO $ peek ptr
{-# INLINE getBool #-}
-- | Helper to work with a Bool parsed from a Value.
withBool :: (Bool -> Decoder a) -> Value -> Decoder a
withBool f = getBool >=> f
withCStringLen :: Text -> (CStringLen -> Decoder a) -> Value -> Decoder a
withCStringLen lbl f valPtr = withRunInIO $ \run ->
alloca $ \strPtr ->
alloca $ \lenPtr -> run $ do
err <- liftIO $ getStringImpl valPtr strPtr lenPtr
handleErrorCode (typePrefix lbl) err
len <- fmap fromIntegral . liftIO $ peek lenPtr
str <- liftIO $ peek strPtr
f (str, len)
{-# INLINE withCStringLen #-}
getString :: Value -> Decoder String
getString = withCStringLen "string" (liftIO . peekCStringLen)
{-# INLINE getString #-}
getText :: Value -> Decoder Text
getText = withCStringLen "text" parseTextFromCStrLen
{-# INLINE getText #-}
parseTextFromCStrLen :: CStringLen -> Decoder Text
parseTextFromCStrLen cstr = do
bs <- liftIO $ Unsafe.unsafePackCStringLen cstr
case A.parseOnly asciiTextAtto bs of
Left err -> fail $ "Could not parse text: " <> err
Right Nothing -> pure $! T.decodeUtf8 bs
Right (Just r) -> pure $! r
{-# INLINE parseTextFromCStrLen #-}
asciiTextAtto :: A.Parser (Maybe Text)
asciiTextAtto = do
s <- A.takeWhile (\w -> w /= 92 && w >= 0x20 && w < 0x80)
let txt = T.decodeLatin1 s
mw <- A.peekWord8
case mw of
Nothing -> pure $ Just txt
_ -> pure Nothing
{-# INLINE asciiTextAtto #-}
getRawByteString :: Value -> Decoder BS.ByteString
getRawByteString valPtr = withRunInIO $ \run ->
alloca $ \strPtr ->
alloca $ \lenPtr -> run $ do
liftIO $ getRawJSONTokenImpl valPtr strPtr lenPtr
len <- fmap fromIntegral . liftIO $ peek lenPtr
str <- liftIO $ peek strPtr
liftIO $ Unsafe.unsafePackCStringLen (str, len)
{-# INLINE getRawByteString #-}
-- | Helper to work with the raw ByteString of the JSON token parsed from the given Value.
withRawByteString :: (BS.ByteString -> Decoder a) -> Value -> Decoder a
withRawByteString f = getRawByteString >=> f
{-# INLINE withRawByteString #-}
-- | Helper to work with a String parsed from a Value.
withString :: (String -> Decoder a) -> Value -> Decoder a
withString f = getString >=> f
-- | Helper to work with a Text parsed from a Value.
withText :: (Text -> Decoder a) -> Value -> Decoder a
withText f = getText >=> f
-- | Returns True if the Value is null.
isNull :: Value -> Decoder Bool
isNull valPtr = fmap toBool . liftIO $ isNullImpl valPtr
{-# INLINE isNull #-}
-- | Helper to work with an Array and its length parsed from a Value.
withArrayLen :: ((Array, Int) -> Decoder a) -> Value -> Decoder a
withArrayLen f val =
allocaArray $ \arrPtr ->
withRunInIO $ \run ->
alloca $ \outLen -> run $ do
err <- liftIO $ getArrayLenFromValueImpl val arrPtr outLen
handleErrorCode (typePrefix "array") err
len <- fmap fromIntegral . liftIO $ peek outLen
f (arrPtr, len)
{-# INLINE withArrayLen #-}
-- | Is more efficient by looping in C++ instead of Haskell.
listOfInt :: Value -> Decoder [Int]
listOfInt =
withArrayLen $ \(arrPtr, len) ->
Foreign.allocaArray len $ \out -> do
err <- liftIO $ intArrayImpl arrPtr out
handleErrorCode "Error decoding array of ints." err
liftIO $ peekArray len out
{-# RULES "list int/listOfInt" list int = listOfInt #-}
-- | Is more efficient by looping in C++ instead of Haskell.
listOfDouble :: Value -> Decoder [Double]
listOfDouble =
withArrayLen $ \(arrPtr, len) ->
Foreign.allocaArray len $ \out -> do
err <- liftIO $ doubleArrayImpl arrPtr out
handleErrorCode "Error decoding array of doubles." err
liftIO $ peekArray len out
{-# RULES "list double/listOfDouble" list double = listOfDouble #-}
-- | Helper to work with an Array parsed from a Value.
withArray :: (Array -> Decoder a) -> Value -> Decoder a
withArray f val =
allocaArray $ \arrPtr -> do
err <- liftIO $ getArrayFromValueImpl val arrPtr
handleErrorCode (typePrefix "array") err
f arrPtr
-- | Helper to work with an ArrayIter started from a Value assumed to be an Array.
withArrayIter :: (ArrayIter -> Decoder a) -> Value -> Decoder a
withArrayIter f valPtr =
allocaArrayIter $ \iterPtr -> do
err <- liftIO $ getArrayIterFromValueImpl valPtr iterPtr
handleErrorCode "" err
f iterPtr
{-# INLINE withArrayIter #-}
-- | Execute a function on each Value in an ArrayIter and
-- accumulate the results into a list.
iterateOverArray :: (Value -> Decoder a) -> ArrayIter -> Decoder [a]
iterateOverArray f iterPtr =
allocaValue $ \valPtr -> go (0 :: Int) DList.empty valPtr
where
{-# INLINE go #-}
go !n !acc valPtr = do
isOver <- fmap toBool . liftIO $ arrayIterIsDoneImpl iterPtr
if not isOver
then withPathIndex n $ do
err <- liftIO $ arrayIterGetCurrentImpl iterPtr valPtr
handleErrorCode "" err
result <- f valPtr
liftIO $ arrayIterMoveNextImpl iterPtr
go (n + 1) (acc <> DList.singleton result) valPtr
else
pure $ DList.toList acc
-- | Find an object field by key, where an exception is thrown
-- if the key is missing.
atKey :: Text -> (Value -> Decoder a) -> Object -> Decoder a
atKey key parser obj = withUnorderedField parser obj key
-- | Find an object field by key, where Nothing is returned
-- if the key is missing.
atKeyOptional :: Text -> (Value -> Decoder a) -> Object -> Decoder (Maybe a)
atKeyOptional key parser obj = withUnorderedOptionalField parser obj key
-- | Uses find_field, which means if you access a field out-of-order
-- this will throw an exception. It also cannot support optional fields.
atKeyStrict :: Text -> (Value -> Decoder a) -> Object -> Decoder a
atKeyStrict key parser obj = withField parser obj key
-- | Parse a homogenous JSON array into a Haskell list.
list :: (Value -> Decoder a) -> Value -> Decoder [a]
list f = withArrayIter $ iterateOverArray f
{-# INLINE[2] list #-}
-- | Parse an object into a homogenous list of key-value tuples.
objectAsKeyValues
:: (Text -> Decoder k)
-- ^ Parses a Text key in the Decoder monad. JSON keys are always text.
-> (Value -> Decoder v)
-- ^ Decoder for the field value.
-> Value
-> Decoder [(k, v)]
objectAsKeyValues kf vf = withObjectIter $ iterateOverFields kf vf
-- | Transforms a parser to return Nothing when the value is null.
nullable :: (Value -> Decoder a) -> Value -> Decoder (Maybe a)
nullable parser val = do
nil <- isNull val
if nil
then pure Nothing
else Just <$> parser val
-- | Parse only a single character.
char :: Value -> Decoder Char
char = getText >=> justOne
where
justOne txt =
case T.uncons txt of
Just (c, "") ->
pure c
_ ->
fail "Expected a single character"
-- | Parse a JSON string into a Haskell String.
-- For best performance you should use `text` instead.
string :: Value -> Decoder String
string = getString
-- | Parse a JSON string into Haskell Text.
text :: Value -> Decoder Text
text = getText
-- | Parse a JSON boolean into a Haskell Bool.
bool :: Value -> Decoder Bool
bool = getBool
-- | Parse a JSON number into an unsigned Haskell Int.
int :: Value -> Decoder Int
int = getInt
{-# INLINE[2] int #-}
-- | Parse a JSON number into a Haskell Double.
double :: Value -> Decoder Double
double = getDouble
{-# INLINE[2] double #-}
-- | Parse a Scientific from a Value.
scientific :: Value -> Decoder Sci.Scientific
scientific = withRawByteString parseScientific