diff --git a/enc/json_decode.go b/enc/json_decode.go index f07894e514..38ddd3a559 100644 --- a/enc/json_decode.go +++ b/enc/json_decode.go @@ -73,6 +73,10 @@ func jsonDecodeTaggedValue(m map[string]interface{}, s store.ChunkSource) (types if v, ok := v.([]interface{}); ok { return jsonDecodeList(v, s) } + case "set": + if v, ok := v.([]interface{}); ok { + return jsonDecodeSet(v, s) + } case "map": if v, ok := v.(map[string]interface{}); ok { return jsonDecodeMap(v, s) @@ -99,6 +103,18 @@ func jsonDecodeList(input []interface{}, s store.ChunkSource) (types.Value, erro return output, nil } +func jsonDecodeSet(input []interface{}, s store.ChunkSource) (types.Value, error) { + vals := []types.Value{} + for _, inVal := range input { + outVal, err := jsonDecodeValue(inVal, s) + if err != nil { + return nil, err + } + vals = append(vals, outVal) + } + return types.NewSet(vals...), nil +} + func jsonDecodeMap(input map[string]interface{}, s store.ChunkSource) (types.Value, error) { output := types.NewMap() for k, inVal := range input { diff --git a/enc/json_decode_test.go b/enc/json_decode_test.go index 66a0d6c5b8..34ea2b1441 100644 --- a/enc/json_decode_test.go +++ b/enc/json_decode_test.go @@ -70,5 +70,11 @@ func TestJSONDecode(t *testing.T) { testDecode(`j {"map":{"bool":false,"int32":{"int32":42},"list":{"ref":"sha1-58bdf8e374b39f9b1e8a64784cf5c09601f4b7ea"},"map":{"ref":"sha1-fa8026bf44f60b64ab674c49cda31a697467973c"},"string":"hotdog"}} //`, types.NewMap("bool", types.Bool(false), "int32", types.Int32(42), "string", types.NewString("hotdog"), "list", types.NewList(), "map", types.NewMap())) + // Sets + testDecode(`j {"set":[]} +`, types.NewSet()) + testDecode(`j {"set":[{"int32":42},"hotdog",{"ref":"sha1-58bdf8e374b39f9b1e8a64784cf5c09601f4b7ea"},false,{"ref":"sha1-fa8026bf44f60b64ab674c49cda31a697467973c"}]} +`, types.NewSet(types.Bool(false), types.Int32(42), types.NewString("hotdog"), types.NewList(), types.NewMap())) + // referenced blobs? } diff --git a/enc/json_encode.go b/enc/json_encode.go index 3d79a206dd..912a2b1c66 100644 --- a/enc/json_encode.go +++ b/enc/json_encode.go @@ -3,6 +3,7 @@ package enc import ( "encoding/json" "fmt" + "sort" . "github.com/attic-labs/noms/dbg" "github.com/attic-labs/noms/ref" @@ -61,6 +62,8 @@ func getJSON(v types.Value, s store.ChunkSink) (interface{}, error) { return getJSONList(v, s) case types.Map: return getJSONMap(v, s) + case types.Set: + return getJSONSet(v, s) case types.String: return v.String(), nil case types.UInt16: @@ -117,16 +120,46 @@ func getJSONMap(m types.Map, s store.ChunkSink) (r interface{}, err error) { return } +func getJSONSet(set types.Set, s store.ChunkSink) (r interface{}, err error) { + // Iteration through Set is random, but we need a deterministic order for serialization. Let's order using the refs of the values in the set. + lookup := map[ref.Ref]types.Value{} + order := ref.RefSlice{} + set.Iter(func(v types.Value) (stop bool) { + order = append(order, v.Ref()) + lookup[v.Ref()] = v + return + }) + sort.Sort(order) + + j := []interface{}{} + for _, r := range order { + v := lookup[r] + var cj interface{} + cj, err = getChildJSON(v, s) + if err != nil { + return nil, err + } + j = append(j, cj) + } + + r = map[string]interface{}{ + "set": j, + } + return +} + func getChildJSON(v types.Value, s store.ChunkSink) (interface{}, error) { var r ref.Ref var err error switch v := v.(type) { - // Blobs, maps, and lists are always out-of-line + // Blobs, lists, maps, and sets are always out-of-line case types.Blob: r, err = WriteValue(v, s) + case types.List: + r, err = WriteValue(v, s) case types.Map: r, err = WriteValue(v, s) - case types.List: + case types.Set: r, err = WriteValue(v, s) default: // Other types are always inline. diff --git a/enc/json_encode_test.go b/enc/json_encode_test.go index 499e1b85f0..4fbf5ca9a6 100644 --- a/enc/json_encode_test.go +++ b/enc/json_encode_test.go @@ -83,4 +83,8 @@ func TestJsonEncode(t *testing.T) { testEncode(`j {"map":{"bool":false,"int32":{"int32":42},"list":{"ref":"sha1-58bdf8e374b39f9b1e8a64784cf5c09601f4b7ea"},"map":{"ref":"sha1-fa8026bf44f60b64ab674c49cda31a697467973c"},"string":"hotdog"}} `, types.NewMap("bool", types.Bool(false), "int32", types.Int32(42), "string", types.NewString("hotdog"), "list", types.NewList(), "map", types.NewMap())) assertChildVals() + + // Sets + testEncode(`j {"set":[]} +`, types.NewSet()) } diff --git a/ref/ref_slice.go b/ref/ref_slice.go new file mode 100644 index 0000000000..7b3ab540d6 --- /dev/null +++ b/ref/ref_slice.go @@ -0,0 +1,43 @@ +package ref + +import ( + . "github.com/attic-labs/noms/dbg" +) + +type RefSlice []Ref + +func (rs RefSlice) Len() int { + return len(rs) +} + +func (rs RefSlice) Less(i, j int) bool { + d1, d2 := rs[i].digest, rs[j].digest + Chk.Equal(len(d1), len(d2)) + for k := 0; k < len(d1); k++ { + b1, b2 := d1[k], d2[k] + if b1 < b2 { + return true + } else if b1 > b2 { + return false + } + } + return false +} + +func (rs RefSlice) Swap(i, j int) { + t := rs[j] + rs[j] = rs[i] + rs[i] = t +} + +func (rs RefSlice) Equals(other RefSlice) bool { + if len(rs) != len(other) { + return false + } + for i := 0; i < len(rs); i++ { + if rs[i] != other[i] { + return false + } + } + return true +} diff --git a/ref/ref_slice_test.go b/ref/ref_slice_test.go new file mode 100644 index 0000000000..8b8dfda820 --- /dev/null +++ b/ref/ref_slice_test.go @@ -0,0 +1,31 @@ +package ref + +import ( + "sort" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestRefSliceSort(t *testing.T) { + assert := assert.New(t) + + rs := RefSlice{} + for i := 1; i <= 3; i++ { + for j := 1; j <= 3; j++ { + d := Sha1Digest{} + for k := 1; k <= j; k++ { + d[k-1] = byte(i) + } + rs = append(rs, New(d)) + } + } + + rs2 := RefSlice(make([]Ref, len(rs))) + copy(rs2, rs) + sort.Sort(sort.Reverse(rs2)) + assert.False(rs.Equals(rs2)) + + sort.Sort(rs2) + assert.True(rs.Equals(rs2)) +} diff --git a/types/flat_map.go b/types/flat_map.go index 691fad9c38..5ead9ce368 100644 --- a/types/flat_map.go +++ b/types/flat_map.go @@ -52,7 +52,7 @@ func (fm flatMap) Remove(k string) Map { return newFlatMap(m) } -func (fm flatMap) Iter(cb IterCallback) { +func (fm flatMap) Iter(cb mapIterCallback) { for k, v := range fm.m { if cb(k, v) { break diff --git a/types/flat_set.go b/types/flat_set.go new file mode 100644 index 0000000000..ff6e56257a --- /dev/null +++ b/types/flat_set.go @@ -0,0 +1,77 @@ +package types + +import ( + "github.com/attic-labs/noms/ref" +) + +type flatSet struct { + m setInternalMap + cr *cachedRef +} + +type setInternalMap map[ref.Ref]Value + +func newFlatSet(m setInternalMap) flatSet { + return flatSet{ + m: m, + cr: &cachedRef{}, + } +} + +func (fs flatSet) Len() uint64 { + return uint64(len(fs.m)) +} + +func (fs flatSet) Has(v Value) bool { + _, ok := fs.m[v.Ref()] + return ok +} + +func (fs flatSet) Insert(values ...Value) Set { + return newFlatSet(buildInternalMap(fs.m, values)) +} + +func (fs flatSet) Remove(values ...Value) Set { + m2 := copyInternalMap(fs.m) + for _, v := range values { + delete(m2, v.Ref()) + } + return newFlatSet(m2) +} + +func (fm flatSet) Iter(cb setIterCallback) { + // TODO: sort iteration order + for _, v := range fm.m { + if cb(v) { + break + } + } +} + +func (fs flatSet) Ref() ref.Ref { + return fs.cr.Ref(fs) +} + +func (fs flatSet) Equals(other Value) bool { + if other == nil { + return false + } else { + return fs.Ref() == other.Ref() + } +} + +func copyInternalMap(m setInternalMap) setInternalMap { + r := setInternalMap{} + for k, v := range m { + r[k] = v + } + return r +} + +func buildInternalMap(old setInternalMap, values []Value) setInternalMap { + m := copyInternalMap(old) + for _, v := range values { + m[v.Ref()] = v + } + return m +} diff --git a/types/map.go b/types/map.go index ed441726de..2344726913 100644 --- a/types/map.go +++ b/types/map.go @@ -1,6 +1,6 @@ package types -type IterCallback func(k string, v Value) bool +type mapIterCallback func(k string, v Value) bool type Map interface { Value @@ -11,7 +11,7 @@ type Map interface { Set(k string, v Value) Map SetM(kv ...interface{}) Map Remove(k string) Map - Iter(IterCallback) + Iter(mapIterCallback) } func NewMap(kv ...interface{}) Map { diff --git a/types/set.go b/types/set.go new file mode 100644 index 0000000000..ebe975f503 --- /dev/null +++ b/types/set.go @@ -0,0 +1,16 @@ +package types + +type setIterCallback func(v Value) bool + +type Set interface { + Value + Len() uint64 + Has(v Value) bool + Iter(setIterCallback) + Insert(v ...Value) Set + Remove(v ...Value) Set +} + +func NewSet(v ...Value) Set { + return newFlatSet(buildInternalMap(setInternalMap{}, v)) +} diff --git a/types/test/cached_ref_test.go b/types/test/cached_ref_test.go index 31f4c11059..1ddb9a34f3 100644 --- a/types/test/cached_ref_test.go +++ b/types/test/cached_ref_test.go @@ -37,6 +37,7 @@ func TestCachedRef(t *testing.T) { NewList(), NewString(""), NewMap(), + NewSet(), } for i := 0; i < 2; i++ { for j, v := range values { diff --git a/types/test/equals_test.go b/types/test/equals_test.go index 3570fc5ea2..29f45f4be9 100644 --- a/types/test/equals_test.go +++ b/types/test/equals_test.go @@ -47,6 +47,8 @@ func TestPrimitiveEquals(t *testing.T) { func() types.Value { return types.NewList(types.NewString("bar")) }, func() types.Value { return types.NewMap() }, func() types.Value { return types.NewMap("a", types.NewString("a")) }, + func() types.Value { return types.NewSet() }, + func() types.Value { return types.NewSet(types.NewString("hi")) }, } for i, f1 := range values { diff --git a/types/test/flat_set_test.go b/types/test/flat_set_test.go new file mode 100644 index 0000000000..d0f4ea3a16 --- /dev/null +++ b/types/test/flat_set_test.go @@ -0,0 +1,82 @@ +package test + +import ( + "testing" + + _ "github.com/attic-labs/noms/enc" + . "github.com/attic-labs/noms/types" + "github.com/stretchr/testify/assert" +) + +func TestSetLen(t *testing.T) { + assert := assert.New(t) + s1 := NewSet(Bool(true), Int32(1), NewString("hi")) + assert.Equal(uint64(3), s1.Len()) + s2 := s1.Insert(Bool(false)) + assert.Equal(uint64(4), s2.Len()) + s3 := s2.Remove(Bool(true)) + assert.Equal(uint64(3), s3.Len()) +} + +func TestSetHas(t *testing.T) { + assert := assert.New(t) + s1 := NewSet(Bool(true), Int32(1), NewString("hi")) + assert.True(s1.Has(Bool(true))) + assert.False(s1.Has(Bool(false))) + assert.True(s1.Has(Int32(1))) + assert.False(s1.Has(Int32(0))) + assert.True(s1.Has(NewString("hi"))) + assert.False(s1.Has(NewString("ho"))) + + s2 := s1.Insert(Bool(false)) + assert.True(s2.Has(Bool(false))) + assert.True(s2.Has(Bool(true))) + + assert.True(s1.Has(Bool(true))) + assert.False(s1.Has(Bool(false))) +} + +func TestSetInsert(t *testing.T) { + assert := assert.New(t) + s := NewSet() + v1 := Bool(false) + v2 := Bool(true) + v3 := Int32(0) + + assert.False(s.Has(v1)) + s = s.Insert(v1) + assert.True(s.Has(v1)) + s = s.Insert(v2) + assert.True(s.Has(v1)) + assert.True(s.Has(v2)) + s2 := s.Insert(v3) + assert.True(s.Has(v1)) + assert.True(s.Has(v2)) + assert.False(s.Has(v3)) + assert.True(s2.Has(v1)) + assert.True(s2.Has(v2)) + assert.True(s2.Has(v3)) +} + +func TestSetRemove(t *testing.T) { + assert := assert.New(t) + v1 := Bool(false) + v2 := Bool(true) + v3 := Int32(0) + s := NewSet(v1, v2, v3) + assert.True(s.Has(v1)) + assert.True(s.Has(v2)) + assert.True(s.Has(v3)) + s = s.Remove(v1) + assert.False(s.Has(v1)) + assert.True(s.Has(v2)) + assert.True(s.Has(v3)) + s2 := s.Remove(v2) + assert.False(s.Has(v1)) + assert.True(s.Has(v2)) + assert.True(s.Has(v3)) + assert.False(s2.Has(v1)) + assert.False(s2.Has(v2)) + assert.True(s2.Has(v3)) + +}