• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mindersec / minder / 14376078638

10 Apr 2025 08:39AM UTC coverage: 56.857%. First build
14376078638

Pull #5515

github

web-flow
Merge ea220ae51 into 0f5ecd80e
Pull Request #5515: Don't return errors when getting properties

27 of 49 new or added lines in 22 files covered. (55.1%)

18305 of 32195 relevant lines covered (56.86%)

37.04 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

91.76
/pkg/entities/properties/properties.go
1
// SPDX-FileCopyrightText: Copyright 2024 The Minder Authors
2
// SPDX-License-Identifier: Apache-2.0
3

4
// Package properties provides a simple way to access properties of an entity
5
package properties
6

7
import (
8
        "fmt"
9
        "iter"
10
        "strconv"
11
        "strings"
12

13
        "github.com/puzpuzpuz/xsync/v3"
14
        "github.com/rs/zerolog"
15
        "golang.org/x/exp/constraints"
16
        "google.golang.org/protobuf/proto"
17
        "google.golang.org/protobuf/types/known/structpb"
18
)
19

20
// NumericalValueToUpstreamID converts a numerical value to a string for use as an upstream ID
21
func NumericalValueToUpstreamID[T constraints.Integer](n T) string {
73✔
22
        return strconv.FormatInt(int64(n), 10)
73✔
23
}
73✔
24

25
// Property is a struct that holds a value. It's just a wrapper around structpb.Value
26
// with typed getters and handling of a nil receiver
27
type Property struct {
28
        value *structpb.Value
29
}
30

31
const (
32
        typeInt64      = "int64"
33
        typeUint64     = "uint64"
34
        internalPrefix = "minder.internal."
35
        typeKey        = "minder.internal.type"
36
        valueKey       = "minder.internal.value"
37
)
38

39
func wrapKeyValue(key, value string) map[string]any {
113✔
40
        return map[string]any{
113✔
41
                typeKey:  key,
113✔
42
                valueKey: value,
113✔
43
        }
113✔
44
}
113✔
45

46
func wrapInt64(value int64) map[string]any {
84✔
47
        return wrapKeyValue(typeInt64, strconv.FormatInt(value, 10))
84✔
48
}
84✔
49

50
func wrapUint64(value uint64) map[string]any {
29✔
51
        return wrapKeyValue(typeUint64, strconv.FormatUint(value, 10))
29✔
52
}
29✔
53

54
func unwrapTypedValue(value *structpb.Value, typ string) (string, error) {
39✔
55
        structValue := value.GetStructValue()
39✔
56
        if structValue == nil {
39✔
57
                return "", fmt.Errorf("value is not a map")
×
58
        }
×
59

60
        mapValue := structValue.GetFields()
39✔
61
        typeVal, ok := mapValue[typeKey]
39✔
62
        if !ok {
40✔
63
                return "", fmt.Errorf("type field not found")
1✔
64
        }
1✔
65

66
        if typeVal.GetStringValue() != typ {
39✔
67
                return "", fmt.Errorf("value is not of type %s", typ)
1✔
68
        }
1✔
69

70
        valPayload, ok := mapValue[valueKey]
37✔
71
        if !ok {
38✔
72
                return "", fmt.Errorf("value field not found")
1✔
73
        }
1✔
74

75
        return valPayload.GetStringValue(), nil
36✔
76
}
77

78
// NewProperty creates a new Property with a given value
79
func NewProperty(value any) (*Property, error) {
687✔
80
        var err error
687✔
81
        var val *structpb.Value
687✔
82

687✔
83
        switch v := value.(type) {
687✔
84
        case int64:
84✔
85
                value = wrapInt64(v)
84✔
86
        case uint64:
29✔
87
                value = wrapUint64(v)
29✔
88
        }
89

90
        val, err = structpb.NewValue(value)
687✔
91
        if err != nil {
688✔
92
                return nil, err
1✔
93
        }
1✔
94
        return &Property{value: val}, nil
686✔
95
}
96

97
func propertyValueAs[T any](value any) (T, error) {
77✔
98
        var zero T
77✔
99
        val, ok := value.(T)
77✔
100
        if !ok {
79✔
101
                return zero, fmt.Errorf("value is not of type %T", zero)
2✔
102
        }
2✔
103
        return val, nil
75✔
104
}
105

106
// AsBool returns the boolean value, or an error if the value is not a boolean
107
func (p *Property) AsBool() (bool, error) {
40✔
108
        if p == nil {
45✔
109
                return false, fmt.Errorf("property is nil")
5✔
110
        }
5✔
111
        return propertyValueAs[bool](p.value.AsInterface())
35✔
112
}
113

114
// GetBool returns the boolean value, or false if the value is not a boolean
115
func (p *Property) GetBool() bool {
32✔
116
        if p == nil {
41✔
117
                return false
9✔
118
        }
9✔
119
        return p.value.GetBoolValue()
23✔
120
}
121

122
// AsString returns the string value, or an error if the value is not a string
123
func (p *Property) AsString() (string, error) {
52✔
124
        if p == nil {
62✔
125
                return "", fmt.Errorf("property is nil")
10✔
126
        }
10✔
127
        return propertyValueAs[string](p.value.AsInterface())
42✔
128
}
129

130
// GetString returns the string value, or an empty string if the value is not a string
131
func (p *Property) GetString() string {
98✔
132
        if p == nil {
150✔
133
                return ""
52✔
134
        }
52✔
135
        return p.value.GetStringValue()
46✔
136
}
137

138
// AsInt64 returns the int64 value, or an error if the value is not an int64
139
func (p *Property) AsInt64() (int64, error) {
41✔
140
        if p == nil {
42✔
141
                return 0, fmt.Errorf("property is nil")
1✔
142
        }
1✔
143

144
        switch value := p.value.Kind.(type) {
40✔
145
        case *structpb.Value_NumberValue:
2✔
146
                return int64(value.NumberValue), nil
2✔
147
        case *structpb.Value_StringValue:
2✔
148
                return strconv.ParseInt(value.StringValue, 10, 64)
2✔
149
        case *structpb.Value_StructValue:
34✔
150
                stringVal, err := unwrapTypedValue(p.value, typeInt64)
34✔
151
                if err != nil {
37✔
152
                        return 0, fmt.Errorf("failed to get int64 value: %w", err)
3✔
153
                }
3✔
154
                return strconv.ParseInt(stringVal, 10, 64)
31✔
155
        default:
2✔
156
                return 0, fmt.Errorf("failed to get int64 value from %T", value)
2✔
157
        }
158
}
159

160
// GetInt64 returns the int64 value, or 0 if the value is not an int64
161
func (p *Property) GetInt64() int64 {
24✔
162
        if p == nil {
26✔
163
                return 0
2✔
164
        }
2✔
165
        i64val, err := p.AsInt64()
22✔
166
        if err != nil {
23✔
167
                return 0
1✔
168
        }
1✔
169
        return i64val
21✔
170
}
171

172
// AsUint64 returns the uint64 value, or an error if the value is not an uint64
173
func (p *Property) AsUint64() (uint64, error) {
12✔
174
        if p == nil {
13✔
175
                return 0, fmt.Errorf("property is nil")
1✔
176
        }
1✔
177

178
        switch value := p.value.Kind.(type) {
11✔
179
        case *structpb.Value_NumberValue:
2✔
180
                return uint64(value.NumberValue), nil
2✔
181
        case *structpb.Value_StringValue:
2✔
182
                return strconv.ParseUint(value.StringValue, 10, 64)
2✔
183
        case *structpb.Value_StructValue:
5✔
184
                stringVal, err := unwrapTypedValue(p.value, typeUint64)
5✔
185
                if err != nil {
5✔
186
                        return 0, fmt.Errorf("failed to get uint64 value: %w", err)
×
187
                }
×
188
                return strconv.ParseUint(stringVal, 10, 64)
5✔
189
        default:
2✔
190
                return 0, fmt.Errorf("failed to get uint64 value from %T", value)
2✔
191
        }
192
}
193

194
// GetUint64 returns the uint64 value, or 0 if the value is not an uint64
195
func (p *Property) GetUint64() uint64 {
7✔
196
        if p == nil {
8✔
197
                return 0
1✔
198
        }
1✔
199
        u64val, err := p.AsUint64()
6✔
200
        if err != nil {
7✔
201
                return 0
1✔
202
        }
1✔
203
        return u64val
5✔
204
}
205

206
// RawValue returns the raw value as an any
207
func (p *Property) RawValue() any {
32✔
208
        if p == nil {
32✔
209
                return nil
×
210
        }
×
211
        return p.value.AsInterface()
32✔
212
}
213

214
// Equal checks if two Properties are equal
215
func (p *Property) Equal(other *Property) bool {
20✔
216
        if p == nil && other == nil {
21✔
217
                return true
1✔
218
        }
1✔
219

220
        if p == nil || other == nil {
21✔
221
                return false
2✔
222
        }
2✔
223

224
        return proto.Equal(p.value, other.value)
17✔
225
}
226

227
// Properties struct that holds the properties map and provides access to Property values
228
type Properties struct {
229
        props *xsync.MapOf[string, Property]
230
}
231

232
// newPropertiesOption is a function that configures NewProperties
233
type newPropertiesOption func(*newPropertiesConfig)
234

235
type newPropertiesConfig struct {
236
        skipPrefixCheck bool
237
}
238

239
// NewProperties creates Properties from a map.
240
// Important: Internal properties (those with the internalPrefix) are ignored.
241
// If you want to include them, use the skipPrefixCheck option.
242
// Property creation errors are ignored in favor of returning a valid Properties object.
243
func NewProperties(props map[string]any, opts ...newPropertiesOption) *Properties {
292✔
244
        config := &newPropertiesConfig{}
292✔
245
        for _, opt := range opts {
298✔
246
                opt(config)
6✔
247
        }
6✔
248

249
        propsMap := xsync.NewMapOf[string, Property](xsync.WithPresize(len(props)))
292✔
250

292✔
251
        for key, value := range props {
928✔
252
                if !config.skipPrefixCheck && strings.HasPrefix(key, internalPrefix) {
637✔
253
                        // We ignore internal properties
1✔
254
                        continue
1✔
255
                }
256

257
                propVal, err := NewProperty(value)
635✔
258
                if err != nil {
635✔
NEW
259
                        continue
×
260
                }
261
                propsMap.Store(key, *propVal)
635✔
262
        }
263
        return &Properties{
292✔
264
                props: propsMap,
292✔
265
        }
292✔
266
}
267

268
// GetProperty returns the Property for a given key or an empty one as a fallback
269
func (p *Properties) GetProperty(key string) *Property {
301✔
270
        if p == nil {
306✔
271
                return nil
5✔
272
        }
5✔
273

274
        prop, ok := p.props.Load(key)
296✔
275
        if !ok {
387✔
276
                return nil
91✔
277
        }
91✔
278
        return &prop
205✔
279
}
280

281
// SetProperty sets the Property for a given key
282
func (p *Properties) SetProperty(key string, prop *Property) {
5✔
283
        if p == nil {
5✔
284
                return
×
285
        }
×
286

287
        p.props.Store(key, *prop)
5✔
288
}
289

290
// SetKeyValue sets the key value pair in the Properties
291
func (p *Properties) SetKeyValue(key string, value any) error {
6✔
292
        if p == nil {
6✔
293
                return nil
×
294
        }
×
295

296
        prop, err := NewProperty(value)
6✔
297
        if err != nil {
7✔
298
                return fmt.Errorf("failed to create property for key %s: %w", key, err)
1✔
299
        }
1✔
300
        p.props.Store(key, *prop)
5✔
301
        return nil
5✔
302
}
303

304
// Iterate implements the seq2 iterator so that the caller can call for key, prop := range Iterate()
305
func (p *Properties) Iterate() iter.Seq2[string, *Property] {
15✔
306
        return func(yield func(string, *Property) bool) {
30✔
307
                p.props.Range(func(key string, v Property) bool {
38✔
308
                        return yield(key, &v)
23✔
309
                })
23✔
310
        }
311
}
312

313
// String implements the fmt.Stringer interface, for debugging purposes
314
func (p *Properties) String() string {
×
315
        data := make([]string, 0, p.props.Size())
×
316
        for k, v := range p.Iterate() {
×
317
                data = append(data, fmt.Sprintf("%s: %v (%T)", k, v.RawValue(), v.RawValue()))
×
318
        }
×
319
        return strings.Join(data, "\n")
×
320
}
321

322
// PropertyFilter is a function that filters properties
323
type PropertyFilter func(key string, prop *Property) bool
324

325
// FilteredCopy returns a new Properties with only the properties that pass the filter
326
func (p *Properties) FilteredCopy(filter PropertyFilter) *Properties {
1✔
327
        if p == nil {
1✔
328
                return nil
×
329
        }
×
330

331
        propsMap := xsync.NewMapOf[string, Property]()
1✔
332
        p.props.Range(func(key string, prop Property) bool {
3✔
333
                if filter(key, &prop) {
3✔
334
                        propsMap.Store(key, prop)
1✔
335
                }
1✔
336
                return true
2✔
337
        })
338

339
        return &Properties{
1✔
340
                props: propsMap,
1✔
341
        }
1✔
342
}
343

344
// Merge merges two Properties into a new one
345
func (p *Properties) Merge(other *Properties) *Properties {
12✔
346
        if p == nil {
13✔
347
                return other
1✔
348
        }
1✔
349

350
        if other == nil {
12✔
351
                return p
1✔
352
        }
1✔
353

354
        propsMap := xsync.NewMapOf[string, Property](xsync.WithPresize(p.props.Size() + other.props.Size()))
10✔
355
        p.props.Range(func(key string, prop Property) bool {
80✔
356
                propsMap.Store(key, prop)
70✔
357
                return true
70✔
358
        })
70✔
359

360
        other.props.Range(func(key string, prop Property) bool {
37✔
361
                propsMap.Store(key, prop)
27✔
362
                return true
27✔
363
        })
27✔
364

365
        return &Properties{
10✔
366
                props: propsMap,
10✔
367
        }
10✔
368
}
369

370
// ToProtoStruct converts the Properties to a protobuf Struct
371
func (p *Properties) ToProtoStruct() *structpb.Struct {
117✔
372
        if p == nil {
119✔
373
                return nil
2✔
374
        }
2✔
375

376
        fields := make(map[string]*structpb.Value)
115✔
377

115✔
378
        p.props.Range(func(key string, prop Property) bool {
334✔
379
                fields[key] = prop.value
219✔
380
                return true
219✔
381
        })
219✔
382

383
        protoStruct := &structpb.Struct{
115✔
384
                Fields: fields,
115✔
385
        }
115✔
386

115✔
387
        return protoStruct
115✔
388
}
389

390
// ToLogDict converts the Properties to a zerolog Dict
391
func (p *Properties) ToLogDict() *zerolog.Event {
19✔
392
        dict := zerolog.Dict()
19✔
393

19✔
394
        if p == nil {
19✔
395
                return dict
×
396
        }
×
397

398
        p.props.Range(func(key string, prop Property) bool {
73✔
399
                dict.Interface(key, prop.value.AsInterface())
54✔
400
                return true
54✔
401
        })
54✔
402

403
        return dict
19✔
404
}
405

406
// Len returns the number of properties
407
func (p *Properties) Len() int {
12✔
408
        if p == nil {
21✔
409
                return 0
9✔
410
        }
9✔
411
        return p.props.Size()
3✔
412
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc