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