Skip to content

Commit 84cf2a6

Browse files
App config handling with JSON/YAML de/encoder and builder (#63)
* Internal filesystem utility functions The `internal/util` package provides application-wide utility functions. The initial implementations are scoped for filesystem actions like checking if a file exists at a given path (and is not a directory) as well as ensuring absolute paths and necenssary write permissions. Epic GH-33 Depends on GH-58 GH-60 <---------------------------------------------------------------------> * App config handling with JSON/YAML de/encoder and builder The `pkg/config` package provides the application-wide configuration struct and constants. The subpackage `pkg/config/encoder` provides the `Encoder` interface that is implemented in the `pkg/config/encoder/json` and `pkg/config/encoder/yaml` packages to handle the de/encoding of JSON and YAML data. To process the data from files the `File` struct of the `pkg/config/source/file` package represents an abstractation to validate and load data from files on the local filesystem. To simplify the usage and handling of configurations the `pkg/config/builder` package provides the `Load` and `Merge` functions by using the builder design pattern in order to allow to chain both functions. Epic GH-33 Depends on GH-58 Resolves GH-60
1 parent 44b96fb commit 84cf2a6

File tree

12 files changed

+464
-0
lines changed

12 files changed

+464
-0
lines changed

go.mod

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,9 @@ go 1.12
44

55
require (
66
github.com/fatih/color v1.7.0
7+
github.com/ghodss/yaml v1.0.0
8+
github.com/imdario/mergo v0.3.7
79
github.com/mattn/go-colorable v0.1.2 // indirect
10+
github.com/mitchellh/go-homedir v1.1.0
11+
gopkg.in/yaml.v2 v2.2.2 // indirect
812
)

go.sum

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,18 @@
11
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
22
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
3+
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
4+
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
5+
github.com/imdario/mergo v0.3.7 h1:Y+UAYTZ7gDEuOfhxKWy+dvb5dRQ6rJjFSdX2HZY1/gI=
6+
github.com/imdario/mergo v0.3.7/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
37
github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU=
48
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
59
github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE=
610
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
11+
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
12+
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
713
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223 h1:DH4skfRX4EBpamg7iV4ZlCpblAHI6s6TDM39bFZumv8=
814
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
15+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
16+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
17+
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
18+
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

internal/util/doc.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// Copyright (C) 2017-present Arctic Ice Studio <[email protected]>
2+
// Copyright (C) 2017-present Sven Greb <[email protected]>
3+
//
4+
// Project: snowsaw
5+
// Repository: https://github.com/arcticicestudio/snowsaw
6+
// License: MIT
7+
8+
// Author: Arctic Ice Studio <[email protected]>
9+
// Author: Sven Greb <[email protected]>
10+
// Since: 0.4.0
11+
12+
// Package util provides application-wide utility functions.
13+
package util

internal/util/filesystem.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// Copyright (C) 2017-present Arctic Ice Studio <[email protected]>
2+
// Copyright (C) 2017-present Sven Greb <[email protected]>
3+
//
4+
// Project: snowsaw
5+
// Repository: https://github.com/arcticicestudio/snowsaw
6+
// License: MIT
7+
8+
// Author: Arctic Ice Studio <[email protected]>
9+
// Author: Sven Greb <[email protected]>
10+
// Since: 0.4.0
11+
12+
package util
13+
14+
import (
15+
"fmt"
16+
"os"
17+
"path/filepath"
18+
)
19+
20+
// AbsPath converts the given path to an absolute path.
21+
func AbsPath(p string) (string, error) {
22+
if filepath.IsAbs(p) {
23+
return filepath.Clean(p), nil
24+
}
25+
26+
p, err := filepath.Abs(p)
27+
if err == nil {
28+
return filepath.Clean(p), nil
29+
}
30+
31+
return "", fmt.Errorf("failed to convert to absolute path: %s", p)
32+
}
33+
34+
// FileExists checks if the file at the given path exists and is not a directory.
35+
func FileExists(path string) (bool, error) {
36+
info, err := os.Stat(path)
37+
if err == nil && !info.IsDir() {
38+
return true, nil
39+
}
40+
if os.IsNotExist(err) {
41+
return false, nil
42+
}
43+
if info.IsDir() {
44+
return false, fmt.Errorf("%s is a directory", path)
45+
}
46+
return false, err
47+
}
48+
49+
// IsFileWritable checks if the given file is writable.
50+
func IsFileWritable(path string) bool {
51+
_, err := os.OpenFile(path, os.O_WRONLY, 0660)
52+
if err != nil {
53+
return false
54+
}
55+
56+
return true
57+
}

pkg/config/builder/builder.go

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
// Copyright (C) 2017-present Arctic Ice Studio <[email protected]>
2+
// Copyright (C) 2017-present Sven Greb <[email protected]>
3+
//
4+
// Project: snowsaw
5+
// Repository: https://github.com/arcticicestudio/snowsaw
6+
// License: MIT
7+
8+
// Author: Arctic Ice Studio <[email protected]>
9+
// Author: Sven Greb <[email protected]>
10+
// Since: 0.4.0
11+
12+
// Package builder provides methods to load and merge configuration files using the builder design pattern.
13+
package builder
14+
15+
import (
16+
"fmt"
17+
"io/ioutil"
18+
"path/filepath"
19+
"reflect"
20+
21+
"github.com/imdario/mergo"
22+
23+
"github.com/arcticicestudio/snowsaw/internal/util"
24+
"github.com/arcticicestudio/snowsaw/pkg/config/encoder"
25+
"github.com/arcticicestudio/snowsaw/pkg/config/source/file"
26+
"github.com/arcticicestudio/snowsaw/pkg/prt"
27+
)
28+
29+
// builder contains the current configuration building state.
30+
type builder struct {
31+
Files []*file.File
32+
}
33+
34+
// Load tries to load all given configuration files.
35+
// It checks if the path is valid and exists, tries to assign a matching encoder.Encoder based on the file extension and
36+
// returns a pointer to a builder to chain and pass the loaded files to the Merge function.
37+
func Load(files ...*file.File) *builder {
38+
s := &builder{Files: []*file.File{}}
39+
40+
for _, f := range files {
41+
// Convert to absolute path and check if file exists, otherwise ignore and check next.
42+
f.Path, _ = util.AbsPath(f.Path)
43+
if exists, _ := util.FileExists(f.Path); !exists {
44+
prt.Debugf("Ignoring non-existent configuration file: %s", f.Path)
45+
continue
46+
}
47+
48+
// Find matching encoder by file extension if not already set.
49+
if f.Encoder == nil {
50+
fileExt := filepath.Ext(f.Path)
51+
if len(fileExt) <= 1 {
52+
prt.Debugf("Ignoring configuration file without supported extension: %s", f.Path)
53+
continue
54+
}
55+
56+
// Strip dot character separating the file name and extension.
57+
fileExt = fileExt[1:]
58+
59+
// Only add files with supported encoders.
60+
for ext, enc := range encoder.ExtensionMapping {
61+
if ext == fileExt {
62+
f.Encoder = enc
63+
s.Files = append(s.Files, f)
64+
break
65+
}
66+
}
67+
} else {
68+
s.Files = append(s.Files, f)
69+
}
70+
}
71+
72+
return s
73+
}
74+
75+
// Into accepts a configuration struct pointer and populates it with the current config state.
76+
func (s *builder) Into(c interface{}) error {
77+
base := reflect.New(reflect.TypeOf(c).Elem()).Interface()
78+
79+
for _, f := range s.Files {
80+
content, err := ioutil.ReadFile(f.Path)
81+
if err != nil {
82+
return err
83+
}
84+
85+
raw := base
86+
// Decode the read content using the assigned encoder.
87+
if encErr := f.Encoder.Decode(content, &raw); encErr != nil {
88+
return fmt.Errorf("%s\n%v", f.Path, err)
89+
}
90+
91+
// Merge decoded data into given base configuration state.
92+
if encErr := mergo.Merge(c, raw, mergo.WithAppendSlice, mergo.WithOverride); encErr != nil {
93+
return fmt.Errorf("%s\n%v", f.Path, err)
94+
}
95+
}
96+
97+
return nil
98+
}

pkg/config/config.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Copyright (C) 2017-present Arctic Ice Studio <[email protected]>
2+
// Copyright (C) 2017-present Sven Greb <[email protected]>
3+
//
4+
// Project: snowsaw
5+
// Repository: https://github.com/arcticicestudio/snowsaw
6+
// License: MIT
7+
8+
// Author: Arctic Ice Studio <[email protected]>
9+
// Author: Sven Greb <[email protected]>
10+
// Since: 0.4.0
11+
12+
// Package config contains application-wide configurations and constants.
13+
// It provides a file abstraction to de/encode, load and validate YAML and JSON data using the builder design pattern.
14+
package config
15+
16+
// Config represents the application-wide configurations.
17+
type Config struct {
18+
// LogLevel is the verbosity level of the application-wide logging behavior.
19+
LogLevel string
20+
}

pkg/config/constants.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
// Copyright (C) 2017-present Arctic Ice Studio <[email protected]>
2+
// Copyright (C) 2017-present Sven Greb <[email protected]>
3+
//
4+
// Project: snowsaw
5+
// Repository: https://github.com/arcticicestudio/snowsaw
6+
// License: MIT
7+
8+
// Author: Arctic Ice Studio <[email protected]>
9+
// Author: Sven Greb <[email protected]>
10+
// Since: 0.4.0
11+
12+
package config
13+
14+
import (
15+
"fmt"
16+
"os"
17+
"path/filepath"
18+
19+
"github.com/mitchellh/go-homedir"
20+
21+
"github.com/arcticicestudio/snowsaw/pkg/config/encoder"
22+
"github.com/arcticicestudio/snowsaw/pkg/config/source/file"
23+
)
24+
25+
const (
26+
// PackageName is the name of this Go module
27+
PackageName = "github.com/arcticicestudio/" + ProjectName
28+
// ProjectName is the name of the project.
29+
ProjectName = "snowsaw"
30+
)
31+
32+
var (
33+
// AppConfig is the main application configuration with initial default values.
34+
AppConfig = Config{}
35+
36+
// AppConfigPaths is the default paths the application will search for configuration files.
37+
AppConfigPaths []*file.File
38+
39+
// BuildDateTime is the date and time this application was build.
40+
BuildDateTime string
41+
42+
// Version is the application version.
43+
Version = "0.0.0"
44+
)
45+
46+
func init() {
47+
AppConfig = Config{
48+
LogLevel: "info",
49+
}
50+
AppConfigPaths = genConfigPaths()
51+
}
52+
53+
func genConfigPaths() []*file.File {
54+
var files []*file.File
55+
56+
// Include user-level dot-file configurations from the user's home directory.
57+
home, err := homedir.Dir()
58+
if err == nil {
59+
for _, ext := range encoder.ExtensionsJson {
60+
files = append(files, file.NewFile(filepath.Join(home, fmt.Sprintf(".%s.%s", ProjectName, ext))))
61+
}
62+
// Since YAML is a superset of JSON, YAML files take precedence over pure JSON based configurations.
63+
for _, ext := range encoder.ExtensionsYaml {
64+
files = append(files, file.NewFile(filepath.Join(home, fmt.Sprintf(".%s.%s", ProjectName, ext))))
65+
}
66+
}
67+
68+
// Files placed in the current working directory take precedence over user-level configurations.
69+
pwd, err := os.Getwd()
70+
if err == nil {
71+
for _, ext := range encoder.ExtensionsJson {
72+
files = append(files, file.NewFile(filepath.Join(pwd, fmt.Sprintf("%s.%s", ProjectName, ext))))
73+
}
74+
// Since YAML is a superset of JSON, YAML files take precedence over pure JSON based configurations.
75+
for _, ext := range encoder.ExtensionsYaml {
76+
files = append(files, file.NewFile(filepath.Join(pwd, fmt.Sprintf("%s.%s", ProjectName, ext))))
77+
}
78+
}
79+
80+
return files
81+
}

pkg/config/encoder/constants.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// Copyright (C) 2017-present Arctic Ice Studio <[email protected]>
2+
// Copyright (C) 2017-present Sven Greb <[email protected]>
3+
//
4+
// Project: snowsaw
5+
// Repository: https://github.com/arcticicestudio/snowsaw
6+
// License: MIT
7+
8+
// Author: Arctic Ice Studio <[email protected]>
9+
// Author: Sven Greb <[email protected]>
10+
// Since: 0.4.0
11+
12+
package encoder
13+
14+
import (
15+
"github.com/arcticicestudio/snowsaw/pkg/config/encoder/json"
16+
"github.com/arcticicestudio/snowsaw/pkg/config/encoder/yaml"
17+
)
18+
19+
var (
20+
// ExtensionMapping maps supported file extensions to their compatible encoders.
21+
ExtensionMapping = map[string]Encoder{
22+
"json": json.NewJsonEncoder(),
23+
"yaml": yaml.NewYamlEncoder(),
24+
"yml": yaml.NewYamlEncoder(),
25+
}
26+
// ExtensionsJson stores all supported extensions for files containing JSON data.
27+
ExtensionsJson = []string{"json"}
28+
// ExtensionsYaml stores all supported extensions for files containing YAML data.
29+
ExtensionsYaml = []string{"yaml", "yml"}
30+
)

pkg/config/encoder/encoder.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// Copyright (C) 2017-present Arctic Ice Studio <[email protected]>
2+
// Copyright (C) 2017-present Sven Greb <[email protected]>
3+
//
4+
// Project: snowsaw
5+
// Repository: https://github.com/arcticicestudio/snowsaw
6+
// License: MIT
7+
8+
// Author: Arctic Ice Studio <[email protected]>
9+
// Author: Sven Greb <[email protected]>
10+
// Since: 0.4.0
11+
12+
// Package encoder handles the de/encoding for data source formats.
13+
package encoder
14+
15+
// Encoder provides functions to de/encode data formats.
16+
type Encoder interface {
17+
Encode(interface{}) ([]byte, error)
18+
Decode([]byte, interface{}) error
19+
}

pkg/config/encoder/json/json.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// Copyright (C) 2017-present Arctic Ice Studio <[email protected]>
2+
// Copyright (C) 2017-present Sven Greb <[email protected]>
3+
//
4+
// Project: snowsaw
5+
// Repository: https://github.com/arcticicestudio/snowsaw
6+
// License: MIT
7+
8+
// Author: Arctic Ice Studio <[email protected]>
9+
// Author: Sven Greb <[email protected]>
10+
// Since: 0.4.0
11+
12+
// Package json provides an encoder to de/encode JSON data.
13+
package json
14+
15+
import (
16+
"encoding/json"
17+
)
18+
19+
// Encoder represents a JSON configuration file encoder.
20+
// See the official JSON specification and documentations for more details: https://json.org
21+
type Encoder struct{}
22+
23+
// Encode encodes the given JSON data.
24+
func (j Encoder) Encode(v interface{}) ([]byte, error) {
25+
return json.Marshal(v)
26+
}
27+
28+
// Decode decodes the given JSON data.
29+
func (j Encoder) Decode(d []byte, v interface{}) error {
30+
return json.Unmarshal(d, v)
31+
}
32+
33+
// NewJsonEncoder returns a new JSON Encoder.
34+
func NewJsonEncoder() Encoder {
35+
return Encoder{}
36+
}

0 commit comments

Comments
 (0)