packages feed

aeson-commit 1.3 → 1.4

raw patch · 4 files changed

+131/−110 lines, 4 filesdep +hspecdep −tastydep −tasty-hspecdep ~aesonPVP ok

version bump matches the API change (PVP)

Dependencies added: hspec

Dependencies removed: tasty, tasty-hspec

Dependency ranges changed: aeson

API changes (from Hackage documentation)

- Data.Aeson.Commit: (.:>) :: FromJSON a => Object -> Text -> (a -> Parser b) -> Commit b
+ Data.Aeson.Commit: (.:>) :: FromJSON a => Object -> Key -> (a -> Parser b) -> Commit b

Files

CHANGELOG.md view
@@ -1,5 +1,12 @@ # Changelog +## [1.4]+### [Added]+- Nix support+- Conditionally qualifications for `aeson-2.0`+### [Changed]+- Reword documentation+ ## [1.3] ### [Changed] - Relax version bounds in preparation for uploading to stackage
aeson-commit.cabal view
@@ -1,6 +1,6 @@ cabal-version:      >=1.10 name:               aeson-commit-version:            1.3+version:            1.4 license:            BSD3 copyright:          2020 Cross Compass Ltd. maintainer:         Jonas Carpay <jonascarpay@gmail.com>@@ -17,8 +17,8 @@ category:           Text, Web, JSON build-type:         Simple extra-source-files:-  README.md   CHANGELOG.md+  README.md  source-repository head   type:     git@@ -30,7 +30,7 @@   default-language: Haskell2010   ghc-options:      -Wall -Wno-name-shadowing   build-depends:-      aeson  >=1.4  && <2+      aeson  >=1.4  && <2.1     , base   >=4.10 && <5     , mtl    >=2.2  && <3     , text   >=1.2  && <2@@ -42,10 +42,9 @@   default-language: Haskell2010   ghc-options:      -Wall -Wno-name-shadowing   build-depends:-      aeson         >=1.4     && <2+      aeson         >=1.4  && <2.1     , aeson-commit-    , aeson-qq      >=0.8     && <1-    , base          >=4.10    && <5-    , tasty         >=1.2     && <2-    , tasty-hspec   >=1.1.5.1 && <2-    , text          >=1.2     && <2+    , aeson-qq      >=0.8  && <1+    , base          >=4.10 && <5+    , hspec+    , text          >=1.2  && <2
src/Data/Aeson/Commit.hs view
@@ -1,55 +1,62 @@+{-# LANGUAGE CPP #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} -{-|-   Commitment mechanism for aeson 'Parser'.-   Commitment means that if some initial parsing succeeds, subsequent failures are unrecoverable.-   In this example, not having the key @nested@ is a normal, recoverable failure, and parsing will continue looking for another key.-   However, if @nested@ is present but malformed, the entire parser fails.--   > parse o = (o .:> "nested") (withObject "nestedObj" (.: "value"))-   >         <|> tryParser (o .: "value")--   > { value: "foo", otherField: "bar" }-   > -> Right "foo"-   >-   > { value: "foo", nested: { value: "bar" } }-   > -> Right "bar"-   >-   > { value: "foo", nested: { bar: 9 } }-   > -> Left "Error in $.nested: key \"value\" not found"-   >-   > { value: "foo", nested: 9 }-   > -> Left "Error in $.nested: parsing nestedObj failed, expected Object, but encountered Number"-   >-   > {}-   > -> Left-   >   "Error in $: No match,-   >    - key \"value\" not found"-   >    - key \"nested\" not found"---}+-- |+--   Commit mechanism for aeson's 'Parser'.+--   To commit means that if some initial parsing succeeds, subsequent failures are unrecoverable.+--+--   In the following example, we use '.:>' to look for a key @.nested.value@, and if that does not exist, @.value@.+--+--   > parse o = (o .:> "nested") (withObject "nestedObj" (.: "value"))+--   >         <|> tryParser (o .: "value")+--+--   Not having the key @nested@ is a normal, recoverable failure, and parsing will continue looking for @value@.+--   However, if @nested@ is present but malformed, parsing fails.+--+--   > { value: "foo", otherField: "bar" }+--   > -> Right "foo"+--   >+--   > { value: "foo", nested: { value: "bar" } }+--   > -> Right "bar"+--   >+--   > { value: "foo", nested: { bar: 9 } }+--   > -> Left "Error in $.nested: key \"value\" not found"+--   >+--   > { value: "foo", nested: 9 }+--   > -> Left "Error in $.nested: parsing nestedObj failed, expected Object, but encountered Number"+--   >+--   > {}+--   > -> Left+--   >   "Error in $: No match,+--   >    - key \"value\" not found"+--   >    - key \"nested\" not found" module Data.Aeson.Commit-  ( commit-  , runCommit-  , (.:>)-  , tryParser-  , liftParser-  , Commit (..)-  ) where+  ( commit,+    runCommit,+    (.:>),+    tryParser,+    liftParser,+    Commit (..),+  )+where -import           Control.Applicative  (Alternative (..))-import           Control.Monad.Except-import           Data.Aeson.Types-import           Data.Text            (Text)-import           Data.Void            (Void, absurd)+import Control.Applicative (Alternative (..))+import Control.Monad.Except+import Data.Aeson.Types+import Data.Void (Void, absurd)+#if MIN_VERSION_aeson(2,0,0)+import qualified Data.Aeson.Key as Key+#else+import Data.Text (Text)+#endif --- | A 'Parser' that has _two_ failure modes; recoverable and non-recoverable.---   The default, recoverable failure is the equivalent to aeson's default 'Parser' behavior.---   The non-recoverable failure mode is used to commit to a branch; to commit means that every subsequent failure is non-recoverable.+-- | A 'Commit' is a 'Parser' that has two failure modes; recoverable and non-recoverable. -----   You turn run a 'Commit' and capture its result in a 'Parser' using 'runCommit'.---   As an additional benefit, it will contain error info for all attempted parsing branches.+--   > tryParser empty <|> p = p+--   > liftParser empty <|> p = empty --+--   'Commit' is typically constructed using 'commit', and consumed using 'runCommit', which captures its result in a 'Parser'.+-- --   The implementation works by wrapping 'Parser' in an 'ExceptT'. --   The derived 'Alternative' instance will then only recover from failures in the 'ExceptT'. --   This means that as soon as we successfully construct a 'Right' value, the 'Alternative' considers the 'Commit' a success, even though the underlying parser might have failed.@@ -64,35 +71,41 @@ commit pre post = Commit $ do   a <- ExceptT $ captureError pre   lift $ post a-    where-      captureError :: Parser b -> Parser (Either [Parser Void] b)-      captureError p = Right <$> p <|> pure (Left [fmap (const undefined) p])+  where+    -- TODO maybe there's a better way to prove something is an error+    captureError :: Parser b -> Parser (Either [Parser Void] b)+    captureError p = Right <$> p <|> pure (Left [fmap (const undefined) p])  -- | Run a 'Commit', capturing its result in a 'Parser'. runCommit :: Commit a -> Parser a runCommit (Commit f) = runExceptT f >>= either handleErrors pure   where-  handleErrors :: [Parser Void] -> Parser a-  handleErrors []     = fail "No parsers tried"-  handleErrors (p:ps) = fmap absurd (go (p:ps) [] [])-    where-    go [] path errors = parserThrowError path ("No match,\n" <> unlines (fmap ("- " <>) errors))-    go (y:ys) _ msgs = parserCatchError y $ \path msg ->-      go ys path (msg:msgs)+    handleErrors :: [Parser Void] -> Parser a+    handleErrors [] = fail "No parsers tried"+    handleErrors (p : ps) = fmap absurd (go (p : ps) [] [])+      where         -- TODO: how do we handle the multiple JSONPaths?         -- Right now the rightmost failure's path is used when presenting         -- the error message. Ideally one path per error would be preferable but         -- `aeson` doesn't support such a thing. When errors are reported in `aeson`         -- a single JSONPath defines how the error message is presented.+        go [] path errors = parserThrowError path ("No match,\n" <> unlines (fmap ("- " <>) errors))+        go (y : ys) _ msgs = parserCatchError y $ \path msg ->+          go ys path (msg : msgs)  -- | Convenience wrapper around 'commit' for when the commit is checking whether a key is present in some object. --   If it is, it will commit and append the key to the JSONPath of the inner context through '<?>', which will give nicer error messages.++#if MIN_VERSION_aeson(2,0,0)+(.:>)  :: FromJSON a => Object -> Key.Key -> (a -> Parser b) -> Commit b+#else (.:>)  :: FromJSON a => Object -> Text -> (a -> Parser b) -> Commit b+#endif (o .:> k) cont = commit (o .: k) (\v -> cont v <?> Key k)  -- | Turn a 'Parser' into a 'Commit' --   Unlike 'liftParser', the parser's failure is recoverable.---   +-- -- > tryParser empty <|> p = p -- > tryParser p = commit p pure tryParser :: Parser a -> Commit a@@ -100,7 +113,7 @@  -- | Turn a 'Parser' into a 'Commit'. --   Unlike 'tryParser', the parser's failure is _not_ recoverable, i.e. the parse is always committed.---   +-- -- > liftParser empty <|> p = empty -- > liftParser p = commit (pure ()) (const p) liftParser :: Parser a -> Commit a
test/tasty/Main.hs view
@@ -1,57 +1,59 @@-{-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}  module Main (main) where -import           Data.Aeson.QQ-import           Data.Aeson.Types-import           Data.Aeson.Commit-import           Control.Applicative-import           Data.Text (Text)-import           Test.Tasty-import           Test.Tasty.Hspec+import Control.Applicative+import Data.Aeson.Commit+import Data.Aeson.QQ+import Data.Aeson.Types+import Data.Text (Text)+import Test.Hspec  tests :: Spec-tests = testParserWithCases pNested-  [ ( "fails"-    , [aesonQQ| {} |]-    , Left $ unlines-      [ "Error in $: No match,"-      , "- key \"value\" not found"-      , "- key \"nested\" not found"-      ]-    )-  , ( "succeeds unnested"-    , [aesonQQ| { value: "top" } |]-    , Right "top"-    )-  , ( "succeeds and prefers nested"-    , [aesonQQ| { value: "top" , nested: { value: "nest" } } |]-    , Right "nest"-    )-  , ( "fails on malformed nested"-    , [aesonQQ| { value: "top", nested: { foo: 9 } } |]-    , Left "Error in $.nested: key \"value\" not found"-    )-  , ( "fails on nested type mismatch"-    , [aesonQQ| { value: "top", nested: 9 } |]-    , Left "Error in $.nested: parsing nestedObj failed, expected Object, but encountered Number"-    )-  ]+tests =+  testParserWithCases+    pNested+    [ ( "fails",+        [aesonQQ| {} |],+        Left $+          unlines+            [ "Error in $: No match,",+              "- key \"value\" not found",+              "- key \"nested\" not found"+            ]+      ),+      ( "succeeds unnested",+        [aesonQQ| { value: "top" } |],+        Right "top"+      ),+      ( "succeeds and prefers nested",+        [aesonQQ| { value: "top" , nested: { value: "nest" } } |],+        Right "nest"+      ),+      ( "fails on malformed nested",+        [aesonQQ| { value: "top", nested: { foo: 9 } } |],+        Left "Error in $.nested: key \"value\" not found"+      ),+      ( "fails on nested type mismatch",+        [aesonQQ| { value: "top", nested: 9 } |],+        Left "Error in $.nested: parsing nestedObj failed, expected Object, but encountered Number"+      )+    ]   where     pNested :: Value -> Parser Text     pNested = withObject "topLevel" $ \o ->-      runCommit-        $ (o .:> "nested") (withObject "nestedObj" (.: "value"))-        <|> tryParser (o .: "value")+      runCommit $+        (o .:> "nested") (withObject "nestedObj" (.: "value"))+          <|> tryParser (o .: "value") -testParserWithCases-  :: (Eq a, Show a)-  => (v -> Parser a)-  -> [(String, v, Either String a)]-  -> Spec+testParserWithCases ::+  (Eq a, Show a) =>+  (v -> Parser a) ->+  [(String, v, Either String a)] ->+  Spec testParserWithCases p =-  mapM_ ( \(name, v, result) -> it name (parseEither p v `shouldBe` result))+  mapM_ (\(name, v, result) -> it name (parseEither p v `shouldBe` result))  main :: IO ()-main = testSpecs tests >>= defaultMain . testGroup "aeson-commit"+main = hspec tests