Update to the latest dependency versions and use distroless/static:nonroot image
This commit is contained in:
+4
@@ -0,0 +1,4 @@
|
||||
coverage.txt
|
||||
bin
|
||||
card.png
|
||||
dist
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
linters:
|
||||
enable:
|
||||
- thelper
|
||||
- gofumpt
|
||||
- tparallel
|
||||
- unconvert
|
||||
- unparam
|
||||
- wastedassign
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
includes:
|
||||
- from_url:
|
||||
url: https://raw.githubusercontent.com/caarlos0/.goreleaserfiles/main/lib.yml
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2022 Carlos Alexandro Becker
|
||||
|
||||
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.
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
SOURCE_FILES?=./...
|
||||
TEST_PATTERN?=.
|
||||
|
||||
export GO111MODULE := on
|
||||
|
||||
setup:
|
||||
go mod tidy
|
||||
.PHONY: setup
|
||||
|
||||
build:
|
||||
go build
|
||||
.PHONY: build
|
||||
|
||||
test:
|
||||
go test -v -failfast -race -coverpkg=./... -covermode=atomic -coverprofile=coverage.txt $(SOURCE_FILES) -run $(TEST_PATTERN) -timeout=2m
|
||||
.PHONY: test
|
||||
|
||||
cover: test
|
||||
go tool cover -html=coverage.txt
|
||||
.PHONY: cover
|
||||
|
||||
fmt:
|
||||
gofumpt -w -l .
|
||||
.PHONY: fmt
|
||||
|
||||
lint:
|
||||
golangci-lint run ./...
|
||||
.PHONY: lint
|
||||
|
||||
ci: build test
|
||||
.PHONY: ci
|
||||
|
||||
card:
|
||||
wget -O card.png -c "https://og.caarlos0.dev/**env**: parse envs to structs.png?theme=light&md=1&fontSize=100px&images=https://github.com/caarlos0.png"
|
||||
.PHONY: card
|
||||
|
||||
.DEFAULT_GOAL := ci
|
||||
+452
@@ -0,0 +1,452 @@
|
||||
# env
|
||||
|
||||
[](https://github.com/caarlos0/env/actions?workflow=build)
|
||||
[](https://codecov.io/gh/caarlos0/env)
|
||||
[](https://pkg.go.dev/github.com/caarlos0/env/v6)
|
||||
|
||||
A simple and zero-dependencies library to parse environment variables into structs.
|
||||
|
||||
## Example
|
||||
|
||||
Get the module with:
|
||||
|
||||
```sh
|
||||
go get github.com/caarlos0/env/v6
|
||||
```
|
||||
|
||||
The usage looks like this:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/caarlos0/env/v6"
|
||||
)
|
||||
|
||||
type config struct {
|
||||
Home string `env:"HOME"`
|
||||
Port int `env:"PORT" envDefault:"3000"`
|
||||
Password string `env:"PASSWORD,unset"`
|
||||
IsProduction bool `env:"PRODUCTION"`
|
||||
Hosts []string `env:"HOSTS" envSeparator:":"`
|
||||
Duration time.Duration `env:"DURATION"`
|
||||
TempFolder string `env:"TEMP_FOLDER" envDefault:"${HOME}/tmp" envExpand:"true"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
cfg := config{}
|
||||
if err := env.Parse(&cfg); err != nil {
|
||||
fmt.Printf("%+v\n", err)
|
||||
}
|
||||
|
||||
fmt.Printf("%+v\n", cfg)
|
||||
}
|
||||
```
|
||||
|
||||
You can run it like this:
|
||||
|
||||
```sh
|
||||
$ PRODUCTION=true HOSTS="host1:host2:host3" DURATION=1s go run main.go
|
||||
{Home:/your/home Port:3000 IsProduction:true Hosts:[host1 host2 host3] Duration:1s}
|
||||
```
|
||||
|
||||
⚠️⚠️⚠️ **Attention:** _unexported fields_ will be **ignored**.
|
||||
|
||||
## Supported types and defaults
|
||||
|
||||
Out of the box all built-in types are supported, plus a few others that
|
||||
are commonly used.
|
||||
|
||||
Complete list:
|
||||
|
||||
- `string`
|
||||
- `bool`
|
||||
- `int`
|
||||
- `int8`
|
||||
- `int16`
|
||||
- `int32`
|
||||
- `int64`
|
||||
- `uint`
|
||||
- `uint8`
|
||||
- `uint16`
|
||||
- `uint32`
|
||||
- `uint64`
|
||||
- `float32`
|
||||
- `float64`
|
||||
- `time.Duration`
|
||||
- `encoding.TextUnmarshaler`
|
||||
- `url.URL`
|
||||
|
||||
Pointers, slices and slices of pointers of those types are also supported.
|
||||
|
||||
You can also use/define a [custom parser func](#custom-parser-funcs) for any
|
||||
other type you want.
|
||||
|
||||
If you set the `envDefault` tag for something, this value will be used in the
|
||||
case of absence of it in the environment.
|
||||
|
||||
By default, slice types will split the environment value on `,`; you can change
|
||||
this behavior by setting the `envSeparator` tag.
|
||||
|
||||
If you set the `envExpand` tag, environment variables (either in `${var}` or
|
||||
`$var` format) in the string will be replaced according with the actual value
|
||||
of the variable.
|
||||
|
||||
## Custom Parser Funcs
|
||||
|
||||
If you have a type that is not supported out of the box by the lib, you are able
|
||||
to use (or define) and pass custom parsers (and their associated `reflect.Type`)
|
||||
to the `env.ParseWithFuncs()` function.
|
||||
|
||||
In addition to accepting a struct pointer (same as `Parse()`), this function
|
||||
also accepts a `map[reflect.Type]env.ParserFunc`.
|
||||
|
||||
If you add a custom parser for, say `Foo`, it will also be used to parse
|
||||
`*Foo` and `[]Foo` types.
|
||||
|
||||
Check the examples in the [go doc](http://pkg.go.dev/github.com/caarlos0/env/v6)
|
||||
for more info.
|
||||
|
||||
### A note about `TextUnmarshaler` and `time.Time`
|
||||
|
||||
Env supports by default anything that implements the `TextUnmarshaler` interface.
|
||||
That includes things like `time.Time` for example.
|
||||
The upside is that depending on the format you need, you don't need to change anything.
|
||||
The downside is that if you do need time in another format, you'll need to create your own type.
|
||||
|
||||
Its fairly straightforward:
|
||||
|
||||
```go
|
||||
type MyTime time.Time
|
||||
|
||||
func (t *MyTime) UnmarshalText(text []byte) error {
|
||||
tt, err := time.Parse("2006-01-02", string(text))
|
||||
*t = MyTime(tt)
|
||||
return err
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
SomeTime MyTime `env:"SOME_TIME"`
|
||||
}
|
||||
```
|
||||
|
||||
And then you can parse `Config` with `env.Parse`.
|
||||
|
||||
## Required fields
|
||||
|
||||
The `env` tag option `required` (e.g., `env:"tagKey,required"`) can be added to ensure that some environment variable is set.
|
||||
In the example above, an error is returned if the `config` struct is changed to:
|
||||
|
||||
```go
|
||||
type config struct {
|
||||
SecretKey string `env:"SECRET_KEY,required"`
|
||||
}
|
||||
```
|
||||
|
||||
## Not Empty fields
|
||||
|
||||
While `required` demands the environment variable to be check, it doesn't check its value.
|
||||
If you want to make sure the environment is set and not empty, you need to use the `notEmpty` tag option instead (`env:"SOME_ENV,notEmpty"`).
|
||||
|
||||
Example:
|
||||
|
||||
```go
|
||||
type config struct {
|
||||
SecretKey string `env:"SECRET_KEY,notEmpty"`
|
||||
}
|
||||
```
|
||||
|
||||
## Unset environment variable after reading it
|
||||
|
||||
The `env` tag option `unset` (e.g., `env:"tagKey,unset"`) can be added
|
||||
to ensure that some environment variable is unset after reading it.
|
||||
|
||||
Example:
|
||||
|
||||
```go
|
||||
type config struct {
|
||||
SecretKey string `env:"SECRET_KEY,unset"`
|
||||
}
|
||||
```
|
||||
|
||||
## From file
|
||||
|
||||
The `env` tag option `file` (e.g., `env:"tagKey,file"`) can be added
|
||||
to in order to indicate that the value of the variable shall be loaded from a file. The path of that file is given
|
||||
by the environment variable associated with it
|
||||
Example below
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
"github.com/caarlos0/env/v6"
|
||||
)
|
||||
|
||||
type config struct {
|
||||
Secret string `env:"SECRET,file"`
|
||||
Password string `env:"PASSWORD,file" envDefault:"/tmp/password"`
|
||||
Certificate string `env:"CERTIFICATE,file" envDefault:"${CERTIFICATE_FILE}" envExpand:"true"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
cfg := config{}
|
||||
if err := env.Parse(&cfg); err != nil {
|
||||
fmt.Printf("%+v\n", err)
|
||||
}
|
||||
|
||||
fmt.Printf("%+v\n", cfg)
|
||||
}
|
||||
```
|
||||
|
||||
```sh
|
||||
$ echo qwerty > /tmp/secret
|
||||
$ echo dvorak > /tmp/password
|
||||
$ echo coleman > /tmp/certificate
|
||||
|
||||
$ SECRET=/tmp/secret \
|
||||
CERTIFICATE_FILE=/tmp/certificate \
|
||||
go run main.go
|
||||
{Secret:qwerty Password:dvorak Certificate:coleman}
|
||||
```
|
||||
|
||||
## Options
|
||||
|
||||
### Environment
|
||||
|
||||
By setting the `Options.Environment` map you can tell `Parse` to add those `keys` and `values`
|
||||
as env vars before parsing is done. These envs are stored in the map and never actually set by `os.Setenv`.
|
||||
This option effectively makes `env` ignore the OS environment variables: only the ones provided in the option are used.
|
||||
|
||||
This can make your testing scenarios a bit more clean and easy to handle.
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/caarlos0/env/v6"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Password string `env:"PASSWORD"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
cfg := &Config{}
|
||||
opts := &env.Options{Environment: map[string]string{
|
||||
"PASSWORD": "MY_PASSWORD",
|
||||
}}
|
||||
|
||||
// Load env vars.
|
||||
if err := env.Parse(cfg, opts); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Print the loaded data.
|
||||
fmt.Printf("%+v\n", cfg.envData)
|
||||
}
|
||||
```
|
||||
|
||||
### Changing default tag name
|
||||
|
||||
You can change what tag name to use for setting the env vars by setting the `Options.TagName`
|
||||
variable.
|
||||
|
||||
For example
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/caarlos0/env/v6"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Password string `json:"PASSWORD"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
cfg := &Config{}
|
||||
opts := &env.Options{TagName: "json"}
|
||||
|
||||
// Load env vars.
|
||||
if err := env.Parse(cfg, opts); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Print the loaded data.
|
||||
fmt.Printf("%+v\n", cfg.envData)
|
||||
}
|
||||
```
|
||||
|
||||
### Prefixes
|
||||
|
||||
You can prefix sub-structs env tags, as well as a whole `env.Parse` call.
|
||||
|
||||
Here's an example flexing it a bit:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/caarlos0/env/v6"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Home string `env:"HOME"`
|
||||
}
|
||||
|
||||
type ComplexConfig struct {
|
||||
Foo Config `envPrefix:"FOO_"`
|
||||
Clean Config
|
||||
Bar Config `envPrefix:"BAR_"`
|
||||
Blah string `env:"BLAH"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
cfg := ComplexConfig{}
|
||||
if err := Parse(&cfg, Options{
|
||||
Prefix: "T_",
|
||||
Environment: map[string]string{
|
||||
"T_FOO_HOME": "/foo",
|
||||
"T_BAR_HOME": "/bar",
|
||||
"T_BLAH": "blahhh",
|
||||
"T_HOME": "/clean",
|
||||
},
|
||||
}); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Load env vars.
|
||||
if err := env.Parse(cfg, opts); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Print the loaded data.
|
||||
fmt.Printf("%+v\n", cfg.envData)
|
||||
}
|
||||
```
|
||||
|
||||
### On set hooks
|
||||
|
||||
You might want to listen to value sets and, for example, log something or do some other kind of logic.
|
||||
You can do this by passing a `OnSet` option:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/caarlos0/env/v6"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Username string `env:"USERNAME" envDefault:"admin"`
|
||||
Password string `env:"PASSWORD"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
cfg := &Config{}
|
||||
opts := &env.Options{
|
||||
OnSet: func(tag string, value interface{}, isDefault bool) {
|
||||
fmt.Printf("Set %s to %v (default? %v)\n", tag, value, isDefault)
|
||||
},
|
||||
}
|
||||
|
||||
// Load env vars.
|
||||
if err := env.Parse(cfg, opts); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Print the loaded data.
|
||||
fmt.Printf("%+v\n", cfg.envData)
|
||||
}
|
||||
```
|
||||
|
||||
## Making all fields to required
|
||||
|
||||
You can make all fields that don't have a default value be required by setting the `RequiredIfNoDef: true` in the `Options`.
|
||||
|
||||
For example
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/caarlos0/env/v6"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Username string `env:"USERNAME" envDefault:"admin"`
|
||||
Password string `env:"PASSWORD"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
cfg := &Config{}
|
||||
opts := &env.Options{RequiredIfNoDef: true}
|
||||
|
||||
// Load env vars.
|
||||
if err := env.Parse(cfg, opts); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Print the loaded data.
|
||||
fmt.Printf("%+v\n", cfg.envData)
|
||||
}
|
||||
```
|
||||
|
||||
## Defaults from code
|
||||
|
||||
You may define default value also in code, by initialising the config data before it's filled by `env.Parse`.
|
||||
Default values defined as struct tags will overwrite existing values during Parse.
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/caarlos0/env/v6"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Username string `env:"USERNAME" envDefault:"admin"`
|
||||
Password string `env:"PASSWORD"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
var cfg = Config{
|
||||
Username: "test",
|
||||
Password: "123456",
|
||||
}
|
||||
|
||||
if err := env.Parse(&cfg); err != nil {
|
||||
fmt.Println("failed:", err)
|
||||
}
|
||||
|
||||
fmt.Printf("%+v", cfg) // {Username:admin Password:123456}
|
||||
}
|
||||
```
|
||||
|
||||
## Stargazers over time
|
||||
|
||||
[](https://starchart.cc/caarlos0/env)
|
||||
+504
@@ -0,0 +1,504 @@
|
||||
package env
|
||||
|
||||
import (
|
||||
"encoding"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// nolint: gochecknoglobals
|
||||
var (
|
||||
// ErrNotAStructPtr is returned if you pass something that is not a pointer to a
|
||||
// Struct to Parse.
|
||||
ErrNotAStructPtr = errors.New("env: expected a pointer to a Struct")
|
||||
|
||||
defaultBuiltInParsers = map[reflect.Kind]ParserFunc{
|
||||
reflect.Bool: func(v string) (interface{}, error) {
|
||||
return strconv.ParseBool(v)
|
||||
},
|
||||
reflect.String: func(v string) (interface{}, error) {
|
||||
return v, nil
|
||||
},
|
||||
reflect.Int: func(v string) (interface{}, error) {
|
||||
i, err := strconv.ParseInt(v, 10, 32)
|
||||
return int(i), err
|
||||
},
|
||||
reflect.Int16: func(v string) (interface{}, error) {
|
||||
i, err := strconv.ParseInt(v, 10, 16)
|
||||
return int16(i), err
|
||||
},
|
||||
reflect.Int32: func(v string) (interface{}, error) {
|
||||
i, err := strconv.ParseInt(v, 10, 32)
|
||||
return int32(i), err
|
||||
},
|
||||
reflect.Int64: func(v string) (interface{}, error) {
|
||||
return strconv.ParseInt(v, 10, 64)
|
||||
},
|
||||
reflect.Int8: func(v string) (interface{}, error) {
|
||||
i, err := strconv.ParseInt(v, 10, 8)
|
||||
return int8(i), err
|
||||
},
|
||||
reflect.Uint: func(v string) (interface{}, error) {
|
||||
i, err := strconv.ParseUint(v, 10, 32)
|
||||
return uint(i), err
|
||||
},
|
||||
reflect.Uint16: func(v string) (interface{}, error) {
|
||||
i, err := strconv.ParseUint(v, 10, 16)
|
||||
return uint16(i), err
|
||||
},
|
||||
reflect.Uint32: func(v string) (interface{}, error) {
|
||||
i, err := strconv.ParseUint(v, 10, 32)
|
||||
return uint32(i), err
|
||||
},
|
||||
reflect.Uint64: func(v string) (interface{}, error) {
|
||||
i, err := strconv.ParseUint(v, 10, 64)
|
||||
return i, err
|
||||
},
|
||||
reflect.Uint8: func(v string) (interface{}, error) {
|
||||
i, err := strconv.ParseUint(v, 10, 8)
|
||||
return uint8(i), err
|
||||
},
|
||||
reflect.Float64: func(v string) (interface{}, error) {
|
||||
return strconv.ParseFloat(v, 64)
|
||||
},
|
||||
reflect.Float32: func(v string) (interface{}, error) {
|
||||
f, err := strconv.ParseFloat(v, 32)
|
||||
return float32(f), err
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
func defaultTypeParsers() map[reflect.Type]ParserFunc {
|
||||
return map[reflect.Type]ParserFunc{
|
||||
reflect.TypeOf(url.URL{}): func(v string) (interface{}, error) {
|
||||
u, err := url.Parse(v)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to parse URL: %v", err)
|
||||
}
|
||||
return *u, nil
|
||||
},
|
||||
reflect.TypeOf(time.Nanosecond): func(v string) (interface{}, error) {
|
||||
s, err := time.ParseDuration(v)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to parse duration: %v", err)
|
||||
}
|
||||
return s, err
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// ParserFunc defines the signature of a function that can be used within `CustomParsers`.
|
||||
type ParserFunc func(v string) (interface{}, error)
|
||||
|
||||
// OnSetFn is a hook that can be run when a value is set.
|
||||
type OnSetFn func(tag string, value interface{}, isDefault bool)
|
||||
|
||||
// Options for the parser.
|
||||
type Options struct {
|
||||
// Environment keys and values that will be accessible for the service.
|
||||
Environment map[string]string
|
||||
|
||||
// TagName specifies another tagname to use rather than the default env.
|
||||
TagName string
|
||||
|
||||
// RequiredIfNoDef automatically sets all env as required if they do not declare 'envDefault'
|
||||
RequiredIfNoDef bool
|
||||
|
||||
// OnSet allows to run a function when a value is set
|
||||
OnSet OnSetFn
|
||||
|
||||
// Prefix define a prefix for each key
|
||||
Prefix string
|
||||
|
||||
// Sets to true if we have already configured once.
|
||||
configured bool
|
||||
}
|
||||
|
||||
// configure will do the basic configurations and defaults.
|
||||
func configure(opts []Options) []Options {
|
||||
// If we have already configured the first item
|
||||
// of options will have been configured set to true.
|
||||
if len(opts) > 0 && opts[0].configured {
|
||||
return opts
|
||||
}
|
||||
|
||||
// Created options with defaults.
|
||||
opt := Options{
|
||||
TagName: "env",
|
||||
Environment: toMap(os.Environ()),
|
||||
configured: true,
|
||||
}
|
||||
|
||||
// Loop over all opts structs and set
|
||||
// to opt if value is not default/empty.
|
||||
for _, item := range opts {
|
||||
if item.Environment != nil {
|
||||
opt.Environment = item.Environment
|
||||
}
|
||||
if item.TagName != "" {
|
||||
opt.TagName = item.TagName
|
||||
}
|
||||
if item.OnSet != nil {
|
||||
opt.OnSet = item.OnSet
|
||||
}
|
||||
if item.Prefix != "" {
|
||||
opt.Prefix = item.Prefix
|
||||
}
|
||||
opt.RequiredIfNoDef = item.RequiredIfNoDef
|
||||
}
|
||||
|
||||
return []Options{opt}
|
||||
}
|
||||
|
||||
func getOnSetFn(opts []Options) OnSetFn {
|
||||
return opts[0].OnSet
|
||||
}
|
||||
|
||||
// getTagName returns the tag name.
|
||||
func getTagName(opts []Options) string {
|
||||
return opts[0].TagName
|
||||
}
|
||||
|
||||
// getEnvironment returns the environment map.
|
||||
func getEnvironment(opts []Options) map[string]string {
|
||||
return opts[0].Environment
|
||||
}
|
||||
|
||||
// Parse parses a struct containing `env` tags and loads its values from
|
||||
// environment variables.
|
||||
func Parse(v interface{}, opts ...Options) error {
|
||||
return ParseWithFuncs(v, map[reflect.Type]ParserFunc{}, opts...)
|
||||
}
|
||||
|
||||
// ParseWithFuncs is the same as `Parse` except it also allows the user to pass
|
||||
// in custom parsers.
|
||||
func ParseWithFuncs(v interface{}, funcMap map[reflect.Type]ParserFunc, opts ...Options) error {
|
||||
opts = configure(opts)
|
||||
|
||||
ptrRef := reflect.ValueOf(v)
|
||||
if ptrRef.Kind() != reflect.Ptr {
|
||||
return ErrNotAStructPtr
|
||||
}
|
||||
ref := ptrRef.Elem()
|
||||
if ref.Kind() != reflect.Struct {
|
||||
return ErrNotAStructPtr
|
||||
}
|
||||
parsers := defaultTypeParsers()
|
||||
for k, v := range funcMap {
|
||||
parsers[k] = v
|
||||
}
|
||||
|
||||
return doParse(ref, parsers, opts)
|
||||
}
|
||||
|
||||
func doParse(ref reflect.Value, funcMap map[reflect.Type]ParserFunc, opts []Options) error {
|
||||
refType := ref.Type()
|
||||
|
||||
var agrErr aggregateError
|
||||
|
||||
for i := 0; i < refType.NumField(); i++ {
|
||||
refField := ref.Field(i)
|
||||
refTypeField := refType.Field(i)
|
||||
|
||||
if err := doParseField(refField, refTypeField, funcMap, opts); err != nil {
|
||||
if val, ok := err.(aggregateError); ok {
|
||||
agrErr.errors = append(agrErr.errors, val.errors...)
|
||||
} else {
|
||||
agrErr.errors = append(agrErr.errors, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(agrErr.errors) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return agrErr
|
||||
}
|
||||
|
||||
func doParseField(refField reflect.Value, refTypeField reflect.StructField, funcMap map[reflect.Type]ParserFunc, opts []Options) error {
|
||||
if !refField.CanSet() {
|
||||
return nil
|
||||
}
|
||||
if reflect.Ptr == refField.Kind() && refField.Elem().Kind() == reflect.Struct {
|
||||
return ParseWithFuncs(refField.Interface(), funcMap, optsWithPrefix(refTypeField, opts)...)
|
||||
}
|
||||
if reflect.Struct == refField.Kind() && refField.CanAddr() && refField.Type().Name() == "" {
|
||||
return ParseWithFuncs(refField.Addr().Interface(), funcMap, optsWithPrefix(refTypeField, opts)...)
|
||||
}
|
||||
value, err := get(refTypeField, opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if value != "" {
|
||||
return set(refField, refTypeField, value, funcMap)
|
||||
}
|
||||
|
||||
if reflect.Struct == refField.Kind() {
|
||||
return doParse(refField, funcMap, optsWithPrefix(refTypeField, opts))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func get(field reflect.StructField, opts []Options) (val string, err error) {
|
||||
var exists bool
|
||||
var isDefault bool
|
||||
var loadFile bool
|
||||
var unset bool
|
||||
var notEmpty bool
|
||||
|
||||
required := opts[0].RequiredIfNoDef
|
||||
prefix := opts[0].Prefix
|
||||
ownKey, tags := parseKeyForOption(field.Tag.Get(getTagName(opts)))
|
||||
key := prefix + ownKey
|
||||
for _, tag := range tags {
|
||||
switch tag {
|
||||
case "":
|
||||
continue
|
||||
case "file":
|
||||
loadFile = true
|
||||
case "required":
|
||||
required = true
|
||||
case "unset":
|
||||
unset = true
|
||||
case "notEmpty":
|
||||
notEmpty = true
|
||||
default:
|
||||
return "", fmt.Errorf("tag option %q not supported", tag)
|
||||
}
|
||||
}
|
||||
expand := strings.EqualFold(field.Tag.Get("envExpand"), "true")
|
||||
defaultValue, defExists := field.Tag.Lookup("envDefault")
|
||||
val, exists, isDefault = getOr(key, defaultValue, defExists, getEnvironment(opts))
|
||||
|
||||
if expand {
|
||||
val = os.ExpandEnv(val)
|
||||
}
|
||||
|
||||
if unset {
|
||||
defer os.Unsetenv(key)
|
||||
}
|
||||
|
||||
if required && !exists && len(ownKey) > 0 {
|
||||
return "", fmt.Errorf(`required environment variable %q is not set`, key)
|
||||
}
|
||||
|
||||
if notEmpty && val == "" {
|
||||
return "", fmt.Errorf("environment variable %q should not be empty", key)
|
||||
}
|
||||
|
||||
if loadFile && val != "" {
|
||||
filename := val
|
||||
val, err = getFromFile(filename)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf(`could not load content of file "%s" from variable %s: %v`, filename, key, err)
|
||||
}
|
||||
}
|
||||
|
||||
if onSetFn := getOnSetFn(opts); onSetFn != nil {
|
||||
onSetFn(key, val, isDefault)
|
||||
}
|
||||
return val, err
|
||||
}
|
||||
|
||||
// split the env tag's key into the expected key and desired option, if any.
|
||||
func parseKeyForOption(key string) (string, []string) {
|
||||
opts := strings.Split(key, ",")
|
||||
return opts[0], opts[1:]
|
||||
}
|
||||
|
||||
func getFromFile(filename string) (value string, err error) {
|
||||
b, err := os.ReadFile(filename)
|
||||
return string(b), err
|
||||
}
|
||||
|
||||
func getOr(key, defaultValue string, defExists bool, envs map[string]string) (string, bool, bool) {
|
||||
value, exists := envs[key]
|
||||
switch {
|
||||
case (!exists || key == "") && defExists:
|
||||
return defaultValue, true, true
|
||||
case !exists:
|
||||
return "", false, false
|
||||
}
|
||||
|
||||
return value, true, false
|
||||
}
|
||||
|
||||
func set(field reflect.Value, sf reflect.StructField, value string, funcMap map[reflect.Type]ParserFunc) error {
|
||||
if tm := asTextUnmarshaler(field); tm != nil {
|
||||
if err := tm.UnmarshalText([]byte(value)); err != nil {
|
||||
return newParseError(sf, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
typee := sf.Type
|
||||
fieldee := field
|
||||
if typee.Kind() == reflect.Ptr {
|
||||
typee = typee.Elem()
|
||||
fieldee = field.Elem()
|
||||
}
|
||||
|
||||
parserFunc, ok := funcMap[typee]
|
||||
if ok {
|
||||
val, err := parserFunc(value)
|
||||
if err != nil {
|
||||
return newParseError(sf, err)
|
||||
}
|
||||
|
||||
fieldee.Set(reflect.ValueOf(val))
|
||||
return nil
|
||||
}
|
||||
|
||||
parserFunc, ok = defaultBuiltInParsers[typee.Kind()]
|
||||
if ok {
|
||||
val, err := parserFunc(value)
|
||||
if err != nil {
|
||||
return newParseError(sf, err)
|
||||
}
|
||||
|
||||
fieldee.Set(reflect.ValueOf(val).Convert(typee))
|
||||
return nil
|
||||
}
|
||||
|
||||
if field.Kind() == reflect.Slice {
|
||||
return handleSlice(field, value, sf, funcMap)
|
||||
}
|
||||
|
||||
return newNoParserError(sf)
|
||||
}
|
||||
|
||||
func handleSlice(field reflect.Value, value string, sf reflect.StructField, funcMap map[reflect.Type]ParserFunc) error {
|
||||
separator := sf.Tag.Get("envSeparator")
|
||||
if separator == "" {
|
||||
separator = ","
|
||||
}
|
||||
parts := strings.Split(value, separator)
|
||||
|
||||
typee := sf.Type.Elem()
|
||||
if typee.Kind() == reflect.Ptr {
|
||||
typee = typee.Elem()
|
||||
}
|
||||
|
||||
if _, ok := reflect.New(typee).Interface().(encoding.TextUnmarshaler); ok {
|
||||
return parseTextUnmarshalers(field, parts, sf)
|
||||
}
|
||||
|
||||
parserFunc, ok := funcMap[typee]
|
||||
if !ok {
|
||||
parserFunc, ok = defaultBuiltInParsers[typee.Kind()]
|
||||
if !ok {
|
||||
return newNoParserError(sf)
|
||||
}
|
||||
}
|
||||
|
||||
result := reflect.MakeSlice(sf.Type, 0, len(parts))
|
||||
for _, part := range parts {
|
||||
r, err := parserFunc(part)
|
||||
if err != nil {
|
||||
return newParseError(sf, err)
|
||||
}
|
||||
v := reflect.ValueOf(r).Convert(typee)
|
||||
if sf.Type.Elem().Kind() == reflect.Ptr {
|
||||
v = reflect.New(typee)
|
||||
v.Elem().Set(reflect.ValueOf(r).Convert(typee))
|
||||
}
|
||||
result = reflect.Append(result, v)
|
||||
}
|
||||
field.Set(result)
|
||||
return nil
|
||||
}
|
||||
|
||||
func asTextUnmarshaler(field reflect.Value) encoding.TextUnmarshaler {
|
||||
if reflect.Ptr == field.Kind() {
|
||||
if field.IsNil() {
|
||||
field.Set(reflect.New(field.Type().Elem()))
|
||||
}
|
||||
} else if field.CanAddr() {
|
||||
field = field.Addr()
|
||||
}
|
||||
|
||||
tm, ok := field.Interface().(encoding.TextUnmarshaler)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return tm
|
||||
}
|
||||
|
||||
func parseTextUnmarshalers(field reflect.Value, data []string, sf reflect.StructField) error {
|
||||
s := len(data)
|
||||
elemType := field.Type().Elem()
|
||||
slice := reflect.MakeSlice(reflect.SliceOf(elemType), s, s)
|
||||
for i, v := range data {
|
||||
sv := slice.Index(i)
|
||||
kind := sv.Kind()
|
||||
if kind == reflect.Ptr {
|
||||
sv = reflect.New(elemType.Elem())
|
||||
} else {
|
||||
sv = sv.Addr()
|
||||
}
|
||||
tm := sv.Interface().(encoding.TextUnmarshaler)
|
||||
if err := tm.UnmarshalText([]byte(v)); err != nil {
|
||||
return newParseError(sf, err)
|
||||
}
|
||||
if kind == reflect.Ptr {
|
||||
slice.Index(i).Set(sv)
|
||||
}
|
||||
}
|
||||
|
||||
field.Set(slice)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func newParseError(sf reflect.StructField, err error) error {
|
||||
return parseError{
|
||||
sf: sf,
|
||||
err: err,
|
||||
}
|
||||
}
|
||||
|
||||
type parseError struct {
|
||||
sf reflect.StructField
|
||||
err error
|
||||
}
|
||||
|
||||
func (e parseError) Error() string {
|
||||
return fmt.Sprintf(`parse error on field "%s" of type "%s": %v`, e.sf.Name, e.sf.Type, e.err)
|
||||
}
|
||||
|
||||
func newNoParserError(sf reflect.StructField) error {
|
||||
return fmt.Errorf(`no parser found for field "%s" of type "%s"`, sf.Name, sf.Type)
|
||||
}
|
||||
|
||||
func optsWithPrefix(field reflect.StructField, opts []Options) []Options {
|
||||
subOpts := make([]Options, len(opts))
|
||||
copy(subOpts, opts)
|
||||
if prefix := field.Tag.Get("envPrefix"); prefix != "" {
|
||||
subOpts[0].Prefix += prefix
|
||||
}
|
||||
return subOpts
|
||||
}
|
||||
|
||||
type aggregateError struct {
|
||||
errors []error
|
||||
}
|
||||
|
||||
func (e aggregateError) Error() string {
|
||||
var sb strings.Builder
|
||||
sb.WriteString("env:")
|
||||
|
||||
for _, err := range e.errors {
|
||||
sb.WriteString(fmt.Sprintf(" %v;", err.Error()))
|
||||
}
|
||||
|
||||
return strings.TrimRight(sb.String(), ";")
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
//go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris
|
||||
// +build darwin dragonfly freebsd linux netbsd openbsd solaris
|
||||
|
||||
package env
|
||||
|
||||
import "strings"
|
||||
|
||||
func toMap(env []string) map[string]string {
|
||||
r := map[string]string{}
|
||||
for _, e := range env {
|
||||
p := strings.SplitN(e, "=", 2)
|
||||
r[p[0]] = p[1]
|
||||
}
|
||||
return r
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
package env
|
||||
|
||||
import "strings"
|
||||
|
||||
func toMap(env []string) map[string]string {
|
||||
r := map[string]string{}
|
||||
for _, e := range env {
|
||||
p := strings.SplitN(e, "=", 2)
|
||||
|
||||
// On Windows, environment variables can start with '='. If so, Split at next character.
|
||||
// See env_windows.go in the Go source: https://github.com/golang/go/blob/master/src/syscall/env_windows.go#L58
|
||||
prefixEqualSign := false
|
||||
if len(e) > 0 && e[0] == '=' {
|
||||
e = e[1:]
|
||||
prefixEqualSign = true
|
||||
}
|
||||
p = strings.SplitN(e, "=", 2)
|
||||
if prefixEqualSign {
|
||||
p[0] = "=" + p[0]
|
||||
}
|
||||
|
||||
r[p[0]] = p[1]
|
||||
}
|
||||
return r
|
||||
}
|
||||
Reference in New Issue
Block a user