Skip to content

Commit

Permalink
Refactor OpenAPI plugin to use POJOs
Browse files Browse the repository at this point in the history
The OpenAPI plugin has now been refactored to use strongly-typed POJOs
rather than the more dynamic Node-based configuration. This is possible
because of the NodeMapper. Now OpenAPI's config extends from JSON
Schema's config. All configuration prefixes were removed from OpenAPI
keys, but the plugin will handle loading the deprecated form of
"openapi." when parsing configuration settings.

Additional configuration settings used to configure conversion plugins
for either OpenAPI or JSON Schema are contained in the "extensions"
property. This property can be access from the JsonSchemaConfig object
and deserialized into a desired POJO using `getExtensions(Class type)`.
This allows typed access to additional configuration settings along with
validation. Subsequent access to the same type is cached.
  • Loading branch information
mtdowling committed Apr 9, 2020
1 parent 326c505 commit cbf8f2e
Show file tree
Hide file tree
Showing 36 changed files with 936 additions and 623 deletions.
301 changes: 215 additions & 86 deletions docs/source/guides/converting-to-openapi.rst

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* 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.
* 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;

/**
* API Gateway OpenAPI configuration.
*/
public final class ApiGatewayConfig {

private boolean disableCloudFormationSubstitution;

public boolean getDisableCloudFormationSubstitution() {
return disableCloudFormationSubstitution;
}

/**
* Disables CloudFormation substitutions of specific paths when they contain
* ${} placeholders. When found, these are expanded into CloudFormation Fn::Sub
* intrinsic functions.
*
* @param disableCloudFormationSubstitution Set to true to disable intrinsics.
*/
public void setDisableCloudFormationSubstitution(boolean disableCloudFormationSubstitution) {
this.disableCloudFormationSubstitution = disableCloudFormationSubstitution;
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
import software.amazon.smithy.model.node.ObjectNode;
import software.amazon.smithy.model.node.StringNode;
import software.amazon.smithy.model.traits.Trait;
import software.amazon.smithy.openapi.OpenApiConstants;
import software.amazon.smithy.openapi.OpenApiConfig;
import software.amazon.smithy.openapi.fromsmithy.Context;
import software.amazon.smithy.openapi.fromsmithy.OpenApiMapper;
import software.amazon.smithy.openapi.model.OpenApi;
Expand All @@ -51,7 +51,7 @@ final class CloudFormationSubstitution implements OpenApiMapper {
* commonly extracted out of CloudFormation. This list may need to be updated over
* time as new features are added. Note that this list only expands to simple
* Fn::Sub. Anything more complex needs to be handled through JSON substitutions
* via {@link OpenApiConstants#SUBSTITUTIONS} as can anything that does not appear
* via {@link OpenApiConfig#setSubstitutions} as can anything that does not appear
* in this list.
*/
private static final List<String> PATHS = Arrays.asList(
Expand All @@ -69,13 +69,21 @@ public byte getOrder() {

@Override
public ObjectNode updateNode(Context<? extends Trait> context, OpenApi openapi, ObjectNode node) {
if (!context.getConfig().getBooleanMemberOrDefault(ApiGatewayConstants.DISABLE_CLOUDFORMATION_SUBSTITUTION)) {
if (!isDisabled(context)) {
return node.accept(new CloudFormationFnSubInjector(PATHS)).expectObjectNode();
}

return node;
}

private boolean isDisabled(Context<?> context) {
// Support the old name for backward compatibility.
return context.getConfig().getExtensions(ApiGatewayConfig.class).getDisableCloudFormationSubstitution()
|| context.getConfig()
.getExtensions()
.getBooleanMemberOrDefault("apigateway.disableCloudFormationSubstitution");
}

private static class CloudFormationFnSubInjector extends NodeVisitor.Default<Node> {
private final Deque<String> stack = new ArrayDeque<>();
private final List<String[]> paths;
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import software.amazon.smithy.model.node.Node;
import software.amazon.smithy.model.node.ObjectNode;
import software.amazon.smithy.model.shapes.ShapeId;
import software.amazon.smithy.openapi.OpenApiConfig;
import software.amazon.smithy.openapi.fromsmithy.OpenApiConverter;
import software.amazon.smithy.utils.IoUtils;

Expand Down Expand Up @@ -55,9 +56,14 @@ public void pluginCanBeDisabled() {
IoUtils.readUtf8File(getClass().getResource("substitution-not-performed.json").getPath()))
.expectObjectNode();

OpenApiConfig config = new OpenApiConfig();
ApiGatewayConfig apiGatewayConfig = new ApiGatewayConfig();
apiGatewayConfig.setDisableCloudFormationSubstitution(true);
config.putExtensions(apiGatewayConfig);

ObjectNode actual = OpenApiConverter.create()
.classLoader(getClass().getClassLoader())
.putSetting(ApiGatewayConstants.DISABLE_CLOUDFORMATION_SUBSTITUTION, true)
.config(config)
.convertToNode(model, ShapeId.from("example.smithy#MyService"));

Node.assertEquals(expected, actual);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@

package software.amazon.smithy.jsonschema;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import software.amazon.smithy.model.node.Node;
import software.amazon.smithy.model.node.NodeMapper;
import software.amazon.smithy.model.node.ObjectNode;
import software.amazon.smithy.model.traits.TimestampFormatTrait;

Expand Down Expand Up @@ -68,8 +68,14 @@ public String toString() {
private UnionStrategy unionStrategy = UnionStrategy.ONE_OF;
private String definitionPointer = "#/definitions";
private ObjectNode schemaDocumentExtensions = Node.objectNode();
private Map<String, Node> extensions = new HashMap<>();
private ObjectNode extensions = Node.objectNode();
private Set<String> disableFeatures = new HashSet<>();
private final ConcurrentHashMap<Class, Object> extensionCache = new ConcurrentHashMap<>();
private final NodeMapper nodeMapper = new NodeMapper();

public JsonSchemaConfig() {
nodeMapper.setWhenMissingSetter(NodeMapper.WhenMissing.INGORE);
}

public boolean getAlphanumericOnlyRefs() {
return alphanumericOnlyRefs;
Expand Down Expand Up @@ -181,17 +187,79 @@ public void setDisableFeatures(Set<String> disableFeatures) {
this.disableFeatures = disableFeatures;
}

public Map<String, Node> getExtensions() {
public ObjectNode getExtensions() {
return extensions;
}

/**
* Attempts to deserialize the {@code extensions} into the targeted
* type using a {@link NodeMapper}.
*
* <p>Extraneous properties are ignored and <em>not</em> warned on
* because many different plugins could be used with different
* configuration POJOs.
*
* <p>The result of calling this method is cached for each type,
* and the cache is cleared when any mutation is made to
* extensions.
*
* @param as Type to deserialize extensions into.
* @param <T> Type to deserialize extensions into.
* @return Returns the deserialized type.
*/
@SuppressWarnings("unchecked")
public <T> T getExtensions(Class<T> as) {
return (T) extensionCache.computeIfAbsent(as, t -> nodeMapper.deserialize(extensions, t));
}

/**
* Sets an arbitrary map of "extensions" used by plugins that need
* configuration.
*
* @param extensions Extensions to set.
*/
public void setExtensions(Map<String, Node> extensions) {
public void setExtensions(ObjectNode extensions) {
this.extensions = Objects.requireNonNull(extensions);
extensionCache.clear();
}

/**
* Add an extension to the "extensions" object node using a POJO.
*
* @param extensionContainer POJO to serialize and merge into extensions.
*/
public void putExtensions(Object extensionContainer) {
ObjectNode serialized = nodeMapper.serialize(extensionContainer).expectObjectNode();
setExtensions(extensions.merge(serialized));
}

/**
* Add an extension to the "extensions" object node.
*
* @param key Property name to set.
* @param value Value to assigned.
*/
public void putExtension(String key, Node value) {
setExtensions(extensions.withMember(key, value));
}

/**
* Add an extension to the "extensions" object node.
*
* @param key Property name to set.
* @param value Value to assigned.
*/
public void putExtension(String key, boolean value) {
putExtension(key, Node.from(value));
}

/**
* Add an extension to the "extensions" object node.
*
* @param key Property name to set.
* @param value Value to assigned.
*/
public void putExtension(String key, String value) {
putExtension(key, Node.from(value));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ public final class JsonSchemaConverter implements ToSmithyBuilder<JsonSchemaConv

private final Model model;
private final PropertyNamingStrategy propertyNamingStrategy;
private final JsonSchemaConfig config;
private JsonSchemaConfig config;
private final Predicate<Shape> shapePredicate;
private final RefStrategy refStrategy;
private final List<JsonSchemaMapper> realizedMappers;
Expand Down Expand Up @@ -150,6 +150,15 @@ public JsonSchemaConfig getConfig() {
return config;
}

/**
* Set the JSON Schema configuration settings.
*
* @param config Config object to set.
*/
public void setConfig(JsonSchemaConfig config) {
this.config = config;
}

/**
* Gets the property naming strategy of the converter.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -791,6 +791,12 @@ public Builder examples(Node examples) {
return this;
}

public Builder extensions(Map<String, Node> extensions) {
this.extensions.clear();
this.extensions.putAll(extensions);
return this;
}

public Builder putExtension(String key, ToNode value) {
extensions.put(key, value);
return this;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -479,4 +479,38 @@ public void convertingToBuilderGivesSameResult() {
SchemaDocument document4 = converter2.toBuilder().build().convert();
assertThat(document3, equalTo(document4));
}

@Test
public void canGetAndSetExtensionsAsPojo() {
Ext ext = new Ext();
ext.setBaz("hi");
ext.setFoo(true);
JsonSchemaConfig config = new JsonSchemaConfig();
config.putExtensions(ext);
Ext ext2 = config.getExtensions(Ext.class);

assertThat(ext2.getBaz(), equalTo("hi"));
assertThat(ext2.isFoo(), equalTo(true));
}

public static final class Ext {
private boolean foo;
private String baz;

public boolean isFoo() {
return foo;
}

public void setFoo(boolean foo) {
this.foo = foo;
}

public String getBaz() {
return baz;
}

public void setBaz(String baz) {
this.baz = baz;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,14 @@ public void handle(Class<?> into, String pointer, String property, Node value) {
public void handle(Class<?> into, String pointer, String property, Node value) {
LOGGER.warning(createMessage(property, pointer, into, value));
}
},

/**
* Ignores unknown properties.
*/
INGORE {
public void handle(Class<?> into, String pointer, String property, Node value) {
}
};

/**
Expand Down
Loading

0 comments on commit cbf8f2e

Please sign in to comment.