packages feed

settei-kdl 0.1.0.0 → 0.2.0.0

raw patch · 5 files changed

+200/−18 lines, 5 filesdep +scientificdep ~setteidep ~settei-kdlPVP ok

version bump matches the API change (PVP)

Dependencies added: scientific

Dependency ranges changed: settei, settei-kdl

API changes (from Hackage documentation)

+ Settei.Kdl: renderKdlErrorText :: KdlSourceError -> Text
+ Settei.Kdl: renderKdlErrorsText :: NonEmpty KdlSourceError -> Text

Files

CHANGELOG.md view
@@ -1,7 +1,14 @@ # Changelog for settei-kdl -## 0.1.0.0 — 2026-07-18+## 0.2.0.0 — 2026-07-19 +- Add `renderKdlErrorText` and `renderKdlErrorsText` for stable, operator-readable,+  secret-safe adapter diagnostics, including related spans when present.++## 0.1.0.0 — 2026-07-19+ - Initial experimental release. - Add the canonical KDL v2 mapping with exact spans, deterministic cardinality, explicit   ambiguity errors, and mounted-file Kubernetes annotations.+- Reject numeric values whose base-10 exponent magnitude exceeds 4096 instead of+  attempting an unbounded exact conversion at load time.
settei-kdl.cabal view
@@ -1,6 +1,6 @@ cabal-version:      3.8 name:               settei-kdl-version:            0.1.0.0+version:            0.2.0.0 synopsis:           KDL v2 sources for Settei description:   Translate a canonical KDL v2 document into a provenance-aware Settei@@ -42,7 +42,8 @@     , containers    >=0.6.8   && <0.8     , generic-lens  >=2.2     && <2.4     , kdl-hs        >=1.1.1   && <1.2-    , settei        ==0.1.0.0+    , scientific    >=0.3.7   && <0.4+    , settei        ==0.2.0.0     , text          >=2.1     && <2.2  test-suite settei-kdl-tests@@ -60,8 +61,8 @@     , base          >=4.21    && <5     , containers    >=0.6.8   && <0.8     , generic-lens  >=2.2     && <2.4-    , settei        ==0.1.0.0-    , settei-kdl    ==0.1.0.0+    , settei        ==0.2.0.0+    , settei-kdl    ==0.2.0.0     , tasty         >=1.5     && <1.6     , tasty-hunit   >=0.10.2  && <0.11     , text          >=2.1     && <2.2
src/Settei/Kdl.hs view
@@ -27,6 +27,8 @@     kdlSpanEndLine,     kdlSpanLine,     readKdlSource,+    renderKdlErrorText,+    renderKdlErrorsText,     withKdlSourcePath,   ) where@@ -38,6 +40,7 @@ import Data.List.NonEmpty qualified as NonEmpty import Data.Map.Strict qualified as Map import Data.Maybe (listToMaybe)+import Data.Scientific (base10Exponent) import Data.Text qualified as Text import Data.Text.IO qualified as TextIO import Data.Text.Read qualified as TextRead@@ -164,6 +167,49 @@ kdlSpanEndColumn :: KdlSpan -> Int kdlSpanEndColumn value = value ^. #endColumn +-- | Render one KDL input failure as a single operator-readable line.+--+-- The line leads with the stable source name, then a parenthetical of the+-- colon-joined available pieces (path, primary span start line, start column),+-- then the structural context and fixed message. When a second trustworthy span+-- exists it is appended as @; also at LINE:COLUMN@. No raw configuration value+-- can appear because 'KdlSourceError' retains none.+renderKdlErrorText :: KdlSourceError -> Text+renderKdlErrorText problem =+  problem ^. #name+    <> locationText+    <> " at "+    <> problem ^. #context+    <> ": "+    <> problem ^. #message+    <> relatedText+  where+    locationText = case locationPieces of+      [] -> ""+      pieces -> " (" <> Text.intercalate ":" pieces <> ")"+    locationPieces =+      maybe [] (pure . Text.pack) (problem ^. #path)+        <> maybe [] spanPieces (problem ^. #location)+    spanPieces spanValue =+      [ Text.pack (show (spanValue ^. #line)),+        Text.pack (show (spanValue ^. #column))+      ]+    relatedText =+      maybe+        ""+        ( \spanValue ->+            "; also at "+              <> Text.pack (show (spanValue ^. #line))+              <> ":"+              <> Text.pack (show (spanValue ^. #column))+        )+        (problem ^. #relatedLocation)++-- | Render every KDL input failure, one line per problem, matching+-- 'Settei.Render.renderErrorsText' in shape and trailing newline.+renderKdlErrorsText :: NonEmpty KdlSourceError -> Text+renderKdlErrorsText = Text.unlines . fmap renderKdlErrorText . NonEmpty.toList+ -- | Decode one KDL v2 document into a Settei file source. -- -- Type annotations, non-finite numbers, duplicate properties, invalid key segments, and@@ -407,12 +453,33 @@       Just (child : _) <- [childGroups ^. at name]     ] +-- | Largest absolute base-10 exponent accepted for a numeric value.+--+-- 'toRational' materializes @10 ^ exponent@ as an exact 'Integer', so cost+-- grows with the exponent's magnitude rather than the input's length. This+-- adapter-local bound leaves core 'RawNumber' values unbounded.+maximumScalarExponent :: Int+maximumScalarExponent = 4096+ translateValue :: KdlSourceOptions -> [Text] -> KDL.Value -> Either KdlSourceError RawValue translateValue options path value = do   rejectValueAnnotation options path value   case valueData value of     KDL.String text -> Right (RawText text)-    KDL.Number number -> Right (RawNumber (toRational number))+    KDL.Number number+      | scalarExponent < negate maximumScalarExponent || scalarExponent > maximumScalarExponent ->+          Left+            ( kdlError+                options+                KdlUnsupportedValue+                (Just (valueLocation value))+                Nothing+                path+                "numeric value exponent is out of the supported range"+            )+      | otherwise -> Right (RawNumber (toRational number))+      where+        scalarExponent = base10Exponent number     KDL.Bool boolean -> Right (RawBool boolean)     KDL.Null -> Right RawNull     KDL.Inf -> unsupported
test/Settei/KdlCharacterizationTest.hs view
@@ -7,7 +7,7 @@ import Settei import Settei.Kdl import Settei.Prelude-import Test.Tasty (TestTree, testGroup)+import Test.Tasty (TestTree, localOption, mkTimeout, testGroup) import Test.Tasty.HUnit (assertBool, testCase, (@?=))  tests :: TestTree@@ -56,7 +56,31 @@         assertBool "syntax error omitted its column" (maybe False ((> 0) . kdlSpanColumn) (kdlErrorSpan problem))         assertBool           "syntax error retained the source excerpt"-          (not (sentinel `Text.isInfixOf` Text.pack (show problem)))+          (not (sentinel `Text.isInfixOf` Text.pack (show problem))),+      localOption (mkTimeout 10000000) $+        testGroup+          "bounded numeric value conversion"+          [ testCase "huge positive exponents are rejected quickly" $ do+              problem <- expectError "huge 1e1000000000\n"+              expectExponentError "$.huge" problem,+            testCase "huge negative exponents are rejected quickly" $ do+              problem <- expectError "tiny 1e-1000000000\n"+              expectExponentError "$.tiny" problem,+            testCase "positive boundary exponents convert exactly" $ do+              input <- expectSource "edge 1e4096\n"+              expectValue "edge" input (RawNumber (10 ^ (4096 :: Integer) % 1)),+            testCase "negative boundary exponents convert exactly" $ do+              input <- expectSource "edge 1e-4096\n"+              expectValue "edge" input (RawNumber (1 % 10 ^ (4096 :: Integer))),+            testCase "exponents immediately above the bound are rejected" $ do+              problem <- expectError "over 1e4097\n"+              expectExponentError "$.over" problem,+            testCase "ordinary decimals and hex values remain exact" $ do+              input <- expectSource "rate 1.5e-3\nmask 0x1A\ncoefficient 12345678901234567890\n"+              expectValue "rate" input (RawNumber (3 % 2000))+              expectValue "mask" input (RawNumber 26)+              expectValue "coefficient" input (RawNumber 12345678901234567890)+          ]     ]  sourceOptions :: KdlSourceOptions@@ -76,6 +100,13 @@ expectValue keyText input expected = case lookupSource (validKey keyText) input of   Right (Just found) -> assertBool "unexpected KDL raw value" (candidateValue found == expected)   _ -> fail "expected KDL candidate"++expectExponentError :: Text -> KdlSourceError -> IO ()+expectExponentError expectedContext problem = do+  kdlErrorCategory problem @?= KdlUnsupportedValue+  fmap kdlSpanLine (kdlErrorSpan problem) @?= Just 1+  kdlErrorContext problem @?= expectedContext+  kdlErrorMessage problem @?= "numeric value exponent is out of the supported range"  validKey :: Text -> Key validKey value = either (error . show) id (parseKey value)
test/Settei/KdlTest.hs view
@@ -15,7 +15,8 @@ tests =   testGroup     "Settei.Kdl"-    [ testCase "nested nodes become segmented keys with exact spans" $ do+    [ errorRenderingTests,+      testCase "nested nodes become segmented keys with exact spans" $ do         input <- expectSource "runtime {\n  environment \"production\"\n}\nservice {\n  http {\n    host \"0.0.0.0\"\n    port 8080\n  }\n}\n"         host <- expectCandidate serviceHttpHost input         assertBool "host did not remain text" (candidateValue host == RawText "0.0.0.0")@@ -65,14 +66,13 @@       testCase "higher KDL overrides leaves and replaces arrays through core" $ do         low <- expectNamedSource "low" "service { host \"old.internal\"; port 7000; names \"one\" \"two\"; }\n"         high <- expectNamedSource "high" "service { port 9000; names \"three\" \"four\"; }\n"-        result <--          expectResolution-            ( resolve+        let result =+              resolve                 defaultResolveOptions                 [low, high]                 ((,,) <$> required hostSetting <*> required portSetting <*> required namesSetting)-            )-        result ^. #value @?= ("old.internal", 9000, ["three", "four"])+        value <- expectResolution result+        value @?= ("old.internal", 9000, ["three", "four"])         case result ^. #report . #nodes . at servicePort of           Just node -> do             node ^. #origin . _Just . #name @?= "high"@@ -82,7 +82,8 @@         let reference = kubernetesRef SecretObject (Just "production") "database-config" (Just "config.kdl")             options = fromKubernetesMountedFile reference sourceOptions         input <- expectSourceWith options ("database { password \"" <> secretSentinel <> "\"; }\n")-        result <- expectResolution (resolve defaultResolveOptions [input] (required passwordSetting))+        let result = resolve defaultResolveOptions [input] (required passwordSetting)+        _ <- expectResolution result         let textOutput = renderResolutionText (result ^. #report)             jsonOutput = renderResolutionJson (result ^. #report)         assertBool "secret reached text output" (not (secretSentinel `Text.isInfixOf` textOutput))@@ -106,6 +107,78 @@         host ^? to candidateOrigin . #location . _Just . #path @?= Just (Text.pack fixturePath)     ] +errorRenderingTests :: TestTree+errorRenderingTests =+  testGroup+    "error rendering"+    [ rendererCase+        "syntax"+        KdlSyntaxError+        "service { broken [\n"+        "application (application.kdl:1:18) at $: invalid KDL v2 syntax",+      testCase "IO errors render a stable located prefix" $ do+        result <- readKdlSource (kdlSourceOptions "missing") "test/fixtures/does-not-exist.kdl"+        case result of+          Left errors -> do+            let problem = NonEmpty.head errors+            kdlErrorCategory problem @?= KdlIoError+            assertBool+              "IO renderer omitted its stable prefix"+              ( "missing (test/fixtures/does-not-exist.kdl) at $: "+                  `Text.isPrefixOf` renderKdlErrorText problem+              )+          Right _ -> fail "expected missing KDL file to fail",+      rendererCase+        "duplicate property"+        KdlDuplicateProperty+        "service port=8000 port=9000\n"+        "application (application.kdl:1:19) at $.service.port: duplicate property; also at 1:9",+      rendererCase+        "invalid name"+        KdlInvalidName+        "\"service.port\" 8080\n"+        "application (application.kdl:1:1) at $.service.port: KDL names must be non-empty Settei key segments without dots",+      rendererCase+        "unsupported annotation"+        KdlUnsupportedAnnotation+        "(record)service { port 8080 }\n"+        "application (application.kdl:1:1) at $.service: KDL node type annotations are not supported",+      rendererCase+        "unsupported value"+        KdlUnsupportedValue+        "ratio #inf\n"+        "application (application.kdl:1:7) at $.ratio: non-finite KDL numbers are not supported",+      rendererCase+        "mixed node shape"+        KdlMixedNodeShape+        "service \"api\" port=8080\n"+        "application (application.kdl:1:1) at $.service: positional arguments cannot be combined with properties or children",+      rendererCase+        "property-child collision"+        KdlPropertyChildCollision+        "service port=7000 {\n  port 8000\n}\n"+        "application (application.kdl:1:9) at $.service.port: a property and child use the same field name; also at 2:3",+      testCase "a missing path omits it while retaining the primary span" $ do+        problem <-+          expectErrorWith+            (kdlSourceOptions "memory")+            "service \"api\" port=8080\n"+        assertBool+          "position-only KDL renderer omitted its location"+          ("memory (1:1)" `Text.isPrefixOf` renderKdlErrorText problem),+      testCase "plural rendering is singular rendering plus a newline" $ do+        problem <- expectError "service \"api\" port=8080\n"+        renderKdlErrorsText (NonEmpty.singleton problem)+          @?= renderKdlErrorText problem <> "\n"+    ]++rendererCase :: String -> KdlErrorCategory -> Text -> Text -> TestTree+rendererCase label expectedCategory input expected =+  testCase label $ do+    problem <- expectError input+    kdlErrorCategory problem @?= expectedCategory+    renderKdlErrorText problem @?= expected+ sourceOptions :: KdlSourceOptions sourceOptions = withKdlSourcePath "application.kdl" (kdlSourceOptions "application") @@ -122,7 +195,10 @@   Right sourceValue -> pure sourceValue  expectError :: Text -> IO KdlSourceError-expectError input = case decodeKdlSource sourceOptions input of+expectError = expectErrorWith sourceOptions++expectErrorWith :: KdlSourceOptions -> Text -> IO KdlSourceError+expectErrorWith options input = case decodeKdlSource options input of   Left errors -> pure (NonEmpty.head errors)   Right _ -> fail "expected KDL decoding to fail" @@ -136,8 +212,8 @@   found <- expectCandidate key input   assertBool "unexpected KDL raw value" (candidateValue found == expected) -expectResolution :: Either (NonEmpty ConfigError) a -> IO a-expectResolution = \case+expectResolution :: ResolveResult a -> IO a+expectResolution result = case result ^. #answer of   Left errors -> fail (Text.unpack (renderErrorsText errors))   Right value -> pure value