diff --git a/commands/command.go b/commands/command.go index 96d79214fb7eeb58bf4e49072eaf769ff17d11af..a98b1989a5c4c01089161d3d0dc3a0d6f052b0bb 100644 --- a/commands/command.go +++ b/commands/command.go @@ -1,120 +1,120 @@ package commands import ( - "fmt" - "strings" - "reflect" + "fmt" + "reflect" + "strings" ) type Command struct { - Help string - Options []Option - f func(*Request, *Response) - subcommands map[string]*Command + Help string + Options []Option + f func(*Request, *Response) + subcommands map[string]*Command } // Register adds a subcommand func (c *Command) Register(id string, sub *Command) error { - if c.subcommands == nil { - c.subcommands = make(map[string]*Command) - } - - // check for duplicate option names (only checks downwards) - names := make(map[string]bool) - globalCommand.checkOptions(names) - c.checkOptions(names) - err := sub.checkOptions(names) - if err != nil { - return err - } - - if _, ok := c.subcommands[id]; ok { - return fmt.Errorf("There is already a subcommand registered with id '%s'", id) - } - - c.subcommands[id] = sub - return nil + if c.subcommands == nil { + c.subcommands = make(map[string]*Command) + } + + // check for duplicate option names (only checks downwards) + names := make(map[string]bool) + globalCommand.checkOptions(names) + c.checkOptions(names) + err := sub.checkOptions(names) + if err != nil { + return err + } + + if _, ok := c.subcommands[id]; ok { + return fmt.Errorf("There is already a subcommand registered with id '%s'", id) + } + + c.subcommands[id] = sub + return nil } // Call invokes the command at the given subcommand path func (c *Command) Call(path []string, req *Request) *Response { - options := make([]Option, len(c.Options)) - copy(options, c.Options) - options = append(options, globalOptions...) - cmd := c - res := &Response{ req: req } - - if path != nil { - for i, id := range path { - cmd = c.Sub(id) - - if cmd == nil { - pathS := strings.Join(path[0:i], "/") - res.SetError(fmt.Errorf("Undefined command: '%s'", pathS), Client) - return res - } - - options = append(options, cmd.Options...) - } - } - - optionsMap := make(map[string]Option) - for _, opt := range options { - for _, name := range opt.Names { - optionsMap[name] = opt - } - } - - for k, v := range req.options { - opt, ok := optionsMap[k] - - if !ok { - res.SetError(fmt.Errorf("Unrecognized command option: '%s'", k), Client) - return res - } - - for _, name := range opt.Names { - if _, ok = req.options[name]; name != k && ok { - res.SetError(fmt.Errorf("Duplicate command options were provided ('%s' and '%s')", - k, name), Client) - return res - } - } - - kind := reflect.TypeOf(v).Kind() - if kind != opt.Type { - res.SetError(fmt.Errorf("Option '%s' should be type '%s', but got type '%s'", - k, opt.Type.String(), kind.String()), Client) - return res - } - } - - cmd.f(req, res) - - return res + options := make([]Option, len(c.Options)) + copy(options, c.Options) + options = append(options, globalOptions...) + cmd := c + res := &Response{req: req} + + if path != nil { + for i, id := range path { + cmd = c.Sub(id) + + if cmd == nil { + pathS := strings.Join(path[0:i], "/") + res.SetError(fmt.Errorf("Undefined command: '%s'", pathS), Client) + return res + } + + options = append(options, cmd.Options...) + } + } + + optionsMap := make(map[string]Option) + for _, opt := range options { + for _, name := range opt.Names { + optionsMap[name] = opt + } + } + + for k, v := range req.options { + opt, ok := optionsMap[k] + + if !ok { + res.SetError(fmt.Errorf("Unrecognized command option: '%s'", k), Client) + return res + } + + for _, name := range opt.Names { + if _, ok = req.options[name]; name != k && ok { + res.SetError(fmt.Errorf("Duplicate command options were provided ('%s' and '%s')", + k, name), Client) + return res + } + } + + kind := reflect.TypeOf(v).Kind() + if kind != opt.Type { + res.SetError(fmt.Errorf("Option '%s' should be type '%s', but got type '%s'", + k, opt.Type.String(), kind.String()), Client) + return res + } + } + + cmd.f(req, res) + + return res } // Sub returns the subcommand with the given id func (c *Command) Sub(id string) *Command { - return c.subcommands[id] + return c.subcommands[id] } func (c *Command) checkOptions(names map[string]bool) error { - for _, opt := range c.Options { - for _, name := range opt.Names { - if _, ok := names[name]; ok { - return fmt.Errorf("Multiple options are using the same name ('%s')", name) - } - names[name] = true - } - } - - for _, cmd := range c.subcommands { - err := cmd.checkOptions(names) - if err != nil { - return err - } - } - - return nil + for _, opt := range c.Options { + for _, name := range opt.Names { + if _, ok := names[name]; ok { + return fmt.Errorf("Multiple options are using the same name ('%s')", name) + } + names[name] = true + } + } + + for _, cmd := range c.subcommands { + err := cmd.checkOptions(names) + if err != nil { + return err + } + } + + return nil } diff --git a/commands/command_test.go b/commands/command_test.go index 95667afee32e5a8abb201bb78ae996e483767014..44586b6a3237762e63074ce92d715c75c8aa2637 100644 --- a/commands/command_test.go +++ b/commands/command_test.go @@ -3,122 +3,122 @@ package commands import "testing" func TestOptionValidation(t *testing.T) { - cmd := Command{ - Options: []Option{ - Option{ []string{ "b", "beep" }, Int }, - Option{ []string{ "B", "boop" }, String }, - }, - f: func(req *Request, res *Response) {}, - } - - req := NewRequest() - req.options["foo"] = 5 - res := cmd.Call(nil, req) - if res.Error == nil { - t.Error("Should have failed (unrecognized command)") - } - - req = NewRequest() - req.options["beep"] = 5 - req.options["b"] = 10 - res = cmd.Call(nil, req) - if res.Error == nil { - t.Error("Should have failed (duplicate options)") - } - - req = NewRequest() - req.options["beep"] = "foo" - res = cmd.Call(nil, req) - if res.Error == nil { - t.Error("Should have failed (incorrect type)") - } - - req = NewRequest() - req.options["beep"] = 5 - res = cmd.Call(nil, req) - if res.Error != nil { - t.Error("Should have passed") - } - - req = NewRequest() - req.options["beep"] = 5 - req.options["boop"] = "test" - res = cmd.Call(nil, req) - if res.Error != nil { - t.Error("Should have passed") - } - - req = NewRequest() - req.options["b"] = 5 - req.options["B"] = "test" - res = cmd.Call(nil, req) - if res.Error != nil { - t.Error("Should have passed") - } - - req = NewRequest() - req.options["enc"] = "json" - res = cmd.Call(nil, req) - if res.Error != nil { - t.Error("Should have passed") - } + cmd := Command{ + Options: []Option{ + Option{[]string{"b", "beep"}, Int}, + Option{[]string{"B", "boop"}, String}, + }, + f: func(req *Request, res *Response) {}, + } + + req := NewRequest() + req.options["foo"] = 5 + res := cmd.Call(nil, req) + if res.Error == nil { + t.Error("Should have failed (unrecognized command)") + } + + req = NewRequest() + req.options["beep"] = 5 + req.options["b"] = 10 + res = cmd.Call(nil, req) + if res.Error == nil { + t.Error("Should have failed (duplicate options)") + } + + req = NewRequest() + req.options["beep"] = "foo" + res = cmd.Call(nil, req) + if res.Error == nil { + t.Error("Should have failed (incorrect type)") + } + + req = NewRequest() + req.options["beep"] = 5 + res = cmd.Call(nil, req) + if res.Error != nil { + t.Error("Should have passed") + } + + req = NewRequest() + req.options["beep"] = 5 + req.options["boop"] = "test" + res = cmd.Call(nil, req) + if res.Error != nil { + t.Error("Should have passed") + } + + req = NewRequest() + req.options["b"] = 5 + req.options["B"] = "test" + res = cmd.Call(nil, req) + if res.Error != nil { + t.Error("Should have passed") + } + + req = NewRequest() + req.options["enc"] = "json" + res = cmd.Call(nil, req) + if res.Error != nil { + t.Error("Should have passed") + } } func TestRegistration(t *testing.T) { - cmds := []*Command{ - &Command{ - Options: []Option{ - Option{ []string{ "beep" }, Int }, - }, - f: func(req *Request, res *Response) {}, - }, - - &Command{ - Options: []Option{ - Option{ []string{ "boop" }, Int }, - }, - f: func(req *Request, res *Response) {}, - }, - - &Command{ - Options: []Option{ - Option{ []string{ "boop" }, String }, - }, - f: func(req *Request, res *Response) {}, - }, - - &Command{ - Options: []Option{ - Option{ []string{ "bop" }, String }, - }, - f: func(req *Request, res *Response) {}, - }, - - &Command{ - Options: []Option{ - Option{ []string{ "enc" }, String }, - }, - f: func(req *Request, res *Response) {}, - }, - } - - err := cmds[0].Register("foo", cmds[1]) - if err != nil { - t.Error("Should have passed") - } - - err = cmds[0].Register("bar", cmds[2]) - if err == nil { - t.Error("Should have failed (option name collision)") - } - - err = cmds[0].Register("foo", cmds[3]) - if err == nil { - t.Error("Should have failed (subcommand name collision)") - } - - err = cmds[0].Register("baz", cmds[4]) - if err == nil { - t.Error("Should have failed (option name collision with global options)") - } + cmds := []*Command{ + &Command{ + Options: []Option{ + Option{[]string{"beep"}, Int}, + }, + f: func(req *Request, res *Response) {}, + }, + + &Command{ + Options: []Option{ + Option{[]string{"boop"}, Int}, + }, + f: func(req *Request, res *Response) {}, + }, + + &Command{ + Options: []Option{ + Option{[]string{"boop"}, String}, + }, + f: func(req *Request, res *Response) {}, + }, + + &Command{ + Options: []Option{ + Option{[]string{"bop"}, String}, + }, + f: func(req *Request, res *Response) {}, + }, + + &Command{ + Options: []Option{ + Option{[]string{"enc"}, String}, + }, + f: func(req *Request, res *Response) {}, + }, + } + + err := cmds[0].Register("foo", cmds[1]) + if err != nil { + t.Error("Should have passed") + } + + err = cmds[0].Register("bar", cmds[2]) + if err == nil { + t.Error("Should have failed (option name collision)") + } + + err = cmds[0].Register("foo", cmds[3]) + if err == nil { + t.Error("Should have failed (subcommand name collision)") + } + + err = cmds[0].Register("baz", cmds[4]) + if err == nil { + t.Error("Should have failed (option name collision with global options)") + } } diff --git a/commands/option.go b/commands/option.go index e14b1bf68ececa43c0bf0ebfcaa809bcbf3b3106..9cf23162b5a4b1cc9be2aa8306005a5e8239cfa2 100644 --- a/commands/option.go +++ b/commands/option.go @@ -3,29 +3,30 @@ package commands import "reflect" const ( - Invalid = reflect.Invalid - Bool = reflect.Bool - Int = reflect.Int - Uint = reflect.Uint - Float = reflect.Float32 - String = reflect.String + Invalid = reflect.Invalid + Bool = reflect.Bool + Int = reflect.Int + Uint = reflect.Uint + Float = reflect.Float32 + String = reflect.String ) // Option is used to specify a field that will be provided by a consumer type Option struct { - Names []string // a list of unique names to - Type reflect.Kind // value must be this type + Names []string // a list of unique names to + Type reflect.Kind // value must be this type - // TODO: add more features(?): - //Default interface{} // the default value (ignored if `Required` is true) - //Required bool // whether or not the option must be provided + // TODO: add more features(?): + //Default interface{} // the default value (ignored if `Required` is true) + //Required bool // whether or not the option must be provided } // options that are used by this package var globalOptions []Option = []Option{ - Option{ []string{ "enc", "encoding" }, String }, + Option{[]string{"enc", "encoding"}, String}, } + // the above array of Options, wrapped in a Command var globalCommand *Command = &Command{ - Options: globalOptions, + Options: globalOptions, } diff --git a/commands/request.go b/commands/request.go index 7373a1a243acffd86e224e0d1726a35513dd847d..0d47c006bb0a61d34f8a07a3c3c32d87cc099267 100644 --- a/commands/request.go +++ b/commands/request.go @@ -2,29 +2,29 @@ package commands // Request represents a call to a command from a consumer type Request struct { - options map[string]interface{} - arguments []string + options map[string]interface{} + arguments []string } func (r *Request) Option(name string) interface{} { - return r.options[name] + return r.options[name] } func (r *Request) SetOption(option Option, value interface{}) { - // saves the option value in the map, indexed by each name - // (so commands can retrieve it using any of the names) - for _, name := range option.Names { - r.options[name] = value - } + // saves the option value in the map, indexed by each name + // (so commands can retrieve it using any of the names) + for _, name := range option.Names { + r.options[name] = value + } } func (r *Request) Arguments() []string { - return r.arguments + return r.arguments } func NewRequest() *Request { - return &Request{ - make(map[string]interface{}), - make([]string, 0), - } + return &Request{ + make(map[string]interface{}), + make([]string, 0), + } } diff --git a/commands/response.go b/commands/response.go index 07e91d12ff2e31d007fb4c3c052d786c7debcb8a..6edaf1a59c24c27b8d673474434b47c245455530 100644 --- a/commands/response.go +++ b/commands/response.go @@ -1,74 +1,77 @@ package commands import ( - "fmt" - "strings" - "encoding/json" - "encoding/xml" + "encoding/json" + "encoding/xml" + "fmt" + "strings" ) type ErrorType uint + const ( - Normal ErrorType = iota // general errors - Client // error was caused by the client, (e.g. invalid CLI usage) - // TODO: add more types of errors for better error-specific handling + Normal ErrorType = iota // general errors + Client // error was caused by the client, (e.g. invalid CLI usage) + // TODO: add more types of errors for better error-specific handling ) // Error is a struct for marshalling errors type Error struct { - Message string - Code ErrorType + Message string + Code ErrorType } type EncodingType string + const ( - Json = "json" - Xml = "xml" - // TODO: support more encoding types + Json = "json" + Xml = "xml" + // TODO: support more encoding types ) -type Marshaller func(v interface{})([]byte, error) +type Marshaller func(v interface{}) ([]byte, error) + var marshallers = map[EncodingType]Marshaller{ - Json: json.Marshal, - Xml: xml.Marshal, + Json: json.Marshal, + Xml: xml.Marshal, } type Response struct { - req *Request - Error error - ErrorType ErrorType - Value interface{} + req *Request + Error error + ErrorType ErrorType + Value interface{} } func (r *Response) SetError(err error, errType ErrorType) { - r.Error = err - r.ErrorType = errType + r.Error = err + r.ErrorType = errType } func (r *Response) FormatError() Error { - return Error{ r.Error.Error(), r.ErrorType } + return Error{r.Error.Error(), r.ErrorType} } func (r *Response) Marshal() ([]byte, error) { - if r.Error == nil && r.Value == nil { - return nil, fmt.Errorf("No error or value set, there is nothing to marshal") - } + if r.Error == nil && r.Value == nil { + return nil, fmt.Errorf("No error or value set, there is nothing to marshal") + } - enc := r.req.Option("enc") - if enc == nil { - return nil, fmt.Errorf("No encoding type was specified") - } - encType := EncodingType(strings.ToLower(enc.(string))) + enc := r.req.Option("enc") + if enc == nil { + return nil, fmt.Errorf("No encoding type was specified") + } + encType := EncodingType(strings.ToLower(enc.(string))) - marshaller, ok := marshallers[encType] - if !ok { - return nil, fmt.Errorf("No marshaller found for encoding type '%s'", enc) - } + marshaller, ok := marshallers[encType] + if !ok { + return nil, fmt.Errorf("No marshaller found for encoding type '%s'", enc) + } - if r.Error != nil { - err := r.FormatError() - return marshaller(err) - } else { - return marshaller(r.Value) - } + if r.Error != nil { + err := r.FormatError() + return marshaller(err) + } else { + return marshaller(r.Value) + } } diff --git a/commands/response_test.go b/commands/response_test.go index e1a96676d23078dd35bb1285ed6be8a5e00f316d..35f8e488f5efe3294dd1a913f5ae664d5131577c 100644 --- a/commands/response_test.go +++ b/commands/response_test.go @@ -1,45 +1,45 @@ package commands import ( - "testing" - "fmt" + "fmt" + "testing" ) type TestOutput struct { - Foo, Bar string - Baz int + Foo, Bar string + Baz int } func TestMarshalling(t *testing.T) { - req := NewRequest() + req := NewRequest() - res := Response{ - req: req, - Value: TestOutput{ "beep", "boop", 1337 }, - } + res := Response{ + req: req, + Value: TestOutput{"beep", "boop", 1337}, + } - _, err := res.Marshal() - if err == nil { - t.Error("Should have failed (no encoding type specified in request)") - } + _, err := res.Marshal() + if err == nil { + t.Error("Should have failed (no encoding type specified in request)") + } - req.SetOption(globalOptions[0], Json) - bytes, err := res.Marshal() - if err != nil { - t.Error("Should have passed") - } - output := string(bytes) - if output != "{\"Foo\":\"beep\",\"Bar\":\"boop\",\"Baz\":1337}" { - t.Error("Incorrect JSON output") - } + req.SetOption(globalOptions[0], Json) + bytes, err := res.Marshal() + if err != nil { + t.Error("Should have passed") + } + output := string(bytes) + if output != "{\"Foo\":\"beep\",\"Bar\":\"boop\",\"Baz\":1337}" { + t.Error("Incorrect JSON output") + } - res.SetError(fmt.Errorf("You broke something!"), Client) - bytes, err = res.Marshal() - if err != nil { - t.Error("Should have passed") - } - output = string(bytes) - if output != "{\"Message\":\"You broke something!\",\"Code\":1}" { - t.Error("Incorrect JSON output") - } + res.SetError(fmt.Errorf("You broke something!"), Client) + bytes, err = res.Marshal() + if err != nil { + t.Error("Should have passed") + } + output = string(bytes) + if output != "{\"Message\":\"You broke something!\",\"Code\":1}" { + t.Error("Incorrect JSON output") + } }