Compare commits

...

7 Commits

Author SHA1 Message Date
88c7439c01 tmp
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2023-02-11 01:04:43 +03:00
f4d0237785 Merge pull request #183 from unistack-org/logger/unwrap
logger/unwrap: fix for nested tagged/untagged
2023-02-08 14:56:51 +03:00
0f343dad0b logger/unwrap: fix for nested tagged/untagged
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2023-02-08 14:54:37 +03:00
7c29afba0b Merge pull request #182 from unistack-org/timeDuration
config/default: handle time.Duration
2023-02-07 06:50:42 +03:00
8159b9d233 config/default: handle time.Duration
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2023-02-07 06:48:36 +03:00
45cdac5c29 config/default: handle time.Duration
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2023-02-07 06:48:12 +03:00
98db0dc8bc config/default: handle time.Duration
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2023-02-07 06:47:46 +03:00
9 changed files with 241 additions and 93 deletions

View File

@@ -5,9 +5,11 @@ import (
"reflect" "reflect"
"strconv" "strconv"
"strings" "strings"
"time"
"github.com/imdario/mergo" "github.com/imdario/mergo"
rutil "go.unistack.org/micro/v3/util/reflect" rutil "go.unistack.org/micro/v3/util/reflect"
mtime "go.unistack.org/micro/v3/util/time"
) )
type defaultConfig struct { type defaultConfig struct {
@@ -75,6 +77,7 @@ func fillValue(value reflect.Value, val string) error {
if !rutil.IsEmpty(value) { if !rutil.IsEmpty(value) {
return nil return nil
} }
switch value.Kind() { switch value.Kind() {
case reflect.Map: case reflect.Map:
t := value.Type() t := value.Type()
@@ -151,11 +154,26 @@ func fillValue(value reflect.Value, val string) error {
} }
value.Set(reflect.ValueOf(int32(v))) value.Set(reflect.ValueOf(int32(v)))
case reflect.Int64: case reflect.Int64:
v, err := strconv.ParseInt(val, 10, 64) switch {
if err != nil { case value.Type().String() == "time.Duration" && value.Type().PkgPath() == "time":
return err v, err := time.ParseDuration(val)
if err != nil {
return err
}
value.Set(reflect.ValueOf(v))
case value.Type().String() == "time.Duration" && value.Type().PkgPath() == "go.unistack.org/micro/v3/util/time":
v, err := mtime.ParseDuration(val)
if err != nil {
return err
}
value.SetInt(int64(v))
default:
v, err := strconv.ParseInt(val, 10, 64)
if err != nil {
return err
}
value.Set(reflect.ValueOf(v))
} }
value.Set(reflect.ValueOf(v))
case reflect.Uint: case reflect.Uint:
v, err := strconv.ParseUint(val, 10, 0) v, err := strconv.ParseUint(val, 10, 0)
if err != nil { if err != nil {

View File

@@ -4,15 +4,19 @@ import (
"context" "context"
"fmt" "fmt"
"testing" "testing"
"time"
"go.unistack.org/micro/v3/config" "go.unistack.org/micro/v3/config"
mtime "go.unistack.org/micro/v3/util/time"
) )
type cfg struct { type cfg struct {
StringValue string `default:"string_value"` StringValue string `default:"string_value"`
IgnoreValue string `json:"-"` IgnoreValue string `json:"-"`
StructValue *cfgStructValue StructValue *cfgStructValue
IntValue int `default:"99"` IntValue int `default:"99"`
DurationValue time.Duration `default:"10s"`
MDurationValue mtime.Duration `default:"10s"`
} }
type cfgStructValue struct { type cfgStructValue struct {

View File

@@ -46,6 +46,11 @@ var (
closeMapBytes = []byte("}") closeMapBytes = []byte("}")
) )
type protoMessage interface {
Reset()
ProtoMessage()
}
type Wrapper struct { type Wrapper struct {
val interface{} val interface{}
s fmt.State s fmt.State
@@ -53,7 +58,7 @@ type Wrapper struct {
opts *Options opts *Options
depth int depth int
ignoreNextType bool ignoreNextType bool
takeAll map[int]bool takeMap map[int]bool
protoWrapperType bool protoWrapperType bool
sqlWrapperType bool sqlWrapperType bool
} }
@@ -111,7 +116,7 @@ func Tagged(b bool) Option {
func Unwrap(val interface{}, opts ...Option) *Wrapper { func Unwrap(val interface{}, opts ...Option) *Wrapper {
options := NewOptions(opts...) options := NewOptions(opts...)
return &Wrapper{val: val, opts: &options, pointers: make(map[uintptr]int), takeAll: make(map[int]bool)} return &Wrapper{val: val, opts: &options, pointers: make(map[uintptr]int), takeMap: make(map[int]bool)}
} }
func (w *Wrapper) unpackValue(v reflect.Value) reflect.Value { func (w *Wrapper) unpackValue(v reflect.Value) reflect.Value {
@@ -237,9 +242,6 @@ func (w *Wrapper) format(v reflect.Value) {
_, _ = w.s.Write(buf) _, _ = w.s.Write(buf)
return return
} }
if w.opts.Tagged {
w.checkTakeAll(v, 1)
}
// Handle invalid reflect values immediately. // Handle invalid reflect values immediately.
kind := v.Kind() kind := v.Kind()
@@ -256,6 +258,10 @@ func (w *Wrapper) format(v reflect.Value) {
w.protoWrapperType = true w.protoWrapperType = true
} else if strings.HasPrefix(reflect.Indirect(v).Type().String(), "sql.Null") { } else if strings.HasPrefix(reflect.Indirect(v).Type().String(), "sql.Null") {
w.sqlWrapperType = true w.sqlWrapperType = true
} else if v.CanInterface() {
if _, ok := v.Interface().(protoMessage); ok {
w.protoWrapperType = true
}
} }
} }
w.formatPtr(v) w.formatPtr(v)
@@ -378,6 +384,12 @@ func (w *Wrapper) format(v reflect.Value) {
prevSkip := false prevSkip := false
for i := 0; i < numFields; i++ { for i := 0; i < numFields; i++ {
switch vt.Field(i).Type.PkgPath() {
case "google.golang.org/protobuf/internal/impl", "google.golang.org/protobuf/internal/pragma":
w.protoWrapperType = true
prevSkip = true
continue
}
if w.protoWrapperType && !vt.Field(i).IsExported() { if w.protoWrapperType && !vt.Field(i).IsExported() {
prevSkip = true prevSkip = true
continue continue
@@ -385,6 +397,9 @@ func (w *Wrapper) format(v reflect.Value) {
prevSkip = true prevSkip = true
continue continue
} }
if _, ok := vt.Field(i).Tag.Lookup("protobuf"); ok && !w.protoWrapperType {
w.protoWrapperType = true
}
sv, ok := vt.Field(i).Tag.Lookup("logger") sv, ok := vt.Field(i).Tag.Lookup("logger")
switch { switch {
case ok: case ok:
@@ -395,11 +410,16 @@ func (w *Wrapper) format(v reflect.Value) {
case "take": case "take":
break break
} }
case w.takeAll[w.depth]:
break
case !ok && w.opts.Tagged: case !ok && w.opts.Tagged:
prevSkip = true // skip top level untagged
continue if w.depth == 1 {
prevSkip = true
continue
}
if tv, ok := w.takeMap[w.depth]; ok && !tv {
prevSkip = true
continue
}
} }
if prevSkip { if prevSkip {
@@ -416,9 +436,7 @@ func (w *Wrapper) format(v reflect.Value) {
_, _ = w.s.Write([]byte(vt.Name)) _, _ = w.s.Write([]byte(vt.Name))
_, _ = w.s.Write(colonBytes) _, _ = w.s.Write(colonBytes)
} }
unpackValue := w.unpackValue(v.Field(i)) w.format(w.unpackValue(v.Field(i)))
w.checkTakeAll(unpackValue, w.depth)
w.format(unpackValue)
numWritten++ numWritten++
} }
w.depth-- w.depth--
@@ -461,6 +479,10 @@ func (w *Wrapper) Format(s fmt.State, verb rune) {
return return
} }
if w.opts.Tagged {
w.buildTakeMap(reflect.ValueOf(w.val), 1)
}
w.format(reflect.ValueOf(w.val)) w.format(reflect.ValueOf(w.val))
} }
@@ -615,24 +637,28 @@ func (w *Wrapper) constructOrigFormat(verb rune) string {
return buf.String() return buf.String()
} }
func (w *Wrapper) checkTakeAll(v reflect.Value, depth int) { func (w *Wrapper) buildTakeMap(v reflect.Value, depth int) {
if _, ok := w.takeAll[depth]; ok {
return
}
if !v.IsValid() || v.IsZero() { if !v.IsValid() || v.IsZero() {
return return
} }
switch v.Kind() { switch v.Kind() {
case reflect.Slice, reflect.Array:
for i := 0; i < v.Len(); i++ {
w.buildTakeMap(v.Index(i), depth+1)
}
w.takeMap[depth] = true
return
case reflect.Struct: case reflect.Struct:
break break
case reflect.Ptr: case reflect.Ptr:
v = v.Elem() v = v.Elem()
if v.Kind() != reflect.Struct { if v.Kind() != reflect.Struct {
w.takeAll[depth] = true w.takeMap[depth] = true
return return
} }
default: default:
w.takeAll[depth] = true w.takeMap[depth] = true
return return
} }
@@ -641,8 +667,15 @@ func (w *Wrapper) checkTakeAll(v reflect.Value, depth int) {
for i := 0; i < v.NumField(); i++ { for i := 0; i < v.NumField(); i++ {
sv, ok := vt.Field(i).Tag.Lookup("logger") sv, ok := vt.Field(i).Tag.Lookup("logger")
if ok && sv == "take" { if ok && sv == "take" {
w.takeAll[depth] = false w.takeMap[depth] = false
} }
w.checkTakeAll(v.Field(i), depth+1) if v.Kind() == reflect.Struct ||
(v.Kind() == reflect.Ptr && v.Elem().Kind() == reflect.Struct) {
w.buildTakeMap(v.Field(i), depth+1)
}
}
if _, ok := w.takeMap[depth]; !ok {
w.takeMap[depth] = true
} }
} }

View File

@@ -124,7 +124,7 @@ func AppendOutgoingContext(ctx context.Context, kv ...string) context.Context {
return NewOutgoingContext(ctx, md) return NewOutgoingContext(ctx, md)
} }
for k, v := range md { for k, v := range md {
omd.Set(k, v) omd[k] = v
} }
return NewOutgoingContext(ctx, omd) return NewOutgoingContext(ctx, omd)
} }
@@ -140,7 +140,7 @@ func AppendIncomingContext(ctx context.Context, kv ...string) context.Context {
return NewIncomingContext(ctx, md) return NewIncomingContext(ctx, md)
} }
for k, v := range md { for k, v := range md {
omd.Set(k, v) omd[k] = v
} }
return NewIncomingContext(ctx, omd) return NewIncomingContext(ctx, omd)
} }

View File

@@ -24,7 +24,7 @@ var (
// Metadata is our way of representing request headers internally. // Metadata is our way of representing request headers internally.
// They're used at the RPC level and translate back and forth // They're used at the RPC level and translate back and forth
// from Transport headers. // from Transport headers.
type Metadata map[string]string type Metadata map[string][]string
type rawMetadata struct { type rawMetadata struct {
md Metadata md Metadata
@@ -42,7 +42,7 @@ type Iterator struct {
} }
// Next advance iterator to next element // Next advance iterator to next element
func (iter *Iterator) Next(k, v *string) bool { func (iter *Iterator) Next(k *string, v *[]string) bool {
if iter.cur+1 > iter.cnt { if iter.cur+1 > iter.cnt {
return false return false
} }
@@ -64,8 +64,11 @@ func (md Metadata) Iterator() *Iterator {
return iter return iter
} }
// Get returns value from metadata by key // Values returns values from metadata by key
func (md Metadata) Get(key string) (string, bool) { func (md Metadata) Values(key string) ([]string, bool) {
if md == nil {
return nil, false
}
// fast path // fast path
val, ok := md[key] val, ok := md[key]
if !ok { if !ok {
@@ -75,16 +78,42 @@ func (md Metadata) Get(key string) (string, bool) {
return val, ok return val, ok
} }
// Get returns value from metadata by key
func (md Metadata) Get(key string) (string, bool) {
if md == nil {
return "", false
}
// fast path
val, ok := md[key]
if !ok {
// slow path
val, ok = md[textproto.CanonicalMIMEHeaderKey(key)]
}
if len(val) == 0 {
return "", false
}
return val[0], ok
}
// Set is used to store value in metadata // Set is used to store value in metadata
func (md Metadata) Set(kv ...string) { func (md Metadata) Set(kv ...string) {
if md == nil {
return
}
if len(kv)%2 == 1 { if len(kv)%2 == 1 {
kv = kv[:len(kv)-1] kv = kv[:len(kv)-1]
} }
for idx := 0; idx < len(kv); idx += 2 { for idx := 0; idx < len(kv); idx += 2 {
md[textproto.CanonicalMIMEHeaderKey(kv[idx])] = kv[idx+1] md[textproto.CanonicalMIMEHeaderKey(kv[idx])] = []string{kv[idx+1]}
} }
} }
// Add is used to append value in metadata
func (md Metadata) Add(k string, v string) {
kn := textproto.CanonicalMIMEHeaderKey(k)
md[kn] = append(md[kn], v)
}
// Del is used to remove value from metadata // Del is used to remove value from metadata
func (md Metadata) Del(keys ...string) { func (md Metadata) Del(keys ...string) {
for _, key := range keys { for _, key := range keys {
@@ -99,7 +128,7 @@ func (md Metadata) Del(keys ...string) {
func Copy(md Metadata) Metadata { func Copy(md Metadata) Metadata {
nmd := New(len(md)) nmd := New(len(md))
for key, val := range md { for key, val := range md {
nmd.Set(key, val) nmd[key] = val
} }
return nmd return nmd
} }
@@ -114,16 +143,24 @@ func New(size int) Metadata {
// Merge merges metadata to existing metadata, overwriting if specified // Merge merges metadata to existing metadata, overwriting if specified
func Merge(omd Metadata, mmd Metadata, overwrite bool) Metadata { func Merge(omd Metadata, mmd Metadata, overwrite bool) Metadata {
var ok bool
nmd := Copy(omd) nmd := Copy(omd)
for key, val := range mmd { for key, nval := range mmd {
_, ok = nmd[key] oval, ok := nmd[key]
switch { switch {
case ok && !overwrite: case ok && !overwrite:
continue continue
case val != "": case len(nval) == 1 && nval[0] != "":
nmd.Set(key, val) nmd[key] = nval
case ok && val == "": case ok && len(nval) > 1:
sort.Strings(nval)
sort.Strings(oval)
for idx, v := range nval {
if oval[idx] != v {
oval[idx] = v
}
}
nmd[key] = oval
case ok && nval[0] == "":
nmd.Del(key) nmd.Del(key)
} }
} }

View File

@@ -68,10 +68,10 @@ func TestPassing(t *testing.T) {
func TestMerge(t *testing.T) { func TestMerge(t *testing.T) {
omd := Metadata{ omd := Metadata{
"key1": "val1", "key1": []string{"val1"},
} }
mmd := Metadata{ mmd := Metadata{
"key2": "val2", "key2": []string{"val2"},
} }
nmd := Merge(omd, mmd, true) nmd := Merge(omd, mmd, true)
@@ -82,13 +82,14 @@ func TestMerge(t *testing.T) {
func TestIterator(t *testing.T) { func TestIterator(t *testing.T) {
md := Metadata{ md := Metadata{
"1Last": "last", "1Last": []string{"last"},
"2First": "first", "2First": []string{"first"},
"3Second": "second", "3Second": []string{"second"},
} }
iter := md.Iterator() iter := md.Iterator()
var k, v string var k string
var v []string
for iter.Next(&k, &v) { for iter.Next(&k, &v) {
// fmt.Printf("k: %s, v: %s\n", k, v) // fmt.Printf("k: %s, v: %s\n", k, v)
@@ -101,20 +102,20 @@ func TestMedataCanonicalKey(t *testing.T) {
v, ok := md.Get("x-request-id") v, ok := md.Get("x-request-id")
if !ok { if !ok {
t.Fatalf("failed to get x-request-id") t.Fatalf("failed to get x-request-id")
} else if v != "12345" { } else if len(v) != 1 && v != "12345" {
t.Fatalf("invalid metadata value: %s != %s", "12345", v) t.Fatalf("invalid metadata value: %s != %s", "12345", v)
} }
v, ok = md.Get("X-Request-Id") v, ok = md.Get("X-Request-Id")
if !ok { if !ok {
t.Fatalf("failed to get x-request-id") t.Fatalf("failed to get x-request-id")
} else if v != "12345" { } else if len(v) != 1 && v != "12345" {
t.Fatalf("invalid metadata value: %s != %s", "12345", v) t.Fatalf("invalid metadata value: %s != %s", "12345", v)
} }
v, ok = md.Get("X-Request-ID") v, ok = md.Get("X-Request-ID")
if !ok { if !ok {
t.Fatalf("failed to get x-request-id") t.Fatalf("failed to get x-request-id")
} else if v != "12345" { } else if len(v) != 1 && v != "12345" {
t.Fatalf("invalid metadata value: %s != %s", "12345", v) t.Fatalf("invalid metadata value: %s != %s", "12345", v)
} }
} }
@@ -135,8 +136,8 @@ func TestMetadataSet(t *testing.T) {
func TestMetadataDelete(t *testing.T) { func TestMetadataDelete(t *testing.T) {
md := Metadata{ md := Metadata{
"Foo": "bar", "Foo": []string{"bar"},
"Baz": "empty", "Baz": []string{"empty"},
} }
md.Del("Baz") md.Del("Baz")
@@ -157,14 +158,14 @@ func TestNilContext(t *testing.T) {
func TestMetadataCopy(t *testing.T) { func TestMetadataCopy(t *testing.T) {
md := Metadata{ md := Metadata{
"Foo": "bar", "Foo": []string{"bar"},
"Bar": "baz", "Bar": []string{"baz"},
} }
cp := Copy(md) cp := Copy(md)
for k, v := range md { for k, v := range md {
if cv := cp[k]; cv != v { if cv := cp[k]; len(cv) != len(v) || (cv[0] != v[0] && cv[1] != v[1]) {
t.Fatalf("Got %s:%s for %s:%s", k, cv, k, v) t.Fatalf("Got %s:%s for %s:%s", k, cv, k, v)
} }
} }
@@ -172,7 +173,7 @@ func TestMetadataCopy(t *testing.T) {
func TestMetadataContext(t *testing.T) { func TestMetadataContext(t *testing.T) {
md := Metadata{ md := Metadata{
"Foo": "bar", "Foo": []string{"bar"},
} }
ctx := NewContext(context.TODO(), md) ctx := NewContext(context.TODO(), md)
@@ -182,7 +183,7 @@ func TestMetadataContext(t *testing.T) {
t.Errorf("Unexpected error retrieving metadata, got %t", ok) t.Errorf("Unexpected error retrieving metadata, got %t", ok)
} }
if emd["Foo"] != md["Foo"] { if len(emd["Foo"]) != len(md["Foo"]) || (emd["Foo"][0] != md["Foo"][0]) {
t.Errorf("Expected key: %s val: %s, got key: %s val: %s", "Foo", md["Foo"], "Foo", emd["Foo"]) t.Errorf("Expected key: %s val: %s, got key: %s val: %s", "Foo", md["Foo"], "Foo", emd["Foo"])
} }

View File

@@ -6,6 +6,7 @@ import (
"time" "time"
"go.unistack.org/micro/v3/logger" "go.unistack.org/micro/v3/logger"
md "go.unistack.org/micro/v3/metadata"
"go.unistack.org/micro/v3/util/id" "go.unistack.org/micro/v3/util/id"
) )
@@ -23,7 +24,7 @@ type node struct {
type record struct { type record struct {
Name string Name string
Version string Version string
Metadata map[string]string Metadata md.Metadata
Nodes map[string]*node Nodes map[string]*node
Endpoints []*Endpoint Endpoints []*Endpoint
} }
@@ -136,9 +137,9 @@ func (m *memory) Register(ctx context.Context, s *Service, opts ...RegisterOptio
// domain is set in metadata so it can be passed to watchers // domain is set in metadata so it can be passed to watchers
if s.Metadata == nil { if s.Metadata == nil {
s.Metadata = map[string]string{"domain": options.Domain} s.Metadata = map[string][]string{"domain": {options.Domain}}
} else { } else {
s.Metadata["domain"] = options.Domain s.Metadata["domain"] = []string{options.Domain}
} }
// ensure the service name exists // ensure the service name exists
@@ -164,15 +165,10 @@ func (m *memory) Register(ctx context.Context, s *Service, opts ...RegisterOptio
continue continue
} }
metadata := make(map[string]string, len(n.Metadata)) metadata := md.Copy(n.Metadata)
// make copy of metadata
for k, v := range n.Metadata {
metadata[k] = v
}
// set the domain // set the domain
metadata["domain"] = options.Domain metadata["domain"] = []string{options.Domain}
// add the node // add the node
srvs[s.Name][s.Version].Nodes[n.ID] = &node{ srvs[s.Name][s.Version].Nodes[n.ID] = &node{
@@ -216,9 +212,9 @@ func (m *memory) Deregister(ctx context.Context, s *Service, opts ...DeregisterO
// domain is set in metadata so it can be passed to watchers // domain is set in metadata so it can be passed to watchers
if s.Metadata == nil { if s.Metadata == nil {
s.Metadata = map[string]string{"domain": options.Domain} s.Metadata = map[string][]string{"domain": {options.Domain}}
} else { } else {
s.Metadata["domain"] = options.Domain s.Metadata["domain"] = []string{options.Domain}
} }
// if the domain doesn't exist, there is nothing to deregister // if the domain doesn't exist, there is nothing to deregister
@@ -426,8 +422,8 @@ func (m *watcher) Next() (*Result, error) {
// extract domain from service metadata // extract domain from service metadata
var domain string var domain string
if r.Service.Metadata != nil && len(r.Service.Metadata["domain"]) > 0 { if r.Service.Metadata != nil && len(r.Service.Metadata["domain"]) > 0 && len(r.Service.Metadata["domain"][0]) > 0 {
domain = r.Service.Metadata["domain"] domain = r.Service.Metadata["domain"][0]
} else { } else {
domain = DefaultDomain domain = DefaultDomain
} }
@@ -452,10 +448,7 @@ func (m *watcher) Stop() {
} }
func serviceToRecord(s *Service, ttl time.Duration) *record { func serviceToRecord(s *Service, ttl time.Duration) *record {
metadata := make(map[string]string, len(s.Metadata)) metadata := md.Copy(s.Metadata)
for k, v := range s.Metadata {
metadata[k] = v
}
nodes := make(map[string]*node, len(s.Nodes)) nodes := make(map[string]*node, len(s.Nodes))
for _, n := range s.Nodes { for _, n := range s.Nodes {
@@ -481,41 +474,28 @@ func serviceToRecord(s *Service, ttl time.Duration) *record {
} }
func recordToService(r *record, domain string) *Service { func recordToService(r *record, domain string) *Service {
metadata := make(map[string]string, len(r.Metadata)) metadata := md.Copy(r.Metadata)
for k, v := range r.Metadata {
metadata[k] = v
}
// set the domain in metadata so it can be determined when a wildcard query is performed // set the domain in metadata so it can be determined when a wildcard query is performed
metadata["domain"] = domain metadata["domain"] = []string{domain}
endpoints := make([]*Endpoint, len(r.Endpoints)) endpoints := make([]*Endpoint, len(r.Endpoints))
for i, e := range r.Endpoints { for i, e := range r.Endpoints {
md := make(map[string]string, len(e.Metadata))
for k, v := range e.Metadata {
md[k] = v
}
endpoints[i] = &Endpoint{ endpoints[i] = &Endpoint{
Name: e.Name, Name: e.Name,
Request: e.Request, Request: e.Request,
Response: e.Response, Response: e.Response,
Metadata: md, Metadata: md.Copy(e.Metadata),
} }
} }
nodes := make([]*Node, len(r.Nodes)) nodes := make([]*Node, len(r.Nodes))
i := 0 i := 0
for _, n := range r.Nodes { for _, n := range r.Nodes {
md := make(map[string]string, len(n.Metadata))
for k, v := range n.Metadata {
md[k] = v
}
nodes[i] = &Node{ nodes[i] = &Node{
ID: n.ID, ID: n.ID,
Address: n.Address, Address: n.Address,
Metadata: md, Metadata: md.Copy(n.Metadata),
} }
i++ i++
} }

48
util/time/duration.go Normal file
View File

@@ -0,0 +1,48 @@
package time
import (
"fmt"
"time"
)
type Duration int64
func ParseDuration(s string) (time.Duration, error) {
if s == "" {
return 0, fmt.Errorf(`time: invalid duration "` + s + `"`)
}
//var sb strings.Builder
/*
for i, r := range s {
switch r {
case 'd':
n, err := strconv.Atoi(s[idx:i])
if err != nil {
return 0, errors.New("time: invalid duration " + s)
}
s[idx:i] = fmt.Sprintf("%d", n*24)
default:
sb.WriteRune(r)
}
}
*/
var td time.Duration
var err error
switch s[len(s)-1] {
case 's', 'm', 'h':
td, err = time.ParseDuration(s)
case 'd':
if td, err = time.ParseDuration(s[:len(s)-1] + "h"); err == nil {
td *= 24
}
case 'y':
if td, err = time.ParseDuration(s[:len(s)-1] + "h"); err == nil {
year := time.Date(time.Now().Year(), time.December, 31, 0, 0, 0, 0, time.Local)
days := year.YearDay()
td *= 24 * time.Duration(days)
}
}
return td, err
}

View File

@@ -0,0 +1,27 @@
package time
import (
"testing"
"time"
)
func TestParseDuration(t *testing.T) {
var td time.Duration
var err error
t.Skip()
td, err = ParseDuration("14d4h")
if err != nil {
t.Fatalf("ParseDuration error: %v", err)
}
if td.String() != "336h0m0s" {
t.Fatalf("ParseDuration 14d != 336h0m0s : %s", td.String())
}
td, err = ParseDuration("1y")
if err != nil {
t.Fatalf("ParseDuration error: %v", err)
}
if td.String() != "8760h0m0s" {
t.Fatalf("ParseDuration 1y != 8760h0m0s : %s", td.String())
}
}