Skip to content

Commit 960f73a

Browse files
committed
Add ability to override call config in interceptor
Interceptors now have a new hook, modifyBeforeCall that accepts the ApiOperation being called, input of the call, and ClientConfig. It is expected to return the same ClientConfig or an updated ClientConfig using ClientConfig#withRequestOverride. This allows for much more dynamic calls with clients, like changing the protocol per/call, applying plugins per/call, and more.
1 parent 3d96135 commit 960f73a

File tree

6 files changed

+214
-21
lines changed

6 files changed

+214
-21
lines changed

client/client-core/src/main/java/software/amazon/smithy/java/client/core/Client.java

Lines changed: 24 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import software.amazon.smithy.java.client.core.auth.scheme.AuthScheme;
1515
import software.amazon.smithy.java.client.core.auth.scheme.AuthSchemeResolver;
1616
import software.amazon.smithy.java.client.core.endpoint.EndpointResolver;
17+
import software.amazon.smithy.java.client.core.interceptors.CallHook;
1718
import software.amazon.smithy.java.client.core.interceptors.ClientInterceptor;
1819
import software.amazon.smithy.java.client.core.plugins.DefaultPlugin;
1920
import software.amazon.smithy.java.context.Context;
@@ -75,20 +76,26 @@ protected <I extends SerializableStruct, O extends SerializableStruct> Completab
7576
ApiOperation<I, O> operation,
7677
RequestOverrideConfig overrideConfig
7778
) {
78-
// Create a copy of the type registry that adds the errors this operation can encounter.
79-
TypeRegistry operationRegistry = TypeRegistry.compose(operation.errorRegistry(), typeRegistry);
80-
81-
ClientPipeline<?, ?> callPipeline;
82-
ClientInterceptor callInterceptor;
83-
IdentityResolvers callIdentityResolvers;
84-
ClientConfig callConfig;
85-
if (overrideConfig == null) {
86-
callConfig = config;
87-
callPipeline = pipeline;
88-
callInterceptor = interceptor;
89-
callIdentityResolvers = identityResolvers;
90-
} else {
91-
callConfig = config.withRequestOverride(overrideConfig);
79+
ClientPipeline<?, ?> callPipeline = pipeline;
80+
IdentityResolvers callIdentityResolvers = identityResolvers;
81+
ClientConfig callConfig = config;
82+
ClientInterceptor callInterceptor = interceptor;
83+
84+
// Apply the given override before potentially applying interceptor based overrides.
85+
var needsRebuild = false;
86+
if (overrideConfig != null) {
87+
needsRebuild = true;
88+
callConfig = callConfig.withRequestOverride(overrideConfig);
89+
callInterceptor = ClientInterceptor.chain(callConfig.interceptors());
90+
}
91+
92+
var updatedConfig = callInterceptor.modifyBeforeCall(new CallHook<>(operation, callConfig, input));
93+
if (updatedConfig != callConfig) {
94+
needsRebuild = true;
95+
callConfig = updatedConfig;
96+
}
97+
98+
if (needsRebuild) {
9299
callPipeline = ClientPipeline.of(callConfig.protocol(), callConfig.transport());
93100
callInterceptor = ClientInterceptor.chain(callConfig.interceptors());
94101
callIdentityResolvers = IdentityResolvers.of(callConfig.identityResolvers());
@@ -97,16 +104,12 @@ protected <I extends SerializableStruct, O extends SerializableStruct> Completab
97104
var callBuilder = ClientCall.<I, O>builder();
98105
callBuilder.input = input;
99106
callBuilder.operation = operation;
100-
callBuilder.endpointResolver = callConfig.endpointResolver();
101-
callBuilder.context = Context.modifiableCopy(callConfig.context());
102107
callBuilder.interceptor = callInterceptor;
103-
callBuilder.supportedAuthSchemes.addAll(callConfig.supportedAuthSchemes());
104-
callBuilder.authSchemeResolver = callConfig.authSchemeResolver();
105108
callBuilder.identityResolvers = callIdentityResolvers;
106-
callBuilder.typeRegistry = operationRegistry;
109+
// Create a copy of the type registry that adds the errors this operation can encounter.
110+
callBuilder.typeRegistry = TypeRegistry.compose(operation.errorRegistry(), typeRegistry);;
107111
callBuilder.retryStrategy = retryStrategy;
108-
callBuilder.retryScope = callConfig.retryScope();
109-
112+
callBuilder.withConfig(callConfig);
110113
return callPipeline.send(callBuilder.build());
111114
}
112115

client/client-core/src/main/java/software/amazon/smithy/java/client/core/ClientCall.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,27 @@ static final class Builder<I extends SerializableStruct, O extends SerializableS
110110

111111
private Builder() {}
112112

113+
void withConfig(ClientConfig callConfig) {
114+
context = Context.modifiableCopy(callConfig.context());
115+
supportedAuthSchemes.addAll(callConfig.supportedAuthSchemes());
116+
117+
if (callConfig.endpointResolver() != null) {
118+
endpointResolver = callConfig.endpointResolver();
119+
}
120+
121+
if (callConfig.authSchemeResolver() != null) {
122+
authSchemeResolver = callConfig.authSchemeResolver();
123+
}
124+
125+
if (callConfig.retryScope() != null) {
126+
retryScope = callConfig.retryScope();
127+
}
128+
129+
if (callConfig.retryStrategy() != null) {
130+
retryStrategy = callConfig.retryStrategy();
131+
}
132+
}
133+
113134
ClientCall<I, O> build() {
114135
return new ClientCall<>(this);
115136
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package software.amazon.smithy.java.client.core.interceptors;
7+
8+
import software.amazon.smithy.java.client.core.ClientConfig;
9+
import software.amazon.smithy.java.client.core.RequestOverrideConfig;
10+
import software.amazon.smithy.java.core.schema.ApiOperation;
11+
import software.amazon.smithy.java.core.schema.SerializableStruct;
12+
13+
/**
14+
* Hook to add request level overrides to client calls using {@link RequestOverrideConfig}.
15+
*
16+
* @param <I> Input shape.
17+
* @param <O> Output shape.
18+
*/
19+
public record CallHook<I extends SerializableStruct, O extends SerializableStruct>(
20+
ApiOperation<I, O> operation,
21+
ClientConfig config,
22+
I input) {
23+
/**
24+
* Get the API operation being called.
25+
*
26+
* @return the operation being called.
27+
*/
28+
@Override
29+
public ApiOperation<I, O> operation() {
30+
return operation;
31+
}
32+
33+
/**
34+
* Get the client config of the hook.
35+
*
36+
* @return the config.
37+
*/
38+
@Override
39+
public ClientConfig config() {
40+
return config;
41+
}
42+
43+
/**
44+
* Get the always present input shape value.
45+
*
46+
* @return the input value.
47+
*/
48+
@Override
49+
public I input() {
50+
return input;
51+
}
52+
53+
/**
54+
* Create a new CallHook using the given config.
55+
*
56+
* @param config Config to use.
57+
* @return the new CallHook or the current hook if the config hasn't changed.
58+
*/
59+
public CallHook<I, O> withConfig(ClientConfig config) {
60+
if (config == this.config) {
61+
return this;
62+
} else {
63+
return new CallHook<>(operation, config, input);
64+
}
65+
}
66+
}

client/client-core/src/main/java/software/amazon/smithy/java/client/core/interceptors/ClientInterceptor.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
import java.util.List;
99
import java.util.function.BiFunction;
1010
import java.util.function.Function;
11+
import software.amazon.smithy.java.client.core.ClientConfig;
12+
import software.amazon.smithy.java.client.core.RequestOverrideConfig;
1113
import software.amazon.smithy.java.core.schema.SerializableStruct;
1214

1315
/**
@@ -48,6 +50,19 @@ static ClientInterceptor chain(List<ClientInterceptor> interceptors) {
4850
return interceptors.isEmpty() ? NOOP : new ClientInterceptorChain(interceptors);
4951
}
5052

53+
/**
54+
* The first hook called before executing a call, allowing modification of the {@link ClientConfig} used
55+
* with the call.
56+
*
57+
* <p>Use {@link ClientConfig#withRequestOverride(RequestOverrideConfig)} to modify the given config.
58+
*
59+
* @param hook Hook data.
60+
* @return the updated ClientConfig, or the ClientConfig given to the hook.
61+
*/
62+
default ClientConfig modifyBeforeCall(CallHook<?, ?> hook) {
63+
return hook.config();
64+
}
65+
5166
/**
5267
* A hook called at the start of an execution, before the client does anything else.
5368
*

client/client-core/src/main/java/software/amazon/smithy/java/client/core/interceptors/ClientInterceptorChain.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import java.util.List;
99
import java.util.function.BiConsumer;
1010
import java.util.function.BiFunction;
11+
import software.amazon.smithy.java.client.core.ClientConfig;
1112
import software.amazon.smithy.java.core.schema.SerializableStruct;
1213
import software.amazon.smithy.java.logging.InternalLogger;
1314

@@ -44,6 +45,15 @@ private <T> void applyToEachThrowLastError(String hookName, BiConsumer<ClientInt
4445
}
4546
}
4647

48+
@Override
49+
public ClientConfig modifyBeforeCall(CallHook<?, ?> hook) {
50+
var config = hook.config();
51+
for (var interceptor : interceptors) {
52+
config = interceptor.modifyBeforeCall(hook.withConfig(config));
53+
}
54+
return config;
55+
}
56+
4757
@Override
4858
public <I extends SerializableStruct> I modifyBeforeSerialization(InputHook<I, ?> hook) {
4959
var input = hook.input();

client/client-core/src/test/java/software/amazon/smithy/java/client/core/ClientTest.java

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,17 @@
1313
import java.io.IOException;
1414
import java.net.URI;
1515
import java.net.URISyntaxException;
16+
import java.time.Duration;
17+
import java.util.HashMap;
1618
import org.junit.jupiter.api.Assertions;
1719
import org.junit.jupiter.api.Test;
1820
import software.amazon.smithy.java.aws.client.restjson.RestJsonClientProtocol;
1921
import software.amazon.smithy.java.client.core.auth.scheme.AuthSchemeResolver;
2022
import software.amazon.smithy.java.client.core.endpoint.EndpointResolver;
2123
import software.amazon.smithy.java.client.core.error.TransportException;
24+
import software.amazon.smithy.java.client.core.interceptors.CallHook;
25+
import software.amazon.smithy.java.client.core.interceptors.ClientInterceptor;
26+
import software.amazon.smithy.java.client.core.interceptors.InputHook;
2227
import software.amazon.smithy.java.client.core.plugins.ApplyModelRetryInfoPlugin;
2328
import software.amazon.smithy.java.client.core.plugins.DefaultPlugin;
2429
import software.amazon.smithy.java.client.core.plugins.InjectIdempotencyTokenPlugin;
@@ -28,7 +33,9 @@
2833
import software.amazon.smithy.java.client.http.mock.MockQueue;
2934
import software.amazon.smithy.java.client.http.plugins.ApplyHttpRetryInfoPlugin;
3035
import software.amazon.smithy.java.client.http.plugins.UserAgentPlugin;
36+
import software.amazon.smithy.java.core.serde.document.Document;
3137
import software.amazon.smithy.java.dynamicclient.DynamicClient;
38+
import software.amazon.smithy.java.http.api.HttpResponse;
3239
import software.amazon.smithy.model.Model;
3340
import software.amazon.smithy.model.shapes.ShapeId;
3441

@@ -115,4 +122,75 @@ public void correctlyWrapsTransportExceptions() throws URISyntaxException {
115122
var exception = Assertions.assertThrows(TransportException.class, () -> c.call("GetSprocket"));
116123
assertSame(exception.getCause(), expectedException);
117124
}
125+
126+
@Test
127+
public void allowsInterceptorRequestOverrides() throws URISyntaxException {
128+
var queue = new MockQueue();
129+
queue.enqueue(HttpResponse.builder().statusCode(200).build());
130+
var id = "abc";
131+
132+
DynamicClient c = DynamicClient.builder()
133+
.model(MODEL)
134+
.service(SERVICE)
135+
.protocol(new RestJsonClientProtocol(SERVICE))
136+
.addPlugin(MockPlugin.builder().addQueue(queue).build())
137+
.addPlugin(config -> config.addInterceptor(new ClientInterceptor() {
138+
@Override
139+
public ClientConfig modifyBeforeCall(CallHook<?, ?> hook) {
140+
var override = RequestOverrideConfig.builder()
141+
.putConfig(CallContext.APPLICATION_ID, id)
142+
.build();
143+
return hook.config().withRequestOverride(override);
144+
}
145+
146+
@Override
147+
public void readBeforeExecution(InputHook<?, ?> hook) {
148+
assertThat(hook.context().get(CallContext.APPLICATION_ID), equalTo(id));
149+
}
150+
}))
151+
.endpointResolver(EndpointResolver.staticEndpoint(new URI("http://localhost")))
152+
.authSchemeResolver(AuthSchemeResolver.NO_AUTH)
153+
.build();
154+
155+
c.call("GetSprocket");
156+
}
157+
158+
@Test
159+
public void allowsInterceptorRequestOverridesOnOtherOverrides() throws URISyntaxException {
160+
var queue = new MockQueue();
161+
queue.enqueue(HttpResponse.builder().statusCode(200).build());
162+
var id = "abc";
163+
164+
DynamicClient c = DynamicClient.builder()
165+
.model(MODEL)
166+
.service(SERVICE)
167+
.protocol(new RestJsonClientProtocol(SERVICE))
168+
.addPlugin(MockPlugin.builder().addQueue(queue).build())
169+
.addPlugin(config -> config.addInterceptor(new ClientInterceptor() {
170+
@Override
171+
public ClientConfig modifyBeforeCall(CallHook<?, ?> hook) {
172+
var override = RequestOverrideConfig.builder()
173+
.putConfig(CallContext.APPLICATION_ID, id)
174+
.build();
175+
return hook.config().withRequestOverride(override);
176+
}
177+
178+
@Override
179+
public void readBeforeExecution(InputHook<?, ?> hook) {
180+
assertThat(hook.context().get(CallContext.APPLICATION_ID), equalTo(id));
181+
assertThat(hook.context().get(CallContext.API_CALL_TIMEOUT), equalTo(Duration.ofMinutes(2)));
182+
}
183+
}))
184+
.endpointResolver(EndpointResolver.staticEndpoint(new URI("http://localhost")))
185+
.authSchemeResolver(AuthSchemeResolver.NO_AUTH)
186+
.build();
187+
188+
// Provide request-level overrides here.
189+
c.call("GetSprocket",
190+
Document.ofObject(new HashMap<>()),
191+
RequestOverrideConfig.builder()
192+
.putConfig(CallContext.API_CALL_TIMEOUT, Duration.ofMinutes(2))
193+
.putConfig(CallContext.APPLICATION_ID, "foo") // this will be overridden in the interceptor
194+
.build());
195+
}
118196
}

0 commit comments

Comments
 (0)