From d1b33aad3527cefa1158001dda817d7f414cb278 Mon Sep 17 00:00:00 2001 From: AlexNg Date: Tue, 27 Aug 2024 22:44:26 +0800 Subject: [PATCH 1/9] feat: Basic implementation Signed-off-by: AlexNg --- cmd/root.go | 5 +++++ go.mod | 3 +++ main.go | 9 +++++++++ 3 files changed, 17 insertions(+) create mode 100644 cmd/root.go create mode 100644 go.mod create mode 100644 main.go diff --git a/cmd/root.go b/cmd/root.go new file mode 100644 index 0000000..2eb92b8 --- /dev/null +++ b/cmd/root.go @@ -0,0 +1,5 @@ +package cmd + +// The main entry point for the command line tool +func Execute() { +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..2ab3839 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/caffeine-addictt/template + +go 1.23.0 diff --git a/main.go b/main.go new file mode 100644 index 0000000..e1052d8 --- /dev/null +++ b/main.go @@ -0,0 +1,9 @@ +package main + +import ( + "github.com/caffeine-addictt/template/cmd" +) + +func main() { + cmd.Execute() +} From a093f8d263b1ec71bc06415fc82ead7bab3a4c1f Mon Sep 17 00:00:00 2001 From: AlexNg Date: Tue, 27 Aug 2024 22:47:40 +0800 Subject: [PATCH 2/9] chore(deps): Add cobra Signed-off-by: AlexNg --- go.mod | 6 ++++++ go.sum | 10 ++++++++++ 2 files changed, 16 insertions(+) create mode 100644 go.sum diff --git a/go.mod b/go.mod index 2ab3839..30ac5c9 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,9 @@ module github.com/caffeine-addictt/template go 1.23.0 + +require ( + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/spf13/cobra v1.8.1 // indirect + github.com/spf13/pflag v1.0.5 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..912390a --- /dev/null +++ b/go.sum @@ -0,0 +1,10 @@ +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= From d19f51bb64cf566349ae2c431b1360a062d5f322 Mon Sep 17 00:00:00 2001 From: AlexNg Date: Wed, 28 Aug 2024 00:03:13 +0800 Subject: [PATCH 3/9] feat: Add value guarded struct Signed-off-by: AlexNg --- cmd/utils/types/value_guard.go | 77 +++++++++++++++++++++++++++++ cmd/utils/types/value_guard_test.go | 75 ++++++++++++++++++++++++++++ 2 files changed, 152 insertions(+) create mode 100644 cmd/utils/types/value_guard.go create mode 100644 cmd/utils/types/value_guard_test.go diff --git a/cmd/utils/types/value_guard.go b/cmd/utils/types/value_guard.go new file mode 100644 index 0000000..b2d28bd --- /dev/null +++ b/cmd/utils/types/value_guard.go @@ -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) +} diff --git a/cmd/utils/types/value_guard_test.go b/cmd/utils/types/value_guard_test.go new file mode 100644 index 0000000..2850f48 --- /dev/null +++ b/cmd/utils/types/value_guard_test.go @@ -0,0 +1,75 @@ +package types_test + +import ( + "fmt" + "testing" + + "github.com/caffeine-addictt/template/cmd/utils/types" +) + +func TestNoParsing(t *testing.T) { + val := "test" + typeString := "my type" + + v := types.NewValueGuardNoParsing(val, typeString) + if err := checkValues(val, typeString, v); err != nil { + t.Fatal(err) + } + + if err := v.Set("new value"); err != nil { + t.Fatalf("Failed to set value: %v", err) + } +} + +func TestParsing(t *testing.T) { + val := "test fail" + typeString := "my type" + + v := types.NewValueGuard(val, func(s string) (string, error) { + if s == "test fail" { + return "", fmt.Errorf("failed to parse") + } + return s, nil + }, typeString) + + if err := checkValues(val, typeString, v); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + // Only error when setting + if err := v.Set(val); err == nil { + t.Fatal("Expected error, got nil") + } +} + +func TestParsingFailEarly(t *testing.T) { + val := "test fail" + typeString := "my type" + + _, err := types.TryNewValueGuard(val, func(s string) (string, error) { + if s == "test fail" { + return "", fmt.Errorf("failed to parse") + } + return s, nil + }, typeString) + + if err == nil { + t.Fatal("Expected error, got nil") + } +} + +func checkValues(val string, typeString string, vg *types.ValueGuard[string]) error { + if vg.Value() != val { + return fmt.Errorf("Expected %s, got %s", val, vg.Value()) + } + + if vg.String() != val { + return fmt.Errorf("Expected %s, got %s", val, vg.String()) + } + + if vg.Type() != typeString { + return fmt.Errorf("Expected %s, got %s", typeString, vg.Type()) + } + + return nil +} From e49092850abc0d9577e5810d0b46cf37bf2d9735 Mon Sep 17 00:00:00 2001 From: AlexNg Date: Wed, 28 Aug 2024 00:11:24 +0800 Subject: [PATCH 4/9] feat: Add options Signed-off-by: AlexNg --- cmd/options/options.go | 12 ++++++++++++ cmd/options/root.go | 11 +++++++++++ 2 files changed, 23 insertions(+) create mode 100644 cmd/options/options.go create mode 100644 cmd/options/root.go diff --git a/cmd/options/options.go b/cmd/options/options.go new file mode 100644 index 0000000..1c6430b --- /dev/null +++ b/cmd/options/options.go @@ -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 +} diff --git a/cmd/options/root.go b/cmd/options/root.go new file mode 100644 index 0000000..af1a947 --- /dev/null +++ b/cmd/options/root.go @@ -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("", ""), +} From 13dabe3fbc8b34b65f78a86eb2e1bb30c5fd73c9 Mon Sep 17 00:00:00 2001 From: AlexNg Date: Wed, 28 Aug 2024 00:12:22 +0800 Subject: [PATCH 5/9] test: Add testing helpers Signed-off-by: AlexNg --- cmd/helpers/testing.go | 28 ++++++++++++++++++++ cmd/helpers/testing_test.go | 52 +++++++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+) create mode 100644 cmd/helpers/testing.go create mode 100644 cmd/helpers/testing_test.go diff --git a/cmd/helpers/testing.go b/cmd/helpers/testing.go new file mode 100644 index 0000000..dc3d326 --- /dev/null +++ b/cmd/helpers/testing.go @@ -0,0 +1,28 @@ +package helpers + +import ( + "bytes" + "strings" + + "github.com/spf13/cobra" +) + +// For testing command execution +// Returns stdout, stderr and error +func ExecuteCommand(cmd *cobra.Command, stdin []string, args ...string) (string, string, error) { + cmd.SetArgs(args) + + stdout := bytes.Buffer{} + stderr := bytes.Buffer{} + + cmd.SetOut(&stdout) + cmd.SetErr(&stderr) + cmd.SetIn(strings.NewReader(strings.Join(stdin, "\n"))) + + err := cmd.Execute() + if err != nil { + return stdout.String(), stderr.String(), err + } + + return stdout.String(), stderr.String(), nil +} diff --git a/cmd/helpers/testing_test.go b/cmd/helpers/testing_test.go new file mode 100644 index 0000000..2ef8f9a --- /dev/null +++ b/cmd/helpers/testing_test.go @@ -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) + } +} From d933b75c34fcb57e234a469e055ea971db47d596 Mon Sep 17 00:00:00 2001 From: AlexNg Date: Wed, 28 Aug 2024 00:12:41 +0800 Subject: [PATCH 6/9] feat: Add root command Signed-off-by: AlexNg --- cmd/commands/root.go | 7 +++++++ cmd/root.go | 26 ++++++++++++++++++++++++++ cmd/root_test.go | 23 +++++++++++++++++++++++ 3 files changed, 56 insertions(+) create mode 100644 cmd/commands/root.go create mode 100644 cmd/root_test.go diff --git a/cmd/commands/root.go b/cmd/commands/root.go new file mode 100644 index 0000000..2b47f24 --- /dev/null +++ b/cmd/commands/root.go @@ -0,0 +1,7 @@ +package commands + +import "github.com/spf13/cobra" + +// To initialize all the commands as subcommands of root +func InitCommands(root *cobra.Command) { +} diff --git a/cmd/root.go b/cmd/root.go index 2eb92b8..898906a 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -1,5 +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) + } } diff --git a/cmd/root_test.go b/cmd/root_test.go new file mode 100644 index 0000000..5c7fdb8 --- /dev/null +++ b/cmd/root_test.go @@ -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) + } +} From 55da4a7249dccc8e0e1d5292ec27beef7f3d5101 Mon Sep 17 00:00:00 2001 From: AlexNg Date: Wed, 28 Aug 2024 00:13:03 +0800 Subject: [PATCH 7/9] feat: Add version command (conforming to semver) Signed-off-by: AlexNg --- cmd/commands/root.go | 1 + cmd/commands/version.go | 14 ++++++++++++++ cmd/commands/version_test.go | 24 ++++++++++++++++++++++++ cmd/global/version.go | 4 ++++ cmd/global/version_test.go | 17 +++++++++++++++++ 5 files changed, 60 insertions(+) create mode 100644 cmd/commands/version.go create mode 100644 cmd/commands/version_test.go create mode 100644 cmd/global/version.go create mode 100644 cmd/global/version_test.go diff --git a/cmd/commands/root.go b/cmd/commands/root.go index 2b47f24..ca97d5b 100644 --- a/cmd/commands/root.go +++ b/cmd/commands/root.go @@ -4,4 +4,5 @@ import "github.com/spf13/cobra" // To initialize all the commands as subcommands of root func InitCommands(root *cobra.Command) { + root.AddCommand(VersionCmd) } diff --git a/cmd/commands/version.go b/cmd/commands/version.go new file mode 100644 index 0000000..80a3085 --- /dev/null +++ b/cmd/commands/version.go @@ -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) + }, +} diff --git a/cmd/commands/version_test.go b/cmd/commands/version_test.go new file mode 100644 index 0000000..0a27d6b --- /dev/null +++ b/cmd/commands/version_test.go @@ -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) + } +} diff --git a/cmd/global/version.go b/cmd/global/version.go new file mode 100644 index 0000000..8976819 --- /dev/null +++ b/cmd/global/version.go @@ -0,0 +1,4 @@ +package global + +// The current app version +const Version = "2.0.0" diff --git a/cmd/global/version_test.go b/cmd/global/version_test.go new file mode 100644 index 0000000..d555a4c --- /dev/null +++ b/cmd/global/version_test.go @@ -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) + } +} From c282c626816ed18c0be46bc76651ece8854863d9 Mon Sep 17 00:00:00 2001 From: AlexNg Date: Wed, 28 Aug 2024 00:25:57 +0800 Subject: [PATCH 8/9] style: Tidying up code Signed-off-by: AlexNg --- cmd/helpers/testing.go | 15 +++++++-------- cmd/utils/types/value_guard_test.go | 2 +- go.mod | 3 ++- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/cmd/helpers/testing.go b/cmd/helpers/testing.go index dc3d326..93fff81 100644 --- a/cmd/helpers/testing.go +++ b/cmd/helpers/testing.go @@ -8,21 +8,20 @@ import ( ) // For testing command execution -// Returns stdout, stderr and error -func ExecuteCommand(cmd *cobra.Command, stdin []string, args ...string) (string, string, error) { +func ExecuteCommand(cmd *cobra.Command, stdin []string, args ...string) (stdout, stderr string, e error) { cmd.SetArgs(args) - stdout := bytes.Buffer{} - stderr := bytes.Buffer{} + out := bytes.Buffer{} + errout := bytes.Buffer{} - cmd.SetOut(&stdout) - cmd.SetErr(&stderr) + cmd.SetOut(&out) + cmd.SetErr(&errout) cmd.SetIn(strings.NewReader(strings.Join(stdin, "\n"))) err := cmd.Execute() if err != nil { - return stdout.String(), stderr.String(), err + return out.String(), errout.String(), err } - return stdout.String(), stderr.String(), nil + return out.String(), errout.String(), nil } diff --git a/cmd/utils/types/value_guard_test.go b/cmd/utils/types/value_guard_test.go index 2850f48..ebb7bed 100644 --- a/cmd/utils/types/value_guard_test.go +++ b/cmd/utils/types/value_guard_test.go @@ -58,7 +58,7 @@ func TestParsingFailEarly(t *testing.T) { } } -func checkValues(val string, typeString string, vg *types.ValueGuard[string]) error { +func checkValues(val, typeString string, vg *types.ValueGuard[string]) error { if vg.Value() != val { return fmt.Errorf("Expected %s, got %s", val, vg.Value()) } diff --git a/go.mod b/go.mod index 30ac5c9..f29e47a 100644 --- a/go.mod +++ b/go.mod @@ -2,8 +2,9 @@ module github.com/caffeine-addictt/template go 1.23.0 +require github.com/spf13/cobra v1.8.1 + require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/spf13/cobra v1.8.1 // indirect github.com/spf13/pflag v1.0.5 // indirect ) From 7d51fc9c0cf7ef97d4f3649eb9fbda6a70fa2aaf Mon Sep 17 00:00:00 2001 From: AlexNg Date: Wed, 28 Aug 2024 00:30:59 +0800 Subject: [PATCH 9/9] ci: Use golang latest in CI Signed-off-by: AlexNg --- .github/workflows/linting.yml | 2 +- .github/workflows/test-worker.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index 11285e3..ec32c37 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -61,7 +61,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v5 with: - language-version: 1.23.0 + language-version: latest - name: Lint run: | diff --git a/.github/workflows/test-worker.yml b/.github/workflows/test-worker.yml index 088d09f..fe84c54 100644 --- a/.github/workflows/test-worker.yml +++ b/.github/workflows/test-worker.yml @@ -67,7 +67,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v5 with: - language-version: 1.23.0 + language-version: latest - name: Test run: |