|
|
|
@ -1,26 +1,25 @@ |
|
|
|
|
package mcfg |
|
|
|
|
|
|
|
|
|
import ( |
|
|
|
|
"context" |
|
|
|
|
"encoding/json" |
|
|
|
|
"fmt" |
|
|
|
|
"reflect" |
|
|
|
|
"strings" |
|
|
|
|
|
|
|
|
|
"github.com/mediocregopher/mediocre-go-lib/mctx" |
|
|
|
|
"github.com/mediocregopher/mediocre-go-lib/mcmp" |
|
|
|
|
"github.com/mediocregopher/mediocre-go-lib/mtime" |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
// Param is a configuration parameter which can be populated by Populate. The
|
|
|
|
|
// Param will exist as part of a Context, relative to its path (see the mctx
|
|
|
|
|
// package for more on Context path). For example, a Param with name "addr"
|
|
|
|
|
// under a Context with path of []string{"foo","bar"} will be setable on the CLI
|
|
|
|
|
// via "--foo-bar-addr". Other configuration Sources may treat the path/name
|
|
|
|
|
// differently, however.
|
|
|
|
|
// Param will exist as part of a Component. For example, a Param with name
|
|
|
|
|
// "addr" under a Component with path of []string{"foo","bar"} will be setable
|
|
|
|
|
// on the CLI via "--foo-bar-addr". Other configuration Sources may treat the
|
|
|
|
|
// path/name differently, however.
|
|
|
|
|
//
|
|
|
|
|
// Param values are always unmarshaled as JSON values into the Into field of the
|
|
|
|
|
// Param, regardless of the actual Source.
|
|
|
|
|
type Param struct { |
|
|
|
|
// How the parameter will be identified within a Context.
|
|
|
|
|
// How the parameter will be identified within a Component.
|
|
|
|
|
Name string |
|
|
|
|
|
|
|
|
|
// A helpful description of how a parameter is expected to be used.
|
|
|
|
@ -44,9 +43,59 @@ type Param struct { |
|
|
|
|
// value of the parameter.
|
|
|
|
|
Into interface{} |
|
|
|
|
|
|
|
|
|
// The Context this Param was added to. NOTE that this will be automatically
|
|
|
|
|
// filled in by WithParam when the Param is added to the Context.
|
|
|
|
|
Context context.Context |
|
|
|
|
// The Component this Param was added to. NOTE that this will be
|
|
|
|
|
// automatically filled in by AddParam when the Param is added to the
|
|
|
|
|
// Component.
|
|
|
|
|
Component *mcmp.Component |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// ParamOption is a modifier which can be passed into most Param-generating
|
|
|
|
|
// functions (e.g. String, Int, etc...)
|
|
|
|
|
type ParamOption func(*Param) |
|
|
|
|
|
|
|
|
|
// ParamRequired returns a ParamOption which ensures the parameter is required
|
|
|
|
|
// to be set by some configuration source. The default value of the parameter
|
|
|
|
|
// will be ignored.
|
|
|
|
|
func ParamRequired() ParamOption { |
|
|
|
|
return func(param *Param) { |
|
|
|
|
param.Required = true |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// ParamDefault returns a ParamOption which ensures the parameter uses the given
|
|
|
|
|
// default value when no Sources set a value for it. If not given then mcfg will
|
|
|
|
|
// use the zero value of the Param's type as the default value.
|
|
|
|
|
//
|
|
|
|
|
// If ParamRequired is given then this does nothing.
|
|
|
|
|
func ParamDefault(value interface{}) ParamOption { |
|
|
|
|
return func(param *Param) { |
|
|
|
|
intoV := reflect.ValueOf(param.Into).Elem() |
|
|
|
|
valueV := reflect.ValueOf(value) |
|
|
|
|
|
|
|
|
|
intoType, valueType := intoV.Type(), valueV.Type() |
|
|
|
|
if intoType != valueType { |
|
|
|
|
panic(fmt.Sprintf("ParamDefault value is type %s, but should be %s", valueType, intoType)) |
|
|
|
|
} else if !intoV.CanSet() { |
|
|
|
|
panic(fmt.Sprintf("Param.Into value %#v can't be set using reflection", param.Into)) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
intoV.Set(valueV) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// ParamUsage returns a ParamOption which sets the usage string on the Param.
|
|
|
|
|
// This is used in some Sources, like SourceCLI, when displaying information
|
|
|
|
|
// about available parameters.
|
|
|
|
|
func ParamUsage(usage string) ParamOption { |
|
|
|
|
// make all usages end with a period, because I say so
|
|
|
|
|
usage = strings.TrimSpace(usage) |
|
|
|
|
if !strings.HasSuffix(usage, ".") { |
|
|
|
|
usage += "." |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
return func(param *Param) { |
|
|
|
|
param.Usage = usage |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func paramFullName(path []string, name string) string { |
|
|
|
@ -67,31 +116,36 @@ func (p Param) fuzzyParse(v string) json.RawMessage { |
|
|
|
|
return json.RawMessage(v) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
type ctxKey string |
|
|
|
|
type cmpParamKey string |
|
|
|
|
|
|
|
|
|
func getParam(ctx context.Context, name string) (Param, bool) { |
|
|
|
|
param, ok := mctx.LocalValue(ctx, ctxKey(name)).(Param) |
|
|
|
|
// used in tests
|
|
|
|
|
func getParam(cmp *mcmp.Component, name string) (Param, bool) { |
|
|
|
|
param, ok := cmp.Value(cmpParamKey(name)).(Param) |
|
|
|
|
return param, ok |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// WithParam returns a Context with the given Param added to it. It will panic
|
|
|
|
|
// if a Param with the same Name already exists in the Context.
|
|
|
|
|
func WithParam(ctx context.Context, param Param) context.Context { |
|
|
|
|
// AddParam adds the given Param to the given Component. It will panic if a
|
|
|
|
|
// Param with the same Name already exists in the Component.
|
|
|
|
|
func AddParam(cmp *mcmp.Component, param Param, opts ...ParamOption) { |
|
|
|
|
param.Name = strings.ToLower(param.Name) |
|
|
|
|
param.Context = ctx |
|
|
|
|
param.Component = cmp |
|
|
|
|
key := cmpParamKey(param.Name) |
|
|
|
|
|
|
|
|
|
if _, ok := getParam(ctx, param.Name); ok { |
|
|
|
|
path := mctx.Path(ctx) |
|
|
|
|
panic(fmt.Sprintf("Context Path:%#v Name:%q already exists", path, param.Name)) |
|
|
|
|
if cmp.HasValue(key) { |
|
|
|
|
path := cmp.Path() |
|
|
|
|
panic(fmt.Sprintf("Component.Path:%#v Param.Name:%q already exists", path, param.Name)) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
return mctx.WithLocalValue(ctx, ctxKey(param.Name), param) |
|
|
|
|
for _, opt := range opts { |
|
|
|
|
opt(¶m) |
|
|
|
|
} |
|
|
|
|
cmp.SetValue(key, param) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func getLocalParams(ctx context.Context) []Param { |
|
|
|
|
localVals := mctx.LocalValues(ctx) |
|
|
|
|
params := make([]Param, 0, len(localVals)) |
|
|
|
|
for _, val := range localVals { |
|
|
|
|
func getLocalParams(cmp *mcmp.Component) []Param { |
|
|
|
|
values := cmp.Values() |
|
|
|
|
params := make([]Param, 0, len(values)) |
|
|
|
|
for _, val := range values { |
|
|
|
|
if param, ok := val.(Param); ok { |
|
|
|
|
params = append(params, param) |
|
|
|
|
} |
|
|
|
@ -99,131 +153,73 @@ func getLocalParams(ctx context.Context) []Param { |
|
|
|
|
return params |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// WithInt64 returns an *int64 which will be populated once Populate is run on
|
|
|
|
|
// the returned Context.
|
|
|
|
|
func WithInt64(ctx context.Context, name string, defaultVal int64, usage string) (context.Context, *int64) { |
|
|
|
|
i := defaultVal |
|
|
|
|
ctx = WithParam(ctx, Param{Name: name, Usage: usage, Into: &i}) |
|
|
|
|
return ctx, &i |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// WithRequiredInt64 returns an *int64 which will be populated once Populate is
|
|
|
|
|
// run on the returned Context, and which must be supplied by a configuration
|
|
|
|
|
// Source.
|
|
|
|
|
func WithRequiredInt64(ctx context.Context, name string, usage string) (context.Context, *int64) { |
|
|
|
|
// Int64 returns an *int64 which will be populated once Populate is run on the
|
|
|
|
|
// Component.
|
|
|
|
|
func Int64(cmp *mcmp.Component, name string, opts ...ParamOption) *int64 { |
|
|
|
|
var i int64 |
|
|
|
|
ctx = WithParam(ctx, Param{Name: name, Required: true, Usage: usage, Into: &i}) |
|
|
|
|
return ctx, &i |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// WithInt returns an *int which will be populated once Populate is run on the
|
|
|
|
|
// returned Context.
|
|
|
|
|
func WithInt(ctx context.Context, name string, defaultVal int, usage string) (context.Context, *int) { |
|
|
|
|
i := defaultVal |
|
|
|
|
ctx = WithParam(ctx, Param{Name: name, Usage: usage, Into: &i}) |
|
|
|
|
return ctx, &i |
|
|
|
|
AddParam(cmp, Param{Name: name, Into: &i}, opts...) |
|
|
|
|
return &i |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// WithRequiredInt returns an *int which will be populated once Populate is run
|
|
|
|
|
// on the returned Context, and which must be supplied by a configuration
|
|
|
|
|
// Source.
|
|
|
|
|
func WithRequiredInt(ctx context.Context, name string, usage string) (context.Context, *int) { |
|
|
|
|
// Int returns an *int which will be populated once Populate is run on the
|
|
|
|
|
// Component.
|
|
|
|
|
func Int(cmp *mcmp.Component, name string, opts ...ParamOption) *int { |
|
|
|
|
var i int |
|
|
|
|
ctx = WithParam(ctx, Param{Name: name, Required: true, Usage: usage, Into: &i}) |
|
|
|
|
return ctx, &i |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// WithFloat64 returns a *float64 which will be populated once Populate is run on
|
|
|
|
|
// the returned Context.
|
|
|
|
|
func WithFloat64(ctx context.Context, name string, defaultVal float64, usage string) (context.Context, *float64) { |
|
|
|
|
f := defaultVal |
|
|
|
|
ctx = WithParam(ctx, Param{Name: name, Usage: usage, Into: &f}) |
|
|
|
|
return ctx, &f |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// WithRequiredFloat64 returns a *float64 which will be populated once Populate
|
|
|
|
|
// is run on the returned Context, and which must be supplied by a configuration
|
|
|
|
|
// Source.
|
|
|
|
|
func WithRequiredFloat64(ctx context.Context, name string, defaultVal float64, usage string) (context.Context, *float64) { |
|
|
|
|
f := defaultVal |
|
|
|
|
ctx = WithParam(ctx, Param{Name: name, Required: true, Usage: usage, Into: &f}) |
|
|
|
|
return ctx, &f |
|
|
|
|
AddParam(cmp, Param{Name: name, Into: &i}, opts...) |
|
|
|
|
return &i |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// WithString returns a *string which will be populated once Populate is run on
|
|
|
|
|
// the returned Context.
|
|
|
|
|
func WithString(ctx context.Context, name, defaultVal, usage string) (context.Context, *string) { |
|
|
|
|
s := defaultVal |
|
|
|
|
ctx = WithParam(ctx, Param{Name: name, Usage: usage, IsString: true, Into: &s}) |
|
|
|
|
return ctx, &s |
|
|
|
|
// Float64 returns a *float64 which will be populated once Populate is run on
|
|
|
|
|
// the Component
|
|
|
|
|
func Float64(cmp *mcmp.Component, name string, opts ...ParamOption) *float64 { |
|
|
|
|
var f float64 |
|
|
|
|
AddParam(cmp, Param{Name: name, Into: &f}, opts...) |
|
|
|
|
return &f |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// WithRequiredString returns a *string which will be populated once Populate is
|
|
|
|
|
// run on the returned Context, and which must be supplied by a configuration
|
|
|
|
|
// Source.
|
|
|
|
|
func WithRequiredString(ctx context.Context, name, usage string) (context.Context, *string) { |
|
|
|
|
// String returns a *string which will be populated once Populate is run on
|
|
|
|
|
// the Component.
|
|
|
|
|
func String(cmp *mcmp.Component, name string, opts ...ParamOption) *string { |
|
|
|
|
var s string |
|
|
|
|
ctx = WithParam(ctx, Param{Name: name, Required: true, Usage: usage, IsString: true, Into: &s}) |
|
|
|
|
return ctx, &s |
|
|
|
|
AddParam(cmp, Param{Name: name, IsString: true, Into: &s}, opts...) |
|
|
|
|
return &s |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// WithBool returns a *bool which will be populated once Populate is run on the
|
|
|
|
|
// returned Context, and which defaults to false if unconfigured.
|
|
|
|
|
// Bool returns a *bool which will be populated once Populate is run on the
|
|
|
|
|
// Component, and which defaults to false if unconfigured.
|
|
|
|
|
//
|
|
|
|
|
// The default behavior of all Sources is that a boolean parameter will be set
|
|
|
|
|
// to true unless the value is "", 0, or false. In the case of the CLI Source
|
|
|
|
|
// the value will also be true when the parameter is used with no value at all,
|
|
|
|
|
// as would be expected.
|
|
|
|
|
func WithBool(ctx context.Context, name, usage string) (context.Context, *bool) { |
|
|
|
|
func Bool(cmp *mcmp.Component, name string, opts ...ParamOption) *bool { |
|
|
|
|
var b bool |
|
|
|
|
ctx = WithParam(ctx, Param{Name: name, Usage: usage, IsBool: true, Into: &b}) |
|
|
|
|
return ctx, &b |
|
|
|
|
AddParam(cmp, Param{Name: name, IsBool: true, Into: &b}, opts...) |
|
|
|
|
return &b |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// WithTS returns an *mtime.TS which will be populated once Populate is run on
|
|
|
|
|
// the returned Context.
|
|
|
|
|
func WithTS(ctx context.Context, name string, defaultVal mtime.TS, usage string) (context.Context, *mtime.TS) { |
|
|
|
|
t := defaultVal |
|
|
|
|
ctx = WithParam(ctx, Param{Name: name, Usage: usage, Into: &t}) |
|
|
|
|
return ctx, &t |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// WithRequiredTS returns an *mtime.TS which will be populated once Populate is
|
|
|
|
|
// run on the returned Context, and which must be supplied by a configuration
|
|
|
|
|
// Source.
|
|
|
|
|
func WithRequiredTS(ctx context.Context, name, usage string) (context.Context, *mtime.TS) { |
|
|
|
|
// TS returns an *mtime.TS which will be populated once Populate is run on
|
|
|
|
|
// the Component.
|
|
|
|
|
func TS(cmp *mcmp.Component, name string, opts ...ParamOption) *mtime.TS { |
|
|
|
|
var t mtime.TS |
|
|
|
|
ctx = WithParam(ctx, Param{Name: name, Required: true, Usage: usage, Into: &t}) |
|
|
|
|
return ctx, &t |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// WithDuration returns an *mtime.Duration which will be populated once Populate
|
|
|
|
|
// is run on the returned Context.
|
|
|
|
|
func WithDuration(ctx context.Context, name string, defaultVal mtime.Duration, usage string) (context.Context, *mtime.Duration) { |
|
|
|
|
d := defaultVal |
|
|
|
|
ctx = WithParam(ctx, Param{Name: name, Usage: usage, IsString: true, Into: &d}) |
|
|
|
|
return ctx, &d |
|
|
|
|
AddParam(cmp, Param{Name: name, Into: &t}, opts...) |
|
|
|
|
return &t |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// WithRequiredDuration returns an *mtime.Duration which will be populated once
|
|
|
|
|
// Populate is run on the returned Context, and which must be supplied by a
|
|
|
|
|
// configuration Source.
|
|
|
|
|
func WithRequiredDuration(ctx context.Context, name string, defaultVal mtime.Duration, usage string) (context.Context, *mtime.Duration) { |
|
|
|
|
// Duration returns an *mtime.Duration which will be populated once Populate
|
|
|
|
|
// is run on the Component.
|
|
|
|
|
func Duration(cmp *mcmp.Component, name string, opts ...ParamOption) *mtime.Duration { |
|
|
|
|
var d mtime.Duration |
|
|
|
|
ctx = WithParam(ctx, Param{Name: name, Required: true, Usage: usage, IsString: true, Into: &d}) |
|
|
|
|
return ctx, &d |
|
|
|
|
AddParam(cmp, Param{Name: name, IsString: true, Into: &d}, opts...) |
|
|
|
|
return &d |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// WithJSON reads the parameter value as a JSON value and unmarshals it into the
|
|
|
|
|
// given interface{} (which should be a pointer). The receiver (into) is also
|
|
|
|
|
// used to determine the default value.
|
|
|
|
|
func WithJSON(ctx context.Context, name string, into interface{}, usage string) context.Context { |
|
|
|
|
return WithParam(ctx, Param{Name: name, Usage: usage, Into: into}) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// WithRequiredJSON reads the parameter value as a JSON value and unmarshals it
|
|
|
|
|
// into the given interface{} (which should be a pointer). The value must be
|
|
|
|
|
// supplied by a configuration Source.
|
|
|
|
|
func WithRequiredJSON(ctx context.Context, name string, into interface{}, usage string) context.Context { |
|
|
|
|
return WithParam(ctx, Param{Name: name, Required: true, Usage: usage, Into: into}) |
|
|
|
|
// JSON reads the parameter value as a JSON value and unmarshals it into the
|
|
|
|
|
// given interface{} (which should be a pointer) once Populate is run on the
|
|
|
|
|
// Component.
|
|
|
|
|
//
|
|
|
|
|
// The receiver (into) is also used to determine the default value. ParamDefault
|
|
|
|
|
// should not be used as one of the opts.
|
|
|
|
|
func JSON(cmp *mcmp.Component, name string, into interface{}, opts ...ParamOption) { |
|
|
|
|
AddParam(cmp, Param{Name: name, Into: into}, opts...) |
|
|
|
|
} |
|
|
|
|