Created
July 5, 2019 18:41
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* 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 {} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@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