Skip to content

Commit 76aa8d7

Browse files
feat: add support for per service options to Config (#3145)
1 parent 8afe327 commit 76aa8d7

File tree

434 files changed

+4138
-427
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

434 files changed

+4138
-427
lines changed

.changelog/70EEF6C5-262D-4DDE-9B4A-B6ADDE267267.json

Lines changed: 419 additions & 0 deletions
Large diffs are not rendered by default.

aws/config.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,11 @@ type Config struct {
199199

200200
// Priority list of preferred auth scheme IDs.
201201
AuthSchemePreference []string
202+
203+
// ServiceOptions provides service specific configuration options that will be applied
204+
// when constructing clients for specific services. Each callback function receives the service ID
205+
// and the service's Options struct, allowing for dynamic configuration based on the service.
206+
ServiceOptions []func(string, any)
202207
}
203208

204209
// NewConfig returns a new Config pointer that can be chained with builder

aws/config_test.go

Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
package aws
2+
3+
import (
4+
"testing"
5+
)
6+
7+
// Mock service options struct for testing
8+
type MockServiceOptions struct {
9+
Field1 bool
10+
Field2 string
11+
Field3 int
12+
}
13+
14+
func TestWithServiceOptions(t *testing.T) {
15+
cfg := NewConfig()
16+
17+
// Add ServiceOptions directly to test the field
18+
cfg.ServiceOptions = []func(string, any){
19+
func(serviceID string, opts any) {
20+
if serviceID == "TestService" {
21+
if mockOpts, ok := opts.(*MockServiceOptions); ok {
22+
mockOpts.Field1 = true
23+
mockOpts.Field2 = "test"
24+
}
25+
}
26+
},
27+
}
28+
29+
if cfg.ServiceOptions == nil {
30+
t.Fatal("ServiceOptions should not be nil")
31+
}
32+
33+
if len(cfg.ServiceOptions) != 1 {
34+
t.Fatalf("Expected 1 callback, got %d", len(cfg.ServiceOptions))
35+
}
36+
37+
mockOpts := &MockServiceOptions{}
38+
for _, callback := range cfg.ServiceOptions {
39+
callback("TestService", mockOpts)
40+
}
41+
42+
if !mockOpts.Field1 {
43+
t.Error("Field1 should be true")
44+
}
45+
46+
if mockOpts.Field2 != "test" {
47+
t.Errorf("Field2 should be 'test', got '%s'", mockOpts.Field2)
48+
}
49+
}
50+
51+
func TestWithServiceOptionsMultiple(t *testing.T) {
52+
cfg := NewConfig()
53+
54+
// Add ServiceOptions directly to test the field
55+
cfg.ServiceOptions = []func(string, any){
56+
func(serviceID string, opts any) {
57+
if serviceID == "Service1" {
58+
if mockOpts, ok := opts.(*MockServiceOptions); ok {
59+
mockOpts.Field1 = true
60+
}
61+
}
62+
},
63+
func(serviceID string, opts any) {
64+
if serviceID == "Service2" {
65+
if mockOpts, ok := opts.(*MockServiceOptions); ok {
66+
mockOpts.Field2 = "test"
67+
}
68+
}
69+
},
70+
}
71+
72+
if len(cfg.ServiceOptions) != 2 {
73+
t.Fatalf("Expected 2 callbacks, got %d", len(cfg.ServiceOptions))
74+
}
75+
76+
mockOpts1 := &MockServiceOptions{}
77+
for _, callback := range cfg.ServiceOptions {
78+
callback("Service1", mockOpts1)
79+
}
80+
81+
if !mockOpts1.Field1 {
82+
t.Error("Service1 Field1 should be true")
83+
}
84+
85+
if mockOpts1.Field2 != "" {
86+
t.Errorf("Service1 Field2 should be empty, got '%s'", mockOpts1.Field2)
87+
}
88+
89+
mockOpts2 := &MockServiceOptions{}
90+
for _, callback := range cfg.ServiceOptions {
91+
callback("Service2", mockOpts2)
92+
}
93+
94+
if mockOpts2.Field1 {
95+
t.Error("Service2 Field1 should be false")
96+
}
97+
98+
if mockOpts2.Field2 != "test" {
99+
t.Errorf("Service2 Field2 should be 'test', got '%s'", mockOpts2.Field2)
100+
}
101+
}
102+
103+
func TestWithServiceOptionsMultipleServiceIDs(t *testing.T) {
104+
cfg := NewConfig()
105+
106+
// Add ServiceOptions directly to test the field
107+
cfg.ServiceOptions = []func(string, any){
108+
func(serviceID string, opts any) {
109+
if mockOpts, ok := opts.(*MockServiceOptions); ok {
110+
switch serviceID {
111+
case "Service1":
112+
mockOpts.Field1 = true
113+
mockOpts.Field2 = "service1"
114+
case "Service2":
115+
mockOpts.Field1 = false
116+
mockOpts.Field2 = "service2"
117+
case "Service3":
118+
mockOpts.Field3 = 42
119+
}
120+
}
121+
},
122+
}
123+
124+
if len(cfg.ServiceOptions) != 1 {
125+
t.Fatalf("Expected 1 callback, got %d", len(cfg.ServiceOptions))
126+
}
127+
128+
mockOpts1 := &MockServiceOptions{}
129+
for _, callback := range cfg.ServiceOptions {
130+
callback("Service1", mockOpts1)
131+
}
132+
133+
if !mockOpts1.Field1 {
134+
t.Error("Service1 Field1 should be true")
135+
}
136+
if mockOpts1.Field2 != "service1" {
137+
t.Errorf("Service1 Field2 should be 'service1', got '%s'", mockOpts1.Field2)
138+
}
139+
if mockOpts1.Field3 != 0 {
140+
t.Errorf("Service1 Field3 should be 0, got %d", mockOpts1.Field3)
141+
}
142+
143+
mockOpts2 := &MockServiceOptions{}
144+
for _, callback := range cfg.ServiceOptions {
145+
callback("Service2", mockOpts2)
146+
}
147+
148+
if mockOpts2.Field1 {
149+
t.Error("Service2 Field1 should be false")
150+
}
151+
if mockOpts2.Field2 != "service2" {
152+
t.Errorf("Service2 Field2 should be 'service2', got '%s'", mockOpts2.Field2)
153+
}
154+
if mockOpts2.Field3 != 0 {
155+
t.Errorf("Service2 Field3 should be 0, got %d", mockOpts2.Field3)
156+
}
157+
158+
mockOpts3 := &MockServiceOptions{}
159+
for _, callback := range cfg.ServiceOptions {
160+
callback("Service3", mockOpts3)
161+
}
162+
163+
if mockOpts3.Field1 {
164+
t.Error("Service3 Field1 should be false")
165+
}
166+
if mockOpts3.Field2 != "" {
167+
t.Errorf("Service3 Field2 should be empty, got '%s'", mockOpts3.Field2)
168+
}
169+
if mockOpts3.Field3 != 42 {
170+
t.Errorf("Service3 Field3 should be 42, got %d", mockOpts3.Field3)
171+
}
172+
}
173+
174+
func TestApplyServiceOptionsNonExistent(t *testing.T) {
175+
mockOpts := &MockServiceOptions{}
176+
177+
// No service options configured, so no callbacks to execute
178+
if mockOpts.Field1 || mockOpts.Field2 != "" || mockOpts.Field3 != 0 {
179+
t.Error("Options should not be modified for non-existent service")
180+
}
181+
}
182+
183+
func TestTypeAssertionFailure(t *testing.T) {
184+
cfg := NewConfig()
185+
186+
// Add ServiceOptions directly to test the field
187+
cfg.ServiceOptions = []func(string, any){
188+
func(serviceID string, opts any) {
189+
if serviceID == "TestService" {
190+
if mockOpts, ok := opts.(*MockServiceOptions); ok {
191+
mockOpts.Field1 = true
192+
}
193+
}
194+
},
195+
}
196+
197+
differentOpts := &struct{ Field string }{Field: "test"}
198+
for _, callback := range cfg.ServiceOptions {
199+
callback("TestService", differentOpts)
200+
}
201+
202+
if differentOpts.Field != "test" {
203+
t.Error("Different options should not be modified")
204+
}
205+
}
206+
207+
func TestChaining(t *testing.T) {
208+
cfg := NewConfig()
209+
210+
cfg.ServiceOptions = []func(string, any){
211+
func(serviceID string, opts any) {
212+
if serviceID == "Service1" {
213+
if mockOpts, ok := opts.(*MockServiceOptions); ok {
214+
mockOpts.Field1 = true
215+
}
216+
}
217+
},
218+
func(serviceID string, opts any) {
219+
if serviceID == "Service2" {
220+
if mockOpts, ok := opts.(*MockServiceOptions); ok {
221+
mockOpts.Field2 = "chained"
222+
}
223+
}
224+
},
225+
}
226+
227+
if len(cfg.ServiceOptions) != 2 {
228+
t.Fatalf("Expected 2 callbacks, got %d", len(cfg.ServiceOptions))
229+
}
230+
231+
mockOpts1 := &MockServiceOptions{}
232+
for _, callback := range cfg.ServiceOptions {
233+
callback("Service1", mockOpts1)
234+
}
235+
236+
if !mockOpts1.Field1 {
237+
t.Error("Service1 Field1 should be true")
238+
}
239+
240+
mockOpts2 := &MockServiceOptions{}
241+
for _, callback := range cfg.ServiceOptions {
242+
callback("Service2", mockOpts2)
243+
}
244+
245+
if mockOpts2.Field2 != "chained" {
246+
t.Errorf("Service2 Field2 should be 'chained', got '%s'", mockOpts2.Field2)
247+
}
248+
}

codegen/smithy-aws-go-codegen/src/main/java/software/amazon/smithy/aws/go/codegen/AddAwsConfigFields.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -750,7 +750,14 @@ private void writeAwsConfigConstructor(Model model, ServiceShape service, GoWrit
750750
writer.write("$L(cfg, &opts)", awsResolverFunction.get());
751751
}
752752

753-
writer.write("return New(opts, optFns...)");
753+
writer.write("return New(opts, func(o *Options) {");
754+
writer.write(" for _, opt := range cfg.ServiceOptions {");
755+
writer.write(" opt(ServiceID, o)");
756+
writer.write(" }");
757+
writer.write(" for _, opt := range optFns {");
758+
writer.write(" opt(o)");
759+
writer.write(" }");
760+
writer.write("})");
754761
});
755762
writer.write("");
756763
}

config/config.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,9 @@ var defaultAWSConfigResolvers = []awsConfigResolver{
9393
resolveInterceptors,
9494

9595
resolveAuthSchemePreference,
96+
97+
// Sets the ServiceOptions if present in LoadOptions
98+
resolveServiceOptions,
9699
}
97100

98101
// A Config represents a generic configuration value or set of values. This type

config/load_options.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,11 @@ type LoadOptions struct {
235235

236236
// Priority list of preferred auth scheme names (e.g. sigv4a).
237237
AuthSchemePreference []string
238+
239+
// ServiceOptions provides service specific configuration options that will be applied
240+
// when constructing clients for specific services. Each callback function receives the service ID
241+
// and the service's Options struct, allowing for dynamic configuration based on the service.
242+
ServiceOptions []func(string, any)
238243
}
239244

240245
func (o LoadOptions) getDefaultsMode(ctx context.Context) (aws.DefaultsMode, bool, error) {
@@ -314,6 +319,10 @@ func (o LoadOptions) getBaseEndpoint(context.Context) (string, bool, error) {
314319
return o.BaseEndpoint, o.BaseEndpoint != "", nil
315320
}
316321

322+
func (o LoadOptions) getServiceOptions(context.Context) ([]func(string, any), bool, error) {
323+
return o.ServiceOptions, len(o.ServiceOptions) > 0, nil
324+
}
325+
317326
// GetServiceBaseEndpoint satisfies (internal/configsources).ServiceBaseEndpointProvider.
318327
//
319328
// The sdkID value is unused because LoadOptions only supports setting a GLOBAL
@@ -1215,6 +1224,15 @@ func WithBaseEndpoint(v string) LoadOptionsFunc {
12151224
}
12161225
}
12171226

1227+
// WithServiceOptions is a helper function to construct functional options
1228+
// that sets ServiceOptions on config's LoadOptions.
1229+
func WithServiceOptions(callbacks ...func(string, any)) LoadOptionsFunc {
1230+
return func(o *LoadOptions) error {
1231+
o.ServiceOptions = append(o.ServiceOptions, callbacks...)
1232+
return nil
1233+
}
1234+
}
1235+
12181236
// WithBeforeExecution adds the BeforeExecutionInterceptor to config.
12191237
func WithBeforeExecution(i smithyhttp.BeforeExecutionInterceptor) LoadOptionsFunc {
12201238
return func(o *LoadOptions) error {

config/provider.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -768,3 +768,19 @@ func getAuthSchemePreference(ctx context.Context, configs configs) ([]string, bo
768768
}
769769
return nil, false
770770
}
771+
772+
type serviceOptionsProvider interface {
773+
getServiceOptions(ctx context.Context) ([]func(string, any), bool, error)
774+
}
775+
776+
func getServiceOptions(ctx context.Context, configs configs) (v []func(string, any), found bool, err error) {
777+
for _, c := range configs {
778+
if p, ok := c.(serviceOptionsProvider); ok {
779+
v, found, err = p.getServiceOptions(ctx)
780+
if err != nil || found {
781+
break
782+
}
783+
}
784+
}
785+
return v, found, err
786+
}

config/resolve.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -429,3 +429,16 @@ func resolveAuthSchemePreference(ctx context.Context, cfg *aws.Config, configs c
429429
}
430430
return nil
431431
}
432+
433+
func resolveServiceOptions(ctx context.Context, cfg *aws.Config, configs configs) error {
434+
serviceOptions, found, err := getServiceOptions(ctx, configs)
435+
if err != nil {
436+
return err
437+
}
438+
if !found {
439+
return nil
440+
}
441+
442+
cfg.ServiceOptions = serviceOptions
443+
return nil
444+
}

internal/kitchensinktest/api_client.go

Lines changed: 8 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/protocoltest/awsrestjson/api_client.go

Lines changed: 8 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)