Skip to content

Commit a1cb699

Browse files
feat: introduce OFREP provider (#477)
Signed-off-by: Kavindu Dodanduwa <[email protected]>
1 parent eb092d4 commit a1cb699

14 files changed

+1530
-0
lines changed

.release-please-manifest.json

+1
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,6 @@
1111
"providers/unleash": "0.0.3-alpha",
1212
"providers/harness": "0.0.4-alpha",
1313
"providers/statsig": "0.0.2",
14+
"providers/ofrep": "0.0.0",
1415
"tests/flagd": "1.4.1"
1516
}

providers/ofrep/README.md

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# OpenFeature Remote Evaluation Protocol Provider
2+
3+
This is the Go implementation of the OFREP provider.
4+
The provider works by evaluating flags against OFREP single flag evaluation endpoint.
5+
6+
## Installation
7+
8+
Use OFREP provider with the latest OpenFeature Go SDK
9+
10+
```sh
11+
go get github.com/open-feature/go-sdk-contrib/providers/ofrep
12+
go get github.com/open-feature/go-sdk
13+
```
14+
15+
## Usage
16+
17+
Initialize the provider with the URL of the OFREP implementing service,
18+
19+
```go
20+
ofrepProvider := ofrep.NewProvider("http://localhost:8016")
21+
```
22+
23+
Then, register the provider with the OpenFeature Go SDK and use derived clients for flag evaluations,
24+
25+
```go
26+
openfeature.SetProvider(ofrepProvider)
27+
```
28+
29+
## Configuration
30+
31+
You can configure the provider using following configuration options,
32+
33+
| Configuration option | Details |
34+
|----------------------|-------------------------------------------------------------------------------------------------------------------------|
35+
| WithApiKeyAuth | Set the token to be used with "X-API-Key" header |
36+
| WithBearerToken | Set the token to be used with "Bearer" HTTP Authorization schema |
37+
| WithClient | Provider a custom, pre-configured http.Client for OFREP service communication |
38+
| WithHeaderProvider | Register a custom header provider for OFREP calls. You may utilize this for custom authentication/authorization headers |
39+
40+
41+
For example, consider below example which set bearer token and provider a customized http client,
42+
43+
```go
44+
provider := ofrep.NewProvider(
45+
"http://localhost:8016",
46+
ofrep.WithBearerToken("TOKEN"),
47+
ofrep.WithClient(&http.Client{
48+
Timeout: 1 * time.Second,
49+
}))
50+
```

providers/ofrep/evaluate.go

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package ofrep
2+
3+
import (
4+
"context"
5+
6+
of "github.com/open-feature/go-sdk/openfeature"
7+
)
8+
9+
// Evaluator contract for flag evaluation
10+
type Evaluator interface {
11+
ResolveBoolean(ctx context.Context, key string, defaultValue bool,
12+
evalCtx map[string]interface{}) of.BoolResolutionDetail
13+
ResolveString(ctx context.Context, key string, defaultValue string,
14+
evalCtx map[string]interface{}) of.StringResolutionDetail
15+
ResolveFloat(ctx context.Context, key string, defaultValue float64,
16+
evalCtx map[string]interface{}) of.FloatResolutionDetail
17+
ResolveInt(ctx context.Context, key string, defaultValue int64,
18+
evalCtx map[string]interface{}) of.IntResolutionDetail
19+
ResolveObject(ctx context.Context, key string, defaultValue interface{},
20+
evalCtx map[string]interface{}) of.InterfaceResolutionDetail
21+
}

providers/ofrep/go.mod

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
module github.com/open-feature/go-sdk-contrib/providers/ofrep
2+
3+
go 1.21.0
4+
5+
require github.com/open-feature/go-sdk v1.10.0
6+
7+
require (
8+
github.com/go-logr/logr v1.4.1 // indirect
9+
golang.org/x/exp v0.0.0-20240205201215-2c58cdc269a3 // indirect
10+
)

providers/ofrep/go.sum

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
2+
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
3+
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
4+
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
5+
github.com/open-feature/go-sdk v1.10.0 h1:druQtYOrN+gyz3rMsXp0F2jW1oBXJb0V26PVQnUGLbM=
6+
github.com/open-feature/go-sdk v1.10.0/go.mod h1:+rkJhLBtYsJ5PZNddAgFILhRAAxwrJ32aU7UEUm4zQI=
7+
golang.org/x/exp v0.0.0-20240205201215-2c58cdc269a3 h1:/RIbNt/Zr7rVhIkQhooTxCxFcdWLGIKnZA4IXNFSrvo=
8+
golang.org/x/exp v0.0.0-20240205201215-2c58cdc269a3/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08=
9+
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
10+
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+194
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
package evaluate
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/open-feature/go-sdk-contrib/providers/ofrep/internal/outbound"
8+
of "github.com/open-feature/go-sdk/openfeature"
9+
)
10+
11+
// Flags is the flag evaluator implementation. It contains domain logic of the OpenFeature flag evaluation.
12+
type Flags struct {
13+
resolver resolver
14+
}
15+
16+
type resolver interface {
17+
resolveSingle(ctx context.Context, key string, evalCtx map[string]interface{}) (*successDto, *of.ResolutionError)
18+
}
19+
20+
func NewFlagsEvaluator(cfg outbound.Configuration) *Flags {
21+
return &Flags{
22+
resolver: NewOutboundResolver(cfg),
23+
}
24+
}
25+
26+
func (h Flags) ResolveBoolean(ctx context.Context, key string, defaultValue bool, evalCtx map[string]interface{}) of.BoolResolutionDetail {
27+
evalSuccess, resolutionError := h.resolver.resolveSingle(ctx, key, evalCtx)
28+
if resolutionError != nil {
29+
return of.BoolResolutionDetail{
30+
Value: defaultValue,
31+
ProviderResolutionDetail: of.ProviderResolutionDetail{
32+
ResolutionError: *resolutionError,
33+
Reason: of.ErrorReason,
34+
},
35+
}
36+
}
37+
38+
b, ok := evalSuccess.Value.(bool)
39+
if !ok {
40+
return of.BoolResolutionDetail{
41+
Value: defaultValue,
42+
ProviderResolutionDetail: of.ProviderResolutionDetail{
43+
ResolutionError: of.NewTypeMismatchResolutionError(fmt.Sprintf(
44+
"resolved value %v is not of boolean type", evalSuccess.Value)),
45+
Reason: of.ErrorReason,
46+
},
47+
}
48+
}
49+
50+
return of.BoolResolutionDetail{
51+
Value: b,
52+
ProviderResolutionDetail: of.ProviderResolutionDetail{
53+
Reason: of.Reason(evalSuccess.Reason),
54+
Variant: evalSuccess.Variant,
55+
FlagMetadata: evalSuccess.Metadata,
56+
},
57+
}
58+
}
59+
60+
func (h Flags) ResolveString(ctx context.Context, key string, defaultValue string, evalCtx map[string]interface{}) of.StringResolutionDetail {
61+
evalSuccess, resolutionError := h.resolver.resolveSingle(ctx, key, evalCtx)
62+
if resolutionError != nil {
63+
return of.StringResolutionDetail{
64+
Value: defaultValue,
65+
ProviderResolutionDetail: of.ProviderResolutionDetail{
66+
ResolutionError: *resolutionError,
67+
Reason: of.ErrorReason,
68+
},
69+
}
70+
}
71+
72+
b, ok := evalSuccess.Value.(string)
73+
if !ok {
74+
return of.StringResolutionDetail{
75+
Value: defaultValue,
76+
ProviderResolutionDetail: of.ProviderResolutionDetail{
77+
ResolutionError: of.NewTypeMismatchResolutionError(fmt.Sprintf(
78+
"resolved value %v is not of string type", evalSuccess.Value)),
79+
Reason: of.ErrorReason,
80+
},
81+
}
82+
}
83+
84+
return of.StringResolutionDetail{
85+
Value: b,
86+
ProviderResolutionDetail: of.ProviderResolutionDetail{
87+
Reason: of.Reason(evalSuccess.Reason),
88+
Variant: evalSuccess.Variant,
89+
FlagMetadata: evalSuccess.Metadata,
90+
},
91+
}
92+
}
93+
94+
func (h Flags) ResolveFloat(ctx context.Context, key string, defaultValue float64, evalCtx map[string]interface{}) of.FloatResolutionDetail {
95+
evalSuccess, resolutionError := h.resolver.resolveSingle(ctx, key, evalCtx)
96+
if resolutionError != nil {
97+
return of.FloatResolutionDetail{
98+
Value: defaultValue,
99+
ProviderResolutionDetail: of.ProviderResolutionDetail{
100+
ResolutionError: *resolutionError,
101+
Reason: of.ErrorReason,
102+
},
103+
}
104+
}
105+
106+
var value float64
107+
108+
switch evalSuccess.Value.(type) {
109+
case float32:
110+
value = float64(evalSuccess.Value.(float32))
111+
case float64:
112+
value = evalSuccess.Value.(float64)
113+
default:
114+
return of.FloatResolutionDetail{
115+
Value: defaultValue,
116+
ProviderResolutionDetail: of.ProviderResolutionDetail{
117+
ResolutionError: of.NewTypeMismatchResolutionError(fmt.Sprintf(
118+
"resolved value %v is not of float type", evalSuccess.Value)),
119+
Reason: of.ErrorReason,
120+
},
121+
}
122+
}
123+
124+
return of.FloatResolutionDetail{
125+
Value: value,
126+
ProviderResolutionDetail: of.ProviderResolutionDetail{
127+
Reason: of.Reason(evalSuccess.Reason),
128+
Variant: evalSuccess.Variant,
129+
FlagMetadata: evalSuccess.Metadata,
130+
},
131+
}
132+
}
133+
134+
func (h Flags) ResolveInt(ctx context.Context, key string, defaultValue int64, evalCtx map[string]interface{}) of.IntResolutionDetail {
135+
evalSuccess, resolutionError := h.resolver.resolveSingle(ctx, key, evalCtx)
136+
if resolutionError != nil {
137+
return of.IntResolutionDetail{
138+
Value: defaultValue,
139+
ProviderResolutionDetail: of.ProviderResolutionDetail{
140+
ResolutionError: *resolutionError,
141+
Reason: of.ErrorReason,
142+
},
143+
}
144+
}
145+
146+
var value int64
147+
148+
switch evalSuccess.Value.(type) {
149+
case int:
150+
value = int64(evalSuccess.Value.(int))
151+
case int64:
152+
value = evalSuccess.Value.(int64)
153+
default:
154+
return of.IntResolutionDetail{
155+
Value: defaultValue,
156+
ProviderResolutionDetail: of.ProviderResolutionDetail{
157+
ResolutionError: of.NewTypeMismatchResolutionError(fmt.Sprintf(
158+
"resolved value %v is not of integer type", evalSuccess.Value)),
159+
Reason: of.ErrorReason,
160+
},
161+
}
162+
}
163+
164+
return of.IntResolutionDetail{
165+
Value: value,
166+
ProviderResolutionDetail: of.ProviderResolutionDetail{
167+
Reason: of.Reason(evalSuccess.Reason),
168+
Variant: evalSuccess.Variant,
169+
FlagMetadata: evalSuccess.Metadata,
170+
},
171+
}
172+
}
173+
174+
func (h Flags) ResolveObject(ctx context.Context, key string, defaultValue interface{}, evalCtx map[string]interface{}) of.InterfaceResolutionDetail {
175+
evalSuccess, resolutionError := h.resolver.resolveSingle(ctx, key, evalCtx)
176+
if resolutionError != nil {
177+
return of.InterfaceResolutionDetail{
178+
Value: defaultValue,
179+
ProviderResolutionDetail: of.ProviderResolutionDetail{
180+
ResolutionError: *resolutionError,
181+
Reason: of.ErrorReason,
182+
},
183+
}
184+
}
185+
186+
return of.InterfaceResolutionDetail{
187+
Value: evalSuccess.Value,
188+
ProviderResolutionDetail: of.ProviderResolutionDetail{
189+
Reason: of.Reason(evalSuccess.Reason),
190+
Variant: evalSuccess.Variant,
191+
FlagMetadata: evalSuccess.Metadata,
192+
},
193+
}
194+
}

0 commit comments

Comments
 (0)