Skip to content

Commit 4496c13

Browse files
authored
API service calls custom query param validation (#3546)
* Implement RequiredTrait * Enforce presence of required query params * Added codegen tests; missing javadoc * Revised codegen tests * Added integration tests for missing query parameter * Added tests for validating nested shapes * Misc fixes * Added ability to select traits to validate * Added codegen tests for verifying marshaller have correct validation enabling method calls * Handle cases where customization.config has no enabledTraitValidations information * Address review comments * Attach RequiredTrait only to operations for enabled services * Undo misc fixes breaking compatibility * Enforce RequiredTrait across all shape locations * Added tests for validating List and Map querystring objects * Updated CustomizationConfig * Updated changelog entry * Verify SdkField is non-null verify checking for traits * Trimmed changelog entry * Fixed new integration tests expectations * Moved new integration tests into separate classes * Updated expectation of existing integration test
1 parent 028ff63 commit 4496c13

File tree

40 files changed

+3001
-28
lines changed

40 files changed

+3001
-28
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"category": "Amazon S3",
3+
"contributor": "",
4+
"type": "bugfix",
5+
"description": "S3 query param validation\n\nThe SDK will raise SdkClientException when a null value is set for a member marked with the required trait."
6+
}

codegen/src/main/java/software/amazon/awssdk/codegen/AddShapes.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323

2424
import java.util.List;
2525
import java.util.Map;
26+
import java.util.Optional;
2627
import software.amazon.awssdk.codegen.internal.TypeUtils;
2728
import software.amazon.awssdk.codegen.model.config.customization.CustomizationConfig;
2829
import software.amazon.awssdk.codegen.model.intermediate.EnumModel;
@@ -192,6 +193,7 @@ private MemberModel generateMemberModel(String c2jMemberName, Member c2jMemberDe
192193
memberModel.setXmlAttribute(c2jMemberDefinition.isXmlAttribute());
193194
memberModel.setUnionEnumTypeName(namingStrategy.getUnionEnumTypeName(memberModel));
194195
memberModel.setContextParam(c2jMemberDefinition.getContextParam());
196+
memberModel.setRequired(isRequiredMember(c2jMemberName, parentShape));
195197

196198

197199
// Pass the xmlNameSpace from the member reference
@@ -309,6 +311,12 @@ private boolean isFlattened(Member member, Shape memberShape) {
309311
|| memberShape.isFlattened();
310312
}
311313

314+
private boolean isRequiredMember(String memberName, Shape memberShape) {
315+
return Optional.ofNullable(memberShape.getRequired())
316+
.map(l -> l.contains(memberName))
317+
.orElse(false);
318+
}
319+
312320
/**
313321
* @param parentShape Shape containing the member in question.
314322
* @param allC2jShapes All shapes in the service model.

codegen/src/main/java/software/amazon/awssdk/codegen/model/config/customization/CustomizationConfig.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,11 @@ public class CustomizationConfig {
215215

216216
private List<String> interceptors = new ArrayList<>();
217217

218+
/**
219+
* Whether marshallers perform validations against members marked with RequiredTrait.
220+
*/
221+
private boolean requiredTraitValidationEnabled = false;
222+
218223
private CustomizationConfig() {
219224
}
220225

@@ -559,4 +564,12 @@ public List<String> getInterceptors() {
559564
public void setInterceptors(List<String> interceptors) {
560565
this.interceptors = interceptors;
561566
}
567+
568+
public boolean isRequiredTraitValidationEnabled() {
569+
return requiredTraitValidationEnabled;
570+
}
571+
572+
public void setRequiredTraitValidationEnabled(boolean requiredTraitValidationEnabled) {
573+
this.requiredTraitValidationEnabled = requiredTraitValidationEnabled;
574+
}
562575
}

codegen/src/main/java/software/amazon/awssdk/codegen/model/intermediate/MemberModel.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ public class MemberModel extends DocumentationModel {
5959

6060
private String deprecatedMessage;
6161

62+
private boolean required;
63+
6264
private ListModel listModel;
6365

6466
private MapModel mapModel;
@@ -316,6 +318,14 @@ public void setDeprecatedMessage(String deprecatedMessage) {
316318
this.deprecatedMessage = deprecatedMessage;
317319
}
318320

321+
public boolean isRequired() {
322+
return required;
323+
}
324+
325+
public void setRequired(boolean required) {
326+
this.required = required;
327+
}
328+
319329
public boolean isEventPayload() {
320330
return eventPayload;
321331
}

codegen/src/main/java/software/amazon/awssdk/codegen/poet/model/ShapeModelSpec.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
import software.amazon.awssdk.core.traits.LocationTrait;
4646
import software.amazon.awssdk.core.traits.MapTrait;
4747
import software.amazon.awssdk.core.traits.PayloadTrait;
48+
import software.amazon.awssdk.core.traits.RequiredTrait;
4849
import software.amazon.awssdk.core.traits.TimestampFormatTrait;
4950
import software.amazon.awssdk.core.traits.XmlAttributeTrait;
5051
import software.amazon.awssdk.core.traits.XmlAttributesTrait;
@@ -184,6 +185,9 @@ private CodeBlock traits(MemberModel m) {
184185
if (m.isXmlAttribute()) {
185186
traits.add(createXmlAttributeTrait());
186187
}
188+
if (customizationConfig.isRequiredTraitValidationEnabled() && m.isRequired()) {
189+
traits.add(createRequiredTrait());
190+
}
187191

188192
if (!traits.isEmpty()) {
189193
return CodeBlock.builder()
@@ -268,6 +272,12 @@ private CodeBlock createPayloadTrait() {
268272
.build();
269273
}
270274

275+
private CodeBlock createRequiredTrait() {
276+
return CodeBlock.builder()
277+
.add("$T.create()", ClassName.get(RequiredTrait.class))
278+
.build();
279+
}
280+
271281
private CodeBlock createMapTrait(MemberModel m) {
272282
return CodeBlock.builder()
273283
.add("$T.builder()\n"
@@ -306,7 +316,7 @@ private CodeBlock createXmlAttributesTrait(MemberModel model) {
306316
String uri = xmlNamespace.getUri();
307317
String prefix = xmlNamespace.getPrefix();
308318
CodeBlock.Builder codeBlockBuilder = CodeBlock.builder()
309-
.add("$T.create(", ClassName.get(XmlAttributesTrait.class));
319+
.add("$T.create(", ClassName.get(XmlAttributesTrait.class));
310320

311321
String namespacePrefix = "xmlns:" + prefix;
312322
codeBlockBuilder.add("$T.of($S, $T.builder().attributeGetter((ignore) -> $S).build())",
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package software.amazon.awssdk.codegen;
17+
18+
import static org.assertj.core.api.Assertions.assertThat;
19+
20+
import java.io.File;
21+
import java.io.IOException;
22+
import org.junit.jupiter.api.BeforeAll;
23+
import org.junit.jupiter.api.Test;
24+
import software.amazon.awssdk.codegen.model.config.customization.CustomizationConfig;
25+
import software.amazon.awssdk.codegen.model.intermediate.IntermediateModel;
26+
import software.amazon.awssdk.codegen.model.intermediate.MemberModel;
27+
import software.amazon.awssdk.codegen.model.intermediate.ShapeModel;
28+
import software.amazon.awssdk.codegen.model.service.Location;
29+
import software.amazon.awssdk.codegen.model.service.ServiceModel;
30+
import software.amazon.awssdk.codegen.poet.model.AwsModelSpecTest;
31+
import software.amazon.awssdk.codegen.utils.ModelLoaderUtils;
32+
33+
class AddShapesTest {
34+
35+
private static IntermediateModel intermediateModel;
36+
37+
@BeforeAll
38+
public static void setUp() throws IOException {
39+
File serviceModelFile = new File(AwsModelSpecTest.class.getResource("service-2.json").getFile());
40+
File customizationConfigFile = new File(AwsModelSpecTest.class
41+
.getResource("customization.config")
42+
.getFile());
43+
44+
intermediateModel = new IntermediateModelBuilder(
45+
C2jModels.builder()
46+
.serviceModel(ModelLoaderUtils.loadModel(ServiceModel.class, serviceModelFile))
47+
.customizationConfig(ModelLoaderUtils.loadModel(CustomizationConfig.class, customizationConfigFile))
48+
.build())
49+
.build();
50+
}
51+
52+
@Test
53+
void generateShapeModel_memberRequiredByShape_setsMemberModelAsRequired() {
54+
String requestShapeName = "QueryParameterOperationRequest";
55+
String queryParamName = "QueryParamOne";
56+
57+
ShapeModel requestShapeModel = intermediateModel.getShapes().get(requestShapeName);
58+
MemberModel requiredMemberModel = requestShapeModel.findMemberModelByC2jName(queryParamName);
59+
60+
assertThat(requestShapeModel.getRequired()).contains(queryParamName);
61+
assertThat(requiredMemberModel.getHttp().getLocation()).isEqualTo(Location.QUERY_STRING);
62+
assertThat(requiredMemberModel.isRequired()).isTrue();
63+
}
64+
65+
@Test
66+
void generateShapeModel_memberNotRequiredByShape_doesNotSetMemberModelAsRequired() {
67+
String requestShapeName = "QueryParameterOperationRequest";
68+
String queryParamName = "QueryParamTwo";
69+
70+
ShapeModel requestShapeModel = intermediateModel.getShapes().get(requestShapeName);
71+
MemberModel requiredMemberModel = requestShapeModel.findMemberModelByC2jName(queryParamName);
72+
73+
assertThat(requestShapeModel.getRequired()).doesNotContain(queryParamName);
74+
assertThat(requiredMemberModel.getHttp().getLocation()).isEqualTo(Location.QUERY_STRING);
75+
assertThat(requiredMemberModel.isRequired()).isFalse();
76+
}
77+
78+
@Test
79+
void generateShapeModel_memberRequiredByNestedShape_setsMemberModelAsRequired() {
80+
String requestShapeName = "NestedQueryParameterOperation";
81+
String queryParamName = "QueryParamOne";
82+
83+
ShapeModel requestShapeModel = intermediateModel.getShapes().get(requestShapeName);
84+
MemberModel requiredMemberModel = requestShapeModel.findMemberModelByC2jName(queryParamName);
85+
86+
assertThat(requestShapeModel.getRequired()).contains(queryParamName);
87+
assertThat(requiredMemberModel.getHttp().getLocation()).isEqualTo(Location.QUERY_STRING);
88+
assertThat(requiredMemberModel.isRequired()).isTrue();
89+
}
90+
91+
}

codegen/src/test/java/software/amazon/awssdk/codegen/poet/model/AwsServiceBaseRequestSpecTest.java

Lines changed: 50 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,36 +20,78 @@
2020

2121
import java.io.File;
2222
import java.io.IOException;
23+
import java.util.Arrays;
24+
import java.util.Optional;
25+
import java.util.regex.Pattern;
26+
import org.assertj.core.api.Assertions;
2327
import org.junit.jupiter.api.BeforeAll;
2428
import org.junit.jupiter.api.Test;
2529
import software.amazon.awssdk.codegen.C2jModels;
2630
import software.amazon.awssdk.codegen.IntermediateModelBuilder;
2731
import software.amazon.awssdk.codegen.model.config.customization.CustomizationConfig;
2832
import software.amazon.awssdk.codegen.model.intermediate.IntermediateModel;
33+
import software.amazon.awssdk.codegen.model.intermediate.ShapeModel;
2934
import software.amazon.awssdk.codegen.model.service.ServiceModel;
35+
import software.amazon.awssdk.codegen.poet.PoetUtils;
3036
import software.amazon.awssdk.codegen.utils.ModelLoaderUtils;
3137

3238
public class AwsServiceBaseRequestSpecTest {
39+
3340
private static IntermediateModel intermediateModel;
3441

3542
@BeforeAll
3643
public static void setUp() throws IOException {
3744
File serviceModelFile = new File(AwsModelSpecTest.class.getResource("service-2.json").getFile());
3845
File customizationConfigFile = new File(AwsModelSpecTest.class
39-
.getResource("customization.config")
40-
.getFile());
46+
.getResource("customization.config")
47+
.getFile());
4148

4249
intermediateModel = new IntermediateModelBuilder(
43-
C2jModels.builder()
44-
.serviceModel(ModelLoaderUtils.loadModel(ServiceModel.class, serviceModelFile))
45-
.customizationConfig(ModelLoaderUtils.loadModel(CustomizationConfig.class, customizationConfigFile))
46-
.build())
47-
.build();
50+
C2jModels.builder()
51+
.serviceModel(ModelLoaderUtils.loadModel(ServiceModel.class, serviceModelFile))
52+
.customizationConfig(ModelLoaderUtils.loadModel(CustomizationConfig.class, customizationConfigFile))
53+
.build())
54+
.build();
4855
}
4956

5057
@Test
51-
public void testGeneration() {
58+
void testGeneration() {
5259
AwsServiceBaseRequestSpec spec = new AwsServiceBaseRequestSpec(intermediateModel);
5360
assertThat(spec, generatesTo(spec.className().simpleName().toLowerCase() + ".java"));
5461
}
62+
63+
@Test
64+
void buildJavaFile_memberRequiredByShape_addsTraitToGeneratedCode() {
65+
String requestShapeName = "QueryParameterOperationRequest";
66+
String queryParamName = "QueryParamOne";
67+
68+
ShapeModel requestShapeModel = intermediateModel.getShapes().get(requestShapeName);
69+
AwsServiceModel spec = new AwsServiceModel(intermediateModel, requestShapeModel);
70+
String codeString = PoetUtils.buildJavaFile(spec).toString();
71+
72+
String uploadIdDeclarationString = findTraitDeclarationString(codeString, queryParamName).get();
73+
Assertions.assertThat(uploadIdDeclarationString).contains("RequiredTrait.create()");
74+
}
75+
76+
@Test
77+
void buildJavaFile_memberNotRequiredByShape_doesNotAddTraitToGeneratedCode() {
78+
String requestShapeName = "QueryParameterOperationRequest";
79+
String queryParamName = "QueryParamTwo";
80+
81+
ShapeModel requestShapeModel = intermediateModel.getShapes().get(requestShapeName);
82+
AwsServiceModel spec = new AwsServiceModel(intermediateModel, requestShapeModel);
83+
String codeString = PoetUtils.buildJavaFile(spec).toString();
84+
85+
String uploadIdDeclarationString = findTraitDeclarationString(codeString, queryParamName).get();
86+
Assertions.assertThat(uploadIdDeclarationString).doesNotContain("RequiredTrait.create()");
87+
}
88+
89+
90+
private static Optional<String> findTraitDeclarationString(String javaFileString, String fieldName) {
91+
return Arrays.stream(Pattern.compile("\n\n").split(javaFileString))
92+
.filter(block -> block.contains(fieldName))
93+
.filter(block -> block.contains("traits"))
94+
.findFirst();
95+
}
96+
5597
}

codegen/src/test/resources/software/amazon/awssdk/codegen/poet/model/customization.config

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,5 +30,6 @@
3030
]
3131
}
3232
},
33-
"underscoresInNameBehavior": "ALLOW"
34-
}
33+
"underscoresInNameBehavior": "ALLOW",
34+
"requiredTraitValidationEnabled": true
35+
}

0 commit comments

Comments
 (0)