Repository: smithy-lang/smithy
Pull request #1916 (2023-08-10 21:48:40)

Add versioning for API Gateway defaults
This PR adds versioning to manage API Gateway defaults.

By default, API Gateway conversions will use defaults of version `2023-08-11` which will set the following OpenAPI config options:
* alphanumericOnlyRefs: true
* disabledFeatures: [default]
* disableDefaultValues: true
* disableIntegerFormat: true

All of these can be disabled by setting `apiGatewayDefaults` to `disabled` in smithy-build.json:
```
{
    "version": "1.0",
    "plugins": {
        "openapi": {
            "service": "example.weather#Weather",
            "protocol": "aws.protocols#restJson1",
            "apiGatewayDefaults": "DISABLED"
        }
    }
}
```

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.



Patch:
diff --git a/docs/source-2.0/guides/converting-to-openapi.rst b/docs/source-2.0/guides/converting-to-openapi.rst
index 56819cfa3..2095436f3 100644
--- a/docs/source-2.0/guides/converting-to-openapi.rst
+++ b/docs/source-2.0/guides/converting-to-openapi.rst
@@ -1176,6 +1176,40 @@ dependency on ``software.amazon.smithy:smithy-aws-apigateway-openapi``.
 Amazon API Gateway configuration settings
 =========================================
 
+apiGatewayDefaults (``string``)
+    Sets recommended default configuration settings and allows for those defaults
+    to be disabled.
+
+    This setting can be set to one of the following:
+
+    * ``2023-08-11`` Set the defaults described below.
+    * ``DISABLED`` Disables setting defaults.
+
+    The ``2023-08-11`` version sets the following configuration settings:
+
+    * :ref:`alphanumericOnlyRefs <generate-openapi-jsonschema-setting-alphanumericOnlyRefs>`: ``true``
+    * :ref:`disableDefaultValues <generate-openapi-setting-disableDefaultValues>`: ``true``
+    * :ref:`disableIntegerFormat <generate-openapi-setting-disableIntegerFormat>`: ``true``
+    * :ref:`disableFeatures <generate-openapi-jsonschema-setting-disableFeatures>`: ``["default"]``
+
+    .. important::
+
+        This setting should be set explicitly to one of the allowed values.
+        If omitted, it will default to ``2023-08-11``.
+
+    .. code-block:: json
+        :caption: smithy-build.json
+
+        {
+            "version": "1.0",
+            "plugins": {
+                "openapi": {
+                    "service": "example.weather#Weather",
+                    "apiGatewayDefaults": "2023-08-11"
+                }
+            }
+        }
+
 apiGatewayType (``string``)
     Defines the type of API Gateway to define in the generated OpenAPI model.
     This setting influences which API Gateway specific plugins apply
diff --git a/smithy-aws-apigateway-openapi/src/main/java/software/amazon/smithy/aws/apigateway/openapi/AddDefaultConfigSettings.java b/smithy-aws-apigateway-openapi/src/main/java/software/amazon/smithy/aws/apigateway/openapi/AddDefaultConfigSettings.java
index 07e4d34b2..de5bf6117 100644
--- a/smithy-aws-apigateway-openapi/src/main/java/software/amazon/smithy/aws/apigateway/openapi/AddDefaultConfigSettings.java
+++ b/smithy-aws-apigateway-openapi/src/main/java/software/amazon/smithy/aws/apigateway/openapi/AddDefaultConfigSettings.java
@@ -16,30 +16,37 @@
 package software.amazon.smithy.aws.apigateway.openapi;
 
 import java.util.List;
+import java.util.logging.Logger;
 import software.amazon.smithy.model.Model;
 import software.amazon.smithy.openapi.OpenApiConfig;
 
 /**
- * Disables OpenAPI and JSON Schema features not supported by API Gateway.
+ * Sets default config settings for API Gateway.
+ *
+ * <p>By default, this disables OpenAPI and JSON Schema features not
+ * supported by API Gateway.</p>
  *
  * <p>API Gateway does not allow characters like "_". API Gateway
  * doesn't support the "default" trait or `int32` or `int64` "format"
  * values.
  */
 final class AddDefaultConfigSettings implements ApiGatewayMapper {
+    private static final Logger LOGGER = Logger.getLogger(AddDefaultConfigSettings.class.getName());
+    private static final ApiGatewayDefaults DEFAULT_VERSION = ApiGatewayDefaults.VERSION_2023_08_11;
+
     @Override
     public List<ApiGatewayConfig.ApiType> getApiTypes() {
         return null;
     }
 
     @Override
-    public void updateDefaultSettings(Model model, OpenApiConfig config) {
-        config.setAlphanumericOnlyRefs(true);
-        config.getDisableFeatures().add("default");
-        config.setDisableDefaultValues(true);
-        // If the `useIntegerType` config has been set, this assures that
-        // `int32` and `int64` formats are not set on those integer types.
-        // See https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-known-issues.html
-        config.setDisableIntegerFormat(true);
+    public void updateDefaultSettings(Model model, OpenApiConfig openApiConfig) {
+        ApiGatewayConfig config = openApiConfig.getExtensions(ApiGatewayConfig.class);
+        if (config.getApiGatewayDefaults() == null) {
+            LOGGER.warning(String.format("`apiGatewayDefaults` configuration not set for opeanapi plugin. Assuming %s.",
+                    DEFAULT_VERSION));
+            config.setApiGatewayDefaults(DEFAULT_VERSION);
+        }
+        config.getApiGatewayDefaults().setDefaults(openApiConfig);
     }
 }
diff --git a/smithy-aws-apigateway-openapi/src/main/java/software/amazon/smithy/aws/apigateway/openapi/ApiGatewayConfig.java b/smithy-aws-apigateway-openapi/src/main/java/software/amazon/smithy/aws/apigateway/openapi/ApiGatewayConfig.java
index 6385ba83b..2a985095f 100644
--- a/smithy-aws-apigateway-openapi/src/main/java/software/amazon/smithy/aws/apigateway/openapi/ApiGatewayConfig.java
+++ b/smithy-aws-apigateway-openapi/src/main/java/software/amazon/smithy/aws/apigateway/openapi/ApiGatewayConfig.java
@@ -60,6 +60,7 @@ public final class ApiGatewayConfig {
     private ApiType apiGatewayType = ApiType.REST;
     private boolean disableCloudFormationSubstitution;
     private Set<String> additionalAllowedCorsHeaders = Collections.emptySet();
+    private ApiGatewayDefaults apiGatewayDefaults;
 
     /**
      * @return Returns true if CloudFormation substitutions are disabled.
@@ -120,4 +121,12 @@ public final class ApiGatewayConfig {
     public void setAdditionalAllowedCorsHeaders(Collection<String> additionalAllowedCorsHeaders) {
         this.additionalAllowedCorsHeaders = SetUtils.caseInsensitiveCopyOf(additionalAllowedCorsHeaders);
     }
+
+    public ApiGatewayDefaults getApiGatewayDefaults() {
+        return this.apiGatewayDefaults;
+    }
+
+    public void setApiGatewayDefaults(ApiGatewayDefaults apiGatewayDefaults) {
+        this.apiGatewayDefaults = apiGatewayDefaults;
+    }
 }
diff --git a/smithy-aws-apigateway-openapi/src/main/java/software/amazon/smithy/aws/apigateway/openapi/ApiGatewayDefaults.java b/smithy-aws-apigateway-openapi/src/main/java/software/amazon/smithy/aws/apigateway/openapi/ApiGatewayDefaults.java
new file mode 100644
index 000000000..f97fe67ee
--- /dev/null
+++ b/smithy-aws-apigateway-openapi/src/main/java/software/amazon/smithy/aws/apigateway/openapi/ApiGatewayDefaults.java
@@ -0,0 +1,51 @@
+/*
+ * 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.smithy.aws.apigateway.openapi;
+
+import software.amazon.smithy.openapi.OpenApiConfig;
+import software.amazon.smithy.utils.SetUtils;
+import software.amazon.smithy.utils.SmithyUnstableApi;
+
+public enum ApiGatewayDefaults {
+    VERSION_2023_08_11("2023-08-11") {
+        @Override
+        public void setDefaults(OpenApiConfig config) {
+            config.setAlphanumericOnlyRefs(true);
+            config.setDisableDefaultValues(true);
+            config.setDisableIntegerFormat(true);
+            config.setDisableFeatures(SetUtils.of("default"));
+        }
+    },
+    DISABLED("DISABLED") {
+        @Override
+        public void setDefaults(OpenApiConfig config) {
+        }
+    };
+
+    private final String version;
+
+    ApiGatewayDefaults(String version) {
+        this.version = version;
+    }
+
+    @Override
+    public String toString() {
+        return version;
+    }
+
+    @SmithyUnstableApi
+    public abstract void setDefaults(OpenApiConfig config);
+}


Test patch:
diff --git a/smithy-aws-apigateway-openapi/src/test/java/software/amazon/smithy/aws/apigateway/openapi/AddDefaultConfigSettingsTest.java b/smithy-aws-apigateway-openapi/src/test/java/software/amazon/smithy/aws/apigateway/openapi/AddDefaultConfigSettingsTest.java
index a026afd5a..45f1f6ea8 100644
--- a/smithy-aws-apigateway-openapi/src/test/java/software/amazon/smithy/aws/apigateway/openapi/AddDefaultConfigSettingsTest.java
+++ b/smithy-aws-apigateway-openapi/src/test/java/software/amazon/smithy/aws/apigateway/openapi/AddDefaultConfigSettingsTest.java
@@ -1,13 +1,9 @@
 package software.amazon.smithy.aws.apigateway.openapi;
 
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.not;
-
-import java.util.Optional;
 import org.junit.jupiter.api.Test;
 import software.amazon.smithy.model.Model;
 import software.amazon.smithy.model.node.Node;
-import software.amazon.smithy.model.node.NodePointer;
+import software.amazon.smithy.model.node.NodeMapper;
 import software.amazon.smithy.model.node.ObjectNode;
 import software.amazon.smithy.model.shapes.ShapeId;
 import software.amazon.smithy.openapi.OpenApiConfig;
@@ -16,38 +12,66 @@ import software.amazon.smithy.utils.IoUtils;
 
 public class AddDefaultConfigSettingsTest {
     @Test
-    public void addsDefaultConfigSettings() {
+    public void defaultsTo2023_08_11() {
         Model model = Model.assembler()
                 .discoverModels(getClass().getClassLoader())
-                .addImport(getClass().getResource("alphanumeric-only.json"))
+                .addImport(getClass().getResource("default-config-settings.smithy"))
                 .assemble()
                 .unwrap();
 
         OpenApiConfig config = new OpenApiConfig();
         config.setService(ShapeId.from("example.smithy#MyService"));
+        config.setUseIntegerType(true);
         ObjectNode result = OpenApiConverter.create()
                 .config(config)
                 .convertToNode(model);
 
-        // Ensure that Foo_Baz became FooBaz.
-        NodePointer pointer = NodePointer.parse("/components/schemas/FooBaz");
-        assertThat(pointer.getValue(result), not(Optional.empty()));
+        Node.assertEquals(result, Node.parse(IoUtils.readUtf8Resource(getClass(), "2023-08-11.openapi.json")));
     }
 
     @Test
-    public void omitsDefaultTraits() {
+    public void usesVersion2023_08_11() {
         Model model = Model.assembler()
                 .discoverModels(getClass().getClassLoader())
-                .addImport(getClass().getResource("omits-default-trait.smithy"))
+                .addImport(getClass().getResource("default-config-settings.smithy"))
                 .assemble()
                 .unwrap();
 
-        OpenApiConfig config = new OpenApiConfig();
-        config.setService(ShapeId.from("example.smithy#MyService"));
+        OpenApiConfig openApiConfig = new OpenApiConfig();
+        openApiConfig.setService(ShapeId.from("example.smithy#MyService"));
+        openApiConfig.setUseIntegerType(true);
+
+        ApiGatewayConfig config = new ApiGatewayConfig();
+        NodeMapper mapper = new NodeMapper();
+        config.setApiGatewayDefaults(ApiGatewayDefaults.VERSION_2023_08_11);
+        openApiConfig.setExtensions(mapper.serialize(config).expectObjectNode());
         ObjectNode result = OpenApiConverter.create()
-                .config(config)
+                .config(openApiConfig)
+                .convertToNode(model);
+
+        Node.assertEquals(result, Node.parse(IoUtils.readUtf8Resource(getClass(), "2023-08-11.openapi.json")));
+    }
+
+    @Test
+    public void canDisableDefaults() {
+        Model model = Model.assembler()
+                .discoverModels(getClass().getClassLoader())
+                .addImport(getClass().getResource("default-config-settings.smithy"))
+                .assemble()
+                .unwrap();
+
+        OpenApiConfig openApiConfig = new OpenApiConfig();
+        openApiConfig.setService(ShapeId.from("example.smithy#MyService"));
+        openApiConfig.setUseIntegerType(true);
+
+        ApiGatewayConfig config = new ApiGatewayConfig();
+        NodeMapper mapper = new NodeMapper();
+        config.setApiGatewayDefaults(ApiGatewayDefaults.DISABLED);
+        openApiConfig.setExtensions(mapper.serialize(config).expectObjectNode());
+        ObjectNode result = OpenApiConverter.create()
+                .config(openApiConfig)
                 .convertToNode(model);
 
-        Node.assertEquals(result, Node.parse(IoUtils.readUtf8Resource(getClass(), "omits-default-trait.openapi.json")));
+        Node.assertEquals(result, Node.parse(IoUtils.readUtf8Resource(getClass(), "disabled-defaults.openapi.json")));
     }
 }
diff --git a/smithy-aws-apigateway-openapi/src/test/java/software/amazon/smithy/aws/apigateway/openapi/AddDefaultRestConfigSettingsTest.java b/smithy-aws-apigateway-openapi/src/test/java/software/amazon/smithy/aws/apigateway/openapi/AddDefaultRestConfigSettingsTest.java
index 2cc154db2..bd47ad195 100644
--- a/smithy-aws-apigateway-openapi/src/test/java/software/amazon/smithy/aws/apigateway/openapi/AddDefaultRestConfigSettingsTest.java
+++ b/smithy-aws-apigateway-openapi/src/test/java/software/amazon/smithy/aws/apigateway/openapi/AddDefaultRestConfigSettingsTest.java
@@ -1,15 +1,8 @@
 package software.amazon.smithy.aws.apigateway.openapi;
 
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.equalTo;
-import static org.hamcrest.Matchers.not;
-
-import java.util.Optional;
-import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
 import software.amazon.smithy.model.Model;
 import software.amazon.smithy.model.node.Node;
-import software.amazon.smithy.model.node.NodePointer;
 import software.amazon.smithy.model.node.ObjectNode;
 import software.amazon.smithy.model.shapes.ShapeId;
 import software.amazon.smithy.openapi.OpenApiConfig;
diff --git a/smithy-aws-apigateway-openapi/src/test/resources/software/amazon/smithy/aws/apigateway/openapi/omits-default-trait.openapi.json b/smithy-aws-apigateway-openapi/src/test/resources/software/amazon/smithy/aws/apigateway/openapi/2023-08-11.openapi.json
similarity index 88%
rename from smithy-aws-apigateway-openapi/src/test/resources/software/amazon/smithy/aws/apigateway/openapi/omits-default-trait.openapi.json
rename to smithy-aws-apigateway-openapi/src/test/resources/software/amazon/smithy/aws/apigateway/openapi/2023-08-11.openapi.json
index 8cdb9e15c..ddca16c5c 100644
--- a/smithy-aws-apigateway-openapi/src/test/resources/software/amazon/smithy/aws/apigateway/openapi/omits-default-trait.openapi.json
+++ b/smithy-aws-apigateway-openapi/src/test/resources/software/amazon/smithy/aws/apigateway/openapi/2023-08-11.openapi.json
@@ -41,6 +41,9 @@
                     "BAR"
                 ]
             },
+            "FooBaz": {
+                "type": "object"
+            },
             "HasDefaultRequestContent": {
                 "type": "object",
                 "properties": {
@@ -69,6 +72,12 @@
                     },
                     "baz": {
                         "$ref": "#/components/schemas/DefaultEnum"
+                    },
+                    "withAlphaOnlyRef": {
+                        "$ref": "#/components/schemas/FooBaz"
+                    },
+                    "anInt": {
+                        "type": "integer"
                     }
                 }
             }
diff --git a/smithy-aws-apigateway-openapi/src/test/resources/software/amazon/smithy/aws/apigateway/openapi/alphanumeric-only.json b/smithy-aws-apigateway-openapi/src/test/resources/software/amazon/smithy/aws/apigateway/openapi/alphanumeric-only.json
deleted file mode 100644
index ee15e1c86..000000000
--- a/smithy-aws-apigateway-openapi/src/test/resources/software/amazon/smithy/aws/apigateway/openapi/alphanumeric-only.json
+++ /dev/null
@@ -1,43 +0,0 @@
-{
-    "smithy": "2.0",
-    "shapes": {
-        "example.smithy#MyService": {
-            "type": "service",
-            "version": "2006-03-01",
-            "operations": [
-                {
-                    "target": "example.smithy#GetPayload"
-                }
-            ],
-            "traits": {
-                "aws.protocols#restJson1": {},
-                "smithy.api#httpBasicAuth": {}
-            }
-        },
-        "example.smithy#GetPayload": {
-            "type": "operation",
-            "output": {
-                "target": "example.smithy#GetPayloadOutput"
-            },
-            "traits": {
-                "smithy.api#readonly": {},
-                "smithy.api#http": {
-                    "uri": "/test",
-                    "method": "GET"
-                }
-            }
-        },
-        "example.smithy#GetPayloadOutput": {
-            "type": "structure",
-            "members": {
-                "foo": {
-                    "target": "example.smithy#Foo_Baz"
-                }
-            }
-        },
-        "example.smithy#Foo_Baz": {
-            "type": "structure",
-            "members": {}
-        }
-    }
-}
diff --git a/smithy-aws-apigateway-openapi/src/test/resources/software/amazon/smithy/aws/apigateway/openapi/omits-default-trait.smithy b/smithy-aws-apigateway-openapi/src/test/resources/software/amazon/smithy/aws/apigateway/openapi/default-config-settings.smithy
similarity index 86%
rename from smithy-aws-apigateway-openapi/src/test/resources/software/amazon/smithy/aws/apigateway/openapi/omits-default-trait.smithy
rename to smithy-aws-apigateway-openapi/src/test/resources/software/amazon/smithy/aws/apigateway/openapi/default-config-settings.smithy
index cefc45c92..2c013ccc4 100644
--- a/smithy-aws-apigateway-openapi/src/test/resources/software/amazon/smithy/aws/apigateway/openapi/omits-default-trait.smithy
+++ b/smithy-aws-apigateway-openapi/src/test/resources/software/amazon/smithy/aws/apigateway/openapi/default-config-settings.smithy
@@ -20,6 +20,8 @@ operation HasDefault {
         foo: String = ""
         bar: StringList = []
         baz: DefaultEnum = "FOO"
+        withAlphaOnlyRef: Foo_Baz,
+        anInt: Long
     }
 }
 
@@ -31,3 +33,5 @@ enum DefaultEnum {
     FOO
     BAR
 }
+
+structure Foo_Baz {}
diff --git a/smithy-aws-apigateway-openapi/src/test/resources/software/amazon/smithy/aws/apigateway/openapi/disabled-defaults.openapi.json b/smithy-aws-apigateway-openapi/src/test/resources/software/amazon/smithy/aws/apigateway/openapi/disabled-defaults.openapi.json
new file mode 100644
index 000000000..6c8269d63
--- /dev/null
+++ b/smithy-aws-apigateway-openapi/src/test/resources/software/amazon/smithy/aws/apigateway/openapi/disabled-defaults.openapi.json
@@ -0,0 +1,98 @@
+{
+    "openapi": "3.0.2",
+    "info": {
+        "title": "MyService",
+        "version": "2020-07-02"
+    },
+    "paths": {
+        "/defaults": {
+            "post": {
+                "operationId": "HasDefault",
+                "requestBody": {
+                    "content": {
+                        "application/json": {
+                            "schema": {
+                                "$ref": "#/components/schemas/HasDefaultRequestContent"
+                            }
+                        }
+                    }
+                },
+                "responses": {
+                    "200": {
+                        "description": "HasDefault 200 response",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/HasDefaultResponseContent"
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    },
+    "components": {
+        "schemas": {
+            "DefaultEnum": {
+                "type": "string",
+                "enum": [
+                    "FOO",
+                    "BAR"
+                ]
+            },
+            "Foo_Baz": {
+                "type": "object"
+            },
+            "HasDefaultRequestContent": {
+                "type": "object",
+                "properties": {
+                    "foo": {
+                        "type": "string",
+                        "default": ""
+                    },
+                    "bar": {
+                        "type": "array",
+                        "default": [],
+                        "items": {
+                            "type": "string"
+                        }
+                    }
+                }
+            },
+            "HasDefaultResponseContent": {
+                "type": "object",
+                "properties": {
+                    "foo": {
+                        "type": "string",
+                        "default": ""
+                    },
+                    "bar": {
+                        "type": "array",
+                        "default": [],
+                        "items": {
+                            "type": "string"
+                        }
+                    },
+                    "baz": {
+                        "allOf": [
+                            {
+                            "$ref": "#/components/schemas/DefaultEnum"
+                            },
+                            {
+                                "default": "FOO"
+                            }
+                        ]
+                    },
+                    "withAlphaOnlyRef": {
+                        "$ref": "#/components/schemas/Foo_Baz"
+                    },
+                    "anInt": {
+                        "type": "integer",
+                        "format": "int64"
+                    }
+                }
+            }
+        }
+    }
+}