diff --git a/flake.nix b/flake.nix index 6705c2e..8be8736 100644 --- a/flake.nix +++ b/flake.nix @@ -25,12 +25,16 @@ defaultPackage = naersk-lib.buildPackage ./.; devShell = with pkgs; mkShell { buildInputs = [ - graphviz # For generating the processing-pipeline infographic + # For Oclis itself cargo + graphviz # For generating the processing-pipeline infographic + pre-commit rustc rustfmt - pre-commit rustPackages.clippy + + # For target languages PureScript and JavaScript + nodejs_20 ] ++ systemSpecificPkgs; RUST_SRC_PATH = rustPlatform.rustLibSrc; }; diff --git a/purescript/makefile b/purescript/makefile new file mode 100644 index 0000000..be07466 --- /dev/null +++ b/purescript/makefile @@ -0,0 +1,8 @@ +.PHONY: help +help: makefile + @tail -n +4 makefile | grep ".PHONY" + + +.PHONY: test +test: + npx spago test diff --git a/purescript/src/CliSpec/readme.md b/purescript/src/CliSpec/readme.md deleted file mode 100644 index d85c4d3..0000000 --- a/purescript/src/CliSpec/readme.md +++ /dev/null @@ -1,2 +0,0 @@ -# CliSpec - diff --git a/purescript/src/CliSpec.purs b/purescript/src/Oclis/Executor.purs similarity index 90% rename from purescript/src/CliSpec.purs rename to purescript/src/Oclis/Executor.purs index 6233600..e8f942a 100644 --- a/purescript/src/CliSpec.purs +++ b/purescript/src/Oclis/Executor.purs @@ -1,13 +1,15 @@ -module CliSpec where +-- | CAUTION: THIS FILE IS GENERATED. DO NOT EDIT MANUALLY! -import CliSpec.Types +module Oclis where + +import Oclis.Types import Prelude (Unit, bind, discard, pure, unit, (#), ($), (-), (<>), (>), (||)) import Ansi.Codes (Color(..)) import Ansi.Output (withGraphics, foreground) -import CliSpec.Parser (tokensToCliArguments) -import CliSpec.Tokenizer (tokenizeCliArguments) +import Oclis.Parser (tokensToCliArguments) +import Oclis.Tokenizer (tokenizeCliArguments) import Data.Argonaut.Decode (decodeJson) import Data.Argonaut.Decode.Error (printJsonDecodeError) import Data.Argonaut.Parser (jsonParser) @@ -36,7 +38,7 @@ errorAndExit message = do setExitCode 1 pure $ Error message -parseCliSpec :: String -> Result String CliSpec +parseCliSpec :: String -> Result String Oclis parseCliSpec cliSpecJsonStr = do let cliSpecRes = fromEither $ jsonParser cliSpecJsonStr @@ -49,12 +51,12 @@ parseCliSpec cliSpecJsonStr = do # fromEither callCommand - :: CliSpec + :: Oclis -> String -> Array CliArgument -> (String -> String -> Array CliArgument -> Effect (Result String Unit)) -> Effect (Result String Unit) -callCommand (CliSpec cliSpec) usageString args executor = do +callCommand (Oclis cliSpec) usageString args executor = do case args # head of Nothing -> do log "No arguments provided" @@ -97,7 +99,7 @@ callCommand (CliSpec cliSpec) usageString args executor = do let commandMb = cliSpec.commands # fromMaybe [] - # find (\(CliSpec cmd) -> cmd.name == cmdName) + # find (\(Oclis cmd) -> cmd.name == cmdName) providedArgs = args # drop 2 case commandMb of @@ -111,7 +113,7 @@ callCommand (CliSpec cliSpec) usageString args executor = do setExitCode 1 pure (Error errStr) - Just (CliSpec _command) -> do + Just (Oclis _command) -> do executor cmdName usageString providedArgs Just arg -> do @@ -135,17 +137,17 @@ repeatString str n = fold $ replicate n str callCliApp - :: CliSpec + :: Oclis -> (String -> String -> Array CliArgument -> Effect (Result String Unit)) -> Effect (Result String Unit) -callCliApp cliSpec@(CliSpec cliSpecRaw) executor = do +callCliApp cliSpec@(Oclis cliSpecRaw) executor = do let lengthLongestCmd :: Int lengthLongestCmd = cliSpecRaw.commands # fromMaybe [] # foldl - ( \acc (CliSpec cmd) -> + ( \acc (Oclis cmd) -> if acc > Str.length cmd.name then acc else Str.length cmd.name ) @@ -162,7 +164,7 @@ callCliApp cliSpec@(CliSpec cliSpecRaw) executor = do ( cliSpecRaw.commands # fromMaybe [] # foldMap - ( \(CliSpec cmd) -> + ( \(Oclis cmd) -> cmd.name <> ( repeatString " " diff --git a/purescript/src/CliSpec/Parser.purs b/purescript/src/Oclis/Parser.purs similarity index 94% rename from purescript/src/CliSpec/Parser.purs rename to purescript/src/Oclis/Parser.purs index 2c345a2..e5c2424 100644 --- a/purescript/src/CliSpec/Parser.purs +++ b/purescript/src/Oclis/Parser.purs @@ -1,4 +1,6 @@ -module CliSpec.Parser +-- | CAUTION: THIS FILE IS GENERATED. DO NOT EDIT MANUALLY! + +module Oclis.Parser ( findFlagLong , findSubCmd , tokensToCliArguments @@ -6,8 +8,8 @@ module CliSpec.Parser import Data.Result -import CliSpec.Tokenizer (CliArgToken(..)) -import CliSpec.Types (CliArgPrim(..), CliArgument(..), CliSpec(..), Option) +import Oclis.Tokenizer (CliArgToken(..)) +import Oclis.Types (CliArgPrim(..), CliArgument(..), Oclis(..), Option) import Data.Array (drop, find, foldl, head, last, zip) import Data.Maybe (Maybe(..), fromMaybe) import Data.String.CodeUnits (singleton) @@ -39,20 +41,20 @@ findOptionLong cliSpecOptionsMb flagName = do # fromMaybe [] # find (\opt -> opt.name == Just flagName) -findSubCmd :: Maybe (Array CliSpec) -> String -> Maybe CliSpec +findSubCmd :: Maybe (Array Oclis) -> String -> Maybe Oclis findSubCmd cliSpecCommands value = do cliSpecCommands # fromMaybe [] - # find (\(CliSpec cmd) -> cmd.name == value) + # find (\(Oclis cmd) -> cmd.name == value) -- | Verify that the remaining tokens are allowed -- | for the given command specification and return -- | the corresponding `CliArgument`s. verifyTokensAreAllowed - :: CliSpec + :: Oclis -> Array CliArgToken -> Result String (Array CliArgument) -verifyTokensAreAllowed (CliSpec cliSpecRaw) tokens = do +verifyTokensAreAllowed (Oclis cliSpecRaw) tokens = do let argsAndTokens = zip (cliSpecRaw.arguments # fromMaybe []) @@ -104,12 +106,12 @@ verifyTokensAreAllowed (CliSpec cliSpecRaw) tokens = do -- | by matching them against the spec. -- | Especially for the differentiation between `Option`s and `Flag`s. tokensToCliArguments - :: CliSpec + :: Oclis -> Array CliArgToken -> Result String (Array CliArgument) -tokensToCliArguments cliSpec@(CliSpec cliSpecRaw) tokens = do +tokensToCliArguments cliSpec@(Oclis cliSpecRaw) tokens = do let - mainCmdRes :: Result String CliSpec + mainCmdRes :: Result String Oclis mainCmdRes = case tokens # head of Just (TextToken cmdName) -> if @@ -237,5 +239,5 @@ tokensToCliArguments cliSpec@(CliSpec cliSpecRaw) tokens = do [] sequence - $ [ mainCmdRes <#> (\(CliSpec cmdSpec) -> CmdArg cmdSpec.name) ] + $ [ mainCmdRes <#> (\(Oclis cmdSpec) -> CmdArg cmdSpec.name) ] <> options diff --git a/purescript/src/CliSpec/Tokenizer.purs b/purescript/src/Oclis/Tokenizer.purs similarity index 95% rename from purescript/src/CliSpec/Tokenizer.purs rename to purescript/src/Oclis/Tokenizer.purs index b307dd2..d00d94f 100644 --- a/purescript/src/CliSpec/Tokenizer.purs +++ b/purescript/src/Oclis/Tokenizer.purs @@ -1,10 +1,12 @@ -module CliSpec.Tokenizer +-- | CAUTION: THIS FILE IS GENERATED. DO NOT EDIT MANUALLY! + +module Oclis.Tokenizer ( CliArgToken(..) , tokenizeCliArgument , tokenizeCliArguments ) where -import CliSpec.Types (CliArgPrim(..)) +import Oclis.Types (CliArgPrim(..)) import Data.Array (concat, drop, groupBy, null, take, (:)) import Data.Array.NonEmpty (toArray) import Data.Foldable (elem) diff --git a/purescript/src/CliSpec/Types.purs b/purescript/src/Oclis/Types.purs similarity index 86% rename from purescript/src/CliSpec/Types.purs rename to purescript/src/Oclis/Types.purs index 35febf9..1080b59 100644 --- a/purescript/src/CliSpec/Types.purs +++ b/purescript/src/Oclis/Types.purs @@ -1,4 +1,6 @@ -module CliSpec.Types where +-- | CAUTION: THIS FILE IS GENERATED. DO NOT EDIT MANUALLY! + +module Oclis.Types where import Data.Argonaut.Decode (decodeJson) import Data.Argonaut.Decode.Class (class DecodeJson) @@ -96,22 +98,22 @@ type CliSpecRaw = , funcName :: Maybe String , options :: Maybe (Array Option) , arguments :: Maybe (Array Argument) - , commands :: Maybe (Array CliSpec) + , commands :: Maybe (Array Oclis) } -- | Must be a newtype to avoid circular references -newtype CliSpec = CliSpec CliSpecRaw +newtype Oclis = Oclis CliSpecRaw -derive instance genericCliSpec :: Generic CliSpec _ -derive instance eqCliSpec :: Eq CliSpec -derive instance newtypeCliSpec :: Newtype CliSpec _ -instance showCliSpec :: Show CliSpec where - show = \(CliSpec specRaw) -> show specRaw +derive instance genericCliSpec :: Generic Oclis _ +derive instance eqCliSpec :: Eq Oclis +derive instance newtypeCliSpec :: Newtype Oclis _ +instance showCliSpec :: Show Oclis where + show = \(Oclis specRaw) -> show specRaw -instance decodeJsonCliSpec :: DecodeJson CliSpec where +instance decodeJsonCliSpec :: DecodeJson Oclis where decodeJson = \json -> do raw <- decodeJson json - pure (CliSpec raw) + pure (Oclis raw) emptyCliSpecRaw :: CliSpecRaw emptyCliSpecRaw = @@ -125,6 +127,6 @@ emptyCliSpecRaw = , commands: Nothing } -emptyCliSpec :: CliSpec +emptyCliSpec :: Oclis emptyCliSpec = - CliSpec emptyCliSpecRaw + Oclis emptyCliSpecRaw diff --git a/purescript/src/Oclis/readme.md b/purescript/src/Oclis/readme.md new file mode 100644 index 0000000..91ffedf --- /dev/null +++ b/purescript/src/Oclis/readme.md @@ -0,0 +1,11 @@ +> [!CAUTION] +> THIS FILE IS GENERATED. DO NOT EDIT MANUALLY. + +# Oclis + +CLI (Command Line Interface) app builder +based on a simple, obvious specification file. + +Check out the documentation at +[github.com/Airsequel/Oclis](https://github.com/Airsequel/Oclis) +for more information. diff --git a/purescript/test/CliSpec.purs b/purescript/test/CliSpec.purs deleted file mode 100644 index 3091fbc..0000000 --- a/purescript/test/CliSpec.purs +++ /dev/null @@ -1,555 +0,0 @@ -module Test.CliSpec where - -import CliSpec (parseCliSpec, callCommand) -import CliSpec.Parser (tokensToCliArguments) -import CliSpec.Tokenizer (CliArgToken(..), tokenizeCliArguments) -import CliSpec.Types - ( CliArgPrim(..) - , CliArgument(..) - , CliSpec(..) - , emptyCliSpec - , emptyCliSpecRaw - ) -import Control.Bind (discard) -import Data.Maybe (Maybe(..)) -import Data.Newtype (over) -import Data.Result (Result(..)) -import Data.String (Pattern(..), split) -import Effect.Class (liftEffect) -import Prelude (Unit, pure, unit, (#), ($)) -import Test.Spec (Spec, describe, it) -import Test.Spec.Assertions (shouldEqual, fail, shouldReturn) - -tokenizeCliStr :: String -> Array CliArgToken -tokenizeCliStr str = - str - # split (Pattern " ") - # tokenizeCliArguments - -tests :: Spec Unit -tests = do - describe "CliSpec" do - describe "Tokenizer" do - it "parses a CLI invocation" do - (tokenizeCliStr "git") - `shouldEqual` [ TextToken "git" ] - - it "parses a standalone flag (for subcommands)" do - (tokenizeCliStr "--help") - `shouldEqual` [ FlagLongToken "help" ] - - it "parses a CLI with an argument" do - (tokenizeCliStr "ls dir") - `shouldEqual` [ TextToken "ls", TextToken "dir" ] - - it "parses a CLI invocation with a long flag" do - (tokenizeCliStr "git --version") - `shouldEqual` [ TextToken "git", FlagLongToken "version" ] - - it "parses a CLI invocation with a short flag" do - (tokenizeCliStr "git -a") - `shouldEqual` [ TextToken "git", FlagShortToken 'a' ] - - it "parses a CLI invocation with several short flags" do - (tokenizeCliStr "git -ab") - `shouldEqual` - [ TextToken "git", FlagShortToken 'a', FlagShortToken 'b' ] - - it "parses a CLI invocation with a long flag and an argument" do - (tokenizeCliStr "git --verbose dir") - `shouldEqual` - [ TextToken "git" - , FlagLongToken "verbose" - , TextToken "dir" - ] - - it "parses a CLI invocation with a long option" do - (tokenizeCliStr "git --git-dir=dir") - `shouldEqual` - [ TextToken "git" - , OptionLongToken "git-dir" (TextArg "dir") - ] - - it "parses a CLI invocation with a short option" do - (tokenizeCliStr "git -d=dir") - `shouldEqual` - [ TextToken "git" - , OptionShortToken 'd' (TextArg "dir") - ] - - describe "Spec Parser" do - let - cliSpec :: CliSpec - cliSpec = CliSpec - ( emptyCliSpecRaw - { name = "git" - , description = "The git command" - , funcName = Just "runApp" - , version = Just "1.0.0" - , commands = Just - [ CliSpec - ( emptyCliSpecRaw - { name = "commit" - , description = "The commit sub-command" - , funcName = Just "runCommit" - , arguments = Just - [ { name: "pathspec" - , description: "File to commit" - , type: "Text" - , optional: Nothing - , default: Nothing - } - ] - } - ) - ] - } - ) - - it "parses a full CLI spec" do - let - cliSpecJson = - """ - { "name": "git", - "description": "The git command", - "funcName": "runApp", - "version": "1.0.0", - "commands": [ - { "name": "commit", - "description": "The commit sub-command", - "funcName": "runCommit", - "arguments": [ - { "name": "pathspec", - "description": "File to commit", - "type": "Text" - } - ] - } - ] - } - """ - - case parseCliSpec cliSpecJson of - Error err -> fail err - Ok parsedCliSpec -> parsedCliSpec `shouldEqual` cliSpec - - it "correctly detects a subcommand with one argument" do - let - cliSpecWithFlag :: CliSpec - cliSpecWithFlag = cliSpec # over CliSpec - ( \spec -> spec - { commands = Just - [ CliSpec - ( emptyCliSpecRaw - { name = "pull" - , description = "The pull sub-command" - , funcName = Just "runPull" - , arguments = Just - [ { name: "repository" - , description: "Name of the repository" - , type: "Text" - , optional: Nothing - , default: Nothing - } - ] - } - ) - ] - } - ) - tokens = tokenizeCliStr "git pull origin" - - tokens `shouldEqual` - [ TextToken "git" - , TextToken "pull" - , TextToken "origin" - ] - (tokensToCliArguments cliSpecWithFlag tokens) - `shouldEqual` - Ok - [ CmdArg "git" - , CmdArg "pull" - , ValArg (TextArg "origin") - ] - - it "correctly detects a subcommand with one long flag and one argument" do - let - cliSpecWithFlag :: CliSpec - cliSpecWithFlag = cliSpec # over CliSpec - ( \spec -> spec - { commands = Just - [ CliSpec - ( emptyCliSpecRaw - { name = "pull" - , description = "The pull sub-command" - , funcName = Just "runPull" - , options = Just - [ { name: Just "progress" - , shortName: Nothing - , description: "Show progress" - , argument: Nothing - , optional: Nothing - , default: Nothing - } - ] - , arguments = Just - [ { name: "repository" - , description: "Name of the repository" - , type: "Text" - , optional: Nothing - , default: Nothing - } - ] - } - ) - ] - } - ) - tokens = tokenizeCliStr "git pull --progress origin" - - tokens `shouldEqual` - [ TextToken "git" - , TextToken "pull" - , FlagLongToken "progress" - , TextToken "origin" - ] - (tokensToCliArguments cliSpecWithFlag tokens) - `shouldEqual` - Ok - [ CmdArg "git" - , CmdArg "pull" - , FlagLong "progress" - , ValArg (TextArg "origin") - ] - - it "redefines a long flag with a value to a long option" do - let - cliSpecWithFlag :: CliSpec - cliSpecWithFlag = cliSpec # over CliSpec - ( \spec -> spec - { commands = Just - [ CliSpec - ( emptyCliSpecRaw - { name = "pull" - , description = "The pull sub-command" - , funcName = Just "runPull" - , options = Just - [ { name: Just "strategy" - , shortName: Nothing - , description: - "Set the preferred merge strategy" - , argument: Just - { name: "strategy" - , description: "Strategy to use" - , type: "Text" - , optional: Just true - , default: Nothing - } - , optional: Nothing - , default: Nothing - } - ] - } - ) - ] - } - ) - tokens = tokenizeCliStr "git pull --strategy recursive" - - tokens `shouldEqual` - [ TextToken "git" - , TextToken "pull" - , FlagLongToken "strategy" - , TextToken "recursive" - ] - (tokensToCliArguments cliSpecWithFlag tokens) - `shouldEqual` - Ok - [ CmdArg "git" - , CmdArg "pull" - , OptionLong "strategy" (TextArg "recursive") - ] - - it "verifies number of args for variable number of allowed args" do - let - cliSpecWithFlag :: CliSpec - cliSpecWithFlag = emptyCliSpec # over CliSpec - ( \spec -> spec - { name = "ls" - , arguments = Just - [ { name: "file" - , description: "File to list" - , type: "Text" - , optional: Just false - , default: Nothing - } - , { name: "file" - , description: "Additional files to list" - , type: "List-Text" - , optional: Just true - , default: Nothing - } - ] - } - ) - - let tokensOne = tokenizeCliStr "ls file1" - (tokensToCliArguments cliSpecWithFlag tokensOne) - `shouldEqual` - Ok - [ CmdArg "ls" - , ValArg (TextArg "file1") - ] - - let tokensTwo = tokenizeCliStr "ls file1 file2" - (tokensToCliArguments cliSpecWithFlag tokensTwo) - `shouldEqual` - Ok - [ CmdArg "ls" - , ValArg (TextArg "file1") - , ValArgList [ TextArg "file2" ] - ] - - let tokensThree = tokenizeCliStr "ls file1 file2 file3" - (tokensToCliArguments cliSpecWithFlag tokensThree) - `shouldEqual` - Ok - [ CmdArg "ls" - , ValArg (TextArg "file1") - , ValArgList [ TextArg "file2", TextArg "file3" ] - ] - - describe "Execution" do - describe "Help" do - let - cliSpec = CliSpec emptyCliSpecRaw - usageString = "Irrelevant" - executor cmdName usageStr providedArgs = do - cmdName `shouldEqual` "help" - usageStr `shouldEqual` usageString - providedArgs `shouldEqual` [] - pure $ Ok unit - - it "shows help output for -h" do - let - toolArgs = [ "git", "-h" ] - tokens = tokenizeCliArguments toolArgs - - case tokensToCliArguments cliSpec tokens of - Error err -> fail err - Ok cliArgs -> - liftEffect (callCommand cliSpec usageString cliArgs executor) - `shouldReturn` (Ok unit) - - it "shows help output for --help" do - let - toolArgs = [ "git", "--help" ] - tokens = tokenizeCliArguments toolArgs - - case tokensToCliArguments cliSpec tokens of - Error err -> fail err - Ok cliArgs -> - liftEffect (callCommand cliSpec usageString cliArgs executor) - `shouldReturn` (Ok unit) - - it "shows help output for `help`" do - let - toolArgs = [ "git", "help" ] - tokens = tokenizeCliArguments toolArgs - - case tokensToCliArguments cliSpec tokens of - Error err -> fail err - Ok cliArgs -> - liftEffect (callCommand cliSpec usageString cliArgs executor) - `shouldReturn` (Ok unit) - - describe "Version" do - let - cliSpec = CliSpec emptyCliSpecRaw - usageString = "Irrelevant" - executor cmdName usageStr providedArgs = do - cmdName `shouldEqual` "help" - usageStr `shouldEqual` usageString - providedArgs `shouldEqual` [] - pure $ Ok unit - - it "shows help output for -v" do - let - toolArgs = [ "git", "-v" ] - tokens = tokenizeCliArguments toolArgs - - case tokensToCliArguments cliSpec tokens of - Error err -> fail err - Ok cliArgs -> - liftEffect (callCommand cliSpec usageString cliArgs executor) - `shouldReturn` (Ok unit) - - it "shows help output for --version" do - let - toolArgs = [ "git", "--version" ] - tokens = tokenizeCliArguments toolArgs - - case tokensToCliArguments cliSpec tokens of - Error err -> fail err - Ok cliArgs -> - liftEffect (callCommand cliSpec usageString cliArgs executor) - `shouldReturn` (Ok unit) - - it "shows help output for `help`" do - let - toolArgs = [ "git", "help" ] - tokens = tokenizeCliArguments toolArgs - - case tokensToCliArguments cliSpec tokens of - Error err -> fail err - Ok cliArgs -> - liftEffect (callCommand cliSpec usageString cliArgs executor) - `shouldReturn` (Ok unit) - - it "executes a sub-command with one argument" do - let - cliSpec = CliSpec - ( emptyCliSpecRaw - { name = "git" - , description = "The git command" - , funcName = Just "runApp" - , version = Just "1.0.0" - , commands = Just - [ CliSpec - ( emptyCliSpecRaw - { name = "pull" - , description = "The pull sub-command" - , funcName = Just "runPull" - , arguments = Just - [ { name: "dir" - , description: "Path to a directory" - , type: "Text" - , optional: Nothing - , default: Nothing - } - ] - } - ) - ] - } - ) - toolArgs = [ "git", "pull", "dir" ] - usageString = "Irrelevant" - executor cmdName usageStr providedArgs = do - cmdName `shouldEqual` "pull" - usageStr `shouldEqual` usageString - providedArgs `shouldEqual` [ (ValArg (TextArg "dir")) ] - pure $ Ok unit - - case tokensToCliArguments cliSpec $ tokenizeCliArguments toolArgs of - Error err -> fail err - Ok cliArgs -> - liftEffect - ( callCommand - cliSpec - usageString - cliArgs - executor - ) `shouldReturn` (Ok unit) - - it "executes a sub-command with one flag" do - let - cliSpec = CliSpec - ( emptyCliSpecRaw - { name = "git" - , description = "The git command" - , funcName = Just "runApp" - , version = Just "1.0.0" - , commands = Just - [ CliSpec - ( emptyCliSpecRaw - { name = "pull" - , description = "The pull sub-command" - , funcName = Just "runPull" - , options = Just - [ { name: Just "stats" - , shortName: Nothing - , description: "Statistics for pull" - , argument: Nothing - , optional: Nothing - , default: Nothing - } - ] - } - ) - ] - } - ) - args = [ "git", "pull", "--stats" ] - usageString = "Irrelevant" - executor cmdName usageStr providedArgs = do - cmdName `shouldEqual` "pull" - usageStr `shouldEqual` usageString - providedArgs `shouldEqual` [ (FlagLong "stats") ] - pure $ Ok unit - - case (tokensToCliArguments cliSpec $ tokenizeCliArguments args) of - Error err -> fail err - Ok cliArgs -> - liftEffect (callCommand cliSpec usageString cliArgs executor) - `shouldReturn` (Ok unit) - - it "executes a sub-command with one option" do - let - cliSpec = CliSpec - ( emptyCliSpecRaw - { name = "git" - , description = "The git command" - , funcName = Just "runApp" - , version = Just "1.0.0" - , commands = Just - [ CliSpec - ( emptyCliSpecRaw - { name = "pull" - , description = "The pull sub-command" - , funcName = Just "runPull" - , options = Just - [ { name: Just "output" - , shortName: Nothing - , description: "Output directory" - , argument: Just - { name: "dir" - , description: "Path to a directory" - , type: "Text" - , optional: Nothing - , default: Nothing - } - , optional: Nothing - , default: Nothing - } - ] - , arguments = Just - [ { name: "dir" - , description: "Path to a directory" - , type: "Text" - , optional: Nothing - , default: Nothing - } - ] - } - ) - ] - } - ) - toolArgs = [ "git", "pull", "--output", "dir" ] - usageString = "Irrelevant" - executor cmdName usageStr providedArgs = do - cmdName `shouldEqual` "pull" - usageStr `shouldEqual` usageString - providedArgs `shouldEqual` [ (OptionLong "output" (TextArg "dir")) ] - pure $ Ok unit - - case (tokensToCliArguments cliSpec $ tokenizeCliArguments toolArgs) of - Error err -> fail err - Ok cliArgs -> - ( liftEffect $ callCommand - cliSpec - usageString - cliArgs - executor - ) `shouldReturn` (Ok unit) diff --git a/purescript/test/Main.purs b/purescript/test/Main.purs index 6d496a9..8efe1cc 100644 --- a/purescript/test/Main.purs +++ b/purescript/test/Main.purs @@ -1,15 +1,564 @@ module Test.Main where -import Prelude (Unit, ($)) +import Prelude (Unit, pure, unit, ($), (#)) +import Oclis (parseCliSpec, callCommand) +import Oclis.Parser (tokensToCliArguments) +import Oclis.Tokenizer (CliArgToken(..), tokenizeCliArguments) +import Oclis.Types + ( CliArgPrim(..) + , CliArgument(..) + , Oclis(..) + , emptyCliSpec + , emptyCliSpecRaw + ) +import Control.Bind (discard) +import Data.Maybe (Maybe(..)) +import Data.Newtype (over) +import Data.Result (Result(..)) +import Data.String (Pattern(..), split) +import Effect.Class (liftEffect) +import Test.Spec (Spec, describe, it) +import Test.Spec.Assertions (shouldEqual, fail, shouldReturn) import Effect (Effect) import Effect.Aff (launchAff_) import Test.Spec.Reporter.Console (consoleReporter) import Test.Spec.Runner (runSpec) -import Test.CliSpec as Test.CliSpec +tokenizeCliStr :: String -> Array CliArgToken +tokenizeCliStr str = + str + # split (Pattern " ") + # tokenizeCliArguments +tests :: Spec Unit +tests = do + describe "Oclis" do + describe "Tokenizer" do + it "parses a CLI invocation" do + (tokenizeCliStr "git") + `shouldEqual` [ TextToken "git" ] + + it "parses a standalone flag (for subcommands)" do + (tokenizeCliStr "--help") + `shouldEqual` [ FlagLongToken "help" ] + + it "parses a CLI with an argument" do + (tokenizeCliStr "ls dir") + `shouldEqual` [ TextToken "ls", TextToken "dir" ] + + it "parses a CLI invocation with a long flag" do + (tokenizeCliStr "git --version") + `shouldEqual` [ TextToken "git", FlagLongToken "version" ] + + it "parses a CLI invocation with a short flag" do + (tokenizeCliStr "git -a") + `shouldEqual` [ TextToken "git", FlagShortToken 'a' ] + + it "parses a CLI invocation with several short flags" do + (tokenizeCliStr "git -ab") + `shouldEqual` + [ TextToken "git", FlagShortToken 'a', FlagShortToken 'b' ] + + it "parses a CLI invocation with a long flag and an argument" do + (tokenizeCliStr "git --verbose dir") + `shouldEqual` + [ TextToken "git" + , FlagLongToken "verbose" + , TextToken "dir" + ] + + it "parses a CLI invocation with a long option" do + (tokenizeCliStr "git --git-dir=dir") + `shouldEqual` + [ TextToken "git" + , OptionLongToken "git-dir" (TextArg "dir") + ] + + it "parses a CLI invocation with a short option" do + (tokenizeCliStr "git -d=dir") + `shouldEqual` + [ TextToken "git" + , OptionShortToken 'd' (TextArg "dir") + ] + + describe "Spec Parser" do + let + cliSpec :: Oclis + cliSpec = Oclis + ( emptyCliSpecRaw + { name = "git" + , description = "The git command" + , funcName = Just "runApp" + , version = Just "1.0.0" + , commands = Just + [ Oclis + ( emptyCliSpecRaw + { name = "commit" + , description = "The commit sub-command" + , funcName = Just "runCommit" + , arguments = Just + [ { name: "pathspec" + , description: "File to commit" + , type: "Text" + , optional: Nothing + , default: Nothing + } + ] + } + ) + ] + } + ) + + it "parses a full CLI spec" do + let + cliSpecJson = + """ + { "name": "git", + "description": "The git command", + "funcName": "runApp", + "version": "1.0.0", + "commands": [ + { "name": "commit", + "description": "The commit sub-command", + "funcName": "runCommit", + "arguments": [ + { "name": "pathspec", + "description": "File to commit", + "type": "Text" + } + ] + } + ] + } + """ + + case parseCliSpec cliSpecJson of + Error err -> fail err + Ok parsedCliSpec -> parsedCliSpec `shouldEqual` cliSpec + + it "correctly detects a subcommand with one argument" do + let + cliSpecWithFlag :: Oclis + cliSpecWithFlag = cliSpec # over Oclis + ( \spec -> spec + { commands = Just + [ Oclis + ( emptyCliSpecRaw + { name = "pull" + , description = "The pull sub-command" + , funcName = Just "runPull" + , arguments = Just + [ { name: "repository" + , description: "Name of the repository" + , type: "Text" + , optional: Nothing + , default: Nothing + } + ] + } + ) + ] + } + ) + tokens = tokenizeCliStr "git pull origin" + + tokens `shouldEqual` + [ TextToken "git" + , TextToken "pull" + , TextToken "origin" + ] + (tokensToCliArguments cliSpecWithFlag tokens) + `shouldEqual` + Ok + [ CmdArg "git" + , CmdArg "pull" + , ValArg (TextArg "origin") + ] + + it "correctly detects a subcommand with one long flag and one argument" do + let + cliSpecWithFlag :: Oclis + cliSpecWithFlag = cliSpec # over Oclis + ( \spec -> spec + { commands = Just + [ Oclis + ( emptyCliSpecRaw + { name = "pull" + , description = "The pull sub-command" + , funcName = Just "runPull" + , options = Just + [ { name: Just "progress" + , shortName: Nothing + , description: "Show progress" + , argument: Nothing + , optional: Nothing + , default: Nothing + } + ] + , arguments = Just + [ { name: "repository" + , description: "Name of the repository" + , type: "Text" + , optional: Nothing + , default: Nothing + } + ] + } + ) + ] + } + ) + tokens = tokenizeCliStr "git pull --progress origin" + + tokens `shouldEqual` + [ TextToken "git" + , TextToken "pull" + , FlagLongToken "progress" + , TextToken "origin" + ] + (tokensToCliArguments cliSpecWithFlag tokens) + `shouldEqual` + Ok + [ CmdArg "git" + , CmdArg "pull" + , FlagLong "progress" + , ValArg (TextArg "origin") + ] + + it "redefines a long flag with a value to a long option" do + let + cliSpecWithFlag :: Oclis + cliSpecWithFlag = cliSpec # over Oclis + ( \spec -> spec + { commands = Just + [ Oclis + ( emptyCliSpecRaw + { name = "pull" + , description = "The pull sub-command" + , funcName = Just "runPull" + , options = Just + [ { name: Just "strategy" + , shortName: Nothing + , description: + "Set the preferred merge strategy" + , argument: Just + { name: "strategy" + , description: "Strategy to use" + , type: "Text" + , optional: Just true + , default: Nothing + } + , optional: Nothing + , default: Nothing + } + ] + } + ) + ] + } + ) + tokens = tokenizeCliStr "git pull --strategy recursive" + + tokens `shouldEqual` + [ TextToken "git" + , TextToken "pull" + , FlagLongToken "strategy" + , TextToken "recursive" + ] + (tokensToCliArguments cliSpecWithFlag tokens) + `shouldEqual` + Ok + [ CmdArg "git" + , CmdArg "pull" + , OptionLong "strategy" (TextArg "recursive") + ] + + it "verifies number of args for variable number of allowed args" do + let + cliSpecWithFlag :: Oclis + cliSpecWithFlag = emptyCliSpec # over Oclis + ( \spec -> spec + { name = "ls" + , arguments = Just + [ { name: "file" + , description: "File to list" + , type: "Text" + , optional: Just false + , default: Nothing + } + , { name: "file" + , description: "Additional files to list" + , type: "List-Text" + , optional: Just true + , default: Nothing + } + ] + } + ) + + let tokensOne = tokenizeCliStr "ls file1" + (tokensToCliArguments cliSpecWithFlag tokensOne) + `shouldEqual` + Ok + [ CmdArg "ls" + , ValArg (TextArg "file1") + ] + + let tokensTwo = tokenizeCliStr "ls file1 file2" + (tokensToCliArguments cliSpecWithFlag tokensTwo) + `shouldEqual` + Ok + [ CmdArg "ls" + , ValArg (TextArg "file1") + , ValArgList [ TextArg "file2" ] + ] + + let tokensThree = tokenizeCliStr "ls file1 file2 file3" + (tokensToCliArguments cliSpecWithFlag tokensThree) + `shouldEqual` + Ok + [ CmdArg "ls" + , ValArg (TextArg "file1") + , ValArgList [ TextArg "file2", TextArg "file3" ] + ] + + describe "Execution" do + describe "Help" do + let + cliSpec = Oclis emptyCliSpecRaw + usageString = "Irrelevant" + executor cmdName usageStr providedArgs = do + cmdName `shouldEqual` "help" + usageStr `shouldEqual` usageString + providedArgs `shouldEqual` [] + pure $ Ok unit + + it "shows help output for -h" do + let + toolArgs = [ "git", "-h" ] + tokens = tokenizeCliArguments toolArgs + + case tokensToCliArguments cliSpec tokens of + Error err -> fail err + Ok cliArgs -> + liftEffect (callCommand cliSpec usageString cliArgs executor) + `shouldReturn` (Ok unit) + + it "shows help output for --help" do + let + toolArgs = [ "git", "--help" ] + tokens = tokenizeCliArguments toolArgs + + case tokensToCliArguments cliSpec tokens of + Error err -> fail err + Ok cliArgs -> + liftEffect (callCommand cliSpec usageString cliArgs executor) + `shouldReturn` (Ok unit) + + it "shows help output for `help`" do + let + toolArgs = [ "git", "help" ] + tokens = tokenizeCliArguments toolArgs + + case tokensToCliArguments cliSpec tokens of + Error err -> fail err + Ok cliArgs -> + liftEffect (callCommand cliSpec usageString cliArgs executor) + `shouldReturn` (Ok unit) + + describe "Version" do + let + cliSpec = Oclis emptyCliSpecRaw + usageString = "Irrelevant" + executor cmdName usageStr providedArgs = do + cmdName `shouldEqual` "help" + usageStr `shouldEqual` usageString + providedArgs `shouldEqual` [] + pure $ Ok unit + + it "shows help output for -v" do + let + toolArgs = [ "git", "-v" ] + tokens = tokenizeCliArguments toolArgs + + case tokensToCliArguments cliSpec tokens of + Error err -> fail err + Ok cliArgs -> + liftEffect (callCommand cliSpec usageString cliArgs executor) + `shouldReturn` (Ok unit) + + it "shows help output for --version" do + let + toolArgs = [ "git", "--version" ] + tokens = tokenizeCliArguments toolArgs + + case tokensToCliArguments cliSpec tokens of + Error err -> fail err + Ok cliArgs -> + liftEffect (callCommand cliSpec usageString cliArgs executor) + `shouldReturn` (Ok unit) + + it "shows help output for `help`" do + let + toolArgs = [ "git", "help" ] + tokens = tokenizeCliArguments toolArgs + + case tokensToCliArguments cliSpec tokens of + Error err -> fail err + Ok cliArgs -> + liftEffect (callCommand cliSpec usageString cliArgs executor) + `shouldReturn` (Ok unit) + + it "executes a sub-command with one argument" do + let + cliSpec = Oclis + ( emptyCliSpecRaw + { name = "git" + , description = "The git command" + , funcName = Just "runApp" + , version = Just "1.0.0" + , commands = Just + [ Oclis + ( emptyCliSpecRaw + { name = "pull" + , description = "The pull sub-command" + , funcName = Just "runPull" + , arguments = Just + [ { name: "dir" + , description: "Path to a directory" + , type: "Text" + , optional: Nothing + , default: Nothing + } + ] + } + ) + ] + } + ) + toolArgs = [ "git", "pull", "dir" ] + usageString = "Irrelevant" + executor cmdName usageStr providedArgs = do + cmdName `shouldEqual` "pull" + usageStr `shouldEqual` usageString + providedArgs `shouldEqual` [ (ValArg (TextArg "dir")) ] + pure $ Ok unit + + case tokensToCliArguments cliSpec $ tokenizeCliArguments toolArgs of + Error err -> fail err + Ok cliArgs -> + liftEffect + ( callCommand + cliSpec + usageString + cliArgs + executor + ) `shouldReturn` (Ok unit) + + it "executes a sub-command with one flag" do + let + cliSpec = Oclis + ( emptyCliSpecRaw + { name = "git" + , description = "The git command" + , funcName = Just "runApp" + , version = Just "1.0.0" + , commands = Just + [ Oclis + ( emptyCliSpecRaw + { name = "pull" + , description = "The pull sub-command" + , funcName = Just "runPull" + , options = Just + [ { name: Just "stats" + , shortName: Nothing + , description: "Statistics for pull" + , argument: Nothing + , optional: Nothing + , default: Nothing + } + ] + } + ) + ] + } + ) + args = [ "git", "pull", "--stats" ] + usageString = "Irrelevant" + executor cmdName usageStr providedArgs = do + cmdName `shouldEqual` "pull" + usageStr `shouldEqual` usageString + providedArgs `shouldEqual` [ (FlagLong "stats") ] + pure $ Ok unit + + case (tokensToCliArguments cliSpec $ tokenizeCliArguments args) of + Error err -> fail err + Ok cliArgs -> + liftEffect (callCommand cliSpec usageString cliArgs executor) + `shouldReturn` (Ok unit) + + it "executes a sub-command with one option" do + let + cliSpec = Oclis + ( emptyCliSpecRaw + { name = "git" + , description = "The git command" + , funcName = Just "runApp" + , version = Just "1.0.0" + , commands = Just + [ Oclis + ( emptyCliSpecRaw + { name = "pull" + , description = "The pull sub-command" + , funcName = Just "runPull" + , options = Just + [ { name: Just "output" + , shortName: Nothing + , description: "Output directory" + , argument: Just + { name: "dir" + , description: "Path to a directory" + , type: "Text" + , optional: Nothing + , default: Nothing + } + , optional: Nothing + , default: Nothing + } + ] + , arguments = Just + [ { name: "dir" + , description: "Path to a directory" + , type: "Text" + , optional: Nothing + , default: Nothing + } + ] + } + ) + ] + } + ) + toolArgs = [ "git", "pull", "--output", "dir" ] + usageString = "Irrelevant" + executor cmdName usageStr providedArgs = do + cmdName `shouldEqual` "pull" + usageStr `shouldEqual` usageString + providedArgs `shouldEqual` [ (OptionLong "output" (TextArg "dir")) ] + pure $ Ok unit + + case (tokensToCliArguments cliSpec $ tokenizeCliArguments toolArgs) of + Error err -> fail err + Ok cliArgs -> + ( liftEffect $ callCommand + cliSpec + usageString + cliArgs + executor + ) `shouldReturn` (Ok unit) main :: Effect Unit main = launchAff_ $ runSpec [ consoleReporter ] do - Test.CliSpec.tests + tests