Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -342,38 +342,54 @@
*/
private boolean isGreedy(Shape parentShape, Map<String, Shape> 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<String> requestUri = findRequestUri(parentShape, allC2jShapes);
if (requestUri.isPresent()
&& requestUri.get().contains(String.format("{%s+}", mapping.getMarshallLocationName()))) {
return true;
}
}
return false;
}

/**
* Given an input shape, finds the Request URI for the operation that input is referenced from.
* 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 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.
* @throws ModelInvalidException If the shape is a direct operation input but requestUri is missing.
*/
private String findRequestUri(Shape parentShape, Map<String, Shape> allC2jShapes) {
private Optional<String> findRequestUri(Shape parentShape, Map<String, Shape> allC2jShapes) {
Optional<Operation> 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 detailMsg = "Could not find request URI for input shape for operation: " + operation;
ValidationEntry entry =
new ValidationEntry().withErrorId(ValidationErrorId.REQUEST_URI_NOT_FOUND)
.withDetailMessage(detailMsg)
.withSeverity(ValidationErrorSeverity.DANGER);
return ModelInvalidException.builder().validationEntries(Collections.singletonList(entry)).build();
});
if (!operation.isPresent()) {
// Not a direct operation input shape, should be ignored.
Copy link
Contributor

@joviegas joviegas Mar 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How does generated code looks like when we have below

  "NestedOptions": {
      "type": "structure",
      "members": {
        "pageSize": {
          "shape": "String",
          "location": "uri",
          "locationName": "pageSize"
        }
      }
    }

when we say

httpLabel is only considered when applied to top-level members of an operation's input structure. This trait has no meaning in any other context and is simply ignored.

what does ignore mean ? should we internally treat it as

  "NestedOptions": {
      "type": "structure",
      "members": {
        "pageSize": {
          "shape": "String",
          "locationName": "pageSize"
        }
      }
    }

and marshall it in the regular payload. , just as we do for shapes where locationName is not specified

Or completely remove pageSize from NestedOptions structure ?

Can we please have a test case comparing a generated code ?

// 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()

Check warning on line 381 in codegen/src/main/java/software/amazon/awssdk/codegen/AddShapes.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Call "Optional#isPresent()" before accessing the value.

See more on https://sonarcloud.io/project/issues?id=aws_aws-sdk-java-v2&issues=AZzo6ZCsWBb7JerGzoPT&open=AZzo6ZCsWBb7JerGzoPT&pullRequest=6789
.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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -48,6 +49,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;

Check warning on line 52 in codegen/src/test/java/software/amazon/awssdk/codegen/CodeGeneratorTest.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this unused import 'software.amazon.awssdk.codegen.validation.ValidationEntry'.

See more on https://sonarcloud.io/project/issues?id=aws_aws-sdk-java-v2&issues=AZzo6ZCdWBb7JerGzoPS&open=AZzo6ZCdWBb7JerGzoPS&pullRequest=6789
import software.amazon.awssdk.codegen.validation.ValidationErrorId;

public class CodeGeneratorTest {
Expand Down Expand Up @@ -176,6 +178,18 @@
});
}

@Test
void execute_uriLocationOnNonInputShape_isIgnored() throws IOException {
C2jModels models = C2jModels.builder()
.customizationConfig(CustomizationConfig.create())
.serviceModel(getUriOnNonInputShapeServiceModel())
.build();

// 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
void execute_operationHasNoRequestUri_throwsValidationError() throws IOException {
C2jModels models = C2jModels.builder()
Expand Down Expand Up @@ -244,6 +258,11 @@
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)) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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"
}
Loading