diff --git a/codegen/codegen-core/src/internal/java/software/amazon/smithy/java/codegen/JavaTypesCodegenPlugin.java b/codegen/codegen-core/src/internal/java/software/amazon/smithy/java/codegen/JavaTypesCodegenPlugin.java index b7639f4298..68634c7f09 100644 --- a/codegen/codegen-core/src/internal/java/software/amazon/smithy/java/codegen/JavaTypesCodegenPlugin.java +++ b/codegen/codegen-core/src/internal/java/software/amazon/smithy/java/codegen/JavaTypesCodegenPlugin.java @@ -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; /** @@ -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"; @@ -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 getClosure(Model model, TypeCodegenSettings settings) { - Set 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(, [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(); } } diff --git a/codegen/codegen-core/src/internal/java/software/amazon/smithy/java/codegen/TypesDirectedJavaCodegen.java b/codegen/codegen-core/src/internal/java/software/amazon/smithy/java/codegen/TypesDirectedJavaCodegen.java index 812352ce49..86da1e969d 100644 --- a/codegen/codegen-core/src/internal/java/software/amazon/smithy/java/codegen/TypesDirectedJavaCodegen.java +++ b/codegen/codegen-core/src/internal/java/software/amazon/smithy/java/codegen/TypesDirectedJavaCodegen.java @@ -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; /** @@ -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); @@ -68,9 +68,7 @@ public CodeGenerationContext createContext( @Override public void generateStructure(GenerateStructureDirective directive) { - if (!isSynthetic(directive.shape())) { - new StructureGenerator<>().accept(directive); - } + new StructureGenerator<>().accept(directive); } @Override @@ -129,8 +127,4 @@ public void customizeBeforeIntegrations(CustomizeDirective directive) { // No-op for types-only mode } - - private static boolean isSynthetic(Shape shape) { - return shape.getId().getNamespace().equals(SyntheticServiceTransform.SYNTHETIC_NAMESPACE); - } } diff --git a/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/CodeGenerationContext.java b/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/CodeGenerationContext.java index a89ff9ac76..83017547e6 100644 --- a/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/CodeGenerationContext.java +++ b/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/CodeGenerationContext.java @@ -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; @@ -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); @@ -186,32 +185,30 @@ public SchemaFieldOrder schemaFieldOrder() { * * @return Set of trait ShapeId's to include in generated Schemas. */ - private Set collectRuntimeTraits() { - ServiceShape shape = model.expectShape(settings.service()) - .asServiceShape() - .orElseThrow( - () -> new CodegenException( - "Expected shapeId: " - + settings.service() + " to be a service shape.")); - + private Set collectRuntimeTraits(ServiceShape service) { // Add all default runtime traits from the prelude Set traits = new HashSet<>(PRELUDE_RUNTIME_TRAITS); - for (var entry : shape.getAllTraits().entrySet()) { - Optional 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 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()); + } } } diff --git a/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/CodegenUtils.java b/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/CodegenUtils.java index 45015f7317..5986376616 100644 --- a/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/CodegenUtils.java +++ b/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/CodegenUtils.java @@ -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; @@ -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 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 @@ -423,7 +438,7 @@ public static boolean isISO8601Date(String string) { * @return the property if found, or null. */ public static T tryGetServiceProperty(ShapeDirective directive, Property prop) { - var service = directive.service(); + var service = directive.getService().orElse(null); if (service != null) { var symbol = directive.symbolProvider().toSymbol(service); if (symbol != null) { diff --git a/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/JavaCodegenSettings.java b/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/JavaCodegenSettings.java index 8984048fca..60db615f4c 100644 --- a/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/JavaCodegenSettings.java +++ b/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/JavaCodegenSettings.java @@ -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; @@ -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 PROPERTIES = List.of( SERVICE, NAME, @@ -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; @@ -87,8 +97,9 @@ public final class JavaCodegenSettings { private final Map> 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; @@ -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 * @@ -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) @@ -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. + * + *

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 getService() { + return Optional.ofNullable(service); + } + + /** + * Gets the id of a pre-authored shape closure to generate, if one was configured. + * + *

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 getClosure() { + return Optional.ofNullable(closure); + } + public String name() { return name; } @@ -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; @@ -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; diff --git a/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/JavaSymbolProvider.java b/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/JavaSymbolProvider.java index fafa7ef442..af1095bbb9 100644 --- a/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/JavaSymbolProvider.java +++ b/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/JavaSymbolProvider.java @@ -16,6 +16,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import software.amazon.smithy.codegen.core.CodegenException; import software.amazon.smithy.codegen.core.Symbol; @@ -71,6 +72,7 @@ public class JavaSymbolProvider implements ShapeVisitor, SymbolProvider private final Model model; private final ServiceShape service; + private final Map renames; private final String packageNamespace; private final String serviceName; private final Set modes; @@ -79,18 +81,38 @@ public class JavaSymbolProvider implements ShapeVisitor, SymbolProvider public JavaSymbolProvider( Model model, ServiceShape service, + Map renames, String packageNamespace, String serviceName, Set modes ) { this.model = model; this.service = service; + this.renames = renames; this.packageNamespace = packageNamespace; this.serviceName = serviceName; this.modes = modes; this.externalTypes = buildExternalTypes(); } + /** + * Backwards-compatible constructor that derives renames from the service. + * + * @deprecated Use {@link #JavaSymbolProvider(Model, ServiceShape, Map, String, String, Set)} and + * pass renames explicitly (e.g. {@code directive.getRenames()}), which also supports + * closure-driven generation. + */ + @Deprecated + public JavaSymbolProvider( + Model model, + ServiceShape service, + String packageNamespace, + String serviceName, + Set modes + ) { + this(model, service, service.getRename(), packageNamespace, serviceName, modes); + } + private static Map buildExternalTypes() { Map mappings = new HashMap<>(); SchemaIndex.getCombinedSchemaIndex().visit(schema -> { @@ -284,7 +306,7 @@ public Symbol memberShape(MemberShape memberShape) { // name conflicts with the enum class name the suffix value is added after "Type". var memberName = CodegenUtils.toMemberName(memberShape, model); var className = CaseUtils.toPascalCase(memberName) + "Type"; - var targetName = CodegenUtils.getDefaultName(container, service); + var targetName = CodegenUtils.getDefaultName(container, renames); if (targetName.equals(className)) { className = className + "Value"; } @@ -391,16 +413,28 @@ public Symbol unionShape(UnionShape unionShape) { return getJavaClassSymbol(unionShape); } + /** + * @deprecated The service shape is now optional, use {@link #serviceShape()} instead. + */ + @Deprecated protected ServiceShape service() { return service; } + protected Optional serviceShape() { + return Optional.ofNullable(service); + } + + protected Map renames() { + return renames; + } + protected String packageNamespace() { return packageNamespace; } private Symbol getJavaClassSymbol(Shape shape) { - String name = CodegenUtils.getDefaultName(shape, service); + String name = CodegenUtils.getDefaultName(shape, renames); return Symbol.builder() .name(name) .putProperty(SymbolProperties.IS_PRIMITIVE, false) diff --git a/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/SyntheticServiceTransform.java b/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/SyntheticServiceTransform.java deleted file mode 100644 index 3a2f0e9f7d..0000000000 --- a/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/SyntheticServiceTransform.java +++ /dev/null @@ -1,148 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -package software.amazon.smithy.java.codegen; - -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import software.amazon.smithy.java.codegen.generators.SyntheticTrait; -import software.amazon.smithy.java.logging.InternalLogger; -import software.amazon.smithy.model.Model; -import software.amazon.smithy.model.shapes.OperationShape; -import software.amazon.smithy.model.shapes.ServiceShape; -import software.amazon.smithy.model.shapes.Shape; -import software.amazon.smithy.model.shapes.ShapeId; -import software.amazon.smithy.model.shapes.StructureShape; -import software.amazon.smithy.model.traits.ErrorTrait; -import software.amazon.smithy.model.traits.PrivateTrait; -import software.amazon.smithy.model.transform.ModelTransformer; -import software.amazon.smithy.utils.SmithyInternalApi; - -/** - * Generates a synthetic service for a set of shapes. - * - *

Adds a set of shapes to the closure of a synthetic service shape. Operation shapes are added directly - * to the service shape while all other shapes are added via a single synthetic operation with a synthetic input - * that references each type as a member. - * - *

Directed codegen requires a root service shape to use for generating types. This service shape also - * provides renames for a set of shapes as well as the list of protocols the shapes should support. This - * transform creates a synthetic service that Directed codegen can use to generate the provided set of shapes. - */ -@SmithyInternalApi -public final class SyntheticServiceTransform { - private static final InternalLogger LOGGER = InternalLogger.getLogger(SyntheticServiceTransform.class); - public static final String SYNTHETIC_NAMESPACE = "smithy.synthetic"; - static final ShapeId SYNTHETIC_SERVICE_ID = ShapeId.fromParts(SYNTHETIC_NAMESPACE, "TypesGenService"); - - /** - * Types-only mode: creates a new synthetic service wrapping closure shapes. - */ - static Model transform(Model model, Set closure, Map renames) { - - ServiceShape.Builder serviceBuilder = ServiceShape.builder() - .id(SYNTHETIC_SERVICE_ID) - .addTrait(new SyntheticTrait()); - serviceBuilder.rename(renames); - - List typesToWrap = new ArrayList<>(); - List errorShapes = new ArrayList<>(); - - for (Shape shape : closure) { - switch (shape.getType()) { - case SERVICE, RESOURCE -> LOGGER.debug( - "Skipping service-associated shape {} for type codegen...", - shape); - case OPERATION -> serviceBuilder.addOperation(shape.asOperationShape().orElseThrow()); - case STRUCTURE, ENUM, INT_ENUM, UNION -> { - typesToWrap.add(shape); - if (shape.hasTrait(ErrorTrait.class)) { - errorShapes.add(shape); - } - } - default -> { - // All other shapes are skipped with no logging as they should be - // implicitly added by aggregate shapes. - } - } - } - - Set shapesToAdd = new HashSet<>(); - if (!typesToWrap.isEmpty()) { - var syntheticShapes = createSyntheticShapes(typesToWrap, errorShapes); - shapesToAdd.addAll(syntheticShapes); - // Find the operation shape to add to the service - for (Shape s : syntheticShapes) { - if (s instanceof OperationShape op) { - serviceBuilder.addOperation(op); - } - } - } - - shapesToAdd.add(serviceBuilder.build()); - return ModelTransformer.create().replaceShapes(model, shapesToAdd); - } - - /** - * Combined mode: adds a synthetic operation to an existing service to include additional shapes - * in its closure. - */ - static Model expandServiceClosure(Model model, ShapeId serviceId, Set additionalShapes) { - if (additionalShapes.isEmpty()) { - return model; - } - - List errorShapes = new ArrayList<>(); - for (Shape shape : additionalShapes) { - if (shape.hasTrait(ErrorTrait.class)) { - errorShapes.add(shape); - } - } - - var syntheticShapes = createSyntheticShapes(new ArrayList<>(additionalShapes), errorShapes); - - var service = model.expectShape(serviceId, ServiceShape.class); - var serviceBuilder = service.toBuilder(); - Set shapesToAdd = new HashSet<>(syntheticShapes); - for (Shape s : syntheticShapes) { - if (s instanceof OperationShape op) { - serviceBuilder.addOperation(op); - } - } - shapesToAdd.add(serviceBuilder.build()); - - return ModelTransformer.create().replaceShapes(model, shapesToAdd); - } - - private static Set createSyntheticShapes(List typesToWrap, List errorShapes) { - var inputBuilder = StructureShape.builder() - .id(ShapeId.fromParts(SYNTHETIC_NAMESPACE, "TypesOperationInput")) - .addTrait(new SyntheticTrait()); - for (int i = 0; i < typesToWrap.size(); i++) { - inputBuilder.addMember("m" + i, typesToWrap.get(i).getId()); - } - var syntheticInput = inputBuilder.build(); - - var syntheticOutput = StructureShape.builder() - .id(ShapeId.fromParts(SYNTHETIC_NAMESPACE, "TypesOperationOutput")) - .addTrait(new SyntheticTrait()) - .build(); - - var opBuilder = OperationShape.builder() - .id(ShapeId.fromParts(SYNTHETIC_NAMESPACE, "TypesOperation")) - .addTrait(new SyntheticTrait()) - .addTrait(new PrivateTrait()) - .input(syntheticInput) - .output(syntheticOutput); - for (Shape error : errorShapes) { - opBuilder.addError(error.toShapeId()); - } - - return Set.of(syntheticInput, syntheticOutput, opBuilder.build()); - } -} diff --git a/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/TypeCodegenSettings.java b/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/TypeCodegenSettings.java index bef5f887e5..2a82792187 100644 --- a/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/TypeCodegenSettings.java +++ b/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/TypeCodegenSettings.java @@ -78,8 +78,6 @@ private static JavaCodegenSettings getCodegenSettings(ObjectNode node) { // Remove unused properties PROPERTIES.forEach(nodeBuilder::withoutMember); - // Add the synthetic service - nodeBuilder.withMember("service", SyntheticServiceTransform.SYNTHETIC_SERVICE_ID.toString()); return JavaCodegenSettings.fromNode(nodeBuilder.build()); } diff --git a/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/generators/BuilderGenerator.java b/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/generators/BuilderGenerator.java index 15f4a3bb2c..c724b558c6 100644 --- a/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/generators/BuilderGenerator.java +++ b/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/generators/BuilderGenerator.java @@ -6,6 +6,7 @@ package software.amazon.smithy.java.codegen.generators; import java.util.List; +import java.util.Map; import software.amazon.smithy.codegen.core.SymbolProvider; import software.amazon.smithy.java.codegen.CodegenUtils; import software.amazon.smithy.java.codegen.writer.JavaWriter; @@ -14,8 +15,8 @@ import software.amazon.smithy.java.core.schema.ShapeBuilder; import software.amazon.smithy.model.Model; import software.amazon.smithy.model.shapes.MemberShape; -import software.amazon.smithy.model.shapes.ServiceShape; import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.shapes.ShapeId; import software.amazon.smithy.model.shapes.ShapeType; /** @@ -26,20 +27,20 @@ abstract class BuilderGenerator implements Runnable { protected final Shape shape; protected final SymbolProvider symbolProvider; protected final Model model; - protected final ServiceShape service; + protected final Map renames; protected BuilderGenerator( JavaWriter writer, Shape shape, SymbolProvider symbolProvider, Model model, - ServiceShape service + Map renames ) { this.writer = writer; this.shape = shape; this.symbolProvider = symbolProvider; this.model = model; - this.service = service; + this.renames = renames; } @Override @@ -111,7 +112,7 @@ protected void generateErrorCorrection(JavaWriter writer) { } protected void generateDeserialization(JavaWriter writer) { - writer.writeInline("${C|}", new StructureDeserializerGenerator(writer, shape, symbolProvider, model, service)); + writer.writeInline("${C|}", new StructureDeserializerGenerator(writer, shape, symbolProvider, model, renames)); } protected abstract void generateProperties(JavaWriter writer); diff --git a/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/generators/DeserializerGenerator.java b/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/generators/DeserializerGenerator.java index 6b6dde1693..7925728253 100644 --- a/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/generators/DeserializerGenerator.java +++ b/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/generators/DeserializerGenerator.java @@ -5,6 +5,7 @@ package software.amazon.smithy.java.codegen.generators; +import java.util.Map; import software.amazon.smithy.codegen.core.Symbol; import software.amazon.smithy.codegen.core.SymbolProvider; import software.amazon.smithy.java.codegen.CodegenUtils; @@ -27,8 +28,8 @@ import software.amazon.smithy.model.shapes.LongShape; import software.amazon.smithy.model.shapes.MapShape; import software.amazon.smithy.model.shapes.MemberShape; -import software.amazon.smithy.model.shapes.ServiceShape; import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.shapes.ShapeId; import software.amazon.smithy.model.shapes.ShapeVisitor; import software.amazon.smithy.model.shapes.ShortShape; import software.amazon.smithy.model.shapes.StringShape; @@ -43,7 +44,7 @@ final class DeserializerGenerator extends ShapeVisitor.DataShapeVisitor im private final Shape shape; private final SymbolProvider symbolProvider; private final Model model; - private final ServiceShape service; + private final Map renames; private final String deserializer; private final String schemaName; @@ -52,7 +53,7 @@ final class DeserializerGenerator extends ShapeVisitor.DataShapeVisitor im Shape shape, SymbolProvider symbolProvider, Model model, - ServiceShape service, + Map renames, String deserializer, String schemaName ) { @@ -60,7 +61,7 @@ final class DeserializerGenerator extends ShapeVisitor.DataShapeVisitor im this.shape = shape; this.symbolProvider = symbolProvider; this.model = model; - this.service = service; + this.renames = renames; this.deserializer = deserializer; this.schemaName = schemaName; } @@ -94,7 +95,7 @@ public Void booleanShape(BooleanShape booleanShape) { public Void listShape(ListShape listShape) { writer.write( "SharedSerde.deserialize$L(${schemaName:L}, ${deserializer:L})", - CodegenUtils.getDefaultName(listShape, service)); + CodegenUtils.getDefaultName(listShape, renames)); return null; } @@ -102,7 +103,7 @@ public Void listShape(ListShape listShape) { public Void mapShape(MapShape mapShape) { writer.write( "SharedSerde.deserialize$L(${schemaName:L}, ${deserializer:L})", - CodegenUtils.getDefaultName(mapShape, service)); + CodegenUtils.getDefaultName(mapShape, renames)); return null; } diff --git a/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/generators/EnumGenerator.java b/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/generators/EnumGenerator.java index aeca39279f..8d1b3e7b48 100644 --- a/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/generators/EnumGenerator.java +++ b/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/generators/EnumGenerator.java @@ -27,8 +27,8 @@ import software.amazon.smithy.model.Model; import software.amazon.smithy.model.shapes.EnumShape; import software.amazon.smithy.model.shapes.IntEnumShape; -import software.amazon.smithy.model.shapes.ServiceShape; import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.shapes.ShapeId; import software.amazon.smithy.utils.SmithyInternalApi; @SmithyInternalApi @@ -97,7 +97,7 @@ default void serialize(${shapeSerializer:T} serializer) { shape, directive.symbolProvider(), directive.model(), - directive.service())); + directive.getRenames())); writer.writeNullMarkedAnnotation(); writer.write(template); writer.popState(); @@ -250,9 +250,9 @@ private static final class EnumBuilderGenerator extends BuilderGenerator { Shape shape, SymbolProvider symbolProvider, Model model, - ServiceShape service + Map renames ) { - super(writer, shape, symbolProvider, model, service); + super(writer, shape, symbolProvider, model, renames); } @Override diff --git a/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/generators/ListGenerator.java b/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/generators/ListGenerator.java index 1e61958960..9eb34328b3 100644 --- a/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/generators/ListGenerator.java +++ b/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/generators/ListGenerator.java @@ -35,7 +35,7 @@ public void accept(GenerateListDirective writer.onSection("sharedSerde", t -> { - var name = CodegenUtils.getDefaultName(directive.shape(), directive.service()); + var name = CodegenUtils.getDefaultName(directive.shape(), directive.getRenames()); var target = directive.model().expectShape(directive.shape().getMember().getTarget()); var valueSchema = writer.format( "$L.listMember()", @@ -121,7 +121,7 @@ public void accept(${shape:B} state, ${shapeDeserializer:T} deserializer) { target, directive.symbolProvider(), directive.model(), - directive.service(), + directive.getRenames(), "deserializer", valueSchema)); writer.write(template); diff --git a/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/generators/MapGenerator.java b/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/generators/MapGenerator.java index 63797e49e5..35df3dbb71 100644 --- a/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/generators/MapGenerator.java +++ b/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/generators/MapGenerator.java @@ -47,7 +47,7 @@ public void accept(GenerateMapDirective getExceptionSymbols( GenerateOperationDirective directive ) { List symbols = new ArrayList<>(); - for (var errorId : operation.getErrors(directive.service())) { + for (var errorId : operation.getErrors(directive.expectService())) { var shape = directive.model().expectShape(errorId); symbols.add(directive.symbolProvider().toSymbol(shape)); } diff --git a/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/generators/ResourceGenerator.java b/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/generators/ResourceGenerator.java index cfb8c0855e..8c4bb17901 100644 --- a/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/generators/ResourceGenerator.java +++ b/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/generators/ResourceGenerator.java @@ -113,7 +113,8 @@ public final class ${shape:T} implements ${resourceType:T} { shape)); var bottomUpIndex = BottomUpIndex.of(directive.model()); - var resourceOptional = bottomUpIndex.getResourceBinding(directive.service(), shape); + var resourceOptional = + bottomUpIndex.getResourceBinding(directive.expectService(), shape); writer.putContext("hasResource", resourceOptional.isPresent()); resourceOptional.ifPresent( resourceShape -> writer.putContext("resource", diff --git a/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/generators/SchemaFieldOrder.java b/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/generators/SchemaFieldOrder.java index b4aa5d9158..43396c0fb9 100644 --- a/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/generators/SchemaFieldOrder.java +++ b/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/generators/SchemaFieldOrder.java @@ -60,7 +60,6 @@ public SchemaFieldOrder(Directive directive, CodeGenerationContext context) { var index = TopologicalIndex.of(directive.model()); var allShapes = Stream.concat(index.getOrderedShapes().stream(), index.getRecursiveShapes().stream()) .filter(connectedShapes::contains) - .filter(s -> !s.hasTrait(SyntheticTrait.class)) .filter(s -> !EXCLUDED_TYPES.contains(s.getType())) .filter(not(Prelude::isPreludeShape)) .collect(Collectors.toCollection(LinkedHashSet::new)); diff --git a/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/generators/SchemaIndexGenerator.java b/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/generators/SchemaIndexGenerator.java index 6879c5bf06..a4cf00d8f8 100644 --- a/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/generators/SchemaIndexGenerator.java +++ b/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/generators/SchemaIndexGenerator.java @@ -69,8 +69,7 @@ private void generateSchemaIndexClass( } for (var shape : directive.connectedShapes().values()) { if ((shape.getType() == ShapeType.ENUM || shape.getType() == ShapeType.INT_ENUM) - && !Prelude.isPreludeShape(shape) - && !shape.hasTrait(SyntheticTrait.class)) { + && !Prelude.isPreludeShape(shape)) { var symbol = directive.symbolProvider().toSymbol(shape); if (!symbol.getProperty(SymbolProperties.EXTERNAL_TYPE).orElse(false)) { totalCount++; @@ -138,8 +137,7 @@ public void run() { .values() .stream() .anyMatch(shape -> (shape.getType() == ShapeType.ENUM || shape.getType() == ShapeType.INT_ENUM) - && !Prelude.isPreludeShape(shape) - && !shape.hasTrait(SyntheticTrait.class)); + && !Prelude.isPreludeShape(shape)); if (hasEnums) { writer.write("_initEnums();"); } @@ -180,8 +178,7 @@ public void run() { boolean hasEnums = false; for (var shape : directive.connectedShapes().values()) { if ((shape.getType() == ShapeType.ENUM || shape.getType() == ShapeType.INT_ENUM) - && !Prelude.isPreludeShape(shape) - && !shape.hasTrait(SyntheticTrait.class)) { + && !Prelude.isPreludeShape(shape)) { hasEnums = true; break; } @@ -190,8 +187,7 @@ public void run() { writer.openBlock("private static void _initEnums() {"); for (var shape : directive.connectedShapes().values()) { if ((shape.getType() == ShapeType.ENUM || shape.getType() == ShapeType.INT_ENUM) - && !Prelude.isPreludeShape(shape) - && !shape.hasTrait(SyntheticTrait.class)) { + && !Prelude.isPreludeShape(shape)) { var symbol = directive.symbolProvider().toSymbol(shape); if (symbol.getProperty(SymbolProperties.EXTERNAL_TYPE).orElse(false)) { continue; diff --git a/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/generators/SerializerMemberGenerator.java b/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/generators/SerializerMemberGenerator.java index 47287874b5..357fba78c9 100644 --- a/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/generators/SerializerMemberGenerator.java +++ b/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/generators/SerializerMemberGenerator.java @@ -5,6 +5,7 @@ package software.amazon.smithy.java.codegen.generators; +import java.util.Map; import software.amazon.smithy.codegen.core.SymbolProvider; import software.amazon.smithy.codegen.core.directed.ContextualDirective; import software.amazon.smithy.java.codegen.CodeGenerationContext; @@ -28,8 +29,8 @@ import software.amazon.smithy.model.shapes.LongShape; import software.amazon.smithy.model.shapes.MapShape; import software.amazon.smithy.model.shapes.MemberShape; -import software.amazon.smithy.model.shapes.ServiceShape; import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.shapes.ShapeId; import software.amazon.smithy.model.shapes.ShapeVisitor; import software.amazon.smithy.model.shapes.ShortShape; import software.amazon.smithy.model.shapes.StringShape; @@ -45,7 +46,7 @@ final class SerializerMemberGenerator extends ShapeVisitor.DataShapeVisitor renames; private final Shape shape; private final String state; private final ContextualDirective directive; @@ -71,7 +72,7 @@ final class SerializerMemberGenerator extends ShapeVisitor.DataShapeVisitor renames) implements Runnable { @Override public void run() { @@ -78,7 +79,7 @@ private void generateMemberSwitchCases(JavaWriter writer) { writer.write( "case $L -> builder.${memberName:L}($C);", idx, - new DeserializerGenerator(writer, member, symbolProvider, model, service, "de", "member")); + new DeserializerGenerator(writer, member, symbolProvider, model, renames, "de", "member")); writer.popState(); } } diff --git a/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/generators/StructureGenerator.java b/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/generators/StructureGenerator.java index 20efd40e46..0604b182dd 100644 --- a/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/generators/StructureGenerator.java +++ b/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/generators/StructureGenerator.java @@ -18,6 +18,7 @@ import java.util.Comparator; import java.util.LinkedHashSet; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.function.Consumer; import software.amazon.smithy.codegen.core.Symbol; @@ -62,8 +63,8 @@ import software.amazon.smithy.model.shapes.LongShape; import software.amazon.smithy.model.shapes.MapShape; import software.amazon.smithy.model.shapes.MemberShape; -import software.amazon.smithy.model.shapes.ServiceShape; import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.shapes.ShapeId; import software.amazon.smithy.model.shapes.ShapeType; import software.amazon.smithy.model.shapes.ShapeVisitor; import software.amazon.smithy.model.shapes.ShortShape; @@ -173,8 +174,7 @@ public final class ${shape:T}${classModifiers:C} { writer, shape, directive.symbolProvider(), - directive.model(), - directive.service())); + directive.model())); writer.putContext( "builder", new StructureBuilderGenerator( @@ -182,7 +182,7 @@ public final class ${shape:T}${classModifiers:C} { shape, directive.symbolProvider(), directive.model(), - directive.service())); + directive.getRenames())); writer.putContext("getMemberValue", new GetMemberValueGenerator(writer, directive.symbolProvider(), shape)); writer.putContext("toBuilder", new ToBuilderGenerator(writer, shape, directive.symbolProvider())); writer.writeNullMarkedAnnotation(); @@ -658,9 +658,9 @@ private static final class StructureBuilderGenerator extends BuilderGenerator { Shape shape, SymbolProvider symbolProvider, Model model, - ServiceShape service + Map renames ) { - super(writer, shape, symbolProvider, model, service); + super(writer, shape, symbolProvider, model, renames); } // Required shapes marked with clientOptional should not be required to create the type. For these shapes, diff --git a/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/generators/StructureSerializerGenerator.java b/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/generators/StructureSerializerGenerator.java index 632f8ff8ff..8ca3b1bb6c 100644 --- a/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/generators/StructureSerializerGenerator.java +++ b/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/generators/StructureSerializerGenerator.java @@ -14,7 +14,6 @@ import software.amazon.smithy.java.core.schema.SerializableShape; import software.amazon.smithy.java.core.serde.ShapeSerializer; import software.amazon.smithy.model.Model; -import software.amazon.smithy.model.shapes.ServiceShape; import software.amazon.smithy.model.shapes.StructureShape; import software.amazon.smithy.model.traits.ErrorTrait; @@ -28,8 +27,7 @@ record StructureSerializerGenerator( JavaWriter writer, StructureShape shape, SymbolProvider symbolProvider, - Model model, - ServiceShape service) implements Runnable { + Model model) implements Runnable { @Override public void run() { diff --git a/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/generators/SyntheticTrait.java b/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/generators/SyntheticTrait.java deleted file mode 100644 index 01b216d8a8..0000000000 --- a/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/generators/SyntheticTrait.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -package software.amazon.smithy.java.codegen.generators; - -import software.amazon.smithy.model.node.Node; -import software.amazon.smithy.model.shapes.ShapeId; -import software.amazon.smithy.model.traits.Trait; -import software.amazon.smithy.utils.SmithyInternalApi; - -@SmithyInternalApi -public final class SyntheticTrait implements Trait { - @Override - public ShapeId toShapeId() { - return null; - } - - @Override - public boolean isSynthetic() { - return true; - } - - @Override - public Node toNode() { - return null; - } -} diff --git a/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/generators/UnionGenerator.java b/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/generators/UnionGenerator.java index 2860c167d4..e6196ea4fb 100644 --- a/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/generators/UnionGenerator.java +++ b/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/generators/UnionGenerator.java @@ -7,6 +7,7 @@ import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.function.Consumer; import software.amazon.smithy.codegen.core.SymbolProvider; @@ -24,8 +25,8 @@ import software.amazon.smithy.java.core.serde.ShapeSerializer; import software.amazon.smithy.model.Model; import software.amazon.smithy.model.shapes.MemberShape; -import software.amazon.smithy.model.shapes.ServiceShape; import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.shapes.ShapeId; import software.amazon.smithy.model.shapes.UnionShape; import software.amazon.smithy.model.traits.UnitTypeTrait; import software.amazon.smithy.utils.SmithyInternalApi; @@ -88,8 +89,7 @@ default T getMemberValue(${sdkSchema:T} member) { writer, shape, directive.symbolProvider(), - directive.model(), - directive.service())); + directive.model())); writer.putContext( "builder", new UnionBuilderGenerator( @@ -97,7 +97,7 @@ default T getMemberValue(${sdkSchema:T} member) { shape, directive.symbolProvider(), directive.model(), - directive.service())); + directive.getRenames())); writer.writeNullMarkedAnnotation(); writer.write(template); writer.popState(); @@ -109,8 +109,7 @@ private record ValueClassGenerator( JavaWriter writer, UnionShape shape, SymbolProvider symbolProvider, - Model model, - ServiceShape service) implements Runnable { + Model model) implements Runnable { @Override public void run() { @@ -221,9 +220,9 @@ private static final class UnionBuilderGenerator extends BuilderGenerator { Shape shape, SymbolProvider symbolProvider, Model model, - ServiceShape service + Map renames ) { - super(writer, shape, symbolProvider, model, service); + super(writer, shape, symbolProvider, model, renames); } @Override diff --git a/codegen/codegen-core/src/test/java/software/amazon/smithy/java/codegen/JavaCodegenSettingsTest.java b/codegen/codegen-core/src/test/java/software/amazon/smithy/java/codegen/JavaCodegenSettingsTest.java new file mode 100644 index 0000000000..2ab6731d2f --- /dev/null +++ b/codegen/codegen-core/src/test/java/software/amazon/smithy/java/codegen/JavaCodegenSettingsTest.java @@ -0,0 +1,53 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.java.codegen; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +class JavaCodegenSettingsTest { + + @Test + void serviceLessWithoutNameDefaultsToLegacyName() { + // Types-only generation has no service to derive a name from. Rather than failing, it falls + // back to the legacy default (and logs a warning) so existing nameless configs keep working. + var settings = JavaCodegenSettings.builder() + .packageNamespace("com.example.standalone") + .build(); + assertThat(settings.getService()).isEmpty(); + assertThat(settings.name()).isEqualTo("TypesGenService"); + } + + @Test + void serviceLessWithExplicitNameSucceeds() { + var settings = JavaCodegenSettings.builder() + .packageNamespace("com.example.standalone") + .name("standalone") + .build(); + assertThat(settings.getService()).isEmpty(); + assertThat(settings.name()).isEqualTo("Standalone"); + } + + @Test + void withServiceDerivesNameFromService() { + var settings = JavaCodegenSettings.builder() + .packageNamespace("com.example") + .service("com.example#MyService") + .build(); + assertThat(settings.name()).isEqualTo("MyService"); + } + + @Test + void explicitNameTakesPrecedenceOverService() { + var settings = JavaCodegenSettings.builder() + .packageNamespace("com.example") + .service("com.example#MyService") + .name("override") + .build(); + assertThat(settings.name()).isEqualTo("Override"); + } +} diff --git a/codegen/codegen-plugin/build.gradle.kts b/codegen/codegen-plugin/build.gradle.kts index 6e9ae18c73..38152daabc 100644 --- a/codegen/codegen-plugin/build.gradle.kts +++ b/codegen/codegen-plugin/build.gradle.kts @@ -64,6 +64,8 @@ addGenerateSrcsTask("software.amazon.smithy.java.codegen.client.TestServerJavaCl addGenerateSrcsTask("software.amazon.smithy.java.codegen.server.TestServerJavaCodegenRunner", "Server", null) // Types codegen test runner addGenerateSrcsTask("software.amazon.smithy.java.codegen.types.TestJavaTypeCodegenRunner", "Types", null) +// Combined client + types codegen test runner +addGenerateSrcsTask("software.amazon.smithy.java.codegen.combined.TestCombinedModeCodegenRunner", "Combined", null) sourceSets { it { @@ -85,11 +87,12 @@ tasks.named("compileJmhJava") { jmh {} // Ensure generate tasks that use it source set resources depend on base generateSources -listOf("generateSourcesClient", "generateSourcesServer", "generateSourcesTypes").forEach { taskName -> - tasks.named(taskName) { - dependsOn("generateSources") +listOf("generateSourcesClient", "generateSourcesServer", "generateSourcesTypes", "generateSourcesCombined") + .forEach { taskName -> + tasks.named(taskName) { + dependsOn("generateSources") + } } -} tasks.test { failOnNoDiscoveredTests = false diff --git a/codegen/codegen-plugin/src/it/java/software/amazon/smithy/java/codegen/combined/CombinedModeIntegTest.java b/codegen/codegen-plugin/src/it/java/software/amazon/smithy/java/codegen/combined/CombinedModeIntegTest.java new file mode 100644 index 0000000000..e87e0fd224 --- /dev/null +++ b/codegen/codegen-plugin/src/it/java/software/amazon/smithy/java/codegen/combined/CombinedModeIntegTest.java @@ -0,0 +1,52 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.java.codegen.combined; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; +import smithy.java.codegen.combined.it.client.CombinedItServiceClient; +import smithy.java.codegen.combined.it.model.Gadget; +import smithy.java.codegen.combined.it.model.StandaloneType; +import software.amazon.smithy.java.core.serde.document.Document; + +/** + * Verifies combined {@code [client, types]} generation produces code that actually compiles and + * runs. The mere ability to import these classes proves the generated sources compiled: + *

    + *
  • {@link CombinedItServiceClient} - the service was generated as a client.
  • + *
  • {@code Gadget} - the {@code Widget} shape was renamed via the service {@code rename} + * block, so the rename threaded all the way through to the generated class name (there is + * no {@code Widget} class to import).
  • + *
  • {@link StandaloneType} - an unconnected type a plain service walk would not reach was + * still generated.
  • + *
+ */ +public class CombinedModeIntegTest { + + @Test + void renamedTypeRoundTrips() { + var gadget = Gadget.builder().name("a").build(); + var output = Gadget.builder(); + Document.of(gadget).deserializeInto(output); + assertEquals(gadget, output.build()); + } + + @Test + void standaloneTypeRoundTrips() { + var standalone = StandaloneType.builder().value("v").build(); + var output = StandaloneType.builder(); + Document.of(standalone).deserializeInto(output); + assertEquals(standalone, output.build()); + } + + @Test + void clientInterfaceIsGenerated() { + // Referencing the generated client type confirms combined mode emitted compilable client + // code alongside the data shapes. + assertEquals("CombinedItServiceClient", CombinedItServiceClient.class.getSimpleName()); + } +} diff --git a/codegen/codegen-plugin/src/main/java/software/amazon/smithy/java/codegen/DirectedJavaCodegen.java b/codegen/codegen-plugin/src/main/java/software/amazon/smithy/java/codegen/DirectedJavaCodegen.java index bf5900fd5c..a61e0bc603 100644 --- a/codegen/codegen-plugin/src/main/java/software/amazon/smithy/java/codegen/DirectedJavaCodegen.java +++ b/codegen/codegen-plugin/src/main/java/software/amazon/smithy/java/codegen/DirectedJavaCodegen.java @@ -21,6 +21,7 @@ import software.amazon.smithy.codegen.core.directed.GenerateServiceDirective; import software.amazon.smithy.codegen.core.directed.GenerateStructureDirective; import software.amazon.smithy.codegen.core.directed.GenerateUnionDirective; +import software.amazon.smithy.codegen.core.directed.ShapeDirective; import software.amazon.smithy.java.codegen.client.generators.BddFileGenerator; import software.amazon.smithy.java.codegen.client.generators.ClientImplementationGenerator; import software.amazon.smithy.java.codegen.client.generators.ClientInterfaceGenerator; @@ -39,7 +40,7 @@ import software.amazon.smithy.java.codegen.generators.UnionGenerator; import software.amazon.smithy.java.codegen.server.generators.OperationInterfaceGenerator; import software.amazon.smithy.java.codegen.server.generators.ServiceGenerator; -import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.knowledge.TopDownIndex; import software.amazon.smithy.model.shapes.ShapeId; import software.amazon.smithy.utils.SmithyInternalApi; import software.amazon.smithy.utils.SmithyUnstableApi; @@ -66,7 +67,8 @@ public SymbolProvider createSymbolProvider( ) { return new JavaSymbolProvider( directive.model(), - directive.service(), + directive.getService().orElse(null), + directive.getRenames(), directive.settings().packageNamespace(), directive.settings().name(), modes); @@ -95,9 +97,7 @@ private String getPluginName() { @Override public void generateStructure(GenerateStructureDirective directive) { - if (!isSynthetic(directive.shape())) { - new StructureGenerator<>().accept(directive); - } + new StructureGenerator<>().accept(directive); } @Override @@ -132,10 +132,13 @@ public void generateIntEnumShape(GenerateIntEnumDirective directive) { - if (isSynthetic(directive.shape())) { + if (isTypesOnly()) { return; } - if (isTypesOnly()) { + // A closure can include operations that are not bound to the primary service. Until + // multi-service generation is supported, skip them so we don't emit operations bound to + // the wrong service. + if (notBoundToPrimaryService(directive)) { return; } if (modes.contains(CodegenMode.SERVER)) { @@ -146,10 +149,17 @@ public void generateOperation(GenerateOperationDirective directive) { - // In TYPES-only mode, generateService is a no-op (the synthetic service has no real service shape) + // In types-only mode there is no service to generate. The director also skips this call when + // generating data shapes only, but it can't hurt to double check. if (isTypesOnly()) { return; } + // A closure can include services other than the primary one. Until multi-service + // generation is supported, generate only the primary service. + var primaryService = directive.getService().orElse(null); + if (primaryService != null && !directive.shape().equals(primaryService)) { + return; + } if (modes.contains(CodegenMode.CLIENT)) { new ClientInterfaceGenerator().accept(directive); @@ -164,7 +174,7 @@ public void generateService(GenerateServiceDirective().accept(directive); if (modes.contains(CodegenMode.CLIENT)) { - var service = directive.service(); + var service = directive.expectService(); if (service.hasTrait(ShapeId.from("smithy.rules#endpointBdd")) || service.hasTrait(ShapeId.from("smithy.rules#endpointRuleSet"))) { new BddFileGenerator().accept(directive); @@ -174,6 +184,11 @@ public void generateService(GenerateServiceDirective directive) { + // A closure can include resources not bound to the primary service. Until multi-service + // generation is supported, skip them. + if (notBoundToPrimaryService(directive)) { + return; + } new ResourceGenerator().accept(directive); } @@ -192,13 +207,25 @@ public void customizeAfterIntegrations(CustomizeDirective directive) { + var service = directive.getService().orElse(null); + if (service == null) { + return false; + } + var index = TopDownIndex.of(directive.model()); + var shape = directive.shape(); + return !index.getContainedOperations(service).contains(shape) + && !index.getContainedResources(service).contains(shape); + } } diff --git a/codegen/codegen-plugin/src/main/java/software/amazon/smithy/java/codegen/JavaCodegenPlugin.java b/codegen/codegen-plugin/src/main/java/software/amazon/smithy/java/codegen/JavaCodegenPlugin.java index ae9d77ff0f..97c5361ce3 100644 --- a/codegen/codegen-plugin/src/main/java/software/amazon/smithy/java/codegen/JavaCodegenPlugin.java +++ b/codegen/codegen-plugin/src/main/java/software/amazon/smithy/java/codegen/JavaCodegenPlugin.java @@ -6,9 +6,11 @@ package software.amazon.smithy.java.codegen; import java.util.EnumSet; -import java.util.HashSet; +import java.util.List; import java.util.Locale; import java.util.Set; +import java.util.StringJoiner; +import java.util.TreeSet; import java.util.stream.Collectors; import software.amazon.smithy.build.PluginContext; import software.amazon.smithy.build.SmithyBuildPlugin; @@ -16,16 +18,15 @@ import software.amazon.smithy.codegen.core.directed.CodegenDirector; import software.amazon.smithy.framework.knowledge.ImplicitErrorIndex; import software.amazon.smithy.framework.transform.AddFrameworkErrorsTransform; -import software.amazon.smithy.java.codegen.generators.SyntheticTrait; 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.metadata.ShapeClosure; import software.amazon.smithy.model.neighbor.Walker; import software.amazon.smithy.model.node.ObjectNode; import software.amazon.smithy.model.selector.Selector; import software.amazon.smithy.model.shapes.ServiceShape; -import software.amazon.smithy.model.shapes.Shape; import software.amazon.smithy.model.shapes.ShapeId; import software.amazon.smithy.model.traits.MixinTrait; import software.amazon.smithy.model.traits.TraitDefinition; @@ -35,6 +36,21 @@ /** * Unified plugin for Java code generation. Supports modes: client, server, types. * + *

The {@code modes} setting selects what is generated and how generation is driven: + *

    + *
  • Service mode ({@code ["client"]}, {@code ["server"]}, or both) - generation is + * driven by the {@code service}, which is required. Produces the client and/or server for + * that service and every shape in its closure.
  • + *
  • Types mode ({@code ["types"]}) - generation is driven by a shape closure built + * from the {@code selector}/{@code shapes} settings, with no {@code service}. Produces only + * data shapes (structures, unions, enums, intEnums, lists, maps). A {@code name} is used + * here only as a label and is not required.
  • + *
  • Combined mode ({@code ["types"]} alongside {@code ["client"]} and/or + * {@code ["server"]}) - generates the service (as in service mode) plus any + * standalone data shapes that are not reachable from the service. The {@code service} is + * still required and remains the primary service.
  • + *
+ * *

Configure via {@code smithy-build.json}: *

{@code
  * {
@@ -52,6 +68,15 @@
 public final class JavaCodegenPlugin implements SmithyBuildPlugin {
     private static final InternalLogger LOGGER = InternalLogger.getLogger(JavaCodegenPlugin.class);
 
+    // Id of the shape closure that drives types and combined-mode 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";
+
+    // The `closure` setting references a pre-authored shape closure by id; the settings below define
+    // a closure inline. The two are mutually exclusive.
+    private static final String CLOSURE = "closure";
+    private static final List INLINE_CLOSURE_SETTINGS = List.of("selector", "shapes", "renames");
+
     @Override
     public String getName() {
         return "java-codegen";
@@ -63,6 +88,8 @@ public void execute(PluginContext context) {
         var modes = parseModes(settingsNode);
         LOGGER.info("Running java-codegen with modes: {}", modes);
 
+        validateClosureSettings(settingsNode, modes);
+
         if (modes.contains(CodegenMode.TYPES)
                 && !modes.contains(CodegenMode.CLIENT)
                 && !modes.contains(CodegenMode.SERVER)) {
@@ -72,6 +99,42 @@ public void execute(PluginContext context) {
         }
     }
 
+    // Validates the closure-related settings uniformly across all modes (types and combined), since
+    // a closure can drive either. Done against the raw settings node so the rules apply before the
+    // mode-specific settings objects are built.
+    private static void validateClosureSettings(ObjectNode settingsNode, Set modes) {
+        boolean hasClosure = settingsNode.getMember(CLOSURE).isPresent();
+
+        // A `closure` only drives the set of generated types, so it is meaningless without TYPES mode.
+        if (hasClosure && !modes.contains(CodegenMode.TYPES)) {
+            throw new CodegenException("The `closure` setting requires the `types` mode, but modes were: " + modes);
+        }
+
+        if (!hasInlineClosureSettings(settingsNode)) {
+            return;
+        }
+
+        // A pre-authored closure fully defines what to generate, so the inline settings must not also
+        // be set; otherwise nudge users that the inline definition could live in the model instead.
+        if (hasClosure) {
+            throw new CodegenException("The `closure` setting references a pre-authored shape closure and "
+                    + "cannot be combined with the inline " + INLINE_CLOSURE_SETTINGS + " setting(s).");
+        }
+        LOGGER.warn("The {} setting(s) define a shape closure inline. Consider authoring a `shapeClosures` "
+                + "entry in your model and referencing it with the `closure` setting so the closure travels "
+                + "with the model.", INLINE_CLOSURE_SETTINGS);
+    }
+
+    // True if any inline closure setting (selector/shapes/renames) is present.
+    private static boolean hasInlineClosureSettings(ObjectNode settingsNode) {
+        for (var setting : INLINE_CLOSURE_SETTINGS) {
+            if (settingsNode.getMember(setting).isPresent()) {
+                return true;
+            }
+        }
+        return false;
+    }
+
     private void setIntegrationsSettings(CodegenDirector runner, ObjectNode settingsNode) {
         settingsNode.asObjectNode()
                 .flatMap(node -> node.getObjectMember("integrations"))
@@ -91,7 +154,16 @@ private void executeServiceMode(PluginContext context, ObjectNode settingsNode,
         // TODO: use built-in once this has been upstreamed
         var model = AddFrameworkErrorsTransform.transform(ModelTransformer.create(), context.getModel());
         if (modes.contains(CodegenMode.TYPES)) {
-            model = expandServiceClosureForTypes(model, settings.service());
+            // Combined mode: the service stays the primary service (so client/server generation is
+            // unchanged) but the generated set also includes standalone types. Either drive from a
+            // pre-authored closure referenced by id (the director enforces the service is a member),
+            // or build a closure of the service plus any standalone types not reachable from it.
+            var closure = settings.getClosure();
+            if (closure.isPresent()) {
+                runner.shapeClosure(closure.get());
+            } else {
+                runner.shapeClosure(combinedClosure(model, settings.service()));
+            }
         }
         validateDependencies(modes, model, settings);
         runner.model(model);
@@ -112,13 +184,18 @@ private void executeTypesMode(PluginContext context, ObjectNode settingsNode, Se
         setIntegrationsSettings(runner, settingsNode);
         runner.directedCodegen(new DirectedJavaCodegen(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);
+        // Drive generation from a pre-authored closure when one is referenced by id, otherwise build
+        // the closure from the inline selector/shapes settings. Either way only data shapes are
+        // generated, and the director resolves the closure's transitive data shapes.
+        var closure = codegenSettings.getClosure();
+        if (closure.isPresent()) {
+            runner.shapeClosure(closure.get());
+        } else {
+            runner.shapeClosure(typesClosure(settings));
+        }
+        runner.generateDataShapesOnly();
+        runner.model(context.getModel());
         runner.integrationClass(JavaCodegenIntegration.class);
         DefaultTransforms.apply(runner, codegenSettings);
         runner.run();
@@ -126,34 +203,73 @@ private void executeTypesMode(PluginContext context, ObjectNode settingsNode, Se
     }
 
     /**
-     * Expands the service closure to include all model types not already reachable from the service.
-     * This is used when TYPES mode is combined with CLIENT or SERVER mode, so that standalone types
-     * outside the service closure are also generated.
+     * Builds the generation closure for combined TYPES + CLIENT/SERVER mode: the full closure of
+     * the primary service plus any standalone types not reachable from the service. The service is
+     * included by id so directed traversal pulls in its operations, I/O, and connected shapes; the
+     * extra standalone types are folded in by id. The service's renames carry over so naming is
+     * unchanged from a plain service build.
      */
-    private static Model expandServiceClosureForTypes(Model model, ShapeId serviceId) {
+    private static ShapeClosure combinedClosure(Model model, ShapeId serviceId) {
         var service = model.expectShape(serviceId, ServiceShape.class);
-        var walker = new Walker(model);
-        var serviceClosure = walker.walkShapes(service);
+        var serviceClosure = new Walker(model).walkShapes(service);
         var implicitErrorIndex = ImplicitErrorIndex.of(model);
 
-        // Default types selector: all structures, unions, enums, and intEnums
-        var selector = Selector.parse(":is(structure, union, enum, intEnum)");
-        var allTypes = selector.shapes(model)
-                .filter(s -> !s.isMemberShape())
-                .filter(s -> !Prelude.isPreludeShape(s))
-                .filter(s -> !s.hasTrait(MixinTrait.class))
-                .filter(s -> !s.hasTrait(TraitDefinition.class))
-                .filter(s -> !s.hasTrait(SyntheticTrait.class))
-                .filter(s -> !implicitErrorIndex.isImplicitError(s.getId()))
-                .filter(s -> !serviceClosure.contains(s))
-                .collect(Collectors.toSet());
-
-        if (allTypes.isEmpty()) {
-            return model;
+        // Default types selector: all structures, unions, enums, and intEnums not already reachable
+        // from the service. Mixins, trait definitions, and implicit (framework) errors are excluded
+        // so they are not emitted as standalone POJOs.
+        var standaloneTypes = new TreeSet();
+        for (var shape : Selector.parse(":is(structure, union, enum, intEnum)").select(model)) {
+            if (shape.isMemberShape()
+                    || Prelude.isPreludeShape(shape)
+                    || shape.hasTrait(MixinTrait.class)
+                    || shape.hasTrait(TraitDefinition.class)
+                    || implicitErrorIndex.isImplicitError(shape.getId())
+                    || serviceClosure.contains(shape)) {
+                continue;
+            }
+            standaloneTypes.add(shape.getId());
         }
 
-        LOGGER.info("Expanding service closure with {} additional type shapes for TYPES mode", allTypes.size());
-        return SyntheticServiceTransform.expandServiceClosure(model, serviceId, allTypes);
+        LOGGER.info("Generating service closure plus {} standalone type shapes for TYPES mode",
+                standaloneTypes.size());
+
+        // Match the service (which pulls in its full directed closure) and each standalone type by id.
+        var selector = new StringBuilder(":is([id='").append(serviceId).append("']");
+        for (var id : standaloneTypes) {
+            selector.append(", [id='").append(id).append("']");
+        }
+        selector.append(")");
+
+        return ShapeClosure.builder()
+                .id(CLOSURE_ID)
+                .includeBySelector(selector.toString())
+                .rename(service.getRename())
+                .build();
+    }
+
+    /**
+     * Builds the types-only generation closure 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(, [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();
+        }
+        return ShapeClosure.builder()
+                .id(CLOSURE_ID)
+                // Exclude trait definitions so only data shapes are generated.
+                .includeBySelector(base + " :not([trait|trait])")
+                .rename(settings.renames())
+                .build();
     }
 
     private static Set parseModes(ObjectNode settingsNode) {
@@ -221,24 +337,4 @@ private static void requireDependency(String className, String moduleName, Strin
                             + "Add 'software.amazon.smithy.java:" + moduleName + "' to your smithyBuild dependencies.");
         }
     }
-
-    private static Set getClosure(Model model, TypeCodegenSettings settings) {
-        Set closure = new HashSet<>();
-        settings.shapes()
-                .stream()
-                .map(model::expectShape)
-                .forEach(closure::add);
-        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");
-        }
-        LOGGER.info("Found {} shapes in generation closure.", closure.size());
-
-        return closure;
-    }
 }
diff --git a/codegen/codegen-plugin/src/main/java/software/amazon/smithy/java/codegen/client/generators/BddFileGenerator.java b/codegen/codegen-plugin/src/main/java/software/amazon/smithy/java/codegen/client/generators/BddFileGenerator.java
index d68c3bc036..fa110f08e0 100644
--- a/codegen/codegen-plugin/src/main/java/software/amazon/smithy/java/codegen/client/generators/BddFileGenerator.java
+++ b/codegen/codegen-plugin/src/main/java/software/amazon/smithy/java/codegen/client/generators/BddFileGenerator.java
@@ -24,8 +24,9 @@ public final class BddFileGenerator
         implements Consumer> {
     @Override
     public void accept(GenerateServiceDirective directive) {
-        var serviceName = directive.service().toShapeId().getName();
-        var bytecode = compileBytecode(directive.service());
+        var service = directive.expectService();
+        var serviceName = service.toShapeId().getName();
+        var bytecode = compileBytecode(service);
         directive.fileManifest()
                 .writeFile(
                         format("./resources/META-INF/endpoints/%s.bdd", serviceName),
diff --git a/codegen/codegen-plugin/src/main/java/software/amazon/smithy/java/codegen/client/generators/ClientImplementationGenerator.java b/codegen/codegen-plugin/src/main/java/software/amazon/smithy/java/codegen/client/generators/ClientImplementationGenerator.java
index bb06cc00da..18c665ea9c 100644
--- a/codegen/codegen-plugin/src/main/java/software/amazon/smithy/java/codegen/client/generators/ClientImplementationGenerator.java
+++ b/codegen/codegen-plugin/src/main/java/software/amazon/smithy/java/codegen/client/generators/ClientImplementationGenerator.java
@@ -20,7 +20,6 @@
 import software.amazon.smithy.java.codegen.CodeGenerationContext;
 import software.amazon.smithy.java.codegen.CodegenUtils;
 import software.amazon.smithy.java.codegen.JavaCodegenSettings;
-import software.amazon.smithy.java.codegen.SyntheticServiceTransform;
 import software.amazon.smithy.java.codegen.client.sections.ClientImplAdditionalMethodsSection;
 import software.amazon.smithy.java.codegen.client.waiters.WaiterCodegenUtils;
 import software.amazon.smithy.java.codegen.generators.TypeRegistryGenerator;
@@ -92,7 +91,7 @@ final class ${impl:T} extends ${client:T} implements ${interface:T} {${?implicit
             var errorSymbols = getImplicitErrorSymbols(
                     directive.symbolProvider(),
                     directive.model(),
-                    directive.service());
+                    directive.expectService());
             writer.putContext("implicitErrors", !errorSymbols.isEmpty());
             writer.putContext(
                     "typeRegistry",
@@ -134,9 +133,6 @@ public void run() {
             writer.putContext("overrideConfig", RequestOverrideConfig.class);
             var opIndex = OperationIndex.of(model);
             for (var operation : TopDownIndex.of(model).getContainedOperations(service)) {
-                if (operation.getId().getNamespace().equals(SyntheticServiceTransform.SYNTHETIC_NAMESPACE)) {
-                    continue;
-                }
                 writer.pushState();
                 writer.putContext("name", StringUtils.uncapitalize(CodegenUtils.getDefaultName(operation, service)));
                 writer.putContext("operation", symbolProvider.toSymbol(operation));
diff --git a/codegen/codegen-plugin/src/main/java/software/amazon/smithy/java/codegen/client/generators/ClientInterfaceGenerator.java b/codegen/codegen-plugin/src/main/java/software/amazon/smithy/java/codegen/client/generators/ClientInterfaceGenerator.java
index 0782fb448e..da9b9c7611 100644
--- a/codegen/codegen-plugin/src/main/java/software/amazon/smithy/java/codegen/client/generators/ClientInterfaceGenerator.java
+++ b/codegen/codegen-plugin/src/main/java/software/amazon/smithy/java/codegen/client/generators/ClientInterfaceGenerator.java
@@ -34,7 +34,6 @@
 import software.amazon.smithy.java.codegen.CodegenUtils;
 import software.amazon.smithy.java.codegen.JavaCodegenSettings;
 import software.amazon.smithy.java.codegen.SymbolProperties;
-import software.amazon.smithy.java.codegen.SyntheticServiceTransform;
 import software.amazon.smithy.java.codegen.client.sections.ClientInterfaceAdditionalMethodsSection;
 import software.amazon.smithy.java.codegen.client.waiters.WaiterCodegenUtils;
 import software.amazon.smithy.java.codegen.integrations.core.GenericTraitInitializer;
@@ -171,12 +170,13 @@ final class RequestOverrideBuilder extends ${requestOverride:T}.OverrideBuilder<
                     writer.putContext("hasDefaultProtocol", defaultProtocolTrait != null);
                     writer.putContext("protocolFactory",
                             new FactoryGenerator(writer, getFactory(defaultProtocolTrait)));
+                    var service = directive.expectService();
                     writer.putContext(
                             "defaultProtocol",
                             new DefaultProtocolGenerator(
                                     writer,
                                     settings.service(),
-                                    directive.service().getVersion(),
+                                    service.getVersion(),
                                     defaultProtocolTrait,
                                     directive.context()));
                     writer.putContext("clientPlugin", ClientPlugin.class);
@@ -187,10 +187,10 @@ final class RequestOverrideBuilder extends ${requestOverride:T}.OverrideBuilder<
                     writer.putContext("impl", symbol.expectProperty(ClientSymbolProperties.CLIENT_IMPL));
                     writer.putContext("hasDefaultTransport", settings.transport() != null);
                     writer.putContext("hasBdd",
-                            directive.service().hasTrait(ENDPOINT_BDD_TRAIT)
-                                    || directive.service().hasTrait(ENDPOINT_RULESET_TRAIT));
+                            service.hasTrait(ENDPOINT_BDD_TRAIT)
+                                    || service.hasTrait(ENDPOINT_RULESET_TRAIT));
                     writer.putContext("loadBddInfo",
-                            new LoadBddInfoGenerator(writer, directive.service().toShapeId().getName()));
+                            new LoadBddInfoGenerator(writer, service.toShapeId().getName()));
                     var hasTransportSettings = settings.transportSettings() != null && !settings.transportSettings()
                             .isEmpty();
                     writer.putContext("hasTransportSettings", hasTransportSettings);
@@ -213,7 +213,7 @@ final class RequestOverrideBuilder extends ${requestOverride:T}.OverrideBuilder<
                                     symbol,
                                     directive.model(),
                                     directive.settings()));
-                    var defaultAuth = getAuthFactoryMapping(directive.model(), directive.service());
+                    var defaultAuth = getAuthFactoryMapping(directive.model(), service);
                     writer.putContext(
                             "defaultAuth",
                             new AuthInitializerGenerator(writer, directive.context(), defaultAuth));
@@ -224,7 +224,7 @@ final class RequestOverrideBuilder extends ${requestOverride:T}.OverrideBuilder<
                     writer.putContext("defaultPlugins", new PluginPropertyWriter(writer, defaultPlugins));
                     writer.putContext("settings", getBuilderSettings(directive.settings()));
 
-                    var serviceSymbol = directive.symbolProvider().toSymbol(directive.service());
+                    var serviceSymbol = directive.symbolProvider().toSymbol(service);
                     writer.putContext("serviceApi", serviceSymbol.expectProperty(SymbolProperties.SERVICE_API_SERVICE));
                     writer.write(template);
                     writer.popState();
@@ -370,9 +370,6 @@ public void run() {
 
             var opIndex = OperationIndex.of(model);
             for (var operation : TopDownIndex.of(model).getContainedOperations(service)) {
-                if (operation.getId().getNamespace().equals(SyntheticServiceTransform.SYNTHETIC_NAMESPACE)) {
-                    continue;
-                }
                 writer.pushState();
                 writer.putContext("name", StringUtils.uncapitalize(CodegenUtils.getDefaultName(operation, service)));
                 writer.putContext("input", symbolProvider.toSymbol(opIndex.expectInputShape(operation)));
diff --git a/codegen/codegen-plugin/src/main/java/software/amazon/smithy/java/codegen/server/generators/ServiceGenerator.java b/codegen/codegen-plugin/src/main/java/software/amazon/smithy/java/codegen/server/generators/ServiceGenerator.java
index c7f0bcf296..a281685841 100644
--- a/codegen/codegen-plugin/src/main/java/software/amazon/smithy/java/codegen/server/generators/ServiceGenerator.java
+++ b/codegen/codegen-plugin/src/main/java/software/amazon/smithy/java/codegen/server/generators/ServiceGenerator.java
@@ -17,7 +17,6 @@
 import software.amazon.smithy.java.codegen.CodeGenerationContext;
 import software.amazon.smithy.java.codegen.JavaCodegenSettings;
 import software.amazon.smithy.java.codegen.ServerSymbolProperties;
-import software.amazon.smithy.java.codegen.SyntheticServiceTransform;
 import software.amazon.smithy.java.codegen.generators.IdStringGenerator;
 import software.amazon.smithy.java.codegen.generators.SchemaFieldGenerator;
 import software.amazon.smithy.java.codegen.generators.TypeRegistryGenerator;
@@ -50,7 +49,6 @@ public void accept(
         TopDownIndex index = TopDownIndex.of(directive.model());
         List operationsInfo = index.getContainedOperations(shape)
                 .stream()
-                .filter(o -> !o.getId().getNamespace().equals(SyntheticServiceTransform.SYNTHETIC_NAMESPACE))
                 .map(o -> {
                     var inputSymbol =
                             directive.symbolProvider().toSymbol(directive.model().expectShape(o.getInputShape()));
@@ -138,7 +136,7 @@ public final class ${service:T} implements ${serviceType:T} {
                             var errorSymbols = getImplicitErrorSymbols(
                                     directive.symbolProvider(),
                                     directive.model(),
-                                    directive.service());
+                                    directive.expectService());
                             writer.putContext(
                                     "typeRegistry",
                                     new TypeRegistryGenerator(writer, errorSymbols));
diff --git a/codegen/codegen-plugin/src/test/java/software/amazon/smithy/java/codegen/combined/CodegenTest.java b/codegen/codegen-plugin/src/test/java/software/amazon/smithy/java/codegen/combined/CodegenTest.java
new file mode 100644
index 0000000000..539857a678
--- /dev/null
+++ b/codegen/codegen-plugin/src/test/java/software/amazon/smithy/java/codegen/combined/CodegenTest.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package software.amazon.smithy.java.codegen.combined;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.net.URL;
+import java.nio.file.Path;
+import java.util.Objects;
+import org.junit.jupiter.api.Test;
+import software.amazon.smithy.build.MockManifest;
+import software.amazon.smithy.build.PluginContext;
+import software.amazon.smithy.build.SmithyBuildPlugin;
+import software.amazon.smithy.java.codegen.JavaCodegenPlugin;
+import software.amazon.smithy.model.Model;
+import software.amazon.smithy.model.node.ArrayNode;
+import software.amazon.smithy.model.node.ObjectNode;
+
+/**
+ * Verifies combined TYPES + CLIENT generation: the primary service is generated as a client
+ * and an unconnected standalone type (which a plain service walk would not reach) is also
+ * generated as a POJO from the same run.
+ */
+public class CodegenTest {
+    private static final URL testFile =
+            Objects.requireNonNull(CodegenTest.class.getResource("combined-it.smithy"));
+    private static final Model model = Model.assembler()
+            .addImport(testFile)
+            .assemble()
+            .unwrap();
+
+    @Test
+    void generatesServiceAndStandaloneTypes() {
+        var manifest = new MockManifest();
+        SmithyBuildPlugin plugin = new JavaCodegenPlugin();
+        var settings = ObjectNode.builder()
+                .withMember("service", "smithy.java.codegen.combined.it#CombinedItService")
+                .withMember("namespace", "test.smithy.codegen")
+                .withMember("modes", ArrayNode.fromStrings("client", "types"))
+                .build();
+        var context = PluginContext.builder()
+                .fileManifest(manifest)
+                .settings(settings)
+                .model(model)
+                .build();
+
+        plugin.execute(context);
+
+        assertThat(manifest.getFiles())
+                // Client artifact for the primary service.
+                .contains(Path.of("/java/test/smithy/codegen/client/CombinedItServiceClient.java"))
+                // Type connected to the service through an operation, generated under its renamed name.
+                .contains(Path.of("/java/test/smithy/codegen/model/Gadget.java"))
+                // Standalone type not reachable from the service is still generated.
+                .contains(Path.of("/java/test/smithy/codegen/model/StandaloneType.java"));
+    }
+
+    @Test
+    void generatesFromAuthoredClosureInCombinedMode() {
+        // The model authors a `shapeClosures` entry that includes the service and the standalone type.
+        // Referencing it by id drives combined generation; the director enforces the service is a member.
+        var manifest = new MockManifest();
+        SmithyBuildPlugin plugin = new JavaCodegenPlugin();
+        var settings = ObjectNode.builder()
+                .withMember("service", "smithy.java.codegen.combined.it#CombinedItService")
+                .withMember("namespace", "test.smithy.codegen")
+                .withMember("modes", ArrayNode.fromStrings("client", "types"))
+                .withMember("closure", "smithy.java.codegen.combined.it#combinedClosure")
+                .build();
+        var context = PluginContext.builder()
+                .fileManifest(manifest)
+                .settings(settings)
+                .model(model)
+                .build();
+
+        plugin.execute(context);
+
+        assertThat(manifest.getFiles())
+                // The service still generates as a client, and the standalone type is included.
+                .contains(Path.of("/java/test/smithy/codegen/client/CombinedItServiceClient.java"))
+                .contains(Path.of("/java/test/smithy/codegen/model/Gadget.java"))
+                .contains(Path.of("/java/test/smithy/codegen/model/StandaloneType.java"));
+    }
+
+    @Test
+    void skipsShapesNotBoundToPrimaryService() {
+        // The authored closure pulls in an operation and a resource that are not bound to the
+        // primary service. Combined mode must skip both artifacts while still generating the
+        // primary service.
+        var manifest = new MockManifest();
+        SmithyBuildPlugin plugin = new JavaCodegenPlugin();
+        var settings = ObjectNode.builder()
+                .withMember("service", "smithy.java.codegen.combined.it#CombinedItService")
+                .withMember("namespace", "test.smithy.codegen")
+                .withMember("modes", ArrayNode.fromStrings("client", "types"))
+                .withMember("closure", "smithy.java.codegen.combined.it#closureWithUnboundShapes")
+                .build();
+        var context = PluginContext.builder()
+                .fileManifest(manifest)
+                .settings(settings)
+                .model(model)
+                .build();
+
+        plugin.execute(context);
+
+        assertThat(manifest.getFiles())
+                // The primary service still generates.
+                .contains(Path.of("/java/test/smithy/codegen/client/CombinedItServiceClient.java"))
+                // No operation or resource artifact is generated for the shapes outside the primary
+                // service's closure. (OrphanOpInput below proves the operation did enter the closure,
+                // so the missing OrphanOp artifact reflects the skip, not an empty closure.)
+                .doesNotContain(Path.of("/java/test/smithy/codegen/model/OrphanOp.java"))
+                .doesNotContain(Path.of("/java/test/smithy/codegen/model/OrphanResource.java"))
+                // The unbound operation's input and output are still generated as standalone data
+                // shapes, which is expected: they are plain data, unlike the operation artifact
+                // whose service wiring would be wrong.
+                .contains(Path.of("/java/test/smithy/codegen/model/OrphanOpInput.java"))
+                .contains(Path.of("/java/test/smithy/codegen/model/OrphanOpOutput.java"));
+    }
+}
diff --git a/codegen/codegen-plugin/src/test/java/software/amazon/smithy/java/codegen/combined/TestCombinedModeCodegenRunner.java b/codegen/codegen-plugin/src/test/java/software/amazon/smithy/java/codegen/combined/TestCombinedModeCodegenRunner.java
new file mode 100644
index 0000000000..11855bfa70
--- /dev/null
+++ b/codegen/codegen-plugin/src/test/java/software/amazon/smithy/java/codegen/combined/TestCombinedModeCodegenRunner.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package software.amazon.smithy.java.codegen.combined;
+
+import java.nio.file.Paths;
+import java.util.Objects;
+import software.amazon.smithy.build.FileManifest;
+import software.amazon.smithy.build.PluginContext;
+import software.amazon.smithy.java.codegen.JavaCodegenPlugin;
+import software.amazon.smithy.model.Model;
+import software.amazon.smithy.model.node.ArrayNode;
+import software.amazon.smithy.model.node.ObjectNode;
+
+/**
+ * Executes the Java codegen plugin in combined {@code [client, types]} mode for integration tests.
+ *
+ * 

The model is loaded in isolation via {@code addImport} (rather than {@code discoverModels}) so + * the combined closure's standalone-type scan is scoped to just this model's shapes. + */ +public final class TestCombinedModeCodegenRunner { + private TestCombinedModeCodegenRunner() { + // Utility class does not have constructor + } + + public static void main(String[] args) { + JavaCodegenPlugin plugin = new JavaCodegenPlugin(); + Model model = Model.assembler(TestCombinedModeCodegenRunner.class.getClassLoader()) + .addImport(Objects.requireNonNull( + TestCombinedModeCodegenRunner.class.getResource("combined-it.smithy"))) + .assemble() + .unwrap(); + PluginContext context = PluginContext.builder() + .fileManifest(FileManifest.create(Paths.get(System.getenv("output")))) + .settings( + ObjectNode.builder() + .withMember("service", "smithy.java.codegen.combined.it#CombinedItService") + .withMember("namespace", "smithy.java.codegen.combined.it") + .withMember("modes", ArrayNode.fromStrings("client", "types")) + .build()) + .model(model) + .build(); + plugin.execute(context); + } +} diff --git a/codegen/codegen-plugin/src/test/java/software/amazon/smithy/java/codegen/types/CodegenTest.java b/codegen/codegen-plugin/src/test/java/software/amazon/smithy/java/codegen/types/CodegenTest.java index e79f0a0bc0..93782a71e1 100644 --- a/codegen/codegen-plugin/src/test/java/software/amazon/smithy/java/codegen/types/CodegenTest.java +++ b/codegen/codegen-plugin/src/test/java/software/amazon/smithy/java/codegen/types/CodegenTest.java @@ -6,6 +6,7 @@ package software.amazon.smithy.java.codegen.types; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertEquals; import java.net.URL; @@ -16,6 +17,7 @@ import software.amazon.smithy.build.MockManifest; import software.amazon.smithy.build.PluginContext; import software.amazon.smithy.build.SmithyBuildPlugin; +import software.amazon.smithy.codegen.core.CodegenException; import software.amazon.smithy.java.codegen.JavaCodegenPlugin; import software.amazon.smithy.model.Model; import software.amazon.smithy.model.node.ArrayNode; @@ -99,4 +101,74 @@ void specificShapesAdded() { Path.of("/resources/META-INF/services/software.amazon.smithy.java.core.schema.SchemaIndex")); } + @Test + void emptySelectorFailsLoudly() { + // A selector that matches no shapes (here, operations in a data-only model) must fail fast + // rather than silently generating nothing. + var settings = settingsBuilder + .withMember("selector", ":is(operation)") + .build(); + var context = contextBuilder.settings(settings).build(); + assertThatThrownBy(() -> plugin.execute(context)) + .isInstanceOf(CodegenException.class) + .hasMessageContaining("no shapes"); + } + + @Test + void appliesRenames() { + var renamed = Path.of("/java/test/smithy/codegen/types/test/model/RenamedStructure.java"); + var original = Path.of("/java/test/smithy/codegen/types/test/model/StructureShape.java"); + var settings = settingsBuilder + .withMember("selector", ":is(structure)") + .withMember("renames", + ObjectNode.builder() + .withMember("smithy.java.codegen.types.test#StructureShape", "RenamedStructure") + .build()) + .build(); + var context = contextBuilder.settings(settings).build(); + plugin.execute(context); + // The renamed shape produces the renamed class file, and the original name is gone. + assertThat(manifest.getFiles()).contains(renamed).doesNotContain(original); + assertThat(manifest.expectFileString(renamed)).contains("public final class RenamedStructure"); + } + + @Test + void generatesFromAuthoredClosure() { + // The model authors a `shapeClosures` entry that selects only StructureShape and renames it. + // Referencing it by id drives generation off that closure instead of an inline selector. + var closureModel = Model.assembler() + .addImport(Objects.requireNonNull(CodegenTest.class.getResource("authored-closure.smithy"))) + .assemble() + .unwrap(); + var settings = settingsBuilder + .withMember("closure", "smithy.java.codegen.types.test#authoredClosure") + .build(); + var context = PluginContext.builder() + .fileManifest(manifest) + .model(closureModel) + .settings(settings) + .build(); + plugin.execute(context); + + var renamed = Path.of("/java/test/smithy/codegen/types/test/model/AuthoredStructure.java"); + assertThat(manifest.getFiles()) + // Only the single closure member is generated, under its closure-defined rename. + .contains(renamed) + .doesNotContain(Path.of("/java/test/smithy/codegen/types/test/model/StructureShape.java")) + .doesNotContain(Path.of("/java/test/smithy/codegen/types/test/model/UnionShape.java")); + assertThat(manifest.expectFileString(renamed)).contains("public final class AuthoredStructure"); + } + + @Test + void closureCannotBeCombinedWithInlineSettings() { + var settings = settingsBuilder + .withMember("closure", "smithy.java.codegen.types.test#authoredClosure") + .withMember("selector", ":is(structure)") + .build(); + var context = contextBuilder.settings(settings).build(); + assertThatThrownBy(() -> plugin.execute(context)) + .isInstanceOf(CodegenException.class) + .hasMessageContaining("cannot be combined with the inline"); + } + } diff --git a/codegen/codegen-plugin/src/test/resources/software/amazon/smithy/java/codegen/combined/combined-it.smithy b/codegen/codegen-plugin/src/test/resources/software/amazon/smithy/java/codegen/combined/combined-it.smithy new file mode 100644 index 0000000000..0861161560 --- /dev/null +++ b/codegen/codegen-plugin/src/test/resources/software/amazon/smithy/java/codegen/combined/combined-it.smithy @@ -0,0 +1,78 @@ +$version: "2" + +metadata shapeClosures = [ + { + // Includes the primary service (so it satisfies combined mode) plus the unconnected + // standalone type. Used to verify driving combined mode from a pre-authored closure. + id: "smithy.java.codegen.combined.it#combinedClosure" + includeBySelector: ":is([id = 'smithy.java.codegen.combined.it#CombinedItService'], [id = 'smithy.java.codegen.combined.it#StandaloneType'])" + rename: { + "smithy.java.codegen.combined.it#Widget": "Gadget" + } + } + { + // Includes the primary service plus shapes not bound to it: a standalone operation and a + // standalone resource. Used to verify that combined mode skips operations and resources + // outside the primary service's closure. + id: "smithy.java.codegen.combined.it#closureWithUnboundShapes" + includeBySelector: ":is([id = 'smithy.java.codegen.combined.it#CombinedItService'], [id = 'smithy.java.codegen.combined.it#OrphanOp'], [id = 'smithy.java.codegen.combined.it#OrphanResource'])" + } +] + +namespace smithy.java.codegen.combined.it + +@protocolDefinition( + traits: [timestampFormat, cors, endpoint, hostLabel, http] +) +@trait(selector: "service") +structure testProtocol {} + +/// Service used to verify combined TYPES + CLIENT generation compiles and runs: the service +/// closure (with a rename applied) plus an unconnected standalone type are generated together. +@testProtocol +service CombinedItService { + version: "today" + operations: [ + GetThing + ] + rename: { + "smithy.java.codegen.combined.it#Widget": "Gadget" + } +} + +operation GetThing { + input := { + id: String + } + output := { + widget: Widget + } +} + +/// Connected to the service through an operation and renamed to Gadget; exercises rename +/// threading end to end (the generated class must be named Gadget, not Widget). +structure Widget { + name: String +} + +/// Not reachable from the service. In combined mode this must still be generated. +structure StandaloneType { + value: String +} + +/// An operation not bound to the primary service. A closure can pull it in, but combined mode must +/// not generate it until multi-service generation is supported. +operation OrphanOp { + input := { + value: String + } + output := { + result: String + } +} + +/// A resource not bound to the primary service. A closure can pull it in, but combined mode must +/// not generate it until multi-service generation is supported. +resource OrphanResource { + identifiers: { id: String } +} diff --git a/codegen/codegen-plugin/src/test/resources/software/amazon/smithy/java/codegen/types/authored-closure.smithy b/codegen/codegen-plugin/src/test/resources/software/amazon/smithy/java/codegen/types/authored-closure.smithy new file mode 100644 index 0000000000..5a388eb8bd --- /dev/null +++ b/codegen/codegen-plugin/src/test/resources/software/amazon/smithy/java/codegen/types/authored-closure.smithy @@ -0,0 +1,33 @@ +$version: "2.0" + +metadata shapeClosures = [ + { + id: "smithy.java.codegen.types.test#authoredClosure" + includeBySelector: "[id = 'smithy.java.codegen.types.test#StructureShape']" + rename: { + "smithy.java.codegen.types.test#StructureShape": "AuthoredStructure" + } + } +] + +namespace smithy.java.codegen.types.test + +structure StructureShape { + fieldA: String + fieldB: String +} + +union UnionShape { + a: String + b: Integer +} + +enum EnumShape { + A + B +} + +intEnum IntEnumShape { + A = 1 + B = 2 +} diff --git a/examples/closure-types/README.md b/examples/closure-types/README.md new file mode 100644 index 0000000000..b6d37b1857 --- /dev/null +++ b/examples/closure-types/README.md @@ -0,0 +1,22 @@ +## Examples: Closure-Driven Combined Generation + +This example demonstrates driving code generation from a shape closure defined in the model rather +than from a service shape alone. It uses "combined mode": a service is generated as a server, and +the data shapes in a modeled shape closure are generated alongside it. + +The bird-watching service (`smithy.example.birds#iBird`) is generated as an RPC v2 CBOR server. The +`smithy.example.birds#fullService` closure includes the whole service namespace, so the event types +(such as `VerifiedSighting`) are generated alongside the server even though some are not reachable +from the service's operations. + +The `closure` setting in `smithy-build.json` references the `shapeClosures` entry authored in the +model, so the closure definition travels with the model rather than living in build configuration. + +### Usage + +To use this example as a template, run the following command with the +[Smithy CLI](https://smithy.io/2.0/guides/smithy-cli/index.html): + +```console +smithy init -t closure-types --url git@github.com:smithy-lang/smithy-java.git +``` diff --git a/examples/closure-types/build.gradle.kts b/examples/closure-types/build.gradle.kts new file mode 100644 index 0000000000..fe87034610 --- /dev/null +++ b/examples/closure-types/build.gradle.kts @@ -0,0 +1,55 @@ + +plugins { + `java-library` + id("software.amazon.smithy.gradle.smithy-base") +} + +dependencies { + val smithyJavaVersion: String by project + + smithyBuild("software.amazon.smithy.java:codegen-plugin:$smithyJavaVersion") + // Combined mode generates a server, so server-api must be on the codegen classpath (the plugin + // validates it) and on the runtime classpath for the generated server. + smithyBuild("software.amazon.smithy.java:server-api:$smithyJavaVersion") + api("software.amazon.smithy.java:server-api:$smithyJavaVersion") + // The RPC v2 CBOR server protocol the generated service is served with at runtime. + implementation("software.amazon.smithy.java:server-rpcv2-cbor:$smithyJavaVersion") + + testImplementation("org.hamcrest:hamcrest:3.0") + testImplementation("org.junit.jupiter:junit-jupiter:6.0.3") + testRuntimeOnly("org.junit.platform:junit-platform-launcher") + testImplementation("org.assertj:assertj-core:3.27.7") +} + +// Add the generated Java sources and resources to the main source set so they compile. +afterEvaluate { + val generatedPath = smithy.getPluginProjectionPath(smithy.sourceProjection.get(), "java-codegen").get() + sourceSets { + main { + java { + srcDir("$generatedPath/java") + } + resources { + srcDir("$generatedPath/resources") + } + } + } +} + +tasks { + val smithyBuild by getting + compileJava { + dependsOn(smithyBuild) + } + processResources { + dependsOn(smithyBuild) + } + withType { + useJUnitPlatform() + } +} + +repositories { + mavenLocal() + mavenCentral() +} diff --git a/examples/closure-types/license.txt b/examples/closure-types/license.txt new file mode 100644 index 0000000000..8a5e500b4a --- /dev/null +++ b/examples/closure-types/license.txt @@ -0,0 +1,4 @@ +/* + * Example license header. + * File header line two + */ diff --git a/examples/closure-types/model/events.smithy b/examples/closure-types/model/events.smithy new file mode 100644 index 0000000000..17aff635d6 --- /dev/null +++ b/examples/closure-types/model/events.smithy @@ -0,0 +1,120 @@ +$version: "2" + +metadata shapeClosures = [ + // A closure that only includes shapes tagged as events. + { + id: "smithy.example.birds#events" + includeBySelector: "[trait|tags|(values) = event]" + } +] + +namespace smithy.example.birds + +/// Reports an unclassified potential sighting of a bird from a video stream, +/// detected by a simple computer vision application. +/// +/// These events are sent to a queue where a more robust algorithm is applied +/// to verify the sighting and classify the species. +/// +/// This is an internal event. +@internal +@tags(["event"]) +structure UnclassifiedStreamSighting { + /// The ID of the camera stream where the bird was detected. + @required + streamId: UUID + + /// The timestamp of the stream where the bird was first detected. + @required + start: Timestamp + + /// The timestamp of the stream where the bird was last detected. + @required + end: Timestamp +} + +/// Reports a sighting from a stream that has been classified. +/// +/// If confidence is high, these may be automatically added to the list +/// of verified sightings. Otherwise these are sent to a queue for +/// review. +/// +/// This is an internal event. +@internal +@tags(["event"]) +structure ClassifiedStreamSighting { + /// The ID of the camera stream where the bird was detected. + @required + streamId: UUID + + /// The timestamp of the stream where the bird was first detected. + @required + start: Timestamp + + /// The timestamp of the stream where the bird was last detected. + @required + end: Timestamp + + /// The proposed classification of the bird. + @required + classification: Classification + + /// The confidence in the classification as a percentage. + @required + @range(min: 0, max: 100) + confidence: Float +} + +/// Reports an unverified sighting submitted by a user. +/// +/// These are sent to a queue for review. +/// +/// This is an internal event +@internal +@tags(["event"]) +@references([ + { + resource: Bird + } + { + resource: Sighting + } +]) +structure UnverifiedSighting for Sighting { + @required + $birdId + + @required + $sightingId +} + +// This is a public event. +/// Reports a verified sighting of a bird. +@tags(["event"]) +@references([ + { + resource: Bird + } + { + resource: Sighting + } +]) +structure VerifiedSighting for Sighting { + @required + $birdId + + @required + classification: Classification + + @required + $sightingId + + @required + $timestamp + + @required + $location + + @required + $verified +} diff --git a/examples/closure-types/model/service.smithy b/examples/closure-types/model/service.smithy new file mode 100644 index 0000000000..dc4d6e980f --- /dev/null +++ b/examples/closure-types/model/service.smithy @@ -0,0 +1,255 @@ +$version: "2" + +metadata shapeClosures = [ + // A closure that includes every shape in the service namespace, + // including events. + { + id: "smithy.example.birds#fullService" + includeNamespaces: ["smithy.example.birds"] + } +] + +namespace smithy.example.birds + +use smithy.protocols#rpcv2Cbor + +/// A service that tracks bird sightings for research purposes. +@rpcv2Cbor +@paginated(inputToken: "nextToken", outputToken: "nextToken", pageSize: "pageSize") +service iBird { + resources: [ + Bird + ] +} + +/// A resource representing the bird itself. +/// +/// These may only be created by the service operators. +resource Bird { + identifiers: { + birdId: UUID + } + properties: { + classification: Classification + } + resources: [ + Sighting + ] + create: CreateBird + read: GetBird + list: ListBirds +} + +/// The taxonomic classification of a bird. +/// +/// Ranks above order are shared by all birds, so they are omitted. +structure Classification { + @required + order: NonEmptyString + + @required + family: NonEmptyString + + @required + genus: NonEmptyString + + @required + species: NonEmptyString + + subspecies: NonEmptyString +} + +/// Adds a bird to the database. This is for internal use only. +@internal +operation CreateBird { + input := for Bird { + @required + $classification + } +} + +/// Retrieves information about a specific bird. +@readonly +operation GetBird { + input := for Bird { + @required + $birdId + } + + output := for Bird { + @required + $birdId + + @required + $classification + } +} + +/// Lists birds present in the database. +@paginated(items: "birds") +@readonly +operation ListBirds { + input := with [PaginatedInput] {} + + output := with [PaginatedOutput] { + @required + birds: BirdList + } +} + +list BirdList { + member: BirdSummary +} + +/// A summary of a bird's properties. +structure BirdSummary for Bird { + $birdId + $classification +} + +/// A resource representing a bird sighting. +/// +/// These may be created either by user submission or by automated monitoring +/// systems. Sightings are verified before appearing in listings. +resource Sighting { + identifiers: { + birdId: UUID + sightingId: UUID + } + properties: { + timestamp: Timestamp + location: Coordinates + image: Image + verified: Boolean + } + create: CreateSighting + read: GetSighting + list: ListSightings +} + +/// Creates a sighting. +operation CreateSighting { + input := for Sighting { + @required + $birdId + + @required + $timestamp + + @required + $location + + @required + image: Image + + // For internal use only. Automated sightings from a stream may set + // this to true if their confidence is high. + @internal + verified: Boolean + } + + output := for Sighting { + @required + $sightingId + } +} + +/// Gets a sighting. +/// +/// Unverified sightings may be retrieved here, even if they don't appear in +/// listings. +@readonly +operation GetSighting { + input := for Sighting { + @required + $birdId + + @required + $sightingId + } + + output := for Sighting { + @required + $timestamp + + @required + $location + + @required + $image + + @required + $verified + } +} + +/// List verified sightings for a particular bird. +@paginated(items: "sightings") +@readonly +operation ListSightings { + input := for Bird with [PaginatedInput] { + @required + $birdId + } + + output := with [PaginatedOutput] { + @required + sightings: SightingSummaryList + } +} + +/// Geographical coordinates from where a sighting took place. +structure Coordinates { + latitude: BigDecimal + longitude: BigDecimal +} + +list SightingSummaryList { + member: SightingSummary +} + +/// A summary of a sighting's properties. +structure SightingSummary for Sighting { + @required + $birdId + + @required + $sightingId + + @required + $timestamp + + @required + $location + + @required + $verified +} + +// A mixin to share input pagination parameters. +@mixin +@private +structure PaginatedInput { + nextToken: NonEmptyString + + @range(min: 1, max: 1000) + pageSize: Integer = 100 +} + +// A mixin to share output pagination parameters. +@mixin +@private +structure PaginatedOutput { + nextToken: NonEmptyString +} + +/// A UUID-v4 string. +@pattern("^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$") +string UUID + +@length(min: 1) +string NonEmptyString + +/// A JPEG image. +@mediaType("image/jpeg") +blob Image diff --git a/examples/closure-types/settings.gradle.kts b/examples/closure-types/settings.gradle.kts new file mode 100644 index 0000000000..f2d2f8859e --- /dev/null +++ b/examples/closure-types/settings.gradle.kts @@ -0,0 +1,19 @@ +/** + * Combined generation of a server plus standalone types, driven by a shape closure. + */ + +pluginManagement { + val smithyGradleVersion: String by settings + + plugins { + id("software.amazon.smithy.gradle.smithy-base").version(smithyGradleVersion) + } + + repositories { + mavenLocal() + mavenCentral() + gradlePluginPortal() + } +} + +rootProject.name = "ClosureTypes" diff --git a/examples/closure-types/smithy-build.json b/examples/closure-types/smithy-build.json new file mode 100644 index 0000000000..f44222ad6e --- /dev/null +++ b/examples/closure-types/smithy-build.json @@ -0,0 +1,12 @@ +{ + "version": "1.0", + "plugins": { + "java-codegen": { + "service": "smithy.example.birds#iBird", + "namespace": "software.amazon.smithy.java.example.closure", + "headerFile": "license.txt", + "modes": ["server", "types"], + "closure": "smithy.example.birds#fullService" + } + } +} diff --git a/examples/closure-types/src/test/java/software/amazon/smithy/java/example/closure/CombinedGenerationTest.java b/examples/closure-types/src/test/java/software/amazon/smithy/java/example/closure/CombinedGenerationTest.java new file mode 100644 index 0000000000..0a72c4f0c8 --- /dev/null +++ b/examples/closure-types/src/test/java/software/amazon/smithy/java/example/closure/CombinedGenerationTest.java @@ -0,0 +1,64 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.java.example.closure; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.Instant; +import org.junit.jupiter.api.Test; +import software.amazon.smithy.java.example.closure.model.Classification; +import software.amazon.smithy.java.example.closure.model.Coordinates; +import software.amazon.smithy.java.example.closure.model.VerifiedSighting; +import software.amazon.smithy.java.example.closure.service.IBird; +import software.amazon.smithy.java.server.Service; + +/** + * Verifies combined-mode generation driven by the {@code fullService} shape closure: the bird + * service is generated as a server (see {@code IBird} under the {@code service} package) and the + * service closure's data shapes, including the event types, are generated alongside it. + */ +public class CombinedGenerationTest { + + @Test + void generatesServerForService() { + // The service is generated as a server Service implementation. + assertThat(Service.class).isAssignableFrom(IBird.class); + } + + @Test + void generatesServiceClosureTypes() { + // Classification is part of the service closure and generates as a normal data shape. + var classification = Classification.builder() + .order("Passeriformes") + .family("Corvidae") + .genus("Corvus") + .species("corax") + .build(); + assertThat(classification.getGenus()).isEqualTo("Corvus"); + assertThat(classification.getSpecies()).isEqualTo("corax"); + } + + @Test + void generatesEventTypesFromClosure() { + // VerifiedSighting is a public event tagged "event"; the closure pulls it into generation + // alongside the server. All of its members are required, so populate them all. + var sighting = VerifiedSighting.builder() + .birdId("bird-1") + .sightingId("sighting-1") + .timestamp(Instant.EPOCH) + .location(Coordinates.builder().build()) + .verified(true) + .classification(Classification.builder() + .order("Passeriformes") + .family("Corvidae") + .genus("Corvus") + .species("corax") + .build()) + .build(); + assertThat(sighting.getBirdId()).isEqualTo("bird-1"); + assertThat(sighting.isVerified()).isTrue(); + } +} diff --git a/examples/standalone-types/README.md b/examples/standalone-types/README.md index 7c8a59a6bf..d7476992d0 100644 --- a/examples/standalone-types/README.md +++ b/examples/standalone-types/README.md @@ -5,7 +5,7 @@ Package that generates Java types for a model without a service. To use this example as a template, run the following command with the [Smithy CLI](https://smithy.io/2.0/guides/smithy-cli/index.html): ```console -smithy init -t restjson-client --url https://github.com/smithy-lang/smithy-java +smithy init -t standalone-types --url https://github.com/smithy-lang/smithy-java ``` or diff --git a/examples/standalone-types/model/person.smithy b/examples/standalone-types/model/person.smithy index 7ee04e66a0..02eb6e9ea2 100644 --- a/examples/standalone-types/model/person.smithy +++ b/examples/standalone-types/model/person.smithy @@ -1,5 +1,15 @@ $version: "2" +metadata shapeClosures = [ + // A closure that includes every shape in the example namespace. Referencing this by id from + // smithy-build.json drives types generation from the model rather than from the plugin's + // default selector, and keeps the closure definition alongside the model. + { + id: "smithy.example#allTypes" + includeNamespaces: ["smithy.example"] + } +] + namespace smithy.example use smithy.framework#ValidationException diff --git a/examples/standalone-types/smithy-build.json b/examples/standalone-types/smithy-build.json index c28581d8e3..dbaaca8ffa 100644 --- a/examples/standalone-types/smithy-build.json +++ b/examples/standalone-types/smithy-build.json @@ -4,7 +4,8 @@ "java-codegen": { "namespace": "software.amazon.smithy.java.example.standalone", "headerFile": "license.txt", - "modes": ["types"] + "modes": ["types"], + "closure": "smithy.example#allTypes" } } } diff --git a/model-bundle/model-bundle-api/smithy-build.json b/model-bundle/model-bundle-api/smithy-build.json index 6b0627b1cf..16c388013f 100644 --- a/model-bundle/model-bundle-api/smithy-build.json +++ b/model-bundle/model-bundle-api/smithy-build.json @@ -7,4 +7,4 @@ "modes": ["types"] } } -} \ No newline at end of file +} diff --git a/protocol-test-harness/src/main/java/software/amazon/smithy/java/protocoltests/harness/ProtocolTestExtension.java b/protocol-test-harness/src/main/java/software/amazon/smithy/java/protocoltests/harness/ProtocolTestExtension.java index 1f4e2124c9..bc669243ba 100644 --- a/protocol-test-harness/src/main/java/software/amazon/smithy/java/protocoltests/harness/ProtocolTestExtension.java +++ b/protocol-test-harness/src/main/java/software/amazon/smithy/java/protocoltests/harness/ProtocolTestExtension.java @@ -126,6 +126,7 @@ public void beforeAll(ExtensionContext context) throws Exception { var symbolProvider = SymbolProvider.cache( new JavaSymbolProvider(serviceModel, service, + service.getRename(), serviceId.getNamespace(), serviceId.getName(), Set.of(CodegenMode.SERVER))); @@ -310,6 +311,7 @@ private static List getTestOperations( var symbolProvider = new JavaSymbolProvider( serviceModel, service, + service.getRename(), service.toShapeId().getNamespace(), service.toShapeId().getName(), Set.of()); diff --git a/settings.gradle.kts b/settings.gradle.kts index ba398180bf..16b9339389 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -107,6 +107,7 @@ include(":examples:event-streaming-client") include(":examples:lambda") include(":examples:restjson-client") include(":examples:standalone-types") +include(":examples:closure-types") include(":examples:mcp-server") include(":examples:mcp-traits-example") diff --git a/smithy-templates.json b/smithy-templates.json index b1d264d003..0ccdf3cb21 100644 --- a/smithy-templates.json +++ b/smithy-templates.json @@ -57,6 +57,14 @@ ".gitignore" ] }, + "closure-types": { + "documentation": "Code generation of both standalone types and mixed service + standalone types, driven by a modeled shape closure.", + "path": "examples/closure-types", + "include": [ + "examples/gradle.properties", + ".gitignore" + ] + }, "mcp-server" : { "documentation" : "Generate or create an MCP server.", "path": "examples/mcp-server",