-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathprovider.go
More file actions
323 lines (296 loc) · 10.7 KB
/
provider.go
File metadata and controls
323 lines (296 loc) · 10.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
package split_openfeature_provider_go
import (
"context"
"encoding/json"
"fmt"
"strconv"
"github.com/open-feature/go-sdk/openfeature"
"github.com/splitio/go-client/v6/splitio/client"
"github.com/splitio/go-client/v6/splitio/conf"
)
const (
// Metadata key for Split treatment config (JSON string), aligned with other Split OpenFeature providers.
flagMetadataConfigKey = "config"
)
type SplitProvider struct {
client *client.SplitClient
}
func NewProvider(splitClient *client.SplitClient) (*SplitProvider, error) {
if splitClient == nil {
return nil, errNilSplitClient
}
return &SplitProvider{
client: splitClient,
}, nil
}
// NewProviderSimple creates a SplitProvider using the given API key and default config.
// It is an alias for NewProviderWithAPIKey for backward compatibility.
func NewProviderSimple(apiKey string) (*SplitProvider, error) {
return NewProviderWithAPIKey(apiKey)
}
// NewProviderWithAPIKey creates a SplitProvider using the given API key and default config.
// The client is created internally and blocks until ready (up to 10 seconds).
// For more control, create a Split client yourself and use NewProvider.
func NewProviderWithAPIKey(apiKey string) (*SplitProvider, error) {
cfg := conf.Default()
factory, err := client.NewSplitFactory(apiKey, cfg)
if err != nil {
return nil, err
}
splitClient := factory.Client()
err = splitClient.BlockUntilReady(10)
if err != nil {
return nil, err
}
return NewProvider(splitClient)
}
func (p *SplitProvider) Metadata() openfeature.Metadata {
return openfeature.Metadata{
Name: "Split",
}
}
func (p *SplitProvider) BooleanEvaluation(ctx context.Context, flag string, defaultValue bool, flatCtx openfeature.FlattenedContext) openfeature.BoolResolutionDetail {
if noTargetingKey(flatCtx) {
return openfeature.BoolResolutionDetail{
Value: defaultValue,
ProviderResolutionDetail: detailTargetingKeyMissing(),
}
}
treatment, config := p.evaluateTreatmentWithConfig(flag, flatCtx)
if noTreatment(treatment) {
return openfeature.BoolResolutionDetail{
Value: defaultValue,
ProviderResolutionDetail: detailFlagNotFound(treatment),
}
}
var value bool
switch treatment {
case "true", "on":
value = true
case "false", "off":
value = false
default:
return openfeature.BoolResolutionDetail{
Value: defaultValue,
ProviderResolutionDetail: detailParseError(treatment),
}
}
return openfeature.BoolResolutionDetail{
Value: value,
ProviderResolutionDetail: detailSuccess(treatment, config),
}
}
func (p *SplitProvider) StringEvaluation(ctx context.Context, flag string, defaultValue string, flatCtx openfeature.FlattenedContext) openfeature.StringResolutionDetail {
if noTargetingKey(flatCtx) {
return openfeature.StringResolutionDetail{
Value: defaultValue,
ProviderResolutionDetail: detailTargetingKeyMissing(),
}
}
treatment, config := p.evaluateTreatmentWithConfig(flag, flatCtx)
if noTreatment(treatment) {
return openfeature.StringResolutionDetail{
Value: defaultValue,
ProviderResolutionDetail: detailFlagNotFound(treatment),
}
}
return openfeature.StringResolutionDetail{
Value: treatment,
ProviderResolutionDetail: detailSuccess(treatment, config),
}
}
func (p *SplitProvider) FloatEvaluation(ctx context.Context, flag string, defaultValue float64, flatCtx openfeature.FlattenedContext) openfeature.FloatResolutionDetail {
if noTargetingKey(flatCtx) {
return openfeature.FloatResolutionDetail{
Value: defaultValue,
ProviderResolutionDetail: detailTargetingKeyMissing(),
}
}
treatment, config := p.evaluateTreatmentWithConfig(flag, flatCtx)
if noTreatment(treatment) {
return openfeature.FloatResolutionDetail{
Value: defaultValue,
ProviderResolutionDetail: detailFlagNotFound(treatment),
}
}
floatEvaluated, parseErr := strconv.ParseFloat(treatment, 64)
if parseErr != nil {
return openfeature.FloatResolutionDetail{
Value: defaultValue,
ProviderResolutionDetail: detailParseError(treatment),
}
}
return openfeature.FloatResolutionDetail{
Value: floatEvaluated,
ProviderResolutionDetail: detailSuccess(treatment, config),
}
}
func (p *SplitProvider) IntEvaluation(ctx context.Context, flag string, defaultValue int64, flatCtx openfeature.FlattenedContext) openfeature.IntResolutionDetail {
if noTargetingKey(flatCtx) {
return openfeature.IntResolutionDetail{
Value: defaultValue,
ProviderResolutionDetail: detailTargetingKeyMissing(),
}
}
treatment, config := p.evaluateTreatmentWithConfig(flag, flatCtx)
if noTreatment(treatment) {
return openfeature.IntResolutionDetail{
Value: defaultValue,
ProviderResolutionDetail: detailFlagNotFound(treatment),
}
}
intEvaluated, parseErr := strconv.ParseInt(treatment, 10, 64)
if parseErr != nil {
return openfeature.IntResolutionDetail{
Value: defaultValue,
ProviderResolutionDetail: detailParseError(treatment),
}
}
return openfeature.IntResolutionDetail{
Value: intEvaluated,
ProviderResolutionDetail: detailSuccess(treatment, config),
}
}
func (p *SplitProvider) ObjectEvaluation(ctx context.Context, flag string, defaultValue interface{}, flatCtx openfeature.FlattenedContext) openfeature.InterfaceResolutionDetail {
if noTargetingKey(flatCtx) {
return openfeature.InterfaceResolutionDetail{
Value: defaultValue,
ProviderResolutionDetail: detailTargetingKeyMissing(),
}
}
treatment, config := p.evaluateTreatmentWithConfig(flag, flatCtx)
if noTreatment(treatment) {
return openfeature.InterfaceResolutionDetail{
Value: defaultValue,
ProviderResolutionDetail: detailFlagNotFound(treatment),
}
}
var data map[string]interface{}
parseErr := json.Unmarshal([]byte(treatment), &data)
if parseErr != nil {
return openfeature.InterfaceResolutionDetail{
Value: defaultValue,
ProviderResolutionDetail: detailParseError(treatment),
}
}
return openfeature.InterfaceResolutionDetail{
Value: data,
ProviderResolutionDetail: detailSuccess(treatment, config),
}
}
// Hooks returns no provider-specific hooks.
func (p *SplitProvider) Hooks() []openfeature.Hook {
return nil
}
// Track sends a tracking event to Split. It implements the openfeature.Tracker interface.
// Key is taken from evaluationContext.TargetingKey(); traffic type from evaluation context attribute "trafficType".
// If either is missing or empty, Track returns without sending (same as key requirement for evaluations).
func (p *SplitProvider) Track(ctx context.Context, trackingEventName string, evaluationContext openfeature.EvaluationContext, details openfeature.TrackingEventDetails) {
key := evaluationContext.TargetingKey()
if key == "" {
return
}
trafficType := evaluationContext.Attribute("trafficType")
if trafficType == nil {
return
}
trafficTypeStr, ok := trafficType.(string)
if !ok || trafficTypeStr == "" {
return
}
value := details.Value()
properties := stringMapFromAttributes(details.Attributes())
_ = p.client.Track(key, trafficTypeStr, trackingEventName, value, properties)
}
// stringMapFromAttributes converts map[string]any to map[string]interface{} for the Split SDK.
// Returns nil if attrs is nil or empty so the Split client receives nil for optional properties.
func stringMapFromAttributes(attrs map[string]any) map[string]interface{} {
if attrs == nil || len(attrs) == 0 {
return nil
}
out := make(map[string]interface{}, len(attrs))
for k, v := range attrs {
out[k] = v
}
return out
}
// *** Helpers ***
// splitKeyAndAttributes returns the targeting key and attributes from a flattened evaluation context.
// Key is taken from flatCtx[TargetingKey]; all other entries become attributes for Split.
func splitKeyAndAttributes(flatCtx openfeature.FlattenedContext) (key string, attrs map[string]interface{}) {
attrs = make(map[string]interface{})
for k, v := range flatCtx {
if k != openfeature.TargetingKey {
attrs[k] = v
}
}
if v := flatCtx[openfeature.TargetingKey]; v != nil {
if s, ok := v.(string); ok {
key = s
} else {
key = fmt.Sprint(v)
}
}
return key, attrs
}
// evaluateTreatmentWithConfig returns treatment and optional config from Split.
// Key and attributes are derived from flatCtx (targetingKey + rest as attributes).
func (p *SplitProvider) evaluateTreatmentWithConfig(flag string, flatCtx openfeature.FlattenedContext) (treatment string, config *string) {
key, attrs := splitKeyAndAttributes(flatCtx)
result := p.client.TreatmentWithConfig(key, flag, attrs)
return result.Treatment, result.Config
}
func flagMetadataWithConfig(config string) openfeature.FlagMetadata {
return openfeature.FlagMetadata{flagMetadataConfigKey: config}
}
func noTargetingKey(flatCtx openfeature.FlattenedContext) bool {
v, ok := flatCtx[openfeature.TargetingKey]
if !ok {
return true
}
s, _ := v.(string)
return s == ""
}
func noTreatment(treatment string) bool {
return treatment == "" || treatment == "control"
}
func detailFlagNotFound(variant string) openfeature.ProviderResolutionDetail {
return openfeature.ProviderResolutionDetail{
ResolutionError: openfeature.NewFlagNotFoundResolutionError("flag not found"),
Reason: openfeature.DefaultReason,
Variant: variant,
}
}
func detailParseError(variant string) openfeature.ProviderResolutionDetail {
return openfeature.ProviderResolutionDetail{
ResolutionError: openfeature.NewParseErrorResolutionError("error parsing treatment to requested type"),
Reason: openfeature.ErrorReason,
Variant: variant,
}
}
func detailTargetingKeyMissing() openfeature.ProviderResolutionDetail {
return openfeature.ProviderResolutionDetail{
ResolutionError: openfeature.NewTargetingKeyMissingResolutionError("targeting key is required and missing"),
Reason: openfeature.ErrorReason,
Variant: "",
}
}
func providerResolutionDetailError(err openfeature.ResolutionError, reason openfeature.Reason, variant string, config string) openfeature.ProviderResolutionDetail {
return openfeature.ProviderResolutionDetail{
ResolutionError: err,
Reason: reason,
Variant: variant,
FlagMetadata: flagMetadataWithConfig(config),
}
}
func detailSuccess(variant string, config *string) openfeature.ProviderResolutionDetail {
meta := openfeature.FlagMetadata(nil)
if config != nil && *config != "" {
meta = openfeature.FlagMetadata{flagMetadataConfigKey: *config}
}
return openfeature.ProviderResolutionDetail{
Reason: openfeature.TargetingMatchReason,
Variant: variant,
FlagMetadata: meta,
}
}