Skip to content

Commit 3a96466

Browse files
author
wood
committed
feat: option-to-pass-optional-resty-or-http-client
1 parent ac2e06e commit 3a96466

File tree

3 files changed

+207
-1
lines changed

3 files changed

+207
-1
lines changed

client.go

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ import (
44
"context"
55
"fmt"
66
"log/slog"
7+
"net/http"
8+
"reflect"
9+
"runtime"
710
"strings"
811
"sync/atomic"
912
"time"
@@ -34,6 +37,7 @@ type Client struct {
3437
defaultFlagHandler func(string) (Flag, error)
3538

3639
client *resty.Client
40+
httpClient *http.Client
3741
ctxLocalEval context.Context
3842
ctxAnalytics context.Context
3943
log *slog.Logger
@@ -52,12 +56,42 @@ func GetEvaluationContextFromCtx(ctx context.Context) (ec EvaluationContext, ok
5256
return ec, ok
5357
}
5458

59+
func getOptionQualifiedName(opt Option) string {
60+
return runtime.FuncForPC(reflect.ValueOf(opt).Pointer()).Name()
61+
}
62+
63+
func isClientOption(name string) bool {
64+
return strings.Contains(name, OptionWithHTTPClient) || strings.Contains(name, OptionWithRestyClient)
65+
}
66+
5567
// NewClient creates instance of Client with given configuration.
5668
func NewClient(apiKey string, options ...Option) *Client {
5769
c := &Client{
5870
apiKey: apiKey,
5971
config: defaultConfig(),
60-
client: resty.New(),
72+
}
73+
74+
for _, opt := range options {
75+
name := getOptionQualifiedName(opt)
76+
if isClientOption(name) {
77+
opt(c)
78+
}
79+
}
80+
81+
if c.client == nil {
82+
if c.httpClient != nil {
83+
c.client = resty.NewWithClient(c.httpClient)
84+
} else {
85+
c.client = resty.New()
86+
}
87+
}
88+
89+
for _, opt := range options {
90+
name := getOptionQualifiedName(opt)
91+
if isClientOption(name) {
92+
continue
93+
}
94+
opt(c)
6195
}
6296

6397
c.client.SetHeaders(map[string]string{
@@ -72,6 +106,16 @@ func NewClient(apiKey string, options ...Option) *Client {
72106
opt(c)
73107
}
74108
}
109+
110+
// If a resty custom client has been provided, client is already set - otherwise we use a custom http client or default to a resty
111+
if c.client == nil {
112+
if c.httpClient != nil {
113+
c.client = resty.NewWithClient(c.httpClient)
114+
} else {
115+
c.client = resty.New()
116+
}
117+
}
118+
75119
c.client = c.client.
76120
SetLogger(newSlogToRestyAdapter(c.log)).
77121
OnBeforeRequest(newRestyLogRequestMiddleware(c.log)).

client_test.go

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515

1616
flagsmith "github.com/Flagsmith/flagsmith-go-client/v4"
1717
"github.com/Flagsmith/flagsmith-go-client/v4/fixtures"
18+
"github.com/go-resty/resty/v2"
1819
"github.com/stretchr/testify/assert"
1920
)
2021

@@ -1019,3 +1020,140 @@ type writerFunc func(p []byte) (n int, err error)
10191020
func (f writerFunc) Write(p []byte) (n int, err error) {
10201021
return f(p)
10211022
}
1023+
1024+
// Helper functions to implement a header interceptor
1025+
func roundTripperWithHeader(key, value string) http.RoundTripper {
1026+
return &injectHeaderTransport{key: key, value: value}
1027+
}
1028+
1029+
type injectHeaderTransport struct {
1030+
key string
1031+
value string
1032+
}
1033+
1034+
func (t *injectHeaderTransport) RoundTrip(req *http.Request) (*http.Response, error) {
1035+
req.Header.Set(t.key, t.value)
1036+
return http.DefaultTransport.RoundTrip(req)
1037+
}
1038+
1039+
func TestCustomHTTPClientIsUsed(t *testing.T) {
1040+
ctx := context.Background()
1041+
1042+
hasCustomHeader := false
1043+
server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
1044+
assert.Equal(t, "/api/v1/flags/", req.URL.Path)
1045+
assert.Equal(t, fixtures.EnvironmentAPIKey, req.Header.Get("x-Environment-Key"))
1046+
if req.Header.Get("X-Test-Client") == "http" {
1047+
hasCustomHeader = true
1048+
}
1049+
rw.Header().Set("Content-Type", "application/json")
1050+
rw.WriteHeader(http.StatusOK)
1051+
_, err := io.WriteString(rw, fixtures.FlagsJson)
1052+
assert.NoError(t, err)
1053+
}))
1054+
defer server.Close()
1055+
1056+
customClient := &http.Client{
1057+
Transport: roundTripperWithHeader("X-Test-Client", "http"),
1058+
}
1059+
1060+
client := flagsmith.NewClient(fixtures.EnvironmentAPIKey,
1061+
flagsmith.WithHTTPClient(customClient),
1062+
flagsmith.WithBaseURL(server.URL+"/api/v1/"))
1063+
1064+
flags, err := client.GetFlags(ctx, nil)
1065+
assert.Equal(t, 1, len(flags.AllFlags()))
1066+
assert.NoError(t, err)
1067+
assert.True(t, hasCustomHeader, "Expected http header")
1068+
flag, err := flags.GetFlag(fixtures.Feature1Name)
1069+
assert.NoError(t, err)
1070+
assert.Equal(t, fixtures.Feature1Value, flag.Value)
1071+
}
1072+
1073+
func TestCustomRestyClientIsUsed(t *testing.T) {
1074+
ctx := context.Background()
1075+
1076+
hasCustomHeader := false
1077+
server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
1078+
if req.Header.Get("X-Custom-Test-Header") == "resty" {
1079+
hasCustomHeader = true
1080+
}
1081+
rw.Header().Set("Content-Type", "application/json")
1082+
rw.WriteHeader(http.StatusOK)
1083+
_, err := io.WriteString(rw, fixtures.FlagsJson)
1084+
assert.NoError(t, err)
1085+
}))
1086+
defer server.Close()
1087+
1088+
restyClient := resty.New().
1089+
SetHeader("X-Custom-Test-Header", "resty")
1090+
1091+
client := flagsmith.NewClient(fixtures.EnvironmentAPIKey,
1092+
flagsmith.WithRestyClient(restyClient),
1093+
flagsmith.WithBaseURL(server.URL+"/api/v1/"))
1094+
1095+
flags, err := client.GetFlags(ctx, nil)
1096+
assert.NoError(t, err)
1097+
assert.Equal(t, 1, len(flags.AllFlags()))
1098+
assert.True(t, hasCustomHeader, "Expected custom resty header")
1099+
}
1100+
1101+
func TestRestyClientOverridesHTTPClient(t *testing.T) {
1102+
ctx := context.Background()
1103+
1104+
var customHeader string
1105+
server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
1106+
customHeader = req.Header.Get("X-Test-Client")
1107+
rw.Header().Set("Content-Type", "application/json")
1108+
rw.WriteHeader(http.StatusOK)
1109+
_, err := io.WriteString(rw, fixtures.FlagsJson)
1110+
assert.NoError(t, err)
1111+
}))
1112+
defer server.Close()
1113+
1114+
httpClient := &http.Client{
1115+
Transport: roundTripperWithHeader("X-Test-Client", "http"),
1116+
}
1117+
1118+
restyClient := resty.New().
1119+
SetHeader("X-Test-Client", "resty")
1120+
1121+
client := flagsmith.NewClient(fixtures.EnvironmentAPIKey,
1122+
flagsmith.WithHTTPClient(httpClient),
1123+
flagsmith.WithRestyClient(restyClient),
1124+
flagsmith.WithBaseURL(server.URL+"/api/v1/"))
1125+
1126+
flags, err := client.GetFlags(ctx, nil)
1127+
1128+
assert.NoError(t, err)
1129+
assert.Equal(t, 1, len(flags.AllFlags()))
1130+
assert.Equal(t, "resty", customHeader, "Expected resty header")
1131+
}
1132+
1133+
func TestDefaultRestyClientIsUsed(t *testing.T) {
1134+
ctx := context.Background()
1135+
1136+
serverCalled := false
1137+
1138+
server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
1139+
serverCalled = true
1140+
1141+
assert.Equal(t, "/api/v1/flags/", req.URL.Path)
1142+
assert.Equal(t, fixtures.EnvironmentAPIKey, req.Header.Get("x-Environment-Key"))
1143+
1144+
rw.Header().Set("Content-Type", "application/json")
1145+
rw.WriteHeader(http.StatusOK)
1146+
_, err := io.WriteString(rw, fixtures.FlagsJson)
1147+
assert.NoError(t, err)
1148+
}))
1149+
defer server.Close()
1150+
1151+
client := flagsmith.NewClient(fixtures.EnvironmentAPIKey,
1152+
flagsmith.WithBaseURL(server.URL+"/api/v1/"))
1153+
1154+
flags, err := client.GetFlags(ctx, nil)
1155+
1156+
assert.NoError(t, err)
1157+
assert.True(t, serverCalled, "Expected server to be")
1158+
assert.Equal(t, 1, len(flags.AllFlags()))
1159+
}

options.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,18 @@ package flagsmith
22

33
import (
44
"context"
5+
"net/http"
56
"strings"
67
"time"
78

89
"log/slog"
10+
11+
"github.com/go-resty/resty/v2"
12+
)
13+
14+
const (
15+
OptionWithHTTPClient = "WithHTTPClient"
16+
OptionWithRestyClient = "WithRestyClient"
917
)
1018

1119
type Option func(c *Client)
@@ -165,3 +173,19 @@ func WithPolling() Option {
165173
c.config.polling = true
166174
}
167175
}
176+
177+
func WithHTTPClient(httpClient *http.Client) Option {
178+
return func(c *Client) {
179+
if httpClient != nil {
180+
c.httpClient = httpClient
181+
}
182+
}
183+
}
184+
185+
func WithRestyClient(restyClient *resty.Client) Option {
186+
return func(c *Client) {
187+
if restyClient != nil {
188+
c.client = restyClient
189+
}
190+
}
191+
}

0 commit comments

Comments
 (0)