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 +7/−0
- settei-optparse-applicative.cabal +5/−5
- src/Settei/Optparse.hs +40/−22
- test/Settei/OptparseTest.hs +36/−22
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 =