diff --git a/.changeset/tall-worms-compare.md b/.changeset/tall-worms-compare.md new file mode 100644 index 000000000..c89d79d61 --- /dev/null +++ b/.changeset/tall-worms-compare.md @@ -0,0 +1,6 @@ +--- +"@livekit/protocol": patch +"github.com/livekit/protocol": patch +--- + +Allow adding, removing items and clearing list fields. diff --git a/livekit/livekit_models.pb.go b/livekit/livekit_models.pb.go index abb893bca..da27d7ec2 100644 --- a/livekit/livekit_models.pb.go +++ b/livekit/livekit_models.pb.go @@ -1313,7 +1313,10 @@ func (x *Pagination) GetLimit() int32 { // ListUpdate is used for updated APIs where 'repeated string' field is modified. type ListUpdate struct { state protoimpl.MessageState `protogen:"open.v1"` - Set []string `protobuf:"bytes,1,rep,name=set,proto3" json:"set,omitempty"` // set the field to a new list + Set []string `protobuf:"bytes,1,rep,name=set,proto3" json:"set,omitempty"` // set the field to a new list + Add []string `protobuf:"bytes,2,rep,name=add,proto3" json:"add,omitempty"` // append items to a list, avoiding duplicates + Del []string `protobuf:"bytes,3,rep,name=del,proto3" json:"del,omitempty"` // delete items from a list + Clear bool `protobuf:"varint,4,opt,name=clear,proto3" json:"clear,omitempty"` // sets the list to an empty list unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -1355,6 +1358,27 @@ func (x *ListUpdate) GetSet() []string { return nil } +func (x *ListUpdate) GetAdd() []string { + if x != nil { + return x.Add + } + return nil +} + +func (x *ListUpdate) GetDel() []string { + if x != nil { + return x.Del + } + return nil +} + +func (x *ListUpdate) GetClear() bool { + if x != nil { + return x.Clear + } + return false +} + type Room struct { state protoimpl.MessageState `protogen:"open.v1"` Sid string `protobuf:"bytes,1,opt,name=sid,proto3" json:"sid,omitempty"` @@ -5394,10 +5418,13 @@ const file_livekit_models_proto_rawDesc = "" + "\n" + "Pagination\x12\x19\n" + "\bafter_id\x18\x01 \x01(\tR\aafterId\x12\x14\n" + - "\x05limit\x18\x02 \x01(\x05R\x05limit\"\x1e\n" + + "\x05limit\x18\x02 \x01(\x05R\x05limit\"X\n" + "\n" + "ListUpdate\x12\x10\n" + - "\x03set\x18\x01 \x03(\tR\x03set\"\x9e\x04\n" + + "\x03set\x18\x01 \x03(\tR\x03set\x12\x10\n" + + "\x03add\x18\x02 \x03(\tR\x03add\x12\x10\n" + + "\x03del\x18\x03 \x03(\tR\x03del\x12\x14\n" + + "\x05clear\x18\x04 \x01(\bR\x05clear\"\x9e\x04\n" + "\x04Room\x12\x10\n" + "\x03sid\x18\x01 \x01(\tR\x03sid\x12\x12\n" + "\x04name\x18\x02 \x01(\tR\x04name\x12#\n" + diff --git a/livekit/types.go b/livekit/types.go index 00cfb4258..32edd4e26 100644 --- a/livekit/types.go +++ b/livekit/types.go @@ -18,6 +18,7 @@ import ( "context" "fmt" "io" + "slices" "buf.build/go/protoyaml" "github.com/dennwc/iters" @@ -226,14 +227,42 @@ func (p *ListUpdate) Validate() error { if p == nil { return nil } + change := len(p.Set)+len(p.Add)+len(p.Del) > 0 + if !p.Clear && !change { + return fmt.Errorf("unsupported list update operation") + } + if p.Clear && change { + return fmt.Errorf("cannot clear and change the list at the same time") + } + if len(p.Set) > 0 && len(p.Add)+len(p.Del) > 0 { + return fmt.Errorf("cannot set and change the list at the same time") + } for _, v := range p.Set { if v == "" { return fmt.Errorf("empty element in the list") } } + for _, v := range p.Add { + if v == "" { + return fmt.Errorf("empty element in the list") + } + } + for _, v := range p.Del { + if v == "" { + return fmt.Errorf("empty element in the list") + } + } return nil } +func (p *ListUpdate) Apply(arr []string) ([]string, error) { + if err := p.Validate(); err != nil { + return arr, err + } + applyListUpdate(&arr, p) + return arr, nil +} + func applyUpdate[T any](dst *T, set *T) { if set != nil { *dst = *set @@ -250,9 +279,28 @@ func applyListUpdate[T ~string](dst *[]T, u *ListUpdate) { if u == nil { return } - arr := make([]T, 0, len(u.Set)) - for _, v := range u.Set { - arr = append(arr, T(v)) + if u.Clear { + *dst = nil + return + } + if len(u.Set) != 0 { + arr := make([]T, 0, len(u.Set)) + for _, v := range u.Set { + arr = append(arr, T(v)) + } + *dst = arr + return + } + arr := slices.Clone(*dst) + for _, v := range u.Del { + if i := slices.Index(arr, T(v)); i >= 0 { + arr = slices.Delete(arr, i, i+1) + } + } + for _, v := range u.Add { + if i := slices.Index(arr, T(v)); i < 0 { + arr = append(arr, T(v)) + } } *dst = arr } diff --git a/livekit/types_test.go b/livekit/types_test.go index b49d457ca..5dbf1db5c 100644 --- a/livekit/types_test.go +++ b/livekit/types_test.go @@ -3,6 +3,7 @@ package livekit import ( "context" "fmt" + "slices" "testing" "github.com/dennwc/iters" @@ -238,3 +239,83 @@ func TestListPageIter(t *testing.T) { require.Equal(t, []testPageItem(nil), got) }) } + +func TestListUpdate(t *testing.T) { + var cases = []struct { + Name string + Arr []string + Update *ListUpdate + Exp []string + Err bool + }{ + { + Name: "empty update", + Update: &ListUpdate{}, + Err: true, + }, + { + Name: "clear and set", + Update: &ListUpdate{Clear: true, Set: []string{"a"}}, + Err: true, + }, + { + Name: "clear and add", + Update: &ListUpdate{Clear: true, Add: []string{"a"}}, + Err: true, + }, + { + Name: "set and add", + Update: &ListUpdate{Set: []string{"a"}, Add: []string{"b"}}, + Err: true, + }, + { + Name: "set and del", + Update: &ListUpdate{Set: []string{"a"}, Del: []string{"b"}}, + Err: true, + }, + { + Name: "clear", + Arr: []string{"a", "b"}, + Update: &ListUpdate{Clear: true}, + Exp: nil, + }, + { + Name: "set", + Arr: []string{"a"}, + Update: &ListUpdate{Set: []string{"b"}}, + Exp: []string{"b"}, + }, + { + Name: "add", + Arr: []string{"a", "b"}, + Update: &ListUpdate{Add: []string{"b", "c"}}, + Exp: []string{"a", "b", "c"}, + }, + { + Name: "del", + Arr: []string{"a", "b"}, + Update: &ListUpdate{Del: []string{"b", "c"}}, + Exp: []string{"a"}, + }, + { + Name: "add and del", + Arr: []string{"a", "b", "c"}, + Update: &ListUpdate{Add: []string{"b", "d"}, Del: []string{"c", "e"}}, + Exp: []string{"a", "b", "d"}, + }, + } + + for _, c := range cases { + t.Run(c.Name, func(t *testing.T) { + prev := slices.Clone(c.Arr) + out, err := c.Update.Apply(c.Arr) + if c.Err { + require.Error(t, err) + return + } + require.NoError(t, err) + require.Equal(t, c.Exp, out) + require.Equal(t, prev, c.Arr) + }) + } +} diff --git a/protobufs/livekit_models.proto b/protobufs/livekit_models.proto index 66e5775c7..22a5a2d73 100644 --- a/protobufs/livekit_models.proto +++ b/protobufs/livekit_models.proto @@ -31,6 +31,9 @@ message Pagination { // ListUpdate is used for updated APIs where 'repeated string' field is modified. message ListUpdate { repeated string set = 1; // set the field to a new list + repeated string add = 2; // append items to a list, avoiding duplicates + repeated string del = 3; // delete items from a list + bool clear = 4; // sets the list to an empty list } message Room {