From 91166055a460aa81ba22ba8d73d5affaf22384e9 Mon Sep 17 00:00:00 2001 From: RanVaknin <50976344+RanVaknin@users.noreply.github.com> Date: Thu, 12 Mar 2026 14:20:22 -0700 Subject: [PATCH 1/9] Include shape name in REQUEST_URI_NOT_FOUND validation error message --- .../amazon/awssdk/codegen/AddShapes.java | 8 ++- .../awssdk/codegen/CodeGeneratorTest.java | 23 ++++++++ .../uri-on-non-input-shape-service.json | 57 +++++++++++++++++++ 3 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 codegen/src/test/resources/software/amazon/awssdk/codegen/uri-on-non-input-shape-service.json diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/AddShapes.java b/codegen/src/main/java/software/amazon/awssdk/codegen/AddShapes.java index 19600e33210e..55975faf69cd 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/AddShapes.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/AddShapes.java @@ -367,7 +367,13 @@ private String findRequestUri(Shape parentShape, Map allC2jShapes return operation.map(o -> o.getHttp().getRequestUri()) .orElseThrow(() -> { - String detailMsg = "Could not find request URI for input shape for operation: " + operation; + String shapeName = allC2jShapes.entrySet().stream() + .filter(e -> e.getValue().equals(parentShape)) + .map(Map.Entry::getKey) + .findFirst() + .orElse("unknown"); + String detailMsg = "Could not find request URI for input shape '" + shapeName + + "'. No operation was found that references this shape as its input."; ValidationEntry entry = new ValidationEntry().withErrorId(ValidationErrorId.REQUEST_URI_NOT_FOUND) .withDetailMessage(detailMsg) diff --git a/codegen/src/test/java/software/amazon/awssdk/codegen/CodeGeneratorTest.java b/codegen/src/test/java/software/amazon/awssdk/codegen/CodeGeneratorTest.java index 05a492ca24c4..0ce91b61b576 100644 --- a/codegen/src/test/java/software/amazon/awssdk/codegen/CodeGeneratorTest.java +++ b/codegen/src/test/java/software/amazon/awssdk/codegen/CodeGeneratorTest.java @@ -48,6 +48,7 @@ import software.amazon.awssdk.codegen.poet.ClientTestModels; import software.amazon.awssdk.codegen.validation.ModelInvalidException; import software.amazon.awssdk.codegen.validation.ModelValidator; +import software.amazon.awssdk.codegen.validation.ValidationEntry; import software.amazon.awssdk.codegen.validation.ValidationErrorId; public class CodeGeneratorTest { @@ -176,6 +177,23 @@ void execute_endpointsTestReferencesUnknownOperationMember_throwsValidationError }); } + @Test + void execute_uriLocationOnNonInputShape_throwsValidationErrorWithShapeName() throws IOException { + C2jModels models = C2jModels.builder() + .customizationConfig(CustomizationConfig.create()) + .serviceModel(getUriOnNonInputShapeServiceModel()) + .build(); + + assertThatThrownBy(() -> generateCodeFromC2jModels(models, outputDir, true, Collections.emptyList())) + .isInstanceOf(ModelInvalidException.class) + .matches(e -> { + ModelInvalidException ex = (ModelInvalidException) e; + ValidationEntry entry = ex.validationEntries().get(0); + return entry.getErrorId() == ValidationErrorId.REQUEST_URI_NOT_FOUND + && entry.getDetailMessage().contains("No operation was found"); + }); + } + @Test void execute_operationHasNoRequestUri_throwsValidationError() throws IOException { C2jModels models = C2jModels.builder() @@ -244,6 +262,11 @@ private ServiceModel getMissingRequestUriServiceModel() throws IOException { return Jackson.load(ServiceModel.class, json); } + private ServiceModel getUriOnNonInputShapeServiceModel() throws IOException { + String json = resourceAsString("uri-on-non-input-shape-service.json"); + return Jackson.load(ServiceModel.class, json); + } + private String resourceAsString(String name) throws IOException { ByteArrayOutputStream baos; try (InputStream resourceAsStream = getClass().getResourceAsStream(name)) { diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/uri-on-non-input-shape-service.json b/codegen/src/test/resources/software/amazon/awssdk/codegen/uri-on-non-input-shape-service.json new file mode 100644 index 000000000000..7462f20501db --- /dev/null +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/uri-on-non-input-shape-service.json @@ -0,0 +1,57 @@ +{ + "version": "2.0", + "metadata": { + "apiVersion": "2010-05-08", + "endpointPrefix": "json-service-endpoint", + "globalEndpoint": "json-service.amazonaws.com", + "protocol": "rest-json", + "serviceAbbreviation": "Rest Json Service", + "serviceFullName": "Some Service That Uses Rest-Json Protocol", + "serviceId": "Rest Json Service", + "signingName": "json-service", + "signatureVersion": "v4", + "uid": "json-service-2010-05-08", + "xmlNamespace": "https://json-service.amazonaws.com/doc/2010-05-08/" + }, + "operations": { + "SomeOperation": { + "name": "SomeOperation", + "http": { + "method": "POST", + "requestUri": "/things/{thingId}" + }, + "input": { + "shape": "SomeOperationRequest" + } + } + }, + "shapes": { + "SomeOperationRequest": { + "type": "structure", + "members": { + "thingId": { + "shape": "String", + "location": "uri", + "locationName": "thingId" + }, + "options": { + "shape": "NestedOptions" + } + } + }, + "NestedOptions": { + "type": "structure", + "members": { + "pageSize": { + "shape": "String", + "location": "uri", + "locationName": "pageSize" + } + } + }, + "String": { + "type": "string" + } + }, + "documentation": "A service with a uri-bound member on a non-input shape" +} From 1b094c08c3fa737beff845e967a57d079edf4568 Mon Sep 17 00:00:00 2001 From: RanVaknin <50976344+RanVaknin@users.noreply.github.com> Date: Fri, 13 Mar 2026 11:50:26 -0700 Subject: [PATCH 2/9] Fix codegen logic to allow non input shape uri bound member --- .../amazon/awssdk/codegen/AddShapes.java | 46 ++++++------------- .../awssdk/codegen/CodeGeneratorTest.java | 14 ++---- 2 files changed, 18 insertions(+), 42 deletions(-) diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/AddShapes.java b/codegen/src/main/java/software/amazon/awssdk/codegen/AddShapes.java index 55975faf69cd..725bf8f38e62 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/AddShapes.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/AddShapes.java @@ -21,7 +21,6 @@ import static software.amazon.awssdk.codegen.internal.Utils.isMapShape; import static software.amazon.awssdk.codegen.internal.Utils.isScalar; -import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Optional; @@ -38,15 +37,10 @@ import software.amazon.awssdk.codegen.model.intermediate.VariableModel; import software.amazon.awssdk.codegen.model.service.Location; import software.amazon.awssdk.codegen.model.service.Member; -import software.amazon.awssdk.codegen.model.service.Operation; import software.amazon.awssdk.codegen.model.service.ServiceModel; import software.amazon.awssdk.codegen.model.service.Shape; import software.amazon.awssdk.codegen.naming.NamingStrategy; import software.amazon.awssdk.codegen.utils.ProtocolUtils; -import software.amazon.awssdk.codegen.validation.ModelInvalidException; -import software.amazon.awssdk.codegen.validation.ValidationEntry; -import software.amazon.awssdk.codegen.validation.ValidationErrorId; -import software.amazon.awssdk.codegen.validation.ValidationErrorSeverity; import software.amazon.awssdk.utils.StringUtils; import software.amazon.awssdk.utils.Validate; @@ -342,9 +336,9 @@ private boolean isRequiredMember(String memberName, Shape memberShape) { */ private boolean isGreedy(Shape parentShape, Map allC2jShapes, ParameterHttpMapping mapping) { if (mapping.getLocation() == Location.URI) { - // If the location is URI we can assume the parent shape is an input shape. - String requestUri = findRequestUri(parentShape, allC2jShapes); - if (requestUri.contains(String.format("{%s+}", mapping.getMarshallLocationName()))) { + Optional requestUri = findRequestUri(parentShape, allC2jShapes); + if (requestUri.isPresent() + && requestUri.get().contains(String.format("{%s+}", mapping.getMarshallLocationName()))) { return true; } } @@ -353,33 +347,19 @@ private boolean isGreedy(Shape parentShape, Map allC2jShapes, Par /** * Given an input shape, finds the Request URI for the operation that input is referenced from. + * Per the Smithy spec, httpLabel on non-input shapes has no meaning and is ignored, + * so this returns Optional.empty() if the shape is not a direct operation input. * - * @param parentShape Input shape to find operation's request URI for. + * @param parentShape Shape to find operation's request URI for. * @param allC2jShapes All shapes in the service model. - * @return Request URI for operation. - * @throws RuntimeException If operation can't be found. + * @return Request URI for operation, or empty if the shape is not a direct operation input. */ - private String findRequestUri(Shape parentShape, Map allC2jShapes) { - Optional operation = builder.getService().getOperations().values().stream() - .filter(o -> o.getInput() != null) - .filter(o -> allC2jShapes.get(o.getInput().getShape()).equals(parentShape)) - .findFirst(); - - return operation.map(o -> o.getHttp().getRequestUri()) - .orElseThrow(() -> { - String shapeName = allC2jShapes.entrySet().stream() - .filter(e -> e.getValue().equals(parentShape)) - .map(Map.Entry::getKey) - .findFirst() - .orElse("unknown"); - String detailMsg = "Could not find request URI for input shape '" + shapeName - + "'. No operation was found that references this shape as its input."; - ValidationEntry entry = - new ValidationEntry().withErrorId(ValidationErrorId.REQUEST_URI_NOT_FOUND) - .withDetailMessage(detailMsg) - .withSeverity(ValidationErrorSeverity.DANGER); - return ModelInvalidException.builder().validationEntries(Collections.singletonList(entry)).build(); - }); + private Optional findRequestUri(Shape parentShape, Map allC2jShapes) { + return builder.getService().getOperations().values().stream() + .filter(o -> o.getInput() != null) + .filter(o -> allC2jShapes.get(o.getInput().getShape()).equals(parentShape)) + .findFirst() + .map(o -> o.getHttp().getRequestUri()); } private String deriveUnmarshallerLocationName(Shape memberShape, String memberName, Member member) { diff --git a/codegen/src/test/java/software/amazon/awssdk/codegen/CodeGeneratorTest.java b/codegen/src/test/java/software/amazon/awssdk/codegen/CodeGeneratorTest.java index 0ce91b61b576..1f94a8b3849a 100644 --- a/codegen/src/test/java/software/amazon/awssdk/codegen/CodeGeneratorTest.java +++ b/codegen/src/test/java/software/amazon/awssdk/codegen/CodeGeneratorTest.java @@ -16,6 +16,7 @@ package software.amazon.awssdk.codegen; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNoException; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; @@ -178,20 +179,15 @@ void execute_endpointsTestReferencesUnknownOperationMember_throwsValidationError } @Test - void execute_uriLocationOnNonInputShape_throwsValidationErrorWithShapeName() throws IOException { + void execute_uriLocationOnNonInputShape_isIgnored() throws IOException { C2jModels models = C2jModels.builder() .customizationConfig(CustomizationConfig.create()) .serviceModel(getUriOnNonInputShapeServiceModel()) .build(); - assertThatThrownBy(() -> generateCodeFromC2jModels(models, outputDir, true, Collections.emptyList())) - .isInstanceOf(ModelInvalidException.class) - .matches(e -> { - ModelInvalidException ex = (ModelInvalidException) e; - ValidationEntry entry = ex.validationEntries().get(0); - return entry.getErrorId() == ValidationErrorId.REQUEST_URI_NOT_FOUND - && entry.getDetailMessage().contains("No operation was found"); - }); + // Per the Smithy spec, httpLabel on non-input shapes has no meaning and is simply ignored. + assertThatNoException().isThrownBy( + () -> generateCodeFromC2jModels(models, outputDir, true, Collections.emptyList())); } @Test From 48e1b37134583cc10c1c1e77c283042d757bba2f Mon Sep 17 00:00:00 2001 From: RanVaknin <50976344+RanVaknin@users.noreply.github.com> Date: Fri, 13 Mar 2026 12:29:50 -0700 Subject: [PATCH 3/9] Fix logic to ignore instead of throw --- .../amazon/awssdk/codegen/AddShapes.java | 45 +++++++++++++++---- 1 file changed, 36 insertions(+), 9 deletions(-) diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/AddShapes.java b/codegen/src/main/java/software/amazon/awssdk/codegen/AddShapes.java index 725bf8f38e62..5344016041e8 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/AddShapes.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/AddShapes.java @@ -21,6 +21,7 @@ import static software.amazon.awssdk.codegen.internal.Utils.isMapShape; import static software.amazon.awssdk.codegen.internal.Utils.isScalar; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Optional; @@ -37,10 +38,15 @@ import software.amazon.awssdk.codegen.model.intermediate.VariableModel; import software.amazon.awssdk.codegen.model.service.Location; import software.amazon.awssdk.codegen.model.service.Member; +import software.amazon.awssdk.codegen.model.service.Operation; import software.amazon.awssdk.codegen.model.service.ServiceModel; import software.amazon.awssdk.codegen.model.service.Shape; import software.amazon.awssdk.codegen.naming.NamingStrategy; import software.amazon.awssdk.codegen.utils.ProtocolUtils; +import software.amazon.awssdk.codegen.validation.ModelInvalidException; +import software.amazon.awssdk.codegen.validation.ValidationEntry; +import software.amazon.awssdk.codegen.validation.ValidationErrorId; +import software.amazon.awssdk.codegen.validation.ValidationErrorSeverity; import software.amazon.awssdk.utils.StringUtils; import software.amazon.awssdk.utils.Validate; @@ -349,17 +355,38 @@ private boolean isGreedy(Shape parentShape, Map allC2jShapes, Par * Given an input shape, finds the Request URI for the operation that input is referenced from. * Per the Smithy spec, httpLabel on non-input shapes has no meaning and is ignored, * so this returns Optional.empty() if the shape is not a direct operation input. - * - * @param parentShape Shape to find operation's request URI for. - * @param allC2jShapes All shapes in the service model. - * @return Request URI for operation, or empty if the shape is not a direct operation input. + * If the shape IS a direct operation input but the operation is missing a requestUri, + * a validation error is thrown. */ private Optional findRequestUri(Shape parentShape, Map allC2jShapes) { - return builder.getService().getOperations().values().stream() - .filter(o -> o.getInput() != null) - .filter(o -> allC2jShapes.get(o.getInput().getShape()).equals(parentShape)) - .findFirst() - .map(o -> o.getHttp().getRequestUri()); + Optional operation = builder.getService().getOperations().values().stream() + .filter(o -> o.getInput() != null) + .filter(o -> allC2jShapes.get(o.getInput().getShape()).equals(parentShape)) + .findFirst(); + + if (!operation.isPresent()) { + // Not a direct operation input shape, should be ignored. + // https://smithy.io/2.0/spec/http-bindings.html#httplabel-is-only-used-on-top-level-input + return Optional.empty(); + } + + String requestUri = operation.get().getHttp().getRequestUri(); + if (requestUri == null) { + String shapeName = allC2jShapes.entrySet().stream() + .filter(e -> e.getValue().equals(parentShape)) + .map(Map.Entry::getKey) + .findFirst() + .get(); + String detailMsg = "Could not find request URI for input shape '" + shapeName + + "'. No operation was found that references this shape as its input."; + ValidationEntry entry = + new ValidationEntry().withErrorId(ValidationErrorId.REQUEST_URI_NOT_FOUND) + .withDetailMessage(detailMsg) + .withSeverity(ValidationErrorSeverity.DANGER); + throw ModelInvalidException.builder().validationEntries(Collections.singletonList(entry)).build(); + } + + return Optional.of(requestUri); } private String deriveUnmarshallerLocationName(Shape memberShape, String memberName, Member member) { From cbb38f4d2d7ab9a2f67b184a22c14006d5fa4e31 Mon Sep 17 00:00:00 2001 From: RanVaknin <50976344+RanVaknin@users.noreply.github.com> Date: Fri, 13 Mar 2026 12:48:30 -0700 Subject: [PATCH 4/9] Fix javadoc --- .../software/amazon/awssdk/codegen/AddShapes.java | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/AddShapes.java b/codegen/src/main/java/software/amazon/awssdk/codegen/AddShapes.java index 5344016041e8..14766b3d46be 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/AddShapes.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/AddShapes.java @@ -352,11 +352,14 @@ private boolean isGreedy(Shape parentShape, Map allC2jShapes, Par } /** - * Given an input shape, finds the Request URI for the operation that input is referenced from. - * Per the Smithy spec, httpLabel on non-input shapes has no meaning and is ignored, - * so this returns Optional.empty() if the shape is not a direct operation input. - * If the shape IS a direct operation input but the operation is missing a requestUri, - * a validation error is thrown. + * Given a shape, finds the Request URI for the operation that references it as input. + * Returns empty if the shape is not a direct operation input. + * Throws if the shape is a direct operation input but the operation is missing a requestUri. + * + * @param parentShape Shape to find operation's request URI for. + * @param allC2jShapes All shapes in the service model. + * @return Request URI for operation, or empty if the shape is not a direct operation input. + * @throws ModelInvalidException If the shape is a direct operation input but requestUri is missing. */ private Optional findRequestUri(Shape parentShape, Map allC2jShapes) { Optional operation = builder.getService().getOperations().values().stream() From 517392f8ea912a4b42f78c4d2e9c83861d0c6703 Mon Sep 17 00:00:00 2001 From: RanVaknin <50976344+RanVaknin@users.noreply.github.com> Date: Mon, 16 Mar 2026 13:53:47 -0700 Subject: [PATCH 5/9] Ignore HTTP binding locations on non-input shapes per Smithy spec --- .../amazon/awssdk/codegen/AddShapes.java | 29 ++- .../amazon/awssdk/codegen/AddShapesTest.java | 2 +- .../awssdk/codegen/CodeGeneratorTest.java | 21 ++ .../codegen/expected-nested-options.java | 201 ++++++++++++++++++ .../model/nestedqueryparameteroperation.java | 4 +- 5 files changed, 251 insertions(+), 6 deletions(-) create mode 100644 codegen/src/test/resources/software/amazon/awssdk/codegen/expected-nested-options.java diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/AddShapes.java b/codegen/src/main/java/software/amazon/awssdk/codegen/AddShapes.java index 14766b3d46be..a64e1ad15fdf 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/AddShapes.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/AddShapes.java @@ -311,8 +311,13 @@ private ParameterHttpMapping generateParameterHttpMapping(Shape parentShape, ParameterHttpMapping mapping = new ParameterHttpMapping(); + // https://smithy.io/2.0/spec/http-bindings.html#httplabel-is-only-used-on-top-level-input + Location location = isDirectOperationInputOrOutput(parentShape, allC2jShapes) + ? Location.forValue(member.getLocation()) + : null; + Shape memberShape = allC2jShapes.get(member.getShape()); - mapping.withLocation(Location.forValue(member.getLocation())) + mapping.withLocation(location) .withPayload(member.isPayload()).withStreaming(member.isStreaming()) .withFlattened(isFlattened(member, memberShape)) .withUnmarshallLocationName(deriveUnmarshallerLocationName(memberShape, memberName, member)) @@ -323,6 +328,26 @@ private ParameterHttpMapping generateParameterHttpMapping(Shape parentShape, return mapping; } + private boolean isDirectOperationInputOrOutput(Shape parentShape, Map allC2jShapes) { + for (Operation operation : builder.getService().getOperations().values()) { + if (operation.getInput() != null) { + String inputShapeName = operation.getInput().getShape(); + Shape inputShape = allC2jShapes.get(inputShapeName); + if (parentShape.equals(inputShape)) { + return true; + } + } + if (operation.getOutput() != null) { + String outputShapeName = operation.getOutput().getShape(); + Shape outputShape = allC2jShapes.get(outputShapeName); + if (parentShape.equals(outputShape)) { + return true; + } + } + } + return false; + } + private boolean isFlattened(Member member, Shape memberShape) { return member.isFlattened() || memberShape.isFlattened(); @@ -354,12 +379,10 @@ private boolean isGreedy(Shape parentShape, Map allC2jShapes, Par /** * Given a shape, finds the Request URI for the operation that references it as input. * Returns empty if the shape is not a direct operation input. - * Throws if the shape is a direct operation input but the operation is missing a requestUri. * * @param parentShape Shape to find operation's request URI for. * @param allC2jShapes All shapes in the service model. * @return Request URI for operation, or empty if the shape is not a direct operation input. - * @throws ModelInvalidException If the shape is a direct operation input but requestUri is missing. */ private Optional findRequestUri(Shape parentShape, Map allC2jShapes) { Optional operation = builder.getService().getOperations().values().stream() diff --git a/codegen/src/test/java/software/amazon/awssdk/codegen/AddShapesTest.java b/codegen/src/test/java/software/amazon/awssdk/codegen/AddShapesTest.java index dde88dd226e0..5f827cdadd44 100644 --- a/codegen/src/test/java/software/amazon/awssdk/codegen/AddShapesTest.java +++ b/codegen/src/test/java/software/amazon/awssdk/codegen/AddShapesTest.java @@ -84,7 +84,7 @@ void generateShapeModel_memberRequiredByNestedShape_setsMemberModelAsRequired() MemberModel requiredMemberModel = requestShapeModel.findMemberModelByC2jName(queryParamName); assertThat(requestShapeModel.getRequired()).contains(queryParamName); - assertThat(requiredMemberModel.getHttp().getLocation()).isEqualTo(Location.QUERY_STRING); + assertThat(requiredMemberModel.getHttp().getLocation()).isNull(); assertThat(requiredMemberModel.isRequired()).isTrue(); } diff --git a/codegen/src/test/java/software/amazon/awssdk/codegen/CodeGeneratorTest.java b/codegen/src/test/java/software/amazon/awssdk/codegen/CodeGeneratorTest.java index 1f94a8b3849a..c4cc96b16691 100644 --- a/codegen/src/test/java/software/amazon/awssdk/codegen/CodeGeneratorTest.java +++ b/codegen/src/test/java/software/amazon/awssdk/codegen/CodeGeneratorTest.java @@ -44,7 +44,10 @@ import software.amazon.awssdk.codegen.model.config.customization.CustomizationConfig; import software.amazon.awssdk.codegen.model.config.customization.UnderscoresInNameBehavior; import software.amazon.awssdk.codegen.model.intermediate.IntermediateModel; +import software.amazon.awssdk.codegen.model.intermediate.MemberModel; +import software.amazon.awssdk.codegen.model.intermediate.ShapeModel; import software.amazon.awssdk.codegen.model.rules.endpoints.EndpointTestSuiteModel; +import software.amazon.awssdk.codegen.model.service.Location; import software.amazon.awssdk.codegen.model.service.ServiceModel; import software.amazon.awssdk.codegen.poet.ClientTestModels; import software.amazon.awssdk.codegen.validation.ModelInvalidException; @@ -188,6 +191,24 @@ void execute_uriLocationOnNonInputShape_isIgnored() throws IOException { // Per the Smithy spec, httpLabel on non-input shapes has no meaning and is simply ignored. assertThatNoException().isThrownBy( () -> generateCodeFromC2jModels(models, outputDir, true, Collections.emptyList())); + + IntermediateModel intermediateModel = new IntermediateModelBuilder(models).build(); + ShapeModel inputShape = intermediateModel.getShapes().get("SomeOperationRequest"); + MemberModel uriMember = inputShape.findMemberModelByC2jName("thingId"); + assertThat(uriMember.getHttp().getLocation()).isEqualTo(Location.URI); + + ShapeModel nestedShape = intermediateModel.getShapes().get("NestedOptions"); + MemberModel nestedUriMember = nestedShape.findMemberModelByC2jName("pageSize"); + assertThat(nestedUriMember.getHttp().getLocation()).isNull(); + assertThat(nestedUriMember.getHttp().isGreedy()).isFalse(); + + Path generatedNestedOptions = Files.walk(outputDir) + .filter(p -> p.getFileName().toString().equals("NestedOptions.java")) + .findFirst() + .orElseThrow(() -> new AssertionError("NestedOptions.java not found in generated output")); + String actual = new String(Files.readAllBytes(generatedNestedOptions), StandardCharsets.UTF_8); + String expected = resourceAsString("expected-nested-options.java"); + assertThat(actual).isEqualTo(expected); } @Test diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/expected-nested-options.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/expected-nested-options.java new file mode 100644 index 000000000000..bb8c7e9e3efc --- /dev/null +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/expected-nested-options.java @@ -0,0 +1,201 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with + * the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +package software.amazon.awssdk.services.restjson.model; + +import java.io.Serializable; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.function.BiConsumer; +import java.util.function.Function; +import software.amazon.awssdk.annotations.Generated; +import software.amazon.awssdk.annotations.Mutable; +import software.amazon.awssdk.annotations.NotThreadSafe; +import software.amazon.awssdk.core.SdkField; +import software.amazon.awssdk.core.SdkPojo; +import software.amazon.awssdk.core.protocol.MarshallLocation; +import software.amazon.awssdk.core.protocol.MarshallingType; +import software.amazon.awssdk.core.traits.LocationTrait; +import software.amazon.awssdk.utils.ToString; +import software.amazon.awssdk.utils.builder.CopyableBuilder; +import software.amazon.awssdk.utils.builder.ToCopyableBuilder; + +/** + */ +@Generated("software.amazon.awssdk:codegen") +public final class NestedOptions implements SdkPojo, Serializable, ToCopyableBuilder { + private static final SdkField PAGE_SIZE_FIELD = SdkField. builder(MarshallingType.STRING) + .memberName("pageSize").getter(getter(NestedOptions::pageSize)).setter(setter(Builder::pageSize)) + .traits(LocationTrait.builder().location(MarshallLocation.PAYLOAD).locationName("pageSize").build()).build(); + + private static final List> SDK_FIELDS = Collections.unmodifiableList(Arrays.asList(PAGE_SIZE_FIELD)); + + private static final Map> SDK_NAME_TO_FIELD = memberNameToFieldInitializer(); + + private static final long serialVersionUID = 1L; + + private final String pageSize; + + private NestedOptions(BuilderImpl builder) { + this.pageSize = builder.pageSize; + } + + /** + * Returns the value of the PageSize property for this object. + * + * @return The value of the PageSize property for this object. + */ + public final String pageSize() { + return pageSize; + } + + @Override + public Builder toBuilder() { + return new BuilderImpl(this); + } + + public static Builder builder() { + return new BuilderImpl(); + } + + public static Class serializableBuilderClass() { + return BuilderImpl.class; + } + + @Override + public final int hashCode() { + int hashCode = 1; + hashCode = 31 * hashCode + Objects.hashCode(pageSize()); + return hashCode; + } + + @Override + public final boolean equals(Object obj) { + return equalsBySdkFields(obj); + } + + @Override + public final boolean equalsBySdkFields(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (!(obj instanceof NestedOptions)) { + return false; + } + NestedOptions other = (NestedOptions) obj; + return Objects.equals(pageSize(), other.pageSize()); + } + + /** + * Returns a string representation of this object. This is useful for testing and debugging. Sensitive data will be + * redacted from this string using a placeholder value. + */ + @Override + public final String toString() { + return ToString.builder("NestedOptions").add("PageSize", pageSize()).build(); + } + + public final Optional getValueForField(String fieldName, Class clazz) { + switch (fieldName) { + case "pageSize": + return Optional.ofNullable(clazz.cast(pageSize())); + default: + return Optional.empty(); + } + } + + @Override + public final List> sdkFields() { + return SDK_FIELDS; + } + + @Override + public final Map> sdkFieldNameToField() { + return SDK_NAME_TO_FIELD; + } + + private static Map> memberNameToFieldInitializer() { + Map> map = new HashMap<>(); + map.put("pageSize", PAGE_SIZE_FIELD); + return Collections.unmodifiableMap(map); + } + + private static Function getter(Function g) { + return obj -> g.apply((NestedOptions) obj); + } + + private static BiConsumer setter(BiConsumer s) { + return (obj, val) -> s.accept((Builder) obj, val); + } + + @Mutable + @NotThreadSafe + public interface Builder extends SdkPojo, CopyableBuilder { + /** + * Sets the value of the PageSize property for this object. + * + * @param pageSize + * The new value for the PageSize property for this object. + * @return Returns a reference to this object so that method calls can be chained together. + */ + Builder pageSize(String pageSize); + } + + static final class BuilderImpl implements Builder { + private String pageSize; + + private BuilderImpl() { + } + + private BuilderImpl(NestedOptions model) { + pageSize(model.pageSize); + } + + public final String getPageSize() { + return pageSize; + } + + public final void setPageSize(String pageSize) { + this.pageSize = pageSize; + } + + @Override + public final Builder pageSize(String pageSize) { + this.pageSize = pageSize; + return this; + } + + @Override + public NestedOptions build() { + return new NestedOptions(this); + } + + @Override + public List> sdkFields() { + return SDK_FIELDS; + } + + @Override + public Map> sdkFieldNameToField() { + return SDK_NAME_TO_FIELD; + } + } +} diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/model/nestedqueryparameteroperation.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/model/nestedqueryparameteroperation.java index c935db9281f0..e743d7c2bea4 100644 --- a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/model/nestedqueryparameteroperation.java +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/model/nestedqueryparameteroperation.java @@ -33,13 +33,13 @@ public final class NestedQueryParameterOperation implements SdkPojo, Serializabl .memberName("QueryParamOne") .getter(getter(NestedQueryParameterOperation::queryParamOne)) .setter(setter(Builder::queryParamOne)) - .traits(LocationTrait.builder().location(MarshallLocation.QUERY_PARAM).locationName("QueryParamOne").build(), + .traits(LocationTrait.builder().location(MarshallLocation.PAYLOAD).locationName("QueryParamOne").build(), RequiredTrait.create()).build(); private static final SdkField QUERY_PARAM_TWO_FIELD = SdkField. builder(MarshallingType.STRING) .memberName("QueryParamTwo").getter(getter(NestedQueryParameterOperation::queryParamTwo)) .setter(setter(Builder::queryParamTwo)) - .traits(LocationTrait.builder().location(MarshallLocation.QUERY_PARAM).locationName("QueryParamTwo").build()).build(); + .traits(LocationTrait.builder().location(MarshallLocation.PAYLOAD).locationName("QueryParamTwo").build()).build(); private static final List> SDK_FIELDS = Collections.unmodifiableList(Arrays.asList(QUERY_PARAM_ONE_FIELD, QUERY_PARAM_TWO_FIELD)); From a4cbd8eba015d2e81a046a0fed135233aeb3f266 Mon Sep 17 00:00:00 2001 From: RanVaknin <50976344+RanVaknin@users.noreply.github.com> Date: Tue, 17 Mar 2026 09:51:00 -0700 Subject: [PATCH 6/9] Account for error shapes as well --- .../software/amazon/awssdk/codegen/AddShapes.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/AddShapes.java b/codegen/src/main/java/software/amazon/awssdk/codegen/AddShapes.java index a64e1ad15fdf..f628c41d8610 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/AddShapes.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/AddShapes.java @@ -36,6 +36,7 @@ import software.amazon.awssdk.codegen.model.intermediate.ReturnTypeModel; import software.amazon.awssdk.codegen.model.intermediate.ShapeModel; import software.amazon.awssdk.codegen.model.intermediate.VariableModel; +import software.amazon.awssdk.codegen.model.service.ErrorMap; import software.amazon.awssdk.codegen.model.service.Location; import software.amazon.awssdk.codegen.model.service.Member; import software.amazon.awssdk.codegen.model.service.Operation; @@ -344,6 +345,16 @@ private boolean isDirectOperationInputOrOutput(Shape parentShape, Map Date: Tue, 17 Mar 2026 14:33:17 -0700 Subject: [PATCH 7/9] Fix comments --- .../amazon/awssdk/codegen/AddShapes.java | 41 +++---- .../amazon/awssdk/codegen/AddShapesTest.java | 16 ++- .../awssdk/codegen/CodeGeneratorTest.java | 11 +- .../codegen/expected-nested-options.java | 102 +++++++++++++++++- .../uri-on-non-input-shape-service.json | 12 ++- 5 files changed, 148 insertions(+), 34 deletions(-) diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/AddShapes.java b/codegen/src/main/java/software/amazon/awssdk/codegen/AddShapes.java index f628c41d8610..70d7be7ec3b0 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/AddShapes.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/AddShapes.java @@ -22,9 +22,11 @@ import static software.amazon.awssdk.codegen.internal.Utils.isScalar; import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; import software.amazon.awssdk.codegen.internal.TypeUtils; import software.amazon.awssdk.codegen.model.config.customization.CustomizationConfig; import software.amazon.awssdk.codegen.model.intermediate.EnumModel; @@ -55,6 +57,7 @@ abstract class AddShapes { private final IntermediateModelBuilder builder; private final NamingStrategy namingStrategy; + private Set directOperationShapes; AddShapes(IntermediateModelBuilder builder) { this.builder = builder; @@ -313,7 +316,7 @@ private ParameterHttpMapping generateParameterHttpMapping(Shape parentShape, ParameterHttpMapping mapping = new ParameterHttpMapping(); // https://smithy.io/2.0/spec/http-bindings.html#httplabel-is-only-used-on-top-level-input - Location location = isDirectOperationInputOrOutput(parentShape, allC2jShapes) + Location location = isDirectOperationShape(parentShape, allC2jShapes) ? Location.forValue(member.getLocation()) : null; @@ -329,34 +332,24 @@ private ParameterHttpMapping generateParameterHttpMapping(Shape parentShape, return mapping; } - private boolean isDirectOperationInputOrOutput(Shape parentShape, Map allC2jShapes) { - for (Operation operation : builder.getService().getOperations().values()) { - if (operation.getInput() != null) { - String inputShapeName = operation.getInput().getShape(); - Shape inputShape = allC2jShapes.get(inputShapeName); - if (parentShape.equals(inputShape)) { - return true; + private boolean isDirectOperationShape(Shape parentShape, Map allC2jShapes) { + if (directOperationShapes == null) { + directOperationShapes = new HashSet<>(); + for (Operation operation : builder.getService().getOperations().values()) { + if (operation.getInput() != null) { + directOperationShapes.add(allC2jShapes.get(operation.getInput().getShape())); } - } - if (operation.getOutput() != null) { - String outputShapeName = operation.getOutput().getShape(); - Shape outputShape = allC2jShapes.get(outputShapeName); - if (parentShape.equals(outputShape)) { - return true; + if (operation.getOutput() != null) { + directOperationShapes.add(allC2jShapes.get(operation.getOutput().getShape())); } - } - - if (operation.getErrors() != null) { - for (ErrorMap error : operation.getErrors()) { - String errorShapeName = error.getShape(); - Shape outputShape = allC2jShapes.get(errorShapeName); - if (parentShape.equals(outputShape)) { - return true; + if (operation.getErrors() != null) { + for (ErrorMap error : operation.getErrors()) { + directOperationShapes.add(allC2jShapes.get(error.getShape())); } } } } - return false; + return directOperationShapes.contains(parentShape); } private boolean isFlattened(Member member, Shape memberShape) { @@ -413,7 +406,7 @@ private Optional findRequestUri(Shape parentShape, Map al .filter(e -> e.getValue().equals(parentShape)) .map(Map.Entry::getKey) .findFirst() - .get(); + .orElseThrow(() -> new IllegalStateException("Shape not found in model: " + parentShape)); String detailMsg = "Could not find request URI for input shape '" + shapeName + "'. No operation was found that references this shape as its input."; ValidationEntry entry = diff --git a/codegen/src/test/java/software/amazon/awssdk/codegen/AddShapesTest.java b/codegen/src/test/java/software/amazon/awssdk/codegen/AddShapesTest.java index 5f827cdadd44..0243c001e90e 100644 --- a/codegen/src/test/java/software/amazon/awssdk/codegen/AddShapesTest.java +++ b/codegen/src/test/java/software/amazon/awssdk/codegen/AddShapesTest.java @@ -84,8 +84,22 @@ void generateShapeModel_memberRequiredByNestedShape_setsMemberModelAsRequired() MemberModel requiredMemberModel = requestShapeModel.findMemberModelByC2jName(queryParamName); assertThat(requestShapeModel.getRequired()).contains(queryParamName); - assertThat(requiredMemberModel.getHttp().getLocation()).isNull(); assertThat(requiredMemberModel.isRequired()).isTrue(); } + @Test + void generateShapeModel_locationOnNestedShape_isIgnored() { + ShapeModel nestedShape = intermediateModel.getShapes().get("NestedQueryParameterOperation"); + MemberModel queryParam = nestedShape.findMemberModelByC2jName("QueryParamOne"); + assertThat(queryParam.getHttp().getLocation()).isNull(); + } + + @Test + void generateShapeModel_locationOnDirectInputShape_isPreserved() { + ShapeModel inputShape = intermediateModel.getShapes().get("QueryParameterOperationRequest"); + assertThat(inputShape.findMemberModelByC2jName("PathParam").getHttp().getLocation()).isEqualTo(Location.URI); + assertThat(inputShape.findMemberModelByC2jName("QueryParamOne").getHttp().getLocation()).isEqualTo(Location.QUERY_STRING); + assertThat(inputShape.findMemberModelByC2jName("StringHeaderMember").getHttp().getLocation()).isEqualTo(Location.HEADER); + } + } diff --git a/codegen/src/test/java/software/amazon/awssdk/codegen/CodeGeneratorTest.java b/codegen/src/test/java/software/amazon/awssdk/codegen/CodeGeneratorTest.java index c4cc96b16691..7da5022cbb83 100644 --- a/codegen/src/test/java/software/amazon/awssdk/codegen/CodeGeneratorTest.java +++ b/codegen/src/test/java/software/amazon/awssdk/codegen/CodeGeneratorTest.java @@ -193,14 +193,15 @@ void execute_uriLocationOnNonInputShape_isIgnored() throws IOException { () -> generateCodeFromC2jModels(models, outputDir, true, Collections.emptyList())); IntermediateModel intermediateModel = new IntermediateModelBuilder(models).build(); + ShapeModel inputShape = intermediateModel.getShapes().get("SomeOperationRequest"); - MemberModel uriMember = inputShape.findMemberModelByC2jName("thingId"); - assertThat(uriMember.getHttp().getLocation()).isEqualTo(Location.URI); + assertThat(inputShape.findMemberModelByC2jName("thingId").getHttp().getLocation()).isEqualTo(Location.URI); ShapeModel nestedShape = intermediateModel.getShapes().get("NestedOptions"); - MemberModel nestedUriMember = nestedShape.findMemberModelByC2jName("pageSize"); - assertThat(nestedUriMember.getHttp().getLocation()).isNull(); - assertThat(nestedUriMember.getHttp().isGreedy()).isFalse(); + assertThat(nestedShape.findMemberModelByC2jName("pageSize").getHttp().getLocation()).isNull(); + assertThat(nestedShape.findMemberModelByC2jName("pageSize").getHttp().isGreedy()).isFalse(); + assertThat(nestedShape.findMemberModelByC2jName("headerParam").getHttp().getLocation()).isNull(); + assertThat(nestedShape.findMemberModelByC2jName("queryParam").getHttp().getLocation()).isNull(); Path generatedNestedOptions = Files.walk(outputDir) .filter(p -> p.getFileName().toString().equals("NestedOptions.java")) diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/expected-nested-options.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/expected-nested-options.java index bb8c7e9e3efc..8254a9771fa5 100644 --- a/codegen/src/test/resources/software/amazon/awssdk/codegen/expected-nested-options.java +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/expected-nested-options.java @@ -43,7 +43,17 @@ public final class NestedOptions implements SdkPojo, Serializable, ToCopyableBui .memberName("pageSize").getter(getter(NestedOptions::pageSize)).setter(setter(Builder::pageSize)) .traits(LocationTrait.builder().location(MarshallLocation.PAYLOAD).locationName("pageSize").build()).build(); - private static final List> SDK_FIELDS = Collections.unmodifiableList(Arrays.asList(PAGE_SIZE_FIELD)); + private static final SdkField HEADER_PARAM_FIELD = SdkField. builder(MarshallingType.STRING) + .memberName("headerParam").getter(getter(NestedOptions::headerParam)).setter(setter(Builder::headerParam)) + .traits(LocationTrait.builder().location(MarshallLocation.PAYLOAD).locationName("x-amz-nested-header").build()) + .build(); + + private static final SdkField QUERY_PARAM_FIELD = SdkField. builder(MarshallingType.STRING) + .memberName("queryParam").getter(getter(NestedOptions::queryParam)).setter(setter(Builder::queryParam)) + .traits(LocationTrait.builder().location(MarshallLocation.PAYLOAD).locationName("nestedQuery").build()).build(); + + private static final List> SDK_FIELDS = Collections.unmodifiableList(Arrays.asList(PAGE_SIZE_FIELD, + HEADER_PARAM_FIELD, QUERY_PARAM_FIELD)); private static final Map> SDK_NAME_TO_FIELD = memberNameToFieldInitializer(); @@ -51,8 +61,14 @@ public final class NestedOptions implements SdkPojo, Serializable, ToCopyableBui private final String pageSize; + private final String headerParam; + + private final String queryParam; + private NestedOptions(BuilderImpl builder) { this.pageSize = builder.pageSize; + this.headerParam = builder.headerParam; + this.queryParam = builder.queryParam; } /** @@ -64,6 +80,24 @@ public final String pageSize() { return pageSize; } + /** + * Returns the value of the HeaderParam property for this object. + * + * @return The value of the HeaderParam property for this object. + */ + public final String headerParam() { + return headerParam; + } + + /** + * Returns the value of the QueryParam property for this object. + * + * @return The value of the QueryParam property for this object. + */ + public final String queryParam() { + return queryParam; + } + @Override public Builder toBuilder() { return new BuilderImpl(this); @@ -81,6 +115,8 @@ public static Class serializableBuilderClass() { public final int hashCode() { int hashCode = 1; hashCode = 31 * hashCode + Objects.hashCode(pageSize()); + hashCode = 31 * hashCode + Objects.hashCode(headerParam()); + hashCode = 31 * hashCode + Objects.hashCode(queryParam()); return hashCode; } @@ -101,7 +137,8 @@ public final boolean equalsBySdkFields(Object obj) { return false; } NestedOptions other = (NestedOptions) obj; - return Objects.equals(pageSize(), other.pageSize()); + return Objects.equals(pageSize(), other.pageSize()) && Objects.equals(headerParam(), other.headerParam()) + && Objects.equals(queryParam(), other.queryParam()); } /** @@ -110,13 +147,18 @@ public final boolean equalsBySdkFields(Object obj) { */ @Override public final String toString() { - return ToString.builder("NestedOptions").add("PageSize", pageSize()).build(); + return ToString.builder("NestedOptions").add("PageSize", pageSize()).add("HeaderParam", headerParam()) + .add("QueryParam", queryParam()).build(); } public final Optional getValueForField(String fieldName, Class clazz) { switch (fieldName) { case "pageSize": return Optional.ofNullable(clazz.cast(pageSize())); + case "headerParam": + return Optional.ofNullable(clazz.cast(headerParam())); + case "queryParam": + return Optional.ofNullable(clazz.cast(queryParam())); default: return Optional.empty(); } @@ -135,6 +177,8 @@ public final Map> sdkFieldNameToField() { private static Map> memberNameToFieldInitializer() { Map> map = new HashMap<>(); map.put("pageSize", PAGE_SIZE_FIELD); + map.put("x-amz-nested-header", HEADER_PARAM_FIELD); + map.put("nestedQuery", QUERY_PARAM_FIELD); return Collections.unmodifiableMap(map); } @@ -157,16 +201,40 @@ public interface Builder extends SdkPojo, CopyableBuilder Date: Tue, 17 Mar 2026 14:58:05 -0700 Subject: [PATCH 8/9] Fix comments 2 --- .../awssdk/codegen/CodeGeneratorTest.java | 4 + .../codegen/expected-nested-options.java | 88 ++++++++++++++++++- .../uri-on-non-input-shape-service.json | 33 +++++++ 3 files changed, 122 insertions(+), 3 deletions(-) diff --git a/codegen/src/test/java/software/amazon/awssdk/codegen/CodeGeneratorTest.java b/codegen/src/test/java/software/amazon/awssdk/codegen/CodeGeneratorTest.java index 7da5022cbb83..194a315537b8 100644 --- a/codegen/src/test/java/software/amazon/awssdk/codegen/CodeGeneratorTest.java +++ b/codegen/src/test/java/software/amazon/awssdk/codegen/CodeGeneratorTest.java @@ -202,6 +202,10 @@ void execute_uriLocationOnNonInputShape_isIgnored() throws IOException { assertThat(nestedShape.findMemberModelByC2jName("pageSize").getHttp().isGreedy()).isFalse(); assertThat(nestedShape.findMemberModelByC2jName("headerParam").getHttp().getLocation()).isNull(); assertThat(nestedShape.findMemberModelByC2jName("queryParam").getHttp().getLocation()).isNull(); + assertThat(nestedShape.findMemberModelByC2jName("prefixHeaders").getHttp().getLocation()).isNull(); + + ShapeModel sharedShape = intermediateModel.getShapes().get("SharedShapeOperationRequest"); + assertThat(sharedShape.findMemberModelByC2jName("sharedId").getHttp().getLocation()).isEqualTo(Location.URI); Path generatedNestedOptions = Files.walk(outputDir) .filter(p -> p.getFileName().toString().equals("NestedOptions.java")) diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/expected-nested-options.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/expected-nested-options.java index 8254a9771fa5..4940bbc56a14 100644 --- a/codegen/src/test/resources/software/amazon/awssdk/codegen/expected-nested-options.java +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/expected-nested-options.java @@ -31,6 +31,9 @@ import software.amazon.awssdk.core.protocol.MarshallLocation; import software.amazon.awssdk.core.protocol.MarshallingType; import software.amazon.awssdk.core.traits.LocationTrait; +import software.amazon.awssdk.core.traits.MapTrait; +import software.amazon.awssdk.core.util.DefaultSdkAutoConstructMap; +import software.amazon.awssdk.core.util.SdkAutoConstructMap; import software.amazon.awssdk.utils.ToString; import software.amazon.awssdk.utils.builder.CopyableBuilder; import software.amazon.awssdk.utils.builder.ToCopyableBuilder; @@ -52,8 +55,22 @@ public final class NestedOptions implements SdkPojo, Serializable, ToCopyableBui .memberName("queryParam").getter(getter(NestedOptions::queryParam)).setter(setter(Builder::queryParam)) .traits(LocationTrait.builder().location(MarshallLocation.PAYLOAD).locationName("nestedQuery").build()).build(); + private static final SdkField> PREFIX_HEADERS_FIELD = SdkField + .> builder(MarshallingType.MAP) + .memberName("prefixHeaders") + .getter(getter(NestedOptions::prefixHeaders)) + .setter(setter(Builder::prefixHeaders)) + .traits(LocationTrait.builder().location(MarshallLocation.PAYLOAD).locationName("x-amz-prefix-").build(), + MapTrait.builder() + .keyLocationName("key") + .valueLocationName("value") + .valueFieldInfo( + SdkField. builder(MarshallingType.STRING) + .traits(LocationTrait.builder().location(MarshallLocation.PAYLOAD) + .locationName("value").build()).build()).build()).build(); + private static final List> SDK_FIELDS = Collections.unmodifiableList(Arrays.asList(PAGE_SIZE_FIELD, - HEADER_PARAM_FIELD, QUERY_PARAM_FIELD)); + HEADER_PARAM_FIELD, QUERY_PARAM_FIELD, PREFIX_HEADERS_FIELD)); private static final Map> SDK_NAME_TO_FIELD = memberNameToFieldInitializer(); @@ -65,10 +82,13 @@ public final class NestedOptions implements SdkPojo, Serializable, ToCopyableBui private final String queryParam; + private final Map prefixHeaders; + private NestedOptions(BuilderImpl builder) { this.pageSize = builder.pageSize; this.headerParam = builder.headerParam; this.queryParam = builder.queryParam; + this.prefixHeaders = builder.prefixHeaders; } /** @@ -98,6 +118,34 @@ public final String queryParam() { return queryParam; } + /** + * For responses, this returns true if the service returned a value for the PrefixHeaders property. This DOES NOT + * check that the value is non-empty (for which, you should check the {@code isEmpty()} method on the property). + * This is useful because the SDK will never return a null collection or map, but you may need to differentiate + * between the service returning nothing (or null) and the service returning an empty collection or map. For + * requests, this returns true if a value for the property was specified in the request builder, and false if a + * value was not specified. + */ + public final boolean hasPrefixHeaders() { + return prefixHeaders != null && !(prefixHeaders instanceof SdkAutoConstructMap); + } + + /** + * Returns the value of the PrefixHeaders property for this object. + *

+ * Attempts to modify the collection returned by this method will result in an UnsupportedOperationException. + *

+ *

+ * This method will never return null. If you would like to know whether the service returned this field (so that + * you can differentiate between null and empty), you can use the {@link #hasPrefixHeaders} method. + *

+ * + * @return The value of the PrefixHeaders property for this object. + */ + public final Map prefixHeaders() { + return prefixHeaders; + } + @Override public Builder toBuilder() { return new BuilderImpl(this); @@ -117,6 +165,7 @@ public final int hashCode() { hashCode = 31 * hashCode + Objects.hashCode(pageSize()); hashCode = 31 * hashCode + Objects.hashCode(headerParam()); hashCode = 31 * hashCode + Objects.hashCode(queryParam()); + hashCode = 31 * hashCode + Objects.hashCode(hasPrefixHeaders() ? prefixHeaders() : null); return hashCode; } @@ -138,7 +187,8 @@ public final boolean equalsBySdkFields(Object obj) { } NestedOptions other = (NestedOptions) obj; return Objects.equals(pageSize(), other.pageSize()) && Objects.equals(headerParam(), other.headerParam()) - && Objects.equals(queryParam(), other.queryParam()); + && Objects.equals(queryParam(), other.queryParam()) && hasPrefixHeaders() == other.hasPrefixHeaders() + && Objects.equals(prefixHeaders(), other.prefixHeaders()); } /** @@ -148,7 +198,7 @@ public final boolean equalsBySdkFields(Object obj) { @Override public final String toString() { return ToString.builder("NestedOptions").add("PageSize", pageSize()).add("HeaderParam", headerParam()) - .add("QueryParam", queryParam()).build(); + .add("QueryParam", queryParam()).add("PrefixHeaders", hasPrefixHeaders() ? prefixHeaders() : null).build(); } public final Optional getValueForField(String fieldName, Class clazz) { @@ -159,6 +209,8 @@ public final Optional getValueForField(String fieldName, Class clazz) return Optional.ofNullable(clazz.cast(headerParam())); case "queryParam": return Optional.ofNullable(clazz.cast(queryParam())); + case "prefixHeaders": + return Optional.ofNullable(clazz.cast(prefixHeaders())); default: return Optional.empty(); } @@ -179,6 +231,7 @@ private static Map> memberNameToFieldInitializer() { map.put("pageSize", PAGE_SIZE_FIELD); map.put("x-amz-nested-header", HEADER_PARAM_FIELD); map.put("nestedQuery", QUERY_PARAM_FIELD); + map.put("x-amz-prefix-", PREFIX_HEADERS_FIELD); return Collections.unmodifiableMap(map); } @@ -219,6 +272,15 @@ public interface Builder extends SdkPojo, CopyableBuilder prefixHeaders); } static final class BuilderImpl implements Builder { @@ -228,6 +290,8 @@ static final class BuilderImpl implements Builder { private String queryParam; + private Map prefixHeaders = DefaultSdkAutoConstructMap.getInstance(); + private BuilderImpl() { } @@ -235,6 +299,7 @@ private BuilderImpl(NestedOptions model) { pageSize(model.pageSize); headerParam(model.headerParam); queryParam(model.queryParam); + prefixHeaders(model.prefixHeaders); } public final String getPageSize() { @@ -279,6 +344,23 @@ public final Builder queryParam(String queryParam) { return this; } + public final Map getPrefixHeaders() { + if (prefixHeaders instanceof SdkAutoConstructMap) { + return null; + } + return prefixHeaders; + } + + public final void setPrefixHeaders(Map prefixHeaders) { + this.prefixHeaders = MapOfStringsCopier.copy(prefixHeaders); + } + + @Override + public final Builder prefixHeaders(Map prefixHeaders) { + this.prefixHeaders = MapOfStringsCopier.copy(prefixHeaders); + return this; + } + @Override public NestedOptions build() { return new NestedOptions(this); diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/uri-on-non-input-shape-service.json b/codegen/src/test/resources/software/amazon/awssdk/codegen/uri-on-non-input-shape-service.json index 08357e6e89b2..f858a11b94ed 100644 --- a/codegen/src/test/resources/software/amazon/awssdk/codegen/uri-on-non-input-shape-service.json +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/uri-on-non-input-shape-service.json @@ -23,6 +23,16 @@ "input": { "shape": "SomeOperationRequest" } + }, + "SharedShapeOperation": { + "name": "SharedShapeOperation", + "http": { + "method": "GET", + "requestUri": "/shared/{sharedId}" + }, + "input": { + "shape": "SharedShape" + } } }, "shapes": { @@ -36,6 +46,9 @@ }, "options": { "shape": "NestedOptions" + }, + "shared": { + "shape": "SharedShape" } } }, @@ -56,11 +69,31 @@ "shape": "String", "location": "querystring", "locationName": "nestedQuery" + }, + "prefixHeaders": { + "shape": "MapOfStrings", + "location": "headers", + "locationName": "x-amz-prefix-" + } + } + }, + "SharedShape": { + "type": "structure", + "members": { + "sharedId": { + "shape": "String", + "location": "uri", + "locationName": "sharedId" } } }, "String": { "type": "string" + }, + "MapOfStrings": { + "type": "map", + "key": {"shape": "String"}, + "value": {"shape": "String"} } }, "documentation": "A service with HTTP binding locations on non-input shapes" From c4a79f0233288960c3c157877d34c6ab2b96f814 Mon Sep 17 00:00:00 2001 From: RanVaknin <50976344+RanVaknin@users.noreply.github.com> Date: Wed, 18 Mar 2026 11:53:34 -0700 Subject: [PATCH 9/9] Add assertion --- .../src/main/java/software/amazon/awssdk/codegen/AddShapes.java | 2 +- .../test/java/software/amazon/awssdk/codegen/AddShapesTest.java | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/AddShapes.java b/codegen/src/main/java/software/amazon/awssdk/codegen/AddShapes.java index 70d7be7ec3b0..a965a12f5f29 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/AddShapes.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/AddShapes.java @@ -315,7 +315,7 @@ private ParameterHttpMapping generateParameterHttpMapping(Shape parentShape, ParameterHttpMapping mapping = new ParameterHttpMapping(); - // https://smithy.io/2.0/spec/http-bindings.html#httplabel-is-only-used-on-top-level-input + // https://smithy.io/2.0/spec/http-bindings.html Location location = isDirectOperationShape(parentShape, allC2jShapes) ? Location.forValue(member.getLocation()) : null; diff --git a/codegen/src/test/java/software/amazon/awssdk/codegen/AddShapesTest.java b/codegen/src/test/java/software/amazon/awssdk/codegen/AddShapesTest.java index 0243c001e90e..45185281666c 100644 --- a/codegen/src/test/java/software/amazon/awssdk/codegen/AddShapesTest.java +++ b/codegen/src/test/java/software/amazon/awssdk/codegen/AddShapesTest.java @@ -84,6 +84,7 @@ void generateShapeModel_memberRequiredByNestedShape_setsMemberModelAsRequired() MemberModel requiredMemberModel = requestShapeModel.findMemberModelByC2jName(queryParamName); assertThat(requestShapeModel.getRequired()).contains(queryParamName); + assertThat(requiredMemberModel.getHttp().getLocation()).isNull(); assertThat(requiredMemberModel.isRequired()).isTrue(); }