From 6279f9de8f19f98b0ab8300cacf8ca39ed9cc5a4 Mon Sep 17 00:00:00 2001 From: Chase Coalwell <782571+srchase@users.noreply.github.com> Date: Wed, 2 Aug 2023 09:32:46 -0700 Subject: [PATCH] Add intEnum support json-schema (#1898) --- .../guides/converting-to-openapi.rst | 57 +++++++++++++++++++ .../smithy/jsonschema/DefaultRefStrategy.java | 8 ++- .../smithy/jsonschema/JsonSchemaConfig.java | 16 ++++++ .../jsonschema/JsonSchemaShapeVisitor.java | 4 ++ .../amazon/smithy/jsonschema/Schema.java | 32 +++++++++-- .../jsonschema/JsonSchemaConverterTest.java | 35 ++++++++++++ .../amazon/smithy/jsonschema/SchemaTest.java | 17 ++++++ .../int-enums-disabled.jsonschema.v07.json | 12 ++++ .../jsonschema/int-enums.jsonschema.v07.json | 19 +++++++ .../amazon/smithy/jsonschema/int-enums.smithy | 12 ++++ 10 files changed, 206 insertions(+), 6 deletions(-) create mode 100644 smithy-jsonschema/src/test/resources/software/amazon/smithy/jsonschema/int-enums-disabled.jsonschema.v07.json create mode 100644 smithy-jsonschema/src/test/resources/software/amazon/smithy/jsonschema/int-enums.jsonschema.v07.json create mode 100644 smithy-jsonschema/src/test/resources/software/amazon/smithy/jsonschema/int-enums.smithy diff --git a/docs/source-2.0/guides/converting-to-openapi.rst b/docs/source-2.0/guides/converting-to-openapi.rst index 39bad112eca..eb3009ad009 100644 --- a/docs/source-2.0/guides/converting-to-openapi.rst +++ b/docs/source-2.0/guides/converting-to-openapi.rst @@ -873,6 +873,63 @@ disableDefaultValues (``boolean``) } +.. _generate-openapi-setting-disableIntEnums: + +disableIntEnums (``boolean``) + Set to true to disable setting the ``enum`` property for intEnum shapes. + + .. code-block:: json + + { + "version": "2.0", + "plugins": { + "openapi": { + "service": "example.weather#Weather", + "disableIntEnums": true + } + } + } + + With this disabled, intEnum shapes will be inlined and the ``enum`` property + will not be set: + + .. code-block:: json + + { + "Foo": { + "type": "object", + "properties": { + "bar": { + "type": "number" + } + } + } + } + + With this enabled (the default), intEnum shapes will have the ``enum`` + property set and the schema will use a ``$ref``. + + .. code-block:: json + + { + "Foo": { + "type": "object", + "properties": { + "bar": { + "$ref": "#/definitions/MyIntEnum" + } + } + }, + "MyIntEnum": { + "type": "number", + "enum": [ + 1, + 2 + ] + } + } + + ---------------- Security schemes ---------------- 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 351cb9478d0..f082670b891 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 @@ -35,8 +35,8 @@ * JSON schema definition. * *
  • - *

    Members that target structures, unions, enums, and maps use a $ref to the - * targeted shape. With the exception of maps, these kinds of shapes are almost + *

    Members that target structures, unions, enums, intEnums, and maps use a $ref to + * the targeted shape. With the exception of maps, these kinds of shapes are almost * always generated as concrete types by code generators, so it's useful to reuse * them throughout the schema. However, this means that member documentation * and other member traits need to be moved in some way to the containing @@ -157,6 +157,10 @@ public boolean isInlined(Shape shape) { return false; } + if (shape.isIntEnumShape() && !config.getDisableIntEnums()) { + return false; + } + // Simple types are always inlined unless the type has the enum trait. return shape instanceof SimpleShape; } 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 9c332b80915..6794a8c2929 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 @@ -113,6 +113,7 @@ public String toString() { private boolean enableOutOfServiceReferences = false; private boolean useIntegerType; private boolean disableDefaultValues = false; + private boolean disableIntEnums = false; public JsonSchemaConfig() { nodeMapper.setWhenMissingSetter(NodeMapper.WhenMissing.IGNORE); @@ -421,6 +422,21 @@ public void setDisableDefaultValues(boolean disableDefaultValues) { } + public boolean getDisableIntEnums() { + return disableIntEnums; + } + + /** + * Set to true to disable setting an `enum` property for intEnums. When disabled, + * intEnums are inlined instead of using a $ref. + * + * @param disableIntEnums True to disable setting `enum` property for intEnums. + */ + public void setDisableIntEnums(boolean disableIntEnums) { + this.disableIntEnums = disableIntEnums; + } + + /** * JSON schema version to use when converting Smithy shapes into Json Schema. * 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 05a32b8b22a..477bf152643 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 @@ -321,6 +321,10 @@ private Schema.Builder updateBuilder(Shape shape, Schema.Builder builder) { .map(EnumTrait::getEnumDefinitionValues) .ifPresent(builder::enumValues); + if (shape.isIntEnumShape() && !converter.getConfig().getDisableIntEnums()) { + builder.intEnumValues(shape.asIntEnumShape().get().getEnumValues().values()); + } + if (shape.hasTrait(DefaultTrait.class) && !converter.getConfig().getDisableDefaultValues()) { builder.defaultValue(shape.expectTrait(DefaultTrait.class).toNode()); } 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 ef2078a247a..08a33ccf826 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 @@ -63,6 +63,7 @@ public final class Schema implements ToNode, ToSmithyBuilder { private final String ref; private final String type; private final Collection enumValues; + private final Collection intEnumValues; private final Node constValue; private final Node defaultValue; @@ -113,6 +114,7 @@ private Schema(Builder builder) { ref = builder.ref; type = builder.type; enumValues = Collections.unmodifiableCollection(builder.enumValues); + intEnumValues = Collections.unmodifiableCollection(builder.intEnumValues); constValue = builder.constValue; defaultValue = builder.defaultValue; @@ -174,6 +176,10 @@ public Optional> getEnumValues() { return Optional.ofNullable(enumValues); } + public Optional> getIntEnumValues() { + return Optional.ofNullable(intEnumValues); + } + public Optional getConstValue() { return Optional.ofNullable(constValue); } @@ -379,9 +385,20 @@ public Node toNode() { result.withMember("required", required.stream().sorted().map(Node::from).collect(ArrayNode.collect())); } - if (!enumValues.isEmpty()) { - result.withOptionalMember("enum", getEnumValues() - .map(v -> v.stream().map(Node::from).collect(ArrayNode.collect()))); + if (!enumValues.isEmpty() || !intEnumValues.isEmpty()) { + ArrayNode.Builder builder = ArrayNode.builder(); + if (getIntEnumValues().isPresent()) { + for (Integer i : getIntEnumValues().get()) { + builder.withValue(i); + } + } + + if (getEnumValues().isPresent()) { + for (String s : getEnumValues().get()) { + builder.withValue(s); + } + } + result.withOptionalMember("enum", builder.build().asArrayNode()); } if (!allOf.isEmpty()) { @@ -486,6 +503,7 @@ public Builder toBuilder() { .ref(ref) .type(type) .enumValues(enumValues) + .intEnumValues(intEnumValues) .constValue(constValue) .defaultValue(defaultValue) @@ -554,6 +572,7 @@ public static final class Builder implements SmithyBuilder { private String ref; private String type; private Collection enumValues = ListUtils.of(); + private Collection intEnumValues = ListUtils.of(); private Node constValue; private Node defaultValue; @@ -625,6 +644,11 @@ public Builder enumValues(Collection enumValues) { return this; } + public Builder intEnumValues(Collection intEnumValues) { + this.intEnumValues = intEnumValues == null ? ListUtils.of() : intEnumValues; + return this; + } + public Builder constValue(Node constValue) { this.constValue = constValue; return this; @@ -858,7 +882,7 @@ public Builder disableProperty(String propertyName) { case "default": return this.defaultValue(null); case "enum": - return this.enumValues(null); + return this.enumValues(null).intEnumValues(null); case "multipleOf": return this.multipleOf(null); case "maximum": 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 23cf2627fe8..f7bea5ec398 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 @@ -722,4 +722,39 @@ public void defaultsCanBeDisabled() { IoUtils.toUtf8String(getClass().getResourceAsStream("default-values-disabled.jsonschema.v07.json"))); Node.assertEquals(document.toNode(), expected); } + + @Test + public void supportsIntEnumsByDefault() { + Model model = Model.assembler() + .addImport(getClass().getResource("int-enums.smithy")) + .assemble() + .unwrap(); + SchemaDocument document = JsonSchemaConverter.builder() + .model(model) + .build() + .convert(); + + Node expected = Node.parse( + IoUtils.toUtf8String(getClass().getResourceAsStream("int-enums.jsonschema.v07.json"))); + Node.assertEquals(document.toNode(), expected); + } + + @Test + public void intEnumsCanBeDisabled() { + Model model = Model.assembler() + .addImport(getClass().getResource("int-enums.smithy")) + .assemble() + .unwrap(); + JsonSchemaConfig config = new JsonSchemaConfig(); + config.setDisableIntEnums(true); + SchemaDocument document = JsonSchemaConverter.builder() + .config(config) + .model(model) + .build() + .convert(); + + Node expected = Node.parse( + IoUtils.toUtf8String(getClass().getResourceAsStream("int-enums-disabled.jsonschema.v07.json"))); + Node.assertEquals(document.toNode(), expected); + } } 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 3c218f32256..e663c9fb92d 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 @@ -17,6 +17,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.not; @@ -25,6 +26,8 @@ import java.util.Set; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import software.amazon.smithy.model.node.ArrayNode; +import software.amazon.smithy.model.node.Node; import software.amazon.smithy.utils.ListUtils; import software.amazon.smithy.utils.SetUtils; @@ -207,4 +210,18 @@ public void removingPropertiesRemovesRequiredPropertiesToo() { assertThat(schema.getProperties().keySet(), contains("bar")); assertThat(schema.getRequired(), contains("bar")); } + + @Test + public void mergesEnumValuesWhenConvertingToNode() { + Schema schema = Schema.builder() + .enumValues(ListUtils.of("foo", "bar")) + .intEnumValues(ListUtils.of(1, 2)) + .build(); + ArrayNode node = schema.toNode().asObjectNode().get().expectArrayMember("enum"); + assertThat(node.getElements(), containsInAnyOrder( + Node.from("foo"), + Node.from("bar"), + Node.from(1), + Node.from(2))); + } } diff --git a/smithy-jsonschema/src/test/resources/software/amazon/smithy/jsonschema/int-enums-disabled.jsonschema.v07.json b/smithy-jsonschema/src/test/resources/software/amazon/smithy/jsonschema/int-enums-disabled.jsonschema.v07.json new file mode 100644 index 00000000000..08b5ac5ba0c --- /dev/null +++ b/smithy-jsonschema/src/test/resources/software/amazon/smithy/jsonschema/int-enums-disabled.jsonschema.v07.json @@ -0,0 +1,12 @@ +{ + "definitions": { + "Foo": { + "type": "object", + "properties": { + "bar": { + "type": "number" + } + } + } + } +} diff --git a/smithy-jsonschema/src/test/resources/software/amazon/smithy/jsonschema/int-enums.jsonschema.v07.json b/smithy-jsonschema/src/test/resources/software/amazon/smithy/jsonschema/int-enums.jsonschema.v07.json new file mode 100644 index 00000000000..63a9209b7a6 --- /dev/null +++ b/smithy-jsonschema/src/test/resources/software/amazon/smithy/jsonschema/int-enums.jsonschema.v07.json @@ -0,0 +1,19 @@ +{ + "definitions": { + "Foo": { + "type": "object", + "properties": { + "bar": { + "$ref": "#/definitions/TestIntEnum" + } + } + }, + "TestIntEnum": { + "type": "number", + "enum": [ + 1, + 2 + ] + } + } +} diff --git a/smithy-jsonschema/src/test/resources/software/amazon/smithy/jsonschema/int-enums.smithy b/smithy-jsonschema/src/test/resources/software/amazon/smithy/jsonschema/int-enums.smithy new file mode 100644 index 00000000000..2cfd18be0c4 --- /dev/null +++ b/smithy-jsonschema/src/test/resources/software/amazon/smithy/jsonschema/int-enums.smithy @@ -0,0 +1,12 @@ +$version: "2.0" + +namespace smithy.example + +structure Foo { + bar: TestIntEnum +} + +intEnum TestIntEnum { + FOO = 1 + BAR = 2 +}