Skip to content

Commit be78c0c

Browse files
authored
Add support for validating models in generator (#6136)
* Add support for validating models in generator This commit introduces `ModelValidator` which allows us to do validation of the `IntermediateModel` before generation starts; violations are emitted to a `validation-report.json` file under `generated-sources/sdk/models`. As part of this change, `CodeGenerator` was updated to accept `IntermediateModel` as a parameter. This is because all validators operate on the `IntermediateModel`, which already have any SDK specific customizations applied. Since some validators require referencing multiple models, (such as when we start validating shared shapes), having the ability to pass in `IntermediateModel` directly means we only need to build these models once, upfront. * Changelog * Add javadocs
1 parent 1594ddd commit be78c0c

File tree

11 files changed

+610
-8
lines changed

11 files changed

+610
-8
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 support for defining service model validators and generating valdiation reports during code generation."
6+
}

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

Lines changed: 99 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,20 +19,33 @@
1919
import java.io.File;
2020
import java.io.IOException;
2121
import java.io.PrintWriter;
22+
import java.util.ArrayList;
23+
import java.util.Collections;
24+
import java.util.List;
2225
import java.util.concurrent.ForkJoinTask;
2326
import software.amazon.awssdk.codegen.emitters.GeneratorTask;
2427
import software.amazon.awssdk.codegen.emitters.GeneratorTaskParams;
2528
import software.amazon.awssdk.codegen.emitters.tasks.AwsGeneratorTasks;
2629
import software.amazon.awssdk.codegen.internal.Jackson;
2730
import software.amazon.awssdk.codegen.internal.Utils;
2831
import software.amazon.awssdk.codegen.model.intermediate.IntermediateModel;
32+
import software.amazon.awssdk.codegen.validation.ModelValidationContext;
33+
import software.amazon.awssdk.codegen.validation.ModelValidationReport;
34+
import software.amazon.awssdk.codegen.validation.ModelValidator;
35+
import software.amazon.awssdk.codegen.validation.ValidationEntry;
2936
import software.amazon.awssdk.utils.Logger;
3037

3138
public class CodeGenerator {
3239
private static final Logger log = Logger.loggerFor(CodeGenerator.class);
3340
private static final String MODEL_DIR_NAME = "models";
3441

35-
private final C2jModels models;
42+
// TODO: add validators
43+
private static final List<ModelValidator> DEFAULT_MODEL_VALIDATORS = Collections.emptyList();
44+
45+
private final C2jModels c2jModels;
46+
47+
private final IntermediateModel intermediateModel;
48+
private final IntermediateModel shareModelsTarget;
3649
private final String sourcesDirectory;
3750
private final String resourcesDirectory;
3851
private final String testsDirectory;
@@ -42,6 +55,9 @@ public class CodeGenerator {
4255
*/
4356
private final String fileNamePrefix;
4457

58+
private final List<ModelValidator> modelValidators;
59+
private final boolean emitValidationReport;
60+
4561
static {
4662
// Make sure ClassName is statically initialized before we do anything in parallel.
4763
// Parallel static initialization of ClassName and TypeName can result in a deadlock:
@@ -50,12 +66,21 @@ public class CodeGenerator {
5066
}
5167

5268
public CodeGenerator(Builder builder) {
53-
this.models = builder.models;
69+
this.c2jModels = builder.models;
70+
this.intermediateModel = builder.intermediateModel;
71+
72+
if (this.c2jModels != null && this.intermediateModel != null) {
73+
throw new IllegalArgumentException("Only one of c2jModels and intermediateModel must be specified");
74+
}
75+
76+
this.shareModelsTarget = builder.shareModelsTarget;
5477
this.sourcesDirectory = builder.sourcesDirectory;
5578
this.testsDirectory = builder.testsDirectory;
5679
this.resourcesDirectory = builder.resourcesDirectory != null ? builder.resourcesDirectory
5780
: builder.sourcesDirectory;
5881
this.fileNamePrefix = builder.fileNamePrefix;
82+
this.modelValidators = builder.modelValidators == null ? DEFAULT_MODEL_VALIDATORS : builder.modelValidators;
83+
this.emitValidationReport = builder.emitValidationReport;
5984
}
6085

6186
public static File getModelDirectory(String outputDirectory) {
@@ -76,13 +101,31 @@ public static Builder builder() {
76101
* code.
77102
*/
78103
public void execute() {
79-
try {
80-
IntermediateModel intermediateModel = new IntermediateModelBuilder(models).build();
104+
ModelValidationReport report = new ModelValidationReport();
105+
106+
IntermediateModel modelToGenerate;
107+
if (c2jModels != null) {
108+
modelToGenerate = new IntermediateModelBuilder(c2jModels).build();
109+
} else {
110+
modelToGenerate = intermediateModel;
111+
}
112+
113+
List<ValidationEntry> validatorEntries = runModelValidators(modelToGenerate);
114+
report.setValidationEntries(validatorEntries);
81115

116+
if (emitValidationReport) {
117+
writeValidationReport(report);
118+
}
119+
120+
if (!validatorEntries.isEmpty()) {
121+
throw new RuntimeException("Validation failed. See validation report for details.");
122+
}
123+
124+
try {
82125
if (fileNamePrefix != null) {
83-
writeIntermediateModel(intermediateModel);
126+
writeIntermediateModel(modelToGenerate);
84127
}
85-
emitCode(intermediateModel);
128+
emitCode(modelToGenerate);
86129

87130
} catch (Exception e) {
88131
log.error(() -> "Failed to generate code. ", e);
@@ -91,7 +134,32 @@ public void execute() {
91134
}
92135
}
93136

137+
private List<ValidationEntry> runModelValidators(IntermediateModel intermediateModel) {
138+
ModelValidationContext ctx = ModelValidationContext.builder()
139+
.intermediateModel(intermediateModel)
140+
.shareModelsTarget(shareModelsTarget)
141+
.build();
142+
143+
List<ValidationEntry> validationEntries = new ArrayList<>();
144+
145+
modelValidators.forEach(v -> validationEntries.addAll(v.validateModels(ctx)));
146+
147+
return validationEntries;
148+
}
149+
150+
private void writeValidationReport(ModelValidationReport report) {
151+
try {
152+
writeModel(report, "validation-report.json");
153+
} catch (IOException e) {
154+
throw new RuntimeException(e);
155+
}
156+
}
157+
94158
private void writeIntermediateModel(IntermediateModel model) throws IOException {
159+
writeModel(model, fileNamePrefix + "-intermediate.json");
160+
}
161+
162+
private void writeModel(Object model, String name) throws IOException {
95163
File modelDir = getModelDirectory(sourcesDirectory);
96164
PrintWriter writer = null;
97165
try {
@@ -100,7 +168,7 @@ private void writeIntermediateModel(IntermediateModel model) throws IOException
100168
throw new RuntimeException("Failed to create " + outDir.getAbsolutePath());
101169
}
102170

103-
File outputFile = new File(modelDir, fileNamePrefix + "-intermediate.json");
171+
File outputFile = new File(modelDir, name);
104172

105173
if (!outputFile.exists() && !outputFile.createNewFile()) {
106174
throw new RuntimeException("Error creating file " + outputFile.getAbsolutePath());
@@ -134,10 +202,14 @@ private GeneratorTask createGeneratorTasks(IntermediateModel intermediateModel)
134202
public static final class Builder {
135203

136204
private C2jModels models;
205+
private IntermediateModel intermediateModel;
206+
private IntermediateModel shareModelsTarget;
137207
private String sourcesDirectory;
138208
private String resourcesDirectory;
139209
private String testsDirectory;
140210
private String fileNamePrefix;
211+
private List<ModelValidator> modelValidators;
212+
private boolean emitValidationReport;
141213

142214
private Builder() {
143215
}
@@ -147,6 +219,16 @@ public Builder models(C2jModels models) {
147219
return this;
148220
}
149221

222+
public Builder intermediateModel(IntermediateModel intermediateModel) {
223+
this.intermediateModel = intermediateModel;
224+
return this;
225+
}
226+
227+
public Builder shareModelsTarget(IntermediateModel shareModelsTarget) {
228+
this.shareModelsTarget = shareModelsTarget;
229+
return this;
230+
}
231+
150232
public Builder sourcesDirectory(String sourcesDirectory) {
151233
this.sourcesDirectory = sourcesDirectory;
152234
return this;
@@ -167,6 +249,16 @@ public Builder intermediateModelFileNamePrefix(String fileNamePrefix) {
167249
return this;
168250
}
169251

252+
public Builder modelValidators(List<ModelValidator> modelValidators) {
253+
this.modelValidators = modelValidators;
254+
return this;
255+
}
256+
257+
public Builder emitValidationReport(boolean emitValidationReport) {
258+
this.emitValidationReport = emitValidationReport;
259+
return this;
260+
}
261+
170262
/**
171263
* @return An immutable {@link CodeGenerator} object.
172264
*/

codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/EndpointRulesSpecUtils.java

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,12 @@
2929
import com.squareup.javapoet.TypeName;
3030
import java.io.IOException;
3131
import java.io.UncheckedIOException;
32+
import java.net.URI;
33+
import java.net.URISyntaxException;
3234
import java.net.URL;
35+
import java.nio.file.Files;
36+
import java.nio.file.Path;
37+
import java.nio.file.Paths;
3338
import java.util.Arrays;
3439
import java.util.Iterator;
3540
import java.util.List;
@@ -52,6 +57,7 @@
5257
import software.amazon.awssdk.utils.internal.CodegenNamingUtils;
5358

5459
public class EndpointRulesSpecUtils {
60+
private static final String RULES_ENGINE_RESOURCE_FILES_PREFIX = "software/amazon/awssdk/codegen/rules/";
5561
private final IntermediateModel intermediateModel;
5662

5763
public EndpointRulesSpecUtils(IntermediateModel intermediateModel) {
@@ -213,16 +219,45 @@ public TypeName resolverReturnType() {
213219

214220
public List<String> rulesEngineResourceFiles() {
215221
URL currentJarUrl = EndpointRulesSpecUtils.class.getProtectionDomain().getCodeSource().getLocation();
222+
223+
// This would happen if the classes aren't loaded from a JAR, e.g. when unit testing
224+
if (!currentJarUrl.toString().endsWith(".jar")) {
225+
return rulesEngineFilesFromDirectory(currentJarUrl);
226+
}
227+
216228
try (JarFile jarFile = new JarFile(currentJarUrl.getFile())) {
217229
return jarFile.stream()
218230
.map(ZipEntry::getName)
219-
.filter(e -> e.startsWith("software/amazon/awssdk/codegen/rules/"))
231+
.filter(e -> e.startsWith(RULES_ENGINE_RESOURCE_FILES_PREFIX))
220232
.collect(Collectors.toList());
221233
} catch (IOException e) {
222234
throw new UncheckedIOException(e);
223235
}
224236
}
225237

238+
public List<String> rulesEngineFilesFromDirectory(URL location) {
239+
URI locationUri;
240+
try {
241+
locationUri = location.toURI();
242+
if (!"file".equals(locationUri.getScheme())) {
243+
throw new RuntimeException("Expected location to be a directory");
244+
}
245+
} catch (URISyntaxException e) {
246+
throw new RuntimeException(e);
247+
}
248+
249+
try {
250+
Path directory = Paths.get(locationUri);
251+
return Files.walk(directory)
252+
// Remove the root directory if the classes, paths are expected to be relative to this directory
253+
.map(f -> directory.relativize(f).toString())
254+
.filter(f -> f.startsWith(RULES_ENGINE_RESOURCE_FILES_PREFIX))
255+
.collect(Collectors.toList());
256+
} catch (IOException e) {
257+
throw new UncheckedIOException(e);
258+
}
259+
}
260+
226261
public List<String> rulesEngineResourceFiles2() {
227262
URL currentJarUrl = EndpointRulesSpecUtils.class.getProtectionDomain().getCodeSource().getLocation();
228263
try (JarFile jarFile = new JarFile(currentJarUrl.getFile())) {
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
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.validation;
17+
18+
import java.util.Optional;
19+
import software.amazon.awssdk.codegen.model.config.customization.ShareModelConfig;
20+
import software.amazon.awssdk.codegen.model.intermediate.IntermediateModel;
21+
22+
/**
23+
* Context object for {@link ModelValidator}s. This object contains all the information available to the validations in order
24+
* for them to perform their tasks.
25+
*/
26+
public final class ModelValidationContext {
27+
private final IntermediateModel intermediateModel;
28+
private final IntermediateModel shareModelsTarget;
29+
30+
private ModelValidationContext(Builder builder) {
31+
this.intermediateModel = builder.intermediateModel;
32+
this.shareModelsTarget = builder.shareModelsTarget;
33+
}
34+
35+
/**
36+
* The service model for which code is being generated.
37+
*/
38+
public IntermediateModel intermediateModel() {
39+
return intermediateModel;
40+
}
41+
42+
/**
43+
* The model of the service that the currently generating service shares models with. In other words, this is the service
44+
* model for the service defined in {@link ShareModelConfig#getShareModelWith()}.
45+
*/
46+
public Optional<IntermediateModel> shareModelsTarget() {
47+
return Optional.ofNullable(shareModelsTarget);
48+
}
49+
50+
public static Builder builder() {
51+
return new Builder();
52+
}
53+
54+
public static class Builder {
55+
private IntermediateModel intermediateModel;
56+
private IntermediateModel shareModelsTarget;
57+
58+
/**
59+
* The service model for which code is being generated.
60+
*/
61+
public Builder intermediateModel(IntermediateModel intermediateModel) {
62+
this.intermediateModel = intermediateModel;
63+
return this;
64+
}
65+
66+
/**
67+
* The model of the service that the currently generating service shares models with. In other words, this is the service
68+
* model for the service defined in {@link ShareModelConfig#getShareModelWith()}.
69+
*/
70+
public Builder shareModelsTarget(IntermediateModel shareModelsTarget) {
71+
this.shareModelsTarget = shareModelsTarget;
72+
return this;
73+
}
74+
75+
public ModelValidationContext build() {
76+
return new ModelValidationContext(this);
77+
}
78+
}
79+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
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.validation;
17+
18+
import java.util.Collections;
19+
import java.util.List;
20+
21+
public class ModelValidationReport {
22+
private List<ValidationEntry> validationEntries = Collections.emptyList();
23+
24+
public List<ValidationEntry> getValidationEntries() {
25+
return validationEntries;
26+
}
27+
28+
public void setValidationEntries(List<ValidationEntry> validationEntries) {
29+
if (validationEntries != null) {
30+
this.validationEntries = validationEntries;
31+
} else {
32+
this.validationEntries = Collections.emptyList();
33+
}
34+
}
35+
36+
public ModelValidationReport withValidationEntries(List<ValidationEntry> validationEntries) {
37+
setValidationEntries(validationEntries);
38+
return this;
39+
}
40+
}

0 commit comments

Comments
 (0)