-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 0057c95
Showing
9 changed files
with
275 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
MIT License | ||
|
||
Copyright (c) 2024 Aran Wilkinson. | ||
|
||
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
# yamlcfg | ||
|
||
yamlcfg is a wrapper around the [gopkg.in/yaml.v3](https://gopkg.in/yaml.v3) library and provides a convient way to configure Golang applications with YAML and environment variables. | ||
|
||
The library can also automatically call `Validate` functions if present on the given config struct. | ||
|
||
## Installation | ||
|
||
To install, run: | ||
|
||
``` | ||
go get github.com/aranw/yamlcfg | ||
``` | ||
|
||
## License | ||
|
||
The yamlcfg package is licensed under the MIT. Please see the LICENSE file for details. | ||
|
||
## Example | ||
|
||
```go | ||
package main | ||
|
||
import ( | ||
"log/slog" | ||
|
||
"github.com/aranw/yamlcfg" | ||
) | ||
|
||
type Config struct { | ||
LogLevel string `yaml:"log_level"` | ||
} | ||
|
||
func (c *Config) Validate() error { | ||
validLevels := [...]string{"debug", "info", "error"} | ||
|
||
validLevel := slices.ContainsFunc(validLevels[:], func(s string) bool { | ||
return strings.EqualFold(s, c.Log.Level) | ||
}) | ||
|
||
if c.Log.Level == "" { | ||
c.Log.Level = "info" | ||
} else if !validLevel { | ||
return errors.New("invalid log level provided") | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func main() { | ||
cfg, err := yamlcfg.Load[Config]("config.yaml") | ||
if err != nil { | ||
slog.Error("loading yaml config", "err", err) | ||
return | ||
} | ||
|
||
_ = cfg.LogLevel | ||
} | ||
|
||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
package yamlcfg | ||
|
||
import ( | ||
"bytes" | ||
"fmt" | ||
"os" | ||
|
||
"gopkg.in/yaml.v3" | ||
) | ||
|
||
// Load takes the given path and attempts to read and unmarshal the config | ||
// The given config will also be validated if it has a Validate function on it | ||
func Load[T any](path string) (*T, error) { | ||
var cfg *T | ||
|
||
b, err := os.ReadFile(path) | ||
if err != nil { | ||
return nil, fmt.Errorf("reading config file: %w", err) | ||
} else if err := UnmarshalConfig(&cfg, b); err != nil { | ||
return nil, fmt.Errorf("unmarshalling config: %w", err) | ||
} | ||
if cfg, ok := interface{}(cfg).(interface { | ||
Validate() error | ||
}); ok { | ||
if err := cfg.Validate(); err != nil { | ||
return nil, fmt.Errorf("validating config: %w", err) | ||
} | ||
} | ||
|
||
return cfg, nil | ||
} | ||
|
||
// UnmarshalConfig takes the provided yaml data and unmarshals it | ||
// into the provided config struct. It will return an error if the | ||
// decoding fails or if the yaml data is not in the expected format. | ||
// It will also expand any environment variables in the yaml data | ||
func UnmarshalConfig[T any](cfg *T, data []byte) error { | ||
// expand any $VAR values in the config from environment variables | ||
data = []byte(os.ExpandEnv(string(data))) | ||
|
||
dec := yaml.NewDecoder(bytes.NewReader(data)) | ||
dec.KnownFields(true) | ||
return dec.Decode(&cfg) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
package yamlcfg | ||
|
||
import ( | ||
"fmt" | ||
"testing" | ||
|
||
"github.com/go-quicktest/qt" | ||
) | ||
|
||
type TestStruct struct { | ||
SomeValue string `yaml:"some_value"` | ||
} | ||
|
||
func (t *TestStruct) Validate() error { | ||
return nil | ||
} | ||
|
||
type TestStructWithFailingValidation struct { | ||
SomeValue string `yaml:"some_value"` | ||
} | ||
|
||
func (t *TestStructWithFailingValidation) Validate() error { | ||
return fmt.Errorf("this is going to fail") | ||
} | ||
|
||
func TestLoad(t *testing.T) { | ||
|
||
t.Run("successfully load and unmarshals config", func(t *testing.T) { | ||
cfg, err := Load[TestStruct]("testdata/test1.yaml") | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
|
||
qt.Assert(t, qt.Equals(cfg.SomeValue, "this is for testing purposes")) | ||
}) | ||
|
||
t.Run("fails to read unknown file", func(t *testing.T) { | ||
cfg, err := Load[TestStruct]("testdata/this_file_does_not_exist.yaml") | ||
qt.Assert(t, qt.IsNotNil(err)) | ||
qt.Assert(t, qt.ErrorMatches(err, "reading config file: .*")) | ||
qt.Assert(t, qt.IsNil(cfg)) | ||
}) | ||
|
||
t.Run("fails to read wrong file type", func(t *testing.T) { | ||
cfg, err := Load[TestStruct]("testdata/gopher.png") | ||
qt.Assert(t, qt.IsNotNil(err)) | ||
qt.Assert(t, qt.ErrorMatches(err, "unmarshalling config: yaml: .*")) | ||
qt.Assert(t, qt.IsNil(cfg)) | ||
}) | ||
|
||
t.Run("fails to validate config struct", func(t *testing.T) { | ||
cfg, err := Load[TestStructWithFailingValidation]("testdata/test1.yaml") | ||
qt.Assert(t, qt.IsNotNil(err)) | ||
qt.Assert(t, qt.ErrorMatches(err, "validating config: this is going to fail")) | ||
qt.Assert(t, qt.IsNil(cfg)) | ||
}) | ||
} | ||
|
||
func TestUnmarshalConfig(t *testing.T) { | ||
t.Run("valid config", func(t *testing.T) { | ||
b := []byte(`name: test`) | ||
cfg := struct { | ||
Name string `yaml:"name"` | ||
}{} | ||
if err := UnmarshalConfig(&cfg, b); err != nil { | ||
t.Fatal(err) | ||
} | ||
|
||
qt.Assert(t, qt.Equals(cfg.Name, "test")) | ||
}) | ||
|
||
t.Run("valid config with env vars", func(t *testing.T) { | ||
t.Setenv("NAME", "testing") | ||
|
||
b := []byte(`name: $NAME`) | ||
cfg := struct { | ||
Name string `yaml:"name"` | ||
}{} | ||
if err := UnmarshalConfig(&cfg, b); err != nil { | ||
t.Fatal(err) | ||
} | ||
|
||
qt.Assert(t, qt.Equals(cfg.Name, "testing")) | ||
}) | ||
|
||
t.Run("invalid config", func(t *testing.T) { | ||
b := []byte(`asdasdasdad******`) | ||
|
||
cfg := struct { | ||
Name string `yaml:"name"` | ||
}{} | ||
if err := UnmarshalConfig(&cfg, b); err == nil { | ||
t.Fatal("expected error") | ||
} | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
package yamlcfg_test | ||
|
||
import ( | ||
"log/slog" | ||
|
||
"github.com/aranw/yamlcfg" | ||
) | ||
|
||
type Config struct { | ||
LogLevel string `yaml:"log_level"` | ||
} | ||
|
||
func ExampleLoad() { | ||
cfg, err := yamlcfg.Load[Config]("config.yaml") | ||
if err != nil { | ||
slog.Error("loading yaml config", "err", err) | ||
return | ||
} | ||
|
||
_ = cfg.LogLevel | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
module github.com/aranw/yamlcfg | ||
|
||
go 1.22.3 | ||
|
||
require ( | ||
github.com/go-quicktest/qt v1.101.0 | ||
gopkg.in/yaml.v3 v3.0.1 | ||
) | ||
|
||
require ( | ||
github.com/google/go-cmp v0.5.9 // indirect | ||
github.com/kr/pretty v0.3.1 // indirect | ||
github.com/kr/text v0.2.0 // indirect | ||
github.com/rogpeppe/go-internal v1.11.0 // indirect | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= | ||
github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= | ||
github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= | ||
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/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= | ||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= | ||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= | ||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= | ||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= | ||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= | ||
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= | ||
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= | ||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= | ||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | ||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= | ||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
some_value: "this is for testing purposes" |