Commit 9ac28065 authored by Steven Allen's avatar Steven Allen

Merge go-ipfs-cmdkit into merge/cmdkit

parents 6a7c2ca2 95858927
package cmdkit
type ArgumentType int
const (
ArgString ArgumentType = iota
ArgFile
)
type Argument struct {
Name string
Type ArgumentType
Required bool // error if no value is specified
Variadic bool // unlimited values can be specfied
SupportsStdin bool // can accept stdin as a value
Recursive bool // supports recursive file adding (with '-r' flag)
Description string
}
func StringArg(name string, required, variadic bool, description string) Argument {
return Argument{
Name: name,
Type: ArgString,
Required: required,
Variadic: variadic,
Description: description,
}
}
func FileArg(name string, required, variadic bool, description string) Argument {
return Argument{
Name: name,
Type: ArgFile,
Required: required,
Variadic: variadic,
Description: description,
}
}
// TODO: modifiers might need a different API?
// e.g. passing enum values into arg constructors variadically
// (`FileArg("file", ArgRequired, ArgStdin, ArgRecursive)`)
func (a Argument) EnableStdin() Argument {
a.SupportsStdin = true
return a
}
func (a Argument) EnableRecursive() Argument {
if a.Type != ArgFile {
panic("Only FileArgs can enable recursive")
}
a.Recursive = true
return a
}
package cmdkit
import (
"encoding/json"
"errors"
"fmt"
)
// ErrorType signfies a category of errors
type ErrorType uint
// ErrorTypes convey what category of error ocurred
const (
ErrNormal ErrorType = iota // general errors
ErrClient // error was caused by the client, (e.g. invalid CLI usage)
ErrImplementation // programmer error in the server
ErrNotFound // == HTTP 404
ErrFatal // abort instantly
// 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
}
// Errorf returns an Error with the given code and format specification
func Errorf(code ErrorType, format string, args ...interface{}) Error {
return Error{
Code: code,
Message: fmt.Sprintf(format, args...),
}
}
func (e Error) Error() string {
return e.Message
}
func (e Error) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Message string
Code ErrorType
Type string
}{
Message: e.Message,
Code: e.Code,
Type: "error",
})
}
func (e *Error) UnmarshalJSON(data []byte) error {
var w struct {
Message string
Code ErrorType
Type string
}
err := json.Unmarshal(data, &w)
if err != nil {
return err
}
if w.Type != "error" {
return errors.New("not of type error")
}
e.Message = w.Message
e.Code = w.Code
return nil
}
package cmdkit
// HelpText is a set of strings used to generate command help text. The help
// text follows formats similar to man pages, but not exactly the same.
type HelpText struct {
// required
Tagline string // used in <cmd usage>
ShortDescription string // used in DESCRIPTION
SynopsisOptionsValues map[string]string // mappings for synopsis generator
// optional - whole section overrides
Usage string // overrides USAGE section
LongDescription string // overrides DESCRIPTION section
Options string // overrides OPTIONS section
Arguments string // overrides ARGUMENTS section
Subcommands string // overrides SUBCOMMANDS section
Synopsis string // overrides SYNOPSIS field
}
package cmdkit
import (
"encoding/json"
"testing"
)
func TestMarshal(t *testing.T) {
type testcase struct {
msg string
code ErrorType
}
tcs := []testcase{
{msg: "error msg", code: 0},
{msg: "error msg", code: 1},
{msg: "some other error msg", code: 1},
}
for _, tc := range tcs {
e := Error{
Message: tc.msg,
Code: tc.code,
}
buf, err := json.Marshal(e)
if err != nil {
t.Fatal(err)
}
m := make(map[string]interface{})
err = json.Unmarshal(buf, &m)
if err != nil {
t.Fatal(err)
}
if len(m) != 3 {
t.Errorf("expected three map elements, got %d", len(m))
}
if m["Message"].(string) != tc.msg {
t.Errorf(`expected m["Message"] to be %q, got %q`, tc.msg, m["Message"])
}
icode := ErrorType(m["Code"].(float64))
if icode != tc.code {
t.Errorf(`expected m["Code"] to be %v, got %v`, tc.code, icode)
}
if m["Type"].(string) != "error" {
t.Errorf(`expected m["Type"] to be %q, got %q`, "error", m["Type"])
}
}
}
func TestUnmarshal(t *testing.T) {
type testcase struct {
json string
msg string
code ErrorType
err string
}
tcs := []testcase{
{json: `{"Message":"error msg","Code":0}`, msg: "error msg", err: "not of type error"},
{json: `{"Message":"error msg","Code":0,"Type":"error"}`, msg: "error msg"},
{json: `{"Message":"error msg","Code":1,"Type":"error"}`, msg: "error msg", code: 1},
{json: `{"Message":"some other error msg","Code":1,"Type":"error"}`, msg: "some other error msg", code: 1},
}
for i, tc := range tcs {
t.Log("at test case", i)
var e Error
err := json.Unmarshal([]byte(tc.json), &e)
if err != nil && err.Error() != tc.err {
t.Errorf("expected parse error %q but got %q", tc.err, err)
} else if err == nil && tc.err != "" {
t.Errorf("expected parse error %q but got %q", tc.err, err)
}
if err != nil {
continue
}
if e.Message != tc.msg {
t.Errorf("expected e.Message to be %q, got %q", tc.msg, e.Message)
}
if e.Code != tc.code {
t.Errorf("expected e.Code to be %q, got %q", tc.code, e.Code)
}
}
}
package cmdkit
import (
"fmt"
"reflect"
"strconv"
"strings"
)
// Types of Command options
const (
Invalid = reflect.Invalid
Bool = reflect.Bool
Int = reflect.Int
Uint = reflect.Uint
Int64 = reflect.Int64
Uint64 = reflect.Uint64
Float = reflect.Float64
String = reflect.String
)
type OptMap map[string]interface{}
// Option is used to specify a field that will be provided by a consumer
type Option interface {
Name() string // the main name of the option
Names() []string // a list of unique names matched with user-provided flags
Type() reflect.Kind // value must be this type
Description() string // a short string that describes this option
WithDefault(interface{}) Option // sets the default value of the option
Default() interface{}
Parse(str string) (interface{}, error)
}
type option struct {
names []string
kind reflect.Kind
description string
defaultVal interface{}
}
func (o *option) Name() string {
return o.names[0]
}
func (o *option) Names() []string {
return o.names
}
func (o *option) Type() reflect.Kind {
return o.kind
}
func (o *option) Description() string {
if len(o.description) == 0 {
return ""
}
if !strings.HasSuffix(o.description, ".") {
o.description += "."
}
if o.defaultVal != nil {
if strings.Contains(o.description, "<<default>>") {
return strings.Replace(o.description, "<<default>>",
fmt.Sprintf("Default: %v.", o.defaultVal), -1)
} else {
return fmt.Sprintf("%s Default: %v.", o.description, o.defaultVal)
}
}
return o.description
}
type converter func(string) (interface{}, error)
var converters = map[reflect.Kind]converter{
Bool: func(v string) (interface{}, error) {
if v == "" {
return true, nil
}
v = strings.ToLower(v)
return strconv.ParseBool(v)
},
Int: func(v string) (interface{}, error) {
val, err := strconv.ParseInt(v, 0, 32)
if err != nil {
return nil, err
}
return int(val), err
},
Uint: func(v string) (interface{}, error) {
val, err := strconv.ParseUint(v, 0, 32)
if err != nil {
return nil, err
}
return uint(val), err
},
Int64: func(v string) (interface{}, error) {
val, err := strconv.ParseInt(v, 0, 64)
if err != nil {
return nil, err
}
return val, err
},
Uint64: func(v string) (interface{}, error) {
val, err := strconv.ParseUint(v, 0, 64)
if err != nil {
return nil, err
}
return val, err
},
Float: func(v string) (interface{}, error) {
return strconv.ParseFloat(v, 64)
},
String: func(v string) (interface{}, error) {
return v, nil
},
}
func (o *option) Parse(v string) (interface{}, error) {
conv, ok := converters[o.Type()]
if !ok {
return nil, fmt.Errorf("option %q takes %s arguments, but was passed %q", o.Name(), o.Type(), v)
}
return conv(v)
}
// constructor helper functions
func NewOption(kind reflect.Kind, names ...string) Option {
var desc string
if len(names) >= 2 {
desc = names[len(names)-1]
names = names[:len(names)-1]
}
return &option{
names: names,
kind: kind,
description: desc,
}
}
func (o *option) WithDefault(v interface{}) Option {
o.defaultVal = v
return o
}
func (o *option) Default() interface{} {
return o.defaultVal
}
// TODO handle description separately. this will take care of the panic case in
// NewOption
// For all func {Type}Option(...string) functions, the last variadic argument
// is treated as the description field.
func BoolOption(names ...string) Option {
return NewOption(Bool, names...)
}
func IntOption(names ...string) Option {
return NewOption(Int, names...)
}
func UintOption(names ...string) Option {
return NewOption(Uint, names...)
}
func Int64Option(names ...string) Option {
return NewOption(Int64, names...)
}
func Uint64Option(names ...string) Option {
return NewOption(Uint64, names...)
}
func FloatOption(names ...string) Option {
return NewOption(Float, names...)
}
func StringOption(names ...string) Option {
return NewOption(String, names...)
}
type OptionValue struct {
Value interface{}
ValueFound bool
Def Option
}
// Found returns true if the option value was provided by the user (not a default value)
func (ov *OptionValue) Found() bool {
return ov.ValueFound
}
// Definition returns the option definition for the provided value
func (ov *OptionValue) Definition() Option {
return ov.Def
}
// value accessor methods, gets the value as a certain type
func (ov *OptionValue) Bool() (value bool, found bool, err error) {
if ov == nil || !ov.ValueFound && ov.Value == nil {
return false, false, nil
}
val, ok := ov.Value.(bool)
if !ok {
err = fmt.Errorf("expected type %T, got %T", val, ov.Value)
}
return val, ov.ValueFound, err
}
func (ov *OptionValue) Int() (value int, found bool, err error) {
if ov == nil || !ov.ValueFound && ov.Value == nil {
return 0, false, nil
}
val, ok := ov.Value.(int)
if !ok {
err = fmt.Errorf("expected type %T, got %T", val, ov.Value)
}
return val, ov.ValueFound, err
}
func (ov *OptionValue) Uint() (value uint, found bool, err error) {
if ov == nil || !ov.ValueFound && ov.Value == nil {
return 0, false, nil
}
val, ok := ov.Value.(uint)
if !ok {
err = fmt.Errorf("expected type %T, got %T", val, ov.Value)
}
return val, ov.ValueFound, err
}
func (ov *OptionValue) Int64() (value int64, found bool, err error) {
if ov == nil || !ov.ValueFound && ov.Value == nil {
return 0, false, nil
}
val, ok := ov.Value.(int64)
if !ok {
err = fmt.Errorf("expected type %T, got %T", val, ov.Value)
}
return val, ov.ValueFound, err
}
func (ov *OptionValue) Uint64() (value uint64, found bool, err error) {
if ov == nil || !ov.ValueFound && ov.Value == nil {
return 0, false, nil
}
val, ok := ov.Value.(uint64)
if !ok {
err = fmt.Errorf("expected type %T, got %T", val, ov.Value)
}
return val, ov.ValueFound, err
}
func (ov *OptionValue) Float() (value float64, found bool, err error) {
if ov == nil || !ov.ValueFound && ov.Value == nil {
return 0, false, nil
}
val, ok := ov.Value.(float64)
if !ok {
err = fmt.Errorf("expected type %T, got %T", val, ov.Value)
}
return val, ov.ValueFound, err
}
func (ov *OptionValue) String() (value string, found bool, err error) {
if ov == nil || !ov.ValueFound && ov.Value == nil {
return "", false, nil
}
val, ok := ov.Value.(string)
if !ok {
err = fmt.Errorf("expected type %T, got %T", val, ov.Value)
}
return val, ov.ValueFound, err
}
package cmdkit
import (
"math"
"reflect"
"strings"
"testing"
)
func TestOptionValueExtractBoolNotFound(t *testing.T) {
t.Log("ensure that no error is returned when value is not found")
optval := &OptionValue{ValueFound: false}
_, _, err := optval.Bool()
if err != nil {
t.Fatal("Found was false. Err should have been nil")
}
}
func TestOptionValueExtractWrongType(t *testing.T) {
t.Log("ensure that error is returned when value if of wrong type")
optval := &OptionValue{Value: "wrong type: a string", ValueFound: true}
_, _, err := optval.Bool()
if err == nil {
t.Fatal("No error returned. Failure.")
}
optval = &OptionValue{Value: "wrong type: a string", ValueFound: true}
_, _, err = optval.Int()
if err == nil {
t.Fatal("No error returned. Failure.")
}
}
func TestLackOfDescriptionOfOptionDoesNotPanic(t *testing.T) {
opt := BoolOption("a", "")
opt.Description()
}
func TestDotIsAddedInDescripton(t *testing.T) {
opt := BoolOption("a", "desc without dot")
dest := opt.Description()
if !strings.HasSuffix(dest, ".") {
t.Fatal("dot should have been added at the end of description")
}
}
func TestOptionName(t *testing.T) {
exp := map[string]interface{}{
"Name()": "main",
"Names()": []string{"main", "m", "alias"},
}
assert := func(name string, value interface{}) {
if !reflect.DeepEqual(value, exp[name]) {
t.Errorf(`expected %s to return %q, got %q`, name, exp[name], value)
}
}
opt := StringOption("main", "m", "alias", `an option with main name "main" and "m" and "alias" as aliases`)
assert("Name()", opt.Name())
assert("Names()", opt.Names())
}
func TestParse(t *testing.T) {
type testcase struct {
opt Option
str string
v interface{}
err string
}
tcs := []testcase{
{opt: StringOption("str"), str: "i'm a string!", v: "i'm a string!"},
{opt: IntOption("int1"), str: "42", v: 42},
{opt: IntOption("int1"), str: "fourtytwo", err: `strconv.ParseInt: parsing "fourtytwo": invalid syntax`},
{opt: IntOption("int2"), str: "-42", v: -42},
{opt: UintOption("uint1"), str: "23", v: uint(23)},
{opt: UintOption("uint2"), str: "-23", err: `strconv.ParseUint: parsing "-23": invalid syntax`},
{opt: Int64Option("int3"), str: "100001", v: int64(100001)},
{opt: Int64Option("int3"), str: "2147483648", v: int64(math.MaxInt32 + 1)},
{opt: Int64Option("int3"), str: "fly", err: `strconv.ParseInt: parsing "fly": invalid syntax`},
{opt: Uint64Option("uint3"), str: "23", v: uint64(23)},
{opt: Uint64Option("uint3"), str: "-23", err: `strconv.ParseUint: parsing "-23": invalid syntax`},
{opt: BoolOption("true"), str: "true", v: true},
{opt: BoolOption("true"), str: "", v: true},
{opt: BoolOption("false"), str: "false", v: false},
{opt: FloatOption("float"), str: "2.718281828459045", v: 2.718281828459045},
}
for _, tc := range tcs {
v, err := tc.opt.Parse(tc.str)
if err != nil && err.Error() != tc.err {
t.Errorf("unexpected error: %s", err)
} else if err == nil && tc.err != "" {
t.Errorf("expected error %q but got nil", tc.err)
}
if v != tc.v {
t.Errorf("expected %v but got %v", tc.v, v)
}
}
}
func TestDescription(t *testing.T) {
type testcase struct {
opt Option
desc string
}
tcs := []testcase{
{opt: StringOption("str", "some random option"), desc: "some random option."},
{opt: StringOption("str", "some random option (<<default>>)"), desc: "some random option (<<default>>)."},
{opt: StringOption("str", "some random option (<<default>>)").WithDefault("random=4"), desc: "some random option (Default: random=4.)."},
{opt: StringOption("str", "some random option").WithDefault("random=4"), desc: "some random option. Default: random=4."},
}
for _, tc := range tcs {
if desc := tc.opt.Description(); desc != tc.desc {
t.Errorf("expected\n%q\nbut got\n%q", tc.desc, desc)
}
}
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment