Skip to content

Commit dd8fe47

Browse files
authored
Add validation for missing request URI (#6182)
* Add validation for missing request URI Throw InvalidModelException if we can't find the request URI for an operation when a member has URI mapping. * Avoid default encoding
1 parent c664392 commit dd8fe47

File tree

6 files changed

+134
-14
lines changed

6 files changed

+134
-14
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"type": "feature",
3+
"category": "AWS SDK for Java v2",
4+
"contributor": "",
5+
"description": "Add code generation validation for missing request URI on an operation."
6+
}

codegen-maven-plugin/src/main/java/software/amazon/awssdk/codegen/maven/plugin/GenerationMojo.java

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717

1818
import java.io.File;
1919
import java.io.IOException;
20+
import java.io.Writer;
21+
import java.nio.charset.StandardCharsets;
2022
import java.nio.file.Files;
2123
import java.nio.file.Path;
2224
import java.nio.file.Paths;
@@ -35,6 +37,7 @@
3537
import software.amazon.awssdk.codegen.C2jModels;
3638
import software.amazon.awssdk.codegen.CodeGenerator;
3739
import software.amazon.awssdk.codegen.IntermediateModelBuilder;
40+
import software.amazon.awssdk.codegen.internal.Jackson;
3841
import software.amazon.awssdk.codegen.internal.Utils;
3942
import software.amazon.awssdk.codegen.model.config.customization.CustomizationConfig;
4043
import software.amazon.awssdk.codegen.model.intermediate.IntermediateModel;
@@ -44,6 +47,8 @@
4447
import software.amazon.awssdk.codegen.model.service.ServiceModel;
4548
import software.amazon.awssdk.codegen.model.service.Waiters;
4649
import software.amazon.awssdk.codegen.utils.ModelLoaderUtils;
50+
import software.amazon.awssdk.codegen.validation.ModelInvalidException;
51+
import software.amazon.awssdk.codegen.validation.ModelValidationReport;
4752
import software.amazon.awssdk.utils.StringUtils;
4853

4954
/**
@@ -84,7 +89,18 @@ public void execute() throws MojoExecutionException {
8489
this.resourcesDirectory = Paths.get(outputDirectory).resolve("generated-resources").resolve("sdk-resources");
8590
this.testsDirectory = Paths.get(outputDirectory).resolve("generated-test-sources").resolve("sdk-tests");
8691

87-
List<GenerationParams> generationParams = initGenerationParams();
92+
List<GenerationParams> generationParams;
93+
94+
try {
95+
generationParams = initGenerationParams();
96+
} catch (ModelInvalidException e) {
97+
if (writeValidationReport) {
98+
ModelValidationReport report = new ModelValidationReport();
99+
report.setValidationEntries(e.validationEntries());
100+
emitValidationReport(report);
101+
}
102+
throw e;
103+
}
88104

89105
Map<String, IntermediateModel> serviceNameToModelMap = new HashMap<>();
90106

@@ -137,6 +153,8 @@ private List<GenerationParams> initGenerationParams() throws MojoExecutionExcept
137153
}).collect(Collectors.toList());
138154
}
139155

156+
157+
140158
private Stream<ModelRoot> findModelRoots() throws MojoExecutionException {
141159
try {
142160
return Files.find(codeGenResources.toPath(), 10, this::isModelFile)
@@ -216,6 +234,17 @@ private <T> Optional<T> loadOptionalModel(Class<T> clzz, Path location) {
216234
return ModelLoaderUtils.loadOptionalModel(clzz, location.toFile());
217235
}
218236

237+
private void emitValidationReport(ModelValidationReport report) {
238+
Path modelsDir = sourcesDirectory.resolve("models");
239+
try {
240+
Writer writer = Files.newBufferedWriter(modelsDir.resolve("validation-report.json"),
241+
StandardCharsets.UTF_8);
242+
Jackson.writeWithObjectMapper(report, writer);
243+
} catch (IOException e) {
244+
getLog().warn("Failed to write validation report to " + modelsDir, e);
245+
}
246+
}
247+
219248
private static class ModelRoot {
220249
private final Path modelRoot;
221250
private final CustomizationConfig customizationConfig;

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

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import static software.amazon.awssdk.codegen.internal.Utils.isMapShape;
2222
import static software.amazon.awssdk.codegen.internal.Utils.isScalar;
2323

24+
import java.util.Collections;
2425
import java.util.List;
2526
import java.util.Map;
2627
import java.util.Optional;
@@ -37,9 +38,14 @@
3738
import software.amazon.awssdk.codegen.model.intermediate.VariableModel;
3839
import software.amazon.awssdk.codegen.model.service.Location;
3940
import software.amazon.awssdk.codegen.model.service.Member;
41+
import software.amazon.awssdk.codegen.model.service.Operation;
4042
import software.amazon.awssdk.codegen.model.service.ServiceModel;
4143
import software.amazon.awssdk.codegen.model.service.Shape;
4244
import software.amazon.awssdk.codegen.naming.NamingStrategy;
45+
import software.amazon.awssdk.codegen.validation.ModelInvalidException;
46+
import software.amazon.awssdk.codegen.validation.ValidationEntry;
47+
import software.amazon.awssdk.codegen.validation.ValidationErrorId;
48+
import software.amazon.awssdk.codegen.validation.ValidationErrorSeverity;
4349
import software.amazon.awssdk.utils.StringUtils;
4450
import software.amazon.awssdk.utils.Validate;
4551

@@ -345,11 +351,20 @@ private boolean isGreedy(Shape parentShape, Map<String, Shape> allC2jShapes, Par
345351
* @throws RuntimeException If operation can't be found.
346352
*/
347353
private String findRequestUri(Shape parentShape, Map<String, Shape> allC2jShapes) {
348-
return builder.getService().getOperations().values().stream()
349-
.filter(o -> o.getInput() != null)
350-
.filter(o -> allC2jShapes.get(o.getInput().getShape()).equals(parentShape))
351-
.map(o -> o.getHttp().getRequestUri())
352-
.findFirst().orElseThrow(() -> new RuntimeException("Could not find request URI for input shape"));
354+
Optional<Operation> operation = builder.getService().getOperations().values().stream()
355+
.filter(o -> o.getInput() != null)
356+
.filter(o -> allC2jShapes.get(o.getInput().getShape()).equals(parentShape))
357+
.findFirst();
358+
359+
return operation.map(o -> o.getHttp().getRequestUri())
360+
.orElseThrow(() -> {
361+
String detailMsg = "Could not find request URI for input shape";
362+
ValidationEntry entry =
363+
new ValidationEntry().withErrorId(ValidationErrorId.REQUEST_URI_NOT_FOUND)
364+
.withDetailMessage(detailMsg)
365+
.withSeverity(ValidationErrorSeverity.DANGER);
366+
return ModelInvalidException.builder().validationEntries(Collections.singletonList(entry)).build();
367+
});
353368
}
354369

355370
private String deriveUnmarshallerLocationName(Shape memberShape, String memberName, Member member) {

codegen/src/main/java/software/amazon/awssdk/codegen/validation/ValidationErrorId.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ public enum ValidationErrorId {
2121
+ " files generated by the code generator."
2222
),
2323
UNKNOWN_SHAPE_MEMBER("The model references an unknown shape member."),
24+
REQUEST_URI_NOT_FOUND("The request URI does not exist."),
2425
;
2526

2627
private final String description;

codegen/src/test/java/software/amazon/awssdk/codegen/CodeGeneratorTest.java

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,10 @@
4040
import org.junit.jupiter.api.BeforeEach;
4141
import org.junit.jupiter.api.Test;
4242
import software.amazon.awssdk.codegen.internal.Jackson;
43+
import software.amazon.awssdk.codegen.model.config.customization.CustomizationConfig;
4344
import software.amazon.awssdk.codegen.model.intermediate.IntermediateModel;
4445
import software.amazon.awssdk.codegen.model.rules.endpoints.EndpointTestSuiteModel;
46+
import software.amazon.awssdk.codegen.model.service.ServiceModel;
4547
import software.amazon.awssdk.codegen.poet.ClientTestModels;
4648
import software.amazon.awssdk.codegen.validation.ModelInvalidException;
4749
import software.amazon.awssdk.codegen.validation.ModelValidator;
@@ -155,6 +157,19 @@ void execute_endpointsTestReferencesUnknownOperationMember_throwsValidationError
155157
});
156158
}
157159

160+
@Test
161+
void execute_operationHasNoRequestUri_throwsValidationError() throws IOException {
162+
C2jModels models = C2jModels.builder()
163+
.customizationConfig(CustomizationConfig.create())
164+
.serviceModel(getMissingRequestUriServiceModel())
165+
.build();
166+
167+
assertThatThrownBy(() -> generateCodeFromC2jModels(models, outputDir, true, Collections.emptyList()))
168+
.isInstanceOf(ModelInvalidException.class)
169+
.matches(e -> ((ModelInvalidException) e).validationEntries().get(0).getErrorId()
170+
== ValidationErrorId.REQUEST_URI_NOT_FOUND);
171+
}
172+
158173
private void generateCodeFromC2jModels(C2jModels c2jModels, Path outputDir) {
159174
generateCodeFromC2jModels(c2jModels, outputDir, false, null);
160175
}
@@ -201,17 +216,28 @@ private static Path validationReportPath(Path root) {
201216
}
202217

203218
private EndpointTestSuiteModel getBrokenEndpointTestSuiteModel() throws IOException {
204-
InputStream resourceAsStream = getClass().getResourceAsStream("incorrect-endpoint-tests.json");
205-
ByteArrayOutputStream baos = new ByteArrayOutputStream();
206-
byte[] buffer = new byte[1024];
207-
int read;
208-
while ((read = resourceAsStream.read(buffer)) != -1) {
209-
baos.write(buffer, 0, read);
210-
}
211-
String json = StandardCharsets.UTF_8.decode(ByteBuffer.wrap(baos.toByteArray())).toString();
219+
String json = resourceAsString("incorrect-endpoint-tests.json");
212220
return Jackson.load(EndpointTestSuiteModel.class, json);
213221
}
214222

223+
private ServiceModel getMissingRequestUriServiceModel() throws IOException {
224+
String json = resourceAsString("no-request-uri-operation-service.json");
225+
return Jackson.load(ServiceModel.class, json);
226+
}
227+
228+
private String resourceAsString(String name) throws IOException {
229+
ByteArrayOutputStream baos;
230+
try (InputStream resourceAsStream = getClass().getResourceAsStream(name)) {
231+
baos = new ByteArrayOutputStream();
232+
byte[] buffer = new byte[1024];
233+
int read;
234+
while ((read = resourceAsStream.read(buffer)) != -1) {
235+
baos.write(buffer, 0, read);
236+
}
237+
}
238+
return StandardCharsets.UTF_8.decode(ByteBuffer.wrap(baos.toByteArray())).toString();
239+
}
240+
215241
private static void deleteDirectory(Path dir) throws IOException {
216242
Files.walkFileTree(dir, new FileVisitor<Path>() {
217243

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
{
2+
"version": "2.0",
3+
"metadata": {
4+
"apiVersion": "2010-05-08",
5+
"endpointPrefix": "json-service-endpoint",
6+
"globalEndpoint": "json-service.amazonaws.com",
7+
"protocol": "rest-json",
8+
"serviceAbbreviation": "Rest Json Service",
9+
"serviceFullName": "Some Service That Uses Rest-Json Protocol",
10+
"serviceId": "Rest Json Service",
11+
"signingName": "json-service",
12+
"signatureVersion": "v4",
13+
"uid": "json-service-2010-05-08",
14+
"xmlNamespace": "https://json-service.amazonaws.com/doc/2010-05-08/"
15+
},
16+
"operations": {
17+
"OperationWithUriMappedParam": {
18+
"name": "OperationWithUriMappedParam",
19+
"http": {
20+
"method": "GET"
21+
},
22+
"input": {
23+
"shape": "OperationWithUriMappedParamRequest"
24+
}
25+
}
26+
},
27+
"shapes": {
28+
"OperationWithUriMappedParamRequest": {
29+
"type": "structure",
30+
"members": {
31+
"StringMember": {
32+
"shape": "String",
33+
"location": "uri",
34+
"locationName": "stringMember"
35+
}
36+
}
37+
},
38+
"String": {
39+
"type": "string"
40+
}
41+
},
42+
"documentation": "A service that is implemented using the rest-json protocol"
43+
}

0 commit comments

Comments
 (0)