maintain envdecode inside ocis-pkg

This commit is contained in:
Willy Kloucek
2021-12-21 10:25:09 +01:00
parent fe1672a000
commit e3bfb66df1
19 changed files with 1348 additions and 16 deletions

View File

@@ -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.

View File

@@ -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
View File

@@ -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
View File

@@ -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=

View File

@@ -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.

View File

@@ -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"

View File

@@ -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.

View 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.

View 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.

View 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
}

View 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)
}
}
}

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.