Skip to content

Commit

Permalink
Add FS interface for glob, file, and directory sourcers. (#2)
Browse files Browse the repository at this point in the history
  • Loading branch information
efritz authored Sep 5, 2019
1 parent bb95686 commit 5e7d4c0
Show file tree
Hide file tree
Showing 15 changed files with 830 additions and 47 deletions.
12 changes: 6 additions & 6 deletions config_mock_test.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

35 changes: 21 additions & 14 deletions directory_sourcer.go
Original file line number Diff line number Diff line change
@@ -1,39 +1,46 @@
package config

import (
"io/ioutil"
"os"
"path/filepath"
)

// NewOptionalDirectorySourcer creates a directory sourcer if the provided directory
// exists. the provided file is not found, a sourcer is returned returns no values.
func NewOptionalDirectorySourcer(dirname string, parser FileParser) Sourcer {
if _, err := os.Stat(dirname); err != nil && os.IsNotExist(err) {
// exists. If the provided file is not found, a sourcer is returned returns no values.
func NewOptionalDirectorySourcer(dirname string, parser FileParser, configs ...DirectorySourcerConfigFunc) Sourcer {
options := getDirectorySourcerConfigOptions(configs)

exists, err := options.fs.Exists(dirname)
if err != nil {
return newErrorSourcer(err)
}

if !exists {
return &fileSourcer{values: map[string]string{}}
}

return NewDirectorySourcer(dirname, parser)
return NewDirectorySourcer(dirname, parser, configs...)
}

// NewDirectorySourcer creates a sourcer that reads files from a directory. For
// details on parsing format, refer to NewFileParser. Each file in a directory
// is read in alphabetical order. Nested directories are ignored when reading
// directory content, and each found regular file is assumed to be parseable by
// the given FileParser.
func NewDirectorySourcer(dirname string, parser FileParser) Sourcer {
entries, err := ioutil.ReadDir(dirname)
func NewDirectorySourcer(dirname string, parser FileParser, configs ...DirectorySourcerConfigFunc) Sourcer {
options := getDirectorySourcerConfigOptions(configs)

filenames, err := options.fs.ListFiles(dirname)
if err != nil {
return newErrorSourcer(err)
}

sourcers := []Sourcer{}
for _, entry := range entries {
if entry.IsDir() {
continue
}

sourcers = append(sourcers, NewFileSourcer(filepath.Join(dirname, entry.Name()), parser))
for _, filename := range filenames {
sourcers = append(sourcers, NewFileSourcer(
filepath.Join(dirname, filename),
parser,
WithFileSourcerFS(options.fs),
))
}

return NewMultiSourcer(sourcers...)
Expand Down
26 changes: 26 additions & 0 deletions directory_sourcer_options.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package config

type (
directorySourcerOptions struct{ fs FileSystem }

// DirectorySourcerConfigFunc is a function used to configure instances of
// directory sourcers.
DirectorySourcerConfigFunc func(*directorySourcerOptions)
)

// WithDirectorySourcerFS sets the FileSystem instance.
func WithDirectorySourcerFS(fs FileSystem) DirectorySourcerConfigFunc {
return func(o *directorySourcerOptions) { o.fs = fs }
}

func getDirectorySourcerConfigOptions(configs []DirectorySourcerConfigFunc) *directorySourcerOptions {
options := &directorySourcerOptions{
fs: &realFileSystem{},
}

for _, f := range configs {
f(options)
}

return options
}
62 changes: 60 additions & 2 deletions directory_sourcer_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package config

import "github.com/aphistic/sweet"
import (
"github.com/aphistic/sweet"
. "github.com/efritz/go-mockgen/matchers"
. "github.com/onsi/gomega"
)

type DirectorySourcerSuite struct{}

Expand All @@ -17,9 +21,63 @@ func (s *DirectorySourcerSuite) TestLoadJSON(t sweet.T) {
ensureEquals(sourcer, []string{"z"}, "9")
}

func (s *DirectorySourcerSuite) TestLoadJSONWithFakeFS(t sweet.T) {
fs := NewMockFileSystem()
fs.ListFilesFunc.SetDefaultReturn([]string{"a.json", "b.json", "c.json"}, nil)

fs.ReadFileFunc.PushReturn([]byte(`{
"a": 1,
"b": 2,
"c": 3,
"x": 7
}`), nil)

fs.ReadFileFunc.PushReturn([]byte(`{
"b": 10,
"c": 20,
"d": 30,
"y": 8
}`), nil)

fs.ReadFileFunc.PushReturn([]byte(`{
"c": 100,
"d": 200,
"e": 300,
"z": 9
}`), nil)

sourcer := NewDirectorySourcer("test-files/dir", nil, WithDirectorySourcerFS((fs)))

ensureEquals(sourcer, []string{"a"}, "1")
ensureEquals(sourcer, []string{"b"}, "10")
ensureEquals(sourcer, []string{"c"}, "100")
ensureEquals(sourcer, []string{"d"}, "200")
ensureEquals(sourcer, []string{"e"}, "300")
ensureEquals(sourcer, []string{"x"}, "7")
ensureEquals(sourcer, []string{"y"}, "8")
ensureEquals(sourcer, []string{"z"}, "9")

Expect(fs.ListFilesFunc).To(BeCalledOnceWith("test-files/dir"))
Expect(fs.ReadFileFunc).To(BeCalledOnceWith("test-files/dir/a.json"))
Expect(fs.ReadFileFunc).To(BeCalledOnceWith("test-files/dir/b.json"))
Expect(fs.ReadFileFunc).To(BeCalledOnceWith("test-files/dir/c.json"))
}

func (s *DirectorySourcerSuite) TestOptionalDirectorySourcer(t sweet.T) {
ensureMissing(
NewOptionalFileSourcer("test-files/no-such-directory", nil),
NewOptionalDirectorySourcer("test-files/no-such-directory", nil),
[]string{"foo"},
)
}

func (s *DirectorySourcerSuite) TestOptionalDirectorySourcerWithFakeFS(t sweet.T) {
fs := NewMockFileSystem()
fs.ExistsFunc.SetDefaultReturn(false, nil)

ensureMissing(
NewOptionalDirectorySourcer("test-files/no-such-directory", nil, WithDirectorySourcerFS(fs)),
[]string{"foo"},
)

Expect(fs.ExistsFunc).To(BeCalledOnceWith("test-files/no-such-directory"))
}
25 changes: 16 additions & 9 deletions file_sourcer.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ package config
import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"

Expand All @@ -30,21 +28,30 @@ var parserMap = map[string]FileParser{

// NewOptionalFileSourcer creates a file sourcer if the provided file exists. If
// the provided file is not found, a sourcer is returned returns no values.
func NewOptionalFileSourcer(filename string, parser FileParser) Sourcer {
if _, err := os.Stat(filename); err != nil && os.IsNotExist(err) {
func NewOptionalFileSourcer(filename string, parser FileParser, configs ...FileSourcerConfigFunc) Sourcer {
options := getFileSourcerConfigOptions(configs)

exists, err := options.fs.Exists(filename)
if err != nil {
return newErrorSourcer(err)
}

if !exists {
return &fileSourcer{values: map[string]string{}}
}

return NewFileSourcer(filename, parser)
return NewFileSourcer(filename, parser, configs...)
}

// NewFileSourcer creates a sourcer that reads content from a file. The format
// of the file is read by the given FileParser. The content of the file must be
// an encoding of a map from string keys to JSON-serializable values. If a nil
// parser is supplied, one will be selected based on the extension of the file.
// JSON, YAML, and TOML files are supported.
func NewFileSourcer(filename string, parser FileParser) Sourcer {
values, err := readFile(filename, parser)
func NewFileSourcer(filename string, parser FileParser, configs ...FileSourcerConfigFunc) Sourcer {
options := getFileSourcerConfigOptions(configs)

values, err := readFile(filename, options.fs, parser)
if err != nil {
return newErrorSourcer(err)
}
Expand Down Expand Up @@ -125,8 +132,8 @@ func commonParser(content []byte, unmarshaller func([]byte, interface{}) error)
//
// Helpers

func readFile(filename string, parser FileParser) (map[string]interface{}, error) {
content, err := ioutil.ReadFile(filename)
func readFile(filename string, fs FileSystem, parser FileParser) (map[string]interface{}, error) {
content, err := fs.ReadFile(filename)
if err != nil {
return nil, fmt.Errorf("failed to read config file '%s' (%s)", filename, err.Error())
}
Expand Down
26 changes: 26 additions & 0 deletions file_sourcer_options.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package config

type (
fileSourcerOptions struct{ fs FileSystem }

// FileSourcerConfigFunc is a function used to configure instances of
// file sourcers.
FileSourcerConfigFunc func(*fileSourcerOptions)
)

// WithFileSourcerFS sets the FileSystem instance.
func WithFileSourcerFS(fs FileSystem) FileSourcerConfigFunc {
return func(o *fileSourcerOptions) { o.fs = fs }
}

func getFileSourcerConfigOptions(configs []FileSourcerConfigFunc) *fileSourcerOptions {
options := &fileSourcerOptions{
fs: &realFileSystem{},
}

for _, f := range configs {
f(options)
}

return options
}
37 changes: 37 additions & 0 deletions file_sourcer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package config

import (
"github.com/aphistic/sweet"
. "github.com/efritz/go-mockgen/matchers"
. "github.com/onsi/gomega"
)

Expand Down Expand Up @@ -30,12 +31,48 @@ func (s *FileSourcerSuite) TestLoadTOMLNoParser(t sweet.T) {
testFileSourcer(NewFileSourcer("test-files/values.toml", nil))
}

func (s *FileSourcerSuite) TestLoadJSONWithFakeFS(t sweet.T) {
fs := NewMockFileSystem()
fs.ReadFileFunc.SetDefaultReturn([]byte(`{
"foo": "bar",
"bar": [1, 2, 3],
"baz": null,
"bonk": {
"x": 1,
"y": 2,
"z": 3
},
"encoded": "{\"w\": 4}",
"deeply": {
"nested": {
"struct": [1, 2, 3]
}
}
}`), nil)

testFileSourcer(NewFileSourcer("test-files/values.json", ParseYAML, WithFileSourcerFS(fs)))
Expect(fs.ReadFileFunc).To(BeCalledOnceWith("test-files/values.json"))
}

func (s *FileSourcerSuite) TestOptionalFileSourcer(t sweet.T) {
ensureMissing(
NewOptionalFileSourcer("test-files/no-such-file.json", nil),
[]string{"foo"},
)
}

func (s *FileSourcerSuite) TestOptionalFileSourcerWithFakeFS(t sweet.T) {
fs := NewMockFileSystem()
fs.ExistsFunc.SetDefaultReturn(false, nil)

ensureMissing(
NewOptionalFileSourcer("test-files/no-such-file.json", nil, WithFileSourcerFS(fs)),
[]string{"foo"},
)

Expect(fs.ExistsFunc).To(BeCalledOnceWith("test-files/no-such-file.json"))
}

func (s *FileSourcerSuite) TestDump(t sweet.T) {
sourcer := NewOptionalFileSourcer("test-files/values.json", ParseYAML)

Expand Down
Loading

0 comments on commit 5e7d4c0

Please sign in to comment.