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 a51ceca2e09..4f034610dd7 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 @@ -85,11 +85,42 @@ public String toString() { } } + /** + * Configures how Smithy enum shapes are converted to JSON Schema + */ + public enum EnumStrategy { + + /** + * Converts to a schema that uses enum, which contains an array of strings + * + *

This is the default setting used if not configured. + */ + ENUM("enum"), + + /** + * Converts to a schema that uses oneOf, with an array of const strings and optional + * descriptions for documentation purposes + */ + ONE_OF("oneOf"); + + private final String stringValue; + + EnumStrategy(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 MapStrategy mapStrategy = MapStrategy.PROPERTY_NAMES; + private EnumStrategy enumStrategy = EnumStrategy.ENUM; private String definitionPointer; private ObjectNode schemaDocumentExtensions = Node.objectNode(); private ObjectNode extensions = Node.objectNode(); @@ -186,6 +217,18 @@ public void setMapStrategy(MapStrategy mapStrategy) { this.mapStrategy = mapStrategy; } + public EnumStrategy getEnumStrategy() { + return enumStrategy; + } + + /** + * Configures how Smithy enum shapes ae converted to JSON Schema + * @param enumStrategy The enum strategy to use + */ + public void setEnumStrategy(EnumStrategy enumStrategy) { + this.enumStrategy = enumStrategy; + } + public String getDefinitionPointer() { return definitionPointer != null ? definitionPointer : jsonSchemaVersion.getDefaultDefinitionPointer(); } 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 d10064f068c..1c4276d5e53 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 @@ -7,11 +7,13 @@ import java.util.ArrayList; import java.util.Collection; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.regex.Pattern; import software.amazon.smithy.model.Model; import software.amazon.smithy.model.node.Node.NonNumericFloat; +import software.amazon.smithy.model.node.StringNode; import software.amazon.smithy.model.shapes.BigDecimalShape; import software.amazon.smithy.model.shapes.BigIntegerShape; import software.amazon.smithy.model.shapes.BlobShape; @@ -19,6 +21,7 @@ import software.amazon.smithy.model.shapes.ByteShape; import software.amazon.smithy.model.shapes.DocumentShape; import software.amazon.smithy.model.shapes.DoubleShape; +import software.amazon.smithy.model.shapes.EnumShape; import software.amazon.smithy.model.shapes.FloatShape; import software.amazon.smithy.model.shapes.IntegerShape; import software.amazon.smithy.model.shapes.ListShape; @@ -248,6 +251,43 @@ public Schema unionShape(UnionShape shape) { } } + @Override + public Schema enumShape(EnumShape shape) { + JsonSchemaConfig.EnumStrategy enumStrategy = converter.getConfig().getEnumStrategy(); + + switch (enumStrategy) { + case ENUM: + return super.enumShape(shape); + case ONE_OF: + Map enumValues = shape.getEnumValues(); + List schemas = new ArrayList<>(); + + for (Map.Entry entry : shape.getAllMembers().entrySet()) { + String memberName = entry.getKey(); + MemberShape member = entry.getValue(); + Schema enumSchema = Schema.builder() + .constValue(StringNode.from(enumValues.get(memberName))) + .description(member.getTrait(DocumentationTrait.class) + .map(DocumentationTrait::getValue) + .orElse(null)) + .build(); + + schemas.add(enumSchema); + } + + return buildSchema(shape, + Schema.builder() + .description(shape.getTrait(DocumentationTrait.class) + .map(DocumentationTrait::getValue) + .orElse(null)) + .type("string") + .oneOf(schemas)); + default: { + throw new SmithyJsonSchemaException(String.format("Unsupported enum strategy: %s", enumStrategy)); + } + } + } + @Override public Schema timestampShape(TimestampShape shape) { return buildSchema(shape, createBuilder(shape, "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 e4b4b00bbd4..c0432128934 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 @@ -817,6 +817,43 @@ public void supportsIntEnumsByDefault() { Node.assertEquals(document.toNode(), expected); } + @Test + public void supportsDefaultEnumStrategy() { + Model model = Model.assembler() + .addImport(getClass().getResource("string-enums.smithy")) + .assemble() + .unwrap(); + + SchemaDocument document = JsonSchemaConverter.builder() + .model(model) + .build() + .convert(); + + Node expected = Node.parse( + IoUtils.toUtf8String(getClass().getResourceAsStream("string-enums.jsonschema.v07.json"))); + Node.assertEquals(document.toNode(), expected); + } + + @Test + public void supportsOneOfEnumStrategy() { + Model model = Model.assembler() + .addImport(getClass().getResource("string-enums.smithy")) + .assemble() + .unwrap(); + + JsonSchemaConfig config = new JsonSchemaConfig(); + config.setEnumStrategy(JsonSchemaConfig.EnumStrategy.ONE_OF); + SchemaDocument document = JsonSchemaConverter.builder() + .model(model) + .config(config) + .build() + .convert(); + + Node expected = Node.parse( + IoUtils.toUtf8String(getClass().getResourceAsStream("string-enums-one-of.jsonschema.v07.json"))); + Node.assertEquals(document.toNode(), expected); + } + @Test public void intEnumsCanBeDisabled() { Model model = Model.assembler() diff --git a/smithy-jsonschema/src/test/resources/software/amazon/smithy/jsonschema/string-enums-one-of.jsonschema.v07.json b/smithy-jsonschema/src/test/resources/software/amazon/smithy/jsonschema/string-enums-one-of.jsonschema.v07.json new file mode 100644 index 00000000000..2bda54d8eeb --- /dev/null +++ b/smithy-jsonschema/src/test/resources/software/amazon/smithy/jsonschema/string-enums-one-of.jsonschema.v07.json @@ -0,0 +1,25 @@ +{ + "definitions": { + "Foo": { + "type": "object", + "properties": { + "bar": { + "$ref": "#/definitions/TestEnum" + } + } + }, + "TestEnum": { + "type": "string", + "description": "This is a test enum", + "oneOf": [ + { + "const": "Foo", + "description": "it really does foo" + }, + { + "const": "Bar" + } + ] + } + } +} diff --git a/smithy-jsonschema/src/test/resources/software/amazon/smithy/jsonschema/string-enums.jsonschema.v07.json b/smithy-jsonschema/src/test/resources/software/amazon/smithy/jsonschema/string-enums.jsonschema.v07.json new file mode 100644 index 00000000000..905f26a4a93 --- /dev/null +++ b/smithy-jsonschema/src/test/resources/software/amazon/smithy/jsonschema/string-enums.jsonschema.v07.json @@ -0,0 +1,20 @@ +{ + "definitions": { + "Foo": { + "type": "object", + "properties": { + "bar": { + "$ref": "#/definitions/TestEnum" + } + } + }, + "TestEnum": { + "type": "string", + "enum": [ + "Foo", + "Bar" + ], + "description": "This is a test enum" + } + } +} diff --git a/smithy-jsonschema/src/test/resources/software/amazon/smithy/jsonschema/string-enums.smithy b/smithy-jsonschema/src/test/resources/software/amazon/smithy/jsonschema/string-enums.smithy new file mode 100644 index 00000000000..798281d6558 --- /dev/null +++ b/smithy-jsonschema/src/test/resources/software/amazon/smithy/jsonschema/string-enums.smithy @@ -0,0 +1,14 @@ +$version: "2.0" + +namespace smithy.example + +structure Foo { + bar: TestEnum +} + +@documentation("This is a test enum") +enum TestEnum { + @documentation("it really does foo") + FOO = "Foo" + BAR = "Bar" +}