Add JSON InputSource to altsrc package

- Implement NewJSONSource* functions for returning an InputSource from
   various JSON data sources.
 - Copy and modify YAML tests for the JSON InputSource
This commit is contained in:
John Weldon 2016-07-02 12:35:48 -07:00
parent ee464a6db7
commit 91563b8ff0
3 changed files with 529 additions and 3 deletions

View File

@ -547,9 +547,9 @@ the yaml input source for any flags that are defined on that command. As a note
the "load" flag used would also have to be defined on the command flags in order
for this code snipped to work.
Currently only YAML files are supported but developers can add support for other
input sources by implementing the altsrc.InputSourceContext for their given
sources.
Currently only YAML and JSON files are supported but developers can add support
for other input sources by implementing the altsrc.InputSourceContext for their
given sources.
Here is a more complete sample of a command using YAML support:

324
altsrc/json_command_test.go Normal file
View File

@ -0,0 +1,324 @@
package altsrc
import (
"flag"
"io/ioutil"
"os"
"testing"
"github.com/urfave/cli"
)
const (
fileName = "current.json"
simpleJSON = `{"test": 15}`
nestedJSON = `{"top": {"test": 15}}`
)
func TestCommandJSONFileTest(t *testing.T) {
cleanup := writeTempFile(t, fileName, simpleJSON)
defer cleanup()
app := &cli.App{}
set := flag.NewFlagSet("test", 0)
test := []string{"test-cmd", "--load", fileName}
set.Parse(test)
c := cli.NewContext(app, set, nil)
command := &cli.Command{
Name: "test-cmd",
Aliases: []string{"tc"},
Usage: "this is for testing",
Description: "testing",
Action: func(c *cli.Context) error {
val := c.Int("test")
expect(t, val, 15)
return nil
},
Flags: []cli.Flag{
NewIntFlag(&cli.IntFlag{Name: "test"}),
&cli.StringFlag{Name: "load"}},
}
command.Before = InitInputSourceWithContext(command.Flags, NewJSONSourceFromFlagFunc("load"))
err := command.Run(c)
expect(t, err, nil)
}
func TestCommandJSONFileTestGlobalEnvVarWins(t *testing.T) {
cleanup := writeTempFile(t, fileName, simpleJSON)
defer cleanup()
app := &cli.App{}
set := flag.NewFlagSet("test", 0)
os.Setenv("THE_TEST", "10")
defer os.Setenv("THE_TEST", "")
test := []string{"test-cmd", "--load", fileName}
set.Parse(test)
c := cli.NewContext(app, set, nil)
command := &cli.Command{
Name: "test-cmd",
Aliases: []string{"tc"},
Usage: "this is for testing",
Description: "testing",
Action: func(c *cli.Context) error {
val := c.Int("test")
expect(t, val, 10)
return nil
},
Flags: []cli.Flag{
NewIntFlag(&cli.IntFlag{Name: "test", EnvVars: []string{"THE_TEST"}}),
&cli.StringFlag{Name: "load"}},
}
command.Before = InitInputSourceWithContext(command.Flags, NewJSONSourceFromFlagFunc("load"))
err := command.Run(c)
expect(t, err, nil)
}
func TestCommandJSONFileTestGlobalEnvVarWinsNested(t *testing.T) {
cleanup := writeTempFile(t, fileName, nestedJSON)
defer cleanup()
app := &cli.App{}
set := flag.NewFlagSet("test", 0)
os.Setenv("THE_TEST", "10")
defer os.Setenv("THE_TEST", "")
test := []string{"test-cmd", "--load", fileName}
set.Parse(test)
c := cli.NewContext(app, set, nil)
command := &cli.Command{
Name: "test-cmd",
Aliases: []string{"tc"},
Usage: "this is for testing",
Description: "testing",
Action: func(c *cli.Context) error {
val := c.Int("top.test")
expect(t, val, 10)
return nil
},
Flags: []cli.Flag{
NewIntFlag(&cli.IntFlag{Name: "top.test", EnvVars: []string{"THE_TEST"}}),
&cli.StringFlag{Name: "load"}},
}
command.Before = InitInputSourceWithContext(command.Flags, NewJSONSourceFromFlagFunc("load"))
err := command.Run(c)
expect(t, err, nil)
}
func TestCommandJSONFileTestSpecifiedFlagWins(t *testing.T) {
cleanup := writeTempFile(t, fileName, simpleJSON)
defer cleanup()
app := &cli.App{}
set := flag.NewFlagSet("test", 0)
test := []string{"test-cmd", "--load", fileName, "--test", "7"}
set.Parse(test)
c := cli.NewContext(app, set, nil)
command := &cli.Command{
Name: "test-cmd",
Aliases: []string{"tc"},
Usage: "this is for testing",
Description: "testing",
Action: func(c *cli.Context) error {
val := c.Int("test")
expect(t, val, 7)
return nil
},
Flags: []cli.Flag{
NewIntFlag(&cli.IntFlag{Name: "test"}),
&cli.StringFlag{Name: "load"}},
}
command.Before = InitInputSourceWithContext(command.Flags, NewJSONSourceFromFlagFunc("load"))
err := command.Run(c)
expect(t, err, nil)
}
func TestCommandJSONFileTestSpecifiedFlagWinsNested(t *testing.T) {
cleanup := writeTempFile(t, fileName, nestedJSON)
defer cleanup()
app := &cli.App{}
set := flag.NewFlagSet("test", 0)
test := []string{"test-cmd", "--load", fileName, "--top.test", "7"}
set.Parse(test)
c := cli.NewContext(app, set, nil)
command := &cli.Command{
Name: "test-cmd",
Aliases: []string{"tc"},
Usage: "this is for testing",
Description: "testing",
Action: func(c *cli.Context) error {
val := c.Int("top.test")
expect(t, val, 7)
return nil
},
Flags: []cli.Flag{
NewIntFlag(&cli.IntFlag{Name: "top.test"}),
&cli.StringFlag{Name: "load"}},
}
command.Before = InitInputSourceWithContext(command.Flags, NewJSONSourceFromFlagFunc("load"))
err := command.Run(c)
expect(t, err, nil)
}
func TestCommandJSONFileTestDefaultValueFileWins(t *testing.T) {
cleanup := writeTempFile(t, fileName, simpleJSON)
defer cleanup()
app := &cli.App{}
set := flag.NewFlagSet("test", 0)
test := []string{"test-cmd", "--load", fileName}
set.Parse(test)
c := cli.NewContext(app, set, nil)
command := &cli.Command{
Name: "test-cmd",
Aliases: []string{"tc"},
Usage: "this is for testing",
Description: "testing",
Action: func(c *cli.Context) error {
val := c.Int("test")
expect(t, val, 15)
return nil
},
Flags: []cli.Flag{
NewIntFlag(&cli.IntFlag{Name: "test", Value: 7}),
&cli.StringFlag{Name: "load"}},
}
command.Before = InitInputSourceWithContext(command.Flags, NewJSONSourceFromFlagFunc("load"))
err := command.Run(c)
expect(t, err, nil)
}
func TestCommandJSONFileTestDefaultValueFileWinsNested(t *testing.T) {
cleanup := writeTempFile(t, fileName, nestedJSON)
defer cleanup()
app := &cli.App{}
set := flag.NewFlagSet("test", 0)
test := []string{"test-cmd", "--load", fileName}
set.Parse(test)
c := cli.NewContext(app, set, nil)
command := &cli.Command{
Name: "test-cmd",
Aliases: []string{"tc"},
Usage: "this is for testing",
Description: "testing",
Action: func(c *cli.Context) error {
val := c.Int("top.test")
expect(t, val, 15)
return nil
},
Flags: []cli.Flag{
NewIntFlag(&cli.IntFlag{Name: "top.test", Value: 7}),
&cli.StringFlag{Name: "load"}},
}
command.Before = InitInputSourceWithContext(command.Flags, NewJSONSourceFromFlagFunc("load"))
err := command.Run(c)
expect(t, err, nil)
}
func TestCommandJSONFileFlagHasDefaultGlobalEnvJSONSetGlobalEnvWins(t *testing.T) {
cleanup := writeTempFile(t, fileName, simpleJSON)
defer cleanup()
app := &cli.App{}
set := flag.NewFlagSet("test", 0)
os.Setenv("THE_TEST", "11")
defer os.Setenv("THE_TEST", "")
test := []string{"test-cmd", "--load", fileName}
set.Parse(test)
c := cli.NewContext(app, set, nil)
command := &cli.Command{
Name: "test-cmd",
Aliases: []string{"tc"},
Usage: "this is for testing",
Description: "testing",
Action: func(c *cli.Context) error {
val := c.Int("test")
expect(t, val, 11)
return nil
},
Flags: []cli.Flag{
NewIntFlag(&cli.IntFlag{Name: "test", Value: 7, EnvVars: []string{"THE_TEST"}}),
&cli.StringFlag{Name: "load"}},
}
command.Before = InitInputSourceWithContext(command.Flags, NewJSONSourceFromFlagFunc("load"))
err := command.Run(c)
expect(t, err, nil)
}
func TestCommandJSONFileFlagHasDefaultGlobalEnvJSONSetGlobalEnvWinsNested(t *testing.T) {
cleanup := writeTempFile(t, fileName, nestedJSON)
defer cleanup()
app := &cli.App{}
set := flag.NewFlagSet("test", 0)
os.Setenv("THE_TEST", "11")
defer os.Setenv("THE_TEST", "")
test := []string{"test-cmd", "--load", fileName}
set.Parse(test)
c := cli.NewContext(app, set, nil)
command := &cli.Command{
Name: "test-cmd",
Aliases: []string{"tc"},
Usage: "this is for testing",
Description: "testing",
Action: func(c *cli.Context) error {
val := c.Int("top.test")
expect(t, val, 11)
return nil
},
Flags: []cli.Flag{
NewIntFlag(&cli.IntFlag{Name: "top.test", Value: 7, EnvVars: []string{"THE_TEST"}}),
&cli.StringFlag{Name: "load"}},
}
command.Before = InitInputSourceWithContext(command.Flags, NewJSONSourceFromFlagFunc("load"))
err := command.Run(c)
expect(t, err, nil)
}
func writeTempFile(t *testing.T, name string, content string) func() {
if err := ioutil.WriteFile(name, []byte(content), 0666); err != nil {
t.Fatalf("cannot write %q: %v", name, err)
}
return func() {
if err := os.Remove(name); err != nil {
t.Errorf("cannot remove %q: %v", name, err)
}
}
}

View File

@ -0,0 +1,202 @@
package altsrc
import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"strings"
"time"
"github.com/urfave/cli"
)
// NewJSONSourceFromFlagFunc returns a func that takes a cli.Context
// and returns an InputSourceContext suitable for retrieving config
// variables from a file containing JSON data with the file name defined
// by the given flag.
func NewJSONSourceFromFlagFunc(flag string) func(c *cli.Context) (InputSourceContext, error) {
return func(context *cli.Context) (InputSourceContext, error) {
return NewJSONSourceFromFile(context.String(flag))
}
}
// NewJSONSourceFromFile returns an InputSourceContext suitable for
// retrieving config variables from a file (or url) containing JSON
// data.
func NewJSONSourceFromFile(f string) (InputSourceContext, error) {
data, err := loadDataFrom(f)
if err != nil {
return nil, err
}
return NewJSONSource(data)
}
// NewJSONSourceFromReader returns an InputSourceContext suitable for
// retrieving config variables from an io.Reader that returns JSON data.
func NewJSONSourceFromReader(r io.Reader) (InputSourceContext, error) {
data, err := ioutil.ReadAll(r)
if err != nil {
return nil, err
}
return NewJSONSource(data)
}
// NewJSONSource returns an InputSourceContext suitable for retrieving
// config variables from raw JSON data.
func NewJSONSource(data []byte) (InputSourceContext, error) {
var deserialized map[string]interface{}
if err := json.Unmarshal(data, &deserialized); err != nil {
return nil, err
}
return &jsonSource{deserialized: deserialized}, nil
}
func (x *jsonSource) Int(name string) (int, error) {
i, err := x.getValue(name)
if err != nil {
return 0, err
}
switch v := i.(type) {
default:
return 0, fmt.Errorf("unexpected type %T for %q", i, name)
case int:
return v, nil
case float64:
return int(float64(v)), nil
case float32:
return int(float32(v)), nil
}
}
func (x *jsonSource) Duration(name string) (time.Duration, error) {
i, err := x.getValue(name)
if err != nil {
return 0, err
}
v, ok := (time.Duration)(0), false
if v, ok = i.(time.Duration); !ok {
return v, fmt.Errorf("unexpected type %T for %q", i, name)
}
return v, nil
}
func (x *jsonSource) Float64(name string) (float64, error) {
i, err := x.getValue(name)
if err != nil {
return 0, err
}
v, ok := (float64)(0), false
if v, ok = i.(float64); !ok {
return v, fmt.Errorf("unexpected type %T for %q", i, name)
}
return v, nil
}
func (x *jsonSource) String(name string) (string, error) {
i, err := x.getValue(name)
if err != nil {
return "", err
}
v, ok := "", false
if v, ok = i.(string); !ok {
return v, fmt.Errorf("unexpected type %T for %q", i, name)
}
return v, nil
}
func (x *jsonSource) StringSlice(name string) ([]string, error) {
i, err := x.getValue(name)
if err != nil {
return nil, err
}
switch v := i.(type) {
default:
return nil, fmt.Errorf("unexpected type %T for %q", i, name)
case []string:
return v, nil
case []interface{}:
c := []string{}
for _, s := range v {
if str, ok := s.(string); ok {
c = append(c, str)
} else {
return c, fmt.Errorf("unexpected item type %T in %T for %q", s, c, name)
}
}
return c, nil
}
}
func (x *jsonSource) IntSlice(name string) ([]int, error) {
i, err := x.getValue(name)
if err != nil {
return nil, err
}
switch v := i.(type) {
default:
return nil, fmt.Errorf("unexpected type %T for %q", i, name)
case []int:
return v, nil
case []interface{}:
c := []int{}
for _, s := range v {
if i2, ok := s.(int); ok {
c = append(c, i2)
} else {
return c, fmt.Errorf("unexpected item type %T in %T for %q", s, c, name)
}
}
return c, nil
}
}
func (x *jsonSource) Generic(name string) (cli.Generic, error) {
i, err := x.getValue(name)
if err != nil {
return nil, err
}
v, ok := (cli.Generic)(nil), false
if v, ok = i.(cli.Generic); !ok {
return v, fmt.Errorf("unexpected type %T for %q", i, name)
}
return v, nil
}
func (x *jsonSource) Bool(name string) (bool, error) {
i, err := x.getValue(name)
if err != nil {
return false, err
}
v, ok := false, false
if v, ok = i.(bool); !ok {
return v, fmt.Errorf("unexpected type %T for %q", i, name)
}
return v, nil
}
func (x *jsonSource) getValue(key string) (interface{}, error) {
return jsonGetValue(key, x.deserialized)
}
func jsonGetValue(key string, m map[string]interface{}) (interface{}, error) {
var ret interface{}
var ok bool
working := m
keys := strings.Split(key, ".")
for ix, k := range keys {
if ret, ok = working[k]; !ok {
return ret, fmt.Errorf("missing key %q", key)
}
if working, ok = ret.(map[string]interface{}); !ok {
if ix < len(keys)-1 {
return ret, fmt.Errorf("unexpected intermediate value at %q segment of %q: %T", k, key, ret)
}
}
}
return ret, nil
}
type jsonSource struct {
deserialized map[string]interface{}
}