Skip to content

Instantly share code, notes, and snippets.

@SpenceDiNicolantonio
Created July 5, 2019 18:41
Show Gist options
  • Save SpenceDiNicolantonio/072ee4471d4143b0f07566bafdf9bdb1 to your computer and use it in GitHub Desktop.
Save SpenceDiNicolantonio/072ee4471d4143b0f07566bafdf9bdb1 to your computer and use it in GitHub Desktop.
[Traversable JSON (and XML) in Apex] Simulated tree structure to represent JSON or XML in a traversable and queriable way #salesforce #apex
/*
* A traversable JSON data structure.
*
* Conceptually, JSON is a general tree, or array of general trees. It is represented in Apex using Map<String, Object>
* for objects, List<Object> for arrays, String for strings, and either Integer or Decimal for numbers. This class wraps
* these data types in order to provide an interface for traversiing and querying the entire JSON structure.
*
* Each node contains a single value that must be a valid JSON value. That is, Map<String, Object>, List<Object>,
* String, Decimal, Integer, or null. When traversing or querying a JSON structure, any value returned is always wrapped
* in a JsonData instance to allow subsequent traversal or query.
*/
public class JsonNode {
private static final Pattern ARRAY_PATTERN = Pattern.compile('^\\[(.*)\\]$');
private static final Pattern TAIL_ARRAY_PATTERN = Pattern.compile('(.*)\\[(.*)\\]$');
private static final Pattern BOOLEAN_PATTERN = Pattern.compile('^(true|false)$');
private static final Pattern DECIMAL_PATTERN = Pattern.compile('^[-+]?\\d+(\\.\\d+)?$');
private static final Pattern DATE_PATTERN = Pattern.compile('^\\d{4}.\\d{2}.\\d{2}$');
private static final Pattern TIME_PATTERN = Pattern.compile('^\\d{4}.\\d{2}.\\d{2} (\\d{2}:\\d{2}:\\d{2} ([-+]\\d{2}:\\d{2})?)?$');
// Value of JSON node
public Object value { get; private set; }
//==================================================================================================================
// Constructors
//==================================================================================================================
/*
* Constructor.
* @param value A Map, List, String, Integer, or decimal value to be held in the node
*/
public JsonNode(Object value) {
if (!isValidValue(value)) {
throw new InvalidJsonDataException('JSON nodes can only contain a map, list, string, integer, or decimal');
}
this.value = value;
}
/*
* Determines whether a given value represents valid JSON data. In the case of a List or Map, its contents will be
* validated recursively. Valid values include Strings, Integers, Decimals, and Maps or Lists containing only valid
* values.
* @param value A value to be validated
* @return True if the given value is a valid JSON value; false otherwise
*/
private Boolean isValidValue(Object value) {
// Nulls, Strings, Integers, and Decimals are valid
if (value == null
|| value instanceof String
|| value instanceof Integer
|| value instanceof Decimal) {
return true;
}
// Lists must be validated recursively
if (value instanceof List<Object>) {
for (Object listValue : (List<Object>) value) {
if (!isValidValue(listValue)) {
return false;
}
}
return true;
}
// Maps must be validated recursively
if (value instanceof Map<String, Object>) {
for (Object mapValue : ((Map<String, Object>) value).values()) {
if (!isValidValue(mapValue)) {
return false;
}
}
return true;
}
// No other types are valid
return false;
}
/*
* Parses a serialized JSON string, converting it to primitive Apex values.
* @param jsonString A string containing valid serialized JSON
* @return The root node of converted JSON data
*/
public static JsonNode parse(String jsonString) {
return new JsonNode(JSON.deserializeUntyped(jsonString));
}
/*
* Parses an XML document, converts it to JSON, and then to primitive Apex values.
* @param document An XML document
* @return The root node of converted JSON data
*/
public static JsonNode parse(DOM.Document document) {
return new JsonNode(parseXmlNode(document.getRootElement(), null));
}
/*
* A recursive method that parses an XML node into a simple apex structure analogous to a JSON object.
* @param node An XML node to process
* @param parent The node's parent node; null if processing the root node
* @return The analogous JSON object structure matching the provided XML node, as represented in Apex
*/
private static Map<String, Object> parseXmlNode(Dom.XmlNode node, Map<String, Object> parent) {
// The root of every XML -> JSON is always a map (since XML tags are named)
if (parent == null) {
parent = new Map<String, Object>();
}
// Iterate over all child elements for a given node
for (Dom.XmlNode child : node.getChildElements()) {
// Pull out some information
String nodeText = child.getText().trim();
String name = child.getName();
// Determine data type
Object value =
String.isBlank(nodeText) ? null : // Nothing
BOOLEAN_PATTERN.matcher(nodeText).find() ? (Object) Boolean.valueOf(nodeText) : // Boolean
DECIMAL_PATTERN.matcher(nodeText).find() ? (Object) Decimal.valueOf(nodeText) : // Decimal
DATE_PATTERN.matcher(nodeText).find() ? (Object) Date.valueOf(nodeText) : // Date
TIME_PATTERN.matcher(nodeText).find() ? (Object) DateTime.valueOf(nodeText) : // Time
(Object) nodeText; // Base case: use plain text
// We have some text to process
if (value != null) {
putJsonValue(parent, name, value);
} else if (child.getNodeType() == Dom.XmlNodeType.ELEMENT) {
// If it's not a comment or text, we will recursively process the data
Map<String, Object> temp = parseXmlNode(child, null);
// If at least one node was processed, add a new element into the array
if (!temp.isEmpty()) {
putJsonValue(parent, name, temp);
}
}
}
return parent;
}
/*
* Adds a JSON value to a map representing a JSON object. If a value already exists for the given key, and the value
* is not a list, the value is converted to a list containing both the original value and the provided value.
* @param parent A map representing a JSON object
* @param key The key for which the value should be associated in the given map
* @param value A value to put in the map
*/
private static void putJsonValue(Map<String, Object> parent, String key, Object value) {
// If the parent doesn't contain the key, put the value and we're done
if (!parent.containsKey(key)) {
parent.put(key, value);
return;
}
// At this point we are dealing with a list
// If existing value is a list, add the new value
// Otherwise, replace the existing value with a list containing both values
Object existingValue = parent.get(key);
if (existingValue instanceof List<Object>) {
List<Object> values = (List<Object>) existingValue;
values.add(value);
} else {
List<Object> values = new list<Object>();
values.add(existingValue);
values.add(value);
parent.put(key, values);
}
}
//==================================================================================================================
// Type checks
//==================================================================================================================
/*
* Determines whether the value of the JSON node is a map.
* @return True if the value is a map; false otherwise
*/
public Boolean isMap() {
return this.value instanceof Map<String, Object>;
}
/*
* Determines whether the value of the JSON node is a list.
* @return True if the value is a list; false otherwise
*/
public Boolean isList() {
return this.value instanceof List<Object>;
}
/*
* Determines whether the value of the JSON node is a string.
* @return True if the value is a string; false otherwise
*/
public Boolean isString() {
return this.value instanceof String;
}
/*
* Determines whether the value of the JSON node is a decimal (or integer).
* @return True if the value is a decimal; false otherwise
*/
public Boolean isDecimal() {
return this.value instanceof Decimal;
}
/*
* Determines whether the value of the JSON node is null.
* @return True if the value is null; false otherwise
*/
public Boolean isNull() {
return this.value == null;
}
//==================================================================================================================
// Convenience accessors
//==================================================================================================================
/*
* Returns the value of the JSON node as a map.
* @return The value of the node, casted to a map
*/
public Map<String, Object> asMap() {
return isNull() ? null : (Map<String, Object>) value;
}
/*
* Returns the value of the JSON node as a list.
* @return The value of the node, casted to a list
*/
public List<Object> asList() {
return isNull() ? null : (List<Object>) value;
}
/*
* Returns the value of the JSON node as a string.
* @return The value of the node, casted to a string
*/
public String asString() {
return isNull() ? null : String.valueOf(value);
}
/*
* Returns the value of the JSON node as a decimal.
* @return The value of the node, casted to a decimal
*/
public Decimal asDecimal() {
return isNull() ? null : (Decimal) Decimal.valueOf(asString());
}
/*
* Returns the value of the JSON node as a integer.
* @return The value of the node, casted to a integer
*/
public Integer asInteger() {
return isNull() ? null : (Integer) Integer.valueOf(asString());
}
/*
* Returns the value of the JSON node as a date/time.
* @return The value of the node, casted to a date/time
*/
public Datetime asDatetime() {
return isNull() ? null : (DateTime) json.deserialize('"' + asString() +'"', Datetime.class);
}
/*
* Serializes the JSON data and then deserializes it as an instance of a given type.
* @return The value of the node, converted to the provided type
*/
public Object asInstanceOf(Type klass) {
return isNull() ? null : JSON.deserialize(JSON.serialize(value), klass);
}
//==================================================================================================================
// Query
//==================================================================================================================
/*
* Traverses the JSON structure to find the value a give path.
* @param path The path to traverse, in dot-notation (e.g. child.grandchild, childArray[2].grandchild)
* @return A JSON node representing the value and the provided path
*/
public JsonNode get(String path) {
// Check for array index notation (e.g. [some_value])
Matcher arrayMatcher = ARRAY_PATTERN.matcher(path);
if (arrayMatcher.matches()) {
String key = arrayMatcher.group(1);
return getValueForKey(key);
}
// split path into components
List<String> pathComponents = path.split('\\.');
// Traverse each component
JsonNode value = this;
for (String pathComponent : pathComponents) {
// Check for array notation on tail
List<String> tail = new String[1];
Matcher matcher = TAIL_ARRAY_PATTERN.matcher(pathComponent);
while (matcher.matches()) {
pathComponent = matcher.group(1);
tail.add(0, matcher.group(2));
matcher = TAIL_ARRAY_PATTERN.matcher(pathComponent);
}
// Get value at component
value = value.getValueForKey(pathComponent);
for (String arrayComponent : tail) {
value = value.getValueForKey(arrayComponent);
}
}
return value;
}
/*
* Traverses a single level in the JSON structure, finding a child node for a given key.
* @param key A textual or integer key, identifying a target child node
* @return The child node identified by the provided key
*/
private JsonNode getValueForKey(String key) {
// Null or empty string for key
if (key == null || key.length() < 1) {
return this;
}
// Map
if (isMap()) {
Map<String, Object> valueMap = (Map<String, Object>) this.value;
return new JsonNode(valueMap.get(key));
}
// List
if (isList()) {
List<Object> valueList = (List<Object>) this.value;
Integer index;
try {
index = Integer.valueOf(key);
} catch (Exception e) {
throw new NonIntegerKeyException('Non-integer key provided for list value');
}
return new JsonNode(valueList.get(index));
}
return new JsonNode(null);
}
//==================================================================================================================
// Other utility
//==================================================================================================================
/*
* Generates an iterable list of child JSON nodes. If the node represents a single value, a list of size one is
* generated; if the node represents a list or map, a list is generated containing all nodes in the respective list
* or map.
* @return An iterable list of child nodes
*/
public List<JsonNode> iterator() {
// Get list of values
List<Object> values =
isList() ? asList() :
isNull() ? new List<Object>() :
new List<Object> { this.value };
// Construct a node for each value and return list
List<JsonNode> nodes = new List<JsonNode>();
for (Object value : values) {
nodes.add(new JsonNode(value));
}
return nodes;
}
/**
* Returns the string representation of the JSON node - this is always the same as invoking toString() on the
* wrapped value.
* @return The string representation of the JSON node
*/
public String toString() {
return value.toString();
}
//==================================================================================================================
// Exceptions
//==================================================================================================================
public class InvalidJsonDataException extends Exception {}
public class NonIntegerKeyException extends Exception {}
}
@IsTest
public class JsonNodeTest {
/**
* Generates a functional JsonNode instance for testing various use cases.
* @return A sample JSON node
*/
private static Map<String, JsonNode> getTestJson() {
Map<String, JsonNode> testNodes = new Map<String, JsonNode>();
testNodes.put('map', new JsonNode(new Map<String, Object>()));
testNodes.put('list', new JsonNode(new List<Object>()));
testNodes.put('string', new JsonNode('test'));
testNodes.put('decimal', new JsonNode(1.32));
testNodes.put('integer', new JsonNode(45));
testNodes.put('date', new JsonNode('2012-09-15'));
testNodes.put('time', new JsonNode('2012-09-15T15:53:00+00:00'));
testNodes.put('null', new JsonNode(null));
testNodes.put('complex', new JsonNode(
new Map<String, Object> {
'string' => 'I am a string',
'map' => new Map<String, Object> {
'nestedString' => 'I am a string nested in a map',
'nestedArray' => new List<Object> {
'nested array value 1',
'nested array value 2'
}
},
'basicArray' => new List<Object> {
'array value 1',
'array value 2',
'array value 3'
},
'arrayOfMaps' => new List<Object> {
new Map<String, Object> {
'nestedMapValue' => 'I am a string in a map in an array'
},
new Map<String, Object> {
'nestedMapValue' => 'I am a string in another map in an array'
}
}
}
));
return testNodes;
}
/**
* Returns a XML document for testing XML-to-JSON conversion. The generated document matches the structure of the
* test JSON node returned from getTestJson().
* @return A sample XML document that can be parsed into a JSON node
*/
private static DOM.Document getTestXml() {
DOM.Document doc = new DOM.Document();
doc.load('<root>\n' +
' <string>I am a string</string>>\n' +
' <map>\n' +
' <nestedString>I am a string nested in a map</nestedString>\n' +
' <nestedArray>nested array value 1</nestedArray>\n' +
' <nestedArray>nested array value 2</nestedArray>\n' +
' </map>\n' +
' <basicArray>array value 1</basicArray>\n' +
' <basicArray>array value 2</basicArray>\n' +
' <basicArray>array value 3</basicArray>\n' +
' <arrayOfMaps>\n' +
' <nestedMapValue>I am a string in a map in an array</nestedMapValue>\n' +
' </arrayOfMaps>\n' +
' <arrayOfMaps>\n' +
' <nestedMapValue>I am a string in another map in an array</nestedMapValue>\n' +
' </arrayOfMaps>\n' +
'</root>');
return doc;
}
//==================================================================================================================
// Constructor
//==================================================================================================================
@IsTest
private static void shouldBeConstructable_withJsonValue() {
Map<String, Object> mapValue = new Map<String, Object>();
List<Object> listValue = new List<Object>();
Test.startTest();
JsonNode mapNode = new JsonNode(mapValue);
JsonNode listNode = new JsonNode(listValue);
JsonNode stringNode = new JsonNode('test');
JsonNode decimalNode = new JsonNode(1.32);
JsonNode integerNode = new JsonNode(45);
JsonNode dateNode = new JsonNode('2012-09-15');
JsonNode timeNode = new JsonNode('2012-09-15T15:53:00+00:00');
JsonNode nullNode = new JsonNode(null);
Test.stopTest();
System.assertEquals(mapValue, mapNode.value);
System.assertEquals(listValue, listNode.value);
System.assertEquals('test', stringNode.value);
System.assertEquals(1.32, decimalNode.value);
System.assertEquals(45, integerNode.value);
System.assertEquals('2012-09-15', dateNode.value);
System.assertEquals('2012-09-15T15:53:00+00:00', timeNode.value);
System.assertEquals(null, nullNode.value);
}
@IsTest
private static void shouldBeConstructable_withJsonValue_exceptionWhenObject() {
Test.startTest();
JsonNode.InvalidJsonDataException caughtException;
try {
new JsonNode(Datetime.now());
} catch(JsonNode.InvalidJsonDataException e) {
caughtException = e;
}
Test.stopTest();
System.assertNotEquals(null, caughtException);
System.assertEquals('JSON nodes can only contain a map, list, string, integer, or decimal', caughtException.getMessage());
}
@IsTest
private static void shouldBeConstructable_withJsonValue_exceptionWhenObjectInMap() {
Test.startTest();
JsonNode.InvalidJsonDataException caughtException;
try {
new JsonNode(new Map<String, Object> {
'Invalid Object' => Datetime.now()
});
} catch(JsonNode.InvalidJsonDataException e) {
caughtException = e;
}
Test.stopTest();
System.assertNotEquals(null, caughtException);
System.assertEquals('JSON nodes can only contain a map, list, string, integer, or decimal', caughtException.getMessage());
}
@IsTest
private static void shouldBeConstructable_withJsonValue_exceptionWhenObjectInList() {
Test.startTest();
JsonNode.InvalidJsonDataException caughtException;
try {
new JsonNode(new List<Object> {
Datetime.now()
});
} catch(JsonNode.InvalidJsonDataException e) {
caughtException = e;
}
Test.stopTest();
System.assertNotEquals(null, caughtException);
System.assertEquals('JSON nodes can only contain a map, list, string, integer, or decimal', caughtException.getMessage());
}
@IsTest
private static void shouldBeConstructable_withJson() {
Map<String, JsonNode> testJson = getTestJson();
String jsonString = JSON.serializePretty(testJson.get('complex').value);
Test.startTest();
JsonNode node = JsonNode.parse(jsonString);
Test.stopTest();
System.assertNotEquals(null, node);
System.assertEquals(testJson.get('complex').value, node.value);
}
@IsTest
private static void shouldBeConstructable_withJson_exceptionWhenInvalid() {
Map<String, JsonNode> testJson = getTestJson();
String jsonString = JSON.serializePretty(testJson.get('complex').value);
// Make JSON invalid
jsonString = '{' + jsonString;
Test.startTest();
JsonNode node;
JSONException caughtException;
try {
node = JsonNode.parse(jsonString);
} catch (JSONException e) {
caughtException = e;
}
Test.stopTest();
System.assertEquals(null, node);
System.assertNotEquals(null, caughtException);
System.assert(caughtException.getMessage().startsWith('Unexpected character'), 'Expected exception to start with \'Unexpected character\': ' + caughtException.getMessage());
}
@IsTest
private static void shouldBeConstructable_withXml() {
Map<String, JsonNode> testJson = getTestJson();
String xmlString = getTestXml();
Test.startTest();
DOM.Document doc = new DOM.Document();
doc.load(xmlString);
JsonNode node = JsonNode.parse(doc);
Test.stopTest();
System.debug(node);
System.assertNotEquals(null, node);
System.assertEquals(testJson.get('complex').value, node.value);
}
@IsTest
private static void shouldBeConstructable_withXml_exceptionWhenInvalid() {
String xmlString = getTestXml();
// Make XML invalid
xmlString = '<unclosed>' + xmlString;
Test.startTest();
JsonNode node;
XmlException caughtException;
try {
DOM.Document doc = new DOM.Document();
doc.load(xmlString);
node = JsonNode.parse(doc);
} catch (XmlException e) {
caughtException = e;
}
Test.stopTest();
System.assertEquals(null, node);
System.assertNotEquals(null, caughtException);
}
//==================================================================================================================
// Type checks
//==================================================================================================================
@IsTest
private static void shouldIndicateWhetherTypeIsMap() {
Map<String, JsonNode> testJson = getTestJson();
Test.startTest();
Boolean isMap = testJson.get('map').isMap();
Boolean isNotMap =
testJson.get('list').isMap() ||
testJson.get('string').isMap() ||
testJson.get('decimal').isMap() ||
testJson.get('integer').isMap() ||
testJson.get('null').isMap();
Test.stopTest();
System.assertEquals(true, isMap);
System.assertEquals(false, isNotMap);
}
@IsTest
private static void shouldIndicateWhetherTypeIsList() {
Map<String, JsonNode> testJson = getTestJson();
Test.startTest();
Boolean isList = testJson.get('list').isList();
Boolean isNotList =
testJson.get('map').isList() ||
testJson.get('string').isList() ||
testJson.get('decimal').isList() ||
testJson.get('integer').isList() ||
testJson.get('null').isList();
Test.stopTest();
System.assertEquals(true, isList);
System.assertEquals(false, isNotList);
}
@IsTest
private static void shouldIndicateWhetherTypeIsString() {
Map<String, JsonNode> testJson = getTestJson();
Test.startTest();
Boolean isString = testJson.get('string').isString();
Boolean isNotString =
testJson.get('map').isString() ||
testJson.get('list').isString() ||
testJson.get('decimal').isString() ||
testJson.get('integer').isString() ||
testJson.get('null').isString();
Test.stopTest();
System.assertEquals(true, isString);
System.assertEquals(false, isNotString);
}
@IsTest
private static void shouldIndicateWhetherTypeIsDecimal() {
Map<String, JsonNode> testJson = getTestJson();
Test.startTest();
Boolean isDecimal = testJson.get('decimal').isDecimal();
Boolean isNotDecimal =
testJson.get('map').isDecimal() ||
testJson.get('list').isDecimal() ||
testJson.get('string').isDecimal() ||
testJson.get('null').isDecimal();
Test.stopTest();
System.assertEquals(true, isDecimal);
System.assertEquals(false, isNotDecimal);
}
@IsTest
private static void shouldIndicateWhetherTypeIsNull() {
Map<String, JsonNode> testJson = getTestJson();
Test.startTest();
Boolean isNull = testJson.get('null').isNull();
Boolean isNotNull =
testJson.get('map').isNull() ||
testJson.get('list').isNull() ||
testJson.get('string').isNull() ||
testJson.get('decimal').isNull() ||
testJson.get('integer').isNull() ||
testJson.get('date').isNull() ||
testJson.get('time').isNull();
Test.stopTest();
System.assertEquals(true, isNull);
System.assertEquals(false, isNotNull);
}
//==================================================================================================================
// Convenience accessors
//==================================================================================================================
@IsTest
private static void shouldProvideValueAsMap() {
Map<String, JsonNode> testJson = getTestJson();
JsonNode node = testJson.get('map');
Test.startTest();
Map<String, Object> result = node.asMap();
Test.stopTest();
System.assertEquals(node.value, result);
}
@IsTest
private static void shouldProvideValueAsMap_exceptionWhenNot() {
Map<String, JsonNode> testJson = getTestJson();
Test.startTest();
TypeException caughtException;
try {
testJson.get('string').asMap();
} catch(TypeException e) {
caughtException = e;
}
Test.stopTest();
System.assertNotEquals(null, caughtException);
}
@IsTest
private static void shouldProvideValueAsList() {
Map<String, JsonNode> testJson = getTestJson();
JsonNode node = testJson.get('list');
Test.startTest();
List<Object> result = node.asList();
Test.stopTest();
System.assertEquals(node.value, result);
}
@IsTest
private static void shouldProvideValueAsList_exceptionWhenNot() {
Map<String, JsonNode> testJson = getTestJson();
Test.startTest();
TypeException caughtException;
try {
testJson.get('string').asList();
} catch(TypeException e) {
caughtException = e;
}
Test.stopTest();
System.assertNotEquals(null, caughtException);
}
@IsTest
private static void shouldProvideValueAsString() {
Map<String, JsonNode> testJson = getTestJson();
JsonNode node = testJson.get('string');
Test.startTest();
String result = node.asString();
Test.stopTest();
System.assertEquals(true, result.equals(node.value));
}
@IsTest
private static void shouldProvideValueAsDecimal() {
Map<String, JsonNode> testJson = getTestJson();
JsonNode decimalNode = testJson.get('decimal');
JsonNode integerNode = testJson.get('integer');
Test.startTest();
Decimal decimalResult = decimalNode.asDecimal();
Decimal integerResult = integerNode.asDecimal();
Test.stopTest();
System.assertEquals(1.32, decimalResult);
System.assertEquals(45, integerResult);
}
@IsTest
private static void shouldProvideValueAsDecimal_exceptionWhenNot() {
Map<String, JsonNode> testJson = getTestJson();
Test.startTest();
TypeException caughtException;
try {
testJson.get('string').asDecimal();
} catch(TypeException e) {
caughtException = e;
}
Test.stopTest();
System.assertNotEquals(null, caughtException);
}
@IsTest
private static void shouldProvideValueAsInteger() {
Map<String, JsonNode> testJson = getTestJson();
JsonNode node = testJson.get('integer');
Test.startTest();
Integer result = node.asInteger();
Test.stopTest();
System.assertEquals(45, result);
}
@IsTest
private static void shouldProvideValueAsInteger_exceptionWhenNot() {
Map<String, JsonNode> testJson = getTestJson();
Test.startTest();
TypeException caughtException;
try {
testJson.get('string').asInteger();
} catch(TypeException e) {
caughtException = e;
}
Test.stopTest();
System.assertNotEquals(null, caughtException);
}
@IsTest
private static void shouldProvideValueAsDatetime() {
Map<String, JsonNode> testJson = getTestJson();
JsonNode dateNode = testJson.get('date');
JsonNode dateTimeNode = testJson.get('time');
Test.startTest();
Datetime dateResult = dateNode.asDatetime();
Datetime dateTimeResult = dateTimeNode.asDatetime();
Test.stopTest();
System.assertEquals(2012, dateResult.yearGmt());
System.assertEquals(9, dateResult.monthGmt());
System.assertEquals(15, dateResult.dayGmt());
System.assertEquals(2012, dateTimeResult.yearGmt());
System.assertEquals(9, dateTimeResult.monthGmt());
System.assertEquals(15, dateTimeResult.dayGmt());
System.assertEquals(15, dateTimeResult.hourGmt());
System.assertEquals(53, dateTimeResult.minuteGmt());
System.assertEquals(0, dateTimeResult.secondGmt());
}
@IsTest
private static void shouldProvideValueAsDatetime_exceptionWhenNot() {
Map<String, JsonNode> testJson = getTestJson();
Test.startTest();
Exception caughtException;
try {
testJson.get('string').asDatetime();
} catch(Exception e) {
caughtException = e;
}
Test.stopTest();
System.assertNotEquals(null, caughtException);
// TODO: Assert specific exception and message
}
@IsTest
private static void shouldProvideValueAsNullWheneverNull() {
Map<String, JsonNode> testJson = getTestJson();
JsonNode node = testJson.get('null');
Test.startTest();
Map<String, Object> asMap = node.asMap();
List<Object> asList = node.asList();
String asString = node.asString();
Decimal asDecimal = node.asDecimal();
Integer asInteger = node.asInteger();
Datetime asDatetime = node.asDatetime();
Object asObject = node.asInstanceOf(Contact.class);
Test.stopTest();
System.assertEquals(null, asMap);
System.assertEquals(null, asList);
System.assertEquals(null, asString);
System.assertEquals(null, asDecimal);
System.assertEquals(null, asInteger);
System.assertEquals(null, asDatetime);
System.assertEquals(null, asObject);
}
@IsTest
private static void shouldProvideAnIterableList_forList() {
Map<String, JsonNode> testJson = getTestJson();
JsonNode node = testJson.get('list');
Test.startTest();
List<JsonNode> iterable = node.iterator();
Test.stopTest();
List<Object> nodeList = node.asList();
System.assertEquals(nodeList.size(), iterable.size());
for (Integer i = 0; i < iterable.size(); i++) {
System.assertEquals(nodeList.get(i), iterable.get(i).value);
}
}
@IsTest
private static void shouldProvideAnIterableList_forMap() {
Map<String, JsonNode> testJson = getTestJson();
JsonNode node = testJson.get('map');
Test.startTest();
List<JsonNode> iterable = node.iterator();
Test.stopTest();
System.assertEquals(1, iterable.size());
System.assertEquals(node.value, iterable.get(0).value);
}
@IsTest
private static void shouldProvideAnIterableList_forSimpleType() {
Map<String, JsonNode> testJson = getTestJson();
JsonNode node = testJson.get('string');
Test.startTest();
List<JsonNode> iterable = node.iterator();
Test.stopTest();
System.assertEquals(1, iterable.size());
System.assertEquals(node.value, iterable.get(0).value);
}
@IsTest
private static void shouldProvideAnIterableList_forNull() {
Map<String, JsonNode> testJson = getTestJson();
JsonNode node = testJson.get('null');
Test.startTest();
List<JsonNode> iterable = node.iterator();
Test.stopTest();
System.assertEquals(0, iterable.size());
}
@IsTest
private static void shouldAllowTraversal_withKey() {
Map<String, JsonNode> testJson = getTestJson();
JsonNode node = testJson.get('complex');
Test.startTest();
Object value = node.get('string').value;
Test.stopTest();
System.assertEquals('I am a string', value);
}
@IsTest
private static void shouldAllowTraversal_withWrongKey() {
Map<String, JsonNode> testJson = getTestJson();
JsonNode node = testJson.get('complex');
Test.startTest();
Object value = node.get('not a valid key').value;
Test.stopTest();
System.assertEquals(null, value);
}
@IsTest
private static void shouldAllowTraversal_throughValueNode() {
Map<String, JsonNode> testJson = getTestJson();
JsonNode stringNode = testJson.get('string');
JsonNode nullNode = testJson.get('null');
Test.startTest();
Object stringValue = stringNode.get('not a valid key').value;
Object nullValue = nullNode.get('not a valid key').value;
Test.stopTest();
System.assertEquals(null, stringValue);
System.assertEquals(null, nullValue);
}
@IsTest
private static void shouldAllowTraversal_withDotNotation() {
Map<String, JsonNode> testJson = getTestJson();
JsonNode node = testJson.get('complex');
Test.startTest();
Object value = node.get('map.nestedString').value;
Test.stopTest();
System.assertEquals('I am a string nested in a map', value);
}
@IsTest
private static void shouldAllowTraversal_withArrayNotation() {
Map<String, JsonNode> testJson = getTestJson();
JsonNode node = testJson.get('complex');
JsonNode arrayNode = node.get('basicArray');
Test.startTest();
Object value1 = arrayNode.get('[0]').value;
Object value2 = node.get('basicArray[1]').value;
Test.stopTest();
System.assertEquals('array value 1', value1);
System.assertEquals('array value 2', value2);
}
@IsTest
private static void shouldAllowTraversal_withMixedNotation() {
Map<String, JsonNode> testJson = getTestJson();
JsonNode node = testJson.get('complex');
Test.startTest();
Object nestedArrayValue0 = node.get('map.nestedArray[0]').value;
Object nestedArrayValue1 = node.get('map.nestedArray[1]').value;
Object nestedMapValue = node.get('arrayOfMaps[0].nestedMapValue').value;
Test.stopTest();
System.assertEquals('nested array value 1', nestedArrayValue0);
System.assertEquals('nested array value 2', nestedArrayValue1);
System.assertEquals('I am a string in a map in an array', nestedMapValue);
}
@IsTest
private static void shouldAllowTraversal_exceptionOnNonIntegerKey() {
Map<String, JsonNode> testJson = getTestJson();
JsonNode arrayNode = testJson.get('complex').get('basicArray');
Test.startTest();
JsonNode.NonIntegerKeyException caughtException;
try {
arrayNode.get('non-integer key');
} catch (JsonNode.NonIntegerKeyException e) {
caughtException = e;
}
Test.stopTest();
System.assertNotEquals(null, caughtException);
System.assertEquals('Non-integer key provided for list value', caughtException.getMessage());
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment