diff --git a/Readme.md b/Readme.md
index bbabbab4..cd031197 100644
--- a/Readme.md
+++ b/Readme.md
@@ -122,7 +122,7 @@ Stub will respond with the expected response only if the request matches any rul
So if you do a `curl -X POST -d '{"service":"Greeter","method":"SayHello","data":{"name":"gripmock"}}' localhost:4771/find` stub service will find a match from listed stubs stored there.
### Input Matching Rule
-Input matching has 3 rules to match an input: **equals**,**contains** and **regex**
+Input matching has 4 rules to match an input: **equals**, **equals_unordered**, **contains** and **regex**
Nested fields are allowed for input matching too for all JSON data types. (`string`, `bool`, `array`, etc.)
@@ -151,6 +151,31 @@ Nested fields are allowed for input matching too for all JSON data types. (`stri
}
```
+**equals_unordered** will match the exact field name and value of input into expected stub, except lists (which are compared as sets). example stub JSON:
+
+
+```
+{
+ .
+ .
+ "input":{
+ "equals_unordered":{
+ "name":"gripmock",
+ "greetings": {
+ "english": "Hello World!",
+ "indonesian": "Halo Dunia!",
+ "turkish": "Merhaba Dünya!"
+ },
+ "ok": true,
+ "numbers": [4, 8, 15, 16, 23, 42]
+ "null": null
+ }
+ }
+ .
+ .
+}
+```
+
**contains** will match input that has the value declared expected fields. example stub JSON:
```
{
diff --git a/stub/storage.go b/stub/storage.go
index 4faf810d..2d21637c 100644
--- a/stub/storage.go
+++ b/stub/storage.go
@@ -7,6 +7,7 @@ import (
"log"
"reflect"
"regexp"
+ "sort"
"sync"
"github.com/lithammer/fuzzysearch/fuzzy"
@@ -81,6 +82,13 @@ func findStub(stub *findStubPayload) (*Output, error) {
}
}
+ if expect := stubrange.Input.EqualsUnordered; expect != nil {
+ closestMatch = append(closestMatch, closeMatch{"equals_unordered", expect})
+ if equalsUnordered(stub.Data, expect) {
+ return &stubrange.Output, nil
+ }
+ }
+
if expect := stubrange.Input.Contains; expect != nil {
closestMatch = append(closestMatch, closeMatch{"contains", expect})
if contains(stubrange.Input.Contains, stub.Data) {
@@ -186,18 +194,40 @@ func regexMatch(expect, actual interface{}) bool {
}
func equals(expect, actual map[string]interface{}) bool {
- return find(expect, actual, true, true, deepEqual)
+ return find(expect, actual, true, true, deepEqual, false)
+}
+
+func equalsUnordered(expect, actual map[string]interface{}) bool {
+ return find(expect, actual, true, true, deepEqual, true)
}
func contains(expect, actual map[string]interface{}) bool {
- return find(expect, actual, true, false, deepEqual)
+ return find(expect, actual, true, false, deepEqual, false)
}
func matches(expect, actual map[string]interface{}) bool {
- return find(expect, actual, true, false, regexMatch)
+ return find(expect, actual, true, false, regexMatch, false)
+}
+
+func equalsIgnoreOrder(expect, actual interface{}) bool {
+ expectSlice, expectOk := expect.([]interface{})
+ actualSlice, actualOk := actual.([]interface{})
+ if !expectOk || !actualOk {
+ return false
+ }
+ if len(expectSlice) != len(actualSlice) {
+ return false
+ }
+ sort.Slice(expectSlice, func(i, j int) bool {
+ return fmt.Sprint(expectSlice[i]) < fmt.Sprint(expectSlice[j])
+ })
+ sort.Slice(actualSlice, func(i, j int) bool {
+ return fmt.Sprint(actualSlice[i]) < fmt.Sprint(actualSlice[j])
+ })
+ return reflect.DeepEqual(expectSlice, actualSlice)
}
-func find(expect, actual interface{}, acc, exactMatch bool, f matchFunc) bool {
+func find(expect, actual interface{}, acc, exactMatch bool, f matchFunc, ignoreOrder bool) bool {
// circuit brake
if acc == false {
@@ -225,9 +255,13 @@ func find(expect, actual interface{}, acc, exactMatch bool, f matchFunc) bool {
}
}
+ if expectArrayOk && actualArrayOk && ignoreOrder {
+ return equalsIgnoreOrder(expectArrayValue, actualArrayValue)
+ }
+
for expectItemIndex, expectItemValue := range expectArrayValue {
actualItemValue := actualArrayValue[expectItemIndex]
- acc = find(expectItemValue, actualItemValue, acc, exactMatch, f)
+ acc = find(expectItemValue, actualItemValue, acc, exactMatch, f, ignoreOrder)
}
return acc
@@ -256,7 +290,7 @@ func find(expect, actual interface{}, acc, exactMatch bool, f matchFunc) bool {
for expectItemKey, expectItemValue := range expectMapValue {
actualItemValue := actualMapValue[expectItemKey]
- acc = find(expectItemValue, actualItemValue, acc, exactMatch, f)
+ acc = find(expectItemValue, actualItemValue, acc, exactMatch, f, ignoreOrder)
}
return acc
diff --git a/stub/stub.go b/stub/stub.go
index 5509e7ea..541e35ea 100644
--- a/stub/stub.go
+++ b/stub/stub.go
@@ -55,9 +55,10 @@ type Stub struct {
}
type Input struct {
- Equals map[string]interface{} `json:"equals"`
- Contains map[string]interface{} `json:"contains"`
- Matches map[string]interface{} `json:"matches"`
+ Equals map[string]interface{} `json:"equals"`
+ EqualsUnordered map[string]interface{} `json:"equals_unordered"`
+ Contains map[string]interface{} `json:"contains"`
+ Matches map[string]interface{} `json:"matches"`
}
type Output struct {
@@ -118,6 +119,8 @@ func validateStub(stub *Stub) error {
break
case stub.Input.Equals != nil:
break
+ case stub.Input.EqualsUnordered != nil:
+ break
case stub.Input.Matches != nil:
break
default:
diff --git a/stub/stub_test.go b/stub/stub_test.go
index 4225abb3..7782d7ea 100644
--- a/stub/stub_test.go
+++ b/stub/stub_test.go
@@ -48,7 +48,7 @@ func TestStub(t *testing.T) {
return httptest.NewRequest("GET", "/", nil)
},
handler: listStub,
- expect: "{\"Testing\":{\"TestMethod\":[{\"Input\":{\"equals\":{\"Hola\":\"Mundo\"},\"contains\":null,\"matches\":null},\"Output\":{\"data\":{\"Hello\":\"World\"},\"error\":\"\"}}]}}\n",
+ expect: "{\"Testing\":{\"TestMethod\":[{\"Input\":{\"equals\":{\"Hola\":\"Mundo\"},\"equals_unordered\":null,\"contains\":null,\"matches\":null},\"Output\":{\"data\":{\"Hello\":\"World\"},\"error\":\"\"}}]}}\n",
},
{
name: "find stub equals",
@@ -99,6 +99,58 @@ func TestStub(t *testing.T) {
handler: handleFindStub,
expect: "{\"data\":{\"Hello\":\"World\"},\"error\":\"\"}\n",
},
+ {
+ name: "add stub equals_unordered",
+ mock: func() *http.Request {
+ payload := `{
+ "service": "TestingUnordered",
+ "method":"TestMethod",
+ "input": {
+ "equals_unordered": {
+ "ids": [1,2]
+ }
+ },
+ "output":{
+ "data":{
+ "hello":"world"
+ }
+ }
+ }`
+ return httptest.NewRequest("POST", "/add", bytes.NewReader([]byte(payload)))
+ },
+ handler: addStub,
+ expect: `Success add stub`,
+ },
+ {
+ name: "find stub equals_unordered",
+ mock: func() *http.Request {
+ payload := `{
+ "service":"TestingUnordered",
+ "method":"TestMethod",
+ "data":{
+ "ids":[1,2]
+ }
+ }`
+ return httptest.NewRequest("GET", "/find", bytes.NewReader([]byte(payload)))
+ },
+ handler: handleFindStub,
+ expect: "{\"data\":{\"hello\":\"world\"},\"error\":\"\"}\n",
+ },
+ {
+ name: "find stub equals_unordered reversed",
+ mock: func() *http.Request {
+ payload := `{
+ "service":"TestingUnordered",
+ "method":"TestMethod",
+ "data":{
+ "ids":[2,1]
+ }
+ }`
+ return httptest.NewRequest("GET", "/find", bytes.NewReader([]byte(payload)))
+ },
+ handler: handleFindStub,
+ expect: "{\"data\":{\"hello\":\"world\"},\"error\":\"\"}\n",
+ },
{
name: "add stub contains",
mock: func() *http.Request {