diff --git a/accessors.go b/accessors.go index 746b86e..fbc2667 100644 --- a/accessors.go +++ b/accessors.go @@ -43,7 +43,7 @@ var mapAccessRegex = regexp.MustCompile(mapAccessRegexString) // // o.Get("books[1].chapters[2].title") func (m Map) Get(selector string) *Value { - rawObj := access(m, selector, nil, false) + rawObj, _ := access(m, selector, nil, false) return &Value{data: rawObj} } @@ -109,7 +109,7 @@ func getKey(s string) (string, string) { // access accesses the object using the selector and performs the // appropriate action. -func access(current any, selector string, value any, isSet bool) any { +func access(current any, selector string, value any, isSet bool) (any, bool) { thisSel, nextSel := getKey(selector) indexes := []int{} @@ -132,7 +132,7 @@ func access(current any, selector string, value any, isSet bool) any { curMSI := current.(map[string]any) if nextSel == "" && isSet && len(indexes) == 0 { curMSI[thisSel] = value - return nil + return nil, true } _, ok := curMSI[thisSel].(map[string]any) @@ -144,9 +144,12 @@ func access(current any, selector string, value any, isSet bool) any { curMSI[thisSel] = map[string]any{} } - current = curMSI[thisSel] + current, ok = curMSI[thisSel] + if !ok { + return nil, false + } default: - current = nil + return nil, false } // do we need to access the item of an array? @@ -160,24 +163,22 @@ func access(current any, selector string, value any, isSet bool) any { if index >= 0 && index < len(array) { if isSet && nextSel == "" && num == 0 { array[index] = value - return nil + return nil, true } current = array[index] } else { - current = nil - break + return nil, false } } else { - current = nil - break + return nil, false } } } if nextSel != "" { - current = access(current, nextSel, value, isSet) + return access(current, nextSel, value, isSet) } - return current + return current, true } func interSlice(slice any) ([]any, bool) { diff --git a/tests.go b/tests.go index d9e0b47..59f2ee5 100644 --- a/tests.go +++ b/tests.go @@ -1,14 +1,15 @@ package objx -// Has gets whether there is something at the specified selector -// or not. +// Has reports whether the specified selector exists, even when the +// value at the selector is nil. // // If m is nil, Has will always return false. func (m Map) Has(selector string) bool { if m == nil { return false } - return !m.Get(selector).IsNil() + _, ok := access(m, selector, nil, false) + return ok } // IsNil gets whether the data is nil or not. diff --git a/tests_test.go b/tests_test.go index 81eeef1..eb6a51d 100644 --- a/tests_test.go +++ b/tests_test.go @@ -2,6 +2,8 @@ package objx_test import ( "testing" + + "github.com/stretchr/objx" ) func TestHas(t *testing.T) { @@ -20,3 +22,62 @@ func TestHas(t *testing.T) { assert.False(t, m.Has("nothing")) } + +func TestHasWithNil(t *testing.T) { + m := objx.Map{ + "nilField": nil, + "nested": objx.Map{ + "nilChild": nil, + "valid": "hello", + }, + "domains": objx.Map{ + "example.com": objx.Map{ + "nilApex": nil, + "apex": "example", + }, + }, + "nilSlice": []any{nil, "value"}, + "nestedSlice": []any{[]any{nil, "nestedVal"}}, + } + + assert.True(t, m.Has("nilField")) + assert.True(t, m.Has("nested.nilChild")) + assert.True(t, m.Has("nested.valid")) + assert.True(t, m.Has("domains[example.com].nilApex")) + assert.True(t, m.Has("domains[example.com][nilApex]")) + assert.True(t, m.Has("domains[example.com].apex")) + assert.True(t, m.Has("nilSlice[0]")) + assert.True(t, m.Has("nilSlice[1]")) + assert.True(t, m.Has("nestedSlice[0][0]")) + assert.True(t, m.Has("nestedSlice[0][1]")) + + assert.False(t, m.Has("nonExistent")) + assert.False(t, m.Has("nested.nonExistent")) + assert.False(t, m.Has("domains[example.com].nonExistent")) + assert.False(t, m.Has("domains[missing.com].nilApex")) + assert.False(t, m.Has("nilSlice[2]")) + assert.False(t, m.Has("nestedSlice[0][2]")) + assert.False(t, m.Has("nestedSlice[1][0]")) + assert.False(t, m.Has("nilField.child")) + assert.False(t, m.Has("nilSlice[0].child")) + + // Verify we can differentiate nil value from not found + assert.True(t, m.Has("nilField")) + assert.True(t, m.Get("nilField").IsNil()) + + assert.False(t, m.Has("nonExistent")) + assert.True(t, m.Get("nonExistent").IsNil()) +} + +func TestHasDifferentiateNilFromNotFound(t *testing.T) { + obj := objx.Map(map[string]any{ + "foo": map[string]any{ + "bar": 5, + "baz": nil, + }, + }) + + assert.True(t, obj.Has("foo.bar")) + assert.True(t, obj.Has("foo.baz")) + assert.False(t, obj.Has("foo.qux")) +}