kdl-hs 1.2.0 → 1.2.1
raw patch · 10 files changed
+516/−174 lines, 10 filesdep ~skeletestPVP: major bump suggested
API removals or changes: PVP suggests a major version bump
Dependency ranges changed: skeletest
API changes (from Hackage documentation)
- KDL.Decoder.Internal.Error: type Context = [ContextItem]
- KDL.Decoder.Monad: type Context = [ContextItem]
+ KDL.Decoder.Internal.Error: Context :: [ContextItem] -> Maybe Span -> Maybe Text -> Context
+ KDL.Decoder.Internal.Error: [path] :: Context -> [ContextItem]
+ KDL.Decoder.Internal.Error: [span] :: Context -> Maybe Span
+ KDL.Decoder.Internal.Error: [srcLine] :: Context -> Maybe Text
+ KDL.Decoder.Internal.Error: data Context
+ KDL.Decoder.Internal.Error: instance Data.Default.Internal.Default KDL.Decoder.Internal.Error.Context
+ KDL.Decoder.Internal.Error: instance GHC.Classes.Eq KDL.Decoder.Internal.Error.Context
+ KDL.Decoder.Internal.Error: instance GHC.Show.Show KDL.Decoder.Internal.Error.Context
+ KDL.Decoder.Monad: Context :: [ContextItem] -> Maybe Span -> Maybe Text -> Context
+ KDL.Decoder.Monad: [path] :: Context -> [ContextItem]
+ KDL.Decoder.Monad: [span] :: Context -> Maybe Span
+ KDL.Decoder.Monad: [srcLine] :: Context -> Maybe Text
+ KDL.Decoder.Monad: data Context
- KDL.Decoder.Internal.DecodeM: addContext :: ContextItem -> DecodeM a -> DecodeM a
+ KDL.Decoder.Internal.DecodeM: addContext :: Span -> ContextItem -> DecodeM a -> DecodeM a
Files
- CHANGELOG.md +4/−0
- kdl-hs.cabal +3/−2
- src/KDL/Decoder/Arrow.hs +75/−25
- src/KDL/Decoder/Internal/DecodeM.hs +20/−4
- src/KDL/Decoder/Internal/Error.hs +85/−21
- test/KDL/Decoder/SharedSpec/Template.hs +212/−89
- test/KDL/DecoderSpec.hs +53/−3
- test/KDL/__snapshots__/DecoderSpec.snap.md +50/−16
- test/KDL/__snapshots__/ParserSpec.snap.md +13/−13
- test/KDL/__snapshots__/RenderSpec.snap.md +1/−1
CHANGELOG.md view
@@ -1,3 +1,7 @@+## v1.2.1++* Improve error messages to include the source line where the error occurred+ ## v1.2.0 * Add `Exception` instance to `DecodeError`
kdl-hs.cabal view
@@ -1,7 +1,7 @@ cabal-version: 3.0 name: kdl-hs-version: 1.2.0+version: 1.2.1 synopsis: KDL language parser and API description: KDL language parser and API. homepage: https://github.com/brandonchinn178/kdl-hs#readme@@ -45,6 +45,7 @@ base < 5 , containers , data-default+ , filepath , megaparsec , prettyprinter >= 1.7.0 , scientific@@ -97,7 +98,7 @@ , pretty-show , process , scientific- , skeletest+ , skeletest >= 0.4 , temporary , text default-language: GHC2021
src/KDL/Decoder/Arrow.hs view
@@ -103,10 +103,12 @@ ) import Control.Monad (unless, when) import Control.Monad.Trans.Class qualified as Trans+import Control.Monad.Trans.Except qualified as ExceptT import Control.Monad.Trans.State.Strict (StateT) import Control.Monad.Trans.State.Strict qualified as StateT import Data.Bifunctor (first) import Data.Bits (finiteBitSize)+import Data.Functor.Identity (runIdentity) import Data.Int (Int64) import Data.List (partition) import Data.List.NonEmpty qualified as NonEmpty@@ -119,6 +121,7 @@ import Data.Set qualified as Set import Data.Text (Text) import Data.Text qualified as Text+import Data.Text.IO qualified as Text import Data.Typeable (Typeable, typeRep) import Data.Word (Word16, Word32, Word64, Word8) import GHC.Int (Int16, Int32, Int8)@@ -133,29 +136,23 @@ TypedValueSchema (..), getValueSchemaNames, )-import KDL.Parser (parse, parseFile)-import KDL.Types (- Ann (..),- Document,- Entry (..),- Identifier (..),- Node (..),- NodeList (..),- Value (..),- ValueData (..),- def,- )+import KDL.Parser (ParseConfig (..), parse, parseFile, parseWith)+import KDL.Types (Ann (..), Document, Entry (..), Identifier (..), IdentifierExtension (..), Node (..), NodeExtension (..), NodeList (..), Span (..), Value (..), ValueData (..), ValueExtension (..), def) import Numeric.Natural (Natural)-import Prelude hiding (any, fail, null)+import Prelude hiding (any, fail, null, span) import Prelude qualified -- | Decode the given KDL configuration with the given decoder. decodeWith :: DocumentDecoder a -> Text -> Either DecodeError a-decodeWith decoder = decodeFromParseResult decoder Nothing . parse+decodeWith decoder input = runIdentity $ do+ let doc = parse input+ decodeFromParseResult Nothing (pure input) decoder doc -- | Read KDL configuration from the given file path and decode it with the given decoder. decodeFileWith :: DocumentDecoder a -> FilePath -> IO (Either DecodeError a)-decodeFileWith decoder fp = decodeFromParseResult decoder (Just fp) <$> parseFile fp+decodeFileWith decoder fp = do+ doc <- parseFile fp+ decodeFromParseResult (Just fp) (Text.readFile fp) decoder doc -- | Decode an already-parsed 'Document' with the given decoder. decodeDocWith :: DocumentDecoder a -> Document -> Either DecodeError a@@ -164,15 +161,49 @@ decoder.run () decodeFromParseResult ::- DocumentDecoder a ->+ (Monad m) => Maybe FilePath ->+ m Text ->+ DocumentDecoder a -> Either Text Document ->- Either DecodeError a-decodeFromParseResult decoder mPath =- first (\e -> e{filepath = mPath}) . \case+ m (Either DecodeError a)+decodeFromParseResult mPath getInput decoder =+ firstM augmentError . \case Left e -> runDecodeM . decodeThrow $ DecodeError_ParseError e Right doc -> decodeDocWith decoder doc+ where+ augmentError originalError = runEarlyReturn $ do+ input <- Trans.lift getInput+ doc <-+ case parseWith def{includeSpans = True} input of+ Right doc -> pure doc+ -- should not happen; return the plain error if it happens+ Left _ -> returnE originalError+ err <-+ case decodeDocWith decoder doc of+ Left err -> pure err+ -- should not happen; return the plain error if it happens+ Right _ -> returnE originalError+ pure+ DecodeError+ { filepath = mPath+ , errors = fmap (addSrcLine input) err.errors+ } + addSrcLine input =+ let inputLines = Text.lines input+ addSrcLine' ctx span = ctx{srcLine = safeIndex (span.startLine - 1) inputLines}+ in first (\ctx -> maybe ctx (addSrcLine' ctx) ctx.span)++ runEarlyReturn m = ExceptT.runExceptT m >>= either pure pure+ returnE = ExceptT.throwE++ firstM f = either (fmap Left . f) (pure . Right)+ safeIndex (n :: Int) = \case+ _ | n < 0 -> Nothing+ [] -> Nothing+ x : xs -> if n == 0 then Just x else safeIndex (n - 1) xs+ {----- Decoder -----} {----- Decoding Document -----}@@ -291,11 +322,25 @@ index <- StateT.gets (getNodeIndex name.value) StateT.modify $ \s -> s{object = s.object{nodes = nodes'}} b <-- Trans.lift . addContext ContextNode{name, index} $+ Trans.lift . addContext (nodeSpan node_) ContextNode{name, index} $ decodeNode node_ StateT.modify $ \s -> s{history = s.history{nodesSeen = inc name.value s.history.nodesSeen}} pure $ Just (node_, b) where+ -- start of Node span -> end of last Entry span+ nodeSpan (node_ :: Node) =+ let startSpan = node_.ext.span+ endSpan =+ case NonEmpty.nonEmpty node_.entries of+ Just entries -> (NonEmpty.last entries).value.ext.span+ Nothing -> node_.name.ext.span+ in Span+ { startLine = startSpan.startLine+ , startCol = startSpan.startCol+ , endLine = endSpan.endLine+ , endCol = endSpan.endCol+ }+ inc k = Map.insertWith (+) k 1 -- | Decode all remaining nodes.@@ -695,7 +740,7 @@ StateT.modify $ \s -> s{object = s.object{entries = entries'}} b <-- Trans.lift . addContext argContext $+ Trans.lift . addContext entry.value.ext.span argContext $ decodeValue a entry.value StateT.modify $ \s -> s{history = s.history{argsSeen = s.history.argsSeen + 1}} pure b@@ -762,7 +807,7 @@ Just (name, prop_, entries') -> do StateT.modify $ \s -> s{object = s.object{entries = entries'}} b <-- Trans.lift . addContext ContextProp{name} $+ Trans.lift . addContext prop_.value.ext.span ContextProp{name} $ decodeValue prop_.value StateT.modify $ \s -> s{history = s.history{propsSeen = Set.insert name s.history.propsSeen}} pure $ Just (name, b)@@ -1080,9 +1125,14 @@ where addLabel (ctx, kind) = let ctx' =- flip map ctx $ \case- ContextArg{label = _, ..} -> ContextArg{label = Just name, ..}- item -> item+ ctx+ { path =+ [ case item of+ ContextArg{label = _, ..} -> ContextArg{label = Just name, ..}+ _ -> item+ | item <- ctx.path+ ]+ } kind' = case kind of DecodeError_ExpectedArg{label = _, ..} -> DecodeError_ExpectedArg{label = Just name, ..}
src/KDL/Decoder/Internal/DecodeM.hs view
@@ -22,10 +22,14 @@ import Control.Applicative (Alternative (..)) import Data.Bifunctor (first)+import Data.Default (def) import Data.List.NonEmpty (NonEmpty (..)) import Data.List.NonEmpty qualified as NonEmpty import Data.Text (Text) import KDL.Decoder.Internal.Error+import KDL.Decoder.Internal.Error qualified as Error+import KDL.Types (Span)+import Prelude hiding (span) -- | The monad that returns either a 'DecodeError' or a result of type @a@. data DecodeM a@@ -91,7 +95,7 @@ EQ -> es1 <> es2 GT -> es1 where- key = length . fst . NonEmpty.head+ key = length . (.path) . fst . NonEmpty.head mergeHintsL :: DecodeHints ->@@ -112,17 +116,29 @@ DecodeM_Found a es -> DecodeM_Found a (map f es) DecodeM_Fail es -> DecodeM_Fail (fmap f es) +mapErrorContext :: (Error.Context -> Error.Context) -> DecodeM a -> DecodeM a+mapErrorContext f = mapErrors (first f)+ -- | Throw an error. decodeThrow :: DecodeErrorKind -> DecodeM a-decodeThrow e = DecodeM_Fail . NonEmpty.singleton $ ([], e)+decodeThrow e = DecodeM_Fail . NonEmpty.singleton $ (def, e) -- | Throw a 'DecodeError_Custom' error. failM :: Text -> DecodeM a failM = decodeThrow . DecodeError_Custom -- | Add context to all errors that occur in the given action.-addContext :: ContextItem -> DecodeM a -> DecodeM a-addContext ctxItem = mapErrors (first (ctxItem :))+addContext :: Span -> ContextItem -> DecodeM a -> DecodeM a+addContext span ctxItem = mapErrorContext $ \ctx ->+ ctx+ { path = ctxItem : ctx.path+ , -- Span should only be attached to the nearest context; i.e. the first+ -- addContext that runs+ span = ctx.span <|> span'+ }+ where+ -- Ignore span if it's empty+ span' = if span == def then Nothing else Just span -- | Discard hints after validating that an error context is successful. discardHints :: DecodeM a -> DecodeM a
src/KDL/Decoder/Internal/Error.hs view
@@ -9,15 +9,15 @@ DecodeError (..), BaseDecodeError, DecodeErrorKind (..),- Context,+ Context (..), ContextItem (..), renderDecodeError, ) where import Control.Exception (Exception (..))+import Data.Default (Default (..)) import Data.List.NonEmpty (NonEmpty) import Data.List.NonEmpty qualified as NonEmpty-import Data.Map qualified as Map import Data.Text (Text) import Data.Text qualified as Text import KDL.Render (@@ -26,8 +26,11 @@ ) import KDL.Types ( Identifier,+ Span (..), Value, )+import System.FilePath (takeFileName)+import Prelude hiding (span) data DecodeError = DecodeError { filepath :: Maybe FilePath@@ -39,8 +42,22 @@ displayException = Text.unpack . renderDecodeError type BaseDecodeError = (Context, DecodeErrorKind)-type Context = [ContextItem] +data Context = Context+ { path :: [ContextItem]+ , span :: Maybe Span+ , srcLine :: Maybe Text+ }+ deriving (Show, Eq)++instance Default Context where+ def =+ Context+ { path = []+ , span = Nothing+ , srcLine = Nothing+ }+ data ContextItem = ContextNode { name :: Identifier@@ -72,35 +89,79 @@ renderDecodeError decodeError = Text.intercalate "\n" . concatMap renderCtxErrors- . groupCtxErrors+ . NonEmpty.groupAllWith1 groupKey $ decodeError.errors where -- Group errors with the same contexts together- groupCtxErrors es =- Map.toAscList . Map.fromListWith (<>) $- [ (ctx, [e])- | (ctx, e) <- NonEmpty.toList es- ]-- addPath =- case decodeError.filepath of- Nothing -> id- Just fp -> let msg = "Failed to decode " <> Text.pack fp <> ":" in (msg :)+ groupKey (ctx, _) = maybe (Left ctx.path) Right ctx.span renderCtxErrors = \case -- Special case parse errors, which shouldn't have a context- (_, [DecodeError_ParseError msg]) -> [msg]- (ctx, errs) -> addPath $ ("At: " <> renderCtxItems ctx) : renderErrors errs+ (_, DecodeError_ParseError msg) NonEmpty.:| _ -> [msg]+ errs ->+ let (ctx, _) = NonEmpty.head errs+ in renderCtx ctx $ (map (renderError . snd) $ NonEmpty.toList errs) - renderCtxItems items- | null items = "<root>"- | otherwise = Text.intercalate " > " . map renderCtxItem $ items- renderCtxItem = \case+ renderCtx (ctx :: Context) =+ case ctx.span of+ Nothing -> renderCtxPath ctx.path+ Just span -> renderCtxFull span ctx++ -- If we don't have the error span, the best we can do is render the context path:+ --+ -- At: foo.kdl > user #0 > arg #0+ -- ├─ error message+ -- └─ another error message+ renderCtxPath path errors =+ let pathDisplay =+ Text.intercalate " > " . concat $+ [ case decodeError.filepath of+ Nothing -> []+ Just fp -> [Text.pack $ takeFileName fp]+ , if null path then ["(root)"] else map renderCtxPathItem path+ ]+ errors' =+ [ (if isLast then "└─ " else "├─ ") <> err+ | (err, isLast) <- withIsLast errors+ ]+ in ("At: " <> pathDisplay) : errors'+ renderCtxPathItem = \case ContextNode{..} -> renderIdentifier name <> " #" <> showT index ContextArg{..} -> renderArg index label ContextProp{..} -> "prop " <> renderIdentifier name - renderErrors = map (" " <>) . concatMap (Text.lines . renderError)+ -- If we have the error span, show a descriptive error message:+ --+ -- foo.kdl:3:16:+ -- • Expected number, got string+ -- |+ -- 3 | some_child bad-value+ -- | ^^^^^^^^^+ renderCtxFull (span :: Span) ctx errors =+ let spanDisplay =+ Text.concat . map (<> ":") $+ [ maybe "<input>" Text.pack decodeError.filepath+ , showT span.startLine+ , showT span.startCol+ ]+ errors' = map (" • " <>) errors+ preview =+ case ctx.srcLine of+ Nothing -> []+ Just line ->+ let lineNum = showT span.startLine+ spaces n = Text.replicate n " "+ renderPrefix isSpace = (if isSpace then spaces (Text.length lineNum) else lineNum) <> " │"+ spanLength =+ if span.startLine == span.endLine+ then span.endCol - span.startCol + 1+ else Text.length line - span.startCol + 1+ in [ renderPrefix True+ , renderPrefix False <> " " <> line+ , renderPrefix True <> spaces span.startCol <> Text.replicate spanLength "^"+ ]+ in spanDisplay : errors' ++ preview+ renderError = \case DecodeError_Custom msg -> msg DecodeError_ParseError msg -> msg@@ -134,10 +195,13 @@ [x] -> x [x, y] -> Text.unwords [x, conj, y] xs -> Text.intercalate ", " $ mapLast ((conj <> " ") <>) xs+ mapLast f = \case [] -> [] [x] -> [f x] x : xs -> x : mapLast f xs++ withIsLast = mapLast (True <$) . map (\x -> (x, False)) -- Replace with Text.show after requiring at least text-2.1.2 showT :: (Show a) => a -> Text
@@ -117,8 +117,8 @@ _RETURN_((foo1, foo2)) KDL.decodeWith decoder config `shouldSatisfy` decodeErrorMsg- [ "At: <root>"- , " Expected another node: foo"+ [ "At: (root)"+ , "└─ Expected another node: foo" ] -- Most behaviors tested with `node`@@ -141,8 +141,11 @@ _STMT_(KDL.nodeWith "foo" decodeFoo) KDL.decodeWith decoder config `shouldSatisfy` decodeErrorMsg- [ "At: foo #0 > arg #0"- , " Expected string, got: 1.0"+ [ "<input>:1:5:"+ , " • Expected string, got: 1.0"+ , " │"+ , "1 │ foo 1.0"+ , " │ ^^^" ] -- Most behaviors tested with `nodeWith`@@ -177,8 +180,11 @@ _STMT_(_APOS_(KDL.nodeWith) "foo" ["FOO"] $ KDL.arg @Int) KDL.decodeWith decoder config `shouldSatisfy` decodeErrorMsg- [ "At: foo #0"- , " Expected annotation to be one of [\"FOO\"], got: test"+ [ "<input>:1:1:"+ , " • Expected annotation to be one of [\"FOO\"], got: test"+ , " │"+ , "1 │ (test)foo 2"+ , " │ ^^^^^^^^^^^" ] describe "remainingNodes" $ do@@ -240,8 +246,11 @@ _STMT_(KDL.remainingNodesWith decodeNode) KDL.decodeWith decoder config `shouldSatisfy` decodeErrorMsg- [ "At: bar #1 > arg #0"- , " Expected number, got: hello"+ [ "<input>:1:19:"+ , " • Expected number, got: hello"+ , " │"+ , "1 │ foo 1; bar 1; bar hello"+ , " │ ^^^^^" ] -- Most behaviors tested with `remainingNodesWith`@@ -278,8 +287,11 @@ _STMT_(_APOS_(KDL.remainingNodesWith) ["FOO"] $ KDL.arg @Int) KDL.decodeWith decoder config `shouldSatisfy` decodeErrorMsg- [ "At: foo #1"- , " Expected annotation to be one of [\"FOO\"], got: test"+ [ "<input>:1:13:"+ , " • Expected annotation to be one of [\"FOO\"], got: test"+ , " │"+ , "1 │ (FOO)foo 1; (test)foo 2"+ , " │ ^^^^^^^^^^^" ] describe "argAt" $ do@@ -297,8 +309,8 @@ _STMT_(KDL.argAt @Int "foo") KDL.decodeWith decoder config `shouldSatisfy` decodeErrorMsg- [ "At: <root>"- , " Expected node: foo"+ [ "At: (root)"+ , "└─ Expected node: foo" ] it "fails if node has no args" $ do@@ -307,8 +319,11 @@ _STMT_(KDL.argAt @Int "foo") KDL.decodeWith decoder config `shouldSatisfy` decodeErrorMsg- [ "At: foo #0"- , " Expected arg #0 with type: number"+ [ "<input>:1:1:"+ , " • Expected arg #0 with type: number"+ , " │"+ , "1 │ foo"+ , " │ ^^^" ] it "fails if arg fails to parse" $ do@@ -317,8 +332,11 @@ _STMT_(KDL.argAt @Text "foo") KDL.decodeWith decoder config `shouldSatisfy` decodeErrorMsg- [ "At: foo #0 > arg #0"- , " Expected string, got: 1"+ [ "<input>:1:5:"+ , " • Expected string, got: 1"+ , " │"+ , "1 │ foo 1"+ , " │ ^" ] it "shows label on missing arg" $ do@@ -327,8 +345,11 @@ _STMT_(KDL.label "value" $ KDL.argAt @Int "foo") KDL.decodeWith decoder config `shouldSatisfy` decodeErrorMsg- [ "At: foo #0"- , " Expected arg 'value' with type: number"+ [ "<input>:1:1:"+ , " • Expected arg 'value' with type: number"+ , " │"+ , "1 │ foo"+ , " │ ^^^" ] it "shows label on invalid arg" $ do@@ -337,8 +358,11 @@ _STMT_(KDL.label "value" $ KDL.argAt @Text "foo") KDL.decodeWith decoder config `shouldSatisfy` decodeErrorMsg- [ "At: foo #0 > arg 'value'"- , " Expected string, got: 1"+ [ "<input>:1:5:"+ , " • Expected string, got: 1"+ , " │"+ , "1 │ foo 1"+ , " │ ^" ] -- Most behaviors tested with `argAt`@@ -381,8 +405,11 @@ _STMT_(_APOS_(KDL.argAtWith) "foo" ["VAL"] KDL.string) KDL.decodeWith decoder config `shouldSatisfy` decodeErrorMsg- [ "At: foo #0 > arg #0"- , " Expected annotation to be one of [\"VAL\"], got: test"+ [ "<input>:1:5:"+ , " • Expected annotation to be one of [\"VAL\"], got: test"+ , " │"+ , "1 │ foo (test)a"+ , " │ ^^^^^^^" ] describe "argsAt" $ do@@ -410,8 +437,11 @@ _STMT_(KDL.argsAt @Int "foo") KDL.decodeWith decoder config `shouldSatisfy` decodeErrorMsg- [ "At: foo #0 > arg #1"- , " Expected number, got: asdf"+ [ "<input>:1:7:"+ , " • Expected number, got: asdf"+ , " │"+ , "1 │ foo 1 asdf"+ , " │ ^^^^" ] -- Most behaviors tested with `argsAt`@@ -454,8 +484,11 @@ _STMT_(_APOS_(KDL.argsAtWith) "foo" ["VAL"] KDL.string) KDL.decodeWith decoder config `shouldSatisfy` decodeErrorMsg- [ "At: foo #0 > arg #1"- , " Expected annotation to be one of [\"VAL\"], got: test"+ [ "<input>:1:12:"+ , " • Expected annotation to be one of [\"VAL\"], got: test"+ , " │"+ , "1 │ foo (VAL)a (test)b"+ , " │ ^^^^^^^" ] describe "dashChildrenAt" $ do@@ -483,8 +516,11 @@ _STMT_(KDL.dashChildrenAt @Int "foo") KDL.decodeWith decoder config `shouldSatisfy` decodeErrorMsg- [ "At: foo #0 > - #0"- , " Unexpected arg #1: 2"+ [ "<input>:1:7:"+ , " • Unexpected arg #1: 2"+ , " │"+ , "1 │ foo { - 1 2; - 3 4; }"+ , " │ ^^^^^" ] it "fails if node has non-dash children" $ do@@ -493,9 +529,12 @@ _STMT_(KDL.dashChildrenAt @Int "foo") KDL.decodeWith decoder config `shouldSatisfy` decodeErrorMsg- [ "At: foo #0"- , " Unexpected node: bar #0"- , " Expected another node: -"+ [ "<input>:1:1:"+ , " • Expected another node: -"+ , " • Unexpected node: bar #0"+ , " │"+ , "1 │ foo { - 1; bar 1 2 3; }"+ , " │ ^^^" ] it "fails if any child fails to parse" $ do@@ -504,8 +543,11 @@ _STMT_(KDL.dashChildrenAt @Int "foo") KDL.decodeWith decoder config `shouldSatisfy` decodeErrorMsg- [ "At: foo #0 > - #1 > arg #0"- , " Expected number, got: asdf"+ [ "<input>:1:14:"+ , " • Expected number, got: asdf"+ , " │"+ , "1 │ foo { - 1; - asdf; }"+ , " │ ^^^^" ] -- Most behaviors tested with `dashChildrenAt`@@ -548,8 +590,11 @@ _STMT_(_APOS_(KDL.dashChildrenAtWith) "foo" ["VAL"] KDL.string) KDL.decodeWith decoder config `shouldSatisfy` decodeErrorMsg- [ "At: foo #0 > - #0 > arg #0"- , " Expected annotation to be one of [\"VAL\"], got: test"+ [ "<input>:1:9:"+ , " • Expected annotation to be one of [\"VAL\"], got: test"+ , " │"+ , "1 │ foo { - (test)a; }"+ , " │ ^^^^^^^" ] describe "dashNodesAt" $ do@@ -589,9 +634,12 @@ _STMT_(KDL.dashNodesAt @Node "foo") KDL.decodeWith decoder config `shouldSatisfy` decodeErrorMsg- [ "At: foo #0"- , " Unexpected node: bar #0"- , " Expected another node: -"+ [ "<input>:1:1:"+ , " • Expected another node: -"+ , " • Unexpected node: bar #0"+ , " │"+ , "1 │ foo { - 1; bar 1 2 3; }"+ , " │ ^^^" ] -- Most behaviors tested with `dashNodesAt`@@ -612,8 +660,11 @@ _STMT_(KDL.dashNodesAtWith "foo" $ KDL.children $ KDL.argAt @Int "bar") KDL.decodeWith decoder config `shouldSatisfy` decodeErrorMsg- [ "At: foo #0 > - #1 > bar #0 > arg #0"- , " Expected number, got: test"+ [ "<input>:1:29:"+ , " • Expected number, got: test"+ , " │"+ , "1 │ foo { - { bar 1; }; - { bar test; }; }"+ , " │ ^^^^" ] describe "NodeDecoder" $ do@@ -645,8 +696,11 @@ _STMT_(KDL.arg @Int) decodeNode "foo" decoder config `shouldSatisfy` decodeErrorMsg- [ "At: foo #0"- , " Expected arg #0 with type: number"+ [ "<input>:1:1:"+ , " • Expected arg #0 with type: number"+ , " │"+ , "1 │ foo"+ , " │ ^^^" ] it "fails if argument fails to parse" $ do@@ -655,8 +709,11 @@ _STMT_(KDL.arg @Int) decodeNode "foo" decoder config `shouldSatisfy` decodeErrorMsg- [ "At: foo #0 > arg #0"- , " Expected number, got: test"+ [ "<input>:1:5:"+ , " • Expected number, got: test"+ , " │"+ , "1 │ foo test"+ , " │ ^^^^" ] it "fails if not all arguments are decoded" $ do@@ -665,8 +722,11 @@ _STMT_(KDL.arg @Int) decodeNode "foo" decoder config `shouldSatisfy` decodeErrorMsg- [ "At: foo #0"- , " Unexpected arg #1: 2"+ [ "<input>:1:1:"+ , " • Unexpected arg #1: 2"+ , " │"+ , "1 │ foo 1 2 3"+ , " │ ^^^^^^^^^" ] it "shows label on missing arg" $ do@@ -675,8 +735,11 @@ _STMT_(KDL.label "value" $ KDL.arg @Int) decodeNode "foo" decoder config `shouldSatisfy` decodeErrorMsg- [ "At: foo #0"- , " Expected arg 'value' with type: number"+ [ "<input>:1:1:"+ , " • Expected arg 'value' with type: number"+ , " │"+ , "1 │ foo"+ , " │ ^^^" ] it "shows expected types on missing arg" $ do@@ -685,8 +748,11 @@ _STMT_(KDL.label "value" $ KDL.argWith $ void KDL.number <|> void KDL.string) decodeNode "foo" decoder config `shouldSatisfy` decodeErrorMsg- [ "At: foo #0"- , " Expected arg 'value' with type: number or string"+ [ "<input>:1:1:"+ , " • Expected arg 'value' with type: number or string"+ , " │"+ , "1 │ foo"+ , " │ ^^^" ] it "shows label on invalid arg" $ do@@ -695,8 +761,11 @@ _STMT_(KDL.label "value" $ KDL.arg @Int) decodeNode "foo" decoder config `shouldSatisfy` decodeErrorMsg- [ "At: foo #0 > arg 'value'"- , " Expected number, got: test"+ [ "<input>:1:5:"+ , " • Expected number, got: test"+ , " │"+ , "1 │ foo test"+ , " │ ^^^^" ] -- Most behaviors tested with `arg`@@ -739,8 +808,11 @@ _STMT_(_APOS_(KDL.argWith) ["VAL"] KDL.string) decodeNode "foo" decoder config `shouldSatisfy` decodeErrorMsg- [ "At: foo #0 > arg #0"- , " Expected annotation to be one of [\"VAL\"], got: test"+ [ "<input>:1:5:"+ , " • Expected annotation to be one of [\"VAL\"], got: test"+ , " │"+ , "1 │ foo (test)a"+ , " │ ^^^^^^^" ] it "supports backtracking annotations" $ do@@ -783,8 +855,11 @@ _STMT_(KDL.prop @Int "test") decodeNode "foo" decoder config `shouldSatisfy` decodeErrorMsg- [ "At: foo #0"- , " Expected prop 'test' with type: number"+ [ "<input>:1:1:"+ , " • Expected prop 'test' with type: number"+ , " │"+ , "1 │ foo 123"+ , " │ ^^^^^^^" ] it "fails if prop fails to parse" $ do@@ -793,8 +868,11 @@ _STMT_(KDL.prop @Int "hello") decodeNode "foo" decoder config `shouldSatisfy` decodeErrorMsg- [ "At: foo #0 > prop hello"- , " Expected number, got: world"+ [ "<input>:1:11:"+ , " • Expected number, got: world"+ , " │"+ , "1 │ foo hello=world"+ , " │ ^^^^^" ] it "fails if not all props are decoded" $ do@@ -803,8 +881,11 @@ _STMT_(KDL.prop @Int "a") decodeNode "foo" decoder config `shouldSatisfy` decodeErrorMsg- [ "At: foo #0"- , " Unexpected prop: b=2"+ [ "<input>:1:1:"+ , " • Unexpected prop: b=2"+ , " │"+ , "1 │ foo a=1 b=2"+ , " │ ^^^^^^^^^^^" ] it "shows expected types on missing prop" $ do@@ -813,8 +894,11 @@ _STMT_(KDL.propWith "test" $ void KDL.number <|> void KDL.string) decodeNode "foo" decoder config `shouldSatisfy` decodeErrorMsg- [ "At: foo #0"- , " Expected prop 'test' with type: number or string"+ [ "<input>:1:1:"+ , " • Expected prop 'test' with type: number or string"+ , " │"+ , "1 │ foo 123"+ , " │ ^^^^^^^" ] -- Most behaviors tested with `prop`@@ -857,8 +941,11 @@ _STMT_(_APOS_(KDL.propWith) "a" ["VAL"] KDL.number) decodeNode "foo" decoder config `shouldSatisfy` decodeErrorMsg- [ "At: foo #0 > prop a"- , " Expected annotation to be one of [\"VAL\"], got: test"+ [ "<input>:1:7:"+ , " • Expected annotation to be one of [\"VAL\"], got: test"+ , " │"+ , "1 │ foo a=(test)1"+ , " │ ^^^^^^^" ] describe "remainingProps" $ do@@ -884,8 +971,11 @@ _STMT_(KDL.remainingProps @Int) decodeNode "foo" decoder config `shouldSatisfy` decodeErrorMsg- [ "At: foo #0 > prop c"- , " Expected number, got: test"+ [ "<input>:1:19:"+ , " • Expected number, got: test"+ , " │"+ , "1 │ foo a=1 b=1 c=2 c=test"+ , " │ ^^^^" ] -- Most behaviors tested with `remainingProps`@@ -932,8 +1022,11 @@ _STMT_(_APOS_(KDL.remainingPropsWith) ["VAL"] KDL.number) decodeNode "foo" decoder config `shouldSatisfy` decodeErrorMsg- [ "At: foo #0 > prop b"- , " Expected annotation to be one of [\"VAL\"], got: test"+ [ "<input>:1:16:"+ , " • Expected annotation to be one of [\"VAL\"], got: test"+ , " │"+ , "1 │ foo a=(VAL)1 b=(test)2"+ , " │ ^^^^^^^" ] describe "children" $ do@@ -971,8 +1064,11 @@ _STMT_(KDL.children $ KDL.node @Node "bar") decodeNode "foo" decoder config `shouldSatisfy` decodeErrorMsg- [ "At: foo #0"- , " Unexpected node: asdf #0"+ [ "<input>:1:1:"+ , " • Unexpected node: asdf #0"+ , " │"+ , "1 │ foo { asdf; bar; }"+ , " │ ^^^" ] describe "ValueDecoder" $ do@@ -1003,8 +1099,11 @@ _STMT_(KDL.argAtWith "foo" KDL.string) KDL.decodeWith decoder config `shouldSatisfy` decodeErrorMsg- [ "At: foo #0 > arg #0"- , " Expected string, got: 1"+ [ "<input>:1:5:"+ , " • Expected string, got: 1"+ , " │"+ , "1 │ foo 1"+ , " │ ^" ] describe "number" $ do@@ -1020,8 +1119,11 @@ _STMT_(KDL.argAtWith "foo" KDL.number) KDL.decodeWith decoder config `shouldSatisfy` decodeErrorMsg- [ "At: foo #0 > arg #0"- , " Expected number, got: asdf"+ [ "<input>:1:5:"+ , " • Expected number, got: asdf"+ , " │"+ , "1 │ foo asdf"+ , " │ ^^^^" ] describe "bool" $ do@@ -1037,8 +1139,11 @@ _STMT_(KDL.argAtWith "foo" KDL.bool) KDL.decodeWith decoder config `shouldSatisfy` decodeErrorMsg- [ "At: foo #0 > arg #0"- , " Expected bool, got: 1"+ [ "<input>:1:5:"+ , " • Expected bool, got: 1"+ , " │"+ , "1 │ foo 1"+ , " │ ^" ] describe "null" $ do@@ -1054,8 +1159,11 @@ _STMT_(KDL.argAtWith "foo" KDL.null) KDL.decodeWith decoder config `shouldSatisfy` decodeErrorMsg- [ "At: foo #0 > arg #0"- , " Expected null, got: 1"+ [ "<input>:1:5:"+ , " • Expected null, got: 1"+ , " │"+ , "1 │ foo 1"+ , " │ ^" ] describe "Combinators" $ do@@ -1074,9 +1182,12 @@ _STMT_(KDL.nodeWith "foo" . KDL.many . KDL.argWith $ KDL.oneOf decodeVal) KDL.decodeWith decoder config `shouldSatisfy` decodeErrorMsg- [ "At: foo #0 > arg #1"- , " Expected bool, got: hello"- , " Expected number, got: hello"+ [ "<input>:1:9:"+ , " • Expected number, got: hello"+ , " • Expected bool, got: hello"+ , " │"+ , "1 │ foo 123 hello"+ , " │ ^^^^^" ] describe "option" $ do@@ -1120,8 +1231,11 @@ _STMT_(KDL.node @MyNode "foo") KDL.decodeWith decoder config `shouldSatisfy` decodeErrorMsg- [ "At: foo #0"- , " Invalid argument: 100"+ [ "<input>:1:1:"+ , " • Invalid argument: 100"+ , " │"+ , "1 │ foo 100"+ , " │ ^^^^^^^" ] it "decodes valid type ann" $ do@@ -1136,8 +1250,11 @@ _STMT_(KDL.node @MyNode "foo") KDL.decodeWith decoder config `shouldSatisfy` decodeErrorMsg- [ "At: foo #0"- , " Expected annotation to be one of [\"MyNode\"], got: bad"+ [ "<input>:1:1:"+ , " • Expected annotation to be one of [\"MyNode\"], got: bad"+ , " │"+ , "1 │ (bad)foo 1"+ , " │ ^^^^^^^^^^" ] newtype MyVal = MyVal Double@@ -1165,8 +1282,11 @@ _STMT_(KDL.argAt @MyVal "foo") KDL.decodeWith decoder config `shouldSatisfy` decodeErrorMsg- [ "At: foo #0 > arg #0"- , " Invalid value: 100.0"+ [ "<input>:1:5:"+ , " • Invalid value: 100.0"+ , " │"+ , "1 │ foo 100.0"+ , " │ ^^^^^" ] it "decodes valid type ann" $ do@@ -1181,6 +1301,9 @@ _STMT_(KDL.argAt @MyVal "foo") KDL.decodeWith decoder config `shouldSatisfy` decodeErrorMsg- [ "At: foo #0 > arg #0"- , " Expected annotation to be one of [\"MyVal\"], got: bad"+ [ "<input>:1:5:"+ , " • Expected annotation to be one of [\"MyVal\"], got: bad"+ , " │"+ , "1 │ foo (bad)1"+ , " │ ^^^^^^" ]
test/KDL/DecoderSpec.hs view
@@ -23,6 +23,14 @@ spec :: Spec spec = do+ spec_decodeWith+ spec_decodeFileWith+ spec_decodeDocWith+ spec_errorMessages+ spec_regressionTests++spec_decodeWith :: Spec+spec_decodeWith = do describe "decodeWith" $ do it "fails with helpful error if parsing fails" $ do let config = "foo 123=123"@@ -49,6 +57,8 @@ $ KDL.optional (KDL.prop @Text "a") KDL.decodeWith decoder config `shouldSatisfy` decodeErrorMsgSnapshot Nothing +spec_decodeFileWith :: Spec+spec_decodeFileWith = do describe "decodeFileWith" $ do it "fails with helpful error if parsing fails" $ do FixtureKdlFile file <- getFixture@@ -78,8 +88,45 @@ $ KDL.optional (KDL.prop @Text "a") KDL.decodeFileWith decoder file `shouldSatisfy` P.returns (decodeErrorMsgSnapshot (Just file)) - spec_regressionTests+spec_decodeDocWith :: Spec+spec_decodeDocWith = do+ describe "decodeDocWith" $ do+ it "fails with user-defined error" $ do+ let config = "foo -1"+ decoder =+ KDL.document . KDL.argAtWith "foo" $+ KDL.withDecoder KDL.number $ \x -> do+ when (x < 0) $ do+ KDL.failM $ "Got negative number: " <> (Text.pack . show) x+ pure x+ Right doc <- pure $ KDL.parseWith KDL.def config+ KDL.decodeDocWith decoder doc+ `shouldSatisfy` decodeErrorMsgSnapshot Nothing + it "shows context in deeply nested error" $ do+ let config = "foo; foo { bar { baz; baz; baz; baz a=1; }; }"+ decoder =+ KDL.document+ . (KDL.many . KDL.nodeWith "foo" . KDL.children)+ . (KDL.many . KDL.nodeWith "bar" . KDL.children)+ . (KDL.many . KDL.nodeWith "baz")+ $ KDL.optional (KDL.prop @Text "a")+ Right doc <- pure $ KDL.parseWith KDL.def config+ KDL.decodeDocWith decoder doc+ `shouldSatisfy` decodeErrorMsgSnapshot Nothing++spec_errorMessages :: Spec+spec_errorMessages = do+ describe "Error messages" $ do+ it "only shows first line when context spans multiple lines" $ do+ let config = "foo \\\n 1"+ decoder =+ KDL.document . KDL.nodeWith "foo" $ do+ _ <- KDL.arg @Int+ _ <- KDL.children $ KDL.argAt @Int "bar"+ pure ()+ KDL.decodeWith decoder config `shouldSatisfy` decodeErrorMsgSnapshot Nothing+ newtype FixtureKdlFile = FixtureKdlFile FilePath instance Fixture FixtureKdlFile where@@ -110,6 +157,9 @@ KDL.fail "Invalid username" KDL.decodeWith decoder config `shouldSatisfy` decodeErrorMsg- [ "At: user #1 > arg #0"- , " Invalid username"+ [ "<input>:1:30:"+ , " • Invalid username"+ , " │"+ , "1 │ user a { foo { bar } }; user a1"+ , " │ ^^" ]
test/KDL/__snapshots__/DecoderSpec.snap.md view
@@ -1,9 +1,9 @@ # test/KDL/DecoderSpec.hs -## decodeFileWith / fails with helpful error if parsing fails+## decodeWith ≫ fails with helpful error if parsing fails ```-test_config.kdl:1:8:+1:8: | 1 | foo 123=123 | ^@@ -11,26 +11,30 @@ expecting children block, decimal point, end of node, exponent, or node prop or arg ``` -## decodeFileWith / fails with user-defined error+## decodeWith ≫ fails with user-defined error ```-Failed to decode test_config.kdl:-At: foo #0 > arg #0- Got negative number: -1.0+<input>:1:5:+ • Got negative number: -1.0+ │+1 │ foo -1+ │ ^^ ``` -## decodeFileWith / shows context in deeply nested error+## decodeWith ≫ shows context in deeply nested error ```-Failed to decode test_config.kdl:-At: foo #1 > bar #0 > baz #3 > prop a- Expected string, got: 1+<input>:1:39:+ • Expected string, got: 1+ │+1 │ foo; foo { bar { baz; baz; baz; baz a=1; }; }+ │ ^ ``` -## decodeWith / fails with helpful error if parsing fails+## decodeFileWith ≫ fails with helpful error if parsing fails ```-1:8:+test_config.kdl:1:8: | 1 | foo 123=123 | ^@@ -38,16 +42,46 @@ expecting children block, decimal point, end of node, exponent, or node prop or arg ``` -## decodeWith / fails with user-defined error+## decodeFileWith ≫ fails with user-defined error ```+test_config.kdl:1:5:+ • Got negative number: -1.0+ │+1 │ foo -1+ │ ^^+```++## decodeFileWith ≫ shows context in deeply nested error++```+test_config.kdl:1:39:+ • Expected string, got: 1+ │+1 │ foo; foo { bar { baz; baz; baz; baz a=1; }; }+ │ ^+```++## decodeDocWith ≫ fails with user-defined error++``` At: foo #0 > arg #0- Got negative number: -1.0+└─ Got negative number: -1.0 ``` -## decodeWith / shows context in deeply nested error+## decodeDocWith ≫ shows context in deeply nested error ``` At: foo #1 > bar #0 > baz #3 > prop a- Expected string, got: 1+└─ Expected string, got: 1+```++## Error messages ≫ only shows first line when context spans multiple lines++```+<input>:1:1:+ • Expected node: bar+ │+1 │ foo \+ │ ^^^^^ ```
test/KDL/__snapshots__/ParserSpec.snap.md view
@@ -1,17 +1,6 @@ # test/KDL/ParserSpec.hs -## parse / error messages / Unquoted numeric prop name--```-1:8:- |-1 | foo 123=123- | ^-unexpected '='-expecting children block, decimal point, end of node, exponent, or node prop or arg-```--## parse / parses a KDL document+## parse ≫ parses a KDL document ```haskell NodeList@@ -157,7 +146,18 @@ } ``` -## parseWith / parses a KDL document with spans+## parse ≫ error messages ≫ Unquoted numeric prop name++```+1:8:+ |+1 | foo 123=123+ | ^+unexpected '='+expecting children block, decimal point, end of node, exponent, or node prop or arg+```++## parseWith ≫ parses a KDL document with spans ```haskell NodeList
test/KDL/__snapshots__/RenderSpec.snap.md view
@@ -1,6 +1,6 @@ # test/KDL/RenderSpec.hs -## render / default formatting / renders correctly+## render ≫ default formatting ≫ renders correctly ``` (Foo)foo (Foo)123.0 a=(Foo)123.0 test b=test {