mdoc-0.1.0.0: src/OptEnvConf/Mdoc.hs
-- |
--
-- Module : OptEnvConf.Mdoc
-- Copyright : (c) 2026 Patrick Brisbin
-- License : AGPL-3
-- Maintainer : pbrisbin@gmail.com
-- Stability : experimental
-- Portability : POSIX
module OptEnvConf.Mdoc
( addToMan1
, addToMan5
) where
import Mdoc.Prelude
import Autodocodec.Schema (JSONSchema (..))
import Data.Aeson qualified as Aeson
import Data.List (intercalate)
import Mdoc.Gen.Argument
import Mdoc.Gen.Config
import Mdoc.Gen.Described
import Mdoc.Gen.EnvVar
import Mdoc.Gen.Flag
import Mdoc.Gen.Man1
import Mdoc.Gen.Man5
import Mdoc.Gen.Option
import Mdoc.Gen.Optionality
import OptEnvConf (ConfDoc (..), EnvDoc (..), OptDoc (..), Parser)
import OptEnvConf.Args (Dashed (..))
import OptEnvConf.Doc
( AnyDocs (..)
, parserConfDocs
, parserEnvDocs
, parserOptDocs
)
addToMan1 :: Parser a -> Man1 -> Man1
addToMan1 p base =
base
{ switches = base.switches <> switches
, options = base.options <> options
, arguments = base.arguments <> getParserArgs p
, environment = base.environment <> getParserEnvs p
}
where
(switches, options) = partitionDescribed $ getParserOpts p
partitionDescribed :: [Described (Either a b)] -> ([Described a], [Described b])
partitionDescribed = go ([], [])
where
go acc [] = acc
go (as, bs) (d : ds) = case d.item of
Left a -> go (as <> [a <$ d], bs) ds
Right b -> go (as, bs <> [b <$ d]) ds
addToMan5 :: Parser a -> Man5 -> Man5
addToMan5 p base =
base
{ configs = base.configs <> getParserConfs p
}
getParserOpts :: Parser a -> [Described (Either Flag Option)]
getParserOpts = walkNonCommandDocs (maybe [] optDocToOpt) . parserOptDocs
getParserArgs :: Parser a -> [Described Argument]
getParserArgs = walkNonCommandDocs (maybe [] optDocToArg) . parserOptDocs
getParserEnvs :: Parser a -> [Described EnvVar]
getParserEnvs = walkNonCommandDocs envDocToEnvVar . parserEnvDocs
-- getParserCmds :: Parser a -> [Command]
-- getParserCmds = walkCommandDocs commandDocToCommand . parserOptDocs
getParserConfs :: Parser a -> [Described Config]
getParserConfs = walkNonCommandDocs confToConfig . parserConfDocs
optDocToOpt :: OptDoc -> [Described (Either Flag Option)]
optDocToOpt doc =
fromMaybe [] $ do
flag <- dashedFlags $ optDocDasheds doc
let item = case optDocMetavar doc of
Nothing -> Left flag
Just schema ->
let
argument = Argument {schema, optionality = Required}
option = Option {flag, argument}
in
Right option
pure
[ Described
{ item
, optionality = maybe Required Defaulted $ optDocDefault doc
, multiple = False
, helpLines = nonEmpty . lines =<< optDocHelp doc
}
]
optDocToArg :: OptDoc -> [Described Argument]
optDocToArg doc =
case (optDocDasheds doc, optDocMetavar doc) of
([], Just schema) ->
[ Described
{ item =
Argument
{ schema
, optionality = Required
}
, optionality = maybe Required Defaulted $ optDocDefault doc
, multiple = False
, helpLines = nonEmpty . lines =<< optDocHelp doc
}
]
_ -> []
envDocToEnvVar :: EnvDoc -> [Described EnvVar]
envDocToEnvVar doc =
[ Described
{ item =
EnvVar
{ names = envDocVars doc
, argument =
envDocMetavar doc <&> \schema ->
Argument {schema, optionality = Required}
}
, optionality = maybe Required Defaulted $ envDocDefault doc
, multiple = False
, helpLines = nonEmpty . lines =<< envDocHelp doc
}
]
-- commandDocToCommand :: CommandDoc (Maybe OptDoc) -> [Command]
-- commandDocToCommand c =
-- [ Command
-- { name = commandDocArgument c
-- , help = Just $ commandDocHelp c
-- , opts = walkNonCommandDocs 0 (maybe [] . optDocToOpt) $ commandDocs c
-- , args = walkNonCommandDocs 0 (maybe [] . optDocToArg) $ commandDocs c
-- , visible = True
-- , required = True
-- , multiple = False
-- , def = Nothing
-- }
-- ]
confToConfig :: ConfDoc -> [Described Config]
confToConfig doc = map (uncurry go . first toList) $ toList $ confDocKeys doc
where
go :: [String] -> JSONSchema -> Described Config
go keys jSchema =
Described
{ item =
Config
{ name = intercalate "." keys
, schema = simplifySchema jSchema
, exampleLines = nonEmpty $ confDocExamples doc
}
, optionality = maybe Required Defaulted $ confDocDefault doc
, multiple = False
, helpLines = nonEmpty . lines =<< confDocHelp doc
}
simplifySchema :: JSONSchema -> Schema
simplifySchema = \case
AnySchema -> "any"
NullSchema -> "null"
BoolSchema -> "boolean"
StringSchema {} -> "string"
IntegerSchema {} -> "number"
NumberSchema {} -> "number"
ArraySchema s -> ListOf $ simplifySchema s
MapSchema s -> simplifySchema s
ObjectSchema {} -> "object"
-- This is only used for `const`, it must match `Value` literally
ValueSchema v -> case v of
Aeson.Object {} -> "json"
Aeson.Array {} -> "json"
Aeson.String t -> Simple t
Aeson.Number n -> Simple $ pack $ show n
Aeson.Bool b -> Simple $ pack $ show b
Aeson.Null -> "null"
AnyOfSchema ss -> anyOf $ toList ss
OneOfSchema ss -> anyOf $ toList ss
CommentSchema c _ -> Simple c
RefSchema t -> Simple t
WithDefSchema _ s -> simplifySchema s
where
-- Work around bug in opt-env-conf where all configs are null|x
anyOf [NullSchema, s] = simplifySchema s
anyOf ss = AnyOf $ map simplifySchema ss
dashedFlags :: [Dashed] -> Maybe Flag
dashedFlags = fmap go . nonEmpty
where
go :: NonEmpty Dashed -> Flag
go ne =
let aliases = map (aliasedAs []) $ tail ne
in aliasedAs aliases $ head ne
aliasedAs :: [Flag] -> Dashed -> Flag
aliasedAs xs = \case
DashedShort x -> Flag x xs
DashedLong x -> GNUFlag (toList x) xs
-- walkCommandDocs :: (CommandDoc a -> [b]) -> AnyDocs a -> [b]
-- walkCommandDocs f = \case
-- AnyDocsCommands _mDefault cmds -> concatMap f cmds
-- AnyDocsAnd ds -> concatMap (walkCommandDocs f) ds
-- AnyDocsOr ds -> concatMap (walkCommandDocs f) ds
-- AnyDocsSingle {} -> []
walkNonCommandDocs :: (a -> [Described b]) -> AnyDocs a -> [Described b]
walkNonCommandDocs f = \case
AnyDocsCommands {} -> []
AnyDocsAnd ds -> concatMap (walkNonCommandDocs f) ds
AnyDocsOr ds -> concatMap (map mkMultiple . walkNonCommandDocs f) ds
AnyDocsSingle d -> f d
where
-- This is suspect, but it seems we can't distinguish if the Or is being used
-- to indicate some/many or optionality. We'll just do treat it as both since
-- it passes our current tests.
mkMultiple d = d {optionality = Optional, multiple = True}