diff --git a/smithy-aws-apigateway-openapi/src/main/java/software/amazon/smithy/aws/apigateway/openapi/AddCorsResponseHeaders.java b/smithy-aws-apigateway-openapi/src/main/java/software/amazon/smithy/aws/apigateway/openapi/AddCorsResponseHeaders.java index 74aede9c2b7..cdbdcde755c 100644 --- a/smithy-aws-apigateway-openapi/src/main/java/software/amazon/smithy/aws/apigateway/openapi/AddCorsResponseHeaders.java +++ b/smithy-aws-apigateway-openapi/src/main/java/software/amazon/smithy/aws/apigateway/openapi/AddCorsResponseHeaders.java @@ -1,5 +1,5 @@ /* - * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * Copyright 2020 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. @@ -17,12 +17,14 @@ import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.logging.Logger; import software.amazon.smithy.jsonschema.Schema; import software.amazon.smithy.model.shapes.OperationShape; import software.amazon.smithy.model.traits.CorsTrait; import software.amazon.smithy.model.traits.Trait; import software.amazon.smithy.openapi.fromsmithy.Context; +import software.amazon.smithy.openapi.model.OperationObject; import software.amazon.smithy.openapi.model.ParameterObject; import software.amazon.smithy.openapi.model.Ref; import software.amazon.smithy.openapi.model.ResponseObject; @@ -48,25 +50,43 @@ public List<ApiGatewayConfig.ApiType> getApiTypes() { } @Override - public ResponseObject updateResponse( + public OperationObject postProcessOperation( Context<? extends Trait> context, OperationShape shape, - String status, + OperationObject operation, String method, - String path, - ResponseObject response + String path ) { return context.getService().getTrait(CorsTrait.class) - .map(corsTrait -> addCorsHeadersToResponse(context, shape, response, corsTrait, method)) - .orElse(response); + .map(trait -> addCorsHeadersToResponses(context, shape, operation, method, trait)) + .orElse(operation); } - private ResponseObject addCorsHeadersToResponse( + private OperationObject addCorsHeadersToResponses( + Context<? extends Trait> context, + OperationShape shape, + OperationObject operationObject, + String method, + CorsTrait trait + ) { + OperationObject.Builder builder = operationObject.toBuilder(); + + for (Map.Entry<String, ResponseObject> entry : operationObject.getResponses().entrySet()) { + ResponseObject updated = createUpdatedResponseWithCorsHeaders( + context, shape, operationObject, method, trait, entry.getValue()); + builder.putResponse(entry.getKey(), updated); + } + + return builder.build(); + } + + private ResponseObject createUpdatedResponseWithCorsHeaders( Context<? extends Trait> context, OperationShape operation, - ResponseObject response, - CorsTrait corsTrait, - String method + OperationObject operationObject, + String method, + CorsTrait trait, + ResponseObject response ) { // Determine which headers have been added to the response. List<String> headers = new ArrayList<>(); @@ -108,7 +128,7 @@ private ResponseObject addCorsHeadersToResponse( // An HTTP response to a CORS request that is *not* a // CORS-preflight request can also include the following header: // `Access-Control-Expose-Headers` - if (!CorsHeader.deduceOperationHeaders(context, operation, corsTrait).isEmpty()) { + if (!CorsHeader.deduceOperationResponseHeaders(context, operationObject, operation, trait).isEmpty()) { headers.add(CorsHeader.EXPOSE_HEADERS.toString()); } diff --git a/smithy-aws-apigateway-openapi/src/main/java/software/amazon/smithy/aws/apigateway/openapi/AddIntegrations.java b/smithy-aws-apigateway-openapi/src/main/java/software/amazon/smithy/aws/apigateway/openapi/AddIntegrations.java index 859b1300457..37f680091d0 100644 --- a/smithy-aws-apigateway-openapi/src/main/java/software/amazon/smithy/aws/apigateway/openapi/AddIntegrations.java +++ b/smithy-aws-apigateway-openapi/src/main/java/software/amazon/smithy/aws/apigateway/openapi/AddIntegrations.java @@ -1,5 +1,5 @@ /* - * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * Copyright 2020 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. @@ -74,7 +74,7 @@ public OperationObject updateOperation( IntegrationTraitIndex index = IntegrationTraitIndex.of(context.getModel()); return index.getIntegrationTrait(context.getService(), shape) .map(trait -> operation.toBuilder() - .putExtension(EXTENSION_NAME, createIntegration(context, shape, trait)) + .putExtension(EXTENSION_NAME, createIntegration(context, operation, shape, trait)) .build()) .orElseGet(() -> { LOGGER.warning("No API Gateway integration trait found for " + shape.getId()); @@ -84,6 +84,7 @@ public OperationObject updateOperation( private ObjectNode createIntegration( Context<? extends Trait> context, + OperationObject operationObject, OperationShape shape, Trait integration ) { @@ -91,7 +92,7 @@ private ObjectNode createIntegration( return context.getService().getTrait(CorsTrait.class) .map(cors -> { LOGGER.fine(() -> String.format("Adding CORS to `%s` operation responses", shape.getId())); - return updateIntegrationWithCors(context, shape, integrationObject, cors); + return updateIntegrationWithCors(context, operationObject, shape, integrationObject, cors); }) .orElse(integrationObject); } @@ -122,6 +123,7 @@ private static ObjectNode getIntegrationAsObject( private ObjectNode updateIntegrationWithCors( Context<? extends Trait> context, + OperationObject operationObject, OperationShape shape, ObjectNode integrationNode, CorsTrait cors @@ -141,7 +143,7 @@ private ObjectNode updateIntegrationWithCors( LOGGER.finer(() -> String.format("Adding the following CORS headers to the API Gateway integration of %s: %s", shape.getId(), corsHeaders)); - Set<String> deducedHeaders = CorsHeader.deduceOperationHeaders(context, shape, cors); + Set<String> deducedHeaders = CorsHeader.deduceOperationResponseHeaders(context, operationObject, shape, cors); LOGGER.fine(() -> String.format("Detected the following headers for operation %s: %s", shape.getId(), deducedHeaders)); diff --git a/smithy-aws-apigateway-openapi/src/main/java/software/amazon/smithy/aws/apigateway/openapi/ApiGatewayMapper.java b/smithy-aws-apigateway-openapi/src/main/java/software/amazon/smithy/aws/apigateway/openapi/ApiGatewayMapper.java index c761f032951..57641b71c6c 100644 --- a/smithy-aws-apigateway-openapi/src/main/java/software/amazon/smithy/aws/apigateway/openapi/ApiGatewayMapper.java +++ b/smithy-aws-apigateway-openapi/src/main/java/software/amazon/smithy/aws/apigateway/openapi/ApiGatewayMapper.java @@ -112,6 +112,18 @@ public OperationObject updateOperation( : operation; } + @Override + public OperationObject postProcessOperation( + Context<? extends Trait> context, + OperationShape shape, + OperationObject operation, + String httpMethodName, String path + ) { + return matchesApiType(context) + ? delegate.postProcessOperation(context, shape, operation, httpMethodName, path) + : operation; + } + @Override public PathItem updatePathItem(Context<? extends Trait> context, String path, PathItem pathItem) { return matchesApiType(context) diff --git a/smithy-aws-apigateway-openapi/src/main/java/software/amazon/smithy/aws/apigateway/openapi/CorsHeader.java b/smithy-aws-apigateway-openapi/src/main/java/software/amazon/smithy/aws/apigateway/openapi/CorsHeader.java index c1dc21b90ba..b87de285ca9 100644 --- a/smithy-aws-apigateway-openapi/src/main/java/software/amazon/smithy/aws/apigateway/openapi/CorsHeader.java +++ b/smithy-aws-apigateway-openapi/src/main/java/software/amazon/smithy/aws/apigateway/openapi/CorsHeader.java @@ -1,5 +1,5 @@ /* - * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * Copyright 2020 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. @@ -22,6 +22,8 @@ import software.amazon.smithy.model.traits.Trait; import software.amazon.smithy.openapi.fromsmithy.Context; import software.amazon.smithy.openapi.fromsmithy.SecuritySchemeConverter; +import software.amazon.smithy.openapi.model.OperationObject; +import software.amazon.smithy.openapi.model.ResponseObject; enum CorsHeader { @@ -43,8 +45,9 @@ public String toString() { return headerName; } - static <T extends Trait> Set<String> deduceOperationHeaders( + static <T extends Trait> Set<String> deduceOperationResponseHeaders( Context<T> context, + OperationObject operationObject, OperationShape shape, CorsTrait cors ) { @@ -58,6 +61,11 @@ static <T extends Trait> Set<String> deduceOperationHeaders( result.addAll(getSecuritySchemeResponseHeaders(context, converter)); } + // Include all headers found in the generated OpenAPI response. + for (ResponseObject responseObject : operationObject.getResponses().values()) { + result.addAll(responseObject.getHeaders().keySet()); + } + return result; } diff --git a/smithy-aws-apigateway-openapi/src/test/resources/software/amazon/smithy/aws/apigateway/openapi/cors-explicit-options.openapi.json b/smithy-aws-apigateway-openapi/src/test/resources/software/amazon/smithy/aws/apigateway/openapi/cors-explicit-options.openapi.json index 591d59c1e73..1d42e983e78 100644 --- a/smithy-aws-apigateway-openapi/src/test/resources/software/amazon/smithy/aws/apigateway/openapi/cors-explicit-options.openapi.json +++ b/smithy-aws-apigateway-openapi/src/test/resources/software/amazon/smithy/aws/apigateway/openapi/cors-explicit-options.openapi.json @@ -63,7 +63,8 @@ "default": { "statusCode": "200", "responseParameters": { - "method.response.header.Access-Control-Allow-Origin": "'https://foo.com'" + "method.response.header.Access-Control-Allow-Origin": "'https://foo.com'", + "method.response.header.Access-Control-Expose-Headers": "'X-Hd'" } } } diff --git a/smithy-aws-apigateway-openapi/src/test/resources/software/amazon/smithy/aws/apigateway/openapi/cors-model.openapi.json b/smithy-aws-apigateway-openapi/src/test/resources/software/amazon/smithy/aws/apigateway/openapi/cors-model.openapi.json index cbd7e4d8357..6b03c019cb6 100644 --- a/smithy-aws-apigateway-openapi/src/test/resources/software/amazon/smithy/aws/apigateway/openapi/cors-model.openapi.json +++ b/smithy-aws-apigateway-openapi/src/test/resources/software/amazon/smithy/aws/apigateway/openapi/cors-model.openapi.json @@ -199,7 +199,7 @@ "default": { "statusCode": "200", "responseParameters": { - "method.response.header.Access-Control-Expose-Headers": "'X-Service-Output-Metadata'", + "method.response.header.Access-Control-Expose-Headers": "'X-Foo-Header,X-Service-Output-Metadata'", "method.response.header.Access-Control-Allow-Origin": "'https://www.example.com'" } } diff --git a/smithy-aws-apigateway-openapi/src/test/resources/software/amazon/smithy/aws/apigateway/openapi/cors-with-additional-headers.openapi.json b/smithy-aws-apigateway-openapi/src/test/resources/software/amazon/smithy/aws/apigateway/openapi/cors-with-additional-headers.openapi.json index b68d0b78c1e..73af2a4cde2 100644 --- a/smithy-aws-apigateway-openapi/src/test/resources/software/amazon/smithy/aws/apigateway/openapi/cors-with-additional-headers.openapi.json +++ b/smithy-aws-apigateway-openapi/src/test/resources/software/amazon/smithy/aws/apigateway/openapi/cors-with-additional-headers.openapi.json @@ -199,7 +199,7 @@ "default": { "statusCode": "200", "responseParameters": { - "method.response.header.Access-Control-Expose-Headers": "'X-Service-Output-Metadata'", + "method.response.header.Access-Control-Expose-Headers": "'X-Foo-Header,X-Service-Output-Metadata'", "method.response.header.Access-Control-Allow-Origin": "'https://www.example.com'" } } diff --git a/smithy-aws-apigateway-openapi/src/test/resources/software/amazon/smithy/aws/apigateway/openapi/cors-with-custom-gateway-response-headers.openapi.json b/smithy-aws-apigateway-openapi/src/test/resources/software/amazon/smithy/aws/apigateway/openapi/cors-with-custom-gateway-response-headers.openapi.json index e604a632e94..3ced368ac52 100644 --- a/smithy-aws-apigateway-openapi/src/test/resources/software/amazon/smithy/aws/apigateway/openapi/cors-with-custom-gateway-response-headers.openapi.json +++ b/smithy-aws-apigateway-openapi/src/test/resources/software/amazon/smithy/aws/apigateway/openapi/cors-with-custom-gateway-response-headers.openapi.json @@ -63,7 +63,8 @@ "default": { "statusCode": "200", "responseParameters": { - "method.response.header.Access-Control-Allow-Origin": "'https://foo.com'" + "method.response.header.Access-Control-Allow-Origin": "'https://foo.com'", + "method.response.header.Access-Control-Expose-Headers": "'X-Hd'" } } } diff --git a/smithy-openapi/src/main/java/software/amazon/smithy/openapi/fromsmithy/OpenApiConverter.java b/smithy-openapi/src/main/java/software/amazon/smithy/openapi/fromsmithy/OpenApiConverter.java index 2fae2eb2de8..b681ac9c408 100644 --- a/smithy-openapi/src/main/java/software/amazon/smithy/openapi/fromsmithy/OpenApiConverter.java +++ b/smithy-openapi/src/main/java/software/amazon/smithy/openapi/fromsmithy/OpenApiConverter.java @@ -426,10 +426,12 @@ private <T extends Trait> void addPaths( String method = result.getMethod(); String path = result.getUri(); PathItem.Builder pathItem = paths.computeIfAbsent(result.getUri(), (uri) -> PathItem.builder()); + // Mark the operation deprecated if the trait's present. if (shape.hasTrait(DeprecatedTrait.class)) { result.getOperation().deprecated(true); } + // Add security requirements to the operation. addOperationSecurity(context, result.getOperation(), shape, plugin); @@ -438,9 +440,10 @@ private <T extends Trait> void addPaths( .map(DocumentationTrait::getValue) .ifPresent(description -> result.getOperation().description(description)); - // Pass the operation through the plugin system and then build it. - OperationObject builtOperation = plugin.updateOperation( - context, shape, result.getOperation().build(), method, path); + OperationObject builtOperation = result.getOperation().build(); + + // Pass the operation through the plugin system. + builtOperation = plugin.updateOperation(context, shape, builtOperation, method, path); // Add tags that are on the operation. builtOperation = addOperationTags(context, shape, builtOperation); // Update each parameter of the operation and rebuild if necessary. @@ -449,6 +452,8 @@ private <T extends Trait> void addPaths( builtOperation = updateResponses(context, shape, builtOperation, method, path, plugin); // Update the request body of the operation and rebuild if necessary. builtOperation = updateRequestBody(context, shape, builtOperation, method, path, plugin); + // Pass the operation through the plugin system for post-processing. + builtOperation = plugin.postProcessOperation(context, shape, builtOperation, method, path); switch (method.toLowerCase(Locale.ENGLISH)) { case "get": diff --git a/smithy-openapi/src/main/java/software/amazon/smithy/openapi/fromsmithy/OpenApiMapper.java b/smithy-openapi/src/main/java/software/amazon/smithy/openapi/fromsmithy/OpenApiMapper.java index fe128f399bf..1f28bf7e1a5 100644 --- a/smithy-openapi/src/main/java/software/amazon/smithy/openapi/fromsmithy/OpenApiMapper.java +++ b/smithy-openapi/src/main/java/software/amazon/smithy/openapi/fromsmithy/OpenApiMapper.java @@ -71,7 +71,10 @@ default byte getOrder() { default void updateDefaultSettings(Model model, OpenApiConfig config) {} /** - * Updates an operation. + * Updates an operation before invoking the plugin system on the contents + * of the operation (specifically, before {@link #updateParameter}, + * {@link #updateRequestBody}, {@link #updateResponse}, + * {@link #updateRequestBody}, and {@link #postProcessOperation}). * * @param context Conversion context. * @param shape Operation being converted. @@ -90,6 +93,29 @@ default OperationObject updateOperation( return operation; } + /** + * Updates an operation after invoking the plugin system on the contents + * of the operation (specifically, after {@link #updateOperation}, + * {@link #updateParameter}, {@link #updateRequestBody}, + * {@link #updateResponse}, and {@link #updateRequestBody}). + * + * @param context Conversion context. + * @param shape Operation being converted. + * @param operation OperationObject being built. + * @param httpMethodName The HTTP method of the operation. + * @param path The HTTP URI of the operation. + * @return Returns the updated operation object. + */ + default OperationObject postProcessOperation( + Context<? extends Trait> context, + OperationShape shape, + OperationObject operation, + String httpMethodName, + String path + ) { + return operation; + } + /** * Updates a path item. * @@ -269,6 +295,23 @@ public OperationObject updateOperation( return operation; } + @Override + public OperationObject postProcessOperation( + Context<? extends Trait> context, + OperationShape shape, + OperationObject operation, + String httpMethodName, + String path + ) { + for (OpenApiMapper plugin : sorted) { + if (operation == null) { + return null; + } + operation = plugin.postProcessOperation(context, shape, operation, httpMethodName, path); + } + return operation; + } + @Override public PathItem updatePathItem(Context<? extends Trait> context, String path, PathItem pathItem) { for (OpenApiMapper plugin : sorted) {