Skip to content
  •  
  •  
  •  
419 changes: 419 additions & 0 deletions .changelog/70EEF6C5-262D-4DDE-9B4A-B6ADDE267267.json

Large diffs are not rendered by default.

27 changes: 27 additions & 0 deletions aws/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,33 @@ type Config struct {
// This variable is sourced from environment variable AWS_RESPONSE_CHECKSUM_VALIDATION or
// the shared config profile attribute "response_checksum_validation".
ResponseChecksumValidation ResponseChecksumValidation

// ServiceOptions provides service specific configuration options that will be applied
// when constructing clients for specific services. Each callback function receives the service ID
// and the service's Options struct, allowing for dynamic configuration based on the service.
ServiceOptions []func(string, any)
}

// ApplyServiceOptions applies service specific options from the config to the given options struct.
// This function is intended to be used by service clients in their NewFromConfig functions.
func (c Config) ApplyServiceOptions(serviceID string, options any) {
for _, callback := range c.ServiceOptions {
callback(serviceID, options)
}
}

// WithServiceOptions adds service specific options to the config.
// This function can be chained with other config builder methods.
func (c *Config) WithServiceOptions(callbacks ...func(string, any)) *Config {
c.ServiceOptions = append(c.ServiceOptions, callbacks...)
return c
}

// WithRegion sets the region for the config.
// This function can be chained with other config builder methods.
func (c *Config) WithRegion(region string) *Config {
c.Region = region
return c
}

// NewConfig returns a new Config pointer that can be chained with builder
Expand Down
228 changes: 228 additions & 0 deletions aws/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
package aws

import (
"testing"
)

// Mock service options struct for testing
type MockServiceOptions struct {
Field1 bool
Field2 string
Field3 int
}

func TestWithServiceOptions(t *testing.T) {
cfg := NewConfig()

cfg = cfg.WithServiceOptions(func(serviceID string, opts any) {
if serviceID == "TestService" {
if mockOpts, ok := opts.(*MockServiceOptions); ok {
mockOpts.Field1 = true
mockOpts.Field2 = "test"
}
}
})

if cfg.ServiceOptions == nil {
t.Fatal("ServiceOptions should not be nil")
}

if len(cfg.ServiceOptions) != 1 {
t.Fatalf("Expected 1 callback, got %d", len(cfg.ServiceOptions))
}

mockOpts := &MockServiceOptions{}
cfg.ApplyServiceOptions("TestService", mockOpts)

if !mockOpts.Field1 {
t.Error("Field1 should be true")
}

if mockOpts.Field2 != "test" {
t.Errorf("Field2 should be 'test', got '%s'", mockOpts.Field2)
}
}

func TestWithServiceOptionsMultiple(t *testing.T) {
cfg := NewConfig()

cfg = cfg.WithServiceOptions(
func(serviceID string, opts any) {
if serviceID == "Service1" {
if mockOpts, ok := opts.(*MockServiceOptions); ok {
mockOpts.Field1 = true
}
}
},
func(serviceID string, opts any) {
if serviceID == "Service2" {
if mockOpts, ok := opts.(*MockServiceOptions); ok {
mockOpts.Field2 = "test"
}
}
},
)

if len(cfg.ServiceOptions) != 2 {
t.Fatalf("Expected 2 callbacks, got %d", len(cfg.ServiceOptions))
}

mockOpts1 := &MockServiceOptions{}
cfg.ApplyServiceOptions("Service1", mockOpts1)

if !mockOpts1.Field1 {
t.Error("Service1 Field1 should be true")
}

if mockOpts1.Field2 != "" {
t.Errorf("Service1 Field2 should be empty, got '%s'", mockOpts1.Field2)
}

mockOpts2 := &MockServiceOptions{}
cfg.ApplyServiceOptions("Service2", mockOpts2)

if mockOpts2.Field1 {
t.Error("Service2 Field1 should be false")
}

if mockOpts2.Field2 != "test" {
t.Errorf("Service2 Field2 should be 'test', got '%s'", mockOpts2.Field2)
}
}

func TestWithServiceOptionsMultipleServiceIDs(t *testing.T) {
cfg := NewConfig()

cfg = cfg.WithServiceOptions(func(serviceID string, opts any) {
if mockOpts, ok := opts.(*MockServiceOptions); ok {
switch serviceID {
case "Service1":
mockOpts.Field1 = true
mockOpts.Field2 = "service1"
case "Service2":
mockOpts.Field1 = false
mockOpts.Field2 = "service2"
case "Service3":
mockOpts.Field3 = 42
}
}
})

if len(cfg.ServiceOptions) != 1 {
t.Fatalf("Expected 1 callback, got %d", len(cfg.ServiceOptions))
}

mockOpts1 := &MockServiceOptions{}
cfg.ApplyServiceOptions("Service1", mockOpts1)

if !mockOpts1.Field1 {
t.Error("Service1 Field1 should be true")
}
if mockOpts1.Field2 != "service1" {
t.Errorf("Service1 Field2 should be 'service1', got '%s'", mockOpts1.Field2)
}
if mockOpts1.Field3 != 0 {
t.Errorf("Service1 Field3 should be 0, got %d", mockOpts1.Field3)
}

mockOpts2 := &MockServiceOptions{}
cfg.ApplyServiceOptions("Service2", mockOpts2)

if mockOpts2.Field1 {
t.Error("Service2 Field1 should be false")
}
if mockOpts2.Field2 != "service2" {
t.Errorf("Service2 Field2 should be 'service2', got '%s'", mockOpts2.Field2)
}
if mockOpts2.Field3 != 0 {
t.Errorf("Service2 Field3 should be 0, got %d", mockOpts2.Field3)
}

mockOpts3 := &MockServiceOptions{}
cfg.ApplyServiceOptions("Service3", mockOpts3)

if mockOpts3.Field1 {
t.Error("Service3 Field1 should be false")
}
if mockOpts3.Field2 != "" {
t.Errorf("Service3 Field2 should be empty, got '%s'", mockOpts3.Field2)
}
if mockOpts3.Field3 != 42 {
t.Errorf("Service3 Field3 should be 42, got %d", mockOpts3.Field3)
}
}

func TestApplyServiceOptionsNonExistent(t *testing.T) {
cfg := NewConfig()

mockOpts := &MockServiceOptions{}

cfg.ApplyServiceOptions("NonExistentService", mockOpts)

if mockOpts.Field1 || mockOpts.Field2 != "" || mockOpts.Field3 != 0 {
t.Error("Options should not be modified for non-existent service")
}
}

func TestTypeAssertionFailure(t *testing.T) {
cfg := NewConfig()

cfg = cfg.WithServiceOptions(func(serviceID string, opts any) {
if serviceID == "TestService" {
if mockOpts, ok := opts.(*MockServiceOptions); ok {
mockOpts.Field1 = true
}
}
})

differentOpts := &struct{ Field string }{Field: "test"}
cfg.ApplyServiceOptions("TestService", differentOpts)

if differentOpts.Field != "test" {
t.Error("Different options should not be modified")
}
}

func TestChaining(t *testing.T) {
cfg := NewConfig()

cfg = cfg.WithRegion("us-west-2").
WithServiceOptions(
func(serviceID string, opts any) {
if serviceID == "Service1" {
if mockOpts, ok := opts.(*MockServiceOptions); ok {
mockOpts.Field1 = true
}
}
},
func(serviceID string, opts any) {
if serviceID == "Service2" {
if mockOpts, ok := opts.(*MockServiceOptions); ok {
mockOpts.Field2 = "chained"
}
}
},
)

if cfg.Region != "us-west-2" {
t.Errorf("Expected region 'us-west-2', got '%s'", cfg.Region)
}

if len(cfg.ServiceOptions) != 2 {
t.Fatalf("Expected 2 callbacks, got %d", len(cfg.ServiceOptions))
}

mockOpts1 := &MockServiceOptions{}
cfg.ApplyServiceOptions("Service1", mockOpts1)

if !mockOpts1.Field1 {
t.Error("Service1 Field1 should be true")
}

mockOpts2 := &MockServiceOptions{}
cfg.ApplyServiceOptions("Service2", mockOpts2)

if mockOpts2.Field2 != "chained" {
t.Errorf("Service2 Field2 should be 'chained', got '%s'", mockOpts2.Field2)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -728,6 +728,7 @@ private void writeAwsConfigConstructor(Model model, ServiceShape service, GoWrit
writer.write("$L(cfg, &opts)", awsResolverFunction.get());
}

writer.write("cfg.ApplyServiceOptions(ServiceID, &opts)");
writer.write("return New(opts, optFns...)");
});
writer.write("");
Expand Down
1 change: 1 addition & 0 deletions service/accessanalyzer/api_client.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 7 additions & 5 deletions service/account/api_client.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions service/acm/api_client.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions service/acmpca/api_client.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions service/aiops/api_client.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions service/amp/api_client.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions service/amplify/api_client.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions service/amplifybackend/api_client.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions service/amplifyuibuilder/api_client.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions service/apigateway/api_client.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions service/apigatewaymanagementapi/api_client.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions service/apigatewayv2/api_client.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading