Skip to content

Commit

Permalink
Add DeepSearch
Browse files Browse the repository at this point in the history
  • Loading branch information
egibs committed Dec 29, 2023
1 parent bd3ffc4 commit 906695a
Show file tree
Hide file tree
Showing 7 changed files with 392 additions and 86 deletions.
91 changes: 85 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ This project was mostly done to hack on some Go after spending awhile way from i

Usage examples can be found in `deepwalk_test.go`, but because code is only as good as its documentation, examples will be added (and added to) below.

Additionally, a naive search method is also provided via `DeepSearch`. This method traverses a data structure and returns all values for the provided key. This method is useful when the structure of the data is not known beforehand or can change.

## Installation

To install `deepwalk`, run the following:
Expand Down Expand Up @@ -88,6 +90,53 @@ if err != nil {
fmt.Println(value)
```

### Structs

Structs can also be traversed with `DeepWalk`:
```go
type TestStruct struct {
Field1 string
Field2 int
NestedStruct struct {
NestedStruct2 struct {
NestedField1 string
NestedField2 int
}
NestedField1 string
NestedField2 int
}
}

testStruct := TestStruct{
Field1: "test",
Field2: 123,
NestedStruct: struct {
NestedStruct2 struct {
NestedField1 string
NestedField2 int
}
NestedField1 string
NestedField2 int
}{
NestedStruct2: struct {
NestedField1 string
NestedField2 int
}{
NestedField1: "nested",
NestedField2: 456,
},
NestedField1: "nested",
NestedField2: 456,
},
}

values, err := DeepWalk(testStruct, []string{"NestedStruct", "NestedStruct2", "NestedField1"}, "<NO_VALUE>", "all")
if err != nil {
fmt.Println(err)
}
fmt.Println(value)
```

## Testing
Several categories of tests are included:
1. A manual JSON object
Expand All @@ -99,7 +148,29 @@ Categories three and four run one-thousand iterations of each variant and each d

Run all of the included tests by running `make test`:
```sh
❯ make test
go test ./... -v
go: downloading github.com/wk8/go-ordered-map/v2 v2.1.8
go: downloading gopkg.in/yaml.v3 v3.0.1
go: downloading github.com/buger/jsonparser v1.1.1
go: downloading github.com/mailru/easyjson v0.7.7
go: downloading github.com/bahlo/generic-list-go v0.2.0
=== RUN TestDeepSearch
=== RUN TestDeepSearch/Test_case_1_-_key_found_in_map
=== RUN TestDeepSearch/Test_case_2_-_key_not_found_in_map
=== RUN TestDeepSearch/Test_case_3_-_key_found_in_nested_map
=== RUN TestDeepSearch/Test_case_4_-_key_not_found_in_nested_map
=== RUN TestDeepSearch/Test_case_7_-_key_found_in_struct
=== RUN TestDeepSearch/Test_case_8_-_key_not_found_in_struct
=== RUN TestDeepSearch/Test_case_9_-_duplicate_key_found_in_map
--- PASS: TestDeepSearch (0.00s)
--- PASS: TestDeepSearch/Test_case_1_-_key_found_in_map (0.00s)
--- PASS: TestDeepSearch/Test_case_2_-_key_not_found_in_map (0.00s)
--- PASS: TestDeepSearch/Test_case_3_-_key_found_in_nested_map (0.00s)
--- PASS: TestDeepSearch/Test_case_4_-_key_not_found_in_nested_map (0.00s)
--- PASS: TestDeepSearch/Test_case_7_-_key_found_in_struct (0.00s)
--- PASS: TestDeepSearch/Test_case_8_-_key_not_found_in_struct (0.00s)
--- PASS: TestDeepSearch/Test_case_9_-_duplicate_key_found_in_map (0.00s)
=== RUN TestDeepwalkMinimalJSON
--- PASS: TestDeepwalkMinimalJSON (0.00s)
=== RUN TestDeepwalkMinimalMap
Expand All @@ -117,8 +188,6 @@ go test ./... -v
=== RUN TestDeepWalk/Test_case_10_-_array_of_maps_with_multiple_matching_keys,_return_last
=== RUN TestDeepWalk/Test_case_11_-_array_of_maps_with_multiple_matching_keys,_return_first
=== RUN TestDeepWalk/Test_case_12_-_array_of_maps_with_multiple_matching_keys,_return_default
=== RUN TestDeepWalk/Test_case_13_-_array_of_maps_with_multiple_matching_keys,_return_default
=== RUN TestDeepWalk/Test_case_14_-_array_of_maps_with_multiple_matching_keys,_return_default
--- PASS: TestDeepWalk (0.00s)
--- PASS: TestDeepWalk/Test_case_1_-_empty_object (0.00s)
--- PASS: TestDeepWalk/Test_case_2_-_empty_keys (0.00s)
Expand All @@ -132,14 +201,24 @@ go test ./... -v
--- PASS: TestDeepWalk/Test_case_10_-_array_of_maps_with_multiple_matching_keys,_return_last (0.00s)
--- PASS: TestDeepWalk/Test_case_11_-_array_of_maps_with_multiple_matching_keys,_return_first (0.00s)
--- PASS: TestDeepWalk/Test_case_12_-_array_of_maps_with_multiple_matching_keys,_return_default (0.00s)
--- PASS: TestDeepWalk/Test_case_13_-_array_of_maps_with_multiple_matching_keys,_return_default (0.00s)
--- PASS: TestDeepWalk/Test_case_14_-_array_of_maps_with_multiple_matching_keys,_return_default (0.00s)
=== RUN TestDeepwalkRandomSuccess
--- PASS: TestDeepwalkRandomSuccess (0.25s)
=== RUN TestDeepwalkRandomDefault
--- PASS: TestDeepwalkRandomDefault (3.22s)
--- PASS: TestDeepwalkRandomDefault (3.18s)
=== RUN TestDeepWalkWithStruct
=== RUN TestDeepWalkWithStruct/Test_Field1
=== RUN TestDeepWalkWithStruct/Test_Field2
=== RUN TestDeepWalkWithStruct/Test_NestedField1
=== RUN TestDeepWalkWithStruct/Test_NestedField2
=== RUN TestDeepWalkWithStruct/Test_Second-level_NestedField1
--- PASS: TestDeepWalkWithStruct (0.00s)
--- PASS: TestDeepWalkWithStruct/Test_Field1 (0.00s)
--- PASS: TestDeepWalkWithStruct/Test_Field2 (0.00s)
--- PASS: TestDeepWalkWithStruct/Test_NestedField1 (0.00s)
--- PASS: TestDeepWalkWithStruct/Test_NestedField2 (0.00s)
--- PASS: TestDeepWalkWithStruct/Test_Second-level_NestedField1 (0.00s)
PASS
ok github.com/egibs/deepwalk 3.600s
ok github.com/egibs/deepwalk 3.529s
```

## Benchmarks
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.0.0
1.1.0
85 changes: 85 additions & 0 deletions deepsearch.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package deepwalk

import "reflect"

// DeepSearch traverses a data structure and returns the value of the specified key
// DeepSearch does not need to know the path to the specified key
// Like DeepWalk, DeepSearch will return either the first, last, or all encountered values
// associated with the provided `searchKey`
func DeepSearch(
obj interface{},
searchKey string,
defaultVal string,
returnVal string,
) (interface{}, error) {
// Return the default value if the object is empty or if the keys or return value are invalid
if IsEmpty(obj) || !ValidKeys([]string{searchKey}) || !ValidReturnVal(returnVal) {
return defaultVal, nil
}

// Return the object if there are no keys to traverse
if len(searchKey) == 0 {
return obj, nil
}

var foundList []interface{}

search(obj, searchKey, &foundList)

return HandleReturnVal(&foundList, defaultVal, returnVal)
}

// search traverses a data structure and returns the value of the specified key
func search(obj interface{}, searchKey string, foundList *[]interface{}) {
switch object := obj.(type) {
case map[string]interface{}:
deepSearchMap(object, searchKey, foundList)
case []interface{}:
deepSearchSlice(object, searchKey, foundList)
default:
deepSearchStruct(object, searchKey, foundList)
}
}

// deepSearchMap handles the case where the object is a map
func deepSearchMap(
obj map[string]interface{},
searchKey string,
foundList *[]interface{},
) {
for key, value := range obj {
if key == searchKey {
*foundList = append(*foundList, value)
}
search(value, searchKey, foundList)
}
}

// deepSearchSlice handles the case where the object is a slice
func deepSearchSlice(
obj []interface{},
searchKey string,
foundList *[]interface{},
) {
for _, item := range obj {
search(item, searchKey, foundList)
}
}

// deepSearchStruct handles the case where the object is a struct
func deepSearchStruct(
obj interface{},
searchKey string,
foundList *[]interface{},
) {
if r := reflect.ValueOf(obj); r.Kind() == reflect.Struct {
for i := 0; i < r.NumField(); i++ {
f := r.Field(i)
if r.Type().Field(i).Name == searchKey {
*foundList = append(*foundList, f.Interface())
} else {
search(f.Interface(), searchKey, foundList)
}
}
}
}
143 changes: 143 additions & 0 deletions deepsearch_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
package deepwalk

import (
"reflect"
"testing"
)

func TestDeepSearch(t *testing.T) {
type args struct {
obj interface{}
searchKey string
defaultVal string
returnVal string
}
tests := []struct {
name string
args args
want interface{}
wantErr bool
}{
{
name: "Test case 1 - key found in map",
args: args{
obj: map[string]interface{}{
"key1": "value1",
"key2": "value2",
},
searchKey: "key1",
defaultVal: "default",
returnVal: "first",
},
want: "value1",
wantErr: false,
},
{
name: "Test case 2 - key not found in map",
args: args{
obj: map[string]interface{}{
"key1": "value1",
"key2": "value2",
},
searchKey: "key3",
defaultVal: "default",
returnVal: "first",
},
want: "default",
wantErr: false,
},
{
name: "Test case 3 - key found in nested map",
args: args{
obj: map[string]interface{}{
"key1": "value1",
"key2": map[string]interface{}{
"key3": "value3",
},
},
searchKey: "key3",
defaultVal: "default",
returnVal: "first",
},
want: "value3",
wantErr: false,
},
{
name: "Test case 4 - key not found in nested map",
args: args{
obj: map[string]interface{}{
"key1": "value1",
"key2": map[string]interface{}{
"key3": "value3",
},
},
searchKey: "key4",
defaultVal: "default",
returnVal: "first",
},
want: "default",
wantErr: false,
},
{
name: "Test case 7 - key found in struct",
args: args{
obj: struct {
Key1 string
Key2 string
}{
Key1: "value1",
Key2: "value2",
},
searchKey: "Key1",
defaultVal: "default",
returnVal: "first",
},
want: "value1",
wantErr: false,
},
{
name: "Test case 8 - key not found in struct",
args: args{
obj: struct {
Key1 string
Key2 string
}{
Key1: "value1",
Key2: "value2",
},
searchKey: "Key3",
defaultVal: "default",
returnVal: "first",
},
want: "default",
wantErr: false,
},
{
name: "Test case 9 - duplicate key found in map",
args: args{
obj: map[string]interface{}{
"key1": "value1",
"key2": map[string]interface{}{
"key1": "value2",
},
},
searchKey: "key1",
defaultVal: "default",
returnVal: "all",
},
want: []interface{}{"value1", "value2"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := DeepSearch(tt.args.obj, tt.args.searchKey, tt.args.defaultVal, tt.args.returnVal)
if (err != nil) != tt.wantErr {
t.Errorf("DeepSearch() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("DeepSearch() = %v, want %v", got, tt.want)
}
})
}
}
Loading

0 comments on commit 906695a

Please sign in to comment.