main
parent
bf1a47102d
commit
24fa5f5b31
13 changed files with 798 additions and 5 deletions
@ -1,2 +1,2 @@ |
||||
authPostmanService |
||||
vendor |
||||
# vendor |
@ -1,2 +0,0 @@ |
||||
github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= |
||||
github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= |
@ -0,0 +1,13 @@ |
||||
language: go |
||||
|
||||
go: |
||||
- 1.4.x |
||||
- 1.5.x |
||||
- 1.6.x |
||||
- 1.7.x |
||||
- 1.8.x |
||||
- 1.9.x |
||||
- 1.10.x |
||||
- 1.11.x |
||||
- 1.12.x |
||||
- tip |
@ -0,0 +1,19 @@ |
||||
Copyright (c) 2013 Kelsey Hightower |
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of |
||||
this software and associated documentation files (the "Software"), to deal in |
||||
the Software without restriction, including without limitation the rights to |
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies |
||||
of the Software, and to permit persons to whom the Software is furnished to do |
||||
so, subject to the following conditions: |
||||
|
||||
The above copyright notice and this permission notice shall be included in all |
||||
copies or substantial portions of the Software. |
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
||||
SOFTWARE. |
@ -0,0 +1,2 @@ |
||||
Kelsey Hightower kelsey.hightower@gmail.com github.com/kelseyhightower |
||||
Travis Parker travis.parker@gmail.com github.com/teepark |
@ -0,0 +1,192 @@ |
||||
# envconfig |
||||
|
||||
[](https://travis-ci.org/kelseyhightower/envconfig) |
||||
|
||||
```Go |
||||
import "github.com/kelseyhightower/envconfig" |
||||
``` |
||||
|
||||
## Documentation |
||||
|
||||
See [godoc](http://godoc.org/github.com/kelseyhightower/envconfig) |
||||
|
||||
## Usage |
||||
|
||||
Set some environment variables: |
||||
|
||||
```Bash |
||||
export MYAPP_DEBUG=false |
||||
export MYAPP_PORT=8080 |
||||
export MYAPP_USER=Kelsey |
||||
export MYAPP_RATE="0.5" |
||||
export MYAPP_TIMEOUT="3m" |
||||
export MYAPP_USERS="rob,ken,robert" |
||||
export MYAPP_COLORCODES="red:1,green:2,blue:3" |
||||
``` |
||||
|
||||
Write some code: |
||||
|
||||
```Go |
||||
package main |
||||
|
||||
import ( |
||||
"fmt" |
||||
"log" |
||||
"time" |
||||
|
||||
"github.com/kelseyhightower/envconfig" |
||||
) |
||||
|
||||
type Specification struct { |
||||
Debug bool |
||||
Port int |
||||
User string |
||||
Users []string |
||||
Rate float32 |
||||
Timeout time.Duration |
||||
ColorCodes map[string]int |
||||
} |
||||
|
||||
func main() { |
||||
var s Specification |
||||
err := envconfig.Process("myapp", &s) |
||||
if err != nil { |
||||
log.Fatal(err.Error()) |
||||
} |
||||
format := "Debug: %v\nPort: %d\nUser: %s\nRate: %f\nTimeout: %s\n" |
||||
_, err = fmt.Printf(format, s.Debug, s.Port, s.User, s.Rate, s.Timeout) |
||||
if err != nil { |
||||
log.Fatal(err.Error()) |
||||
} |
||||
|
||||
fmt.Println("Users:") |
||||
for _, u := range s.Users { |
||||
fmt.Printf(" %s\n", u) |
||||
} |
||||
|
||||
fmt.Println("Color codes:") |
||||
for k, v := range s.ColorCodes { |
||||
fmt.Printf(" %s: %d\n", k, v) |
||||
} |
||||
} |
||||
``` |
||||
|
||||
Results: |
||||
|
||||
```Bash |
||||
Debug: false |
||||
Port: 8080 |
||||
User: Kelsey |
||||
Rate: 0.500000 |
||||
Timeout: 3m0s |
||||
Users: |
||||
rob |
||||
ken |
||||
robert |
||||
Color codes: |
||||
red: 1 |
||||
green: 2 |
||||
blue: 3 |
||||
``` |
||||
|
||||
## Struct Tag Support |
||||
|
||||
Envconfig supports the use of struct tags to specify alternate, default, and required |
||||
environment variables. |
||||
|
||||
For example, consider the following struct: |
||||
|
||||
```Go |
||||
type Specification struct { |
||||
ManualOverride1 string `envconfig:"manual_override_1"` |
||||
DefaultVar string `default:"foobar"` |
||||
RequiredVar string `required:"true"` |
||||
IgnoredVar string `ignored:"true"` |
||||
AutoSplitVar string `split_words:"true"` |
||||
RequiredAndAutoSplitVar string `required:"true" split_words:"true"` |
||||
} |
||||
``` |
||||
|
||||
Envconfig has automatic support for CamelCased struct elements when the |
||||
`split_words:"true"` tag is supplied. Without this tag, `AutoSplitVar` above |
||||
would look for an environment variable called `MYAPP_AUTOSPLITVAR`. With the |
||||
setting applied it will look for `MYAPP_AUTO_SPLIT_VAR`. Note that numbers |
||||
will get globbed into the previous word. If the setting does not do the |
||||
right thing, you may use a manual override. |
||||
|
||||
Envconfig will process value for `ManualOverride1` by populating it with the |
||||
value for `MYAPP_MANUAL_OVERRIDE_1`. Without this struct tag, it would have |
||||
instead looked up `MYAPP_MANUALOVERRIDE1`. With the `split_words:"true"` tag |
||||
it would have looked up `MYAPP_MANUAL_OVERRIDE1`. |
||||
|
||||
```Bash |
||||
export MYAPP_MANUAL_OVERRIDE_1="this will be the value" |
||||
|
||||
# export MYAPP_MANUALOVERRIDE1="and this will not" |
||||
``` |
||||
|
||||
If envconfig can't find an environment variable value for `MYAPP_DEFAULTVAR`, |
||||
it will populate it with "foobar" as a default value. |
||||
|
||||
If envconfig can't find an environment variable value for `MYAPP_REQUIREDVAR`, |
||||
it will return an error when asked to process the struct. If |
||||
`MYAPP_REQUIREDVAR` is present but empty, envconfig will not return an error. |
||||
|
||||
If envconfig can't find an environment variable in the form `PREFIX_MYVAR`, and there |
||||
is a struct tag defined, it will try to populate your variable with an environment |
||||
variable that directly matches the envconfig tag in your struct definition: |
||||
|
||||
```shell |
||||
export SERVICE_HOST=127.0.0.1 |
||||
export MYAPP_DEBUG=true |
||||
``` |
||||
```Go |
||||
type Specification struct { |
||||
ServiceHost string `envconfig:"SERVICE_HOST"` |
||||
Debug bool |
||||
} |
||||
``` |
||||
|
||||
Envconfig won't process a field with the "ignored" tag set to "true", even if a corresponding |
||||
environment variable is set. |
||||
|
||||
## Supported Struct Field Types |
||||
|
||||
envconfig supports these struct field types: |
||||
|
||||
* string |
||||
* int8, int16, int32, int64 |
||||
* bool |
||||
* float32, float64 |
||||
* slices of any supported type |
||||
* maps (keys and values of any supported type) |
||||
* [encoding.TextUnmarshaler](https://golang.org/pkg/encoding/#TextUnmarshaler) |
||||
* [encoding.BinaryUnmarshaler](https://golang.org/pkg/encoding/#BinaryUnmarshaler) |
||||
* [time.Duration](https://golang.org/pkg/time/#Duration) |
||||
|
||||
Embedded structs using these fields are also supported. |
||||
|
||||
## Custom Decoders |
||||
|
||||
Any field whose type (or pointer-to-type) implements `envconfig.Decoder` can |
||||
control its own deserialization: |
||||
|
||||
```Bash |
||||
export DNS_SERVER=8.8.8.8 |
||||
``` |
||||
|
||||
```Go |
||||
type IPDecoder net.IP |
||||
|
||||
func (ipd *IPDecoder) Decode(value string) error { |
||||
*ipd = IPDecoder(net.ParseIP(value)) |
||||
return nil |
||||
} |
||||
|
||||
type DNSConfig struct { |
||||
Address IPDecoder `envconfig:"DNS_SERVER"` |
||||
} |
||||
``` |
||||
|
||||
Also, envconfig will use a `Set(string) error` method like from the |
||||
[flag.Value](https://godoc.org/flag#Value) interface if implemented. |
@ -0,0 +1,8 @@ |
||||
// Copyright (c) 2013 Kelsey Hightower. All rights reserved.
|
||||
// Use of this source code is governed by the MIT License that can be found in
|
||||
// the LICENSE file.
|
||||
|
||||
// Package envconfig implements decoding of environment variables based on a user
|
||||
// defined specification. A typical use is using environment variables for
|
||||
// configuration settings.
|
||||
package envconfig |
@ -0,0 +1,7 @@ |
||||
// +build appengine go1.5
|
||||
|
||||
package envconfig |
||||
|
||||
import "os" |
||||
|
||||
var lookupEnv = os.LookupEnv |
@ -0,0 +1,7 @@ |
||||
// +build !appengine,!go1.5
|
||||
|
||||
package envconfig |
||||
|
||||
import "syscall" |
||||
|
||||
var lookupEnv = syscall.Getenv |
@ -0,0 +1,382 @@ |
||||
// Copyright (c) 2013 Kelsey Hightower. All rights reserved.
|
||||
// Use of this source code is governed by the MIT License that can be found in
|
||||
// the LICENSE file.
|
||||
|
||||
package envconfig |
||||
|
||||
import ( |
||||
"encoding" |
||||
"errors" |
||||
"fmt" |
||||
"os" |
||||
"reflect" |
||||
"regexp" |
||||
"strconv" |
||||
"strings" |
||||
"time" |
||||
) |
||||
|
||||
// ErrInvalidSpecification indicates that a specification is of the wrong type.
|
||||
var ErrInvalidSpecification = errors.New("specification must be a struct pointer") |
||||
|
||||
var gatherRegexp = regexp.MustCompile("([^A-Z]+|[A-Z]+[^A-Z]+|[A-Z]+)") |
||||
var acronymRegexp = regexp.MustCompile("([A-Z]+)([A-Z][^A-Z]+)") |
||||
|
||||
// A ParseError occurs when an environment variable cannot be converted to
|
||||
// the type required by a struct field during assignment.
|
||||
type ParseError struct { |
||||
KeyName string |
||||
FieldName string |
||||
TypeName string |
||||
Value string |
||||
Err error |
||||
} |
||||
|
||||
// Decoder has the same semantics as Setter, but takes higher precedence.
|
||||
// It is provided for historical compatibility.
|
||||
type Decoder interface { |
||||
Decode(value string) error |
||||
} |
||||
|
||||
// Setter is implemented by types can self-deserialize values.
|
||||
// Any type that implements flag.Value also implements Setter.
|
||||
type Setter interface { |
||||
Set(value string) error |
||||
} |
||||
|
||||
func (e *ParseError) Error() string { |
||||
return fmt.Sprintf("envconfig.Process: assigning %[1]s to %[2]s: converting '%[3]s' to type %[4]s. details: %[5]s", e.KeyName, e.FieldName, e.Value, e.TypeName, e.Err) |
||||
} |
||||
|
||||
// varInfo maintains information about the configuration variable
|
||||
type varInfo struct { |
||||
Name string |
||||
Alt string |
||||
Key string |
||||
Field reflect.Value |
||||
Tags reflect.StructTag |
||||
} |
||||
|
||||
// GatherInfo gathers information about the specified struct
|
||||
func gatherInfo(prefix string, spec interface{}) ([]varInfo, error) { |
||||
s := reflect.ValueOf(spec) |
||||
|
||||
if s.Kind() != reflect.Ptr { |
||||
return nil, ErrInvalidSpecification |
||||
} |
||||
s = s.Elem() |
||||
if s.Kind() != reflect.Struct { |
||||
return nil, ErrInvalidSpecification |
||||
} |
||||
typeOfSpec := s.Type() |
||||
|
||||
// over allocate an info array, we will extend if needed later
|
||||
infos := make([]varInfo, 0, s.NumField()) |
||||
for i := 0; i < s.NumField(); i++ { |
||||
f := s.Field(i) |
||||
ftype := typeOfSpec.Field(i) |
||||
if !f.CanSet() || isTrue(ftype.Tag.Get("ignored")) { |
||||
continue |
||||
} |
||||
|
||||
for f.Kind() == reflect.Ptr { |
||||
if f.IsNil() { |
||||
if f.Type().Elem().Kind() != reflect.Struct { |
||||
// nil pointer to a non-struct: leave it alone
|
||||
break |
||||
} |
||||
// nil pointer to struct: create a zero instance
|
||||
f.Set(reflect.New(f.Type().Elem())) |
||||
} |
||||
f = f.Elem() |
||||
} |
||||
|
||||
// Capture information about the config variable
|
||||
info := varInfo{ |
||||
Name: ftype.Name, |
||||
Field: f, |
||||
Tags: ftype.Tag, |
||||
Alt: strings.ToUpper(ftype.Tag.Get("envconfig")), |
||||
} |
||||
|
||||
// Default to the field name as the env var name (will be upcased)
|
||||
info.Key = info.Name |
||||
|
||||
// Best effort to un-pick camel casing as separate words
|
||||
if isTrue(ftype.Tag.Get("split_words")) { |
||||
words := gatherRegexp.FindAllStringSubmatch(ftype.Name, -1) |
||||
if len(words) > 0 { |
||||
var name []string |
||||
for _, words := range words { |
||||
if m := acronymRegexp.FindStringSubmatch(words[0]); len(m) == 3 { |
||||
name = append(name, m[1], m[2]) |
||||
} else { |
||||
name = append(name, words[0]) |
||||
} |
||||
} |
||||
|
||||
info.Key = strings.Join(name, "_") |
||||
} |
||||
} |
||||
if info.Alt != "" { |
||||
info.Key = info.Alt |
||||
} |
||||
if prefix != "" { |
||||
info.Key = fmt.Sprintf("%s_%s", prefix, info.Key) |
||||
} |
||||
info.Key = strings.ToUpper(info.Key) |
||||
infos = append(infos, info) |
||||
|
||||
if f.Kind() == reflect.Struct { |
||||
// honor Decode if present
|
||||
if decoderFrom(f) == nil && setterFrom(f) == nil && textUnmarshaler(f) == nil && binaryUnmarshaler(f) == nil { |
||||
innerPrefix := prefix |
||||
if !ftype.Anonymous { |
||||
innerPrefix = info.Key |
||||
} |
||||
|
||||
embeddedPtr := f.Addr().Interface() |
||||
embeddedInfos, err := gatherInfo(innerPrefix, embeddedPtr) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
infos = append(infos[:len(infos)-1], embeddedInfos...) |
||||
|
||||
continue |
||||
} |
||||
} |
||||
} |
||||
return infos, nil |
||||
} |
||||
|
||||
// CheckDisallowed checks that no environment variables with the prefix are set
|
||||
// that we don't know how or want to parse. This is likely only meaningful with
|
||||
// a non-empty prefix.
|
||||
func CheckDisallowed(prefix string, spec interface{}) error { |
||||
infos, err := gatherInfo(prefix, spec) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
vars := make(map[string]struct{}) |
||||
for _, info := range infos { |
||||
vars[info.Key] = struct{}{} |
||||
} |
||||
|
||||
if prefix != "" { |
||||
prefix = strings.ToUpper(prefix) + "_" |
||||
} |
||||
|
||||
for _, env := range os.Environ() { |
||||
if !strings.HasPrefix(env, prefix) { |
||||
continue |
||||
} |
||||
v := strings.SplitN(env, "=", 2)[0] |
||||
if _, found := vars[v]; !found { |
||||
return fmt.Errorf("unknown environment variable %s", v) |
||||
} |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
// Process populates the specified struct based on environment variables
|
||||
func Process(prefix string, spec interface{}) error { |
||||
infos, err := gatherInfo(prefix, spec) |
||||
|
||||
for _, info := range infos { |
||||
|
||||
// `os.Getenv` cannot differentiate between an explicitly set empty value
|
||||
// and an unset value. `os.LookupEnv` is preferred to `syscall.Getenv`,
|
||||
// but it is only available in go1.5 or newer. We're using Go build tags
|
||||
// here to use os.LookupEnv for >=go1.5
|
||||
value, ok := lookupEnv(info.Key) |
||||
if !ok && info.Alt != "" { |
||||
value, ok = lookupEnv(info.Alt) |
||||
} |
||||
|
||||
def := info.Tags.Get("default") |
||||
if def != "" && !ok { |
||||
value = def |
||||
} |
||||
|
||||
req := info.Tags.Get("required") |
||||
if !ok && def == "" { |
||||
if isTrue(req) { |
||||
key := info.Key |
||||
if info.Alt != "" { |
||||
key = info.Alt |
||||
} |
||||
return fmt.Errorf("required key %s missing value", key) |
||||
} |
||||
continue |
||||
} |
||||
|
||||
err = processField(value, info.Field) |
||||
if err != nil { |
||||
return &ParseError{ |
||||
KeyName: info.Key, |
||||
FieldName: info.Name, |
||||
TypeName: info.Field.Type().String(), |
||||
Value: value, |
||||
Err: err, |
||||
} |
||||
} |
||||
} |
||||
|
||||
return err |
||||
} |
||||
|
||||
// MustProcess is the same as Process but panics if an error occurs
|
||||
func MustProcess(prefix string, spec interface{}) { |
||||
if err := Process(prefix, spec); err != nil { |
||||
panic(err) |
||||
} |
||||
} |
||||
|
||||
func processField(value string, field reflect.Value) error { |
||||
typ := field.Type() |
||||
|
||||
decoder := decoderFrom(field) |
||||
if decoder != nil { |
||||
return decoder.Decode(value) |
||||
} |
||||
// look for Set method if Decode not defined
|
||||
setter := setterFrom(field) |
||||
if setter != nil { |
||||
return setter.Set(value) |
||||
} |
||||
|
||||
if t := textUnmarshaler(field); t != nil { |
||||
return t.UnmarshalText([]byte(value)) |
||||
} |
||||
|
||||
if b := binaryUnmarshaler(field); b != nil { |
||||
return b.UnmarshalBinary([]byte(value)) |
||||
} |
||||
|
||||
if typ.Kind() == reflect.Ptr { |
||||
typ = typ.Elem() |
||||
if field.IsNil() { |
||||
field.Set(reflect.New(typ)) |
||||
} |
||||
field = field.Elem() |
||||
} |
||||
|
||||
switch typ.Kind() { |
||||
case reflect.String: |
||||
field.SetString(value) |
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: |
||||
var ( |
||||
val int64 |
||||
err error |
||||
) |
||||
if field.Kind() == reflect.Int64 && typ.PkgPath() == "time" && typ.Name() == "Duration" { |
||||
var d time.Duration |
||||
d, err = time.ParseDuration(value) |
||||
val = int64(d) |
||||
} else { |
||||
val, err = strconv.ParseInt(value, 0, typ.Bits()) |
||||
} |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
field.SetInt(val) |
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: |
||||
val, err := strconv.ParseUint(value, 0, typ.Bits()) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
field.SetUint(val) |
||||
case reflect.Bool: |
||||
val, err := strconv.ParseBool(value) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
field.SetBool(val) |
||||
case reflect.Float32, reflect.Float64: |
||||
val, err := strconv.ParseFloat(value, typ.Bits()) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
field.SetFloat(val) |
||||
case reflect.Slice: |
||||
sl := reflect.MakeSlice(typ, 0, 0) |
||||
if typ.Elem().Kind() == reflect.Uint8 { |
||||
sl = reflect.ValueOf([]byte(value)) |
||||
} else if len(strings.TrimSpace(value)) != 0 { |
||||
vals := strings.Split(value, ",") |
||||
sl = reflect.MakeSlice(typ, len(vals), len(vals)) |
||||
for i, val := range vals { |
||||
err := processField(val, sl.Index(i)) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
} |
||||
} |
||||
field.Set(sl) |
||||
case reflect.Map: |
||||
mp := reflect.MakeMap(typ) |
||||
if len(strings.TrimSpace(value)) != 0 { |
||||
pairs := strings.Split(value, ",") |
||||
for _, pair := range pairs { |
||||
kvpair := strings.Split(pair, ":") |
||||
if len(kvpair) != 2 { |
||||
return fmt.Errorf("invalid map item: %q", pair) |
||||
} |
||||
k := reflect.New(typ.Key()).Elem() |
||||
err := processField(kvpair[0], k) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
v := reflect.New(typ.Elem()).Elem() |
||||
err = processField(kvpair[1], v) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
mp.SetMapIndex(k, v) |
||||
} |
||||
} |
||||
field.Set(mp) |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
func interfaceFrom(field reflect.Value, fn func(interface{}, *bool)) { |
||||
// it may be impossible for a struct field to fail this check
|
||||
if !field.CanInterface() { |
||||
return |
||||
} |
||||
var ok bool |
||||
fn(field.Interface(), &ok) |
||||
if !ok && field.CanAddr() { |
||||
fn(field.Addr().Interface(), &ok) |
||||
} |
||||
} |
||||
|
||||
func decoderFrom(field reflect.Value) (d Decoder) { |
||||
interfaceFrom(field, func(v interface{}, ok *bool) { d, *ok = v.(Decoder) }) |
||||
return d |
||||
} |
||||
|
||||
func setterFrom(field reflect.Value) (s Setter) { |
||||
interfaceFrom(field, func(v interface{}, ok *bool) { s, *ok = v.(Setter) }) |
||||
return s |
||||
} |
||||
|
||||
func textUnmarshaler(field reflect.Value) (t encoding.TextUnmarshaler) { |
||||
interfaceFrom(field, func(v interface{}, ok *bool) { t, *ok = v.(encoding.TextUnmarshaler) }) |
||||
return t |
||||
} |
||||
|
||||
func binaryUnmarshaler(field reflect.Value) (b encoding.BinaryUnmarshaler) { |
||||
interfaceFrom(field, func(v interface{}, ok *bool) { b, *ok = v.(encoding.BinaryUnmarshaler) }) |
||||
return b |
||||
} |
||||
|
||||
func isTrue(s string) bool { |
||||
b, _ := strconv.ParseBool(s) |
||||
return b |
||||
} |
@ -0,0 +1,164 @@ |
||||
// Copyright (c) 2016 Kelsey Hightower and others. All rights reserved.
|
||||
// Use of this source code is governed by the MIT License that can be found in
|
||||
// the LICENSE file.
|
||||
|
||||
package envconfig |
||||
|
||||
import ( |
||||
"encoding" |
||||
"fmt" |
||||
"io" |
||||
"os" |
||||
"reflect" |
||||
"strconv" |
||||
"strings" |
||||
"text/tabwriter" |
||||
"text/template" |
||||
) |
||||
|
||||
const ( |
||||
// DefaultListFormat constant to use to display usage in a list format
|
||||
DefaultListFormat = `This application is configured via the environment. The following environment |
||||
variables can be used: |
||||
{{range .}} |
||||
{{usage_key .}} |
||||
[description] {{usage_description .}} |
||||
[type] {{usage_type .}} |
||||
[default] {{usage_default .}} |
||||
[required] {{usage_required .}}{{end}} |
||||
` |
||||
// DefaultTableFormat constant to use to display usage in a tabular format
|
||||
DefaultTableFormat = `This application is configured via the environment. The following environment |
||||
variables can be used: |
||||
|
||||
KEY TYPE DEFAULT REQUIRED DESCRIPTION |
||||
{{range .}}{{usage_key .}} {{usage_type .}} {{usage_default .}} {{usage_required .}} {{usage_description .}} |
||||
{{end}}` |
||||
) |
||||
|
||||
var ( |
||||
decoderType = reflect.TypeOf((*Decoder)(nil)).Elem() |
||||
setterType = reflect.TypeOf((*Setter)(nil)).Elem() |
||||
textUnmarshalerType = reflect.TypeOf((*encoding.TextUnmarshaler)(nil)).Elem() |
||||
binaryUnmarshalerType = reflect.TypeOf((*encoding.BinaryUnmarshaler)(nil)).Elem() |
||||
) |
||||
|
||||
func implementsInterface(t reflect.Type) bool { |
||||
return t.Implements(decoderType) || |
||||
reflect.PtrTo(t).Implements(decoderType) || |
||||
t.Implements(setterType) || |
||||
reflect.PtrTo(t).Implements(setterType) || |
||||
t.Implements(textUnmarshalerType) || |
||||
reflect.PtrTo(t).Implements(textUnmarshalerType) || |
||||
t.Implements(binaryUnmarshalerType) || |
||||
reflect.PtrTo(t).Implements(binaryUnmarshalerType) |
||||
} |
||||
|
||||
// toTypeDescription converts Go types into a human readable description
|
||||
func toTypeDescription(t reflect.Type) string { |
||||
switch t.Kind() { |
||||
case reflect.Array, reflect.Slice: |
||||
if t.Elem().Kind() == reflect.Uint8 { |
||||
return "String" |
||||
} |
||||
return fmt.Sprintf("Comma-separated list of %s", toTypeDescription(t.Elem())) |
||||
case reflect.Map: |
||||
return fmt.Sprintf( |
||||
"Comma-separated list of %s:%s pairs", |
||||
toTypeDescription(t.Key()), |
||||
toTypeDescription(t.Elem()), |
||||
) |
||||
case reflect.Ptr: |
||||
return toTypeDescription(t.Elem()) |
||||
case reflect.Struct: |
||||
if implementsInterface(t) && t.Name() != "" { |
||||
return t.Name() |
||||
} |
||||
return "" |
||||
case reflect.String: |
||||
name := t.Name() |
||||
if name != "" && name != "string" { |
||||
return name |
||||
} |
||||
return "String" |
||||
case reflect.Bool: |
||||
name := t.Name() |
||||
if name != "" && name != "bool" { |
||||
return name |
||||
} |
||||
return "True or False" |
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: |
||||
name := t.Name() |
||||
if name != "" && !strings.HasPrefix(name, "int") { |
||||
return name |
||||
} |
||||
return "Integer" |
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: |
||||
name := t.Name() |
||||
if name != "" && !strings.HasPrefix(name, "uint") { |
||||
return name |
||||
} |
||||
return "Unsigned Integer" |
||||
case reflect.Float32, reflect.Float64: |
||||
name := t.Name() |
||||
if name != "" && !strings.HasPrefix(name, "float") { |
||||
return name |
||||
} |
||||
return "Float" |
||||
} |
||||
return fmt.Sprintf("%+v", t) |
||||
} |
||||
|
||||
// Usage writes usage information to stdout using the default header and table format
|
||||
func Usage(prefix string, spec interface{}) error { |
||||
// The default is to output the usage information as a table
|
||||
// Create tabwriter instance to support table output
|
||||
tabs := tabwriter.NewWriter(os.Stdout, 1, 0, 4, ' ', 0) |
||||
|
||||
err := Usagef(prefix, spec, tabs, DefaultTableFormat) |
||||
tabs.Flush() |
||||
return err |
||||
} |
||||
|
||||
// Usagef writes usage information to the specified io.Writer using the specifed template specification
|
||||
func Usagef(prefix string, spec interface{}, out io.Writer, format string) error { |
||||
|
||||
// Specify the default usage template functions
|
||||
functions := template.FuncMap{ |
||||
"usage_key": func(v varInfo) string { return v.Key }, |
||||
"usage_description": func(v varInfo) string { return v.Tags.Get("desc") }, |
||||
"usage_type": func(v varInfo) string { return toTypeDescription(v.Field.Type()) }, |
||||
"usage_default": func(v varInfo) string { return v.Tags.Get("default") }, |
||||
"usage_required": func(v varInfo) (string, error) { |
||||
req := v.Tags.Get("required") |
||||
if req != "" { |
||||
reqB, err := strconv.ParseBool(req) |
||||
if err != nil { |
||||
return "", err |
||||
} |
||||
if reqB { |
||||
req = "true" |
||||
} |
||||
} |
||||
return req, nil |
||||
}, |
||||
} |
||||
|
||||
tmpl, err := template.New("envconfig").Funcs(functions).Parse(format) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
return Usaget(prefix, spec, out, tmpl) |
||||
} |
||||
|
||||
// Usaget writes usage information to the specified io.Writer using the specified template
|
||||
func Usaget(prefix string, spec interface{}, out io.Writer, tmpl *template.Template) error { |
||||
// gather first
|
||||
infos, err := gatherInfo(prefix, spec) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
return tmpl.Execute(out, infos) |
||||
} |
@ -0,0 +1,3 @@ |
||||
# github.com/kelseyhightower/envconfig v1.4.0 |
||||
## explicit |
||||
github.com/kelseyhightower/envconfig |
Loading…
Reference in new issue