detectJsonTimestampFormat(Shape shape) {
}
return Optional.empty();
}
+
+ public boolean getSupportNonNumericFloats() {
+ return supportNonNumericFloats;
+ }
+
+ /**
+ * Set to true to add support for NaN, Infinity, and -Infinity in float
+ * and double shapes. These values will be serialized as strings. The
+ * OpenAPI document will be updated to refer to them as a "oneOf" of number
+ * and string.
+ *
+ * By default, non-numeric values are not supported.
+ *
+ * @param supportNonNumericFloats True if non-numeric float values should be supported.
+ */
+ public void setSupportNonNumericFloats(boolean supportNonNumericFloats) {
+ this.supportNonNumericFloats = supportNonNumericFloats;
+ }
}
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 6c46addcc61..00bf8e5940d 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
@@ -18,8 +18,10 @@
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
+import java.util.Set;
import java.util.regex.Pattern;
import software.amazon.smithy.model.Model;
+import software.amazon.smithy.model.node.Node.NonNumericFloat;
import software.amazon.smithy.model.shapes.BigDecimalShape;
import software.amazon.smithy.model.shapes.BigIntegerShape;
import software.amazon.smithy.model.shapes.BlobShape;
@@ -53,6 +55,7 @@
import software.amazon.smithy.utils.ListUtils;
final class JsonSchemaShapeVisitor extends ShapeVisitor.Default {
+ private static final Set NON_NUMERIC_FLOAT_VALUES = NonNumericFloat.stringRepresentations();
private final Model model;
private final JsonSchemaConverter converter;
@@ -150,12 +153,30 @@ public Schema longShape(LongShape shape) {
@Override
public Schema floatShape(FloatShape shape) {
- return buildSchema(shape, createBuilder(shape, "number"));
+ return buildFloatSchema(shape);
}
@Override
public Schema doubleShape(DoubleShape shape) {
- return buildSchema(shape, createBuilder(shape, "number"));
+ return buildFloatSchema(shape);
+ }
+
+ private Schema buildFloatSchema(Shape shape) {
+ Schema.Builder numberBuilder = createBuilder(shape, "number");
+ if (!converter.getConfig().getSupportNonNumericFloats()) {
+ return buildSchema(shape, numberBuilder);
+ }
+
+ Schema nonNumericValues = Schema.builder()
+ .type("string")
+ .enumValues(NON_NUMERIC_FLOAT_VALUES)
+ .build();
+
+ Schema.Builder nonNumericNumberBuilder = createBuilder(shape, "number")
+ .type(null)
+ .oneOf(ListUtils.of(numberBuilder.build(), nonNumericValues));
+
+ return buildSchema(shape, nonNumericNumberBuilder);
}
@Override
diff --git a/smithy-jsonschema/src/test/java/software/amazon/smithy/jsonschema/SupportNonNumericFloatsTest.java b/smithy-jsonschema/src/test/java/software/amazon/smithy/jsonschema/SupportNonNumericFloatsTest.java
new file mode 100644
index 00000000000..61b36a158e9
--- /dev/null
+++ b/smithy-jsonschema/src/test/java/software/amazon/smithy/jsonschema/SupportNonNumericFloatsTest.java
@@ -0,0 +1,34 @@
+package software.amazon.smithy.jsonschema;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.empty;
+import static org.hamcrest.Matchers.not;
+
+import org.junit.jupiter.api.Test;
+import software.amazon.smithy.model.Model;
+import software.amazon.smithy.model.node.Node;
+import software.amazon.smithy.utils.IoUtils;
+
+public class SupportNonNumericFloatsTest {
+ @Test
+ public void addsNonNumericFloatSupport() {
+ Model model = Model.assembler()
+ .discoverModels(getClass().getClassLoader())
+ .addImport(getClass().getResource("non-numeric-floats.json"))
+ .assemble()
+ .unwrap();
+
+ JsonSchemaConfig config = new JsonSchemaConfig();
+ config.setSupportNonNumericFloats(true);
+ SchemaDocument result = JsonSchemaConverter.builder()
+ .config(config)
+ .model(model)
+ .build()
+ .convert();
+ assertThat(result.getDefinitions().keySet(), not(empty()));
+
+ Node expectedNode = Node.parse(IoUtils.toUtf8String(
+ getClass().getResourceAsStream("non-numeric-floats.jsonschema.json")));
+ Node.assertEquals(result, expectedNode);
+ }
+}
diff --git a/smithy-jsonschema/src/test/resources/software/amazon/smithy/jsonschema/non-numeric-floats.json b/smithy-jsonschema/src/test/resources/software/amazon/smithy/jsonschema/non-numeric-floats.json
new file mode 100644
index 00000000000..e83756bcc07
--- /dev/null
+++ b/smithy-jsonschema/src/test/resources/software/amazon/smithy/jsonschema/non-numeric-floats.json
@@ -0,0 +1,37 @@
+{
+ "smithy": "1.0",
+ "shapes": {
+ "example.smithy#MyService": {
+ "type": "service",
+ "version": "2006-03-01",
+ "operations": [
+ {
+ "target": "example.smithy#MyOperation"
+ }
+ ]
+ },
+ "example.smithy#MyOperation": {
+ "type": "operation",
+ "input": {
+ "target": "example.smithy#MyOperationInput"
+ },
+ "traits": {
+ "smithy.api#http": {
+ "uri": "/foo",
+ "method": "POST"
+ }
+ }
+ },
+ "example.smithy#MyOperationInput": {
+ "type": "structure",
+ "members": {
+ "floatMember": {
+ "target": "smithy.api#Float"
+ },
+ "doubleMember": {
+ "target": "smithy.api#Double"
+ }
+ }
+ }
+ }
+}
diff --git a/smithy-jsonschema/src/test/resources/software/amazon/smithy/jsonschema/non-numeric-floats.jsonschema.json b/smithy-jsonschema/src/test/resources/software/amazon/smithy/jsonschema/non-numeric-floats.jsonschema.json
new file mode 100644
index 00000000000..9145a798812
--- /dev/null
+++ b/smithy-jsonschema/src/test/resources/software/amazon/smithy/jsonschema/non-numeric-floats.jsonschema.json
@@ -0,0 +1,39 @@
+{
+ "definitions": {
+ "MyOperationInput": {
+ "type": "object",
+ "properties": {
+ "doubleMember": {
+ "oneOf": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "string",
+ "enum": [
+ "NaN",
+ "Infinity",
+ "-Infinity"
+ ]
+ }
+ ]
+ },
+ "floatMember": {
+ "oneOf": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "string",
+ "enum": [
+ "NaN",
+ "Infinity",
+ "-Infinity"
+ ]
+ }
+ ]
+ }
+ }
+ }
+ }
+}
diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/node/Node.java b/smithy-model/src/main/java/software/amazon/smithy/model/node/Node.java
index b38f1f06351..92e2d7f43e5 100644
--- a/smithy-model/src/main/java/software/amazon/smithy/model/node/Node.java
+++ b/smithy-model/src/main/java/software/amazon/smithy/model/node/Node.java
@@ -21,10 +21,12 @@
import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
+import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
+import java.util.Set;
import java.util.TreeMap;
import java.util.function.Supplier;
import java.util.stream.Collectors;
@@ -722,4 +724,56 @@ public Node arrayNode(ArrayNode node) {
}
});
}
+
+ /**
+ * Non-numeric values for floats and doubles.
+ */
+ public enum NonNumericFloat {
+ NAN("NaN"),
+ POSITIVE_INFINITY("Infinity"),
+ NEGATIVE_INFINITY("-Infinity");
+
+ private final String stringRepresentation;
+
+ NonNumericFloat(String stringRepresentation) {
+ this.stringRepresentation = stringRepresentation;
+ }
+
+ /**
+ * @return The string representation of this non-numeric float.
+ */
+ public String getStringRepresentation() {
+ return stringRepresentation;
+ }
+
+ /**
+ * @return All the possible string representations of non-numeric floats.
+ */
+ public static Set stringRepresentations() {
+ Set values = new LinkedHashSet<>();
+ for (NonNumericFloat value : NonNumericFloat.values()) {
+ values.add(value.getStringRepresentation());
+ }
+ return values;
+ }
+
+ /**
+ * Convert a string value into a NonNumericFloat.
+ *
+ * @param value A string representation of a non-numeric float value.
+ * @return A NonNumericFloat that represents the given string value or empty if there is no associated value.
+ */
+ public static Optional fromStringRepresentation(String value) {
+ switch (value) {
+ case "NaN":
+ return Optional.of(NAN);
+ case "Infinity":
+ return Optional.of(POSITIVE_INFINITY);
+ case "-Infinity":
+ return Optional.of(NEGATIVE_INFINITY);
+ default:
+ return Optional.empty();
+ }
+ }
+ }
}
diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/validation/NodeValidationVisitor.java b/smithy-model/src/main/java/software/amazon/smithy/model/validation/NodeValidationVisitor.java
index 5883bda0b2b..c7d3dfa8f08 100644
--- a/smithy-model/src/main/java/software/amazon/smithy/model/validation/NodeValidationVisitor.java
+++ b/smithy-model/src/main/java/software/amazon/smithy/model/validation/NodeValidationVisitor.java
@@ -211,7 +211,7 @@ private List validateNaturalNumber(Shape shape, Long min, Long
@Override
public List floatShape(FloatShape shape) {
- return value.isNumberNode()
+ return value.isNumberNode() || value.isStringNode()
? applyPlugins(shape)
: invalidShape(shape, NodeType.NUMBER);
}
@@ -224,7 +224,7 @@ public List documentShape(DocumentShape shape) {
@Override
public List doubleShape(DoubleShape shape) {
- return value.isNumberNode()
+ return value.isNumberNode() || value.isStringNode()
? applyPlugins(shape)
: invalidShape(shape, NodeType.NUMBER);
}
diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/validation/node/NodeValidatorPlugin.java b/smithy-model/src/main/java/software/amazon/smithy/model/validation/node/NodeValidatorPlugin.java
index f59c487cfc3..ffb5b7caf30 100644
--- a/smithy-model/src/main/java/software/amazon/smithy/model/validation/node/NodeValidatorPlugin.java
+++ b/smithy-model/src/main/java/software/amazon/smithy/model/validation/node/NodeValidatorPlugin.java
@@ -51,6 +51,7 @@ public interface NodeValidatorPlugin {
*/
static List getBuiltins() {
return ListUtils.of(
+ new NonNumericFloatValuesPlugin(),
new BlobLengthPlugin(),
new CollectionLengthPlugin(),
new IdRefPlugin(),
diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/validation/node/NonNumericFloatValuesPlugin.java b/smithy-model/src/main/java/software/amazon/smithy/model/validation/node/NonNumericFloatValuesPlugin.java
new file mode 100644
index 00000000000..94c26f2a7ee
--- /dev/null
+++ b/smithy-model/src/main/java/software/amazon/smithy/model/validation/node/NonNumericFloatValuesPlugin.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package software.amazon.smithy.model.validation.node;
+
+import java.util.Set;
+import java.util.function.BiConsumer;
+import software.amazon.smithy.model.FromSourceLocation;
+import software.amazon.smithy.model.node.Node;
+import software.amazon.smithy.model.node.Node.NonNumericFloat;
+import software.amazon.smithy.model.shapes.Shape;
+
+/**
+ * Validates the specific set of non-numeric values allowed for floats and doubles.
+ */
+final class NonNumericFloatValuesPlugin implements NodeValidatorPlugin {
+ private static final Set NON_NUMERIC_FLOAT_VALUES = NonNumericFloat.stringRepresentations();
+
+ @Override
+ public void apply(Shape shape, Node value, Context context, BiConsumer emitter) {
+ if (!(shape.isFloatShape() || shape.isDoubleShape()) || !value.isStringNode()) {
+ return;
+ }
+ String nodeValue = value.expectStringNode().getValue();
+ if (!NON_NUMERIC_FLOAT_VALUES.contains(nodeValue)) {
+ emitter.accept(value, String.format(
+ "Value for `%s` must either be numeric or one of the following strings: [\"%s\"], but was \"%s\"",
+ shape.getId(), String.join("\", \"", NON_NUMERIC_FLOAT_VALUES), nodeValue
+ ));
+ }
+ }
+}
diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/validation/node/RangeTraitPlugin.java b/smithy-model/src/main/java/software/amazon/smithy/model/validation/node/RangeTraitPlugin.java
index dd3665938ed..a23796743cb 100644
--- a/smithy-model/src/main/java/software/amazon/smithy/model/validation/node/RangeTraitPlugin.java
+++ b/smithy-model/src/main/java/software/amazon/smithy/model/validation/node/RangeTraitPlugin.java
@@ -18,26 +18,61 @@
import java.math.BigDecimal;
import java.util.function.BiConsumer;
import software.amazon.smithy.model.FromSourceLocation;
+import software.amazon.smithy.model.node.Node;
+import software.amazon.smithy.model.node.Node.NonNumericFloat;
import software.amazon.smithy.model.node.NumberNode;
-import software.amazon.smithy.model.shapes.NumberShape;
+import software.amazon.smithy.model.node.StringNode;
import software.amazon.smithy.model.shapes.Shape;
import software.amazon.smithy.model.traits.RangeTrait;
/**
* Validates the range trait on number shapes or members that target them.
*/
-final class RangeTraitPlugin extends MemberAndShapeTraitPlugin {
+class RangeTraitPlugin implements NodeValidatorPlugin {
- RangeTraitPlugin() {
- super(NumberShape.class, NumberNode.class, RangeTrait.class);
+ @Override
+ public final void apply(Shape shape, Node value, Context context, BiConsumer emitter) {
+ if (shape.hasTrait(RangeTrait.class)) {
+ if (value.isNumberNode()) {
+ check(shape, shape.expectTrait(RangeTrait.class), value.expectNumberNode(), emitter);
+ } else if (value.isStringNode()) {
+ checkNonNumeric(shape, shape.expectTrait(RangeTrait.class), value.expectStringNode(), emitter);
+ }
+ }
+ }
+
+ private void checkNonNumeric(
+ Shape shape,
+ RangeTrait trait,
+ StringNode node,
+ BiConsumer emitter
+ ) {
+ NonNumericFloat.fromStringRepresentation(node.getValue()).ifPresent(value -> {
+ if (value.equals(NonNumericFloat.NAN)) {
+ emitter.accept(node, String.format(
+ "Value provided for `%s` must be a number because the `smithy.api#range` trait is applied, "
+ + "but found \"%s\"",
+ shape.getId(), node.getValue()));
+ }
+
+ if (trait.getMin().isPresent() && value.equals(NonNumericFloat.NEGATIVE_INFINITY)) {
+ emitter.accept(node, String.format(
+ "Value provided for `%s` must be greater than or equal to %s, but found \"%s\"",
+ shape.getId(), trait.getMin().get(), node.getValue()));
+ }
+
+ if (trait.getMax().isPresent() && value.equals(NonNumericFloat.POSITIVE_INFINITY)) {
+ emitter.accept(node, String.format(
+ "Value provided for `%s` must be less than or equal to %s, but found \"%s\"",
+ shape.getId(), trait.getMax().get(), node.getValue()));
+ }
+ });
}
- @Override
protected void check(
Shape shape,
RangeTrait trait,
NumberNode node,
- Context context,
BiConsumer emitter
) {
Number number = node.getValue();
diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/validation/NodeValidationVisitorTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/validation/NodeValidationVisitorTest.java
index f78e1527dc0..809250248b7 100644
--- a/smithy-model/src/test/java/software/amazon/smithy/model/validation/NodeValidationVisitorTest.java
+++ b/smithy-model/src/test/java/software/amazon/smithy/model/validation/NodeValidationVisitorTest.java
@@ -140,15 +140,29 @@ public static Collection