-
Notifications
You must be signed in to change notification settings - Fork 996
Fix codegen to ignore httpLabel on non-input shapes per Smithy spec #6789
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
0dd930d
2559160
58dbf2f
691d561
560d15a
d739627
3231818
d75e303
2d77293
65826f0
c9173e8
2d52a01
1302754
16c162b
68fc993
7929105
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -311,18 +311,67 @@ private ParameterHttpMapping generateParameterHttpMapping(Shape parentShape, | |
|
|
||
| ParameterHttpMapping mapping = new ParameterHttpMapping(); | ||
|
|
||
| // Per the Smithy spec, HTTP binding traits are only honored on top-level shapes (direct operation | ||
| // input/output/error). When a location trait is ignored, its locationName is also ignored so the member | ||
| // name is used as the wire name. https://smithy.io/2.0/spec/http-bindings.html | ||
| Location resolvedLocation = resolveLocation(parentShape, member, allC2jShapes); | ||
| boolean locationIgnored = member.getLocation() != null && resolvedLocation == null; | ||
|
|
||
| Shape memberShape = allC2jShapes.get(member.getShape()); | ||
| mapping.withLocation(Location.forValue(member.getLocation())) | ||
| String marshallLocationName = locationIgnored | ||
| ? memberName : deriveMarshallerLocationName(memberShape, memberName, member, protocol); | ||
| String unmarshallLocationName = locationIgnored | ||
| ? memberName : deriveUnmarshallerLocationName(memberShape, memberName, member); | ||
|
|
||
| mapping.withLocation(resolvedLocation) | ||
| .withPayload(member.isPayload()).withStreaming(member.isStreaming()) | ||
| .withFlattened(isFlattened(member, memberShape)) | ||
| .withUnmarshallLocationName(deriveUnmarshallerLocationName(memberShape, memberName, member)) | ||
| .withMarshallLocationName( | ||
| deriveMarshallerLocationName(memberShape, memberName, member, protocol)) | ||
| .withUnmarshallLocationName(unmarshallLocationName) | ||
| .withMarshallLocationName(marshallLocationName) | ||
| .withIsGreedy(isGreedy(parentShape, allC2jShapes, mapping)); | ||
|
|
||
| return mapping; | ||
| } | ||
|
|
||
| private Location resolveLocation(Shape parentShape, Member member, Map<String, Shape> allC2jShapes) { | ||
| Location location = Location.forValue(member.getLocation()); | ||
| if (location == null) { | ||
| return null; | ||
| } | ||
| switch (location) { | ||
| case URI: | ||
| case QUERY_STRING: | ||
| return isDirectInputShape(parentShape, allC2jShapes) ? location : null; | ||
| case HEADER: | ||
| case HEADERS: | ||
| return isTopLevelShape(parentShape, allC2jShapes) ? location : null; | ||
| case STATUS_CODE: | ||
| return isDirectOutputShape(parentShape, allC2jShapes) ? location : null; | ||
| default: | ||
| return location; | ||
| } | ||
| } | ||
|
|
||
| private boolean isDirectInputShape(Shape shape, Map<String, Shape> allC2jShapes) { | ||
| return builder.getService().getOperations().values().stream() | ||
| .filter(o -> o.getInput() != null) | ||
| .anyMatch(o -> allC2jShapes.get(o.getInput().getShape()).equals(shape)); | ||
| } | ||
|
|
||
| private boolean isDirectOutputShape(Shape shape, Map<String, Shape> allC2jShapes) { | ||
| return builder.getService().getOperations().values().stream() | ||
| .filter(o -> o.getOutput() != null) | ||
| .anyMatch(o -> allC2jShapes.get(o.getOutput().getShape()).equals(shape)); | ||
| } | ||
|
|
||
| private boolean isTopLevelShape(Shape shape, Map<String, Shape> allC2jShapes) { | ||
| return builder.getService().getOperations().values().stream() | ||
| .anyMatch(o -> (o.getInput() != null && allC2jShapes.get(o.getInput().getShape()).equals(shape)) | ||
| || (o.getOutput() != null && allC2jShapes.get(o.getOutput().getShape()).equals(shape)) | ||
| || (o.getErrors() != null && o.getErrors().stream() | ||
| .anyMatch(e -> allC2jShapes.get(e.getShape()).equals(shape)))); | ||
| } | ||
|
|
||
| private boolean isFlattened(Member member, Shape memberShape) { | ||
| return member.isFlattened() | ||
| || memberShape.isFlattened(); | ||
|
|
@@ -342,38 +391,52 @@ private boolean isRequiredMember(String memberName, Shape memberShape) { | |
| */ | ||
| 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. | ||
| * | ||
| * @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<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. | ||
RanVaknin marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| // 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() | ||
| .orElseThrow(() -> new IllegalStateException("Shape not found in model: " + parentShape)); | ||
| String detailMsg = "Operation referencing input shape '" + shapeName | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we have any test cases asserting this error and error message so that shape name is printed correctly ?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added 👍 |
||
| + "' has no requestUri configured in its HTTP binding."; | ||
| 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) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -84,8 +84,37 @@ void generateShapeModel_memberRequiredByNestedShape_setsMemberModelAsRequired() | |
| MemberModel requiredMemberModel = requestShapeModel.findMemberModelByC2jName(queryParamName); | ||
|
|
||
| assertThat(requestShapeModel.getRequired()).contains(queryParamName); | ||
| assertThat(requiredMemberModel.getHttp().getLocation()).isEqualTo(Location.QUERY_STRING); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why are we removing this assertion ?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Because it was asserting the buggy behavior.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks Ran |
||
| 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); | ||
| } | ||
|
|
||
| @Test | ||
| void generateShapeModel_locationNameOnNestedShape_usesMemberNameForMarshalling() { | ||
RanVaknin marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| ShapeModel inputShape = intermediateModel.getShapes().get("NestedQueryParameterOperation"); | ||
| assertThat(inputShape.findMemberModelByC2jName("NestedHeaderMember").getHttp().getMarshallLocationName()).isEqualTo("NestedHeaderMember"); | ||
| } | ||
|
|
||
| @Test | ||
| void generateShapeModel_locationNameOnTopLevelShape_honorsLocationName() { | ||
| ShapeModel inputShape = intermediateModel.getShapes().get("QueryParameterOperationRequest"); | ||
| MemberModel member = inputShape.findMemberModelByC2jName("PayloadMemberWithCustomName"); | ||
| assertThat(member.getHttp().getLocation()).isNull(); | ||
| assertThat(member.getHttp().getMarshallLocationName()).isEqualTo("CustomWireName"); | ||
| } | ||
|
|
||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This probably isnt a big deal (but I ran into it in the Smithy POC...) - the
equalsmethod in shape is just a reference check. That is probably actually what we want here so its fine. But would be an issue if the shape itself was duplicated or something.