git.iko.soy/rowtype-yoga/purescript-yoga-json
on main help sign in
Improve error handling
commitb6b2a538882bd8150f8e262bb8dd75ea78403956 authorMark Eibes <mark.eibes@gmail.com> dateThu 21 Jul 2022, 19:13 (4 years ago) parent5d39e7a
5 files changed+137−23
src/Yoga/JSON.purs+44−12 src/Yoga/JSON/Error.purs+33 test/BasicsSpec.purs+2−2 test/ErrorsSpec.purs+53−4 test/Util.purs+5−5
src/Yoga/JSON.purs+44−12
@@ -41,13 +41,14 @@ import Data.Array.NonEmpty (NonEmptyArray, fromArray, toArray)
4141import Data.Bifunctor (lmap)
4242import Data.BigInt (BigInt)
4343import Data.BigInt as BigInt
44−import Data.DateTime (DateTime(..))
45−import Data.DateTime as DateTime
46−import Data.Either (Either(..), either, hush, note)
44+import Data.DateTime (DateTime)
45+import Data.Either (Either(..), hush, note)
46+import Data.Foldable (class Foldable, foldl)
4747import Data.FoldableWithIndex (foldrWithIndex)
48+import Data.FunctorWithIndex (mapWithIndex)
4849import Data.Identity (Identity(..))
4950import Data.Int as Int
50−import Data.JSDate (JSDate, toISOString)
51+import Data.JSDate (JSDate)
5152import Data.JSDate as JSDate
5253import Data.List.NonEmpty (NonEmptyList, singleton)
5354import Data.Map (Map)
@@ -57,8 +58,7 @@ import Data.Newtype (class Newtype)
5758import Data.Nullable (Nullable, toMaybe, toNullable)
5859import Data.Number as Number
5960import Data.Symbol (class IsSymbol, reflectSymbol)
60−import Data.Traversable (sequence, traverse)
61−import Data.TraversableWithIndex (traverseWithIndex)
61+import Data.Traversable (traverse)
6262import Data.Tuple (Tuple(..))
6363import Data.Variant (Variant, inj, on)
6464import Effect.Exception (message, try)
@@ -216,7 +216,7 @@ instance ReadForeign Boolean where
216216 readImpl = readBoolean
217217
218218instance ReadForeign a ⇒ ReadForeign (Array a) where
219− readImpl = traverseWithIndex readAtIdx <=< readArray
219+ readImpl = sequenceCombining <<< mapWithIndex readAtIdx <=< readArray
220220
221221instance ReadForeign a ⇒ ReadForeign (Maybe a) where
222222 readImpl = readNullOrUndefined readImpl
@@ -242,8 +242,21 @@ instance (ReadForeign a, ReadForeign b) ⇒ ReadForeign (Either a b) where
242242 _ -> except $ Left (pure $ ForeignError $ "Invalid Either tag " <> tpe)
243243
244244instance ReadForeign a ⇒ ReadForeign (Object.Object a) where
245− readImpl = sequence <<< Object.mapWithKey readProp <=< readObject'
245+ readImpl = gatherErrors <<< Object.mapWithKey readProp <=< readObject'
246246 where
247+ gatherErrors :: Object (F a) -> F (Object a)
248+ gatherErrors = Object.toUnfoldable
249+ >>> Array.foldl fn (Right (Object.empty))
250+ >>> except
251+ where
252+ fn :: _ -> _ -> (Either MultipleErrors (Object a))
253+ fn acc (Tuple k v) = do
254+ case acc, runExcept v of
255+ Left errs, Left errsNew -> Left (errs <> errsNew)
256+ Left errs, Right _ -> Left errs
257+ Right obj, Right value -> Right (Object.insert k value obj)
258+ Right _, Left errs -> Left errs
259+
247260 readProp key value = except $ lmap (map (ErrorAtProperty key))
248261 (readImpl value # runExcept)
249262
@@ -321,16 +334,20 @@ instance
321334 , Row.Cons name ty from' to
322335 ) ⇒
323336 ReadForeignFields (Cons name ty tail) from to where
324− getFields _ obj = do
325− compose <$> first <*> rest
337+ getFields _ obj = except
338+ case runExcept first, runExcept rest of
339+ Right f, Right r -> Right (f <<< r)
340+ Left e1, Left e2 -> Left (e1 <> e2)
341+ Right _, Left es -> Left es
342+ Left es, Right _ -> Left es
326343 where
327− value = withExcept' (readImpl =<< readProp name obj)
344+ value = enrichErrorWithPropName (readImpl =<< readProp name obj)
328345 first = Builder.insert nameP <$> value
329346 rest = getFields tailP obj
330347 nameP = Proxy ∷ Proxy name
331348 tailP = Proxy ∷ Proxy tail
332349 name = reflectSymbol nameP
333− withExcept' = (withExcept <<< map) (ErrorAtProperty name)
350+ enrichErrorWithPropName = (withExcept <<< map) (ErrorAtProperty name)
334351
335352readAtIdx ∷ ∀ a. ReadForeign a ⇒ Int → Foreign → F a
336353readAtIdx i f = withExcept (map (ErrorAtIndex i)) (readImpl f)
@@ -552,3 +569,18 @@ instance ReadForeign DateTime where
552569unsafeStringToInt :: String → Int
553570unsafeStringToInt = Int.fromString >>>
554571 (fromMaybe' \_ -> unsafeCrashWith "impossible")
572+
573+sequenceCombining :: forall f a.
574+ Monoid (f a) =>
575+ Foldable f =>
576+ Applicative f =>
577+ f (F a) -> F (f a)
578+sequenceCombining = foldl fn (Right mempty) >>> except
579+ where
580+ fn :: _ -> _ -> (Either MultipleErrors (f a))
581+ fn acc elem = do
582+ case acc, runExcept elem of
583+ Left errs, Left errsNew -> Left (errs <> errsNew)
584+ Left errs, Right _ -> Left errs
585+ Right values, Right value -> Right (values <> pure value)
586+ Right _, Left errs -> Left errs
src/Yoga/JSON/Error.purs+33
@@ -14,3 +14,36 @@ toJSONPath fe = "$" <> path
1414 ErrorAtIndex i e -> "[" <> show i <> "]" <> go e
1515 ErrorAtProperty _ (TypeMismatch _ "undefined") -> ""
1616 ErrorAtProperty prop e -> "." <> prop <> go e
17+
18+renderHumanError :: ForeignError -> String
19+renderHumanError = errorToJSON >>> toHuman
20+ where
21+ toHuman { message, path } = message <> " at " <> path
22+
23+errorToJSON ∷
24+ ForeignError →
25+ { message ∷ String
26+ , path ∷ String
27+ }
28+errorToJSON err = go err
29+ where
30+ path = toJSONPath err
31+ go = case _ of
32+ ForeignError s →
33+ { path
34+ , message: s
35+ }
36+ TypeMismatch exp "Undefined" →
37+ { path
38+ , message: "Must provide a value of type '" <> exp <> "'"
39+ }
40+ TypeMismatch exp "undefined" →
41+ { path
42+ , message: "Must provide a value of type '" <> exp <> "'"
43+ }
44+ TypeMismatch exp act →
45+ { path
46+ , message: "Must provide a value of type '" <> exp <> "' instead of '" <> act <> "'"
47+ }
48+ ErrorAtIndex _ e → go e
49+ ErrorAtProperty _ e → go e
test/BasicsSpec.purs+2−2
@@ -17,9 +17,9 @@ import Data.Tuple (Tuple(..))
1717import Data.Tuple.Nested ((/\))
1818import Data.Variant (Variant, inj)
1919import Effect.Class (liftEffect)
20−import Effect.Now (nowDate, nowDateTime)
20+import Effect.Now (nowDateTime)
2121import Foreign.Object as Object
22−import Partial.Unsafe (unsafeCrashWith, unsafePartial)
22+import Partial.Unsafe (unsafeCrashWith)
2323import Test.Spec (Spec, describe, it)
2424import Test.Spec.Assertions (shouldEqual)
2525import Test.Util (roundtrips)
test/ErrorsSpec.purs+53−4
@@ -3,16 +3,65 @@ module Test.ErrorsSpec where
33import Prelude
44
55import Data.Array as Array
6−import Data.Either (blush)
6+import Data.Array.NonEmpty as NEA
7+import Data.Bifunctor (lmap)
8+import Data.Either (Either(..), blush, either)
79import Data.Maybe (Maybe(..))
10+import Foreign (ForeignError(..))
11+import Foreign.Object (Object)
12+import Foreign.Object as Object
813import Test.Spec (Spec, describe, it)
914import Test.Spec.Assertions (shouldEqual)
1015import Type.Proxy (Proxy(..))
11−import Yoga.JSON (class ReadForeign, readJSON)
12−import Yoga.JSON.Error (toJSONPath)
16+import Yoga.JSON (class ReadForeign, E, readJSON)
17+import Yoga.JSON as JSON
18+import Yoga.JSON.Error (errorToJSON, renderHumanError, toJSONPath)
1319
1420spec ∷ Spec Unit
1521spec = describe "Errors" do
22+ describe "are returned at once" do
23+ it "returns multiple errors for object" do
24+ let
25+ res :: E (Object Int)
26+ res = JSON.readJSON """{ "something": "no", "else": true }"""
27+ e1 = ErrorAtProperty "something" (TypeMismatch "Int" "String")
28+ e2 = ErrorAtProperty "else" (TypeMismatch "Int" "Boolean")
29+ res `shouldEqual` (Left (pure e1 <> pure e2))
30+
31+ it "returns multiple errors for record" do
32+ let
33+ res :: E { a :: Int, b :: String }
34+ res = JSON.readJSON """{ "b": true }"""
35+ e1 = ErrorAtProperty "a" (TypeMismatch "Int" "Undefined")
36+ e2 = ErrorAtProperty "b" (TypeMismatch "String" "Boolean")
37+ res `shouldEqual` (Left (pure e1 <> pure e2))
38+
39+ it "returns multiple errors for array" do
40+ let
41+ res :: E (Array Int)
42+ res = JSON.readJSON """[1,"a",2,true]"""
43+ e1 = ErrorAtIndex 1 (TypeMismatch "Int" "String")
44+ e2 = ErrorAtIndex 3 (TypeMismatch "Int" "Boolean")
45+ res `shouldEqual` (Left (pure e1 <> pure e2))
46+
47+ describe "produces human-friendly errors" do
48+ it "For some object" do
49+ let
50+ res :: E (Object Int)
51+ res = JSON.readJSON """{ "something": "no", "else": true }"""
52+ e1 = "Must provide a value of type 'Int' instead of 'String' at $.something"
53+ e2 = "Must provide a value of type 'Int' instead of 'Boolean' at $.else"
54+ (res # lmap (map renderHumanError >>> Array.fromFoldable))
55+ `shouldEqual` (Left [e1, e2])
56+
57+ it "For deeply nested keys" do
58+ let
59+ res :: E { a :: { b :: { c :: { d :: String }}}}
60+ res = JSON.readJSON """{ "a": { "b": { "c": { }}}}"""
61+ e = "Must provide a value of type 'String' at $.a.b.c.d"
62+ (res # lmap (map renderHumanError >>> Array.fromFoldable))
63+ `shouldEqual` (Left [e])
64+
1665 describe "have the correct JSON path" do
1766 it "empty json" do
1867 (getErrorPath (Proxy ∷ _ {}) "") `shouldEqual` Just [ "$" ]
@@ -33,4 +82,4 @@ spec = describe "Errors" do
3382getErrorPath ∷ ∀ a. ReadForeign a ⇒ Proxy a → String → Maybe (Array String)
3483getErrorPath _ x = do
3584 let e = (readJSON x ∷ _ a) # blush
36− (e <#> map toJSONPath <#> Array.fromFoldable)
85+ (e <#> map toJSONPath <#> Array.fromFoldable)
test/Util.purs+5−5
@@ -5,11 +5,11 @@ import Prelude
55import Data.Either (Either(..))
66import Data.Semigroup.Foldable (intercalateMap)
77import Effect.Aff (Aff)
8−import Foreign (Foreign, renderForeignError)
8+import Foreign (Foreign)
99import Test.Spec.Assertions (fail)
1010import Type.Proxy (Proxy(..))
1111import Yoga.JSON (class ReadForeign, class WriteForeign, read, readJSON, write, writeJSON)
12−import Yoga.JSON.Error (toJSONPath)
12+import Yoga.JSON.Error (renderHumanError, toJSONPath)
1313
1414roundtrips ∷ ∀ a. Show a ⇒ Eq a ⇒ ReadForeign a ⇒ WriteForeign a ⇒ a → Aff Unit
1515roundtrips x = do
@@ -18,10 +18,10 @@ roundtrips x = do
1818
1919shouldRead ∷ ∀ a. ReadForeign a ⇒ Proxy a → Foreign → Aff Unit
2020shouldRead _ = read >>> case _ of
21− Left e → fail (intercalateMap "\n" renderForeignError e <> "\n" <> intercalateMap "\n" toJSONPath e)
21+ Left e → fail (intercalateMap "\n" renderHumanError e <> "\n" <> intercalateMap "\n" toJSONPath e)
2222 Right (_ ∷ a) → pure unit
2323
2424shouldReadJSON ∷ ∀ a. ReadForeign a ⇒ Proxy a → String → Aff Unit
2525shouldReadJSON _ = readJSON >>> case _ of
26− Left e → fail (intercalateMap "\n" renderForeignError e <> "\n" <> intercalateMap "\n" toJSONPath e)
27− Right (_ ∷ a) → pure unit
26+ Left e → fail (intercalateMap "\n" renderHumanError e <> "\n" <> intercalateMap "\n" toJSONPath e)
27+ Right (_ ∷ a) → pure unit