add config
This commit is contained in:
70
config/source/file/README.md
Normal file
70
config/source/file/README.md
Normal file
@@ -0,0 +1,70 @@
|
||||
# File Source
|
||||
|
||||
The file source reads config from a file.
|
||||
|
||||
It uses the File extension to determine the Format e.g `config.yaml` has the yaml format.
|
||||
It does not make use of encoders or interpet the file data. If a file extension is not present
|
||||
the source Format will default to the Encoder in options.
|
||||
|
||||
## Example
|
||||
|
||||
A config file format in json
|
||||
|
||||
```json
|
||||
{
|
||||
"hosts": {
|
||||
"database": {
|
||||
"address": "10.0.0.1",
|
||||
"port": 3306
|
||||
},
|
||||
"cache": {
|
||||
"address": "10.0.0.2",
|
||||
"port": 6379
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## New Source
|
||||
|
||||
Specify file source with path to file. Path is optional and will default to `config.json`
|
||||
|
||||
```go
|
||||
fileSource := file.NewSource(
|
||||
file.WithPath("/tmp/config.json"),
|
||||
)
|
||||
```
|
||||
|
||||
## File Format
|
||||
|
||||
To load different file formats e.g yaml, toml, xml simply specify them with their extension
|
||||
|
||||
```
|
||||
fileSource := file.NewSource(
|
||||
file.WithPath("/tmp/config.yaml"),
|
||||
)
|
||||
```
|
||||
|
||||
If you want to specify a file without extension, ensure you set the encoder to the same format
|
||||
|
||||
```
|
||||
e := toml.NewEncoder()
|
||||
|
||||
fileSource := file.NewSource(
|
||||
file.WithPath("/tmp/config"),
|
||||
source.WithEncoder(e),
|
||||
)
|
||||
```
|
||||
|
||||
## Load Source
|
||||
|
||||
Load the source into config
|
||||
|
||||
```go
|
||||
// Create new config
|
||||
conf := config.NewConfig()
|
||||
|
||||
// Load file source
|
||||
conf.Load(fileSource)
|
||||
```
|
||||
|
||||
65
config/source/file/file.go
Normal file
65
config/source/file/file.go
Normal file
@@ -0,0 +1,65 @@
|
||||
// Package file is a file source. Expected format is json
|
||||
package file
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
||||
"github.com/micro/go-micro/config/source"
|
||||
)
|
||||
|
||||
type file struct {
|
||||
path string
|
||||
opts source.Options
|
||||
}
|
||||
|
||||
var (
|
||||
DefaultPath = "config.json"
|
||||
)
|
||||
|
||||
func (f *file) Read() (*source.ChangeSet, error) {
|
||||
fh, err := os.Open(f.path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer fh.Close()
|
||||
b, err := ioutil.ReadAll(fh)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
info, err := fh.Stat()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cs := &source.ChangeSet{
|
||||
Format: format(f.path, f.opts.Encoder),
|
||||
Source: f.String(),
|
||||
Timestamp: info.ModTime(),
|
||||
Data: b,
|
||||
}
|
||||
cs.Checksum = cs.Sum()
|
||||
|
||||
return cs, nil
|
||||
}
|
||||
|
||||
func (f *file) String() string {
|
||||
return "file"
|
||||
}
|
||||
|
||||
func (f *file) Watch() (source.Watcher, error) {
|
||||
if _, err := os.Stat(f.path); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return newWatcher(f)
|
||||
}
|
||||
|
||||
func NewSource(opts ...source.Option) source.Source {
|
||||
options := source.NewOptions(opts...)
|
||||
path := DefaultPath
|
||||
f, ok := options.Context.Value(filePathKey{}).(string)
|
||||
if ok {
|
||||
path = f
|
||||
}
|
||||
return &file{opts: options, path: path}
|
||||
}
|
||||
37
config/source/file/file_test.go
Normal file
37
config/source/file/file_test.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestFile(t *testing.T) {
|
||||
data := []byte(`{"foo": "bar"}`)
|
||||
path := filepath.Join(os.TempDir(), fmt.Sprintf("file.%d", time.Now().UnixNano()))
|
||||
fh, err := os.Create(path)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
defer func() {
|
||||
fh.Close()
|
||||
os.Remove(path)
|
||||
}()
|
||||
|
||||
_, err = fh.Write(data)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
f := NewSource(WithPath(path))
|
||||
c, err := f.Read()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
t.Logf("%+v", c)
|
||||
if string(c.Data) != string(data) {
|
||||
t.Error("data from file does not match")
|
||||
}
|
||||
}
|
||||
15
config/source/file/format.go
Normal file
15
config/source/file/format.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/micro/go-micro/config/encoder"
|
||||
)
|
||||
|
||||
func format(p string, e encoder.Encoder) string {
|
||||
parts := strings.Split(p, ".")
|
||||
if len(parts) > 1 {
|
||||
return parts[len(parts)-1]
|
||||
}
|
||||
return e.String()
|
||||
}
|
||||
31
config/source/file/format_test.go
Normal file
31
config/source/file/format_test.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/micro/go-micro/config/source"
|
||||
)
|
||||
|
||||
func TestFormat(t *testing.T) {
|
||||
opts := source.NewOptions()
|
||||
e := opts.Encoder
|
||||
|
||||
testCases := []struct {
|
||||
p string
|
||||
f string
|
||||
}{
|
||||
{"/foo/bar.json", "json"},
|
||||
{"/foo/bar.yaml", "yaml"},
|
||||
{"/foo/bar.xml", "xml"},
|
||||
{"/foo/bar.conf.ini", "ini"},
|
||||
{"conf", e.String()},
|
||||
}
|
||||
|
||||
for _, d := range testCases {
|
||||
f := format(d.p, e)
|
||||
if f != d.f {
|
||||
t.Fatalf("%s: expected %s got %s", d.p, d.f, f)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
19
config/source/file/options.go
Normal file
19
config/source/file/options.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/micro/go-micro/config/source"
|
||||
)
|
||||
|
||||
type filePathKey struct{}
|
||||
|
||||
// WithPath sets the path to file
|
||||
func WithPath(p string) source.Option {
|
||||
return func(o *source.Options) {
|
||||
if o.Context == nil {
|
||||
o.Context = context.Background()
|
||||
}
|
||||
o.Context = context.WithValue(o.Context, filePathKey{}, p)
|
||||
}
|
||||
}
|
||||
66
config/source/file/watcher.go
Normal file
66
config/source/file/watcher.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"github.com/micro/go-micro/config/source"
|
||||
)
|
||||
|
||||
type watcher struct {
|
||||
f *file
|
||||
|
||||
fw *fsnotify.Watcher
|
||||
exit chan bool
|
||||
}
|
||||
|
||||
func newWatcher(f *file) (source.Watcher, error) {
|
||||
fw, err := fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fw.Add(f.path)
|
||||
|
||||
return &watcher{
|
||||
f: f,
|
||||
fw: fw,
|
||||
exit: make(chan bool),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (w *watcher) Next() (*source.ChangeSet, error) {
|
||||
// is it closed?
|
||||
select {
|
||||
case <-w.exit:
|
||||
return nil, errors.New("watcher stopped")
|
||||
default:
|
||||
}
|
||||
|
||||
// try get the event
|
||||
select {
|
||||
case event, _ := <-w.fw.Events:
|
||||
if event.Op == fsnotify.Rename {
|
||||
// check existence of file, and add watch again
|
||||
_, err := os.Stat(event.Name)
|
||||
if err == nil || os.IsExist(err) {
|
||||
w.fw.Add(event.Name)
|
||||
}
|
||||
}
|
||||
|
||||
c, err := w.f.Read()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c, nil
|
||||
case err := <-w.fw.Errors:
|
||||
return nil, err
|
||||
case <-w.exit:
|
||||
return nil, errors.New("watcher stopped")
|
||||
}
|
||||
}
|
||||
|
||||
func (w *watcher) Stop() error {
|
||||
return w.fw.Close()
|
||||
}
|
||||
Reference in New Issue
Block a user