Skip to content

Commit

Permalink
Implement the fallback mode
Browse files Browse the repository at this point in the history
  • Loading branch information
jfahrer committed Jul 10, 2019
1 parent 70ac585 commit 1e238fa
Show file tree
Hide file tree
Showing 6 changed files with 94 additions and 54 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,8 @@ alias irb='donner run irb'
To make aliases work nicely system wide, Donner supports a `--fallback` and `--strict` flag.

* When executing `donner run --strict some-command` and the provided command is not defined under `commands` or `aliases`, Donner will fail executing the command.
* When executing `donner run --falback` no `.donner.yml` is found in the current directory, Donner executes the command as is in the current shell.
* When executing `donner run --strict --falback` and the provided command is not defined under `commands` or `aliases`, Donner executes the command as is in the current shell.
* When executing `donner run --fallback` with no `.donner.yml` in the current directory, Donner executes the command as is.
* When executing `donner run --strict --fallback` and the provided command is not defined, Donner executes the command as is.


This is also supported with aliases:
Expand Down
39 changes: 24 additions & 15 deletions donner.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import (
"github.com/urfave/cli"
)

// ErrMissingCommand is thrown if no handler for execution is provided
// ErrMissingCommand is thrown if no command to execute is provided on the command line
var ErrMissingCommand = errors.New("no command for execution specified")

func main() {
Expand All @@ -30,13 +30,14 @@ func main() {
SkipArgReorder: true,
Flags: []cli.Flag{
cli.BoolFlag{Name: "strict,s", Usage: "enable strict mode"},
cli.BoolFlag{Name: "fallback,f", Usage: "enable fallback mode"},
},
Action: func(c *cli.Context) error {
cfg, err := readConfig()
cfg, err := makeConfig(c.Bool("fallback"))
if err != nil {
return err
}
return execCommand(cfg, c.Args(), c.Bool("strict"))
return execCommand(cfg, c.Args(), c.Bool("strict"), c.Bool("fallback"))
},
},
{
Expand All @@ -48,7 +49,7 @@ func main() {
cli.BoolFlag{Name: "fallback,f", Usage: "fallback to local commands"},
},
Action: func(c *cli.Context) error {
cfg, err := readConfig()
cfg, err := makeConfig(false)
if err != nil {
return err
}
Expand All @@ -66,13 +67,13 @@ func main() {
}

// execCommand dispatches the call to the OS
func execCommand(cfg *Cfg, cliArgs []string, strict bool) error {
func execCommand(cfg *Cfg, cliArgs []string, strict, fallback bool) error {
if len(cliArgs) < 1 {
// TODO show usage?
return ErrMissingCommand
}

execHandler, err := cfg.GetHandlerFor(cliArgs[0], strict)
execHandler, err := cfg.GetHandlerFor(cliArgs[0], strict, fallback)
if err != nil {
return err
}
Expand All @@ -98,17 +99,25 @@ func execCommand(cfg *Cfg, cliArgs []string, strict bool) error {
return nil
}

func readConfig() (*Cfg, error) {
// TODO handle 'yaml' case
dat, err := ioutil.ReadFile(".donner.yml")
if err != nil {
return nil, err
}
func makeConfig(allowNoConfig bool) (*Cfg, error) {
cfg := GenerateConfig()

cfg, err := GenerateConfig(dat)
if err != nil {
dat, err := readConfig()

if allowNoConfig && os.IsNotExist(err) {
return cfg, nil
} else if err != nil {
return nil, err
}
err = cfg.Load(dat)
return cfg, err
}

return cfg, nil
func readConfig() ([]byte, error) {
_, err := os.Stat(".donner.yml") // TODO handle 'yaml' case
if err == nil {
dat, err := ioutil.ReadFile(".donner.yml")
return dat, err
}
return nil, err
}
21 changes: 12 additions & 9 deletions donner_test.go
Original file line number Diff line number Diff line change
@@ -1,27 +1,30 @@
package main

import (
"github.com/stretchr/testify/assert"
"testing"

"github.com/stretchr/testify/assert"
)

func TestExecCommand(t *testing.T) {
tests := map[string]struct {
config *Cfg
params []string
expErr error
strictMode bool
config *Cfg
params []string
expErr error
strictMode bool
fallbackMode bool
}{
"missing command": {
config: &Cfg{},
expErr: ErrMissingCommand,
strictMode: true,
config: &Cfg{},
expErr: ErrMissingCommand,
strictMode: true,
fallbackMode: true,
},
}

for name, test := range tests {
t.Run(name, func(t *testing.T) {
err := execCommand(test.config, test.params, test.strictMode)
err := execCommand(test.config, test.params, test.strictMode, test.fallbackMode)
assert.EqualError(t, err, test.expErr.Error())
})
}
Expand Down
12 changes: 12 additions & 0 deletions handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,18 @@ type Handler interface {
validate() error
}

// FallbackHandler does not wrap the command and will just execute it as is
type FallbackHandler struct{}

// BuildCommand simply returns the passed in command
func (h *FallbackHandler) BuildCommand(command []string) []string {
return command
}

func (h *FallbackHandler) validate() error {
return nil
}

// DockerRunHandler wraps a command with `docker container run`
type DockerRunHandler struct {
Remove bool
Expand Down
43 changes: 27 additions & 16 deletions parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@ var ErrUndefinedCommand = errors.New("the command you're trying to run doesn't e
var ErrNoHandlerDefined = errors.New("no handler specified in strategy")

// ErrInvalidStrategy is thrown when a invalid handler is referenced
var ErrInvalidStrategy = errors.New("invalid sttrategy specified in command")
var ErrInvalidStrategy = errors.New("invalid strategy specified in command")

// ErrPathSpecifiedInCommand is thrown when a command is specified using a relative or absolute path rather then the executable name
var ErrPathSpecifiedInCommand = errors.New("Path instead name of executable defined")

var handlerFactories = map[string]func(map[string]interface{}) (Handler, error){
"docker_run": InitDockerRunHandler,
Expand All @@ -32,35 +35,40 @@ var handlerFactories = map[string]func(map[string]interface{}) (Handler, error){

// Cfg is the uber object in our YAML file
type Cfg struct {
commands map[string]Handler
handler map[string]Handler
defaultHandler Handler
commands map[string]Handler
handler map[string]Handler
defaultHandler Handler
fallbackHandler Handler
}

// GenerateConfig is the main entry point from which we generate the config
func GenerateConfig(file []byte) (*Cfg, error) {
yamlConfig, err := parseYaml(file)
if err != nil {
return nil, err
func GenerateConfig() *Cfg {
return &Cfg{
handler: map[string]Handler{},
commands: map[string]Handler{},
fallbackHandler: &FallbackHandler{},
}
}

cfg := &Cfg{
handler: map[string]Handler{},
commands: map[string]Handler{},
// Load will load the config from the provided file
func (cfg *Cfg) Load(yaml []byte) error {
yamlConfig, err := parseYaml(yaml)
if err != nil {
return err
}

err = cfg.configFromYaml(yamlConfig)

return cfg, err
return cfg.configFromYaml(yamlConfig)
}

// GetHandlerFor will try to find a handler for the specified command
func (cfg *Cfg) GetHandlerFor(command string, strictMode bool) (Handler, error) {
func (cfg *Cfg) GetHandlerFor(command string, strictMode, fallbackMode bool) (Handler, error) {
executable := path.Base(command)
if handler, ok := cfg.commands[executable]; ok {
return handler, nil
} else if strictMode {
} else if strictMode && !fallbackMode {
return nil, ErrUndefinedCommand
} else if fallbackMode {
return cfg.fallbackHandler, nil
} else if handler = cfg.defaultHandler; handler != nil {
return handler, nil
}
Expand Down Expand Up @@ -100,6 +108,9 @@ func (cfg *Cfg) configFromYaml(yaml *yamlCfg) error {
}

for command, strategy := range yaml.Commands {
if path.Base(command) != command {
return ErrPathSpecifiedInCommand
}
if handler, ok := cfg.handler[strategy]; ok {
cfg.commands[command] = handler
} else {
Expand Down
29 changes: 17 additions & 12 deletions parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import (
)

func TestListCommands(t *testing.T) {
cfg, err := GenerateConfig([]byte(fullYaml))
cfg := GenerateConfig()
err := cfg.Load([]byte(fullYaml))
assert.NoError(t, err)
assert.ElementsMatch(t, cfg.ListCommands(), []string{"ls", "bundle"})
}
Expand All @@ -19,9 +20,10 @@ func TestGenerateHandler(t *testing.T) {
settings map[string]interface{}
expErr string
}{
"valid handler": {&Cfg{handler: map[string]Handler{}}, "test", map[string]interface{}{"handler": "docker_compose_run", "service": "app",}, ""},
"valid handler": {&Cfg{handler: map[string]Handler{}}, "test", map[string]interface{}{"handler": "docker_compose_run", "service": "app"}, ""},
"invalid handler": {&Cfg{}, "test", map[string]interface{}{"handler": "docker_compose_run"}, "error in strategy test: field service required but not set"},
"additional fields": {&Cfg{}, "foo", map[string]interface{}{"handler": "docker_compose_run", "service": "test", "other": "field"}, "error in strategy foo: additonal field(s) detected: other"},
"invalid type": {&Cfg{}, "foo", map[string]interface{}{"handler": "docker_compose_run", "service": true, "remove": "foo"}, "error in strategy foo: 2 error(s) decoding:\n\n* 'Remove' expected type 'bool', got unconvertible type 'string'\n* 'Service' expected type 'string', got unconvertible type 'bool'"},
}

for name, test := range tests {
Expand All @@ -37,24 +39,27 @@ func TestGenerateHandler(t *testing.T) {
}
}

func Test_GetHandlerFor(t *testing.T) {
cfg, err := GenerateConfig([]byte(fullYaml))
func TestGetHandlerFor(t *testing.T) {
cfg := GenerateConfig()
err := cfg.Load([]byte(fullYaml))
assert.NoError(t, err)

tests := map[string]struct {
command string
strictMode bool
expErr error
command string
strictMode bool
fallbackMode bool
expErr error
}{
"known cmd": {"ls", false, nil},
"unknown cmd": {"some-cmd", false, nil},
"unknown cmd, strict": {"some-cmd", true, ErrUndefinedCommand},
"custom cmd with path, strict": {"/bin/ls", true, nil},
"known cmd": {command: "ls"},
"unknown cmd": {command: "some-cmd"},
"known cmd with path, strict": {command: "ls", strictMode: true, fallbackMode: true},
"unknown cmd, strict,": {command: "some-cmd", strictMode: true, expErr: ErrUndefinedCommand},
"unknown cmd, strict, fallback": {command: "some-cmd", strictMode: true, expErr: ErrUndefinedCommand},
}

for name, test := range tests {
t.Run(name, func(t *testing.T) {
handler, err := cfg.GetHandlerFor(test.command, test.strictMode)
handler, err := cfg.GetHandlerFor(test.command, test.strictMode, test.fallbackMode)
if test.expErr != nil {
assert.EqualError(t, err, test.expErr.Error())
} else {
Expand Down

0 comments on commit 1e238fa

Please sign in to comment.