Skip to content

Commit

Permalink
Fix: simpler file structure (#18)
Browse files Browse the repository at this point in the history
* fix: pkg util -> conf

  - `conf` is more easy to understand

* fix: add `cmd` prefix in file name of commands

* refactor: common.go for exported func and const
  • Loading branch information
KEINOS authored Feb 9, 2021
1 parent 86a041b commit 28da346
Show file tree
Hide file tree
Showing 13 changed files with 255 additions and 258 deletions.
2 changes: 0 additions & 2 deletions .github/golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,6 @@ linters-settings:
- TODO
- BUG
- FIXME
- NOTE
- SEEISSUE
gofmt:
# simplify code: gofmt with `-s` option, true by default
simplify: true
Expand Down
7 changes: 0 additions & 7 deletions cmd/hello.go → cmd/cmd_hello.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,3 @@
/*
Package cmd hello.go defines `hello` command which simply displays "Hello, world!".
It's a child of `root` command.
For more detailed sample see `ext` command ("helloExtended.go"), which is the child
command of `hello`.
*/
package cmd

import (
Expand Down
22 changes: 8 additions & 14 deletions cmd/helloExtended.go → cmd/cmd_helloExtended.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,3 @@
/*
Package cmd helloExtended.go defaines `ext` command which is an extended version
of `hello` with various features and their tests.
It's a sub command of `hello` (grand child of `root`) as well.
*/
package cmd

import (
Expand Down Expand Up @@ -32,17 +26,17 @@ func createHelloExtCmd() *cobra.Command {
Use: "ext [args]", // command name and additional usage.
Short: "Extended 'hello' command.",
Long: `About:
'ext' is a sub command of 'hello' which displays "Hello, world!" in various ways.
`,
'ext' is a sub command of 'hello' which displays "Hello, world!" in various ways.`,
Example: `
Hello-Cobra hello ext
Hello-Cobra hello ext foo
Hello-Cobra hello ext // Hello, world!
Hello-Cobra hello ext foo // Hello, foo!
Hello-Cobra hello ext foo bar // Hello, foo bar!
Hello-Cobra hello ext --who foo bar // Hello, foo and bar!
Hello-Cobra hello ext --who "foo bar" // Hello, foo bar!
Hello-Cobra hello ext foo bar --reverse // !rab oof ,olleH
Hello-Cobra hello ext -h
Hello-Cobra hello ext --help
Hello-Cobra hello ext --reverse foo bar
`,
Hello-Cobra hello ext --help`,
RunE: func(cmd *cobra.Command, args []string) error {
return runHelloExt(cmd, args)
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ type TDataProvider []struct {
}

// ----------------------------------------------------------------------------
// Test functions
// Test Functions
// ----------------------------------------------------------------------------

func Test_helloExtCmd_NoFlags(t *testing.T) {
Expand Down
File renamed without changes.
92 changes: 92 additions & 0 deletions cmd/cmd_root.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package cmd

import (
"fmt"

"github.com/KEINOS/Hello-Cobra/conf"
"github.com/spf13/cobra"
)

// ===============================================================================
// Root command
// ===============================================================================

// rootCmd is the mother command of all other commands.
// Each child/sub command must register themself in their `init()` to `rootCmd`.
// - See how: hello.go
var rootCmd = createRootCmd()

// createRootCmd creates the `root` command.
func createRootCmd() *cobra.Command {
cmdTmp := &cobra.Command{
// The first word in "Use:" will be used as a command name.
Use: "Hello-Cobra",
// One-line description of the app. This will not appear on command help
// but in the generated document.
Short: "A sample of Cobra usage.",
// Detailed description of the app.
Long: `About:
A simple CLI app to see how Cobra works.`,
Example: `
Hello-Cobra hello
Hello-Cobra hello ext --reverse foo bar
Hello-Cobra hello --help
Hello-Cobra help hello
Hello-Cobra help hello ext`,
}

// OnInitialize appends the passed function to initializers to be run when
// each command's `Execute` method was called after `init`.
cobra.OnInitialize(func() {
// Load user conf file if exists.
loadConfig(&ConfApp, &ConfUser)
})

// Additinal global flags of the app.
// These flags will be available to every command under root command.
cmdTmp.PersistentFlags().StringVarP(
&ConfApp.PathFileConf,
"config",
"c",
"",
"File path of config.",
)

// Additional flags of root command.
// Flags() is the drop-in replacement for Go's flag package to implement
// POSIX/GNU-style --flags. For the detailed usage see:
// https://github.com/spf13/pflag
cmdTmp.Flags().BoolP(
"toggle",
"t",
false,
"A flag for the main command but does nothing.",
)

// Return the actual root command
return cmdTmp
}

// init runs on app initialization. Regardless of whether a command was specified
// or not.
//
// NOTE: that each child(sub commands) of root command should be added/registered
// themself in their `init()` via `rootCmd.AddCommand()`.
// See other command's `init()` func.
func init() {}

// loadConfig sets the object in the arg with the results exits with an error if user defined conf file didn't exist.
// Otherwise searches the default file and if not found then use the default value.
func loadConfig(configApp *conf.TConfigApp, configUser interface{}) {
// Overwrite "configUser" with conf file value if file found.
if err := conf.LoadConfig(*configApp, &configUser); err != nil {
// Exits if user defined conf file fails to read
if "" != configApp.PathFileConf {
msg := fmt.Errorf("Failed to read configuration file.\n Error msg: %v", err)
osExit(EchoStdErrIfError(msg))
}
// Conf file not found. Using default. Set flag to true.
configApp.IsUsingDefaultConf = true
}
}
47 changes: 47 additions & 0 deletions cmd/cmd_root_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package cmd

import (
"testing"

"github.com/KEINOS/Hello-Cobra/conf"
"github.com/kami-zh/go-capturer"
"github.com/stretchr/testify/assert"
)

func Test_loadConfigFail(t *testing.T) {
// Save current function in osExt
oldOsExit := osExit
// restore osExit at the end
defer func() { osExit = oldOsExit }()

var (
expectExitCode int
actualExitCode int = 0 // This should turn into 1

confAppDummy conf.TConfigApp
confUserDummy struct {
NameToGreet string `mapstructure:"name_to_greet"` // // Dont'f forget to define `mapstructure`
}
)

// Assign mock of "osExit" to capture the exit-status-code.
osExit = func(code int) {
actualExitCode = 1
}

var capturedMsg string = capturer.CaptureStdout(func() {
// Test user defined bad file path
confAppDummy = conf.TConfigApp{
PathFileConf: "./foobar.json",
PathDirConf: "",
NameFileConf: "",
NameTypeConf: "",
}
confUserDummy.NameToGreet = "bar"
expectExitCode = 1
loadConfig(&confAppDummy, &confUserDummy)
})

// Assertion
assert.Equal(t, expectExitCode, actualExitCode, "Msg: "+capturedMsg)
}
88 changes: 88 additions & 0 deletions cmd/common.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
Package cmd is the package of the actual commands of this CLI app.
*/
package cmd

import (
"fmt"
"os"

"github.com/KEINOS/Hello-Cobra/conf"
)

// ===============================================================================
// Constants
// ===============================================================================

const (
// SUCCESS is an alias of exit status code to ease read.
SUCCESS int = 0
// FAILURE is an alias of exit status code to ease read.
FAILURE int = 1
)

// ===============================================================================
// Application Settings
// ===============================================================================

// TConfUser defines the data structure to store values from a conf file. Viper
// will read these values from the config file or env variables. `mapstructure`
// defines the key name in the conf file.
// Ex) `{"name_to_greet": "foo"}` will be `NameToGreet = "foo"```
type TConfUser struct {
NameToGreet string `mapstructure:"name_to_greet"`
}

var (
// ConfApp is the basic app settings.
ConfApp = conf.TConfigApp{
PathDirConf: ".",
NameFileConf: "config",
NameTypeConf: "json",
PathFileConf: "", // User defined file path
IsUsingDefaultConf: false, // Set to true if conf file not fond
}

// ConfUser holds the values read from the config file. The values here are
// the default.
ConfUser = TConfUser{
NameToGreet: "", // Conf for `hello` and `hello ext` command.
}
)

// osExit is a copy of `os.Exit` to ease the "exit status" test.
// See: https://stackoverflow.com/a/40801733/8367711
var osExit = os.Exit

// ============================================================================
// Exported functions of cmd package. (https://tour.golang.org/basics/3)
//
// Define functions that were used outside this package such as `main` pkg.
// These will work as `cmd` method.
// Ex) cmd.EchoStdErrIfError(err)
// ============================================================================

// EchoStdErrIfError is an STDERR wrappter and returns 0(zero) or 1.
// It does nothing if the error is nil and returns 0.
func EchoStdErrIfError(err error) int {
if err != nil {
fmt.Fprintln(os.Stderr, err)

return FAILURE
}

return SUCCESS
}

// Execute is the main function of `cmd` package.
// It adds all the child commands to the root's command tree and sets their flag
// settings. Then runs/executes the `rootCmd` to find appropriate matches for child
// commands with corresponding flags and args.
//
// Usually `cmd.Execute` will be called by the `main.main()` and it only needs to
// happen once to the rootCmd.
// Returns `error` when it fails to execute.
func Execute() error {
// Read conf file values to ConfUser with ConfApp settings
return rootCmd.Execute()
}
44 changes: 1 addition & 43 deletions cmd/root_test.go → cmd/common_test.go
Original file line number Diff line number Diff line change
@@ -1,55 +1,13 @@
/*
Package cmd root
*/
package cmd

import (
"errors"
"testing"

"github.com/KEINOS/Hello-Cobra/util"
"github.com/kami-zh/go-capturer"
"github.com/stretchr/testify/assert"
)

func Test_loadConfigFail(t *testing.T) {
// Save current function in osExt
oldOsExit := osExit
// restore osExit at the end
defer func() { osExit = oldOsExit }()

var (
expectExitCode int
actualExitCode int = 0 // This should turn into 1

confAppDummy util.TypeConfigApp
confUserDummy struct {
NameToGreet string `mapstructure:"name_to_greet"` // // Dont'f forget to define `mapstructure`
}
)

// Assign mock of "osExit" to capture the exit-status-code.
osExit = func(code int) {
actualExitCode = 1
}

var capturedMsg string = capturer.CaptureStdout(func() {
// Test user defined bad file path
confAppDummy = util.TypeConfigApp{
PathFileConf: "./foobar.json",
PathDirConf: "",
NameFileConf: "",
NameTypeConf: "",
}
confUserDummy.NameToGreet = "bar"
expectExitCode = 1
loadConfig(&confAppDummy, &confUserDummy)
})

// Assertion
assert.Equal(t, expectExitCode, actualExitCode, "Msg: "+capturedMsg)
}

func TestEchoStdErrIfError(t *testing.T) {
var (
expectStatus int = 1
Expand Down Expand Up @@ -98,7 +56,7 @@ func TestExecute(t *testing.T) {
assert.FailNowf(t, "Failed to execute 'root.Execute()'.", "Error msg: %v", err)
}
})
contains = "A simple CLI app to see how Cobra works to create commands."
contains = "A simple CLI app to see how Cobra works."

assert.Contains(t, result, contains, "When no arg, should return help message.")
}
Loading

0 comments on commit 28da346

Please sign in to comment.