software.amazon.smithy.model.node.Node Maven / Gradle / Ivy
/*
* Copyright 2019 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.node;
import static java.lang.String.format;
import java.io.InputStream;
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;
import software.amazon.smithy.model.FromSourceLocation;
import software.amazon.smithy.model.SourceException;
import software.amazon.smithy.model.SourceLocation;
import software.amazon.smithy.model.loader.ModelSyntaxException;
import software.amazon.smithy.model.node.internal.NodeHandler;
import software.amazon.smithy.utils.IoUtils;
/**
* Base class of for all Smithy model nodes.
*
* When loading a Smithy model the data is loaded from the source model
* file into a tree of nodes. These nodes represent the unvalidated
* structure of the model document.
*/
public abstract class Node implements FromSourceLocation, ToNode {
private final SourceLocation sourceLocation;
/**
* This is intentionally package-private to make Node a closed set
* of concrete classes and prevent it from being extended outside
* of this package.
*
* @param sourceLocation Where the node was sourced from.
*/
Node(SourceLocation sourceLocation) {
this.sourceLocation = Objects.requireNonNull(sourceLocation);
}
/**
* Attempts to parse the given JSON string and return a Node.
*
* @param json JSON text to parse.
* @return Returns the parsed Node on success.
* @throws ModelSyntaxException if the JSON text is invalid.
*/
public static Node parse(String json) {
return NodeHandler.parse("", json, false);
}
/**
* Attempts to parse the given JSON string and File Name and return a Node.
*
* @param json JSON text to parse.
* @param file Filename corresponding to json text
* @return Returns the parsed Node on success.
* @throws ModelSyntaxException if the JSON text is invalid.
*/
public static Node parse(String json, String file) {
return NodeHandler.parse(file, json, false);
}
/**
* Attempts to parse the given JSON input stream and returns a Node.
*
* @param json JSON input stream to parse. The input stream is closed
* automatically when the content is fully parsed.
* @return Returns the parsed Node on success.
* @throws ModelSyntaxException if the JSON text is invalid.
*/
public static Node parse(InputStream json) {
return parse(json, "");
}
/**
* Attempts to parse the given JSON input stream and returns a Node.
*
* @param json JSON input stream to parse. The input stream is closed
* automatically when the content is fully parsed.
* @param file Filename corresponding to json text
* @return Returns the parsed Node on success.
* @throws ModelSyntaxException if the JSON text is invalid.
*/
public static Node parse(InputStream json, String file) {
return parse(IoUtils.toUtf8String(json), file);
}
/**
* Attempts to parse the given JSON string and File Name and return a Node.
*
*
This parser allows for comments in the JSON.
*
* @param json JSON text to parse.
* @param file Filename corresponding to json text
* @return Returns the parsed Node on success.
* @throws ModelSyntaxException if the JSON text is invalid.
*/
public static Node parseJsonWithComments(String json, String file) {
return NodeHandler.parse(file, json, true);
}
/**
* Attempts to parse the given JSON string and return a Node.
*
*
This parser allows for comments in the JSON.
*
* @param json JSON text to parse.
* @return Returns the parsed Node on success.
* @throws ModelSyntaxException if the JSON text is invalid.
*/
public static Node parseJsonWithComments(String json) {
return parseJsonWithComments(json, "");
}
/**
* Writes the contents of a Node to a pretty-printed JSON string.
*
* @param node Node to write.
* @return Returns the serialized Node.
*/
public static String prettyPrintJson(Node node) {
return prettyPrintJson(node, " ");
}
/**
* Writes the contents of a Node to a pretty-printed JSON string.
*
* @param node Node to write.
* @param indentString String to use for indention.
* @return Returns the serialized Node.
*/
public static String prettyPrintJson(Node node, String indentString) {
return NodeHandler.prettyPrint(node, indentString);
}
/**
* Writes the contents of a Node to a non-pretty-printed JSON string.
*
* @param node Node to write.
* @return Returns the serialized Node.
*/
public static String printJson(Node node) {
return NodeHandler.print(node);
}
/**
* Create a {@link StringNode} from a String value.
*
* @param value Value to create a node from.
* @return Returns the created StringNode.
*/
public static StringNode from(String value) {
return new StringNode(value, SourceLocation.none());
}
/**
* Create a {@link NumberNode} from a Number value.
*
* @param number Value to create a node from.
* @return Returns the created NumberNode.
*/
public static NumberNode from(Number number) {
return new NumberNode(number, SourceLocation.none());
}
/**
* Create a {@link BooleanNode} from a boolean value.
*
* @param value Value to create a node from.
* @return Returns the created BooleanNode.
*/
public static BooleanNode from(boolean value) {
return new BooleanNode(value, SourceLocation.none());
}
/**
* Create a Node from a potentially null {@link ToNode} value.
*
* @param value Value to create a node from.
* @return Returns the created Node.
*/
public static Node from(ToNode value) {
return value == null ? Node.nullNode() : value.toNode();
}
/**
* Creates an {@link ArrayNode} from a Collection of Node values.
*
* @param values String values to add to the ArrayNode.
* @return Returns the created ArrayNode.
*/
@SuppressWarnings("unchecked")
public static ArrayNode fromNodes(List extends Node> values) {
return new ArrayNode((List) values, SourceLocation.none());
}
/**
* Creates an {@link ArrayNode} from a variadic list of Node values.
*
* @param values String values to add to the ArrayNode.
* @return Returns the created ArrayNode.
*/
public static ArrayNode fromNodes(Node... values) {
return fromNodes(Arrays.asList(values));
}
/**
* Creates an {@link ArrayNode} from a Collection of String values.
*
* @param values String values to add to the ArrayNode.
* @return Returns the created ArrayNode.
*/
public static ArrayNode fromStrings(Collection values) {
return fromNodes(values.stream().map(Node::from).collect(Collectors.toList()));
}
/**
* Creates an {@link ArrayNode} from a variadic list of String values.
*
* @param values String values to add to the ArrayNode.
* @return Returns the created ArrayNode.
*/
public static ArrayNode fromStrings(String... values) {
return fromStrings(Arrays.asList(values));
}
/**
* Creates an {@link ObjectNode.Builder}.
*
* @return Returns the ObjectNode builder.
*/
public static ObjectNode.Builder objectNodeBuilder() {
return ObjectNode.builder();
}
/**
* Creates an empty {@link ObjectNode}.
* @return Returns the ObjectNode.
*/
public static ObjectNode objectNode() {
return ObjectNode.EMPTY;
}
/**
* Creates an {@link ObjectNode} from the given map of Nodes.
* @param values Values to add to the object node.
* @return Returns the created ObjectNode.
*/
public static ObjectNode objectNode(Map values) {
return new ObjectNode(values, SourceLocation.none());
}
/**
* Creates an empty {@link ArrayNode}.
* @return Returns the ArrayNode.
*/
public static ArrayNode arrayNode() {
return ArrayNode.EMPTY;
}
/**
* Creates an {@link ArrayNode} from a variadic list of Nodes.
* @param nodes Nodes to add to the array.
* @return Returns the created ArrayNode.
*/
public static ArrayNode arrayNode(Node... nodes) {
return new ArrayNode(Arrays.asList(nodes), SourceLocation.none());
}
/**
* Creates a {@link NullNode}.
* @return Returns the NullNode.
*/
public static NullNode nullNode() {
return new NullNode(SourceLocation.none());
}
/**
* Expects an array of strings and returns the loaded strings.
*
* @param descriptor Name of the property being loaded.
* @param node Node to load.
* @return Returns the loaded strings.
* @throws SourceException on error.
*/
public static List loadArrayOfString(String descriptor, Node node) {
return node.expectArrayNode(() -> "Expected `" + descriptor + "` to be an array of strings. Found {type}.")
.getElementsAs(StringNode::getValue);
}
/**
* Testing helper used to compare two Nodes for equivalence.
*
* Compares two Node values and throws if they aren't equal. The
* thrown exception contains a message that shows the differences
* between the two Nodes as returned by {@link #diff(ToNode, ToNode)}.
*
* @param actual Node to use as the starting node.
* @param expected Node to compare against.
* @throws ExpectationNotMetException if the nodes are not equivalent.
*/
public static void assertEquals(ToNode actual, ToNode expected) {
Node actualNode = actual.toNode();
Node expectedNode = expected.toNode();
if (!actualNode.equals(expectedNode)) {
throw new ExpectationNotMetException(String.format(
"Actual node did not match expected Node.%nActual:%n%s%nExpected:%n%s%nDiff: %s",
Node.prettyPrintJson(actualNode),
Node.prettyPrintJson(expectedNode),
String.join(System.lineSeparator(), diff(actualNode, expectedNode))), actualNode);
}
}
/**
* Computes the differences between two Nodes as a String.
*
* @param actual Node to use as the starting node.
* @param expected Node to compare against.
* @return Returns the differences as a String.
*/
public static List diff(ToNode actual, ToNode expected) {
return NodeDiff.diff(actual, expected);
}
/**
* Gets the type of the node.
*
* @return Returns the node type.
*/
public abstract NodeType getType();
/**
* Accepts a visitor with the node.
*
* @param visitor Visitor to dispatch to.
* @param visitor return type.
* @return Returns the accepted result.
*/
public abstract R accept(NodeVisitor visitor);
/**
* Checks if this node is an object type.
*
* @return Returns true if this node is an object type.
*/
public final boolean isObjectNode() {
return getType() == NodeType.OBJECT;
}
/**
* Checks if this node is an array type.
*
* @return Returns true if this node is an array type.
*/
public final boolean isArrayNode() {
return getType() == NodeType.ARRAY;
}
/**
* Checks if this node is a string type.
*
* @return Returns true if this node is a string type.
*/
public final boolean isStringNode() {
return getType() == NodeType.STRING;
}
/**
* Checks if this node is a number type.
*
* @return Returns true if this node is a number type.
*/
public final boolean isNumberNode() {
return getType() == NodeType.NUMBER;
}
/**
* Checks if this node is a boolean type.
*
* @return Returns true if this node is a boolean type.
*/
public final boolean isBooleanNode() {
return getType() == NodeType.BOOLEAN;
}
/**
* Checks if this node is a null type.
*
* @return Returns true if this node is a null type.
*/
public final boolean isNullNode() {
return getType() == NodeType.NULL;
}
/**
* Gets the node as an ObjectNode if it is an object.
*
* @return Returns the optional object node.
*/
public Optional asObjectNode() {
return Optional.empty();
}
/**
* Gets the node as an ArrayNode if it is an array.
*
* @return Returns the optional array node.
*/
public Optional asArrayNode() {
return Optional.empty();
}
/**
* Gets the node as an StringNode if it is an string.
*
* @return Returns the optional StringNode.
*/
public Optional asStringNode() {
return Optional.empty();
}
/**
* Gets the node as an BooleanNode if it is an boolean.
*
* @return Returns the optional BooleanNode.
*/
public Optional asBooleanNode() {
return Optional.empty();
}
/**
* Gets the node as an NumberNode if it is an number.
*
* @return Returns the optional NumberNode.
*/
public Optional asNumberNode() {
return Optional.empty();
}
/**
* Gets the node as an NullNode if it is a null.
*
* @return Returns the optional NullNode.
*/
public Optional asNullNode() {
return Optional.empty();
}
/**
* Casts the current node to an {@code ObjectNode}.
*
* @return Returns an object node.
* @throws ExpectationNotMetException when the node is not an {@code ObjectNode}.
*/
public final ObjectNode expectObjectNode() {
return expectObjectNode((String) null);
}
/**
* Casts the current node to an {@code ObjectNode}, throwing
* {@link ExpectationNotMetException} when the node is the wrong type.
*
* @param message Error message to use if the node is of the wrong type.
* @return Returns an object node.
* @throws ExpectationNotMetException when the node is not an {@code ObjectNode}.
*/
public ObjectNode expectObjectNode(String message) {
throw new ExpectationNotMetException(expandMessage(message, NodeType.OBJECT.toString()), this);
}
/**
* Casts the current node to an {@code ObjectNode}, throwing
* {@link ExpectationNotMetException} when the node is the wrong type.
*
* @param message Error message supplier.
* @return Returns an object node.
* @throws ExpectationNotMetException when the node is not an {@code ObjectNode}.
*/
public ObjectNode expectObjectNode(Supplier message) {
return expectObjectNode(message.get());
}
/**
* Casts the current node to an {@code ArrayNode}.
*
* @return Returns an array node.
* @throws ExpectationNotMetException when the node is not an {@code ArrayNode}.
*/
public final ArrayNode expectArrayNode() {
return expectArrayNode((String) null);
}
/**
* Casts the current node to an {@code ArrayNode}, throwing
* {@link ExpectationNotMetException} when the node is the wrong type.
*
* @param message Error message to use if the node is of the wrong type.
* @return Returns an array node.
* @throws ExpectationNotMetException when the node is the wrong type.
*/
public ArrayNode expectArrayNode(String message) {
throw new ExpectationNotMetException(expandMessage(message, NodeType.ARRAY.toString()), this);
}
/**
* Casts the current node to an {@code ArrayNode}, throwing
* {@link ExpectationNotMetException} when the node is the wrong type.
*
* @param message Error message supplier.
* @return Returns an array node.
* @throws ExpectationNotMetException when the node is the wrong type.
*/
public ArrayNode expectArrayNode(Supplier message) {
return expectArrayNode(message.get());
}
/**
* Casts the current node to a {@code StringNode}.
*
* @return Returns a string node.
* @throws ExpectationNotMetException when the node is the wrong type.
*/
public final StringNode expectStringNode() {
return expectStringNode((String) null);
}
/**
* Casts the current node to a {@code StringNode}, throwing
* {@link ExpectationNotMetException} when the node is the wrong type.
*
* @param message Error message to use if the node is of the wrong type.
* @return Returns a string node.
* @throws ExpectationNotMetException when the node is the wrong type.
*/
public StringNode expectStringNode(String message) {
throw new ExpectationNotMetException(expandMessage(message, NodeType.STRING.toString()), this);
}
/**
* Casts the current node to a {@code StringNode}, throwing
* {@link ExpectationNotMetException} when the node is the wrong type.
*
* @param message Error message supplier.
* @return Returns a string node.
* @throws ExpectationNotMetException when the node is the wrong type.
*/
public StringNode expectStringNode(Supplier message) {
return expectStringNode(message.get());
}
/**
* Casts the current node to a {@code NumberNode}.
*
* @return Returns a number node.
* @throws ExpectationNotMetException when the node is the wrong type.
*/
public final NumberNode expectNumberNode() {
return expectNumberNode((String) null);
}
/**
* Casts the current node to a {@code NumberNode}, throwing
* {@link ExpectationNotMetException} when the node is the wrong type.
*
* @param message Error message to use if the node is of the wrong type.
* @return Returns a number node.
* @throws ExpectationNotMetException when the node is the wrong type.
*/
public NumberNode expectNumberNode(String message) {
throw new ExpectationNotMetException(expandMessage(message, NodeType.NUMBER.toString()), this);
}
/**
* Casts the current node to a {@code NumberNode}, throwing
* {@link ExpectationNotMetException} when the node is the wrong type.
*
* @param message Error message supplier.
* @return Returns a number node.
* @throws ExpectationNotMetException when the node is the wrong type.
*/
public NumberNode expectNumberNode(Supplier message) {
return expectNumberNode(message.get());
}
/**
* Casts the current node to a {@code BooleanNode}.
*
* @return Returns a boolean node.
* @throws ExpectationNotMetException when the node is the wrong type.
*/
public final BooleanNode expectBooleanNode() {
return expectBooleanNode((String) null);
}
/**
* Casts the current node to a {@code BooleanNode}, throwing
* {@link ExpectationNotMetException} when the node is the wrong type.
*
* @param message Error message to use if the node is of the wrong type.
* @return Returns a boolean node.
* @throws ExpectationNotMetException when the node is the wrong type.
*/
public BooleanNode expectBooleanNode(String message) {
throw new ExpectationNotMetException(expandMessage(message, NodeType.BOOLEAN.toString()), this);
}
/**
* Casts the current node to a {@code BooleanNode}, throwing
* {@link ExpectationNotMetException} when the node is the wrong type.
*
* @param message Error message supplier.
* @return Returns a boolean node.
* @throws ExpectationNotMetException when the node is the wrong type.
*/
public BooleanNode expectBooleanNode(Supplier message) {
return expectBooleanNode(message.get());
}
/**
* Casts the current node to a {@code NullNode}.
*
* @return Returns a null node.
* @throws ExpectationNotMetException when the node is the wrong type.
*/
public final NullNode expectNullNode() {
return expectNullNode((String) null);
}
/**
* Casts the current node to a {@code NullNode}, throwing
* {@link ExpectationNotMetException} when the node is the wrong type.
*
* @param message Error message to use if the node is of the wrong type.
* @return Returns a null node.
* @throws ExpectationNotMetException when the node is the wrong type.
*/
public NullNode expectNullNode(String message) {
throw new ExpectationNotMetException(expandMessage(message, NodeType.NULL.toString()), this);
}
/**
* Casts the current node to a {@code NullNode}, throwing
* {@link ExpectationNotMetException} when the node is the wrong type.
*
* @param message Error message supplier.
* @return Returns a null node.
* @throws ExpectationNotMetException when the node is the wrong type.
*/
public NullNode expectNullNode(Supplier message) {
return expectNullNode(message.get());
}
@Override
public final SourceLocation getSourceLocation() {
return sourceLocation;
}
@Override
public final Node toNode() {
return this;
}
private String expandMessage(String message, String expectedType) {
return (message == null
? format("Expected %s, but found {type}.", expectedType)
: message).replace("{type}", getType().toString());
}
/**
* Returns a node with sorted keys and sorted keys of all nested object
* nodes.
*
* @return Returns the node in which all object nodes have sorted keys.
*/
public final Node withDeepSortedKeys() {
return withDeepSortedKeys(Comparator.comparing(StringNode::getValue));
}
/**
* Returns a node with sorted keys and sorted keys of all nested object
* nodes using a custom comparator.
*
* @param keyComparator Compares keys.
* @return Returns the node in which all object nodes have sorted keys.
*/
public final Node withDeepSortedKeys(Comparator keyComparator) {
return sortNode(this, keyComparator);
}
private static Node sortNode(Node node, Comparator keyComparator) {
return node.accept(new NodeVisitor.Default() {
@Override
protected Node getDefault(Node node) {
return node;
}
@Override
public Node objectNode(ObjectNode node) {
Map members = new TreeMap<>(keyComparator);
node.getMembers().forEach((k, v) -> members.put(k, sortNode(v, keyComparator)));
return new ObjectNode(members, node.getSourceLocation());
}
@Override
public Node arrayNode(ArrayNode node) {
return node.getElements().stream()
.map(element -> sortNode(element, keyComparator))
.collect(ArrayNode.collect(node.getSourceLocation()));
}
});
}
/**
* 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();
}
}
}
}