1
0
mirror of https://github.com/go-micro/go-micro.git synced 2025-08-04 21:42:57 +02:00

add config

This commit is contained in:
Asim Aslam
2019-05-30 23:11:13 +01:00
parent b4dc822ae3
commit 5e6491b7b0
58 changed files with 3784 additions and 0 deletions

View File

@ -0,0 +1,71 @@
# cli Source
The cli source reads config from parsed flags via a cli.Context.
## Format
We expect the use of the `micro/cli` package. Upper case flags will be lower cased. Dashes will be used as delimiters for nesting.
### Example
```go
micro.Flags(
cli.StringFlag{
Name: "database-address",
Value: "127.0.0.1",
Usage: "the db address",
},
cli.IntFlag{
Name: "database-port",
Value: 3306,
Usage: "the db port",
},
)
```
Becomes
```json
{
"database": {
"address": "127.0.0.1",
"port": 3306
}
}
```
## New and Load Source
Because a cli.Context is needed to retrieve the flags and their values, it is recommended to build your source from within a cli.Action.
```go
func main() {
// New Service
service := micro.NewService(
micro.Name("example"),
micro.Flags(
cli.StringFlag{
Name: "database-address",
Value: "127.0.0.1",
Usage: "the db address",
},
),
)
var clisrc source.Source
service.Init(
micro.Action(func(c *cli.Context) {
clisrc = cli.NewSource(
cli.Context(c),
)
// Alternatively, just setup your config right here
}),
)
// ... Load and use that source ...
conf := config.NewConfig()
conf.Load(clisrc)
}
```

146
config/source/cli/cli.go Normal file
View File

@ -0,0 +1,146 @@
package cli
import (
"flag"
"io/ioutil"
"os"
"strings"
"time"
"github.com/imdario/mergo"
"github.com/micro/cli"
"github.com/micro/go-micro/cmd"
"github.com/micro/go-micro/config/source"
)
type cliSource struct {
opts source.Options
ctx *cli.Context
}
func (c *cliSource) Read() (*source.ChangeSet, error) {
var changes map[string]interface{}
for _, name := range c.ctx.GlobalFlagNames() {
tmp := toEntry(name, c.ctx.GlobalGeneric(name))
mergo.Map(&changes, tmp) // need to sort error handling
}
for _, name := range c.ctx.FlagNames() {
tmp := toEntry(name, c.ctx.Generic(name))
mergo.Map(&changes, tmp) // need to sort error handling
}
b, err := c.opts.Encoder.Encode(changes)
if err != nil {
return nil, err
}
cs := &source.ChangeSet{
Format: c.opts.Encoder.String(),
Data: b,
Timestamp: time.Now(),
Source: c.String(),
}
cs.Checksum = cs.Sum()
return cs, nil
}
func toEntry(name string, v interface{}) map[string]interface{} {
n := strings.ToLower(name)
keys := strings.FieldsFunc(n, split)
reverse(keys)
tmp := make(map[string]interface{})
for i, k := range keys {
if i == 0 {
tmp[k] = v
continue
}
tmp = map[string]interface{}{k: tmp}
}
return tmp
}
func reverse(ss []string) {
for i := len(ss)/2 - 1; i >= 0; i-- {
opp := len(ss) - 1 - i
ss[i], ss[opp] = ss[opp], ss[i]
}
}
func split(r rune) bool {
return r == '-' || r == '_'
}
func (c *cliSource) Watch() (source.Watcher, error) {
return source.NewNoopWatcher()
}
func (c *cliSource) String() string {
return "cli"
}
// NewSource returns a config source for integrating parsed flags from a micro/cli.Context.
// Hyphens are delimiters for nesting, and all keys are lowercased. The assumption is that
// command line flags have already been parsed.
//
// Example:
// cli.StringFlag{Name: "db-host"},
//
//
// {
// "database": {
// "host": "localhost"
// }
// }
func NewSource(opts ...source.Option) source.Source {
options := source.NewOptions(opts...)
var ctx *cli.Context
c, ok := options.Context.Value(contextKey{}).(*cli.Context)
if ok {
ctx = c
}
// no context
if ctx == nil {
// get the default app/flags
app := cmd.App()
flags := app.Flags
// create flagset
set := flag.NewFlagSet(app.Name, flag.ContinueOnError)
// apply flags to set
for _, f := range flags {
f.Apply(set)
}
// parse flags
set.SetOutput(ioutil.Discard)
set.Parse(os.Args[1:])
// normalise flags
normalizeFlags(app.Flags, set)
// create context
ctx = cli.NewContext(app, set, nil)
}
return &cliSource{
ctx: ctx,
opts: options,
}
}
// WithContext returns a new source with the context specified.
// The assumption is that Context is retrieved within an app.Action function.
func WithContext(ctx *cli.Context, opts ...source.Option) source.Source {
return &cliSource{
ctx: ctx,
opts: source.NewOptions(opts...),
}
}

View File

@ -0,0 +1,65 @@
package cli
import (
"encoding/json"
"os"
"testing"
"github.com/micro/cli"
"github.com/micro/go-micro/cmd"
"github.com/micro/go-micro/config/source"
)
func test(t *testing.T, withContext bool) {
var src source.Source
// setup app
app := cmd.App()
app.Name = "testapp"
app.Flags = []cli.Flag{
cli.StringFlag{Name: "db-host"},
}
// with context
if withContext {
// set action
app.Action = func(c *cli.Context) {
src = WithContext(c)
}
// run app
app.Run([]string{"run", "-db-host", "localhost"})
// no context
} else {
// set args
os.Args = []string{"run", "-db-host", "localhost"}
src = NewSource()
}
// test config
c, err := src.Read()
if err != nil {
t.Error(err)
}
var actual map[string]interface{}
if err := json.Unmarshal(c.Data, &actual); err != nil {
t.Error(err)
}
actualDB := actual["db"].(map[string]interface{})
if actualDB["host"] != "localhost" {
t.Errorf("expected localhost, got %v", actualDB["name"])
}
}
func TestCliSource(t *testing.T) {
// without context
test(t, false)
}
func TestCliSourceWithContext(t *testing.T) {
// with context
test(t, true)
}

View File

@ -0,0 +1,20 @@
package cli
import (
"context"
"github.com/micro/cli"
"github.com/micro/go-micro/config/source"
)
type contextKey struct{}
// Context sets the cli context
func Context(c *cli.Context) source.Option {
return func(o *source.Options) {
if o.Context == nil {
o.Context = context.Background()
}
o.Context = context.WithValue(o.Context, contextKey{}, c)
}
}

50
config/source/cli/util.go Normal file
View File

@ -0,0 +1,50 @@
package cli
import (
"errors"
"flag"
"strings"
"github.com/micro/cli"
)
func copyFlag(name string, ff *flag.Flag, set *flag.FlagSet) {
switch ff.Value.(type) {
case *cli.StringSlice:
default:
set.Set(name, ff.Value.String())
}
}
func normalizeFlags(flags []cli.Flag, set *flag.FlagSet) error {
visited := make(map[string]bool)
set.Visit(func(f *flag.Flag) {
visited[f.Name] = true
})
for _, f := range flags {
parts := strings.Split(f.GetName(), ",")
if len(parts) == 1 {
continue
}
var ff *flag.Flag
for _, name := range parts {
name = strings.Trim(name, " ")
if visited[name] {
if ff != nil {
return errors.New("Cannot use two forms of the same flag: " + name + " " + ff.Name)
}
ff = set.Lookup(name)
}
}
if ff == nil {
continue
}
for _, name := range parts {
name = strings.Trim(name, " ")
if !visited[name] {
copyFlag(name, ff, set)
}
}
}
return nil
}