packages feed

mdoc-0.1.1.4: src/OptEnvConf/Mdoc.hs

-- |
--
-- Module      : OptEnvConf.Mdoc
-- Copyright   : (c) 2026 Patrick Brisbin
-- License     : AGPL-3
-- Maintainer  : pbrisbin@gmail.com
-- Stability   : experimental
-- Portability : POSIX
--
-- A small, semi-representative, not type-checked example:
--
-- @
-- -- This input parser
-- parser :: Parser Options
-- parser = Option
--   \<$> setting [short 'C', long "context", help "Include context" <> metavar "num"]
--   \<*> setting [short 'v', long "debug", env \"DEBUG\", help "Log more verbosely"]
--   \<*> some1 (setting [argument, metavar "file"])
-- @
--
-- @
-- .\" Generates this mandoc source
-- .Sh SYNOPSIS
-- .Nm
-- .Op Fl Ar Cv
-- .Op Fl Fl debug
-- .Ar file
-- .Op Ar file ...
-- .Sh DESCRIPTION
-- The options are as follows:
-- .Bl -tag -width indent
-- .It Fl C Ar num
-- Include context
-- .It Fl v | Fl Fl verbose
-- Log more verbosely
-- .It Ar file
-- .El
-- .Sh ENVIRONMENT
-- The following environment variables affect the execution of
-- .Bl -tag -width \"DEBUG\"
-- .It Cm DEBUG
-- Log more verbosely
-- .El
-- .Sh EXIT STATUS
-- .Ex -std
-- @
--
-- Which renders something like:
--
-- @
-- SYNOPSIS
--   thing [-Cv] [--debug] file [file ...]
--
-- DESCRIPTION
--   The options are as follows:
--
--   -C                 Include context
--
--   -v | --debug       Log more verbosely
--
--   file
--
-- ENVIRONMENT
--   The following environment variables affect the execution of thing:
--
--   DEBUG Log more verbosely
--
-- EXIT STATUS
--   The thing utility exits 0 for success, and >0 if an error occurs.
-- @
--
-- For something more complete, see "Mdoc.GenSpec".
module OptEnvConf.Mdoc
  ( addToMan1
  , addToMan5
  ) where

import Mdoc.Prelude

import Autodocodec.Schema.Mdoc qualified as JSONSchema
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 Mdoc.Optics
import OptEnvConf (ConfDoc (..), EnvDoc (..), OptDoc (..), Parser)
import OptEnvConf.Args (Dashed (..))
import OptEnvConf.Doc
  ( AnyDocs (..)
  , parserConfDocs
  , parserEnvDocs
  , parserOptDocs
  )

addToMan1 :: Parser a -> Man1 -> Man1
addToMan1 p m =
  m
    & field @"switches" <>~ switches
    & field @"options" <>~ options
    & field @"arguments" <>~ getParserArgs p
    & field @"environment" <>~ getParserEnvs p
 where
  (switches, options) = partitionDescribed $ getParserOpts p

addToMan5 :: Parser a -> Man5 -> Man5
addToMan5 p m = m & field @"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 =
  concatMap (\(keys, js) -> addMeta $ JSONSchema.getConfigs (Just keys) js)
    $ toList
    $ confDocKeys doc
 where
  addMeta :: [Described Config] -> [Described Config]
  addMeta = \case
    [] -> []
    (d : ds) ->
      d
        { optionality = maybe Required Defaulted $ confDocDefault doc
        , multiple = False
        , helpLines = nonEmpty . lines =<< confDocHelp doc
        }
        : ds

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}