diff --git a/.changes/next-release/feature-AWSSDKforJavav2-7cf1e5c.json b/.changes/next-release/feature-AWSSDKforJavav2-7cf1e5c.json new file mode 100644 index 000000000000..3fc51c387d75 --- /dev/null +++ b/.changes/next-release/feature-AWSSDKforJavav2-7cf1e5c.json @@ -0,0 +1,6 @@ +{ + "type": "feature", + "category": "AWS SDK for Java v2", + "contributor": "", + "description": "Add code generation validation for missing request URI on an operation." +} diff --git a/.changes/next-release/feature-AWSSDKforJavav2-bd762da.json b/.changes/next-release/feature-AWSSDKforJavav2-bd762da.json new file mode 100644 index 000000000000..d4fc9915dd8d --- /dev/null +++ b/.changes/next-release/feature-AWSSDKforJavav2-bd762da.json @@ -0,0 +1,6 @@ +{ + "type": "feature", + "category": "AWS SDK for Java v2", + "contributor": "", + "description": "Add support for defining service model validators and generating valdiation reports during code generation." +} diff --git a/.changes/next-release/feature-AWSSDKforJavav2-f004fae.json b/.changes/next-release/feature-AWSSDKforJavav2-f004fae.json new file mode 100644 index 000000000000..184769e3f85c --- /dev/null +++ b/.changes/next-release/feature-AWSSDKforJavav2-f004fae.json @@ -0,0 +1,6 @@ +{ + "type": "feature", + "category": "AWS SDK for Java v2", + "contributor": "", + "description": "Add support for validating that shared models between two services are identical." +} diff --git a/.changes/next-release/feature-CodeGeneratorMavenPlugin-64c91e8.json b/.changes/next-release/feature-CodeGeneratorMavenPlugin-64c91e8.json new file mode 100644 index 000000000000..91090c74555f --- /dev/null +++ b/.changes/next-release/feature-CodeGeneratorMavenPlugin-64c91e8.json @@ -0,0 +1,6 @@ +{ + "type": "feature", + "category": "Code Generator Maven Plugin", + "contributor": "", + "description": "Update the generator plugin to support model validation during code generation. In addition, this adds the `writeValidationReport` flag to support writing the validation report to disk." +} diff --git a/codegen-maven-plugin/pom.xml b/codegen-maven-plugin/pom.xml index 4fdf47dfebc2..825cc5f465ab 100644 --- a/codegen-maven-plugin/pom.xml +++ b/codegen-maven-plugin/pom.xml @@ -57,6 +57,11 @@ software.amazon.awssdk ${awsjavasdk.version} + + utils + software.amazon.awssdk + ${awsjavasdk.version} + org.junit.jupiter junit-jupiter diff --git a/codegen-maven-plugin/src/main/java/software/amazon/awssdk/codegen/maven/plugin/GenerationMojo.java b/codegen-maven-plugin/src/main/java/software/amazon/awssdk/codegen/maven/plugin/GenerationMojo.java index 4ce4e7be116b..3d17b4d84bb2 100644 --- a/codegen-maven-plugin/src/main/java/software/amazon/awssdk/codegen/maven/plugin/GenerationMojo.java +++ b/codegen-maven-plugin/src/main/java/software/amazon/awssdk/codegen/maven/plugin/GenerationMojo.java @@ -17,11 +17,17 @@ import java.io.File; import java.io.IOException; +import java.io.Writer; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.attribute.BasicFileAttributes; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import java.util.Optional; +import java.util.stream.Collectors; import java.util.stream.Stream; import org.apache.maven.plugin.AbstractMojo; import org.apache.maven.plugin.MojoExecutionException; @@ -30,21 +36,26 @@ import org.apache.maven.project.MavenProject; import software.amazon.awssdk.codegen.C2jModels; import software.amazon.awssdk.codegen.CodeGenerator; +import software.amazon.awssdk.codegen.IntermediateModelBuilder; +import software.amazon.awssdk.codegen.internal.Jackson; import software.amazon.awssdk.codegen.internal.Utils; import software.amazon.awssdk.codegen.model.config.customization.CustomizationConfig; +import software.amazon.awssdk.codegen.model.intermediate.IntermediateModel; import software.amazon.awssdk.codegen.model.rules.endpoints.EndpointTestSuiteModel; import software.amazon.awssdk.codegen.model.service.EndpointRuleSetModel; import software.amazon.awssdk.codegen.model.service.Paginators; import software.amazon.awssdk.codegen.model.service.ServiceModel; import software.amazon.awssdk.codegen.model.service.Waiters; import software.amazon.awssdk.codegen.utils.ModelLoaderUtils; +import software.amazon.awssdk.codegen.validation.ModelInvalidException; +import software.amazon.awssdk.codegen.validation.ModelValidationReport; +import software.amazon.awssdk.utils.StringUtils; /** * The Maven mojo to generate Java client code using software.amazon.awssdk:codegen module. */ @Mojo(name = "generate") public class GenerationMojo extends AbstractMojo { - private static final String MODEL_FILE = "service-2.json"; private static final String CUSTOMIZATION_CONFIG_FILE = "customization.config"; private static final String WAITERS_FILE = "waiters-2.json"; @@ -62,6 +73,8 @@ public class GenerationMojo extends AbstractMojo { @Parameter(property = "writeIntermediateModel", defaultValue = "false") private boolean writeIntermediateModel; + @Parameter(property = "writeValidationReport", defaultValue = "false") + private boolean writeValidationReport; @Parameter(defaultValue = "${project}", readonly = true) private MavenProject project; @@ -76,22 +89,72 @@ public void execute() throws MojoExecutionException { this.resourcesDirectory = Paths.get(outputDirectory).resolve("generated-resources").resolve("sdk-resources"); this.testsDirectory = Paths.get(outputDirectory).resolve("generated-test-sources").resolve("sdk-tests"); - findModelRoots().forEach(p -> { - Path modelRootPath = p.modelRoot; - getLog().info("Loading from: " + modelRootPath.toString()); - generateCode(C2jModels.builder() - .customizationConfig(p.customizationConfig) - .serviceModel(loadServiceModel(modelRootPath)) - .waitersModel(loadWaiterModel(modelRootPath)) - .paginatorsModel(loadPaginatorModel(modelRootPath)) - .endpointRuleSetModel(loadEndpointRuleSetModel(modelRootPath)) - .endpointTestSuiteModel(loadEndpointTestSuiteModel(modelRootPath)) - .build()); + List generationParams; + + try { + generationParams = initGenerationParams(); + } catch (ModelInvalidException e) { + if (writeValidationReport) { + ModelValidationReport report = new ModelValidationReport(); + report.setValidationEntries(e.validationEntries()); + emitValidationReport(report); + } + throw e; + } + + Map serviceNameToModelMap = new HashMap<>(); + + generationParams.forEach( + params -> { + IntermediateModel model = params.intermediateModel; + String lowercaseServiceName = StringUtils.lowerCase(model.getMetadata().getServiceName()); + IntermediateModel previous = serviceNameToModelMap.put(lowercaseServiceName, model); + if (previous != null) { + String warning = String.format("Multiple service models found with service name %s. Model validation " + + "will likely be incorrect", lowercaseServiceName); + getLog().warn(warning); + } + }); + + // Update each param with the intermediate model it shares models with, if any + generationParams.forEach(params -> { + CustomizationConfig customizationConfig = params.intermediateModel.getCustomizationConfig(); + + if (customizationConfig.getShareModelConfig() != null) { + String shareModelWithName = customizationConfig.getShareModelConfig().getShareModelWith(); + params.withShareModelsTarget(serviceNameToModelMap.get(shareModelWithName)); + } }); + + generationParams.forEach(this::generateCode); + project.addCompileSourceRoot(sourcesDirectory.toFile().getAbsolutePath()); project.addTestCompileSourceRoot(testsDirectory.toFile().getAbsolutePath()); } + private List initGenerationParams() throws MojoExecutionException { + List modelRoots = findModelRoots().collect(Collectors.toList()); + + return modelRoots.stream().map(r -> { + Path modelRootPath = r.modelRoot; + getLog().info("Loading from: " + modelRootPath.toString()); + C2jModels c2jModels = C2jModels.builder() + .customizationConfig(r.customizationConfig) + .serviceModel(loadServiceModel(modelRootPath)) + .waitersModel(loadWaiterModel(modelRootPath)) + .paginatorsModel(loadPaginatorModel(modelRootPath)) + .endpointRuleSetModel(loadEndpointRuleSetModel(modelRootPath)) + .endpointTestSuiteModel(loadEndpointTestSuiteModel(modelRootPath)) + .build(); + String intermediateModelFileNamePrefix = intermediateModelFileNamePrefix(c2jModels); + IntermediateModel intermediateModel = new IntermediateModelBuilder(c2jModels).build(); + return new GenerationParams().withIntermediateModel(intermediateModel) + .withIntermediateModelFileNamePrefix(intermediateModelFileNamePrefix); + }).collect(Collectors.toList()); + } + + + private Stream findModelRoots() throws MojoExecutionException { try { return Files.find(codeGenResources.toPath(), 10, this::isModelFile) @@ -111,13 +174,15 @@ private boolean isModelFile(Path p, BasicFileAttributes a) { return p.toString().endsWith(MODEL_FILE); } - private void generateCode(C2jModels models) { + private void generateCode(GenerationParams params) { CodeGenerator.builder() - .models(models) + .intermediateModel(params.intermediateModel) + .shareModelsTarget(params.shareModelsTarget) .sourcesDirectory(sourcesDirectory.toFile().getAbsolutePath()) .resourcesDirectory(resourcesDirectory.toFile().getAbsolutePath()) .testsDirectory(testsDirectory.toFile().getAbsolutePath()) - .intermediateModelFileNamePrefix(intermediateModelFileNamePrefix(models)) + .intermediateModelFileNamePrefix(params.intermediateModelFileNamePrefix) + .emitValidationReport(writeValidationReport) .build() .execute(); } @@ -169,6 +234,17 @@ private Optional loadOptionalModel(Class clzz, Path location) { return ModelLoaderUtils.loadOptionalModel(clzz, location.toFile()); } + private void emitValidationReport(ModelValidationReport report) { + Path modelsDir = sourcesDirectory.resolve("models"); + try (Writer writer = Files.newBufferedWriter(modelsDir.resolve("validation-report.json"), + StandardCharsets.UTF_8);) { + + Jackson.writeWithObjectMapper(report, writer); + } catch (IOException e) { + getLog().warn("Failed to write validation report to " + modelsDir, e); + } + } + private static class ModelRoot { private final Path modelRoot; private final CustomizationConfig customizationConfig; @@ -178,4 +254,25 @@ private ModelRoot(Path modelRoot, CustomizationConfig customizationConfig) { this.customizationConfig = customizationConfig; } } + + private static class GenerationParams { + private IntermediateModel intermediateModel; + private IntermediateModel shareModelsTarget; + private String intermediateModelFileNamePrefix; + + public GenerationParams withIntermediateModel(IntermediateModel intermediateModel) { + this.intermediateModel = intermediateModel; + return this; + } + + public GenerationParams withShareModelsTarget(IntermediateModel shareModelsTarget) { + this.shareModelsTarget = shareModelsTarget; + return this; + } + + public GenerationParams withIntermediateModelFileNamePrefix(String intermediateModelFileNamePrefix) { + this.intermediateModelFileNamePrefix = intermediateModelFileNamePrefix; + return this; + } + } } diff --git a/codegen/pom.xml b/codegen/pom.xml index a84277054daa..035db17318bc 100644 --- a/codegen/pom.xml +++ b/codegen/pom.xml @@ -239,5 +239,10 @@ mockito-core compile + + nl.jqno.equalsverifier + equalsverifier + test + diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/AddShapes.java b/codegen/src/main/java/software/amazon/awssdk/codegen/AddShapes.java index 6f9bec29617f..46b15ae7dbbd 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/AddShapes.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/AddShapes.java @@ -21,6 +21,7 @@ import static software.amazon.awssdk.codegen.internal.Utils.isMapShape; import static software.amazon.awssdk.codegen.internal.Utils.isScalar; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Optional; @@ -37,10 +38,15 @@ import software.amazon.awssdk.codegen.model.intermediate.VariableModel; import software.amazon.awssdk.codegen.model.service.Location; import software.amazon.awssdk.codegen.model.service.Member; +import software.amazon.awssdk.codegen.model.service.Operation; import software.amazon.awssdk.codegen.model.service.ServiceModel; import software.amazon.awssdk.codegen.model.service.Shape; import software.amazon.awssdk.codegen.naming.NamingStrategy; import software.amazon.awssdk.codegen.utils.ProtocolUtils; +import software.amazon.awssdk.codegen.validation.ModelInvalidException; +import software.amazon.awssdk.codegen.validation.ValidationEntry; +import software.amazon.awssdk.codegen.validation.ValidationErrorId; +import software.amazon.awssdk.codegen.validation.ValidationErrorSeverity; import software.amazon.awssdk.utils.StringUtils; import software.amazon.awssdk.utils.Validate; @@ -346,11 +352,20 @@ private boolean isGreedy(Shape parentShape, Map allC2jShapes, Par * @throws RuntimeException If operation can't be found. */ private String findRequestUri(Shape parentShape, Map allC2jShapes) { - return builder.getService().getOperations().values().stream() - .filter(o -> o.getInput() != null) - .filter(o -> allC2jShapes.get(o.getInput().getShape()).equals(parentShape)) - .map(o -> o.getHttp().getRequestUri()) - .findFirst().orElseThrow(() -> new RuntimeException("Could not find request URI for input shape")); + Optional 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"; + ValidationEntry entry = + new ValidationEntry().withErrorId(ValidationErrorId.REQUEST_URI_NOT_FOUND) + .withDetailMessage(detailMsg) + .withSeverity(ValidationErrorSeverity.DANGER); + return ModelInvalidException.builder().validationEntries(Collections.singletonList(entry)).build(); + }); } private String deriveUnmarshallerLocationName(Shape memberShape, String memberName, Member member) { diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/CodeGenerator.java b/codegen/src/main/java/software/amazon/awssdk/codegen/CodeGenerator.java index 4c097fadb4d2..0361fc6d0fdd 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/CodeGenerator.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/CodeGenerator.java @@ -19,6 +19,9 @@ import java.io.File; import java.io.IOException; import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import java.util.concurrent.ForkJoinTask; import software.amazon.awssdk.codegen.emitters.GeneratorTask; import software.amazon.awssdk.codegen.emitters.GeneratorTaskParams; @@ -26,13 +29,26 @@ import software.amazon.awssdk.codegen.internal.Jackson; import software.amazon.awssdk.codegen.internal.Utils; import software.amazon.awssdk.codegen.model.intermediate.IntermediateModel; +import software.amazon.awssdk.codegen.validation.ModelInvalidException; +import software.amazon.awssdk.codegen.validation.ModelValidationContext; +import software.amazon.awssdk.codegen.validation.ModelValidationReport; +import software.amazon.awssdk.codegen.validation.ModelValidator; +import software.amazon.awssdk.codegen.validation.SharedModelsValidator; +import software.amazon.awssdk.codegen.validation.ValidationEntry; import software.amazon.awssdk.utils.Logger; public class CodeGenerator { private static final Logger log = Logger.loggerFor(CodeGenerator.class); private static final String MODEL_DIR_NAME = "models"; - private final C2jModels models; + private static final List DEFAULT_MODEL_VALIDATORS = Collections.singletonList( + new SharedModelsValidator() + ); + + private final C2jModels c2jModels; + + private final IntermediateModel intermediateModel; + private final IntermediateModel shareModelsTarget; private final String sourcesDirectory; private final String resourcesDirectory; private final String testsDirectory; @@ -42,6 +58,9 @@ public class CodeGenerator { */ private final String fileNamePrefix; + private final List modelValidators; + private final boolean emitValidationReport; + static { // Make sure ClassName is statically initialized before we do anything in parallel. // Parallel static initialization of ClassName and TypeName can result in a deadlock: @@ -50,12 +69,21 @@ public class CodeGenerator { } public CodeGenerator(Builder builder) { - this.models = builder.models; + this.c2jModels = builder.models; + this.intermediateModel = builder.intermediateModel; + + if (this.c2jModels != null && this.intermediateModel != null) { + throw new IllegalArgumentException("Only one of c2jModels and intermediateModel must be specified"); + } + + this.shareModelsTarget = builder.shareModelsTarget; this.sourcesDirectory = builder.sourcesDirectory; this.testsDirectory = builder.testsDirectory; this.resourcesDirectory = builder.resourcesDirectory != null ? builder.resourcesDirectory : builder.sourcesDirectory; this.fileNamePrefix = builder.fileNamePrefix; + this.modelValidators = builder.modelValidators == null ? DEFAULT_MODEL_VALIDATORS : builder.modelValidators; + this.emitValidationReport = builder.emitValidationReport; } public static File getModelDirectory(String outputDirectory) { @@ -76,22 +104,72 @@ public static Builder builder() { * code. */ public void execute() { - try { - IntermediateModel intermediateModel = new IntermediateModelBuilder(models).build(); + ModelValidationReport report = new ModelValidationReport(); + + IntermediateModel modelToGenerate; + if (c2jModels != null) { + modelToGenerate = new IntermediateModelBuilder(c2jModels).build(); + } else { + modelToGenerate = intermediateModel; + } + List validatorEntries = runModelValidators(modelToGenerate); + report.setValidationEntries(validatorEntries); + + if (emitValidationReport) { + writeValidationReport(report); + } + + if (!validatorEntries.isEmpty()) { + throw new RuntimeException("Validation failed. See validation report for details."); + } + + try { if (fileNamePrefix != null) { - writeIntermediateModel(intermediateModel); + writeIntermediateModel(modelToGenerate); } - emitCode(intermediateModel); + emitCode(modelToGenerate); } catch (Exception e) { log.error(() -> "Failed to generate code. ", e); + + if (e instanceof ModelInvalidException && emitValidationReport) { + ModelInvalidException invalidException = (ModelInvalidException) e; + report.setValidationEntries(invalidException.validationEntries()); + writeValidationReport(report); + } + throw new RuntimeException( "Failed to generate code. Exception message : " + e.getMessage(), e); } } + private List runModelValidators(IntermediateModel intermediateModel) { + ModelValidationContext ctx = ModelValidationContext.builder() + .intermediateModel(intermediateModel) + .shareModelsTarget(shareModelsTarget) + .build(); + + List validationEntries = new ArrayList<>(); + + modelValidators.forEach(v -> validationEntries.addAll(v.validateModels(ctx))); + + return validationEntries; + } + + private void writeValidationReport(ModelValidationReport report) { + try { + writeModel(report, "validation-report.json"); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + private void writeIntermediateModel(IntermediateModel model) throws IOException { + writeModel(model, fileNamePrefix + "-intermediate.json"); + } + + private void writeModel(Object model, String name) throws IOException { File modelDir = getModelDirectory(sourcesDirectory); PrintWriter writer = null; try { @@ -100,7 +178,7 @@ private void writeIntermediateModel(IntermediateModel model) throws IOException throw new RuntimeException("Failed to create " + outDir.getAbsolutePath()); } - File outputFile = new File(modelDir, fileNamePrefix + "-intermediate.json"); + File outputFile = new File(modelDir, name); if (!outputFile.exists() && !outputFile.createNewFile()) { throw new RuntimeException("Error creating file " + outputFile.getAbsolutePath()); @@ -134,10 +212,14 @@ private GeneratorTask createGeneratorTasks(IntermediateModel intermediateModel) public static final class Builder { private C2jModels models; + private IntermediateModel intermediateModel; + private IntermediateModel shareModelsTarget; private String sourcesDirectory; private String resourcesDirectory; private String testsDirectory; private String fileNamePrefix; + private List modelValidators; + private boolean emitValidationReport; private Builder() { } @@ -147,6 +229,16 @@ public Builder models(C2jModels models) { return this; } + public Builder intermediateModel(IntermediateModel intermediateModel) { + this.intermediateModel = intermediateModel; + return this; + } + + public Builder shareModelsTarget(IntermediateModel shareModelsTarget) { + this.shareModelsTarget = shareModelsTarget; + return this; + } + public Builder sourcesDirectory(String sourcesDirectory) { this.sourcesDirectory = sourcesDirectory; return this; @@ -167,6 +259,16 @@ public Builder intermediateModelFileNamePrefix(String fileNamePrefix) { return this; } + public Builder modelValidators(List modelValidators) { + this.modelValidators = modelValidators; + return this; + } + + public Builder emitValidationReport(boolean emitValidationReport) { + this.emitValidationReport = emitValidationReport; + return this; + } + /** * @return An immutable {@link CodeGenerator} object. */ diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/emitters/tasks/BaseGeneratorTasks.java b/codegen/src/main/java/software/amazon/awssdk/codegen/emitters/tasks/BaseGeneratorTasks.java index 731f70e0cba3..cdabdbf219cd 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/emitters/tasks/BaseGeneratorTasks.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/emitters/tasks/BaseGeneratorTasks.java @@ -71,6 +71,8 @@ protected void compute() { ForkJoinTask.invokeAll(createTasks()); log.info(" Completed " + taskName + "."); } + } catch (RuntimeException e) { + throw e; } catch (Exception e) { throw new RuntimeException(e); } diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/model/intermediate/ArgumentModel.java b/codegen/src/main/java/software/amazon/awssdk/codegen/model/intermediate/ArgumentModel.java index 5013db7d3f9e..16e848303a4f 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/model/intermediate/ArgumentModel.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/model/intermediate/ArgumentModel.java @@ -15,6 +15,8 @@ package software.amazon.awssdk.codegen.model.intermediate; +import java.util.Objects; + public class ArgumentModel extends DocumentationModel { private String name; @@ -61,4 +63,28 @@ public ArgumentModel withIsEnumArg(boolean isEnumArg) { this.isEnumArg = isEnumArg; return this; } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) { + return false; + } + if (!super.equals(o)) { + return false; + } + + ArgumentModel that = (ArgumentModel) o; + return isEnumArg == that.isEnumArg + && Objects.equals(name, that.name) + && Objects.equals(type, that.type); + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + Objects.hashCode(name); + result = 31 * result + Objects.hashCode(type); + result = 31 * result + Boolean.hashCode(isEnumArg); + return result; + } } diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/model/intermediate/AuthorizerModel.java b/codegen/src/main/java/software/amazon/awssdk/codegen/model/intermediate/AuthorizerModel.java index ce98c0dfea8e..316f4e741139 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/model/intermediate/AuthorizerModel.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/model/intermediate/AuthorizerModel.java @@ -16,6 +16,7 @@ package software.amazon.awssdk.codegen.model.intermediate; import com.fasterxml.jackson.annotation.JsonIgnore; +import java.util.Objects; import software.amazon.awssdk.codegen.model.service.Location; public class AuthorizerModel extends DocumentationModel { @@ -63,4 +64,30 @@ public String getAddAuthTokenMethod() { authTokenLocation)); } } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) { + return false; + } + if (!super.equals(o)) { + return false; + } + + AuthorizerModel that = (AuthorizerModel) o; + return Objects.equals(name, that.name) + && Objects.equals(interfaceName, that.interfaceName) + && authTokenLocation == that.authTokenLocation + && Objects.equals(tokenName, that.tokenName); + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + Objects.hashCode(name); + result = 31 * result + Objects.hashCode(interfaceName); + result = 31 * result + Objects.hashCode(authTokenLocation); + result = 31 * result + Objects.hashCode(tokenName); + return result; + } } diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/model/intermediate/DocumentationModel.java b/codegen/src/main/java/software/amazon/awssdk/codegen/model/intermediate/DocumentationModel.java index 5be891040acc..55fd39f4a7c7 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/model/intermediate/DocumentationModel.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/model/intermediate/DocumentationModel.java @@ -17,6 +17,8 @@ import static software.amazon.awssdk.codegen.internal.DocumentationUtils.escapeIllegalCharacters; +import java.util.Objects; + public class DocumentationModel { protected String documentation; @@ -28,4 +30,22 @@ public String getDocumentation() { public void setDocumentation(String documentation) { this.documentation = escapeIllegalCharacters(documentation); } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + DocumentationModel that = (DocumentationModel) o; + return Objects.equals(documentation, that.documentation); + } + + @Override + public int hashCode() { + return Objects.hashCode(documentation); + } } diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/model/intermediate/EndpointDiscovery.java b/codegen/src/main/java/software/amazon/awssdk/codegen/model/intermediate/EndpointDiscovery.java index 91a5f3b60f25..e372079fc541 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/model/intermediate/EndpointDiscovery.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/model/intermediate/EndpointDiscovery.java @@ -26,4 +26,22 @@ public boolean isRequired() { public void setRequired(boolean required) { this.required = required; } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + EndpointDiscovery that = (EndpointDiscovery) o; + return required == that.required; + } + + @Override + public int hashCode() { + return Boolean.hashCode(required); + } } diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/model/intermediate/EnumModel.java b/codegen/src/main/java/software/amazon/awssdk/codegen/model/intermediate/EnumModel.java index f469b5de99fd..652f2c2aca6e 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/model/intermediate/EnumModel.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/model/intermediate/EnumModel.java @@ -15,6 +15,8 @@ package software.amazon.awssdk.codegen.model.intermediate; +import java.util.Objects; + /** * Represents a single enum field in a enum. */ @@ -49,4 +51,23 @@ public String getValue() { return value; } + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + EnumModel enumModel = (EnumModel) o; + return Objects.equals(value, enumModel.value) && Objects.equals(name, enumModel.name); + } + + @Override + public int hashCode() { + int result = Objects.hashCode(value); + result = 31 * result + Objects.hashCode(name); + return result; + } } diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/model/intermediate/MemberModel.java b/codegen/src/main/java/software/amazon/awssdk/codegen/model/intermediate/MemberModel.java index fddf93d4d72d..3e905aa1ed56 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/model/intermediate/MemberModel.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/model/intermediate/MemberModel.java @@ -28,6 +28,7 @@ import com.squareup.javapoet.ClassName; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import software.amazon.awssdk.codegen.internal.TypeUtils; import software.amazon.awssdk.codegen.model.service.ContextParam; @@ -785,4 +786,98 @@ public void ignoreDataTypeConversionFailures(boolean ignoreDataTypeConversionFai public boolean ignoreDataTypeConversionFailures() { return ignoreDataTypeConversionFailures; } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) { + return false; + } + if (!super.equals(o)) { + return false; + } + + MemberModel that = (MemberModel) o; + return deprecated == that.deprecated + && required == that.required + && synthetic == that.synthetic + && idempotencyToken == that.idempotencyToken + && isJsonValue == that.isJsonValue + && eventPayload == that.eventPayload + && eventHeader == that.eventHeader + && endpointDiscoveryId == that.endpointDiscoveryId + && sensitive == that.sensitive + && xmlAttribute == that.xmlAttribute + && ignoreDataTypeConversionFailures == that.ignoreDataTypeConversionFailures + && Objects.equals(name, that.name) + && Objects.equals(c2jName, that.c2jName) + && Objects.equals(c2jShape, that.c2jShape) + && Objects.equals(variable, that.variable) + && Objects.equals(setterModel, that.setterModel) + && Objects.equals(getterModel, that.getterModel) + && Objects.equals(http, that.http) + && Objects.equals(deprecatedMessage, that.deprecatedMessage) + && Objects.equals(listModel, that.listModel) + && Objects.equals(mapModel, that.mapModel) + && Objects.equals(enumType, that.enumType) + && Objects.equals(xmlNameSpaceUri, that.xmlNameSpaceUri) + && Objects.equals(shape, that.shape) + && Objects.equals(fluentGetterMethodName, that.fluentGetterMethodName) + && Objects.equals(fluentEnumGetterMethodName, that.fluentEnumGetterMethodName) + && Objects.equals(fluentSetterMethodName, that.fluentSetterMethodName) + && Objects.equals(fluentEnumSetterMethodName, that.fluentEnumSetterMethodName) + && Objects.equals(existenceCheckMethodName, that.existenceCheckMethodName) + && Objects.equals(beanStyleGetterName, that.beanStyleGetterName) + && Objects.equals(beanStyleSetterName, that.beanStyleSetterName) + && Objects.equals(unionEnumTypeName, that.unionEnumTypeName) + && Objects.equals(timestampFormat, that.timestampFormat) + && Objects.equals(deprecatedName, that.deprecatedName) + && Objects.equals(fluentDeprecatedGetterMethodName, that.fluentDeprecatedGetterMethodName) + && Objects.equals(fluentDeprecatedSetterMethodName, that.fluentDeprecatedSetterMethodName) + && Objects.equals(deprecatedBeanStyleSetterMethodName, that.deprecatedBeanStyleSetterMethodName) + && Objects.equals(contextParam, that.contextParam); + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + Objects.hashCode(name); + result = 31 * result + Objects.hashCode(c2jName); + result = 31 * result + Objects.hashCode(c2jShape); + result = 31 * result + Objects.hashCode(variable); + result = 31 * result + Objects.hashCode(setterModel); + result = 31 * result + Objects.hashCode(getterModel); + result = 31 * result + Objects.hashCode(http); + result = 31 * result + Boolean.hashCode(deprecated); + result = 31 * result + Objects.hashCode(deprecatedMessage); + result = 31 * result + Boolean.hashCode(required); + result = 31 * result + Boolean.hashCode(synthetic); + result = 31 * result + Objects.hashCode(listModel); + result = 31 * result + Objects.hashCode(mapModel); + result = 31 * result + Objects.hashCode(enumType); + result = 31 * result + Objects.hashCode(xmlNameSpaceUri); + result = 31 * result + Boolean.hashCode(idempotencyToken); + result = 31 * result + Objects.hashCode(shape); + result = 31 * result + Objects.hashCode(fluentGetterMethodName); + result = 31 * result + Objects.hashCode(fluentEnumGetterMethodName); + result = 31 * result + Objects.hashCode(fluentSetterMethodName); + result = 31 * result + Objects.hashCode(fluentEnumSetterMethodName); + result = 31 * result + Objects.hashCode(existenceCheckMethodName); + result = 31 * result + Objects.hashCode(beanStyleGetterName); + result = 31 * result + Objects.hashCode(beanStyleSetterName); + result = 31 * result + Objects.hashCode(unionEnumTypeName); + result = 31 * result + Boolean.hashCode(isJsonValue); + result = 31 * result + Objects.hashCode(timestampFormat); + result = 31 * result + Boolean.hashCode(eventPayload); + result = 31 * result + Boolean.hashCode(eventHeader); + result = 31 * result + Boolean.hashCode(endpointDiscoveryId); + result = 31 * result + Boolean.hashCode(sensitive); + result = 31 * result + Boolean.hashCode(xmlAttribute); + result = 31 * result + Objects.hashCode(deprecatedName); + result = 31 * result + Objects.hashCode(fluentDeprecatedGetterMethodName); + result = 31 * result + Objects.hashCode(fluentDeprecatedSetterMethodName); + result = 31 * result + Objects.hashCode(deprecatedBeanStyleSetterMethodName); + result = 31 * result + Objects.hashCode(contextParam); + result = 31 * result + Boolean.hashCode(ignoreDataTypeConversionFailures); + return result; + } } diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/model/intermediate/OperationModel.java b/codegen/src/main/java/software/amazon/awssdk/codegen/model/intermediate/OperationModel.java index a2a060c7a915..6b192644da1d 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/model/intermediate/OperationModel.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/model/intermediate/OperationModel.java @@ -19,6 +19,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.Objects; import software.amazon.awssdk.codegen.checksum.HttpChecksum; import software.amazon.awssdk.codegen.compression.RequestCompression; import software.amazon.awssdk.codegen.docs.ClientType; @@ -379,4 +380,63 @@ public boolean isUnsignedPayload() { public void setUnsignedPayload(boolean unsignedPayload) { this.unsignedPayload = unsignedPayload; } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) { + return false; + } + if (!super.equals(o)) { + return false; + } + + OperationModel that = (OperationModel) o; + return deprecated == that.deprecated && hasBlobMemberAsPayload == that.hasBlobMemberAsPayload + && hasStringMemberAsPayload == that.hasStringMemberAsPayload && isAuthenticated == that.isAuthenticated + && isPaginated == that.isPaginated && endpointOperation == that.endpointOperation + && endpointCacheRequired == that.endpointCacheRequired && httpChecksumRequired == that.httpChecksumRequired + && unsignedPayload == that.unsignedPayload && Objects.equals(operationName, that.operationName) + && Objects.equals(serviceProtocol, that.serviceProtocol) + && Objects.equals(deprecatedMessage, that.deprecatedMessage) && Objects.equals(input, that.input) + && Objects.equals(returnType, that.returnType) && Objects.equals(exceptions, that.exceptions) + && Objects.equals(simpleMethods, that.simpleMethods) && authType == that.authType + && Objects.equals(auth, that.auth) && Objects.equals(endpointDiscovery, that.endpointDiscovery) + && Objects.equals(inputShape, that.inputShape) && Objects.equals(outputShape, that.outputShape) + && Objects.equals(endpointTrait, that.endpointTrait) && Objects.equals(httpChecksum, that.httpChecksum) + && Objects.equals(requestcompression, that.requestcompression) + && Objects.equals(staticContextParams, that.staticContextParams) + && Objects.equals(operationContextParams, that.operationContextParams); + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + Objects.hashCode(operationName); + result = 31 * result + Objects.hashCode(serviceProtocol); + result = 31 * result + Boolean.hashCode(deprecated); + result = 31 * result + Objects.hashCode(deprecatedMessage); + result = 31 * result + Objects.hashCode(input); + result = 31 * result + Objects.hashCode(returnType); + result = 31 * result + Objects.hashCode(exceptions); + result = 31 * result + Objects.hashCode(simpleMethods); + result = 31 * result + Boolean.hashCode(hasBlobMemberAsPayload); + result = 31 * result + Boolean.hashCode(hasStringMemberAsPayload); + result = 31 * result + Boolean.hashCode(isAuthenticated); + result = 31 * result + Objects.hashCode(authType); + result = 31 * result + Objects.hashCode(auth); + result = 31 * result + Boolean.hashCode(isPaginated); + result = 31 * result + Boolean.hashCode(endpointOperation); + result = 31 * result + Boolean.hashCode(endpointCacheRequired); + result = 31 * result + Objects.hashCode(endpointDiscovery); + result = 31 * result + Objects.hashCode(inputShape); + result = 31 * result + Objects.hashCode(outputShape); + result = 31 * result + Objects.hashCode(endpointTrait); + result = 31 * result + Boolean.hashCode(httpChecksumRequired); + result = 31 * result + Objects.hashCode(httpChecksum); + result = 31 * result + Objects.hashCode(requestcompression); + result = 31 * result + Objects.hashCode(staticContextParams); + result = 31 * result + Objects.hashCode(operationContextParams); + result = 31 * result + Boolean.hashCode(unsignedPayload); + return result; + } } diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/model/intermediate/ParameterHttpMapping.java b/codegen/src/main/java/software/amazon/awssdk/codegen/model/intermediate/ParameterHttpMapping.java index 22ed4a8e6880..fc9a776059a7 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/model/intermediate/ParameterHttpMapping.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/model/intermediate/ParameterHttpMapping.java @@ -15,6 +15,7 @@ package software.amazon.awssdk.codegen.model.intermediate; +import java.util.Objects; import software.amazon.awssdk.codegen.model.service.Location; import software.amazon.awssdk.core.protocol.MarshallLocation; @@ -199,4 +200,40 @@ public MarshallLocation getMarshallLocation() { } } + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + ParameterHttpMapping that = (ParameterHttpMapping) o; + return isPayload == that.isPayload + && isStreaming == that.isStreaming + && flattened == that.flattened + && isGreedy == that.isGreedy + && requiresLength == that.requiresLength + && Objects.equals(unmarshallLocationName, that.unmarshallLocationName) + && Objects.equals(marshallLocationName, that.marshallLocationName) + && Objects.equals(additionalUnmarshallingPath, that.additionalUnmarshallingPath) + && Objects.equals(additionalMarshallingPath, that.additionalMarshallingPath) + && location == that.location; + } + + @Override + public int hashCode() { + int result = Objects.hashCode(unmarshallLocationName); + result = 31 * result + Objects.hashCode(marshallLocationName); + result = 31 * result + Objects.hashCode(additionalUnmarshallingPath); + result = 31 * result + Objects.hashCode(additionalMarshallingPath); + result = 31 * result + Boolean.hashCode(isPayload); + result = 31 * result + Boolean.hashCode(isStreaming); + result = 31 * result + Objects.hashCode(location); + result = 31 * result + Boolean.hashCode(flattened); + result = 31 * result + Boolean.hashCode(isGreedy); + result = 31 * result + Boolean.hashCode(requiresLength); + return result; + } } diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/model/intermediate/ReturnTypeModel.java b/codegen/src/main/java/software/amazon/awssdk/codegen/model/intermediate/ReturnTypeModel.java index 77dff4c71481..1d46c2802cda 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/model/intermediate/ReturnTypeModel.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/model/intermediate/ReturnTypeModel.java @@ -15,6 +15,8 @@ package software.amazon.awssdk.codegen.model.intermediate; +import java.util.Objects; + public class ReturnTypeModel { private String returnType; @@ -48,4 +50,24 @@ public ReturnTypeModel withDocumentation(String documentation) { setDocumentation(documentation); return this; } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + ReturnTypeModel that = (ReturnTypeModel) o; + return Objects.equals(returnType, that.returnType) && Objects.equals(documentation, that.documentation); + } + + @Override + public int hashCode() { + int result = Objects.hashCode(returnType); + result = 31 * result + Objects.hashCode(documentation); + return result; + } } diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/model/intermediate/ShapeModel.java b/codegen/src/main/java/software/amazon/awssdk/codegen/model/intermediate/ShapeModel.java index 098ea46bc7e4..3c26965302d5 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/model/intermediate/ShapeModel.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/model/intermediate/ShapeModel.java @@ -26,6 +26,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.stream.Collectors; import software.amazon.awssdk.codegen.model.intermediate.customization.ShapeCustomizationInfo; import software.amazon.awssdk.codegen.model.service.XmlNamespace; @@ -669,4 +670,84 @@ public ShapeModel withIsThrottling(boolean throttling) { this.throttling = throttling; return this; } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) { + return false; + } + if (!super.equals(o)) { + return false; + } + + ShapeModel that = (ShapeModel) o; + return deprecated == that.deprecated + && hasPayloadMember == that.hasPayloadMember + && hasHeaderMember == that.hasHeaderMember + && hasStatusCodeMember == that.hasStatusCodeMember + && hasStreamingMember == that.hasStreamingMember + && hasRequiresLengthMember == that.hasRequiresLengthMember + && wrapper == that.wrapper + && simpleMethod == that.simpleMethod + && fault == that.fault + && isEventStream == that.isEventStream + && isEvent == that.isEvent + && document == that.document + && union == that.union + && retryable == that.retryable + && throttling == that.throttling + && Objects.equals(c2jName, that.c2jName) + && Objects.equals(shapeName, that.shapeName) + && Objects.equals(deprecatedMessage, that.deprecatedMessage) + && Objects.equals(type, that.type) + && Objects.equals(required, that.required) + && Objects.equals(requestSignerClassFqcn, that.requestSignerClassFqcn) + && Objects.equals(endpointDiscovery, that.endpointDiscovery) + && Objects.equals(members, that.members) + && Objects.equals(enums, that.enums) + && Objects.equals(variable, that.variable) + && Objects.equals(marshaller, that.marshaller) + && Objects.equals(unmarshaller, that.unmarshaller) + && Objects.equals(errorCode, that.errorCode) + && Objects.equals(httpStatusCode, that.httpStatusCode) + && Objects.equals(customization, that.customization) + && Objects.equals(xmlNamespace, that.xmlNamespace); + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + Objects.hashCode(c2jName); + result = 31 * result + Objects.hashCode(shapeName); + result = 31 * result + Boolean.hashCode(deprecated); + result = 31 * result + Objects.hashCode(deprecatedMessage); + result = 31 * result + Objects.hashCode(type); + result = 31 * result + Objects.hashCode(required); + result = 31 * result + Boolean.hashCode(hasPayloadMember); + result = 31 * result + Boolean.hashCode(hasHeaderMember); + result = 31 * result + Boolean.hashCode(hasStatusCodeMember); + result = 31 * result + Boolean.hashCode(hasStreamingMember); + result = 31 * result + Boolean.hashCode(hasRequiresLengthMember); + result = 31 * result + Boolean.hashCode(wrapper); + result = 31 * result + Boolean.hashCode(simpleMethod); + result = 31 * result + Objects.hashCode(requestSignerClassFqcn); + result = 31 * result + Objects.hashCode(endpointDiscovery); + result = 31 * result + Objects.hashCode(members); + result = 31 * result + Objects.hashCode(enums); + result = 31 * result + Objects.hashCode(variable); + result = 31 * result + Objects.hashCode(marshaller); + result = 31 * result + Objects.hashCode(unmarshaller); + result = 31 * result + Objects.hashCode(errorCode); + result = 31 * result + Objects.hashCode(httpStatusCode); + result = 31 * result + Boolean.hashCode(fault); + result = 31 * result + Objects.hashCode(customization); + result = 31 * result + Boolean.hashCode(isEventStream); + result = 31 * result + Boolean.hashCode(isEvent); + result = 31 * result + Objects.hashCode(xmlNamespace); + result = 31 * result + Boolean.hashCode(document); + result = 31 * result + Boolean.hashCode(union); + result = 31 * result + Boolean.hashCode(retryable); + result = 31 * result + Boolean.hashCode(throttling); + return result; + } } diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/model/intermediate/VariableModel.java b/codegen/src/main/java/software/amazon/awssdk/codegen/model/intermediate/VariableModel.java index bdf0668a9d21..b9355009e748 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/model/intermediate/VariableModel.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/model/intermediate/VariableModel.java @@ -17,6 +17,7 @@ import java.util.Collection; import java.util.List; +import java.util.Objects; public class VariableModel extends DocumentationModel { @@ -98,4 +99,31 @@ public String getVariableSetterType() { public String toString() { return variableName; } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + if (!super.equals(o)) { + return false; + } + + VariableModel that = (VariableModel) o; + return Objects.equals(variableName, that.variableName) + && Objects.equals(variableType, that.variableType) + && Objects.equals(variableDeclarationType, that.variableDeclarationType); + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + Objects.hashCode(variableName); + result = 31 * result + Objects.hashCode(variableType); + result = 31 * result + Objects.hashCode(variableDeclarationType); + return result; + } } diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/model/intermediate/customization/ArtificialResultWrapper.java b/codegen/src/main/java/software/amazon/awssdk/codegen/model/intermediate/customization/ArtificialResultWrapper.java index e8adab25b48c..dd0b91d86301 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/model/intermediate/customization/ArtificialResultWrapper.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/model/intermediate/customization/ArtificialResultWrapper.java @@ -15,6 +15,8 @@ package software.amazon.awssdk.codegen.model.intermediate.customization; +import java.util.Objects; + public class ArtificialResultWrapper { private String wrappedMemberName; private String wrappedMemberSimpleType; @@ -34,4 +36,25 @@ public String getWrappedMemberSimpleType() { public void setWrappedMemberSimpleType(String wrappedMemberSimpleType) { this.wrappedMemberSimpleType = wrappedMemberSimpleType; } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + ArtificialResultWrapper that = (ArtificialResultWrapper) o; + return Objects.equals(wrappedMemberName, that.wrappedMemberName) + && Objects.equals(wrappedMemberSimpleType, that.wrappedMemberSimpleType); + } + + @Override + public int hashCode() { + int result = Objects.hashCode(wrappedMemberName); + result = 31 * result + Objects.hashCode(wrappedMemberSimpleType); + return result; + } } diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/model/intermediate/customization/ShapeCustomizationInfo.java b/codegen/src/main/java/software/amazon/awssdk/codegen/model/intermediate/customization/ShapeCustomizationInfo.java index b6d3950985b2..2e031eabb9a4 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/model/intermediate/customization/ShapeCustomizationInfo.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/model/intermediate/customization/ShapeCustomizationInfo.java @@ -16,6 +16,7 @@ package software.amazon.awssdk.codegen.model.intermediate.customization; import com.fasterxml.jackson.annotation.JsonIgnore; +import java.util.Objects; public class ShapeCustomizationInfo { @@ -72,4 +73,33 @@ public void setStaxTargetDepthOffset(int staxTargetDepthOffset) { public boolean hasStaxTargetDepthOffset() { return hasStaxTargetDepthOffset; } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + ShapeCustomizationInfo that = (ShapeCustomizationInfo) o; + return skipGeneratingModelClass == that.skipGeneratingModelClass + && skipGeneratingMarshaller == that.skipGeneratingMarshaller + && skipGeneratingUnmarshaller == that.skipGeneratingUnmarshaller + && staxTargetDepthOffset == that.staxTargetDepthOffset + && hasStaxTargetDepthOffset == that.hasStaxTargetDepthOffset + && Objects.equals(artificialResultWrapper, that.artificialResultWrapper); + } + + @Override + public int hashCode() { + int result = Objects.hashCode(artificialResultWrapper); + result = 31 * result + Boolean.hashCode(skipGeneratingModelClass); + result = 31 * result + Boolean.hashCode(skipGeneratingMarshaller); + result = 31 * result + Boolean.hashCode(skipGeneratingUnmarshaller); + result = 31 * result + staxTargetDepthOffset; + result = 31 * result + Boolean.hashCode(hasStaxTargetDepthOffset); + return result; + } } diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/model/service/ContextParam.java b/codegen/src/main/java/software/amazon/awssdk/codegen/model/service/ContextParam.java index 96f363cd84f1..8650d1145bcb 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/model/service/ContextParam.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/model/service/ContextParam.java @@ -15,6 +15,8 @@ package software.amazon.awssdk.codegen.model.service; +import java.util.Objects; + public class ContextParam { private String name; @@ -25,4 +27,22 @@ public String getName() { public void setName(String name) { this.name = name; } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + ContextParam that = (ContextParam) o; + return Objects.equals(name, that.name); + } + + @Override + public int hashCode() { + return Objects.hashCode(name); + } } diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/EndpointRulesClientTestSpec.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/EndpointRulesClientTestSpec.java index ce7adb6066ee..d077473f532a 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/EndpointRulesClientTestSpec.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/EndpointRulesClientTestSpec.java @@ -61,6 +61,10 @@ import software.amazon.awssdk.codegen.poet.PoetExtension; import software.amazon.awssdk.codegen.poet.PoetUtils; import software.amazon.awssdk.codegen.utils.AuthUtils; +import software.amazon.awssdk.codegen.validation.ModelInvalidException; +import software.amazon.awssdk.codegen.validation.ValidationEntry; +import software.amazon.awssdk.codegen.validation.ValidationErrorId; +import software.amazon.awssdk.codegen.validation.ValidationErrorSeverity; import software.amazon.awssdk.core.SdkSystemSetting; import software.amazon.awssdk.core.async.AsyncRequestBody; import software.amazon.awssdk.core.rules.testing.AsyncTestCase; @@ -445,6 +449,20 @@ private CodeBlock requestCreation(OperationModel opModel, Map if (opParams != null) { opParams.forEach((n, v) -> { MemberModel memberModel = opModel.getInputShape().getMemberByC2jName(n); + + if (memberModel == null) { + String detailMsg = String.format("Endpoint test definition references member '%s' on the input shape '%s' " + + "but no such member is defined.", n, opModel.getInputShape().getC2jName()); + ValidationEntry entry = + new ValidationEntry() + .withSeverity(ValidationErrorSeverity.DANGER) + .withErrorId(ValidationErrorId.UNKNOWN_SHAPE_MEMBER) + .withDetailMessage(detailMsg); + + throw ModelInvalidException.builder() + .validationEntries(Collections.singletonList(entry)) + .build(); + } CodeBlock memberValue = createMemberValue(memberModel, v); b.add(".$N($L)", memberModel.getFluentSetterMethodName(), memberValue); }); diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/EndpointRulesSpecUtils.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/EndpointRulesSpecUtils.java index bad36fe6594f..dfcf68b056fd 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/EndpointRulesSpecUtils.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/EndpointRulesSpecUtils.java @@ -29,7 +29,12 @@ import com.squareup.javapoet.TypeName; import java.io.IOException; import java.io.UncheckedIOException; +import java.net.URI; +import java.net.URISyntaxException; import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.Arrays; import java.util.Iterator; import java.util.List; @@ -52,6 +57,7 @@ import software.amazon.awssdk.utils.internal.CodegenNamingUtils; public class EndpointRulesSpecUtils { + private static final String RULES_ENGINE_RESOURCE_FILES_PREFIX = "software/amazon/awssdk/codegen/rules/"; private final IntermediateModel intermediateModel; public EndpointRulesSpecUtils(IntermediateModel intermediateModel) { @@ -213,16 +219,45 @@ public TypeName resolverReturnType() { public List rulesEngineResourceFiles() { URL currentJarUrl = EndpointRulesSpecUtils.class.getProtectionDomain().getCodeSource().getLocation(); + + // This would happen if the classes aren't loaded from a JAR, e.g. when unit testing + if (!currentJarUrl.toString().endsWith(".jar")) { + return rulesEngineFilesFromDirectory(currentJarUrl); + } + try (JarFile jarFile = new JarFile(currentJarUrl.getFile())) { return jarFile.stream() .map(ZipEntry::getName) - .filter(e -> e.startsWith("software/amazon/awssdk/codegen/rules/")) + .filter(e -> e.startsWith(RULES_ENGINE_RESOURCE_FILES_PREFIX)) .collect(Collectors.toList()); } catch (IOException e) { throw new UncheckedIOException(e); } } + public List rulesEngineFilesFromDirectory(URL location) { + URI locationUri; + try { + locationUri = location.toURI(); + if (!"file".equals(locationUri.getScheme())) { + throw new RuntimeException("Expected location to be a directory"); + } + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } + + try { + Path directory = Paths.get(locationUri); + return Files.walk(directory) + // Remove the root directory if the classes, paths are expected to be relative to this directory + .map(f -> directory.relativize(f).toString()) + .filter(f -> f.startsWith(RULES_ENGINE_RESOURCE_FILES_PREFIX)) + .collect(Collectors.toList()); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + public List rulesEngineResourceFiles2() { URL currentJarUrl = EndpointRulesSpecUtils.class.getProtectionDomain().getCodeSource().getLocation(); try (JarFile jarFile = new JarFile(currentJarUrl.getFile())) { diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/validation/ModelInvalidException.java b/codegen/src/main/java/software/amazon/awssdk/codegen/validation/ModelInvalidException.java new file mode 100644 index 000000000000..28f482328253 --- /dev/null +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/validation/ModelInvalidException.java @@ -0,0 +1,58 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.codegen.validation; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Exception thrown during code generation to signal that the model is invalid. + */ +public class ModelInvalidException extends RuntimeException { + private final List validationEntries; + + private ModelInvalidException(Builder b) { + super("Validation failed with the following errors: " + b.validationEntries); + this.validationEntries = Collections.unmodifiableList(new ArrayList<>(b.validationEntries)); + } + + public List validationEntries() { + return validationEntries; + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private List validationEntries; + + public Builder validationEntries(List validationEntries) { + if (validationEntries == null) { + this.validationEntries = Collections.emptyList(); + } else { + this.validationEntries = validationEntries; + } + + return this; + } + + public ModelInvalidException build() { + return new ModelInvalidException(this); + } + } +} diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/validation/ModelValidationContext.java b/codegen/src/main/java/software/amazon/awssdk/codegen/validation/ModelValidationContext.java new file mode 100644 index 000000000000..55c2bedcba19 --- /dev/null +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/validation/ModelValidationContext.java @@ -0,0 +1,79 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.codegen.validation; + +import java.util.Optional; +import software.amazon.awssdk.codegen.model.config.customization.ShareModelConfig; +import software.amazon.awssdk.codegen.model.intermediate.IntermediateModel; + +/** + * Context object for {@link ModelValidator}s. This object contains all the information available to the validations in order + * for them to perform their tasks. + */ +public final class ModelValidationContext { + private final IntermediateModel intermediateModel; + private final IntermediateModel shareModelsTarget; + + private ModelValidationContext(Builder builder) { + this.intermediateModel = builder.intermediateModel; + this.shareModelsTarget = builder.shareModelsTarget; + } + + /** + * The service model for which code is being generated. + */ + public IntermediateModel intermediateModel() { + return intermediateModel; + } + + /** + * The model of the service that the currently generating service shares models with. In other words, this is the service + * model for the service defined in {@link ShareModelConfig#getShareModelWith()}. + */ + public Optional shareModelsTarget() { + return Optional.ofNullable(shareModelsTarget); + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private IntermediateModel intermediateModel; + private IntermediateModel shareModelsTarget; + + /** + * The service model for which code is being generated. + */ + public Builder intermediateModel(IntermediateModel intermediateModel) { + this.intermediateModel = intermediateModel; + return this; + } + + /** + * The model of the service that the currently generating service shares models with. In other words, this is the service + * model for the service defined in {@link ShareModelConfig#getShareModelWith()}. + */ + public Builder shareModelsTarget(IntermediateModel shareModelsTarget) { + this.shareModelsTarget = shareModelsTarget; + return this; + } + + public ModelValidationContext build() { + return new ModelValidationContext(this); + } + } +} diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/validation/ModelValidationReport.java b/codegen/src/main/java/software/amazon/awssdk/codegen/validation/ModelValidationReport.java new file mode 100644 index 000000000000..1112dc2190d5 --- /dev/null +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/validation/ModelValidationReport.java @@ -0,0 +1,40 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.codegen.validation; + +import java.util.Collections; +import java.util.List; + +public class ModelValidationReport { + private List validationEntries = Collections.emptyList(); + + public List getValidationEntries() { + return validationEntries; + } + + public void setValidationEntries(List validationEntries) { + if (validationEntries != null) { + this.validationEntries = validationEntries; + } else { + this.validationEntries = Collections.emptyList(); + } + } + + public ModelValidationReport withValidationEntries(List validationEntries) { + setValidationEntries(validationEntries); + return this; + } +} diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/validation/ModelValidator.java b/codegen/src/main/java/software/amazon/awssdk/codegen/validation/ModelValidator.java new file mode 100644 index 000000000000..b544a030eaf5 --- /dev/null +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/validation/ModelValidator.java @@ -0,0 +1,22 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.codegen.validation; + +import java.util.List; + +public interface ModelValidator { + List validateModels(ModelValidationContext context); +} diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/validation/SharedModelsValidator.java b/codegen/src/main/java/software/amazon/awssdk/codegen/validation/SharedModelsValidator.java new file mode 100644 index 000000000000..6b7f8471da7c --- /dev/null +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/validation/SharedModelsValidator.java @@ -0,0 +1,210 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.codegen.validation; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import software.amazon.awssdk.codegen.model.intermediate.IntermediateModel; +import software.amazon.awssdk.codegen.model.intermediate.ListModel; +import software.amazon.awssdk.codegen.model.intermediate.MapModel; +import software.amazon.awssdk.codegen.model.intermediate.MemberModel; +import software.amazon.awssdk.codegen.model.intermediate.ShapeModel; +import software.amazon.awssdk.utils.Logger; + +/** + * Validator that ensures any shapes shared between two services are completely identical. This validator returns a validation + * entry for each shape that is present in both service models but has differing definitions in each model. + */ +public final class SharedModelsValidator implements ModelValidator { + private static final Logger LOG = Logger.loggerFor(SharedModelsValidator.class); + + @Override + public List validateModels(ModelValidationContext context) { + if (!context.shareModelsTarget().isPresent()) { + return Collections.emptyList(); + } + + return validateSharedShapes(context.intermediateModel(), context.shareModelsTarget().get()); + } + + private List validateSharedShapes(IntermediateModel m1, IntermediateModel m2) { + List errors = new ArrayList<>(); + + Map m1Shapes = m1.getShapes(); + Map m2Shapes = m2.getShapes(); + + m1Shapes.forEach((name, m1Shape) -> { + if (!m2Shapes.containsKey(name)) { + return; + } + + ShapeModel m2Shape = m2Shapes.get(name); + + if (!shapesAreIdentical(m1Shape, m2Shape)) { + String detailMsg = String.format("Services '%s' and '%s' have differing definitions of the shared model '%s'", + m1.getMetadata().getServiceName(), + m2.getMetadata().getServiceName(), + name); + LOG.warn(() -> detailMsg); + + errors.add(new ValidationEntry().withErrorId(ValidationErrorId.SHARED_MODELS_DIFFER) + .withSeverity(ValidationErrorSeverity.DANGER) + .withDetailMessage(detailMsg)); + } + }); + + return errors; + } + + private boolean shapesAreIdentical(ShapeModel m1, ShapeModel m2) { + // Note: We can't simply do m1.equals(m2) because shared models can still differ slightly in the + // marshalling/unmarshalling info such as the exact request operation name on the wire. + // In particular, we leave out comparing the `unmarshaller` and `marshaller` members of ShapeModel. + // Additionally, the List are not compared with equals() because we handle MemberModel equality specially + // as well. + return m1.isDeprecated() == m2.isDeprecated() + && m1.isHasPayloadMember() == m2.isHasPayloadMember() + && m1.isHasHeaderMember() == m2.isHasHeaderMember() + && m1.isHasStatusCodeMember() == m2.isHasStatusCodeMember() + && m1.isHasStreamingMember() == m2.isHasStreamingMember() + && m1.isHasRequiresLengthMember() == m2.isHasRequiresLengthMember() + && m1.isWrapper() == m2.isWrapper() + && m1.isSimpleMethod() == m2.isSimpleMethod() + && m1.isFault() == m2.isFault() + && m1.isEventStream() == m2.isEventStream() + && m1.isEvent() == m2.isEvent() + && m1.isDocument() == m2.isDocument() + && m1.isUnion() == m2.isUnion() + && m1.isRetryable() == m2.isRetryable() + && m1.isThrottling() == m2.isThrottling() + && Objects.equals(m1.getC2jName(), m2.getC2jName()) + && Objects.equals(m1.getShapeName(), m2.getShapeName()) + && Objects.equals(m1.getDeprecatedMessage(), m2.getDeprecatedMessage()) + && Objects.equals(m1.getType(), m2.getType()) + && Objects.equals(m1.getRequired(), m2.getRequired()) + && Objects.equals(m1.getRequestSignerClassFqcn(), m2.getRequestSignerClassFqcn()) + && Objects.equals(m1.getEndpointDiscovery(), m2.getEndpointDiscovery()) + && memberListsAreIdentical(m1.getMembers(), m2.getMembers()) + && Objects.equals(m1.getEnums(), m2.getEnums()) + && Objects.equals(m1.getVariable(), m2.getVariable()) + && Objects.equals(m1.getErrorCode(), m2.getErrorCode()) + && Objects.equals(m1.getHttpStatusCode(), m2.getHttpStatusCode()) + && Objects.equals(m1.getCustomization(), m2.getCustomization()) + && Objects.equals(m1.getXmlNamespace(), m2.getXmlNamespace()) + ; + } + + private boolean memberListsAreIdentical(List memberList1, List memberList2) { + if (memberList1.size() != memberList2.size()) { + return false; + } + + for (int i = 0; i < memberList1.size(); i++) { + MemberModel m1 = memberList1.get(i); + MemberModel m2 = memberList2.get(i); + if (!memberModelsAreIdentical(m1, m2)) { + return false; + } + } + + return true; + } + + private boolean memberModelsAreIdentical(MemberModel m1, MemberModel m2) { + // Similar to ShapeModel, can't call equals() directly. It has a ShapeModel property that is ignored, and ListModel and + // MapModel are treated similarly + return m1.isDeprecated() == m2.isDeprecated() + && m1.isRequired() == m2.isRequired() + && m1.isSynthetic() == m2.isSynthetic() + && m1.isIdempotencyToken() == m2.isIdempotencyToken() + && m1.isJsonValue() == m2.isJsonValue() + && m1.isEventPayload() == m2.isEventPayload() + && m1.isEventHeader() == m2.isEventHeader() + && m1.isEndpointDiscoveryId() == m2.isEndpointDiscoveryId() + && m1.isSensitive() == m2.isSensitive() + && m1.isXmlAttribute() == m2.isXmlAttribute() + && m1.ignoreDataTypeConversionFailures() == m2.ignoreDataTypeConversionFailures() + && Objects.equals(m1.getName(), m2.getName()) + && Objects.equals(m1.getC2jName(), m2.getC2jName()) + && Objects.equals(m1.getC2jShape(), m2.getC2jShape()) + && Objects.equals(m1.getVariable(), m2.getVariable()) + && Objects.equals(m1.getSetterModel(), m2.getSetterModel()) + && Objects.equals(m1.getGetterModel(), m2.getGetterModel()) + && Objects.equals(m1.getHttp(), m2.getHttp()) + && Objects.equals(m1.getDeprecatedMessage(), m2.getDeprecatedMessage()) + // Note: not equals() + && listModelsAreIdentical(m1.getListModel(), m2.getListModel()) + // Note: not equals() + && mapModelsAreIdentical(m1.getMapModel(), m2.getMapModel()) + && Objects.equals(m1.getEnumType(), m2.getEnumType()) + && Objects.equals(m1.getXmlNameSpaceUri(), m2.getXmlNameSpaceUri()) + && Objects.equals(m1.getFluentGetterMethodName(), m2.getFluentGetterMethodName()) + && Objects.equals(m1.getFluentEnumGetterMethodName(), m2.getFluentEnumGetterMethodName()) + && Objects.equals(m1.getFluentSetterMethodName(), m2.getFluentSetterMethodName()) + && Objects.equals(m1.getFluentEnumSetterMethodName(), m2.getFluentEnumSetterMethodName()) + && Objects.equals(m1.getExistenceCheckMethodName(), m2.getExistenceCheckMethodName()) + && Objects.equals(m1.getBeanStyleGetterMethodName(), m2.getBeanStyleGetterMethodName()) + && Objects.equals(m1.getBeanStyleSetterMethodName(), m2.getBeanStyleSetterMethodName()) + && Objects.equals(m1.getUnionEnumTypeName(), m2.getUnionEnumTypeName()) + && Objects.equals(m1.getTimestampFormat(), m2.getTimestampFormat()) + && Objects.equals(m1.getDeprecatedName(), m2.getDeprecatedName()) + && Objects.equals(m1.getDeprecatedFluentGetterMethodName(), m2.getDeprecatedFluentGetterMethodName()) + && Objects.equals(m1.getDeprecatedFluentSetterMethodName(), m2.getDeprecatedFluentSetterMethodName()) + && Objects.equals(m1.getDeprecatedBeanStyleSetterMethodName(), m2.getDeprecatedBeanStyleSetterMethodName()) + && Objects.equals(m1.getContextParam(), m2.getContextParam()); + } + + private boolean listModelsAreIdentical(ListModel m1, ListModel m2) { + if (m1 == null ^ m2 == null) { + return false; + } + + if (m1 == null) { + return true; + } + + return Objects.equals(m1.getImplType(), m2.getImplType()) + && Objects.equals(m1.getMemberType(), m2.getMemberType()) + && Objects.equals(m1.getInterfaceType(), m2.getInterfaceType()) + // Note: not equals() + && memberModelsAreIdentical(m1.getListMemberModel(), m2.getListMemberModel()) + && Objects.equals(m1.getMemberLocationName(), m2.getMemberLocationName()) + && Objects.equals(m1.getMemberAdditionalMarshallingPath(), m2.getMemberAdditionalMarshallingPath()) + && Objects.equals(m1.getMemberAdditionalUnmarshallingPath(), m2.getMemberAdditionalUnmarshallingPath()); + } + + private boolean mapModelsAreIdentical(MapModel m1, MapModel m2) { + if (m1 == null ^ m2 == null) { + return false; + } + + if (m1 == null) { + return true; + } + + return Objects.equals(m1.getImplType(), m2.getImplType()) + && Objects.equals(m1.getInterfaceType(), m2.getInterfaceType()) + && Objects.equals(m1.getKeyLocationName(), m2.getKeyLocationName()) + // Note: not equals() + && memberModelsAreIdentical(m1.getKeyModel(), m2.getKeyModel()) + && Objects.equals(m1.getValueLocationName(), m2.getValueLocationName()) + // Note: not equals() + && memberModelsAreIdentical(m1.getValueModel(), m2.getValueModel()); + } +} diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/validation/ValidationEntry.java b/codegen/src/main/java/software/amazon/awssdk/codegen/validation/ValidationEntry.java new file mode 100644 index 000000000000..4e84bd625185 --- /dev/null +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/validation/ValidationEntry.java @@ -0,0 +1,72 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.codegen.validation; + +import software.amazon.awssdk.utils.ToString; + +public final class ValidationEntry { + private ValidationErrorId errorId; + private ValidationErrorSeverity severity; + private String detailMessage; + + public ValidationErrorId getErrorId() { + return errorId; + } + + public void setErrorId(ValidationErrorId errorId) { + this.errorId = errorId; + } + + public ValidationEntry withErrorId(ValidationErrorId errorId) { + setErrorId(errorId); + return this; + } + + public ValidationErrorSeverity getSeverity() { + return severity; + } + + public void setSeverity(ValidationErrorSeverity severity) { + this.severity = severity; + } + + public ValidationEntry withSeverity(ValidationErrorSeverity severity) { + setSeverity(severity); + return this; + } + + public String getDetailMessage() { + return detailMessage; + } + + public void setDetailMessage(String detailMessage) { + this.detailMessage = detailMessage; + } + + public ValidationEntry withDetailMessage(String detailMessage) { + setDetailMessage(detailMessage); + return this; + } + + @Override + public String toString() { + return ToString.builder("ValidationEntry") + .add("errorId", errorId) + .add("severity", severity) + .add("detailMessage", detailMessage) + .build(); + } +} diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/validation/ValidationErrorId.java b/codegen/src/main/java/software/amazon/awssdk/codegen/validation/ValidationErrorId.java new file mode 100644 index 000000000000..37c488e11fc5 --- /dev/null +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/validation/ValidationErrorId.java @@ -0,0 +1,34 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.codegen.validation; + +public enum ValidationErrorId { + SHARED_MODELS_DIFFER( + "The shared models between two services differ in their definition, which causes differences in the source" + + " files generated by the code generator." + ), + UNKNOWN_SHAPE_MEMBER("The model references an unknown shape member."), + REQUEST_URI_NOT_FOUND("The request URI does not exist."), + ; + + private final String description; + + ValidationErrorId(String description) { + this.description = description; + } + + +} diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/validation/ValidationErrorSeverity.java b/codegen/src/main/java/software/amazon/awssdk/codegen/validation/ValidationErrorSeverity.java new file mode 100644 index 000000000000..39b6b015e42a --- /dev/null +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/validation/ValidationErrorSeverity.java @@ -0,0 +1,22 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.codegen.validation; + +public enum ValidationErrorSeverity { + // Denotes an error that MUST be addressed. + DANGER, + ; +} diff --git a/codegen/src/test/java/software/amazon/awssdk/codegen/CodeGeneratorTest.java b/codegen/src/test/java/software/amazon/awssdk/codegen/CodeGeneratorTest.java new file mode 100644 index 000000000000..c776e0295bea --- /dev/null +++ b/codegen/src/test/java/software/amazon/awssdk/codegen/CodeGeneratorTest.java @@ -0,0 +1,267 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.codegen; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.nio.file.FileVisitResult; +import java.nio.file.FileVisitor; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.codegen.internal.Jackson; +import software.amazon.awssdk.codegen.model.config.customization.CustomizationConfig; +import software.amazon.awssdk.codegen.model.intermediate.IntermediateModel; +import software.amazon.awssdk.codegen.model.rules.endpoints.EndpointTestSuiteModel; +import software.amazon.awssdk.codegen.model.service.ServiceModel; +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.ValidationErrorId; + +public class CodeGeneratorTest { + private static final String VALIDATION_REPORT_NAME = "validation-report.json"; + + private Path outputDir; + + @BeforeEach + void methodSetup() throws IOException { + outputDir = Files.createTempDirectory(null); + } + + @AfterEach + void methodTeardown() throws IOException { + deleteDirectory(outputDir); + } + + @Test + void build_cj2ModelsAndIntermediateModelSet_throws() { + assertThatThrownBy(() -> CodeGenerator.builder() + .models(C2jModels.builder().build()) + .intermediateModel(new IntermediateModel()) + .build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Only one of"); + } + + @Test + void execute_emitValidationReportIsFalse_doesNotEmitValidationReport() throws IOException { + generateCodeFromC2jModels(ClientTestModels.awsJsonServiceC2jModels(), outputDir); + assertThat(Files.exists(validationReportPath(outputDir))).isFalse(); + } + + @Test + void execute_emitValidationReportIsTrue_emitsValidationReport() throws IOException { + generateCodeFromC2jModels(ClientTestModels.awsJsonServiceC2jModels(), outputDir, true, null); + assertThat(Files.exists(validationReportPath(outputDir))).isTrue(); + } + + @Test + void execute_invokesModelValidators() { + ModelValidator mockValidator = mock(ModelValidator.class); + when(mockValidator.validateModels(any())).thenReturn(Collections.emptyList()); + + generateCodeFromC2jModels(ClientTestModels.awsJsonServiceC2jModels(), outputDir, true, + Collections.singletonList(mockValidator)); + + verify(mockValidator).validateModels(any()); + } + + @Test + void execute_c2jModelsAndIntermediateModel_generateSameCode() throws IOException { + Path c2jModelsOutputDir = outputDir.resolve("c2jModels"); + generateCodeFromC2jModels(ClientTestModels.awsJsonServiceC2jModels(), c2jModelsOutputDir, false, Collections.emptyList()); + + Path intermediateModelOutputDir = outputDir.resolve("intermediate-model"); + generateCodeFromIntermediateModel(ClientTestModels.awsJsonServiceModels(), intermediateModelOutputDir); + + List c2jModels_generatedFiles = Files.walk(c2jModelsOutputDir) + .sorted() + .map(c2jModelsOutputDir::relativize) + .collect(Collectors.toList()); + + List intermediateModels_generatedFiles = Files.walk(intermediateModelOutputDir) + .sorted() + .map(intermediateModelOutputDir::relativize) + .collect(Collectors.toList()); + + assertThat(c2jModels_generatedFiles).isNotEmpty(); + + // Ensure same exact set of files + assertThat(c2jModels_generatedFiles).isEqualTo(intermediateModels_generatedFiles); + + // All files should be exactly the same + for (Path generatedFile : c2jModels_generatedFiles) { + Path c2jGenerated = c2jModelsOutputDir.resolve(generatedFile); + Path intermediateGenerated = intermediateModelOutputDir.resolve(generatedFile); + + if (Files.isDirectory(c2jGenerated)) { + assertThat(Files.isDirectory(intermediateGenerated)).isTrue(); + } else { + assertThat(readToString(c2jGenerated)).isEqualTo(readToString(intermediateGenerated)); + } + } + } + + @Test + void execute_endpointsTestReferencesUnknownOperationMember_throwsValidationError() throws IOException { + ModelValidator mockValidator = mock(ModelValidator.class); + when(mockValidator.validateModels(any())).thenReturn(Collections.emptyList()); + + C2jModels referenceModels = ClientTestModels.awsJsonServiceC2jModels(); + + C2jModels c2jModelsWithBadTest = + C2jModels.builder() + .endpointTestSuiteModel(getBrokenEndpointTestSuiteModel()) + .customizationConfig(referenceModels.customizationConfig()) + .serviceModel(referenceModels.serviceModel()) + .paginatorsModel(referenceModels.paginatorsModel()) + .build(); + + assertThatThrownBy(() -> generateCodeFromC2jModels(c2jModelsWithBadTest, outputDir, true, + Collections.singletonList(mockValidator))) + .hasCauseInstanceOf(ModelInvalidException.class) + .matches(e -> { + ModelInvalidException exception = (ModelInvalidException) e.getCause(); + return exception.validationEntries().get(0).getErrorId() == ValidationErrorId.UNKNOWN_SHAPE_MEMBER; + }); + } + + @Test + void execute_operationHasNoRequestUri_throwsValidationError() throws IOException { + C2jModels models = C2jModels.builder() + .customizationConfig(CustomizationConfig.create()) + .serviceModel(getMissingRequestUriServiceModel()) + .build(); + + assertThatThrownBy(() -> generateCodeFromC2jModels(models, outputDir, true, Collections.emptyList())) + .isInstanceOf(ModelInvalidException.class) + .matches(e -> ((ModelInvalidException) e).validationEntries().get(0).getErrorId() + == ValidationErrorId.REQUEST_URI_NOT_FOUND); + } + + private void generateCodeFromC2jModels(C2jModels c2jModels, Path outputDir) { + generateCodeFromC2jModels(c2jModels, outputDir, false, null); + } + + private void generateCodeFromC2jModels(C2jModels c2jModels, Path outputDir, + boolean emitValidationReport, + List modelValidators) { + Path sources = outputDir.resolve("generated-sources").resolve("sdk"); + Path resources = outputDir.resolve("generated-resources").resolve("sdk-resources"); + Path tests = outputDir.resolve("generated-test-sources").resolve("sdk-tests"); + + CodeGenerator.builder() + .models(c2jModels) + .sourcesDirectory(sources.toAbsolutePath().toString()) + .resourcesDirectory(resources.toAbsolutePath().toString()) + .testsDirectory(tests.toAbsolutePath().toString()) + .emitValidationReport(emitValidationReport) + .modelValidators(modelValidators) + .build() + .execute(); + } + + private void generateCodeFromIntermediateModel(IntermediateModel intermediateModel, Path outputDir) { + Path sources = outputDir.resolve("generated-sources").resolve("sdk"); + Path resources = outputDir.resolve("generated-resources").resolve("sdk-resources"); + Path tests = outputDir.resolve("generated-test-sources").resolve("sdk-tests"); + + CodeGenerator.builder() + .intermediateModel(intermediateModel) + .sourcesDirectory(sources.toAbsolutePath().toString()) + .resourcesDirectory(resources.toAbsolutePath().toString()) + .testsDirectory(tests.toAbsolutePath().toString()) + .build() + .execute(); + } + + private static String readToString(Path p) throws IOException { + ByteBuffer bb = ByteBuffer.wrap(Files.readAllBytes(p)); + return StandardCharsets.UTF_8.decode(bb).toString(); + } + + private static Path validationReportPath(Path root) { + return root.resolve(Paths.get("generated-sources", "sdk", "models", VALIDATION_REPORT_NAME)); + } + + private EndpointTestSuiteModel getBrokenEndpointTestSuiteModel() throws IOException { + String json = resourceAsString("incorrect-endpoint-tests.json"); + return Jackson.load(EndpointTestSuiteModel.class, json); + } + + private ServiceModel getMissingRequestUriServiceModel() throws IOException { + String json = resourceAsString("no-request-uri-operation-service.json"); + return Jackson.load(ServiceModel.class, json); + } + + private String resourceAsString(String name) throws IOException { + ByteArrayOutputStream baos; + try (InputStream resourceAsStream = getClass().getResourceAsStream(name)) { + baos = new ByteArrayOutputStream(); + byte[] buffer = new byte[1024]; + int read; + while ((read = resourceAsStream.read(buffer)) != -1) { + baos.write(buffer, 0, read); + } + } + return StandardCharsets.UTF_8.decode(ByteBuffer.wrap(baos.toByteArray())).toString(); + } + + private static void deleteDirectory(Path dir) throws IOException { + Files.walkFileTree(dir, new FileVisitor() { + + @Override + public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + Files.delete(file); + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException { + return FileVisitResult.TERMINATE; + } + + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { + Files.delete(dir); + return FileVisitResult.CONTINUE; + } + }); + } +} diff --git a/codegen/src/test/java/software/amazon/awssdk/codegen/model/intermediate/ArgumentModelTest.java b/codegen/src/test/java/software/amazon/awssdk/codegen/model/intermediate/ArgumentModelTest.java new file mode 100644 index 000000000000..107a6e6cdb66 --- /dev/null +++ b/codegen/src/test/java/software/amazon/awssdk/codegen/model/intermediate/ArgumentModelTest.java @@ -0,0 +1,29 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.codegen.model.intermediate; + +import nl.jqno.equalsverifier.EqualsVerifier; +import org.junit.jupiter.api.Test; + +public class ArgumentModelTest { + @Test + public void equals_isCorrect() { + EqualsVerifier.simple() + .forClass(ArgumentModel.class) + .usingGetClass() + .verify(); + } +} diff --git a/codegen/src/test/java/software/amazon/awssdk/codegen/model/intermediate/AuthorizerModelTest.java b/codegen/src/test/java/software/amazon/awssdk/codegen/model/intermediate/AuthorizerModelTest.java new file mode 100644 index 000000000000..28d8dd845412 --- /dev/null +++ b/codegen/src/test/java/software/amazon/awssdk/codegen/model/intermediate/AuthorizerModelTest.java @@ -0,0 +1,26 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.codegen.model.intermediate; + +import nl.jqno.equalsverifier.EqualsVerifier; +import org.junit.jupiter.api.Test; + +public class AuthorizerModelTest { + @Test + public void equals_isCorrect() { + EqualsVerifier.simple().forClass(AuthorizerModel.class).usingGetClass().verify(); + } +} diff --git a/codegen/src/test/java/software/amazon/awssdk/codegen/model/intermediate/MemberModelTest.java b/codegen/src/test/java/software/amazon/awssdk/codegen/model/intermediate/MemberModelTest.java new file mode 100644 index 000000000000..bd4a0859603f --- /dev/null +++ b/codegen/src/test/java/software/amazon/awssdk/codegen/model/intermediate/MemberModelTest.java @@ -0,0 +1,40 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.codegen.model.intermediate; + +import nl.jqno.equalsverifier.EqualsVerifier; +import org.junit.jupiter.api.Test; + +public class MemberModelTest { + @Test + public void equals_isCorrect() { + ListModel redListModel = new ListModel(); + redListModel.setMemberLocationName("RedLocation"); + ListModel blueListModel = new ListModel(); + blueListModel.setMemberLocationName("BlueLocation"); + + MemberModel redMemberModel = new MemberModel(); + redMemberModel.setC2jName("RedC2jName"); + MemberModel blueMemberModel = new MemberModel(); + blueMemberModel.setC2jName("BlueC2jName"); + + EqualsVerifier.simple().forClass(MemberModel.class) + .withPrefabValues(ListModel.class, redListModel, blueListModel) + .withPrefabValues(MemberModel.class, redMemberModel, blueMemberModel) + .usingGetClass() + .verify(); + } +} diff --git a/codegen/src/test/java/software/amazon/awssdk/codegen/model/intermediate/OperationModelTest.java b/codegen/src/test/java/software/amazon/awssdk/codegen/model/intermediate/OperationModelTest.java new file mode 100644 index 000000000000..531d0b1aa55e --- /dev/null +++ b/codegen/src/test/java/software/amazon/awssdk/codegen/model/intermediate/OperationModelTest.java @@ -0,0 +1,35 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.codegen.model.intermediate; + +import nl.jqno.equalsverifier.EqualsVerifier; +import org.junit.jupiter.api.Test; + +public class OperationModelTest { + @Test + void equals_isCorrect() { + MemberModel blueMemberModel = new MemberModel(); + blueMemberModel.setName("blue"); + MemberModel redMemberModel = new MemberModel(); + redMemberModel.setName("red"); + + EqualsVerifier.simple() + .forClass(OperationModel.class) + .withPrefabValues(MemberModel.class, blueMemberModel, redMemberModel) + .usingGetClass() + .verify(); + } +} diff --git a/codegen/src/test/java/software/amazon/awssdk/codegen/model/intermediate/ParameterHttpMappingTest.java b/codegen/src/test/java/software/amazon/awssdk/codegen/model/intermediate/ParameterHttpMappingTest.java new file mode 100644 index 000000000000..cd142cb34c2c --- /dev/null +++ b/codegen/src/test/java/software/amazon/awssdk/codegen/model/intermediate/ParameterHttpMappingTest.java @@ -0,0 +1,29 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.codegen.model.intermediate; + +import nl.jqno.equalsverifier.EqualsVerifier; +import org.junit.jupiter.api.Test; + +public class ParameterHttpMappingTest { + @Test + void equals_isCorrect() { + EqualsVerifier.simple() + .forClass(ParameterHttpMapping.class) + .usingGetClass() + .verify(); + } +} diff --git a/codegen/src/test/java/software/amazon/awssdk/codegen/model/intermediate/ReturnTypeModelTest.java b/codegen/src/test/java/software/amazon/awssdk/codegen/model/intermediate/ReturnTypeModelTest.java new file mode 100644 index 000000000000..53e99f514403 --- /dev/null +++ b/codegen/src/test/java/software/amazon/awssdk/codegen/model/intermediate/ReturnTypeModelTest.java @@ -0,0 +1,29 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.codegen.model.intermediate; + +import nl.jqno.equalsverifier.EqualsVerifier; +import org.junit.jupiter.api.Test; + +public class ReturnTypeModelTest { + @Test + void equals_isCorrect() { + EqualsVerifier.simple() + .forClass(ReturnTypeModel.class) + .usingGetClass() + .verify(); + } +} diff --git a/codegen/src/test/java/software/amazon/awssdk/codegen/model/intermediate/ShapeModelTest.java b/codegen/src/test/java/software/amazon/awssdk/codegen/model/intermediate/ShapeModelTest.java new file mode 100644 index 000000000000..08fb79681e96 --- /dev/null +++ b/codegen/src/test/java/software/amazon/awssdk/codegen/model/intermediate/ShapeModelTest.java @@ -0,0 +1,37 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.codegen.model.intermediate; + +import nl.jqno.equalsverifier.EqualsVerifier; +import org.junit.jupiter.api.Test; + +public class ShapeModelTest { + + + @Test + public void equals_isCorrect() { + MemberModel blueMemberModel = new MemberModel(); + blueMemberModel.setName("blue"); + MemberModel redMemberModel = new MemberModel(); + redMemberModel.setName("red"); + + EqualsVerifier.simple() + .forClass(ShapeModel.class) + .withPrefabValues(MemberModel.class, blueMemberModel, redMemberModel) + .usingGetClass() + .verify(); + } +} diff --git a/codegen/src/test/java/software/amazon/awssdk/codegen/model/intermediate/VariableModelTest.java b/codegen/src/test/java/software/amazon/awssdk/codegen/model/intermediate/VariableModelTest.java new file mode 100644 index 000000000000..55ea2f39123a --- /dev/null +++ b/codegen/src/test/java/software/amazon/awssdk/codegen/model/intermediate/VariableModelTest.java @@ -0,0 +1,29 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.codegen.model.intermediate; + +import nl.jqno.equalsverifier.EqualsVerifier; +import org.junit.jupiter.api.Test; + +public class VariableModelTest { + @Test + void equals_isCorrect() { + EqualsVerifier.simple() + .forClass(VariableModel.class) + .usingGetClass() + .verify(); + } +} diff --git a/codegen/src/test/java/software/amazon/awssdk/codegen/model/intermediate/customization/ArtificialResultWrapperTest.java b/codegen/src/test/java/software/amazon/awssdk/codegen/model/intermediate/customization/ArtificialResultWrapperTest.java new file mode 100644 index 000000000000..aa29412a5f5f --- /dev/null +++ b/codegen/src/test/java/software/amazon/awssdk/codegen/model/intermediate/customization/ArtificialResultWrapperTest.java @@ -0,0 +1,29 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.codegen.model.intermediate.customization; + +import nl.jqno.equalsverifier.EqualsVerifier; +import org.junit.jupiter.api.Test; + +public class ArtificialResultWrapperTest { + @Test + void equals_isCorrect() { + EqualsVerifier.simple() + .forClass(ArtificialResultWrapper.class) + .usingGetClass() + .verify(); + } +} diff --git a/codegen/src/test/java/software/amazon/awssdk/codegen/model/intermediate/customization/ShapeCustomizationInfoTest.java b/codegen/src/test/java/software/amazon/awssdk/codegen/model/intermediate/customization/ShapeCustomizationInfoTest.java new file mode 100644 index 000000000000..3126117f100d --- /dev/null +++ b/codegen/src/test/java/software/amazon/awssdk/codegen/model/intermediate/customization/ShapeCustomizationInfoTest.java @@ -0,0 +1,29 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.codegen.model.intermediate.customization; + +import nl.jqno.equalsverifier.EqualsVerifier; +import org.junit.jupiter.api.Test; + +public class ShapeCustomizationInfoTest { + @Test + void equals_isCorrect() { + EqualsVerifier.simple() + .forClass(ShapeCustomizationInfo.class) + .usingGetClass() + .verify(); + } +} diff --git a/codegen/src/test/java/software/amazon/awssdk/codegen/model/service/ContextParamTest.java b/codegen/src/test/java/software/amazon/awssdk/codegen/model/service/ContextParamTest.java new file mode 100644 index 000000000000..937688b70cb4 --- /dev/null +++ b/codegen/src/test/java/software/amazon/awssdk/codegen/model/service/ContextParamTest.java @@ -0,0 +1,29 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.codegen.model.service; + +import nl.jqno.equalsverifier.EqualsVerifier; +import org.junit.jupiter.api.Test; + +public class ContextParamTest { + @Test + void equals_isCorrect() { + EqualsVerifier.simple() + .forClass(ContextParam.class) + .usingGetClass() + .verify(); + } +} diff --git a/codegen/src/test/java/software/amazon/awssdk/codegen/poet/ClientTestModels.java b/codegen/src/test/java/software/amazon/awssdk/codegen/poet/ClientTestModels.java index 54dd331118ce..36360b4b3b37 100644 --- a/codegen/src/test/java/software/amazon/awssdk/codegen/poet/ClientTestModels.java +++ b/codegen/src/test/java/software/amazon/awssdk/codegen/poet/ClientTestModels.java @@ -47,6 +47,17 @@ public static IntermediateModel awsJsonServiceModels() { return new IntermediateModelBuilder(models).build(); } + public static C2jModels awsJsonServiceC2jModels() { + File serviceModel = new File(ClientTestModels.class.getResource("client/c2j/json/service-2.json").getFile()); + File customizationModel = new File(ClientTestModels.class.getResource("client/c2j/json/customization.config").getFile()); + File paginatorsModel = new File(ClientTestModels.class.getResource("client/c2j/json/paginators.json").getFile()); + return C2jModels.builder() + .serviceModel(getServiceModel(serviceModel)) + .customizationConfig(getCustomizationConfig(customizationModel)) + .paginatorsModel(getPaginatorsModel(paginatorsModel)) + .build(); + } + public static IntermediateModel cborServiceModels() { File serviceModel = new File(ClientTestModels.class.getResource("client/c2j/json/service-2.json").getFile()); File customizationModel = new File(ClientTestModels.class.getResource("client/c2j/cbor/customization.config").getFile()); diff --git a/codegen/src/test/java/software/amazon/awssdk/codegen/validation/SharedModelsValidatorTest.java b/codegen/src/test/java/software/amazon/awssdk/codegen/validation/SharedModelsValidatorTest.java new file mode 100644 index 000000000000..a485956d94bc --- /dev/null +++ b/codegen/src/test/java/software/amazon/awssdk/codegen/validation/SharedModelsValidatorTest.java @@ -0,0 +1,148 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.codegen.validation; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.codegen.model.intermediate.IntermediateModel; +import software.amazon.awssdk.codegen.model.intermediate.MemberModel; +import software.amazon.awssdk.codegen.model.intermediate.Metadata; +import software.amazon.awssdk.codegen.model.intermediate.ShapeModel; +import software.amazon.awssdk.codegen.poet.ClientTestModels; + +public class SharedModelsValidatorTest { + private final ModelValidator validator = new SharedModelsValidator(); + + @Test + void validateModels_noTargetService_noValidationErrors() { + assertThat(runValidation(ClientTestModels.awsJsonServiceModels(), null)).isEmpty(); + } + + @Test + void validateModels_targetServiceTriviallyIdentical_noValidationErrors() { + assertThat(runValidation(ClientTestModels.awsJsonServiceModels(), ClientTestModels.awsJsonServiceModels())).isEmpty(); + } + + @Test + void validateModels_noSharedShapes_noValidationErrors() { + IntermediateModel target = ClientTestModels.awsJsonServiceModels(); + Map renamedShapes = target.getShapes() + .entrySet() + .stream() + .collect(Collectors.toMap(e -> "Copy" + e.getKey(), Map.Entry::getValue)); + target.setShapes(renamedShapes); + + assertThat(runValidation(ClientTestModels.awsJsonServiceModels(), target)).isEmpty(); + } + + @Test + void validateModels_sharedShapesNotIdentical_emitsValidationError() { + IntermediateModel target = ClientTestModels.awsJsonServiceModels(); + Map modifiedShapes = target.getShapes() + .entrySet() + .stream() + .collect(Collectors.toMap(Map.Entry::getKey, + e -> { + ShapeModel shapeModel = e.getValue(); + shapeModel.setDeprecated(!shapeModel.isDeprecated()); + return shapeModel; + })); + + target.setShapes(modifiedShapes); + + List validationEntries = runValidation(ClientTestModels.awsJsonServiceModels(), target); + + assertThat(validationEntries).hasSize(modifiedShapes.size()); + + assertThat(validationEntries).allMatch(e -> e.getErrorId() == ValidationErrorId.SHARED_MODELS_DIFFER + && e.getSeverity() == ValidationErrorSeverity.DANGER); + } + + @Test + void validateModels_shapesDontHaveSameMemberNames_emitsValidationError() { + IntermediateModel fooService = new IntermediateModel(); + fooService.setMetadata(new Metadata().withServiceName("Foo")); + + IntermediateModel barService = new IntermediateModel(); + barService.setMetadata(new Metadata().withServiceName("Bar")); + + String shapeName = "TestShape"; + + ShapeModel shape1 = new ShapeModel(); + MemberModel member1 = new MemberModel(); + member1.setName("Shape1Member"); + shape1.setMembers(Arrays.asList(member1)); + + ShapeModel shape2 = new ShapeModel(); + MemberModel member2 = new MemberModel(); + member2.setName("Shape2Member"); + shape2.setMembers(Arrays.asList(member2)); + + Map fooServiceShapes = new HashMap<>(); + fooServiceShapes.put(shapeName, shape1); + fooService.setShapes(fooServiceShapes); + + Map barServiceShapes = new HashMap<>(); + barServiceShapes.put(shapeName, shape2); + barService.setShapes(barServiceShapes); + + List validationEntries = runValidation(fooService, barService); + + assertThat(validationEntries).hasSize(1); + } + + @Test + void validateModels_shapesDontHaveSameMembers_emitsValidationError() { + IntermediateModel fooService = new IntermediateModel(); + fooService.setMetadata(new Metadata().withServiceName("Foo")); + + IntermediateModel barService = new IntermediateModel(); + barService.setMetadata(new Metadata().withServiceName("Bar")); + + String shapeName = "TestShape"; + ShapeModel shape1 = new ShapeModel(); + + ShapeModel shape2 = new ShapeModel(); + shape2.setMembers(Arrays.asList(new MemberModel(), new MemberModel())); + + Map fooServiceShapes = new HashMap<>(); + fooServiceShapes.put(shapeName, shape1); + fooService.setShapes(fooServiceShapes); + + Map barServiceShapes = new HashMap<>(); + barServiceShapes.put(shapeName, shape2); + barService.setShapes(barServiceShapes); + + List validationEntries = runValidation(fooService, barService); + + assertThat(validationEntries).hasSize(1); + } + + private List runValidation(IntermediateModel m1, IntermediateModel m2) { + ModelValidationContext ctx = ModelValidationContext.builder() + .intermediateModel(m1) + .shareModelsTarget(m2) + .build(); + + return validator.validateModels(ctx); + } +} diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/incorrect-endpoint-tests.json b/codegen/src/test/resources/software/amazon/awssdk/codegen/incorrect-endpoint-tests.json new file mode 100644 index 000000000000..861ba12cf3c5 --- /dev/null +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/incorrect-endpoint-tests.json @@ -0,0 +1,26 @@ +{ + "testCases": [ + { + "documentation": "Test references undefined operation member", + "expect": { + "error": "Some error" + }, + "operationInputs": [ + { + "builtInParams": { + "AWS::Region": "us-east-1" + }, + "operationName": "APostOperation", + "operationParams": { + "Foo": "bar" + } + } + ], + "params": { + "Region": "us-east-1", + "UseFIPS": false, + "UseDualStack": false + } + } + ] +} \ No newline at end of file diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/no-request-uri-operation-service.json b/codegen/src/test/resources/software/amazon/awssdk/codegen/no-request-uri-operation-service.json new file mode 100644 index 000000000000..d7caffad37e0 --- /dev/null +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/no-request-uri-operation-service.json @@ -0,0 +1,43 @@ +{ + "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": { + "OperationWithUriMappedParam": { + "name": "OperationWithUriMappedParam", + "http": { + "method": "GET" + }, + "input": { + "shape": "OperationWithUriMappedParamRequest" + } + } + }, + "shapes": { + "OperationWithUriMappedParamRequest": { + "type": "structure", + "members": { + "StringMember": { + "shape": "String", + "location": "uri", + "locationName": "stringMember" + } + } + }, + "String": { + "type": "string" + } + }, + "documentation": "A service that is implemented using the rest-json protocol" +}