Skip to content

Commit

Permalink
Add JSON Schema support for patternProperties
Browse files Browse the repository at this point in the history
  • Loading branch information
kstich committed Oct 21, 2020
1 parent 10df05c commit 263df67
Show file tree
Hide file tree
Showing 5 changed files with 178 additions and 4 deletions.
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

0 comments on commit 263df67

Please sign in to comment.