packages feed

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 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
test/KDL/Decoder/SharedSpec/Template.hs view
@@ -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 {