From 326c505931d1377049ec10fae35cacd166ffe8ec Mon Sep 17 00:00:00 2001 From: Michael Dowling Date: Wed, 8 Apr 2020 13:11:27 -0700 Subject: [PATCH 1/3] Update JSON schema config to use POJO --- .../smithy/jsonschema/DefaultRefStrategy.java | 32 +-- .../smithy/jsonschema/DisableMapper.java | 9 +- .../smithy/jsonschema/JsonSchemaConfig.java | 197 +++++++++++++++++ .../jsonschema/JsonSchemaConstants.java | 199 ------------------ .../jsonschema/JsonSchemaConverter.java | 42 +--- .../smithy/jsonschema/JsonSchemaMapper.java | 3 +- .../jsonschema/JsonSchemaShapeVisitor.java | 15 +- .../jsonschema/PropertyNamingStrategy.java | 6 +- .../amazon/smithy/jsonschema/RefStrategy.java | 6 +- .../amazon/smithy/jsonschema/Schema.java | 72 +++---- .../smithy/jsonschema/TimestampMapper.java | 12 +- .../smithy/jsonschema/package-info.java | 22 -- .../jsonschema/DeconflictingStrategyTest.java | 11 +- .../jsonschema/DefaultRefStrategyTest.java | 48 ++--- .../smithy/jsonschema/DisableMapperTest.java | 8 +- .../jsonschema/JsonSchemaConverterTest.java | 53 +---- .../PropertyNamingStrategyTest.java | 11 +- .../amazon/smithy/jsonschema/SchemaTest.java | 68 +++--- .../jsonschema/TimestampMapperTest.java | 17 +- 19 files changed, 350 insertions(+), 481 deletions(-) create mode 100644 smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/JsonSchemaConfig.java delete mode 100644 smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/JsonSchemaConstants.java delete mode 100644 smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/package-info.java diff --git a/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/DefaultRefStrategy.java b/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/DefaultRefStrategy.java index 3e6a09555ed..8d8264e9cce 100644 --- a/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/DefaultRefStrategy.java +++ b/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/DefaultRefStrategy.java @@ -17,14 +17,12 @@ import java.util.regex.Pattern; import software.amazon.smithy.model.Model; -import software.amazon.smithy.model.node.ObjectNode; import software.amazon.smithy.model.shapes.CollectionShape; import software.amazon.smithy.model.shapes.MemberShape; import software.amazon.smithy.model.shapes.Shape; import software.amazon.smithy.model.shapes.ShapeId; import software.amazon.smithy.model.shapes.SimpleShape; import software.amazon.smithy.model.traits.EnumTrait; -import software.amazon.smithy.utils.StringUtils; /** * This ref strategy converts Smithy shapes into the following: @@ -55,27 +53,22 @@ */ final class DefaultRefStrategy implements RefStrategy { - private static final Pattern SPLIT_PATTERN = Pattern.compile("\\."); private static final Pattern NON_ALPHA_NUMERIC = Pattern.compile("[^A-Za-z0-9]"); private final Model model; - private final boolean alphanumericOnly; - private final boolean keepNamespaces; private final String rootPointer; private final PropertyNamingStrategy propertyNamingStrategy; - private final ObjectNode config; + private final JsonSchemaConfig config; - DefaultRefStrategy(Model model, ObjectNode config, PropertyNamingStrategy propertyNamingStrategy) { + DefaultRefStrategy(Model model, JsonSchemaConfig config, PropertyNamingStrategy propertyNamingStrategy) { this.model = model; this.propertyNamingStrategy = propertyNamingStrategy; this.config = config; rootPointer = computePointer(config); - alphanumericOnly = config.getBooleanMemberOrDefault(JsonSchemaConstants.ALPHANUMERIC_ONLY_REFS); - keepNamespaces = config.getBooleanMemberOrDefault(JsonSchemaConstants.KEEP_NAMESPACES); } - private static String computePointer(ObjectNode config) { - String pointer = config.getStringMemberOrDefault(JsonSchemaConstants.DEFINITION_POINTER, DEFAULT_POINTER); + private static String computePointer(JsonSchemaConfig config) { + String pointer = config.getDefinitionPointer(); if (!pointer.endsWith("/")) { pointer += "/"; } @@ -89,10 +82,7 @@ public String toPointer(ShapeId id) { return createMemberPointer(member); } - StringBuilder builder = new StringBuilder(); - appendNamespace(builder, id); - builder.append(id.getName()); - return rootPointer + stripNonAlphaNumericCharsIfNecessary(builder.toString()); + return rootPointer + stripNonAlphaNumericCharsIfNecessary(id.getName()); } private String createMemberPointer(MemberShape member) { @@ -158,18 +148,8 @@ public boolean isInlined(Shape shape) { return shape instanceof SimpleShape; } - private void appendNamespace(StringBuilder builder, ShapeId id) { - // Append each namespace part, capitalizing each segment. - // For example, "smithy.example" becomes "SmithyExample". - if (keepNamespaces) { - for (String part : SPLIT_PATTERN.split(id.getNamespace())) { - builder.append(StringUtils.capitalize(part)); - } - } - } - private String stripNonAlphaNumericCharsIfNecessary(String result) { - return alphanumericOnly + return config.getAlphanumericOnlyRefs() ? NON_ALPHA_NUMERIC.matcher(result).replaceAll("") : result; } diff --git a/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/DisableMapper.java b/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/DisableMapper.java index b46752ed1c3..0ce318829eb 100644 --- a/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/DisableMapper.java +++ b/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/DisableMapper.java @@ -15,12 +15,11 @@ package software.amazon.smithy.jsonschema; -import software.amazon.smithy.model.node.ObjectNode; import software.amazon.smithy.model.shapes.Shape; /** * Removes keywords from a Schema builder that have been disabled using - * the settings object {@code disable.*} flags. + * {@link JsonSchemaConfig#setDisableFeatures}. */ final class DisableMapper implements JsonSchemaMapper { @Override @@ -29,9 +28,9 @@ public byte getOrder() { } @Override - public Schema.Builder updateSchema(Shape shape, Schema.Builder schema, ObjectNode config) { - for (String key : config.getMembersByPrefix("disable.").keySet()) { - schema.disableProperty(key); + public Schema.Builder updateSchema(Shape shape, Schema.Builder schema, JsonSchemaConfig config) { + for (String feature : config.getDisableFeatures()) { + schema.disableProperty(feature); } return schema; diff --git a/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/JsonSchemaConfig.java b/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/JsonSchemaConfig.java new file mode 100644 index 00000000000..5801ba3b340 --- /dev/null +++ b/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/JsonSchemaConfig.java @@ -0,0 +1,197 @@ +/* + * 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.jsonschema; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.node.ObjectNode; +import software.amazon.smithy.model.traits.TimestampFormatTrait; + +/** + * JSON Schema configuration options. + */ +public class JsonSchemaConfig { + + /** + * Configures how Smithy union shapes are converted to JSON Schema. + */ + public enum UnionStrategy { + /** + * Converts to a schema that uses "oneOf". + * + *

This is the default setting used if not configured. + */ + ONE_OF("oneOf"), + + /** + * Converts to an empty object "{}". + */ + OBJECT("object"), + + /** + * Converts to an object with properties just like a structure. + */ + STRUCTURE("structure"); + + private String stringValue; + + UnionStrategy(String stringValue) { + this.stringValue = stringValue; + } + + @Override + public String toString() { + return stringValue; + } + } + + private boolean alphanumericOnlyRefs; + private boolean useJsonName; + private TimestampFormatTrait.Format defaultTimestampFormat = TimestampFormatTrait.Format.DATE_TIME; + private UnionStrategy unionStrategy = UnionStrategy.ONE_OF; + private String definitionPointer = "#/definitions"; + private ObjectNode schemaDocumentExtensions = Node.objectNode(); + private Map extensions = new HashMap<>(); + private Set disableFeatures = new HashSet<>(); + + public boolean getAlphanumericOnlyRefs() { + return alphanumericOnlyRefs; + } + + /** + * Creates shape name pointers that strip out non-alphanumeric characters. + * + *

This is necessary for compatibility with some vendors like + * Amazon API Gateway that only allow alphanumeric shape names. + * + * @param alphanumericOnlyRefs Set to true to strip non-alphanumeric characters. + */ + public void setAlphanumericOnlyRefs(boolean alphanumericOnlyRefs) { + this.alphanumericOnlyRefs = alphanumericOnlyRefs; + } + + public boolean getUseJsonName() { + return useJsonName; + } + + /** + * Uses the value of the jsonName trait when creating JSON schema + * properties for structure and union shapes. + * + *

This property has no effect if a {@link PropertyNamingStrategy} is + * manually configured on a {@link JsonSchemaConverter}. + * + * @param useJsonName Set to true to use jsonName traits when creating refs.. + */ + public void setUseJsonName(boolean useJsonName) { + this.useJsonName = useJsonName; + } + + public TimestampFormatTrait.Format getDefaultTimestampFormat() { + return defaultTimestampFormat; + } + + /** + * Sets the assumed timestampFormat trait for timestamps with no + * timestampFormat trait. The provided value is expected to be a string. + * + *

Defaults to "date-time" if not set. Can be set to "date-time", + * "epoch-seconds", or "http-date". + * + * @param defaultTimestampFormat The default timestamp format to use when none is set. + */ + public void setDefaultTimestampFormat(TimestampFormatTrait.Format defaultTimestampFormat) { + this.defaultTimestampFormat = defaultTimestampFormat; + } + + public UnionStrategy getUnionStrategy() { + return unionStrategy; + } + + /** + * Configures how Smithy union shapes are converted to JSON Schema. + * + * @param unionStrategy The union strategy to use. + */ + public void setUnionStrategy(UnionStrategy unionStrategy) { + this.unionStrategy = unionStrategy; + } + + public String getDefinitionPointer() { + return definitionPointer; + } + + /** + * Configures location of where definitions are written using JSON Pointer. + * + *

The provided String value MUST start with "#/" and can use nested "/" + * characters to place schemas in nested object properties. The provided + * JSON Pointer does not support escaping. + * + *

Defaults to "#/definitions" if no value is specified. OpenAPI + * artifacts will want to use "#/components/schemas". + * + * @param definitionPointer The root definition pointer to use. + */ + public void setDefinitionPointer(String definitionPointer) { + this.definitionPointer = Objects.requireNonNull(definitionPointer); + } + + public ObjectNode getSchemaDocumentExtensions() { + return schemaDocumentExtensions; + } + + /** + * Adds custom key-value pairs to the JSON Schema document generated from + * a Smithy model. + * + * @param schemaDocumentExtensions Custom extensions to merge into the created schema. + */ + public void setSchemaDocumentExtensions(ObjectNode schemaDocumentExtensions) { + this.schemaDocumentExtensions = Objects.requireNonNull(schemaDocumentExtensions); + } + + public Set getDisableFeatures() { + return disableFeatures; + } + + /** + * Disables OpenAPI features by their property name name (e.g., "allOf"). + * + * @param disableFeatures Feature names to disable. + */ + public void setDisableFeatures(Set disableFeatures) { + this.disableFeatures = disableFeatures; + } + + public Map getExtensions() { + return extensions; + } + + /** + * Sets an arbitrary map of "extensions" used by plugins that need + * configuration. + * + * @param extensions Extensions to set. + */ + public void setExtensions(Map extensions) { + this.extensions = Objects.requireNonNull(extensions); + } +} diff --git a/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/JsonSchemaConstants.java b/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/JsonSchemaConstants.java deleted file mode 100644 index 8d202ebbce3..00000000000 --- a/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/JsonSchemaConstants.java +++ /dev/null @@ -1,199 +0,0 @@ -/* - * Copyright 2019 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.jsonschema; - -import software.amazon.smithy.model.node.ObjectNode; - -public final class JsonSchemaConstants { - /** - * Keeps Smithy namespaces in the converted shape ID that is generated - * in the definitions map of a JSON Schema document for a shape. - * - *

This property has no effect if a {@link RefStrategy} is - * manually configured on a {@link JsonSchemaConverter}. - */ - public static final String KEEP_NAMESPACES = "keepNamespaces"; - - /** - * Creates shape name pointers that strip out non-alphanumeric characters. - * - *

This is necessary for compatibility with some vendors like - * Amazon API Gateway that only allow alphanumeric shape names. - */ - public static final String ALPHANUMERIC_ONLY_REFS = "alphanumericOnlyRefs"; - - /** - * Uses the value of the jsonName trait when creating JSON schema - * properties for structure and union shapes. - * - *

This property has no effect if a {@link PropertyNamingStrategy} is - * manually configured on a {@link JsonSchemaConverter}. - */ - public static final String USE_JSON_NAME = "useJsonName"; - - /** - * Sets the assumed timestampFormat trait for timestamps with no - * timestampFormat trait. The provided value is expected to be a string. - * - *

Defaults to "date-time" if not set. Can be set to "date-time", - * "epoch-seconds", or "http-date". - */ - public static final String DEFAULT_TIMESTAMP_FORMAT = "defaultTimestampFormat"; - - /** - * Configures how Smithy union shapes are converted to JSON Schema. - * - *

This value expects a string that can be set to one of the following - * value: - * - *

- * - *

Any other value will raise an {@link UnsupportedOperationException} - * when the first union shape is encountered. - */ - public static final String UNION_STRATEGY = "unionStrategy"; - - /** - * Configures location of where definitions are written using JSON Pointer. - * - *

The provided String value MUST start with "#/" and can use nested "/" - * characters to place schemas in nested object properties. The provided - * JSON Pointer does not support escaping. - * - *

Defaults to "#/definitions" if no value is specified. OpenAPI - * artifacts will want to use "#/components/schemas". - */ - public static final String DEFINITION_POINTER = "definitionPointer"; - - /** - * Adds custom key-value pairs to the JSON Schema document generated from - * a Smithy model. - * - *

The value provided for this configuration setting is required to be - * a {@link ObjectNode}. - */ - public static final String SCHEMA_DOCUMENT_EXTENSIONS = "schemaDocumentExtensions"; - - /** Strips any instances of "const" from schemas. */ - public static final String DISABLE_CONST = "disable.const"; - - /** Strips any instances of "default" from schemas. */ - public static final String DISABLE_DEFAULT = "disable.default"; - - /** Strips any instances of "enum" from schemas. */ - public static final String DISABLE_ENUM = "disable.enum"; - - /** Strips any instances of "multipleOf" from schemas. */ - public static final String DISABLE_MULTIPLE_OF = "disable.multipleOf"; - - /** Strips any instances of "maximum" from schemas. */ - public static final String DISABLE_MAXIMUM = "disable.maximum"; - - /** Strips any instances of "exclusiveMaximum" from schemas. */ - public static final String DISABLE_EXCLUSIVE_MAXIMUM = "disable.exclusiveMaximum"; - - /** Strips any instances of "minimum" from schemas. */ - public static final String DISABLE_MINIMUM = "disable.minimum"; - - /** Strips any instances of "exclusiveMinimum" from schemas. */ - public static final String DISABLE_EXCLUSIVE_MINIMUM = "disable.exclusiveMinimum"; - - /** Strips any instances of "maxLength" from schemas. */ - public static final String DISABLE_MAX_LENGTH = "disable.maxLength"; - - /** Strips any instances of "minLength" from schemas. */ - public static final String DISABLE_MIN_LENGTH = "disable.minLength"; - - /** Strips any instances of "pattern" from schemas. */ - public static final String DISABLE_PATTERN = "disable.pattern"; - - /** Strips any instances of "items" from schemas. */ - public static final String DISABLE_ITEMS = "disable.items"; - - /** Strips any instances of "maxItems" from schemas. */ - public static final String DISABLE_MAX_ITEMS = "disable.maxItems"; - - /** Strips any instances of "minItems" from schemas. */ - public static final String DISABLE_MIN_ITEMS = "disable.minItems"; - - /** Strips any instances of "uniqueItems" from schemas. */ - public static final String DISABLE_UNIQUE_ITEMS = "disable.uniqueItems"; - - /** Strips any instances of "properties" from schemas. */ - public static final String DISABLE_PROPERTIES = "disable.properties"; - - /** Strips any instances of "additionalProperties" from schemas. */ - public static final String DISABLE_ADDITIONAL_PROPERTIES = "disable.additionalProperties"; - - /** Strips any instances of "required" from schemas. */ - public static final String DISABLE_REQUIRED = "disable.required"; - - /** Strips any instances of "maxProperties" from schemas. */ - public static final String DISABLE_MAX_PROPERTIES = "disable.maxProperties"; - - /** Strips any instances of "minProperties" from schemas. */ - public static final String DISABLE_MIN_PROPERTIES = "disable.minProperties"; - - /** Strips any instances of "propertyNames" from schemas. */ - public static final String DISABLE_PROPERTY_NAMES = "disable.propertyNames"; - - /** Strips any instances of "allOf" from schemas. */ - public static final String DISABLE_ALL_OF = "disable.allOf"; - - /** Strips any instances of "anyOf" from schemas. */ - public static final String DISABLE_ANY_OF = "disable.anyOf"; - - /** Strips any instances of "oneOf" from schemas. */ - public static final String DISABLE_ONE_OF = "disable.oneOf"; - - /** Strips any instances of "not" from schemas. */ - public static final String DISABLE_NOT = "disable.not"; - - /** Strips any instances of "title" from schemas. */ - public static final String DISABLE_TITLE = "disable.title"; - - /** Strips any instances of "description" from schemas. */ - public static final String DISABLE_DESCRIPTION = "disable.description"; - - /** Strips any instances of "format" from schemas. */ - public static final String DISABLE_FORMAT = "disable.format"; - - /** Strips any instances of "readOnly" from schemas. */ - public static final String DISABLE_READ_ONLY = "disable.readOnly"; - - /** Strips any instances of "writeOnly" from schemas. */ - public static final String DISABLE_WRITE_ONLY = "disable.writeOnly"; - - /** Strips any instances of "comment" from schemas. */ - public static final String DISABLE_COMMENT = "disable.comment"; - - /** Strips any instances of "contentEncoding" from schemas. */ - public static final String DISABLE_CONTENT_ENCODING = "disable.contentEncoding"; - - /** Strips any instances of "contentMediaType" from schemas. */ - public static final String DISABLE_CONTENT_MEDIA_TYPE = "disable.contentMediaType"; - - /** Strips any instances of "examples" from schemas. */ - public static final String DISABLE_EXAMPLES = "disable.examples"; - - private JsonSchemaConstants() {} -} diff --git a/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/JsonSchemaConverter.java b/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/JsonSchemaConverter.java index e563ca3e32f..06154779b1d 100644 --- a/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/JsonSchemaConverter.java +++ b/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/JsonSchemaConverter.java @@ -54,7 +54,7 @@ public final class JsonSchemaConverter implements ToSmithyBuilder shapePredicate; private final RefStrategy refStrategy; private final List realizedMappers; @@ -95,8 +95,7 @@ private JsonSchemaConverter(Builder builder) { visitor = new JsonSchemaShapeVisitor(model, this, realizedMappers); // Compute the number of segments in the root definition section. - rootDefinitionPointer = config.getStringMemberOrDefault( - JsonSchemaConstants.DEFINITION_POINTER, RefStrategy.DEFAULT_POINTER); + rootDefinitionPointer = config.getDefinitionPointer(); rootDefinitionSegments = countSegments(rootDefinitionPointer); LOGGER.fine(() -> "Using the following root JSON schema pointer: " + rootDefinitionPointer + " (" + rootDefinitionSegments + " segments)"); @@ -147,7 +146,7 @@ public static Builder builder() { * * @return Returns the config object. */ - public ObjectNode getConfig() { + public JsonSchemaConfig getConfig() { return config; } @@ -179,7 +178,7 @@ public String toPointer(ToShapeId id) { * Checks if the given JSON pointer points to a top-level definition. * *

Note that this expects the pointer to exactly start with the same - * string that is configured as {@link JsonSchemaConstants#DEFINITION_POINTER}, + * string that is configured as {@link JsonSchemaConfig#getDefinitionPointer()}, * or the default value of "#/definitions". If the number of segments * in the provided pointer is also equal to the number of segments * in the default pointer + 1, then it is considered a top-level pointer. @@ -274,10 +273,12 @@ private boolean isUnsupportedShapeType(Shape shape) { } private void addExtensions(SchemaDocument.Builder builder) { - getConfig().getObjectMember(JsonSchemaConstants.SCHEMA_DOCUMENT_EXTENSIONS).ifPresent(extensions -> { + ObjectNode extensions = config.getSchemaDocumentExtensions(); + + if (!extensions.isEmpty()) { LOGGER.fine(() -> "Adding JSON schema extensions: " + Node.prettyPrintJson(extensions)); builder.extensions(extensions); - }); + } } @Override @@ -296,7 +297,7 @@ public static final class Builder implements SmithyBuilder private Model model; private ShapeId rootShape; private PropertyNamingStrategy propertyNamingStrategy = DEFAULT_PROPERTY_STRATEGY; - private ObjectNode config = Node.objectNode(); + private JsonSchemaConfig config = new JsonSchemaConfig(); private Predicate shapePredicate = shape -> true; private final List mappers = new ArrayList<>(); @@ -349,34 +350,11 @@ public Builder shapePredicate(Predicate shapePredicate) { * @param config Config to use. * @return Returns the converter. */ - public Builder config(ObjectNode config) { + public Builder config(JsonSchemaConfig config) { this.config = Objects.requireNonNull(config); return this; } - /** - * Merges the current config object with the given config object. - * - * @param config Config to merge with. - * @return Returns the converter. - */ - public Builder mergeConfig(ObjectNode config) { - this.config = this.config.merge(Objects.requireNonNull(config)); - return this; - } - - /** - * Sets a configuration setting. - * - * @param key Key to set. - * @param value Value to set. - * @return Returns the converter. - */ - public Builder putConfig(String key, Node value) { - this.config = this.config.withMember(key, value); - return this; - } - /** * Sets a custom property naming strategy. * diff --git a/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/JsonSchemaMapper.java b/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/JsonSchemaMapper.java index 261be3a8389..fbcd186b68e 100644 --- a/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/JsonSchemaMapper.java +++ b/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/JsonSchemaMapper.java @@ -15,7 +15,6 @@ package software.amazon.smithy.jsonschema; -import software.amazon.smithy.model.node.ObjectNode; import software.amazon.smithy.model.shapes.Shape; /** @@ -44,5 +43,5 @@ default byte getOrder() { * @param config JSON Schema config. * @return Returns an updated schema builder. */ - Schema.Builder updateSchema(Shape shape, Schema.Builder schemaBuilder, ObjectNode config); + Schema.Builder updateSchema(Shape shape, Schema.Builder schemaBuilder, JsonSchemaConfig config); } diff --git a/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/JsonSchemaShapeVisitor.java b/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/JsonSchemaShapeVisitor.java index efbc39c5183..b9b98a681ce 100644 --- a/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/JsonSchemaShapeVisitor.java +++ b/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/JsonSchemaShapeVisitor.java @@ -53,9 +53,6 @@ import software.amazon.smithy.utils.ListUtils; final class JsonSchemaShapeVisitor extends ShapeVisitor.Default { - private static final String UNION_STRATEGY_ONE_OF = "oneOf"; - private static final String UNION_STRATEGY_OBJECT = "object"; - private static final String UNION_STRATEGY_STRUCTURE = "structure"; private final Model model; private final JsonSchemaConverter converter; @@ -185,15 +182,14 @@ private Schema structuredShape(Shape container, Collection memberSh @Override public Schema unionShape(UnionShape shape) { - String unionStrategy = converter.getConfig().getStringMemberOrDefault( - JsonSchemaConstants.UNION_STRATEGY, UNION_STRATEGY_ONE_OF); + JsonSchemaConfig.UnionStrategy unionStrategy = converter.getConfig().getUnionStrategy(); switch (unionStrategy) { - case UNION_STRATEGY_OBJECT: + case OBJECT: return buildSchema(shape, createBuilder(shape, "object")); - case UNION_STRATEGY_STRUCTURE: + case STRUCTURE: return structuredShape(shape, shape.getAllMembers().values()); - case UNION_STRATEGY_ONE_OF: + case ONE_OF: List schemas = new ArrayList<>(); for (MemberShape member : shape.getAllMembers().values()) { String memberName = converter.toPropertyName(member); @@ -205,8 +201,7 @@ public Schema unionShape(UnionShape shape) { } return buildSchema(shape, createBuilder(shape, "object").type(null).oneOf(schemas)); default: - throw new SmithyJsonSchemaException(String.format( - "Unknown %s strategy: %s", JsonSchemaConstants.UNION_STRATEGY, unionStrategy)); + throw new SmithyJsonSchemaException(String.format("Unsupported union strategy: %s", unionStrategy)); } } diff --git a/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/PropertyNamingStrategy.java b/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/PropertyNamingStrategy.java index 347ad615062..cf0f8f6812c 100644 --- a/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/PropertyNamingStrategy.java +++ b/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/PropertyNamingStrategy.java @@ -15,7 +15,6 @@ package software.amazon.smithy.jsonschema; -import software.amazon.smithy.model.node.ObjectNode; import software.amazon.smithy.model.shapes.MemberShape; import software.amazon.smithy.model.shapes.Shape; import software.amazon.smithy.model.traits.JsonNameTrait; @@ -33,7 +32,7 @@ public interface PropertyNamingStrategy { * @param config Config to use. * @return Returns the computed member name. */ - String toPropertyName(Shape containingShape, MemberShape member, ObjectNode config); + String toPropertyName(Shape containingShape, MemberShape member, JsonSchemaConfig config); /** * Creates a naming strategy that just uses the member name as-is. @@ -53,8 +52,7 @@ static PropertyNamingStrategy createMemberNameStrategy() { static PropertyNamingStrategy createDefaultStrategy() { return (containingShape, member, config) -> { // Use the jsonName trait if configured to do so. - if (config.getBooleanMemberOrDefault(JsonSchemaConstants.USE_JSON_NAME) - && member.hasTrait(JsonNameTrait.class)) { + if (config.getUseJsonName() && member.hasTrait(JsonNameTrait.class)) { return member.expectTrait(JsonNameTrait.class).getValue(); } diff --git a/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/RefStrategy.java b/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/RefStrategy.java index ed762ca0d29..8a0eb44d2bf 100644 --- a/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/RefStrategy.java +++ b/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/RefStrategy.java @@ -16,7 +16,6 @@ package software.amazon.smithy.jsonschema; import software.amazon.smithy.model.Model; -import software.amazon.smithy.model.node.ObjectNode; import software.amazon.smithy.model.shapes.Shape; import software.amazon.smithy.model.shapes.ShapeId; @@ -27,7 +26,6 @@ * future if we *really* need to. Ideally we don't. */ interface RefStrategy { - String DEFAULT_POINTER = "#/definitions"; /** * Given a shape ID, returns the value used in a $ref to refer to it. @@ -63,7 +61,7 @@ interface RefStrategy { * the following ref is created "#/definitions/SmithyExampleFoo". * *

This implementation honors the value configured in - * {@link JsonSchemaConstants#DEFINITION_POINTER} to create a $ref + * {@link JsonSchemaConfig#getDefinitionPointer()} to create a $ref * pointer to a shape. * * @param model Model being converted. @@ -73,7 +71,7 @@ interface RefStrategy { */ static RefStrategy createDefaultStrategy( Model model, - ObjectNode config, + JsonSchemaConfig config, PropertyNamingStrategy propertyNamingStrategy ) { RefStrategy delegate = new DefaultRefStrategy(model, config, propertyNamingStrategy); diff --git a/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/Schema.java b/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/Schema.java index 4bcf90a9519..f6c76dd50e3 100644 --- a/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/Schema.java +++ b/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/Schema.java @@ -802,80 +802,80 @@ public Builder removeExtension(String key) { } /** - * Applies a "disable.X" key to a schema builder. + * Applies a "disableX" key to a schema builder. * - * @param disableKey Disable key to apply (e.g., "disable.propertyNames"). + * @param disableKey Disable key to apply (e.g., "disablePropertyNames"). * @return Returns the builder. */ public Builder disableProperty(String disableKey) { switch (disableKey) { - case JsonSchemaConstants.DISABLE_CONST: + case "const": return this.constValue(null); - case JsonSchemaConstants.DISABLE_DEFAULT: + case "default": return this.defaultValue(null); - case JsonSchemaConstants.DISABLE_ENUM: + case "enum": return this.enumValues(null); - case JsonSchemaConstants.DISABLE_MULTIPLE_OF: + case "multipleOf": return this.multipleOf(null); - case JsonSchemaConstants.DISABLE_MAXIMUM: + case "maximum": return this.maximum(null); - case JsonSchemaConstants.DISABLE_EXCLUSIVE_MAXIMUM: + case "exclusiveMaximum": return this.exclusiveMaximum(null); - case JsonSchemaConstants.DISABLE_MINIMUM: + case "minimum": return this.minimum(null); - case JsonSchemaConstants.DISABLE_EXCLUSIVE_MINIMUM: + case "exclusiveMinimum": return this.exclusiveMinimum(null); - case JsonSchemaConstants.DISABLE_MAX_LENGTH: + case "maxLength": return this.maxLength(null); - case JsonSchemaConstants.DISABLE_MIN_LENGTH: + case "minLength": return this.minLength(null); - case JsonSchemaConstants.DISABLE_PATTERN: + case "pattern": return this.pattern(null); - case JsonSchemaConstants.DISABLE_ITEMS: + case "items": return this.items(null); - case JsonSchemaConstants.DISABLE_MAX_ITEMS: + case "maxItems": return this.maxItems(null); - case JsonSchemaConstants.DISABLE_MIN_ITEMS: + case "minItems": return this.minItems(null); - case JsonSchemaConstants.DISABLE_UNIQUE_ITEMS: + case "uniqueItems": return this.uniqueItems(false); - case JsonSchemaConstants.DISABLE_PROPERTIES: + case "properties": return this.properties(null); - case JsonSchemaConstants.DISABLE_ADDITIONAL_PROPERTIES: + case "additionalProperties": return this.additionalProperties(null); - case JsonSchemaConstants.DISABLE_REQUIRED: + case "required": return this.required(null); - case JsonSchemaConstants.DISABLE_MAX_PROPERTIES: + case "maxProperties": return this.maxProperties(null); - case JsonSchemaConstants.DISABLE_MIN_PROPERTIES: + case "minProperties": return this.minProperties(null); - case JsonSchemaConstants.DISABLE_PROPERTY_NAMES: + case "propertyNames": return this.propertyNames(null); - case JsonSchemaConstants.DISABLE_ALL_OF: + case "allOf": return this.allOf(null); - case JsonSchemaConstants.DISABLE_ANY_OF: + case "anyOf": return this.anyOf(null); - case JsonSchemaConstants.DISABLE_ONE_OF: + case "oneOf": return this.oneOf(null); - case JsonSchemaConstants.DISABLE_NOT: + case "not": return this.not(null); - case JsonSchemaConstants.DISABLE_TITLE: + case "title": return this.title(null); - case JsonSchemaConstants.DISABLE_DESCRIPTION: + case "description": return this.description(null); - case JsonSchemaConstants.DISABLE_FORMAT: + case "format": return this.format(null); - case JsonSchemaConstants.DISABLE_READ_ONLY: + case "readOnly": return this.readOnly(false); - case JsonSchemaConstants.DISABLE_WRITE_ONLY: + case "writeOnly": return this.writeOnly(false); - case JsonSchemaConstants.DISABLE_COMMENT: + case "comment": return this.comment(null); - case JsonSchemaConstants.DISABLE_CONTENT_ENCODING: + case "contentEncoding": return this.contentEncoding(null); - case JsonSchemaConstants.DISABLE_CONTENT_MEDIA_TYPE: + case "contentMediaType": return this.contentMediaType(null); - case JsonSchemaConstants.DISABLE_EXAMPLES: + case "examples": return this.examples(null); default: LOGGER.warning("Unknown JSON Schema config 'disable' property: " + disableKey); diff --git a/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/TimestampMapper.java b/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/TimestampMapper.java index 1d66a036364..431e4f700fa 100644 --- a/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/TimestampMapper.java +++ b/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/TimestampMapper.java @@ -15,16 +15,14 @@ package software.amazon.smithy.jsonschema; -import software.amazon.smithy.model.node.ObjectNode; import software.amazon.smithy.model.shapes.Shape; import software.amazon.smithy.model.traits.TimestampFormatTrait; /** * Updates builders based on timestamp shapes, timestampFormat traits, and - * the value of {@link JsonSchemaConstants#DEFAULT_TIMESTAMP_FORMAT}. + * the value of {@link JsonSchemaConfig#getDefaultTimestampFormat()}. */ final class TimestampMapper implements JsonSchemaMapper { - private static final String DEFAULT_TIMESTAMP_FORMAT = TimestampFormatTrait.DATE_TIME; @Override public byte getOrder() { @@ -32,7 +30,7 @@ public byte getOrder() { } @Override - public Schema.Builder updateSchema(Shape shape, Schema.Builder builder, ObjectNode config) { + public Schema.Builder updateSchema(Shape shape, Schema.Builder builder, JsonSchemaConfig config) { String format = extractTimestampFormat(shape, config); if (format == null) { @@ -52,13 +50,11 @@ public Schema.Builder updateSchema(Shape shape, Schema.Builder builder, ObjectNo } } - private static String extractTimestampFormat(Shape shape, ObjectNode config) { + private static String extractTimestampFormat(Shape shape, JsonSchemaConfig config) { if (shape.isTimestampShape() || shape.hasTrait(TimestampFormatTrait.class)) { return shape.getTrait(TimestampFormatTrait.class) .map(TimestampFormatTrait::getValue) - .orElseGet(() -> config.getStringMemberOrDefault( - JsonSchemaConstants.DEFAULT_TIMESTAMP_FORMAT, - DEFAULT_TIMESTAMP_FORMAT)); + .orElseGet(() -> config.getDefaultTimestampFormat().toString()); } return null; diff --git a/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/package-info.java b/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/package-info.java deleted file mode 100644 index 4ff56146d1a..00000000000 --- a/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/package-info.java +++ /dev/null @@ -1,22 +0,0 @@ -/* - * 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. - */ - -/** - * Converts Smithy models to JSON Schema. - */ -@SmithyUnstableApi -package software.amazon.smithy.jsonschema; - -import software.amazon.smithy.utils.SmithyUnstableApi; diff --git a/smithy-jsonschema/src/test/java/software/amazon/smithy/jsonschema/DeconflictingStrategyTest.java b/smithy-jsonschema/src/test/java/software/amazon/smithy/jsonschema/DeconflictingStrategyTest.java index 0c703f2e3c5..6cdcd84839b 100644 --- a/smithy-jsonschema/src/test/java/software/amazon/smithy/jsonschema/DeconflictingStrategyTest.java +++ b/smithy-jsonschema/src/test/java/software/amazon/smithy/jsonschema/DeconflictingStrategyTest.java @@ -6,8 +6,6 @@ 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.ObjectNode; import software.amazon.smithy.model.shapes.IntegerShape; import software.amazon.smithy.model.shapes.ListShape; import software.amazon.smithy.model.shapes.MemberShape; @@ -27,7 +25,8 @@ public void canDeconflictNamesWhereListsAreActuallyDifferent() { Model model = Model.builder().addShapes(str, integer, a, b, memberA, memberB).build(); PropertyNamingStrategy propertyNamingStrategy = PropertyNamingStrategy.createDefaultStrategy(); - RefStrategy strategy = RefStrategy.createDefaultStrategy(model, Node.objectNode(), propertyNamingStrategy); + RefStrategy strategy = RefStrategy + .createDefaultStrategy(model, new JsonSchemaConfig(), propertyNamingStrategy); assertThat(strategy.toPointer(a.getId()), equalTo("#/definitions/Page")); assertThat(strategy.toPointer(b.getId()), equalTo("#/definitions/PageComFoo")); } @@ -40,16 +39,16 @@ public void detectsUnsupportedConflicts() { PropertyNamingStrategy propertyNamingStrategy = PropertyNamingStrategy.createDefaultStrategy(); Assertions.assertThrows(ConflictingShapeNameException.class, () -> { - RefStrategy.createDefaultStrategy(model, Node.objectNode(), propertyNamingStrategy); + RefStrategy.createDefaultStrategy(model, new JsonSchemaConfig(), propertyNamingStrategy); }); } @Test public void deconflictingStrategyPassesThroughToDelegate() { - ObjectNode config = Node.objectNode(); Model model = Model.builder().build(); PropertyNamingStrategy propertyNamingStrategy = PropertyNamingStrategy.createDefaultStrategy(); - RefStrategy strategy = RefStrategy.createDefaultStrategy(model, config, propertyNamingStrategy); + RefStrategy strategy = RefStrategy + .createDefaultStrategy(model, new JsonSchemaConfig(), propertyNamingStrategy); assertThat(strategy.toPointer(ShapeId.from("com.foo#Nope")), equalTo("#/definitions/Nope")); } diff --git a/smithy-jsonschema/src/test/java/software/amazon/smithy/jsonschema/DefaultRefStrategyTest.java b/smithy-jsonschema/src/test/java/software/amazon/smithy/jsonschema/DefaultRefStrategyTest.java index db6b00f4c82..c9d25d71e57 100644 --- a/smithy-jsonschema/src/test/java/software/amazon/smithy/jsonschema/DefaultRefStrategyTest.java +++ b/smithy-jsonschema/src/test/java/software/amazon/smithy/jsonschema/DefaultRefStrategyTest.java @@ -5,8 +5,6 @@ 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.ObjectNode; import software.amazon.smithy.model.shapes.ListShape; import software.amazon.smithy.model.shapes.MapShape; import software.amazon.smithy.model.shapes.MemberShape; @@ -17,12 +15,11 @@ public class DefaultRefStrategyTest { private PropertyNamingStrategy propertyNamingStrategy = PropertyNamingStrategy.createDefaultStrategy(); - private ObjectNode config = Node.objectNode(); @Test public void usesDefaultPointer() { RefStrategy ref = RefStrategy.createDefaultStrategy( - Model.builder().build(), Node.objectNode(), propertyNamingStrategy); + Model.builder().build(), new JsonSchemaConfig(), propertyNamingStrategy); String pointer = ref.toPointer(ShapeId.from("smithy.example#Foo")); assertThat(pointer, equalTo("#/definitions/Foo")); @@ -30,9 +27,10 @@ public void usesDefaultPointer() { @Test public void usesCustomPointerAndAppendsSlashWhenNecessary() { - RefStrategy ref = RefStrategy.createDefaultStrategy(Model.builder().build(), Node.objectNodeBuilder() - .withMember(JsonSchemaConstants.DEFINITION_POINTER, Node.from("#/components/schemas")) - .build(), propertyNamingStrategy); + JsonSchemaConfig config = new JsonSchemaConfig(); + config.setDefinitionPointer("#/components/schemas"); + RefStrategy ref = RefStrategy + .createDefaultStrategy(Model.builder().build(), config, propertyNamingStrategy); String pointer = ref.toPointer(ShapeId.from("smithy.example#Foo")); assertThat(pointer, equalTo("#/components/schemas/Foo")); @@ -40,29 +38,21 @@ public void usesCustomPointerAndAppendsSlashWhenNecessary() { @Test public void usesCustomPointerAndOmitsSlashWhenNecessary() { - RefStrategy ref = RefStrategy.createDefaultStrategy(Model.builder().build(), Node.objectNodeBuilder() - .withMember(JsonSchemaConstants.DEFINITION_POINTER, Node.from("#/components/schemas")) - .build(), propertyNamingStrategy); + JsonSchemaConfig config = new JsonSchemaConfig(); + config.setDefinitionPointer("#/components/schemas"); + RefStrategy ref = RefStrategy + .createDefaultStrategy(Model.builder().build(), config, propertyNamingStrategy); String pointer = ref.toPointer(ShapeId.from("smithy.example#Foo")); assertThat(pointer, equalTo("#/components/schemas/Foo")); } - @Test - public void includesNamespacesWhenRequested() { - RefStrategy ref = RefStrategy.createDefaultStrategy(Model.builder().build(), Node.objectNodeBuilder() - .withMember(JsonSchemaConstants.KEEP_NAMESPACES, true) - .build(), propertyNamingStrategy); - String pointer = ref.toPointer(ShapeId.from("smithy.example#Foo")); - - assertThat(pointer, equalTo("#/definitions/SmithyExampleFoo")); - } - @Test public void stripsNonAlphanumericCharactersWhenRequested() { - RefStrategy ref = RefStrategy.createDefaultStrategy(Model.builder().build(), Node.objectNodeBuilder() - .withMember(JsonSchemaConstants.ALPHANUMERIC_ONLY_REFS, true) - .build(), propertyNamingStrategy); + JsonSchemaConfig config = new JsonSchemaConfig(); + config.setAlphanumericOnlyRefs(true); + RefStrategy ref = RefStrategy + .createDefaultStrategy(Model.builder().build(), config, propertyNamingStrategy); String pointer = ref.toPointer(ShapeId.from("smithy.example#Foo_Bar")); assertThat(pointer, equalTo("#/definitions/FooBar")); @@ -80,7 +70,8 @@ public void addsListAndSetMembers() { .member(member) .build(); Model model = Model.builder().addShapes(string, list, member).build(); - RefStrategy ref = RefStrategy.createDefaultStrategy(model, config, propertyNamingStrategy); + RefStrategy ref = RefStrategy + .createDefaultStrategy(model, new JsonSchemaConfig(), propertyNamingStrategy); String pointer = ref.toPointer(member.getId()); assertThat(pointer, equalTo("#/definitions/Scripts/items")); @@ -103,7 +94,8 @@ public void addsMapMembers() { .value(value) .build(); Model model = Model.builder().addShapes(string, map, key, value).build(); - RefStrategy ref = RefStrategy.createDefaultStrategy(model, config, propertyNamingStrategy); + RefStrategy ref = RefStrategy + .createDefaultStrategy(model, new JsonSchemaConfig(), propertyNamingStrategy); assertThat(ref.toPointer(key.getId()), equalTo("#/definitions/Scripts/propertyNames")); assertThat(ref.toPointer(value.getId()), equalTo("#/definitions/Scripts/additionalProperties")); @@ -121,7 +113,8 @@ public void addsStructureMembers() { .addMember(member) .build(); Model model = Model.builder().addShapes(string, struct, member).build(); - RefStrategy ref = RefStrategy.createDefaultStrategy(model, config, propertyNamingStrategy); + RefStrategy ref = RefStrategy + .createDefaultStrategy(model, new JsonSchemaConfig(), propertyNamingStrategy); assertThat(ref.toPointer(struct.getId()), equalTo("#/definitions/Scripts")); assertThat(ref.toPointer(member.getId()), equalTo("#/definitions/Scripts/properties/pages")); @@ -137,7 +130,8 @@ public void usesRefForStructureMembers() { .id("foo.bar#Bam") .build(); Model model = Model.builder().addShapes(baz, bam).build(); - RefStrategy ref = RefStrategy.createDefaultStrategy(model, config, propertyNamingStrategy); + RefStrategy ref = RefStrategy + .createDefaultStrategy(model, new JsonSchemaConfig(), propertyNamingStrategy); assertThat(ref.toPointer(baz.getMember("bam").get().getId()), equalTo("#/definitions/Bam")); } diff --git a/smithy-jsonschema/src/test/java/software/amazon/smithy/jsonschema/DisableMapperTest.java b/smithy-jsonschema/src/test/java/software/amazon/smithy/jsonschema/DisableMapperTest.java index 8beabe407c0..3841ef1878f 100644 --- a/smithy-jsonschema/src/test/java/software/amazon/smithy/jsonschema/DisableMapperTest.java +++ b/smithy-jsonschema/src/test/java/software/amazon/smithy/jsonschema/DisableMapperTest.java @@ -20,18 +20,16 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import org.junit.jupiter.api.Test; -import software.amazon.smithy.model.node.Node; -import software.amazon.smithy.model.node.ObjectNode; import software.amazon.smithy.model.shapes.StringShape; +import software.amazon.smithy.utils.SetUtils; public class DisableMapperTest { @Test public void removesDisabledKeywords() { StringShape shape = StringShape.builder().id("smithy.example#String").build(); Schema.Builder builder = Schema.builder().type("string").format("foo"); - ObjectNode config = Node.objectNodeBuilder() - .withMember(JsonSchemaConstants.DISABLE_FORMAT, true) - .build(); + JsonSchemaConfig config = new JsonSchemaConfig(); + config.setDisableFeatures(SetUtils.of("format")); Schema schema = new DisableMapper().updateSchema(shape, builder, config).build(); assertThat(schema.getType().get(), equalTo("string")); diff --git a/smithy-jsonschema/src/test/java/software/amazon/smithy/jsonschema/JsonSchemaConverterTest.java b/smithy-jsonschema/src/test/java/software/amazon/smithy/jsonschema/JsonSchemaConverterTest.java index bfdb6c387c6..f0a78b55bf9 100644 --- a/smithy-jsonschema/src/test/java/software/amazon/smithy/jsonschema/JsonSchemaConverterTest.java +++ b/smithy-jsonschema/src/test/java/software/amazon/smithy/jsonschema/JsonSchemaConverterTest.java @@ -28,13 +28,10 @@ import java.math.BigDecimal; import java.util.List; import java.util.Locale; -import java.util.Optional; import java.util.function.Predicate; -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.ObjectNode; import software.amazon.smithy.model.shapes.BigDecimalShape; import software.amazon.smithy.model.shapes.BigIntegerShape; import software.amazon.smithy.model.shapes.BlobShape; @@ -189,10 +186,8 @@ public void excludesPrivateShapes() { @Test public void addsExtensionsFromConfig() { Model model = Model.builder().build(); - ObjectNode config = Node.objectNodeBuilder() - .withMember(JsonSchemaConstants.SCHEMA_DOCUMENT_EXTENSIONS, Node.objectNode() - .withMember("foo", Node.from("bar"))) - .build(); + JsonSchemaConfig config = new JsonSchemaConfig(); + config.setSchemaDocumentExtensions(Node.objectNode().withMember("foo", Node.from("bar"))); SchemaDocument doc = JsonSchemaConverter.builder().config(config).model(model).build().convert(); assertThat(doc.getDefinitions().keySet(), empty()); @@ -429,10 +424,10 @@ public void supportsUnionObject() { MemberShape member = MemberShape.builder().id("a.b#Union$foo").target("smithy.api#String").build(); UnionShape union = UnionShape.builder().id("a.b#Union").addMember(member).build(); Model model = Model.builder().addShapes(union, member, string).build(); + JsonSchemaConfig config = new JsonSchemaConfig(); + config.setUnionStrategy(JsonSchemaConfig.UnionStrategy.OBJECT); SchemaDocument document = JsonSchemaConverter.builder() - .config(Node.objectNodeBuilder() - .withMember(JsonSchemaConstants.UNION_STRATEGY, "object") - .build()) + .config(config) .model(model) .build() .convertShape(union); @@ -449,10 +444,10 @@ public void supportsUnionStructure() { MemberShape member = MemberShape.builder().id("a.b#Union$foo").target("smithy.api#String").build(); UnionShape union = UnionShape.builder().id("a.b#Union").addMember(member).build(); Model model = Model.builder().addShapes(union, member, string).build(); + JsonSchemaConfig config = new JsonSchemaConfig(); + config.setUnionStrategy(JsonSchemaConfig.UnionStrategy.STRUCTURE); SchemaDocument document = JsonSchemaConverter.builder() - .config(Node.objectNodeBuilder() - .withMember(JsonSchemaConstants.UNION_STRATEGY, "structure") - .build()) + .config(config) .model(model) .build() .convertShape(union); @@ -463,23 +458,6 @@ public void supportsUnionStructure() { assertThat(schema.getProperties().keySet(), contains("foo")); } - @Test - public void throwsForUnsupportUnionSetting() { - Assertions.assertThrows(SmithyJsonSchemaException.class, () -> { - StringShape string = StringShape.builder().id("smithy.api#String").build(); - MemberShape member = MemberShape.builder().id("a.b#Union$foo").target("smithy.api#String").build(); - UnionShape union = UnionShape.builder().id("a.b#Union").addMember(member).build(); - Model model = Model.builder().addShapes(union, member, string).build(); - JsonSchemaConverter.builder() - .config(Node.objectNodeBuilder() - .withMember(JsonSchemaConstants.UNION_STRATEGY, "not-valid") - .build()) - .model(model) - .build() - .convert(); - }); - } - @Test public void convertingToBuilderGivesSameResult() { Model model = Model.assembler() @@ -501,19 +479,4 @@ public void convertingToBuilderGivesSameResult() { SchemaDocument document4 = converter2.toBuilder().build().convert(); assertThat(document3, equalTo(document4)); } - - @Test - public void addsSchemaDocumentExtensions() { - Model model = Model.assembler() - .addImport(getClass().getResource("test-service.json")) - .assemble() - .unwrap(); - JsonSchemaConverter converter = JsonSchemaConverter.builder() - .model(model) - .putConfig(JsonSchemaConstants.SCHEMA_DOCUMENT_EXTENSIONS, Node.objectNode().withMember("foo", "bar")) - .build(); - SchemaDocument document = converter.convert(); - - assertThat(document.getExtension("foo"), equalTo(Optional.of(Node.from("bar")))); - } } diff --git a/smithy-jsonschema/src/test/java/software/amazon/smithy/jsonschema/PropertyNamingStrategyTest.java b/smithy-jsonschema/src/test/java/software/amazon/smithy/jsonschema/PropertyNamingStrategyTest.java index 4193adbc582..def9364381f 100644 --- a/smithy-jsonschema/src/test/java/software/amazon/smithy/jsonschema/PropertyNamingStrategyTest.java +++ b/smithy-jsonschema/src/test/java/software/amazon/smithy/jsonschema/PropertyNamingStrategyTest.java @@ -19,8 +19,6 @@ import static org.hamcrest.Matchers.equalTo; import org.junit.jupiter.api.Test; -import software.amazon.smithy.model.node.Node; -import software.amazon.smithy.model.node.ObjectNode; import software.amazon.smithy.model.shapes.MemberShape; import software.amazon.smithy.model.shapes.StructureShape; import software.amazon.smithy.model.traits.JsonNameTrait; @@ -35,7 +33,8 @@ public void defaultStrategyUsesJsonNameTraitIfConfigured() { .addTrait(new JsonNameTrait("FOO")) .build(); StructureShape struct = StructureShape.builder().id("smithy.example#Structure").addMember(member).build(); - ObjectNode config = Node.objectNodeBuilder().withMember(JsonSchemaConstants.USE_JSON_NAME, true).build(); + JsonSchemaConfig config = new JsonSchemaConfig(); + config.setUseJsonName(true); String memberName = strategy.toPropertyName(struct, member, config); assertThat(memberName, equalTo("FOO")); @@ -50,7 +49,7 @@ public void defaultStrategyIgnoresJsonNameTraitIfNotConfigured() { .addTrait(new JsonNameTrait("FOO")) .build(); StructureShape struct = StructureShape.builder().id("smithy.example#Structure").addMember(member).build(); - ObjectNode config = Node.objectNode(); + JsonSchemaConfig config = new JsonSchemaConfig(); String memberName = strategy.toPropertyName(struct, member, config); assertThat(memberName, equalTo("foo")); @@ -61,7 +60,7 @@ public void defaultStrategyUsesMemberName() { PropertyNamingStrategy strategy = PropertyNamingStrategy.createDefaultStrategy(); MemberShape member = MemberShape.builder().id("smithy.example#Structure$foo").target("a.b#C").build(); StructureShape struct = StructureShape.builder().id("smithy.example#Structure").addMember(member).build(); - ObjectNode config = Node.objectNode(); + JsonSchemaConfig config = new JsonSchemaConfig(); String memberName = strategy.toPropertyName(struct, member, config); assertThat(memberName, equalTo("foo")); @@ -76,7 +75,7 @@ public void memberNameStrategyUsesMemberName() { .addTrait(new JsonNameTrait("FOO")) .build(); StructureShape struct = StructureShape.builder().id("smithy.example#Structure").addMember(member).build(); - ObjectNode config = Node.objectNode(); + JsonSchemaConfig config = new JsonSchemaConfig(); String memberName = strategy.toPropertyName(struct, member, config); assertThat(memberName, equalTo("foo")); diff --git a/smithy-jsonschema/src/test/java/software/amazon/smithy/jsonschema/SchemaTest.java b/smithy-jsonschema/src/test/java/software/amazon/smithy/jsonschema/SchemaTest.java index 860aedcd54d..801a3401b01 100644 --- a/smithy-jsonschema/src/test/java/software/amazon/smithy/jsonschema/SchemaTest.java +++ b/smithy-jsonschema/src/test/java/software/amazon/smithy/jsonschema/SchemaTest.java @@ -35,40 +35,40 @@ public class SchemaTest { public void canRemoveSettings() { Schema.Builder builder = Schema.builder(); Set values = SetUtils.of( - JsonSchemaConstants.DISABLE_CONTENT_MEDIA_TYPE, - JsonSchemaConstants.DISABLE_ADDITIONAL_PROPERTIES, - JsonSchemaConstants.DISABLE_ALL_OF, - JsonSchemaConstants.DISABLE_ANY_OF, - JsonSchemaConstants.DISABLE_COMMENT, - JsonSchemaConstants.DISABLE_CONST, - JsonSchemaConstants.DISABLE_CONTENT_ENCODING, - JsonSchemaConstants.DISABLE_DEFAULT, - JsonSchemaConstants.DISABLE_DESCRIPTION, - JsonSchemaConstants.DISABLE_ENUM, - JsonSchemaConstants.DISABLE_EXAMPLES, - JsonSchemaConstants.DISABLE_EXCLUSIVE_MAXIMUM, - JsonSchemaConstants.DISABLE_EXCLUSIVE_MINIMUM, - JsonSchemaConstants.DISABLE_FORMAT, - JsonSchemaConstants.DISABLE_ITEMS, - JsonSchemaConstants.DISABLE_MAX_ITEMS, - JsonSchemaConstants.DISABLE_MAX_LENGTH, - JsonSchemaConstants.DISABLE_MAX_PROPERTIES, - JsonSchemaConstants.DISABLE_MAXIMUM, - JsonSchemaConstants.DISABLE_MIN_ITEMS, - JsonSchemaConstants.DISABLE_MIN_LENGTH, - JsonSchemaConstants.DISABLE_MIN_PROPERTIES, - JsonSchemaConstants.DISABLE_MINIMUM, - JsonSchemaConstants.DISABLE_MULTIPLE_OF, - JsonSchemaConstants.DISABLE_NOT, - JsonSchemaConstants.DISABLE_ONE_OF, - JsonSchemaConstants.DISABLE_PATTERN, - JsonSchemaConstants.DISABLE_PROPERTIES, - JsonSchemaConstants.DISABLE_PROPERTY_NAMES, - JsonSchemaConstants.DISABLE_READ_ONLY, - JsonSchemaConstants.DISABLE_REQUIRED, - JsonSchemaConstants.DISABLE_TITLE, - JsonSchemaConstants.DISABLE_UNIQUE_ITEMS, - JsonSchemaConstants.DISABLE_WRITE_ONLY + "const", + "default", + "enum", + "multipleOf", + "maximum", + "exclusiveMaximum", + "minimum", + "exclusiveMinimum", + "maxLength", + "minLength", + "pattern", + "items", + "maxItems", + "minItems", + "uniqueItems", + "properties", + "additionalProperties", + "required", + "maxProperties", + "minProperties", + "propertyNames", + "allOf", + "anyOf", + "oneOf", + "not", + "title", + "description", + "format", + "readOnly", + "writeOnly", + "comment", + "contentEncoding", + "contentMediaType", + "examples" ); for (String value : values) { diff --git a/smithy-jsonschema/src/test/java/software/amazon/smithy/jsonschema/TimestampMapperTest.java b/smithy-jsonschema/src/test/java/software/amazon/smithy/jsonschema/TimestampMapperTest.java index 03b811ac1f3..bcad16900ca 100644 --- a/smithy-jsonschema/src/test/java/software/amazon/smithy/jsonschema/TimestampMapperTest.java +++ b/smithy-jsonschema/src/test/java/software/amazon/smithy/jsonschema/TimestampMapperTest.java @@ -20,8 +20,6 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import org.junit.jupiter.api.Test; -import software.amazon.smithy.model.node.Node; -import software.amazon.smithy.model.node.ObjectNode; import software.amazon.smithy.model.shapes.TimestampShape; import software.amazon.smithy.model.traits.TimestampFormatTrait; @@ -32,7 +30,7 @@ public void convertsDateTimeToStringAndDateTimeFormat() { .id("smithy.example#Timestamp") .addTrait(new TimestampFormatTrait(TimestampFormatTrait.DATE_TIME)) .build(); - Schema schema = new TimestampMapper().updateSchema(shape, Schema.builder(), Node.objectNode()).build(); + Schema schema = new TimestampMapper().updateSchema(shape, Schema.builder(), new JsonSchemaConfig()).build(); assertThat(schema.getType().get(), equalTo("string")); assertThat(schema.getFormat().get(), equalTo("date-time")); @@ -44,7 +42,7 @@ public void convertsHttpDateToString() { .id("smithy.example#Timestamp") .addTrait(new TimestampFormatTrait(TimestampFormatTrait.HTTP_DATE)) .build(); - Schema schema = new TimestampMapper().updateSchema(shape, Schema.builder(), Node.objectNode()).build(); + Schema schema = new TimestampMapper().updateSchema(shape, Schema.builder(), new JsonSchemaConfig()).build(); assertThat(schema.getType().get(), equalTo("string")); assertFalse(schema.getFormat().isPresent()); @@ -56,7 +54,7 @@ public void convertsEpochSecondsToNumber() { .id("smithy.example#Timestamp") .addTrait(new TimestampFormatTrait(TimestampFormatTrait.EPOCH_SECONDS)) .build(); - Schema schema = new TimestampMapper().updateSchema(shape, Schema.builder(), Node.objectNode()).build(); + Schema schema = new TimestampMapper().updateSchema(shape, Schema.builder(), new JsonSchemaConfig()).build(); assertThat(schema.getType().get(), equalTo("number")); assertFalse(schema.getFormat().isPresent()); @@ -68,7 +66,7 @@ public void convertsEpochUnknownToNumber() { .id("smithy.example#Timestamp") .addTrait(new TimestampFormatTrait("epoch-millis")) .build(); - Schema schema = new TimestampMapper().updateSchema(shape, Schema.builder(), Node.objectNode()).build(); + Schema schema = new TimestampMapper().updateSchema(shape, Schema.builder(), new JsonSchemaConfig()).build(); assertThat(schema.getType().get(), equalTo("number")); assertFalse(schema.getFormat().isPresent()); @@ -77,9 +75,8 @@ public void convertsEpochUnknownToNumber() { @Test public void supportsDefaultTimestampFormat() { TimestampShape shape = TimestampShape.builder().id("smithy.example#Timestamp").build(); - ObjectNode config = Node.objectNodeBuilder() - .withMember(JsonSchemaConstants.DEFAULT_TIMESTAMP_FORMAT, TimestampFormatTrait.DATE_TIME) - .build(); + JsonSchemaConfig config = new JsonSchemaConfig(); + config.setDefaultTimestampFormat(TimestampFormatTrait.Format.DATE_TIME); Schema schema = new TimestampMapper().updateSchema(shape, Schema.builder(), config).build(); assertThat(schema.getType().get(), equalTo("string")); @@ -91,7 +88,7 @@ public void assumesDateTimeStringWhenNoFormatOrDefaultPresent() { TimestampShape shape = TimestampShape.builder() .id("smithy.example#Timestamp") .build(); - Schema schema = new TimestampMapper().updateSchema(shape, Schema.builder(), Node.objectNode()).build(); + Schema schema = new TimestampMapper().updateSchema(shape, Schema.builder(), new JsonSchemaConfig()).build(); assertThat(schema.getType().get(), equalTo("string")); assertThat(schema.getFormat().get(), equalTo("date-time")); From cbf8f2e86d3b70b7d2d4a1096e0fa11828e492e2 Mon Sep 17 00:00:00 2001 From: Michael Dowling Date: Wed, 8 Apr 2020 16:32:58 -0700 Subject: [PATCH 2/3] Refactor OpenAPI plugin to use POJOs 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. --- docs/source/guides/converting-to-openapi.rst | 301 +++++++++++++----- .../apigateway/openapi/ApiGatewayConfig.java | 39 +++ .../openapi/ApiGatewayConstants.java | 30 -- .../openapi/CloudFormationSubstitution.java | 14 +- .../aws/apigateway/openapi/package-info.java | 22 -- .../CloudFormationSubstitutionTest.java | 8 +- .../smithy/jsonschema/JsonSchemaConfig.java | 78 ++++- .../jsonschema/JsonSchemaConverter.java | 11 +- .../amazon/smithy/jsonschema/Schema.java | 6 + .../jsonschema/JsonSchemaConverterTest.java | 34 ++ .../amazon/smithy/model/node/NodeMapper.java | 8 + .../amazon/smithy/openapi/OpenApiConfig.java | 287 +++++++++++++++++ .../smithy/openapi/OpenApiConstants.java | 146 --------- .../smithy/openapi/fromsmithy/Context.java | 17 +- .../openapi/fromsmithy/OpenApiConverter.java | 149 +++------ .../fromsmithy/OpenApiJsonSchemaMapper.java | 114 +++---- .../openapi/fromsmithy/OpenApiProtocol.java | 13 +- .../openapi/fromsmithy/Smithy2OpenApi.java | 39 ++- .../mappers/CheckForGreedyLabels.java | 5 +- .../mappers/CheckForPrefixHeaders.java | 11 +- .../fromsmithy/mappers/OpenApiJsonAdd.java | 39 ++- .../mappers/OpenApiJsonSubstitutions.java | 26 +- .../mappers/RemoveUnusedComponents.java | 9 +- .../fromsmithy/mappers/UnsupportedTraits.java | 3 +- .../protocols/AwsRestJson1Protocol.java | 15 +- .../smithy/openapi/model/Component.java | 5 - .../amazon/smithy/openapi/package-info.java | 22 -- .../fromsmithy/OpenApiConverterTest.java | 26 +- .../OpenApiJsonSchemaMapperTest.java | 36 +-- .../mappers/CheckForGreedyLabelsTest.java | 7 +- .../mappers/CheckForPrefixHeadersTest.java | 6 +- .../mappers/OpenApiJsonAddTest.java | 7 +- .../OpenApiJsonSubstitutionsPluginTest.java | 8 +- .../mappers/RemoveUnusedComponentsTest.java | 6 +- .../mappers/UnsupportedTraitsPluginTest.java | 6 +- .../protocols/AwsRestJson1ProtocolTest.java | 6 +- 36 files changed, 936 insertions(+), 623 deletions(-) create mode 100644 smithy-aws-apigateway-openapi/src/main/java/software/amazon/smithy/aws/apigateway/openapi/ApiGatewayConfig.java delete mode 100644 smithy-aws-apigateway-openapi/src/main/java/software/amazon/smithy/aws/apigateway/openapi/ApiGatewayConstants.java delete mode 100644 smithy-aws-apigateway-openapi/src/main/java/software/amazon/smithy/aws/apigateway/openapi/package-info.java create mode 100644 smithy-openapi/src/main/java/software/amazon/smithy/openapi/OpenApiConfig.java delete mode 100644 smithy-openapi/src/main/java/software/amazon/smithy/openapi/OpenApiConstants.java delete mode 100644 smithy-openapi/src/main/java/software/amazon/smithy/openapi/package-info.java diff --git a/docs/source/guides/converting-to-openapi.rst b/docs/source/guides/converting-to-openapi.rst index 94ee5c19a0b..690b1216f42 100644 --- a/docs/source/guides/converting-to-openapi.rst +++ b/docs/source/guides/converting-to-openapi.rst @@ -10,6 +10,7 @@ specifications. :local: :backlinks: none + ------------ Introduction ------------ @@ -26,6 +27,7 @@ Smithy models can be converted to OpenAPI through smithy-build using the ``openapi`` plugin or through code using the `software.amazon.smithy:smithy-openapi`_ Java package. + -------------------------------------- Differences between Smithy and OpenAPI -------------------------------------- @@ -46,10 +48,14 @@ strongly-typed languages. Unsupported features ==================== -Converting a Smithy model to OpenAPI results in a trimmed-down, lossy -representation of a model for a specific HTTP protocol. Various features in -a Smithy model are not currently supported in the OpenAPI conversion: +Converting a Smithy model to OpenAPI is a lossy conversion. Various features +in a Smithy model are not currently supported in the OpenAPI conversion. + +**Unsupported features** +* :ref:`endpoint-trait` and :ref:`hostLabel-trait`: These traits are used + to dynamically alter the endpoint of an operation based on input. They + are not supported in OpenAPI. * :ref:`HTTP prefix headers `: "Prefix headers" are used in Smithy to bind all headers under a common prefix into a single property of the input or output of an API operation. This can @@ -60,19 +66,7 @@ a Smithy model are not currently supported in the OpenAPI conversion: ``/foo/{baz+}/bar``). Some OpenAPI vendors/tooling support greedy labels (for example, Amazon API Gateway) while other do not. The converter will pass greedy labels through into the OpenAPI document by default, but they - can be forbidden through the ``openapi.forbidGreedyLabels`` flag. -* :ref:`Event streams `: Event streams are a way of sending - many different messages over a stream. This is not currently implemented - in the converter (see `#80 `_). -* Streaming: Smithy allows blob and string shapes to be marked as - streaming, meaning that their contents should not be loaded into - memory by clients or servers. This is not currently something supported - as a built-in feature of OpenAPI (we could potentially add an extension - to mark a specific type as streaming). -* :ref:`Custom traits `: Custom traits defined in a Smithy - model are not converted and added to the OpenAPI specification. Copying - Smithy traits into OpenAPI as extensions requires the use of a custom - ``software.amazon.smithy.openapi.fromsmithy.OpenApiExtension``. + can be forbidden through the ``forbidGreedyLabels`` flag. * Non-RESTful routing: HTTP routing schemes that aren't based on methods and unique URIs are not supported in OpenAPI (for example, routing to operations based on a specific header or query string @@ -81,8 +75,24 @@ a Smithy model are not currently supported in the OpenAPI conversion: not supported with OpenAPI (for example, an MQTT-based protocol modeled with Smithy would need to also support an HTTP-based protocol to be converted to OpenAPI). + +**Compatibility notes** + +* Streaming: Smithy allows blob and string shapes to be marked as + streaming, meaning that their contents should not be loaded into + memory by clients or servers. While this isn't technically unsupported in + OpenAPI, some vendors like API Gateway do not currently support streaming + large payloads. + +**Lossy metadata** + * Resources: Smithy resource metadata is not carried over into the OpenAPI specification. +* :ref:`Custom traits `: Custom traits defined in a Smithy + model are not converted and added to the OpenAPI specification. Copying + Smithy traits into OpenAPI as extensions requires the use of a custom + ``software.amazon.smithy.openapi.fromsmithy.OpenApiExtension``. + --------------------------------------- Converting to OpenAPI with smithy-build @@ -106,7 +116,7 @@ specification from a Smithy model using a buildscript dependency: buildscript { dependencies { - classpath("software.amazon.smithy:smithy-openapi:0.9.7") + classpath("software.amazon.smithy:smithy-openapi:0.9.9") } } @@ -132,7 +142,7 @@ that builds an OpenAPI specification from a service for the .. important:: - A buildscript dependency on "software.amazon.smithy:smithy-openapi:0.9.7" is + A buildscript dependency on "software.amazon.smithy:smithy-openapi:0.9.9" is required in order for smithy-build to map the "openapi" plugin name to the correct Java library implementation. @@ -143,7 +153,6 @@ OpenAPI configuration settings The ``openapi`` plugin is highly configurable to support different OpenAPI tools and vendors. - .. tip:: You typically only need to configure the ``service`` and @@ -153,10 +162,22 @@ The following key-value pairs are supported: service (string) **Required**. The Smithy service :ref:`shape ID ` to convert. + For example, ``smithy.example#Weather``. + + .. code-block:: json + + { + "version": "1.0", + "plugins": { + "openapi": { + "service": "smithy.example#Weather" + } + } + } protocol (string) - The protocol shape ID to use when converting Smithy to OpenAPI (for - example, ``aws.protocols#restJson1``). + The protocol shape ID to use when converting Smithy to OpenAPI. + For example, ``aws.protocols#restJson1``. Smithy will try to match the provided protocol name with an implementation of ``software.amazon.smithy.openapi.fromsmithy.OpenApiProtocol`` @@ -165,15 +186,41 @@ protocol (string) .. important:: - This property is required if a service supports multiple protocols. + ``protocol`` is required if a service supports multiple protocols. + + .. code-block:: json + + { + "version": "1.0", + "plugins": { + "openapi": { + "service": "smithy.example#Weather", + "protocol": "aws.protocols#restJson1" + } + } + } + +tags (boolean) + Whether or not to include Smithy :ref:`tags ` in the result + as `OpenAPI tags`_. The following example adds all tags in the Smithy + model to the OpenAPI model. + + .. code-block:: json -openapi.tags (boolean) - Whether or not to include Smithy :ref:`tags ` in the result. + { + "version": "1.0", + "plugins": { + "openapi": { + "service": "smithy.example#Weather", + "tags": true + } + } + } -openapi.supportedTags ([string]) - Limits the exported ``openapi.tags`` to a specific set of tags. The value - must be a list of strings. This property requires that ``openapi.tags`` - is set to ``true`` in order to have an effect. +supportedTags ([string]) + Limits the exported ``tags`` to a specific set of tags. The value + must be a list of strings. This property requires that ``tags`` is set to + ``true`` in order to have an effect. .. code-block:: json @@ -182,21 +229,30 @@ openapi.supportedTags ([string]) "plugins": { "openapi": { "service": "smithy.example#Weather", - "openapi.tags": true, - "openapi.supportedTags": ["foo", "baz", "bar"] + "tags": true, + "supportedTags": ["foo", "baz", "bar"] } } } -openapi.defaultBlobFormat (string) +defaultBlobFormat (string) Sets the default format property used when converting blob shapes in Smithy to strings in OpenAPI. Defaults to "byte", meaning Base64 encoded. + See `OpenAPI Data types`_ for more information. -openapi.use.xml (boolean) - Enables converting Smithy XML traits to OpenAPI XML properties. (this - feature is not yet implemented). + .. code-block:: json -openapi.externalDocs ([string]) + { + "version": "1.0", + "plugins": { + "openapi": { + "service": "smithy.example#Weather", + "defaultBlobFormat": "byte" + } + } + } + +externalDocs ([string]) Limits the source of converted "externalDocs" fields to the specified priority ordered list of names in an :ref:`externaldocumentation-trait`. This list is case insensitive. By default, this is a list of the following @@ -210,7 +266,7 @@ openapi.externalDocs ([string]) "plugins": { "openapi": { "service": "smithy.example#Weather", - "openapi.externalDocs": [ + "externalDocs": [ "Homepage", "Custom" ] @@ -218,33 +274,65 @@ openapi.externalDocs ([string]) } } -openapi.keepUnusedComponents (boolean) - Set to ``true`` to prevent unused components from being removed from the - created specification. +keepUnusedComponents (boolean) + Set to ``true`` to prevent unused OpenAPI ``components`` from being + removed from the created specification. + + .. code-block:: json -openapi.aws.jsonContentType (string) + { + "version": "1.0", + "plugins": { + "openapi": { + "service": "smithy.example#Weather", + "keepUnusedComponents": true + } + } + } + +jsonContentType (string) Sets a custom media-type to associate with the JSON payload of JSON-based protocols. -openapi.forbidGreedyLabels (boolean) + .. code-block:: json + + { + "version": "1.0", + "plugins": { + "openapi": { + "service": "smithy.example#Weather", + "jsonContentType": "application/x-amz-json-1.1" + } + } + } + +forbidGreedyLabels (boolean) Set to true to forbid greedy URI labels. By default, greedy labels will appear as-is in the path generated for an operation. For example, "/{foo+}". -openapi.onHttpPrefixHeaders (string) + .. code-block:: json + + { + "version": "1.0", + "plugins": { + "openapi": { + "service": "smithy.example#Weather", + "forbidGreedyLabels": true + } + } + } + +onHttpPrefixHeaders (string) Specifies what to do when the :ref:`httpPrefixHeaders-trait` is found in a model. OpenAPI does not support ``httpPrefixHeaders``. By default, the conversion will fail when this trait is encountered, but this behavior - can be customized using the following values for the ``openapi.onHttpPrefixHeaders`` + can be customized using the following values for the ``onHttpPrefixHeaders`` setting: * FAIL: The default setting that causes the build to fail. * WARN: The header is omitted from the OpenAPI model and a warning is logged. - .. note:: - - Additional values may be supported by other mappers or protocols. - .. code-block:: json { @@ -252,39 +340,33 @@ openapi.onHttpPrefixHeaders (string) "plugins": { "openapi": { "service": "smithy.example#Weather", - "openapi.onHttpPrefixHeaders": "WARN" + "onHttpPrefixHeaders": "WARN" } } } -openapi.ignoreUnsupportedTrait (boolean) +ignoreUnsupportedTraits (boolean) Emits warnings rather than failing when unsupported traits like - ``eventStream`` are encountered. + ``endpoint`` and ``hostLabel`` are encountered. -openapi.disablePrimitiveInlining (boolean) - Disables the automatic inlining of primitive ``$ref`` targets. - - Inlining these primitive references helps to make the generated - OpenAPI models more idiomatic while leaving complex types as-is so that - they support recursive types. - - A *primitive reference* is considered one of the following OpenAPI types: - - * integer - * number - * boolean - * string + .. code-block:: json - A *primitive collection* is an array that has an "items" property that - targets a primitive reference, or an object with no "properties" and an - "additionalProperties" reference that targets a primitive type. + { + "version": "1.0", + "plugins": { + "openapi": { + "service": "smithy.example#Weather", + "ignoreUnsupportedTraits": true + } + } + } -openapi.substitutions (``Map``) +substitutions (``Map``) Defines a map of strings to any JSON value to find and replace in the generated OpenAPI model. String values are replaced if the string in its entirety matches - one of the keys provided in the ``openapi.substitutions`` map. The + one of the keys provided in the ``substitutions`` map. The corresponding value is then substituted for the string; this could even result in a string changing into an object, array, etc. @@ -295,7 +377,7 @@ openapi.substitutions (``Map``) .. warning:: - When possible, prefer ``openapi.jsonAdd`` instead because the update + When possible, prefer ``jsonAdd`` instead because the update performed on the generated document is more explicit and resilient to change. @@ -306,7 +388,7 @@ openapi.substitutions (``Map``) "plugins": { "openapi": { "service": "smithy.example#Weather", - "openapi.substitutions": { + "substitutions": { "REPLACE_ME": ["this is a", " replacement"], "ANOTHER_REPLACEMENT": "Hello!!!" } @@ -314,7 +396,7 @@ openapi.substitutions (``Map``) } } -openapi.jsonAdd (``Map``) +jsonAdd (``Map``) Adds or replaces the JSON value in the generated OpenAPI document at the given JSON pointer locations with a different JSON value. The value must be a map where each key is a valid JSON pointer string as defined in @@ -333,7 +415,7 @@ openapi.jsonAdd (``Map``) "plugins": { "openapi": { "service": "smithy.example#Weather", - "openapi.jsonAdd": { + "jsonAdd": { "/info/title": "Replaced title value", "/info/nested/foo": { "hi": "Adding this object created intermediate objects too!" @@ -348,19 +430,22 @@ openapi.jsonAdd (``Map``) JSON schema configuration settings ================================== -stripNamespaces (boolean) - Strips Smithy namespaces from the converted shape ID that is generated - in the definitions map of a JSON Schema document for a shape. This - requires that shape names across all namespaces are unique. - -includePrivateShapes (boolean) - Includes shapes marked with the :ref:`private-trait`. - useJsonName (boolean) Uses the value of the :ref:`jsonName-trait` when creating JSON schema - properties for structure and union shapes. + properties for structure and union shapes. This property MAY be + automatically set to ``true`` depending on the protocol being converted. - TODO: This is enabled automatically with AWS protocols? + .. code-block:: json + + { + "version": "1.0", + "plugins": { + "openapi": { + "service": "smithy.example#Weather", + "useJsonName": true + } + } + } defaultTimestampFormat (string) Sets the assumed :ref:`timestampFormat-trait` value for timestamps with @@ -368,6 +453,18 @@ defaultTimestampFormat (string) a string. Defaults to "date-time" if not set. Can be set to "date-time", "epoch-seconds", or "http-date". + .. code-block:: json + + { + "version": "1.0", + "plugins": { + "openapi": { + "service": "smithy.example#Weather", + "defaultTimestampFormat": "epoch-seconds" + } + } + } + unionStrategy (string) Configures how Smithy union shapes are converted to JSON Schema. @@ -379,8 +476,38 @@ unionStrategy (string) * structure: Converts to an object with properties just like a structure. + .. code-block:: json + + { + "version": "1.0", + "plugins": { + "openapi": { + "service": "smithy.example#Weather", + "unionStrategy": "oneOf" + } + } + } + schemaDocumentExtensions (``Map``) Adds custom top-level key-value pairs to the created OpenAPI specification. + Any existing value is overwritten. + + .. code-block:: json + + { + "version": "1.0", + "plugins": { + "openapi": { + "service": "smithy.example#Weather", + "schemaDocumentExtensions": { + "x-my-custom-top-level-property": "Hello!", + "x-another-custom-top-level-property": { + "can be": ["complex", "value", "too!"] + } + } + } + } + } Amazon API Gateway extensions @@ -397,7 +524,7 @@ dependency on ``software.amazon.smithy:smithy-aws-apigateway-openapi``. buildscript { dependencies { - classpath("software.amazon.smithy:smithy-aws-apigateway-openapi:0.9.7") + classpath("software.amazon.smithy:smithy-aws-apigateway-openapi:0.9.9") } } @@ -535,8 +662,8 @@ uses the ``Fn::Sub`` variable syntax (``*`` means any value): .. note:: - This functionality can be disabled by setting the ``apigateway.disableCloudFormationSubstitution`` - OpenAPI configuration property to ``true``. + This functionality can be disabled by setting the ``disableCloudFormationSubstitution`` + configuration property to ``true``. Amazon Cognito User Pools @@ -603,7 +730,7 @@ shows how to install ``software.amazon.smithy:smithy-openapi`` through Gradle: buildscript { dependencies { - classpath("software.amazon.smithy:smithy-openapi:0.9.7") + classpath("software.amazon.smithy:smithy-openapi:0.9.9") } } @@ -643,3 +770,5 @@ The conversion process is highly extensible through .. _intrinsic functions: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference.html .. _`Fn::Sub`: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-sub.html .. _x-amazon-apigateway-api-key-source: https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-swagger-extensions-api-key-source.html +.. _OpenAPI tags: https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#tagObject +.. _OpenAPI Data types: https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#data-types 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 new file mode 100644 index 00000000000..85fdc1b2e54 --- /dev/null +++ b/smithy-aws-apigateway-openapi/src/main/java/software/amazon/smithy/aws/apigateway/openapi/ApiGatewayConfig.java @@ -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; + } +} diff --git a/smithy-aws-apigateway-openapi/src/main/java/software/amazon/smithy/aws/apigateway/openapi/ApiGatewayConstants.java b/smithy-aws-apigateway-openapi/src/main/java/software/amazon/smithy/aws/apigateway/openapi/ApiGatewayConstants.java deleted file mode 100644 index d02bb78e63e..00000000000 --- a/smithy-aws-apigateway-openapi/src/main/java/software/amazon/smithy/aws/apigateway/openapi/ApiGatewayConstants.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright 2019 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 constants. - */ -public final class ApiGatewayConstants { - /** - * If set to true, disables CloudFormation substitutions of specific paths - * when they contain ${} placeholders. When found, these are expanded into - * CloudFormation Fn::Sub intrinsic functions. - */ - public static final String DISABLE_CLOUDFORMATION_SUBSTITUTION = "apigateway.disableCloudFormationSubstitution"; - - private ApiGatewayConstants() {} -} diff --git a/smithy-aws-apigateway-openapi/src/main/java/software/amazon/smithy/aws/apigateway/openapi/CloudFormationSubstitution.java b/smithy-aws-apigateway-openapi/src/main/java/software/amazon/smithy/aws/apigateway/openapi/CloudFormationSubstitution.java index cfec3b5b6fe..c007e480344 100644 --- a/smithy-aws-apigateway-openapi/src/main/java/software/amazon/smithy/aws/apigateway/openapi/CloudFormationSubstitution.java +++ b/smithy-aws-apigateway-openapi/src/main/java/software/amazon/smithy/aws/apigateway/openapi/CloudFormationSubstitution.java @@ -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; @@ -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 PATHS = Arrays.asList( @@ -69,13 +69,21 @@ public byte getOrder() { @Override public ObjectNode updateNode(Context 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 { private final Deque stack = new ArrayDeque<>(); private final List paths; diff --git a/smithy-aws-apigateway-openapi/src/main/java/software/amazon/smithy/aws/apigateway/openapi/package-info.java b/smithy-aws-apigateway-openapi/src/main/java/software/amazon/smithy/aws/apigateway/openapi/package-info.java deleted file mode 100644 index c91d9cdda41..00000000000 --- a/smithy-aws-apigateway-openapi/src/main/java/software/amazon/smithy/aws/apigateway/openapi/package-info.java +++ /dev/null @@ -1,22 +0,0 @@ -/* - * 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. - */ - -/** - * Defines traits for integrating Smithy with Amazon API Gateway. - */ -@SmithyUnstableApi -package software.amazon.smithy.aws.apigateway.openapi; - -import software.amazon.smithy.utils.SmithyUnstableApi; diff --git a/smithy-aws-apigateway-openapi/src/test/java/software/amazon/smithy/aws/apigateway/openapi/CloudFormationSubstitutionTest.java b/smithy-aws-apigateway-openapi/src/test/java/software/amazon/smithy/aws/apigateway/openapi/CloudFormationSubstitutionTest.java index 32cbf0bdefe..5b6789ebeeb 100644 --- a/smithy-aws-apigateway-openapi/src/test/java/software/amazon/smithy/aws/apigateway/openapi/CloudFormationSubstitutionTest.java +++ b/smithy-aws-apigateway-openapi/src/test/java/software/amazon/smithy/aws/apigateway/openapi/CloudFormationSubstitutionTest.java @@ -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; @@ -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); diff --git a/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/JsonSchemaConfig.java b/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/JsonSchemaConfig.java index 5801ba3b340..98d7a81bc62 100644 --- a/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/JsonSchemaConfig.java +++ b/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/JsonSchemaConfig.java @@ -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; @@ -68,8 +68,14 @@ public String toString() { private UnionStrategy unionStrategy = UnionStrategy.ONE_OF; private String definitionPointer = "#/definitions"; private ObjectNode schemaDocumentExtensions = Node.objectNode(); - private Map extensions = new HashMap<>(); + private ObjectNode extensions = Node.objectNode(); private Set disableFeatures = new HashSet<>(); + private final ConcurrentHashMap extensionCache = new ConcurrentHashMap<>(); + private final NodeMapper nodeMapper = new NodeMapper(); + + public JsonSchemaConfig() { + nodeMapper.setWhenMissingSetter(NodeMapper.WhenMissing.INGORE); + } public boolean getAlphanumericOnlyRefs() { return alphanumericOnlyRefs; @@ -181,17 +187,79 @@ public void setDisableFeatures(Set disableFeatures) { this.disableFeatures = disableFeatures; } - public Map getExtensions() { + public ObjectNode getExtensions() { return extensions; } + /** + * Attempts to deserialize the {@code extensions} into the targeted + * type using a {@link NodeMapper}. + * + *

Extraneous properties are ignored and not warned on + * because many different plugins could be used with different + * configuration POJOs. + * + *

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 Type to deserialize extensions into. + * @return Returns the deserialized type. + */ + @SuppressWarnings("unchecked") + public T getExtensions(Class 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 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)); } } diff --git a/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/JsonSchemaConverter.java b/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/JsonSchemaConverter.java index 06154779b1d..33ce7f77152 100644 --- a/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/JsonSchemaConverter.java +++ b/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/JsonSchemaConverter.java @@ -54,7 +54,7 @@ public final class JsonSchemaConverter implements ToSmithyBuilder shapePredicate; private final RefStrategy refStrategy; private final List realizedMappers; @@ -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. * diff --git a/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/Schema.java b/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/Schema.java index f6c76dd50e3..237c6eeb264 100644 --- a/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/Schema.java +++ b/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/Schema.java @@ -791,6 +791,12 @@ public Builder examples(Node examples) { return this; } + public Builder extensions(Map extensions) { + this.extensions.clear(); + this.extensions.putAll(extensions); + return this; + } + public Builder putExtension(String key, ToNode value) { extensions.put(key, value); return this; diff --git a/smithy-jsonschema/src/test/java/software/amazon/smithy/jsonschema/JsonSchemaConverterTest.java b/smithy-jsonschema/src/test/java/software/amazon/smithy/jsonschema/JsonSchemaConverterTest.java index f0a78b55bf9..c9268d722b1 100644 --- a/smithy-jsonschema/src/test/java/software/amazon/smithy/jsonschema/JsonSchemaConverterTest.java +++ b/smithy-jsonschema/src/test/java/software/amazon/smithy/jsonschema/JsonSchemaConverterTest.java @@ -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; + } + } } diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/node/NodeMapper.java b/smithy-model/src/main/java/software/amazon/smithy/model/node/NodeMapper.java index d335dc892d7..74ab6fc1e7f 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/node/NodeMapper.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/node/NodeMapper.java @@ -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) { + } }; /** diff --git a/smithy-openapi/src/main/java/software/amazon/smithy/openapi/OpenApiConfig.java b/smithy-openapi/src/main/java/software/amazon/smithy/openapi/OpenApiConfig.java new file mode 100644 index 00000000000..768760fa358 --- /dev/null +++ b/smithy-openapi/src/main/java/software/amazon/smithy/openapi/OpenApiConfig.java @@ -0,0 +1,287 @@ +/* + * 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.openapi; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import software.amazon.smithy.jsonschema.JsonSchemaConfig; +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.openapi.fromsmithy.OpenApiProtocol; +import software.amazon.smithy.openapi.fromsmithy.Smithy2OpenApiExtension; +import software.amazon.smithy.utils.ListUtils; + +/** + * "openapi" smithy-build plugin configuration settings. + */ +public class OpenApiConfig extends JsonSchemaConfig { + + /** The supported version of OpenAPI. */ + public static final String VERSION = "3.0.2"; + + /** Specifies what to do when the httpPrefixHeaders trait is found in a model. */ + public enum HttpPrefixHeadersStrategy { + /** The default setting that causes the build to fail. */ + FAIL, + + /** The header is omitted from the OpenAPI model and a warning is logged. */ + WARN + } + + /** The JSON pointer to where OpenAPI schema components should be written. */ + private static final String SCHEMA_COMPONENTS_POINTER = "#/components/schemas"; + + private ShapeId service; + private ShapeId protocol; + private boolean tags; + private List supportedTags = Collections.emptyList(); + private String defaultBlobFormat = "byte"; + private boolean keepUnusedComponents; + private String jsonContentType = "application/json"; + private boolean forbidGreedyLabels; + private HttpPrefixHeadersStrategy onHttpPrefixHeaders = HttpPrefixHeadersStrategy.FAIL; + private boolean ignoreUnsupportedTraits; + private Map substitutions = Collections.emptyMap(); + private Map jsonAdd = Collections.emptyMap(); + private List externalDocs = ListUtils.of( + "Homepage", "API Reference", "User Guide", "Developer Guide", "Reference", "Guide"); + + public OpenApiConfig() { + super(); + setDefinitionPointer(SCHEMA_COMPONENTS_POINTER); + } + + public ShapeId getService() { + return service; + } + + /** + * Sets the service shape ID to convert. + * + *

For example, smithy.example#Weather. + * + * @param service the Smithy service shape ID to convert. + */ + public void setService(ShapeId service) { + this.service = service; + } + + public ShapeId getProtocol() { + return protocol; + } + + /** + * Sets the protocol shape ID to use when converting Smithy to OpenAPI. + * + *

For example, aws.protocols#restJson1. + * + *

Smithy will try to match the provided protocol name with an + * implementation of {@link OpenApiProtocol} registered with a + * service provider implementation of {@link Smithy2OpenApiExtension}. + * + *

This property is required if a service supports multiple protocols. + * + * @param protocol The protocol shape ID to use. + */ + public void setProtocol(ShapeId protocol) { + this.protocol = protocol; + } + + public String getDefaultBlobFormat() { + return defaultBlobFormat; + } + + /** + * Sets the default OpenAPI format property used when converting blob + * shapes in Smithy to strings in OpenAPI. + * + *

Defaults to "byte", meaning Base64 encoded. + * + * @param defaultBlobFormat Default blob OpenAPI format to use. + */ + public void setDefaultBlobFormat(String defaultBlobFormat) { + this.defaultBlobFormat = Objects.requireNonNull(defaultBlobFormat); + } + + public boolean getTags() { + return tags; + } + + /** + * Sets whether or not to include Smithy tags in the result as + * OpenAPI tags. + * + * @param tags Set to true to enable tags. + */ + public void setTags(boolean tags) { + this.tags = tags; + } + + public List getSupportedTags() { + return supportedTags; + } + + /** + * Limits the exported {@code tags} to a specific set of tags. + * + *

The value must be a list of strings. This property requires that + * {@link #getTags()} is set to true in order to have an effect. + * + * @param supportedTags The set of tags to export. + */ + public void setSupportedTags(List supportedTags) { + this.supportedTags = Objects.requireNonNull(supportedTags); + } + + public boolean getKeepUnusedComponents() { + return keepUnusedComponents; + } + + /** + * Set to true to prevent unused OpenAPI components from being + * removed from the created specification. + * + * @param keepUnusedComponents Set to true to keep unused components. + */ + public void setKeepUnusedComponents(boolean keepUnusedComponents) { + this.keepUnusedComponents = keepUnusedComponents; + } + + public String getJsonContentType() { + return jsonContentType; + } + + /** + * Sets a custom media-type to associate with the JSON payload of + * JSON-based protocols. + * + * @param jsonContentType Content-Type to use for JSON protocols by default. + */ + public void setJsonContentType(String jsonContentType) { + this.jsonContentType = Objects.requireNonNull(jsonContentType); + } + + public boolean getForbidGreedyLabels() { + return forbidGreedyLabels; + } + + /** + * Set to true to forbid greedy URI labels. + * + *

By default, greedy labels will appear as-is in the path generated + * for an operation. For example, {@code "/{foo+}"}. + * + * @param forbidGreedyLabels Set to true to forbid greedy labels. + */ + public void setForbidGreedyLabels(boolean forbidGreedyLabels) { + this.forbidGreedyLabels = forbidGreedyLabels; + } + + public HttpPrefixHeadersStrategy getOnHttpPrefixHeaders() { + return onHttpPrefixHeaders; + } + + /** + * Specifies what to do when the {@code }httpPrefixHeaders} trait is + * found in a model. + * + *

OpenAPI does not support httpPrefixHeaders. By default, the + * conversion will fail when this trait is encountered, but this + * behavior can be customized. By default, the conversion fails when + * prefix headers are encountered. + * + * @param onHttpPrefixHeaders Strategy to use for prefix headers. + */ + public void setOnHttpPrefixHeaders(HttpPrefixHeadersStrategy onHttpPrefixHeaders) { + this.onHttpPrefixHeaders = Objects.requireNonNull(onHttpPrefixHeaders); + } + + public boolean getIgnoreUnsupportedTraits() { + return ignoreUnsupportedTraits; + } + + /** + * Set to true to emit warnings rather than failing when unsupported + * traits like {@code endpoint} and {@code hostLabel} are encountered. + * + * @param ignoreUnsupportedTraits True to ignore unsupported traits. + */ + public void setIgnoreUnsupportedTraits(boolean ignoreUnsupportedTraits) { + this.ignoreUnsupportedTraits = ignoreUnsupportedTraits; + } + + public Map getSubstitutions() { + return substitutions; + } + + /** + * Defines a map of strings to any JSON value to find and replace in the + * generated OpenAPI model. + * + *

String values are replaced if the string in its entirety matches one + * of the keys provided in the {@code substitutions} map. The + * corresponding value is then substituted for the string; this could + * even result in a string changing into an object, array, etc. + * + * @param substitutions Map of substitutions. + */ + public void setSubstitutions(Map substitutions) { + this.substitutions = Objects.requireNonNull(substitutions); + } + + public Map getJsonAdd() { + return jsonAdd; + } + + /** + * Adds or replaces the JSON value in the generated OpenAPI document + * at the given JSON pointer locations with a different JSON value. + * + *

The value must be a map where each key is a valid JSON pointer + * string as defined in RFC 6901. Each value in the map is the JSON + * value to add or replace at the given target. + * + *

Values are added using similar semantics of the "add" operation + * of JSON Patch, as specified in RFC 6902, with the exception that + * adding properties to an undefined object will create nested + * objects in the result as needed. + * + * @param jsonAdd Map of JSON path to values to patch in. + */ + public void setJsonAdd(Map jsonAdd) { + this.jsonAdd = Objects.requireNonNull(jsonAdd); + } + + public List getExternalDocs() { + return externalDocs; + } + + /** + * Limits the source of converted "externalDocs" fields to the specified + * priority ordered list of names in an externalDocumentation trait. + * + *

This list is case insensitive. By default, this is a list of the + * following values: "Homepage", "API Reference", "User Guide", + * "Developer Guide", "Reference", and "Guide". + * + * @param externalDocs External docs to look for and convert, in order. + */ + public void setExternalDocs(List externalDocs) { + this.externalDocs = Objects.requireNonNull(externalDocs); + } +} diff --git a/smithy-openapi/src/main/java/software/amazon/smithy/openapi/OpenApiConstants.java b/smithy-openapi/src/main/java/software/amazon/smithy/openapi/OpenApiConstants.java deleted file mode 100644 index 4a009b7b73c..00000000000 --- a/smithy-openapi/src/main/java/software/amazon/smithy/openapi/OpenApiConstants.java +++ /dev/null @@ -1,146 +0,0 @@ -/* - * Copyright 2019 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.openapi; - -import java.util.List; -import software.amazon.smithy.build.JsonSubstitutions; -import software.amazon.smithy.model.node.ArrayNode; -import software.amazon.smithy.utils.ListUtils; - -public final class OpenApiConstants { - /** The supported version of OpenAPI. */ - public static final String VERSION = "3.0.2"; - - /** The Smithy service Shape ID to convert. */ - public static final String SERVICE = "service"; - - /** The protocol trait shape ID to use when converting Smithy to OpenAPI. */ - public static final String PROTOCOL = "protocol"; - - /** Whether or not to include tags in the result. */ - public static final String OPEN_API_TAGS = "openapi.tags"; - - /** Limits the exported tags to a specific set of tags. The value must be an {@link ArrayNode} of Strings. */ - public static final String OPEN_API_SUPPORTED_TAGS = "openapi.supportedTags"; - - /** - * Configures JSON schema shapes to use OpenAPI features and not use - * unsupported OpenAPI features. - * - * @see Supported JSON Schema Keywords - */ - public static final String OPEN_API_MODE = "openapi.mode"; - - /** Adds support for "nullable" from OpenAPI, added for any boxed primitive. */ - public static final String OPEN_API_USE_NULLABLE = "openapi.use.nullable"; - - /** Adds support for OpenAPI's "deprecated" keyword. */ - public static final String OPEN_API_USE_DEPRECATED = "openapi.use.deprecated"; - - /** Adds support for OpenAPI's "externalDocs" keyword. */ - public static final String OPEN_API_USE_EXTERNAL_DOCS = "openapi.use.externalDocs"; - - /** - * Limits the source of converted "externalDocs" fields to the specified - * priority ordered list of names in an {@code externalDocumentation} - * trait. This list is case insensitive. The value must be an - * {@link ArrayNode} of Strings. - * - * By default, this is a list of the following values: "homepage", "api reference", - * "user guide", "developer guide", "reference", and "guide". - */ - public static final String OPEN_API_CONVERTED_EXTERNAL_DOCS = "openapi.externalDocs"; - - /** The default set of converted "externalDocs" entry names when enabled. */ - public static final List OPEN_API_DEFAULT_CONVERTED_EXTERNAL_DOCS = - ListUtils.of("homepage", "api reference", "user guide", "developer guide", "reference", "guide"); - - /** Adds support for OpenAPI's custom JSON Schema formats. */ - public static final String OPEN_API_USE_FORMATS = "openapi.use.formats"; - - /** - * Sets the default format of blob shapes when used with {@link #OPEN_API_USE_FORMATS}. - * - *

Defaults to "byte", meaning Base64 encoded.

- */ - public static final String OPEN_API_DEFAULT_BLOB_FORMAT = "openapi.defaultBlobFormat"; - - /** Adds support for "xml" from OpenAPI. TODO: Not implemented. */ - public static final String OPEN_API_USE_XML = "openapi.use.xml"; - - /** The JSON pointer to where OpenAPI schema components should be written. */ - public static final String SCHEMA_COMPONENTS_POINTER = "#/components/schemas"; - - /** Set to true to prevent unused components from being removed from the artifact. */ - public static final String OPENAPI_KEEP_UNUSED_COMPONENTS = "openapi.keepUnusedComponents"; - - /** The content-type to use with aws.json and aws.rest-json protocols. */ - public static final String AWS_JSON_CONTENT_TYPE = "openapi.aws.jsonContentType"; - - /** - * Defines if greedy URI path labels are forbidden. By default, greedy - * labels will appear as-is in the path generated for an operation. - * For example, "/{foo+}" - */ - public static final String FORBID_GREEDY_LABELS = "openapi.forbidGreedyLabels"; - - /** - * Determines what to do when the {@code httpPrefixHeaders} trait is found - * in a model. OpenAPI does not support httpPrefixHeaders. By default, the - * conversion will fail. This setting must be set to a string. The following - * string values are supported generally, though additional values may be - * supported by other mappers or protocols. - * - * - */ - public static final String ON_HTTP_PREFIX_HEADERS = "openapi.onHttpPrefixHeaders"; - public static final String ON_HTTP_PREFIX_HEADERS_FAIL = "FAIL"; - public static final String ON_HTTP_PREFIX_HEADERS_WARN = "WARN"; - - /** Ignores unsupported trait like eventStream and logs them rather than fail. */ - public static final String IGNORE_UNSUPPORTED_TRAITS = "openapi.ignoreUnsupportedTraits"; - - /** - * Defines a map of String to any Node value to find and replace in the - * generated OpenAPI model. - * - *

String values are replaced if the string in its entirety matches - * one of the keys provided in the {@code openapi.substitutions} map. The - * corresponding value is then substituted for the string-- this could even - * result in a string changing into an object, array, etc. - * - * @see JsonSubstitutions - */ - public static final String SUBSTITUTIONS = "openapi.substitutions"; - - /** - * Adds or replaces the JSON value at the given JSON pointer locations with a - * different JSON value. - * - *

The value must be a Map of String to Node where each key is a JSON - * Pointer, and each value is the value to replace at that location. The - * mutation of the model follows the same semantics as the "add" operation - * of JSON Patch as specified in RFC 6902, with the exception that missing - * intermediate objects are created as necessary. Attempting to modifyproperties - * of objects that do not exist will log a warning. - */ - public static final String JSON_ADD = "openapi.jsonAdd"; - - private OpenApiConstants() {} -} diff --git a/smithy-openapi/src/main/java/software/amazon/smithy/openapi/fromsmithy/Context.java b/smithy-openapi/src/main/java/software/amazon/smithy/openapi/fromsmithy/Context.java index bf10dc03b62..e16c6f42b22 100644 --- a/smithy-openapi/src/main/java/software/amazon/smithy/openapi/fromsmithy/Context.java +++ b/smithy-openapi/src/main/java/software/amazon/smithy/openapi/fromsmithy/Context.java @@ -24,18 +24,18 @@ import software.amazon.smithy.jsonschema.Schema; import software.amazon.smithy.jsonschema.SchemaDocument; import software.amazon.smithy.model.Model; -import software.amazon.smithy.model.node.ObjectNode; import software.amazon.smithy.model.shapes.MemberShape; import software.amazon.smithy.model.shapes.ServiceShape; import software.amazon.smithy.model.shapes.ToShapeId; import software.amazon.smithy.model.traits.Trait; -import software.amazon.smithy.openapi.OpenApiConstants; +import software.amazon.smithy.openapi.OpenApiConfig; import software.amazon.smithy.openapi.OpenApiException; /** * Smithy to OpenAPI conversion context object. */ public final class Context { + private final Model model; private final ServiceShape service; private final JsonSchemaConverter jsonSchemaConverter; @@ -44,10 +44,12 @@ public final class Context { private final SchemaDocument schemas; private final List> securitySchemeConverters; private Map synthesizedSchemas = Collections.synchronizedMap(new TreeMap<>()); + private OpenApiConfig config; - public Context( + Context( Model model, ServiceShape service, + OpenApiConfig config, JsonSchemaConverter jsonSchemaConverter, OpenApiProtocol openApiProtocol, SchemaDocument schemas, @@ -55,6 +57,7 @@ public Context( ) { this.model = model; this.service = service; + this.config = config; this.jsonSchemaConverter = jsonSchemaConverter; this.protocolTrait = service.expectTrait(openApiProtocol.getProtocolType()); this.openApiProtocol = openApiProtocol; @@ -81,14 +84,14 @@ public ServiceShape getService() { } /** - * Gets the queryable configuration object used for the conversion. + * Gets the configuration object used for the conversion. * *

Plugins can query this object for configuration values. * * @return Returns the configuration object. */ - public ObjectNode getConfig() { - return jsonSchemaConverter.getConfig(); + public OpenApiConfig getConfig() { + return config; } /** @@ -227,6 +230,6 @@ public Map getSynthesizedSchemas() { */ public String putSynthesizedSchema(String name, Schema schema) { synthesizedSchemas.put(Objects.requireNonNull(name), Objects.requireNonNull(schema)); - return OpenApiConstants.SCHEMA_COMPONENTS_POINTER + "/" + name; + return config.getDefinitionPointer() + "/" + name; } } 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 72f0a97e09c..599962f7046 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 @@ -17,6 +17,7 @@ import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.LinkedHashSet; @@ -28,7 +29,7 @@ import java.util.logging.Logger; import java.util.stream.Collectors; import java.util.stream.Stream; -import software.amazon.smithy.jsonschema.JsonSchemaConstants; +import software.amazon.smithy.jsonschema.JsonSchemaConfig; import software.amazon.smithy.jsonschema.JsonSchemaConverter; import software.amazon.smithy.jsonschema.JsonSchemaMapper; import software.amazon.smithy.jsonschema.Schema; @@ -36,10 +37,7 @@ import software.amazon.smithy.model.Model; import software.amazon.smithy.model.knowledge.ServiceIndex; import software.amazon.smithy.model.knowledge.TopDownIndex; -import software.amazon.smithy.model.node.Node; import software.amazon.smithy.model.node.ObjectNode; -import software.amazon.smithy.model.node.StringNode; -import software.amazon.smithy.model.node.ToNode; import software.amazon.smithy.model.shapes.OperationShape; import software.amazon.smithy.model.shapes.ServiceShape; import software.amazon.smithy.model.shapes.Shape; @@ -48,7 +46,7 @@ import software.amazon.smithy.model.traits.TitleTrait; import software.amazon.smithy.model.traits.Trait; import software.amazon.smithy.model.validation.ValidationUtils; -import software.amazon.smithy.openapi.OpenApiConstants; +import software.amazon.smithy.openapi.OpenApiConfig; import software.amazon.smithy.openapi.OpenApiException; import software.amazon.smithy.openapi.model.ComponentsObject; import software.amazon.smithy.openapi.model.InfoObject; @@ -70,9 +68,8 @@ public final class OpenApiConverter { private static final Logger LOGGER = Logger.getLogger(OpenApiConverter.class.getName()); - private Map settings = new HashMap<>(); private ClassLoader classLoader = OpenApiConverter.class.getClassLoader(); - private JsonSchemaConverter jsonSchemaConverter; + private OpenApiConfig config = new OpenApiConfig(); private final List mappers = new ArrayList<>(); private OpenApiConverter() {} @@ -82,13 +79,25 @@ public static OpenApiConverter create() { } /** - * Set the converter used to build Smithy shapes. + * Get the OpenAPI configuration settings. * - * @param jsonSchemaConverter Shape converter to use. - * @return Returns the OpenApiConverter. + * @return Returns the config object. + */ + public OpenApiConfig getConfig() { + return config; + } + + /** + * Set the OpenAPI configuration settings. + * + *

This also updates the configuration object of any previously set + * {@link JsonSchemaConfig}. + * + * @param config Config object to set. + * @return Returns the converter. */ - public OpenApiConverter jsonSchemaConverter(JsonSchemaConverter jsonSchemaConverter) { - this.jsonSchemaConverter = jsonSchemaConverter; + public OpenApiConverter config(OpenApiConfig config) { + this.config = config; return this; } @@ -107,55 +116,6 @@ public OpenApiConverter addOpenApiMapper(OpenApiMapper mapper) { return this; } - /** - * Puts a setting on the converter. - * - * @param setting Setting name to set. - * @param value Setting value to set. - * @param value type to set. - * @return Returns the OpenApiConverter. - */ - public OpenApiConverter putSetting(String setting, T value) { - settings.put(setting, value.toNode()); - return this; - } - - /** - * Puts a setting on the converter. - * - * @param setting Setting name to set. - * @param value Setting value to set. - * @return Returns the OpenApiConverter. - */ - public OpenApiConverter putSetting(String setting, String value) { - settings.put(setting, Node.from(value)); - return this; - } - - /** - * Puts a setting on the converter. - * - * @param setting Setting name to set. - * @param value Setting value to set. - * @return Returns the OpenApiConverter. - */ - public OpenApiConverter putSetting(String setting, Number value) { - settings.put(setting, Node.from(value)); - return this; - } - - /** - * Puts a setting on the converter. - * - * @param setting Setting name to set. - * @param value Setting value to set. - * @return Returns the OpenApiConverter. - */ - public OpenApiConverter putSetting(String setting, boolean value) { - settings.put(setting, Node.from(value)); - return this; - } - /** * Sets a {@link ClassLoader} to use to discover {@link JsonSchemaMapper}, * {@link OpenApiMapper}, and {@link OpenApiProtocol} service providers @@ -171,16 +131,6 @@ public OpenApiConverter classLoader(ClassLoader classLoader) { return this; } - /** - * Sets the protocol trait to use when converting the model. - * - * @param protocolTraitId Protocol to use when converting. - * @return Returns the OpenApiConverter. - */ - public OpenApiConverter protocolTraitId(ShapeId protocolTraitId) { - return putSetting(OpenApiConstants.PROTOCOL, protocolTraitId.toString()); - } - /** * Converts the given service shape to OpenAPI model using the given * Smithy model. @@ -232,12 +182,6 @@ private ConversionEnvironment createConversionEnvironment(Model } } - // Update the JSON schema config with the settings from this class and - // configure it to use OpenAPI settings. - ObjectNode.Builder configBuilder = Node.objectNodeBuilder() - .withMember(OpenApiConstants.OPEN_API_MODE, true) - .withMember(JsonSchemaConstants.DEFINITION_POINTER, OpenApiConstants.SCHEMA_COMPONENTS_POINTER); - // Find the service shape. ServiceShape service = model.getShape(serviceShapeId) .orElseThrow(() -> new IllegalArgumentException(String.format( @@ -246,13 +190,11 @@ private ConversionEnvironment createConversionEnvironment(Model .orElseThrow(() -> new IllegalArgumentException(String.format( "Shape `%s` is not a service shape", serviceShapeId))); - settings.forEach(configBuilder::withMember); - ObjectNode config = configBuilder.build(); - Trait protocolTrait = loadOrDeriveProtocolTrait(model, service, config); + Trait protocolTrait = loadOrDeriveProtocolTrait(model, service); OpenApiProtocol openApiProtocol = loadOpenApiProtocol(service, protocolTrait, extensions); - // Merge in protocol default values. - config = openApiProtocol.getDefaultSettings().merge(config); + // Update with protocol default values. + openApiProtocol.updateDefaultSettings(config); jsonSchemaConverterBuilder.config(config); // Only convert shapes in the closure of the targeted service. @@ -263,7 +205,7 @@ private ConversionEnvironment createConversionEnvironment(Model // Populate component schemas from the built document. for (Map.Entry entry : document.getDefinitions().entrySet()) { - String key = entry.getKey().replace(OpenApiConstants.SCHEMA_COMPONENTS_POINTER + "/", ""); + String key = entry.getKey().replace(config.getDefinitionPointer() + "/", ""); components.putSchema(key, entry.getValue()); } @@ -272,7 +214,7 @@ private ConversionEnvironment createConversionEnvironment(Model model, service, extensions); Context context = new Context<>( - model, service, jsonSchemaConverter, + model, service, config, jsonSchemaConverter, openApiProtocol, document, securitySchemeConverters); return new ConversionEnvironment<>(context, extensions, components, mappers); @@ -285,12 +227,12 @@ private ConversionEnvironment createConversionEnvironment(Model // // If the derived protocol trait cannot be found on the service, an exception // is thrown. - private Trait loadOrDeriveProtocolTrait(Model model, ServiceShape service, ObjectNode config) { + private Trait loadOrDeriveProtocolTrait(Model model, ServiceShape service) { ServiceIndex serviceIndex = model.getKnowledge(ServiceIndex.class); Set serviceProtocols = serviceIndex.getProtocols(service).keySet(); - if (config.getMember(OpenApiConstants.PROTOCOL).isPresent()) { - ShapeId protocolTraitId = config.expectStringMember(OpenApiConstants.PROTOCOL).expectShapeId(); + if (config.getProtocol() != null) { + ShapeId protocolTraitId = config.getProtocol(); return service.findTrait(protocolTraitId).orElseThrow(() -> { return new OpenApiException(String.format( "Unable to find protocol `%s` on service `%s`. This service supports the following " @@ -341,7 +283,7 @@ private OpenApi convertWithEnvironment(ConversionEnvironment context = environment.context; OpenApiMapper mapper = environment.mapper; OpenApiProtocol openApiProtocol = environment.context.getOpenApiProtocol(); - OpenApi.Builder openapi = OpenApi.builder().openapi(OpenApiConstants.VERSION).info(createInfo(service)); + OpenApi.Builder openapi = OpenApi.builder().openapi(OpenApiConfig.VERSION).info(createInfo(service)); mapper.before(context, openapi); @@ -350,10 +292,8 @@ private OpenApi convertWithEnvironment(ConversionEnvironment { - openapi.addTag(TagObject.builder().name(tag).build()); - }); + for (String tag : getSupportedTags(service)) { + openapi.addTag(TagObject.builder().name(tag).build()); } addPaths(context, openapi, openApiProtocol, mapper); @@ -367,9 +307,7 @@ private OpenApi convertWithEnvironment(ConversionEnvironment> loadSecuritySchemes( } // Gets the tags of a shape that are allowed in the OpenAPI model. - private Stream getSupportedTags(ObjectNode config, Tagged tagged) { - List supported = config.getArrayMember(OpenApiConstants.OPEN_API_SUPPORTED_TAGS) - .map(array -> array.getElementsAs(StringNode::getValue)) - .orElse(null); + private List getSupportedTags(Tagged tagged) { + if (!config.getTags()) { + return Collections.emptyList(); + } + List supported = config.getSupportedTags(); return tagged.getTags() .stream() - .filter(tag -> supported == null || supported.contains(tag)); + .filter(tag -> supported == null || supported.contains(tag)) + .collect(Collectors.toList()); } private InfoObject createInfo(ServiceShape service) { @@ -579,14 +519,9 @@ private OperationObject addOperationTags( Shape shape, OperationObject operation ) { - ObjectNode config = context.getConfig(); - // Include @tags trait tags of the operation that are compatible with OpenAPI settings. - if (context.getConfig().getBooleanMemberOrDefault(OpenApiConstants.OPEN_API_TAGS)) { - List tags = getSupportedTags(config, shape).collect(Collectors.toList()); - if (!tags.isEmpty()) { - return operation.toBuilder().tags(tags).build(); - } + if (context.getConfig().getTags()) { + return operation.toBuilder().tags(getSupportedTags(shape)).build(); } return operation; diff --git a/smithy-openapi/src/main/java/software/amazon/smithy/openapi/fromsmithy/OpenApiJsonSchemaMapper.java b/smithy-openapi/src/main/java/software/amazon/smithy/openapi/fromsmithy/OpenApiJsonSchemaMapper.java index c9517a2e143..c1456619b85 100644 --- a/smithy-openapi/src/main/java/software/amazon/smithy/openapi/fromsmithy/OpenApiJsonSchemaMapper.java +++ b/smithy-openapi/src/main/java/software/amazon/smithy/openapi/fromsmithy/OpenApiJsonSchemaMapper.java @@ -17,126 +17,100 @@ import static java.util.function.Function.identity; +import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Optional; import java.util.Set; -import java.util.logging.Logger; -import java.util.stream.Collectors; +import software.amazon.smithy.jsonschema.JsonSchemaConfig; import software.amazon.smithy.jsonschema.JsonSchemaMapper; import software.amazon.smithy.jsonschema.Schema; import software.amazon.smithy.model.node.Node; -import software.amazon.smithy.model.node.ObjectNode; -import software.amazon.smithy.model.node.StringNode; import software.amazon.smithy.model.shapes.Shape; import software.amazon.smithy.model.traits.BoxTrait; import software.amazon.smithy.model.traits.DeprecatedTrait; import software.amazon.smithy.model.traits.ExternalDocumentationTrait; import software.amazon.smithy.model.traits.SensitiveTrait; -import software.amazon.smithy.openapi.OpenApiConstants; +import software.amazon.smithy.openapi.OpenApiConfig; import software.amazon.smithy.openapi.model.ExternalDocumentation; import software.amazon.smithy.utils.MapUtils; import software.amazon.smithy.utils.SetUtils; /** - * Applies OpenAPI extensions to a {@link Schema}. + * Applies OpenAPI extensions to a {@link Schema} using configuration settings + * found in {@link OpenApiConfig}. * - *

This mapper understands the following setting: - * - *

+ *

Note: the properties and features added by this mapper can be removed using + * {@link OpenApiConfig#setDisableFeatures}. */ public class OpenApiJsonSchemaMapper implements JsonSchemaMapper { - private static final Logger LOGGER = Logger.getLogger(OpenApiJsonSchemaMapper.class.getName()); - private static final String DEFAULT_BLOB_FORMAT = "byte"; /** See https://swagger.io/docs/specification/data-models/keywords/. */ private static final Set UNSUPPORTED_KEYWORD_DIRECTIVES = SetUtils.of( - "disable.propertyNames", - "disable.contentMediaType"); + "propertyNames", + "contentMediaType"); @Override - public Schema.Builder updateSchema(Shape shape, Schema.Builder builder, ObjectNode config) { - boolean enabled = config.getBooleanMemberOrDefault(OpenApiConstants.OPEN_API_MODE); - - if (enabled || config.getBooleanMemberOrDefault(OpenApiConstants.OPEN_API_USE_EXTERNAL_DOCS)) { - getResolvedExternalDocs(shape, config) - .map(ExternalDocumentation::toNode) - .ifPresent(docs -> builder.putExtension("externalDocs", docs)); - } + public Schema.Builder updateSchema(Shape shape, Schema.Builder builder, JsonSchemaConfig config) { + getResolvedExternalDocs(shape, config) + .map(ExternalDocumentation::toNode) + .ifPresent(docs -> builder.putExtension("externalDocs", docs)); - if (enabled || config.getBooleanMemberOrDefault(OpenApiConstants.OPEN_API_USE_NULLABLE)) { - if (shape.hasTrait(BoxTrait.class)) { - builder.putExtension("nullable", Node.from(true)); - } + if (shape.hasTrait(BoxTrait.class)) { + builder.putExtension("nullable", Node.from(true)); } - if (enabled || config.getBooleanMemberOrDefault(OpenApiConstants.OPEN_API_USE_DEPRECATED)) { - if (shape.hasTrait(DeprecatedTrait.class)) { - builder.putExtension("deprecated", Node.from(true)); - } + if (shape.hasTrait(DeprecatedTrait.class)) { + builder.putExtension("deprecated", Node.from(true)); } - if (enabled || config.getBooleanMemberOrDefault(OpenApiConstants.OPEN_API_USE_FORMATS)) { - // Don't overwrite an existing format setting. - if (!builder.getFormat().isPresent()) { - if (shape.isIntegerShape()) { - builder.format("int32"); - } else if (shape.isLongShape()) { - builder.format("int64"); - } else if (shape.isFloatShape()) { - builder.format("float"); - } else if (shape.isDoubleShape()) { - builder.format("double"); - } else if (shape.isBlobShape()) { - return builder.format(config.getStringMemberOrDefault( - OpenApiConstants.OPEN_API_DEFAULT_BLOB_FORMAT, DEFAULT_BLOB_FORMAT)); - } else if (shape.hasTrait(SensitiveTrait.class)) { - builder.format("password"); + // Don't overwrite an existing format setting. + if (!builder.getFormat().isPresent()) { + if (shape.isIntegerShape()) { + builder.format("int32"); + } else if (shape.isLongShape()) { + builder.format("int64"); + } else if (shape.isFloatShape()) { + builder.format("float"); + } else if (shape.isDoubleShape()) { + builder.format("double"); + } else if (shape.isBlobShape()) { + if (config instanceof OpenApiConfig) { + String blobFormat = ((OpenApiConfig) config).getDefaultBlobFormat(); + return builder.format(blobFormat); } + } else if (shape.hasTrait(SensitiveTrait.class)) { + builder.format("password"); } } - if (config.getBooleanMemberOrDefault(OpenApiConstants.OPEN_API_USE_XML)) { - LOGGER.warning(OpenApiConstants.OPEN_API_USE_XML + " is not yet implemented"); - } - // Remove unsupported JSON Schema keywords. - if (enabled) { - UNSUPPORTED_KEYWORD_DIRECTIVES.forEach(builder::disableProperty); - } + UNSUPPORTED_KEYWORD_DIRECTIVES.forEach(builder::disableProperty); return builder; } - static Optional getResolvedExternalDocs(Shape shape, ObjectNode config) { + static Optional getResolvedExternalDocs(Shape shape, JsonSchemaConfig config) { Optional traitOptional = shape.getTrait(ExternalDocumentationTrait.class); - if (!traitOptional.isPresent()) { + + if (!traitOptional.isPresent() || !(config instanceof OpenApiConfig)) { return Optional.empty(); } + OpenApiConfig openApiConfig = (OpenApiConfig) config; + // Get the valid list of lower case names to look for when converting. - List externalDocKeys = config.getArrayMember(OpenApiConstants.OPEN_API_CONVERTED_EXTERNAL_DOCS) - .map(node -> node.getElementsAs(StringNode::getValue)) - .orElse(OpenApiConstants.OPEN_API_DEFAULT_CONVERTED_EXTERNAL_DOCS) - .stream() - .map(s -> s.toLowerCase(Locale.US)) - .collect(Collectors.toList()); + List externalDocKeys = new ArrayList<>(openApiConfig.getExternalDocs().size()); + for (String key : openApiConfig.getExternalDocs()) { + externalDocKeys.add(key.toLowerCase(Locale.ENGLISH)); + } // Get lower case keys to check for when converting. Map traitUrls = traitOptional.get().getUrls(); Map lowercaseKeyMap = traitUrls.keySet().stream() .collect(MapUtils.toUnmodifiableMap(i -> i.toLowerCase(Locale.US), identity())); + for (String externalDocKey : externalDocKeys) { // Compare the lower case name, but use the specified name. if (lowercaseKeyMap.containsKey(externalDocKey)) { diff --git a/smithy-openapi/src/main/java/software/amazon/smithy/openapi/fromsmithy/OpenApiProtocol.java b/smithy-openapi/src/main/java/software/amazon/smithy/openapi/fromsmithy/OpenApiProtocol.java index 673745cf445..35c8b9c14ca 100644 --- a/smithy-openapi/src/main/java/software/amazon/smithy/openapi/fromsmithy/OpenApiProtocol.java +++ b/smithy-openapi/src/main/java/software/amazon/smithy/openapi/fromsmithy/OpenApiProtocol.java @@ -18,13 +18,12 @@ import java.util.Optional; import java.util.Set; import software.amazon.smithy.model.knowledge.HttpBindingIndex; -import software.amazon.smithy.model.node.Node; -import software.amazon.smithy.model.node.ObjectNode; import software.amazon.smithy.model.pattern.UriPattern; import software.amazon.smithy.model.shapes.OperationShape; import software.amazon.smithy.model.shapes.ToShapeId; import software.amazon.smithy.model.traits.HttpTrait; import software.amazon.smithy.model.traits.Trait; +import software.amazon.smithy.openapi.OpenApiConfig; import software.amazon.smithy.openapi.OpenApiException; import software.amazon.smithy.openapi.model.OpenApi; import software.amazon.smithy.openapi.model.OperationObject; @@ -49,14 +48,12 @@ public interface OpenApiProtocol { Class getProtocolType(); /** - * Configures protocol-specific default values when they are not present - * in the configuration context. Only values that are not explicitly set - * are modified as a result of calling this method. + * Sets protocol-specific default values on the OpenAPI configuration + * object. * - * @return Returns a map of property names to their default values to set. + * @param config Configuration object to modify. */ - default ObjectNode getDefaultSettings() { - return Node.objectNode(); + default void updateDefaultSettings(OpenApiConfig config) { } /** diff --git a/smithy-openapi/src/main/java/software/amazon/smithy/openapi/fromsmithy/Smithy2OpenApi.java b/smithy-openapi/src/main/java/software/amazon/smithy/openapi/fromsmithy/Smithy2OpenApi.java index d83976c13b3..0bd968e977b 100644 --- a/smithy-openapi/src/main/java/software/amazon/smithy/openapi/fromsmithy/Smithy2OpenApi.java +++ b/smithy-openapi/src/main/java/software/amazon/smithy/openapi/fromsmithy/Smithy2OpenApi.java @@ -15,12 +15,16 @@ package software.amazon.smithy.openapi.fromsmithy; +import java.util.Map; +import java.util.logging.Logger; import software.amazon.smithy.build.PluginContext; import software.amazon.smithy.build.SmithyBuildPlugin; -import software.amazon.smithy.jsonschema.JsonSchemaConstants; +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.shapes.ShapeId; -import software.amazon.smithy.openapi.OpenApiConstants; +import software.amazon.smithy.openapi.OpenApiConfig; +import software.amazon.smithy.openapi.OpenApiException; /** * Converts Smithy to an OpenAPI model and saves it as a JSON file. @@ -28,10 +32,12 @@ *

This plugin requires a setting named "service" that is the * Shape ID of the Smithy service shape to convert to OpenAPI. * - *

Constants defined in {@link JsonSchemaConstants} and - * {@link OpenApiConstants} can be provided in the settings object. + *

This plugin is configured using {@link OpenApiConfig}. */ public final class Smithy2OpenApi implements SmithyBuildPlugin { + + private static final Logger LOGGER = Logger.getLogger(Smithy2OpenApi.class.getName()); + @Override public String getName() { return "openapi"; @@ -40,12 +46,29 @@ public String getName() { @Override public void execute(PluginContext context) { OpenApiConverter converter = OpenApiConverter.create(); - context.getSettings().getStringMap().forEach(converter::putSetting); context.getPluginClassLoader().ifPresent(converter::classLoader); - ShapeId shapeId = ShapeId.from(context.getSettings() - .expectStringMember(OpenApiConstants.SERVICE) - .getValue()); + // Remove deprecated "openapi." prefixes from configuration settings. + ObjectNode mapped = context.getSettings(); + for (Map.Entry entry : mapped.getStringMap().entrySet()) { + if (entry.getKey().startsWith("openapi.")) { + String expected = entry.getKey().substring(8); + LOGGER.warning("Deprecated `openapi` configuration setting found: " + entry.getKey() + + ". Use " + expected + " instead"); + mapped = mapped.withoutMember(entry.getKey()); + mapped = mapped.withMember(expected, entry.getValue()); + } + } + + NodeMapper mapper = new NodeMapper(); + OpenApiConfig config = new OpenApiConfig(); + mapper.deserializeInto(mapped, config); + + ShapeId shapeId = config.getService(); + + if (shapeId == null) { + throw new OpenApiException(getName() + " is missing required property, `service`"); + } ObjectNode openApiNode = converter.convertToNode(context.getModel(), shapeId); context.getFileManifest().writeJson(shapeId.getName() + ".openapi.json", openApiNode); diff --git a/smithy-openapi/src/main/java/software/amazon/smithy/openapi/fromsmithy/mappers/CheckForGreedyLabels.java b/smithy-openapi/src/main/java/software/amazon/smithy/openapi/fromsmithy/mappers/CheckForGreedyLabels.java index ed3101bf2cd..c2a3d997658 100644 --- a/smithy-openapi/src/main/java/software/amazon/smithy/openapi/fromsmithy/mappers/CheckForGreedyLabels.java +++ b/smithy-openapi/src/main/java/software/amazon/smithy/openapi/fromsmithy/mappers/CheckForGreedyLabels.java @@ -17,7 +17,6 @@ import java.util.logging.Logger; import software.amazon.smithy.model.traits.Trait; -import software.amazon.smithy.openapi.OpenApiConstants; import software.amazon.smithy.openapi.OpenApiException; import software.amazon.smithy.openapi.fromsmithy.Context; import software.amazon.smithy.openapi.fromsmithy.OpenApiMapper; @@ -39,8 +38,6 @@ public byte getOrder() { @Override public OpenApi after(Context context, OpenApi openApi) { - boolean forbid = context.getConfig().getBooleanMemberOrDefault(OpenApiConstants.FORBID_GREEDY_LABELS); - for (String path : openApi.getPaths().keySet()) { // Throw an exception or warning when greedy URI labels are found in the path. if (path.contains("+}")) { @@ -48,7 +45,7 @@ public OpenApi after(Context context, OpenApi openApi) { + "tools support this style of URI labels. Greedy URI labels are expected " + "to capture all remaining components of a URI, so if a tool does not " + "support them, the API will not function properly."; - if (forbid) { + if (context.getConfig().getForbidGreedyLabels()) { throw new OpenApiException(message); } else { LOGGER.warning(message); diff --git a/smithy-openapi/src/main/java/software/amazon/smithy/openapi/fromsmithy/mappers/CheckForPrefixHeaders.java b/smithy-openapi/src/main/java/software/amazon/smithy/openapi/fromsmithy/mappers/CheckForPrefixHeaders.java index 9c526558144..afe5cae767f 100644 --- a/smithy-openapi/src/main/java/software/amazon/smithy/openapi/fromsmithy/mappers/CheckForPrefixHeaders.java +++ b/smithy-openapi/src/main/java/software/amazon/smithy/openapi/fromsmithy/mappers/CheckForPrefixHeaders.java @@ -23,7 +23,7 @@ import software.amazon.smithy.model.shapes.OperationShape; import software.amazon.smithy.model.shapes.ToShapeId; import software.amazon.smithy.model.traits.Trait; -import software.amazon.smithy.openapi.OpenApiConstants; +import software.amazon.smithy.openapi.OpenApiConfig; import software.amazon.smithy.openapi.OpenApiException; import software.amazon.smithy.openapi.fromsmithy.Context; import software.amazon.smithy.openapi.fromsmithy.OpenApiMapper; @@ -65,15 +65,14 @@ private void checkForResponseHeaders( } private void check(Context context, List bindings) { - String setting = context.getConfig().getStringMemberOrDefault( - OpenApiConstants.ON_HTTP_PREFIX_HEADERS, OpenApiConstants.ON_HTTP_PREFIX_HEADERS_FAIL); + OpenApiConfig.HttpPrefixHeadersStrategy strategy = context.getConfig().getOnHttpPrefixHeaders(); for (HttpBinding binding : bindings) { - switch (setting) { - case OpenApiConstants.ON_HTTP_PREFIX_HEADERS_WARN: + switch (strategy) { + case WARN: LOGGER.warning(createMessage(binding)); break; - case OpenApiConstants.ON_HTTP_PREFIX_HEADERS_FAIL: + case FAIL: throw new OpenApiException(createMessage(binding)); default: break; diff --git a/smithy-openapi/src/main/java/software/amazon/smithy/openapi/fromsmithy/mappers/OpenApiJsonAdd.java b/smithy-openapi/src/main/java/software/amazon/smithy/openapi/fromsmithy/mappers/OpenApiJsonAdd.java index a643fca8716..fe3b7443d67 100644 --- a/smithy-openapi/src/main/java/software/amazon/smithy/openapi/fromsmithy/mappers/OpenApiJsonAdd.java +++ b/smithy-openapi/src/main/java/software/amazon/smithy/openapi/fromsmithy/mappers/OpenApiJsonAdd.java @@ -16,11 +16,11 @@ package software.amazon.smithy.openapi.fromsmithy.mappers; import java.util.Map; +import java.util.logging.Logger; 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.traits.Trait; -import software.amazon.smithy.openapi.OpenApiConstants; import software.amazon.smithy.openapi.OpenApiException; import software.amazon.smithy.openapi.fromsmithy.Context; import software.amazon.smithy.openapi.fromsmithy.OpenApiMapper; @@ -35,6 +35,9 @@ * This is run after substitutions so it is unaffected by them. */ public final class OpenApiJsonAdd implements OpenApiMapper { + + private static final Logger LOGGER = Logger.getLogger(OpenApiJsonAdd.class.getName()); + @Override public byte getOrder() { // This is run after substitutions. @@ -43,20 +46,24 @@ public byte getOrder() { @Override public ObjectNode updateNode(Context context, OpenApi openapi, ObjectNode node) { - return context.getConfig().getObjectMember(OpenApiConstants.JSON_ADD) - .map(add -> { - ObjectNode result = node; - for (Map.Entry entry : add.getStringMap().entrySet()) { - try { - result = NodePointer.parse(entry.getKey()) - .addWithIntermediateValues(result, entry.getValue().toNode()) - .expectObjectNode(); - } catch (IllegalArgumentException e) { - throw new OpenApiException(e.getMessage(), e); - } - } - return result; - }) - .orElse(node); + Map add = context.getConfig().getJsonAdd(); + + if (add.isEmpty()) { + return node; + } + + ObjectNode result = node; + for (Map.Entry entry : add.entrySet()) { + try { + LOGGER.info(() -> "OpenAPI `jsonAdd`: adding `" + entry.getKey() + "`"); + result = NodePointer.parse(entry.getKey()) + .addWithIntermediateValues(result, entry.getValue().toNode()) + .expectObjectNode(); + } catch (IllegalArgumentException e) { + throw new OpenApiException(e.getMessage(), e); + } + } + + return result; } } diff --git a/smithy-openapi/src/main/java/software/amazon/smithy/openapi/fromsmithy/mappers/OpenApiJsonSubstitutions.java b/smithy-openapi/src/main/java/software/amazon/smithy/openapi/fromsmithy/mappers/OpenApiJsonSubstitutions.java index 1705bf794d0..f14be3790aa 100644 --- a/smithy-openapi/src/main/java/software/amazon/smithy/openapi/fromsmithy/mappers/OpenApiJsonSubstitutions.java +++ b/smithy-openapi/src/main/java/software/amazon/smithy/openapi/fromsmithy/mappers/OpenApiJsonSubstitutions.java @@ -15,11 +15,12 @@ package software.amazon.smithy.openapi.fromsmithy.mappers; +import java.util.Map; import java.util.logging.Logger; import software.amazon.smithy.build.JsonSubstitutions; +import software.amazon.smithy.model.node.Node; import software.amazon.smithy.model.node.ObjectNode; import software.amazon.smithy.model.traits.Trait; -import software.amazon.smithy.openapi.OpenApiConstants; import software.amazon.smithy.openapi.fromsmithy.Context; import software.amazon.smithy.openapi.fromsmithy.OpenApiMapper; import software.amazon.smithy.openapi.model.OpenApi; @@ -38,15 +39,18 @@ public byte getOrder() { @Override public ObjectNode updateNode(Context context, OpenApi openapi, ObjectNode node) { - return context.getConfig().getObjectMember(OpenApiConstants.SUBSTITUTIONS) - .map(substitutions -> { - LOGGER.warning("Using " + OpenApiConstants.SUBSTITUTIONS + " is discouraged. DO NOT use " - + "placeholders in your Smithy model for properties that are used by other tools " - + "like SDKs or service frameworks; placeholders should only ever be used in " - + "models for metadata that is specific to generating OpenAPI artifacts.\n\n" - + "Prefer safer alternatives like " + OpenApiConstants.JSON_ADD); - return JsonSubstitutions.create(substitutions).apply(node).expectObjectNode(); - }) - .orElse(node); + Map substitutions = context.getConfig().getSubstitutions(); + + if (substitutions.isEmpty()) { + return node; + } + + LOGGER.warning("Using `substitutions` is discouraged. DO NOT use placeholders in your Smithy model " + + "for properties that are used by other tools like SDKs or service frameworks; " + + "placeholders should only ever be used in models for metadata that is specific to " + + "generating OpenAPI artifacts.\n\n" + + "Prefer safer alternatives like `jsonAdd`"); + + return JsonSubstitutions.create(substitutions).apply(node).expectObjectNode(); } } diff --git a/smithy-openapi/src/main/java/software/amazon/smithy/openapi/fromsmithy/mappers/RemoveUnusedComponents.java b/smithy-openapi/src/main/java/software/amazon/smithy/openapi/fromsmithy/mappers/RemoveUnusedComponents.java index f921cc05610..5477bf0dd9d 100644 --- a/smithy-openapi/src/main/java/software/amazon/smithy/openapi/fromsmithy/mappers/RemoveUnusedComponents.java +++ b/smithy-openapi/src/main/java/software/amazon/smithy/openapi/fromsmithy/mappers/RemoveUnusedComponents.java @@ -29,7 +29,6 @@ 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.fromsmithy.Context; import software.amazon.smithy.openapi.fromsmithy.OpenApiMapper; import software.amazon.smithy.openapi.model.ComponentsObject; @@ -57,7 +56,7 @@ public byte getOrder() { @Override public OpenApi after(Context context, OpenApi openapi) { - if (context.getConfig().getBooleanMemberOrDefault(OpenApiConstants.OPENAPI_KEEP_UNUSED_COMPONENTS)) { + if (context.getConfig().getKeepUnusedComponents()) { return openapi; } @@ -66,16 +65,16 @@ public OpenApi after(Context context, OpenApi openapi) { do { current = result; - result = removalRound(current); + result = removalRound(context, current); } while (!result.equals(current)); result = removeUnusedSecuritySchemes(result); return result; } - private OpenApi removalRound(OpenApi openapi) { + private OpenApi removalRound(Context context, OpenApi openapi) { // Create a set of every component pointer (currently just schemas). - String schemaPointerPrefix = OpenApiConstants.SCHEMA_COMPONENTS_POINTER + "/"; + String schemaPointerPrefix = context.getConfig().getDefinitionPointer() + "/"; Set pointers = openapi.getComponents().getSchemas().keySet().stream() .map(key -> schemaPointerPrefix + key) .collect(Collectors.toSet()); diff --git a/smithy-openapi/src/main/java/software/amazon/smithy/openapi/fromsmithy/mappers/UnsupportedTraits.java b/smithy-openapi/src/main/java/software/amazon/smithy/openapi/fromsmithy/mappers/UnsupportedTraits.java index 9e54768cd03..56c20487501 100644 --- a/smithy-openapi/src/main/java/software/amazon/smithy/openapi/fromsmithy/mappers/UnsupportedTraits.java +++ b/smithy-openapi/src/main/java/software/amazon/smithy/openapi/fromsmithy/mappers/UnsupportedTraits.java @@ -21,7 +21,6 @@ import java.util.stream.Collectors; import software.amazon.smithy.model.shapes.ShapeId; import software.amazon.smithy.model.traits.Trait; -import software.amazon.smithy.openapi.OpenApiConstants; import software.amazon.smithy.openapi.OpenApiException; import software.amazon.smithy.openapi.fromsmithy.Context; import software.amazon.smithy.openapi.fromsmithy.OpenApiMapper; @@ -63,7 +62,7 @@ public void before(Context context, OpenApi.Builder builder) { + "model directly, they have no direct corollary in OpenAPI and can not be included in " + "the generated model."); - if (context.getConfig().getBooleanMemberOrDefault(OpenApiConstants.IGNORE_UNSUPPORTED_TRAITS)) { + if (context.getConfig().getIgnoreUnsupportedTraits()) { LOGGER.warning(message.toString()); } else { throw new OpenApiException(message.toString()); diff --git a/smithy-openapi/src/main/java/software/amazon/smithy/openapi/fromsmithy/protocols/AwsRestJson1Protocol.java b/smithy-openapi/src/main/java/software/amazon/smithy/openapi/fromsmithy/protocols/AwsRestJson1Protocol.java index 104742a7729..0341a799a14 100644 --- a/smithy-openapi/src/main/java/software/amazon/smithy/openapi/fromsmithy/protocols/AwsRestJson1Protocol.java +++ b/smithy-openapi/src/main/java/software/amazon/smithy/openapi/fromsmithy/protocols/AwsRestJson1Protocol.java @@ -19,16 +19,13 @@ import java.util.Set; import java.util.stream.Collectors; import software.amazon.smithy.aws.traits.protocols.RestJson1Trait; -import software.amazon.smithy.jsonschema.JsonSchemaConstants; import software.amazon.smithy.jsonschema.Schema; import software.amazon.smithy.model.knowledge.HttpBinding; -import software.amazon.smithy.model.node.Node; -import software.amazon.smithy.model.node.ObjectNode; import software.amazon.smithy.model.shapes.Shape; import software.amazon.smithy.model.shapes.ShapeId; import software.amazon.smithy.model.shapes.StructureShape; import software.amazon.smithy.model.traits.TimestampFormatTrait; -import software.amazon.smithy.openapi.OpenApiConstants; +import software.amazon.smithy.openapi.OpenApiConfig; import software.amazon.smithy.openapi.fromsmithy.Context; /** @@ -41,16 +38,14 @@ public Class getProtocolType() { } @Override - public ObjectNode getDefaultSettings() { - return Node.objectNode() - .withMember(JsonSchemaConstants.USE_JSON_NAME, true) - .withMember(JsonSchemaConstants.DEFAULT_TIMESTAMP_FORMAT, TimestampFormatTrait.EPOCH_SECONDS); + public void updateDefaultSettings(OpenApiConfig config) { + config.setUseJsonName(true); + config.setDefaultTimestampFormat(TimestampFormatTrait.Format.EPOCH_SECONDS); } @Override String getDocumentMediaType(Context context, Shape operationOrError, MessageType message) { - return context.getConfig().getStringMemberOrDefault( - OpenApiConstants.AWS_JSON_CONTENT_TYPE, "application/json"); + return context.getConfig().getJsonContentType(); } @Override diff --git a/smithy-openapi/src/main/java/software/amazon/smithy/openapi/model/Component.java b/smithy-openapi/src/main/java/software/amazon/smithy/openapi/model/Component.java index 965e16ce13e..10188d04d7d 100644 --- a/smithy-openapi/src/main/java/software/amazon/smithy/openapi/model/Component.java +++ b/smithy-openapi/src/main/java/software/amazon/smithy/openapi/model/Component.java @@ -41,11 +41,6 @@ protected Component(Builder builder) { extensions.putAll(builder.getExtensions()); } - public final Component addExtension(String name, Node value) { - extensions.put(name, value); - return this; - } - public final Optional getExtension(String name) { return Optional.ofNullable(extensions.get(name)); } diff --git a/smithy-openapi/src/main/java/software/amazon/smithy/openapi/package-info.java b/smithy-openapi/src/main/java/software/amazon/smithy/openapi/package-info.java deleted file mode 100644 index ba91cf36880..00000000000 --- a/smithy-openapi/src/main/java/software/amazon/smithy/openapi/package-info.java +++ /dev/null @@ -1,22 +0,0 @@ -/* - * 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. - */ - -/** - * Converts Smithy models to OpenAPI 3.0+. - */ -@SmithyUnstableApi -package software.amazon.smithy.openapi; - -import software.amazon.smithy.utils.SmithyUnstableApi; diff --git a/smithy-openapi/src/test/java/software/amazon/smithy/openapi/fromsmithy/OpenApiConverterTest.java b/smithy-openapi/src/test/java/software/amazon/smithy/openapi/fromsmithy/OpenApiConverterTest.java index 0277dac15df..70e305a69c5 100644 --- a/smithy-openapi/src/test/java/software/amazon/smithy/openapi/fromsmithy/OpenApiConverterTest.java +++ b/smithy-openapi/src/test/java/software/amazon/smithy/openapi/fromsmithy/OpenApiConverterTest.java @@ -29,14 +29,13 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; -import software.amazon.smithy.jsonschema.JsonSchemaConstants; import software.amazon.smithy.model.Model; import software.amazon.smithy.model.node.Node; import software.amazon.smithy.model.node.ObjectNode; import software.amazon.smithy.model.shapes.Shape; import software.amazon.smithy.model.shapes.ShapeId; import software.amazon.smithy.model.traits.Trait; -import software.amazon.smithy.openapi.OpenApiConstants; +import software.amazon.smithy.openapi.OpenApiConfig; import software.amazon.smithy.openapi.OpenApiException; import software.amazon.smithy.openapi.model.OpenApi; import software.amazon.smithy.openapi.model.PathItem; @@ -74,9 +73,11 @@ public void passesThroughTags() { .discoverModels() .assemble() .unwrap(); + OpenApiConfig config = new OpenApiConfig(); + config.setTags(true); + config.setSupportedTags(ListUtils.of("baz", "foo")); OpenApi result = OpenApiConverter.create() - .putSetting(OpenApiConstants.OPEN_API_TAGS, true) - .putSetting(OpenApiConstants.OPEN_API_SUPPORTED_TAGS, Node.fromStrings("baz", "foo")) + .config(config) .convert(model, ShapeId.from("smithy.example#Service")); Node expectedNode = Node.parse(IoUtils.toUtf8String( getClass().getResourceAsStream("tagged-service.openapi.json"))); @@ -116,8 +117,10 @@ public void mustBeAbleToResolveProtocolServiceProvider() { @Test public void loadsProtocolFromConfiguration() { + OpenApiConfig config = new OpenApiConfig(); + config.setProtocol(ShapeId.from("aws.protocols#restJson1")); ObjectNode result = OpenApiConverter.create() - .putSetting(OpenApiConstants.PROTOCOL, "aws.protocols#restJson1") + .config(config) .convertToNode(testService, ShapeId.from("example.rest#RestService")); Node expectedNode = Node.parse(IoUtils.toUtf8String( getClass().getResourceAsStream("test-service.openapi.json"))); @@ -143,8 +146,10 @@ public void failsToDeriveFromMultipleProtocols() { @Test public void failsWhenConfiguredProtocolIsNoFound() { Exception thrown = Assertions.assertThrows(OpenApiException.class, () -> { + OpenApiConfig config = new OpenApiConfig(); + config.setProtocol(ShapeId.from("aws.protocols#restJson99")); OpenApiConverter.create() - .putSetting(OpenApiConstants.PROTOCOL, "aws.protocols#restJson99") + .config(config) .convertToNode(testService, ShapeId.from("example.rest#RestService")); }); @@ -267,9 +272,10 @@ public void canChangeSecurityRequirementName() { @Test public void mergesInSchemaDocumentExtensions() { + OpenApiConfig config = new OpenApiConfig(); + config.setSchemaDocumentExtensions(Node.objectNode().withMember("foo", "baz")); ObjectNode result = OpenApiConverter.create() - .putSetting(JsonSchemaConstants.SCHEMA_DOCUMENT_EXTENSIONS, Node.objectNode() - .withMember("foo", "baz")) + .config(config) .convertToNode(testService, ShapeId.from("example.rest#RestService")); assertThat(result.getMember("foo"), equalTo(Optional.of(Node.from("baz")))); @@ -283,8 +289,10 @@ public void convertsStreamingService() { .discoverModels() .assemble() .unwrap(); + OpenApiConfig config = new OpenApiConfig(); + config.setProtocol(ShapeId.from("aws.protocols#restJson1")); ObjectNode result = OpenApiConverter.create() - .putSetting(OpenApiConstants.PROTOCOL, "aws.protocols#restJson1") + .config(config) .convertToNode(model, ShapeId.from("smithy.example#Streaming")); Node expectedNode = Node.parse(IoUtils.toUtf8String( getClass().getResourceAsStream("streaming-service.openapi.json"))); diff --git a/smithy-openapi/src/test/java/software/amazon/smithy/openapi/fromsmithy/OpenApiJsonSchemaMapperTest.java b/smithy-openapi/src/test/java/software/amazon/smithy/openapi/fromsmithy/OpenApiJsonSchemaMapperTest.java index 8cf99be106e..88590c01c85 100644 --- a/smithy-openapi/src/test/java/software/amazon/smithy/openapi/fromsmithy/OpenApiJsonSchemaMapperTest.java +++ b/smithy-openapi/src/test/java/software/amazon/smithy/openapi/fromsmithy/OpenApiJsonSchemaMapperTest.java @@ -21,7 +21,6 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import org.junit.jupiter.api.Test; -import software.amazon.smithy.jsonschema.JsonSchemaConstants; import software.amazon.smithy.jsonschema.JsonSchemaConverter; import software.amazon.smithy.jsonschema.Schema; import software.amazon.smithy.jsonschema.SchemaDocument; @@ -40,7 +39,8 @@ import software.amazon.smithy.model.traits.DeprecatedTrait; import software.amazon.smithy.model.traits.ExternalDocumentationTrait; import software.amazon.smithy.model.traits.SensitiveTrait; -import software.amazon.smithy.openapi.OpenApiConstants; +import software.amazon.smithy.openapi.OpenApiConfig; +import software.amazon.smithy.utils.ListUtils; public class OpenApiJsonSchemaMapperTest { @Test @@ -51,11 +51,8 @@ public void convertsModels() { .assemble() .unwrap(); SchemaDocument document = JsonSchemaConverter.builder() - .config(Node.objectNodeBuilder() - .withMember(OpenApiConstants.OPEN_API_MODE, true) - .withMember(JsonSchemaConstants.DEFINITION_POINTER, OpenApiConstants.SCHEMA_COMPONENTS_POINTER) - .build()) .addMapper(new OpenApiJsonSchemaMapper()) + .config(new OpenApiConfig()) .model(model) .build() .convert(); @@ -71,7 +68,6 @@ public void stripsUnsupportedKeywords() { MapShape shape = MapShape.builder().id("smithy.example#Map").key(key).value(value).build(); Model model = Model.builder().addShapes(string, shape, key, value).build(); SchemaDocument document = JsonSchemaConverter.builder() - .config(Node.objectNodeBuilder().withMember(OpenApiConstants.OPEN_API_MODE, true).build()) .addMapper(new OpenApiJsonSchemaMapper()) .model(model) .build() @@ -91,8 +87,8 @@ public void supportsExternalDocs() { .build(); Model model = Model.builder().addShape(shape).build(); SchemaDocument document = JsonSchemaConverter.builder() - .config(Node.objectNodeBuilder().withMember(OpenApiConstants.OPEN_API_MODE, true).build()) .addMapper(new OpenApiJsonSchemaMapper()) + .config(new OpenApiConfig()) .model(model) .build() .convertShape(shape); @@ -101,6 +97,7 @@ public void supportsExternalDocs() { .withMember("description", key) .withMember("url", link) .build(); + Node.assertEquals(document.getRootSchema().getExtension("externalDocs").get(), expectedDocs); } @@ -113,11 +110,10 @@ public void supportsCustomExternalDocNames() { .addTrait(ExternalDocumentationTrait.builder().addUrl(key, link).build()) .build(); Model model = Model.builder().addShape(shape).build(); + OpenApiConfig config = new OpenApiConfig(); + config.setExternalDocs(ListUtils.of("Custom Name")); SchemaDocument document = JsonSchemaConverter.builder() - .config(Node.objectNodeBuilder() - .withMember(OpenApiConstants.OPEN_API_MODE, true) - .withMember(OpenApiConstants.OPEN_API_CONVERTED_EXTERNAL_DOCS, Node.fromStrings("Custom Name")) - .build()) + .config(config) .addMapper(new OpenApiJsonSchemaMapper()) .model(model) .build() @@ -135,7 +131,6 @@ public void supportsBoxTrait() { IntegerShape shape = IntegerShape.builder().id("a.b#C").addTrait(new BoxTrait()).build(); Model model = Model.builder().addShape(shape).build(); SchemaDocument document = JsonSchemaConverter.builder() - .config(Node.objectNodeBuilder().withMember(OpenApiConstants.OPEN_API_MODE, true).build()) .addMapper(new OpenApiJsonSchemaMapper()) .model(model) .build() @@ -149,7 +144,6 @@ public void supportsDeprecatedTrait() { IntegerShape shape = IntegerShape.builder().id("a.b#C").addTrait(DeprecatedTrait.builder().build()).build(); Model model = Model.builder().addShape(shape).build(); SchemaDocument document = JsonSchemaConverter.builder() - .config(Node.objectNodeBuilder().withMember(OpenApiConstants.OPEN_API_MODE, true).build()) .addMapper(new OpenApiJsonSchemaMapper()) .model(model) .build() @@ -163,7 +157,6 @@ public void supportsInt32() { IntegerShape shape = IntegerShape.builder().id("a.b#C").build(); Model model = Model.builder().addShape(shape).build(); SchemaDocument document = JsonSchemaConverter.builder() - .config(Node.objectNodeBuilder().withMember(OpenApiConstants.OPEN_API_MODE, true).build()) .addMapper(new OpenApiJsonSchemaMapper()) .model(model) .build() @@ -177,7 +170,6 @@ public void supportsInt64() { LongShape shape = LongShape.builder().id("a.b#C").build(); Model model = Model.builder().addShape(shape).build(); SchemaDocument document = JsonSchemaConverter.builder() - .config(Node.objectNodeBuilder().withMember(OpenApiConstants.OPEN_API_MODE, true).build()) .addMapper(new OpenApiJsonSchemaMapper()) .model(model) .build() @@ -191,7 +183,6 @@ public void supportsFloatFormat() { FloatShape shape = FloatShape.builder().id("a.b#C").build(); Model model = Model.builder().addShape(shape).build(); SchemaDocument document = JsonSchemaConverter.builder() - .config(Node.objectNodeBuilder().withMember(OpenApiConstants.OPEN_API_MODE, true).build()) .addMapper(new OpenApiJsonSchemaMapper()) .model(model) .build() @@ -205,7 +196,6 @@ public void supportsDoubleFormat() { DoubleShape shape = DoubleShape.builder().id("a.b#C").build(); Model model = Model.builder().addShape(shape).build(); SchemaDocument document = JsonSchemaConverter.builder() - .config(Node.objectNodeBuilder().withMember(OpenApiConstants.OPEN_API_MODE, true).build()) .addMapper(new OpenApiJsonSchemaMapper()) .model(model) .build() @@ -219,7 +209,7 @@ public void blobFormatDefaultsToByte() { BlobShape shape = BlobShape.builder().id("a.b#C").build(); Model model = Model.builder().addShape(shape).build(); SchemaDocument document = JsonSchemaConverter.builder() - .config(Node.objectNodeBuilder().withMember(OpenApiConstants.OPEN_API_MODE, true).build()) + .config(new OpenApiConfig()) .addMapper(new OpenApiJsonSchemaMapper()) .model(model) .build() @@ -232,11 +222,10 @@ public void blobFormatDefaultsToByte() { public void blobFormatOverriddenToBinary() { BlobShape shape = BlobShape.builder().id("a.b#C").build(); Model model = Model.builder().addShape(shape).build(); + OpenApiConfig config = new OpenApiConfig(); + config.setDefaultBlobFormat("binary"); SchemaDocument document = JsonSchemaConverter.builder() - .config(Node.objectNodeBuilder() - .withMember(OpenApiConstants.OPEN_API_MODE, true) - .withMember(OpenApiConstants.OPEN_API_DEFAULT_BLOB_FORMAT, "binary") - .build()) + .config(config) .addMapper(new OpenApiJsonSchemaMapper()) .model(model) .build() @@ -250,7 +239,6 @@ public void supportsSensitiveTrait() { StringShape shape = StringShape.builder().id("a.b#C").addTrait(new SensitiveTrait()).build(); Model model = Model.builder().addShape(shape).build(); SchemaDocument document = JsonSchemaConverter.builder() - .config(Node.objectNodeBuilder().withMember(OpenApiConstants.OPEN_API_MODE, true).build()) .addMapper(new OpenApiJsonSchemaMapper()) .model(model) .build() diff --git a/smithy-openapi/src/test/java/software/amazon/smithy/openapi/fromsmithy/mappers/CheckForGreedyLabelsTest.java b/smithy-openapi/src/test/java/software/amazon/smithy/openapi/fromsmithy/mappers/CheckForGreedyLabelsTest.java index 79b69965e88..883e03a62e7 100644 --- a/smithy-openapi/src/test/java/software/amazon/smithy/openapi/fromsmithy/mappers/CheckForGreedyLabelsTest.java +++ b/smithy-openapi/src/test/java/software/amazon/smithy/openapi/fromsmithy/mappers/CheckForGreedyLabelsTest.java @@ -6,7 +6,7 @@ import org.junit.jupiter.api.Test; import software.amazon.smithy.model.Model; import software.amazon.smithy.model.shapes.ShapeId; -import software.amazon.smithy.openapi.OpenApiConstants; +import software.amazon.smithy.openapi.OpenApiConfig; import software.amazon.smithy.openapi.OpenApiException; import software.amazon.smithy.openapi.fromsmithy.OpenApiConverter; @@ -34,9 +34,12 @@ public void logsInsteadOfThrows() { @Test public void keepsUnusedSchemas() { + OpenApiConfig config = new OpenApiConfig(); + config.setForbidGreedyLabels(true); + Exception thrown = Assertions.assertThrows(OpenApiException.class, () -> { OpenApiConverter.create() - .putSetting(OpenApiConstants.FORBID_GREEDY_LABELS, true) + .config(config) .convert(model, ShapeId.from("smithy.example#Greedy")); }); diff --git a/smithy-openapi/src/test/java/software/amazon/smithy/openapi/fromsmithy/mappers/CheckForPrefixHeadersTest.java b/smithy-openapi/src/test/java/software/amazon/smithy/openapi/fromsmithy/mappers/CheckForPrefixHeadersTest.java index d3525d9a9a8..f4252ac52a6 100644 --- a/smithy-openapi/src/test/java/software/amazon/smithy/openapi/fromsmithy/mappers/CheckForPrefixHeadersTest.java +++ b/smithy-openapi/src/test/java/software/amazon/smithy/openapi/fromsmithy/mappers/CheckForPrefixHeadersTest.java @@ -6,7 +6,7 @@ import org.junit.jupiter.api.Test; import software.amazon.smithy.model.Model; import software.amazon.smithy.model.shapes.ShapeId; -import software.amazon.smithy.openapi.OpenApiConstants; +import software.amazon.smithy.openapi.OpenApiConfig; import software.amazon.smithy.openapi.OpenApiException; import software.amazon.smithy.openapi.fromsmithy.OpenApiConverter; @@ -29,8 +29,10 @@ public static void after() { @Test public void canIgnorePrefixHeaders() { + OpenApiConfig config = new OpenApiConfig(); + config.setOnHttpPrefixHeaders(OpenApiConfig.HttpPrefixHeadersStrategy.WARN); OpenApiConverter.create() - .putSetting(OpenApiConstants.ON_HTTP_PREFIX_HEADERS, OpenApiConstants.ON_HTTP_PREFIX_HEADERS_WARN) + .config(config) .convert(model, ShapeId.from("smithy.example#PrefixHeaders")); } diff --git a/smithy-openapi/src/test/java/software/amazon/smithy/openapi/fromsmithy/mappers/OpenApiJsonAddTest.java b/smithy-openapi/src/test/java/software/amazon/smithy/openapi/fromsmithy/mappers/OpenApiJsonAddTest.java index cc8e93edb23..76130f76c29 100644 --- a/smithy-openapi/src/test/java/software/amazon/smithy/openapi/fromsmithy/mappers/OpenApiJsonAddTest.java +++ b/smithy-openapi/src/test/java/software/amazon/smithy/openapi/fromsmithy/mappers/OpenApiJsonAddTest.java @@ -22,7 +22,7 @@ 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.OpenApiConstants; +import software.amazon.smithy.openapi.OpenApiConfig; import software.amazon.smithy.openapi.fromsmithy.OpenApiConverter; public class OpenApiJsonAddTest { @@ -43,8 +43,11 @@ public void addsWithPointers() { .withMember("/info/title", "custom") .build(); + OpenApiConfig config = new OpenApiConfig(); + config.setJsonAdd(addNode.getStringMap()); + ObjectNode openApi = OpenApiConverter.create() - .putSetting(OpenApiConstants.JSON_ADD, addNode) + .config(config) .convertToNode(model, ShapeId.from("smithy.example#Service")); String description = NodePointer.parse("/info/description").getValue(openApi).expectStringNode().getValue(); diff --git a/smithy-openapi/src/test/java/software/amazon/smithy/openapi/fromsmithy/mappers/OpenApiJsonSubstitutionsPluginTest.java b/smithy-openapi/src/test/java/software/amazon/smithy/openapi/fromsmithy/mappers/OpenApiJsonSubstitutionsPluginTest.java index 1312fbbd876..39c73ab1dc1 100644 --- a/smithy-openapi/src/test/java/software/amazon/smithy/openapi/fromsmithy/mappers/OpenApiJsonSubstitutionsPluginTest.java +++ b/smithy-openapi/src/test/java/software/amazon/smithy/openapi/fromsmithy/mappers/OpenApiJsonSubstitutionsPluginTest.java @@ -21,8 +21,9 @@ 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.OpenApiConstants; +import software.amazon.smithy.openapi.OpenApiConfig; import software.amazon.smithy.openapi.fromsmithy.OpenApiConverter; +import software.amazon.smithy.utils.MapUtils; public class OpenApiJsonSubstitutionsPluginTest { @Test @@ -33,9 +34,10 @@ public void removesBySubstitution() { .assemble() .unwrap(); + OpenApiConfig config = new OpenApiConfig(); + config.setSubstitutions(MapUtils.of("SUB_HELLO", Node.from("hello"))); ObjectNode openApi = OpenApiConverter.create() - .putSetting(OpenApiConstants.SUBSTITUTIONS, Node.objectNode() - .withMember("SUB_HELLO", Node.from("hello"))) + .config(config) .convertToNode(model, ShapeId.from("smithy.example#Service")); String description = openApi.getObjectMember("info").get().getStringMember("description").get().getValue(); diff --git a/smithy-openapi/src/test/java/software/amazon/smithy/openapi/fromsmithy/mappers/RemoveUnusedComponentsTest.java b/smithy-openapi/src/test/java/software/amazon/smithy/openapi/fromsmithy/mappers/RemoveUnusedComponentsTest.java index 01e25cc1011..a7c0d4f7a7f 100644 --- a/smithy-openapi/src/test/java/software/amazon/smithy/openapi/fromsmithy/mappers/RemoveUnusedComponentsTest.java +++ b/smithy-openapi/src/test/java/software/amazon/smithy/openapi/fromsmithy/mappers/RemoveUnusedComponentsTest.java @@ -6,7 +6,7 @@ import org.junit.jupiter.api.Test; import software.amazon.smithy.model.Model; import software.amazon.smithy.model.shapes.ShapeId; -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.OpenApiConverter; import software.amazon.smithy.openapi.fromsmithy.OpenApiMapper; @@ -39,8 +39,10 @@ public void removesUnusedSchemas() { @Test public void keepsUnusedSchemas() { + OpenApiConfig config = new OpenApiConfig(); + config.setKeepUnusedComponents(true); OpenApi result = OpenApiConverter.create() - .putSetting(OpenApiConstants.OPENAPI_KEEP_UNUSED_COMPONENTS, true) + .config(config) .convert(model, ShapeId.from("smithy.example#Small")); // The input structure remains in the output even though it's unreferenced. diff --git a/smithy-openapi/src/test/java/software/amazon/smithy/openapi/fromsmithy/mappers/UnsupportedTraitsPluginTest.java b/smithy-openapi/src/test/java/software/amazon/smithy/openapi/fromsmithy/mappers/UnsupportedTraitsPluginTest.java index 00700508ea1..dd59c7860b4 100644 --- a/smithy-openapi/src/test/java/software/amazon/smithy/openapi/fromsmithy/mappers/UnsupportedTraitsPluginTest.java +++ b/smithy-openapi/src/test/java/software/amazon/smithy/openapi/fromsmithy/mappers/UnsupportedTraitsPluginTest.java @@ -6,7 +6,7 @@ import org.junit.jupiter.api.Test; import software.amazon.smithy.model.Model; import software.amazon.smithy.model.shapes.ShapeId; -import software.amazon.smithy.openapi.OpenApiConstants; +import software.amazon.smithy.openapi.OpenApiConfig; import software.amazon.smithy.openapi.OpenApiException; import software.amazon.smithy.openapi.fromsmithy.OpenApiConverter; @@ -29,8 +29,10 @@ public static void after() { @Test public void logsWhenUnsupportedTraitsAreFound() { + OpenApiConfig config = new OpenApiConfig(); + config.setIgnoreUnsupportedTraits(true); OpenApiConverter.create() - .putSetting(OpenApiConstants.IGNORE_UNSUPPORTED_TRAITS, true) + .config(config) .convert(model, ShapeId.from("smithy.example#EndpointService")); } diff --git a/smithy-openapi/src/test/java/software/amazon/smithy/openapi/fromsmithy/protocols/AwsRestJson1ProtocolTest.java b/smithy-openapi/src/test/java/software/amazon/smithy/openapi/fromsmithy/protocols/AwsRestJson1ProtocolTest.java index b7b91e305ef..4ff8ee8aaa1 100644 --- a/smithy-openapi/src/test/java/software/amazon/smithy/openapi/fromsmithy/protocols/AwsRestJson1ProtocolTest.java +++ b/smithy-openapi/src/test/java/software/amazon/smithy/openapi/fromsmithy/protocols/AwsRestJson1ProtocolTest.java @@ -10,7 +10,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.OpenApiConstants; +import software.amazon.smithy.openapi.OpenApiConfig; import software.amazon.smithy.openapi.fromsmithy.OpenApiConverter; import software.amazon.smithy.openapi.model.OpenApi; import software.amazon.smithy.utils.IoUtils; @@ -57,8 +57,10 @@ public void canUseCustomMediaType() { .discoverModels() .assemble() .unwrap(); + OpenApiConfig config = new OpenApiConfig(); + config.setJsonContentType("application/x-amz-json-1.0"); OpenApi result = OpenApiConverter.create() - .putSetting(OpenApiConstants.AWS_JSON_CONTENT_TYPE, "application/x-amz-json-1.0") + .config(config) .convert(model, ShapeId.from("smithy.example#Service")); Assertions.assertTrue(Node.printJson(result.toNode()).contains("application/x-amz-json-1.0")); From 9a838695472dcd9f3fbf3b6369b508734034b85b Mon Sep 17 00:00:00 2001 From: Michael Dowling Date: Thu, 9 Apr 2020 15:27:18 -0700 Subject: [PATCH 3/3] Add better deprecated property handling --- .../openapi/CloudFormationSubstitution.java | 10 +-- .../amazon/smithy/jsonschema/Schema.java | 10 +-- .../amazon/smithy/openapi/OpenApiConfig.java | 70 +++++++++++++++++++ .../openapi/fromsmithy/Smithy2OpenApi.java | 24 +------ .../openapi/fromsmithy/OpenApiConfigTest.java | 46 ++++++++++++ 5 files changed, 123 insertions(+), 37 deletions(-) create mode 100644 smithy-openapi/src/test/java/software/amazon/smithy/openapi/fromsmithy/OpenApiConfigTest.java diff --git a/smithy-aws-apigateway-openapi/src/main/java/software/amazon/smithy/aws/apigateway/openapi/CloudFormationSubstitution.java b/smithy-aws-apigateway-openapi/src/main/java/software/amazon/smithy/aws/apigateway/openapi/CloudFormationSubstitution.java index c007e480344..823c4b0cbe9 100644 --- a/smithy-aws-apigateway-openapi/src/main/java/software/amazon/smithy/aws/apigateway/openapi/CloudFormationSubstitution.java +++ b/smithy-aws-apigateway-openapi/src/main/java/software/amazon/smithy/aws/apigateway/openapi/CloudFormationSubstitution.java @@ -69,21 +69,13 @@ public byte getOrder() { @Override public ObjectNode updateNode(Context context, OpenApi openapi, ObjectNode node) { - if (!isDisabled(context)) { + if (!context.getConfig().getExtensions(ApiGatewayConfig.class).getDisableCloudFormationSubstitution()) { 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 { private final Deque stack = new ArrayDeque<>(); private final List paths; diff --git a/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/Schema.java b/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/Schema.java index 237c6eeb264..13b035faa0f 100644 --- a/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/Schema.java +++ b/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/Schema.java @@ -808,13 +808,13 @@ public Builder removeExtension(String key) { } /** - * Applies a "disableX" key to a schema builder. + * Disables a specific JSON schema property by name. * - * @param disableKey Disable key to apply (e.g., "disablePropertyNames"). + * @param propertyName Property name to remove (e.g., "propertyNames"). * @return Returns the builder. */ - public Builder disableProperty(String disableKey) { - switch (disableKey) { + public Builder disableProperty(String propertyName) { + switch (propertyName) { case "const": return this.constValue(null); case "default": @@ -884,7 +884,7 @@ public Builder disableProperty(String disableKey) { case "examples": return this.examples(null); default: - LOGGER.warning("Unknown JSON Schema config 'disable' property: " + disableKey); + LOGGER.warning("Unknown JSON Schema config 'disable' property: " + propertyName); return this; } } diff --git a/smithy-openapi/src/main/java/software/amazon/smithy/openapi/OpenApiConfig.java b/smithy-openapi/src/main/java/software/amazon/smithy/openapi/OpenApiConfig.java index 768760fa358..780f4366c34 100644 --- a/smithy-openapi/src/main/java/software/amazon/smithy/openapi/OpenApiConfig.java +++ b/smithy-openapi/src/main/java/software/amazon/smithy/openapi/OpenApiConfig.java @@ -16,15 +16,20 @@ package software.amazon.smithy.openapi; import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.logging.Logger; import software.amazon.smithy.jsonschema.JsonSchemaConfig; 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.shapes.ShapeId; import software.amazon.smithy.openapi.fromsmithy.OpenApiProtocol; import software.amazon.smithy.openapi.fromsmithy.Smithy2OpenApiExtension; import software.amazon.smithy.utils.ListUtils; +import software.amazon.smithy.utils.StringUtils; /** * "openapi" smithy-build plugin configuration settings. @@ -46,6 +51,26 @@ public enum HttpPrefixHeadersStrategy { /** The JSON pointer to where OpenAPI schema components should be written. */ private static final String SCHEMA_COMPONENTS_POINTER = "#/components/schemas"; + private static final Logger LOGGER = Logger.getLogger(OpenApiConfig.class.getName()); + private static final Map DEPRECATED_PROPERTY_RENAMES = new HashMap<>(); + + static { + DEPRECATED_PROPERTY_RENAMES.put("openapi.tags", "tags"); + DEPRECATED_PROPERTY_RENAMES.put("openapi.supportedTags", "supportedTags"); + DEPRECATED_PROPERTY_RENAMES.put("openapi.defaultBlobFormat", "defaultBlobFormat"); + DEPRECATED_PROPERTY_RENAMES.put("openapi.keepUnusedComponents", "keepUnusedComponents"); + DEPRECATED_PROPERTY_RENAMES.put("openapi.aws.jsonContentType", "jsonContentType"); + DEPRECATED_PROPERTY_RENAMES.put("openapi.forbidGreedyLabels", "forbidGreedyLabels"); + DEPRECATED_PROPERTY_RENAMES.put("openapi.onHttpPrefixHeaders", "onHttpPrefixHeaders"); + // There was a typo in the docs, so might as well fix that here. + DEPRECATED_PROPERTY_RENAMES.put("openapi.ignoreUnsupportedTrait", "ignoreUnsupportedTraits"); + DEPRECATED_PROPERTY_RENAMES.put("openapi.ignoreUnsupportedTraits", "ignoreUnsupportedTraits"); + DEPRECATED_PROPERTY_RENAMES.put("openapi.substitutions", "substitutions"); + // Cheating a little here, but oh well. + DEPRECATED_PROPERTY_RENAMES.put("apigateway.disableCloudFormationSubstitution", + "disableCloudFormationSubstitution"); + } + private ShapeId service; private ShapeId protocol; private boolean tags; @@ -284,4 +309,49 @@ public List getExternalDocs() { public void setExternalDocs(List externalDocs) { this.externalDocs = Objects.requireNonNull(externalDocs); } + + /** + * Creates an OpenApiConfig from a Node value. + * + *

This method first converts deprecated keys into their new locations and + * formats, and then uses the {@link NodeMapper} on the converted input + * object. Note that this class can be deserialized using a NodeMapper too + * since the NodeMapper will look for a static, public, fromNode method. + * + * @param input Input to deserialize. + * @return Returns the deserialized OpenApiConfig. + */ + public static OpenApiConfig fromNode(Node input) { + NodeMapper mapper = new NodeMapper(); + ObjectNode node = fixDeprecatedKeys(input.expectObjectNode()); + OpenApiConfig config = new OpenApiConfig(); + return mapper.deserializeInto(node, config); + } + + private static ObjectNode fixDeprecatedKeys(ObjectNode node) { + ObjectNode mapped = node; + + // Remove deprecated "openapi." prefixes from configuration settings. + for (Map.Entry entry : mapped.getStringMap().entrySet()) { + if (DEPRECATED_PROPERTY_RENAMES.containsKey(entry.getKey())) { + // Fixes specific renamed keys. + String rename = DEPRECATED_PROPERTY_RENAMES.get(entry.getKey()); + LOGGER.warning("Deprecated `openapi` configuration setting found: " + entry.getKey() + + ". Use " + rename + " instead"); + mapped = mapped.withMember(rename, entry.getValue()); + mapped = mapped.withoutMember(entry.getKey()); + } else if (entry.getKey().startsWith("disable.")) { + // These are now added into the "disableFeatures" property. + String property = StringUtils.uncapitalize(entry.getKey().substring(8)); + throw new OpenApiException("Unsupported `openapi` configuration setting found: " + entry.getKey() + + ". Add `" + property + "` to the `disableFeatures` property instead"); + } else if (entry.getKey().startsWith("openapi.use.")) { + throw new OpenApiException(String.format( + "The `%s` `openapi` plugin property is no longer supported. Use the " + + "`disableFeatures` property instead to disable features.", entry.getKey())); + } + } + + return mapped; + } } diff --git a/smithy-openapi/src/main/java/software/amazon/smithy/openapi/fromsmithy/Smithy2OpenApi.java b/smithy-openapi/src/main/java/software/amazon/smithy/openapi/fromsmithy/Smithy2OpenApi.java index 0bd968e977b..7cac415e736 100644 --- a/smithy-openapi/src/main/java/software/amazon/smithy/openapi/fromsmithy/Smithy2OpenApi.java +++ b/smithy-openapi/src/main/java/software/amazon/smithy/openapi/fromsmithy/Smithy2OpenApi.java @@ -15,12 +15,8 @@ package software.amazon.smithy.openapi.fromsmithy; -import java.util.Map; -import java.util.logging.Logger; import software.amazon.smithy.build.PluginContext; import software.amazon.smithy.build.SmithyBuildPlugin; -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.shapes.ShapeId; import software.amazon.smithy.openapi.OpenApiConfig; @@ -36,8 +32,6 @@ */ public final class Smithy2OpenApi implements SmithyBuildPlugin { - private static final Logger LOGGER = Logger.getLogger(Smithy2OpenApi.class.getName()); - @Override public String getName() { return "openapi"; @@ -47,23 +41,7 @@ public String getName() { public void execute(PluginContext context) { OpenApiConverter converter = OpenApiConverter.create(); context.getPluginClassLoader().ifPresent(converter::classLoader); - - // Remove deprecated "openapi." prefixes from configuration settings. - ObjectNode mapped = context.getSettings(); - for (Map.Entry entry : mapped.getStringMap().entrySet()) { - if (entry.getKey().startsWith("openapi.")) { - String expected = entry.getKey().substring(8); - LOGGER.warning("Deprecated `openapi` configuration setting found: " + entry.getKey() - + ". Use " + expected + " instead"); - mapped = mapped.withoutMember(entry.getKey()); - mapped = mapped.withMember(expected, entry.getValue()); - } - } - - NodeMapper mapper = new NodeMapper(); - OpenApiConfig config = new OpenApiConfig(); - mapper.deserializeInto(mapped, config); - + OpenApiConfig config = OpenApiConfig.fromNode(context.getSettings()); ShapeId shapeId = config.getService(); if (shapeId == null) { diff --git a/smithy-openapi/src/test/java/software/amazon/smithy/openapi/fromsmithy/OpenApiConfigTest.java b/smithy-openapi/src/test/java/software/amazon/smithy/openapi/fromsmithy/OpenApiConfigTest.java new file mode 100644 index 00000000000..076c76a9cb8 --- /dev/null +++ b/smithy-openapi/src/test/java/software/amazon/smithy/openapi/fromsmithy/OpenApiConfigTest.java @@ -0,0 +1,46 @@ +package software.amazon.smithy.openapi.fromsmithy; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.openapi.OpenApiConfig; +import software.amazon.smithy.openapi.OpenApiException; + +public class OpenApiConfigTest { + @Test + public void throwsOnDisableProperties() { + Node disableTest = Node.objectNode().withMember("disable.additionalProperties", Node.from(true)); + + Exception thrown = Assertions.assertThrows(OpenApiException.class, () -> { + OpenApiConfig.fromNode(disableTest); + }); + + assertThat(thrown.getMessage(), containsString("disableFeatures")); + } + + @Test + public void throwsOnOpenApiUseProperties() { + Node openApiUseTest = Node.objectNode().withMember("openapi.use.xml", Node.from(true)); + + Exception thrown = Assertions.assertThrows(OpenApiException.class, () -> { + OpenApiConfig.fromNode(openApiUseTest); + }); + + assertThat(thrown.getMessage(), containsString("disableFeatures")); + } + + @Test + public void convertsExplicitlyMappedProperties() { + Node mappedTest = Node.objectNode() + .withMember("openapi.tags", Node.from(true)) + .withMember("openapi.ignoreUnsupportedTraits", Node.from(true)); + OpenApiConfig config = OpenApiConfig.fromNode(mappedTest); + + assertThat(config.getTags(), equalTo(true)); + assertThat(config.getIgnoreUnsupportedTraits(), equalTo(true)); + } +}