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

Commands Refactor Part 1 #262

Merged
merged 66 commits into from
Nov 4, 2014
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
19e5fba
commands: Made Response#Error return an *Error instead of an error
mappum Oct 22, 2014
94683bb
commands: Removed unused output stream field from Response
mappum Oct 23, 2014
3cebd21
commands: Made Response implement io.Reader
mappum Oct 23, 2014
e8b37ac
commands: Simplified Error string output
mappum Oct 23, 2014
a65c99e
commands: Added Request#Options so consumers can iterate through prov…
mappum Oct 23, 2014
8b61daa
commands/http: Moved HTTP RPC into commands/http package
mappum Oct 24, 2014
43c61a4
commands/http: Moved HTTP RPC handler into commands/http
mappum Oct 24, 2014
53e1359
commands: Silently skip unrecognized options, it should be up to the …
mappum Oct 27, 2014
b033e33
commands: Fixed test
mappum Oct 27, 2014
20f86c0
commands: Added Context() to Request
mappum Oct 28, 2014
48bd73c
commands/http: Made client load RPC host from config
mappum Oct 28, 2014
289bce6
commands: Added Request#SetContext()
mappum Oct 28, 2014
29b96b6
commands/http: Made Handler set request contexts
mappum Oct 28, 2014
52bc8bd
commands/http: Moved http request parsing into a Parse function
mappum Oct 29, 2014
7531625
commands/http: Parse command args
mappum Oct 29, 2014
42633e5
commands/http: Send request arguments in client requests
mappum Oct 29, 2014
507192e
commands/http: Use request body as command input stream
mappum Oct 29, 2014
81f2925
commands: Gave Requests a reference to the command they are being cal…
mappum Oct 29, 2014
38f8f1c
commands: Added a Format function to Command, for creating human-read…
mappum Oct 29, 2014
b54801c
commands: Added plaintext marshalling to Response#Marshal()
mappum Oct 29, 2014
714e13b
commands/http: Explicitly define the MIME types for each encoding
mappum Oct 29, 2014
32a2959
commands/http: Don't try to parse HTTP response into a Response, just…
mappum Oct 29, 2014
6302356
commands: Fixed tests
mappum Oct 29, 2014
ef290fa
commands: Added a Type field for defining output struct formats
mappum Oct 29, 2014
c0a9871
commands/http: Made HTTP client unmarshal response values based on th…
mappum Oct 29, 2014
b1bf60b
fix(cmd/ipfs2, commands) imports
Oct 29, 2014
4911dc0
refactor(commands) swap argument order to match Http(w, r) idiom
Oct 29, 2014
b61cfd9
commands: Don't error when marshalling empty Responses
mappum Oct 29, 2014
460387f
commands: Added 'Private' field to Command
mappum Oct 31, 2014
1e0cabd
commands/http: Pass root command in as field instead of statically de…
mappum Oct 31, 2014
ea09268
commands/http: 404 when trying to call root command
mappum Oct 31, 2014
abcebb0
commands/http: Improved client error handling
mappum Oct 31, 2014
cfa56dd
commands/http: Error if trying to run private command
mappum Oct 31, 2014
c0d3edd
commands/cli: Made Parse handle multiple root commands
mappum Oct 31, 2014
83b2ba0
commands: Removed Command#Private field
mappum Oct 31, 2014
30e9687
commands/cli: Error if no subcommand matched
mappum Oct 31, 2014
827f1dd
commands: Changed Request arguments to a []interface{}
mappum Nov 3, 2014
3a8d60c
commands: Removed inpout stream from Request
mappum Nov 3, 2014
39c78fb
commands: Fixed tests
mappum Nov 3, 2014
40858b4
commands/http: Added stream argument handling to client and request p…
mappum Nov 3, 2014
f7aa2b9
commands: Renamed ArgPath to ArgFile
mappum Nov 3, 2014
ee2c769
commands/cli: Open argument files when creating Requests (Moved out o…
mappum Nov 3, 2014
e8d0cbf
commands: Check argument validity when running commands
mappum Nov 3, 2014
2c8fc85
commands/cli: Made parser handle variadic arguments
mappum Nov 3, 2014
405cfd9
commands/http: Made parser/client handle variadic arguments
mappum Nov 3, 2014
586a019
commands: Fixed Request#CheckArguments not erroring when required arg…
mappum Nov 3, 2014
dbeffb6
commands: CLI Parse: Don't parse args until after creating request
mappum Nov 3, 2014
75649f3
commands: Moved argument checking into a Command method, fail early w…
mappum Nov 3, 2014
69a56de
commands: Renamed Response#Value to Response#Output
mappum Nov 3, 2014
f6c3888
commands: Return a reader in a Response#Reader method, instead of mak…
mappum Nov 4, 2014
bc6938d
commands: Cleaned up argument validation
mappum Nov 4, 2014
446acdc
commands/http: Ensure request URLs start with expected prefix
mappum Nov 4, 2014
4552fce
commands/http: Respond with error if encoding option isn't a string
mappum Nov 4, 2014
f76048f
commands/http: Unexported Handler fields and created constructor
mappum Nov 4, 2014
958e524
commands: Nicer syntax for Argument definition
mappum Nov 4, 2014
068e10c
commands/cli: Better comment for parsePath
mappum Nov 4, 2014
0149f65
commands: Replaced 'Formatter' with 'Marshaller'
mappum Nov 4, 2014
33ad56e
commands: Safer type coercion when choosing marshaller
mappum Nov 4, 2014
2a1116c
commands: Allow overriding marshaller for any encoding type
mappum Nov 4, 2014
5e5d534
commands/http: Refactored API to a Client object that takes a string …
mappum Nov 4, 2014
33b0990
commands/http: Cleaner URL formation in client
mappum Nov 4, 2014
e57cd9b
commands/http: Use net/url querystring encoder
mappum Nov 4, 2014
c8ae4b6
commands/http: Decomposed Client#Send function
mappum Nov 4, 2014
df6c700
commands/http: Renamed variable for clarity
mappum Nov 4, 2014
db9c7f7
commands: Fixed panic when trying to marshal without a command set in…
mappum Nov 4, 2014
1b9b603
commands/http: Cleaned up argument handling in Parse
mappum Nov 4, 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
15 changes: 15 additions & 0 deletions commands/argument.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package commands

type ArgumentType int

const (
ArgString ArgumentType = iota
ArgFile
)

type Argument struct {
Name string
Type ArgumentType
Required bool
Variadic bool
}
84 changes: 73 additions & 11 deletions commands/cli/parse.go
Original file line number Diff line number Diff line change
@@ -1,26 +1,63 @@
package cli

import (
"errors"
"fmt"
"os"
"strings"

"github.com/jbenet/go-ipfs/commands"
cmds "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)
func Parse(input []string, roots ...*cmds.Command) (cmds.Request, *cmds.Command, error) {
var root, cmd *cmds.Command
var path, stringArgs []string
var opts map[string]interface{}

// use the root that matches the longest path (most accurately matches request)
maxLength := 0
for _, r := range roots {
p, i, c := parsePath(input, r)
o, s, err := parseOptions(i)
if err != nil {
return nil, nil, err
}

length := len(p)
if length > maxLength {
maxLength = length
root = r
path = p
cmd = c
opts = o
stringArgs = s
}
}

if maxLength == 0 {
return nil, nil, errors.New("Not a valid subcommand")
}

args, err := parseArgs(stringArgs, cmd)
if err != nil {
return nil, nil, err
}

req := cmds.NewRequest(path, opts, args, cmd)

err = cmd.CheckArguments(req)
if err != nil {
return nil, err
return nil, nil, err
}

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

// parsePath gets the command path from the command line input
func parsePath(input []string, root *commands.Command) ([]string, []string) {
// parsePath separates the command path and the opts and args from a command string
// returns command path slice, rest slice, and the corresponding *cmd.Command
func parsePath(input []string, root *cmds.Command) ([]string, []string, *cmds.Command) {
cmd := root
i := 0

Expand All @@ -29,15 +66,16 @@ func parsePath(input []string, root *commands.Command) ([]string, []string) {
break
}

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

i++
}

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

// parseOptions parses the raw string values of the given options
Expand Down Expand Up @@ -77,3 +115,27 @@ func parseOptions(input []string) (map[string]interface{}, []string, error) {

return opts, args, nil
}

func parseArgs(stringArgs []string, cmd *cmds.Command) ([]interface{}, error) {
var argDef cmds.Argument
args := make([]interface{}, len(stringArgs))

for i, arg := range stringArgs {
if i < len(cmd.Arguments) {
argDef = cmd.Arguments[i]
}

if argDef.Type == cmds.ArgString {
args[i] = arg

} else {
in, err := os.Open(arg)
if err != nil {
return nil, err
}
args[i] = in
}
}

return args, nil
}
8 changes: 6 additions & 2 deletions commands/cli/parse_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@ import (
)

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

Expand All @@ -37,11 +38,14 @@ func TestOptionParsing(t *testing.T) {
t.Error("Should have failed (duplicate option name)")
}

path, args := parsePath([]string{"test", "beep", "boop"}, cmd)
path, args, sub := parsePath([]string{"test", "beep", "boop"}, cmd)
if len(path) != 1 || path[0] != "test" {
t.Errorf("Returned path was defferent than expected: %v", path)
}
if len(args) != 2 || args[0] != "beep" || args[1] != "boop" {
t.Errorf("Returned args were different than expected: %v", args)
}
if sub != subCmd {
t.Errorf("Returned command was different than expected")
}
}
91 changes: 89 additions & 2 deletions commands/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package commands
import (
"errors"
"fmt"
"io"
"strings"

u "github.com/jbenet/go-ipfs/util"
Expand All @@ -12,20 +13,35 @@ 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)
type Function func(Response, Request)

// Marshaller is a function that takes in a Response, and returns a marshalled []byte
// (or an error on failure)
type Marshaller func(Response) ([]byte, error)

// TODO: check Argument definitions when creating a Command
// (might need to use a Command constructor)
// * make sure any variadic args are at the end
// * make sure there aren't duplicate names
// * make sure optional arguments aren't followed by required arguments

// 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
Arguments []Argument
Run Function
Marshallers map[EncodingType]Marshaller
Type interface{}
Subcommands map[string]*Command
}

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

var ErrNoFormatter = errors.New("This command cannot be formatted to plain text")

// Call invokes the command for the given Request
func (c *Command) Call(req Request) Response {
res := NewResponse(req)
Expand All @@ -42,6 +58,12 @@ func (c *Command) Call(req Request) Response {
return res
}

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

options, err := c.GetOptions(req.Path())
if err != nil {
res.SetError(err, ErrClient)
Expand All @@ -54,7 +76,7 @@ func (c *Command) Call(req Request) Response {
return res
}

cmd.Run(req, res)
cmd.Run(res, req)

return res
}
Expand Down Expand Up @@ -116,7 +138,72 @@ func (c *Command) GetOptions(path []string) (map[string]Option, error) {
return optionsMap, nil
}

func (c *Command) CheckArguments(req Request) error {
args := req.Arguments()
argDefs := c.Arguments

// if we have more arg values provided than argument definitions,
// and the last arg definition is not variadic (or there are no definitions), return an error
notVariadic := len(argDefs) == 0 || !argDefs[len(argDefs)-1].Variadic
if notVariadic && len(args) > len(argDefs) {
return fmt.Errorf("Expected %v arguments, got %v", len(argDefs), len(args))
}

// iterate over the arg definitions
for i, argDef := range c.Arguments {

// the value for this argument definition. can be nil if it wasn't provided by the caller
var v interface{}
if i < len(args) {
v = args[i]
}

err := checkArgValue(v, argDef)
if err != nil {
return err
}

// any additional values are for the variadic arg definition
if argDef.Variadic && i < len(args)-1 {
for _, val := range args[i+1:] {
err := checkArgValue(val, argDef)
if err != nil {
return err
}
}
}
}

return nil
}

// Subcommand returns the subcommand with the given id
func (c *Command) Subcommand(id string) *Command {
return c.Subcommands[id]
}

// checkArgValue returns an error if a given arg value is not valid for the given Argument
func checkArgValue(v interface{}, def Argument) error {
if v == nil {
if def.Required {
return fmt.Errorf("Argument '%s' is required", def.Name)
}

return nil
}

if def.Type == ArgFile {
_, ok := v.(io.Reader)
if !ok {
return fmt.Errorf("Argument '%s' isn't valid", def.Name)
}

} else if def.Type == ArgString {
_, ok := v.(string)
if !ok {
return fmt.Errorf("Argument '%s' must be a string", def.Name)
}
}

return nil
}
20 changes: 10 additions & 10 deletions commands/command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,13 @@ func TestOptionValidation(t *testing.T) {
Option{[]string{"b", "beep"}, Int},
Option{[]string{"B", "boop"}, String},
},
Run: func(req Request, res Response) {},
Run: func(res Response, req Request) {},
}

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)
res := cmd.Call(req)
if res.Error() == nil {
t.Error("Should have failed (duplicate options)")
}
Expand Down Expand Up @@ -56,6 +49,13 @@ func TestOptionValidation(t *testing.T) {
t.Error("Should have passed")
}

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

req = NewEmptyRequest()
req.SetOption(EncShort, "json")
res = cmd.Call(req)
Expand All @@ -79,7 +79,7 @@ func TestOptionValidation(t *testing.T) {
}

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

cmdA := &Command{
Options: []Option{
Expand Down
Loading