packages feed

pa-json 0.2.1.0 → 0.3.0.0

raw patch · 3 files changed

+101/−47 lines, 3 filesdep +pa-field-parserdep −hspec-coredep −hspec-expectationsPVP ok

version bump matches the API change (PVP)

Dependencies added: pa-field-parser

Dependencies removed: hspec-core, hspec-expectations

API changes (from Hackage documentation)

- Json: jsonArray :: [Value] -> Value
- Json: test_keyRenamed :: Spec
+ Json: asArraySet :: (Ord a, Monad m) => ParseT err m a -> ParseT err m (Set a)
+ Json: asErrorTree :: Functor m => ParseT Error m a -> ParseT ErrorTree m a
+ Json: asObjectMap :: Monad m => ParseT err m a -> ParseT err m (Map Text a)
+ Json: asUtcTime :: Monad m => ParseT Error m UTCTime
+ Json: asUtcTimeLenient :: Monad m => ParseT Error m UTCTime
+ Json: countArrayElements :: Monad m => ParseT Error m Natural
+ Json: mkJsonArray :: [Value] -> Value
+ Json: toParseJSON :: Parse Error a -> Value -> Parser a
+ Json: toParseJSONErrorTree :: Parse ErrorTree a -> Value -> Parser a

Files

CHANGELOG.md view
@@ -1,5 +1,11 @@ # Revision history for pa-json +## 0.3.0.0 -- 2023-10-15++- rename `jsonArray` to `mkJsonArray`+- add `toParseJson`, `toParseJSONErrorTree`, `asErrorTree`+- add `asArraySet`, `asObjectMap`, `countArrayElements`, `asUtcTime`, `asUtcTimeLenient`+ ## 0.2.1.0 -- 2023-06-29  - `Json.Enc`: Add `base64` and `base64Bytes`
pa-json.cabal view
@@ -1,7 +1,7 @@ cabal-version:      3.0 -- !!! ATTN: file autogenerated from pa-template.cabal.mustache on toplevel !!! name:               pa-json-version:            0.2.1.0+version:            0.3.0.0 synopsis:           Our JSON parsers/encoders description:        The interface of `aeson` is unfortunately extremely … suboptimal. Here’s some wrappers trying to improve the situation, which we use internally. license:            BSD-3-Clause@@ -75,6 +75,7 @@       base <5,       pa-prelude,       pa-label,+      pa-field-parser >= 0.3.0.0,       pa-error-tree,       aeson,       aeson-pretty,@@ -86,6 +87,4 @@       time,       text,       vector,-      hspec-core,-      hspec-expectations, 
src/Json.hs view
@@ -4,18 +4,48 @@ module Json where  import Data.Aeson (FromJSON (parseJSON), ToJSON (toEncoding, toJSON), Value (..), withObject)+import Data.Aeson qualified as Json import Data.Aeson.BetterErrors qualified as Json-import Data.Aeson.KeyMap qualified as KeyMap+import Data.Aeson.Types qualified import Data.Error.Tree+import Data.Map.Strict qualified as Map import Data.Maybe (catMaybes)+import Data.Set (Set)+import Data.Set qualified as Set import Data.Text qualified as Text+import Data.Time (UTCTime) import Data.Vector qualified as Vector+import FieldParser (FieldParser)+import FieldParser qualified as Field import Label import PossehlAnalyticsPrelude-import Test.Hspec.Core.Spec (describe, it)-import Test.Hspec.Core.Spec qualified as Hspec-import Test.Hspec.Expectations (shouldBe) +-- | Use a "Data.Aeson.BetterErrors" parser to implement 'FromJSON'’s 'parseJSON' method.+--+-- @+-- instance FromJSON Foo where+--   parseJSON = Json.toParseJSON parseFoo+-- @+toParseJSON ::+  -- | the error type is 'Error', if you need 'ErrorTree' use 'toParseJSONErrorTree'+  Json.Parse Error a ->+  Value ->+  Data.Aeson.Types.Parser a+toParseJSON = Json.toAesonParser prettyError++-- | Use a "Data.Aeson.BetterErrors" parser to implement 'FromJSON'’s 'parseJSON' method.+--+-- @+-- instance FromJSON Foo where+--   parseJSON = Json.toParseJSON parseFoo+-- @+toParseJSONErrorTree ::+  -- | the error type is 'ErrorTree', if you need 'Error' use 'toParseJSON'+  Json.Parse ErrorTree a ->+  Value ->+  Data.Aeson.Types.Parser a+toParseJSONErrorTree = Json.toAesonParser prettyErrorTree+ -- | Convert a 'Json.ParseError' to a corresponding 'ErrorTree' -- -- TODO: build a different version of 'Json.displayError' so that we can nest 'ErrorTree' as well@@ -38,6 +68,54 @@     & singleError     & nestedError contextMsg +-- | Lift the parser error to an error tree+asErrorTree :: (Functor m) => Json.ParseT Error m a -> Json.ParseT ErrorTree m a+asErrorTree = Json.mapError singleError++-- | Parse the json array into a 'Set'.+asArraySet ::+  (Ord a, Monad m) =>+  Json.ParseT err m a ->+  Json.ParseT err m (Set a)+asArraySet inner = Set.fromList <$> Json.eachInArray inner++-- | Parse the json object into a 'Map'.+asObjectMap ::+  (Monad m) =>+  Json.ParseT err m a ->+  Json.ParseT err m (Map Text a)+asObjectMap inner = Map.fromList <$> Json.eachInObject inner++-- | Parse as json array and count the number of elements in the array.+countArrayElements :: (Monad m) => Json.ParseT Error m Natural+countArrayElements = Field.toJsonParser ((jsonArray <&> Vector.length) >>> Field.integralToNatural)+  where+    -- I don’t want to add this to the FieldParser module, cause users should not be dealing with arrays manually.+    jsonArray :: FieldParser Json.Value (Vector Json.Value)+    jsonArray = Field.FieldParser $ \case+      Json.Array vec -> Right vec+      _ -> Left "Not a json array"++-- | Json string containing a UTC timestamp,+-- @yyyy-mm-ddThh:mm:ss[.sss]Z@ (ISO 8601:2004(E) sec. 4.3.2 extended format)+asUtcTime :: (Monad m) => Json.ParseT Error m UTCTime+asUtcTime = Field.toJsonParser (Field.jsonString >>> Field.utcTime)++-- | Json string containing a UTC timestamp.+-- | Accepts multiple timezone formats.+-- Do not use this if you can force the input to use the `Z` UTC notation (e.g. in a CSV), use 'utcTime' instead.+--+-- Accepts+--+-- * UTC timestamps: @yyyy-mm-ddThh:mm:ss[.sss]Z@+-- * timestamps with time zone: @yyyy-mm-ddThh:mm:ss[.sss]±hh:mm@+--+-- ( both ISO 8601:2004(E) sec. 4.3.2 extended format)+--+-- The time zone of the second kind of timestamp is taken into account, but normalized to UTC (it’s not preserved what the original time zone was)+asUtcTimeLenient :: (Monad m) => Json.ParseT Error m UTCTime+asUtcTimeLenient = Field.toJsonParser (Field.jsonString >>> Field.utcTimeLenient)+ -- | Parse a key from the object, à la 'Json.key', return a labelled value. -- -- We don’t provide a version that infers the json object key,@@ -50,7 +128,7 @@ -- @@ keyLabel ::   forall label err m a.-  Monad m =>+  (Monad m) =>   Text ->   Json.ParseT err m a ->   Json.ParseT err m (Label label a)@@ -67,7 +145,7 @@ -- @@ keyLabel' ::   forall label err m a.-  Monad m =>+  (Monad m) =>   Proxy label ->   Text ->   Json.ParseT err m a ->@@ -86,7 +164,7 @@ -- @@ keyLabelMay ::   forall label err m a.-  Monad m =>+  (Monad m) =>   Text ->   Json.ParseT err m a ->   Json.ParseT err m (Label label (Maybe a))@@ -103,20 +181,22 @@ -- @@ keyLabelMay' ::   forall label err m a.-  Monad m =>+  (Monad m) =>   Proxy label ->   Text ->   Json.ParseT err m a ->   Json.ParseT err m (Label label (Maybe a)) keyLabelMay' Proxy key parser = label @label <$> Json.keyMay key parser +-- NOTE: keyRenamed Test in "Json.JsonTest", due to import cycles.+ -- | Like 'Json.key', but allows a list of keys that are tried in order. -- -- This is intended for renaming keys in an object. -- The first key is the most up-to-date version of a key, the others are for backward-compatibility. -- -- If a key (new or old) exists, the inner parser will always be executed for that key.-keyRenamed :: Monad m => NonEmpty Text -> Json.ParseT err m a -> Json.ParseT err m a+keyRenamed :: (Monad m) => NonEmpty Text -> Json.ParseT err m a -> Json.ParseT err m a keyRenamed (newKey :| oldKeys) inner =   keyRenamedTryOldKeys oldKeys inner >>= \case     Nothing -> Json.key newKey inner@@ -128,14 +208,14 @@ -- The first key is the most up-to-date version of a key, the others are for backward-compatibility. -- -- If a key (new or old) exists, the inner parser will always be executed for that key.-keyRenamedMay :: Monad m => NonEmpty Text -> Json.ParseT err m a -> Json.ParseT err m (Maybe a)+keyRenamedMay :: (Monad m) => NonEmpty Text -> Json.ParseT err m a -> Json.ParseT err m (Maybe a) keyRenamedMay (newKey :| oldKeys) inner =   keyRenamedTryOldKeys oldKeys inner >>= \case     Nothing -> Json.keyMay newKey inner     Just parse -> Just <$> parse  -- | Helper function for 'keyRenamed' and 'keyRenamedMay' that returns the parser for the first old key that exists, if any.-keyRenamedTryOldKeys :: Monad m => [Text] -> Json.ParseT err m a -> Json.ParseT err m (Maybe (Json.ParseT err m a))+keyRenamedTryOldKeys :: (Monad m) => [Text] -> Json.ParseT err m a -> Json.ParseT err m (Maybe (Json.ParseT err m a)) keyRenamedTryOldKeys oldKeys inner = do   oldKeys & traverse tryOld <&> catMaybes <&> nonEmpty <&> \case     Nothing -> Nothing@@ -146,37 +226,6 @@         Just () -> Just $ Json.key key inner         Nothing -> Nothing -test_keyRenamed :: Hspec.Spec-test_keyRenamed = do-  describe "keyRenamed" $ do-    let parser = keyRenamed ("new" :| ["old"]) Json.asText-    let p = Json.parseValue @() parser-    it "accepts the new key and the old key" $ do-      p (Object (KeyMap.singleton "new" (String "text")))-        `shouldBe` (Right "text")-      p (Object (KeyMap.singleton "old" (String "text")))-        `shouldBe` (Right "text")-    it "fails with the old key in the error if the inner parser is wrong" $ do-      p (Object (KeyMap.singleton "old" Null))-        `shouldBe` (Left (Json.BadSchema [Json.ObjectKey "old"] (Json.WrongType Json.TyString Null)))-    it "fails with the new key in the error if the inner parser is wrong" $ do-      p (Object (KeyMap.singleton "new" Null))-        `shouldBe` (Left (Json.BadSchema [Json.ObjectKey "new"] (Json.WrongType Json.TyString Null)))-    it "fails if the key is missing" $ do-      p (Object KeyMap.empty)-        `shouldBe` (Left (Json.BadSchema [] (Json.KeyMissing "new")))-  describe "keyRenamedMay" $ do-    let parser = keyRenamedMay ("new" :| ["old"]) Json.asText-    let p = Json.parseValue @() parser-    it "accepts the new key and the old key" $ do-      p (Object (KeyMap.singleton "new" (String "text")))-        `shouldBe` (Right (Just "text"))-      p (Object (KeyMap.singleton "old" (String "text")))-        `shouldBe` (Right (Just "text"))-    it "allows the old and new key to be missing" $ do-      p (Object KeyMap.empty)-        `shouldBe` (Right Nothing)- -- | A simple type isomorphic to `()` that that transforms to an empty json object and parses data EmptyObject = EmptyObject   deriving stock (Show, Eq)@@ -190,5 +239,5 @@   toEncoding EmptyObject = toEncoding $ Object mempty  -- | Create a json array from a list of json values.-jsonArray :: [Value] -> Value-jsonArray xs = xs & Vector.fromList & Array+mkJsonArray :: [Value] -> Value+mkJsonArray xs = xs & Vector.fromList & Array