Skip to content

Commit

Permalink
Allow adding member docs to $ref
Browse files Browse the repository at this point in the history
Fixes smithy-lang#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.
  • Loading branch information
milesziemer committed Sep 23, 2024
1 parent 7813acb commit c1ff12b
Show file tree
Hide file tree
Showing 9 changed files with 208 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ public String toString() {
private boolean useIntegerType;
private boolean disableDefaultValues = false;
private boolean disableIntEnums = false;
private boolean addReferenceDescriptions = false;

public JsonSchemaConfig() {
nodeMapper.setWhenMissingSetter(NodeMapper.WhenMissing.IGNORE);
Expand Down Expand Up @@ -456,4 +457,26 @@ public JsonSchemaVersion getJsonSchemaVersion() {
public void setJsonSchemaVersion(JsonSchemaVersion schemaVersion) {
this.jsonSchemaVersion = Objects.requireNonNull(schemaVersion);
}

/**
* Whether to add the {@code description} property to Schema References
* when converting Smithy member shapes into JSON Schema with the value
* of the member's documentation.
*
* <p>Defaults to {@code false}.</p>
*
* @return Whether to add descriptions to Schema References.
*/
public boolean getAddReferenceDescriptions() {
return addReferenceDescriptions;
}

/**
* Sets whether the {@code description} property should be added to Schema References.
*
* @param addReferenceDescriptions Whether to add descriptions to Schema References
*/
public void setAddReferenceDescriptions(boolean addReferenceDescriptions) {
this.addReferenceDescriptions = addReferenceDescriptions;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,11 @@ private Schema createRef(MemberShape member) {
if (member.hasTrait(DeprecatedTrait.class) && getJsonSchemaVersion() != JsonSchemaVersion.DRAFT07) {
refBuilder.deprecated(true);
}

if (converter.getConfig().getAddReferenceDescriptions()) {
descriptionMessage(member).ifPresent(refBuilder::description);
}

// 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 def = Schema.builder().defaultValue(member.expectTrait(DefaultTrait.class).toNode()).build();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -887,4 +887,24 @@ public void dontAddDeprecatedTraitOnAMemberWhenOldVersion() {
Schema memberSchema = document.getRootSchema().getProperties().get("member");
assertThat(memberSchema.isDeprecated(), equalTo(false));
}

@Test
public void canAddMemberDocumentation() {
Model model = Model.assembler()
.addImport(getClass().getResource("member-documentation.smithy"))
.assemble()
.unwrap();

JsonSchemaConfig config = new JsonSchemaConfig();
config.setAddReferenceDescriptions(true);
SchemaDocument document = JsonSchemaConverter.builder()
.config(config)
.model(model)
.build()
.convert();

Node expected = Node.parse(
IoUtils.toUtf8String(getClass().getResourceAsStream("member-documentation.jsonschema.json")));
Node.assertEquals(document.toNode(), expected);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"definitions": {
"Foo": {
"type": "object",
"properties": {
"foo": {
"type": "string",
"description": "simple docs"
},
"bar": {
"$ref": "#/definitions/Bar",
"description": "structure docs"
}
}
},
"Bar": {
"type": "object"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
$version: "2.0"

namespace smithy.example

structure Foo {
/// simple docs
foo: String

/// structure docs
bar: Bar
}

structure Bar {}
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
import software.amazon.smithy.model.validation.ValidationUtils;
import software.amazon.smithy.openapi.OpenApiConfig;
import software.amazon.smithy.openapi.OpenApiException;
import software.amazon.smithy.openapi.OpenApiVersion;
import software.amazon.smithy.openapi.model.ComponentsObject;
import software.amazon.smithy.openapi.model.InfoObject;
import software.amazon.smithy.openapi.model.OpenApi;
Expand Down Expand Up @@ -174,6 +175,12 @@ private ConversionEnvironment<? extends Trait> createConversionEnvironment(Model
throw new OpenApiException("openapi is missing required property, `service`");
}

if (config.getAddReferenceDescriptions() && config.getVersion() == OpenApiVersion.VERSION_3_0_2) {
throw new OpenApiException(
"openapi property `addReferenceDescriptions` requires openapi version 3.1.0 or later.\n"
+ "Suggestion: Add `\"version\"`: \"3.1.0\" to your openapi config.");
}

// Find the service shape.
ServiceShape service = model.getShape(serviceShapeId)
.orElseThrow(() -> new IllegalArgumentException(String.format(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.not;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;

import java.util.Collections;
import java.util.List;
Expand Down Expand Up @@ -639,4 +640,37 @@ public void removesMixins() {

Node.assertEquals(result, expectedNode);
}

@Test
public void convertsMemberDocumentation() {
Model model = Model.assembler()
.addImport(getClass().getResource("documentation-test-members.smithy"))
.discoverModels()
.assemble()
.unwrap();
OpenApiConfig config = new OpenApiConfig();
config.setService(ShapeId.from("smithy.example#MyDocs"));
config.setVersion(OpenApiVersion.VERSION_3_1_0);
config.setAddReferenceDescriptions(true);
Node result = OpenApiConverter.create().config(config).convertToNode(model);
Node expectedNode = Node.parse(IoUtils.toUtf8String(
getClass().getResourceAsStream("documentation-test-members.openapi.json")));

Node.assertEquals(result, expectedNode);
}

@Test
public void convertingMemberDocsRequired3_1() {
Model model = Model.assembler()
.addImport(getClass().getResource("documentation-test-members.smithy"))
.discoverModels()
.assemble()
.unwrap();
OpenApiConfig config = new OpenApiConfig();
config.setService(ShapeId.from("smithy.example#MyDocs"));
config.setAddReferenceDescriptions(true);
OpenApiConverter converter = OpenApiConverter.create().config(config);

assertThrows(OpenApiException.class, () -> converter.convertToNode(model));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
{
"openapi": "3.1.0",
"info": {
"title": "MyDocs",
"version": "2018-01-01",
"description": "Service"
},
"paths": {
"/": {
"get": {
"description": "Operation",
"operationId": "MyDocsOperation",
"responses": {
"200": {
"description": "MyDocsOperation 200 response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/MyDocsOperationResponseContent"
}
}
}
}
}
}
}
},
"components": {
"schemas": {
"MyDocsOperationResponseContent": {
"type": "object",
"description": "Output",
"properties": {
"foo": {
"type": "string",
"description": "foo member."
},
"nested": {
"$ref": "#/components/schemas/Nested",
"description": "nested member."
}
}
},
"Nested": {
"type": "object",
"description": "Nested",
"properties": {
"baz": {
"type": "string"
}
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
$version: "2.0"

namespace smithy.example

/// Service
@aws.protocols#restJson1
service MyDocs {
version: "2018-01-01",
operations: [MyDocsOperation]
}

/// Operation
@http(method: "GET", uri: "/")
@readonly
operation MyDocsOperation {
output: Output
}

/// Output
structure Output {
/// foo member.
foo: String,

/// nested member.
nested: Nested,
}

/// Nested
structure Nested {
baz: String,
}

0 comments on commit c1ff12b

Please sign in to comment.