Initial commit

This commit is contained in:
Donny
2019-04-22 20:46:32 +08:00
commit 49ab8aadd1
25441 changed files with 4055000 additions and 0 deletions

52
library/conf/BUILD Normal file
View File

@@ -0,0 +1,52 @@
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
"go_test",
)
package(default_visibility = ["//visibility:public"])
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [
":package-srcs",
"//library/conf/dsn:all-srcs",
"//library/conf/env:all-srcs",
"//library/conf/flagvar:all-srcs",
"//library/conf/paladin:all-srcs",
],
tags = ["automanaged"],
)
go_library(
name = "go_default_library",
srcs = [
"client.go",
"client_v2.go",
],
importpath = "go-common/library/conf",
tags = ["automanaged"],
deps = [
"//library/conf/env:go_default_library",
"//library/ecode:go_default_library",
"//library/log:go_default_library",
],
)
go_test(
name = "go_default_test",
srcs = [
"client_test.go",
"client_v2_test.go",
],
embed = [":go_default_library"],
rundir = ".",
tags = ["automanaged"],
)

452
library/conf/client.go Normal file
View File

@@ -0,0 +1,452 @@
package conf
import (
"bytes"
"crypto/md5"
"encoding/hex"
"encoding/json"
"flag"
"fmt"
"io/ioutil"
"net"
"net/http"
"net/url"
"os"
"path"
"strings"
"sync/atomic"
"time"
"go-common/library/conf/env"
"go-common/library/log"
)
const (
// code
_codeOk = 0
_codeNotModified = -304
// api
_apiGet = "http://%s/v1/config/get2?%s"
_apiCheck = "http://%s/v1/config/check?%s"
// timeout
_retryInterval = 1 * time.Second
_httpTimeout = 60 * time.Second
_unknownVersion = -1
commonKey = "common.toml"
)
var (
conf config
)
type version struct {
Code int `json:"code"`
Message string `json:"message"`
Data *struct {
Version int64 `json:"version"`
} `json:"data"`
}
type result struct {
Code int `json:"code"`
Message string `json:"message"`
Data *data `json:"data"`
}
type data struct {
Version int64 `json:"version"`
Content string `json:"content"`
Md5 string `json:"md5"`
}
// Namespace the key-value config object.
type Namespace struct {
Name string `json:"name"`
Data map[string]string `json:"data"`
}
type config struct {
Svr string
Ver string
Path string
Filename string
Host string
Addr string
Env string
Token string
Appoint string
// NOTE: new caster
Region string
Zone string
AppID string
DeployEnv string
TreeID string
}
// Client is config client.
type Client struct {
ver int64 // NOTE: for config v1
diff *ver // NOTE: for config v2
customize string
httpCli *http.Client
data atomic.Value
event chan string
useV2 bool
watchFile map[string]struct{}
watchAll bool
}
func init() {
// env
conf.Svr = os.Getenv("CONF_APPID")
conf.Ver = os.Getenv("CONF_VERSION")
conf.Addr = os.Getenv("CONF_HOST")
conf.Host = os.Getenv("CONF_HOSTNAME")
conf.Path = os.Getenv("CONF_PATH")
conf.Env = os.Getenv("CONF_ENV")
conf.Token = os.Getenv("CONF_TOKEN")
conf.Appoint = os.Getenv("CONF_APPOINT")
conf.Region = os.Getenv("REGION")
conf.Zone = os.Getenv("ZONE")
conf.AppID = os.Getenv("APP_ID")
conf.DeployEnv = os.Getenv("DEPLOY_ENV")
conf.TreeID = os.Getenv("TREE_ID")
// flags
hostname, _ := os.Hostname()
flag.StringVar(&conf.Svr, "conf_appid", conf.Svr, `app name.`)
flag.StringVar(&conf.Ver, "conf_version", conf.Ver, `app version.`)
flag.StringVar(&conf.Addr, "conf_host", conf.Addr, `config center api host.`)
flag.StringVar(&conf.Host, "conf_hostname", hostname, `hostname.`)
flag.StringVar(&conf.Path, "conf_path", conf.Path, `config file path.`)
flag.StringVar(&conf.Env, "conf_env", conf.Env, `config Env.`)
flag.StringVar(&conf.Token, "conf_token", conf.Token, `config Token.`)
flag.StringVar(&conf.Appoint, "conf_appoint", conf.Appoint, `config Appoint.`)
/*
flag.StringVar(&conf.Region, "region", conf.Region, `region.`)
flag.StringVar(&conf.Zone, "zone", conf.Zone, `zone.`)
flag.StringVar(&conf.AppID, "app_id", conf.AppID, `app id.`)
flag.StringVar(&conf.DeployEnv, "deploy_env", conf.DeployEnv, `deploy env.`)
*/
conf.Region = env.Region
conf.Zone = env.Zone
conf.AppID = env.AppID
conf.DeployEnv = env.DeployEnv
// FIXME(linli) remove treeid
flag.StringVar(&conf.TreeID, "tree_id", conf.TreeID, `tree id.`)
}
// New new a ugc config center client.
func New() (cli *Client, err error) {
cli = &Client{
httpCli: &http.Client{Timeout: _httpTimeout},
event: make(chan string, 10),
}
if conf.Svr != "" && conf.Host != "" && conf.Path != "" && conf.Addr != "" && conf.Ver != "" && conf.Env != "" && conf.Token != "" &&
(strings.HasPrefix(conf.Ver, "shsb") || (strings.HasPrefix(conf.Ver, "shylf"))) {
if err = cli.init(); err != nil {
return nil, err
}
go cli.updateproc()
return
}
if conf.Zone != "" && conf.AppID != "" && conf.Host != "" && conf.Path != "" && conf.Addr != "" && conf.Ver != "" && conf.DeployEnv != "" && conf.Token != "" {
if err = cli.init2(); err != nil {
return nil, err
}
go cli.updateproc2()
cli.useV2 = true
return
}
err = fmt.Errorf("at least one params is empty. app=%s, version=%s, hostname=%s, addr=%s, path=%s, Env=%s, Token =%s, DeployEnv=%s, TreeID=%s, appID=%s",
conf.Svr, conf.Ver, conf.Host, conf.Addr, conf.Path, conf.Env, conf.Token, conf.DeployEnv, conf.TreeID, conf.AppID)
return
}
// Path get confFile Path.
func (c *Client) Path() string {
return conf.Path
}
// Toml return config value.
func (c *Client) Toml() (cf string, ok bool) {
if c.useV2 {
return c.Toml2()
}
var (
m map[string]*Namespace
n *Namespace
)
if m, ok = c.data.Load().(map[string]*Namespace); !ok {
return
}
if n, ok = m[""]; !ok {
return
}
cf, ok = n.Data[commonKey]
return
}
// Value return config value.
func (c *Client) Value(key string) (cf string, ok bool) {
if c.useV2 {
return c.Value2(key)
}
var (
m map[string]*Namespace
n *Namespace
)
if m, ok = c.data.Load().(map[string]*Namespace); !ok {
return
}
if n, ok = m[""]; !ok {
return
}
cf, ok = n.Data[key]
return
}
// SetCustomize set customize value.
func (c *Client) SetCustomize(value string) {
c.customize = value
}
// Event client update event.
func (c *Client) Event() <-chan string {
return c.event
}
// Watch watch filename change.
func (c *Client) Watch(filename ...string) {
if c.watchFile == nil {
c.watchFile = map[string]struct{}{}
}
for _, f := range filename {
c.watchFile[f] = struct{}{}
}
}
// WatchAll watch all filename change.
func (c *Client) WatchAll() {
c.watchAll = true
}
// checkLocal check local config is ok
func (c *Client) init() (err error) {
var ver int64
if ver, err = c.checkVersion(_unknownVersion); err != nil {
fmt.Printf("get remote version error(%v)\n", err)
return
}
for i := 0; i < 3; i++ {
if ver == _unknownVersion {
fmt.Println("get null version")
return
}
if err = c.download(ver); err == nil {
return
}
fmt.Printf("retry times: %d, c.download() error(%v)\n", i, err)
time.Sleep(_retryInterval)
}
return
}
func (c *Client) updateproc() (err error) {
var ver int64
for {
time.Sleep(_retryInterval)
if ver, err = c.checkVersion(c.ver); err != nil {
log.Error("c.checkVersion(%d) error(%v)", c.ver, err)
continue
} else if ver == c.ver {
continue
}
if err = c.download(ver); err != nil {
log.Error("c.download() error(%s)", err)
continue
}
c.event <- ""
}
}
// download download config from config service
func (c *Client) download(ver int64) (err error) {
var data *data
if data, err = c.getConfig(ver); err != nil {
return
}
return c.update(data)
}
// poll config server
func (c *Client) checkVersion(reqVer int64) (ver int64, err error) {
var (
url string
req *http.Request
resp *http.Response
rb []byte
)
if url = c.makeURL(_apiCheck, reqVer); url == "" {
err = fmt.Errorf("checkVersion() c.makeUrl() error url empty")
return
}
// http
if req, err = http.NewRequest("GET", url, nil); err != nil {
return
}
if resp, err = c.httpCli.Do(req); err != nil {
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
err = fmt.Errorf("checkVersion() http error url(%s) status: %d", url, resp.StatusCode)
return
}
// ok
if rb, err = ioutil.ReadAll(resp.Body); err != nil {
return
}
v := &version{}
if err = json.Unmarshal(rb, v); err != nil {
return
}
switch v.Code {
case _codeOk:
if v.Data == nil {
err = fmt.Errorf("checkVersion() response error result: %v", v)
return
}
ver = v.Data.Version
case _codeNotModified:
ver = reqVer
default:
err = fmt.Errorf("checkVersion() response error result: %v", v)
}
return
}
// updateVersion update config version
func (c *Client) getConfig(ver int64) (data *data, err error) {
var (
url string
req *http.Request
resp *http.Response
rb []byte
res = &result{}
)
if url = c.makeURL(_apiGet, ver); url == "" {
err = fmt.Errorf("getConfig() c.makeUrl() error url empty")
return
}
// http
if req, err = http.NewRequest("GET", url, nil); err != nil {
return
}
if resp, err = c.httpCli.Do(req); err != nil {
return
}
defer resp.Body.Close()
// ok
if resp.StatusCode != http.StatusOK {
err = fmt.Errorf("getConfig() http error url(%s) status: %d", url, resp.StatusCode)
return
}
if rb, err = ioutil.ReadAll(resp.Body); err != nil {
return
}
if err = json.Unmarshal(rb, res); err != nil {
return
}
switch res.Code {
case _codeOk:
// has new config
if res.Data == nil {
err = fmt.Errorf("getConfig() response error result: %v", res)
return
}
data = res.Data
default:
err = fmt.Errorf("getConfig() response error result: %v", res)
}
return
}
// update write config
func (c *Client) update(d *data) (err error) {
var (
tmp = make(map[string]*Namespace)
bs = []byte(d.Content)
buf = new(bytes.Buffer)
n *Namespace
ok bool
)
// md5 file
if mh := md5.Sum(bs); hex.EncodeToString(mh[:]) != d.Md5 {
err = fmt.Errorf("md5 mismatch, local:%s, remote:%s", hex.EncodeToString(mh[:]), d.Md5)
return
}
// write conf
if err = json.Unmarshal(bs, &tmp); err != nil {
return
}
for _, value := range tmp {
for k, v := range value.Data {
if strings.Contains(k, ".toml") {
buf.WriteString(v)
buf.WriteString("\n")
}
if err = ioutil.WriteFile(path.Join(conf.Path, k), []byte(v), 0644); err != nil {
return
}
}
}
if n, ok = tmp[""]; !ok {
n = &Namespace{Data: make(map[string]string)}
tmp[""] = n
}
n.Data[commonKey] = buf.String()
// update current version
c.ver = d.Version
c.data.Store(tmp)
return
}
// makeUrl signed url
func (c *Client) makeURL(api string, ver int64) (query string) {
params := url.Values{}
// service
params.Set("service", conf.Svr)
params.Set("hostname", conf.Host)
params.Set("build", conf.Ver)
params.Set("version", fmt.Sprint(ver))
params.Set("ip", localIP())
params.Set("environment", conf.Env)
params.Set("token", conf.Token)
params.Set("appoint", conf.Appoint)
params.Set("customize", c.customize)
// api
query = fmt.Sprintf(api, conf.Addr, params.Encode())
return
}
// localIP return local IP of the host.
func localIP() string {
addrs, err := net.InterfaceAddrs()
if err != nil {
return ""
}
for _, address := range addrs {
// check the address type and if it is not a loopback the display it
if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
if ipnet.IP.To4() != nil {
return ipnet.IP.String()
}
}
}
return ""
}

View File

@@ -0,0 +1,90 @@
package conf
import (
"net/http"
"testing"
)
func TestConf_client(t *testing.T) {
c := initConf()
testClientValue(t, c)
testCheckVersion(t, c)
testUpdate(t, c)
testDownload(t, c)
testGetConfig(t, c)
}
func TestClientNew(t *testing.T) {
initConf()
if _, err := New(); err != nil {
t.Errorf("client.New() error(%v)", err)
t.FailNow()
}
}
func testClientValue(t *testing.T, c *Client) {
key := "breaker"
testUpdate(t, c)
test1, ok := c.Value(key)
if !ok {
t.Errorf("client.Value() error")
t.FailNow()
}
t.Logf("get the result test1(%s)", test1)
}
func testCheckVersion(t *testing.T, c *Client) {
ver, err := c.checkVersion(_unknownVersion)
if err != nil && ver == _unknownVersion {
t.Errorf("client.checkVersion() error(%v) ver(%d)", err, ver)
t.FailNow()
}
}
func testDownload(t *testing.T, c *Client) {
ver := int64(102)
if err := c.download(ver); err != nil {
t.Errorf("client.downloda() error(%v) ", err)
t.FailNow()
}
}
func testUpdate(t *testing.T, c *Client) {
data := &data{
Version: 199,
Content: "{\"\":{\"name\":\"\",\"data\":{\"breaker\":\"fuck778\",\"degrade\":\"shit233333\"}},\"redis\":{\"name\":\"redis\",\"data\":{\"444\":\"555\",\"address\":\"172.123.0\",\"array\":\"4,12,test,4\",\"float\":\"3.123\",\"router\":\"test=1,fuck=shit,abc=test\",\"switch\":\"true\",\"timeout\":\"30s\"}}}",
Md5: "0843192c43148cbbf43aabb24e3e6442",
}
if err := c.update(data); err != nil {
t.Errorf("client.update() error(%v)", err)
t.FailNow()
}
}
func testGetConfig(t *testing.T, c *Client) {
ver := int64(102)
data, err := c.getConfig(ver)
if err != nil {
t.Errorf("client.getconfiig() error(%v)", err)
t.FailNow()
}
t.Logf("get the result data(%v)", data)
}
func initConf() (c *Client) {
conf.Addr = "172.16.33.134:9011"
conf.Host = "testHost"
conf.Path = "./"
conf.Svr = "config_test"
conf.Ver = "shsb-docker-1"
conf.Env = "10"
conf.Token = "qmVUPwNXnNfcSpuyqbiIBb0H4GcbSZFV"
//conf.Appoint = "88"
c = &Client{
httpCli: &http.Client{Timeout: _httpTimeout},
event: make(chan string, 10),
}
c.data.Store(make(map[string]*Namespace))
return
}

435
library/conf/client_v2.go Normal file
View File

@@ -0,0 +1,435 @@
package conf
import (
"bytes"
"crypto/md5"
"encoding/hex"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"path"
"strings"
"time"
"go-common/library/ecode"
"go-common/library/log"
)
const (
// api
_apiGet1 = "http://%s/config/v2/get?%s"
_apiCheck1 = "http://%s/config/v2/check?%s"
_apiCreate = "http://%s/config/v2/create"
_apiUpdate = "http://%s/config/v2/update"
_apiConfIng = "http://%s/config/v2/config/ing?%s"
)
type version1 struct {
Code int `json:"code"`
Message string `json:"message"`
Data *ver `json:"data"`
}
type ver struct {
Version int64 `json:"version"`
Diffs []int64 `json:"diffs"`
}
type confIng struct {
Code int `json:"code"`
Message string `json:"message"`
Data *Value `json:"data"`
}
type res struct {
Code int `json:"code"`
Message string `json:"message"`
}
//Value value.
type Value struct {
CID int64 `json:"cid"`
Name string `json:"name"`
Config string `json:"config"`
}
// Toml2 return config value.
func (c *Client) Toml2() (cf string, ok bool) {
var (
m map[string]*Value
val *Value
)
if m, ok = c.data.Load().(map[string]*Value); !ok {
return
}
if val, ok = m[commonKey]; !ok {
return
}
cf = val.Config
return
}
// Value2 return config value.
func (c *Client) Value2(key string) (cf string, ok bool) {
var (
m map[string]*Value
val *Value
)
if m, ok = c.data.Load().(map[string]*Value); !ok {
return
}
if val, ok = m[key]; !ok {
return
}
cf = val.Config
return
}
// init check local config is ok
func (c *Client) init2() (err error) {
var v *ver
c.data.Store(make(map[string]*Value))
if v, err = c.checkVersion2(&ver{Version: _unknownVersion}); err != nil {
fmt.Printf("get remote version error(%v)\n", err)
return
}
for i := 0; i < 3; i++ {
if v.Version == _unknownVersion {
fmt.Println("get null version")
return
}
if err = c.download2(v, true); err == nil {
return
}
fmt.Printf("retry times: %d, c.download() error(%v)\n", i, err)
time.Sleep(_retryInterval)
}
return
}
func (c *Client) updateproc2() (err error) {
var ver *ver
for {
time.Sleep(_retryInterval)
if ver, err = c.checkVersion2(c.diff); err != nil {
log.Error("c.checkVersion(%d) error(%v)", c.ver, err)
continue
} else if ver.Version == c.diff.Version {
continue
}
if err = c.download2(ver, false); err != nil {
log.Error("c.download() error(%s)", err)
continue
}
}
}
// download download config from config service
func (c *Client) download2(ver *ver, isFirst bool) (err error) {
var (
d *data
tmp []*Value
oConfs, confs map[string]*Value
buf = new(bytes.Buffer)
ok bool
)
if d, err = c.getConfig2(ver); err != nil {
return
}
bs := []byte(d.Content)
// md5 file
if mh := md5.Sum(bs); hex.EncodeToString(mh[:]) != d.Md5 {
err = fmt.Errorf("md5 mismatch, local:%s, remote:%s", hex.EncodeToString(mh[:]), d.Md5)
return
}
// write conf
if err = json.Unmarshal(bs, &tmp); err != nil {
return
}
confs = make(map[string]*Value)
if oConfs, ok = c.data.Load().(map[string]*Value); ok {
for k, v := range oConfs {
confs[k] = v
}
}
for _, v := range tmp {
if err = ioutil.WriteFile(path.Join(conf.Path, v.Name), []byte(v.Config), 0644); err != nil {
return
}
confs[v.Name] = v
}
for _, v := range confs {
if strings.Contains(v.Name, ".toml") {
buf.WriteString(v.Config)
buf.WriteString("\n")
}
}
confs[commonKey] = &Value{Config: buf.String()}
// update current version
c.diff = ver
c.data.Store(confs)
if isFirst {
return
}
for _, v := range tmp {
if c.watchAll {
c.event <- v.Name
continue
}
if c.watchFile == nil {
continue
}
if _, ok := c.watchFile[v.Name]; ok {
c.event <- v.Name
}
}
return
}
// poll config server
func (c *Client) checkVersion2(reqVer *ver) (ver *ver, err error) {
var (
url string
req *http.Request
resp *http.Response
rb []byte
)
if url, err = c.makeURL2(_apiCheck1, reqVer); err != nil {
err = fmt.Errorf("checkVersion() c.makeUrl() error url empty")
return
}
// http
if req, err = http.NewRequest("GET", url, nil); err != nil {
return
}
if resp, err = c.httpCli.Do(req); err != nil {
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
err = fmt.Errorf("checkVersion() http error url(%s) status: %d", url, resp.StatusCode)
return
}
// ok
if rb, err = ioutil.ReadAll(resp.Body); err != nil {
return
}
v := &version1{}
if err = json.Unmarshal(rb, v); err != nil {
return
}
switch v.Code {
case _codeOk:
if v.Data == nil {
err = fmt.Errorf("checkVersion() response error result: %v", v)
return
}
ver = v.Data
case _codeNotModified:
ver = reqVer
default:
err = fmt.Errorf("checkVersion() response error result: %v", v)
}
return
}
// updateVersion update config version
func (c *Client) getConfig2(ver *ver) (data *data, err error) {
var (
url string
req *http.Request
resp *http.Response
rb []byte
res = &result{}
)
if url, err = c.makeURL2(_apiGet1, ver); err != nil {
err = fmt.Errorf("getConfig() c.makeUrl() error url empty")
return
}
// http
if req, err = http.NewRequest("GET", url, nil); err != nil {
return
}
if resp, err = c.httpCli.Do(req); err != nil {
return
}
defer resp.Body.Close()
// ok
if resp.StatusCode != http.StatusOK {
err = fmt.Errorf("getConfig() http error url(%s) status: %d", url, resp.StatusCode)
return
}
if rb, err = ioutil.ReadAll(resp.Body); err != nil {
return
}
if err = json.Unmarshal(rb, res); err != nil {
return
}
switch res.Code {
case _codeOk:
// has new config
if res.Data == nil {
err = fmt.Errorf("getConfig() response error result: %v", res)
return
}
data = res.Data
default:
err = fmt.Errorf("getConfig() response error result: %v", res)
}
return
}
// makeUrl signed url
func (c *Client) makeURL2(api string, ver *ver) (query string, err error) {
var ids []byte
params := url.Values{}
// service
params.Set("service", service())
params.Set("hostname", conf.Host)
params.Set("build", conf.Ver)
params.Set("version", fmt.Sprint(ver.Version))
if ids, err = json.Marshal(ver.Diffs); err != nil {
return
}
params.Set("ids", string(ids))
params.Set("ip", localIP())
params.Set("token", conf.Token)
params.Set("appoint", conf.Appoint)
params.Set("customize", c.customize)
// api
query = fmt.Sprintf(api, conf.Addr, params.Encode())
return
}
//Create create.
func (c *Client) Create(name, content, operator, mark string) (err error) {
var (
resp *http.Response
rb []byte
res = &res{}
)
params := url.Values{}
params.Set("service", service())
params.Set("name", name)
params.Set("content", content)
params.Set("operator", operator)
params.Set("mark", mark)
params.Set("token", conf.Token)
if resp, err = c.httpCli.PostForm(fmt.Sprintf(_apiCreate, conf.Addr), params); err != nil {
return
}
defer resp.Body.Close()
// ok
if resp.StatusCode != http.StatusOK {
err = fmt.Errorf("Create() http error url(%s) status: %d", fmt.Sprintf(_apiCreate, conf.Addr), resp.StatusCode)
return
}
if rb, err = ioutil.ReadAll(resp.Body); err != nil {
return
}
if err = json.Unmarshal(rb, res); err != nil {
return
}
if res.Code != ecode.OK.Code() {
err = ecode.Int(res.Code)
}
return
}
//Update update.
func (c *Client) Update(ID int64, content, operator, mark string) (err error) {
var (
resp *http.Response
rb []byte
res = &result{}
)
params := url.Values{}
params.Set("conf_id", fmt.Sprintf("%d", ID))
params.Set("content", content)
params.Set("operator", operator)
params.Set("mark", mark)
params.Set("service", service())
params.Set("token", conf.Token)
if resp, err = c.httpCli.PostForm(fmt.Sprintf(_apiUpdate, conf.Addr), params); err != nil {
return
}
defer resp.Body.Close()
// ok
if resp.StatusCode != http.StatusOK {
err = fmt.Errorf("Update() http error url(%s) status: %d", fmt.Sprintf(_apiUpdate, conf.Addr), resp.StatusCode)
return
}
if rb, err = ioutil.ReadAll(resp.Body); err != nil {
return
}
if err = json.Unmarshal(rb, res); err != nil {
return
}
if res.Code != ecode.OK.Code() {
err = ecode.Int(res.Code)
}
return
}
//ConfIng confIng.
func (c *Client) ConfIng(name string) (v *Value, err error) {
var (
req *http.Request
resp *http.Response
rb []byte
res = &confIng{}
)
params := url.Values{}
params.Set("name", name)
params.Set("service", service())
params.Set("token", conf.Token)
// http
if req, err = http.NewRequest("GET", fmt.Sprintf(_apiConfIng, conf.Addr, params.Encode()), nil); err != nil {
return
}
if resp, err = c.httpCli.Do(req); err != nil {
return
}
defer resp.Body.Close()
// ok
if resp.StatusCode != http.StatusOK {
err = fmt.Errorf("ConfIng() http error url(%s) status: %d", _apiCreate, resp.StatusCode)
return
}
if rb, err = ioutil.ReadAll(resp.Body); err != nil {
return
}
if err = json.Unmarshal(rb, res); err != nil {
return
}
if res.Code != ecode.OK.Code() {
err = ecode.Int(res.Code)
return
}
v = res.Data
return
}
//Configs configs.
func (c *Client) Configs() (confs []*Value, ok bool) {
var (
m map[string]*Value
)
if m, ok = c.data.Load().(map[string]*Value); !ok {
return
}
for _, v := range m {
if v.CID == 0 {
continue
}
confs = append(confs, v)
}
return
}
func service() string {
return fmt.Sprintf("%s_%s_%s", conf.TreeID, conf.DeployEnv, conf.Zone)
}

View File

@@ -0,0 +1,96 @@
package conf
import (
"net/http"
"testing"
)
func TestConf_client2(t *testing.T) {
c := initConf2()
testClientValue2(t, c)
testCheckVersion2(t, c)
testDownload2(t, c)
testGetConfig2(t, c)
}
func testClientValue2(t *testing.T, c *Client) {
key := "test.toml"
testDownload2(t, c)
test1, ok := c.Value2(key)
if !ok {
t.Errorf("client.Value() error")
t.FailNow()
}
t.Logf("get the result test1(%s)", test1)
}
func testCheckVersion2(t *testing.T, c *Client) {
unknow := &ver{Version: _unknownVersion}
ver, err := c.checkVersion2(unknow)
if err != nil {
t.Errorf("client.checkVersion() error(%v) ver(%d)", err, ver)
t.FailNow()
}
}
func testDownload2(t *testing.T, c *Client) {
ver := &ver{Version: 13}
if err := c.download2(ver, true); err != nil {
t.Errorf("client.downloda() error(%v) ", err)
t.FailNow()
}
}
func testGetConfig2(t *testing.T, c *Client) {
ver := &ver{Version: 13}
data, err := c.getConfig2(ver)
if err != nil {
t.Errorf("client.getconfiig() error(%v)", err)
t.FailNow()
}
t.Logf("get the result data(%v)", data)
}
func TestClient_Create(t *testing.T) {
c := initConf2()
if err := c.Create("zjx11.toml", "test comment", "zjx", "mark"); err != nil {
t.Errorf("client.Create() error(%v)", err)
t.FailNow()
}
}
func TestClient_Update(t *testing.T) {
c := initConf2()
if err := c.Update(21, "test comment11", "zjx", "mark"); err != nil {
t.Errorf("client.Create() error(%v)", err)
t.FailNow()
}
}
func TestClient_ConfIng(t *testing.T) {
c := initConf2()
if val, err := c.ConfIng("zjx1.toml"); err != nil {
t.Errorf("client.Create() error(%v)", err)
t.FailNow()
} else {
t.Logf("%v", val)
}
}
func initConf2() (c *Client) {
conf.Addr = "172.16.33.134:9011"
conf.Host = "testHost"
conf.Path = "./"
conf.AppID = "main.common-arch.msm-service"
conf.Svr = "msm-service"
conf.Ver = "server-1"
conf.DeployEnv = "dev"
conf.Zone = "sh001"
conf.Token = "45338e440bdc11e880ce02420a0a0204"
conf.TreeID = "2888"
c = &Client{
httpCli: &http.Client{Timeout: _httpTimeout},
event: make(chan string, 10),
}
return
}

56
library/conf/dsn/BUILD Normal file
View File

@@ -0,0 +1,56 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_test",
"go_library",
)
go_test(
name = "go_default_test",
srcs = [
"dsn_test.go",
"query_test.go",
],
embed = [":go_default_library"],
rundir = ".",
tags = ["automanaged"],
deps = ["//library/time:go_default_library"],
)
go_library(
name = "go_default_library",
srcs = [
"doc.go",
"dsn.go",
"query.go",
],
importpath = "go-common/library/conf/dsn",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = ["//vendor/gopkg.in/go-playground/validator.v9:go_default_library"],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)
go_test(
name = "go_default_xtest",
srcs = ["example_test.go"],
tags = ["automanaged"],
deps = [
"//library/conf/dsn:go_default_library",
"//library/time:go_default_library",
],
)

63
library/conf/dsn/doc.go Normal file
View File

@@ -0,0 +1,63 @@
// Package dsn implements dsn parse with struct bind
/*
DSN 格式类似 URI, DSN 结构如下图
network:[//[username[:password]@]address[:port][,address[:port]]][/path][?query][#fragment]
与 URI 的主要区别在于 scheme 被替换为 network, host 被替换为 address 并且支持多个 address.
network 与 net 包中 network 意义相同, tcp、udp、unix 等, address 支持多个使用 ',' 分割, 如果
network 为 unix 等本地 sock 协议则使用 Path, 有且只有一个
dsn 包主要提供了 Parse, Bind 和 validate 功能
Parse 解析 dsn 字符串成 DSN struct, DSN struct 与 url.URL 几乎完全一样
Bind 提供将 DSN 数据绑定到一个 struct 的功能, 通过 tag dsn:"key,[default]" 指定绑定的字段, 目前支持两种类型的数据绑定
内置变量 key:
network string tcp, udp, unix 等, 参考 net 包中的 network
username string
password string
address string or []string address 可以绑定到 string 或者 []string, 如果为 string 则取 address 第一个
Query: 通过 query.name 可以取到 query 上的数据
数组可以通过传递多个获得
array=1&array=2&array3 -> []int `tag:"query.array"`
struct 支持嵌套
foo.sub.name=hello&foo.tm=hello
struct Foo {
Tm string `dsn:"query.tm"`
Sub struct {
Name string `dsn:"query.name"`
} `dsn:"query.sub"`
}
默认值: 通过 dsn:"key,[default]" 默认值暂时不支持数组
忽略 Bind: 通过 dsn:"-" 忽略 Bind
自定义 Bind: 可以同时实现 encoding.TextUnmarshaler 自定义 Bind 实现
Validate: 参考 https://github.com/go-playground/validator
使用参考: example_test.go
DSN 命名规范:
没有历史遗留的情况下,尽量使用 Address, Network, Username, Password 等命名,代替之前的 Proto 和 Addr 等命名
Query 命名参考, 使用驼峰小写开头:
timeout 通用超时
dialTimeout 连接建立超时
readTimeout 读操作超时
writeTimeout 写操作超时
readsTimeout 批量读超时
writesTimeout 批量写超时
*/
package dsn

108
library/conf/dsn/dsn.go Normal file
View File

@@ -0,0 +1,108 @@
// Package dsn provide parse dsn and bind to struct
// see http://git.bilibili.co/platform/go-common/issues/279
package dsn
import (
"net/url"
"reflect"
"strings"
"gopkg.in/go-playground/validator.v9"
)
var _validator *validator.Validate
func init() {
_validator = validator.New()
}
// DSN a DSN represents a parsed DSN as same as url.URL.
type DSN struct {
*url.URL
}
// Bind dsn to specify struct and validate use use go-playground/validator format
//
// The bind of each struct field can be customized by the format string
// stored under the 'dsn' key in the struct field's tag. The format string
// gives the name of the field, possibly followed by a comma-separated
// list of options. The name may be empty in order to specify options
// without overriding the default field name.
//
// A two type data you can bind to struct
// built-in values, use below keys to bind built-in value
// username
// password
// address
// network
// the value in query string, use query.{name} to bind value in query string
//
// As a special case, if the field tag is "-", the field is always omitted.
// NOTE: that a field with name "-" can still be generated using the tag "-,".
//
// Examples of struct field tags and their meanings:
// // Field bind username
// Field string `dsn:"username"`
// // Field is ignored by this package.
// Field string `dsn:"-"`
// // Field bind value from query
// Field string `dsn:"query.name"`
//
func (d *DSN) Bind(v interface{}) (url.Values, error) {
assignFuncs := make(map[string]assignFunc)
if d.User != nil {
username := d.User.Username()
password, ok := d.User.Password()
if ok {
assignFuncs["password"] = stringsAssignFunc(password)
}
assignFuncs["username"] = stringsAssignFunc(username)
}
assignFuncs["address"] = addressesAssignFunc(d.Addresses())
assignFuncs["network"] = stringsAssignFunc(d.Scheme)
query, err := bindQuery(d.Query(), v, assignFuncs)
if err != nil {
return nil, err
}
return query, _validator.Struct(v)
}
func addressesAssignFunc(addresses []string) assignFunc {
return func(v reflect.Value, to tagOpt) error {
if v.Kind() == reflect.String {
if addresses[0] == "" && to.Default != "" {
v.SetString(to.Default)
} else {
v.SetString(addresses[0])
}
return nil
}
if !(v.Kind() == reflect.Slice && v.Type().Elem().Kind() == reflect.String) {
return &BindTypeError{Value: strings.Join(addresses, ","), Type: v.Type()}
}
vals := reflect.MakeSlice(v.Type(), len(addresses), len(addresses))
for i, address := range addresses {
vals.Index(i).SetString(address)
}
if v.CanSet() {
v.Set(vals)
}
return nil
}
}
// Addresses parse host split by ','
// For Unix networks, return ['path']
func (d *DSN) Addresses() []string {
switch d.Scheme {
case "unix", "unixgram", "unixpacket":
return []string{d.Path}
}
return strings.Split(d.Host, ",")
}
// Parse parses rawdsn into a URL structure.
func Parse(rawdsn string) (*DSN, error) {
u, err := url.Parse(rawdsn)
return &DSN{URL: u}, err
}

View File

@@ -0,0 +1,79 @@
package dsn
import (
"net/url"
"reflect"
"testing"
"time"
xtime "go-common/library/time"
)
type config struct {
Network string `dsn:"network"`
Addresses []string `dsn:"address"`
Username string `dsn:"username"`
Password string `dsn:"password"`
Timeout xtime.Duration `dsn:"query.timeout"`
Sub Sub `dsn:"query.sub"`
Def string `dsn:"query.def,hello"`
}
type Sub struct {
Foo int `dsn:"query.foo"`
}
func TestBind(t *testing.T) {
var cfg config
rawdsn := "tcp://root:toor@172.12.23.34,178.23.34.45?timeout=1s&sub.foo=1&hello=world"
dsn, err := Parse(rawdsn)
if err != nil {
t.Fatal(err)
}
values, err := dsn.Bind(&cfg)
if err != nil {
t.Error(err)
}
if !reflect.DeepEqual(values, url.Values{"hello": {"world"}}) {
t.Errorf("unexpect values get %v", values)
}
cfg2 := config{
Network: "tcp",
Addresses: []string{"172.12.23.34", "178.23.34.45"},
Password: "toor",
Username: "root",
Sub: Sub{Foo: 1},
Timeout: xtime.Duration(time.Second),
Def: "hello",
}
if !reflect.DeepEqual(cfg, cfg2) {
t.Errorf("unexpect config get %v, expect %v", cfg, cfg2)
}
}
type config2 struct {
Network string `dsn:"network"`
Address string `dsn:"address"`
Timeout xtime.Duration `dsn:"query.timeout"`
}
func TestUnix(t *testing.T) {
var cfg config2
rawdsn := "unix:///run/xxx.sock?timeout=1s&sub.foo=1&hello=world"
dsn, err := Parse(rawdsn)
if err != nil {
t.Fatal(err)
}
_, err = dsn.Bind(&cfg)
if err != nil {
t.Error(err)
}
cfg2 := config2{
Network: "unix",
Address: "/run/xxx.sock",
Timeout: xtime.Duration(time.Second),
}
if !reflect.DeepEqual(cfg, cfg2) {
t.Errorf("unexpect config2 get %v, expect %v", cfg, cfg2)
}
}

View File

@@ -0,0 +1,31 @@
package dsn_test
import (
"log"
"go-common/library/conf/dsn"
xtime "go-common/library/time"
)
// Config struct
type Config struct {
Network string `dsn:"network" validate:"required"`
Host string `dsn:"host" validate:"required"`
Username string `dsn:"username" validate:"required"`
Password string `dsn:"password" validate:"required"`
Timeout xtime.Duration `dsn:"query.timeout,1s"`
Offset int `dsn:"query.offset" validate:"gte=0"`
}
func ExampleParse() {
cfg := &Config{}
d, err := dsn.Parse("tcp://root:toor@172.12.12.23:2233?timeout=10s")
if err != nil {
log.Fatal(err)
}
_, err = d.Bind(cfg)
if err != nil {
log.Fatal(err)
}
log.Printf("%v", cfg)
}

422
library/conf/dsn/query.go Normal file
View File

@@ -0,0 +1,422 @@
package dsn
import (
"encoding"
"net/url"
"reflect"
"runtime"
"strconv"
"strings"
)
const (
_tagID = "dsn"
_queryPrefix = "query."
)
// InvalidBindError describes an invalid argument passed to DecodeQuery.
// (The argument to DecodeQuery must be a non-nil pointer.)
type InvalidBindError struct {
Type reflect.Type
}
func (e *InvalidBindError) Error() string {
if e.Type == nil {
return "Bind(nil)"
}
if e.Type.Kind() != reflect.Ptr {
return "Bind(non-pointer " + e.Type.String() + ")"
}
return "Bind(nil " + e.Type.String() + ")"
}
// BindTypeError describes a query value that was
// not appropriate for a value of a specific Go type.
type BindTypeError struct {
Value string
Type reflect.Type
}
func (e *BindTypeError) Error() string {
return "cannot decode " + e.Value + " into Go value of type " + e.Type.String()
}
type assignFunc func(v reflect.Value, to tagOpt) error
func stringsAssignFunc(val string) assignFunc {
return func(v reflect.Value, to tagOpt) error {
if v.Kind() != reflect.String || !v.CanSet() {
return &BindTypeError{Value: "string", Type: v.Type()}
}
if val == "" {
v.SetString(to.Default)
} else {
v.SetString(val)
}
return nil
}
}
// bindQuery parses url.Values and stores the result in the value pointed to by v.
// if v is nil or not a pointer, bindQuery returns an InvalidDecodeError
func bindQuery(query url.Values, v interface{}, assignFuncs map[string]assignFunc) (url.Values, error) {
if assignFuncs == nil {
assignFuncs = make(map[string]assignFunc)
}
d := decodeState{
data: query,
used: make(map[string]bool),
assignFuncs: assignFuncs,
}
err := d.decode(v)
ret := d.unused()
return ret, err
}
type tagOpt struct {
Name string
Default string
}
func parseTag(tag string) tagOpt {
vs := strings.SplitN(tag, ",", 2)
if len(vs) == 2 {
return tagOpt{Name: vs[0], Default: vs[1]}
}
return tagOpt{Name: vs[0]}
}
type decodeState struct {
data url.Values
used map[string]bool
assignFuncs map[string]assignFunc
}
func (d *decodeState) unused() url.Values {
ret := make(url.Values)
for k, v := range d.data {
if !d.used[k] {
ret[k] = v
}
}
return ret
}
func (d *decodeState) decode(v interface{}) (err error) {
defer func() {
if r := recover(); r != nil {
if _, ok := r.(runtime.Error); ok {
panic(r)
}
err = r.(error)
}
}()
rv := reflect.ValueOf(v)
if rv.Kind() != reflect.Ptr || rv.IsNil() {
return &InvalidBindError{reflect.TypeOf(v)}
}
return d.root(rv)
}
func (d *decodeState) root(v reflect.Value) error {
var tu encoding.TextUnmarshaler
tu, v = d.indirect(v)
if tu != nil {
return tu.UnmarshalText([]byte(d.data.Encode()))
}
// TODO support map, slice as root
if v.Kind() != reflect.Struct {
return &BindTypeError{Value: d.data.Encode(), Type: v.Type()}
}
tv := v.Type()
for i := 0; i < tv.NumField(); i++ {
fv := v.Field(i)
field := tv.Field(i)
to := parseTag(field.Tag.Get(_tagID))
if to.Name == "-" {
continue
}
if af, ok := d.assignFuncs[to.Name]; ok {
if err := af(fv, tagOpt{}); err != nil {
return err
}
continue
}
if !strings.HasPrefix(to.Name, _queryPrefix) {
continue
}
to.Name = to.Name[len(_queryPrefix):]
if err := d.value(fv, "", to); err != nil {
return err
}
}
return nil
}
func combinekey(prefix string, to tagOpt) string {
key := to.Name
if prefix != "" {
key = prefix + "." + key
}
return key
}
func (d *decodeState) value(v reflect.Value, prefix string, to tagOpt) (err error) {
key := combinekey(prefix, to)
d.used[key] = true
var tu encoding.TextUnmarshaler
tu, v = d.indirect(v)
if tu != nil {
if val, ok := d.data[key]; ok {
return tu.UnmarshalText([]byte(val[0]))
}
if to.Default != "" {
return tu.UnmarshalText([]byte(to.Default))
}
return
}
switch v.Kind() {
case reflect.Bool:
err = d.valueBool(v, prefix, to)
case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int:
err = d.valueInt64(v, prefix, to)
case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint:
err = d.valueUint64(v, prefix, to)
case reflect.Float32, reflect.Float64:
err = d.valueFloat64(v, prefix, to)
case reflect.String:
err = d.valueString(v, prefix, to)
case reflect.Slice:
err = d.valueSlice(v, prefix, to)
case reflect.Struct:
err = d.valueStruct(v, prefix, to)
case reflect.Ptr:
if !d.hasKey(combinekey(prefix, to)) {
break
}
if !v.CanSet() {
break
}
nv := reflect.New(v.Type().Elem())
v.Set(nv)
err = d.value(nv, prefix, to)
}
return
}
func (d *decodeState) hasKey(key string) bool {
for k := range d.data {
if strings.HasPrefix(k, key+".") || k == key {
return true
}
}
return false
}
func (d *decodeState) valueBool(v reflect.Value, prefix string, to tagOpt) error {
key := combinekey(prefix, to)
val := d.data.Get(key)
if val == "" {
if to.Default == "" {
return nil
}
val = to.Default
}
return d.setBool(v, val)
}
func (d *decodeState) setBool(v reflect.Value, val string) error {
bval, err := strconv.ParseBool(val)
if err != nil {
return &BindTypeError{Value: val, Type: v.Type()}
}
v.SetBool(bval)
return nil
}
func (d *decodeState) valueInt64(v reflect.Value, prefix string, to tagOpt) error {
key := combinekey(prefix, to)
val := d.data.Get(key)
if val == "" {
if to.Default == "" {
return nil
}
val = to.Default
}
return d.setInt64(v, val)
}
func (d *decodeState) setInt64(v reflect.Value, val string) error {
ival, err := strconv.ParseInt(val, 10, 64)
if err != nil {
return &BindTypeError{Value: val, Type: v.Type()}
}
v.SetInt(ival)
return nil
}
func (d *decodeState) valueUint64(v reflect.Value, prefix string, to tagOpt) error {
key := combinekey(prefix, to)
val := d.data.Get(key)
if val == "" {
if to.Default == "" {
return nil
}
val = to.Default
}
return d.setUint64(v, val)
}
func (d *decodeState) setUint64(v reflect.Value, val string) error {
uival, err := strconv.ParseUint(val, 10, 64)
if err != nil {
return &BindTypeError{Value: val, Type: v.Type()}
}
v.SetUint(uival)
return nil
}
func (d *decodeState) valueFloat64(v reflect.Value, prefix string, to tagOpt) error {
key := combinekey(prefix, to)
val := d.data.Get(key)
if val == "" {
if to.Default == "" {
return nil
}
val = to.Default
}
return d.setFloat64(v, val)
}
func (d *decodeState) setFloat64(v reflect.Value, val string) error {
fval, err := strconv.ParseFloat(val, 64)
if err != nil {
return &BindTypeError{Value: val, Type: v.Type()}
}
v.SetFloat(fval)
return nil
}
func (d *decodeState) valueString(v reflect.Value, prefix string, to tagOpt) error {
key := combinekey(prefix, to)
val := d.data.Get(key)
if val == "" {
if to.Default == "" {
return nil
}
val = to.Default
}
return d.setString(v, val)
}
func (d *decodeState) setString(v reflect.Value, val string) error {
v.SetString(val)
return nil
}
func (d *decodeState) valueSlice(v reflect.Value, prefix string, to tagOpt) error {
key := combinekey(prefix, to)
strs, ok := d.data[key]
if !ok {
strs = strings.Split(to.Default, ",")
}
if len(strs) == 0 {
return nil
}
et := v.Type().Elem()
var setFunc func(reflect.Value, string) error
switch et.Kind() {
case reflect.Bool:
setFunc = d.setBool
case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int:
setFunc = d.setInt64
case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint:
setFunc = d.setUint64
case reflect.Float32, reflect.Float64:
setFunc = d.setFloat64
case reflect.String:
setFunc = d.setString
default:
return &BindTypeError{Type: et, Value: strs[0]}
}
vals := reflect.MakeSlice(v.Type(), len(strs), len(strs))
for i, str := range strs {
if err := setFunc(vals.Index(i), str); err != nil {
return err
}
}
if v.CanSet() {
v.Set(vals)
}
return nil
}
func (d *decodeState) valueStruct(v reflect.Value, prefix string, to tagOpt) error {
tv := v.Type()
for i := 0; i < tv.NumField(); i++ {
fv := v.Field(i)
field := tv.Field(i)
fto := parseTag(field.Tag.Get(_tagID))
if fto.Name == "-" {
continue
}
if af, ok := d.assignFuncs[fto.Name]; ok {
if err := af(fv, tagOpt{}); err != nil {
return err
}
continue
}
if !strings.HasPrefix(fto.Name, _queryPrefix) {
continue
}
fto.Name = fto.Name[len(_queryPrefix):]
if err := d.value(fv, to.Name, fto); err != nil {
return err
}
}
return nil
}
func (d *decodeState) indirect(v reflect.Value) (encoding.TextUnmarshaler, reflect.Value) {
v0 := v
haveAddr := false
if v.Kind() != reflect.Ptr && v.Type().Name() != "" && v.CanAddr() {
haveAddr = true
v = v.Addr()
}
for {
if v.Kind() == reflect.Interface && !v.IsNil() {
e := v.Elem()
if e.Kind() == reflect.Ptr && !e.IsNil() && e.Elem().Kind() == reflect.Ptr {
haveAddr = false
v = e
continue
}
}
if v.Kind() != reflect.Ptr {
break
}
if v.Elem().Kind() != reflect.Ptr && v.CanSet() {
break
}
if v.IsNil() {
v.Set(reflect.New(v.Type().Elem()))
}
if v.Type().NumMethod() > 0 {
if u, ok := v.Interface().(encoding.TextUnmarshaler); ok {
return u, reflect.Value{}
}
}
if haveAddr {
v = v0
haveAddr = false
} else {
v = v.Elem()
}
}
return nil, v
}

View File

@@ -0,0 +1,128 @@
package dsn
import (
"net/url"
"reflect"
"testing"
"time"
xtime "go-common/library/time"
)
type cfg1 struct {
Name string `dsn:"query.name"`
Def string `dsn:"query.def,hello"`
DefSlice []int `dsn:"query.defslice,1,2,3,4"`
Ignore string `dsn:"-"`
FloatNum float64 `dsn:"query.floatNum"`
}
type cfg2 struct {
Timeout xtime.Duration `dsn:"query.timeout"`
}
type cfg3 struct {
Username string `dsn:"username"`
Timeout xtime.Duration `dsn:"query.timeout"`
}
type cfg4 struct {
Timeout xtime.Duration `dsn:"query.timeout,1s"`
}
func TestDecodeQuery(t *testing.T) {
type args struct {
query url.Values
v interface{}
assignFuncs map[string]assignFunc
}
tests := []struct {
name string
args args
want url.Values
cfg interface{}
wantErr bool
}{
{
name: "test generic",
args: args{
query: url.Values{
"name": {"hello"},
"Ignore": {"test"},
"floatNum": {"22.33"},
"adb": {"123"},
},
v: &cfg1{},
},
want: url.Values{
"Ignore": {"test"},
"adb": {"123"},
},
cfg: &cfg1{
Name: "hello",
Def: "hello",
DefSlice: []int{1, 2, 3, 4},
FloatNum: 22.33,
},
},
{
name: "test go-common/library/time",
args: args{
query: url.Values{
"timeout": {"1s"},
},
v: &cfg2{},
},
want: url.Values{},
cfg: &cfg2{xtime.Duration(time.Second)},
},
{
name: "test empty go-common/library/time",
args: args{
query: url.Values{},
v: &cfg2{},
},
want: url.Values{},
cfg: &cfg2{},
},
{
name: "test go-common/library/time",
args: args{
query: url.Values{},
v: &cfg4{},
},
want: url.Values{},
cfg: &cfg4{xtime.Duration(time.Second)},
},
{
name: "test build-in value",
args: args{
query: url.Values{
"timeout": {"1s"},
},
v: &cfg3{},
assignFuncs: map[string]assignFunc{"username": stringsAssignFunc("hello")},
},
want: url.Values{},
cfg: &cfg3{
Timeout: xtime.Duration(time.Second),
Username: "hello",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := bindQuery(tt.args.query, tt.args.v, tt.args.assignFuncs)
if (err != nil) != tt.wantErr {
t.Errorf("DecodeQuery() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("DecodeQuery() = %v, want %v", got, tt.want)
}
if !reflect.DeepEqual(tt.args.v, tt.cfg) {
t.Errorf("DecodeQuery() = %v, want %v", tt.args.v, tt.cfg)
}
})
}
}

37
library/conf/env/BUILD vendored Normal file
View File

@@ -0,0 +1,37 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
"go_test",
)
go_library(
name = "go_default_library",
srcs = ["env.go"],
importpath = "go-common/library/conf/env",
tags = ["automanaged"],
visibility = ["//visibility:public"],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)
go_test(
name = "go_default_test",
srcs = ["env_test.go"],
embed = [":go_default_library"],
rundir = ".",
tags = ["automanaged"],
)

92
library/conf/env/env.go vendored Normal file
View File

@@ -0,0 +1,92 @@
// Package env get env & app config, all the public field must after init()
// finished and flag.Parse().
package env
import (
"flag"
"os"
)
// deploy env.
const (
DeployEnvDev = "dev"
DeployEnvFat1 = "fat1"
DeployEnvUat = "uat"
DeployEnvPre = "pre"
DeployEnvProd = "prod"
)
// env default value.
const (
// env
_region = "sh"
_zone = "sh001"
_deployEnv = "dev"
)
// env configuration.
var (
// Region avaliable region where app at.
Region string
// Zone avaliable zone where app at.
Zone string
// Hostname machine hostname.
Hostname string
// DeployEnv deploy env where app at.
DeployEnv string
// IP FIXME(haoguanwei) #240
IP = os.Getenv("POD_IP")
// AppID is global unique application id, register by service tree.
// such as main.arch.disocvery.
AppID string
// Color is the identification of different experimental group in one caster cluster.
Color string
)
// app default value.
const (
_httpPort = "8000"
_gorpcPort = "8099"
_grpcPort = "9000"
)
// app configraution.
var (
// HTTPPort app listen http port.
HTTPPort string
// GORPCPort app listen gorpc port.
GORPCPort string
// GRPCPort app listen grpc port.
GRPCPort string
)
func init() {
var err error
if Hostname, err = os.Hostname(); err != nil || Hostname == "" {
Hostname = os.Getenv("HOSTNAME")
}
addFlag(flag.CommandLine)
}
func addFlag(fs *flag.FlagSet) {
// env
fs.StringVar(&Region, "region", defaultString("REGION", _region), "avaliable region. or use REGION env variable, value: sh etc.")
fs.StringVar(&Zone, "zone", defaultString("ZONE", _zone), "avaliable zone. or use ZONE env variable, value: sh001/sh002 etc.")
fs.StringVar(&DeployEnv, "deploy.env", defaultString("DEPLOY_ENV", _deployEnv), "deploy env. or use DEPLOY_ENV env variable, value: dev/fat1/uat/pre/prod etc.")
fs.StringVar(&AppID, "appid", os.Getenv("APP_ID"), "appid is global unique application id, register by service tree. or use APP_ID env variable.")
fs.StringVar(&Color, "deploy.color", os.Getenv("DEPLOY_COLOR"), "deploy.color is the identification of different experimental group.")
// app
fs.StringVar(&HTTPPort, "http.port", defaultString("DISCOVERY_HTTP_PORT", _httpPort), "app listen http port, default: 8000")
fs.StringVar(&GORPCPort, "gorpc.port", defaultString("DISCOVERY_GORPC_PORT", _gorpcPort), "app listen gorpc port, default: 8099")
fs.StringVar(&GRPCPort, "grpc.port", defaultString("DISCOVERY_GRPC_PORT", _grpcPort), "app listen grpc port, default: 9000")
}
func defaultString(env, value string) string {
v := os.Getenv(env)
if v == "" {
return value
}
return v
}

122
library/conf/env/env_test.go vendored Normal file
View File

@@ -0,0 +1,122 @@
package env
import (
"flag"
"fmt"
"os"
"testing"
)
func TestDefaultString(t *testing.T) {
v := defaultString("a", "test")
if v != "test" {
t.Fatal("v must be test")
}
if err := os.Setenv("a", "test1"); err != nil {
t.Fatal(err)
}
v = defaultString("a", "test")
if v != "test1" {
t.Fatal("v must be test1")
}
}
func TestEnv(t *testing.T) {
tests := []struct {
flag string
env string
def string
val *string
}{
{
"region",
"REGION",
_region,
&Region,
},
{
"zone",
"ZONE",
_zone,
&Zone,
},
{
"deploy.env",
"DEPLOY_ENV",
_deployEnv,
&DeployEnv,
},
{
"appid",
"APP_ID",
"",
&AppID,
},
{
"http.port",
"DISCOVERY_HTTP_PORT",
_httpPort,
&HTTPPort,
},
{
"gorpc.port",
"DISCOVERY_GORPC_PORT",
_gorpcPort,
&GORPCPort,
},
{
"grpc.port",
"DISCOVERY_GRPC_PORT",
_grpcPort,
&GRPCPort,
},
{
"deploy.color",
"DEPLOY_COLOR",
"",
&Color,
},
}
for _, test := range tests {
// flag set value
t.Run(fmt.Sprintf("%s: flag set", test.env), func(t *testing.T) {
fs := flag.NewFlagSet("", flag.ContinueOnError)
addFlag(fs)
err := fs.Parse([]string{fmt.Sprintf("-%s=%s", test.flag, "test")})
if err != nil {
t.Fatal(err)
}
if *test.val != "test" {
t.Fatal("val must be test")
}
})
// flag not set, env set
t.Run(fmt.Sprintf("%s: flag not set, env set", test.env), func(t *testing.T) {
*test.val = ""
os.Setenv(test.env, "test2")
fs := flag.NewFlagSet("", flag.ContinueOnError)
addFlag(fs)
err := fs.Parse([]string{})
if err != nil {
t.Fatal(err)
}
if *test.val != "test2" {
t.Fatal("val must be test")
}
})
// flag not set, env not set
t.Run(fmt.Sprintf("%s: flag not set, env not set", test.env), func(t *testing.T) {
*test.val = ""
os.Setenv(test.env, "")
fs := flag.NewFlagSet("", flag.ContinueOnError)
addFlag(fs)
err := fs.Parse([]string{})
if err != nil {
t.Fatal(err)
}
if *test.val != test.def {
t.Fatal("val must be test")
}
})
}
}

View File

@@ -0,0 +1,28 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
)
go_library(
name = "go_default_library",
srcs = ["flagvar.go"],
importpath = "go-common/library/conf/flagvar",
tags = ["automanaged"],
visibility = ["//visibility:public"],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@@ -0,0 +1,18 @@
package flagvar
import (
"strings"
)
// StringVars []string implement flag.Value
type StringVars []string
func (s StringVars) String() string {
return strings.Join(s, ",")
}
// Set implement flag.Value
func (s *StringVars) Set(val string) error {
*s = append(*s, val)
return nil
}

View File

@@ -0,0 +1,80 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
"go_test",
)
go_library(
name = "go_default_library",
srcs = [
"client.go",
"default.go",
"file.go",
"helper.go",
"map.go",
"mock.go",
"sven.go",
"toml.go",
"value.go",
],
importpath = "go-common/library/conf/paladin",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//library/conf/env:go_default_library",
"//library/ecode:go_default_library",
"//library/log:go_default_library",
"//library/net/ip:go_default_library",
"//library/net/netutil:go_default_library",
"//vendor/github.com/BurntSushi/toml:go_default_library",
"//vendor/github.com/naoina/toml:go_default_library",
"//vendor/github.com/pkg/errors:go_default_library",
],
)
go_test(
name = "go_default_xtest",
srcs = [
"example_test.go",
"file_test.go",
"map_test.go",
"mock_test.go",
],
tags = ["automanaged"],
deps = [
"//library/conf/paladin:go_default_library",
"//vendor/github.com/naoina/toml:go_default_library",
"//vendor/github.com/stretchr/testify/assert:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)
go_test(
name = "go_default_test",
srcs = [
"sven_test.go",
"value_test.go",
],
embed = [":go_default_library"],
tags = ["automanaged"],
deps = [
"//library/conf/env:go_default_library",
"//vendor/github.com/naoina/toml:go_default_library",
"//vendor/github.com/stretchr/testify/assert:go_default_library",
],
)

View File

@@ -0,0 +1,24 @@
### paladin
##### Version 1.1.2
> 1.修改map key to lower case
##### Version 1.1.1
> 1.修复错误时会panic
##### Version 1.1.0
> 1.修正Var统一显示处理panic
> 2.修复本地文件路径问题
> 3.添加helper快捷处理default value
##### Version 1.0.2
> 1.修下unmarshal方法为toml
##### Version 1.0.1
> 1.default/map/value添加Unmarshal方法
##### Version 1.0.0
> 1.支持sven、file、mock配置读取
> 2.支持struct、paladin.Map对象解析
> 3.支持Set接口进行配置Reload
> 4.支持Watch自定义订阅key value变化

View File

@@ -0,0 +1,10 @@
# Owner
maojian
# Author
chenzhihui
# Reviewer
maojian
haoguanwei
lintanghui

View File

@@ -0,0 +1,10 @@
# See the OWNERS docs at https://go.k8s.io/owners
approvers:
- chenzhihui
- maojian
reviewers:
- chenzhihui
- haoguanwei
- lintanghui
- maojian

View File

@@ -0,0 +1,86 @@
#### paladin
##### 项目简介
paladin 是一个config SDK客户端包括了sven、file、mock几个抽象功能方便使用本地文件或者sven配置中心并且集成了对象自动reload功能。
sven:
```
caster配置项
配置地址CONF_HOST: config.bilibili.co
配置版本CONF_VERSION: docker-1/server-1
配置路径CONF_PATH: /data/conf/app
配置TokenCONF_TOKEN: token
配置指定版本CONF_APPOINT: 27600
依赖环境变量:
TREE_ID/DEPLOY_ENV/ZONE/HOSTNAME/POD_IP
```
local files:
```
/data/app/msm-service -conf=/data/conf/app/msm-servie.toml
// or multi file
/data/app/msm-service -conf=/data/conf/app/
```
example:
```
type exampleConf struct {
Bool bool
Int int64
Float float64
String string
}
func (e *exampleConf) Set(text string) error {
var ec exampleConf
if err := toml.Unmarshal([]byte(text), &ec); err != nil {
return err
}
*e = ec
return nil
}
func ExampleClient() {
if err := paladin.Init(); err != nil {
panic(err)
}
var (
ec exampleConf
eo exampleConf
m paladin.TOML
strs []string
)
// config unmarshal
if err := paladin.Get("example.toml").UnmarshalTOML(&ec); err != nil {
panic(err)
}
// config setter
if err := paladin.Watch("example.toml", &ec); err != nil {
panic(err)
}
// paladin map
if err := paladin.Watch("example.toml", &m); err != nil {
panic(err)
}
s, err := m.Value("key").String()
b, err := m.Value("key").Bool()
i, err := m.Value("key").Int64()
f, err := m.Value("key").Float64()
// value slice
err = m.Value("strings").Slice(&strs)
// watch key
for event := range paladin.WatchEvent(context.TODO(), "key") {
fmt.Println(event)
}
}
```
##### 编译环境
- **请只用 Golang v1.8.x 以上版本编译执行**
##### 依赖包
> 1. github.com/naoina/toml
> 2. github.com/pkg/errors

View File

@@ -0,0 +1,49 @@
package paladin
import (
"context"
)
const (
// EventAdd config add event.
EventAdd EventType = iota
// EventUpdate config update event.
EventUpdate
// EventRemove config remove event.
EventRemove
)
// EventType is config event.
type EventType int
// Event is watch event.
type Event struct {
Event EventType
Key string
Value string
}
// Watcher is config watcher.
type Watcher interface {
WatchEvent(context.Context, ...string) <-chan Event
Close() error
}
// Setter is value setter.
type Setter interface {
Set(string) error
}
// Getter is value getter.
type Getter interface {
// Get a config value by a config key(may be a sven filename).
Get(string) *Value
// GetAll return all config key->value map.
GetAll() *Map
}
// Client is config client.
type Client interface {
Watcher
Getter
}

View File

@@ -0,0 +1,84 @@
package paladin
import (
"context"
"flag"
"go-common/library/log"
)
var (
// DefaultClient default client.
DefaultClient Client
confPath string
vars = make(map[string][]Setter) // NOTE: no thread safe
)
func init() {
flag.StringVar(&confPath, "conf", "", "default config path")
}
// Init init config client.
func Init() (err error) {
if confPath != "" {
DefaultClient, err = NewFile(confPath)
} else {
DefaultClient, err = NewSven()
}
if err != nil {
return
}
go func() {
for event := range DefaultClient.WatchEvent(context.Background()) {
if event.Event != EventUpdate && event.Event != EventAdd {
continue
}
if sets, ok := vars[event.Key]; ok {
for _, s := range sets {
if err := s.Set(event.Value); err != nil {
log.Error("paladin: vars:%v event:%v error(%v)", s, event, err)
}
}
}
}
}()
return
}
// Watch watch on a key. The configuration implements the setter interface, which is invoked when the configuration changes.
func Watch(key string, s Setter) error {
v := DefaultClient.Get(key)
str, err := v.Raw()
if err != nil {
return err
}
if err := s.Set(str); err != nil {
return err
}
vars[key] = append(vars[key], s)
return nil
}
// WatchEvent watch on multi keys. Events are returned when the configuration changes.
func WatchEvent(ctx context.Context, keys ...string) <-chan Event {
return DefaultClient.WatchEvent(ctx, keys...)
}
// Get return value by key.
func Get(key string) *Value {
return DefaultClient.Get(key)
}
// GetAll return all config map.
func GetAll() *Map {
return DefaultClient.GetAll()
}
// Keys return values key.
func Keys() []string {
return DefaultClient.GetAll().Keys()
}
// Close close watcher.
func Close() error {
return DefaultClient.Close()
}

View File

@@ -0,0 +1,112 @@
package paladin_test
import (
"context"
"fmt"
"go-common/library/conf/paladin"
"github.com/naoina/toml"
)
type exampleConf struct {
Bool bool
Int int64
Float float64
String string
Strings []string
}
func (e *exampleConf) Set(text string) error {
var ec exampleConf
if err := toml.Unmarshal([]byte(text), &ec); err != nil {
return err
}
*e = ec
return nil
}
// ExampleClient is a example client usage.
// exmaple.toml:
/*
bool = true
int = 100
float = 100.1
string = "text"
strings = ["a", "b", "c"]
*/
func ExampleClient() {
if err := paladin.Init(); err != nil {
panic(err)
}
var ec exampleConf
// var setter
if err := paladin.Watch("example.toml", &ec); err != nil {
panic(err)
}
if err := paladin.Get("example.toml").UnmarshalTOML(&ec); err != nil {
panic(err)
}
// use exampleConf
// watch event key
go func() {
for event := range paladin.WatchEvent(context.TODO(), "key") {
fmt.Println(event)
}
}()
}
// ExampleMap is a example map usage.
// exmaple.toml:
/*
bool = true
int = 100
float = 100.1
string = "text"
strings = ["a", "b", "c"]
[object]
string = "text"
bool = true
int = 100
float = 100.1
strings = ["a", "b", "c"]
*/
func ExampleMap() {
var (
m paladin.TOML
strs []string
)
// paladin toml
if err := paladin.Watch("example.toml", &m); err != nil {
panic(err)
}
// value string
s, err := m.Get("string").String()
if err != nil {
s = "default"
}
fmt.Println(s)
// value bool
b, err := m.Get("bool").Bool()
if err != nil {
b = false
}
fmt.Println(b)
// value int
i, err := m.Get("int").Int64()
if err != nil {
i = 100
}
fmt.Println(i)
// value float
f, err := m.Get("float").Float64()
if err != nil {
f = 100.1
}
fmt.Println(f)
// value slice
if err = m.Get("strings").Slice(&strs); err == nil {
fmt.Println(strs)
}
}

View File

@@ -0,0 +1,82 @@
package paladin
import (
"context"
"errors"
"io/ioutil"
"os"
"path"
"path/filepath"
)
var _ Client = &file{}
// file is file config client.
type file struct {
ch chan Event
values *Map
}
// NewFile new a config file client.
// conf = /data/conf/app/
// conf = /data/conf/app/xxx.toml
func NewFile(base string) (Client, error) {
// paltform slash
base = filepath.FromSlash(base)
fi, err := os.Stat(base)
if err != nil {
panic(err)
}
// dirs or file to paths
var paths []string
if fi.IsDir() {
files, err := ioutil.ReadDir(base)
if err != nil {
panic(err)
}
for _, file := range files {
if !file.IsDir() {
paths = append(paths, path.Join(base, file.Name()))
}
}
} else {
paths = append(paths, base)
}
// laod config file to values
values := make(map[string]*Value, len(paths))
for _, file := range paths {
if file == "" {
return nil, errors.New("paladin: path is empty")
}
b, err := ioutil.ReadFile(file)
if err != nil {
return nil, err
}
s := string(b)
values[path.Base(file)] = &Value{val: s, raw: s}
}
m := new(Map)
m.Store(values)
return &file{values: m, ch: make(chan Event, 10)}, nil
}
// Get return value by key.
func (f *file) Get(key string) *Value {
return f.values.Get(key)
}
// GetAll return value map.
func (f *file) GetAll() *Map {
return f.values
}
// WatchEvent watch multi key.
func (f *file) WatchEvent(ctx context.Context, key ...string) <-chan Event {
return f.ch
}
// Close close watcher.
func (f *file) Close() error {
close(f.ch)
return nil
}

View File

@@ -0,0 +1,67 @@
package paladin_test
import (
"io/ioutil"
"os"
"testing"
"go-common/library/conf/paladin"
"github.com/stretchr/testify/assert"
)
func TestNewFile(t *testing.T) {
// test data
path := "/tmp/test_conf/"
assert.Nil(t, os.MkdirAll(path, 0700))
assert.Nil(t, ioutil.WriteFile(path+"test.toml", []byte(`
text = "hello"
number = 100
slice = [1, 2, 3]
sliceStr = ["1", "2", "3"]
`), 0644))
// test client
cli, err := paladin.NewFile(path + "test.toml")
assert.Nil(t, err)
assert.NotNil(t, cli)
// test map
m := paladin.Map{}
text, err := cli.Get("test.toml").String()
assert.Nil(t, err)
assert.Nil(t, m.Set(text), "text")
s, err := m.Get("text").String()
assert.Nil(t, err)
assert.Equal(t, s, "hello", "text")
n, err := m.Get("number").Int64()
assert.Nil(t, err)
assert.Equal(t, n, int64(100), "number")
}
func TestNewFilePath(t *testing.T) {
// test data
path := "/tmp/test_conf/"
assert.Nil(t, os.MkdirAll(path, 0700))
assert.Nil(t, ioutil.WriteFile(path+"test.toml", []byte(`
text = "hello"
number = 100
`), 0644))
assert.Nil(t, ioutil.WriteFile(path+"abc.toml", []byte(`
text = "hello"
number = 100
`), 0644))
// test client
cli, err := paladin.NewFile(path)
assert.Nil(t, err)
assert.NotNil(t, cli)
// test map
m := paladin.Map{}
text, err := cli.Get("test.toml").String()
assert.Nil(t, err)
assert.Nil(t, m.Set(text), "text")
s, err := m.Get("text").String()
assert.Nil(t, err, s)
assert.Equal(t, s, "hello", "text")
n, err := m.Get("number").Int64()
assert.Nil(t, err, s)
assert.Equal(t, n, int64(100), "number")
}

View File

@@ -0,0 +1,76 @@
package paladin
import "time"
// Bool return bool value.
func Bool(v *Value, def bool) bool {
b, err := v.Bool()
if err != nil {
return def
}
return b
}
// Int return int value.
func Int(v *Value, def int) int {
i, err := v.Int()
if err != nil {
return def
}
return i
}
// Int32 return int32 value.
func Int32(v *Value, def int32) int32 {
i, err := v.Int32()
if err != nil {
return def
}
return i
}
// Int64 return int64 value.
func Int64(v *Value, def int64) int64 {
i, err := v.Int64()
if err != nil {
return def
}
return i
}
// Float32 return float32 value.
func Float32(v *Value, def float32) float32 {
f, err := v.Float32()
if err != nil {
return def
}
return f
}
// Float64 return float32 value.
func Float64(v *Value, def float64) float64 {
f, err := v.Float64()
if err != nil {
return def
}
return f
}
// String return string value.
func String(v *Value, def string) string {
s, err := v.String()
if err != nil {
return def
}
return s
}
// Duration parses a duration string. A duration string is a possibly signed sequence of decimal numbers
// each with optional fraction and a unit suffix, such as "300ms", "-1.5h" or "2h45m". Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h".
func Duration(v *Value, def time.Duration) time.Duration {
dur, err := v.Duration()
if err != nil {
return def
}
return dur
}

View File

@@ -0,0 +1,55 @@
package paladin
import (
"strings"
"sync/atomic"
)
// keyNamed key naming to lower case.
func keyNamed(key string) string {
return strings.ToLower(key)
}
// Map is config map, key(filename) -> value(file).
type Map struct {
values atomic.Value
}
// Store sets the value of the Value to values map.
func (m *Map) Store(values map[string]*Value) {
dst := make(map[string]*Value, len(values))
for k, v := range values {
dst[keyNamed(k)] = v
}
m.values.Store(dst)
}
// Load returns the value set by the most recent Store.
func (m *Map) Load() map[string]*Value {
return m.values.Load().(map[string]*Value)
}
// Exist check if values map exist a key.
func (m *Map) Exist(key string) bool {
_, ok := m.Load()[keyNamed(key)]
return ok
}
// Get return get value by key.
func (m *Map) Get(key string) *Value {
v, ok := m.Load()[keyNamed(key)]
if ok {
return v
}
return &Value{}
}
// Keys return map keys.
func (m *Map) Keys() []string {
values := m.Load()
keys := make([]string, 0, len(values))
for key := range values {
keys = append(keys, key)
}
return keys
}

View File

@@ -0,0 +1,94 @@
package paladin_test
import (
"testing"
"go-common/library/conf/paladin"
"github.com/naoina/toml"
"github.com/stretchr/testify/assert"
)
type fruit struct {
Fruit []struct {
Name string
}
}
func (f *fruit) Set(text string) error {
return toml.Unmarshal([]byte(text), f)
}
func TestMap(t *testing.T) {
s := `
# kv
text = "hello"
number = 100
point = 100.1
boolean = true
KeyCase = "test"
# slice
numbers = [1, 2, 3]
strings = ["a", "b", "c"]
empty = []
[[fruit]]
name = "apple"
[[fruit]]
name = "banana"
# table
[database]
server = "192.168.1.1"
connection_max = 5000
enabled = true
[pool]
[pool.breaker]
xxx = "xxx"
`
m := paladin.Map{}
assert.Nil(t, m.Set(s), s)
str, err := m.Get("text").String()
assert.Nil(t, err)
assert.Equal(t, str, "hello", "text")
n, err := m.Get("number").Int64()
assert.Nil(t, err)
assert.Equal(t, n, int64(100), "number")
p, err := m.Get("point").Float64()
assert.Nil(t, err)
assert.Equal(t, p, 100.1, "point")
b, err := m.Get("boolean").Bool()
assert.Nil(t, err)
assert.Equal(t, b, true, "boolean")
// key lower case
lb, err := m.Get("Boolean").Bool()
assert.Nil(t, err)
assert.Equal(t, lb, true, "boolean")
lt, err := m.Get("KeyCase").String()
assert.Nil(t, err)
assert.Equal(t, lt, "test", "key case")
var sliceInt []int64
err = m.Get("numbers").Slice(&sliceInt)
assert.Nil(t, err)
assert.Equal(t, sliceInt, []int64{1, 2, 3})
var sliceStr []string
err = m.Get("strings").Slice(&sliceStr)
assert.Nil(t, err)
assert.Equal(t, sliceStr, []string{"a", "b", "c"})
err = m.Get("strings").Slice(&sliceStr)
assert.Nil(t, err)
assert.Equal(t, sliceStr, []string{"a", "b", "c"})
// errors
err = m.Get("strings").Slice(sliceInt)
assert.NotNil(t, err)
err = m.Get("strings").Slice(&sliceInt)
assert.NotNil(t, err)
var obj struct {
Name string
}
err = m.Get("strings").Slice(obj)
assert.NotNil(t, err)
err = m.Get("strings").Slice(&obj)
assert.NotNil(t, err)
}

View File

@@ -0,0 +1,45 @@
package paladin
import (
"context"
)
var _ Client = &mock{}
// mock is mock config client.
type mock struct {
ch chan Event
values *Map
}
// NewMock new a config mock client.
func NewMock(vs map[string]string) Client {
values := make(map[string]*Value, len(vs))
for k, v := range vs {
values[k] = &Value{val: v, raw: v}
}
m := new(Map)
m.Store(values)
return &mock{values: m, ch: make(chan Event)}
}
// Get return value by key.
func (m *mock) Get(key string) *Value {
return m.values.Get(key)
}
// GetAll return value map.
func (m *mock) GetAll() *Map {
return m.values
}
// WatchEvent watch multi key.
func (m *mock) WatchEvent(ctx context.Context, key ...string) <-chan Event {
return m.ch
}
// Close close watcher.
func (m *mock) Close() error {
close(m.ch)
return nil
}

View File

@@ -0,0 +1,37 @@
package paladin_test
import (
"testing"
"go-common/library/conf/paladin"
"github.com/stretchr/testify/assert"
)
func TestMock(t *testing.T) {
cs := map[string]string{
"key_toml": `
key_bool = true
key_int = 100
key_float = 100.1
key_string = "text"
`,
}
cli := paladin.NewMock(cs)
// test vlaue
var m paladin.TOML
err := cli.Get("key_toml").Unmarshal(&m)
assert.Nil(t, err)
b, err := m.Get("key_bool").Bool()
assert.Nil(t, err)
assert.Equal(t, b, true)
i, err := m.Get("key_int").Int64()
assert.Nil(t, err)
assert.Equal(t, i, int64(100))
f, err := m.Get("key_float").Float64()
assert.Nil(t, err)
assert.Equal(t, f, float64(100.1))
s, err := m.Get("key_string").String()
assert.Nil(t, err)
assert.Equal(t, s, "text")
}

View File

@@ -0,0 +1,372 @@
package paladin
import (
"context"
"encoding/json"
"flag"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"os"
"path"
"strconv"
"sync"
"time"
"go-common/library/conf/env"
"go-common/library/ecode"
"go-common/library/log"
xip "go-common/library/net/ip"
"go-common/library/net/netutil"
"github.com/pkg/errors"
)
const (
_apiGet = "http://%s/config/v2/get?%s"
_apiCheck = "http://%s/config/v2/check?%s"
_maxLoadRetries = 3
)
var (
_ Client = &sven{}
svenHost string
svenVersion string
svenPath string
svenToken string
svenAppoint string
svenTreeid string
_debug bool
)
func init() {
flag.StringVar(&svenHost, "conf_host", os.Getenv("CONF_HOST"), `config api host.`)
flag.StringVar(&svenVersion, "conf_version", os.Getenv("CONF_VERSION"), `app version.`)
flag.StringVar(&svenPath, "conf_path", os.Getenv("CONF_PATH"), `config file path.`)
flag.StringVar(&svenToken, "conf_token", os.Getenv("CONF_TOKEN"), `config token.`)
flag.StringVar(&svenAppoint, "conf_appoint", os.Getenv("CONF_APPOINT"), `config appoint.`)
flag.StringVar(&svenTreeid, "tree_id", os.Getenv("TREE_ID"), `tree id.`)
if env.DeployEnv == env.DeployEnvDev {
_debug = true
}
}
type watcher struct {
keys []string
ch chan Event
}
func newWatcher(keys []string) *watcher {
return &watcher{keys: keys, ch: make(chan Event, 5)}
}
func (w *watcher) HasKey(key string) bool {
if len(w.keys) == 0 {
return true
}
for _, k := range w.keys {
if k == key {
return true
}
}
return false
}
func (w *watcher) Handle(event Event) {
select {
case w.ch <- event:
default:
log.Error("paladin: discard event:%+v", event)
}
}
func (w *watcher) Chan() <-chan Event {
return w.ch
}
func (w *watcher) Close() {
close(w.ch)
}
// sven is sven config client.
type sven struct {
values *Map
wmu sync.RWMutex
watchers map[*watcher]struct{}
httpCli *http.Client
backoff *netutil.BackoffConfig
}
// NewSven new a config client.
func NewSven() (Client, error) {
s := &sven{
values: new(Map),
watchers: make(map[*watcher]struct{}),
httpCli: &http.Client{Timeout: 60 * time.Second},
backoff: &netutil.BackoffConfig{
MaxDelay: 5 * time.Second,
BaseDelay: 1.0 * time.Second,
Factor: 1.6,
Jitter: 0.2,
},
}
if err := s.checkEnv(); err != nil {
return nil, err
}
ver, err := s.load()
if err != nil {
return nil, err
}
go s.watchproc(ver)
return s, nil
}
func (s *sven) checkEnv() error {
if svenHost == "" || svenVersion == "" || svenPath == "" || svenToken == "" || svenTreeid == "" {
return fmt.Errorf("config env invalid. conf_host(%s) conf_version(%s) conf_path(%s) conf_token(%s) conf_appoint(%s) tree_id(%s)", svenHost, svenVersion, svenPath, svenToken, svenAppoint, svenTreeid)
}
return nil
}
// Get return value by key.
func (s *sven) Get(key string) *Value {
return s.values.Get(key)
}
// GetAll return value map.
func (s *sven) GetAll() *Map {
return s.values
}
// WatchEvent watch with the specified keys.
func (s *sven) WatchEvent(ctx context.Context, keys ...string) <-chan Event {
w := newWatcher(keys)
s.wmu.Lock()
s.watchers[w] = struct{}{}
s.wmu.Unlock()
return w.Chan()
}
// Close close watcher.
func (s *sven) Close() (err error) {
s.wmu.RLock()
for w := range s.watchers {
w.Close()
}
s.wmu.RUnlock()
return
}
func (s *sven) fireEvent(event Event) {
s.wmu.RLock()
for w := range s.watchers {
if w.HasKey(event.Key) {
w.Handle(event)
}
}
s.wmu.RUnlock()
}
func (s *sven) load() (ver int64, err error) {
var (
v *version
cs []*content
)
if v, err = s.check(-1); err != nil {
log.Error("paladin: s.check(-1) error(%v)", err)
return
}
for i := 0; i < _maxLoadRetries; i++ {
if cs, err = s.config(v); err == nil {
all := make(map[string]*Value, len(cs))
for _, v := range cs {
all[v.Name] = &Value{val: v.Config, raw: v.Config}
}
s.values.Store(all)
return v.Version, nil
}
log.Error("paladin: s.config(%v) error(%v)", ver, err)
time.Sleep(s.backoff.Backoff(i))
}
return 0, err
}
func (s *sven) watchproc(ver int64) {
var retry int
for {
v, err := s.check(ver)
if err != nil {
if ecode.NotModified.Equal(err) {
time.Sleep(time.Second)
continue
}
log.Error("paladin: s.check(%d) error(%v)", ver, err)
retry++
time.Sleep(s.backoff.Backoff(retry))
continue
}
cs, err := s.config(v)
if err != nil {
log.Error("paladin: s.config(%v) error(%v)", ver, err)
retry++
time.Sleep(s.backoff.Backoff(retry))
continue
}
all := s.values.Load()
news := make(map[string]*Value, len(cs))
for _, v := range cs {
if _, ok := all[v.Name]; !ok {
go s.fireEvent(Event{Event: EventAdd, Key: v.Name, Value: v.Config})
} else if v.Config != "" {
go s.fireEvent(Event{Event: EventUpdate, Key: v.Name, Value: v.Config})
} else {
go s.fireEvent(Event{Event: EventRemove, Key: v.Name, Value: v.Config})
}
news[v.Name] = &Value{val: v.Config, raw: v.Config}
}
for k, v := range all {
if _, ok := news[k]; !ok {
news[k] = v
}
}
s.values.Store(news)
ver = v.Version
retry = 0
}
}
type version struct {
Version int64 `json:"version"`
Diffs []int64 `json:"diffs"`
}
type config struct {
Version int64 `json:"version"`
Content string `json:"content"`
Md5 string `json:"md5"`
}
type content struct {
Cid int64 `json:"cid"`
Name string `json:"name"`
Config string `json:"config"`
}
func (s *sven) check(ver int64) (v *version, err error) {
params := newParams()
params.Set("version", strconv.FormatInt(ver, 10))
params.Set("appoint", svenAppoint)
var res struct {
Code int `json:"code"`
Data *version `json:"data"`
}
uri := fmt.Sprintf(_apiCheck, svenHost, params.Encode())
if _debug {
fmt.Printf("paladin: check(%d) uri(%s)\n", ver, uri)
}
req, err := http.NewRequest("GET", uri, nil)
if err != nil {
return
}
resp, err := s.httpCli.Do(req)
if err != nil {
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
err = errors.Errorf("paladin: httpCli.GET(%s) error(%d)", params.Encode(), resp.StatusCode)
return
}
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return
}
if err = json.Unmarshal(b, &res); err != nil {
return
}
if ec := ecode.Int(res.Code); !ec.Equal(ecode.OK) {
err = ec
return
}
if res.Data == nil {
err = errors.Errorf("paladin: http version is nil. params(%s)", params.Encode())
return
}
v = res.Data
return
}
func (s *sven) config(ver *version) (cts []*content, err error) {
ids, _ := json.Marshal(ver.Diffs)
params := newParams()
params.Set("version", strconv.FormatInt(ver.Version, 10))
params.Set("ids", string(ids))
var res struct {
Code int `json:"code"`
Data *config `json:"data"`
}
uri := fmt.Sprintf(_apiGet, svenHost, params.Encode())
if _debug {
fmt.Printf("paladin: config(%+v) uri(%s)\n", ver, uri)
}
req, err := http.NewRequest("GET", uri, nil)
if err != nil {
return
}
resp, err := s.httpCli.Do(req)
if err != nil {
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
err = errors.Errorf("paladin: httpCli.GET(%s) error(%d)", params.Encode(), resp.StatusCode)
return
}
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return
}
if err = json.Unmarshal(b, &res); err != nil {
return
}
if !ecode.Int(res.Code).Equal(ecode.OK) || res.Data == nil {
err = errors.Errorf("paladin: http config is nil. params(%s) ecode(%d)", params.Encode(), res.Code)
return
}
if err = json.Unmarshal([]byte(res.Data.Content), &cts); err != nil {
return
}
for _, c := range cts {
if err = ioutil.WriteFile(path.Join(svenPath, c.Name), []byte(c.Config), 0644); err != nil {
return
}
}
return
}
func newParams() url.Values {
params := url.Values{}
params.Set("service", serviceName())
params.Set("build", svenVersion)
params.Set("token", svenToken)
params.Set("hostname", env.Hostname)
params.Set("ip", ipAddr())
return params
}
func ipAddr() string {
if env.IP != "" {
return env.IP
}
return xip.InternalIP()
}
func serviceName() string {
return fmt.Sprintf("%s_%s_%s", svenTreeid, env.DeployEnv, env.Zone)
}

View File

@@ -0,0 +1,119 @@
package paladin
import (
"context"
"testing"
"time"
"go-common/library/conf/env"
"github.com/naoina/toml"
"github.com/stretchr/testify/assert"
)
type testObj struct {
Bool bool
Int int64
Float float64
String string
}
func (t *testObj) Set(text string) error {
return toml.Unmarshal([]byte(text), t)
}
type testConf struct {
Bool bool
Int int64
Float float64
String string
Object *testObj
}
func (t *testConf) Set(text string) error {
return toml.Unmarshal([]byte(text), t)
}
func TestSven(t *testing.T) {
svenHost = "config.bilibili.co"
svenVersion = "server-1"
svenPath = "/tmp"
svenToken = "1afe5efaf45e11e7b3f8c6cd4f230d8c"
svenAppoint = ""
svenTreeid = "2888"
env.Region = "sh"
env.Zone = "sh001"
env.Hostname = "test"
env.DeployEnv = "dev"
env.AppID = "main.common-arch.msm-service"
sven, err := NewSven()
assert.Nil(t, err)
testSvenMap(t, sven)
testSvenValue(t, sven)
testWatch(t, sven)
}
func testSvenMap(t *testing.T, cli Client) {
m := Map{}
text, err := cli.Get("test.toml").String()
assert.Nil(t, err)
assert.Nil(t, m.Set(text), text)
b, err := m.Get("bool").Bool()
assert.Nil(t, err)
assert.Equal(t, b, true, "bool")
// int64
i, err := m.Get("int").Int64()
assert.Nil(t, err)
assert.Equal(t, i, int64(100), "int64")
// float64
f, err := m.Get("float").Float64()
assert.Nil(t, err)
assert.Equal(t, f, 100.1, "float64")
// string
s, err := m.Get("string").String()
assert.Nil(t, err)
assert.Equal(t, s, "text", "string")
// error
n, err := m.Get("not_exsit").String()
assert.NotNil(t, err)
assert.Equal(t, n, "", "not_exsit")
obj := new(testObj)
text, err = m.Get("object").Raw()
assert.Nil(t, err)
assert.Nil(t, obj.Set(text))
assert.Equal(t, obj.Bool, true, "bool")
assert.Equal(t, obj.Int, int64(100), "int64")
assert.Equal(t, obj.Float, 100.1, "float64")
assert.Equal(t, obj.String, "text", "string")
}
func testSvenValue(t *testing.T, cli Client) {
v := new(testConf)
text, err := cli.Get("test.toml").Raw()
assert.Nil(t, err)
assert.Nil(t, v.Set(text))
assert.Equal(t, v.Bool, true, "bool")
assert.Equal(t, v.Int, int64(100), "int64")
assert.Equal(t, v.Float, 100.1, "float64")
assert.Equal(t, v.String, "text", "string")
assert.Equal(t, v.Object.Bool, true, "bool")
assert.Equal(t, v.Object.Int, int64(100), "int64")
assert.Equal(t, v.Object.Float, 100.1, "float64")
assert.Equal(t, v.Object.String, "text", "string")
}
func testWatch(t *testing.T, cli Client) {
ch := cli.WatchEvent(context.Background())
select {
case <-time.After(time.Second):
t.Log("watch timeout")
case e := <-ch:
s, err := cli.Get("static").String()
assert.Nil(t, err)
assert.Equal(t, s, e.Value, "watch value")
t.Logf("watch event:%+v", e)
}
}

View File

@@ -0,0 +1,68 @@
package paladin
import (
"reflect"
"strconv"
"github.com/naoina/toml"
"github.com/pkg/errors"
)
// TOML is toml map.
type TOML = Map
// Set set the map by value.
func (m *TOML) Set(text string) error {
if err := m.UnmarshalText([]byte(text)); err != nil {
return err
}
return nil
}
// UnmarshalText implemented toml.
func (m *TOML) UnmarshalText(text []byte) error {
raws := map[string]interface{}{}
if err := toml.Unmarshal(text, &raws); err != nil {
return err
}
values := map[string]*Value{}
for k, v := range raws {
k = keyNamed(k)
rv := reflect.ValueOf(v)
switch rv.Kind() {
case reflect.Map:
b, err := toml.Marshal(v)
if err != nil {
return err
}
// NOTE: value is map[string]interface{}
values[k] = &Value{val: v, raw: string(b)}
case reflect.Slice:
raw := map[string]interface{}{
k: v,
}
b, err := toml.Marshal(raw)
if err != nil {
return err
}
// NOTE: value is []interface{}
values[k] = &Value{val: v, raw: string(b)}
case reflect.Bool:
b := v.(bool)
values[k] = &Value{val: b, raw: strconv.FormatBool(b)}
case reflect.Int64:
i := v.(int64)
values[k] = &Value{val: i, raw: strconv.FormatInt(i, 10)}
case reflect.Float64:
f := v.(float64)
values[k] = &Value{val: f, raw: strconv.FormatFloat(f, 'f', -1, 64)}
case reflect.String:
s := v.(string)
values[k] = &Value{val: s, raw: s}
default:
return errors.Errorf("UnmarshalTOML: unknown kind(%v)", rv.Kind())
}
}
m.Store(values)
return nil
}

View File

@@ -0,0 +1,170 @@
package paladin
import (
"encoding"
"reflect"
"time"
"github.com/BurntSushi/toml"
"github.com/pkg/errors"
)
// ErrNotExist value key not exist.
var (
ErrNotExist = errors.New("paladin: value key not exist")
ErrTypeAssertion = errors.New("paladin: value type assertion no match")
ErrDifferentTypes = errors.New("paladin: value different types")
)
// Value is config value, maybe a json/toml/ini/string file.
type Value struct {
val interface{}
slice interface{}
raw string
}
// Bool return bool value.
func (v *Value) Bool() (bool, error) {
if v.val == nil {
return false, ErrNotExist
}
b, ok := v.val.(bool)
if !ok {
return false, ErrTypeAssertion
}
return b, nil
}
// Int return int value.
func (v *Value) Int() (int, error) {
i, err := v.Int64()
if err != nil {
return 0, nil
}
return int(i), nil
}
// Int32 return int32 value.
func (v *Value) Int32() (int32, error) {
i, err := v.Int64()
if err != nil {
return 0, nil
}
return int32(i), nil
}
// Int64 return int64 value.
func (v *Value) Int64() (int64, error) {
if v.val == nil {
return 0, ErrNotExist
}
i, ok := v.val.(int64)
if !ok {
return 0, ErrTypeAssertion
}
return i, nil
}
// Float32 return float32 value.
func (v *Value) Float32() (float32, error) {
f, err := v.Float64()
if err != nil {
return 0.0, err
}
return float32(f), nil
}
// Float64 return float64 value.
func (v *Value) Float64() (float64, error) {
if v.val == nil {
return 0.0, ErrNotExist
}
f, ok := v.val.(float64)
if !ok {
return 0.0, ErrTypeAssertion
}
return f, nil
}
// String return string value.
func (v *Value) String() (string, error) {
if v.val == nil {
return "", ErrNotExist
}
s, ok := v.val.(string)
if !ok {
return "", ErrTypeAssertion
}
return s, nil
}
// Duration parses a duration string. A duration string is a possibly signed sequence of decimal numbers
// each with optional fraction and a unit suffix, such as "300ms", "-1.5h" or "2h45m". Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h".
func (v *Value) Duration() (time.Duration, error) {
s, err := v.String()
if err != nil {
return time.Duration(0), err
}
return time.ParseDuration(s)
}
// Raw return raw value.
func (v *Value) Raw() (string, error) {
if v.val == nil {
return "", ErrNotExist
}
return v.raw, nil
}
// Slice scan a slcie interface.
func (v *Value) Slice(dst interface{}) error {
// NOTE: val is []interface{}, slice is []type
if v.val == nil {
return ErrNotExist
}
rv := reflect.ValueOf(dst)
if rv.Kind() != reflect.Ptr || rv.Elem().Kind() != reflect.Slice {
return ErrDifferentTypes
}
el := rv.Elem()
kind := el.Type().Elem().Kind()
if v.slice == nil {
src, ok := v.val.([]interface{})
if !ok {
return ErrDifferentTypes
}
for _, s := range src {
if reflect.TypeOf(s).Kind() != kind {
return ErrTypeAssertion
}
el = reflect.Append(el, reflect.ValueOf(s))
}
v.slice = el.Interface()
rv.Elem().Set(el)
return nil
}
sv := reflect.ValueOf(v.slice)
if sv.Type().Elem().Kind() != kind {
return ErrTypeAssertion
}
rv.Elem().Set(sv)
return nil
}
// Unmarshal is the interface implemented by an object that can unmarshal a textual representation of itself.
func (v *Value) Unmarshal(un encoding.TextUnmarshaler) error {
text, err := v.Raw()
if err != nil {
return err
}
return un.UnmarshalText([]byte(text))
}
// UnmarshalTOML unmarhsal toml to struct.
func (v *Value) UnmarshalTOML(dst interface{}) error {
text, err := v.Raw()
if err != nil {
return err
}
return toml.Unmarshal([]byte(text), dst)
}

View File

@@ -0,0 +1,206 @@
package paladin
import (
"fmt"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
type testUnmarshler struct {
Text string
Int int
}
func TestValueUnmarshal(t *testing.T) {
s := `
int = 100
text = "hello"
`
v := Value{val: s, raw: s}
obj := new(testUnmarshler)
assert.Nil(t, v.UnmarshalTOML(obj))
// error
v = Value{val: nil, raw: ""}
assert.NotNil(t, v.UnmarshalTOML(obj))
}
func TestValue(t *testing.T) {
var tests = []struct {
in interface{}
out interface{}
}{
{
"text",
"text",
},
{
time.Duration(time.Second * 10),
"10s",
},
{
int64(100),
int64(100),
},
{
float64(100.1),
float64(100.1),
},
{
true,
true,
},
{
nil,
nil,
},
}
for _, test := range tests {
t.Run(fmt.Sprint(test.in), func(t *testing.T) {
v := Value{val: test.in, raw: fmt.Sprint(test.in)}
switch test.in.(type) {
case nil:
s, err := v.String()
assert.NotNil(t, err)
assert.Equal(t, s, "", test.in)
i, err := v.Int64()
assert.NotNil(t, err)
assert.Equal(t, i, int64(0), test.in)
f, err := v.Float64()
assert.NotNil(t, err)
assert.Equal(t, f, float64(0.0), test.in)
b, err := v.Bool()
assert.NotNil(t, err)
assert.Equal(t, b, false, test.in)
case string:
val, err := v.String()
assert.Nil(t, err)
assert.Equal(t, val, test.out.(string), test.in)
case int64:
val, err := v.Int()
assert.Nil(t, err)
assert.Equal(t, val, int(test.out.(int64)), test.in)
val32, err := v.Int32()
assert.Nil(t, err)
assert.Equal(t, val32, int32(test.out.(int64)), test.in)
val64, err := v.Int64()
assert.Nil(t, err)
assert.Equal(t, val64, test.out.(int64), test.in)
case float64:
val32, err := v.Float32()
assert.Nil(t, err)
assert.Equal(t, val32, float32(test.out.(float64)), test.in)
val64, err := v.Float64()
assert.Nil(t, err)
assert.Equal(t, val64, test.out.(float64), test.in)
case bool:
val, err := v.Bool()
assert.Nil(t, err)
assert.Equal(t, val, test.out.(bool), test.in)
case time.Duration:
v.val = test.out
val, err := v.Duration()
assert.Nil(t, err)
assert.Equal(t, val, test.in.(time.Duration), test.out)
}
})
}
}
func TestValueSlice(t *testing.T) {
var tests = []struct {
in interface{}
out interface{}
}{
{
nil,
nil,
},
{
[]interface{}{"a", "b", "c"},
[]string{"a", "b", "c"},
},
{
[]interface{}{1, 2, 3},
[]int64{1, 2, 3},
},
{
[]interface{}{1.1, 1.2, 1.3},
[]float64{1.1, 1.2, 1.3},
},
{
[]interface{}{true, false, true},
[]bool{true, false, true},
},
}
for _, test := range tests {
t.Run(fmt.Sprint(test.in), func(t *testing.T) {
v := Value{val: test.in, raw: fmt.Sprint(test.in)}
switch test.in.(type) {
case nil:
var s []string
assert.NotNil(t, v.Slice(&s))
case []string:
var s []string
assert.Nil(t, v.Slice(&s))
assert.Equal(t, s, test.out)
case []int64:
var s []int64
assert.Nil(t, v.Slice(&s))
assert.Equal(t, s, test.out)
case []float64:
var s []float64
assert.Nil(t, v.Slice(&s))
assert.Equal(t, s, test.out)
case []bool:
var s []bool
assert.Nil(t, v.Slice(&s))
assert.Equal(t, s, test.out)
}
})
}
}
func BenchmarkValueInt(b *testing.B) {
v := &Value{val: int64(100), raw: "100"}
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
v.Int64()
}
})
}
func BenchmarkValueFloat(b *testing.B) {
v := &Value{val: float64(100.1), raw: "100.1"}
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
v.Float64()
}
})
}
func BenchmarkValueBool(b *testing.B) {
v := &Value{val: true, raw: "true"}
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
v.Bool()
}
})
}
func BenchmarkValueString(b *testing.B) {
v := &Value{val: "text", raw: "text"}
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
v.String()
}
})
}
func BenchmarkValueSlice(b *testing.B) {
v := &Value{val: []interface{}{1, 2, 3}, raw: "100"}
b.RunParallel(func(pb *testing.PB) {
var slice []int64
for pb.Next() {
v.Slice(&slice)
}
})
}