-
-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: A simple CLI implementation (#81)
* Added version command * Added CLI options and flags * Update CI --------- Signed-off-by: AlexNg <[email protected]>
- Loading branch information
Showing
18 changed files
with
406 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
package commands | ||
|
||
import "github.com/spf13/cobra" | ||
|
||
// To initialize all the commands as subcommands of root | ||
func InitCommands(root *cobra.Command) { | ||
root.AddCommand(VersionCmd) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
package commands | ||
|
||
import ( | ||
"github.com/caffeine-addictt/template/cmd/global" | ||
"github.com/spf13/cobra" | ||
) | ||
|
||
var VersionCmd = &cobra.Command{ | ||
Use: "version", | ||
Aliases: []string{"ver"}, | ||
Run: func(cmd *cobra.Command, args []string) { | ||
cmd.Println(global.Version) | ||
}, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
package commands_test | ||
|
||
import ( | ||
"testing" | ||
|
||
"github.com/caffeine-addictt/template/cmd/commands" | ||
"github.com/caffeine-addictt/template/cmd/global" | ||
"github.com/caffeine-addictt/template/cmd/helpers" | ||
) | ||
|
||
func TestVersionOut(t *testing.T) { | ||
stdout, stderr, err := helpers.ExecuteCommand(commands.VersionCmd, []string{}) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
|
||
if stdout != global.Version+"\n" { | ||
t.Errorf("Expected version %s, got %s", global.Version, stdout) | ||
} | ||
|
||
if stderr != "" { | ||
t.Fatalf("Expected no stderr, got %s", err) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
package global | ||
|
||
// The current app version | ||
const Version = "2.0.0" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
package global_test | ||
|
||
import ( | ||
"regexp" | ||
"testing" | ||
|
||
"github.com/caffeine-addictt/template/cmd/global" | ||
) | ||
|
||
// Regex taken from https://semver.org | ||
var semverRegex = regexp.MustCompile(`^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$`) | ||
|
||
func TestFollowsSemVer(t *testing.T) { | ||
if !semverRegex.MatchString(global.Version) { | ||
t.Fatalf("%v does not follow semver", global.Version) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
package helpers | ||
|
||
import ( | ||
"bytes" | ||
"strings" | ||
|
||
"github.com/spf13/cobra" | ||
) | ||
|
||
// For testing command execution | ||
func ExecuteCommand(cmd *cobra.Command, stdin []string, args ...string) (stdout, stderr string, e error) { | ||
cmd.SetArgs(args) | ||
|
||
out := bytes.Buffer{} | ||
errout := bytes.Buffer{} | ||
|
||
cmd.SetOut(&out) | ||
cmd.SetErr(&errout) | ||
cmd.SetIn(strings.NewReader(strings.Join(stdin, "\n"))) | ||
|
||
err := cmd.Execute() | ||
if err != nil { | ||
return out.String(), errout.String(), err | ||
} | ||
|
||
return out.String(), errout.String(), nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
package helpers_test | ||
|
||
import ( | ||
"testing" | ||
|
||
"github.com/caffeine-addictt/template/cmd/helpers" | ||
"github.com/spf13/cobra" | ||
) | ||
|
||
func TestExecuteCommandCapturesStderr(t *testing.T) { | ||
msg := "I'm in stderr" | ||
dummyCmd := cobra.Command{ | ||
Run: func(cmd *cobra.Command, args []string) { | ||
cmd.PrintErr(msg) | ||
}, | ||
} | ||
|
||
stdout, stderr, err := helpers.ExecuteCommand(&dummyCmd, []string{}, "") | ||
if err != nil { | ||
t.Fatalf("Failed to execute command: %v", stderr) | ||
} | ||
|
||
if stderr != msg { | ||
t.Fatalf("Expected stderr to be '%s', got '%s'", msg, stderr) | ||
} | ||
|
||
if stdout != "" { | ||
t.Fatalf("Expected stdout to be empty, got '%s'", stdout) | ||
} | ||
} | ||
|
||
func TestExecuteCommandCapturesStdout(t *testing.T) { | ||
msg := "I'm in stdout" | ||
dummyCmd := cobra.Command{ | ||
Run: func(cmd *cobra.Command, args []string) { | ||
cmd.Print(msg) | ||
}, | ||
} | ||
|
||
stdout, stderr, err := helpers.ExecuteCommand(&dummyCmd, []string{}, "") | ||
if err != nil { | ||
t.Fatalf("Failed to execute command: %v", stderr) | ||
} | ||
|
||
if stdout != msg { | ||
t.Fatalf("Expected stdout to be '%s', got '%s'", msg, stdout) | ||
} | ||
|
||
if stderr != "" { | ||
t.Fatalf("Expected stderr to be empty, got '%s'", stderr) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
package options | ||
|
||
import "github.com/caffeine-addictt/template/cmd/utils/types" | ||
|
||
type Options struct { | ||
// The repository Url to use | ||
// Should be this repository by default | ||
Repo types.ValueGuard[string] | ||
|
||
// Wheter or not debug mode should be enabled | ||
Debug bool | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
package options | ||
|
||
import ( | ||
"github.com/caffeine-addictt/template/cmd/utils/types" | ||
) | ||
|
||
// The global options for the CLI | ||
var Opts = Options{ | ||
Debug: false, | ||
Repo: *types.NewValueGuardNoParsing("", "<repo>"), | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
package cmd | ||
|
||
import ( | ||
"log" | ||
|
||
"github.com/caffeine-addictt/template/cmd/commands" | ||
"github.com/caffeine-addictt/template/cmd/options" | ||
"github.com/spf13/cobra" | ||
) | ||
|
||
// The Root command | ||
var RootCmd = &cobra.Command{ | ||
Use: "template", | ||
Short: "Let's make starting new projects feel like a breeze again.", | ||
Long: "This tool helps you to create a new project from templates.", | ||
} | ||
|
||
// Setting up configuration | ||
func init() { | ||
RootCmd.PersistentFlags().BoolVarP(&options.Opts.Debug, "debug", "d", false, "Debug mode") | ||
RootCmd.PersistentFlags().VarP(&options.Opts.Repo, "repo", "r", "Community source repository for templates") | ||
|
||
commands.InitCommands(RootCmd) | ||
} | ||
|
||
// The main entry point for the command line tool | ||
func Execute() { | ||
if err := RootCmd.Execute(); err != nil { | ||
log.Fatal(err) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
package cmd_test | ||
|
||
import ( | ||
"testing" | ||
|
||
"github.com/caffeine-addictt/template/cmd" | ||
"github.com/caffeine-addictt/template/cmd/helpers" | ||
) | ||
|
||
func TestRootCommandCanRun(t *testing.T) { | ||
stdout, stderr, err := helpers.ExecuteCommand(cmd.RootCmd, []string{}) | ||
if err != nil { | ||
t.Fatalf("Failed to run root command: %v", err) | ||
} | ||
|
||
if stdout == "" { | ||
t.Fatalf("Expected non-empty stdout, but got: %s", stdout) | ||
} | ||
|
||
if stderr != "" { | ||
t.Fatalf("Expected empty stderr, but got: %s", stderr) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
package types | ||
|
||
import ( | ||
"errors" | ||
"fmt" | ||
) | ||
|
||
// A value guard to validate values being set | ||
type ValueGuard[T any] struct { | ||
// The value | ||
value T | ||
// The validation and mutation function | ||
parseValue func(v T) (T, error) | ||
// The human readable string type | ||
typeString string | ||
} | ||
|
||
// Creating a new value guard with value parsing | ||
func TryNewValueGuard[T any](value T, parseValue func(v T) (T, error), typeString string) (*ValueGuard[T], error) { | ||
v := ValueGuard[T]{ | ||
parseValue: parseValue, | ||
typeString: typeString, | ||
} | ||
|
||
if err := v.Set(value); err != nil { | ||
return nil, err | ||
} | ||
return &v, nil | ||
} | ||
|
||
// Creating a new value guard without validating value | ||
func NewValueGuard[T any](value T, parseValue func(v T) (T, error), typeString string) *ValueGuard[T] { | ||
return &ValueGuard[T]{ | ||
value: value, | ||
typeString: typeString, | ||
parseValue: parseValue, | ||
} | ||
} | ||
|
||
// Creating a new value guard without value parsing | ||
func NewValueGuardNoParsing[T any](value T, typeString string) *ValueGuard[T] { | ||
return &ValueGuard[T]{ | ||
value: value, | ||
typeString: typeString, | ||
} | ||
} | ||
|
||
// Access the underlying value | ||
func (v *ValueGuard[T]) Value() T { | ||
return v.value | ||
} | ||
|
||
// Mutate the underlying value | ||
func (v *ValueGuard[T]) Set(value T) error { | ||
if v.parseValue != nil { | ||
value, err := v.parseValue(value) | ||
if err != nil { | ||
return errors.Join(fmt.Errorf("Cannot set value %v", value), err) | ||
} | ||
|
||
v.value = value | ||
return nil | ||
} | ||
|
||
v.value = value | ||
return nil | ||
} | ||
|
||
// Access the human readable string type | ||
func (v *ValueGuard[T]) Type() string { | ||
return v.typeString | ||
} | ||
|
||
// Return the string representation | ||
func (v *ValueGuard[T]) String() string { | ||
return fmt.Sprintf("%v", v.value) | ||
} |
Oops, something went wrong.