registry-options-0.1.0.0: test/Test/Data/Registry/Options/ParserSpec.hs
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE PartialTypeSignatures #-}
module Test.Data.Registry.Options.ParserSpec where
import Data.Coerce
import Data.Registry
import Data.Registry.Options hiding (defaults)
import Data.Registry.Options qualified as Defaults
import Protolude
import Test.Data.Registry.Options.Fs
import Test.Tasty.Hedgehogx hiding (Command, defaultValue)
test_parse_option = test "parse an option" $ do
let p = make @(Parser "text" Text) (option @"text" @Text [] <: defaults)
parse p "--t eric" === Right "eric"
parse p "--text eric" === Right "eric"
parse p "--typo eric" === Left "missing default value for argument: text"
test_parse_flag = test "parse a flag" $ do
-- with no default value
let p = make @(Parser "int" Int) (flag @"int" @Int 10 Nothing [] <: defaults)
annotate "a flag cannot work as an option, the active value is always taken"
parse p "--int 1" === Right 10
parse p "--i 1" === Right 10
annotate "a flag has an active value"
parse p "--int" === Right 10
parse p "-i" === Right 10
parse p "--typo" === Left "missing default value for argument: int"
-- with a default value
let p1 = make @(Parser "int" Int) (flag @"int" @Int 10 (Just 100) [] <: defaults)
annotate "a flag cannot work as an option, the active value is always taken"
parse p1 "--int 1" === Right 10
parse p1 "-i 1" === Right 10
annotate "a flag has an active value"
parse p1 "--int" === Right 10
parse p1 "-i" === Right 10
parse p1 "--typo" === Right 100
test_parse_switch = test "parse a switch" $ do
let p = make @(Parser "bool" Bool) (switch @"bool" [] <: defaults)
parse p "-b" === Right True
parse p "--bool" === Right True
parse p "--typo" === Right False
test_parse_switches = test "parse several short switches" $ do
let f (pa :: Parser "a" Bool) (pb :: Parser "b" Bool) (pc :: Parser "c" Bool) =
(,,) <$> coerce pa <*> coerce pb <*> coerce pc :: Parser "abc" (Bool, Bool, Bool)
let r =
fun f
<: switch @"a" []
<: switch @"b" []
<: switch @"c" []
<: defaults
let p = make @(Parser "abc" (Bool, Bool, Bool)) $ r
parse p "-abc" === Right (True, True, True)
test_parse_argument = test "parse an argument" $ do
let p = make @(Parser "argument" Text) (argument @"argument" @Text [] <: defaults)
parse p "eric" === Right "eric"
test_parse_constructor = test "parse a constructor" $ do
let parsers =
fun constructor1
<: option @"text" @Text []
<: flag @"int" @Int 10 (Just 100) []
<: switch @"bool" []
<: argument @"file" @File []
<: defaults
let p = getParser @Constructor1 parsers
-- the order of options does not matter
-- but the convention is that options go before arguments
parse p "-b --int --text eric file1" === Right (Constructor1 "eric" True 10 file1)
parse p "-b --text eric --int file1" === Right (Constructor1 "eric" True 10 file1)
parse p "-b --text eric file1" === Right (Constructor1 "eric" True 100 file1)
annotateShow "-- can be used to separate arguments from options"
parse p "-b --text eric --int -- file1" === Right (Constructor1 "eric" True 10 file1)
test_add_help = test "the help text can be specified for each option, and names can be changed" $ do
let _parsers =
fun constructor1
<: option @"text" @Text [help "a text", metavar "SOME_TEXT", name "some-text"]
<: flag @"int" @Int 10 (Just 100) [help "an int"]
<: switch @"bool" [help "a bool"]
<: argument @"file" @File [help "a file path"]
<: defaults
success
test_parse_repeated_options = test "parse options with repeated values" $ do
let r =
fun (nonEmptyParser @"filesNonEmpty" @File)
<: fun (list1Parser @"files1" @File)
<: fun (listParser @"files" @File)
<: option @"filesNonEmpty" @File []
<: option @"files1" @File []
<: option @"files" @File []
<: defaults
let p = make @(Parser "files" [File]) r
let p1 = make @(Parser "files1" [File]) r
let pNonEmpty = make @(Parser "filesNonEmpty" (NonEmpty File)) r
parse p "" === Right []
parse p "--files" === Right []
parse p "--files file1 file2 -- args" === Right [File "file1", File "file2"]
parse p1 "" === Left "missing default value for argument: files1"
parse p1 "--files1" === Left "missing active value for argument: files1"
parse p1 "--files1 file1 file2 -- args" === Right [File "file1", File "file2"]
parse pNonEmpty "" === Left "missing default value for argument: files-non-empty"
parse pNonEmpty "--files-non-empty" === Left "missing active value for argument: files-non-empty"
parse pNonEmpty "--files-non-empty file1 file2 -- args" === Right (File "file1" :| [File "file2"])
test_parse_repeated_arguments = test "parse arguments with repeated values" $ do
let r =
fun (nonEmptyParser @"filesNonEmpty" @File)
<: fun (list1Parser @"files1" @File)
<: fun (listParser @"files" @File)
<: argument @"filesNonEmpty" @File []
<: argument @"files1" @File []
<: argument @"files" @File []
<: defaults
let p = make @(Parser "files" [File]) r
let p1 = make @(Parser "files1" [File]) r
let pNonEmpty = make @(Parser "filesNonEmpty" (NonEmpty File)) r
parse p "" === Right []
parse p "file1 file2" === Right [File "file1", File "file2"]
parse p1 "" === Left "missing default value for argument: FILE"
parse p1 "file1 file2" === Right [File "file1", File "file2"]
parse pNonEmpty "" === Left "missing default value for argument: FILE"
parse pNonEmpty "file1 file2" === Right (File "file1" :| [File "file2"])
test_parse_optional = test "parse optional options and arguments" $ do
let parsers =
fun constructor1
<: setDefaultValue @"text" @Text "eric"
<: setDefaultValue @"int" @Int 100
<: setDefaultValue @"bool" True
<: setDefaultValue @"file" (File "file1")
--
<: option @"text" @Text []
<: flag @"int" @Int 10 Nothing []
<: switch @"bool" []
<: argument @"file" @File []
<: defaults
let p = getParser @Constructor1 parsers
-- the order of options does not matter
-- but the convention is that options go before arguments
parse p "" === Right (Constructor1 "eric" True 100 file1)
test_parse_alternatives = test "parse alternative options and arguments" $ do
let parsers =
fun simpleAlternative
<: flag @"bool" True Nothing []
<: option @"text" @Text []
<: option @"int" @Int []
<: defaults
let p = getParser @SimpleAlternative parsers
parse p "" === Left "missing default value for argument: int"
parse p "-b" === Right (SimpleAlternative1 True)
parse p "--text hello" === Right (SimpleAlternative2 "hello")
takeOptionValue ["repeat"] (optionLexemes "repeat" "10") === Just ("repeat", Just "10", mempty)
parse p "--int 10" === Right (SimpleAlternative3 10)
test_parse_command = test "parse a command" $ do
let p =
make @(Parser Command Copy) $
fun (copyCommand "copy")
<: switch @"force" []
<: setDefaultValue @"retries" @(Maybe Int) Nothing
<: fun (maybeParser @"retries" @Int)
<: option @"retries" @Int []
<: positional @"source" @Text 0 []
<: positional @"target" @Text 1 []
<: defaults
parse p "copy -f source target" === Right (Copy True Nothing "source" "target")
test_parse_named = test "parse a flag name" $ do
let p =
make @(Parser "language" Language) $
named @"language" @Language []
<: addDecoder languageDecoder
<: defaults
parse p "--haskell" === Right Haskell
parse p "--idris" === Right Idris
parse p "--other" === Left "Flag not found for data type `Language`"
annotate "matched flags must be removed from the input strings"
parseLexed p (lexArgs ["--haskell", "--other"]) === Right (Haskell, flagLexemes "other")
-- * HELPERS
getParser :: forall a. (Typeable a) => Registry _ _ -> Parser Command a
getParser = make @(Parser Command a)
constructor1 :: Parser "text" Text -> Parser "bool" Bool -> Parser "int" Int -> Parser "file" File -> Parser "Command" Constructor1
constructor1 p1 p2 p3 p4 = Constructor1 <$> coerce p1 <*> coerce p2 <*> coerce p3 <*> coerce p4
defaults = funTo @Decoder File <: Defaults.defaults
data Constructor1 = Constructor1 Text Bool Int File
deriving (Eq, Show)
data SimpleAlternative
= SimpleAlternative1 Bool
| SimpleAlternative2 Text
| SimpleAlternative3 Int
deriving (Eq, Show)
simpleAlternative :: Parser "bool" Bool -> Parser "text" Text -> Parser "int" Int -> Parser Command SimpleAlternative
simpleAlternative p1 p2 p3 = (SimpleAlternative1 <$> coerce p1) <|> (SimpleAlternative2 <$> coerce p2) <|> (SimpleAlternative3 <$> coerce p3)
file1 :: File
file1 = File "file1"
-- COPY EXAMPLE for 2 arguments
copyCommand :: Text -> Parser "force" Bool -> Parser "retries" (Maybe Int) -> Parser "source" Text -> Parser "target" Text -> Parser Command Copy
copyCommand commandName p1 p2 p3 p4 = Parser noHelp $ \ls ->
case lexedArguments ls of
(n : _)
| commandName == n ->
parseLexed (Copy <$> coerce p1 <*> coerce p2 <*> coerce p3 <*> coerce p4) (popArgumentValue ls)
_ ->
Left $ "command not found, expected: " <> commandName
data Language = Haskell | Idris deriving (Eq, Show)
languageDecoder :: Text -> Either Text Language
languageDecoder "haskell" = Right Haskell
languageDecoder "idris" = Right Idris
languageDecoder _other = Left "wrong language, expected: Haskell or Idris"