| package json.specs |
| |
| import org.apache.tapestry5.json.JSONArray |
| import org.apache.tapestry5.json.JSONLiteral |
| import org.apache.tapestry5.json.JSONObject |
| import org.apache.tapestry5.json.JSONString |
| import spock.lang.Specification |
| import spock.lang.Unroll |
| |
| class JSONObjectSpec extends Specification { |
| |
| def "copy an existing JSONObject"() { |
| def master = new JSONObject("fred", "flintstone", "barney", "rubble") |
| |
| when: |
| |
| def emptyCopy = new JSONObject(master) |
| |
| then: |
| |
| emptyCopy.keys().empty |
| |
| when: |
| |
| def limitedCopy = new JSONObject(master, "fred") |
| |
| then: |
| |
| limitedCopy.keys().size() == 1 |
| limitedCopy.get("fred") == "flintstone" |
| |
| when: |
| |
| def fullCopy = new JSONObject(master, "fred", "barney") |
| |
| then: |
| |
| fullCopy.toCompactString() == /{"fred":"flintstone","barney":"rubble"}/ |
| } |
| |
| def "copy all properties of JSONObject"() { |
| def master = new JSONObject("fred", "flintstone", "barney", "rubble") |
| |
| when: |
| |
| def fullCopy = master.copy() |
| |
| then: |
| |
| master == fullCopy |
| |
| // And they are independent: |
| |
| when: |
| |
| master.put("wilma", "flintstone") |
| |
| then: |
| |
| master != fullCopy |
| } |
| |
| def "unknown keys when copying a JSONObject are ignored"() { |
| def master = new JSONObject("fred", "flintstone", "barney", "rubble") |
| |
| when: |
| |
| def copy = new JSONObject(master, "fred", "wilma") |
| |
| then: |
| |
| copy.keys().size() == 1 |
| copy.get("fred") == "flintstone" |
| } |
| |
| def "get with unknown key is an exception"() { |
| def master = new JSONObject("fred", "flintstone") |
| |
| when: |
| |
| master.get "barney" |
| |
| then: |
| |
| RuntimeException e = thrown() |
| |
| e.message == /JSONObject["barney"] not found./ |
| } |
| |
| @Unroll |
| def "ensure that stored value #typeName #value is #expectedTypeName #expected"() { |
| |
| when: |
| |
| def object = new JSONObject().put("key", value) |
| |
| then: |
| |
| object."get${expectedTypeName.capitalize()}"("key") == expected |
| |
| where: |
| |
| value | expected | expectedType |
| "true" | true | boolean |
| "TRUE" | true | boolean |
| "false" | false | boolean |
| "FALSE" | false | boolean |
| Boolean.TRUE | true | boolean |
| Boolean.FALSE | false | boolean |
| 3.5d | 3.5d | double |
| 1000l | 1000d | double |
| "-101.7" | -101.7d | double |
| "3" | 3 | int |
| 97l | 97 | int |
| "-8.76" | -8 | int |
| 92d | 92l | long |
| "-200" | -200l | long |
| 123 | "123" | String |
| |
| typeName = value.class.simpleName |
| expectedTypeName = expectedType.simpleName |
| } |
| |
| @Unroll |
| def "conversion of double #input to string should be '#expected'"() { |
| expect: |
| |
| JSONObject.doubleToString(input) == expected |
| |
| where: |
| |
| input | expected |
| 3d | "3" |
| -22.5d | "-22.5" |
| 0d | "0" |
| Double.NaN | "null" |
| Double.NEGATIVE_INFINITY | "null" |
| Double.POSITIVE_INFINITY | "null" |
| } |
| |
| def "exception when getBoolean() on a value that is not a boolean"() { |
| |
| def object = new JSONObject().put("akey", 37) |
| |
| when: |
| |
| object.getBoolean "akey" |
| |
| then: |
| |
| RuntimeException e = thrown() |
| |
| e.message == /JSONObject["akey"] is not a Boolean./ |
| } |
| |
| def "accumulate simple values"() { |
| def object = new JSONObject() |
| |
| def key = "acckey" |
| |
| when: |
| |
| object.accumulate key, "alpha" |
| |
| then: "first value added is just stored" |
| |
| object.toCompactString() == /{"acckey":"alpha"}/ |
| |
| when: "subsequent values added are accumulated in a JSONArray" |
| |
| object.accumulate key, "beta" |
| object.accumulate key, "gamma" |
| |
| then: |
| |
| object.toCompactString() == /{"acckey":["alpha","beta","gamma"]}/ |
| |
| object.getJSONArray(key).length() == 3 |
| } |
| |
| def "accumulate when the stored value is already an array just appends the value"() { |
| def object = new JSONObject() |
| |
| when: |
| |
| object.accumulate "key", new JSONArray("fred", "barney") |
| |
| then: |
| |
| object.toCompactString() == /{"key":["fred","barney"]}/ |
| |
| when: |
| |
| object.accumulate "key", "wilma" |
| |
| then: |
| |
| object.toCompactString() == /{"key":["fred","barney","wilma"]}/ |
| } |
| |
| def "can create a JSONObject from a JSON string"() { |
| when: |
| |
| def object = new JSONObject(/ { fred: "flintstone", caveman: true, friends: ["barney"] }/) |
| |
| then: |
| |
| object.get("fred") == "flintstone" |
| object.getBoolean("caveman") == true |
| |
| def friends = object.getJSONArray "friends" |
| |
| friends.length() == 1 |
| friends.get(0) == "barney" |
| } |
| |
| def "append to an unknown key creates a JSONArray for the value"() { |
| def object = new JSONObject() |
| |
| when: |
| |
| object.append "friends", "barney" |
| |
| then: |
| |
| object.toCompactString() == /{"friends":["barney"]}/ |
| } |
| |
| def "append to an existing key containing a JSONArray appends to the array"() { |
| def object = new JSONObject(/{friends: ["barney"] }/) |
| |
| when: |
| |
| object.append "friends", "zaphod" |
| |
| then: |
| |
| object.toCompactString() == /{"friends":["barney","zaphod"]}/ |
| } |
| |
| def "appending to a key whose value is not a JSONArray is an exception"() { |
| def object = new JSONObject(/{friends: 0 }/) |
| |
| when: |
| |
| object.append "friends", "zaphod" |
| |
| then: |
| |
| RuntimeException e = thrown() |
| |
| e.message == /JSONObject["friends"] is not a JSONArray./ |
| } |
| |
| def "getDouble() with a non-numeric value is an exception"() { |
| def object = new JSONObject(/{notdouble: true}/) |
| |
| when: |
| |
| object.getDouble "notdouble" |
| |
| then: |
| |
| RuntimeException e = thrown() |
| |
| e.message == /JSONObject["notdouble"] is not a number./ |
| } |
| |
| def "getDouble() with a string that can not be parsed as a number is an exception"() { |
| def object = new JSONObject(/{notdouble: "this is a fact"}/) |
| |
| when: |
| |
| object.getDouble "notdouble" |
| |
| then: |
| |
| RuntimeException e = thrown() |
| |
| e.message == /JSONObject["notdouble"] is not a number./ |
| |
| } |
| |
| def "has() will identify which keys have values and which do not"() { |
| def object = new JSONObject(/{fred: 'flintstone'}/) |
| |
| expect: |
| |
| object.has("fred") |
| !object.has("barney") |
| } |
| |
| def "getJSONArray() for a value that is not a JSONArray is an exception"() { |
| def object = new JSONObject(/{notarray: 22.7}/) |
| |
| when: |
| |
| object.getJSONArray "notarray" |
| |
| then: |
| |
| RuntimeException e = thrown() |
| |
| e.message == /JSONObject["notarray"] is not a JSONArray./ |
| } |
| |
| def "length() of a JSONObject is the number of keys"() { |
| when: |
| |
| def object = new JSONObject() |
| |
| then: |
| |
| object.length() == 0 |
| |
| when: |
| |
| object.put "fred", "flintstone" |
| |
| then: |
| |
| object.length() == 1 |
| |
| when: |
| |
| object.accumulate "fred", "murray" |
| |
| then: |
| |
| object.length() == 1 |
| |
| when: |
| |
| object.put "barney", "rubble" |
| |
| then: |
| |
| object.length() == 2 |
| } |
| |
| def "names() returns the keys as a JSONArray"() { |
| def object = new JSONObject(/ {fred: 'flintstone', barney: 'rubble' }/) |
| |
| when: |
| |
| def names = object.names() |
| |
| then: |
| |
| names instanceof JSONArray |
| names.length() == 2 |
| |
| new ArrayList(names.toList()).sort() == ["barney", "fred"] |
| } |
| |
| def "names() with no properties returns null"() { |
| |
| when: |
| |
| def object = new JSONObject() |
| |
| then: |
| |
| object.names() == null |
| } |
| |
| def "the null literal is the NULL object"() { |
| when: |
| |
| def object = new JSONObject(/{nullkey: null}/) |
| |
| then: |
| |
| object.get("nullkey") is JSONObject.NULL |
| object.isNull("nullkey") |
| } |
| |
| def "the NULL object is output as null literal"() { |
| when: |
| |
| def object = new JSONObject().put("nullkey", JSONObject.NULL) |
| |
| then: |
| |
| object.isNull("nullkey") |
| object.toCompactString() == /{"nullkey":null}/ |
| } |
| |
| def "the NULL object matches Java's null"() { |
| expect: |
| |
| JSONObject.NULL.equals(null) |
| } |
| |
| @Unroll |
| def "#desc invalid input is an exception"() { |
| when: |
| |
| new JSONObject(input) |
| |
| then: |
| |
| RuntimeException e = thrown() |
| |
| e.message.trim().startsWith expected |
| |
| where: |
| |
| input | expected | desc |
| "{ " | "A JSONObject text must end with '}' at character 3" | "unmatched open brace" |
| "fred" | "A JSONObject text must begin with '{' at character 1 of " | "missing open brace" |
| /{ "akey" }/ | /Expected a ':' after a key at character 10 of/ | "missing value after key" |
| /{ "fred" : 1 "barney" }/ | /Expected a ',' or '}' at character 14 of/ | "missing property separator" |
| /{ "list" : [1, 2/ | /Expected a ',' or ']' at character 16 of/ | "missing seperator or closing bracket" |
| '''/* unclosed''' | /Unclosed comment at character 11 of/ | "unclosed C-style comment" |
| /{ "fred \n}/ | /Unterminated string at character 11 of/ | "unterminated string at line end" |
| /{ fred: ,}/ | /Missing value at character 8 of / | "missing value after key" |
| } |
| |
| def "can use ':' or '=>' as key seperator, and ';' as property separator"() { |
| def object = new JSONObject(/{ "fred" = 1; "barney" => 2}/) |
| |
| expect: |
| |
| object.getInt("fred") == 1 |
| object.getInt("barney") == 2 |
| } |
| |
| @Unroll |
| def "conversion of #typeName #value to string is #expected"() { |
| expect: |
| |
| JSONObject.numberToString(value) == expected |
| |
| where: |
| |
| value | expected |
| new BigDecimal("100.0000000") | "100" |
| |
| typeName = value.class.simpleName |
| } |
| |
| @Unroll |
| def "quote(): #desc"() { |
| expect: |
| |
| JSONObject.quote(value) == expected |
| |
| where: |
| |
| value | expected | desc |
| null | /""/ | "null is empty string" |
| "" | /""/ | "empty string is empty string" |
| '''"/\b\t\n\f\r\u2001/a</''' | '''"\\"/\\b\\t\\n\\f\\r\\u2001/a<\\/"''' | "special characters" |
| } |
| |
| def "non-finite numbers are not allowed"() { |
| def object = new JSONObject() |
| |
| when: |
| |
| object.put "nonfinite", value |
| |
| then: |
| |
| RuntimeException e = thrown() |
| |
| e.message == /JSON does not allow non-finite numbers./ |
| |
| where: |
| |
| value << [Double.NaN, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY, Float.NaN, |
| Float.NEGATIVE_INFINITY, Float.POSITIVE_INFINITY] |
| |
| } |
| |
| def "parse an empty object is empty"() { |
| when: |
| |
| def object = new JSONObject(/{}/) |
| |
| then: |
| |
| object.length() == 0 |
| } |
| |
| def "put a Java null is a remove"() { |
| def object = new JSONObject(/{fred:'flintstone', barney:'rubble'}/) |
| |
| when: |
| |
| object.put("fred", null) |
| |
| then: |
| |
| object.toCompactString() == /{"barney":"rubble"}/ |
| } |
| |
| def "only specific object types may be added"() { |
| when: |
| |
| JSONObject.testValidity([:]) |
| |
| then: |
| |
| RuntimeException e = thrown() |
| |
| e.message == '''JSONObject properties may be one of Boolean, Number, String, org.apache.tapestry5.json.JSONArray, org.apache.tapestry5.json.JSONLiteral, org.apache.tapestry5.json.JSONObject, org.apache.tapestry5.json.JSONObject$Null, org.apache.tapestry5.json.JSONString. Type java.util.LinkedHashMap is not allowed.''' |
| } |
| |
| def "JSONString can output anything it wants"() { |
| def string = { "*VALUE*" } as JSONString |
| |
| def object = new JSONObject() |
| |
| when: |
| |
| object.put("key", string) |
| |
| then: |
| |
| object.toCompactString() == /{"key":*VALUE*}/ |
| } |
| |
| def "equals() implementation"() { |
| def json = /{ "key" : 99 }/ |
| |
| when: |
| |
| def obj1 = new JSONObject(json) |
| def obj2 = new JSONObject(json) |
| |
| then: |
| |
| obj1 == obj2 |
| obj2 == obj1 |
| |
| obj1.equals(null) == false |
| obj1 != json |
| |
| when: |
| |
| obj2.put("screw", "the pooch") |
| |
| then: |
| |
| obj1 != obj2 |
| } |
| |
| def "escaped characters in the JSON are parsed correctly"() { |
| when: |
| |
| def object = new JSONObject("{ \"key\" : \"\\\"/\\b\\t\\n\\f\\r\\u2001/a<\\/\\x20\" }") |
| |
| then: |
| |
| object.get("key") == '''"/\b\t\n\f\r\u2001/a</ ''' |
| } |
| |
| def "parse a nested object"() { |
| def object = new JSONObject(/ { key: { name: "inner" }}/) |
| |
| expect: |
| |
| object.getJSONObject("key").getString("name") == "inner" |
| } |
| |
| def "parse boolean literals"() { |
| def object = new JSONObject(/{ t: true, f: false }/) |
| |
| expect: |
| |
| object.getBoolean("t") == true |
| object.getBoolean("f") == false |
| } |
| |
| def "parse numeric forms"() { |
| def object = new JSONObject(/{ hex: 0x50, oct: 030, posInt: +50, negInt: -50, |
| long: 4294968530, float: -32.7 }/) |
| |
| expect: |
| |
| object.getInt("hex") == 80 |
| object.getInt("oct") == 24 |
| object.getInt("posInt") == 50 |
| object.getInt("negInt") == -50 |
| object.getLong("long") == 4294968530l |
| object.getDouble("float") == -32.7d |
| } |
| |
| def "NULL toString() is null keyword"() { |
| expect: |
| |
| JSONObject.NULL.toString() == "null" |
| } |
| |
| def "JSONLiteral can output any content"() { |
| |
| def text = '''function(x) { $('bar').show(); }''' |
| |
| def literal = new JSONLiteral(text) |
| |
| when: |
| |
| def object = new JSONObject().put("callback", literal) |
| |
| then: |
| |
| object.toCompactString() == /{"callback":${text}}/ |
| literal.toString() == text |
| } |
| |
| def "pretty print"() { |
| def object = new JSONObject("fred", "flintstone", "barney", "rubble") |
| |
| expect: |
| |
| object.toString() == '''{ |
| "fred" : "flintstone", |
| "barney" : "rubble" |
| }''' |
| } |
| |
| def "C style comments are ignored"() { |
| when: |
| |
| def object = new JSONObject('''/* C comment */ { "foo" : "bar" /* *ignored* */ }''') |
| |
| then: |
| |
| object.get("foo") == "bar" |
| } |
| |
| def "double slash comments extend to end of line"() { |
| when: |
| |
| def object = new JSONObject('''{ foo: // This is a double-slash comment |
| "bar" }''') |
| |
| then: |
| |
| object.get("foo") == "bar" |
| } |
| |
| def "hash comments extend to end of line"() { |
| when: |
| |
| def object = new JSONObject('''{ foo: # This is a hash comment |
| "bar" }''') |
| |
| then: |
| |
| object.get("foo") == "bar" |
| |
| } |
| |
| def "pretty-print an empty JSONObject"() { |
| def object = new JSONObject() |
| |
| expect: |
| |
| object.toString() == /{}/ |
| } |
| |
| def "complex pretty-print"() { |
| when: |
| |
| def object = new JSONObject(''' |
| { outer: { inner: "here" }, |
| number: 22.7, |
| array: [1, 2, 3], |
| boolean: true, |
| otherwise: 'a quoted string' |
| } |
| ''') |
| |
| then: |
| |
| object.toString() == '''{ |
| "outer" : { |
| "inner" : "here" |
| }, |
| "number" : 22.7, |
| "array" : [ |
| 1, |
| 2, |
| 3 |
| ], |
| "boolean" : true, |
| "otherwise" : "a quoted string" |
| }''' |
| } |
| |
| def "toCompactString()"() { |
| def object = new JSONObject("kermit", "frog") |
| |
| expect: |
| |
| object.toString(true) == /{"kermit":"frog"}/ |
| object.toString(false) == /{ |
| "kermit" : "frog" |
| }/ |
| } |
| |
| def "print to PrintWriter)"() { |
| def object = new JSONObject("kermit", "frog") |
| |
| expect: |
| |
| print(object, true) == /{"kermit":"frog"}/ |
| print(object, false) == /{ |
| "kermit" : "frog" |
| }/ |
| } |
| |
| def withPrintWriter(closure) { |
| def caw = new CharArrayWriter() |
| def pw = new PrintWriter(caw) |
| |
| closure(pw) |
| |
| pw.close() |
| |
| return caw.toString() |
| } |
| |
| def print(object, compact) { |
| |
| withPrintWriter { pw -> object.print(pw, compact) } |
| } |
| |
| def "prettyPrint() to PrintWriter"() { |
| def object = new JSONObject("kermit", "frog") |
| |
| when: |
| |
| def json = withPrintWriter { pw -> object.prettyPrint(pw) } |
| |
| then: |
| |
| json == /{ |
| "kermit" : "frog" |
| }/ |
| |
| } |
| |
| def "getString() at index"() { |
| def array = new JSONArray("one", 2, false) |
| |
| expect: |
| |
| array.getString(0) == "one" |
| array.getString(1) == "2" |
| array.getString(2) == "false" |
| } |
| |
| def "toMap() is not modifiable"() { |
| def object = new JSONObject() |
| |
| def map = object.toMap() |
| |
| when: |
| |
| map.clear() |
| |
| then: |
| |
| thrown(UnsupportedOperationException) |
| } |
| |
| def "can access contents of object as a map"() { |
| def object = new JSONObject("foo", "bar") |
| .put("null", JSONObject.NULL) |
| .put("number", 6) |
| |
| when: |
| |
| def map = object.toMap() |
| |
| then: |
| |
| map.foo == "bar" |
| map.number == 6 |
| map["null"].is(JSONObject.NULL) |
| } |
| |
| def "the map returned by toMap() is live"() { |
| def object = new JSONObject("foo", "bar") |
| |
| def map = object.toMap() |
| |
| when: |
| |
| object.put "foo", null |
| |
| then: |
| |
| object.length() == 0 |
| map.isEmpty() |
| } |
| |
| def "can add new properties via putAll()"() { |
| def object = new JSONObject("fred", "flintstone") |
| |
| when: |
| |
| def result = object.putAll([barney: "rubble", wilma: "flintstone"]) |
| |
| then: |
| |
| result.is object |
| object.toCompactString() == /{"fred":"flintstone","barney":"rubble","wilma":"flintstone"}/ |
| } |
| |
| def "in() where the value is not JSONObject is an exception"() { |
| def object = new JSONObject("time", "to rock") |
| |
| when: |
| |
| object.in("time") |
| |
| then: |
| |
| IllegalStateException e = thrown() |
| |
| e.message == /JSONObject["time"] is not a JSONObject./ |
| } |
| |
| def "in() creates a JSONObject if necessary"() { |
| def object = new JSONObject() |
| |
| when: |
| |
| object.in("nested").put("created", true) |
| |
| then: |
| |
| object.toCompactString() == /{"nested":{"created":true}}/ |
| } |
| |
| def "in() returns the JSONObject"() { |
| def object = new JSONObject(/{nested: {}}/) |
| |
| when: |
| |
| object.in("nested").put("time", "to rock") |
| |
| then: |
| |
| object.toCompactString() == /{"nested":{"time":"to rock"}}/ |
| } |
| |
| def "constructor supports non-string keys and values"() { |
| when: |
| |
| def object = new JSONObject(true, "truthy", false, false) |
| |
| then: |
| |
| object.has "true" |
| object.getString("true") == "truthy" |
| object.has "false" |
| object.get("false").is false |
| } |
| |
| private static copyViaSerialization(source) { |
| |
| def bos = new ByteArrayOutputStream() |
| |
| bos.withObjectOutputStream { it << source } |
| |
| def bis = new ByteArrayInputStream(bos.toByteArray()) |
| |
| bis.withObjectInputStream { it.readObject() } |
| } |
| |
| def "serialize and de-serialize"() { |
| when: |
| |
| def source = new JSONObject("string", "a string", "null", JSONObject.NULL) |
| |
| def copy = copyViaSerialization source |
| |
| then: |
| |
| source == copy |
| |
| } |
| |
| } |