Add ConfigFlag for loading configuration through a flag.

This commit is contained in:
Alec Thomas
2018-09-11 09:56:57 +10:00
parent 3b6f48371a
commit 862837e6fa
9 changed files with 167 additions and 39 deletions
+6 -7
View File
@@ -10,7 +10,7 @@
1. [Command handling](#command-handling)
1. [Switch on the command string](#switch-on-the-command-string)
1. [Attach a `Run(...) error` method to each command](#attach-a-run-error-method-to-each-command)
1. [BeforeHook\(\), AfterHook\(\) and the Bind\(\) option](#beforehook-afterhook-and-the-bind-option)
1. [BeforeApply\(\), AfterApply\(\) and the Bind\(\) option](#BeforeApply-AfterApply-and-the-bind-option)
1. [Flags](#flags)
1. [Commands and sub-commands](#commands-and-sub-commands)
1. [Branching positional arguments](#branching-positional-arguments)
@@ -212,12 +212,11 @@ func main() {
```
## BeforeHook(), AfterHook() and the Bind() option
## Hooks: BeforeResolve(), BeforeSet(), AfterSet() and the Bind() option
If a node in the grammar has a `BeforeHook(...) error` and/or `AfterHook(...) error` method, those methods will
be called before validation/assignment and after validation/assignment, respectively.
If a node in the grammar has a `BeforeResolve(...)`, `BeforeApply(...) error` and/or `AfterApply(...) error` method, those methods will be called before validation/assignment and after validation/assignment, respectively.
The `--help` flag is implemented with a `BeforeHook`.
The `--help` flag is implemented with a `BeforeApply` hook.
Arguments to hooks are provided via the `Bind(...)` option. `*Kong`, `*Context` and `*Path` are also bound.
@@ -227,7 +226,7 @@ eg.
// A flag with a hook that, if triggered, will set the debug loggers output to stdout.
var debugFlag bool
func (d debugFlag) BeforeHook(logger *log.Logger) error {
func (d debugFlag) BeforeApply(logger *log.Logger) error {
logger.SetOutput(os.Stdout)
return nil
}
@@ -493,7 +492,7 @@ The default help output is usually sufficient, but if not there are two solution
### `Bind(...)` - bind values for callback hooks and Run() methods
See the [section on hooks](#beforehook-afterhook-and-the-bind-option) for details.
See the [section on hooks](#BeforeApply-AfterApply-and-the-bind-option) for details.
### Other options
+1 -1
View File
@@ -24,7 +24,7 @@ type VersionFlag string
func (v VersionFlag) Decode(ctx *kong.DecodeContext) error { return nil }
func (v VersionFlag) IsBool() bool { return true }
func (v VersionFlag) BeforeHook(app *kong.Kong, vars kong.Vars) error {
func (v VersionFlag) BeforeApply(app *kong.Kong, vars kong.Vars) error {
fmt.Println(vars["version"])
app.Exit(0)
return nil
+29 -13
View File
@@ -50,15 +50,16 @@ type Context struct {
// Error that occurred during trace, if any.
Error error
values map[*Value]reflect.Value // Temporary values during tracing.
scan *Scanner
values map[*Value]reflect.Value // Temporary values during tracing.
resolvers []ResolverFunc // Extra context-specific resolvers.
scan *Scanner
}
// Trace path of "args" through the gammar tree.
// Trace path of "args" through the grammar tree.
//
// The returned Context will include a Path of all commands, arguments, positionals and flags.
//
// Note that this will not modify the target grammar. Call Apply() to do so.
// Call Resolve() after this, then finally Apply() to write parsed values into the target grammar.
func Trace(k *Kong, args []string) (*Context, error) {
c := &Context{
Kong: k,
@@ -70,7 +71,7 @@ func Trace(k *Kong, args []string) (*Context, error) {
scan: Scan(args...),
}
c.Error = c.trace(c.Model.Node)
return c, c.traceResolvers()
return c, nil
}
// Value returns the value for a particular path element.
@@ -173,14 +174,25 @@ func (c *Context) Command() string {
return strings.Join(command, " ")
}
// FlagValue returns the set value of a flag, if it was encountered and exists.
func (c *Context) FlagValue(flag *Flag) reflect.Value {
// AddResolver adds a context-specific resolver.
//
// This is most useful in the BeforeResolve() hook.
func (c *Context) AddResolver(resolver ResolverFunc) {
c.resolvers = append(c.resolvers, resolver)
}
// FlagValue returns the set value of a flag if it was encountered and exists.
func (c *Context) FlagValue(flag *Flag) interface{} {
for _, trace := range c.Path {
if trace.Flag == flag {
return c.values[trace.Flag.Value]
v, ok := c.values[trace.Flag.Value]
if !ok {
return nil
}
return v.Interface()
}
}
return reflect.Value{}
return nil
}
// Recursively reset values to defaults (as specified in the grammar) or the zero value.
@@ -363,9 +375,13 @@ func findPotentialCandidates(needle string, haystack []string, format string, ar
return fmt.Errorf("%s", prefix)
}
// Walk through flags from existing nodes in the path.
func (c *Context) traceResolvers() error {
if len(c.resolvers) == 0 {
// Resolve walks through the traced path, applying resolvers to any unset flags.
func (c *Context) Resolve() error {
// Combine application-level resolvers and context resolvers.
resolvers := []ResolverFunc{}
resolvers = append(resolvers, c.Kong.resolvers...)
resolvers = append(resolvers, c.resolvers...)
if len(resolvers) == 0 {
return nil
}
@@ -376,7 +392,7 @@ func (c *Context) traceResolvers() error {
if _, ok := c.values[flag.Value]; ok {
continue
}
for _, resolver := range c.resolvers {
for _, resolver := range resolvers {
s, err := resolver(c, path, flag)
if err != nil {
return err
+19
View File
@@ -0,0 +1,19 @@
package kong
// BeforeResolve is a documentation-only interface describing hooks that run before values are set.
type BeforeResolve interface {
// This is not the correct signature - see README for details.
BeforeResolve(args ...interface{}) error
}
// BeforeApply is a documentation-only interface describing hooks that run before values are set.
type BeforeApply interface {
// This is not the correct signature - see README for details.
BeforeApply(args ...interface{}) error
}
// AfterApply is a documentation-only interface describing hooks that run after values are set.
type AfterApply interface {
// This is not the correct signature - see README for details.
AfterApply(args ...interface{}) error
}
+24 -3
View File
@@ -43,6 +43,7 @@ type Kong struct {
Stderr io.Writer
bindings bindings
loader ConfigurationFunc
resolvers []ResolverFunc
registry *Registry
@@ -161,7 +162,7 @@ func mergeVars(base, extra map[string]string) map[string]string {
type helpValue bool
func (h helpValue) BeforeHook(ctx *Context) error {
func (h helpValue) BeforeApply(ctx *Context) error {
options := ctx.Kong.helpOptions
options.Summary = false
err := ctx.Kong.help(options, ctx)
@@ -209,7 +210,13 @@ func (k *Kong) Parse(args []string) (ctx *Context, err error) {
if ctx.Error != nil {
return nil, &ParseError{error: ctx.Error, Context: ctx}
}
if err = k.applyHook(ctx, "BeforeHook"); err != nil {
if err = k.applyHook(ctx, "BeforeResolve"); err != nil {
return nil, &ParseError{error: err, Context: ctx}
}
if err = ctx.Resolve(); err != nil {
return nil, &ParseError{error: err, Context: ctx}
}
if err = k.applyHook(ctx, "BeforeApply"); err != nil {
return nil, &ParseError{error: err, Context: ctx}
}
if err = ctx.Validate(); err != nil {
@@ -218,7 +225,7 @@ func (k *Kong) Parse(args []string) (ctx *Context, err error) {
if _, err = ctx.Apply(); err != nil {
return nil, &ParseError{error: err, Context: ctx}
}
if err = k.applyHook(ctx, "AfterHook"); err != nil {
if err = k.applyHook(ctx, "AfterApply"); err != nil {
return nil, &ParseError{error: err, Context: ctx}
}
return ctx, nil
@@ -306,6 +313,20 @@ func (k *Kong) FatalIfErrorf(err error, args ...interface{}) {
k.Exit(1)
}
// LoadConfig from path using the loader configured via Configuration(loader).
//
// "path" will have ~/ expanded.
func (k *Kong) LoadConfig(path string) (ResolverFunc, error) {
path = expandPath(path)
r, err := os.Open(path) // nolint: gas
if err != nil {
return nil, err
}
defer r.Close()
return k.loader(r)
}
func catch(err *error) {
msg := recover()
if test, ok := msg.(Error); ok {
+4 -4
View File
@@ -362,12 +362,12 @@ type hookContext struct {
type hookValue string
func (h *hookValue) BeforeHook(ctx *hookContext) error {
func (h *hookValue) BeforeApply(ctx *hookContext) error {
ctx.values = append(ctx.values, "before:"+string(*h))
return nil
}
func (h *hookValue) AfterHook(ctx *hookContext) error {
func (h *hookValue) AfterApply(ctx *hookContext) error {
ctx.values = append(ctx.values, "after:"+string(*h))
return nil
}
@@ -377,12 +377,12 @@ type hookCmd struct {
Three hookValue
}
func (h *hookCmd) BeforeHook(ctx *hookContext) error {
func (h *hookCmd) BeforeApply(ctx *hookContext) error {
ctx.cmd = true
return nil
}
func (h *hookCmd) AfterHook(ctx *hookContext) error {
func (h *hookCmd) AfterApply(ctx *hookContext) error {
ctx.cmd = true
return nil
}
+5 -11
View File
@@ -2,7 +2,6 @@ package kong
import (
"io"
"os"
"os/user"
"path/filepath"
"reflect"
@@ -116,8 +115,8 @@ func Writers(stdout, stderr io.Writer) OptionFunc {
//
// There are two hook points:
//
// BeforeHook(...) error
// AfterHook(...) error
// BeforeApply(...) error
// AfterApply(...) error
//
// Called before validation/assignment, and immediately after validation/assignment, respectively.
func Bind(args ...interface{}) OptionFunc {
@@ -179,17 +178,12 @@ type ConfigurationFunc func(r io.Reader) (ResolverFunc, error)
// ~ expansion will occur on the provided paths.
func Configuration(loader ConfigurationFunc, paths ...string) OptionFunc {
return func(k *Kong) error {
k.loader = loader
for _, path := range paths {
path = expandPath(path)
r, err := os.Open(path) // nolint: gas
if err != nil {
continue
}
resolver, err := loader(r)
if err == nil {
resolver, _ := k.LoadConfig(path)
if resolver != nil {
k.resolvers = append(k.resolvers, resolver)
}
_ = r.Close()
}
return nil
}
+35
View File
@@ -0,0 +1,35 @@
package kong
import (
"fmt"
)
// ConfigFlag uses the configured (via kong.Configuration(loader)) configuration loader to load configuration
// from a file specified by a flag.
//
// Use this as a flag value to support loading of custom configuration via a flag.
type ConfigFlag string
// BeforeResolve adds a resolver.
func (c ConfigFlag) BeforeResolve(kong *Kong, ctx *Context, trace *Path) error {
if kong.loader == nil {
return fmt.Errorf("Kong must be configured with kong.Configuration(...)")
}
path := string(ctx.FlagValue(trace.Flag).(ConfigFlag))
resolver, err := kong.LoadConfig(path)
if err != nil {
return err
}
ctx.AddResolver(resolver)
return nil
}
// VersionFlag is a flag type that can be used to display a version number, stored in the "version" variable.
type VersionFlag bool
// BeforeApply writes the version variable and terminates with a 0 exit status.
func (v VersionFlag) BeforeApply(app *Kong, vars Vars) error {
fmt.Fprintln(app.Stdout, vars["version"])
app.Exit(0)
return nil
}
+44
View File
@@ -0,0 +1,44 @@
package kong
import (
"io/ioutil"
"os"
"strings"
"testing"
"github.com/stretchr/testify/require"
)
func TestConfigFlag(t *testing.T) {
var cli struct {
Config ConfigFlag
Flag string
}
w, err := ioutil.TempFile("", "")
require.NoError(t, err)
defer os.Remove(w.Name())
w.WriteString(`{"flag": "hello world"}`) // nolint: errcheck
w.Close()
p := Must(&cli, Configuration(JSON))
_, err = p.Parse([]string{"--config", w.Name()})
require.NoError(t, err)
require.Equal(t, "hello world", cli.Flag)
}
func TestVersionFlag(t *testing.T) {
var cli struct {
Version VersionFlag
}
w := &strings.Builder{}
p := Must(&cli, Vars{"version": "0.1.1"})
p.Stdout = w
called := 1
p.Exit = func(s int) { called = s }
_, err := p.Parse([]string{"--version"})
require.NoError(t, err)
require.Equal(t, "0.1.1", strings.TrimSpace(w.String()))
require.Equal(t, 0, called)
}