Skip to content
Draft
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 @@ -5,17 +5,14 @@

package software.amazon.smithy.java.codegen;

import java.util.HashSet;
import java.util.Set;
import java.util.StringJoiner;
import software.amazon.smithy.build.PluginContext;
import software.amazon.smithy.build.SmithyBuildPlugin;
import software.amazon.smithy.codegen.core.CodegenException;
import software.amazon.smithy.codegen.core.directed.CodegenDirector;
import software.amazon.smithy.java.codegen.writer.JavaWriter;
import software.amazon.smithy.java.logging.InternalLogger;
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.loader.Prelude;
import software.amazon.smithy.model.shapes.Shape;
import software.amazon.smithy.model.metadata.ShapeClosure;
import software.amazon.smithy.utils.SmithyInternalApi;

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

// Id of the shape closure that drives types-only generation. Namespaced so it cannot collide
// with a model shape id or a model-authored closure.
private static final String CLOSURE_ID = "software.amazon.smithy.java.codegen#types";

@Override
public String getName() {
return "internal-types-only";
Expand All @@ -60,35 +61,40 @@ public void execute(PluginContext context) {
runner.settings(codegenSettings);
runner.directedCodegen(new TypesDirectedJavaCodegen(modes));
runner.fileManifest(context.getFileManifest());
runner.service(codegenSettings.service());

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

private static Set<Shape> getClosure(Model model, TypeCodegenSettings settings) {
Set<Shape> closure = new HashSet<>();
for (var shapeId : settings.shapes()) {
closure.add(model.expectShape(shapeId));
}
settings.selector()
.shapes(model)
.filter(s -> !s.isMemberShape())
.filter(s -> !Prelude.isPreludeShape(s))
.forEach(closure::add);

if (closure.isEmpty()) {
throw new CodegenException("Could not generate types. No shapes found in closure");
/**
* Builds the shape closure to generate from the configured selector, explicitly listed shapes,
* and renames. Explicit shapes are folded into the selector as id-equality clauses and trait
* definitions are excluded so only data shapes are generated.
*/
private static ShapeClosure typesClosure(TypeCodegenSettings settings) {
// Fold the explicit shapes into the configured selector as id-equality alternatives,
// e.g. :is(<selector>, [id='ns#A'], [id='ns#B']).
String base = settings.selector().toString();
if (!settings.shapes().isEmpty()) {
var joiner = new StringJoiner(", ", ":is(", ")");
joiner.add(base);
for (var shape : settings.shapes()) {
joiner.add("[id='" + shape + "']");
}
base = joiner.toString();
}
LOGGER.info("Found {} shapes in generation closure.", closure.size());

return closure;
return ShapeClosure.builder()
.id(CLOSURE_ID)
// Exclude trait definitions so only data shapes are generated.
.includeBySelector(base + " :not([trait|trait])")
.rename(settings.renames())
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@
import software.amazon.smithy.java.codegen.generators.SharedSerdeGenerator;
import software.amazon.smithy.java.codegen.generators.StructureGenerator;
import software.amazon.smithy.java.codegen.generators.UnionGenerator;
import software.amazon.smithy.model.shapes.Shape;
import software.amazon.smithy.utils.SmithyInternalApi;

/**
Expand All @@ -53,7 +52,8 @@ public SymbolProvider createSymbolProvider(
) {
return new JavaSymbolProvider(
directive.model(),
directive.service(),
directive.getService().orElse(null),
directive.getRenames(),
directive.settings().packageNamespace(),
directive.settings().name(),
modes);
Expand All @@ -68,9 +68,7 @@ public CodeGenerationContext createContext(

@Override
public void generateStructure(GenerateStructureDirective<CodeGenerationContext, JavaCodegenSettings> directive) {
if (!isSynthetic(directive.shape())) {
new StructureGenerator<>().accept(directive);
}
new StructureGenerator<>().accept(directive);
}

@Override
Expand Down Expand Up @@ -129,8 +127,4 @@ public void customizeBeforeIntegrations(CustomizeDirective<CodeGenerationContext
public void customizeAfterIntegrations(CustomizeDirective<CodeGenerationContext, JavaCodegenSettings> directive) {
// No-op for types-only mode
}

private static boolean isSynthetic(Shape shape) {
return shape.getId().getNamespace().equals(SyntheticServiceTransform.SYNTHETIC_NAMESPACE);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
import java.util.stream.Collectors;
import software.amazon.smithy.build.FileManifest;
import software.amazon.smithy.codegen.core.CodegenContext;
import software.amazon.smithy.codegen.core.CodegenException;
import software.amazon.smithy.codegen.core.SymbolProvider;
import software.amazon.smithy.codegen.core.WriterDelegator;
import software.amazon.smithy.codegen.core.directed.CreateContextDirective;
Expand Down Expand Up @@ -123,7 +122,7 @@ public CodeGenerationContext(
fileManifest,
this.symbolProvider,
new JavaWriter.Factory(settings));
this.runtimeTraits = collectRuntimeTraits();
this.runtimeTraits = collectRuntimeTraits(directive.getService().orElse(null));
this.traitInitializers = collectTraitInitializers();
this.plugin = plugin;
this.schemaFieldOrder = new SchemaFieldOrder(directive, this);
Expand Down Expand Up @@ -186,32 +185,30 @@ public SchemaFieldOrder schemaFieldOrder() {
*
* @return Set of trait ShapeId's to include in generated Schemas.
*/
private Set<ShapeId> collectRuntimeTraits() {
ServiceShape shape = model.expectShape(settings.service())
.asServiceShape()
.orElseThrow(
() -> new CodegenException(
"Expected shapeId: "
+ settings.service() + " to be a service shape."));

private Set<ShapeId> collectRuntimeTraits(ServiceShape service) {
// Add all default runtime traits from the prelude
Set<ShapeId> traits = new HashSet<>(PRELUDE_RUNTIME_TRAITS);
for (var entry : shape.getAllTraits().entrySet()) {
Optional<Shape> traitShapeOptional = model.getShape(entry.getKey());
if (traitShapeOptional.isEmpty()) {
LOGGER.debug("Skipping unknown trait: {}", entry.getKey());
continue;
}
var traitShape = traitShapeOptional.get();
// Add all traits supported by a protocol the service supports
if (traitShape.hasTrait(ProtocolDefinitionTrait.class)) {
var protocolDef = traitShape.expectTrait(ProtocolDefinitionTrait.class);
traits.addAll(protocolDef.getTraits());
}
// Add all traits supported by auth schemes the service supports
if (traitShape.hasTrait(AuthDefinitionTrait.class)) {
var authDef = traitShape.expectTrait(AuthDefinitionTrait.class);
traits.addAll(authDef.getTraits());

// Protocol- and auth-scheme-supported traits are rooted in the primary service. Pure types
// mode has no service, so only the prelude and customer-configured traits apply.
if (service != null) {
for (var entry : service.getAllTraits().entrySet()) {
Optional<Shape> traitShapeOptional = model.getShape(entry.getKey());
if (traitShapeOptional.isEmpty()) {
LOGGER.debug("Skipping unknown trait: {}", entry.getKey());
continue;
}
var traitShape = traitShapeOptional.get();
// Add all traits supported by a protocol the service supports
if (traitShape.hasTrait(ProtocolDefinitionTrait.class)) {
var protocolDef = traitShape.expectTrait(ProtocolDefinitionTrait.class);
traits.addAll(protocolDef.getTraits());
}
// Add all traits supported by auth schemes the service supports
if (traitShape.hasTrait(AuthDefinitionTrait.class)) {
var authDef = traitShape.expectTrait(AuthDefinitionTrait.class);
traits.addAll(authDef.getTraits());
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import java.net.URL;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
Expand Down Expand Up @@ -111,10 +112,24 @@ public static Symbol fromBoxedClass(Class<?> unboxed, Class<?> boxed) {
* Gets the default class name to use for a given Smithy {@link Shape}.
*
* @param shape Shape to get name for.
* @param service Service whose renames provide contextual names, or null if none.
* @return Default name.
*/
public static String getDefaultName(Shape shape, ServiceShape service) {
String baseName = shape.getId().getName(service);
return getDefaultName(shape, service == null ? Map.of() : service.getRename());
}

/**
* Gets the default class name to use for a given Smithy {@link Shape}.
*
* @param shape Shape to get name for.
* @param renames Renames applied to the generated shapes (a shape id maps to its
* contextual name). Used instead of a service so the closure-driven types path
* can apply renames without a service.
* @return Default name.
*/
public static String getDefaultName(Shape shape, Map<ShapeId, String> renames) {
String baseName = renames.getOrDefault(shape.getId(), shape.getId().getName());

// If the name contains any problematic delimiters, use PascalCase converter,
// otherwise, just capitalize first letter to avoid messing with user-defined
Expand Down Expand Up @@ -423,7 +438,7 @@ public static boolean isISO8601Date(String string) {
* @return the property if found, or null.
*/
public static <T> T tryGetServiceProperty(ShapeDirective<?, ?, ?> directive, Property<T> prop) {
var service = directive.service();
var service = directive.getService().orElse(null);
if (service != null) {
var symbol = directive.symbolProvider().toSymbol(service);
if (symbol != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import software.amazon.smithy.codegen.core.CodegenException;
import software.amazon.smithy.codegen.core.Symbol;
Expand Down Expand Up @@ -50,6 +51,13 @@ public final class JavaCodegenSettings {
private static final String RUNTIME_TRAITS = "runtimeTraits";
private static final String RUNTIME_TRAITS_SELECTOR = "runtimeTraitsSelector";
private static final String MODES = "modes";
private static final String CLOSURE = "closure";

// Legacy default name for types-only generation. Before types generation was driven by a shape
// closure, it relied on a synthetic service named "TypesGenService", so the generated name
// defaulted to that when none was configured. The name is unused by types-only output, but this
// value is preserved so existing configs without a 'name' keep behaving as they did.
private static final String LEGACY_TYPES_NAME = "TypesGenService";
private static final List<String> PROPERTIES = List.of(
SERVICE,
NAME,
Expand All @@ -66,9 +74,11 @@ public final class JavaCodegenSettings {
EDITION,
RUNTIME_TRAITS,
RUNTIME_TRAITS_SELECTOR,
MODES);
MODES,
CLOSURE);

private final ShapeId service;
private final String closure;
private final String name;
private final String packageNamespace;
private final String header;
Expand All @@ -87,8 +97,9 @@ public final class JavaCodegenSettings {
private final Map<String, Set<Symbol>> generatedSymbols = new HashMap<>();

private JavaCodegenSettings(Builder builder) {
this.service = Objects.requireNonNull(builder.service);
this.name = StringUtils.capitalize(Objects.requireNonNullElse(builder.name, service.getName()));
this.service = builder.service;
this.closure = builder.closure;
this.name = StringUtils.capitalize(resolveName(builder.name, service));
this.packageNamespace = Objects.requireNonNull(builder.packageNamespace);
this.header = getHeader(builder.headerFilePath, builder.sourceLocation);
this.addNullnessAnnotations = builder.addNullnessAnnotations;
Expand All @@ -105,6 +116,20 @@ private JavaCodegenSettings(Builder builder) {
this.httpConfig = builder.httpConfig;
}

// Resolves the generated name: an explicit name wins, otherwise the service name when a service
// is set, otherwise the legacy types-only default (with a warning nudging callers to set one).
private static String resolveName(String configuredName, ShapeId service) {
if (configuredName != null) {
return configuredName;
}
if (service != null) {
return service.getName();
}
LOGGER.warn("No 'name' configured for types-only generation; defaulting to '{}'. Set the "
+ "'name' setting explicitly to silence this warning.", LEGACY_TYPES_NAME);
return LEGACY_TYPES_NAME;
}

/**
* Creates a settings object from a plugin settings node
*
Expand All @@ -114,7 +139,8 @@ private JavaCodegenSettings(Builder builder) {
public static JavaCodegenSettings fromNode(ObjectNode settingsNode) {
var builder = new Builder();
settingsNode.warnIfAdditionalProperties(PROPERTIES)
.expectStringMember(SERVICE, builder::service)
.getStringMember(SERVICE, builder::service)
.getStringMember(CLOSURE, builder::closure)
.getStringMember(NAME, builder::name)
.expectStringMember(NAMESPACE, builder::packageNamespace)
.getStringMember(HEADER_FILE, builder::headerFilePath)
Expand All @@ -135,10 +161,45 @@ public static JavaCodegenSettings fromNode(ObjectNode settingsNode) {
return builder.build();
}

/**
* Gets the primary service to generate.
*
* @return the configured service id.
* @see #getService() for a non-throwing variant.
*/
public ShapeId service() {
if (service == null) {
throw new CodegenException(
"No service is configured for this code generation because types-only generation "
+ "has no primary service.");
}
return service;
}

/**
* Gets the primary service to generate, if one is configured.
*
* <p>Types-only generation is driven by a shape closure and has no primary service, so this
* returns an empty optional in that case.
*
* @return the configured service id, or empty if none is set.
*/
public Optional<ShapeId> getService() {
return Optional.ofNullable(service);
}

/**
* Gets the id of a pre-authored shape closure to generate, if one was configured.
*
* <p>When set, generation is driven by the {@code shapeClosures} metadata entry with this id in
* the model, rather than by an inline {@code selector}/{@code shapes} configuration.
*
* @return the configured shape closure id, or empty if none is set.
*/
public Optional<String> getClosure() {
return Optional.ofNullable(closure);
}

public String name() {
return name;
}
Expand Down Expand Up @@ -232,6 +293,7 @@ public static Builder builder() {

public static final class Builder {
private ShapeId service;
private String closure;
private String name;
private String packageNamespace;
private String headerFilePath;
Expand All @@ -254,6 +316,11 @@ public Builder service(String string) {
return this;
}

public Builder closure(String closure) {
this.closure = closure;
return this;
}

public Builder name(String name) {
this.name = name;
return this;
Expand Down
Loading
Loading