From a9d94e7009cf34ae1d028a3746218047852cd14c Mon Sep 17 00:00:00 2001 From: ginuerzh Date: Sun, 24 May 2026 20:59:30 +0800 Subject: [PATCH] fix(metadata): nil-Metadata from NewMetadata(nil), missing float32 in GetFloat; add doc comments and tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit NewMetadata(nil) previously returned a nil Metadata interface, causing panics on direct .Get()/.IsExists() calls. Now returns an empty map. GetFloat added float32→float64 promotion, matching GetString behavior. Doc comments for all 13 exported symbols across both packages. 41 unit tests (11 metadata + 30 util) at 100% coverage. --- metadata/metadata.go | 10 +- metadata/metadata_test.go | 123 ++++++++++++ metadata/util/util.go | 32 +++ metadata/util/util_test.go | 398 +++++++++++++++++++++++++++++++++++++ 4 files changed, 560 insertions(+), 3 deletions(-) create mode 100644 metadata/metadata_test.go create mode 100644 metadata/util/util_test.go diff --git a/metadata/metadata.go b/metadata/metadata.go index e01bc923..8948b9d7 100644 --- a/metadata/metadata.go +++ b/metadata/metadata.go @@ -1,3 +1,5 @@ +// Package metadata provides the default implementation of the core/metadata.Metadata +// interface using a map[string]any with case-insensitive keys. package metadata import ( @@ -8,10 +10,9 @@ import ( type mapMetadata map[string]any +// NewMetadata creates a new Metadata from the given map. Keys are lowercased for +// case-insensitive lookup. If m is nil, an empty Metadata is returned. func NewMetadata(m map[string]any) metadata.Metadata { - if m == nil { - return nil - } md := make(map[string]any) for k, v := range m { md[strings.ToLower(k)] = v @@ -19,15 +20,18 @@ func NewMetadata(m map[string]any) metadata.Metadata { return mapMetadata(md) } +// IsExists reports whether the key is present in the metadata. func (m mapMetadata) IsExists(key string) bool { _, ok := m[strings.ToLower(key)] return ok } +// Set stores a value for the given key. func (m mapMetadata) Set(key string, value any) { m[strings.ToLower(key)] = value } +// Get retrieves the value for the given key, or nil if the key is not present. func (m mapMetadata) Get(key string) any { if m != nil { return m[strings.ToLower(key)] diff --git a/metadata/metadata_test.go b/metadata/metadata_test.go new file mode 100644 index 00000000..34fe3ac1 --- /dev/null +++ b/metadata/metadata_test.go @@ -0,0 +1,123 @@ +package metadata + +import ( + "testing" + + "github.com/go-gost/core/metadata" +) + +func TestNewMetadata(t *testing.T) { + md := NewMetadata(map[string]any{"Key": "val", "FOO": "bar"}) + if md == nil { + t.Fatal("NewMetadata returned nil") + } + + if !md.IsExists("key") { + t.Error("key not found") + } + if !md.IsExists("KEY") { + t.Error("KEY not found (case-insensitive)") + } + if !md.IsExists("foo") { + t.Error("foo not found") + } + if md.IsExists("baz") { + t.Error("baz should not exist") + } +} + +func TestNewMetadata_Nil(t *testing.T) { + md := NewMetadata(nil) + if md == nil { + t.Fatal("NewMetadata(nil) returned nil — expected empty Metadata") + } + if md.IsExists("any") { + t.Error("empty metadata should have no keys") + } +} + +func TestNewMetadata_Empty(t *testing.T) { + md := NewMetadata(map[string]any{}) + if md == nil { + t.Fatal("NewMetadata(empty) returned nil") + } + if md.IsExists("any") { + t.Error("empty metadata should have no keys") + } +} + +func TestMapMetadata_IsExists(t *testing.T) { + md := NewMetadata(map[string]any{"foo": "bar"}).(mapMetadata) + if !md.IsExists("foo") { + t.Error("IsExists(foo) should be true") + } + if !md.IsExists("FOO") { + t.Error("IsExists(FOO) should be true (case-insensitive)") + } + if md.IsExists("bar") { + t.Error("IsExists(bar) should be false") + } +} + +func TestMapMetadata_Get(t *testing.T) { + md := NewMetadata(map[string]any{"foo": "bar", "num": 42}).(mapMetadata) + + if v := md.Get("foo"); v != "bar" { + t.Errorf("Get(foo) = %v, want bar", v) + } + if v := md.Get("FOO"); v != "bar" { + t.Errorf("Get(FOO) = %v, want bar (case-insensitive)", v) + } + if v := md.Get("num"); v != 42 { + t.Errorf("Get(num) = %v, want 42", v) + } + if v := md.Get("baz"); v != nil { + t.Errorf("Get(baz) = %v, want nil", v) + } +} + +func TestMapMetadata_Get_NilReceiver(t *testing.T) { + var m mapMetadata + if v := m.Get("foo"); v != nil { + t.Errorf("Get on nil mapMetadata = %v, want nil", v) + } +} + +func TestMapMetadata_IsExists_NilReceiver(t *testing.T) { + var m mapMetadata + if m.IsExists("foo") { + t.Error("IsExists on nil mapMetadata should be false") + } +} + +func TestMapMetadata_Set(t *testing.T) { + md := NewMetadata(nil).(mapMetadata) + md.Set("foo", "bar") + if !md.IsExists("foo") { + t.Error("IsExists(foo) should be true after Set") + } + if v := md.Get("foo"); v != "bar" { + t.Errorf("Get(foo) = %v, want bar", v) + } +} + +func TestMapMetadata_Set_CaseInsensitive(t *testing.T) { + md := NewMetadata(nil).(mapMetadata) + md.Set("FOO", "bar") + if v := md.Get("foo"); v != "bar" { + t.Errorf("Get(foo) after Set(FOO) = %v, want bar", v) + } +} + +func TestMapMetadata_Set_Overwrite(t *testing.T) { + md := NewMetadata(map[string]any{"foo": "old"}).(mapMetadata) + md.Set("FOO", "new") + if v := md.Get("foo"); v != "new" { + t.Errorf("Get(foo) after overwrite = %v, want new", v) + } +} + +func TestInterfaceCompliance(t *testing.T) { + var _ metadata.Metadata = NewMetadata(nil) + var _ metadata.Metadata = mapMetadata(nil) +} diff --git a/metadata/util/util.go b/metadata/util/util.go index c5dad69f..5e633ec4 100644 --- a/metadata/util/util.go +++ b/metadata/util/util.go @@ -1,3 +1,6 @@ +// Package util provides typed accessor functions for reading values from a +// metadata.Metadata store. Each getter accepts one or more fallback keys and +// returns the first match. All functions are nil-safe on the metadata argument. package util import ( @@ -8,6 +11,8 @@ import ( "github.com/go-gost/core/metadata" ) +// IsExists reports whether at least one of the given keys is present in md. +// Returns false if md is nil. func IsExists(md metadata.Metadata, keys ...string) bool { if md == nil { return false @@ -21,6 +26,9 @@ func IsExists(md metadata.Metadata, keys ...string) bool { return false } +// GetBool returns the bool value for the first matching key. String values are +// parsed with strconv.ParseBool, and non-zero integers are treated as true. +// Returns false if md is nil or no key matches. func GetBool(md metadata.Metadata, keys ...string) (v bool) { if md == nil { return @@ -44,6 +52,9 @@ func GetBool(md metadata.Metadata, keys ...string) (v bool) { return } +// GetInt returns the int value for the first matching key. String values are +// parsed with strconv.Atoi, and true/false booleans are treated as 1/0. +// Returns 0 if md is nil or no key matches. func GetInt(md metadata.Metadata, keys ...string) (v int) { if md == nil { return @@ -69,6 +80,9 @@ func GetInt(md metadata.Metadata, keys ...string) (v int) { return } +// GetFloat returns the float64 value for the first matching key. String values +// are parsed with strconv.ParseFloat, float32 values are promoted, and int +// values are converted. Returns 0 if md is nil or no key matches. func GetFloat(md metadata.Metadata, keys ...string) (v float64) { if md == nil { return @@ -80,6 +94,8 @@ func GetFloat(md metadata.Metadata, keys ...string) (v float64) { } switch vv := md.Get(key).(type) { + case float32: + v = float64(vv) case float64: v = vv case int: @@ -92,6 +108,10 @@ func GetFloat(md metadata.Metadata, keys ...string) (v float64) { return } +// GetDuration returns the time.Duration value for the first matching key. +// String values are parsed with time.ParseDuration; if that fails they are +// retried as integer seconds via strconv.Atoi. Integer values are treated as +// seconds. Returns 0 if md is nil or no key matches. func GetDuration(md metadata.Metadata, keys ...string) (v time.Duration) { if md == nil { return @@ -117,6 +137,9 @@ func GetDuration(md metadata.Metadata, keys ...string) (v time.Duration) { return } +// GetString returns the string value for the first matching key. Non-string +// values (int, int64, uint, uint64, bool, float32, float64) are formatted via +// strconv. Returns "" if md is nil or no key matches. func GetString(md metadata.Metadata, keys ...string) (v string) { if md == nil { return @@ -151,6 +174,9 @@ func GetString(md metadata.Metadata, keys ...string) (v string) { return } +// GetStrings returns the []string value for the first matching key. []any +// slices are converted element-by-element, skipping non-string values. +// Returns nil if md is nil or no key matches. func GetStrings(md metadata.Metadata, keys ...string) (ss []string) { if md == nil { return @@ -176,6 +202,9 @@ func GetStrings(md metadata.Metadata, keys ...string) (ss []string) { return } +// GetStringMap returns the map[string]any value for the first matching key. +// map[any]any values are converted by formatting keys with %v. Returns nil if +// md is nil or no key matches. func GetStringMap(md metadata.Metadata, keys ...string) (m map[string]any) { if md == nil { return @@ -200,6 +229,9 @@ func GetStringMap(md metadata.Metadata, keys ...string) (m map[string]any) { return } +// GetStringMapString returns the map[string]string value for the first matching +// key. Both map[string]any and map[any]any values are converted by formatting +// values with %v. Returns nil if md is nil or no key matches. func GetStringMapString(md metadata.Metadata, keys ...string) (m map[string]string) { if md == nil { return diff --git a/metadata/util/util_test.go b/metadata/util/util_test.go new file mode 100644 index 00000000..adcad264 --- /dev/null +++ b/metadata/util/util_test.go @@ -0,0 +1,398 @@ +package util + +import ( + "testing" + "time" + + "github.com/go-gost/core/metadata" + mdx "github.com/go-gost/x/metadata" +) + +func TestIsExists(t *testing.T) { + md := mdx.NewMetadata(map[string]any{"foo": "bar"}) + if !IsExists(md, "foo") { + t.Error("IsExists(foo) should be true") + } + if IsExists(md, "bar") { + t.Error("IsExists(bar) should be false") + } + if !IsExists(md, "BAR", "foo") { + t.Error("IsExists(BAR, foo): foo exists, should return true") + } +} + +func TestIsExists_TrueWithFallback(t *testing.T) { + md := mdx.NewMetadata(map[string]any{"foo": "bar"}) + if !IsExists(md, "bar", "foo") { + t.Error("IsExists(bar, foo): foo exists, should return true") + } + if IsExists(md, "bar", "baz") { + t.Error("IsExists(bar, baz): neither exists, should return false") + } +} + +func TestIsExists_Nil(t *testing.T) { + if IsExists(nil, "foo") { + t.Error("IsExists on nil Metadata should be false") + } +} + +func TestGetBool(t *testing.T) { + md := mdx.NewMetadata(map[string]any{ + "b": true, + "i": 1, + "i0": 0, + "s": "true", + "s2": "false", + "bad": "notabool", + }) + if v := GetBool(md, "b"); v != true { + t.Errorf("GetBool(b) = %v, want true", v) + } + if v := GetBool(md, "i"); v != true { + t.Errorf("GetBool(i=1) = %v, want true", v) + } + if v := GetBool(md, "i0"); v != false { + t.Errorf("GetBool(i=0) = %v, want false", v) + } + if v := GetBool(md, "s"); v != true { + t.Errorf("GetBool(s=true) = %v, want true", v) + } + if v := GetBool(md, "s2"); v != false { + t.Errorf("GetBool(s=false) = %v, want false", v) + } + if v := GetBool(md, "bad"); v != false { + t.Errorf("GetBool(bad) = %v, want false (parse error)", v) + } +} + +func TestGetBool_MissingKey(t *testing.T) { + md := mdx.NewMetadata(map[string]any{}) + if v := GetBool(md, "nonexistent"); v != false { + t.Errorf("GetBool(nonexistent) = %v, want false", v) + } +} + +func TestGetBool_Nil(t *testing.T) { + if v := GetBool(nil, "foo"); v != false { + t.Errorf("GetBool on nil = %v, want false", v) + } +} + +func TestGetInt(t *testing.T) { + md := mdx.NewMetadata(map[string]any{ + "i": 42, + "s": "99", + "b": true, + "bf": false, + "bad": "notanumber", + }) + if v := GetInt(md, "i"); v != 42 { + t.Errorf("GetInt(i) = %v, want 42", v) + } + if v := GetInt(md, "s"); v != 99 { + t.Errorf("GetInt(s) = %v, want 99", v) + } + if v := GetInt(md, "b"); v != 1 { + t.Errorf("GetInt(b=true) = %v, want 1", v) + } + if v := GetInt(md, "bf"); v != 0 { + t.Errorf("GetInt(bf=false) = %v, want 0", v) + } + if v := GetInt(md, "bad"); v != 0 { + t.Errorf("GetInt(bad) = %v, want 0", v) + } +} + +func TestGetInt_MissingKey(t *testing.T) { + md := mdx.NewMetadata(nil) + if v := GetInt(md, "nonexistent"); v != 0 { + t.Errorf("GetInt(nonexistent) = %v, want 0", v) + } +} + +func TestGetInt_Nil(t *testing.T) { + if v := GetInt(nil, "foo"); v != 0 { + t.Errorf("GetInt on nil = %v, want 0", v) + } +} + +func TestGetFloat(t *testing.T) { + md := mdx.NewMetadata(map[string]any{ + "f64": float64(3.14), + "f32": float32(2.5), + "i": 42, + "s": "1.5", + "bad": "notanumber", + }) + if v := GetFloat(md, "f64"); v != 3.14 { + t.Errorf("GetFloat(f64) = %v, want 3.14", v) + } + if v := GetFloat(md, "f32"); v != 2.5 { + t.Errorf("GetFloat(f32) = %v, want 2.5", v) + } + if v := GetFloat(md, "i"); v != 42.0 { + t.Errorf("GetFloat(i) = %v, want 42.0", v) + } + if v := GetFloat(md, "s"); v != 1.5 { + t.Errorf("GetFloat(s) = %v, want 1.5", v) + } + if v := GetFloat(md, "bad"); v != 0 { + t.Errorf("GetFloat(bad) = %v, want 0", v) + } +} + +func TestGetFloat_MissingKey(t *testing.T) { + md := mdx.NewMetadata(nil) + if v := GetFloat(md, "nonexistent"); v != 0 { + t.Errorf("GetFloat(nonexistent) = %v, want 0", v) + } +} + +func TestGetFloat_Nil(t *testing.T) { + if v := GetFloat(nil, "foo"); v != 0 { + t.Errorf("GetFloat on nil = %v, want 0", v) + } +} + +func TestGetDuration(t *testing.T) { + md := mdx.NewMetadata(map[string]any{ + "ds": "5s", + "dms": "100ms", + "di": 30, + "dis": "60", + "bad": "notaduration", + }) + if v := GetDuration(md, "ds"); v != 5*time.Second { + t.Errorf("GetDuration(5s) = %v, want %v", v, 5*time.Second) + } + if v := GetDuration(md, "dms"); v != 100*time.Millisecond { + t.Errorf("GetDuration(100ms) = %v, want %v", v, 100*time.Millisecond) + } + if v := GetDuration(md, "di"); v != 30*time.Second { + t.Errorf("GetDuration(30) = %v, want %v", v, 30*time.Second) + } + if v := GetDuration(md, "dis"); v != 60*time.Second { + t.Errorf("GetDuration(\"60\") = %v, want %v", v, 60*time.Second) + } + if v := GetDuration(md, "bad"); v != 0 { + t.Errorf("GetDuration(bad) = %v, want 0", v) + } +} + +func TestGetDuration_MissingKey(t *testing.T) { + md := mdx.NewMetadata(nil) + if v := GetDuration(md, "nonexistent"); v != 0 { + t.Errorf("GetDuration(nonexistent) = %v, want 0", v) + } +} + +func TestGetDuration_Nil(t *testing.T) { + if v := GetDuration(nil, "foo"); v != 0 { + t.Errorf("GetDuration on nil = %v, want 0", v) + } +} + +func TestGetString(t *testing.T) { + md := mdx.NewMetadata(map[string]any{ + "s": "hello", + "i": 42, + "i64": int64(99), + "u": uint(7), + "u64": uint64(100), + "b": true, + "f32": float32(3.14), + "f64": float64(2.718), + }) + if v := GetString(md, "s"); v != "hello" { + t.Errorf("GetString(s) = %q, want hello", v) + } + if v := GetString(md, "i"); v != "42" { + t.Errorf("GetString(i) = %q, want 42", v) + } + if v := GetString(md, "i64"); v != "99" { + t.Errorf("GetString(i64) = %q, want 99", v) + } + if v := GetString(md, "u"); v != "7" { + t.Errorf("GetString(u) = %q, want 7", v) + } + if v := GetString(md, "u64"); v != "100" { + t.Errorf("GetString(u64) = %q, want 100", v) + } + if v := GetString(md, "b"); v != "true" { + t.Errorf("GetString(b) = %q, want true", v) + } + if v := GetString(md, "f32"); v != "3.14" { + t.Errorf("GetString(f32) = %q, want 3.14", v) + } + if v := GetString(md, "f64"); v != "2.718" { + t.Errorf("GetString(f64) = %q, want 2.718", v) + } +} + +func TestGetString_MissingKey(t *testing.T) { + md := mdx.NewMetadata(nil) + if v := GetString(md, "nonexistent"); v != "" { + t.Errorf("GetString(nonexistent) = %q, want empty", v) + } +} + +func TestGetString_Nil(t *testing.T) { + if v := GetString(nil, "foo"); v != "" { + t.Errorf("GetString on nil = %q, want empty", v) + } +} + +func TestGetStrings(t *testing.T) { + md := mdx.NewMetadata(map[string]any{ + "ss": []string{"a", "b", "c"}, + "sa": []any{"x", "y", "z"}, + "mixed": []any{"hello", 42, "world"}, + }) + if v := GetStrings(md, "ss"); len(v) != 3 || v[0] != "a" || v[1] != "b" || v[2] != "c" { + t.Errorf("GetStrings(ss) = %v, want [a b c]", v) + } + if v := GetStrings(md, "sa"); len(v) != 3 || v[0] != "x" || v[1] != "y" || v[2] != "z" { + t.Errorf("GetStrings(sa) = %v, want [x y z]", v) + } + if v := GetStrings(md, "mixed"); len(v) != 2 || v[0] != "hello" || v[1] != "world" { + t.Errorf("GetStrings(mixed) = %v, want [hello world]", v) + } +} + +func TestGetStrings_MissingKey(t *testing.T) { + md := mdx.NewMetadata(nil) + if v := GetStrings(md, "nonexistent"); v != nil { + t.Errorf("GetStrings(nonexistent) = %v, want nil", v) + } +} + +func TestGetStrings_Nil(t *testing.T) { + if v := GetStrings(nil, "foo"); v != nil { + t.Errorf("GetStrings on nil = %v, want nil", v) + } +} + +func TestGetStringMap(t *testing.T) { + md := mdx.NewMetadata(map[string]any{ + "m": map[string]any{"k1": "v1", "k2": 42}, + "a": map[any]any{"x": "y", 1: "one"}, + }) + m := GetStringMap(md, "m") + if m["k1"] != "v1" { + t.Errorf("m[k1] = %v, want v1", m["k1"]) + } + if m["k2"] != 42 { + t.Errorf("m[k2] = %v, want 42", m["k2"]) + } + + a := GetStringMap(md, "a") + if a["x"] != "y" { + t.Errorf("a[x] = %v, want y", a["x"]) + } + if a["1"] != "one" { + t.Errorf("a[1] = %v, want one", a["1"]) + } +} + +func TestGetStringMap_MissingKey(t *testing.T) { + md := mdx.NewMetadata(nil) + if v := GetStringMap(md, "nonexistent"); v != nil { + t.Errorf("GetStringMap(nonexistent) = %v, want nil", v) + } +} + +func TestGetStringMap_Nil(t *testing.T) { + if v := GetStringMap(nil, "foo"); v != nil { + t.Errorf("GetStringMap on nil = %v, want nil", v) + } +} + +func TestGetStringMapString(t *testing.T) { + md := mdx.NewMetadata(map[string]any{ + "m": map[string]any{"k1": "v1", "k2": 42}, + "a": map[any]any{"x": "y", 1: "one"}, + }) + m := GetStringMapString(md, "m") + if m["k1"] != "v1" { + t.Errorf("m[k1] = %v, want v1", m["k1"]) + } + if m["k2"] != "42" { + t.Errorf("m[k2] = %v, want 42", m["k2"]) + } + + a := GetStringMapString(md, "a") + if a["x"] != "y" { + t.Errorf("a[x] = %v, want y", a["x"]) + } + if a["1"] != "one" { + t.Errorf("a[1] = %v, want one", a["1"]) + } +} + +func TestGetStringMapString_MissingKey(t *testing.T) { + md := mdx.NewMetadata(nil) + if v := GetStringMapString(md, "nonexistent"); v != nil { + t.Errorf("GetStringMapString(nonexistent) = %v, want nil", v) + } +} + +func TestGetStringMapString_Nil(t *testing.T) { + if v := GetStringMapString(nil, "foo"); v != nil { + t.Errorf("GetStringMapString on nil = %v, want nil", v) + } +} + +func TestFallbackKeys(t *testing.T) { + md := mdx.NewMetadata(map[string]any{ + "second": "fallback-val", + }) + if v := GetString(md, "first", "second"); v != "fallback-val" { + t.Errorf("GetString with fallback = %q, want fallback-val", v) + } + if v := GetBool(md, "first", "second"); v != false { + t.Errorf("GetBool: string should not match bool fallback, got %v", v) + } +} + +func TestFallbackKeys_FirstWins(t *testing.T) { + md := mdx.NewMetadata(map[string]any{ + "first": "winner", + "second": "loser", + }) + if v := GetString(md, "first", "second"); v != "winner" { + t.Errorf("GetString(first, second) = %q, want winner", v) + } +} + +// Ensure nil interface (not just nil mapMetadata) works for all functions. +func TestNilInterface(t *testing.T) { + var md metadata.Metadata + if IsExists(md, "foo") { + t.Error("IsExists on nil Metadata interface should be false") + } + if GetBool(md, "foo") { + t.Error("GetBool on nil Metadata interface should be false") + } + if GetInt(md, "foo") != 0 { + t.Error("GetInt on nil Metadata interface should be 0") + } + if GetFloat(md, "foo") != 0 { + t.Error("GetFloat on nil Metadata interface should be 0") + } + if GetDuration(md, "foo") != 0 { + t.Error("GetDuration on nil Metadata interface should be 0") + } + if GetString(md, "foo") != "" { + t.Error("GetString on nil Metadata interface should be empty") + } + if GetStrings(md, "foo") != nil { + t.Error("GetStrings on nil Metadata interface should be nil") + } + if GetStringMap(md, "foo") != nil { + t.Error("GetStringMap on nil Metadata interface should be nil") + } + if GetStringMapString(md, "foo") != nil { + t.Error("GetStringMapString on nil Metadata interface should be nil") + } +}