mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-03-03 14:57:22 -05:00
maintain envdecode inside ocis-pkg
This commit is contained in:
@@ -7,10 +7,10 @@ import (
|
||||
|
||||
"github.com/owncloud/ocis/accounts/pkg/config"
|
||||
ociscfg "github.com/owncloud/ocis/ocis-pkg/config"
|
||||
"github.com/owncloud/ocis/ocis-pkg/config/envdecode"
|
||||
"github.com/owncloud/ocis/ocis-pkg/version"
|
||||
"github.com/thejerf/suture/v4"
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/wkloucek/envdecode"
|
||||
)
|
||||
|
||||
// Execute is the entry point for the ocis-accounts command.
|
||||
|
||||
@@ -6,10 +6,10 @@ import (
|
||||
|
||||
"github.com/owncloud/ocis/glauth/pkg/config"
|
||||
ociscfg "github.com/owncloud/ocis/ocis-pkg/config"
|
||||
"github.com/owncloud/ocis/ocis-pkg/config/envdecode"
|
||||
"github.com/owncloud/ocis/ocis-pkg/version"
|
||||
"github.com/thejerf/suture/v4"
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/wkloucek/envdecode"
|
||||
)
|
||||
|
||||
// Execute is the entry point for the ocis-glauth command.
|
||||
|
||||
1
go.mod
1
go.mod
@@ -55,7 +55,6 @@ require (
|
||||
github.com/stretchr/testify v1.7.0
|
||||
github.com/thejerf/suture/v4 v4.0.1
|
||||
github.com/urfave/cli/v2 v2.3.0
|
||||
github.com/wkloucek/envdecode v0.0.0-20211216135343-360f0d3c2679
|
||||
go-micro.dev/v4 v4.5.0
|
||||
go.opencensus.io v0.23.0
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.28.0
|
||||
|
||||
2
go.sum
2
go.sum
@@ -1306,8 +1306,6 @@ github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgq
|
||||
github.com/vultr/govultr/v2 v2.0.0/go.mod h1:2PsEeg+gs3p/Fo5Pw8F9mv+DUBEOlrNZ8GmCTGmhOhs=
|
||||
github.com/wk8/go-ordered-map v0.2.0 h1:KlvGyHstD1kkGZkPtHCyCfRYS0cz84uk6rrW/Dnhdtk=
|
||||
github.com/wk8/go-ordered-map v0.2.0/go.mod h1:9ZIbRunKbuvfPKyBP1SIKLcXNlv74YCOZ3t3VTS6gRk=
|
||||
github.com/wkloucek/envdecode v0.0.0-20211216135343-360f0d3c2679 h1:aFJVdr5Lo6QrfgW4nlmguvATkSp+iOfIg6rcdTfu9eM=
|
||||
github.com/wkloucek/envdecode v0.0.0-20211216135343-360f0d3c2679/go.mod h1:lEir1NV8XGJ16mCsne3GrW6MbiQyhf5WUk55kvu9rYs=
|
||||
github.com/xanzy/ssh-agent v0.3.0/go.mod h1:3s9xbODqPuuhK9JV1R321M/FlMZSBvE5aY6eAcqrDh0=
|
||||
github.com/xanzy/ssh-agent v0.3.1 h1:AmzO1SSWxw73zxFZPRwaMN1MohDw8UyHnmuxyceTEGo=
|
||||
github.com/xanzy/ssh-agent v0.3.1/go.mod h1:QIE4lCeL7nkC25x+yA3LBIYfwCc1TFziCtG7cBAac6w=
|
||||
|
||||
@@ -6,10 +6,10 @@ import (
|
||||
|
||||
"github.com/owncloud/ocis/graph-explorer/pkg/config"
|
||||
ociscfg "github.com/owncloud/ocis/ocis-pkg/config"
|
||||
"github.com/owncloud/ocis/ocis-pkg/config/envdecode"
|
||||
"github.com/owncloud/ocis/ocis-pkg/version"
|
||||
"github.com/thejerf/suture/v4"
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/wkloucek/envdecode"
|
||||
)
|
||||
|
||||
// Execute is the entry point for the graph-explorer command.
|
||||
|
||||
@@ -4,8 +4,8 @@ import (
|
||||
"context"
|
||||
"os"
|
||||
|
||||
"github.com/owncloud/ocis/ocis-pkg/config/envdecode"
|
||||
"github.com/thejerf/suture/v4"
|
||||
"github.com/wkloucek/envdecode"
|
||||
|
||||
"github.com/owncloud/ocis/graph/pkg/config"
|
||||
ociscfg "github.com/owncloud/ocis/ocis-pkg/config"
|
||||
|
||||
@@ -6,10 +6,10 @@ import (
|
||||
|
||||
"github.com/owncloud/ocis/idp/pkg/config"
|
||||
ociscfg "github.com/owncloud/ocis/ocis-pkg/config"
|
||||
"github.com/owncloud/ocis/ocis-pkg/config/envdecode"
|
||||
"github.com/owncloud/ocis/ocis-pkg/version"
|
||||
"github.com/thejerf/suture/v4"
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/wkloucek/envdecode"
|
||||
)
|
||||
|
||||
// Execute is the entry point for the ocis-idp command.
|
||||
|
||||
21
ocis-pkg/config/envdecode/LICENSE
Normal file
21
ocis-pkg/config/envdecode/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014 Joe Shaw
|
||||
|
||||
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.
|
||||
88
ocis-pkg/config/envdecode/README.md
Normal file
88
ocis-pkg/config/envdecode/README.md
Normal file
@@ -0,0 +1,88 @@
|
||||
`envdecode` is a Go package for populating structs from environment
|
||||
variables. It's basically a fork of https://github.com/joeshaw/envdecode,
|
||||
but changed to support multiple environment variables (precedence).
|
||||
|
||||
`envdecode` uses struct tags to map environment variables to fields,
|
||||
allowing you you use any names you want for environment variables.
|
||||
`envdecode` will recurse into nested structs, including pointers to
|
||||
nested structs, but it will not allocate new pointers to structs.
|
||||
|
||||
## API
|
||||
|
||||
Full API docs are available on
|
||||
[godoc.org](https://godoc.org/github.com/owncloud/ocis/ocis-pkg/config/envdecode).
|
||||
|
||||
Define a struct with `env` struct tags:
|
||||
|
||||
```go
|
||||
type Config struct {
|
||||
Hostname string `env:"SERVER_HOSTNAME,default=localhost"`
|
||||
Port uint16 `env:"HTTP_PORT;SERVER_PORT,default=8080"`
|
||||
|
||||
AWS struct {
|
||||
ID string `env:"AWS_ACCESS_KEY_ID"`
|
||||
Secret string `env:"AWS_SECRET_ACCESS_KEY,required"`
|
||||
SnsTopics []string `env:"AWS_SNS_TOPICS"`
|
||||
}
|
||||
|
||||
Timeout time.Duration `env:"TIMEOUT,default=1m,strict"`
|
||||
}
|
||||
```
|
||||
|
||||
Fields _must be exported_ (i.e. begin with a capital letter) in order
|
||||
for `envdecode` to work with them. An error will be returned if a
|
||||
struct with no exported fields is decoded (including one that contains
|
||||
no `env` tags at all).
|
||||
Default values may be provided by appending ",default=value" to the
|
||||
struct tag. Required values may be marked by appending ",required" to the
|
||||
struct tag. Strict values may be marked by appending ",strict" which will
|
||||
return an error on Decode if there is an error while parsing.
|
||||
|
||||
Then call `envdecode.Decode`:
|
||||
|
||||
```go
|
||||
var cfg Config
|
||||
err := envdecode.Decode(&cfg)
|
||||
```
|
||||
|
||||
If you want all fields to act `strict`, you may use `envdecode.StrictDecode`:
|
||||
|
||||
```go
|
||||
var cfg Config
|
||||
err := envdecode.StrictDecode(&cfg)
|
||||
```
|
||||
|
||||
All parse errors will fail fast and return an error in this mode.
|
||||
|
||||
## Supported types
|
||||
|
||||
- Structs (and pointer to structs)
|
||||
- Slices of below defined types, separated by semicolon
|
||||
- `bool`
|
||||
- `float32`, `float64`
|
||||
- `int`, `int8`, `int16`, `int32`, `int64`
|
||||
- `uint`, `uint8`, `uint16`, `uint32`, `uint64`
|
||||
- `string`
|
||||
- `time.Duration`, using the [`time.ParseDuration()` format](http://golang.org/pkg/time/#ParseDuration)
|
||||
- `*url.URL`, using [`url.Parse()`](https://godoc.org/net/url#Parse)
|
||||
- Types those implement a `Decoder` interface
|
||||
|
||||
## Custom `Decoder`
|
||||
|
||||
If you want a field to be decoded with custom behavior, you may implement the interface `Decoder` for the filed type.
|
||||
|
||||
```go
|
||||
type Config struct {
|
||||
IPAddr IP `env:"IP_ADDR"`
|
||||
}
|
||||
|
||||
type IP net.IP
|
||||
|
||||
// Decode implements the interface `envdecode.Decoder`
|
||||
func (i *IP) Decode(repl string) error {
|
||||
*i = net.ParseIP(repl)
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
`Decoder` is the interface implemented by an object that can decode an environment variable string representation of itself.
|
||||
431
ocis-pkg/config/envdecode/envdecode.go
Normal file
431
ocis-pkg/config/envdecode/envdecode.go
Normal file
@@ -0,0 +1,431 @@
|
||||
// Package envdecode is a package for populating structs from environment
|
||||
// variables, using struct tags.
|
||||
package envdecode
|
||||
|
||||
import (
|
||||
"encoding"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/url"
|
||||
"os"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ErrInvalidTarget indicates that the target value passed to
|
||||
// Decode is invalid. Target must be a non-nil pointer to a struct.
|
||||
var ErrInvalidTarget = errors.New("target must be non-nil pointer to struct that has at least one exported field with a valid env tag.")
|
||||
var ErrNoTargetFieldsAreSet = errors.New("none of the target fields were set from environment variables")
|
||||
|
||||
// FailureFunc is called when an error is encountered during a MustDecode
|
||||
// operation. It prints the error and terminates the process.
|
||||
//
|
||||
// This variable can be assigned to another function of the user-programmer's
|
||||
// design, allowing for graceful recovery of the problem, such as loading
|
||||
// from a backup configuration file.
|
||||
var FailureFunc = func(err error) {
|
||||
log.Fatalf("envdecode: an error was encountered while decoding: %v\n", err)
|
||||
}
|
||||
|
||||
// Decoder is the interface implemented by an object that can decode an
|
||||
// environment variable string representation of itself.
|
||||
type Decoder interface {
|
||||
Decode(string) error
|
||||
}
|
||||
|
||||
// Decode environment variables into the provided target. The target
|
||||
// must be a non-nil pointer to a struct. Fields in the struct must
|
||||
// be exported, and tagged with an "env" struct tag with a value
|
||||
// containing the name of the environment variable. An error is
|
||||
// returned if there are no exported members tagged.
|
||||
//
|
||||
// Default values may be provided by appending ",default=value" to the
|
||||
// struct tag. Required values may be marked by appending ",required"
|
||||
// to the struct tag. It is an error to provide both "default" and
|
||||
// "required". Strict values may be marked by appending ",strict" which
|
||||
// will return an error on Decode if there is an error while parsing.
|
||||
// If everything must be strict, consider using StrictDecode instead.
|
||||
//
|
||||
// All primitive types are supported, including bool, floating point,
|
||||
// signed and unsigned integers, and string. Boolean and numeric
|
||||
// types are decoded using the standard strconv Parse functions for
|
||||
// those types. Structs and pointers to structs are decoded
|
||||
// recursively. time.Duration is supported via the
|
||||
// time.ParseDuration() function and *url.URL is supported via the
|
||||
// url.Parse() function. Slices are supported for all above mentioned
|
||||
// primitive types. Semicolon is used as delimiter in environment variables.
|
||||
func Decode(target interface{}) error {
|
||||
nFields, err := decode(target, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// if we didn't do anything - the user probably did something
|
||||
// wrong like leave all fields unexported.
|
||||
if nFields == 0 {
|
||||
return ErrNoTargetFieldsAreSet
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// StrictDecode is similar to Decode except all fields will have an implicit
|
||||
// ",strict" on all fields.
|
||||
func StrictDecode(target interface{}) error {
|
||||
nFields, err := decode(target, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// if we didn't do anything - the user probably did something
|
||||
// wrong like leave all fields unexported.
|
||||
if nFields == 0 {
|
||||
return ErrInvalidTarget
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func decode(target interface{}, strict bool) (int, error) {
|
||||
s := reflect.ValueOf(target)
|
||||
if s.Kind() != reflect.Ptr || s.IsNil() {
|
||||
return 0, ErrInvalidTarget
|
||||
}
|
||||
|
||||
s = s.Elem()
|
||||
if s.Kind() != reflect.Struct {
|
||||
return 0, ErrInvalidTarget
|
||||
}
|
||||
|
||||
t := s.Type()
|
||||
setFieldCount := 0
|
||||
for i := 0; i < s.NumField(); i++ {
|
||||
// Localize the umbrella `strict` value to the specific field.
|
||||
strict := strict
|
||||
|
||||
f := s.Field(i)
|
||||
|
||||
switch f.Kind() {
|
||||
case reflect.Ptr:
|
||||
if f.Elem().Kind() != reflect.Struct {
|
||||
break
|
||||
}
|
||||
|
||||
f = f.Elem()
|
||||
fallthrough
|
||||
|
||||
case reflect.Struct:
|
||||
if !f.Addr().CanInterface() {
|
||||
continue
|
||||
}
|
||||
|
||||
ss := f.Addr().Interface()
|
||||
_, custom := ss.(Decoder)
|
||||
if custom {
|
||||
break
|
||||
}
|
||||
|
||||
n, err := decode(ss, strict)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
setFieldCount += n
|
||||
}
|
||||
|
||||
if !f.CanSet() {
|
||||
continue
|
||||
}
|
||||
|
||||
tag := t.Field(i).Tag.Get("env")
|
||||
if tag == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
parts := strings.Split(tag, ",")
|
||||
overrides := strings.Split(parts[0], `;`)
|
||||
|
||||
var env string
|
||||
for _, override := range overrides {
|
||||
v := os.Getenv(override)
|
||||
if v != "" {
|
||||
env = v
|
||||
}
|
||||
}
|
||||
|
||||
required := false
|
||||
hasDefault := false
|
||||
defaultValue := ""
|
||||
|
||||
for _, o := range parts[1:] {
|
||||
if !required {
|
||||
required = strings.HasPrefix(o, "required")
|
||||
}
|
||||
if strings.HasPrefix(o, "default=") {
|
||||
hasDefault = true
|
||||
defaultValue = o[8:]
|
||||
}
|
||||
if !strict {
|
||||
strict = strings.HasPrefix(o, "strict")
|
||||
}
|
||||
}
|
||||
|
||||
if required && hasDefault {
|
||||
panic(`envdecode: "default" and "required" may not be specified in the same annotation`)
|
||||
}
|
||||
if env == "" && required {
|
||||
return 0, fmt.Errorf("the environment variable \"%s\" is missing", parts[0])
|
||||
}
|
||||
if env == "" {
|
||||
env = defaultValue
|
||||
}
|
||||
if env == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
setFieldCount++
|
||||
|
||||
unmarshaler, implementsUnmarshaler := f.Addr().Interface().(encoding.TextUnmarshaler)
|
||||
decoder, implmentsDecoder := f.Addr().Interface().(Decoder)
|
||||
if implmentsDecoder {
|
||||
if err := decoder.Decode(env); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
} else if implementsUnmarshaler {
|
||||
if err := unmarshaler.UnmarshalText([]byte(env)); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
} else if f.Kind() == reflect.Slice {
|
||||
decodeSlice(&f, env)
|
||||
} else {
|
||||
if err := decodePrimitiveType(&f, env); err != nil && strict {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return setFieldCount, nil
|
||||
}
|
||||
|
||||
func decodeSlice(f *reflect.Value, env string) {
|
||||
parts := strings.Split(env, ";")
|
||||
|
||||
values := parts[:0]
|
||||
for _, x := range parts {
|
||||
if x != "" {
|
||||
values = append(values, strings.TrimSpace(x))
|
||||
}
|
||||
}
|
||||
|
||||
valuesCount := len(values)
|
||||
slice := reflect.MakeSlice(f.Type(), valuesCount, valuesCount)
|
||||
if valuesCount > 0 {
|
||||
for i := 0; i < valuesCount; i++ {
|
||||
e := slice.Index(i)
|
||||
decodePrimitiveType(&e, values[i])
|
||||
}
|
||||
}
|
||||
|
||||
f.Set(slice)
|
||||
}
|
||||
|
||||
func decodePrimitiveType(f *reflect.Value, env string) error {
|
||||
switch f.Kind() {
|
||||
case reflect.Bool:
|
||||
v, err := strconv.ParseBool(env)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f.SetBool(v)
|
||||
|
||||
case reflect.Float32, reflect.Float64:
|
||||
bits := f.Type().Bits()
|
||||
v, err := strconv.ParseFloat(env, bits)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f.SetFloat(v)
|
||||
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
if t := f.Type(); t.PkgPath() == "time" && t.Name() == "Duration" {
|
||||
v, err := time.ParseDuration(env)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f.SetInt(int64(v))
|
||||
} else {
|
||||
bits := f.Type().Bits()
|
||||
v, err := strconv.ParseInt(env, 0, bits)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f.SetInt(v)
|
||||
}
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
bits := f.Type().Bits()
|
||||
v, err := strconv.ParseUint(env, 0, bits)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f.SetUint(v)
|
||||
|
||||
case reflect.String:
|
||||
f.SetString(env)
|
||||
|
||||
case reflect.Ptr:
|
||||
if t := f.Type().Elem(); t.Kind() == reflect.Struct && t.PkgPath() == "net/url" && t.Name() == "URL" {
|
||||
v, err := url.Parse(env)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f.Set(reflect.ValueOf(v))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MustDecode calls Decode and terminates the process if any errors
|
||||
// are encountered.
|
||||
func MustDecode(target interface{}) {
|
||||
err := Decode(target)
|
||||
if err != nil {
|
||||
FailureFunc(err)
|
||||
}
|
||||
}
|
||||
|
||||
// MustStrictDecode calls StrictDecode and terminates the process if any errors
|
||||
// are encountered.
|
||||
func MustStrictDecode(target interface{}) {
|
||||
err := StrictDecode(target)
|
||||
if err != nil {
|
||||
FailureFunc(err)
|
||||
}
|
||||
}
|
||||
|
||||
//// Configuration info for Export
|
||||
|
||||
type ConfigInfo struct {
|
||||
Field string
|
||||
EnvVar string
|
||||
Value string
|
||||
DefaultValue string
|
||||
HasDefault bool
|
||||
Required bool
|
||||
UsesEnv bool
|
||||
}
|
||||
|
||||
type ConfigInfoSlice []*ConfigInfo
|
||||
|
||||
func (c ConfigInfoSlice) Less(i, j int) bool {
|
||||
return c[i].EnvVar < c[j].EnvVar
|
||||
}
|
||||
func (c ConfigInfoSlice) Len() int {
|
||||
return len(c)
|
||||
}
|
||||
func (c ConfigInfoSlice) Swap(i, j int) {
|
||||
c[i], c[j] = c[j], c[i]
|
||||
}
|
||||
|
||||
// Returns a list of final configuration metadata sorted by envvar name
|
||||
func Export(target interface{}) ([]*ConfigInfo, error) {
|
||||
s := reflect.ValueOf(target)
|
||||
if s.Kind() != reflect.Ptr || s.IsNil() {
|
||||
return nil, ErrInvalidTarget
|
||||
}
|
||||
|
||||
cfg := []*ConfigInfo{}
|
||||
|
||||
s = s.Elem()
|
||||
if s.Kind() != reflect.Struct {
|
||||
return nil, ErrInvalidTarget
|
||||
}
|
||||
|
||||
t := s.Type()
|
||||
for i := 0; i < s.NumField(); i++ {
|
||||
f := s.Field(i)
|
||||
fName := t.Field(i).Name
|
||||
|
||||
fElem := f
|
||||
if f.Kind() == reflect.Ptr {
|
||||
fElem = f.Elem()
|
||||
}
|
||||
if fElem.Kind() == reflect.Struct {
|
||||
ss := fElem.Addr().Interface()
|
||||
subCfg, err := Export(ss)
|
||||
if err != ErrInvalidTarget {
|
||||
f = fElem
|
||||
for _, v := range subCfg {
|
||||
v.Field = fmt.Sprintf("%s.%s", fName, v.Field)
|
||||
cfg = append(cfg, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tag := t.Field(i).Tag.Get("env")
|
||||
if tag == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
parts := strings.Split(tag, ",")
|
||||
|
||||
ci := &ConfigInfo{
|
||||
Field: fName,
|
||||
EnvVar: parts[0],
|
||||
UsesEnv: os.Getenv(parts[0]) != "",
|
||||
}
|
||||
|
||||
for _, o := range parts[1:] {
|
||||
if strings.HasPrefix(o, "default=") {
|
||||
ci.HasDefault = true
|
||||
ci.DefaultValue = o[8:]
|
||||
} else if strings.HasPrefix(o, "required") {
|
||||
ci.Required = true
|
||||
}
|
||||
}
|
||||
|
||||
if f.Kind() == reflect.Ptr && f.IsNil() {
|
||||
ci.Value = ""
|
||||
} else if stringer, ok := f.Interface().(fmt.Stringer); ok {
|
||||
ci.Value = stringer.String()
|
||||
} else {
|
||||
switch f.Kind() {
|
||||
case reflect.Bool:
|
||||
ci.Value = strconv.FormatBool(f.Bool())
|
||||
|
||||
case reflect.Float32, reflect.Float64:
|
||||
bits := f.Type().Bits()
|
||||
ci.Value = strconv.FormatFloat(f.Float(), 'f', -1, bits)
|
||||
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
ci.Value = strconv.FormatInt(f.Int(), 10)
|
||||
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
ci.Value = strconv.FormatUint(f.Uint(), 10)
|
||||
|
||||
case reflect.String:
|
||||
ci.Value = f.String()
|
||||
|
||||
case reflect.Slice:
|
||||
ci.Value = fmt.Sprintf("%v", f.Interface())
|
||||
|
||||
default:
|
||||
// Unable to determine string format for value
|
||||
return nil, ErrInvalidTarget
|
||||
}
|
||||
}
|
||||
|
||||
cfg = append(cfg, ci)
|
||||
}
|
||||
|
||||
// No configuration tags found, assume invalid input
|
||||
if len(cfg) == 0 {
|
||||
return nil, ErrInvalidTarget
|
||||
}
|
||||
|
||||
sort.Sort(ConfigInfoSlice(cfg))
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
795
ocis-pkg/config/envdecode/envdecode_test.go
Normal file
795
ocis-pkg/config/envdecode/envdecode_test.go
Normal file
@@ -0,0 +1,795 @@
|
||||
package envdecode
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"net/url"
|
||||
"os"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strconv"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type nested struct {
|
||||
String string `env:"TEST_STRING"`
|
||||
}
|
||||
|
||||
type testConfig struct {
|
||||
String string `env:"TEST_STRING"`
|
||||
Int64 int64 `env:"TEST_INT64"`
|
||||
Uint16 uint16 `env:"TEST_UINT16"`
|
||||
Float64 float64 `env:"TEST_FLOAT64"`
|
||||
Bool bool `env:"TEST_BOOL"`
|
||||
Duration time.Duration `env:"TEST_DURATION"`
|
||||
URL *url.URL `env:"TEST_URL"`
|
||||
|
||||
StringSlice []string `env:"TEST_STRING_SLICE"`
|
||||
Int64Slice []int64 `env:"TEST_INT64_SLICE"`
|
||||
Uint16Slice []uint16 `env:"TEST_UINT16_SLICE"`
|
||||
Float64Slice []float64 `env:"TEST_FLOAT64_SLICE"`
|
||||
BoolSlice []bool `env:"TEST_BOOL_SLICE"`
|
||||
DurationSlice []time.Duration `env:"TEST_DURATION_SLICE"`
|
||||
URLSlice []*url.URL `env:"TEST_URL_SLICE"`
|
||||
|
||||
UnsetString string `env:"TEST_UNSET_STRING"`
|
||||
UnsetInt64 int64 `env:"TEST_UNSET_INT64"`
|
||||
UnsetDuration time.Duration `env:"TEST_UNSET_DURATION"`
|
||||
UnsetURL *url.URL `env:"TEST_UNSET_URL"`
|
||||
UnsetSlice []string `env:"TEST_UNSET_SLICE"`
|
||||
|
||||
InvalidInt64 int64 `env:"TEST_INVALID_INT64"`
|
||||
|
||||
UnusedField string
|
||||
unexportedField string
|
||||
|
||||
IgnoredPtr *bool `env:"TEST_BOOL"`
|
||||
|
||||
Nested nested
|
||||
NestedPtr *nested
|
||||
|
||||
DecoderStruct decoderStruct `env:"TEST_DECODER_STRUCT"`
|
||||
DecoderStructPtr *decoderStruct `env:"TEST_DECODER_STRUCT_PTR"`
|
||||
|
||||
DecoderString decoderString `env:"TEST_DECODER_STRING"`
|
||||
|
||||
UnmarshalerNumber unmarshalerNumber `env:"TEST_UNMARSHALER_NUMBER"`
|
||||
|
||||
DefaultInt int `env:"TEST_UNSET,asdf=asdf,default=1234"`
|
||||
DefaultSliceInt []int `env:"TEST_UNSET,asdf=asdf,default=1;2;3"`
|
||||
DefaultDuration time.Duration `env:"TEST_UNSET,asdf=asdf,default=24h"`
|
||||
DefaultURL *url.URL `env:"TEST_UNSET,default=http://example.com"`
|
||||
|
||||
cantInterfaceField sync.Mutex
|
||||
}
|
||||
|
||||
type testConfigNoSet struct {
|
||||
Some string `env:"TEST_THIS_ENV_WILL_NOT_BE_SET"`
|
||||
}
|
||||
|
||||
type testConfigRequired struct {
|
||||
Required string `env:"TEST_REQUIRED,required"`
|
||||
}
|
||||
|
||||
type testConfigRequiredDefault struct {
|
||||
RequiredDefault string `env:"TEST_REQUIRED_DEFAULT,required,default=test"`
|
||||
}
|
||||
|
||||
type testConfigOverride struct {
|
||||
OverrideString string `env:"TEST_OVERRIDE_A;TEST_OVERRIDE_B,default=override_default"`
|
||||
}
|
||||
|
||||
type testNoExportedFields struct {
|
||||
aString string `env:"TEST_STRING"`
|
||||
anInt64 int64 `env:"TEST_INT64"`
|
||||
aUint16 uint16 `env:"TEST_UINT16"`
|
||||
aFloat64 float64 `env:"TEST_FLOAT64"`
|
||||
aBool bool `env:"TEST_BOOL"`
|
||||
}
|
||||
|
||||
type testNoTags struct {
|
||||
String string
|
||||
}
|
||||
|
||||
type decoderStruct struct {
|
||||
String string
|
||||
}
|
||||
|
||||
func (d *decoderStruct) Decode(env string) error {
|
||||
return json.Unmarshal([]byte(env), &d)
|
||||
}
|
||||
|
||||
type decoderString string
|
||||
|
||||
func (d *decoderString) Decode(env string) error {
|
||||
r, l := []rune(env), len(env)
|
||||
|
||||
for i := 0; i < l/2; i++ {
|
||||
r[i], r[l-1-i] = r[l-1-i], r[i]
|
||||
}
|
||||
|
||||
*d = decoderString(r)
|
||||
return nil
|
||||
}
|
||||
|
||||
type unmarshalerNumber uint8
|
||||
|
||||
func (o *unmarshalerNumber) UnmarshalText(raw []byte) error {
|
||||
n, err := strconv.ParseUint(string(raw), 8, 8) // parse text as octal number
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*o = unmarshalerNumber(n)
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestDecode(t *testing.T) {
|
||||
int64Val := int64(-(1 << 50))
|
||||
int64AsString := fmt.Sprintf("%d", int64Val)
|
||||
piAsString := fmt.Sprintf("%.48f", math.Pi)
|
||||
|
||||
os.Setenv("TEST_STRING", "foo")
|
||||
os.Setenv("TEST_INT64", int64AsString)
|
||||
os.Setenv("TEST_UINT16", "60000")
|
||||
os.Setenv("TEST_FLOAT64", piAsString)
|
||||
os.Setenv("TEST_BOOL", "true")
|
||||
os.Setenv("TEST_DURATION", "10m")
|
||||
os.Setenv("TEST_URL", "https://example.com")
|
||||
os.Setenv("TEST_INVALID_INT64", "asdf")
|
||||
os.Setenv("TEST_STRING_SLICE", "foo;bar")
|
||||
os.Setenv("TEST_INT64_SLICE", int64AsString+";"+int64AsString)
|
||||
os.Setenv("TEST_UINT16_SLICE", "60000;50000")
|
||||
os.Setenv("TEST_FLOAT64_SLICE", piAsString+";"+piAsString)
|
||||
os.Setenv("TEST_BOOL_SLICE", "true; false; true")
|
||||
os.Setenv("TEST_DURATION_SLICE", "10m; 20m")
|
||||
os.Setenv("TEST_URL_SLICE", "https://example.com")
|
||||
os.Setenv("TEST_DECODER_STRUCT", "{\"string\":\"foo\"}")
|
||||
os.Setenv("TEST_DECODER_STRUCT_PTR", "{\"string\":\"foo\"}")
|
||||
os.Setenv("TEST_DECODER_STRING", "oof")
|
||||
os.Setenv("TEST_UNMARSHALER_NUMBER", "07")
|
||||
|
||||
var tc testConfig
|
||||
tc.NestedPtr = &nested{}
|
||||
tc.DecoderStructPtr = &decoderStruct{}
|
||||
|
||||
err := Decode(&tc)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if tc.String != "foo" {
|
||||
t.Fatalf(`Expected "foo", got "%s"`, tc.String)
|
||||
}
|
||||
|
||||
if tc.Int64 != -(1 << 50) {
|
||||
t.Fatalf("Expected %d, got %d", -(1 << 50), tc.Int64)
|
||||
}
|
||||
|
||||
if tc.Uint16 != 60000 {
|
||||
t.Fatalf("Expected 60000, got %d", tc.Uint16)
|
||||
}
|
||||
|
||||
if tc.Float64 != math.Pi {
|
||||
t.Fatalf("Expected %.48f, got %.48f", math.Pi, tc.Float64)
|
||||
}
|
||||
|
||||
if !tc.Bool {
|
||||
t.Fatal("Expected true, got false")
|
||||
}
|
||||
|
||||
duration, _ := time.ParseDuration("10m")
|
||||
if tc.Duration != duration {
|
||||
t.Fatalf("Expected %d, got %d", duration, tc.Duration)
|
||||
}
|
||||
|
||||
if tc.URL == nil {
|
||||
t.Fatalf("Expected https://example.com, got nil")
|
||||
} else if tc.URL.String() != "https://example.com" {
|
||||
t.Fatalf("Expected https://example.com, got %s", tc.URL.String())
|
||||
}
|
||||
|
||||
expectedStringSlice := []string{"foo", "bar"}
|
||||
if !reflect.DeepEqual(tc.StringSlice, expectedStringSlice) {
|
||||
t.Fatalf("Expected %s, got %s", expectedStringSlice, tc.StringSlice)
|
||||
}
|
||||
|
||||
expectedInt64Slice := []int64{int64Val, int64Val}
|
||||
if !reflect.DeepEqual(tc.Int64Slice, expectedInt64Slice) {
|
||||
t.Fatalf("Expected %#v, got %#v", expectedInt64Slice, tc.Int64Slice)
|
||||
}
|
||||
|
||||
expectedUint16Slice := []uint16{60000, 50000}
|
||||
if !reflect.DeepEqual(tc.Uint16Slice, expectedUint16Slice) {
|
||||
t.Fatalf("Expected %#v, got %#v", expectedUint16Slice, tc.Uint16Slice)
|
||||
}
|
||||
|
||||
expectedFloat64Slice := []float64{math.Pi, math.Pi}
|
||||
if !reflect.DeepEqual(tc.Float64Slice, expectedFloat64Slice) {
|
||||
t.Fatalf("Expected %#v, got %#v", expectedFloat64Slice, tc.Float64Slice)
|
||||
}
|
||||
|
||||
expectedBoolSlice := []bool{true, false, true}
|
||||
if !reflect.DeepEqual(tc.BoolSlice, expectedBoolSlice) {
|
||||
t.Fatalf("Expected %#v, got %#v", expectedBoolSlice, tc.BoolSlice)
|
||||
}
|
||||
|
||||
duration2, _ := time.ParseDuration("20m")
|
||||
expectedDurationSlice := []time.Duration{duration, duration2}
|
||||
if !reflect.DeepEqual(tc.DurationSlice, expectedDurationSlice) {
|
||||
t.Fatalf("Expected %s, got %s", expectedDurationSlice, tc.DurationSlice)
|
||||
}
|
||||
|
||||
urlVal, _ := url.Parse("https://example.com")
|
||||
expectedUrlSlice := []*url.URL{urlVal}
|
||||
if !reflect.DeepEqual(tc.URLSlice, expectedUrlSlice) {
|
||||
t.Fatalf("Expected %s, got %s", expectedUrlSlice, tc.URLSlice)
|
||||
}
|
||||
|
||||
if tc.UnsetString != "" {
|
||||
t.Fatal("Got non-empty string unexpectedly")
|
||||
}
|
||||
|
||||
if tc.UnsetInt64 != 0 {
|
||||
t.Fatal("Got non-zero int unexpectedly")
|
||||
}
|
||||
|
||||
if tc.UnsetDuration != time.Duration(0) {
|
||||
t.Fatal("Got non-zero time.Duration unexpectedly")
|
||||
}
|
||||
|
||||
if tc.UnsetURL != nil {
|
||||
t.Fatal("Got non-zero *url.URL unexpectedly")
|
||||
}
|
||||
|
||||
if len(tc.UnsetSlice) > 0 {
|
||||
t.Fatal("Got not-empty string slice unexpectedly")
|
||||
}
|
||||
|
||||
if tc.InvalidInt64 != 0 {
|
||||
t.Fatal("Got non-zero int unexpectedly")
|
||||
}
|
||||
|
||||
if tc.UnusedField != "" {
|
||||
t.Fatal("Expected empty field")
|
||||
}
|
||||
|
||||
if tc.unexportedField != "" {
|
||||
t.Fatal("Expected empty field")
|
||||
}
|
||||
|
||||
if tc.IgnoredPtr != nil {
|
||||
t.Fatal("Expected nil pointer")
|
||||
}
|
||||
|
||||
if tc.Nested.String != "foo" {
|
||||
t.Fatalf(`Expected "foo", got "%s"`, tc.Nested.String)
|
||||
}
|
||||
|
||||
if tc.NestedPtr.String != "foo" {
|
||||
t.Fatalf(`Expected "foo", got "%s"`, tc.NestedPtr.String)
|
||||
}
|
||||
|
||||
if tc.DefaultInt != 1234 {
|
||||
t.Fatalf("Expected 1234, got %d", tc.DefaultInt)
|
||||
}
|
||||
|
||||
expectedDefaultSlice := []int{1, 2, 3}
|
||||
if !reflect.DeepEqual(tc.DefaultSliceInt, expectedDefaultSlice) {
|
||||
t.Fatalf("Expected %d, got %d", expectedDefaultSlice, tc.DefaultSliceInt)
|
||||
}
|
||||
|
||||
defaultDuration, _ := time.ParseDuration("24h")
|
||||
if tc.DefaultDuration != defaultDuration {
|
||||
t.Fatalf("Expected %d, got %d", defaultDuration, tc.DefaultInt)
|
||||
}
|
||||
|
||||
if tc.DefaultURL.String() != "http://example.com" {
|
||||
t.Fatalf("Expected http://example.com, got %s", tc.DefaultURL.String())
|
||||
}
|
||||
|
||||
if tc.DecoderStruct.String != "foo" {
|
||||
t.Fatalf("Expected foo, got %s", tc.DecoderStruct.String)
|
||||
}
|
||||
|
||||
if tc.DecoderStructPtr.String != "foo" {
|
||||
t.Fatalf("Expected foo, got %s", tc.DecoderStructPtr.String)
|
||||
}
|
||||
|
||||
if tc.DecoderString != "foo" {
|
||||
t.Fatalf("Expected foo, got %s", tc.DecoderString)
|
||||
}
|
||||
|
||||
if tc.UnmarshalerNumber != 07 {
|
||||
t.Fatalf("Expected 07, got %04o", tc.UnmarshalerNumber)
|
||||
}
|
||||
|
||||
os.Setenv("TEST_REQUIRED", "required")
|
||||
var tcr testConfigRequired
|
||||
|
||||
err = Decode(&tcr)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if tcr.Required != "required" {
|
||||
t.Fatalf("Expected \"required\", got %s", tcr.Required)
|
||||
}
|
||||
|
||||
_, err = Export(&tcr)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var tco testConfigOverride
|
||||
err = Decode(&tco)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if tco.OverrideString != "override_default" {
|
||||
t.Fatalf(`Expected "override_default" but got %s`, tco.OverrideString)
|
||||
}
|
||||
|
||||
os.Setenv("TEST_OVERRIDE_A", "override_a")
|
||||
|
||||
tco = testConfigOverride{}
|
||||
err = Decode(&tco)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if tco.OverrideString != "override_a" {
|
||||
t.Fatalf(`Expected "override_a" but got %s`, tco.OverrideString)
|
||||
}
|
||||
|
||||
os.Setenv("TEST_OVERRIDE_B", "override_b")
|
||||
|
||||
tco = testConfigOverride{}
|
||||
err = Decode(&tco)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if tco.OverrideString != "override_b" {
|
||||
t.Fatalf(`Expected "override_b" but got %s`, tco.OverrideString)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeErrors(t *testing.T) {
|
||||
var b bool
|
||||
err := Decode(&b)
|
||||
if err != ErrInvalidTarget {
|
||||
t.Fatal("Should have gotten an error decoding into a bool")
|
||||
}
|
||||
|
||||
var tc testConfig
|
||||
err = Decode(tc)
|
||||
if err != ErrInvalidTarget {
|
||||
t.Fatal("Should have gotten an error decoding into a non-pointer")
|
||||
}
|
||||
|
||||
var tcp *testConfig
|
||||
err = Decode(tcp)
|
||||
if err != ErrInvalidTarget {
|
||||
t.Fatal("Should have gotten an error decoding to a nil pointer")
|
||||
}
|
||||
|
||||
var tnt testNoTags
|
||||
err = Decode(&tnt)
|
||||
if err != ErrNoTargetFieldsAreSet {
|
||||
t.Fatal("Should have gotten an error decoding a struct with no tags")
|
||||
}
|
||||
|
||||
var tcni testNoExportedFields
|
||||
err = Decode(&tcni)
|
||||
if err != ErrNoTargetFieldsAreSet {
|
||||
t.Fatal("Should have gotten an error decoding a struct with no unexported fields")
|
||||
}
|
||||
|
||||
var tcr testConfigRequired
|
||||
os.Clearenv()
|
||||
err = Decode(&tcr)
|
||||
if err == nil {
|
||||
t.Fatal("An error was expected but recieved:", err)
|
||||
}
|
||||
|
||||
var tcns testConfigNoSet
|
||||
err = Decode(&tcns)
|
||||
if err != ErrNoTargetFieldsAreSet {
|
||||
t.Fatal("Should have gotten an error decoding when no env variables are set")
|
||||
}
|
||||
|
||||
missing := false
|
||||
FailureFunc = func(err error) {
|
||||
missing = true
|
||||
}
|
||||
MustDecode(&tcr)
|
||||
if !missing {
|
||||
t.Fatal("The FailureFunc should have been called but it was not")
|
||||
}
|
||||
|
||||
var tcrd testConfigRequiredDefault
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
}
|
||||
}()
|
||||
err = Decode(&tcrd)
|
||||
t.Fatal("This should not have been reached. A panic should have occured.")
|
||||
}
|
||||
|
||||
func TestOnlyNested(t *testing.T) {
|
||||
os.Setenv("TEST_STRING", "foo")
|
||||
|
||||
// No env vars in the outer level are ok, as long as they're
|
||||
// in the inner struct.
|
||||
var o struct {
|
||||
Inner nested
|
||||
}
|
||||
if err := Decode(&o); err != nil {
|
||||
t.Fatalf("Expected no error, got %s", err)
|
||||
}
|
||||
|
||||
// No env vars in the inner levels are ok, as long as they're
|
||||
// in the outer struct.
|
||||
var o2 struct {
|
||||
Inner noConfig
|
||||
X string `env:"TEST_STRING"`
|
||||
}
|
||||
if err := Decode(&o2); err != nil {
|
||||
t.Fatalf("Expected no error, got %s", err)
|
||||
}
|
||||
|
||||
// No env vars in either outer or inner levels should result
|
||||
// in error
|
||||
var o3 struct {
|
||||
Inner noConfig
|
||||
}
|
||||
if err := Decode(&o3); err != ErrNoTargetFieldsAreSet {
|
||||
t.Fatalf("Expected ErrInvalidTarget, got %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func ExampleDecode() {
|
||||
type Example struct {
|
||||
// A string field, without any default
|
||||
String string `env:"EXAMPLE_STRING"`
|
||||
|
||||
// A uint16 field, with a default value of 100
|
||||
Uint16 uint16 `env:"EXAMPLE_UINT16,default=100"`
|
||||
}
|
||||
|
||||
os.Setenv("EXAMPLE_STRING", "an example!")
|
||||
|
||||
var e Example
|
||||
err := Decode(&e)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// If TEST_STRING is set, e.String will contain its value
|
||||
fmt.Println(e.String)
|
||||
|
||||
// If TEST_UINT16 is set, e.Uint16 will contain its value.
|
||||
// Otherwise, it will contain the default value, 100.
|
||||
fmt.Println(e.Uint16)
|
||||
|
||||
// Output:
|
||||
// an example!
|
||||
// 100
|
||||
}
|
||||
|
||||
//// Export tests
|
||||
|
||||
type testConfigExport struct {
|
||||
String string `env:"TEST_STRING"`
|
||||
Int64 int64 `env:"TEST_INT64"`
|
||||
Uint16 uint16 `env:"TEST_UINT16"`
|
||||
Float64 float64 `env:"TEST_FLOAT64"`
|
||||
Bool bool `env:"TEST_BOOL"`
|
||||
Duration time.Duration `env:"TEST_DURATION"`
|
||||
URL *url.URL `env:"TEST_URL"`
|
||||
|
||||
StringSlice []string `env:"TEST_STRING_SLICE"`
|
||||
|
||||
UnsetString string `env:"TEST_UNSET_STRING"`
|
||||
UnsetInt64 int64 `env:"TEST_UNSET_INT64"`
|
||||
UnsetDuration time.Duration `env:"TEST_UNSET_DURATION"`
|
||||
UnsetURL *url.URL `env:"TEST_UNSET_URL"`
|
||||
|
||||
UnusedField string
|
||||
unexportedField string
|
||||
|
||||
IgnoredPtr *bool `env:"TEST_IGNORED_POINTER"`
|
||||
|
||||
Nested nestedConfigExport
|
||||
NestedPtr *nestedConfigExportPointer
|
||||
NestedPtrUnset *nestedConfigExportPointer
|
||||
|
||||
NestedTwice nestedTwiceConfig
|
||||
|
||||
NoConfig noConfig
|
||||
NoConfigPtr *noConfig
|
||||
NoConfigPtrSet *noConfig
|
||||
|
||||
RequiredInt int `env:"TEST_REQUIRED_INT,required"`
|
||||
|
||||
DefaultBool bool `env:"TEST_DEFAULT_BOOL,default=true"`
|
||||
DefaultInt int `env:"TEST_DEFAULT_INT,default=1234"`
|
||||
DefaultDuration time.Duration `env:"TEST_DEFAULT_DURATION,default=24h"`
|
||||
DefaultURL *url.URL `env:"TEST_DEFAULT_URL,default=http://example.com"`
|
||||
DefaultIntSet int `env:"TEST_DEFAULT_INT_SET,default=99"`
|
||||
DefaultIntSlice []int `env:"TEST_DEFAULT_INT_SLICE,default=99;33"`
|
||||
}
|
||||
|
||||
type nestedConfigExport struct {
|
||||
String string `env:"TEST_NESTED_STRING"`
|
||||
}
|
||||
|
||||
type nestedConfigExportPointer struct {
|
||||
String string `env:"TEST_NESTED_STRING_POINTER"`
|
||||
}
|
||||
|
||||
type noConfig struct {
|
||||
Int int
|
||||
}
|
||||
|
||||
type nestedTwiceConfig struct {
|
||||
Nested nestedConfigInner
|
||||
}
|
||||
|
||||
type nestedConfigInner struct {
|
||||
String string `env:"TEST_NESTED_TWICE_STRING"`
|
||||
}
|
||||
|
||||
type testConfigStrict struct {
|
||||
InvalidInt64Strict int64 `env:"TEST_INVALID_INT64,strict,default=1"`
|
||||
InvalidInt64Implicit int64 `env:"TEST_INVALID_INT64_IMPLICIT,default=1"`
|
||||
|
||||
Nested struct {
|
||||
InvalidInt64Strict int64 `env:"TEST_INVALID_INT64_NESTED,strict,required"`
|
||||
InvalidInt64Implicit int64 `env:"TEST_INVALID_INT64_NESTED_IMPLICIT,required"`
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvalidStrict(t *testing.T) {
|
||||
cases := []struct {
|
||||
decoder func(interface{}) error
|
||||
rootValue string
|
||||
nestedValue string
|
||||
rootValueImplicit string
|
||||
nestedValueImplicit string
|
||||
pass bool
|
||||
}{
|
||||
{Decode, "1", "1", "1", "1", true},
|
||||
{Decode, "1", "1", "1", "asdf", true},
|
||||
{Decode, "1", "1", "asdf", "1", true},
|
||||
{Decode, "1", "1", "asdf", "asdf", true},
|
||||
{Decode, "1", "asdf", "1", "1", false},
|
||||
{Decode, "asdf", "1", "1", "1", false},
|
||||
{Decode, "asdf", "asdf", "1", "1", false},
|
||||
{StrictDecode, "1", "1", "1", "1", true},
|
||||
{StrictDecode, "asdf", "1", "1", "1", false},
|
||||
{StrictDecode, "1", "asdf", "1", "1", false},
|
||||
{StrictDecode, "1", "1", "asdf", "1", false},
|
||||
{StrictDecode, "1", "1", "1", "asdf", false},
|
||||
{StrictDecode, "asdf", "asdf", "1", "1", false},
|
||||
{StrictDecode, "1", "asdf", "asdf", "1", false},
|
||||
{StrictDecode, "1", "1", "asdf", "asdf", false},
|
||||
{StrictDecode, "1", "asdf", "asdf", "asdf", false},
|
||||
{StrictDecode, "asdf", "asdf", "asdf", "asdf", false},
|
||||
}
|
||||
|
||||
for _, test := range cases {
|
||||
os.Setenv("TEST_INVALID_INT64", test.rootValue)
|
||||
os.Setenv("TEST_INVALID_INT64_NESTED", test.nestedValue)
|
||||
os.Setenv("TEST_INVALID_INT64_IMPLICIT", test.rootValueImplicit)
|
||||
os.Setenv("TEST_INVALID_INT64_NESTED_IMPLICIT", test.nestedValueImplicit)
|
||||
|
||||
var tc testConfigStrict
|
||||
if err := test.decoder(&tc); test.pass != (err == nil) {
|
||||
t.Fatalf("Have err=%s wanted pass=%v", err, test.pass)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestExport(t *testing.T) {
|
||||
testFloat64 := fmt.Sprintf("%.48f", math.Pi)
|
||||
testFloat64Output := strconv.FormatFloat(math.Pi, 'f', -1, 64)
|
||||
testInt64 := fmt.Sprintf("%d", -(1 << 50))
|
||||
|
||||
os.Setenv("TEST_STRING", "foo")
|
||||
os.Setenv("TEST_INT64", testInt64)
|
||||
os.Setenv("TEST_UINT16", "60000")
|
||||
os.Setenv("TEST_FLOAT64", testFloat64)
|
||||
os.Setenv("TEST_BOOL", "true")
|
||||
os.Setenv("TEST_DURATION", "10m")
|
||||
os.Setenv("TEST_URL", "https://example.com")
|
||||
os.Setenv("TEST_STRING_SLICE", "foo;bar")
|
||||
os.Setenv("TEST_NESTED_STRING", "nest_foo")
|
||||
os.Setenv("TEST_NESTED_STRING_POINTER", "nest_foo_ptr")
|
||||
os.Setenv("TEST_NESTED_TWICE_STRING", "nest_twice_foo")
|
||||
os.Setenv("TEST_REQUIRED_INT", "101")
|
||||
os.Setenv("TEST_DEFAULT_INT_SET", "102")
|
||||
os.Setenv("TEST_DEFAULT_INT_SLICE", "1;2;3")
|
||||
|
||||
var tc testConfigExport
|
||||
tc.NestedPtr = &nestedConfigExportPointer{}
|
||||
tc.NoConfigPtrSet = &noConfig{}
|
||||
|
||||
err := Decode(&tc)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
rc, err := Export(&tc)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
expected := []*ConfigInfo{
|
||||
&ConfigInfo{
|
||||
Field: "String",
|
||||
EnvVar: "TEST_STRING",
|
||||
Value: "foo",
|
||||
UsesEnv: true,
|
||||
},
|
||||
&ConfigInfo{
|
||||
Field: "Int64",
|
||||
EnvVar: "TEST_INT64",
|
||||
Value: testInt64,
|
||||
UsesEnv: true,
|
||||
},
|
||||
&ConfigInfo{
|
||||
Field: "Uint16",
|
||||
EnvVar: "TEST_UINT16",
|
||||
Value: "60000",
|
||||
UsesEnv: true,
|
||||
},
|
||||
&ConfigInfo{
|
||||
Field: "Float64",
|
||||
EnvVar: "TEST_FLOAT64",
|
||||
Value: testFloat64Output,
|
||||
UsesEnv: true,
|
||||
},
|
||||
&ConfigInfo{
|
||||
Field: "Bool",
|
||||
EnvVar: "TEST_BOOL",
|
||||
Value: "true",
|
||||
UsesEnv: true,
|
||||
},
|
||||
&ConfigInfo{
|
||||
Field: "Duration",
|
||||
EnvVar: "TEST_DURATION",
|
||||
Value: "10m0s",
|
||||
UsesEnv: true,
|
||||
},
|
||||
&ConfigInfo{
|
||||
Field: "URL",
|
||||
EnvVar: "TEST_URL",
|
||||
Value: "https://example.com",
|
||||
UsesEnv: true,
|
||||
},
|
||||
&ConfigInfo{
|
||||
Field: "StringSlice",
|
||||
EnvVar: "TEST_STRING_SLICE",
|
||||
Value: "[foo bar]",
|
||||
UsesEnv: true,
|
||||
},
|
||||
|
||||
&ConfigInfo{
|
||||
Field: "UnsetString",
|
||||
EnvVar: "TEST_UNSET_STRING",
|
||||
Value: "",
|
||||
},
|
||||
&ConfigInfo{
|
||||
Field: "UnsetInt64",
|
||||
EnvVar: "TEST_UNSET_INT64",
|
||||
Value: "0",
|
||||
},
|
||||
&ConfigInfo{
|
||||
Field: "UnsetDuration",
|
||||
EnvVar: "TEST_UNSET_DURATION",
|
||||
Value: "0s",
|
||||
},
|
||||
&ConfigInfo{
|
||||
Field: "UnsetURL",
|
||||
EnvVar: "TEST_UNSET_URL",
|
||||
Value: "",
|
||||
},
|
||||
|
||||
&ConfigInfo{
|
||||
Field: "IgnoredPtr",
|
||||
EnvVar: "TEST_IGNORED_POINTER",
|
||||
Value: "",
|
||||
},
|
||||
|
||||
&ConfigInfo{
|
||||
Field: "Nested.String",
|
||||
EnvVar: "TEST_NESTED_STRING",
|
||||
Value: "nest_foo",
|
||||
UsesEnv: true,
|
||||
},
|
||||
&ConfigInfo{
|
||||
Field: "NestedPtr.String",
|
||||
EnvVar: "TEST_NESTED_STRING_POINTER",
|
||||
Value: "nest_foo_ptr",
|
||||
UsesEnv: true,
|
||||
},
|
||||
|
||||
&ConfigInfo{
|
||||
Field: "NestedTwice.Nested.String",
|
||||
EnvVar: "TEST_NESTED_TWICE_STRING",
|
||||
Value: "nest_twice_foo",
|
||||
UsesEnv: true,
|
||||
},
|
||||
|
||||
&ConfigInfo{
|
||||
Field: "RequiredInt",
|
||||
EnvVar: "TEST_REQUIRED_INT",
|
||||
Value: "101",
|
||||
UsesEnv: true,
|
||||
Required: true,
|
||||
},
|
||||
|
||||
&ConfigInfo{
|
||||
Field: "DefaultBool",
|
||||
EnvVar: "TEST_DEFAULT_BOOL",
|
||||
Value: "true",
|
||||
DefaultValue: "true",
|
||||
HasDefault: true,
|
||||
},
|
||||
&ConfigInfo{
|
||||
Field: "DefaultInt",
|
||||
EnvVar: "TEST_DEFAULT_INT",
|
||||
Value: "1234",
|
||||
DefaultValue: "1234",
|
||||
HasDefault: true,
|
||||
},
|
||||
&ConfigInfo{
|
||||
Field: "DefaultDuration",
|
||||
EnvVar: "TEST_DEFAULT_DURATION",
|
||||
Value: "24h0m0s",
|
||||
DefaultValue: "24h",
|
||||
HasDefault: true,
|
||||
},
|
||||
&ConfigInfo{
|
||||
Field: "DefaultURL",
|
||||
EnvVar: "TEST_DEFAULT_URL",
|
||||
Value: "http://example.com",
|
||||
DefaultValue: "http://example.com",
|
||||
HasDefault: true,
|
||||
},
|
||||
&ConfigInfo{
|
||||
Field: "DefaultIntSet",
|
||||
EnvVar: "TEST_DEFAULT_INT_SET",
|
||||
Value: "102",
|
||||
DefaultValue: "99",
|
||||
HasDefault: true,
|
||||
UsesEnv: true,
|
||||
},
|
||||
&ConfigInfo{
|
||||
Field: "DefaultIntSlice",
|
||||
EnvVar: "TEST_DEFAULT_INT_SLICE",
|
||||
Value: "[1 2 3]",
|
||||
DefaultValue: "99;33",
|
||||
HasDefault: true,
|
||||
UsesEnv: true,
|
||||
},
|
||||
}
|
||||
|
||||
sort.Sort(ConfigInfoSlice(expected))
|
||||
|
||||
if len(rc) != len(expected) {
|
||||
t.Fatalf("Have %d results, expected %d", len(rc), len(expected))
|
||||
}
|
||||
|
||||
for n, v := range rc {
|
||||
ci := expected[n]
|
||||
if *ci != *v {
|
||||
t.Fatalf("have %+v, expected %+v", v, ci)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,10 +5,10 @@ import (
|
||||
|
||||
"github.com/owncloud/ocis/ocis-pkg/config"
|
||||
ociscfg "github.com/owncloud/ocis/ocis-pkg/config"
|
||||
"github.com/owncloud/ocis/ocis-pkg/config/envdecode"
|
||||
"github.com/owncloud/ocis/ocis-pkg/version"
|
||||
"github.com/owncloud/ocis/ocis/pkg/register"
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/wkloucek/envdecode"
|
||||
)
|
||||
|
||||
// Execute is the entry point for the ocis command.
|
||||
|
||||
@@ -5,11 +5,11 @@ import (
|
||||
"os"
|
||||
|
||||
ociscfg "github.com/owncloud/ocis/ocis-pkg/config"
|
||||
"github.com/owncloud/ocis/ocis-pkg/config/envdecode"
|
||||
"github.com/owncloud/ocis/ocis-pkg/version"
|
||||
"github.com/owncloud/ocis/ocs/pkg/config"
|
||||
"github.com/thejerf/suture/v4"
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/wkloucek/envdecode"
|
||||
)
|
||||
|
||||
// Execute is the entry point for the ocis-ocs command.
|
||||
|
||||
@@ -5,11 +5,11 @@ import (
|
||||
"os"
|
||||
|
||||
ociscfg "github.com/owncloud/ocis/ocis-pkg/config"
|
||||
"github.com/owncloud/ocis/ocis-pkg/config/envdecode"
|
||||
"github.com/owncloud/ocis/ocis-pkg/version"
|
||||
"github.com/owncloud/ocis/proxy/pkg/config"
|
||||
"github.com/thejerf/suture/v4"
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/wkloucek/envdecode"
|
||||
)
|
||||
|
||||
// Execute is the entry point for the ocis-proxy command.
|
||||
|
||||
@@ -5,11 +5,11 @@ import (
|
||||
"os"
|
||||
|
||||
ociscfg "github.com/owncloud/ocis/ocis-pkg/config"
|
||||
"github.com/owncloud/ocis/ocis-pkg/config/envdecode"
|
||||
"github.com/owncloud/ocis/ocis-pkg/version"
|
||||
"github.com/owncloud/ocis/settings/pkg/config"
|
||||
"github.com/thejerf/suture/v4"
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/wkloucek/envdecode"
|
||||
)
|
||||
|
||||
// Execute is the entry point for the ocis-settings command.
|
||||
|
||||
@@ -5,11 +5,11 @@ import (
|
||||
"os"
|
||||
|
||||
ociscfg "github.com/owncloud/ocis/ocis-pkg/config"
|
||||
"github.com/owncloud/ocis/ocis-pkg/config/envdecode"
|
||||
"github.com/owncloud/ocis/ocis-pkg/version"
|
||||
"github.com/owncloud/ocis/store/pkg/config"
|
||||
"github.com/thejerf/suture/v4"
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/wkloucek/envdecode"
|
||||
)
|
||||
|
||||
// Execute is the entry point for the ocis-store command.
|
||||
|
||||
@@ -5,11 +5,11 @@ import (
|
||||
"os"
|
||||
|
||||
ociscfg "github.com/owncloud/ocis/ocis-pkg/config"
|
||||
"github.com/owncloud/ocis/ocis-pkg/config/envdecode"
|
||||
"github.com/owncloud/ocis/ocis-pkg/version"
|
||||
"github.com/owncloud/ocis/thumbnails/pkg/config"
|
||||
"github.com/thejerf/suture/v4"
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/wkloucek/envdecode"
|
||||
)
|
||||
|
||||
// Execute is the entry point for the ocis-thumbnails command.
|
||||
|
||||
@@ -5,11 +5,11 @@ import (
|
||||
"os"
|
||||
|
||||
ociscfg "github.com/owncloud/ocis/ocis-pkg/config"
|
||||
"github.com/owncloud/ocis/ocis-pkg/config/envdecode"
|
||||
"github.com/owncloud/ocis/ocis-pkg/version"
|
||||
"github.com/owncloud/ocis/web/pkg/config"
|
||||
"github.com/thejerf/suture/v4"
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/wkloucek/envdecode"
|
||||
)
|
||||
|
||||
// Execute is the entry point for the web command.
|
||||
|
||||
@@ -5,11 +5,11 @@ import (
|
||||
"os"
|
||||
|
||||
ociscfg "github.com/owncloud/ocis/ocis-pkg/config"
|
||||
"github.com/owncloud/ocis/ocis-pkg/config/envdecode"
|
||||
"github.com/owncloud/ocis/ocis-pkg/version"
|
||||
"github.com/owncloud/ocis/webdav/pkg/config"
|
||||
"github.com/thejerf/suture/v4"
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/wkloucek/envdecode"
|
||||
)
|
||||
|
||||
// Execute is the entry point for the ocis-webdav command.
|
||||
|
||||
Reference in New Issue
Block a user