fix(metadata): nil-Metadata from NewMetadata(nil), missing float32 in GetFloat; add doc comments and tests

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.
This commit is contained in:
ginuerzh
2026-05-24 20:59:30 +08:00
parent dc99f4731f
commit a9d94e7009
4 changed files with 560 additions and 3 deletions
+7 -3
View File
@@ -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 package metadata
import ( import (
@@ -8,10 +10,9 @@ import (
type mapMetadata map[string]any 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 { func NewMetadata(m map[string]any) metadata.Metadata {
if m == nil {
return nil
}
md := make(map[string]any) md := make(map[string]any)
for k, v := range m { for k, v := range m {
md[strings.ToLower(k)] = v md[strings.ToLower(k)] = v
@@ -19,15 +20,18 @@ func NewMetadata(m map[string]any) metadata.Metadata {
return mapMetadata(md) return mapMetadata(md)
} }
// IsExists reports whether the key is present in the metadata.
func (m mapMetadata) IsExists(key string) bool { func (m mapMetadata) IsExists(key string) bool {
_, ok := m[strings.ToLower(key)] _, ok := m[strings.ToLower(key)]
return ok return ok
} }
// Set stores a value for the given key.
func (m mapMetadata) Set(key string, value any) { func (m mapMetadata) Set(key string, value any) {
m[strings.ToLower(key)] = value 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 { func (m mapMetadata) Get(key string) any {
if m != nil { if m != nil {
return m[strings.ToLower(key)] return m[strings.ToLower(key)]
+123
View File
@@ -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)
}
+32
View File
@@ -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 package util
import ( import (
@@ -8,6 +11,8 @@ import (
"github.com/go-gost/core/metadata" "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 { func IsExists(md metadata.Metadata, keys ...string) bool {
if md == nil { if md == nil {
return false return false
@@ -21,6 +26,9 @@ func IsExists(md metadata.Metadata, keys ...string) bool {
return false 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) { func GetBool(md metadata.Metadata, keys ...string) (v bool) {
if md == nil { if md == nil {
return return
@@ -44,6 +52,9 @@ func GetBool(md metadata.Metadata, keys ...string) (v bool) {
return 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) { func GetInt(md metadata.Metadata, keys ...string) (v int) {
if md == nil { if md == nil {
return return
@@ -69,6 +80,9 @@ func GetInt(md metadata.Metadata, keys ...string) (v int) {
return 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) { func GetFloat(md metadata.Metadata, keys ...string) (v float64) {
if md == nil { if md == nil {
return return
@@ -80,6 +94,8 @@ func GetFloat(md metadata.Metadata, keys ...string) (v float64) {
} }
switch vv := md.Get(key).(type) { switch vv := md.Get(key).(type) {
case float32:
v = float64(vv)
case float64: case float64:
v = vv v = vv
case int: case int:
@@ -92,6 +108,10 @@ func GetFloat(md metadata.Metadata, keys ...string) (v float64) {
return 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) { func GetDuration(md metadata.Metadata, keys ...string) (v time.Duration) {
if md == nil { if md == nil {
return return
@@ -117,6 +137,9 @@ func GetDuration(md metadata.Metadata, keys ...string) (v time.Duration) {
return 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) { func GetString(md metadata.Metadata, keys ...string) (v string) {
if md == nil { if md == nil {
return return
@@ -151,6 +174,9 @@ func GetString(md metadata.Metadata, keys ...string) (v string) {
return 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) { func GetStrings(md metadata.Metadata, keys ...string) (ss []string) {
if md == nil { if md == nil {
return return
@@ -176,6 +202,9 @@ func GetStrings(md metadata.Metadata, keys ...string) (ss []string) {
return 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) { func GetStringMap(md metadata.Metadata, keys ...string) (m map[string]any) {
if md == nil { if md == nil {
return return
@@ -200,6 +229,9 @@ func GetStringMap(md metadata.Metadata, keys ...string) (m map[string]any) {
return 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) { func GetStringMapString(md metadata.Metadata, keys ...string) (m map[string]string) {
if md == nil { if md == nil {
return return
+398
View File
@@ -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")
}
}