Skip to content

Commit

Permalink
Added Marshaler/Unmarshaler interfaces
Browse files Browse the repository at this point in the history
This adds support for customised marshalling and
unmarshalling of user defined types as options.
Fixes #32.
  • Loading branch information
jessevdk committed Oct 31, 2013
1 parent 1989f0c commit 918da78
Show file tree
Hide file tree
Showing 5 changed files with 137 additions and 31 deletions.
97 changes: 84 additions & 13 deletions convert.go
Expand Up @@ -11,6 +11,19 @@ import (
"time"
)

// Marshaler is the interface implemented by types that can marshal themselves
// to a string representation of the flag.
type Marshaler interface {
MarshalFlag() (string, error)
}

// Unmarshaler is the interface implemented by types that can unmarshal a flag
// argument to themselves. The provided value is directly passed from the
// command line.
type Unmarshaler interface {
UnmarshalFlag(value string) error
}

func getBase(options multiTag, base int) (int, error) {
sbase := options.Get("base")

Expand All @@ -25,29 +38,45 @@ func getBase(options multiTag, base int) (int, error) {
return base, err
}

func convertToString(val reflect.Value, options multiTag) string {
func convertMarshal(val reflect.Value) (bool, string, error) {
// Check first for the Marshaler interface
if val.Type().NumMethod() > 0 && val.CanInterface() {
if marshaler, ok := val.Interface().(Marshaler); ok {
ret, err := marshaler.MarshalFlag()
return true, ret, err
}
}

return false, "", nil
}

func convertToString(val reflect.Value, options multiTag) (string, error) {
if ok, ret, err := convertMarshal(val); ok {
return ret, err
}

tp := val.Type()

switch tp.Kind() {
case reflect.String:
return val.String()
return val.String(), nil
case reflect.Bool:
if val.Bool() {
return "true"
return "true", nil
} else {
return "false"
return "false", nil
}
case reflect.Int, reflect.Int16, reflect.Int32, reflect.Int64:
base, _ := getBase(options, 10)
return strconv.FormatInt(val.Int(), base)
return strconv.FormatInt(val.Int(), base), nil
case reflect.Uint, reflect.Uint16, reflect.Uint32, reflect.Uint64:
base, _ := getBase(options, 10)
return strconv.FormatUint(val.Uint(), base)
return strconv.FormatUint(val.Uint(), base), nil
case reflect.Float32, reflect.Float64:
return strconv.FormatFloat(val.Float(), 'g', -1, tp.Bits())
return strconv.FormatFloat(val.Float(), 'g', -1, tp.Bits()), nil
case reflect.Slice:
if val.Len() == 0 {
return ""
return "", nil
}

ret := "["
Expand All @@ -57,10 +86,16 @@ func convertToString(val reflect.Value, options multiTag) string {
ret += ", "
}

ret += convertToString(val.Index(i), options)
item, err := convertToString(val.Index(i), options)

if err != nil {
return "", err
}

ret += item
}

return ret + "]"
return ret + "]", nil
case reflect.Map:
ret := "{"

Expand All @@ -69,18 +104,50 @@ func convertToString(val reflect.Value, options multiTag) string {
ret += ", "
}

ret += convertToString(val.MapIndex(key), options)
item, err := convertToString(val.MapIndex(key), options)

if err != nil {
return "", err
}

ret += item
}

return ret + "}"
return ret + "}", nil
case reflect.Ptr:
return convertToString(reflect.Indirect(val), options)
case reflect.Interface:
if !val.IsNil() {
return convertToString(val.Elem(), options)
}
}

return "", nil
}

func convertUnmarshal(val string, retval reflect.Value) (bool, error) {
if retval.Type().NumMethod() > 0 && retval.CanInterface() {
if unmarshaler, ok := retval.Interface().(Unmarshaler); ok {
return true, unmarshaler.UnmarshalFlag(val)
}
}

if retval.Type().Kind() != reflect.Ptr && retval.CanAddr() {
return convertUnmarshal(val, retval.Addr())
}

if retval.Type().Kind() == reflect.Interface && !retval.IsNil() {
return convertUnmarshal(val, retval.Elem())
}

return ""
return false, nil
}

func convert(val string, retval reflect.Value, options multiTag) error {
if ok, err := convertUnmarshal(val, retval); ok {
return err
}

tp := retval.Type()

// Support for time.Duration
Expand Down Expand Up @@ -192,6 +259,10 @@ func convert(val string, retval reflect.Value, options multiTag) error {
}

return convert(val, reflect.Indirect(retval), options)
case reflect.Interface:
if !retval.IsNil() {
return convert(val, retval.Elem(), options)
}
}

return nil
Expand Down
40 changes: 40 additions & 0 deletions examples/main.go
@@ -1,15 +1,52 @@
package main

import (
"errors"
"fmt"
"github.com/jessevdk/go-flags"
"os"
"strconv"
"strings"
)

type EditorOptions struct {
Input string `short:"i" long:"input" description:"Input file" default:"-"`
Output string `short:"o" long:"output" description:"Output file" default:"-"`
}

type Point struct {
X, Y int
}

func (p *Point) UnmarshalFlag(value string) error {
parts := strings.Split(value, ",")

if len(parts) != 2 {
return errors.New("Expected two numbers separated by a ,")
}

x, err := strconv.ParseInt(parts[0], 10, 32)

if err != nil {
return err
}

y, err := strconv.ParseInt(parts[1], 10, 32)

if err != nil {
return err
}

p.X = int(x)
p.Y = int(y)

return nil
}

func (p Point) MarshalFlag() (string, error) {
return fmt.Sprintf("%d,%d", p.X, p.Y), nil
}

type Options struct {
// Example of verbosity with level
Verbose []bool `short:"v" long:"verbose" description:"Verbose output"`
Expand All @@ -22,6 +59,9 @@ type Options struct {

// Example of option group
Editor EditorOptions `group:"Editor Options"`

// Example of custom type Marshal/Unmarshal
Point Point `long:"point" description:"A x,y point" default:"1,2"`
}

var options Options
Expand Down
4 changes: 4 additions & 0 deletions flags.go
Expand Up @@ -49,6 +49,10 @@
// Then, the AuthorInfo map can be filled with something like
// -a name:Jesse -a "surname:van den Kieboom".
//
// Finally, for full control over the conversion between command line argument
// values and options, user defined types can choose to implement the Marshaler
// and Unmarshaler interfaces.
//
// Available field tags:
// short: the short name of the option (single character)
// long: the long name of the option
Expand Down
2 changes: 1 addition & 1 deletion help.go
Expand Up @@ -142,7 +142,7 @@ func (p *Parser) writeHelpOption(writer *bufio.Writer, option *Option, info alig
}

if showdef {
def = convertToString(option.Value, option.tag)
def, _ = convertToString(option.Value, option.tag)
}
} else if len(defs) != 0 {
def = strings.Join(defs, ", ")
Expand Down
25 changes: 8 additions & 17 deletions ini.go
Expand Up @@ -94,28 +94,19 @@ func writeIni(parser *Parser, writer io.Writer, options IniOptions) {
switch val.Type().Kind() {
case reflect.Slice:
for idx := 0; idx < val.Len(); idx++ {
fmt.Fprintf(writer,
"%s = %s\n",
option.iniName(),
convertToString(val.Index(idx),
option.tag))
v, _ := convertToString(val.Index(idx), option.tag)
fmt.Fprintf(writer, "%s = %s\n", option.iniName(), v)
}
case reflect.Map:
for _, key := range val.MapKeys() {
fmt.Fprintf(writer,
"%s = %s:%s\n",
option.iniName(),
convertToString(key,
option.tag),
convertToString(val.MapIndex(key),
option.tag))
k, _ := convertToString(key, option.tag)
v, _ := convertToString(val.MapIndex(key), option.tag)

fmt.Fprintf(writer, "%s = %s:%s\n", option.iniName(), k, v)
}
default:
fmt.Fprintf(writer,
"%s = %s\n",
option.iniName(),
convertToString(val,
option.tag))
v, _ := convertToString(val, option.tag)
fmt.Fprintf(writer, "%s = %s\n", option.iniName(), v)
}
}
})
Expand Down

0 comments on commit 918da78

Please sign in to comment.