packages feed

opt-env-conf-test-0.0.0.4: test/OptEnvConf/APISpec.hs

{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}

module OptEnvConf.APISpec (spec) where

import Autodocodec
import Data.Map (Map)
import Data.Text (Text)
import Data.Version
import GHC.Stack (HasCallStack, withFrozenCallStack)
import OptEnvConf
import OptEnvConf.Test
import OptEnvConf.TestUtils
import Test.Syd
import Text.Colour
import Text.Show.Pretty as Pretty

spec :: Spec
spec = do
  exampleParserSpec "empty" "empty parser" emptyParser
  exampleParserSpec "many-args" "many args parser" manyArgsParser
  exampleParserSpec "some-args" "some args parser" someArgsParser
  exampleParserSpec "optional" "optional argument" optionalParser
  exampleParserSpec "big-config" "example with a big configuration" bigConfigParser
  exampleParserSpec "sub-settings" "example with a sub settings" subSettingsParser
  exampleParserSpec "hidden" "example with hidden settings" hiddenParser
  exampleParserSpec "enable-disable" "enableDisableSwitch example" enableDisableParser
  exampleParserSpec "yes-no" "yesNoSwitch example" yesNoParser
  exampleParserSpec "enable-disable-optional" "enableDisableSwitch' example" enableDisableParser'
  exampleParserSpec "yes-no-optional" "yesNoSwitch' example" yesNoParser'
  exampleParserSpec "verbose" "verbosity example" verboseParser
  exampleParserSpec "same-help" "example where multiple options use the same help string" sameHelpParser
  exampleParserSpec "sum-type" "Sum type example" sumTypeParser
  exampleParserSpec "secret" "Secrets example" secretParser
  exampleParserSpec "with-default" "withDefault example" withDefaultParser
  exampleParserSpec "greet" "hello world example" greetParser
  exampleParserSpec "three-commands" "example with three commands" threeCommandsParser
  exampleParserSpec "sub-commands" "example with subcommands" subCommandsParser

exampleParserSpec :: (HasCallStack) => FilePath -> String -> Parser a -> Spec
exampleParserSpec dir progDesc p = withFrozenCallStack $ describe dir $ do
  let version = makeVersion [0, 0, 0]

  it "passes the linter" $
    parserLintTest p

  it "shows the parser in the same way" $
    goldenStringFile ("test_resources/docs/" <> dir <> "/show.txt") $
      case Pretty.parseValue (showParserABit (parserEraseSrcLocs p)) of
        Nothing -> expectationFailure "Error parsing value"
        Just v -> pure $ Pretty.valToStr v

  it "produces the same docs structure as before" $
    pureGoldenStringFile ("test_resources/docs/" <> dir <> "/docs.txt") $
      ppShow $
        parserDocs p

  it "produces the same opt docs structure as before" $
    pureGoldenStringFile ("test_resources/docs/" <> dir <> "/opt-docs.txt") $
      ppShow $
        parserOptDocs p

  it "produces the same env docs structure as before" $
    pureGoldenStringFile ("test_resources/docs/" <> dir <> "/env-docs.txt") $
      ppShow $
        parserConfDocs p

  it "produces the same conf docs structure as before" $
    pureGoldenStringFile ("test_resources/docs/" <> dir <> "/config-docs.txt") $
      ppShow $
        parserEnvDocs p

  it "documents the version page in the same way" $
    pureGoldenChunksFile ("test_resources/docs/" <> dir <> "/version.txt") $
      renderVersionPage dir version

  it "documents the help page in the same way" $
    pureGoldenChunksFile ("test_resources/docs/" <> dir <> "/help.txt") $
      renderHelpPage dir version progDesc $
        parserDocs p

  it "documents the short opt parser in the same way" $
    pureGoldenChunksFile ("test_resources/docs/" <> dir <> "/opt-short.txt") $
      renderShortOptDocs dir $
        parserOptDocs p

  it "documents the long opt parser in the same way" $
    pureGoldenChunksFile ("test_resources/docs/" <> dir <> "/opt-long.txt") $
      renderLongOptDocs $
        parserOptDocs p

  it "documents the env parser in the same way" $
    pureGoldenChunksFile ("test_resources/docs/" <> dir <> "/env.txt") $
      renderEnvDocs $
        parserEnvDocs p

  it "documents the conf parser in the same way" $
    pureGoldenChunksFile ("test_resources/docs/" <> dir <> "/config.txt") $
      renderConfDocs $
        parserConfDocs p

  it "documents the man page in the same way" $
    pureGoldenTextFile ("test_resources/docs/" <> dir <> "/man.txt") $
      renderChunksText WithoutColours $
        renderManPage dir version progDesc $
          parserDocs p

  it "renders the reference documentation in the same way" $
    pureGoldenChunksFile ("test_resources/docs/" <> dir <> "/reference.txt") $
      renderReferenceDocumentation dir $
        parserDocs p

  it "renders the Nix options the same way" $
    pureGoldenNixOptions ("test_resources/docs/" <> dir <> "/nix-options.nix") p

sameHelpParser :: Parser (Either Int String, Bool)
sameHelpParser =
  withoutConfig $
    let h = help "int or string"
     in (,)
          <$> choice
            [ Left
                <$> setting
                  [ h,
                    name "int",
                    reader auto,
                    metavar "INT"
                  ],
              Right
                <$> setting
                  [ h,
                    name "string",
                    reader str,
                    metavar "STR"
                  ]
            ]
          <*> yesNoSwitch
            [ h,
              name "other",
              value True
            ]

data SumType
  = SumTypeA
  | SumTypeB
  deriving (Show)

instance HasCodec SumType where
  codec = bimapCodec parseSumType renderSumType codec

parseSumType :: String -> Either String SumType
parseSumType = \case
  "a" -> pure SumTypeA
  "b" -> pure SumTypeB
  s -> Left $ unwords ["Unknown 'SumType':", s]

renderSumType :: SumType -> String
renderSumType = \case
  SumTypeA -> "a"
  SumTypeB -> "b"

sumTypeParser :: Parser SumType
sumTypeParser =
  withoutConfig $
    let h = help "example"
     in choice
          [ setting
              [ help "a",
                switch SumTypeA,
                short 'a'
              ],
            setting
              [ help "b",
                switch SumTypeB,
                short 'b'
              ],
            setting
              [ h,
                reader $ eitherReader parseSumType,
                env "sum-type",
                metavar "SUM_TYPE"
              ],
            setting
              [ h,
                conf "sum-type"
              ],
            setting
              [ h,
                valueWithShown renderSumType SumTypeA
              ]
          ]

secretParser :: Parser (Text, Text, Text, Text, Text)
secretParser =
  withoutConfig $
    (,,,,)
      <$> secretTextFileSetting
        [ help "First example secret",
          name "first-secret-file"
        ]
      <*> secretTextFileOrBareSetting
        [ help "Second example secret, bare or in a file",
          name "second-secret"
        ]
      <*> secretTextFileOrBareSetting
        [ help "Second example secret, bare or in a file, only option",
          option,
          long "second-secret"
        ]
      <*> secretTextFileOrBareSetting
        [ help "Second example secret, bare or in a file, only env",
          env "second-secret"
        ]
      <*> secretTextFileOrBareSetting
        [ help "Second example secret, bare or in a file, only conf",
          conf "second-secret"
        ]

data FirstOrSecond = First | Second
  deriving (Show)

instance HasParser FirstOrSecond where
  settingsParser =
    -- This version has no default value, so we can add one with 'withDefault'
    choice
      [ setting
          [ help "first",
            switch First,
            short 'f',
            long "first"
          ],
        setting
          [ help "second",
            switch Second,
            short 's',
            long "second"
          ]
      ]

withDefaultParser :: Parser FirstOrSecond
withDefaultParser = withDefault Second settingsParser

data Greet = Greet !String !String !Bool

greetParser :: Parser Greet
greetParser =
  subEnv "GREET_" $
    withLocalYamlConfig $
      Greet
        <$> setting
          [ reader str,
            option,
            short 'g',
            long "greeting",
            metavar "GREETING",
            env "GREETING",
            conf "greeting",
            value "Hello",
            example "Greetings",
            help "Greeting to use"
          ]
        <*> setting
          [ reader str,
            argument,
            help "Who to greet",
            value "world",
            metavar "SUBJECT"
          ]
        <*> setting
          [ reader exists,
            switch True,
            short 'p',
            long "polite",
            env "POLITE",
            conf "polite",
            metavar "ANY",
            value False,
            help "Whether to be polite"
          ]

data BigConfig = BigConfig
  { bigConfigMap :: Map String (Map String Int),
    bigConfigSubObject :: Maybe Text
  }

instance HasCodec BigConfig where
  codec =
    object "BigConfig" $
      BigConfig
        <$> requiredField' "map" .= bigConfigMap
        <*> optionalField' "sub" .= bigConfigSubObject

bigConfigParser :: Parser BigConfig
bigConfigParser =
  withLocalYamlConfig $
    setting
      [ conf "big",
        help "big configuration object"
      ]

subSettingsParser :: Parser String
subSettingsParser =
  withLocalYamlConfig $
    subAll "foo" $
      subAll "bar" $
        setting
          [ reader str,
            name "quux",
            help "Example with sub-settings",
            metavar "STR"
          ]

manyArgsParser :: Parser [String]
manyArgsParser =
  many
    ( setting
        [ reader str,
          argument,
          help "Argument",
          metavar "ARGUMENT"
        ]
    )

someArgsParser :: Parser [String]
someArgsParser =
  some
    ( setting
        [ reader str,
          argument,
          help "Argument",
          metavar "ARGUMENT"
        ]
    )

data Optional = Optional (Maybe String)

optionalParser :: Parser Optional
optionalParser =
  Optional
    <$> optional
      ( setting
          [ reader str,
            argument,
            help "Argument",
            metavar "ARGUMENT"
          ]
      )

data Hidden = Hidden String

hiddenParser :: Parser Hidden
hiddenParser =
  Hidden
    <$> setting
      [ reader str,
        argument,
        hidden,
        metavar "STR",
        value "default",
        help "Example of a hidden setting"
      ]

data EnableDisable = EnableDisable Bool

enableDisableParser :: Parser EnableDisable
enableDisableParser =
  withoutConfig $
    EnableDisable
      <$> enableDisableSwitch
        [ long "example",
          help "Example of an enable/disable switch",
          env "EXAMPLE",
          conf "example",
          value True
        ]

enableDisableParser' :: Parser (Maybe EnableDisable)
enableDisableParser' =
  withoutConfig $
    fmap EnableDisable
      <$> optional
        ( enableDisableSwitch
            [ long "example",
              help "Example of an enable/disable switch",
              env "EXAMPLE",
              conf "example"
            ]
        )

data YesNo = YesNo Bool

yesNoParser :: Parser YesNo
yesNoParser =
  withoutConfig $
    YesNo
      <$> yesNoSwitch
        [ long "example",
          help "Example of a yes/no switch",
          env "EXAMPLE",
          conf "example",
          value True
        ]

yesNoParser' :: Parser (Maybe YesNo)
yesNoParser' =
  withoutConfig $
    fmap YesNo
      <$> optional
        ( yesNoSwitch
            [ long "example",
              help "Example of a yes/no switch",
              env "EXAMPLE",
              conf "example"
            ]
        )

data Empty = Empty

emptyParser :: Parser Empty
emptyParser =
  pure Empty

data ThreeCommands
  = One !String
  | Two !Int !Bool
  | Three

threeCommandsParser :: Parser ThreeCommands
threeCommandsParser =
  withoutConfig $
    commands
      [ command "one" "first" $
          One
            <$> setting
              [ help "argument",
                reader str,
                metavar "STR",
                argument
              ],
        command "two" "second" $
          Two
            <$> setting
              [ help "number",
                reader auto,
                option,
                metavar "INT",
                name "number",
                short 'n',
                shownExample 5
              ]
            <*> enableDisableSwitch
              [ help "enable extra",
                name "enable",
                value False
              ],
        command "three-very-long-command-name" "third" (pure Three),
        defaultCommand "one"
      ]

data SubCommands
  = Top !String
  | Sub !Sub1 !Sub2

subCommandsParser :: Parser SubCommands
subCommandsParser =
  withLocalYamlConfig $
    commands
      [ command "top" "command without subcommands" $
          Top
            <$> setting
              [ help "name",
                reader str,
                metavar "NAME",
                name "name",
                example "john"
              ],
        command "sub" "command with subcommands" $
          Sub <$> sub1Parser <*> sub2Parser
      ]

data Sub1 = A | B

sub1Parser :: Parser Sub1
sub1Parser =
  commands
    [ command "a" "A" $ pure A,
      command "b" "B" $ pure B
    ]

data Sub2 = C | D

sub2Parser :: Parser Sub2
sub2Parser =
  commands
    [ command "c" "C" $ pure C,
      command "d" "D" $ pure D,
      defaultCommand "c"
    ]

verboseParser :: Parser Int
verboseParser =
  length
    <$> many
      ( setting
          [ help "Verbosity level. Use multiple to increase verbosity",
            short 'v',
            switch ()
          ]
      )