Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,9 @@ public abstract class Client implements Closeable {
private final ClientInterceptor interceptor;
private final IdentityResolvers identityResolvers;
private final RetryStrategy retryStrategy;
private final ClientCallDecorator<Client> callDecorator;

@SuppressWarnings("unchecked")
protected Client(Builder<?, ?> builder) {
ClientConfig.Builder configBuilder = builder.configBuilder();
this.config = configBuilder.build();
Expand All @@ -61,6 +63,7 @@ protected Client(Builder<?, ?> builder) {
if (retryStrategy instanceof Claimable c) {
c.claim(this);
}
this.callDecorator = (ClientCallDecorator<Client>) this.config.callDecorator();
}

/**
Expand All @@ -77,6 +80,24 @@ protected <I extends SerializableStruct, O extends SerializableStruct> O call(
I input,
ApiOperation<I, O> operation,
RequestOverrideConfig overrideConfig
) {
if (callDecorator != null) {
return callDecorator.apply(
this,
operation,
input,
overrideConfig,
overrideConfig == null ? null : overrideConfig.context(),
this::doCall);
}

return this.doCall(input, operation, overrideConfig);
Comment thread
timocov marked this conversation as resolved.
}

private <I extends SerializableStruct, O extends SerializableStruct> O doCall(
I input,
ApiOperation<I, O> operation,
RequestOverrideConfig overrideConfig
) {
ProtocolEventStreamWriter<SerializableStruct, SerializableStruct, Frame<?>> eventStreamWriter = null;
if (operation.inputEventBuilderSupplier() != null) {
Expand Down Expand Up @@ -367,7 +388,7 @@ public B retryScope(String retryScope) {
* plugin class. Plugins are applied in a sorted {@link ClientPlugin.Phase} and insertion order.
*
* @see ClientConfig.Builder#pluginPredicate()
* @see ClientConfig.Builder#addPlugin(ClientPlugin)
* @see ClientConfig.Builder#addPlugin(ClientPlugin)
* @param plugin Plugin to add.
* @return the builder.
*/
Expand Down Expand Up @@ -417,6 +438,18 @@ public boolean test(ClientPlugin plugin) {
}.and(configBuilder.pluginPredicate()));
}

/**
* Sets the decorator to wrap client call execution.
*
* @param callDecorator the client call decorator.
* @return the builder.
*/
@SuppressWarnings("unchecked")
public B callDecorator(ClientCallDecorator<I> callDecorator) {
configBuilder.callDecorator(callDecorator);
return (B) this;
}

/**
* Creates the client.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/

package software.amazon.smithy.java.client.core;

import software.amazon.smithy.java.context.Context;
import software.amazon.smithy.java.core.schema.ApiOperation;
import software.amazon.smithy.java.core.schema.SerializableStruct;

/**
* A decorator that wraps client call execution, allowing cross-cutting behavior to be applied
* around operation invocations.
*
* <p>Implementations can inspect or modify the input, add logging, metrics, caching, or other
* concerns before delegating to the next invoker in the chain.
*
* @param <C> the client type this decorator is applied to.
*/
@FunctionalInterface
public interface ClientCallDecorator<C> {
/**
* Applies decoration logic around a client call.
*
* @param client the client instance making the call.
* @param operation the API operation being invoked.
* @param input the operation input.
* @param overrideConfig optional per-request configuration overrides, or {@code null}.
* @param overrideContext optional per-request context overrides, or {@code null}.
* @param next the next invoker in the chain to delegate the actual call to.
* @param <I> the input type.
* @param <O> the output type.
* @return the operation output.
*/
<I extends SerializableStruct, O extends SerializableStruct> O apply(
C client,
ApiOperation<I, O> operation,
I input,
RequestOverrideConfig overrideConfig,
Context overrideContext,
ClientCallInvoker next
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/

package software.amazon.smithy.java.client.core;

import software.amazon.smithy.java.core.schema.ApiOperation;
import software.amazon.smithy.java.core.schema.SerializableStruct;

/**
* Invokes a client operation call with the given input and configuration.
*
* <p>This functional interface represents the core call execution logic that a
* {@link ClientCallDecorator} delegates to after applying its decoration.
*/
@FunctionalInterface
public interface ClientCallInvoker {
/**
* Invokes the operation.
*
* @param input the operation input.
* @param operation the API operation being invoked.
* @param overrideConfig optional per-request configuration overrides, or {@code null}.
* @param <I> the input type.
* @param <O> the output type.
* @return the operation output.
*/
<I extends SerializableStruct, O extends SerializableStruct> O invoke(
I input,
ApiOperation<I, O> operation,
RequestOverrideConfig overrideConfig
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ public final class ClientConfig {
private final RetryStrategy retryStrategy;
private final String retryScope;
private final Set<Class<? extends ClientPlugin>> appliedPluginClasses;
private final ClientCallDecorator<?> callDecorator;

private ClientConfig(Builder builder) {
// Collect and apply plugins, updating builder.appliedPluginClasses as we go
Expand Down Expand Up @@ -95,6 +96,7 @@ private ClientConfig(Builder builder) {

this.context = Context.unmodifiableCopy(builder.context);
this.service = Objects.requireNonNull(builder.service, "Missing required service schema");
this.callDecorator = builder.callDecorator;
}

private static List<ClientPlugin> collectPlugins(
Expand Down Expand Up @@ -223,6 +225,10 @@ String retryScope() {
return retryScope;
}

ClientCallDecorator<?> callDecorator() {
return callDecorator;
}

/**
* Create a new builder to build {@link ClientConfig}.
*
Expand Down Expand Up @@ -322,6 +328,7 @@ public static final class Builder {
private final Map<Class<? extends ClientPlugin>, ClientPlugin> plugins = new LinkedHashMap<>();
// Mutable set that tracks which plugin classes have been applied to this builder
private final Set<Class<? extends ClientPlugin>> appliedPluginClasses = new HashSet<>();
private ClientCallDecorator<?> callDecorator;

public Builder() {
plugins.put(DefaultPlugin.class, DefaultPlugin.INSTANCE);
Expand Down Expand Up @@ -659,6 +666,17 @@ public Builder addPluginPredicate(Predicate<ClientPlugin> pluginPredicate) {
return pluginPredicate(this.pluginPredicate.and(pluginPredicate));
}

/**
* Sets the decorator to wrap client call execution.
*
* @param callDecorator the client call decorator.
* @return the builder.
*/
public Builder callDecorator(ClientCallDecorator<?> callDecorator) {
this.callDecorator = callDecorator;
return this;
}

/**
* Get the plugin predicate of the builder.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@
import software.amazon.smithy.java.client.http.plugins.HttpChecksumPlugin;
import software.amazon.smithy.java.client.http.plugins.RequestCompressionPlugin;
import software.amazon.smithy.java.client.http.plugins.UserAgentPlugin;
import software.amazon.smithy.java.context.Context;
import software.amazon.smithy.java.core.schema.ApiOperation;
import software.amazon.smithy.java.core.schema.SerializableStruct;
import software.amazon.smithy.java.core.serde.document.Document;
import software.amazon.smithy.java.dynamicclient.DynamicClient;
import software.amazon.smithy.java.dynamicclient.plugins.DetectProtocolPlugin;
Expand Down Expand Up @@ -301,4 +304,41 @@ public void readBeforeExecution(InputHook<?, ?> hook) {

c.call("GetSprocket");
}

@Test
public void setsCallDecorator() throws URISyntaxException {
var queue = new MockQueue();
queue.enqueue(HttpResponse.create().setStatusCode(200).toUnmodifiable());

DynamicClient c = DynamicClient.builder()
.model(MODEL)
.serviceId(SERVICE)
.protocol(new RestJsonClientProtocol(SERVICE))
.addPlugin(MockPlugin.builder().addQueue(queue).build())
.addPlugin(config -> config.callDecorator(new ClientCallDecorator<DynamicClient>() {
@Override
public <I extends SerializableStruct, O extends SerializableStruct> O apply(
DynamicClient client,
ApiOperation<I, O> operation,
I input,
RequestOverrideConfig overrideConfig,
Context overrideContext,
ClientCallInvoker next
) {
assertThat(overrideContext.get(ClientContext.APPLICATION_ID), equalTo("foo"));
throw new IllegalStateException("Prevent calling the service");
}
}))
.authSchemeResolver(AuthSchemeResolver.NO_AUTH)
.endpointResolver(EndpointResolver.staticEndpoint(new URI("http://localhost")))
.build();

Assertions.assertThrows(IllegalStateException.class, () -> {
c.call("GetSprocket",
Document.ofObject(new HashMap<>()),
RequestOverrideConfig.builder()
.putConfig(ClientContext.APPLICATION_ID, "foo")
.build());
}, "Prevent calling the service");
}
}