From 1f0cf182340ff7c02c4caa202d60f1a66999cc86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Vil=C3=A1?= Date: Thu, 11 Apr 2024 17:16:47 +0100 Subject: [PATCH] Support `deprecated` in OpenAPI conversion (#2221) * Add failing test for deprecated on a field * Set deprecated for fields in the json schema conversion * add `deprecated` to sample cfn template * move test to right place, fix it and only do this for later json schema versions * add another test * undo import changes * undo change * use more consistent style * add negative tests * correct test * reformat Co-authored-by: Kevin Stich * reformat Co-authored-by: Kevin Stich * reformat Co-authored-by: Kevin Stich * reformat Co-authored-by: Kevin Stich --------- Co-authored-by: Kevin Stich --- .../jsonschema/JsonSchemaShapeVisitor.java | 18 ++- .../amazon/smithy/jsonschema/Schema.java | 20 ++++ .../jsonschema/JsonSchemaConverterTest.java | 107 ++++++++++++++++++ 3 files changed, 142 insertions(+), 3 deletions(-) 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 46550dfff93..0bd36b43532 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 @@ -43,6 +43,7 @@ import software.amazon.smithy.model.shapes.TimestampShape; import software.amazon.smithy.model.shapes.UnionShape; import software.amazon.smithy.model.traits.DefaultTrait; +import software.amazon.smithy.model.traits.DeprecatedTrait; import software.amazon.smithy.model.traits.DocumentationTrait; import software.amazon.smithy.model.traits.EnumTrait; import software.amazon.smithy.model.traits.LengthTrait; @@ -100,13 +101,16 @@ private Schema createRef(MemberShape member) { if (converter.isInlined(member)) { return member.accept(this); } else { + Schema.Builder refBuilder = Schema.builder().ref(converter.toPointer(member.getTarget())); + if (member.hasTrait(DeprecatedTrait.class) && getJsonSchemaVersion() != JsonSchemaVersion.DRAFT07) { + refBuilder.deprecated(true); + } // Wrap the ref and default in an allOf if disableDefaultValues has been not been disabled on config. if (member.hasTrait(DefaultTrait.class) && !converter.getConfig().getDisableDefaultValues()) { - Schema ref = Schema.builder().ref(converter.toPointer(member.getTarget())).build(); Schema def = Schema.builder().defaultValue(member.expectTrait(DefaultTrait.class).toNode()).build(); - return Schema.builder().allOf(ListUtils.of(ref, def)).build(); + return Schema.builder().allOf(ListUtils.of(refBuilder.build(), def)).build(); } - return Schema.builder().ref(converter.toPointer(member.getTarget())).build(); + return refBuilder.build(); } } @@ -329,6 +333,10 @@ private Schema.Builder updateBuilder(Shape shape, Schema.Builder builder) { builder.defaultValue(shape.expectTrait(DefaultTrait.class).toNode()); } + if (shape.hasTrait(DeprecatedTrait.class) && getJsonSchemaVersion() != JsonSchemaVersion.DRAFT07) { + builder.deprecated(true); + } + return builder; } @@ -351,4 +359,8 @@ private Schema buildSchema(Shape shape, Schema.Builder builder) { return builder.build(); } + + private JsonSchemaVersion getJsonSchemaVersion() { + return converter.getConfig().getJsonSchemaVersion(); + } } 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 08a33ccf826..7d820b970cd 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 @@ -102,6 +102,7 @@ public final class Schema implements ToNode, ToSmithyBuilder { private final boolean writeOnly; private final String comment; private final Node examples; + private final boolean deprecated; private final String contentEncoding; private final String contentMediaType; @@ -153,6 +154,7 @@ private Schema(Builder builder) { writeOnly = builder.writeOnly; comment = builder.comment; examples = builder.examples; + deprecated = builder.deprecated; contentEncoding = builder.contentEncoding; contentMediaType = builder.contentMediaType; @@ -312,6 +314,10 @@ public Optional getExamples() { return Optional.ofNullable(examples); } + public boolean isDeprecated() { + return deprecated; + } + public Optional getContentEncoding() { return Optional.ofNullable(contentEncoding); } @@ -364,6 +370,7 @@ public Node toNode() { .withOptionalMember("comment", getComment().map(Node::from)) .withOptionalMember("examples", getExamples()) + .withOptionalMember("deprecated", this.deprecated ? Optional.of(Node.from(true)) : Optional.empty()) .withOptionalMember("title", getTitle().map(Node::from)) .withOptionalMember("description", getDescription().map(Node::from)) .withOptionalMember("format", getFormat().map(Node::from)) @@ -421,6 +428,10 @@ public Node toNode() { result.withMember("writeOnly", Node.from(true)); } + if (deprecated) { + result.withMember("deprecated", Node.from(true)); + } + for (Map.Entry entry : extensions.entrySet()) { result.withMember(entry.getKey(), entry.getValue().toNode()); } @@ -540,6 +551,7 @@ public Builder toBuilder() { .writeOnly(writeOnly) .comment(comment) .examples(examples) + .deprecated(deprecated) .contentEncoding(contentEncoding) .contentMediaType(contentMediaType); @@ -611,6 +623,7 @@ public static final class Builder implements SmithyBuilder { private boolean writeOnly; private String comment; private Node examples; + private boolean deprecated; private String contentEncoding; private String contentMediaType; @@ -853,6 +866,11 @@ public Builder examples(Node examples) { return this; } + public Builder deprecated(boolean deprecated) { + this.deprecated = deprecated; + return this; + } + public Builder extensions(Map extensions) { this.extensions.clear(); this.extensions.putAll(extensions); @@ -945,6 +963,8 @@ public Builder disableProperty(String propertyName) { return this.contentMediaType(null); case "examples": return this.examples(null); + case "deprecated": + return this.deprecated(false); default: LOGGER.warning("Unknown JSON Schema config 'disable' property: " + propertyName); 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 c65c8b9b353..21410679d43 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 @@ -54,6 +54,7 @@ import software.amazon.smithy.model.shapes.StringShape; import software.amazon.smithy.model.shapes.StructureShape; import software.amazon.smithy.model.shapes.UnionShape; +import software.amazon.smithy.model.traits.DeprecatedTrait; import software.amazon.smithy.model.traits.DocumentationTrait; import software.amazon.smithy.model.traits.EnumDefinition; import software.amazon.smithy.model.traits.EnumTrait; @@ -780,4 +781,110 @@ public void intEnumsCanBeDisabled() { IoUtils.toUtf8String(getClass().getResourceAsStream("int-enums-disabled.jsonschema.v07.json"))); Node.assertEquals(document.toNode(), expected); } + + @Test + public void supportsDeprecatedTraitOnAStruct() { + StringShape string = StringShape.builder().id("smithy.api#String").build(); + StructureShape shape = StructureShape.builder() + .id(ShapeId.from("a.b#C")) + .addMember(MemberShape.builder() + .id(ShapeId.from("a.b#C$member")) + .target(string.getId()) + .build()) + .addTrait(DeprecatedTrait.builder() + .message("I'm deprecated") + .since("sinceVersion") + .build()) + .build(); + Model model = Model.builder().addShapes(shape, string).build(); + JsonSchemaConfig config = new JsonSchemaConfig(); + config.setJsonSchemaVersion(JsonSchemaVersion.DRAFT2020_12); + SchemaDocument document = JsonSchemaConverter.builder() + .model(model) + .config(config) + .build() + .convertShape(shape); + + assertThat(document.getRootSchema().isDeprecated(), equalTo(true)); + } + + @Test + public void dontAddDeprecatedTraitOnAStructWhenOldVersion() { + StringShape string = StringShape.builder().id("smithy.api#String").build(); + StructureShape shape = StructureShape.builder() + .id(ShapeId.from("a.b#C")) + .addMember(MemberShape.builder() + .id(ShapeId.from("a.b#C$member")) + .target(string.getId()) + .build()) + .addTrait(DeprecatedTrait.builder() + .message("I'm deprecated") + .since("sinceVersion") + .build()) + .build(); + Model model = Model.builder().addShapes(shape, string).build(); + JsonSchemaConfig config = new JsonSchemaConfig(); + config.setJsonSchemaVersion(JsonSchemaVersion.DRAFT07); + SchemaDocument document = JsonSchemaConverter.builder() + .model(model) + .config(config) + .build() + .convertShape(shape); + + assertThat(document.getRootSchema().isDeprecated(), equalTo(false)); + } + + @Test + public void supportsDeprecatedTraitOnAMember() { + StringShape string = StringShape.builder().id("smithy.api#String").build(); + StructureShape shape = StructureShape.builder() + .id(ShapeId.from("a.b#C")) + .addMember(MemberShape.builder() + .id(ShapeId.from("a.b#C$member")) + .target(string.getId()) + .addTrait(DeprecatedTrait.builder() + .message("I'm deprecated") + .since("sinceVersion") + .build()) + .build()) + .build(); + Model model = Model.builder().addShapes(shape, string).build(); + JsonSchemaConfig config = new JsonSchemaConfig(); + config.setJsonSchemaVersion(JsonSchemaVersion.DRAFT2020_12); + SchemaDocument document = JsonSchemaConverter.builder() + .model(model) + .config(config) + .build() + .convertShape(shape); + + Schema memberSchema = document.getRootSchema().getProperties().get("member"); + assertThat(memberSchema.isDeprecated(), equalTo(true)); + } + + @Test + public void dontAddDeprecatedTraitOnAMemberWhenOldVersion() { + StringShape string = StringShape.builder().id("smithy.api#String").build(); + StructureShape shape = StructureShape.builder() + .id(ShapeId.from("a.b#C")) + .addMember(MemberShape.builder() + .id(ShapeId.from("a.b#C$member")) + .target(string.getId()) + .addTrait(DeprecatedTrait.builder() + .message("I'm deprecated") + .since("sinceVersion") + .build()) + .build()) + .build(); + Model model = Model.builder().addShapes(shape, string).build(); + JsonSchemaConfig config = new JsonSchemaConfig(); + config.setJsonSchemaVersion(JsonSchemaVersion.DRAFT07); + SchemaDocument document = JsonSchemaConverter.builder() + .model(model) + .config(config) + .build() + .convertShape(shape); + + Schema memberSchema = document.getRootSchema().getProperties().get("member"); + assertThat(memberSchema.isDeprecated(), equalTo(false)); + } }