├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── GoEnvConfig.go ├── LICENSE.md ├── README.md ├── go.mod ├── go.sum └── test ├── GoEnvConfig_test.go └── deps └── deps.go /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | vendor/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.11.x 5 | - tip 6 | 7 | os: 8 | - linux 9 | - osx 10 | 11 | dist: trusty 12 | 13 | sudo: false 14 | install: true 15 | 16 | before_install: 17 | - go get -t -v ./... 18 | 19 | env: 20 | -GO111MODULE=on 21 | 22 | script: 23 | - go build 24 | - go test ./... -coverprofile=coverage.txt -covermode=atomic -coverpkg=github.com/j7mbo/goenvconfig 25 | 26 | after_success: 27 | - bash <(curl -s https://codecov.io/bash) -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How can I contribute? 2 | 3 | Thanks for considering contributing to GoEnvConfig and the greater community! 4 | 5 | Please see the following list on how to contribute: 6 | 7 | - Create a new issue describing the bug / enhancement 8 | - Fork the repository, push your code, add tests, and create a pull request 9 | - After the code has been reviewed and agreed on in the issue: 10 | - Squash the code into one commit 11 | - Ensure the commit message follows the same format as other commits 12 | 13 | Repeated contributors may be added as Contributors to the Github Repo. -------------------------------------------------------------------------------- /GoEnvConfig.go: -------------------------------------------------------------------------------- 1 | /* Package goenvconfig provides immutability for configuration automatically loaded from environment variables. */ 2 | package goenvconfig 3 | 4 | import ( 5 | "errors" 6 | "os" 7 | "reflect" 8 | "strconv" 9 | "unsafe" 10 | ) 11 | 12 | const ( 13 | envKey = "env" 14 | envDefaultKey = "default" 15 | ) 16 | 17 | /* GoEnvParser represents an object capable of parsing environment variables into a struct, given specific tags. */ 18 | type GoEnvParser interface { 19 | Parse(object interface{}) error 20 | } 21 | 22 | type goEnvParser struct{} 23 | 24 | /* NewGoEnvParser returns a new GoEnvParser. */ 25 | func NewGoEnvParser() GoEnvParser { 26 | return &goEnvParser{} 27 | } 28 | 29 | /* Parse accepts a struct pointer and populates private properties according to "env" and "default" tag keys. */ 30 | func (*goEnvParser) Parse(object interface{}) error { 31 | if reflect.TypeOf(object).Kind() != reflect.Ptr { 32 | return errors.New("objects passed to env.Parse() must be of kind pointer") 33 | } 34 | 35 | addressableCopy := createAddressableCopy(object) 36 | 37 | for i := 0; i < addressableCopy.NumField(); i++ { 38 | fieldRef := addressableCopy.Field(i) 39 | fieldRef = reflect.NewAt(fieldRef.Type(), unsafe.Pointer(fieldRef.UnsafeAddr())).Elem() 40 | 41 | newValue := getValueForTag(reflect.TypeOf(object).Elem(), i) 42 | 43 | switch fieldRef.Type().Kind() { 44 | case reflect.Int: 45 | if newInt, err := strconv.ParseInt(newValue, 10, 32); err == nil { 46 | fieldRef.SetInt(newInt) 47 | } 48 | case reflect.String: 49 | fieldRef.SetString(newValue) 50 | } 51 | } 52 | 53 | object = addressableCopy.Interface() 54 | 55 | return nil 56 | } 57 | 58 | func createAddressableCopy(object interface{}) reflect.Value { 59 | originalValue := reflect.ValueOf(object) 60 | objectCopy := reflect.New(originalValue.Type()).Elem() 61 | objectCopy.Set(originalValue) 62 | 63 | if originalValue.Type().Kind() == reflect.Ptr { 64 | objectCopy = objectCopy.Elem() 65 | } 66 | 67 | return objectCopy 68 | } 69 | 70 | func getValueForTag(addressableCopy reflect.Type, fieldNum int) string { 71 | if envKey, ok := addressableCopy.Field(fieldNum).Tag.Lookup(envKey); ok { 72 | if envVar, exists := os.LookupEnv(envKey); exists { 73 | return envVar 74 | } 75 | } 76 | 77 | if envDefaultValue, ok := addressableCopy.Field(fieldNum).Tag.Lookup(envDefaultKey); ok { 78 | return envDefaultValue 79 | } 80 | 81 | return "" 82 | } 83 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019-2020 James Mallison 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a 6 | copy of this software and associated documentation files (the "Software"), 7 | to deal in the Software without restriction, including without limitation 8 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | and/or sell copies of the Software, and to permit persons to whom the 10 | Software is furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included 13 | in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 21 | IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GoEnvConfig 2 | *Immutable configuration loaded from environment variables.* 3 | 4 | [![Build Status](https://travis-ci.com/J7mbo/GoEnvConfig.svg?branch=master)](https://travis-ci.com/J7mbo/GoEnvConfig) 5 | [![codecov](https://img.shields.io/codecov/c/github/j7mbo/GoEnvConfig.svg?branch=master)](https://codecov.io/gh/J7mbo/GoEnvConfig) 6 | [![GoDoc](https://godoc.org/github.com/J7mbo/GoEnvConfig?status.svg)](https://godoc.org/github.com/J7mbo/GoEnvConfig) 7 | [![Version](https://img.shields.io/github/tag/j7mbo/GoEnvConfig.svg?label=version)](github.com/j7mbo/GoEnvConfig) 8 | [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE.md) 9 | 10 | Automatically load environmental variables into structs with private properties. 11 | 12 | ## Installation 13 | 14 | ```bash 15 | go get github.com/j7mbo/goenvconfig 16 | ``` 17 | 18 | ## Example 19 | 20 | Bash: 21 | ```bash 22 | export PORT=1337 23 | ``` 24 | 25 | Go: 26 | ```go 27 | package main 28 | 29 | import ( 30 | "github.com/j7mbo/goenvconfig" 31 | "fmt" 32 | ) 33 | 34 | type Config struct { 35 | host string `env:"HOME" default:"localhost"` 36 | port int `env:"PORT" default:"8080"` 37 | } 38 | 39 | func (c *Config) GetHost() string { return c.host } 40 | func (c *Config) GetPort() int { return c.port } 41 | 42 | func main() { 43 | config := Config{} 44 | parser := goenvconfig.NewGoEnvParser() 45 | 46 | if err := parser.Parse(&config) { 47 | panic(err) 48 | } 49 | 50 | fmt.Println(config.GetHost()) // localhost 51 | fmt.Println(config.GetPort()) // 1337 52 | } 53 | ``` 54 | 55 | ## Supported Types 56 | 57 | For now the following simple types are supported: 58 | 59 | - int 60 | - string 61 | 62 | ## Why 63 | 64 | Just because you want to automatically load environment variables into configuration structs does not mean you should 65 | expose modifiable exported properties on your configuration object. Instead the struct should be immutable with 66 | properties only accessible via getters. 67 | 68 | You can either idiomatically create a factory method thereby greatly reducing the simplicity of an automated solution, 69 | or you do something you're "not supposed to" and use a library that utilises the `reflect` and `unsafe` packages. -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/j7mbo/goenvconfig 2 | 3 | require github.com/stretchr/testify v1.3.0 4 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 4 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 5 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 6 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 7 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 8 | -------------------------------------------------------------------------------- /test/GoEnvConfig_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "github.com/j7mbo/goenvconfig" 5 | "github.com/j7mbo/goenvconfig/test/deps" 6 | "github.com/stretchr/testify/suite" 7 | "os" 8 | "testing" 9 | ) 10 | 11 | type InjectorTestSuite struct { 12 | suite.Suite 13 | parser goenvconfig.GoEnvParser 14 | } 15 | 16 | func TestInjectorTestSuite(t *testing.T) { 17 | tests := new(InjectorTestSuite) 18 | 19 | suite.Run(t, tests) 20 | } 21 | 22 | func (s *InjectorTestSuite) SetupSuite() { 23 | s.parser = goenvconfig.NewGoEnvParser() 24 | } 25 | 26 | func (s *InjectorTestSuite) TestCanSetStringVarFromEnv() { 27 | dep := deps.Dep{} 28 | 29 | _ = os.Setenv("s", "TEST") 30 | 31 | _ = s.parser.Parse(&dep) 32 | 33 | s.Assert().Equal(dep.GetString(), "TEST") 34 | } 35 | 36 | func (s *InjectorTestSuite) TestCanSetIntVarFromEnv() { 37 | dep := deps.Dep{} 38 | 39 | _ = os.Setenv("i", "42") 40 | 41 | _ = s.parser.Parse(&dep) 42 | 43 | s.Assert().Equal(dep.GetInt(), 42) 44 | } 45 | 46 | func (s *InjectorTestSuite) TestDefaultUsedWhenNoEnvVarFound() { 47 | dep := deps.Dep{} 48 | 49 | _ = s.parser.Parse(&dep) 50 | 51 | s.Assert().Equal(dep.GetInt(), 1337) 52 | } 53 | 54 | func (s *InjectorTestSuite) TestPassingInAStructValueReturnsError() { 55 | dep := deps.Dep{} 56 | 57 | err := s.parser.Parse(dep) 58 | 59 | s.Assert().Error(err) 60 | } 61 | 62 | func (s *InjectorTestSuite) TestCanStillDoPublicPropertiesAlso() { 63 | dep := deps.Dep{} 64 | 65 | _ = os.Setenv("ip", "1338") 66 | 67 | _ = s.parser.Parse(&dep) 68 | 69 | s.Assert().Equal(dep.GetPublicInt(), 1338) 70 | } 71 | 72 | func (s *InjectorTestSuite) TearDownTest() { 73 | _ = os.Unsetenv("t") 74 | _ = os.Unsetenv("i") 75 | _ = os.Unsetenv("ip") 76 | } 77 | -------------------------------------------------------------------------------- /test/deps/deps.go: -------------------------------------------------------------------------------- 1 | package deps 2 | 3 | /* These exist here because they should be in a separate package to show private properties being modified. */ 4 | 5 | type Dep struct { 6 | i int `env:"i" default:"1337"` 7 | s string `env:"s"` 8 | ip int `env:"ip"` 9 | } 10 | 11 | func (d *Dep) GetInt() int { return d.i } 12 | func (d *Dep) GetString() string { return d.s } 13 | func (d *Dep) GetPublicInt() int { return d.ip } 14 | --------------------------------------------------------------------------------