Compare commits
7 Commits
bd2b9f02b1
...
b2e3ad0d67
Author | SHA1 | Date |
---|---|---|
Maxim Lebedev | b2e3ad0d67 | |
Maxim Lebedev | a935ec015d | |
Maxim Lebedev | d056179cf7 | |
Maxim Lebedev | 6ef014482c | |
Maxim Lebedev | 5ea6fe0c4b | |
Maxim Lebedev | dcfc62e081 | |
Maxim Lebedev | aeba4bbb8a |
2
go.mod
2
go.mod
|
@ -8,3 +8,5 @@ require (
|
||||||
)
|
)
|
||||||
|
|
||||||
require golang.org/x/net v0.15.0
|
require golang.org/x/net v0.15.0
|
||||||
|
|
||||||
|
require github.com/caarlos0/env/v9 v9.0.0
|
||||||
|
|
2
go.sum
2
go.sum
|
@ -1,3 +1,5 @@
|
||||||
|
github.com/caarlos0/env/v9 v9.0.0 h1:SI6JNsOA+y5gj9njpgybykATIylrRMklbs5ch6wO6pc=
|
||||||
|
github.com/caarlos0/env/v9 v9.0.0/go.mod h1:ye5mlCVMYh6tZ+vCgrs/B95sj88cg5Tlnc0XIzgZ020=
|
||||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8=
|
golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8=
|
||||||
|
|
|
@ -117,8 +117,8 @@ func WriteError(w http.ResponseWriter, description string, status int) {
|
||||||
out.Error = "unauthorized"
|
out.Error = "unauthorized"
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = json.NewEncoder(w).Encode(out)
|
|
||||||
|
|
||||||
w.Header().Set(common.HeaderContentType, common.MIMEApplicationJSONCharsetUTF8)
|
w.Header().Set(common.HeaderContentType, common.MIMEApplicationJSONCharsetUTF8)
|
||||||
w.WriteHeader(status)
|
w.WriteHeader(status)
|
||||||
|
|
||||||
|
_ = json.NewEncoder(w).Encode(out)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,108 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"flag"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"runtime"
|
||||||
|
"runtime/pprof"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/caarlos0/env/v9"
|
||||||
|
|
||||||
|
"source.toby3d.me/toby3d/pub/internal/domain"
|
||||||
|
mediahttpdelivery "source.toby3d.me/toby3d/pub/internal/media/delivery/http"
|
||||||
|
mediamemoryrepo "source.toby3d.me/toby3d/pub/internal/media/repository/memory"
|
||||||
|
mediaucase "source.toby3d.me/toby3d/pub/internal/media/usecase"
|
||||||
|
"source.toby3d.me/toby3d/pub/internal/urlutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
config = new(domain.Config)
|
||||||
|
logger = log.New(os.Stdout, "Micropub\t", log.LstdFlags)
|
||||||
|
)
|
||||||
|
|
||||||
|
var cpuProfilePath, memProfilePath string
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
flag.StringVar(&cpuProfilePath, "cpuprofile", "", "set path to saving CPU memory profile")
|
||||||
|
flag.StringVar(&memProfilePath, "memprofile", "", "set path to saving pprof memory profile")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if err := env.ParseWithOptions(config, env.Options{
|
||||||
|
Prefix: "MICROPUB_",
|
||||||
|
}); err != nil {
|
||||||
|
logger.Fatal("cannot parse environment variables into config:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
ctx := context.Background()
|
||||||
|
mediaRepo := mediamemoryrepo.NewMemoryMediaRepository()
|
||||||
|
mediaUseCase := mediaucase.NewMediaUseCase(mediaRepo)
|
||||||
|
mediaHandler := mediahttpdelivery.NewHandler(mediaUseCase, *config)
|
||||||
|
|
||||||
|
server := http.Server{
|
||||||
|
ErrorLog: logger,
|
||||||
|
Addr: config.HTTP.Bind,
|
||||||
|
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
head, _ := urlutil.ShiftPath(r.RequestURI)
|
||||||
|
|
||||||
|
switch head {
|
||||||
|
default:
|
||||||
|
http.NotFound(w, r)
|
||||||
|
case "media":
|
||||||
|
mediaHandler.ServeHTTP(w, r)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
done := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
|
||||||
|
if cpuProfilePath != "" {
|
||||||
|
cpuProfile, err := os.Create(cpuProfilePath)
|
||||||
|
if err != nil {
|
||||||
|
logger.Fatalln("could not create CPU profile:", err)
|
||||||
|
}
|
||||||
|
defer cpuProfile.Close()
|
||||||
|
|
||||||
|
if err = pprof.StartCPUProfile(cpuProfile); err != nil {
|
||||||
|
logger.Fatalln("could not start CPU profile:", err)
|
||||||
|
}
|
||||||
|
defer pprof.StopCPUProfile()
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
logger.Printf("started at %s, available at %s", config.HTTP.Bind, config.HTTP.BaseURL())
|
||||||
|
if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||||
|
logger.Fatalln("cannot listen and serve:", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
<-done
|
||||||
|
|
||||||
|
if err := server.Shutdown(ctx); err != nil {
|
||||||
|
logger.Fatalln("failed shutdown of server:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if memProfilePath == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
memProfile, err := os.Create(memProfilePath)
|
||||||
|
if err != nil {
|
||||||
|
logger.Fatalln("could not create memory profile:", err)
|
||||||
|
}
|
||||||
|
defer memProfile.Close()
|
||||||
|
|
||||||
|
runtime.GC() // NOTE(toby3d): get up-to-date statistics
|
||||||
|
|
||||||
|
if err = pprof.WriteHeapProfile(memProfile); err != nil {
|
||||||
|
logger.Fatalln("could not write memory profile:", err)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
coverage.txt
|
||||||
|
bin
|
||||||
|
card.png
|
||||||
|
dist
|
|
@ -0,0 +1,8 @@
|
||||||
|
linters:
|
||||||
|
enable:
|
||||||
|
- thelper
|
||||||
|
- gofumpt
|
||||||
|
- tparallel
|
||||||
|
- unconvert
|
||||||
|
- unparam
|
||||||
|
- wastedassign
|
|
@ -0,0 +1,3 @@
|
||||||
|
includes:
|
||||||
|
- from_url:
|
||||||
|
url: https://raw.githubusercontent.com/caarlos0/.goreleaserfiles/main/lib.yml
|
|
@ -0,0 +1,7 @@
|
||||||
|
Carlos Alexandro Becker <caarlos0@users.noreply.github.com> Carlos A Becker <caarlos0@gmail.com>
|
||||||
|
Carlos Alexandro Becker <caarlos0@users.noreply.github.com> Carlos A Becker <caarlos0@users.noreply.github.com>
|
||||||
|
Carlos Alexandro Becker <caarlos0@users.noreply.github.com> Carlos Alexandro Becker <caarlos0@gmail.com>
|
||||||
|
Carlos Alexandro Becker <caarlos0@users.noreply.github.com> Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
|
||||||
|
Carlos Alexandro Becker <caarlos0@users.noreply.github.com> Carlos Becker <caarlos0@gmail.com>
|
||||||
|
dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
|
||||||
|
actions-user <actions@github.com> github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
|
|
@ -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.
|
|
@ -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
|
|
@ -0,0 +1,577 @@
|
||||||
|
# env
|
||||||
|
|
||||||
|
[![Build Status](https://img.shields.io/github/actions/workflow/status/caarlos0/env/build.yml?branch=main&style=for-the-badge)](https://github.com/caarlos0/env/actions?workflow=build)
|
||||||
|
[![Coverage Status](https://img.shields.io/codecov/c/gh/caarlos0/env.svg?logo=codecov&style=for-the-badge)](https://codecov.io/gh/caarlos0/env)
|
||||||
|
[![](http://img.shields.io/badge/godoc-reference-5272B4.svg?style=for-the-badge)](https://pkg.go.dev/github.com/caarlos0/env/v9)
|
||||||
|
|
||||||
|
A simple and zero-dependencies library to parse environment variables into
|
||||||
|
`struct`s.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
Get the module with:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
go get github.com/caarlos0/env/v9
|
||||||
|
```
|
||||||
|
|
||||||
|
The usage looks like this:
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/caarlos0/env/v9"
|
||||||
|
)
|
||||||
|
|
||||||
|
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,expand" envDefault:"${HOME}/tmp"`
|
||||||
|
}
|
||||||
|
|
||||||
|
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}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Caveats
|
||||||
|
|
||||||
|
> **Warning**
|
||||||
|
>
|
||||||
|
> **This is important!**
|
||||||
|
|
||||||
|
- _Unexported fields_ are **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, and maps of those types are also
|
||||||
|
supported.
|
||||||
|
|
||||||
|
You can also use/define a [custom parser func](#custom-parser-funcs) for any
|
||||||
|
other type you want.
|
||||||
|
|
||||||
|
You can also use custom keys and values in your maps, as long as you provide a
|
||||||
|
parser function for them.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
## 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.ParseWithOptions()` function.
|
||||||
|
|
||||||
|
In addition to accepting a struct pointer (same as `Parse()`), this function
|
||||||
|
also accepts a `Options{}`, and you can set your custom parsers in the `FuncMap`
|
||||||
|
field.
|
||||||
|
|
||||||
|
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/v9)
|
||||||
|
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"`
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Warning**
|
||||||
|
>
|
||||||
|
> Note that being set is not the same as being empty.
|
||||||
|
> If the variable is set, but empty, the field will have its type's default
|
||||||
|
> value.
|
||||||
|
> This also means that custom parser funcs will not be invoked.
|
||||||
|
|
||||||
|
## Expand vars
|
||||||
|
|
||||||
|
If you set the `expand` option, environment variables (either in `${var}` or
|
||||||
|
`$var` format) in the string will be replaced according with the actual value
|
||||||
|
of the variable. For example:
|
||||||
|
|
||||||
|
```go
|
||||||
|
type config struct {
|
||||||
|
SecretKey string `env:"SECRET_KEY,expand"`
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This also works with `envDefault`.
|
||||||
|
|
||||||
|
## Not Empty fields
|
||||||
|
|
||||||
|
While `required` demands the environment variable to be set, 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
|
||||||
|
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:
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
"github.com/caarlos0/env/v9"
|
||||||
|
)
|
||||||
|
|
||||||
|
type config struct {
|
||||||
|
Secret string `env:"SECRET,file"`
|
||||||
|
Password string `env:"PASSWORD,file" envDefault:"/tmp/password"`
|
||||||
|
Certificate string `env:"CERTIFICATE,file,expand" envDefault:"${CERTIFICATE_FILE}"`
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
### Use field names as environment variables by default
|
||||||
|
|
||||||
|
If you don't want to set the `env` tag on every field, you can use the
|
||||||
|
`UseFieldNameByDefault` option.
|
||||||
|
|
||||||
|
It will use the field name as environment variable name.
|
||||||
|
|
||||||
|
Here's an example:
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/caarlos0/env/v9"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Username string // will use $USERNAME
|
||||||
|
Password string // will use $PASSWORD
|
||||||
|
UserFullName string // will use $USER_FULL_NAME
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
cfg := &Config{}
|
||||||
|
opts := env.Options{UseFieldNameByDefault: true}
|
||||||
|
|
||||||
|
// Load env vars.
|
||||||
|
if err := env.ParseWithOptions(cfg, opts); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print the loaded data.
|
||||||
|
fmt.Printf("%+v\n", cfg)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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/v9"
|
||||||
|
)
|
||||||
|
|
||||||
|
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.ParseWithOptions(cfg, opts); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print the loaded data.
|
||||||
|
fmt.Printf("%+v\n", cfg)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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/v9"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Password string `json:"PASSWORD"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
cfg := &Config{}
|
||||||
|
opts := env.Options{TagName: "json"}
|
||||||
|
|
||||||
|
// Load env vars.
|
||||||
|
if err := env.ParseWithOptions(cfg, opts); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print the loaded data.
|
||||||
|
fmt.Printf("%+v\n", cfg)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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/v9"
|
||||||
|
)
|
||||||
|
|
||||||
|
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{}
|
||||||
|
opts := env.Options{
|
||||||
|
Prefix: "T_",
|
||||||
|
Environment: map[string]string{
|
||||||
|
"T_FOO_HOME": "/foo",
|
||||||
|
"T_BAR_HOME": "/bar",
|
||||||
|
"T_BLAH": "blahhh",
|
||||||
|
"T_HOME": "/clean",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load env vars.
|
||||||
|
if err := env.ParseWithOptions(cfg, opts); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print the loaded data.
|
||||||
|
fmt.Printf("%+v\n", cfg)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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/v9"
|
||||||
|
)
|
||||||
|
|
||||||
|
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.ParseWithOptions(cfg, opts); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print the loaded data.
|
||||||
|
fmt.Printf("%+v\n", cfg)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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/v9"
|
||||||
|
)
|
||||||
|
|
||||||
|
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.ParseWithOptions(cfg, opts); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print the loaded data.
|
||||||
|
fmt.Printf("%+v\n", cfg)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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/v9"
|
||||||
|
)
|
||||||
|
|
||||||
|
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}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error handling
|
||||||
|
|
||||||
|
You can handle the errors the library throws like so:
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/caarlos0/env/v9"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Username string `env:"USERNAME" envDefault:"admin"`
|
||||||
|
Password string `env:"PASSWORD"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
var cfg Config
|
||||||
|
err := env.Parse(&cfg)
|
||||||
|
if e, ok := err.(*env.AggregateError); ok {
|
||||||
|
for _, er := range e.Errors {
|
||||||
|
switch v := er.(type) {
|
||||||
|
case env.ParseError:
|
||||||
|
// handle it
|
||||||
|
case env.NotStructPtrError:
|
||||||
|
// handle it
|
||||||
|
case env.NoParserError:
|
||||||
|
// handle it
|
||||||
|
case env.NoSupportedTagOptionError:
|
||||||
|
// handle it
|
||||||
|
default:
|
||||||
|
fmt.Printf("Unknown error type %v", v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("%+v", cfg) // {Username:admin Password:123456}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Info**
|
||||||
|
>
|
||||||
|
> If you want to check if an specific error is in the chain, you can also use
|
||||||
|
> `errors.Is()`.
|
||||||
|
|
||||||
|
## Stargazers over time
|
||||||
|
|
||||||
|
[![Stargazers over time](https://starchart.cc/caarlos0/env.svg)](https://starchart.cc/caarlos0/env)
|
|
@ -0,0 +1,520 @@
|
||||||
|
package env
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
"unicode"
|
||||||
|
)
|
||||||
|
|
||||||
|
// nolint: gochecknoglobals
|
||||||
|
var (
|
||||||
|
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, newParseValueError("unable to parse URL", err)
|
||||||
|
}
|
||||||
|
return *u, nil
|
||||||
|
},
|
||||||
|
reflect.TypeOf(time.Nanosecond): func(v string) (interface{}, error) {
|
||||||
|
s, err := time.ParseDuration(v)
|
||||||
|
if err != nil {
|
||||||
|
return nil, newParseValueError("unable to parse duration", 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
|
||||||
|
|
||||||
|
// UseFieldNameByDefault defines whether or not env should use the field
|
||||||
|
// name by default if the `env` key is missing.
|
||||||
|
UseFieldNameByDefault bool
|
||||||
|
|
||||||
|
// Custom parse functions for different types.
|
||||||
|
FuncMap map[reflect.Type]ParserFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultOptions() Options {
|
||||||
|
return Options{
|
||||||
|
TagName: "env",
|
||||||
|
Environment: toMap(os.Environ()),
|
||||||
|
FuncMap: defaultTypeParsers(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func customOptions(opt Options) Options {
|
||||||
|
defOpts := defaultOptions()
|
||||||
|
if opt.TagName == "" {
|
||||||
|
opt.TagName = defOpts.TagName
|
||||||
|
}
|
||||||
|
if opt.Environment == nil {
|
||||||
|
opt.Environment = defOpts.Environment
|
||||||
|
}
|
||||||
|
if opt.FuncMap == nil {
|
||||||
|
opt.FuncMap = map[reflect.Type]ParserFunc{}
|
||||||
|
}
|
||||||
|
for k, v := range defOpts.FuncMap {
|
||||||
|
opt.FuncMap[k] = v
|
||||||
|
}
|
||||||
|
return opt
|
||||||
|
}
|
||||||
|
|
||||||
|
func optionsWithEnvPrefix(field reflect.StructField, opts Options) Options {
|
||||||
|
return Options{
|
||||||
|
Environment: opts.Environment,
|
||||||
|
TagName: opts.TagName,
|
||||||
|
RequiredIfNoDef: opts.RequiredIfNoDef,
|
||||||
|
OnSet: opts.OnSet,
|
||||||
|
Prefix: opts.Prefix + field.Tag.Get("envPrefix"),
|
||||||
|
UseFieldNameByDefault: opts.UseFieldNameByDefault,
|
||||||
|
FuncMap: opts.FuncMap,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse parses a struct containing `env` tags and loads its values from
|
||||||
|
// environment variables.
|
||||||
|
func Parse(v interface{}) error {
|
||||||
|
return parseInternal(v, defaultOptions())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse parses a struct containing `env` tags and loads its values from
|
||||||
|
// environment variables.
|
||||||
|
func ParseWithOptions(v interface{}, opts Options) error {
|
||||||
|
return parseInternal(v, customOptions(opts))
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseInternal(v interface{}, opts Options) error {
|
||||||
|
ptrRef := reflect.ValueOf(v)
|
||||||
|
if ptrRef.Kind() != reflect.Ptr {
|
||||||
|
return newAggregateError(NotStructPtrError{})
|
||||||
|
}
|
||||||
|
ref := ptrRef.Elem()
|
||||||
|
if ref.Kind() != reflect.Struct {
|
||||||
|
return newAggregateError(NotStructPtrError{})
|
||||||
|
}
|
||||||
|
return doParse(ref, opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
func doParse(ref reflect.Value, 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, 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, opts Options) error {
|
||||||
|
if !refField.CanSet() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if reflect.Ptr == refField.Kind() && !refField.IsNil() {
|
||||||
|
return parseInternal(refField.Interface(), optionsWithEnvPrefix(refTypeField, opts))
|
||||||
|
}
|
||||||
|
if reflect.Struct == refField.Kind() && refField.CanAddr() && refField.Type().Name() == "" {
|
||||||
|
return parseInternal(refField.Addr().Interface(), optionsWithEnvPrefix(refTypeField, opts))
|
||||||
|
}
|
||||||
|
value, err := get(refTypeField, opts)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if value != "" {
|
||||||
|
return set(refField, refTypeField, value, opts.FuncMap)
|
||||||
|
}
|
||||||
|
|
||||||
|
if reflect.Struct == refField.Kind() {
|
||||||
|
return doParse(refField, optionsWithEnvPrefix(refTypeField, opts))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const underscore rune = '_'
|
||||||
|
|
||||||
|
func toEnvName(input string) string {
|
||||||
|
var output []rune
|
||||||
|
for i, c := range input {
|
||||||
|
if i > 0 && output[i-1] != underscore && c != underscore && unicode.ToUpper(c) == c {
|
||||||
|
output = append(output, underscore)
|
||||||
|
}
|
||||||
|
output = append(output, unicode.ToUpper(c))
|
||||||
|
}
|
||||||
|
return string(output)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
var expand bool
|
||||||
|
|
||||||
|
required := opts.RequiredIfNoDef
|
||||||
|
ownKey, tags := parseKeyForOption(field.Tag.Get(opts.TagName))
|
||||||
|
if ownKey == "" && opts.UseFieldNameByDefault {
|
||||||
|
ownKey = toEnvName(field.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tag := range tags {
|
||||||
|
switch tag {
|
||||||
|
case "":
|
||||||
|
continue
|
||||||
|
case "file":
|
||||||
|
loadFile = true
|
||||||
|
case "required":
|
||||||
|
required = true
|
||||||
|
case "unset":
|
||||||
|
unset = true
|
||||||
|
case "notEmpty":
|
||||||
|
notEmpty = true
|
||||||
|
case "expand":
|
||||||
|
expand = true
|
||||||
|
default:
|
||||||
|
return "", newNoSupportedTagOptionError(tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
prefix := opts.Prefix
|
||||||
|
key := prefix + ownKey
|
||||||
|
defaultValue, defExists := field.Tag.Lookup("envDefault")
|
||||||
|
val, exists, isDefault = getOr(key, defaultValue, defExists, opts.Environment)
|
||||||
|
|
||||||
|
if expand {
|
||||||
|
val = os.ExpandEnv(val)
|
||||||
|
}
|
||||||
|
|
||||||
|
if unset {
|
||||||
|
defer os.Unsetenv(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
if required && !exists && len(ownKey) > 0 {
|
||||||
|
return "", newEnvVarIsNotSet(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
if notEmpty && val == "" {
|
||||||
|
return "", newEmptyEnvVarError(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
if loadFile && val != "" {
|
||||||
|
filename := val
|
||||||
|
val, err = getFromFile(filename)
|
||||||
|
if err != nil {
|
||||||
|
return "", newLoadFileContentError(filename, key, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.OnSet != nil {
|
||||||
|
if ownKey != "" {
|
||||||
|
opts.OnSet(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 && value == "" && 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
|
||||||
|
}
|
||||||
|
|
||||||
|
switch field.Kind() {
|
||||||
|
case reflect.Slice:
|
||||||
|
return handleSlice(field, value, sf, funcMap)
|
||||||
|
case reflect.Map:
|
||||||
|
return handleMap(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 handleMap(field reflect.Value, value string, sf reflect.StructField, funcMap map[reflect.Type]ParserFunc) error {
|
||||||
|
keyType := sf.Type.Key()
|
||||||
|
keyParserFunc, ok := funcMap[keyType]
|
||||||
|
if !ok {
|
||||||
|
keyParserFunc, ok = defaultBuiltInParsers[keyType.Kind()]
|
||||||
|
if !ok {
|
||||||
|
return newNoParserError(sf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
elemType := sf.Type.Elem()
|
||||||
|
elemParserFunc, ok := funcMap[elemType]
|
||||||
|
if !ok {
|
||||||
|
elemParserFunc, ok = defaultBuiltInParsers[elemType.Kind()]
|
||||||
|
if !ok {
|
||||||
|
return newNoParserError(sf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
separator := sf.Tag.Get("envSeparator")
|
||||||
|
if separator == "" {
|
||||||
|
separator = ","
|
||||||
|
}
|
||||||
|
|
||||||
|
result := reflect.MakeMap(sf.Type)
|
||||||
|
for _, part := range strings.Split(value, separator) {
|
||||||
|
pairs := strings.Split(part, ":")
|
||||||
|
if len(pairs) != 2 {
|
||||||
|
return newParseError(sf, fmt.Errorf(`%q should be in "key:value" format`, part))
|
||||||
|
}
|
||||||
|
|
||||||
|
key, err := keyParserFunc(pairs[0])
|
||||||
|
if err != nil {
|
||||||
|
return newParseError(sf, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
elem, err := elemParserFunc(pairs[1])
|
||||||
|
if err != nil {
|
||||||
|
return newParseError(sf, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result.SetMapIndex(reflect.ValueOf(key).Convert(keyType), reflect.ValueOf(elem).Convert(elemType))
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
//go:build !windows
|
||||||
|
|
||||||
|
package env
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
func toMap(env []string) map[string]string {
|
||||||
|
r := map[string]string{}
|
||||||
|
for _, e := range env {
|
||||||
|
p := strings.SplitN(e, "=", 2)
|
||||||
|
if len(p) == 2 {
|
||||||
|
r[p[0]] = p[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
//go:build windows
|
||||||
|
|
||||||
|
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]
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(p) == 2 {
|
||||||
|
r[p[0]] = p[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
|
@ -0,0 +1,164 @@
|
||||||
|
package env
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// An aggregated error wrapper to combine gathered errors. This allows either to display all errors or convert them individually
|
||||||
|
// List of the available errors
|
||||||
|
// ParseError
|
||||||
|
// NotStructPtrError
|
||||||
|
// NoParserError
|
||||||
|
// NoSupportedTagOptionError
|
||||||
|
// EnvVarIsNotSetError
|
||||||
|
// EmptyEnvVarError
|
||||||
|
// LoadFileContentError
|
||||||
|
// ParseValueError
|
||||||
|
type AggregateError struct {
|
||||||
|
Errors []error
|
||||||
|
}
|
||||||
|
|
||||||
|
func newAggregateError(initErr error) error {
|
||||||
|
return AggregateError{
|
||||||
|
[]error{
|
||||||
|
initErr,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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(), ";")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Is conforms with errors.Is.
|
||||||
|
func (e AggregateError) Is(err error) bool {
|
||||||
|
for _, ie := range e.Errors {
|
||||||
|
if reflect.TypeOf(ie) == reflect.TypeOf(err) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// The error occurs when it's impossible to convert the value for given type.
|
||||||
|
type ParseError struct {
|
||||||
|
Name string
|
||||||
|
Type reflect.Type
|
||||||
|
Err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func newParseError(sf reflect.StructField, err error) error {
|
||||||
|
return ParseError{sf.Name, sf.Type, err}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ParseError) Error() string {
|
||||||
|
return fmt.Sprintf(`parse error on field "%s" of type "%s": %v`, e.Name, e.Type, e.Err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The error occurs when pass something that is not a pointer to a Struct to Parse
|
||||||
|
type NotStructPtrError struct{}
|
||||||
|
|
||||||
|
func (e NotStructPtrError) Error() string {
|
||||||
|
return "expected a pointer to a Struct"
|
||||||
|
}
|
||||||
|
|
||||||
|
// This error occurs when there is no parser provided for given type
|
||||||
|
// Supported types and defaults: https://github.com/caarlos0/env#supported-types-and-defaults
|
||||||
|
// How to create a custom parser: https://github.com/caarlos0/env#custom-parser-funcs
|
||||||
|
type NoParserError struct {
|
||||||
|
Name string
|
||||||
|
Type reflect.Type
|
||||||
|
}
|
||||||
|
|
||||||
|
func newNoParserError(sf reflect.StructField) error {
|
||||||
|
return NoParserError{sf.Name, sf.Type}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e NoParserError) Error() string {
|
||||||
|
return fmt.Sprintf(`no parser found for field "%s" of type "%s"`, e.Name, e.Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
// This error occurs when the given tag is not supported
|
||||||
|
// In-built supported tags: "", "file", "required", "unset", "notEmpty", "expand", "envDefault", "envSeparator"
|
||||||
|
// How to create a custom tag: https://github.com/caarlos0/env#changing-default-tag-name
|
||||||
|
type NoSupportedTagOptionError struct {
|
||||||
|
Tag string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newNoSupportedTagOptionError(tag string) error {
|
||||||
|
return NoSupportedTagOptionError{tag}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e NoSupportedTagOptionError) Error() string {
|
||||||
|
return fmt.Sprintf("tag option %q not supported", e.Tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
// This error occurs when the required variable is not set
|
||||||
|
// Read about required fields: https://github.com/caarlos0/env#required-fields
|
||||||
|
type EnvVarIsNotSetError struct {
|
||||||
|
Key string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newEnvVarIsNotSet(key string) error {
|
||||||
|
return EnvVarIsNotSetError{key}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e EnvVarIsNotSetError) Error() string {
|
||||||
|
return fmt.Sprintf(`required environment variable %q is not set`, e.Key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// This error occurs when the variable which must be not empty is existing but has an empty value
|
||||||
|
// Read about not empty fields: https://github.com/caarlos0/env#not-empty-fields
|
||||||
|
type EmptyEnvVarError struct {
|
||||||
|
Key string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newEmptyEnvVarError(key string) error {
|
||||||
|
return EmptyEnvVarError{key}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e EmptyEnvVarError) Error() string {
|
||||||
|
return fmt.Sprintf("environment variable %q should not be empty", e.Key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// This error occurs when it's impossible to load the value from the file
|
||||||
|
// Read about From file feature: https://github.com/caarlos0/env#from-file
|
||||||
|
type LoadFileContentError struct {
|
||||||
|
Filename string
|
||||||
|
Key string
|
||||||
|
Err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func newLoadFileContentError(filename, key string, err error) error {
|
||||||
|
return LoadFileContentError{filename, key, err}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e LoadFileContentError) Error() string {
|
||||||
|
return fmt.Sprintf(`could not load content of file "%s" from variable %s: %v`, e.Filename, e.Key, e.Err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// This error occurs when it's impossible to convert value using given parser
|
||||||
|
// Supported types and defaults: https://github.com/caarlos0/env#supported-types-and-defaults
|
||||||
|
// How to create a custom parser: https://github.com/caarlos0/env#custom-parser-funcs
|
||||||
|
type ParseValueError struct {
|
||||||
|
Msg string
|
||||||
|
Err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func newParseValueError(message string, err error) error {
|
||||||
|
return ParseValueError{message, err}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ParseValueError) Error() string {
|
||||||
|
return fmt.Sprintf("%s: %v", e.Msg, e.Err)
|
||||||
|
}
|
|
@ -1,3 +1,6 @@
|
||||||
|
# github.com/caarlos0/env/v9 v9.0.0
|
||||||
|
## explicit; go 1.17
|
||||||
|
github.com/caarlos0/env/v9
|
||||||
# github.com/google/go-cmp v0.5.9
|
# github.com/google/go-cmp v0.5.9
|
||||||
## explicit; go 1.13
|
## explicit; go 1.13
|
||||||
github.com/google/go-cmp/cmp
|
github.com/google/go-cmp/cmp
|
||||||
|
|
Loading…
Reference in New Issue