packages feed

settei-optparse-applicative 0.1.0.0 → 0.2.0.0

raw patch · 4 files changed

+88/−49 lines, 4 filesdep ~setteidep ~settei-envdep ~settei-optparse-applicativePVP ok

version bump matches the API change (PVP)

Dependency ranges changed: settei, settei-env, settei-optparse-applicative

API changes (from Hackage documentation)

- Settei.Optparse: NoExplain :: ExplainMode
- Settei.Optparse: [explainMode] :: SetteiOptions -> !ExplainMode
- Settei.Optparse: data ExplainMode
- Settei.Optparse: explainModeOptions :: Parser ExplainMode
- Settei.Optparse: explainModeOptionsWith :: Mod FlagFields ExplainMode -> Mod FlagFields ExplainMode -> Parser ExplainMode
- Settei.Optparse: instance GHC.Classes.Eq Settei.Optparse.ExplainMode
- Settei.Optparse: instance GHC.Classes.Ord Settei.Optparse.ExplainMode
- Settei.Optparse: instance GHC.Internal.Generics.Generic Settei.Optparse.ExplainMode
- Settei.Optparse: instance GHC.Internal.Show.Show Settei.Optparse.ExplainMode
+ Settei.Optparse: CheckConfig :: DiagnosticMode
+ Settei.Optparse: DescribeConfigJson :: DiagnosticMode
+ Settei.Optparse: DescribeConfigText :: DiagnosticMode
+ Settei.Optparse: NoDiagnostic :: DiagnosticMode
+ Settei.Optparse: [diagnosticMode] :: SetteiOptions -> !DiagnosticMode
+ Settei.Optparse: data DiagnosticMode
+ Settei.Optparse: diagnosticModeOptions :: Parser DiagnosticMode
+ Settei.Optparse: instance GHC.Classes.Eq Settei.Optparse.DiagnosticMode
+ Settei.Optparse: instance GHC.Classes.Ord Settei.Optparse.DiagnosticMode
+ Settei.Optparse: instance GHC.Internal.Generics.Generic Settei.Optparse.DiagnosticMode
+ Settei.Optparse: instance GHC.Internal.Show.Show Settei.Optparse.DiagnosticMode
+ Settei.Optparse: resolutionDiagnostic :: DiagnosticMode -> ResolveResult a -> Maybe Text
+ Settei.Optparse: schemaDiagnostic :: DiagnosticMode -> Schema -> Maybe Text
- Settei.Optparse: ExplainJson :: ExplainMode
+ Settei.Optparse: ExplainJson :: DiagnosticMode
- Settei.Optparse: ExplainText :: ExplainMode
+ Settei.Optparse: ExplainText :: DiagnosticMode
- Settei.Optparse: SetteiOptions :: ![FilePath] -> ![CliOverride] -> !ExplainMode -> SetteiOptions
+ Settei.Optparse: SetteiOptions :: ![FilePath] -> ![CliOverride] -> !DiagnosticMode -> SetteiOptions

Files

CHANGELOG.md view
@@ -1,5 +1,12 @@ # Changelog for settei-optparse-applicative +## 0.2.0.0 — 2026-07-19++- Breaking: replace `ExplainMode` and its parsers with `DiagnosticMode`, which also+  supports `--check-config`, `--describe-config`, and `--describe-config-json`.+- Add `schemaDiagnostic` and `resolutionDiagnostic` helpers for the source-free and+  post-resolution diagnostic paths.+ ## 0.1.0.0 — 2026-07-18  - Initial experimental release.
settei-optparse-applicative.cabal view
@@ -1,6 +1,6 @@ cabal-version:   3.8 name:            settei-optparse-applicative-version:         0.1.0.0+version:         0.2.0.0 synopsis:        optparse-applicative sources for Settei description:   Parse generic and named command-line overrides into provenance-aware Settei@@ -40,7 +40,7 @@     , containers            >=0.6.8   && <0.8     , generic-lens          >=2.2     && <2.4     , optparse-applicative  >=0.19    && <0.20-    , settei                ==0.1.0.0+    , settei                ==0.2.0.0     , text                  >=2.1     && <2.2  test-suite settei-optparse-applicative-tests@@ -54,9 +54,9 @@     , containers                   >=0.6.8   && <0.8     , generic-lens                 >=2.2     && <2.4     , optparse-applicative         >=0.19    && <0.20-    , settei                       ==0.1.0.0-    , settei-env                   ==0.1.0.0-    , settei-optparse-applicative  ==0.1.0.0+    , settei                       ==0.2.0.0+    , settei-env                   ==0.2.0.0+    , settei-optparse-applicative  ==0.2.0.0     , tasty                        >=1.5     && <1.6     , tasty-hunit                  >=0.10.2  && <0.11     , text                         >=2.1     && <2.2
src/Settei/Optparse.hs view
@@ -3,7 +3,7 @@ -- Description: optparse-applicative parsers that produce ordered Settei sources. module Settei.Optparse   ( CliOverride,-    ExplainMode (..),+    DiagnosticMode (..),     SetteiOptions (..),     cliOverride,     cliOverrideKey,@@ -12,11 +12,12 @@     cliSources,     configPathOptions,     configPathOptionsWith,-    explainModeOptions,-    explainModeOptionsWith,+    diagnosticModeOptions,     namedOption,     overrideOptions,     overrideOptionsWith,+    resolutionDiagnostic,+    schemaDiagnostic,     setteiOptions,   ) where@@ -26,7 +27,7 @@ import Data.List.NonEmpty qualified as NonEmpty import Data.Map.Strict qualified as Map import Data.Text qualified as Text-import Options.Applicative (FlagFields, Mod, OptionFields, Parser)+import Options.Applicative (Mod, OptionFields, Parser) import Options.Applicative qualified as Options import Settei import Settei.Prelude@@ -42,15 +43,21 @@   }   deriving stock (Generic, Eq) --- | Whether an application should print a configuration explanation.-data ExplainMode = NoExplain | ExplainText | ExplainJson+-- | Which configuration diagnostic, if any, an application should perform.+data DiagnosticMode+  = NoDiagnostic+  | ExplainText+  | ExplainJson+  | CheckConfig+  | DescribeConfigText+  | DescribeConfigJson   deriving stock (Generic, Eq, Ord, Show)  -- | Reusable configuration and diagnostic command-line options. data SetteiOptions = SetteiOptions   { configPaths :: ![FilePath],     overrides :: ![CliOverride],-    explainMode :: !ExplainMode+    diagnosticMode :: !DiagnosticMode   }   deriving stock (Generic, Eq) @@ -61,7 +68,7 @@   deriving stock (Generic, Eq)  newtype DiagnosticOptions = DiagnosticOptions-  { explainMode :: ExplainMode+  { diagnosticMode :: DiagnosticMode   }   deriving stock (Generic, Eq) @@ -128,20 +135,31 @@ configPathOptionsWith :: Mod OptionFields FilePath -> Parser [FilePath] configPathOptionsWith modifiers = Applicative.many (Options.strOption modifiers) --- | Parse the default mutually exclusive explanation flags.-explainModeOptions :: Parser ExplainMode-explainModeOptions =-  explainModeOptionsWith-    (Options.long "explain-config" <> Options.help "Explain the resolved configuration as text")-    (Options.long "explain-config-json" <> Options.help "Explain the resolved configuration as JSON")+-- | Parse the default mutually exclusive configuration diagnostic flags.+diagnosticModeOptions :: Parser DiagnosticMode+diagnosticModeOptions =+  Options.flag' ExplainText (Options.long "explain-config" <> Options.help "Explain the resolved configuration as text")+    Applicative.<|> Options.flag' ExplainJson (Options.long "explain-config-json" <> Options.help "Explain the resolved configuration as JSON")+    Applicative.<|> Options.flag' CheckConfig (Options.long "check-config" <> Options.help "Validate configuration and exit")+    Applicative.<|> Options.flag' DescribeConfigText (Options.long "describe-config" <> Options.help "Print the static configuration schema")+    Applicative.<|> Options.flag' DescribeConfigJson (Options.long "describe-config-json" <> Options.help "Print the static configuration schema as JSON")+    Applicative.<|> pure NoDiagnostic --- | Parse caller-named mutually exclusive text and JSON explanation flags.-explainModeOptionsWith :: Mod FlagFields ExplainMode -> Mod FlagFields ExplainMode -> Parser ExplainMode-explainModeOptionsWith textModifiers jsonModifiers =-  Options.flag' ExplainText textModifiers-    Applicative.<|> Options.flag' ExplainJson jsonModifiers-    Applicative.<|> pure NoExplain+-- | Render a diagnostic that must not load configuration sources.+schemaDiagnostic :: DiagnosticMode -> Schema -> Maybe Text+schemaDiagnostic mode schema = case mode of+  DescribeConfigText -> Just (renderSchemaText schema)+  DescribeConfigJson -> Just (renderSchemaJson schema <> "\n")+  _ -> Nothing +-- | Render a diagnostic available after resolution.+resolutionDiagnostic :: DiagnosticMode -> ResolveResult a -> Maybe Text+resolutionDiagnostic mode result = case mode of+  ExplainText -> Just (renderResolutionText (result ^. #report))+  ExplainJson -> Just (renderResolutionJson (result ^. #report) <> "\n")+  CheckConfig -> Just "configuration valid\n"+  _ -> Nothing+ -- | Parse the reusable Configuration and Diagnostics option groups. setteiOptions :: Parser SetteiOptions setteiOptions = assemble <$> configurationOptions <*> diagnosticOptions@@ -150,7 +168,7 @@       SetteiOptions         { configPaths = configuration ^. #configPaths,           overrides = configuration ^. #overrides,-          explainMode = diagnostics ^. #explainMode+          diagnosticMode = diagnostics ^. #diagnosticMode         }  configurationOptions :: Parser ConfigurationOptions@@ -163,7 +181,7 @@ diagnosticOptions =   Options.parserOptionGroup     "Diagnostics"-    (DiagnosticOptions <$> explainModeOptions)+    (DiagnosticOptions <$> diagnosticModeOptions)  overrideReader :: Options.ReadM CliOverride overrideReader = Options.eitherReader $ \input ->
test/Settei/OptparseTest.hs view
@@ -19,14 +19,13 @@     [ testCase "final repeated --set wins with shadow trace" $ do         overrides <- expectParse overrideOptions ["--set", "service.port=9000", "--set", "service.port=9001"]         environment <- environmentPort 8000-        result <--          expectResolution-            ( resolve+        let result =+              resolve                 defaultResolveOptions                 ([portSource "built-in" BuiltInSource 6000, portSource "file" (FileSource "memory") 7000, environment] <> cliSources "arguments" overrides)                 (required portSetting)-            )-        result ^. #value @?= 9001+        value <- expectResolution result+        value @?= 9001         case result ^. #report . #nodes . at servicePort of           Just node -> do             node ^. #origin . _Just . #annotations . at "command-line.occurrence" @?= Just "2"@@ -39,7 +38,7 @@       testCase "malformed final CLI value does not fall back" $ do         overrides <- expectParse overrideOptions ["--set", "service.port=9000", "--set", "service.port=broken"]         environment <- environmentPort 8000-        case resolve defaultResolveOptions (environment : cliSources "arguments" overrides) (required portSetting) of+        case (resolve defaultResolveOptions (environment : cliSources "arguments" overrides) (required portSetting)) ^. #answer of           Left errors -> case NonEmpty.toList errors of             [DecodeError problem] -> do               problem ^. #origin . #name @?= "arguments --set #2"@@ -48,7 +47,8 @@           Right _ -> fail "expected the malformed final override to fail",       testCase "secret override spelling omits the value" $ do         overrides <- expectParse overrideOptions ["--set", "database.password=" <> Text.unpack secretSentinel]-        result <- expectResolution (resolve defaultResolveOptions (cliSources "arguments" overrides) (required passwordSetting))+        let result = resolve defaultResolveOptions (cliSources "arguments" overrides) (required passwordSetting)+        _ <- expectResolution result         let textOutput = renderResolutionText (result ^. #report)             jsonOutput = renderResolutionJson (result ^. #report)         assertBool "secret reached text output" (not (secretSentinel `Text.isInfixOf` textOutput))@@ -64,8 +64,9 @@         case input of           Nothing -> fail "expected the named option source"           Just sourceValue -> do-            result <- expectResolution (resolve defaultResolveOptions [sourceValue] (required portSetting))-            result ^. #value @?= 8080,+            let result = resolve defaultResolveOptions [sourceValue] (required portSetting)+            value <- expectResolution result+            value @?= 8080,       testCase "setteiOptions parses grouped paths, overrides, and diagnostics" $ do         parsed <-           expectParse@@ -80,10 +81,19 @@             ]         parsed ^. #configPaths @?= ["base.yaml", "local.yaml"]         length (parsed ^. #overrides) @?= 1-        parsed ^. #explainMode @?= ExplainJson,-      testCase "explanation modes are mutually exclusive" $ do-        let parsed = parse explainModeOptions ["--explain-config", "--explain-config-json"]-        assertBool "both explanation modes unexpectedly parsed" (Options.getParseResult parsed == Nothing),+        parsed ^. #diagnosticMode @?= ExplainJson,+      testCase "diagnostic modes default and are mutually exclusive" $ do+        defaulted <- expectParse diagnosticModeOptions []+        defaulted @?= NoDiagnostic+        let parsed = parse diagnosticModeOptions ["--explain-config", "--explain-config-json"]+        assertBool "both diagnostic modes unexpectedly parsed" (Options.getParseResult parsed == Nothing),+      testCase "diagnostic helpers render the requested outputs" $ do+        let schema = describe (required portSetting)+            result = resolve defaultResolveOptions [portSource "test" BuiltInSource 8080] (required portSetting)+        assertBool "text schema diagnostic missing" (schemaDiagnostic DescribeConfigText schema /= Nothing)+        assertBool "JSON schema diagnostic missing" (schemaDiagnostic DescribeConfigJson schema /= Nothing)+        resolutionDiagnostic CheckConfig result @?= Just "configuration valid\n"+        resolutionDiagnostic NoDiagnostic result @?= Nothing,       testCase "help groups configuration and diagnostics by intent" $ do         let parserInfo = Options.info (setteiOptions Options.<**> Options.helper) Options.fullDesc         case Options.execParserPure Options.defaultPrefs parserInfo ["--help"] of@@ -104,19 +114,23 @@     Nothing -> fail "expected command-line parsing to succeed"     Just value -> pure value -expectResolution :: Either (NonEmpty ConfigError) a -> IO a-expectResolution = \case+expectResolution :: ResolveResult a -> IO a+expectResolution result = case result ^. #answer of   Left _ -> fail "expected configuration resolution to succeed"   Right value -> pure value  environmentPort :: Int -> IO Source-environmentPort value =-  case envSource-    "environment"-    [binding (EnvName "SERVICE_PORT") servicePort]-    (envSnapshot [("SERVICE_PORT", Text.pack (show value))]) of-    Left _ -> fail "expected a valid environment source"-    Right sourceValue -> pure sourceValue+environmentPort value = do+  validated <-+    either+      (fail . Text.unpack . renderEnvErrorsText)+      pure+      (bindings [binding (EnvName "SERVICE_PORT") servicePort])+  pure+    ( environmentSource+        validated+        (envSnapshot [("SERVICE_PORT", Text.pack (show value))])+    )  portSource :: Text -> SourceKind -> Int -> Source portSource name kind value =