Skip to content

Commit

Permalink
feat: add support for toml and .env file formats in ReadFile function
Browse files Browse the repository at this point in the history
  • Loading branch information
dsbasko committed Feb 27, 2024
1 parent 7c73118 commit 45a8c20
Show file tree
Hide file tree
Showing 6 changed files with 134 additions and 2 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ Thumbs.db
.env
coverage.out
bin
cmd
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
This project is a Go library for reading configuration data from various sources such as environment variables, command-line flags, and configuration files. The library provides a unified interface for reading configuration data, making it easier to manage and maintain your application's configuration.

## Attention
The library uses the [env](github.com/caarlos0/env) and [flaggy](github.com/integrii/flaggy) codebase to work with environment variables and flags. This is a temporary solution, maybe I’ll write my own implementation later. Thanks to the authors of these libraries for the work done!
The library uses the [env](github.com/caarlos0/env), [toml](github.com/BurntSushi/toml), [godotenv](github.com/joho/godotenv) and [flaggy](github.com/integrii/flaggy) codebase to work with environment variables and flags. This is a temporary solution, maybe I’ll write my own implementation later. Thanks to the authors of these libraries for the work done!

### Installation
To install the library, use the go get command:
Expand All @@ -17,7 +17,7 @@ The library provides several functions for reading configuration data:
- `MustReadEnv(cfg any)`: Similar to `ReadEnv` but panics if the reading process fails.
- `ReadFlag(cfg any) error`: Reads command-line flags into the provided `cfg` structure. Each field in the `cfg` structure represents a command-line flag.
- `MustReadFlag(cfg any)`: Similar to `ReadFlag` but panics if the reading process fails.
- `ReadFile(path string, cfg any) error`: Reads configuration from a file into the provided `cfg` structure. The path parameter is the path to the configuration file. Each field in the `cfg` structure represents a configuration option. Supported file formats include JSON and YAML.
- `ReadFile(path string, cfg any) error`: Reads configuration from a file into the provided `cfg` structure. The path parameter is the path to the configuration file. Each field in the `cfg` structure represents a configuration option. Supported file formats include JSON, YAML, TOML and .env.
- `MustReadFile(path string, cfg any)`: Similar to `ReadFile` but panics if the reading process fails.

Here is an example of how to use the library:
Expand Down
5 changes: 5 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,8 @@ require (
github.com/caarlos0/env/v10 v10.0.0
github.com/integrii/flaggy v1.5.2
)

require (
github.com/BurntSushi/toml v1.3.2 // indirect
github.com/joho/godotenv v1.5.1 // indirect
)
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/caarlos0/env/v10 v10.0.0 h1:yIHUBZGsyqCnpTkbjk8asUlx6RFhhEs+h7TOBdgdzXA=
github.com/caarlos0/env/v10 v10.0.0/go.mod h1:ZfulV76NvVPw3tm591U4SwL3Xx9ldzBP9aGxzeN7G18=
github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/integrii/flaggy v1.5.2 h1:bWV20MQEngo4hWhno3i5Z9ISPxLPKj9NOGNwTWb/8IQ=
github.com/integrii/flaggy v1.5.2/go.mod h1:dO13u7SYuhk910nayCJ+s1DeAAGC1THCMj1uSFmwtQ8=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
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=
Expand Down
36 changes: 36 additions & 0 deletions internal/file/read.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ import (
"path/filepath"
"strings"

"github.com/dsbasko/go-cfg/pkg/structs"

"github.com/BurntSushi/toml"
"github.com/joho/godotenv"
"gopkg.in/yaml.v3"
)

Expand All @@ -31,6 +35,14 @@ func Read(path string, cfg any) error {
if err = parseYAML(file, cfg); err != nil {
return fmt.Errorf("failed to parse yaml: %w", err)
}
case ".toml":
if err = parseTOML(file, cfg); err != nil {
return fmt.Errorf("failed to parse toml: %w", err)
}
case ".env":
if err = parseENV(file, cfg); err != nil {
return fmt.Errorf("failed to parse env: %w", err)
}
}

return nil
Expand All @@ -49,3 +61,27 @@ func parseJSON(r io.Reader, cfg any) error {
func parseYAML(r io.Reader, cfg any) error {
return yaml.NewDecoder(r).Decode(cfg)
}

// parseTOML is a helper function used by Read to parse the TOML content of the file.
// It takes an io.Reader and a pointer to a struct where each field represents a configuration option.
// The function returns an error if the parsing process fails.
func parseTOML(r io.Reader, cfg any) error {
_, err := toml.NewDecoder(r).Decode(cfg)
return err
}

// parseENV parses the environment variables from the given io.Reader and populates the provided configuration struct.
// It uses the godotenv package to parse the data.
// If an error occurs during parsing or populating the struct, it returns the error.
func parseENV(r io.Reader, cfg any) error {
data, err := godotenv.Parse(r)
if err != nil {
return err
}

if err := structs.FromMap(data, cfg); err != nil {
return err
}

return nil
}
86 changes: 86 additions & 0 deletions pkg/structs/from-map.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package structs

import (
"fmt"
"reflect"
"strconv"
)

// FromMap maps the values from the given data map to the corresponding fields in the cfg struct.
// The cfg parameter must be a pointer to a struct. The mapping is done based on the "env" tag of each field.
// If a field has a non-empty "env" tag, its value will be set to the corresponding value from the data map.
// Returns an error if the cfg parameter is not a pointer to a struct.
func FromMap(inMap map[string]string, inStruct any) error { // nolint: funlen,gocyclo,nolintlint
valueOf := reflect.ValueOf(inStruct)

if valueOf.Kind() != reflect.Ptr || valueOf.IsNil() {
return fmt.Errorf("must be a non-nil pointer to a struct")
}

valueOf = valueOf.Elem()
if valueOf.Kind() != reflect.Struct {
return fmt.Errorf("must be a pointer to a struct")
}

for i := 0; i < valueOf.NumField(); i++ {
field := valueOf.Type().Field(i)

// If the field is a struct, call FromMap recursively
// to map the values from the data map to the fields of the struct.
if field.Type.Kind() == reflect.Struct {
if err := FromMap(inMap, valueOf.Field(i).Addr().Interface()); err != nil {
return err
}
continue
}

tag := field.Tag.Get("env")
if tag != "" {
switch field.Type.Kind() {
case reflect.String:
valueOf.Field(i).SetString(inMap[tag])
case reflect.Int:
valInt, _ := strconv.ParseInt(inMap[tag], 10, 0)
valueOf.Field(i).SetInt(valInt)
case reflect.Int8:
valInt, _ := strconv.ParseInt(inMap[tag], 10, 8)
valueOf.Field(i).SetInt(valInt)
case reflect.Int16:
valInt, _ := strconv.ParseInt(inMap[tag], 10, 16)
valueOf.Field(i).SetInt(valInt)
case reflect.Int32:
valInt, _ := strconv.ParseInt(inMap[tag], 10, 32)
valueOf.Field(i).SetInt(valInt)
case reflect.Int64:
valInt, _ := strconv.ParseInt(inMap[tag], 10, 64)
valueOf.Field(i).SetInt(valInt)
case reflect.Uint:
valUint, _ := strconv.ParseUint(inMap[tag], 10, 0)
valueOf.Field(i).SetUint(valUint)
case reflect.Uint8:
valUint, _ := strconv.ParseUint(inMap[tag], 10, 8)
valueOf.Field(i).SetUint(valUint)
case reflect.Uint16:
valUint, _ := strconv.ParseUint(inMap[tag], 10, 16)
valueOf.Field(i).SetUint(valUint)
case reflect.Uint32:
valUint, _ := strconv.ParseUint(inMap[tag], 10, 32)
valueOf.Field(i).SetUint(valUint)
case reflect.Uint64:
valUint, _ := strconv.ParseUint(inMap[tag], 10, 64)
valueOf.Field(i).SetUint(valUint)
case reflect.Float64:
valFloat, _ := strconv.ParseFloat(inMap[tag], 64)
valueOf.Field(i).SetFloat(valFloat)
case reflect.Float32:
valFloat, _ := strconv.ParseFloat(inMap[tag], 32)
valueOf.Field(i).SetFloat(valFloat)
case reflect.Bool:
valBool, _ := strconv.ParseBool(inMap[tag])
valueOf.Field(i).SetBool(valBool)
}
}
}

return nil
}

0 comments on commit 45a8c20

Please sign in to comment.