diff --git a/pkg/runtime/values/array.go b/pkg/runtime/values/array.go index 415e2867..65e6df77 100644 --- a/pkg/runtime/values/array.go +++ b/pkg/runtime/values/array.go @@ -228,7 +228,7 @@ func (t *Array) Sort() *Array { copy(c, t.value) sort.SliceStable(c, func(i, j int) bool { - return c[i].Compare(c[j]) == 0 + return c[i].Compare(c[j]) == -1 }) res := new(Array) diff --git a/pkg/runtime/values/object.go b/pkg/runtime/values/object.go index c5b56438..504d0c07 100644 --- a/pkg/runtime/values/object.go +++ b/pkg/runtime/values/object.go @@ -56,6 +56,9 @@ func (t *Object) String() string { return string(marshaled) } +// Compare compares the source object with other core.Value +// The behavior of the Compare is similar +// to the comparison of objects in ArangoDB func (t *Object) Compare(other core.Value) int { switch other.Type() { case core.ObjectType: @@ -73,18 +76,33 @@ func (t *Object) Compare(other core.Value) int { var res = 0 - var val core.Value - var exists bool + sortedT := sort.StringSlice(t.Keys()) + sortedT.Sort() - other.ForEach(func(otherVal core.Value, key string) bool { - res = -1 + sortedOther := sort.StringSlice(other.Keys()) + sortedOther.Sort() - if val, exists = t.value[key]; exists { - res = val.Compare(otherVal) + var tVal, otherVal core.Value + var tKey, otherKey string + + for i := 0; i < len(t.value) && res == 0; i++ { + tKey, otherKey = sortedT[i], sortedOther[i] + + if tKey == otherKey { + tVal, _ = t.Get(NewString(tKey)) + otherVal, _ = other.Get(NewString(tKey)) + res = tVal.Compare(otherVal) + continue } - return res == 0 - }) + if tKey < otherKey { + res = 1 + } else { + res = -1 + } + + break + } return res default: diff --git a/pkg/runtime/values/object_test.go b/pkg/runtime/values/object_test.go index ec7ac240..3092e2a7 100644 --- a/pkg/runtime/values/object_test.go +++ b/pkg/runtime/values/object_test.go @@ -157,6 +157,68 @@ func TestObject(t *testing.T) { So(obj1.Compare(obj2), ShouldEqual, -1) }) + + Convey("ArangoDB compatibility", func() { + Convey("It should return 1 when {a:1} and {b:2}", func() { + obj1 := values.NewObjectWith(values.NewObjectProperty("a", values.NewInt(1))) + obj2 := values.NewObjectWith(values.NewObjectProperty("b", values.NewInt(2))) + + So(obj1.Compare(obj2), ShouldEqual, 1) + }) + + Convey("It should return 0 when {a:1} and {a:1}", func() { + obj1 := values.NewObjectWith(values.NewObjectProperty("a", values.NewInt(1))) + obj2 := values.NewObjectWith(values.NewObjectProperty("a", values.NewInt(1))) + + So(obj1.Compare(obj2), ShouldEqual, 0) + }) + + Convey("It should return 0 {a:1, c:2} and {c:2, a:1}", func() { + obj1 := values.NewObjectWith( + values.NewObjectProperty("a", values.NewInt(1)), + values.NewObjectProperty("c", values.NewInt(2)), + ) + obj2 := values.NewObjectWith( + values.NewObjectProperty("c", values.NewInt(2)), + values.NewObjectProperty("a", values.NewInt(1)), + ) + + So(obj1.Compare(obj2), ShouldEqual, 0) + }) + + Convey("It should return -1 when {a:1} and {a:2}", func() { + obj1 := values.NewObjectWith(values.NewObjectProperty("a", values.NewInt(1))) + obj2 := values.NewObjectWith(values.NewObjectProperty("a", values.NewInt(2))) + + So(obj1.Compare(obj2), ShouldEqual, -1) + }) + + Convey("It should return 1 when {a:1, c:2} and {c:2, b:2}", func() { + obj1 := values.NewObjectWith( + values.NewObjectProperty("a", values.NewInt(1)), + values.NewObjectProperty("c", values.NewInt(2)), + ) + obj2 := values.NewObjectWith( + values.NewObjectProperty("c", values.NewInt(2)), + values.NewObjectProperty("b", values.NewInt(2)), + ) + + So(obj1.Compare(obj2), ShouldEqual, 1) + }) + + Convey("It should return 1 {a:1, c:3} and {c:2, a:1}", func() { + obj1 := values.NewObjectWith( + values.NewObjectProperty("a", values.NewInt(1)), + values.NewObjectProperty("c", values.NewInt(3)), + ) + obj2 := values.NewObjectWith( + values.NewObjectProperty("c", values.NewInt(2)), + values.NewObjectProperty("a", values.NewInt(1)), + ) + + So(obj1.Compare(obj2), ShouldEqual, 1) + }) + }) }) Convey(".Hash", t, func() { diff --git a/pkg/stdlib/objects/lib.go b/pkg/stdlib/objects/lib.go index b03f27e9..47b6c430 100644 --- a/pkg/stdlib/objects/lib.go +++ b/pkg/stdlib/objects/lib.go @@ -4,10 +4,11 @@ import "github.com/MontFerret/ferret/pkg/runtime/core" func NewLib() map[string]core.Function { return map[string]core.Function{ - "HAS": Has, - "KEYS": Keys, - "KEEP": Keep, - "MERGE": Merge, - "ZIP": Zip, + "HAS": Has, + "KEYS": Keys, + "KEEP": Keep, + "MERGE": Merge, + "ZIP": Zip, + "VALUES": Values, } } diff --git a/pkg/stdlib/objects/values.go b/pkg/stdlib/objects/values.go new file mode 100644 index 00000000..89f5d1da --- /dev/null +++ b/pkg/stdlib/objects/values.go @@ -0,0 +1,37 @@ +package objects + +import ( + "context" + + "github.com/MontFerret/ferret/pkg/runtime/values" + + "github.com/MontFerret/ferret/pkg/runtime/core" +) + +// Values return the attribute values of the object as an array. +// @params obj (Object) - an object. +// @returns (Array of Value) - the values of document returned in any order. +func Values(_ context.Context, args ...core.Value) (core.Value, error) { + err := core.ValidateArgs(args, 1, 1) + if err != nil { + return values.None, err + } + + err = core.ValidateType(args[0], core.ObjectType) + if err != nil { + return values.None, err + } + + obj := args[0].(*values.Object) + vals := values.NewArray(0) + + obj.ForEach(func(val core.Value, key string) bool { + if values.IsCloneable(val) { + val = val.(core.Cloneable).Clone() + } + vals.Push(val) + return true + }) + + return vals, nil +} diff --git a/pkg/stdlib/objects/values_test.go b/pkg/stdlib/objects/values_test.go new file mode 100644 index 00000000..2453edcd --- /dev/null +++ b/pkg/stdlib/objects/values_test.go @@ -0,0 +1,229 @@ +package objects_test + +import ( + "context" + "testing" + + "github.com/MontFerret/ferret/pkg/runtime/values" + "github.com/MontFerret/ferret/pkg/stdlib/objects" + + . "github.com/smartystreets/goconvey/convey" +) + +func TestValues(t *testing.T) { + Convey("Invalid arguments", t, func() { + Convey("When there is no arguments", func() { + actual, err := objects.Values(context.Background()) + + So(err, ShouldBeError) + So(actual.Compare(values.None), ShouldEqual, 0) + }) + + Convey("When 2 arguments", func() { + obj := values.NewObjectWith( + values.NewObjectProperty("k1", values.NewInt(0)), + values.NewObjectProperty("k2", values.NewInt(1)), + ) + + actual, err := objects.Values(context.Background(), obj, obj) + + So(err, ShouldBeError) + So(actual.Compare(values.None), ShouldEqual, 0) + + actual, err = objects.Values(context.Background(), obj, values.NewInt(0)) + + So(err, ShouldBeError) + So(actual.Compare(values.None), ShouldEqual, 0) + }) + + Convey("When there is not object argument", func() { + actual, err := objects.Values(context.Background(), values.NewInt(0)) + + So(err, ShouldBeError) + So(actual.Compare(values.None), ShouldEqual, 0) + }) + }) + + Convey("When simple type attributes (same type)", t, func() { + obj := values.NewObjectWith( + values.NewObjectProperty("k1", values.NewInt(0)), + values.NewObjectProperty("k2", values.NewInt(1)), + ) + expected := values.NewArrayWith( + values.NewInt(0), values.NewInt(1), + ).Sort() + + actual, err := objects.Values(context.Background(), obj) + actualSorted := actual.(*values.Array).Sort() + + So(err, ShouldBeNil) + So(actualSorted.Compare(expected), ShouldEqual, 0) + }) + + Convey("When simple type attributes (different types)", t, func() { + obj := values.NewObjectWith( + values.NewObjectProperty("k1", values.NewInt(0)), + values.NewObjectProperty("k2", values.NewString("v2")), + ) + expected := values.NewArrayWith( + values.NewInt(0), values.NewString("v2"), + ).Sort() + + actual, err := objects.Values(context.Background(), obj) + actualSorted := actual.(*values.Array).Sort() + + So(err, ShouldBeNil) + So(actualSorted.Compare(expected), ShouldEqual, 0) + }) + + Convey("When complex type attributes (array)", t, func() { + arr1 := values.NewArrayWith( + values.NewInt(0), values.NewInt(1), + ) + arr2 := values.NewArrayWith( + values.NewInt(2), values.NewInt(3), + ) + obj := values.NewObjectWith( + values.NewObjectProperty("k1", arr1), + values.NewObjectProperty("k2", arr2), + ) + expected := values.NewArrayWith(arr1, arr2).Sort() + + actual, err := objects.Values(context.Background(), obj) + actualSorted := actual.(*values.Array).Sort() + + So(err, ShouldBeNil) + So(actualSorted.Compare(expected), ShouldEqual, 0) + }) + + Convey("When complex type attributes (object)", t, func() { + obj1 := values.NewObjectWith( + values.NewObjectProperty("int0", values.NewInt(0)), + ) + obj2 := values.NewObjectWith( + values.NewObjectProperty("int1", values.NewInt(1)), + ) + obj := values.NewObjectWith( + values.NewObjectProperty("k1", obj1), + values.NewObjectProperty("k2", obj2), + ) + expected := values.NewArrayWith(obj1, obj2).Sort() + + actual, err := objects.Values(context.Background(), obj) + actualSorted := actual.(*values.Array).Sort() + + So(err, ShouldBeNil) + So(actualSorted.Compare(expected), ShouldEqual, 0) + }) + + Convey("When complex type attributes (object and array)", t, func() { + obj1 := values.NewObjectWith( + values.NewObjectProperty("k1", values.NewInt(0)), + ) + arr1 := values.NewArrayWith( + values.NewInt(0), values.NewInt(1), + ) + obj := values.NewObjectWith( + values.NewObjectProperty("obj", obj1), + values.NewObjectProperty("arr", arr1), + ) + expected := values.NewArrayWith(obj1, arr1).Sort() + + actual, err := objects.Values(context.Background(), obj) + actualSorted := actual.(*values.Array).Sort() + + So(err, ShouldBeNil) + So(actualSorted.Compare(expected), ShouldEqual, 0) + }) + + Convey("When both type attributes", t, func() { + obj1 := values.NewObjectWith( + values.NewObjectProperty("k1", values.NewInt(0)), + ) + arr1 := values.NewArrayWith( + values.NewInt(0), values.NewInt(1), + ) + int1 := values.NewInt(0) + obj := values.NewObjectWith( + values.NewObjectProperty("obj", obj1), + values.NewObjectProperty("arr", arr1), + values.NewObjectProperty("int", int1), + ) + expected := values.NewArrayWith(obj1, arr1, int1).Sort() + + actual, err := objects.Values(context.Background(), obj) + actualSorted := actual.(*values.Array).Sort() + + So(err, ShouldBeNil) + So(actualSorted.Compare(expected), ShouldEqual, 0) + }) + + Convey("Result is independent on the source object (array)", t, func() { + arr := values.NewArrayWith(values.NewInt(0)) + obj := values.NewObjectWith( + values.NewObjectProperty("arr", arr), + ) + expected := values.NewArrayWith( + values.NewArrayWith( + values.NewInt(0), + ), + ) + + actual, err := objects.Values(context.Background(), obj) + actualSorted := actual.(*values.Array).Sort() + + So(err, ShouldBeNil) + + arr.Push(values.NewInt(1)) + + So(actualSorted.Compare(expected), ShouldEqual, 0) + }) + + Convey("Result is independent on the source object (object)", t, func() { + nested := values.NewObjectWith( + values.NewObjectProperty("int", values.NewInt(0)), + ) + obj := values.NewObjectWith( + values.NewObjectProperty("nested", nested), + ) + expected := values.NewArrayWith( + values.NewObjectWith( + values.NewObjectProperty("int", values.NewInt(0)), + ), + ) + + actual, err := objects.Values(context.Background(), obj) + actualSorted := actual.(*values.Array).Sort() + + So(err, ShouldBeNil) + + nested.Set("new", values.NewInt(1)) + + So(actualSorted.Compare(expected), ShouldEqual, 0) + }) +} + +func TestValuesStress(t *testing.T) { + Convey("Stress", t, func() { + for i := 0; i < 100; i++ { + obj1 := values.NewObjectWith( + values.NewObjectProperty("int0", values.NewInt(0)), + ) + obj2 := values.NewObjectWith( + values.NewObjectProperty("int1", values.NewInt(1)), + ) + obj := values.NewObjectWith( + values.NewObjectProperty("k1", obj1), + values.NewObjectProperty("k2", obj2), + ) + expected := values.NewArrayWith(obj2, obj1).Sort() + + actual, err := objects.Values(context.Background(), obj) + actualSorted := actual.(*values.Array).Sort() + + So(err, ShouldBeNil) + So(actualSorted.Length(), ShouldEqual, expected.Length()) + So(actualSorted.Compare(expected), ShouldEqual, 0) + } + }) +}