Skip to content

Commit 0c220a4

Browse files
Update codegen to support closure-based generation
This updates the code generators to support generating based on shape closures. Shape closures are a new way to define a set of shapes to generate in the model without rooting that set at a service shape. This has implications that cause some chages throughout the generator. Notably, a service might not exist. There are a number of places that required a service shape to exist that had to be updated to tolerate it being missing. Notably renames may now be driven through the closure definition itself rather than a service shape, so renames needed to be passed through in a lot of generator classes. Previously types codegen was implemented by creating a synthetic service shape and/or operation and attaching the shapes to it. The generator would then skip those synthetic shapes so that no unwanted scaffolding was generated. That whole system was removed. Another important implication of shape closures is that a closure can have multiple services. For now, smithy-java is keeping the restriction to have exactly 0 or 1 services. Supporting multiple can be done down the line if there's a need.
1 parent c249679 commit 0c220a4

39 files changed

Lines changed: 812 additions & 397 deletions

File tree

codegen/codegen-core/src/internal/java/software/amazon/smithy/java/codegen/JavaTypesCodegenPlugin.java

Lines changed: 33 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,14 @@
55

66
package software.amazon.smithy.java.codegen;
77

8-
import java.util.HashSet;
98
import java.util.Set;
9+
import java.util.StringJoiner;
1010
import software.amazon.smithy.build.PluginContext;
1111
import software.amazon.smithy.build.SmithyBuildPlugin;
12-
import software.amazon.smithy.codegen.core.CodegenException;
1312
import software.amazon.smithy.codegen.core.directed.CodegenDirector;
1413
import software.amazon.smithy.java.codegen.writer.JavaWriter;
1514
import software.amazon.smithy.java.logging.InternalLogger;
16-
import software.amazon.smithy.model.Model;
17-
import software.amazon.smithy.model.loader.Prelude;
18-
import software.amazon.smithy.model.shapes.Shape;
15+
import software.amazon.smithy.model.metadata.ShapeClosure;
1916
import software.amazon.smithy.utils.SmithyInternalApi;
2017

2118
/**
@@ -42,6 +39,10 @@
4239
public final class JavaTypesCodegenPlugin implements SmithyBuildPlugin {
4340
private static final InternalLogger LOGGER = InternalLogger.getLogger(JavaTypesCodegenPlugin.class);
4441

42+
// Id of the shape closure that drives types-only generation. Namespaced so it cannot collide
43+
// with a model shape id or a model-authored closure.
44+
private static final String CLOSURE_ID = "software.amazon.smithy.java.codegen#types";
45+
4546
@Override
4647
public String getName() {
4748
return "internal-types-only";
@@ -60,35 +61,40 @@ public void execute(PluginContext context) {
6061
runner.settings(codegenSettings);
6162
runner.directedCodegen(new TypesDirectedJavaCodegen(modes));
6263
runner.fileManifest(context.getFileManifest());
63-
runner.service(codegenSettings.service());
6464

65-
// Compute closure and create synthetic service
66-
var closure = getClosure(context.getModel(), settings);
67-
LOGGER.info("Found {} shapes in generation closure", closure.size());
68-
var model = SyntheticServiceTransform.transform(context.getModel(), closure, settings.renames());
69-
runner.model(model);
65+
// Generate the data shapes selected by the settings as a shape closure, with no
66+
// primary service. The director resolves the closure's transitive data shapes.
67+
runner.shapeClosure(typesClosure(settings));
68+
runner.generateDataShapesOnly();
69+
runner.model(context.getModel());
7070
runner.integrationClass(JavaCodegenIntegration.class);
7171
DefaultTransforms.apply(runner, codegenSettings);
7272
runner.run();
7373
LOGGER.info("Successfully generated Java class files.");
7474
}
7575

76-
private static Set<Shape> getClosure(Model model, TypeCodegenSettings settings) {
77-
Set<Shape> closure = new HashSet<>();
78-
for (var shapeId : settings.shapes()) {
79-
closure.add(model.expectShape(shapeId));
80-
}
81-
settings.selector()
82-
.shapes(model)
83-
.filter(s -> !s.isMemberShape())
84-
.filter(s -> !Prelude.isPreludeShape(s))
85-
.forEach(closure::add);
86-
87-
if (closure.isEmpty()) {
88-
throw new CodegenException("Could not generate types. No shapes found in closure");
76+
/**
77+
* Builds the shape closure to generate from the configured selector, explicitly listed shapes,
78+
* and renames. Explicit shapes are folded into the selector as id-equality clauses and trait
79+
* definitions are excluded so only data shapes are generated.
80+
*/
81+
private static ShapeClosure typesClosure(TypeCodegenSettings settings) {
82+
// Fold the explicit shapes into the configured selector as id-equality alternatives,
83+
// e.g. :is(<selector>, [id='ns#A'], [id='ns#B']).
84+
String base = settings.selector().toString();
85+
if (!settings.shapes().isEmpty()) {
86+
var joiner = new StringJoiner(", ", ":is(", ")");
87+
joiner.add(base);
88+
for (var shape : settings.shapes()) {
89+
joiner.add("[id='" + shape + "']");
90+
}
91+
base = joiner.toString();
8992
}
90-
LOGGER.info("Found {} shapes in generation closure.", closure.size());
91-
92-
return closure;
93+
return ShapeClosure.builder()
94+
.id(CLOSURE_ID)
95+
// Exclude trait definitions so only data shapes are generated.
96+
.includeBySelector(base + " :not([trait|trait])")
97+
.rename(settings.renames())
98+
.build();
9399
}
94100
}

codegen/codegen-core/src/internal/java/software/amazon/smithy/java/codegen/TypesDirectedJavaCodegen.java

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@
3030
import software.amazon.smithy.java.codegen.generators.SharedSerdeGenerator;
3131
import software.amazon.smithy.java.codegen.generators.StructureGenerator;
3232
import software.amazon.smithy.java.codegen.generators.UnionGenerator;
33-
import software.amazon.smithy.model.shapes.Shape;
3433
import software.amazon.smithy.utils.SmithyInternalApi;
3534

3635
/**
@@ -53,7 +52,8 @@ public SymbolProvider createSymbolProvider(
5352
) {
5453
return new JavaSymbolProvider(
5554
directive.model(),
56-
directive.service(),
55+
directive.getService().orElse(null),
56+
directive.getRenames(),
5757
directive.settings().packageNamespace(),
5858
directive.settings().name(),
5959
modes);
@@ -68,9 +68,7 @@ public CodeGenerationContext createContext(
6868

6969
@Override
7070
public void generateStructure(GenerateStructureDirective<CodeGenerationContext, JavaCodegenSettings> directive) {
71-
if (!isSynthetic(directive.shape())) {
72-
new StructureGenerator<>().accept(directive);
73-
}
71+
new StructureGenerator<>().accept(directive);
7472
}
7573

7674
@Override
@@ -129,8 +127,4 @@ public void customizeBeforeIntegrations(CustomizeDirective<CodeGenerationContext
129127
public void customizeAfterIntegrations(CustomizeDirective<CodeGenerationContext, JavaCodegenSettings> directive) {
130128
// No-op for types-only mode
131129
}
132-
133-
private static boolean isSynthetic(Shape shape) {
134-
return shape.getId().getNamespace().equals(SyntheticServiceTransform.SYNTHETIC_NAMESPACE);
135-
}
136130
}

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

Lines changed: 23 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
import java.util.stream.Collectors;
1515
import software.amazon.smithy.build.FileManifest;
1616
import software.amazon.smithy.codegen.core.CodegenContext;
17-
import software.amazon.smithy.codegen.core.CodegenException;
1817
import software.amazon.smithy.codegen.core.SymbolProvider;
1918
import software.amazon.smithy.codegen.core.WriterDelegator;
2019
import software.amazon.smithy.codegen.core.directed.CreateContextDirective;
@@ -123,7 +122,7 @@ public CodeGenerationContext(
123122
fileManifest,
124123
this.symbolProvider,
125124
new JavaWriter.Factory(settings));
126-
this.runtimeTraits = collectRuntimeTraits();
125+
this.runtimeTraits = collectRuntimeTraits(directive.getService().orElse(null));
127126
this.traitInitializers = collectTraitInitializers();
128127
this.plugin = plugin;
129128
this.schemaFieldOrder = new SchemaFieldOrder(directive, this);
@@ -186,32 +185,30 @@ public SchemaFieldOrder schemaFieldOrder() {
186185
*
187186
* @return Set of trait ShapeId's to include in generated Schemas.
188187
*/
189-
private Set<ShapeId> collectRuntimeTraits() {
190-
ServiceShape shape = model.expectShape(settings.service())
191-
.asServiceShape()
192-
.orElseThrow(
193-
() -> new CodegenException(
194-
"Expected shapeId: "
195-
+ settings.service() + " to be a service shape."));
196-
188+
private Set<ShapeId> collectRuntimeTraits(ServiceShape service) {
197189
// Add all default runtime traits from the prelude
198190
Set<ShapeId> traits = new HashSet<>(PRELUDE_RUNTIME_TRAITS);
199-
for (var entry : shape.getAllTraits().entrySet()) {
200-
Optional<Shape> traitShapeOptional = model.getShape(entry.getKey());
201-
if (traitShapeOptional.isEmpty()) {
202-
LOGGER.debug("Skipping unknown trait: {}", entry.getKey());
203-
continue;
204-
}
205-
var traitShape = traitShapeOptional.get();
206-
// Add all traits supported by a protocol the service supports
207-
if (traitShape.hasTrait(ProtocolDefinitionTrait.class)) {
208-
var protocolDef = traitShape.expectTrait(ProtocolDefinitionTrait.class);
209-
traits.addAll(protocolDef.getTraits());
210-
}
211-
// Add all traits supported by auth schemes the service supports
212-
if (traitShape.hasTrait(AuthDefinitionTrait.class)) {
213-
var authDef = traitShape.expectTrait(AuthDefinitionTrait.class);
214-
traits.addAll(authDef.getTraits());
191+
192+
// Protocol- and auth-scheme-supported traits are rooted in the primary service. Pure types
193+
// mode has no service, so only the prelude and customer-configured traits apply.
194+
if (service != null) {
195+
for (var entry : service.getAllTraits().entrySet()) {
196+
Optional<Shape> traitShapeOptional = model.getShape(entry.getKey());
197+
if (traitShapeOptional.isEmpty()) {
198+
LOGGER.debug("Skipping unknown trait: {}", entry.getKey());
199+
continue;
200+
}
201+
var traitShape = traitShapeOptional.get();
202+
// Add all traits supported by a protocol the service supports
203+
if (traitShape.hasTrait(ProtocolDefinitionTrait.class)) {
204+
var protocolDef = traitShape.expectTrait(ProtocolDefinitionTrait.class);
205+
traits.addAll(protocolDef.getTraits());
206+
}
207+
// Add all traits supported by auth schemes the service supports
208+
if (traitShape.hasTrait(AuthDefinitionTrait.class)) {
209+
var authDef = traitShape.expectTrait(AuthDefinitionTrait.class);
210+
traits.addAll(authDef.getTraits());
211+
}
215212
}
216213
}
217214

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

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import java.net.URL;
1010
import java.util.List;
1111
import java.util.Locale;
12+
import java.util.Map;
1213
import java.util.Objects;
1314
import java.util.regex.Pattern;
1415
import java.util.stream.Collectors;
@@ -111,10 +112,24 @@ public static Symbol fromBoxedClass(Class<?> unboxed, Class<?> boxed) {
111112
* Gets the default class name to use for a given Smithy {@link Shape}.
112113
*
113114
* @param shape Shape to get name for.
115+
* @param service Service whose renames provide contextual names, or null if none.
114116
* @return Default name.
115117
*/
116118
public static String getDefaultName(Shape shape, ServiceShape service) {
117-
String baseName = shape.getId().getName(service);
119+
return getDefaultName(shape, service == null ? Map.of() : service.getRename());
120+
}
121+
122+
/**
123+
* Gets the default class name to use for a given Smithy {@link Shape}.
124+
*
125+
* @param shape Shape to get name for.
126+
* @param renames Renames applied to the generated shapes (a shape id maps to its
127+
* contextual name). Used instead of a service so the closure-driven types path
128+
* can apply renames without a service.
129+
* @return Default name.
130+
*/
131+
public static String getDefaultName(Shape shape, Map<ShapeId, String> renames) {
132+
String baseName = renames.getOrDefault(shape.getId(), shape.getId().getName());
118133

119134
// If the name contains any problematic delimiters, use PascalCase converter,
120135
// otherwise, just capitalize first letter to avoid messing with user-defined
@@ -423,7 +438,7 @@ public static boolean isISO8601Date(String string) {
423438
* @return the property if found, or null.
424439
*/
425440
public static <T> T tryGetServiceProperty(ShapeDirective<?, ?, ?> directive, Property<T> prop) {
426-
var service = directive.service();
441+
var service = directive.getService().orElse(null);
427442
if (service != null) {
428443
var symbol = directive.symbolProvider().toSymbol(service);
429444
if (symbol != null) {

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

Lines changed: 71 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import java.util.Locale;
1515
import java.util.Map;
1616
import java.util.Objects;
17+
import java.util.Optional;
1718
import java.util.Set;
1819
import software.amazon.smithy.codegen.core.CodegenException;
1920
import software.amazon.smithy.codegen.core.Symbol;
@@ -50,6 +51,13 @@ public final class JavaCodegenSettings {
5051
private static final String RUNTIME_TRAITS = "runtimeTraits";
5152
private static final String RUNTIME_TRAITS_SELECTOR = "runtimeTraitsSelector";
5253
private static final String MODES = "modes";
54+
private static final String CLOSURE = "closure";
55+
56+
// Legacy default name for types-only generation. Before types generation was driven by a shape
57+
// closure, it relied on a synthetic service named "TypesGenService", so the generated name
58+
// defaulted to that when none was configured. The name is unused by types-only output, but this
59+
// value is preserved so existing configs without a 'name' keep behaving as they did.
60+
private static final String LEGACY_TYPES_NAME = "TypesGenService";
5361
private static final List<String> PROPERTIES = List.of(
5462
SERVICE,
5563
NAME,
@@ -66,9 +74,11 @@ public final class JavaCodegenSettings {
6674
EDITION,
6775
RUNTIME_TRAITS,
6876
RUNTIME_TRAITS_SELECTOR,
69-
MODES);
77+
MODES,
78+
CLOSURE);
7079

7180
private final ShapeId service;
81+
private final String closure;
7282
private final String name;
7383
private final String packageNamespace;
7484
private final String header;
@@ -87,8 +97,9 @@ public final class JavaCodegenSettings {
8797
private final Map<String, Set<Symbol>> generatedSymbols = new HashMap<>();
8898

8999
private JavaCodegenSettings(Builder builder) {
90-
this.service = Objects.requireNonNull(builder.service);
91-
this.name = StringUtils.capitalize(Objects.requireNonNullElse(builder.name, service.getName()));
100+
this.service = builder.service;
101+
this.closure = builder.closure;
102+
this.name = StringUtils.capitalize(resolveName(builder.name, service));
92103
this.packageNamespace = Objects.requireNonNull(builder.packageNamespace);
93104
this.header = getHeader(builder.headerFilePath, builder.sourceLocation);
94105
this.addNullnessAnnotations = builder.addNullnessAnnotations;
@@ -105,6 +116,20 @@ private JavaCodegenSettings(Builder builder) {
105116
this.httpConfig = builder.httpConfig;
106117
}
107118

119+
// Resolves the generated name: an explicit name wins, otherwise the service name when a service
120+
// is set, otherwise the legacy types-only default (with a warning nudging callers to set one).
121+
private static String resolveName(String configuredName, ShapeId service) {
122+
if (configuredName != null) {
123+
return configuredName;
124+
}
125+
if (service != null) {
126+
return service.getName();
127+
}
128+
LOGGER.warn("No 'name' configured for types-only generation; defaulting to '{}'. Set the "
129+
+ "'name' setting explicitly to silence this warning.", LEGACY_TYPES_NAME);
130+
return LEGACY_TYPES_NAME;
131+
}
132+
108133
/**
109134
* Creates a settings object from a plugin settings node
110135
*
@@ -114,7 +139,8 @@ private JavaCodegenSettings(Builder builder) {
114139
public static JavaCodegenSettings fromNode(ObjectNode settingsNode) {
115140
var builder = new Builder();
116141
settingsNode.warnIfAdditionalProperties(PROPERTIES)
117-
.expectStringMember(SERVICE, builder::service)
142+
.getStringMember(SERVICE, builder::service)
143+
.getStringMember(CLOSURE, builder::closure)
118144
.getStringMember(NAME, builder::name)
119145
.expectStringMember(NAMESPACE, builder::packageNamespace)
120146
.getStringMember(HEADER_FILE, builder::headerFilePath)
@@ -135,10 +161,45 @@ public static JavaCodegenSettings fromNode(ObjectNode settingsNode) {
135161
return builder.build();
136162
}
137163

164+
/**
165+
* Gets the primary service to generate.
166+
*
167+
* @return the configured service id.
168+
* @see #getService() for a non-throwing variant.
169+
*/
138170
public ShapeId service() {
171+
if (service == null) {
172+
throw new CodegenException(
173+
"No service is configured for this code generation because types-only generation "
174+
+ "has no primary service.");
175+
}
139176
return service;
140177
}
141178

179+
/**
180+
* Gets the primary service to generate, if one is configured.
181+
*
182+
* <p>Types-only generation is driven by a shape closure and has no primary service, so this
183+
* returns an empty optional in that case.
184+
*
185+
* @return the configured service id, or empty if none is set.
186+
*/
187+
public Optional<ShapeId> getService() {
188+
return Optional.ofNullable(service);
189+
}
190+
191+
/**
192+
* Gets the id of a pre-authored shape closure to generate, if one was configured.
193+
*
194+
* <p>When set, generation is driven by the {@code shapeClosures} metadata entry with this id in
195+
* the model, rather than by an inline {@code selector}/{@code shapes} configuration.
196+
*
197+
* @return the configured shape closure id, or empty if none is set.
198+
*/
199+
public Optional<String> getClosure() {
200+
return Optional.ofNullable(closure);
201+
}
202+
142203
public String name() {
143204
return name;
144205
}
@@ -232,6 +293,7 @@ public static Builder builder() {
232293

233294
public static final class Builder {
234295
private ShapeId service;
296+
private String closure;
235297
private String name;
236298
private String packageNamespace;
237299
private String headerFilePath;
@@ -254,6 +316,11 @@ public Builder service(String string) {
254316
return this;
255317
}
256318

319+
public Builder closure(String closure) {
320+
this.closure = closure;
321+
return this;
322+
}
323+
257324
public Builder name(String name) {
258325
this.name = name;
259326
return this;

0 commit comments

Comments
 (0)