Skip to content

Commit 090b8f0

Browse files
committed
feat(ofrep): add WithFromEnv() for environment variable configuration
Add experimental WithFromEnv() option to configure the OFREP provider using environment variables. New features: - Add WithFromEnv() configuration option supporting: - OFREP_ENDPOINT: base URI for the OFREP service - OFREP_TIMEOUT: timeout duration (e.g., "30s", "500ms") - OFREP_API_KEY: API key for X-API-Key authentication - OFREP_BEARER_TOKEN: token for Bearer authentication - OFREP_HEADERS: comma-separated custom headers
1 parent 7ca90eb commit 090b8f0

File tree

3 files changed

+242
-0
lines changed

3 files changed

+242
-0
lines changed

providers/ofrep/README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ Use OFREP provider with the latest OpenFeature Go SDK
99

1010
```sh
1111
go get github.com/open-feature/go-sdk-contrib/providers/ofrep
12+
go get github.com/open-feature/go-sdk
1213
```
1314

1415
## Usage
@@ -38,6 +39,7 @@ You can configure the provider using following configuration options,
3839
| WithHeader | Set a custom header to be used for authorization |
3940
| WithBaseURI | Set the base URI of the OFREP service |
4041
| WithTimeout | Set the timeout for the http client used for communication with the OFREP service (ignored if custom client is used) |
42+
| WithFromEnv | Configure the provider using environment variables (experimental) |
4143

4244
For example, consider below example which sets bearer token and provides a customized http client,
4345

@@ -49,3 +51,23 @@ provider := ofrep.NewProvider(
4951
Timeout: 1 * time.Second,
5052
}))
5153
```
54+
55+
### Environment Variable Configuration (Experimental)
56+
57+
You can use the `WithFromEnv()` option to configure the provider using environment variables:
58+
59+
```go
60+
provider := ofrep.NewProvider(
61+
"http://localhost:8016",
62+
ofrep.WithFromEnv())
63+
```
64+
65+
Supported environment variables:
66+
67+
| Environment Variable | Description | Example |
68+
| -------------------- | --------------------------------------------------------------------- | ------------------------- |
69+
| OFREP_ENDPOINT | Base URI for the OFREP service (overrides the baseUri parameter) | `http://localhost:8016` |
70+
| OFREP_TIMEOUT | Timeout duration for HTTP requests (ignored if custom client is used) | `30s`, `500ms` |
71+
| OFREP_API_KEY | API key for X-API-Key authentication | `your-api-key` |
72+
| OFREP_BEARER_TOKEN | Token for Bearer authentication | `your-bearer-token` |
73+
| OFREP_HEADERS | Comma-separated custom headers | `Key1=Value1,Key2=Value2` |

providers/ofrep/provider.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ import (
44
"context"
55
"fmt"
66
"net/http"
7+
"os"
8+
"strconv"
9+
"strings"
710
"time"
811

912
"github.com/open-feature/go-sdk-contrib/providers/ofrep/internal/evaluate"
@@ -126,3 +129,53 @@ func WithTimeout(timeout time.Duration) func(*outbound.Configuration) {
126129
c.Timeout = timeout
127130
}
128131
}
132+
133+
// WithFromEnv uses environment variables to configure the provider.
134+
//
135+
// Experimental: This feature is experimental and may change in future versions.
136+
//
137+
// Supported environment variables:
138+
// - OFREP_ENDPOINT: base URI for the OFREP service
139+
// - OFREP_TIMEOUT: timeout duration (e.g., "30s", "500ms")
140+
// - OFREP_API_KEY: API key for X-API-Key authentication
141+
// - OFREP_BEARER_TOKEN: token for Bearer authentication
142+
// - OFREP_HEADERS: comma-separated custom headers (e.g., "Key1=Value1,Key2=Value2")
143+
func WithFromEnv() func(*outbound.Configuration) {
144+
envHandlers := map[string]func(*outbound.Configuration, string){
145+
"OFREP_ENDPOINT": func(c *outbound.Configuration, v string) {
146+
WithBaseURI(v)(c)
147+
},
148+
"OFREP_TIMEOUT": func(c *outbound.Configuration, v string) {
149+
if t, err := time.ParseDuration(v); err == nil && t > 0 {
150+
WithTimeout(t)(c)
151+
return
152+
}
153+
// as the specification is not finalized, also support raw milliseconds
154+
t, err := strconv.Atoi(v)
155+
if err == nil && t > 0 {
156+
WithTimeout(time.Duration(t) * time.Millisecond)(c)
157+
}
158+
},
159+
"OFREP_API_KEY": func(c *outbound.Configuration, v string) {
160+
WithApiKeyAuth(v)(c)
161+
},
162+
"OFREP_BEARER_TOKEN": func(c *outbound.Configuration, v string) {
163+
WithBearerToken(v)(c)
164+
},
165+
"OFREP_HEADERS": func(c *outbound.Configuration, v string) {
166+
for pair := range strings.SplitSeq(v, ",") {
167+
kv := strings.SplitN(strings.TrimSpace(pair), "=", 2)
168+
if len(kv) == 2 {
169+
WithHeader(kv[0], kv[1])(c)
170+
}
171+
}
172+
},
173+
}
174+
return func(c *outbound.Configuration) {
175+
for key, handler := range envHandlers {
176+
if v := os.Getenv(key); v != "" {
177+
handler(c, v)
178+
}
179+
}
180+
}
181+
}

providers/ofrep/provider_test.go

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,3 +147,170 @@ func (r mockHandler) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
147147
r.t.Logf("error wriging bytes: %v", err)
148148
}
149149
}
150+
151+
func TestWithFromEnv(t *testing.T) {
152+
tests := []struct {
153+
name string
154+
envVars map[string]string
155+
initialConfig outbound.Configuration
156+
wantBaseURI string
157+
wantTimeout time.Duration
158+
wantHeaders map[string]string
159+
}{
160+
{
161+
name: "configure endpoint from env",
162+
envVars: map[string]string{
163+
"OFREP_ENDPOINT": "http://test.example.com",
164+
},
165+
initialConfig: outbound.Configuration{},
166+
wantBaseURI: "http://test.example.com",
167+
},
168+
{
169+
name: "configure timeout from env",
170+
envVars: map[string]string{
171+
"OFREP_TIMEOUT": "5s",
172+
},
173+
initialConfig: outbound.Configuration{},
174+
wantTimeout: 5 * time.Second,
175+
},
176+
{
177+
name: "configure timeout from env with raw milliseconds",
178+
envVars: map[string]string{
179+
"OFREP_TIMEOUT": "3000",
180+
},
181+
initialConfig: outbound.Configuration{},
182+
wantTimeout: 3 * time.Second,
183+
},
184+
{
185+
name: "configure timeout from env with invalid data",
186+
envVars: map[string]string{
187+
"OFREP_TIMEOUT": "s5s",
188+
},
189+
initialConfig: outbound.Configuration{Timeout: 33 * time.Second},
190+
wantTimeout: 33 * time.Second,
191+
},
192+
{
193+
name: "configure timeout from env with negative duration",
194+
envVars: map[string]string{
195+
"OFREP_TIMEOUT": "-5s",
196+
},
197+
initialConfig: outbound.Configuration{Timeout: 33 * time.Second},
198+
wantTimeout: 33 * time.Second,
199+
},
200+
{
201+
name: "configure timeout from env with negative milliseconds",
202+
envVars: map[string]string{
203+
"OFREP_TIMEOUT": "-5000",
204+
},
205+
initialConfig: outbound.Configuration{Timeout: 33 * time.Second},
206+
wantTimeout: 33 * time.Second,
207+
},
208+
{
209+
name: "ignore invalid timeout",
210+
envVars: map[string]string{
211+
"OFREP_TIMEOUT": "invalid",
212+
},
213+
initialConfig: outbound.Configuration{Timeout: 10 * time.Second},
214+
wantTimeout: 10 * time.Second,
215+
},
216+
{
217+
name: "configure api key from env",
218+
envVars: map[string]string{
219+
"OFREP_API_KEY": "test-api-key",
220+
},
221+
initialConfig: outbound.Configuration{},
222+
wantHeaders: map[string]string{
223+
"X-API-Key": "test-api-key",
224+
},
225+
},
226+
{
227+
name: "configure bearer token from env",
228+
envVars: map[string]string{
229+
"OFREP_BEARER_TOKEN": "test-token",
230+
},
231+
initialConfig: outbound.Configuration{},
232+
wantHeaders: map[string]string{
233+
"Authorization": "Bearer test-token",
234+
},
235+
},
236+
{
237+
name: "configure custom headers from env",
238+
envVars: map[string]string{
239+
"OFREP_HEADERS": "X-Custom-1=Value1,X-Custom-2=Value2",
240+
},
241+
initialConfig: outbound.Configuration{},
242+
wantHeaders: map[string]string{
243+
"X-Custom-1": "Value1",
244+
"X-Custom-2": "Value2",
245+
},
246+
},
247+
{
248+
name: "configure all options from env",
249+
envVars: map[string]string{
250+
"OFREP_ENDPOINT": "http://all.example.com",
251+
"OFREP_TIMEOUT": "3s",
252+
"OFREP_API_KEY": "all-key",
253+
"OFREP_HEADERS": "X-Test=TestValue",
254+
},
255+
initialConfig: outbound.Configuration{},
256+
wantBaseURI: "http://all.example.com",
257+
wantTimeout: 3 * time.Second,
258+
wantHeaders: map[string]string{
259+
"X-API-Key": "all-key",
260+
"X-Test": "TestValue",
261+
},
262+
},
263+
{
264+
name: "empty env variables do not override defaults",
265+
envVars: map[string]string{
266+
"OFREP_ENDPOINT": "",
267+
"OFREP_TIMEOUT": "",
268+
},
269+
initialConfig: outbound.Configuration{
270+
BaseURI: "http://default.example.com",
271+
Timeout: 15 * time.Second,
272+
},
273+
wantBaseURI: "http://default.example.com",
274+
wantTimeout: 15 * time.Second,
275+
},
276+
}
277+
278+
for _, tt := range tests {
279+
t.Run(tt.name, func(t *testing.T) {
280+
for key, value := range tt.envVars {
281+
t.Setenv(key, value)
282+
}
283+
284+
c := tt.initialConfig
285+
WithFromEnv()(&c)
286+
287+
if tt.wantBaseURI != "" && c.BaseURI != tt.wantBaseURI {
288+
t.Errorf("expected BaseURI %s, but got %s", tt.wantBaseURI, c.BaseURI)
289+
}
290+
291+
if tt.wantTimeout != 0 && c.Timeout != tt.wantTimeout {
292+
t.Errorf("expected Timeout %v, but got %v", tt.wantTimeout, c.Timeout)
293+
}
294+
295+
actualHeaders := make(map[string]string)
296+
for _, cb := range c.Callbacks {
297+
k, v := cb()
298+
actualHeaders[k] = v
299+
}
300+
301+
if tt.wantHeaders != nil {
302+
for expectedKey, expectedValue := range tt.wantHeaders {
303+
if actualValue, ok := actualHeaders[expectedKey]; !ok {
304+
t.Errorf("expected header %s not found", expectedKey)
305+
} else if actualValue != expectedValue {
306+
t.Errorf("expected %s=%s, but got %s=%s", expectedKey, expectedValue, expectedKey, actualValue)
307+
}
308+
}
309+
}
310+
311+
if len(tt.wantHeaders) == 0 && len(actualHeaders) != 0 {
312+
t.Errorf("expected no headers, but got %v", actualHeaders)
313+
}
314+
})
315+
}
316+
}

0 commit comments

Comments
 (0)