From b96fba47ddcc3ba88872e20b0705125565689d8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Sugawara=20=28=E2=88=A9=EF=BD=80-=C2=B4=29?= =?UTF-8?q?=E2=8A=83=E2=94=81=E7=82=8E=E7=82=8E=E7=82=8E=E7=82=8E=E7=82=8E?= Date: Fri, 3 Apr 2026 20:28:20 -0700 Subject: [PATCH 1/3] Make input/output structs implement closeable when these have streaming members --- .../generators/StructureGenerator.java | 106 +++++++++++++++++- fuzz-test-harness/smithy-build.json | 1 - 2 files changed, 101 insertions(+), 6 deletions(-) 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 386aefcba..47b07f30b 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 @@ -5,6 +5,7 @@ package software.amazon.smithy.java.codegen.generators; +import java.io.Closeable; import java.math.BigDecimal; import java.math.BigInteger; import java.nio.ByteBuffer; @@ -18,6 +19,7 @@ import java.util.List; import java.util.Objects; import java.util.function.Consumer; +import software.amazon.smithy.codegen.core.Symbol; import software.amazon.smithy.codegen.core.SymbolProvider; import software.amazon.smithy.codegen.core.directed.ShapeDirective; import software.amazon.smithy.java.codegen.CodeGenerationContext; @@ -35,6 +37,9 @@ import software.amazon.smithy.java.core.serde.document.Document; import software.amazon.smithy.java.io.datastream.DataStream; import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.knowledge.EventStreamIndex; +import software.amazon.smithy.model.knowledge.EventStreamInfo; +import software.amazon.smithy.model.knowledge.OperationIndex; import software.amazon.smithy.model.node.ArrayNode; import software.amazon.smithy.model.node.BooleanNode; import software.amazon.smithy.model.node.Node; @@ -80,6 +85,8 @@ public final class StructureGenerator< T extends ShapeDirective> implements Consumer { + private EventStreamInfo eventStreamInfo; + @Override public void accept(T directive) { if (directive.shape().hasTrait(UnitTypeTrait.class) || directive.symbol() @@ -89,11 +96,12 @@ public void accept(T directive) { return; } var shape = directive.shape(); + setEventStreamInfo(directive.model(), shape); directive.context().writerDelegator().useShapeWriter(shape, writer -> { writer.pushState(new ClassSection(shape)); var template = """ - public final class ${shape:T} ${^isError}implements ${serializableStruct:T}${/isError}${?isError}extends ${sdkException:T}${/isError} { + public final class ${shape:T}${classModifiers:C} { ${schemas:C|} @@ -115,17 +123,25 @@ public final class ${shape:T} ${^isError}implements ${serializableStruct:T}${/is ${getMemberValue:C|} + ${?isCloseable}${close:C|}${/isCloseable} + ${toBuilder:C|} ${builder:C|} } """; + var sdkError = CodegenUtils.tryGetServiceProperty(directive, SymbolProperties.SERVICE_EXCEPTION); + var isCloseable = eventStreamInfo != null; + writer.putContext("classModifiers", + new ClassModifiers(writer, + shape, + sdkError, + directive.symbolProvider(), + directive.model(), + isCloseable)); writer.putContext("isError", shape.hasTrait(ErrorTrait.class)); + writer.putContext("isCloseable", isCloseable); writer.putContext("shape", directive.symbol()); - writer.putContext("serializableStruct", SerializableStruct.class); - - var sdkError = CodegenUtils.tryGetServiceProperty(directive, SymbolProperties.SERVICE_EXCEPTION); - writer.putContext("sdkException", sdkError == null ? ModeledException.class : sdkError); writer.putContext("id", new IdStringGenerator(writer, shape)); writer.putContext( @@ -150,6 +166,7 @@ public final class ${shape:T} ${^isError}implements ${serializableStruct:T}${/is "hashCode", new HashCodeGenerator(writer, shape, directive.symbolProvider(), directive.model())); writer.putContext("toString", new ToStringGenerator(writer)); + writer.putContext("close", new CloseGenerator(writer, shape, directive.symbolProvider(), eventStreamInfo)); writer.putContext( "serializer", new StructureSerializerGenerator( @@ -168,6 +185,7 @@ public final class ${shape:T} ${^isError}implements ${serializableStruct:T}${/is directive.model(), directive.service())); writer.putContext("getMemberValue", new GetMemberValueGenerator(writer, directive.symbolProvider(), shape)); + writer.putContext("toBuilder", new ToBuilderGenerator(writer, shape, directive.symbolProvider())); writer.writeNullMarkedAnnotation(); writer.write(template); @@ -175,6 +193,57 @@ public final class ${shape:T} ${^isError}implements ${serializableStruct:T}${/is }); } + private void setEventStreamInfo(Model model, StructureShape shape) { + var operationIndex = OperationIndex.of(model); + var isInputStructure = operationIndex.isInputStructure(shape); + var isOutputStructure = operationIndex.isOutputStructure(shape); + if (!isInputStructure && !isOutputStructure) { + return; + } + var eventStreamIndex = EventStreamIndex.of(model); + if (isInputStructure) { + for (var op : operationIndex.getInputBindings(shape)) { + var inputInfo = eventStreamIndex.getInputInfo(op); + if (inputInfo.isPresent()) { + this.eventStreamInfo = inputInfo.get(); + return; + } + } + } + if (isOutputStructure) { + for (var op : operationIndex.getOutputBindings(shape)) { + var outputInfo = eventStreamIndex.getOutputInfo(op); + if (outputInfo.isPresent()) { + this.eventStreamInfo = outputInfo.get(); + return; + } + } + } + } + + private record ClassModifiers( + JavaWriter writer, + Shape shape, + Symbol sdkError, + SymbolProvider symbolProvider, + Model model, + boolean isCloseable) + implements + Runnable { + @Override + public void run() { + if (shape.hasTrait(ErrorTrait.class)) { + writer.writeInline(" extends $T", sdkError == null ? ModeledException.class : sdkError); + return; + } + if (isCloseable) { + writer.writeInline(" implements $T, $T", SerializableStruct.class, Closeable.class); + } else { + writer.writeInline(" implements $T", SerializableStruct.class); + } + } + } + private record PropertyGenerator(JavaWriter writer, Shape shape, SymbolProvider symbolProvider, Model model) implements Runnable { @@ -484,6 +553,33 @@ private void writeMemberHash(JavaWriter writer, MemberShape member) { } } + private record CloseGenerator( + JavaWriter writer, + StructureShape shape, + SymbolProvider symbolProvider, + EventStreamInfo eventStreamInfo) implements Runnable { + @Override + public void run() { + var memberName = symbolProvider.toMemberName(eventStreamInfo.getEventStreamMember()); + + writer.pushState(); + writer.putContext("memberName", memberName); + writer.write( + """ + /** + * Closes the underlying stream. + */ + @Override + public void close() { + if (${memberName:L} != null) { + ${memberName:L}.close(); + } + } + """); + writer.popState(); + } + } + private record ToBuilderGenerator( JavaWriter writer, StructureShape shape, diff --git a/fuzz-test-harness/smithy-build.json b/fuzz-test-harness/smithy-build.json index 1310ce1be..02ec88aac 100644 --- a/fuzz-test-harness/smithy-build.json +++ b/fuzz-test-harness/smithy-build.json @@ -3,7 +3,6 @@ "plugins": { "java-codegen": { "namespace": "software.smithy.fuzz.test", - "useExternalTypes": true, "modes": ["types"] } } From f25c53f0d48f99ed8d13747d934b0946572850d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Sugawara=20=28=E2=88=A9=EF=BD=80-=C2=B4=29?= =?UTF-8?q?=E2=8A=83=E2=94=81=E7=82=8E=E7=82=8E=E7=82=8E=E7=82=8E=E7=82=8E?= Date: Sat, 4 Apr 2026 11:20:11 -0700 Subject: [PATCH 2/3] Update to make it Closeable on any streaming member --- .../generators/StructureGenerator.java | 36 +++++++------------ 1 file changed, 12 insertions(+), 24 deletions(-) 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 47b07f30b..a106c3ea8 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 @@ -37,8 +37,6 @@ import software.amazon.smithy.java.core.serde.document.Document; import software.amazon.smithy.java.io.datastream.DataStream; import software.amazon.smithy.model.Model; -import software.amazon.smithy.model.knowledge.EventStreamIndex; -import software.amazon.smithy.model.knowledge.EventStreamInfo; import software.amazon.smithy.model.knowledge.OperationIndex; import software.amazon.smithy.model.node.ArrayNode; import software.amazon.smithy.model.node.BooleanNode; @@ -85,7 +83,7 @@ public final class StructureGenerator< T extends ShapeDirective> implements Consumer { - private EventStreamInfo eventStreamInfo; + private MemberShape streamingMember; @Override public void accept(T directive) { @@ -131,7 +129,7 @@ public final class ${shape:T}${classModifiers:C} { } """; var sdkError = CodegenUtils.tryGetServiceProperty(directive, SymbolProperties.SERVICE_EXCEPTION); - var isCloseable = eventStreamInfo != null; + var isCloseable = streamingMember != null; writer.putContext("classModifiers", new ClassModifiers(writer, shape, @@ -166,7 +164,7 @@ public final class ${shape:T}${classModifiers:C} { "hashCode", new HashCodeGenerator(writer, shape, directive.symbolProvider(), directive.model())); writer.putContext("toString", new ToStringGenerator(writer)); - writer.putContext("close", new CloseGenerator(writer, shape, directive.symbolProvider(), eventStreamInfo)); + writer.putContext("close", new CloseGenerator(writer, shape, directive.symbolProvider(), streamingMember)); writer.putContext( "serializer", new StructureSerializerGenerator( @@ -194,29 +192,19 @@ public final class ${shape:T}${classModifiers:C} { } private void setEventStreamInfo(Model model, StructureShape shape) { + if (!model.isTraitApplied(StreamingTrait.class)) { + return; + } var operationIndex = OperationIndex.of(model); var isInputStructure = operationIndex.isInputStructure(shape); var isOutputStructure = operationIndex.isOutputStructure(shape); if (!isInputStructure && !isOutputStructure) { return; } - var eventStreamIndex = EventStreamIndex.of(model); - if (isInputStructure) { - for (var op : operationIndex.getInputBindings(shape)) { - var inputInfo = eventStreamIndex.getInputInfo(op); - if (inputInfo.isPresent()) { - this.eventStreamInfo = inputInfo.get(); - return; - } - } - } - if (isOutputStructure) { - for (var op : operationIndex.getOutputBindings(shape)) { - var outputInfo = eventStreamIndex.getOutputInfo(op); - if (outputInfo.isPresent()) { - this.eventStreamInfo = outputInfo.get(); - return; - } + for (var member : shape.members()) { + var target = model.expectShape(member.getTarget()); + if (target.hasTrait(StreamingTrait.class)) { + streamingMember = member; } } } @@ -557,10 +545,10 @@ private record CloseGenerator( JavaWriter writer, StructureShape shape, SymbolProvider symbolProvider, - EventStreamInfo eventStreamInfo) implements Runnable { + MemberShape streamingMember) implements Runnable { @Override public void run() { - var memberName = symbolProvider.toMemberName(eventStreamInfo.getEventStreamMember()); + var memberName = symbolProvider.toMemberName(streamingMember); writer.pushState(); writer.putContext("memberName", memberName); From bfb1808c5e1a106c1a3d411eeab7996adaa312da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Sugawara=20=28=E2=88=A9=EF=BD=80-=C2=B4=29?= =?UTF-8?q?=E2=8A=83=E2=94=81=E7=82=8E=E7=82=8E=E7=82=8E=E7=82=8E=E7=82=8E?= Date: Sat, 4 Apr 2026 11:23:00 -0700 Subject: [PATCH 3/3] Update the method name as well --- .../smithy/java/codegen/generators/StructureGenerator.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) 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 a106c3ea8..79f238578 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 @@ -94,7 +94,7 @@ public void accept(T directive) { return; } var shape = directive.shape(); - setEventStreamInfo(directive.model(), shape); + setStreamingMember(directive.model(), shape); directive.context().writerDelegator().useShapeWriter(shape, writer -> { writer.pushState(new ClassSection(shape)); var template = @@ -183,7 +183,6 @@ public final class ${shape:T}${classModifiers:C} { directive.model(), directive.service())); writer.putContext("getMemberValue", new GetMemberValueGenerator(writer, directive.symbolProvider(), shape)); - writer.putContext("toBuilder", new ToBuilderGenerator(writer, shape, directive.symbolProvider())); writer.writeNullMarkedAnnotation(); writer.write(template); @@ -191,7 +190,7 @@ public final class ${shape:T}${classModifiers:C} { }); } - private void setEventStreamInfo(Model model, StructureShape shape) { + private void setStreamingMember(Model model, StructureShape shape) { if (!model.isTraitApplied(StreamingTrait.class)) { return; }