diff --git a/.chloggen/add-as-read-only-pcommon-func.yaml b/.chloggen/add-as-read-only-pcommon-func.yaml new file mode 100644 index 000000000000..4e0481c35e55 --- /dev/null +++ b/.chloggen/add-as-read-only-pcommon-func.yaml @@ -0,0 +1,25 @@ +# Use this changelog template to create an entry for release notes. + +# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix' +change_type: enhancement + +# The name of the component, or a single word describing the area of concern, (e.g. otlpreceiver) +component: pdata/pcommon + +# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`). +note: Add the `AsReadOnly` function to allow creating read-only shallow copies of `pcommon.Map` and `pcommon.Slice`. + +# One or more tracking issues or pull requests related to the change +issues: [12995] + +# (Optional) One or more lines of additional information to render under the primary note. +# These lines will be padded with 2 spaces and then inserted directly into the document. +# Use pipe (|) for multiline entries. +subtext: + +# Optional: The change log or logs in which this entry should be included. +# e.g. '[user]' or '[user, api]' +# Include 'user' if the change is relevant to end users. +# Include 'api' if there is a change to a library API. +# Default: '[user]' +change_logs: [api] diff --git a/pdata/pcommon/map.go b/pdata/pcommon/map.go index 32c148c03c25..f73e4bac338d 100644 --- a/pdata/pcommon/map.go +++ b/pdata/pcommon/map.go @@ -335,3 +335,9 @@ func (m Map) Equal(val Map) bool { }) return fullEqual } + +// AsReadOnly returns a read-only shallow copy of this Map instance. +func (m Map) AsReadOnly() Map { + state := internal.StateReadOnly + return newMap(m.getOrig(), &state) +} diff --git a/pdata/pcommon/map_test.go b/pdata/pcommon/map_test.go index 55dcd41aad3c..9b72e91c33f0 100644 --- a/pdata/pcommon/map_test.go +++ b/pdata/pcommon/map_test.go @@ -92,6 +92,30 @@ func TestMapReadOnly(t *testing.T) { assert.Panics(t, func() { _ = m.FromRaw(map[string]any{"k1": "v1"}) }) } +func TestMapReadOnlyValues(t *testing.T) { + state := internal.StateReadOnly + m := newMap(&[]otlpcommon.KeyValue{ + {Key: "k1", Value: otlpcommon.AnyValue{Value: &otlpcommon.AnyValue_StringValue{StringValue: "v1"}}}, + {Key: "k2", Value: otlpcommon.AnyValue{Value: &otlpcommon.AnyValue_StringValue{StringValue: "v2"}}}, + }, &state) + + // Value returned by Get(k) must be read-only. + gv, ok := m.Get("k1") + assert.True(t, ok) + assert.Panics(t, func() { gv.SetStr("foo") }) + + // Value passed to the Range() callback must be read-only. + m.Range(func(_ string, rv Value) bool { + assert.Panics(t, func() { rv.SetStr("foo") }) + return true + }) + + // Yielded values from All() must be read-only. + for _, iv := range m.All() { + assert.Panics(t, func() { iv.SetStr("foo") }) + } +} + func TestMapPutEmpty(t *testing.T) { m := NewMap() v := m.PutEmpty("k1") @@ -532,6 +556,30 @@ func TestMap_RemoveIf(t *testing.T) { assert.True(t, exists) } +func TestMap_AsReadOnly(t *testing.T) { + src := NewMap() + src.PutStr("k1", "v1") + src.PutStr("k2", "v2") + + cp := src.AsReadOnly() + + // Assert read-only state and values + readOnlyState := internal.StateReadOnly + assert.Equal(t, &readOnlyState, cp.getState()) + assert.NotEqual(t, src, cp) + assert.Equal(t, src.AsRaw(), cp.AsRaw()) + + // Assert it's a shallow copy + k1Val, _ := src.Get("k1") + require.NotNil(t, k1Val) + k1Val.SetStr("shallow") + + srcVal, _ := src.Get("k1") + cpVal, _ := cp.Get("k1") + assert.NotEqual(t, srcVal, cpVal) + assert.Equal(t, srcVal.AsRaw(), cpVal.AsRaw()) +} + func generateTestEmptyMap(t *testing.T) Map { m := NewMap() assert.NoError(t, m.FromRaw(map[string]any{"k": map[string]any(nil)})) diff --git a/pdata/pcommon/slice.go b/pdata/pcommon/slice.go index 7e8037df3226..471d0b9dc687 100644 --- a/pdata/pcommon/slice.go +++ b/pdata/pcommon/slice.go @@ -195,3 +195,9 @@ func (es Slice) Equal(val Slice) bool { } return true } + +// AsReadOnly returns a read-only shallow copy of this Slice instance. +func (es Slice) AsReadOnly() Slice { + state := internal.StateReadOnly + return newSlice(es.getOrig(), &state) +} diff --git a/pdata/pcommon/slice_test.go b/pdata/pcommon/slice_test.go index 95cccc98f3ac..d52a3f829a47 100644 --- a/pdata/pcommon/slice_test.go +++ b/pdata/pcommon/slice_test.go @@ -54,6 +54,24 @@ func TestSliceReadOnly(t *testing.T) { assert.Panics(t, func() { _ = es.FromRaw([]any{3}) }) } +func TestSliceReadOnlyValues(t *testing.T) { + state := internal.StateReadOnly + es := newSlice(&[]otlpcommon.AnyValue{ + {Value: &otlpcommon.AnyValue_StringValue{StringValue: "v1"}}, + {Value: &otlpcommon.AnyValue_StringValue{StringValue: "v2"}}, + }, &state) + + // Value returned by At(i) must be read-only. + av := es.At(0) + assert.NotEmpty(t, av) + assert.Panics(t, func() { av.SetStr("foo") }) + + // Yielded values from All() must be read-only. + for _, iv := range es.All() { + assert.Panics(t, func() { iv.SetStr("foo") }) + } +} + func TestSlice_CopyTo(t *testing.T) { dest := NewSlice() // Test CopyTo to empty @@ -192,3 +210,21 @@ func BenchmarkSliceEqual(b *testing.B) { _ = es.Equal(cmp) } } + +func TestSlice_AsReadOnly(t *testing.T) { + src := NewSlice() + src.AppendEmpty().SetStr("v1") + src.AppendEmpty().SetStr("v2") + + cp := src.AsReadOnly() + + // Assert read-only state and values + readOnlyState := internal.StateReadOnly + assert.Equal(t, &readOnlyState, cp.getState()) + assert.NotEqual(t, src, cp) + assert.Equal(t, src.AsRaw(), cp.AsRaw()) + + // Assert it's a shallow copy + src.At(0).SetStr("shallow") + assert.Equal(t, src.At(0).AsRaw(), cp.At(0).AsRaw()) +}