mirror of
https://github.com/go-micro/go-micro.git
synced 2024-11-24 08:02:32 +02:00
bundle qson lib in util (#1561)
* copy qson from https://github.com/joncalhoun/qson as author not want to maintain repo * latest code contains our fix to proper decode strings with escaped & symbol * replace package in api/handler/rpc Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
This commit is contained in:
parent
e55c23164a
commit
6fa27373ed
@ -10,7 +10,6 @@ import (
|
||||
"strings"
|
||||
|
||||
jsonpatch "github.com/evanphx/json-patch/v5"
|
||||
"github.com/joncalhoun/qson"
|
||||
"github.com/micro/go-micro/v2/api"
|
||||
"github.com/micro/go-micro/v2/api/handler"
|
||||
"github.com/micro/go-micro/v2/api/internal/proto"
|
||||
@ -24,6 +23,7 @@ import (
|
||||
"github.com/micro/go-micro/v2/metadata"
|
||||
"github.com/micro/go-micro/v2/registry"
|
||||
"github.com/micro/go-micro/v2/util/ctx"
|
||||
"github.com/micro/go-micro/v2/util/qson"
|
||||
"github.com/oxtoacart/bpool"
|
||||
)
|
||||
|
||||
|
1
go.mod
1
go.mod
@ -39,7 +39,6 @@ require (
|
||||
github.com/hpcloud/tail v1.0.0
|
||||
github.com/imdario/mergo v0.3.8
|
||||
github.com/jonboulle/clockwork v0.1.0 // indirect
|
||||
github.com/joncalhoun/qson v0.0.0-20170526102502-8a9cab3a62b1
|
||||
github.com/json-iterator/go v1.1.9 // indirect
|
||||
github.com/kr/pretty v0.1.0
|
||||
github.com/lib/pq v1.3.0
|
||||
|
2
go.sum
2
go.sum
@ -238,8 +238,6 @@ github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJS
|
||||
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
||||
github.com/jonboulle/clockwork v0.1.0 h1:VKV+ZcuP6l3yW9doeqz6ziZGgcynBVQO+obU0+0hcPo=
|
||||
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
||||
github.com/joncalhoun/qson v0.0.0-20170526102502-8a9cab3a62b1 h1:lnrOS18wZBYrzdDmnUeg1OVk+kQ3rxG8mZWU89DpMIA=
|
||||
github.com/joncalhoun/qson v0.0.0-20170526102502-8a9cab3a62b1/go.mod h1:DFXrEwSRX0p/aSvxE21319menCBFeQO0jXpRj7LEZUA=
|
||||
github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
|
21
util/qson/LICENSE
Normal file
21
util/qson/LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 Jon Calhoun
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
55
util/qson/README.md
Normal file
55
util/qson/README.md
Normal file
@ -0,0 +1,55 @@
|
||||
# qson
|
||||
|
||||
This is copy from https://github.com/joncalhoun/qson
|
||||
As author says he is not acrivelly maintains the repo and not plan to do that.
|
||||
|
||||
## Usage
|
||||
|
||||
You can either turn a URL query param into a JSON byte array, or unmarshal that directly into a Go object.
|
||||
|
||||
Transforming the URL query param into a JSON byte array:
|
||||
|
||||
```go
|
||||
import "github.com/joncalhoun/qson"
|
||||
|
||||
func main() {
|
||||
b, err := qson.ToJSON("bar%5Bone%5D%5Btwo%5D=2&bar[one][red]=112")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Println(string(b))
|
||||
// Should output: {"bar":{"one":{"red":112,"two":2}}}
|
||||
}
|
||||
```
|
||||
|
||||
Or unmarshalling directly into a Go object using JSON struct tags:
|
||||
|
||||
```go
|
||||
import "github.com/joncalhoun/qson"
|
||||
|
||||
type unmarshalT struct {
|
||||
A string `json:"a"`
|
||||
B unmarshalB `json:"b"`
|
||||
}
|
||||
type unmarshalB struct {
|
||||
C int `json:"c"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
var out unmarshalT
|
||||
query := "a=xyz&b[c]=456"
|
||||
err := Unmarshal(&out, query)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
// out should equal
|
||||
// unmarshalT{
|
||||
// A: "xyz",
|
||||
// B: unmarshalB{
|
||||
// C: 456,
|
||||
// },
|
||||
// }
|
||||
}
|
||||
```
|
||||
|
||||
To get a query string like in the two previous examples you can use the `RawQuery` field on the [net/url.URL](https://golang.org/pkg/net/url/#URL) type.
|
34
util/qson/merge.go
Normal file
34
util/qson/merge.go
Normal file
@ -0,0 +1,34 @@
|
||||
package qson
|
||||
|
||||
// merge merges a with b if they are either both slices
|
||||
// or map[string]interface{} types. Otherwise it returns b.
|
||||
func merge(a interface{}, b interface{}) interface{} {
|
||||
switch aT := a.(type) {
|
||||
case map[string]interface{}:
|
||||
return mergeMap(aT, b.(map[string]interface{}))
|
||||
case []interface{}:
|
||||
return mergeSlice(aT, b.([]interface{}))
|
||||
default:
|
||||
return b
|
||||
}
|
||||
}
|
||||
|
||||
// mergeMap merges a with b, attempting to merge any nested
|
||||
// values in nested maps but eventually overwriting anything
|
||||
// in a that can't be merged with whatever is in b.
|
||||
func mergeMap(a map[string]interface{}, b map[string]interface{}) map[string]interface{} {
|
||||
for bK, bV := range b {
|
||||
if _, ok := a[bK]; ok {
|
||||
a[bK] = merge(a[bK], bV)
|
||||
} else {
|
||||
a[bK] = bV
|
||||
}
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
// mergeSlice merges a with b and returns the result.
|
||||
func mergeSlice(a []interface{}, b []interface{}) []interface{} {
|
||||
a = append(a, b...)
|
||||
return a
|
||||
}
|
37
util/qson/merge_test.go
Normal file
37
util/qson/merge_test.go
Normal file
@ -0,0 +1,37 @@
|
||||
package qson
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestMergeSlice(t *testing.T) {
|
||||
a := []interface{}{"a"}
|
||||
b := []interface{}{"b"}
|
||||
actual := mergeSlice(a, b)
|
||||
if len(actual) != 2 {
|
||||
t.Errorf("Expected size to be 2.")
|
||||
}
|
||||
if actual[0] != "a" {
|
||||
t.Errorf("Expected index 0 to have value a. Actual: %s", actual[0])
|
||||
}
|
||||
if actual[1] != "b" {
|
||||
t.Errorf("Expected index 1 to have value b. Actual: %s", actual[1])
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergeMap(t *testing.T) {
|
||||
a := map[string]interface{}{
|
||||
"a": "b",
|
||||
}
|
||||
b := map[string]interface{}{
|
||||
"b": "c",
|
||||
}
|
||||
actual := mergeMap(a, b)
|
||||
if len(actual) != 2 {
|
||||
t.Errorf("Expected size to be 2.")
|
||||
}
|
||||
if actual["a"] != "b" {
|
||||
t.Errorf("Expected key \"a\" to have value b. Actual: %s", actual["a"])
|
||||
}
|
||||
if actual["b"] != "c" {
|
||||
t.Errorf("Expected key \"b\" to have value c. Actual: %s", actual["b"])
|
||||
}
|
||||
}
|
154
util/qson/qson.go
Normal file
154
util/qson/qson.go
Normal file
@ -0,0 +1,154 @@
|
||||
// Package qson implmenets decoding of URL query params
|
||||
// into JSON and Go values (using JSON struct tags).
|
||||
//
|
||||
// See https://golang.org/pkg/encoding/json/ for more
|
||||
// details on JSON struct tags.
|
||||
package qson
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrInvalidParam is returned when invalid data is provided to the ToJSON or Unmarshal function.
|
||||
// Specifically, this will be returned when there is no equals sign present in the URL query parameter.
|
||||
ErrInvalidParam error = errors.New("qson: invalid url query param provided")
|
||||
|
||||
bracketSplitter *regexp.Regexp
|
||||
)
|
||||
|
||||
func init() {
|
||||
bracketSplitter = regexp.MustCompile("\\[|\\]")
|
||||
}
|
||||
|
||||
// Unmarshal will take a dest along with URL
|
||||
// query params and attempt to first turn the query params
|
||||
// into JSON and then unmarshal those into the dest variable
|
||||
//
|
||||
// BUG(joncalhoun): If a URL query param value is something
|
||||
// like 123 but is expected to be parsed into a string this
|
||||
// will currently result in an error because the JSON
|
||||
// transformation will assume this is intended to be an int.
|
||||
// This should only affect the Unmarshal function and
|
||||
// could likely be fixed, but someone will need to submit a
|
||||
// PR if they want that fixed.
|
||||
func Unmarshal(dst interface{}, query string) error {
|
||||
b, err := ToJSON(query)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return json.Unmarshal(b, dst)
|
||||
}
|
||||
|
||||
// ToJSON will turn a query string like:
|
||||
// cat=1&bar%5Bone%5D%5Btwo%5D=2&bar[one][red]=112
|
||||
// Into a JSON object with all the data merged as nicely as
|
||||
// possible. Eg the example above would output:
|
||||
// {"bar":{"one":{"two":2,"red":112}}}
|
||||
func ToJSON(query string) ([]byte, error) {
|
||||
var (
|
||||
builder interface{} = make(map[string]interface{})
|
||||
)
|
||||
params := strings.Split(query, "&")
|
||||
for _, part := range params {
|
||||
tempMap, err := queryToMap(part)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
builder = merge(builder, tempMap)
|
||||
}
|
||||
return json.Marshal(builder)
|
||||
}
|
||||
|
||||
// queryToMap turns something like a[b][c]=4 into
|
||||
// map[string]interface{}{
|
||||
// "a": map[string]interface{}{
|
||||
// "b": map[string]interface{}{
|
||||
// "c": 4,
|
||||
// },
|
||||
// },
|
||||
// }
|
||||
func queryToMap(param string) (map[string]interface{}, error) {
|
||||
rawKey, rawValue, err := splitKeyAndValue(param)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rawValue, err = url.QueryUnescape(rawValue)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rawKey, err = url.QueryUnescape(rawKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pieces := bracketSplitter.Split(rawKey, -1)
|
||||
key := pieces[0]
|
||||
|
||||
// If len==1 then rawKey has no [] chars and we can just
|
||||
// decode this as key=value into {key: value}
|
||||
if len(pieces) == 1 {
|
||||
var value interface{}
|
||||
// First we try parsing it as an int, bool, null, etc
|
||||
err = json.Unmarshal([]byte(rawValue), &value)
|
||||
if err != nil {
|
||||
// If we got an error we try wrapping the value in
|
||||
// quotes and processing it as a string
|
||||
err = json.Unmarshal([]byte("\""+rawValue+"\""), &value)
|
||||
if err != nil {
|
||||
// If we can't decode as a string we return the err
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return map[string]interface{}{
|
||||
key: value,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// If len > 1 then we have something like a[b][c]=2
|
||||
// so we need to turn this into {"a": {"b": {"c": 2}}}
|
||||
// To do this we break our key into two pieces:
|
||||
// a and b[c]
|
||||
// and then we set {"a": queryToMap("b[c]", value)}
|
||||
ret := make(map[string]interface{}, 0)
|
||||
ret[key], err = queryToMap(buildNewKey(rawKey) + "=" + rawValue)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// When URL params have a set of empty brackets (eg a[]=1)
|
||||
// it is assumed to be an array. This will get us the
|
||||
// correct value for the array item and return it as an
|
||||
// []interface{} so that it can be merged properly.
|
||||
if pieces[1] == "" {
|
||||
temp := ret[key].(map[string]interface{})
|
||||
ret[key] = []interface{}{temp[""]}
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// buildNewKey will take something like:
|
||||
// origKey = "bar[one][two]"
|
||||
// pieces = [bar one two ]
|
||||
// and return "one[two]"
|
||||
func buildNewKey(origKey string) string {
|
||||
pieces := bracketSplitter.Split(origKey, -1)
|
||||
ret := origKey[len(pieces[0])+1:]
|
||||
ret = ret[:len(pieces[1])] + ret[len(pieces[1])+1:]
|
||||
return ret
|
||||
}
|
||||
|
||||
// splitKeyAndValue splits a URL param at the last equal
|
||||
// sign and returns the two strings. If no equal sign is
|
||||
// found, the ErrInvalidParam error is returned.
|
||||
func splitKeyAndValue(param string) (string, string, error) {
|
||||
li := strings.LastIndex(param, "=")
|
||||
if li == -1 {
|
||||
return "", "", ErrInvalidParam
|
||||
}
|
||||
return param[:li], param[li+1:], nil
|
||||
}
|
170
util/qson/qson_test.go
Normal file
170
util/qson/qson_test.go
Normal file
@ -0,0 +1,170 @@
|
||||
package qson
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func ExampleUnmarshal() {
|
||||
type Ex struct {
|
||||
A string `json:"a"`
|
||||
B struct {
|
||||
C int `json:"c"`
|
||||
} `json:"b"`
|
||||
}
|
||||
var ex Ex
|
||||
if err := Unmarshal(&ex, "a=xyz&b[c]=456"); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Printf("%+v\n", ex)
|
||||
// Output: {A:xyz B:{C:456}}
|
||||
}
|
||||
|
||||
type unmarshalT struct {
|
||||
A string `json:"a"`
|
||||
B unmarshalB `json:"b"`
|
||||
}
|
||||
type unmarshalB struct {
|
||||
C int `json:"c"`
|
||||
D string `json:"D"`
|
||||
}
|
||||
|
||||
func TestUnmarshal(t *testing.T) {
|
||||
query := "a=xyz&b[c]=456"
|
||||
expected := unmarshalT{
|
||||
A: "xyz",
|
||||
B: unmarshalB{
|
||||
C: 456,
|
||||
},
|
||||
}
|
||||
var actual unmarshalT
|
||||
err := Unmarshal(&actual, query)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if expected != actual {
|
||||
t.Errorf("Expected: %+v Actual: %+v", expected, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func ExampleToJSON() {
|
||||
b, err := ToJSON("a=xyz&b[c]=456")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Printf(string(b))
|
||||
// Output: {"a":"xyz","b":{"c":456}}
|
||||
}
|
||||
|
||||
func TestToJSONNested(t *testing.T) {
|
||||
query := "bar%5Bone%5D%5Btwo%5D=2&bar[one][red]=112"
|
||||
expected := `{"bar":{"one":{"red":112,"two":2}}}`
|
||||
actual, err := ToJSON(query)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
actualStr := string(actual)
|
||||
if actualStr != expected {
|
||||
t.Errorf("Expected: %s Actual: %s", expected, actualStr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestToJSONPlain(t *testing.T) {
|
||||
query := "cat=1&dog=2"
|
||||
expected := `{"cat":1,"dog":2}`
|
||||
actual, err := ToJSON(query)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
actualStr := string(actual)
|
||||
if actualStr != expected {
|
||||
t.Errorf("Expected: %s Actual: %s", expected, actualStr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestToJSONSlice(t *testing.T) {
|
||||
query := "cat[]=1&cat[]=34"
|
||||
expected := `{"cat":[1,34]}`
|
||||
actual, err := ToJSON(query)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
actualStr := string(actual)
|
||||
if actualStr != expected {
|
||||
t.Errorf("Expected: %s Actual: %s", expected, actualStr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestToJSONBig(t *testing.T) {
|
||||
query := "distinct_id=763_1495187301909_3495×tamp=1495187523&event=product_add_cart¶ms%5BproductRefId%5D=8284563078¶ms%5Bapps%5D%5B%5D=precommend¶ms%5Bapps%5D%5B%5D=bsales¶ms%5Bsource%5D=item¶ms%5Boptions%5D%5Bsegment%5D=cart_recommendation¶ms%5Boptions%5D%5Btype%5D=up_sell¶ms%5BtimeExpire%5D=1495187599642¶ms%5Brecommend_system_product_source%5D=item¶ms%5Bproduct_id%5D=8284563078¶ms%5Bvariant_id%5D=27661944134¶ms%5Bsku%5D=00483332%20(black)¶ms%5Bsources%5D%5B%5D=product_recommendation¶ms%5Bcart_token%5D=dc2c336a009edf2762128e65806dfb1d¶ms%5Bquantity%5D=1¶ms%5Bnew_popup_upsell_mobile%5D=false¶ms%5BclientDevice%5D=desktop¶ms%5BclientIsMobile%5D=false¶ms%5BclientIsSmallScreen%5D=false¶ms%5Bnew_popup_crossell_mobile%5D=false&api_key=14c5b7dacea9157029265b174491d340"
|
||||
expected := `{"api_key":"14c5b7dacea9157029265b174491d340","distinct_id":"763_1495187301909_3495","event":"product_add_cart","params":{"apps":["precommend","bsales"],"cart_token":"dc2c336a009edf2762128e65806dfb1d","clientDevice":"desktop","clientIsMobile":false,"clientIsSmallScreen":false,"new_popup_crossell_mobile":false,"new_popup_upsell_mobile":false,"options":{"segment":"cart_recommendation","type":"up_sell"},"productRefId":8284563078,"product_id":8284563078,"quantity":1,"recommend_system_product_source":"item","sku":"00483332 (black)","source":"item","sources":["product_recommendation"],"timeExpire":1495187599642,"variant_id":27661944134},"timestamp":1495187523}`
|
||||
actual, err := ToJSON(query)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
actualStr := string(actual)
|
||||
if actualStr != expected {
|
||||
t.Errorf("Expected: %s Actual: %s", expected, actualStr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestToJSONDuplicateKey(t *testing.T) {
|
||||
query := "cat=1&cat=2"
|
||||
expected := `{"cat":2}`
|
||||
actual, err := ToJSON(query)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
actualStr := string(actual)
|
||||
if actualStr != expected {
|
||||
t.Errorf("Expected: %s Actual: %s", expected, actualStr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitKeyAndValue(t *testing.T) {
|
||||
param := "a[dog][=cat]=123"
|
||||
eKey, eValue := "a[dog][=cat]", "123"
|
||||
aKey, aValue, err := splitKeyAndValue(param)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if eKey != aKey {
|
||||
t.Errorf("Keys do not match. Expected: %s Actual: %s", eKey, aKey)
|
||||
}
|
||||
if eValue != aValue {
|
||||
t.Errorf("Values do not match. Expected: %s Actual: %s", eValue, aValue)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncodedAmpersand(t *testing.T) {
|
||||
query := "a=xyz&b[d]=ben%26jerry"
|
||||
expected := unmarshalT{
|
||||
A: "xyz",
|
||||
B: unmarshalB{
|
||||
D: "ben&jerry",
|
||||
},
|
||||
}
|
||||
var actual unmarshalT
|
||||
err := Unmarshal(&actual, query)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if expected != actual {
|
||||
t.Errorf("Expected: %+v Actual: %+v", expected, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncodedAmpersand2(t *testing.T) {
|
||||
query := "filter=parent%3Dflow12345%26request%3Dreq12345&meta.limit=20&meta.offset=0"
|
||||
expected := map[string]interface{}{"filter": "parent=flow12345&request=req12345", "meta.limit": float64(20), "meta.offset": float64(0)}
|
||||
actual := make(map[string]interface{})
|
||||
err := Unmarshal(&actual, query)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
for k, v := range actual {
|
||||
if nv, ok := expected[k]; !ok || nv != v {
|
||||
t.Errorf("Expected: %+v Actual: %+v", expected, actual)
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user