Skip to content

Commit b174b35

Browse files
committed
Add customizeSettings to JavaCodegenIntegration
This allows integrations to customize things like default settings and default plugins before interceptors() run. Closes #1212
1 parent fc624b1 commit b174b35

10 files changed

Lines changed: 281 additions & 4 deletions

File tree

codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/JavaCodegenIntegration.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
package software.amazon.smithy.java.codegen;
77

88
import java.util.List;
9+
import software.amazon.smithy.codegen.core.CodegenContext;
910
import software.amazon.smithy.codegen.core.SmithyIntegration;
1011
import software.amazon.smithy.java.codegen.writer.JavaWriter;
1112
import software.amazon.smithy.model.traits.Trait;
@@ -23,4 +24,25 @@ public interface JavaCodegenIntegration
2324
default List<TraitInitializer<? extends Trait>> traitInitializers() {
2425
return List.of();
2526
}
27+
28+
/**
29+
* Customizes {@link JavaCodegenSettings} before code generation begins.
30+
*
31+
* <p>This is invoked once per integration immediately after the {@link CodeGenerationContext} is
32+
* created and <em>before</em> any interceptors are registered or shapes are generated. It is the
33+
* correct place to programmatically register
34+
* {@linkplain JavaCodegenSettings#addDefaultPlugin(String) default plugins} or
35+
* {@linkplain JavaCodegenSettings#addDefaultSetting(String) default settings}, because those values
36+
* are read while the client builder is generated.
37+
*
38+
* <p>Mutating settings from {@link #customize(CodegenContext)} is too late: by the time
39+
* {@code customize} runs the client builder has already been generated and the change has no effect.
40+
*
41+
* <p>The full context is provided so implementations can inspect the model (for example, to register
42+
* a plugin only for services that use a particular auth scheme). Use
43+
* {@link CodeGenerationContext#settings()} to register defaults.
44+
*
45+
* @param context code generation context whose settings may be customized.
46+
*/
47+
default void customizeSettings(CodeGenerationContext context) {}
2648
}

codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/JavaCodegenSettings.java

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import java.io.File;
99
import java.util.ArrayList;
10+
import java.util.Collection;
1011
import java.util.Collections;
1112
import java.util.HashMap;
1213
import java.util.HashSet;
@@ -96,7 +97,7 @@ private JavaCodegenSettings(Builder builder) {
9697
this.transportName = builder.transportName;
9798
this.transportSettings = builder.transportSettings;
9899
this.defaultPlugins = new ArrayList<>(builder.defaultPlugins);
99-
this.defaultSettings = Collections.unmodifiableList(builder.defaultSettings);
100+
this.defaultSettings = new ArrayList<>(builder.defaultSettings);
100101
this.relativeDate = builder.relativeDate;
101102
this.relativeVersion = builder.relativeVersion;
102103
this.edition = Objects.requireNonNullElse(builder.edition, SmithyJavaCodegenEdition.LATEST);
@@ -167,16 +168,62 @@ public ObjectNode transportSettings() {
167168
return transportSettings;
168169
}
169170

171+
/**
172+
* Fully-qualified class names of {@code ClientPlugin}s applied by default to generated clients.
173+
*
174+
* @return an unmodifiable view of the configured default plugins.
175+
*/
170176
public List<String> defaultPlugins() {
171177
return Collections.unmodifiableList(defaultPlugins);
172178
}
173179

180+
/**
181+
* Registers a {@code ClientPlugin} to apply by default to generated clients.
182+
*
183+
* <p>The plugin must have a public, zero-arg constructor. Integrations should call this from
184+
* {@link JavaCodegenIntegration#customizeSettings(CodeGenerationContext)}, which runs before the
185+
* client builder is generated; calling it later (for example from
186+
* {@link JavaCodegenIntegration#customize(CodeGenerationContext)}) has no effect because the default
187+
* plugins have already been read.
188+
*
189+
* @param pluginClass fully-qualified class name of the plugin to add.
190+
*/
174191
public void addDefaultPlugin(String pluginClass) {
175192
defaultPlugins.add(pluginClass);
176193
}
177194

195+
/**
196+
* Fully-qualified class names of {@code ClientSetting}s applied by default to generated client builders.
197+
*
198+
* @return an unmodifiable view of the configured default settings.
199+
*/
178200
public List<String> defaultSettings() {
179-
return defaultSettings;
201+
return Collections.unmodifiableList(defaultSettings);
202+
}
203+
204+
/**
205+
* Registers a {@code ClientSetting} to apply by default to generated client builders.
206+
*
207+
* <p>Integrations should call this from
208+
* {@link JavaCodegenIntegration#customizeSettings(CodeGenerationContext)}, which runs before the
209+
* client builder is generated; calling it later (for example from
210+
* {@link JavaCodegenIntegration#customize(CodeGenerationContext)}) has no effect because the default
211+
* settings have already been read.
212+
*
213+
* @param settingClass fully-qualified class name of the setting to add.
214+
*/
215+
public void addDefaultSetting(String settingClass) {
216+
defaultSettings.add(settingClass);
217+
}
218+
219+
/**
220+
* Registers {@code ClientSetting}s to apply by default to generated client builders.
221+
*
222+
* @param settingClasses fully-qualified class names of the settings to add.
223+
* @see #addDefaultSetting(String)
224+
*/
225+
public void addDefaultSettings(Collection<String> settingClasses) {
226+
defaultSettings.addAll(settingClasses);
180227
}
181228

182229
public String relativeDate() {

codegen/codegen-plugin/src/main/java/software/amazon/smithy/java/codegen/DirectedJavaCodegen.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,10 @@ public CodeGenerationContext createContext(
7878
) {
7979
String pluginName = getPluginName();
8080
this.context = new CodeGenerationContext(directive, pluginName);
81+
// Allow integrations to customize settings
82+
for (var integration : directive.integrations()) {
83+
integration.customizeSettings(context);
84+
}
8185
return context;
8286
}
8387

codegen/codegen-plugin/src/main/java/software/amazon/smithy/java/codegen/client/integrations/aws/AwsCredentialChainIntegration.java

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import software.amazon.smithy.java.codegen.CodeGenerationContext;
99
import software.amazon.smithy.java.codegen.JavaCodegenIntegration;
1010
import software.amazon.smithy.java.codegen.JavaCodegenSettings;
11+
import software.amazon.smithy.java.logging.InternalLogger;
1112
import software.amazon.smithy.model.Model;
1213
import software.amazon.smithy.model.knowledge.ServiceIndex;
1314
import software.amazon.smithy.model.shapes.ServiceShape;
@@ -21,18 +22,40 @@
2122
@SmithyInternalApi
2223
public final class AwsCredentialChainIntegration implements JavaCodegenIntegration {
2324

25+
private static final InternalLogger LOGGER = InternalLogger.getLogger(AwsCredentialChainIntegration.class);
2426
private static final String PLUGIN_CLASS = "software.amazon.smithy.java.aws.client.core.AwsCredentialChainPlugin";
2527
private static final ShapeId SIGV4 = ShapeId.from("aws.auth#sigv4");
2628
private static final ShapeId SIGV4A = ShapeId.from("aws.auth#sigv4a");
2729

2830
@Override
29-
public void customize(CodeGenerationContext context) {
31+
public void customizeSettings(CodeGenerationContext context) {
3032
Model model = context.model();
3133
JavaCodegenSettings settings = context.settings();
3234
var service = model.expectShape(settings.service(), ServiceShape.class);
3335
var authSchemes = ServiceIndex.of(model).getEffectiveAuthSchemes(service);
3436
if (authSchemes.containsKey(SIGV4) || authSchemes.containsKey(SIGV4A)) {
35-
settings.addDefaultPlugin(PLUGIN_CLASS);
37+
// Only register the plugin if it is actually on the codegen classpath. A model can carry AWS auth
38+
// traits without depending on the AWS client runtime (e.g. serde benchmarks), in which case the
39+
// plugin cannot be resolved and we skip it rather than failing the build.
40+
if (isPluginOnClasspath()) {
41+
settings.addDefaultPlugin(PLUGIN_CLASS);
42+
} else {
43+
LOGGER.warn(
44+
"Service {} uses an AWS auth scheme but {} is not on the classpath; "
45+
+ "skipping the default credential chain plugin. Add a dependency on "
46+
+ "'aws-client-core' to enable it.",
47+
service.getId(),
48+
PLUGIN_CLASS);
49+
}
50+
}
51+
}
52+
53+
private static boolean isPluginOnClasspath() {
54+
try {
55+
Class.forName(PLUGIN_CLASS, false, AwsCredentialChainIntegration.class.getClassLoader());
56+
return true;
57+
} catch (ClassNotFoundException e) {
58+
return false;
3659
}
3760
}
3861
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
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.codegen.client.integrations.settings;
7+
8+
import software.amazon.smithy.java.codegen.CodeGenerationContext;
9+
import software.amazon.smithy.java.codegen.JavaCodegenIntegration;
10+
import software.amazon.smithy.model.shapes.ShapeId;
11+
12+
/**
13+
* Test integration used to verify the lifecycle ordering of settings customization.
14+
*
15+
* <p>It registers one default plugin from {@link #customizeSettings(CodeGenerationContext)} (which
16+
* runs before the client builder is generated and should take effect) and another from
17+
* {@link #customize(CodeGenerationContext)} (which runs after generation and should be a no-op).
18+
*
19+
* <p>This integration is discovered via {@code ServiceLoader} for every codegen run in this module,
20+
* so it only acts when the marker service is being generated to avoid perturbing other tests.
21+
*/
22+
public final class DefaultPluginSettingsIntegration implements JavaCodegenIntegration {
23+
24+
private static final ShapeId MARKER_SERVICE =
25+
ShapeId.from("smithy.java.codegen.settings#DefaultPluginSettingsService");
26+
27+
@Override
28+
public String name() {
29+
return "default-plugin-settings-test";
30+
}
31+
32+
@Override
33+
public void customizeSettings(CodeGenerationContext context) {
34+
if (context.settings().service().equals(MARKER_SERVICE)) {
35+
context.settings().addDefaultPlugin(EarlyDefaultPlugin.class.getCanonicalName());
36+
}
37+
}
38+
39+
@Override
40+
public void customize(CodeGenerationContext context) {
41+
if (context.settings().service().equals(MARKER_SERVICE)) {
42+
context.settings().addDefaultPlugin(LateDefaultPlugin.class.getCanonicalName());
43+
}
44+
}
45+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
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.codegen.client.integrations.settings;
7+
8+
import static org.hamcrest.MatcherAssert.assertThat;
9+
import static org.hamcrest.Matchers.containsString;
10+
import static org.hamcrest.Matchers.not;
11+
import static org.junit.jupiter.api.Assertions.assertFalse;
12+
13+
import java.nio.file.Paths;
14+
import java.util.Objects;
15+
import org.junit.jupiter.api.BeforeEach;
16+
import org.junit.jupiter.api.Test;
17+
import software.amazon.smithy.build.MockManifest;
18+
import software.amazon.smithy.build.PluginContext;
19+
import software.amazon.smithy.java.codegen.JavaCodegenPlugin;
20+
import software.amazon.smithy.model.Model;
21+
import software.amazon.smithy.model.node.ArrayNode;
22+
import software.amazon.smithy.model.node.ObjectNode;
23+
24+
/**
25+
* Verifies that {@link software.amazon.smithy.java.codegen.JavaCodegenIntegration#customizeSettings}
26+
* is invoked early enough for default plugins registered there to be wired into generated clients,
27+
* while default plugins registered from {@code customize} (which runs after generation) are not.
28+
*
29+
* <p>The wiring is driven by {@link DefaultPluginSettingsIntegration}, discovered via {@code ServiceLoader}.
30+
*/
31+
public class DefaultPluginSettingsIntegrationTest {
32+
33+
private final MockManifest manifest = new MockManifest();
34+
35+
@BeforeEach
36+
public void setup() {
37+
var model = Model.assembler()
38+
.addImport(Objects.requireNonNull(
39+
DefaultPluginSettingsIntegrationTest.class.getResource("default-plugin-settings.smithy")))
40+
.assemble()
41+
.unwrap();
42+
var context = PluginContext.builder()
43+
.fileManifest(manifest)
44+
.settings(ObjectNode.builder()
45+
.withMember("service", "smithy.java.codegen.settings#DefaultPluginSettingsService")
46+
.withMember("namespace", "smithy.java.codegen.settings")
47+
.withMember("modes", ArrayNode.fromStrings("client"))
48+
.build())
49+
.model(model)
50+
.build();
51+
new JavaCodegenPlugin().execute(context);
52+
assertFalse(manifest.getFiles().isEmpty());
53+
}
54+
55+
@Test
56+
void pluginAddedFromCustomizeSettingsIsWiredIntoClient() {
57+
var client = clientInterfaceSource();
58+
// Registered from customizeSettings() -> present as a default plugin.
59+
assertThat(client, containsString("new EarlyDefaultPlugin()"));
60+
assertThat(client,
61+
containsString("private final List<ClientPlugin> defaultPlugins = List.of(earlyDefaultPlugin)"));
62+
}
63+
64+
@Test
65+
void pluginAddedFromCustomizeIsTooLateAndIgnored() {
66+
var client = clientInterfaceSource();
67+
// Registered from customize() -> too late, never wired in.
68+
assertThat(client, not(containsString("LateDefaultPlugin")));
69+
}
70+
71+
private String clientInterfaceSource() {
72+
return manifest.getFiles()
73+
.stream()
74+
.map(Object::toString)
75+
.filter(p -> p.endsWith("DefaultPluginSettingsServiceClient.java"))
76+
.findFirst()
77+
.flatMap(p -> manifest.getFileString(Paths.get(p)))
78+
.orElseThrow(
79+
() -> new AssertionError("Generated client interface not found in " + manifest.getFiles()));
80+
}
81+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
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.codegen.client.integrations.settings;
7+
8+
import software.amazon.smithy.java.client.core.ClientConfig;
9+
import software.amazon.smithy.java.client.core.ClientPlugin;
10+
11+
/**
12+
* Test plugin registered as a default plugin from
13+
* {@code JavaCodegenIntegration#customizeSettings} (which runs early enough to take effect).
14+
*/
15+
public final class EarlyDefaultPlugin implements ClientPlugin {
16+
@Override
17+
public void configureClient(ClientConfig.Builder config) {
18+
// no-op
19+
}
20+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
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.codegen.client.integrations.settings;
7+
8+
import software.amazon.smithy.java.client.core.ClientConfig;
9+
import software.amazon.smithy.java.client.core.ClientPlugin;
10+
11+
/**
12+
* Test plugin registered as a default plugin from {@code JavaCodegenIntegration#customize}, which
13+
* runs <em>after</em> the client builder has already been generated and therefore has no effect.
14+
*/
15+
public final class LateDefaultPlugin implements ClientPlugin {
16+
@Override
17+
public void configureClient(ClientConfig.Builder config) {
18+
// no-op
19+
}
20+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
software.amazon.smithy.java.codegen.client.integrations.settings.DefaultPluginSettingsIntegration
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
$version: "2"
2+
3+
namespace smithy.java.codegen.settings
4+
5+
service DefaultPluginSettingsService {
6+
operations: [
7+
NoOpOperation
8+
]
9+
}
10+
11+
operation NoOpOperation {
12+
input := {}
13+
output := {}
14+
}

0 commit comments

Comments
 (0)