add config
This commit is contained in:
83
config/reader/json/json.go
Normal file
83
config/reader/json/json.go
Normal file
@@ -0,0 +1,83 @@
|
||||
package json
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/imdario/mergo"
|
||||
"github.com/micro/go-micro/config/encoder"
|
||||
"github.com/micro/go-micro/config/encoder/json"
|
||||
"github.com/micro/go-micro/config/reader"
|
||||
"github.com/micro/go-micro/config/source"
|
||||
)
|
||||
|
||||
type jsonReader struct {
|
||||
opts reader.Options
|
||||
json encoder.Encoder
|
||||
}
|
||||
|
||||
func (j *jsonReader) Merge(changes ...*source.ChangeSet) (*source.ChangeSet, error) {
|
||||
var merged map[string]interface{}
|
||||
|
||||
for _, m := range changes {
|
||||
if m == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(m.Data) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
codec, ok := j.opts.Encoding[m.Format]
|
||||
if !ok {
|
||||
// fallback
|
||||
codec = j.json
|
||||
}
|
||||
|
||||
var data map[string]interface{}
|
||||
if err := codec.Decode(m.Data, &data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := mergo.Map(&merged, data, mergo.WithOverride); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
b, err := j.json.Encode(merged)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cs := &source.ChangeSet{
|
||||
Timestamp: time.Now(),
|
||||
Data: b,
|
||||
Source: "json",
|
||||
Format: j.json.String(),
|
||||
}
|
||||
cs.Checksum = cs.Sum()
|
||||
|
||||
return cs, nil
|
||||
}
|
||||
|
||||
func (j *jsonReader) Values(ch *source.ChangeSet) (reader.Values, error) {
|
||||
if ch == nil {
|
||||
return nil, errors.New("changeset is nil")
|
||||
}
|
||||
if ch.Format != "json" {
|
||||
return nil, errors.New("unsupported format")
|
||||
}
|
||||
return newValues(ch)
|
||||
}
|
||||
|
||||
func (j *jsonReader) String() string {
|
||||
return "json"
|
||||
}
|
||||
|
||||
// NewReader creates a json reader
|
||||
func NewReader(opts ...reader.Option) reader.Reader {
|
||||
options := reader.NewOptions(opts...)
|
||||
return &jsonReader{
|
||||
json: json.NewEncoder(),
|
||||
opts: options,
|
||||
}
|
||||
}
|
||||
43
config/reader/json/json_test.go
Normal file
43
config/reader/json/json_test.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package json
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/micro/go-micro/config/source"
|
||||
)
|
||||
|
||||
func TestReader(t *testing.T) {
|
||||
data := []byte(`{"foo": "bar", "baz": {"bar": "cat"}}`)
|
||||
|
||||
testData := []struct {
|
||||
path []string
|
||||
value string
|
||||
}{
|
||||
{
|
||||
[]string{"foo"},
|
||||
"bar",
|
||||
},
|
||||
{
|
||||
[]string{"baz", "bar"},
|
||||
"cat",
|
||||
},
|
||||
}
|
||||
|
||||
r := NewReader()
|
||||
|
||||
c, err := r.Merge(&source.ChangeSet{Data: data}, &source.ChangeSet{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
values, err := r.Values(c)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
for _, test := range testData {
|
||||
if v := values.Get(test.path...).String(""); v != test.value {
|
||||
t.Fatalf("Expected %s got %s for path %v", test.value, v, test.path)
|
||||
}
|
||||
}
|
||||
}
|
||||
208
config/reader/json/values.go
Normal file
208
config/reader/json/values.go
Normal file
@@ -0,0 +1,208 @@
|
||||
package json
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
simple "github.com/bitly/go-simplejson"
|
||||
"github.com/micro/go-micro/config/reader"
|
||||
"github.com/micro/go-micro/config/source"
|
||||
)
|
||||
|
||||
type jsonValues struct {
|
||||
ch *source.ChangeSet
|
||||
sj *simple.Json
|
||||
}
|
||||
|
||||
type jsonValue struct {
|
||||
*simple.Json
|
||||
}
|
||||
|
||||
func newValues(ch *source.ChangeSet) (reader.Values, error) {
|
||||
sj := simple.New()
|
||||
data, _ := reader.ReplaceEnvVars(ch.Data)
|
||||
if err := sj.UnmarshalJSON(data); err != nil {
|
||||
sj.SetPath(nil, string(ch.Data))
|
||||
}
|
||||
return &jsonValues{ch, sj}, nil
|
||||
}
|
||||
|
||||
func newValue(s *simple.Json) reader.Value {
|
||||
if s == nil {
|
||||
s = simple.New()
|
||||
}
|
||||
return &jsonValue{s}
|
||||
}
|
||||
|
||||
func (j *jsonValues) Get(path ...string) reader.Value {
|
||||
return &jsonValue{j.sj.GetPath(path...)}
|
||||
}
|
||||
|
||||
func (j *jsonValues) Del(path ...string) {
|
||||
// delete the tree?
|
||||
if len(path) == 0 {
|
||||
j.sj = simple.New()
|
||||
return
|
||||
}
|
||||
|
||||
if len(path) == 1 {
|
||||
j.sj.Del(path[0])
|
||||
return
|
||||
}
|
||||
|
||||
vals := j.sj.GetPath(path[:len(path)-1]...)
|
||||
vals.Del(path[len(path)-1])
|
||||
j.sj.SetPath(path[:len(path)-1], vals.Interface())
|
||||
return
|
||||
}
|
||||
|
||||
func (j *jsonValues) Set(val interface{}, path ...string) {
|
||||
j.sj.SetPath(path, val)
|
||||
}
|
||||
|
||||
func (j *jsonValues) Bytes() []byte {
|
||||
b, _ := j.sj.MarshalJSON()
|
||||
return b
|
||||
}
|
||||
|
||||
func (j *jsonValues) Map() map[string]interface{} {
|
||||
m, _ := j.sj.Map()
|
||||
return m
|
||||
}
|
||||
|
||||
func (j *jsonValues) Scan(v interface{}) error {
|
||||
b, err := j.sj.MarshalJSON()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return json.Unmarshal(b, v)
|
||||
}
|
||||
|
||||
func (j *jsonValues) String() string {
|
||||
return "json"
|
||||
}
|
||||
|
||||
func (j *jsonValue) Bool(def bool) bool {
|
||||
b, err := j.Json.Bool()
|
||||
if err == nil {
|
||||
return b
|
||||
}
|
||||
|
||||
str, ok := j.Interface().(string)
|
||||
if !ok {
|
||||
return def
|
||||
}
|
||||
|
||||
b, err = strconv.ParseBool(str)
|
||||
if err != nil {
|
||||
return def
|
||||
}
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
func (j *jsonValue) Int(def int) int {
|
||||
i, err := j.Json.Int()
|
||||
if err == nil {
|
||||
return i
|
||||
}
|
||||
|
||||
str, ok := j.Interface().(string)
|
||||
if !ok {
|
||||
return def
|
||||
}
|
||||
|
||||
i, err = strconv.Atoi(str)
|
||||
if err != nil {
|
||||
return def
|
||||
}
|
||||
|
||||
return i
|
||||
}
|
||||
|
||||
func (j *jsonValue) String(def string) string {
|
||||
return j.Json.MustString(def)
|
||||
}
|
||||
|
||||
func (j *jsonValue) Float64(def float64) float64 {
|
||||
f, err := j.Json.Float64()
|
||||
if err == nil {
|
||||
return f
|
||||
}
|
||||
|
||||
str, ok := j.Interface().(string)
|
||||
if !ok {
|
||||
return def
|
||||
}
|
||||
|
||||
f, err = strconv.ParseFloat(str, 64)
|
||||
if err != nil {
|
||||
return def
|
||||
}
|
||||
|
||||
return f
|
||||
}
|
||||
|
||||
func (j *jsonValue) Duration(def time.Duration) time.Duration {
|
||||
v, err := j.Json.String()
|
||||
if err != nil {
|
||||
return def
|
||||
}
|
||||
|
||||
value, err := time.ParseDuration(v)
|
||||
if err != nil {
|
||||
return def
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
func (j *jsonValue) StringSlice(def []string) []string {
|
||||
v, err := j.Json.String()
|
||||
if err == nil {
|
||||
sl := strings.Split(v, ",")
|
||||
if len(sl) > 1 {
|
||||
return sl
|
||||
}
|
||||
}
|
||||
return j.Json.MustStringArray(def)
|
||||
}
|
||||
|
||||
func (j *jsonValue) StringMap(def map[string]string) map[string]string {
|
||||
m, err := j.Json.Map()
|
||||
if err != nil {
|
||||
return def
|
||||
}
|
||||
|
||||
res := map[string]string{}
|
||||
|
||||
for k, v := range m {
|
||||
res[k] = fmt.Sprintf("%v", v)
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
func (j *jsonValue) Scan(v interface{}) error {
|
||||
b, err := j.Json.MarshalJSON()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return json.Unmarshal(b, v)
|
||||
}
|
||||
|
||||
func (j *jsonValue) Bytes() []byte {
|
||||
b, err := j.Json.Bytes()
|
||||
if err != nil {
|
||||
// try return marshalled
|
||||
b, err = j.Json.MarshalJSON()
|
||||
if err != nil {
|
||||
return []byte{}
|
||||
}
|
||||
return b
|
||||
}
|
||||
return b
|
||||
}
|
||||
39
config/reader/json/values_test.go
Normal file
39
config/reader/json/values_test.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package json
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/micro/go-micro/config/source"
|
||||
)
|
||||
|
||||
func TestValues(t *testing.T) {
|
||||
data := []byte(`{"foo": "bar", "baz": {"bar": "cat"}}`)
|
||||
|
||||
testData := []struct {
|
||||
path []string
|
||||
value string
|
||||
}{
|
||||
{
|
||||
[]string{"foo"},
|
||||
"bar",
|
||||
},
|
||||
{
|
||||
[]string{"baz", "bar"},
|
||||
"cat",
|
||||
},
|
||||
}
|
||||
|
||||
values, err := newValues(&source.ChangeSet{
|
||||
Data: data,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
for _, test := range testData {
|
||||
if v := values.Get(test.path...).String(""); v != test.value {
|
||||
t.Fatalf("Expected %s got %s for path %v", test.value, v, test.path)
|
||||
}
|
||||
}
|
||||
}
|
||||
42
config/reader/options.go
Normal file
42
config/reader/options.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package reader
|
||||
|
||||
import (
|
||||
"github.com/micro/go-micro/config/encoder"
|
||||
"github.com/micro/go-micro/config/encoder/hcl"
|
||||
"github.com/micro/go-micro/config/encoder/json"
|
||||
"github.com/micro/go-micro/config/encoder/toml"
|
||||
"github.com/micro/go-micro/config/encoder/xml"
|
||||
"github.com/micro/go-micro/config/encoder/yaml"
|
||||
)
|
||||
|
||||
type Options struct {
|
||||
Encoding map[string]encoder.Encoder
|
||||
}
|
||||
|
||||
type Option func(o *Options)
|
||||
|
||||
func NewOptions(opts ...Option) Options {
|
||||
options := Options{
|
||||
Encoding: map[string]encoder.Encoder{
|
||||
"json": json.NewEncoder(),
|
||||
"yaml": yaml.NewEncoder(),
|
||||
"toml": toml.NewEncoder(),
|
||||
"xml": xml.NewEncoder(),
|
||||
"hcl": hcl.NewEncoder(),
|
||||
"yml": yaml.NewEncoder(),
|
||||
},
|
||||
}
|
||||
for _, o := range opts {
|
||||
o(&options)
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
||||
func WithEncoder(e encoder.Encoder) Option {
|
||||
return func(o *Options) {
|
||||
if o.Encoding == nil {
|
||||
o.Encoding = make(map[string]encoder.Encoder)
|
||||
}
|
||||
o.Encoding[e.String()] = e
|
||||
}
|
||||
}
|
||||
23
config/reader/preprocessor.go
Normal file
23
config/reader/preprocessor.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package reader
|
||||
|
||||
import (
|
||||
"os"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
func ReplaceEnvVars(raw []byte) ([]byte, error) {
|
||||
re := regexp.MustCompile(`\$\{([A-Za-z0-9_]+)\}`)
|
||||
if re.Match(raw) {
|
||||
dataS := string(raw)
|
||||
res := re.ReplaceAllStringFunc(dataS, replaceEnvVars)
|
||||
return []byte(res), nil
|
||||
} else {
|
||||
return raw, nil
|
||||
}
|
||||
}
|
||||
|
||||
func replaceEnvVars(element string) string {
|
||||
v := element[2 : len(element)-1]
|
||||
el := os.Getenv(v)
|
||||
return el
|
||||
}
|
||||
73
config/reader/preprocessor_test.go
Normal file
73
config/reader/preprocessor_test.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package reader
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestReplaceEnvVars(t *testing.T) {
|
||||
os.Setenv("myBar", "cat")
|
||||
os.Setenv("MYBAR", "cat")
|
||||
os.Setenv("my_Bar", "cat")
|
||||
os.Setenv("myBar_", "cat")
|
||||
|
||||
testData := []struct {
|
||||
expected string
|
||||
data []byte
|
||||
}{
|
||||
// Right use cases
|
||||
{
|
||||
`{"foo": "bar", "baz": {"bar": "cat"}}`,
|
||||
[]byte(`{"foo": "bar", "baz": {"bar": "${myBar}"}}`),
|
||||
},
|
||||
{
|
||||
`{"foo": "bar", "baz": {"bar": "cat"}}`,
|
||||
[]byte(`{"foo": "bar", "baz": {"bar": "${MYBAR}"}}`),
|
||||
},
|
||||
{
|
||||
`{"foo": "bar", "baz": {"bar": "cat"}}`,
|
||||
[]byte(`{"foo": "bar", "baz": {"bar": "${my_Bar}"}}`),
|
||||
},
|
||||
{
|
||||
`{"foo": "bar", "baz": {"bar": "cat"}}`,
|
||||
[]byte(`{"foo": "bar", "baz": {"bar": "${myBar_}"}}`),
|
||||
},
|
||||
// Wrong use cases
|
||||
{
|
||||
`{"foo": "bar", "baz": {"bar": "${myBar-}"}}`,
|
||||
[]byte(`{"foo": "bar", "baz": {"bar": "${myBar-}"}}`),
|
||||
},
|
||||
{
|
||||
`{"foo": "bar", "baz": {"bar": "${}"}}`,
|
||||
[]byte(`{"foo": "bar", "baz": {"bar": "${}"}}`),
|
||||
},
|
||||
{
|
||||
`{"foo": "bar", "baz": {"bar": "$sss}"}}`,
|
||||
[]byte(`{"foo": "bar", "baz": {"bar": "$sss}"}}`),
|
||||
},
|
||||
{
|
||||
`{"foo": "bar", "baz": {"bar": "${sss"}}`,
|
||||
[]byte(`{"foo": "bar", "baz": {"bar": "${sss"}}`),
|
||||
},
|
||||
{
|
||||
`{"foo": "bar", "baz": {"bar": "{something}"}}`,
|
||||
[]byte(`{"foo": "bar", "baz": {"bar": "{something}"}}`),
|
||||
},
|
||||
// Use cases without replace env vars
|
||||
{
|
||||
`{"foo": "bar", "baz": {"bar": "cat"}}`,
|
||||
[]byte(`{"foo": "bar", "baz": {"bar": "cat"}}`),
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testData {
|
||||
res, err := ReplaceEnvVars(test.data)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if strings.Compare(test.expected, string(res)) != 0 {
|
||||
t.Fatalf("Expected %s got %s", test.expected, res)
|
||||
}
|
||||
}
|
||||
}
|
||||
36
config/reader/reader.go
Normal file
36
config/reader/reader.go
Normal file
@@ -0,0 +1,36 @@
|
||||
// Package reader parses change sets and provides config values
|
||||
package reader
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/micro/go-micro/config/source"
|
||||
)
|
||||
|
||||
// Reader is an interface for merging changesets
|
||||
type Reader interface {
|
||||
Merge(...*source.ChangeSet) (*source.ChangeSet, error)
|
||||
Values(*source.ChangeSet) (Values, error)
|
||||
String() string
|
||||
}
|
||||
|
||||
// Values is returned by the reader
|
||||
type Values interface {
|
||||
Bytes() []byte
|
||||
Get(path ...string) Value
|
||||
Map() map[string]interface{}
|
||||
Scan(v interface{}) error
|
||||
}
|
||||
|
||||
// Value represents a value of any type
|
||||
type Value interface {
|
||||
Bool(def bool) bool
|
||||
Int(def int) int
|
||||
String(def string) string
|
||||
Float64(def float64) float64
|
||||
Duration(def time.Duration) time.Duration
|
||||
StringSlice(def []string) []string
|
||||
StringMap(def map[string]string) map[string]string
|
||||
Scan(val interface{}) error
|
||||
Bytes() []byte
|
||||
}
|
||||
Reference in New Issue
Block a user