Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Command Interface #150

Merged
merged 73 commits into from
Oct 22, 2014
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
73 commits
Select commit Hold shift + click to select a range
dd2a105
commands: Implemented Command
mappum Oct 8, 2014
30ea427
commands: Created Option struct
mappum Oct 8, 2014
15b7388
commands: Request struct
mappum Oct 8, 2014
dd68296
commands: Wrote tests for command option validation
mappum Oct 8, 2014
5b18844
commands: Check for option name collisions
mappum Oct 9, 2014
e593c18
commands: Added tests for Command.Register
mappum Oct 9, 2014
47ebf17
commands: Created a list of global options (for options owned by comm…
mappum Oct 10, 2014
d7e9afc
commands: Use global options when registering and calling commands
mappum Oct 10, 2014
97ce60f
commands: Added global options list to command tests
mappum Oct 10, 2014
f31fd53
commands: Added Response
mappum Oct 10, 2014
b2ee05a
commands: Updated Command to use Response for output rather than (int…
mappum Oct 10, 2014
df034c9
commands: Updated Command tests for new Response API
mappum Oct 10, 2014
d1595ce
commands: Added basic methods to Request
mappum Oct 10, 2014
95b0dd2
commands: Added an Error struct for creating marshallable errors
mappum Oct 10, 2014
a3a8437
commands: Added marshalling to Response
mappum Oct 10, 2014
808d9c1
commands: Wrote tests for Response marshalling
mappum Oct 10, 2014
308ee5c
commands: Added Request#SetOption so we can set options with multiple…
mappum Oct 10, 2014
01938ac
commands: Updated Response test to use safer option setting
mappum Oct 10, 2014
aa592ce
commands: Added error marshalling to Response
mappum Oct 10, 2014
94ca264
commands: Added test for Response error marshalling
mappum Oct 10, 2014
4367097
commands: Formatted code
mappum Oct 10, 2014
bf32818
commands/cli: Added CLI option parsing
mappum Oct 14, 2014
b3eecf4
commands/cli: Added simple option parser test
mappum Oct 14, 2014
4bd3a77
commands/cli: Added path/args parsing
mappum Oct 14, 2014
f437230
commands/cli: Added path/args test
mappum Oct 14, 2014
1b35615
commands: Made Command#GetOption method, for getting all options for …
mappum Oct 14, 2014
66b0727
commands/cli: Renamed parse functions to parse*
mappum Oct 14, 2014
bb32633
commands/cli: Refactored parsing to always get the command path at th…
mappum Oct 14, 2014
08885c0
commands/cli: Fixed tests for refactor
mappum Oct 14, 2014
66e6da3
commands/cli: Added value parsing for single-dash options
mappum Oct 14, 2014
5d9fa93
commands/cli: Added test for single-dash option value
mappum Oct 14, 2014
97b8719
commands/cli: Removed parser string handling since the go runtime han…
mappum Oct 14, 2014
86bc450
commands/cli: Pass option definitions as an argument to parseOptions
mappum Oct 14, 2014
793a8de
commands: Refactored to make Request contain command path
mappum Oct 14, 2014
e1a4b8d
commands: Added Request#SetPath method
mappum Oct 14, 2014
c575b50
commands: Added option value conversion, and moved option validation …
mappum Oct 15, 2014
1e8719e
commands: Fixed tests
mappum Oct 15, 2014
47eea7f
commands: Added a option validation test for convertible string values
mappum Oct 15, 2014
7a36278
commands: Allow setting Request fields in NewRequest
mappum Oct 15, 2014
968ec34
commands/cli: Made Parse return a Request object instead of separate …
mappum Oct 15, 2014
09311d4
commands: Added 'NewEmptyRequest'
mappum Oct 16, 2014
4b0f44e
commands: Fixed tests
mappum Oct 16, 2014
4af61ad
commands: Added Command#Resolve
mappum Oct 16, 2014
c054fb3
commands: Added simple Command#Resolve test
mappum Oct 16, 2014
d2176c0
commands: Added Command#Get
mappum Oct 16, 2014
4f06c6f
commands: Formatted code
mappum Oct 16, 2014
e5e121a
commands: Made Request#Option also return an existence bool
mappum Oct 16, 2014
f87c418
commands/cli: Refactored CLI parsing to match go tooling conventions
mappum Oct 18, 2014
b48b12e
commands/cli: Fixed test for new parsing
mappum Oct 18, 2014
117af86
commands/cli: Error if there are duplicate values for an option
mappum Oct 18, 2014
a9fa767
commands/cli: Added test for detecting duplicate options
mappum Oct 18, 2014
7673ce6
fmt, lint, + vet commands/
jbenet Oct 20, 2014
09d2277
f -> run, Function type.
jbenet Oct 20, 2014
84fa7bc
AddOptionNames func
jbenet Oct 20, 2014
92528ba
Sub -> Subcommand
jbenet Oct 20, 2014
4986600
parsePath no err
jbenet Oct 20, 2014
bbef82f
"enc" -> EncShort
jbenet Oct 20, 2014
b10fc2c
turned req + res into interfaces
jbenet Oct 20, 2014
c0b28dc
commands: Added input stream field to Request
mappum Oct 20, 2014
7bd7ed6
commands: Added output stream field to Response
mappum Oct 20, 2014
b022ba4
commands: Fixed tests
mappum Oct 20, 2014
71ff571
commands/cli: Made Parse return component fields instead of a Request
mappum Oct 21, 2014
4896123
commands: Export command Run function
mappum Oct 21, 2014
8786878
commands: Fixed tests
mappum Oct 21, 2014
b65a5ba
commands: Made Error implement error interface
mappum Oct 21, 2014
dd84a3e
commands: Got rid of Response#Stream() in favor of setting value to a…
mappum Oct 21, 2014
4f10f03
commands: Fixed tests
mappum Oct 21, 2014
6ff98df
commands: Do command collision check in GetOptions
mappum Oct 22, 2014
ca44d0d
commands: Removed Command#Register and exported Subcommands so subcom…
mappum Oct 22, 2014
dd81bf6
commands: Fixed tests
mappum Oct 22, 2014
d464e3d
commands: go fmt
jbenet Oct 21, 2014
12a6a87
commands/cli: Made Parse return a Request (again)
mappum Oct 22, 2014
4303dcc
commands: Added Request#SetStream
mappum Oct 22, 2014
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions commands/cli/parse.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package cli

import (
"fmt"
"strings"

"github.com/jbenet/go-ipfs/commands"
)

// Parse parses the input commandline string (cmd, flags, and args).
// returns the corresponding command Request object.
func Parse(input []string, root *commands.Command) (commands.Request, error) {
path, input := parsePath(input, root)
opts, args, err := parseOptions(input)
if err != nil {
return nil, err
}

return commands.NewRequest(path, opts, args, nil), nil
}

// parsePath gets the command path from the command line input
func parsePath(input []string, root *commands.Command) ([]string, []string) {
cmd := root
i := 0

for _, blob := range input {
if strings.HasPrefix(blob, "-") {
break
}

cmd := cmd.Subcommand(blob)
if cmd == nil {
break
}

i++
}

return input[:i], input[i:]
}

// parseOptions parses the raw string values of the given options
// returns the parsed options as strings, along with the CLI args
func parseOptions(input []string) (map[string]interface{}, []string, error) {
opts := make(map[string]interface{})
args := []string{}

for i := 0; i < len(input); i++ {
blob := input[i]

if strings.HasPrefix(blob, "-") {
name := blob[1:]
value := ""

// support single and double dash
if strings.HasPrefix(name, "-") {
name = name[1:]
}

if strings.Contains(name, "=") {
split := strings.SplitN(name, "=", 2)
name = split[0]
value = split[1]
}

if _, ok := opts[name]; ok {
return nil, nil, fmt.Errorf("Duplicate values for option '%s'", name)
}

opts[name] = value

} else {
args = append(args, blob)
}
}

return opts, args, nil
}
47 changes: 47 additions & 0 deletions commands/cli/parse_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package cli

import (
//"fmt"
"testing"

"github.com/jbenet/go-ipfs/commands"
)

func TestOptionParsing(t *testing.T) {
cmd := &commands.Command{
Options: []commands.Option{
commands.Option{Names: []string{"b"}, Type: commands.String},
},
Subcommands: map[string]*commands.Command{
"test": &commands.Command{},
},
}

opts, input, err := parseOptions([]string{"--beep", "-boop=lol", "test2", "-c", "beep", "--foo=5"})
/*for k, v := range opts {
fmt.Printf("%s: %s\n", k, v)
}
fmt.Printf("%s\n", input)*/
if err != nil {
t.Error("Should have passed")
}
if len(opts) != 4 || opts["beep"] != "" || opts["boop"] != "lol" || opts["c"] != "" || opts["foo"] != "5" {
t.Error("Returned options were defferent than expected: %v", opts)
}
if len(input) != 2 || input[0] != "test2" || input[1] != "beep" {
t.Error("Returned input was different than expected: %v", input)
}

_, _, err = parseOptions([]string{"-beep=1", "-boop=2", "-beep=3"})
if err == nil {
t.Error("Should have failed (duplicate option name)")
}

path, args := parsePath([]string{"test", "beep", "boop"}, cmd)
if len(path) != 1 || path[0] != "test" {
t.Error("Returned path was defferent than expected: %v", path)
}
if len(args) != 2 || args[0] != "beep" || args[1] != "boop" {
t.Error("Returned args were different than expected: %v", args)
}
}
122 changes: 122 additions & 0 deletions commands/command.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package commands

import (
"errors"
"fmt"
"strings"

u "github.com/jbenet/go-ipfs/util"
)

var log = u.Logger("command")

// Function is the type of function that Commands use.
// It reads from the Request, and writes results to the Response.
type Function func(Request, Response)

// Command is a runnable command, with input arguments and options (flags).
// It can also have Subcommands, to group units of work into sets.
type Command struct {
Help string
Options []Option
Run Function
Subcommands map[string]*Command
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • f -> function (meaningful var name at object level)
  • make a type for the function
  • comments on exported symbols (golint)
// Function is the type of function that Commands use. 
// It reads from the Request, and writes results to the Response.
type Function func(*Request, *Response)

// Command is a runnable command, with input arguments and options (flags).
// It can also have subcommands, to group units of work into sets.
type Command struct {
    Help        string
    Options     []Option

    function    Function
    subcommands map[string]*Command
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

addressed


// ErrNotCallable signals a command that cannot be called.
var ErrNotCallable = errors.New("This command can't be called directly. Try one of its subcommands.")

// Call invokes the command for the given Request
func (c *Command) Call(req Request) Response {
res := NewResponse(req)

cmds, err := c.Resolve(req.Path())
if err != nil {
res.SetError(err, ErrClient)
return res
}
cmd := cmds[len(cmds)-1]

if cmd.Run == nil {
res.SetError(ErrNotCallable, ErrClient)
return res
}

options, err := c.GetOptions(req.Path())
if err != nil {
res.SetError(err, ErrClient)
return res
}

err = req.ConvertOptions(options)
if err != nil {
res.SetError(err, ErrClient)
return res
}

cmd.Run(req, res)

return res
}

// Resolve gets the subcommands at the given path
func (c *Command) Resolve(path []string) ([]*Command, error) {
cmds := make([]*Command, len(path)+1)
cmds[0] = c

cmd := c
for i, name := range path {
cmd = cmd.Subcommand(name)

if cmd == nil {
pathS := strings.Join(path[0:i], "/")
return nil, fmt.Errorf("Undefined command: '%s'", pathS)
}

cmds[i+1] = cmd
}

return cmds, nil
}

// Get resolves and returns the Command addressed by path
func (c *Command) Get(path []string) (*Command, error) {
cmds, err := c.Resolve(path)
if err != nil {
return nil, err
}
return cmds[len(cmds)-1], nil
}

// GetOptions gets the options in the given path of commands
func (c *Command) GetOptions(path []string) (map[string]Option, error) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 i like this

options := make([]Option, len(c.Options))

cmds, err := c.Resolve(path)
if err != nil {
return nil, err
}
cmds = append(cmds, globalCommand)

for _, cmd := range cmds {
options = append(options, cmd.Options...)
}

optionsMap := make(map[string]Option)
for _, opt := range options {
for _, name := range opt.Names {
if _, found := optionsMap[name]; found {
return nil, fmt.Errorf("Option name '%s' used multiple times", name)
}

optionsMap[name] = opt
}
}

return optionsMap, nil
}

// Subcommand returns the subcommand with the given id
func (c *Command) Subcommand(id string) *Command {
return c.Subcommands[id]
}
146 changes: 146 additions & 0 deletions commands/command_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
package commands

import "testing"

func TestOptionValidation(t *testing.T) {
cmd := Command{
Options: []Option{
Option{[]string{"b", "beep"}, Int},
Option{[]string{"B", "boop"}, String},
},
Run: func(req Request, res Response) {},
}

req := NewEmptyRequest()
req.SetOption("foo", 5)
res := cmd.Call(req)
if res.Error() == nil {
t.Error("Should have failed (unrecognized option)")
}

req = NewEmptyRequest()
req.SetOption("beep", 5)
req.SetOption("b", 10)
res = cmd.Call(req)
if res.Error() == nil {
t.Error("Should have failed (duplicate options)")
}

req = NewEmptyRequest()
req.SetOption("beep", "foo")
res = cmd.Call(req)
if res.Error() == nil {
t.Error("Should have failed (incorrect type)")
}

req = NewEmptyRequest()
req.SetOption("beep", 5)
res = cmd.Call(req)
if res.Error() != nil {
t.Error(res.Error(), "Should have passed")
}

req = NewEmptyRequest()
req.SetOption("beep", 5)
req.SetOption("boop", "test")
res = cmd.Call(req)
if res.Error() != nil {
t.Error("Should have passed")
}

req = NewEmptyRequest()
req.SetOption("b", 5)
req.SetOption("B", "test")
res = cmd.Call(req)
if res.Error() != nil {
t.Error("Should have passed")
}

req = NewEmptyRequest()
req.SetOption(EncShort, "json")
res = cmd.Call(req)
if res.Error() != nil {
t.Error("Should have passed")
}

req = NewEmptyRequest()
req.SetOption("b", "100")
res = cmd.Call(req)
if res.Error() != nil {
t.Error("Should have passed")
}

req = NewEmptyRequest()
req.SetOption("b", ":)")
res = cmd.Call(req)
if res.Error == nil {
t.Error(res.Error, "Should have failed (string value not convertible to int)")
}
}

func TestRegistration(t *testing.T) {
noop := func(req Request, res Response) {}

cmdA := &Command{
Options: []Option{
Option{[]string{"beep"}, Int},
},
Run: noop,
}

cmdB := &Command{
Options: []Option{
Option{[]string{"beep"}, Int},
},
Run: noop,
Subcommands: map[string]*Command{
"a": cmdA,
},
}

cmdC := &Command{
Options: []Option{
Option{[]string{"encoding"}, String},
},
Run: noop,
}

res := cmdB.Call(NewRequest([]string{"a"}, nil, nil, nil))
if res.Error() == nil {
t.Error("Should have failed (option name collision)")
}

res = cmdC.Call(NewEmptyRequest())
if res.Error() == nil {
t.Error("Should have failed (option name collision with global options)")
}
}

func TestResolving(t *testing.T) {
cmdC := &Command{}
cmdB := &Command{
Subcommands: map[string]*Command{
"c": cmdC,
},
}
cmdB2 := &Command{}
cmdA := &Command{
Subcommands: map[string]*Command{
"b": cmdB,
"B": cmdB2,
},
}
cmd := &Command{
Subcommands: map[string]*Command{
"a": cmdA,
},
}

cmds, err := cmd.Resolve([]string{"a", "b", "c"})
if err != nil {
t.Error(err)
}
if len(cmds) != 4 || cmds[0] != cmd || cmds[1] != cmdA || cmds[2] != cmdB || cmds[3] != cmdC {
t.Error("Returned command path is different than expected", cmds)
}
}
Loading