Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add JSON Schema support for patternProperties #611

Merged
merged 1 commit into from
Oct 21, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions docs/source/1.0/guides/converting-to-openapi.rst
Original file line number Diff line number Diff line change
Expand Up @@ -514,6 +514,31 @@ unionStrategy (``string``)
}
}

mapStrategy (``string``)
Configures how Smithy map shapes are converted to JSON Schema.

This property must be a string set to one of the following values:

* ``propertyNames``: Converts to a schema that uses a combination of
"propertyNames" and "additionalProperties". This is the default setting
used if not configured.
* ``patternProperties``: Converts to a schema that uses
"patternProperties". If a map's key member or its target does not have a
"pattern" trait, a default indicating one or more of any character (".+")
is applied.

.. code-block:: json

{
"version": "1.0",
"plugins": {
"openapi": {
"service": "smithy.example#Weather",
"mapStrategy": "propertyNames"
}
}
}

schemaDocumentExtensions (``Map<String, any>``)
Adds custom top-level key-value pairs to the created OpenAPI specification.
Any existing value is overwritten.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,42 @@ public String toString() {
}
}

/**
* Configures how Smithy map shapes are converted to JSON Schema.
*/
public enum MapStrategy {
/**
* Converts to a schema that uses a combination of "propertyNames"
* and "additionalProperties".
*
* <p>This is the default setting used if not configured.
*/
PROPERTY_NAMES("propertyNames"),

/**
* Converts to a schema that uses "patternProperties". If a map's key
* member or its target does not have a {@code pattern} trait, a default
* indicating one or more of any character (".+") is applied.
*/
PATTERN_PROPERTIES("patternProperties");

private String stringValue;

MapStrategy(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 String definitionPointer = "#/definitions";
private ObjectNode schemaDocumentExtensions = Node.objectNode();
private ObjectNode extensions = Node.objectNode();
Expand Down Expand Up @@ -140,6 +172,19 @@ public void setUnionStrategy(UnionStrategy unionStrategy) {
this.unionStrategy = unionStrategy;
}

public MapStrategy getMapStrategy() {
return mapStrategy;
}

/**
* Configures how Smithy map shapes are converted to JSON Schema.
*
* @param mapStrategy The map strategy to use.
*/
public void setMapStrategy(MapStrategy mapStrategy) {
this.mapStrategy = mapStrategy;
}

public String getDefinitionPointer() {
return definitionPointer;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,9 +109,23 @@ private Schema createRef(MemberShape member) {

@Override
public Schema mapShape(MapShape shape) {
return buildSchema(shape, createBuilder(shape, "object")
.propertyNames(createRef(shape.getKey()))
.additionalProperties(createRef(shape.getValue())));
JsonSchemaConfig.MapStrategy mapStrategy = converter.getConfig().getMapStrategy();

switch (mapStrategy) {
case PROPERTY_NAMES:
return buildSchema(shape, createBuilder(shape, "object")
.propertyNames(createRef(shape.getKey()))
.additionalProperties(createRef(shape.getValue())));
case PATTERN_PROPERTIES:
String keyPattern = shape.getKey().getMemberTrait(model, PatternTrait.class)
.map(PatternTrait::getPattern)
.map(Pattern::pattern)
.orElse(".+");
return buildSchema(shape, createBuilder(shape, "object")
.putPatternProperty(keyPattern, createRef(shape.getValue())));
default:
throw new SmithyJsonSchemaException(String.format("Unsupported map strategy: %s", mapStrategy));
}
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@
* version of JSON Schema. The following properties are not supported:
*
* <ul>
* <li>patternProperties</li>
* <li>dependencies</li>
* <li>if</li>
* <li>then</li>
Expand Down Expand Up @@ -88,6 +87,7 @@ public final class Schema implements ToNode, ToSmithyBuilder<Schema> {
private final Map<String, Schema> properties;
private final Schema additionalProperties;
private final Schema propertyNames;
private final Map<String, Schema> patternProperties;

private final List<Schema> allOf;
private final List<Schema> anyOf;
Expand Down Expand Up @@ -137,6 +137,7 @@ private Schema(Builder builder) {
maxProperties = builder.maxProperties;
minProperties = builder.minProperties;
propertyNames = builder.propertyNames;
patternProperties = builder.patternProperties;

allOf = ListUtils.copyOf(builder.allOf);
oneOf = ListUtils.copyOf(builder.oneOf);
Expand Down Expand Up @@ -257,6 +258,10 @@ public Optional<Schema> getPropertyNames() {
return Optional.ofNullable(propertyNames);
}

public Map<String, Schema> getPatternProperties() {
return patternProperties;
}

public List<Schema> getAllOf() {
return allOf;
}
Expand Down Expand Up @@ -365,6 +370,11 @@ public Node toNode() {
.collect(ObjectNode.collectStringKeys(Map.Entry::getKey, e -> e.getValue().toNode())));
}

if (!patternProperties.isEmpty()) {
result.withMember("patternProperties", patternProperties.entrySet().stream()
.collect(ObjectNode.collectStringKeys(Map.Entry::getKey, e -> e.getValue().toNode())));
}

if (!required.isEmpty()) {
result.withMember("required", required.stream().sorted().map(Node::from).collect(ArrayNode.collect()));
}
Expand Down Expand Up @@ -516,6 +526,7 @@ public Builder toBuilder() {
.contentEncoding(contentEncoding)
.contentMediaType(contentMediaType);
properties.forEach(builder::putProperty);
patternProperties.forEach(builder::putPatternProperty);
extensions.forEach(builder::putExtension);
return builder;
}
Expand Down Expand Up @@ -567,6 +578,7 @@ public static final class Builder implements SmithyBuilder<Schema> {
private Map<String, Schema> properties = new HashMap<>();
private Schema additionalProperties;
private Schema propertyNames;
private Map<String, Schema> patternProperties = new HashMap<>();

private List<Schema> allOf = ListUtils.of();
private List<Schema> anyOf = ListUtils.of();
Expand Down Expand Up @@ -728,6 +740,26 @@ public Builder propertyNames(Schema propertyNames) {
return this;
}

public Builder patternProperties(Map<String, Schema> patternProperties) {
this.patternProperties.clear();

if (patternProperties != null) {
patternProperties.forEach(this::putPatternProperty);
}

return this;
}

public Builder putPatternProperty(String key, Schema value) {
this.patternProperties.put(key, value);
return this;
}

public Builder removePatternProperty(String key) {
patternProperties.remove(key);
return this;
}

public Builder allOf(List<Schema> allOf) {
this.allOf = allOf == null ? ListUtils.of() : allOf;
return this;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,64 @@ public void supportsUnionStructure() {
assertThat(schema.getProperties().keySet(), contains("foo"));
}

@Test
public void supportsMapPropertyNames() {
ShapeId shapeId = ShapeId.from("smithy.api#String");
StringShape string = StringShape.builder().id(shapeId).build();
MapShape map = MapShape.builder().id("a.b#Map").key(shapeId).value(shapeId).build();
Model model = Model.builder().addShapes(map, string).build();
SchemaDocument document = JsonSchemaConverter.builder().model(model).build().convertShape(map);
Schema schema = document.getRootSchema();

assertTrue(schema.getPropertyNames().isPresent());
assertThat(schema.getPropertyNames().get().getType().get(), equalTo("string"));
assertTrue(schema.getAdditionalProperties().isPresent());
assertThat(schema.getAdditionalProperties().get().getType().get(), equalTo("string"));
}

@Test
public void supportsMapPatternProperties() {
ShapeId shapeId = ShapeId.from("smithy.api#String");
StringShape string = StringShape.builder().id(shapeId).build();
String pattern = "[a-z]{1,16}";
StringShape key = StringShape.builder().id("a.b#Key")
.addTrait(new PatternTrait(pattern)).build();
MapShape map = MapShape.builder().id("a.b#Map").key(key.getId()).value(shapeId).build();
Model model = Model.builder().addShapes(map, key, string).build();
JsonSchemaConfig config = new JsonSchemaConfig();
config.setMapStrategy(JsonSchemaConfig.MapStrategy.PATTERN_PROPERTIES);
SchemaDocument document = JsonSchemaConverter.builder()
.config(config)
.model(model)
.build()
.convertShape(map);
Schema schema = document.getRootSchema();

assertThat(schema.getPatternProperties().size(), equalTo(1));
assertTrue(schema.getPatternProperties().containsKey(pattern));
assertThat(schema.getPatternProperties().get(pattern).getType().get(), equalTo("string"));
}

@Test
public void supportsMapPatternPropertiesWithDefaultPattern() {
ShapeId shapeId = ShapeId.from("smithy.api#String");
StringShape string = StringShape.builder().id(shapeId).build();
MapShape map = MapShape.builder().id("a.b#Map").key(shapeId).value(shapeId).build();
Model model = Model.builder().addShapes(map, string).build();
JsonSchemaConfig config = new JsonSchemaConfig();
config.setMapStrategy(JsonSchemaConfig.MapStrategy.PATTERN_PROPERTIES);
SchemaDocument document = JsonSchemaConverter.builder()
.config(config)
.model(model)
.build()
.convertShape(map);
Schema schema = document.getRootSchema();

assertThat(schema.getPatternProperties().size(), equalTo(1));
assertTrue(schema.getPatternProperties().containsKey(".+"));
assertThat(schema.getPatternProperties().get(".+").getType().get(), equalTo("string"));
}

@Test
public void convertingToBuilderGivesSameResult() {
Model model = Model.assembler()
Expand Down