packages feed

mdoc-0.3.1.0: src/OptEnvConf/Mdoc.hs

-- |
--
-- Module      : OptEnvConf.Mdoc
-- Copyright   : (c) 2026 Patrick Brisbin
-- License     : AGPL-3
-- Maintainer  : pbrisbin@gmail.com
-- Stability   : experimental
-- Portability : POSIX
--
-- See "Mdoc.Examples.OptEnvConf".
module OptEnvConf.Mdoc
  ( getPage
  ) where

import Mdoc.Prelude

import Autodocodec.Schema.Mdoc qualified as JSONSchema
import Control.Monad (foldM)
import Control.Monad.State (MonadState (..), evalState, modify)
import Mdoc.Data.Argument
import Mdoc.Data.Command
import Mdoc.Data.Config
import Mdoc.Data.Described
import Mdoc.Data.EnvVar
import Mdoc.Data.Flag
import Mdoc.Data.Option
import Mdoc.Data.Optionality
import Mdoc.Data.Page
import OptEnvConf (CommandDoc (..), Parser, SetDoc (..))
import OptEnvConf.Args (Dashed (..))
import OptEnvConf.Doc (AnyDocs (..), parserDocs)

getPage :: Parser a -> Page
getPage = foldSetDocs addToPage . parserDocs

getCommand :: Int -> CommandDoc (Maybe SetDoc) -> Command
getCommand index doc =
  Command
    { index
    , name = commandDocArgument doc
    , description = textToMdoc $ pack $ commandDocHelp doc
    , synopsis = getSynopsis $ foldSetDocs addToPage $ commandDocs doc
    }

addToPage
  :: Int
  -> Page
  -> Either [CommandDoc (Maybe SetDoc)] (Described SetDoc)
  -> Page
addToPage index acc = \case
  Left cdocs -> addCommands (zipWith getCommand [1 ..] cdocs) acc
  Right d ->
    flip appEndo acc
      $ mconcat
      $ catMaybes
        [ Endo . addSwitch <$> traverse setDocToFlag d
        , Endo . addOption <$> traverse setDocToOption d
        , Endo . addArgument <$> traverse (setDocToArgument index) d
        , Endo . addEnvVar <$> traverse setDocToEnvVar d
        , Just $ foldMap (Endo . addConfig) $ traverse setDocToConfigs d
        ]

setDocToFlag :: SetDoc -> Maybe Flag
setDocToFlag doc = do
  flag <- dashedFlags $ setDocDasheds doc
  flag <$ guard (isNothing $ setDocMetavar doc)

setDocToOption :: SetDoc -> Maybe Option
setDocToOption doc = do
  flag <- dashedFlags $ setDocDasheds doc
  schema <- setDocMetavar doc
  pure $ Option {flag, argument = fromString schema}

setDocToArgument :: Int -> SetDoc -> Maybe Argument
setDocToArgument index doc = do
  guard $ null $ setDocDasheds doc
  schema <- setDocMetavar doc
  pure $ Argument {index, schema, optionality = Required}

setDocToEnvVar :: SetDoc -> Maybe EnvVar
setDocToEnvVar doc = do
  names <- setDocEnvVars doc
  pure $ EnvVar {names, argument = setDocMetavar doc <&> fromString}

setDocToConfigs :: SetDoc -> [Config]
setDocToConfigs =
  map (.item)
    . concatMap (\(keys, js) -> JSONSchema.getConfigs (Just keys) js)
    . maybe [] toList
    . setDocConfKeys

foldSetDocs
  :: (Int -> Page -> Either [CommandDoc (Maybe SetDoc)] (Described SetDoc) -> Page)
  -> AnyDocs (Maybe SetDoc)
  -> Page
foldSetDocs f = flip evalState 0 . foldSetDocsM f mempty

foldSetDocsM
  :: MonadState Int m
  => (Int -> Page -> Either [CommandDoc (Maybe SetDoc)] (Described SetDoc) -> Page)
  -> Page
  -> AnyDocs (Maybe SetDoc)
  -> m Page
foldSetDocsM f acc = \case
  AnyDocsCommands _ cdocs -> do
    index <- get
    modify (+ 1)
    pure $ f index acc $ Left cdocs
  AnyDocsAnd ds -> foldM (foldSetDocsM f) acc ds
  AnyDocsOr ds -> foldM (foldSetDocsM fAsMultiple) acc ds
  AnyDocsSingle Nothing -> pure acc -- hidden/internal
  AnyDocsSingle (Just d) -> do
    index <- get
    modify (+ 1)
    pure
      $ f index acc
      $ Right
      $ Described
        { item = d
        , optionality = maybe Required Defaulted (setDocDefault d)
        , multiple = False
        , help = textToMdoc . pack =<< setDocHelp 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 treat it as both since it
  -- passes our current tests.
  fAsMultiple i m e = f i m $ second (\d -> d {optionality = Optional, multiple = True}) e

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