Skip to content

Commit 259b205

Browse files
authored
feat: add support for homogenous arrays (#1203)
1 parent bf26e9a commit 259b205

File tree

10 files changed

+417
-6
lines changed

10 files changed

+417
-6
lines changed

attribute/builder.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,31 @@ func Bool(key string, v bool) Builder {
3030
return Builder{key, BoolValue(v)}
3131
}
3232

33+
// BoolSlice returns a Builder for a bool slice.
34+
func BoolSlice(key string, v []bool) Builder {
35+
return Builder{key, BoolSliceValue(v)}
36+
}
37+
38+
// IntSlice returns a Builder for an int slice.
39+
func IntSlice(key string, v []int) Builder {
40+
return Builder{key, IntSliceValue(v)}
41+
}
42+
43+
// Int64Slice returns a Builder for an int64 slice.
44+
func Int64Slice(key string, v []int64) Builder {
45+
return Builder{key, Int64SliceValue(v)}
46+
}
47+
48+
// Float64Slice returns a Builder for a float64 slice.
49+
func Float64Slice(key string, v []float64) Builder {
50+
return Builder{key, Float64SliceValue(v)}
51+
}
52+
53+
// StringSlice returns a Builder for a string slice.
54+
func StringSlice(key string, v []string) Builder {
55+
return Builder{key, StringSliceValue(v)}
56+
}
57+
3358
// Valid checks for valid key and type.
3459
func (b *Builder) Valid() bool {
3560
return len(b.Key) > 0 && b.Value.Type() != INVALID

attribute/builder_test.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package attribute
22

33
import (
44
"testing"
5+
6+
"github.com/google/go-cmp/cmp"
57
)
68

79
func TestBuilder(t *testing.T) {
@@ -42,10 +44,47 @@ func TestBuilder(t *testing.T) {
4244
wantType: STRING,
4345
wantValue: "foo",
4446
},
47+
{
48+
name: "BoolSlice() correctly returns a bool slice builder",
49+
value: BoolSlice(k, []bool{true, false, true}),
50+
wantType: BOOLSLICE,
51+
wantValue: []bool{true, false, true},
52+
},
53+
{
54+
name: "IntSlice() correctly returns an int slice builder",
55+
value: IntSlice(k, []int{1, 2, 3}),
56+
wantType: INT64SLICE,
57+
wantValue: []int64{1, 2, 3},
58+
},
59+
{
60+
name: "Int64Slice() correctly returns an int64 slice builder",
61+
value: Int64Slice(k, []int64{42, 43, 44}),
62+
wantType: INT64SLICE,
63+
wantValue: []int64{42, 43, 44},
64+
},
65+
{
66+
name: "Float64Slice() correctly returns a float64 slice builder",
67+
value: Float64Slice(k, []float64{1.5, 2.5, 3.5}),
68+
wantType: FLOAT64SLICE,
69+
wantValue: []float64{1.5, 2.5, 3.5},
70+
},
71+
{
72+
name: "StringSlice() correctly returns a string slice builder",
73+
value: StringSlice(k, []string{"foo", "bar", "baz"}),
74+
wantType: STRINGSLICE,
75+
wantValue: []string{"foo", "bar", "baz"},
76+
},
4577
} {
4678
if testcase.value.Value.Type() != testcase.wantType {
4779
t.Errorf("wrong value type, got %#v, expected %#v", testcase.value.Value.Type(), testcase.wantType)
4880
}
81+
if testcase.value.Key != k {
82+
t.Errorf("wrong key, got %#v, expected %#v", testcase.value.Key, k)
83+
}
84+
got := testcase.value.Value.AsInterface()
85+
if diff := cmp.Diff(testcase.wantValue, got); diff != "" {
86+
t.Errorf("+got, -want: %s", diff)
87+
}
4988
}
5089
}
5190

@@ -79,6 +118,26 @@ func TestBuilder_Valid(t *testing.T) {
79118
},
80119
wantValid: false,
81120
},
121+
{
122+
name: "BoolSlice builder is valid",
123+
builder: BoolSlice("key", []bool{true}),
124+
wantValid: true,
125+
},
126+
{
127+
name: "Int64Slice builder is valid",
128+
builder: Int64Slice("key", []int64{42}),
129+
wantValid: true,
130+
},
131+
{
132+
name: "Float64Slice builder is valid",
133+
builder: Float64Slice("key", []float64{1.5}),
134+
wantValid: true,
135+
},
136+
{
137+
name: "StringSlice builder is valid",
138+
builder: StringSlice("key", []string{"foo"}),
139+
wantValid: true,
140+
},
82141
} {
83142
valid := tt.builder.Valid()
84143
if tt.wantValid != valid {

attribute/slicehelpers.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package attribute
2+
3+
import "reflect"
4+
5+
func asSlice[T any](v any) []T {
6+
rv := reflect.ValueOf(v)
7+
if rv.Kind() != reflect.Array {
8+
return nil
9+
}
10+
cpy := make([]T, rv.Len())
11+
if len(cpy) > 0 {
12+
_ = reflect.Copy(reflect.ValueOf(cpy), rv)
13+
}
14+
return cpy
15+
}

attribute/value.go

Lines changed: 120 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package attribute
1919
import (
2020
"encoding/json"
2121
"fmt"
22+
"reflect"
2223
"strconv"
2324
)
2425

@@ -30,6 +31,7 @@ type Value struct {
3031
vtype Type
3132
numeric uint64
3233
stringly string
34+
slice any
3335
}
3436

3537
const (
@@ -43,6 +45,14 @@ const (
4345
FLOAT64
4446
// STRING is a string Type Value.
4547
STRING
48+
// BOOLSLICE is a slice of booleans Type Value.
49+
BOOLSLICE
50+
// INT64SLICE is a slice of 64-bit signed integral numbers Type Value.
51+
INT64SLICE
52+
// FLOAT64SLICE is a slice of 64-bit floating point numbers Type Value.
53+
FLOAT64SLICE
54+
// STRINGSLICE is a slice of strings Type Value.
55+
STRINGSLICE
4656
// UINT64 is a 64-bit unsigned integral Type Value.
4757
//
4858
// This type is intentionally not exposed through the Builder API.
@@ -57,11 +67,30 @@ func BoolValue(v bool) Value {
5767
}
5868
}
5969

70+
// BoolSliceValue creates a BOOLSLICE Value.
71+
func BoolSliceValue(v []bool) Value {
72+
cp := reflect.New(reflect.ArrayOf(len(v), reflect.TypeFor[bool]())).Elem()
73+
reflect.Copy(cp, reflect.ValueOf(v))
74+
return Value{vtype: BOOLSLICE, slice: cp.Interface()}
75+
}
76+
6077
// IntValue creates an INT64 Value.
6178
func IntValue(v int) Value {
6279
return Int64Value(int64(v))
6380
}
6481

82+
// IntSliceValue creates an INTSLICE Value.
83+
func IntSliceValue(v []int) Value {
84+
cp := reflect.New(reflect.ArrayOf(len(v), reflect.TypeFor[int64]()))
85+
for i, val := range v {
86+
cp.Elem().Index(i).SetInt(int64(val))
87+
}
88+
return Value{
89+
vtype: INT64SLICE,
90+
slice: cp.Elem().Interface(),
91+
}
92+
}
93+
6594
// Int64Value creates an INT64 Value.
6695
func Int64Value(v int64) Value {
6796
return Value{
@@ -70,6 +99,13 @@ func Int64Value(v int64) Value {
7099
}
71100
}
72101

102+
// Int64SliceValue creates an INT64SLICE Value.
103+
func Int64SliceValue(v []int64) Value {
104+
cp := reflect.New(reflect.ArrayOf(len(v), reflect.TypeFor[int64]())).Elem()
105+
reflect.Copy(cp, reflect.ValueOf(v))
106+
return Value{vtype: INT64SLICE, slice: cp.Interface()}
107+
}
108+
73109
// Float64Value creates a FLOAT64 Value.
74110
func Float64Value(v float64) Value {
75111
return Value{
@@ -78,6 +114,13 @@ func Float64Value(v float64) Value {
78114
}
79115
}
80116

117+
// Float64SliceValue creates a FLOAT64SLICE Value.
118+
func Float64SliceValue(v []float64) Value {
119+
cp := reflect.New(reflect.ArrayOf(len(v), reflect.TypeFor[float64]())).Elem()
120+
reflect.Copy(cp, reflect.ValueOf(v))
121+
return Value{vtype: FLOAT64SLICE, slice: cp.Interface()}
122+
}
123+
81124
// StringValue creates a STRING Value.
82125
func StringValue(v string) Value {
83126
return Value{
@@ -86,6 +129,13 @@ func StringValue(v string) Value {
86129
}
87130
}
88131

132+
// StringSliceValue creates a STRINGSLICE Value.
133+
func StringSliceValue(v []string) Value {
134+
cp := reflect.New(reflect.ArrayOf(len(v), reflect.TypeFor[string]())).Elem()
135+
reflect.Copy(cp, reflect.ValueOf(v))
136+
return Value{vtype: STRINGSLICE, slice: cp.Interface()}
137+
}
138+
89139
// Uint64Value creates a UINT64 Value.
90140
//
91141
// This constructor is intentionally not exposed through the Builder API.
@@ -107,24 +157,60 @@ func (v Value) AsBool() bool {
107157
return rawToBool(v.numeric)
108158
}
109159

160+
// AsBoolSlice returns the []bool value. Make sure that the Value's type is
161+
// BOOLSLICE.
162+
func (v Value) AsBoolSlice() []bool {
163+
if v.vtype != BOOLSLICE {
164+
return nil
165+
}
166+
return asSlice[bool](v.slice)
167+
}
168+
110169
// AsInt64 returns the int64 value. Make sure that the Value's type is
111170
// INT64.
112171
func (v Value) AsInt64() int64 {
113172
return rawToInt64(v.numeric)
114173
}
115174

175+
// AsInt64Slice returns the []int64 value. Make sure that the Value's type is
176+
// INT64SLICE.
177+
func (v Value) AsInt64Slice() []int64 {
178+
if v.vtype != INT64SLICE {
179+
return nil
180+
}
181+
return asSlice[int64](v.slice)
182+
}
183+
116184
// AsFloat64 returns the float64 value. Make sure that the Value's
117185
// type is FLOAT64.
118186
func (v Value) AsFloat64() float64 {
119187
return rawToFloat64(v.numeric)
120188
}
121189

190+
// AsFloat64Slice returns the []float64 value. Make sure that the Value's type is
191+
// FLOAT64SLICE.
192+
func (v Value) AsFloat64Slice() []float64 {
193+
if v.vtype != FLOAT64SLICE {
194+
return nil
195+
}
196+
return asSlice[float64](v.slice)
197+
}
198+
122199
// AsString returns the string value. Make sure that the Value's type
123200
// is STRING.
124201
func (v Value) AsString() string {
125202
return v.stringly
126203
}
127204

205+
// AsStringSlice returns the []string value. Make sure that the Value's type is
206+
// STRINGSLICE.
207+
func (v Value) AsStringSlice() []string {
208+
if v.vtype != STRINGSLICE {
209+
return nil
210+
}
211+
return asSlice[string](v.slice)
212+
}
213+
128214
// AsUint64 returns the uint64 value. Make sure that the Value's type is
129215
// UINT64.
130216
func (v Value) AsUint64() uint64 {
@@ -138,12 +224,20 @@ func (v Value) AsInterface() interface{} {
138224
switch v.Type() {
139225
case BOOL:
140226
return v.AsBool()
227+
case BOOLSLICE:
228+
return v.AsBoolSlice()
141229
case INT64:
142230
return v.AsInt64()
231+
case INT64SLICE:
232+
return v.AsInt64Slice()
143233
case FLOAT64:
144234
return v.AsFloat64()
235+
case FLOAT64SLICE:
236+
return v.AsFloat64Slice()
145237
case STRING:
146238
return v.stringly
239+
case STRINGSLICE:
240+
return v.AsStringSlice()
147241
case UINT64:
148242
return v.numeric
149243
}
@@ -153,12 +247,20 @@ func (v Value) AsInterface() interface{} {
153247
// String returns a string representation of Value's data.
154248
func (v Value) String() string {
155249
switch v.Type() {
250+
case BOOLSLICE:
251+
return fmt.Sprint(v.AsBoolSlice())
156252
case BOOL:
157253
return strconv.FormatBool(v.AsBool())
254+
case INT64SLICE:
255+
return fmt.Sprint(v.AsInt64Slice())
158256
case INT64:
159257
return strconv.FormatInt(v.AsInt64(), 10)
258+
case FLOAT64SLICE:
259+
return fmt.Sprint(v.AsFloat64Slice())
160260
case FLOAT64:
161261
return fmt.Sprint(v.AsFloat64())
262+
case STRINGSLICE:
263+
return fmt.Sprint(v.AsStringSlice())
162264
case STRING:
163265
return v.stringly
164266
case UINT64:
@@ -183,12 +285,20 @@ func (t Type) String() string {
183285
switch t {
184286
case BOOL:
185287
return "bool"
288+
case BOOLSLICE:
289+
return "boolslice"
186290
case INT64:
187291
return "int64"
292+
case INT64SLICE:
293+
return "int64slice"
188294
case FLOAT64:
189295
return "float64"
296+
case FLOAT64SLICE:
297+
return "float64slice"
190298
case STRING:
191299
return "string"
300+
case STRINGSLICE:
301+
return "stringslice"
192302
case UINT64:
193303
return "uint64"
194304
}
@@ -198,10 +308,14 @@ func (t Type) String() string {
198308
// mapTypesToStr is a map from attribute.Type to the primitive types the server understands.
199309
// https://develop.sentry.dev/sdk/foundations/data-model/attributes/#primitive-types
200310
var mapTypesToStr = map[Type]string{
201-
INVALID: "",
202-
BOOL: "boolean",
203-
INT64: "integer",
204-
FLOAT64: "double",
205-
STRING: "string",
206-
UINT64: "integer", // wire format: same "integer" type
311+
INVALID: "",
312+
BOOL: "boolean",
313+
INT64: "integer",
314+
FLOAT64: "double",
315+
STRING: "string",
316+
BOOLSLICE: "array",
317+
INT64SLICE: "array",
318+
FLOAT64SLICE: "array",
319+
STRINGSLICE: "array",
320+
UINT64: "integer", // wire format: same "integer" type
207321
}

0 commit comments

Comments
 (0)