packages feed

hermes-json 0.6.0.0 → 0.6.1.0

raw patch · 7 files changed

+142/−13 lines, 7 filesdep ~basedep ~bytestringdep ~primitivePVP ok

version bump matches the API change (PVP)

Dependency ranges changed: base, bytestring, primitive, text

API changes (from Hackage documentation)

+ Data.Hermes: formatException :: HermesException -> Text
+ Data.Hermes: liftObjectDecoder :: Decoder a -> FieldsDecoder a
+ Data.Hermes: objectAsMapExcluding :: Ord k => [Text] -> (Text -> Decoder k) -> Decoder v -> Decoder (Map k v)
+ Data.Hermes.Decoder.Value: objectAsMapExcluding :: Ord k => [Text] -> (Text -> Decoder k) -> Decoder v -> Decoder (Map k v)

Files

CHANGELOG.md view
@@ -1,5 +1,19 @@ # Revision history for hermes-json +## 0.6.1.0 -- 2023-08-27++### Changed+* Bump upper bounds on dependencies++### Fixed+* Fix bug in `FieldsDecoder` `Alternative` instance that was causing seg faults+  on missing keys++### Added+* Add `liftObjectDecoder`+* Add `objectAsMapExcluding`+* Add `formatException`+ ## 0.6.0.0 -- 2023-08-22  ### Breaking Changes:
README.md view
@@ -79,6 +79,10 @@ * Partial decoding of Twitter status objects to highlight the on-demand benefits * Decoding entire documents into `Data.Aeson.Value` +Please be aware that GHC does not report C-allocated memory. simdjson does actually+allocate more memory than appears here, but we still strive to keep our Haskell memory+footprint as small as possible.+ ### Specs  * GHC 9.4.6 w/ -O1
hermes-json.cabal view
@@ -1,6 +1,6 @@ cabal-version:      3.0 name:               hermes-json-version:            0.6.0.0+version:            0.6.1.0 category:           Text, Web, JSON, FFI synopsis:           Fast JSON decoding via simdjson C++ bindings description:@@ -61,15 +61,15 @@     Data.Hermes.Decoder.Internal     Data.Hermes.Decoder.Internal.Scientific   build-depends:-    base               >= 4.13 && < 4.19,-    bytestring         >= 0.10.12 && < 0.12,+    base               >= 4.13 && < 4.20,+    bytestring         >= 0.10.12 && < 0.13,     containers         >= 0.6.5 && < 0.7,     deepseq            >= 1.4.4 && < 1.5,     dlist              >= 0.8 && < 1.1,     integer-conversion >= 0.1 && < 0.2,-    primitive          >= 0.7.0 && < 0.9,+    primitive          >= 0.7.0 && < 0.10,     scientific         >= 0.3.6 && < 0.4,-    text               >= 2.0 && < 2.1,+    text               >= 2.0 && < 2.2,     text-iso8601       >= 0.1 && < 0.2,     transformers       >= 0.5.6 && < 0.7,     time               >= 1.9.3 && < 1.13,
src/Data/Hermes.hs view
@@ -52,6 +52,8 @@   , nullable   , objectAsKeyValues   , objectAsMap+  , objectAsMapExcluding+  , liftObjectDecoder     -- ** Date and time     -- | Parses date and time types from Data.Time using the     -- same Text parsers as Data.Aeson via@@ -67,6 +69,7 @@     -- * Error Types   , HermesException(..)   , DocumentError(..)+  , formatException     -- * Value helpers   , getType   , isNull@@ -102,6 +105,8 @@   , HermesException(..)   , decodeEither   , decodeEitherIO+  , formatException+  , liftObjectDecoder   , mkHermesEnv   , mkHermesEnv_   , parseByteString
src/Data/Hermes/Decoder/Internal.hs view
@@ -22,6 +22,7 @@   , local   , decodeEither   , decodeEitherIO+  , formatException   , mkHermesEnv   , mkHermesEnv_   , withHermesEnv@@ -32,6 +33,7 @@   , parseByteStringIO   , liftIO   , withRunInIO+  , liftObjectDecoder   ) where  import           Control.Applicative (Alternative(..))@@ -190,7 +192,8 @@  instance Alternative FieldsDecoder where   {-# INLINE (<|>) #-}-  (FieldsDecoder a) <|> (FieldsDecoder b) = FieldsDecoder $ \obj -> a obj <|> b obj+  (FieldsDecoder a) <|> (FieldsDecoder b) = FieldsDecoder $ \obj -> Decoder $ \val ->+    runDecoder (a obj) val <|> runDecoder (b obj) val   {-# INLINE empty #-}   empty = FieldsDecoder $ const empty @@ -264,6 +267,15 @@ instance Exception HermesException instance NFData HermesException +formatException :: HermesException -> Text+formatException ex = case ex of+  SIMDException doc     -> formatDoc doc+  InternalException doc -> formatDoc doc+  where+    formatDoc err+      | T.null (path err) = "Document error: " <> errorMsg err+      | otherwise = "Error in " <> path err <> ": " <> errorMsg err+ -- | Record containing all pertinent information for troubleshooting an exception. data DocumentError =   DocumentError@@ -315,7 +327,7 @@   then pure ()   else do     errStr <- liftIO $ F.peekCString =<< getErrorMessageImpl errInt-    throwSIMD $ pre <> " " <> T.pack errStr+    throwSIMD . T.strip $ T.unwords [pre, T.pack errStr] {-# INLINE handleErrorCode #-}  withRunInIO :: ((forall a. DecoderM a -> IO a) -> IO b) -> DecoderM b@@ -335,3 +347,17 @@ local :: (HermesEnv -> HermesEnv) -> DecoderM a -> DecoderM a local f (DecoderM m) = DecoderM . ReaderT $ runReaderT m . f {-# INLINE local #-}++-- | Resets the object being iterated and runs the provided `Decoder` on the+-- object but as a `FieldsDecoder`. This is mostly a convenience for+-- re-entering an object after starting iteration, which isn't very+-- useful or performant.+--+-- > object $ (,)+-- >   <$> atKey "hello" text+-- >   <*> liftObjectDecoder (objectAsMap pure text)))+liftObjectDecoder :: Decoder a -> FieldsDecoder a+liftObjectDecoder decoder = FieldsDecoder $ \(Object obj) -> Decoder $ \_ -> do+  liftIO $ resetObjectImpl (Object obj)+  runDecoder decoder (Value $ F.castPtr obj)+{-# INLINE liftObjectDecoder #-}
src/Data/Hermes/Decoder/Value.hs view
@@ -19,6 +19,7 @@   , object   , objectAsKeyValues   , objectAsMap+  , objectAsMapExcluding   , parseScientific   , scientific   , string@@ -218,6 +219,21 @@ objectAsMap kf vf = withObjectIter $ iterateOverFieldsMap kf vf {-# INLINE objectAsMap #-} +-- | Parse an object into a strict `Map`.+-- Skips any fields in the given list, which adds a slight performance penalty.+objectAsMapExcluding+  :: Ord k+  => [Text]+  -- ^ List of field names to exclude.+  -> (Text -> Decoder k)+  -- ^ Parses a Text key in the Decoder monad. JSON keys are always text.+  -> Decoder v+  -- ^ Decoder for the field value.+  -> Decoder (Map k v)+objectAsMapExcluding fields kf vf =+  withObjectIter $ iterateOverFieldsMapExcluding fields kf vf+{-# INLINE objectAsMapExcluding #-}+ withObjectAsMap   :: Ord k   => (Text -> Decoder k)@@ -427,6 +443,45 @@         else           pure acc {-# INLINE iterateOverFieldsMap #-}++iterateOverFieldsMapExcluding+  :: Ord a+  => [Text]+  -> (Text -> Decoder a)+  -> Decoder b+  -> ObjectIter+  -> DecoderM (Map a b)+iterateOverFieldsMapExcluding fields fk fv iterPtr =+  withRunInIO $ \run ->+    F.alloca $ \lenPtr ->+      F.alloca $ \keyPtr ->+        allocaValue $ \valPtr -> run $ go M.empty keyPtr lenPtr valPtr+  where+    go !acc keyPtr lenPtr valPtr = do+      isOver <- fmap F.toBool . liftIO $ objectIterIsDoneImpl iterPtr+      if not isOver+        then do+          err <- liftIO $ objectIterGetCurrentImpl iterPtr keyPtr lenPtr valPtr+          handleErrorCode "" err+          kLen <- fmap fromIntegral . liftIO $ F.peek lenPtr+          kStr <- liftIO $ F.peek keyPtr+          keyTxt <- parseTextFromCStrLen (kStr, kLen)+          if keyTxt `elem` fields+            then do+              liftIO $ objectIterMoveNextImpl iterPtr+              go acc keyPtr lenPtr valPtr+            else do+              (k, v)+                <-+                  withKey keyTxt $ do+                    k <- runDecoder (fk keyTxt) valPtr+                    v <- runDecoder fv valPtr+                    pure (k, v)+              liftIO $ objectIterMoveNextImpl iterPtr+              go (M.insert k v acc) keyPtr lenPtr valPtr+        else+          pure acc+{-# INLINE iterateOverFieldsMapExcluding #-}  -- | Execute a function on each Field in an ObjectIter and -- accumulate key-value tuples into a list.
tests/test.hs view
@@ -37,7 +37,7 @@ properties = testGroup "Properties" [rtProp, rtPropOptional, rtErrors, rtRecursiveDataType]  units :: TestTree-units = testGroup "Units" [altCases]+units = testGroup "Units" [altCases, objectFields]  rtRecursiveDataType :: TestTree rtRecursiveDataType = testProperty "Round Trip With Recursive Data Type" $@@ -48,21 +48,21 @@  rtProp :: TestTree rtProp = testProperty "Round Trip With Aeson.ToJSON" $-  withTests 1000 . property $ do+  property $ do     p <- forAll genPerson     dp <- roundtrip decodePerson p     p === dp  rtPropOptional :: TestTree rtPropOptional = testProperty "Round Trip With Aeson.ToJSON (Optional Keys)" $-  withTests 1000 . property $ do+  property $ do     p <- forAll genPersonOptional     dp <- roundtrip decodePersonOptional p     p === dp  rtErrors :: TestTree rtErrors = testProperty "Errors Should Not Break Referential Transparency" $-  withTests 1000 . property $ do+  property $ do     p <- forAll        $ Gen.element        [ "{"@@ -120,12 +120,18 @@         "{ \"key1\": 1, \"key2\": 2 }"       @?= Right (Map.fromList [("key1", 1), ("key2", 2)]) -  , testCase "Alternative Keys" $+  , testCase "Alternative Keys (value error)" $       decodeEither         (object $ atKey "key1" (1 <$ bool) <|> atKey "key2" int)         "{ \"key1\": 1, \"key2\": 2 }"       @?= Right 2 +  , testCase "Alternative Keys (key error)" $+      decodeEither+        (object $ atKey "nope" int <|> atKey "key2" int)+        "{ \"key1\": 1, \"key2\": 2 }"+      @?= Right 2+   , testCase "Alternative Objects" $       decodeEither         (object (atKey "key1" (2 <$ bool)) <|> object (atKey "key1" int))@@ -133,6 +139,13 @@       @?= Right 1   ] +objectFields :: TestTree+objectFields = testGroup "Object Fields"+  [ testCase "liftObjectDecoder" $+      decodeEither decodeFriendSlurp "{ \"id\": 1, \"first\": \"Bob\", \"last\": \"Kelso\" }"+    @?= Right (FriendWithMap 1 (Map.fromList [("first", "Bob"), ("last", "Kelso")]))+  ]+ data Person =   Person     { _id           :: Text@@ -336,7 +349,7 @@     1000000000000000000000000  genScientific :: Gen Scientific-genScientific = fmap Sci.fromFloatDigits $ genDouble+genScientific = fmap Sci.fromFloatDigits genDouble  newtype Peano = Peano Word16   deriving (Eq, Show)@@ -355,3 +368,15 @@     case mPeano of       Just (Peano subTree) -> Peano $ 1 + subTree       Nothing -> Peano 0++data FriendWithMap =+  FriendWithMap+    { id   :: Int+    , rest :: Map Text Text+    } deriving (Eq, Show)++decodeFriendSlurp :: Decoder FriendWithMap+decodeFriendSlurp = object $+  FriendWithMap+    <$> atKey "id" int+    <*> liftObjectDecoder (objectAsMapExcluding ["id"] pure text)