diff --git a/Readme.md b/Readme.md index 5dd08be..88dd723 100644 --- a/Readme.md +++ b/Readme.md @@ -1 +1,9 @@ # Miniauth. + + +## Config + +| Name | Description | Default | +|------|-------------|---------| +| USERSTORE_SQLITE_PATH | Path to the Sqlite Database | `none` | +| PORT | Port for the Webserver | 8080 | diff --git a/go.mod b/go.mod index d9f387d..a0b5594 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/go-passwd/validator v0.0.0-20180902184246-0b4c967e436b github.com/google/uuid v1.6.0 github.com/rs/zerolog v1.33.0 + github.com/sethvargo/go-envconfig v1.1.1 github.com/stretchr/testify v1.9.0 github.com/tursodatabase/libsql-client-go v0.0.0-20240902231107-85af5b9d094d golang.org/x/crypto v0.23.0 diff --git a/go.sum b/go.sum index eed30bf..b4c7bc5 100644 --- a/go.sum +++ b/go.sum @@ -37,8 +37,8 @@ github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaC github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= -github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo= github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= @@ -75,6 +75,8 @@ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qq github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= +github.com/sethvargo/go-envconfig v1.1.1 h1:JDu8Q9baIzJf47NPkzhIB6aLYL0vQ+pPypoYrejS9QY= +github.com/sethvargo/go-envconfig v1.1.1/go.mod h1:JLd0KFWQYzyENqnEPWWZ49i4vzZo/6nRidxI8YvGiHw= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= diff --git a/main.go b/main.go index 962d9ea..51373e7 100644 --- a/main.go +++ b/main.go @@ -1,19 +1,39 @@ package main import ( + "context" "embed" + "fmt" "html/template" + "git.keks.cloud/kekskurse/miniauth/pkg/userstore" "git.keks.cloud/kekskurse/miniauth/pkg/web" "github.com/gin-gonic/gin" + "github.com/rs/zerolog/log" + "github.com/sethvargo/go-envconfig" ) //go:embed templates/* var templatesFS embed.FS func main() { + cnf := config() router := setupRouter() - router.Run(":8080") + router.Run(fmt.Sprintf(":%v", cnf.Port)) +} + +type gloableConfig struct { + UserStoreConfig userstore.Config `env:", prefix=USERSTORE_"` + Port int `env:"PORT, default=8080"` +} + +func config() gloableConfig { + var c gloableConfig + err := envconfig.Process(context.Background(), &c) + if err != nil { + log.Fatal().Err(err).Msg("cant parse config") + } + return c } func loadTemplates() *template.Template { diff --git a/pkg/userstore/store.go b/pkg/userstore/store.go index 033726b..fbfc2a4 100644 --- a/pkg/userstore/store.go +++ b/pkg/userstore/store.go @@ -20,7 +20,7 @@ var schema string type Config struct { SQLite struct { - Path string + Path string `env:"SQLITE_PATH"` } Logger zerolog.Logger } diff --git a/vendor/github.com/rs/zerolog/log/log.go b/vendor/github.com/rs/zerolog/log/log.go new file mode 100644 index 0000000..a96ec50 --- /dev/null +++ b/vendor/github.com/rs/zerolog/log/log.go @@ -0,0 +1,131 @@ +// Package log provides a global logger for zerolog. +package log + +import ( + "context" + "fmt" + "io" + "os" + + "github.com/rs/zerolog" +) + +// Logger is the global logger. +var Logger = zerolog.New(os.Stderr).With().Timestamp().Logger() + +// Output duplicates the global logger and sets w as its output. +func Output(w io.Writer) zerolog.Logger { + return Logger.Output(w) +} + +// With creates a child logger with the field added to its context. +func With() zerolog.Context { + return Logger.With() +} + +// Level creates a child logger with the minimum accepted level set to level. +func Level(level zerolog.Level) zerolog.Logger { + return Logger.Level(level) +} + +// Sample returns a logger with the s sampler. +func Sample(s zerolog.Sampler) zerolog.Logger { + return Logger.Sample(s) +} + +// Hook returns a logger with the h Hook. +func Hook(h zerolog.Hook) zerolog.Logger { + return Logger.Hook(h) +} + +// Err starts a new message with error level with err as a field if not nil or +// with info level if err is nil. +// +// You must call Msg on the returned event in order to send the event. +func Err(err error) *zerolog.Event { + return Logger.Err(err) +} + +// Trace starts a new message with trace level. +// +// You must call Msg on the returned event in order to send the event. +func Trace() *zerolog.Event { + return Logger.Trace() +} + +// Debug starts a new message with debug level. +// +// You must call Msg on the returned event in order to send the event. +func Debug() *zerolog.Event { + return Logger.Debug() +} + +// Info starts a new message with info level. +// +// You must call Msg on the returned event in order to send the event. +func Info() *zerolog.Event { + return Logger.Info() +} + +// Warn starts a new message with warn level. +// +// You must call Msg on the returned event in order to send the event. +func Warn() *zerolog.Event { + return Logger.Warn() +} + +// Error starts a new message with error level. +// +// You must call Msg on the returned event in order to send the event. +func Error() *zerolog.Event { + return Logger.Error() +} + +// Fatal starts a new message with fatal level. The os.Exit(1) function +// is called by the Msg method. +// +// You must call Msg on the returned event in order to send the event. +func Fatal() *zerolog.Event { + return Logger.Fatal() +} + +// Panic starts a new message with panic level. The message is also sent +// to the panic function. +// +// You must call Msg on the returned event in order to send the event. +func Panic() *zerolog.Event { + return Logger.Panic() +} + +// WithLevel starts a new message with level. +// +// You must call Msg on the returned event in order to send the event. +func WithLevel(level zerolog.Level) *zerolog.Event { + return Logger.WithLevel(level) +} + +// Log starts a new message with no level. Setting zerolog.GlobalLevel to +// zerolog.Disabled will still disable events produced by this method. +// +// You must call Msg on the returned event in order to send the event. +func Log() *zerolog.Event { + return Logger.Log() +} + +// Print sends a log event using debug level and no extra field. +// Arguments are handled in the manner of fmt.Print. +func Print(v ...interface{}) { + Logger.Debug().CallerSkipFrame(1).Msg(fmt.Sprint(v...)) +} + +// Printf sends a log event using debug level and no extra field. +// Arguments are handled in the manner of fmt.Printf. +func Printf(format string, v ...interface{}) { + Logger.Debug().CallerSkipFrame(1).Msgf(format, v...) +} + +// Ctx returns the Logger associated with the ctx. If no logger +// is associated, a disabled logger is returned. +func Ctx(ctx context.Context) *zerolog.Logger { + return zerolog.Ctx(ctx) +} diff --git a/vendor/github.com/sethvargo/go-envconfig/AUTHORS b/vendor/github.com/sethvargo/go-envconfig/AUTHORS new file mode 100644 index 0000000..2107f4a --- /dev/null +++ b/vendor/github.com/sethvargo/go-envconfig/AUTHORS @@ -0,0 +1,8 @@ +# This is the list of envconfig authors for copyright purposes. +# +# This does not necessarily list everyone who has contributed code, since in +# some cases, their employer may be the copyright holder. To see the full list +# of contributors, see the revision history in source control. + +Google LLC +Seth Vargo diff --git a/vendor/github.com/sethvargo/go-envconfig/LICENSE b/vendor/github.com/sethvargo/go-envconfig/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/vendor/github.com/sethvargo/go-envconfig/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/vendor/github.com/sethvargo/go-envconfig/Makefile b/vendor/github.com/sethvargo/go-envconfig/Makefile new file mode 100644 index 0000000..b95f7a7 --- /dev/null +++ b/vendor/github.com/sethvargo/go-envconfig/Makefile @@ -0,0 +1,22 @@ +# Copyright The envconfig Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +test: + @go test \ + -count=1 \ + -race \ + -shuffle=on \ + -timeout=10m \ + ./... +.PHONY: test diff --git a/vendor/github.com/sethvargo/go-envconfig/README.md b/vendor/github.com/sethvargo/go-envconfig/README.md new file mode 100644 index 0000000..49c2fe1 --- /dev/null +++ b/vendor/github.com/sethvargo/go-envconfig/README.md @@ -0,0 +1,306 @@ +# Envconfig + +[][godoc] + +Envconfig populates struct field values based on environment variables or +arbitrary lookup functions. It supports pre-setting mutations, which is useful +for things like converting values to uppercase, trimming whitespace, or looking +up secrets. + +## Usage + +Define a struct with fields using the `env` tag: + +```go +type MyConfig struct { + Port string `env:"PORT"` + Username string `env:"USERNAME"` +} +``` + +Set some environment variables: + +```sh +export PORT=5555 +export USERNAME=yoyo +``` + +Process it using envconfig: + +```go +package main + +import ( + "context" + "log" + + "github.com/sethvargo/go-envconfig" +) + +func main() { + ctx := context.Background() + + var c MyConfig + if err := envconfig.Process(ctx, &c); err != nil { + log.Fatal(err) + } + + // c.Port = 5555 + // c.Username = "yoyo" +} +``` + +You can also use nested structs, just remember that any fields you want to +process must be public: + +```go +type MyConfig struct { + Database *DatabaseConfig +} + +type DatabaseConfig struct { + Port string `env:"PORT"` + Username string `env:"USERNAME"` +} +``` + +## Configuration + +Use the `env` struct tag to define configuration. See the [godoc][] for usage +examples. + +- `required` - marks a field as required. If a field is required, decoding + will error if the environment variable is unset. + + ```go + type MyStruct struct { + Port string `env:"PORT, required"` + } + ``` + +- `default` - sets the default value for the environment variable is not set. + The environment variable must not be set (e.g. `unset PORT`). If the + environment variable is the empty string, envconfig considers that a "value" + and the default will **not** be used. + + You can also set the default value to the value from another field or a + value from a different environment variable. + + ```go + type MyStruct struct { + Port string `env:"PORT, default=5555"` + User string `env:"USER, default=$CURRENT_USER"` + } + ``` + +- `prefix` - sets the prefix to use for looking up environment variable keys + on child structs and fields. This is useful for shared configurations: + + ```go + type RedisConfig struct { + Host string `env:"REDIS_HOST"` + User string `env:"REDIS_USER"` + } + + type ServerConfig struct { + // CacheConfig will process values from $CACHE_REDIS_HOST and + // $CACHE_REDIS_USER respectively. + CacheConfig *RedisConfig `env:", prefix=CACHE_"` + + // RateLimitConfig will process values from $RATE_LIMIT_REDIS_HOST and + // $RATE_LIMIT_REDIS_USER respectively. + RateLimitConfig *RedisConfig `env:", prefix=RATE_LIMIT_"` + } + ``` + +- `overwrite` - force overwriting existing non-zero struct values if the + environment variable was provided. + + ```go + type MyStruct struct { + Port string `env:"PORT, overwrite"` + } + ``` + + The rules for overwrite + default are: + + - If the struct field has the zero value and a default is set: + + - If no environment variable is specified, the struct field will be + populated with the default value. + + - If an environment variable is specified, the struct field will be + populate with the environment variable value. + + - If the struct field has a non-zero value and a default is set: + + - If no environment variable is specified, the struct field's existing + value will be used (the default is ignored). + + - If an environment variable is specified, the struct field's existing + value will be overwritten with the environment variable value. + +- `delimiter` - choose a custom character to denote individual slice and map + entries. The default value is the comma (`,`). + + ```go + type MyStruct struct { + MyVar []string `env:"MYVAR, delimiter=;"` + ``` + + ```bash + export MYVAR="a;b;c;d" # []string{"a", "b", "c", "d"} + ``` + +- `separator` - choose a custom character to denote the separation between + keys and values in map entries. The default value is the colon (`:`) Define + a separator with `separator`: + + ```go + type MyStruct struct { + MyVar map[string]string `env:"MYVAR, separator=|"` + } + ``` + + ```bash + export MYVAR="a|b,c|d" # map[string]string{"a":"b", "c":"d"} + ``` + +- `noinit` - do not initialize struct fields unless environment variables were + provided. The default behavior is to deeply initialize all fields to their + default (zero) value. + + ```go + type MyStruct struct { + MyVar *url.URL `env:"MYVAR, noinit"` + } + ``` + +- `decodeunset` - force envconfig to run decoders even on unset environment + variable values. The default behavior is to skip running decoders on unset + environment variable values. + + ```go + type MyStruct struct { + MyVar *url.URL `env:"MYVAR, decodeunset"` + } + ``` + + +## Decoding + +> [!NOTE] +> +> Complex types are only decoded or unmarshalled when the environment variable +> is defined or a default value is specified. + + +### Durations + +In the environment, `time.Duration` values are specified as a parsable Go +duration: + +```go +type MyStruct struct { + MyVar time.Duration `env:"MYVAR"` +} +``` + +```bash +export MYVAR="10m" # 10 * time.Minute +``` + + +### TextUnmarshaler / BinaryUnmarshaler + +Types that implement `TextUnmarshaler` or `BinaryUnmarshaler` are processed as +such. + + +### json.Unmarshaler + +Types that implement `json.Unmarshaler` are processed as such. + + +### gob.Decoder + +Types that implement `gob.Decoder` are processed as such. + + +### Slices + +Slices are specified as comma-separated values. + +```go +type MyStruct struct { + MyVar []string `env:"MYVAR"` +} +``` + +```bash +export MYVAR="a,b,c,d" # []string{"a", "b", "c", "d"} +``` + +Note that byte slices are special cased and interpreted as strings from the +environment. + + +### Maps + +Maps are specified as comma-separated key:value pairs: + +```go +type MyStruct struct { + MyVar map[string]string `env:"MYVAR"` +} +``` + +```bash +export MYVAR="a:b,c:d" # map[string]string{"a":"b", "c":"d"} +``` + + +### Structs + +Envconfig walks the entire struct, including nested structs, so deeply-nested +fields are also supported. + +If a nested struct is a pointer type, it will automatically be instantianted to +the non-nil value. To change this behavior, see +[Initialization](#Initialization). + + +### Custom Decoders + +You can also define your own decoders. See the [godoc][godoc] for more +information. + + +## Testing + +Relying on the environment in tests can be troublesome because environment +variables are global, which makes it difficult to parallelize the tests. +Envconfig supports extracting data from anything that returns a value: + +```go +lookuper := envconfig.MapLookuper(map[string]string{ + "FOO": "bar", + "ZIP": "zap", +}) + +var config Config +envconfig.ProcessWith(ctx, &envconfig.Config{ + Target: &config, + Lookuper: lookuper, +}) +``` + +Now you can parallelize all your tests by providing a map for the lookup +function. In fact, that's how the tests in this repo work, so check there for an +example. + +You can also combine multiple lookupers with `MultiLookuper`. See the GoDoc for +more information and examples. + + +[godoc]: https://pkg.go.dev/mod/github.com/sethvargo/go-envconfig diff --git a/vendor/github.com/sethvargo/go-envconfig/decoding.go b/vendor/github.com/sethvargo/go-envconfig/decoding.go new file mode 100644 index 0000000..3d1cd28 --- /dev/null +++ b/vendor/github.com/sethvargo/go-envconfig/decoding.go @@ -0,0 +1,57 @@ +// Copyright The envconfig Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package envconfig + +import ( + "encoding/base64" + "encoding/hex" + "strings" +) + +// Base64Bytes is a slice of bytes where the information is base64-encoded in +// the environment variable. +type Base64Bytes []byte + +// EnvDecode implements env.Decoder. +func (b *Base64Bytes) EnvDecode(val string) error { + val = strings.ReplaceAll(val, "+", "-") + val = strings.ReplaceAll(val, "/", "_") + val = strings.TrimRight(val, "=") + + var err error + *b, err = base64.RawURLEncoding.DecodeString(val) + return err +} + +// Bytes returns the underlying bytes. +func (b Base64Bytes) Bytes() []byte { + return []byte(b) +} + +// HexBytes is a slice of bytes where the information is hex-encoded in the +// environment variable. +type HexBytes []byte + +// EnvDecode implements env.Decoder. +func (b *HexBytes) EnvDecode(val string) error { + var err error + *b, err = hex.DecodeString(val) + return err +} + +// Bytes returns the underlying bytes. +func (b HexBytes) Bytes() []byte { + return []byte(b) +} diff --git a/vendor/github.com/sethvargo/go-envconfig/envconfig.go b/vendor/github.com/sethvargo/go-envconfig/envconfig.go new file mode 100644 index 0000000..58919aa --- /dev/null +++ b/vendor/github.com/sethvargo/go-envconfig/envconfig.go @@ -0,0 +1,877 @@ +// Copyright The envconfig Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package envconfig populates struct fields based on environment variable +// values (or anything that responds to "Lookup"). Structs declare their +// environment dependencies using the "env" tag with the key being the name of +// the environment variable, case sensitive. +// +// type MyStruct struct { +// A string `env:"A"` // resolves A to $A +// B string `env:"B,required"` // resolves B to $B, errors if $B is unset +// C string `env:"C,default=foo"` // resolves C to $C, defaults to "foo" +// +// D string `env:"D,required,default=foo"` // error, cannot be required and default +// E string `env:""` // error, must specify key +// } +// +// All built-in types are supported except Func and Chan. If you need to define +// a custom decoder, implement Decoder: +// +// type MyStruct struct { +// field string +// } +// +// func (v *MyStruct) EnvDecode(val string) error { +// v.field = fmt.Sprintf("PREFIX-%s", val) +// return nil +// } +// +// In the environment, slices are specified as comma-separated values: +// +// export MYVAR="a,b,c,d" // []string{"a", "b", "c", "d"} +// +// In the environment, maps are specified as comma-separated key:value pairs: +// +// export MYVAR="a:b,c:d" // map[string]string{"a":"b", "c":"d"} +// +// For more configuration options and examples, see the documentation. +package envconfig + +import ( + "context" + "encoding" + "encoding/gob" + "encoding/json" + "errors" + "fmt" + "os" + "reflect" + "strconv" + "strings" + "time" + "unicode" +) + +const ( + envTag = "env" + + optDecodeUnset = "decodeunset" + optDefault = "default=" + optDelimiter = "delimiter=" + optNoInit = "noinit" + optOverwrite = "overwrite" + optPrefix = "prefix=" + optRequired = "required" + optSeparator = "separator=" +) + +// internalError is a custom error type for errors returned by envconfig. +type internalError string + +// Error implements error. +func (e internalError) Error() string { + return string(e) +} + +const ( + ErrInvalidEnvvarName = internalError("invalid environment variable name") + ErrInvalidMapItem = internalError("invalid map item") + ErrLookuperNil = internalError("lookuper cannot be nil") + ErrMissingKey = internalError("missing key") + ErrMissingRequired = internalError("missing required value") + ErrNoInitNotPtr = internalError("field must be a pointer to have noinit") + ErrNotPtr = internalError("input must be a pointer") + ErrNotStruct = internalError("input must be a struct") + ErrPrefixNotStruct = internalError("prefix is only valid on struct types") + ErrPrivateField = internalError("cannot parse private fields") + ErrRequiredAndDefault = internalError("field cannot be required and have a default value") + ErrUnknownOption = internalError("unknown option") +) + +// Lookuper is an interface that provides a lookup for a string-based key. +type Lookuper interface { + // Lookup searches for the given key and returns the corresponding string + // value. If a value is found, it returns the value and true. If a value is + // not found, it returns the empty string and false. + Lookup(key string) (string, bool) +} + +// osLookuper looks up environment configuration from the local environment. +type osLookuper struct{} + +// Verify implements interface. +var _ Lookuper = (*osLookuper)(nil) + +func (o *osLookuper) Lookup(key string) (string, bool) { + return os.LookupEnv(key) +} + +// OsLookuper returns a lookuper that uses the environment ([os.LookupEnv]) to +// resolve values. +func OsLookuper() Lookuper { + return new(osLookuper) +} + +type mapLookuper map[string]string + +var _ Lookuper = (*mapLookuper)(nil) + +func (m mapLookuper) Lookup(key string) (string, bool) { + v, ok := m[key] + return v, ok +} + +// MapLookuper looks up environment configuration from a provided map. This is +// useful for testing, especially in parallel, since it does not require you to +// mutate the parent environment (which is stateful). +func MapLookuper(m map[string]string) Lookuper { + return mapLookuper(m) +} + +type multiLookuper struct { + ls []Lookuper +} + +var _ Lookuper = (*multiLookuper)(nil) + +func (m *multiLookuper) Lookup(key string) (string, bool) { + for _, l := range m.ls { + if v, ok := l.Lookup(key); ok { + return v, true + } + } + return "", false +} + +// PrefixLookuper looks up environment configuration using the specified prefix. +// This is useful if you want all your variables to start with a particular +// prefix like "MY_APP_". +func PrefixLookuper(prefix string, l Lookuper) Lookuper { + if typ, ok := l.(*prefixLookuper); ok { + return &prefixLookuper{prefix: typ.prefix + prefix, l: typ.l} + } + return &prefixLookuper{prefix: prefix, l: l} +} + +type prefixLookuper struct { + l Lookuper + prefix string +} + +func (p *prefixLookuper) Lookup(key string) (string, bool) { + return p.l.Lookup(p.Key(key)) +} + +func (p *prefixLookuper) Key(key string) string { + return p.prefix + key +} + +func (p *prefixLookuper) Unwrap() Lookuper { + l := p.l + for v, ok := l.(unwrappableLookuper); ok; { + l = v.Unwrap() + } + return l +} + +// unwrappableLookuper is a lookuper that can return the underlying lookuper. +type unwrappableLookuper interface { + Unwrap() Lookuper +} + +// MultiLookuper wraps a collection of lookupers. It does not combine them, and +// lookups appear in the order in which they are provided to the initializer. +func MultiLookuper(lookupers ...Lookuper) Lookuper { + return &multiLookuper{ls: lookupers} +} + +// keyedLookuper is an extension to the [Lookuper] interface that returns the +// underlying key (used by the [PrefixLookuper] or custom implementations). +type keyedLookuper interface { + Key(key string) string +} + +// Decoder is an interface that custom types/fields can implement to control how +// decoding takes place. For example: +// +// type MyType string +// +// func (mt MyType) EnvDecode(val string) error { +// return "CUSTOM-"+val +// } +type Decoder interface { + EnvDecode(val string) error +} + +// options are internal options for decoding. +type options struct { + Default string + Delimiter string + Prefix string + Separator string + NoInit bool + Overwrite bool + DecodeUnset bool + Required bool +} + +// Config represent inputs to the envconfig decoding. +type Config struct { + // Target is the destination structure to decode. This value is required, and + // it must be a pointer to a struct. + Target any + + // Lookuper is the lookuper implementation to use. If not provided, it + // defaults to the OS Lookuper. + Lookuper Lookuper + + // DefaultDelimiter is the default value to use for the delimiter in maps and + // slices. This can be overridden on a per-field basis, which takes + // precedence. The default value is ",". + DefaultDelimiter string + + // DefaultSeparator is the default value to use for the separator in maps. + // This can be overridden on a per-field basis, which takes precedence. The + // default value is ":". + DefaultSeparator string + + // DefaultNoInit is the default value for skipping initialization of + // unprovided fields. The default value is false (deeply initialize all + // fields and nested structs). + DefaultNoInit bool + + // DefaultOverwrite is the default value for overwriting an existing value set + // on the struct before processing. The default value is false. + DefaultOverwrite bool + + // DefaultDecodeUnset is the default value for running decoders even when no + // value was given for the environment variable. + DefaultDecodeUnset bool + + // DefaultRequired is the default value for marking a field as required. The + // default value is false. + DefaultRequired bool + + // Mutators is an optional list of mutators to apply to lookups. + Mutators []Mutator +} + +// Process decodes the struct using values from environment variables. See +// [ProcessWith] for a more customizable version. +// +// As a special case, if the input for the target is a [*Config], then this +// function will call [ProcessWith] using the provided config, with any mutation +// appended. +func Process(ctx context.Context, i any, mus ...Mutator) error { + if v, ok := i.(*Config); ok { + v.Mutators = append(v.Mutators, mus...) + return ProcessWith(ctx, v) + } + return ProcessWith(ctx, &Config{ + Target: i, + Mutators: mus, + }) +} + +// MustProcess is a helper that calls [Process] and panics if an error is +// encountered. Unlike [Process], the input value is returned, making it ideal +// for anonymous initializations: +// +// var env = envconfig.MustProcess(context.Background(), &struct{ +// Field string `env:"FIELD,required"` +// }) +// +// This is not recommend for production services, but it can be useful for quick +// CLIs and scripts that want to take advantage of envconfig's environment +// parsing at the expense of testability and graceful error handling. +func MustProcess[T any](ctx context.Context, i T, mus ...Mutator) T { + if err := Process(ctx, i, mus...); err != nil { + panic(err) + } + return i +} + +// ProcessWith executes the decoding process using the provided [Config]. +func ProcessWith(ctx context.Context, c *Config) error { + if c == nil { + c = new(Config) + } + + if c.Lookuper == nil { + c.Lookuper = OsLookuper() + } + + // Deep copy the slice and remove any nil mutators. + var mus []Mutator + for _, m := range c.Mutators { + if m != nil { + mus = append(mus, m) + } + } + c.Mutators = mus + + return processWith(ctx, c) +} + +// processWith is a helper that retains configuration from the parent structs. +func processWith(ctx context.Context, c *Config) error { + i := c.Target + + l := c.Lookuper + if l == nil { + return ErrLookuperNil + } + + v := reflect.ValueOf(i) + if v.Kind() != reflect.Ptr { + return ErrNotPtr + } + + e := v.Elem() + if e.Kind() != reflect.Struct { + return ErrNotStruct + } + + t := e.Type() + + structDelimiter := c.DefaultDelimiter + if structDelimiter == "" { + structDelimiter = "," + } + + structNoInit := c.DefaultNoInit + + structSeparator := c.DefaultSeparator + if structSeparator == "" { + structSeparator = ":" + } + + structOverwrite := c.DefaultOverwrite + structDecodeUnset := c.DefaultDecodeUnset + structRequired := c.DefaultRequired + + mutators := c.Mutators + + for i := 0; i < t.NumField(); i++ { + ef := e.Field(i) + tf := t.Field(i) + tag := tf.Tag.Get(envTag) + + if !ef.CanSet() { + if tag != "" { + // There's an "env" tag on a private field, we can't alter it, and it's + // likely a mistake. Return an error so the user can handle. + return fmt.Errorf("%s: %w", tf.Name, ErrPrivateField) + } + + // Otherwise continue to the next field. + continue + } + + // Parse the key and options. + key, opts, err := keyAndOpts(tag) + if err != nil { + return fmt.Errorf("%s: %w", tf.Name, err) + } + + // NoInit is only permitted on pointers. + if opts.NoInit && + ef.Kind() != reflect.Ptr && + ef.Kind() != reflect.Slice && + ef.Kind() != reflect.Map && + ef.Kind() != reflect.UnsafePointer { + return fmt.Errorf("%s: %w", tf.Name, ErrNoInitNotPtr) + } + + // Compute defaults from local tags. + delimiter := structDelimiter + if v := opts.Delimiter; v != "" { + delimiter = v + } + separator := structSeparator + if v := opts.Separator; v != "" { + separator = v + } + + noInit := structNoInit || opts.NoInit + overwrite := structOverwrite || opts.Overwrite + decodeUnset := structDecodeUnset || opts.DecodeUnset + required := structRequired || opts.Required + + isNilStructPtr := false + setNilStruct := func(v reflect.Value) { + origin := e.Field(i) + if isNilStructPtr { + empty := reflect.New(origin.Type().Elem()).Interface() + + // If a struct (after traversal) equals to the empty value, it means + // nothing was changed in any sub-fields. With the noinit opt, we skip + // setting the empty value to the original struct pointer (keep it nil). + if !reflect.DeepEqual(v.Interface(), empty) || !noInit { + origin.Set(v) + } + } + } + + // Initialize pointer structs. + pointerWasSet := false + for ef.Kind() == reflect.Ptr { + if ef.IsNil() { + if ef.Type().Elem().Kind() != reflect.Struct { + // This is a nil pointer to something that isn't a struct, like + // *string. Move along. + break + } + + isNilStructPtr = true + // Use an empty struct of the type so we can traverse. + ef = reflect.New(ef.Type().Elem()).Elem() + } else { + pointerWasSet = true + ef = ef.Elem() + } + } + + // Special case handle structs. This has to come after the value resolution in + // case the struct has a custom decoder. + if ef.Kind() == reflect.Struct { + for ef.CanAddr() { + ef = ef.Addr() + } + + // Lookup the value, ignoring an error if the key isn't defined. This is + // required for nested structs that don't declare their own `env` keys, + // but have internal fields with an `env` defined. + val, found, usedDefault, err := lookup(key, required, opts.Default, l) + if err != nil && !errors.Is(err, ErrMissingKey) { + return fmt.Errorf("%s: %w", tf.Name, err) + } + + if found || usedDefault || decodeUnset { + if ok, err := processAsDecoder(val, ef); ok { + if err != nil { + return err + } + + setNilStruct(ef) + continue + } + } + + plu := l + if opts.Prefix != "" { + plu = PrefixLookuper(opts.Prefix, l) + } + + if err := processWith(ctx, &Config{ + Target: ef.Interface(), + Lookuper: plu, + DefaultDelimiter: delimiter, + DefaultSeparator: separator, + DefaultNoInit: noInit, + DefaultOverwrite: overwrite, + DefaultRequired: required, + Mutators: mutators, + }); err != nil { + return fmt.Errorf("%s: %w", tf.Name, err) + } + + setNilStruct(ef) + continue + } + + // It's invalid to have a prefix on a non-struct field. + if opts.Prefix != "" { + return ErrPrefixNotStruct + } + + // Stop processing if there's no env tag (this comes after nested parsing), + // in case there's an env tag in an embedded struct. + if tag == "" { + continue + } + + // The field already has a non-zero value and overwrite is false, do not + // overwrite. + if (pointerWasSet || !ef.IsZero()) && !overwrite { + continue + } + + val, found, usedDefault, err := lookup(key, required, opts.Default, l) + if err != nil { + return fmt.Errorf("%s: %w", tf.Name, err) + } + + // If the field already has a non-zero value and there was no value directly + // specified, do not overwrite the existing field. We only want to overwrite + // when the envvar was provided directly. + if (pointerWasSet || !ef.IsZero()) && !found { + continue + } + + // Apply any mutators. Mutators are applied after the lookup, but before any + // type conversions. They always resolve to a string (or error), so we don't + // call mutators when the environment variable was not set. + if found || usedDefault { + originalKey := key + resolvedKey := originalKey + if keyer, ok := l.(keyedLookuper); ok { + resolvedKey = keyer.Key(resolvedKey) + } + originalValue := val + stop := false + + for _, mu := range mutators { + val, stop, err = mu.EnvMutate(ctx, originalKey, resolvedKey, originalValue, val) + if err != nil { + return fmt.Errorf("%s: %w", tf.Name, err) + } + if stop { + break + } + } + } + + // Set value. + if err := processField(val, ef, delimiter, separator, noInit); err != nil { + return fmt.Errorf("%s(%q): %w", tf.Name, val, err) + } + } + + return nil +} + +// SplitString splits the given string on the provided rune, unless the rune is +// escaped by the escape character. +func splitString(s, on, esc string) []string { + a := strings.Split(s, on) + + for i := len(a) - 2; i >= 0; i-- { + if strings.HasSuffix(a[i], esc) { + a[i] = a[i][:len(a[i])-len(esc)] + on + a[i+1] + a = append(a[:i+1], a[i+2:]...) + } + } + return a +} + +// keyAndOpts parses the given tag value (e.g. env:"foo,required") and +// returns the key name and options as a list. +func keyAndOpts(tag string) (string, *options, error) { + parts := splitString(tag, ",", "\\") + key, tagOpts := strings.TrimSpace(parts[0]), parts[1:] + + if key != "" && !validateEnvName(key) { + return "", nil, fmt.Errorf("%q: %w ", key, ErrInvalidEnvvarName) + } + + var opts options + +LOOP: + for i, o := range tagOpts { + o = strings.TrimLeftFunc(o, unicode.IsSpace) + search := strings.ToLower(o) + + switch { + case search == optDecodeUnset: + opts.DecodeUnset = true + case search == optOverwrite: + opts.Overwrite = true + case search == optRequired: + opts.Required = true + case search == optNoInit: + opts.NoInit = true + case strings.HasPrefix(search, optPrefix): + opts.Prefix = strings.TrimPrefix(o, optPrefix) + case strings.HasPrefix(search, optDelimiter): + opts.Delimiter = strings.TrimPrefix(o, optDelimiter) + case strings.HasPrefix(search, optSeparator): + opts.Separator = strings.TrimPrefix(o, optSeparator) + case strings.HasPrefix(search, optDefault): + // If a default value was given, assume everything after is the provided + // value, including comma-seprated items. + o = strings.TrimLeft(strings.Join(tagOpts[i:], ","), " ") + opts.Default = strings.TrimPrefix(o, optDefault) + break LOOP + default: + return "", nil, fmt.Errorf("%q: %w", o, ErrUnknownOption) + } + } + + return key, &opts, nil +} + +// lookup looks up the given key using the provided Lookuper and options. The +// first boolean parameter indicates whether the value was found in the +// lookuper. The second boolean parameter indicates whether the default value +// was used. +func lookup(key string, required bool, defaultValue string, l Lookuper) (string, bool, bool, error) { + if key == "" { + // The struct has something like `env:",required"`, which is likely a + // mistake. We could try to infer the envvar from the field name, but that + // feels too magical. + return "", false, false, ErrMissingKey + } + + if required && defaultValue != "" { + // Having a default value on a required value doesn't make sense. + return "", false, false, ErrRequiredAndDefault + } + + // Lookup value. + val, found := l.Lookup(key) + if !found { + if required { + if keyer, ok := l.(keyedLookuper); ok { + key = keyer.Key(key) + } + + return "", false, false, fmt.Errorf("%w: %s", ErrMissingRequired, key) + } + + if defaultValue != "" { + // Expand the default value. This allows for a default value that maps to + // a different environment variable. + val = os.Expand(defaultValue, func(i string) string { + lookuper := l + if v, ok := lookuper.(unwrappableLookuper); ok { + lookuper = v.Unwrap() + } + + s, ok := lookuper.Lookup(i) + if ok { + return s + } + return "" + }) + + return val, false, true, nil + } + } + + return val, found, false, nil +} + +// processAsDecoder processes the given value as a decoder or custom +// unmarshaller. +func processAsDecoder(v string, ef reflect.Value) (bool, error) { + // Keep a running error. It's possible that a property might implement + // multiple decoders, and we don't know *which* decoder will succeed. If we + // get through all of them, we'll return the most recent error. + var imp bool + var err error + + // Resolve any pointers. + for ef.CanAddr() { + ef = ef.Addr() + } + + if ef.CanInterface() { + iface := ef.Interface() + + // If a developer chooses to implement the Decoder interface on a type, + // never attempt to use other decoders in case of failure. EnvDecode's + // decoding logic is "the right one", and the error returned (if any) + // is the most specific we can get. + if dec, ok := iface.(Decoder); ok { + imp = true + err = dec.EnvDecode(v) + return imp, err + } + + if tu, ok := iface.(encoding.TextUnmarshaler); ok { + imp = true + if err = tu.UnmarshalText([]byte(v)); err == nil { + return imp, nil + } + } + + if tu, ok := iface.(json.Unmarshaler); ok { + imp = true + if err = tu.UnmarshalJSON([]byte(v)); err == nil { + return imp, nil + } + } + + if tu, ok := iface.(encoding.BinaryUnmarshaler); ok { + imp = true + if err = tu.UnmarshalBinary([]byte(v)); err == nil { + return imp, nil + } + } + + if tu, ok := iface.(gob.GobDecoder); ok { + imp = true + if err = tu.GobDecode([]byte(v)); err == nil { + return imp, nil + } + } + } + + return imp, err +} + +func processField(v string, ef reflect.Value, delimiter, separator string, noInit bool) error { + // If the input value is empty and initialization is skipped, do nothing. + if v == "" && noInit { + return nil + } + + // Handle pointers and uninitialized pointers. + for ef.Type().Kind() == reflect.Ptr { + if ef.IsNil() { + ef.Set(reflect.New(ef.Type().Elem())) + } + ef = ef.Elem() + } + + tf := ef.Type() + tk := tf.Kind() + + // Handle existing decoders. + if ok, err := processAsDecoder(v, ef); ok { + return err + } + + // We don't check if the value is empty earlier, because the user might want + // to define a custom decoder and treat the empty variable as a special case. + // However, if we got this far, none of the remaining parsers will succeed, so + // bail out now. + if v == "" { + return nil + } + + switch tk { + case reflect.Bool: + b, err := strconv.ParseBool(v) + if err != nil { + return err + } + ef.SetBool(b) + case reflect.Float32, reflect.Float64: + f, err := strconv.ParseFloat(v, tf.Bits()) + if err != nil { + return err + } + ef.SetFloat(f) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32: + i, err := strconv.ParseInt(v, 0, tf.Bits()) + if err != nil { + return err + } + ef.SetInt(i) + case reflect.Int64: + // Special case time.Duration values. + if tf.PkgPath() == "time" && tf.Name() == "Duration" { + d, err := time.ParseDuration(v) + if err != nil { + return err + } + ef.SetInt(int64(d)) + } else { + i, err := strconv.ParseInt(v, 0, tf.Bits()) + if err != nil { + return err + } + ef.SetInt(i) + } + case reflect.String: + ef.SetString(v) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + i, err := strconv.ParseUint(v, 0, tf.Bits()) + if err != nil { + return err + } + ef.SetUint(i) + + case reflect.Interface: + return fmt.Errorf("cannot decode into interfaces") + + // Maps + case reflect.Map: + vals := strings.Split(v, delimiter) + mp := reflect.MakeMapWithSize(tf, len(vals)) + for _, val := range vals { + pair := strings.SplitN(val, separator, 2) + if len(pair) < 2 { + return fmt.Errorf("%s: %w", val, ErrInvalidMapItem) + } + mKey, mVal := strings.TrimSpace(pair[0]), strings.TrimSpace(pair[1]) + + k := reflect.New(tf.Key()).Elem() + if err := processField(mKey, k, delimiter, separator, noInit); err != nil { + return fmt.Errorf("%s: %w", mKey, err) + } + + v := reflect.New(tf.Elem()).Elem() + if err := processField(mVal, v, delimiter, separator, noInit); err != nil { + return fmt.Errorf("%s: %w", mVal, err) + } + + mp.SetMapIndex(k, v) + } + ef.Set(mp) + + // Slices + case reflect.Slice: + // Special case: []byte + if tf.Elem().Kind() == reflect.Uint8 { + ef.Set(reflect.ValueOf([]byte(v))) + } else { + vals := strings.Split(v, delimiter) + s := reflect.MakeSlice(tf, len(vals), len(vals)) + for i, val := range vals { + val = strings.TrimSpace(val) + if err := processField(val, s.Index(i), delimiter, separator, noInit); err != nil { + return fmt.Errorf("%s: %w", val, err) + } + } + ef.Set(s) + } + } + + return nil +} + +// validateEnvName validates the given string conforms to being a valid +// environment variable. +// +// Per IEEE Std 1003.1-2001 environment variables consist solely of uppercase +// letters, digits, and _, and do not begin with a digit. +func validateEnvName(s string) bool { + if s == "" { + return false + } + + for i, r := range s { + if (i == 0 && !isLetter(r) && r != '_') || (!isLetter(r) && !isNumber(r) && r != '_') { + return false + } + } + + return true +} + +// isLetter returns true if the given rune is a letter between a-z,A-Z. This is +// different than unicode.IsLetter which includes all L character cases. +func isLetter(r rune) bool { + return (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') +} + +// isNumber returns true if the given run is a number between 0-9. This is +// different than unicode.IsNumber in that it only allows 0-9. +func isNumber(r rune) bool { + return r >= '0' && r <= '9' +} diff --git a/vendor/github.com/sethvargo/go-envconfig/mutator.go b/vendor/github.com/sethvargo/go-envconfig/mutator.go new file mode 100644 index 0000000..ccfadba --- /dev/null +++ b/vendor/github.com/sethvargo/go-envconfig/mutator.go @@ -0,0 +1,76 @@ +// Copyright The envconfig Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package envconfig + +import "context" + +// Mutator is the interface for a mutator function. Mutators act like middleware +// and alter values for subsequent processing. This is useful if you want to +// mutate the environment variable value before it's converted to the proper +// type. +// +// Mutators are only called on defined values (or when decodeunset is true). +type Mutator interface { + // EnvMutate is called to alter the environment variable value. + // + // - `originalKey` is the unmodified environment variable name as it was defined + // on the struct. + // + // - `resolvedKey` is the fully-resolved environment variable name, which may + // include prefixes or modifications from processing. When there are + // no modifications, this will be equivalent to `originalKey`. + // + // - `originalValue` is the unmodified environment variable's value before any + // mutations were run. + // + // - `currentValue` is the currently-resolved value, which may have been + // modified by previous mutators and may be modified in the future by + // subsequent mutators in the stack. + // + // The function returns (in order): + // + // - The new value to use in both future mutations and final processing. + // + // - A boolean which indicates whether future mutations in the stack should be + // applied. + // + // - Any errors that occurred. + // + EnvMutate(ctx context.Context, originalKey, resolvedKey, originalValue, currentValue string) (newValue string, stop bool, err error) +} + +var _ Mutator = (MutatorFunc)(nil) + +// MutatorFunc implements the [Mutator] and provides a quick way to create an +// anonymous function. +type MutatorFunc func(ctx context.Context, originalKey, resolvedKey, originalValue, currentValue string) (newValue string, stop bool, err error) + +// EnvMutate implements [Mutator]. +func (m MutatorFunc) EnvMutate(ctx context.Context, originalKey, resolvedKey, originalValue, currentValue string) (newValue string, stop bool, err error) { + return m(ctx, originalKey, resolvedKey, originalValue, currentValue) +} + +// LegacyMutatorFunc is a helper that eases the transition from the previous +// MutatorFunc signature. It wraps the previous-style mutator function and +// returns a new one. Since the former mutator function had less data, this is +// inherently lossy. +// +// Deprecated: Use [MutatorFunc] instead. +func LegacyMutatorFunc(fn func(ctx context.Context, key, value string) (string, error)) MutatorFunc { + return func(ctx context.Context, originalKey, resolvedKey, originalValue, currentValue string) (newValue string, stop bool, err error) { + v, err := fn(ctx, originalKey, currentValue) + return v, true, err + } +} diff --git a/vendor/modules.txt b/vendor/modules.txt index bded61b..e118c85 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -139,6 +139,10 @@ github.com/remyoudompheng/bigfft github.com/rs/zerolog github.com/rs/zerolog/internal/cbor github.com/rs/zerolog/internal/json +github.com/rs/zerolog/log +# github.com/sethvargo/go-envconfig v1.1.1 +## explicit; go 1.20 +github.com/sethvargo/go-envconfig # github.com/stretchr/testify v1.9.0 ## explicit; go 1.17 github.com/stretchr/testify/assert