Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[acc-errors] Accumulate errors for Seq and Vector parsers #600

Open
wants to merge 4 commits into
base: acc-errors
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions Data/Aeson/AccParser.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
module Data.Aeson.AccParser
(
AccParser (AccParser, getParser)
, accSequence
, accTraverse
, (<*>+)
) where

import Prelude ()
import Prelude.Compat

import Data.Aeson.Types.Internal (Parser (..), runParser)
import Data.List.NonEmpty (NonEmpty ((:|)))
import qualified Data.List.NonEmpty as NonEmpty

newtype AccParser a = AccParser { getParser :: Parser a }
deriving Functor

instance Applicative AccParser where
pure = AccParser . pure
f <*> a = AccParser (getParser f <*>+ getParser a)

-- | A variant of 'Control.Applicative.liftA2' that lazily accumulates errors
-- from both subparsers.
liftP2 :: (a -> b -> c) -> Parser a -> Parser b -> Parser c
liftP2 f pa pb = Parser $ \path kf ks ->
runParser pa path
(\(e :| es) -> kf (e :| es ++ runParser pb path NonEmpty.toList (const [])))
(\a -> runParser pb path kf (\b -> ks (f a b)))
{-# INLINE liftP2 #-}

accSequence :: Traversable t => t (Parser a) -> Parser (t a)
accSequence = accTraverse id

accTraverse :: Traversable t => (a -> Parser b) -> t a -> Parser (t b)
accTraverse f s = getParser $ traverse' (AccParser . f) s

-- Making sure we are using Applicative AccParser
traverse' :: Traversable t => (a -> AccParser b) -> t a -> AccParser (t b)
traverse' = traverse

infixl 4 <*>+

-- | A variant of ('<*>') that lazily accumulates errors from both subparsers.
(<*>+) :: Parser (a -> b) -> Parser a -> Parser b
(<*>+) = liftP2 id
{-# INLINE (<*>+) #-}
2 changes: 0 additions & 2 deletions Data/Aeson/Types.hs
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,6 @@ module Data.Aeson.Types
, parseMaybe
, ToJSON(..)
, KeyValue(..)
, liftP2
, (<*>+)
, modifyFailure
, parserThrowError
, parserCatchError
Expand Down
15 changes: 8 additions & 7 deletions Data/Aeson/Types/FromJSON.hs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ import Data.Aeson.Internal.Functions (mapKey)
import Data.Aeson.Parser.Internal (eitherDecodeWith, jsonEOF)
import Data.Aeson.Types.Generic
import Data.Aeson.Types.Internal
import Data.Aeson.AccParser
import Data.Attoparsec.Number (Number(..))
import Data.Bits (unsafeShiftR)
import Data.Fixed (Fixed, HasResolution)
Expand Down Expand Up @@ -164,8 +165,8 @@ parseIndexedJSONPair keyParser valParser idx value = p value <?> Index idx
p = withArray "(k,v)" $ \ab ->
let n = V.length ab
in if n == 2
then (,) <$> parseJSONElemAtIndex keyParser 0 ab
<*> parseJSONElemAtIndex valParser 1 ab
then (,) <$> parseJSONElemAtIndex keyParser 0 ab
<*>+ parseJSONElemAtIndex valParser 1 ab
else fail $ "cannot unpack array of length " ++
show n ++ " into a pair"
{-# INLINE parseIndexedJSONPair #-}
Expand Down Expand Up @@ -606,7 +607,7 @@ parseJSON2 = liftParseJSON2 parseJSON parseJSONList parseJSON parseJSONList

-- | Helper function to use with 'liftParseJSON'. See 'Data.Aeson.ToJSON.listEncoding'.
listParser :: (Value -> Parser a) -> Value -> Parser [a]
listParser f (Array xs) = fmap V.toList (V.mapM f xs)
listParser f (Array xs) = getParser $ V.toList <$> traverse (AccParser . f) xs
listParser _ v = typeMismatch "[a]" v
{-# INLINE listParser #-}

Expand Down Expand Up @@ -1413,7 +1414,7 @@ parseVersionText = go . readP_to_S parseVersion . unpack

instance FromJSON1 NonEmpty where
liftParseJSON p _ = withArray "NonEmpty a" $
(>>= ne) . Tr.sequence . zipWith (parseIndexedJSON p) [0..] . V.toList
(>>= ne) . accSequence . zipWith (parseIndexedJSON p) [0..] . V.toList
where
ne [] = fail "Expected a NonEmpty but got an empty list"
ne (x:xs) = pure (x :| xs)
Expand All @@ -1438,7 +1439,7 @@ instance FromJSON Scientific where
instance FromJSON1 DList.DList where
liftParseJSON p _ = withArray "DList a" $
fmap DList.fromList .
Tr.sequence . zipWith (parseIndexedJSON p) [0..] . V.toList
accSequence . zipWith (parseIndexedJSON p) [0..] . V.toList
{-# INLINE liftParseJSON #-}

instance (FromJSON a) => FromJSON (DList.DList a) where
Expand Down Expand Up @@ -1529,7 +1530,7 @@ instance (FromJSON1 f, FromJSON1 g, FromJSON a) => FromJSON (Sum f g a) where
instance FromJSON1 Seq.Seq where
liftParseJSON p _ = withArray "Seq a" $
fmap Seq.fromList .
Tr.sequence . zipWith (parseIndexedJSON p) [0..] . V.toList
accSequence . zipWith (parseIndexedJSON p) [0..] . V.toList
{-# INLINE liftParseJSON #-}

instance (FromJSON a) => FromJSON (Seq.Seq a) where
Expand Down Expand Up @@ -1607,7 +1608,7 @@ instance FromJSONKey UUID.UUID where

instance FromJSON1 Vector where
liftParseJSON p _ = withArray "Vector a" $
V.mapM (uncurry $ parseIndexedJSON p) . V.indexed
accTraverse (uncurry $ parseIndexedJSON p) . V.indexed
{-# INLINE liftParseJSON #-}

instance (FromJSON a) => FromJSON (Vector a) where
Expand Down
21 changes: 2 additions & 19 deletions Data/Aeson/Types/Internal.hs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ module Data.Aeson.Types.Internal
, emptyObject

-- * Type conversion
, Parser
, Parser (Parser)
, runParser
, Result(..)
, IResult(..)
, JSONPathElement(..)
Expand All @@ -43,8 +44,6 @@ module Data.Aeson.Types.Internal
, parse
, parseEither
, parseMaybe
, liftP2
, (<*>+)
, modifyFailure
, parserThrowError
, parserCatchError
Expand Down Expand Up @@ -340,22 +339,6 @@ apP d e = do
return (b a)
{-# INLINE apP #-}

-- | A variant of 'Control.Applicative.liftA2' that lazily accumulates errors
-- from both subparsers.
liftP2 :: (a -> b -> c) -> Parser a -> Parser b -> Parser c
liftP2 f pa pb = Parser $ \path kf ks ->
runParser pa path
(\(e :| es) -> kf (e :| es ++ runParser pb path NonEmpty.toList (const [])))
(\a -> runParser pb path kf (\b -> ks (f a b)))
{-# INLINE liftP2 #-}

infixl 4 <*>+

-- | A variant of ('<*>') that lazily accumulates errors from both subparsers.
(<*>+) :: Parser (a -> b) -> Parser a -> Parser b
(<*>+) = liftP2 id
{-# INLINE (<*>+) #-}

-- | A JSON \"object\" (key\/value map).
type Object = HashMap Text Value

Expand Down
2 changes: 2 additions & 0 deletions aeson.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ library

exposed-modules:
Data.Aeson
Data.Aeson.AccParser
Data.Aeson.Encoding
Data.Aeson.Parser
Data.Aeson.Text
Expand Down Expand Up @@ -196,6 +197,7 @@ test-suite tests
SerializationFormatSpec
Types
UnitTests
UnitTests.AccErrors
UnitTests.NullaryConstructors

build-depends:
Expand Down
9 changes: 8 additions & 1 deletion tests/Tests.hs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,15 @@ import Test.Framework (defaultMain)
import qualified DataFamilies.Properties as DF
import qualified Properties
import qualified UnitTests
import qualified UnitTests.AccErrors as AccErrors

main :: IO ()
main = do
ioTests <- UnitTests.ioTests
defaultMain (DF.tests : Properties.tests : UnitTests.tests : ioTests)
defaultMain
( AccErrors.tests
: DF.tests
: Properties.tests
: UnitTests.tests
: ioTests
)
58 changes: 58 additions & 0 deletions tests/UnitTests/AccErrors.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
{-# LANGUAGE NoMonomorphismRestriction #-}
{-# LANGUAGE OverloadedStrings #-}
module UnitTests.AccErrors (tests) where

import Prelude ()
import Prelude.Compat hiding (seq)

import Data.Aeson
import Data.DList (DList)
import Data.Aeson.Parser.Internal
import Data.Aeson.Types ()
import Data.Aeson.Internal
import Data.List.NonEmpty (NonEmpty)
import Data.Semigroup
import Data.Vector (Vector)
import Test.Framework
import Test.Framework.Providers.HUnit
import Test.HUnit hiding (Test)
import qualified Data.ByteString.Lazy as L
import qualified Data.List.NonEmpty as NL
import qualified Data.Sequence as Seq

tests :: Test
tests = testGroup "Error accumulation" [
testCase "Seq" seq
, testCase "Vector" vector
, testCase "NonEmpty" nonEmpty
, testCase "DList" dlist
]

decoder :: FromJSON a
=> L.ByteString
-> Either (NonEmpty (JSONPath, String)) a
decoder = verboseDecodeWith jsonEOF ifromJSON

seq :: Assertion
seq = do
let res = decoder "[true, null]" :: Either (NonEmpty (JSONPath, String)) (Seq.Seq Int)
let message i s = ([Index i], "expected Int, encountered " <> s)
res @=? Left (NL.fromList [message 0 "Boolean", message 1 "Null"])

vector :: Assertion
vector = do
let res = decoder "[true, null]" :: Either (NonEmpty (JSONPath, String)) (Vector Int)
let message i s = ([Index i], "expected Int, encountered " <> s)
res @=? Left (NL.fromList [message 0 "Boolean", message 1 "Null"])

nonEmpty :: Assertion
nonEmpty = do
let res = decoder "[true, null]" :: Either (NonEmpty (JSONPath, String)) (NL.NonEmpty Int)
let message i s = ([Index i], "expected Int, encountered " <> s)
res @=? Left (NL.fromList [message 0 "Boolean", message 1 "Null"])

dlist :: Assertion
dlist = do
let res = decoder "[true, null]" :: Either (NonEmpty (JSONPath, String)) (DList Int)
let message i s = ([Index i], "expected Int, encountered " <> s)
res @=? Left (NL.fromList [message 0 "Boolean", message 1 "Null"])