Skip to content

Commit f630cbb

Browse files
authored
Allow adding member docs to $ref (#2402)
Fixes #2400. When a member targets a structure, it becomes a schema reference when converted to JSON Schema. Previously, we didn't add member docs to the converted object, possibly because earlier versions of open api or JSON Schema did not support it. Reading through [this issue](OAI/OpenAPI-Specification#1514) the [OAI spec](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#reference-object), and the [JSON Schema Spec](https://json-schema.org/draft/2020-12/json-schema-core#section-8.2.3), it seems that OpenAPI 3.1 and JSON Schema 2020-12 support the `description` property alongside `$ref`. I wasn't able to find anything about whether it is supported in [JSON Schema 07](https://json-schema.org/draft-07/json-schema-release-notes). This commit adds a new config option, `addReferenceDescriptions` that will add the `description` property alongside `$ref` when the member has documentation. I made it opt-in through the config option so we don't cause any existing documentation to be changed unexpectedly.
1 parent 741b7e9 commit f630cbb

File tree

10 files changed

+260
-0
lines changed

10 files changed

+260
-0
lines changed

docs/source-2.0/guides/model-translations/converting-to-openapi.rst

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1053,6 +1053,58 @@ disableIntEnums (``boolean``)
10531053
}
10541054
10551055
1056+
.. _generate-openapi-setting-addReferenceDescriptions:
1057+
1058+
addReferenceDescriptions (``boolean``)
1059+
Set to ``true`` to add the ``description`` property to ``$ref`` members
1060+
with the value of the member's :ref:`documentation-trait` trait.
1061+
1062+
.. important::
1063+
1064+
This property is only supported when :ref:`version <generate-openapi-setting-version>`
1065+
is set to ``3.1.0``.
1066+
1067+
By default, ``$ref`` members will have no ``description``:
1068+
1069+
.. code-block:: smithy
1070+
:caption: example.smithy
1071+
1072+
structure Foo {
1073+
/// Member docs
1074+
bar: Bar
1075+
}
1076+
1077+
.. code-block:: json
1078+
:caption: Example.openapi.json
1079+
1080+
{
1081+
"Foo": {
1082+
"type": "object",
1083+
"properties": {
1084+
"bar": {
1085+
"$ref": "#/definitions/Bar"
1086+
}
1087+
}
1088+
}
1089+
}
1090+
1091+
With this enabled, member docs will be added:
1092+
1093+
.. code-block:: json
1094+
:caption: Example.openapi.json
1095+
1096+
{
1097+
"Foo": {
1098+
"type": "object",
1099+
"properties": {
1100+
"bar": {
1101+
"$ref": "#/definitions/Bar",
1102+
"description": "Member docs"
1103+
}
1104+
}
1105+
}
1106+
}
1107+
10561108
----------------
10571109
Security schemes
10581110
----------------

smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/JsonSchemaConfig.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ public String toString() {
114114
private boolean useIntegerType;
115115
private boolean disableDefaultValues = false;
116116
private boolean disableIntEnums = false;
117+
private boolean addReferenceDescriptions = false;
117118

118119
public JsonSchemaConfig() {
119120
nodeMapper.setWhenMissingSetter(NodeMapper.WhenMissing.IGNORE);
@@ -456,4 +457,26 @@ public JsonSchemaVersion getJsonSchemaVersion() {
456457
public void setJsonSchemaVersion(JsonSchemaVersion schemaVersion) {
457458
this.jsonSchemaVersion = Objects.requireNonNull(schemaVersion);
458459
}
460+
461+
/**
462+
* Whether to add the {@code description} property to Schema References
463+
* when converting Smithy member shapes into JSON Schema with the value
464+
* of the member's documentation.
465+
*
466+
* <p>Defaults to {@code false}.</p>
467+
*
468+
* @return Whether to add descriptions to Schema References.
469+
*/
470+
public boolean getAddReferenceDescriptions() {
471+
return addReferenceDescriptions;
472+
}
473+
474+
/**
475+
* Sets whether the {@code description} property should be added to Schema References.
476+
*
477+
* @param addReferenceDescriptions Whether to add descriptions to Schema References
478+
*/
479+
public void setAddReferenceDescriptions(boolean addReferenceDescriptions) {
480+
this.addReferenceDescriptions = addReferenceDescriptions;
481+
}
459482
}

smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/JsonSchemaShapeVisitor.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,11 @@ private Schema createRef(MemberShape member) {
106106
if (member.hasTrait(DeprecatedTrait.class) && getJsonSchemaVersion() != JsonSchemaVersion.DRAFT07) {
107107
refBuilder.deprecated(true);
108108
}
109+
110+
if (converter.getConfig().getAddReferenceDescriptions()) {
111+
descriptionMessage(member).ifPresent(refBuilder::description);
112+
}
113+
109114
// Wrap the ref and default in an allOf if disableDefaultValues has been not been disabled on config.
110115
if (member.hasTrait(DefaultTrait.class) && !converter.getConfig().getDisableDefaultValues()) {
111116
Schema def = Schema.builder().defaultValue(member.expectTrait(DefaultTrait.class).toNode()).build();

smithy-jsonschema/src/test/java/software/amazon/smithy/jsonschema/JsonSchemaConverterTest.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -887,4 +887,24 @@ public void dontAddDeprecatedTraitOnAMemberWhenOldVersion() {
887887
Schema memberSchema = document.getRootSchema().getProperties().get("member");
888888
assertThat(memberSchema.isDeprecated(), equalTo(false));
889889
}
890+
891+
@Test
892+
public void canAddMemberDocumentation() {
893+
Model model = Model.assembler()
894+
.addImport(getClass().getResource("member-documentation.smithy"))
895+
.assemble()
896+
.unwrap();
897+
898+
JsonSchemaConfig config = new JsonSchemaConfig();
899+
config.setAddReferenceDescriptions(true);
900+
SchemaDocument document = JsonSchemaConverter.builder()
901+
.config(config)
902+
.model(model)
903+
.build()
904+
.convert();
905+
906+
Node expected = Node.parse(
907+
IoUtils.toUtf8String(getClass().getResourceAsStream("member-documentation.jsonschema.json")));
908+
Node.assertEquals(document.toNode(), expected);
909+
}
890910
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"definitions": {
3+
"Foo": {
4+
"type": "object",
5+
"properties": {
6+
"foo": {
7+
"type": "string",
8+
"description": "simple docs"
9+
},
10+
"bar": {
11+
"$ref": "#/definitions/Bar",
12+
"description": "structure docs"
13+
}
14+
}
15+
},
16+
"Bar": {
17+
"type": "object"
18+
}
19+
}
20+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
$version: "2.0"
2+
3+
namespace smithy.example
4+
5+
structure Foo {
6+
/// simple docs
7+
foo: String
8+
9+
/// structure docs
10+
bar: Bar
11+
}
12+
13+
structure Bar {}

smithy-openapi/src/main/java/software/amazon/smithy/openapi/fromsmithy/OpenApiConverter.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
import software.amazon.smithy.model.validation.ValidationUtils;
5252
import software.amazon.smithy.openapi.OpenApiConfig;
5353
import software.amazon.smithy.openapi.OpenApiException;
54+
import software.amazon.smithy.openapi.OpenApiVersion;
5455
import software.amazon.smithy.openapi.model.ComponentsObject;
5556
import software.amazon.smithy.openapi.model.InfoObject;
5657
import software.amazon.smithy.openapi.model.OpenApi;
@@ -174,6 +175,12 @@ private ConversionEnvironment<? extends Trait> createConversionEnvironment(Model
174175
throw new OpenApiException("openapi is missing required property, `service`");
175176
}
176177

178+
if (config.getAddReferenceDescriptions() && config.getVersion() == OpenApiVersion.VERSION_3_0_2) {
179+
throw new OpenApiException(
180+
"openapi property `addReferenceDescriptions` requires openapi version 3.1.0 or later.\n"
181+
+ "Suggestion: Add `\"version\"`: \"3.1.0\" to your openapi config.");
182+
}
183+
177184
// Find the service shape.
178185
ServiceShape service = model.getShape(serviceShapeId)
179186
.orElseThrow(() -> new IllegalArgumentException(String.format(

smithy-openapi/src/test/java/software/amazon/smithy/openapi/fromsmithy/OpenApiConverterTest.java

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import static org.hamcrest.Matchers.equalTo;
2323
import static org.hamcrest.Matchers.not;
2424
import static org.junit.jupiter.api.Assertions.assertFalse;
25+
import static org.junit.jupiter.api.Assertions.assertThrows;
2526

2627
import java.util.Collections;
2728
import java.util.List;
@@ -639,4 +640,37 @@ public void removesMixins() {
639640

640641
Node.assertEquals(result, expectedNode);
641642
}
643+
644+
@Test
645+
public void convertsMemberDocumentation() {
646+
Model model = Model.assembler()
647+
.addImport(getClass().getResource("documentation-test-members.smithy"))
648+
.discoverModels()
649+
.assemble()
650+
.unwrap();
651+
OpenApiConfig config = new OpenApiConfig();
652+
config.setService(ShapeId.from("smithy.example#MyDocs"));
653+
config.setVersion(OpenApiVersion.VERSION_3_1_0);
654+
config.setAddReferenceDescriptions(true);
655+
Node result = OpenApiConverter.create().config(config).convertToNode(model);
656+
Node expectedNode = Node.parse(IoUtils.toUtf8String(
657+
getClass().getResourceAsStream("documentation-test-members.openapi.json")));
658+
659+
Node.assertEquals(result, expectedNode);
660+
}
661+
662+
@Test
663+
public void convertingMemberDocsRequired3_1() {
664+
Model model = Model.assembler()
665+
.addImport(getClass().getResource("documentation-test-members.smithy"))
666+
.discoverModels()
667+
.assemble()
668+
.unwrap();
669+
OpenApiConfig config = new OpenApiConfig();
670+
config.setService(ShapeId.from("smithy.example#MyDocs"));
671+
config.setAddReferenceDescriptions(true);
672+
OpenApiConverter converter = OpenApiConverter.create().config(config);
673+
674+
assertThrows(OpenApiException.class, () -> converter.convertToNode(model));
675+
}
642676
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
{
2+
"openapi": "3.1.0",
3+
"info": {
4+
"title": "MyDocs",
5+
"version": "2018-01-01",
6+
"description": "Service"
7+
},
8+
"paths": {
9+
"/": {
10+
"get": {
11+
"description": "Operation",
12+
"operationId": "MyDocsOperation",
13+
"responses": {
14+
"200": {
15+
"description": "MyDocsOperation 200 response",
16+
"content": {
17+
"application/json": {
18+
"schema": {
19+
"$ref": "#/components/schemas/MyDocsOperationResponseContent"
20+
}
21+
}
22+
}
23+
}
24+
}
25+
}
26+
}
27+
},
28+
"components": {
29+
"schemas": {
30+
"MyDocsOperationResponseContent": {
31+
"type": "object",
32+
"description": "Output",
33+
"properties": {
34+
"foo": {
35+
"type": "string",
36+
"description": "foo member."
37+
},
38+
"nested": {
39+
"$ref": "#/components/schemas/Nested",
40+
"description": "nested member."
41+
}
42+
}
43+
},
44+
"Nested": {
45+
"type": "object",
46+
"description": "Nested",
47+
"properties": {
48+
"baz": {
49+
"type": "string"
50+
}
51+
}
52+
}
53+
}
54+
}
55+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
$version: "2.0"
2+
3+
namespace smithy.example
4+
5+
/// Service
6+
@aws.protocols#restJson1
7+
service MyDocs {
8+
version: "2018-01-01",
9+
operations: [MyDocsOperation]
10+
}
11+
12+
/// Operation
13+
@http(method: "GET", uri: "/")
14+
@readonly
15+
operation MyDocsOperation {
16+
output: Output
17+
}
18+
19+
/// Output
20+
structure Output {
21+
/// foo member.
22+
foo: String,
23+
24+
/// nested member.
25+
nested: Nested,
26+
}
27+
28+
/// Nested
29+
structure Nested {
30+
baz: String,
31+
}

0 commit comments

Comments
 (0)