Skip to content

Commit d03fae2

Browse files
Show filepath in error message in decodeFileWith
1 parent 67f5144 commit d03fae2

5 files changed

Lines changed: 92 additions & 13 deletions

File tree

kdl-hs.cabal

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ test-suite kdl-tests
6969
build-depends:
7070
base
7171
, containers
72+
, filepath
7273
, kdl-hs
7374
, skeletest
7475
, temporary

src/KDL/Decoder/Arrow.hs

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ import Control.Monad (unless, when)
9292
import Control.Monad.Trans.Class qualified as Trans
9393
import Control.Monad.Trans.State.Strict (StateT)
9494
import Control.Monad.Trans.State.Strict qualified as StateT
95+
import Data.Bifunctor (first)
9596
import Data.Bits (finiteBitSize)
9697
import Data.Int (Int64)
9798
import Data.List (partition)
@@ -134,22 +135,27 @@ import Prelude qualified
134135

135136
-- | Decode the given KDL configuration with the given decoder.
136137
decodeWith :: DocumentDecoder a -> Text -> Either DecodeError a
137-
decodeWith decoder = decodeFromParseResult decoder . parse
138+
decodeWith decoder = decodeFromParseResult decoder Nothing . parse
138139

139140
-- | Read KDL configuration from the given file path and decode it with the given decoder.
140141
decodeFileWith :: DocumentDecoder a -> FilePath -> IO (Either DecodeError a)
141-
decodeFileWith decoder = fmap (decodeFromParseResult decoder) . parseFile
142+
decodeFileWith decoder fp = decodeFromParseResult decoder (Just fp) <$> parseFile fp
142143

143144
-- | Decode an already-parsed 'Document' with the given decoder.
144145
decodeDocWith :: DocumentDecoder a -> Document -> Either DecodeError a
145146
decodeDocWith (UnsafeDocumentDecoder decoder) doc =
146147
runDecodeM . runDecodeStateM doc emptyDecodeHistory $
147148
decoder.run ()
148149

149-
decodeFromParseResult :: DocumentDecoder a -> Either Text Document -> Either DecodeError a
150-
decodeFromParseResult decoder = \case
151-
Left e -> runDecodeM . decodeThrow $ DecodeError_ParseError e
152-
Right doc -> decodeDocWith decoder doc
150+
decodeFromParseResult ::
151+
DocumentDecoder a ->
152+
Maybe FilePath ->
153+
Either Text Document ->
154+
Either DecodeError a
155+
decodeFromParseResult decoder mPath =
156+
first (\e -> e{filepath = mPath}) . \case
157+
Left e -> runDecodeM . decodeThrow $ DecodeError_ParseError e
158+
Right doc -> decodeDocWith decoder doc
153159

154160
{----- Decoder -----}
155161

src/KDL/Decoder/Internal/DecodeM.hs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{-# LANGUAGE DuplicateRecordFields #-}
22
{-# LANGUAGE LambdaCase #-}
3+
{-# LANGUAGE OverloadedRecordDot #-}
34
{-# LANGUAGE OverloadedStrings #-}
45
{-# LANGUAGE RecordWildCards #-}
56
{-# LANGUAGE NoFieldSelectors #-}
@@ -70,7 +71,7 @@ runDecodeM (DecodeM f) = f Left Left Right
7071
-- This error is non-fatal and can be handled by '<|>'. See 'makeFatal'
7172
-- for more information.
7273
decodeThrow :: BaseDecodeError -> DecodeM a
73-
decodeThrow e = DecodeM $ \_ onFail _ -> onFail $ DecodeError [([], e)]
74+
decodeThrow e = DecodeM $ \_ onFail _ -> onFail $ DecodeError Nothing [([], e)]
7475

7576
-- | Throw a 'DecodeError_Custom' error.
7677
failM :: Text -> DecodeM a
@@ -91,4 +92,4 @@ makeNonFatal (DecodeM f) = DecodeM $ \_ onFail onSuccess -> f onFail onFail onSu
9192
addContext :: ContextItem -> DecodeM a -> DecodeM a
9293
addContext ctxItem (DecodeM f) = DecodeM $ \onFatal onFail onSuccess -> f (onFatal . addCtx) (onFail . addCtx) onSuccess
9394
where
94-
addCtx (DecodeError es) = DecodeError [(ctxItem : ctx, msg) | (ctx, msg) <- es]
95+
addCtx e = e{errors = [(ctxItem : ctx, msg) | (ctx, msg) <- e.errors]}

src/KDL/Decoder/Internal/Error.hs

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{-# LANGUAGE DuplicateRecordFields #-}
22
{-# LANGUAGE LambdaCase #-}
3+
{-# LANGUAGE OverloadedRecordDot #-}
34
{-# LANGUAGE OverloadedStrings #-}
45
{-# LANGUAGE RecordWildCards #-}
56
{-# LANGUAGE NoFieldSelectors #-}
@@ -12,6 +13,7 @@ module KDL.Decoder.Internal.Error (
1213
renderDecodeError,
1314
) where
1415

16+
import Control.Applicative ((<|>))
1517
import Data.Map qualified as Map
1618
import Data.Text (Text)
1719
import Data.Text qualified as Text
@@ -24,12 +26,15 @@ import KDL.Types (
2426
Value,
2527
)
2628

27-
data DecodeError = DecodeError [(Context, BaseDecodeError)]
29+
data DecodeError = DecodeError
30+
{ filepath :: Maybe FilePath
31+
, errors :: [(Context, BaseDecodeError)]
32+
}
2833
deriving (Show, Eq)
2934
instance Semigroup DecodeError where
30-
DecodeError e1 <> DecodeError e2 = DecodeError (e1 <> e2)
35+
DecodeError fp1 e1 <> DecodeError fp2 e2 = DecodeError (fp1 <|> fp2) (e1 <> e2)
3136
instance Monoid DecodeError where
32-
mempty = DecodeError []
37+
mempty = DecodeError Nothing []
3338

3439
type Context = [ContextItem]
3540

@@ -60,10 +65,19 @@ data BaseDecodeError
6065
deriving (Show, Eq)
6166

6267
renderDecodeError :: DecodeError -> Text
63-
renderDecodeError = Text.intercalate "\n" . map renderCtxErrors . groupCtxErrors
68+
renderDecodeError decodeError =
69+
Text.intercalate "\n"
70+
. addPath decodeError.filepath
71+
. map renderCtxErrors
72+
. groupCtxErrors
73+
$ decodeError.errors
6474
where
6575
-- Group errors with the same contexts together
66-
groupCtxErrors (DecodeError es) = Map.toAscList $ Map.fromListWith (<>) [(ctx, [e]) | (ctx, e) <- es]
76+
groupCtxErrors es = Map.toAscList $ Map.fromListWith (<>) [(ctx, [e]) | (ctx, e) <- es]
77+
78+
addPath = \case
79+
Nothing -> id
80+
Just fp -> let msg = "Failed to decode " <> Text.pack fp <> ":" in (msg :)
6781

6882
renderCtxErrors = \case
6983
-- Special case parse errors, which shouldn't have a context

test/KDL/DecoderSpec.hs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import KDL qualified
99
import KDL.TestUtils.Error (decodeErrorMsg)
1010
import KDL.Types (Node)
1111
import Skeletest
12+
import Skeletest.Predicate qualified as P
13+
import System.FilePath ((</>))
1214

1315
spec :: Spec
1416
spec = do
@@ -53,3 +55,58 @@ spec = do
5355
[ "At: foo #1 > bar #0 > baz #3 > prop a"
5456
, " Expected text, got: 1"
5557
]
58+
59+
describe "decodeFileWith" $ do
60+
it "fails with helpful error if parsing fails" $ do
61+
FixtureKdlFile file <- getFixture
62+
writeFile file "foo hello= 123"
63+
let decoder = KDL.document $ KDL.node @Node "foo"
64+
KDL.decodeFileWith decoder file
65+
`shouldSatisfy` (P.returns . decodeErrorMsg)
66+
[ "Failed to decode " <> Text.pack file <> ":"
67+
, "1:10:"
68+
, " |"
69+
, "1 | foo hello= 123"
70+
, " | ^^"
71+
, "unexpected \"= \""
72+
, "expecting Node Child, Node Space, or Node Terminator"
73+
]
74+
75+
it "fails with user-defined error" $ do
76+
FixtureKdlFile file <- getFixture
77+
writeFile file "foo -1"
78+
let decoder =
79+
KDL.document . KDL.argAtWith "foo" $
80+
KDL.withDecoder KDL.number $ \x -> do
81+
when (x < 0) $ do
82+
KDL.failM $ "Got negative number: " <> (Text.pack . show) x
83+
pure x
84+
KDL.decodeFileWith decoder file
85+
`shouldSatisfy` (P.returns . decodeErrorMsg)
86+
[ "Failed to decode " <> Text.pack file <> ":"
87+
, "At: foo #0 > arg #0"
88+
, " Got negative number: -1.0"
89+
]
90+
91+
it "shows context in deeply nested error" $ do
92+
FixtureKdlFile file <- getFixture
93+
writeFile file "foo; foo { bar { baz; baz; baz; baz a=1; }; }"
94+
let decoder =
95+
KDL.document
96+
. (KDL.many . KDL.nodeWith "foo" . KDL.children)
97+
. (KDL.many . KDL.nodeWith "bar" . KDL.children)
98+
. (KDL.many . KDL.nodeWith "baz")
99+
$ KDL.optional (KDL.prop @Text "a")
100+
KDL.decodeFileWith decoder file
101+
`shouldSatisfy` (P.returns . decodeErrorMsg)
102+
[ "Failed to decode " <> Text.pack file <> ":"
103+
, "At: foo #1 > bar #0 > baz #3 > prop a"
104+
, " Expected text, got: 1"
105+
]
106+
107+
newtype FixtureKdlFile = FixtureKdlFile FilePath
108+
109+
instance Fixture FixtureKdlFile where
110+
fixtureAction = do
111+
FixtureTmpDir tmpdir <- getFixture
112+
pure . noCleanup $ FixtureKdlFile (tmpdir </> "kdl-hs-test.kdl")

0 commit comments

Comments
 (0)