Commit 312222e8 authored by Michael Comella's avatar Michael Comella Committed by Sebastian Kaspari
Browse files

Issue #651 - review: Move JSONArray.mapNotNull to ktx component.

This required the function to become more generic: it handles an array
of any single-typed objects, not just JSONObjects now.
parent fcbbe247
......@@ -7,7 +7,8 @@ package mozilla.components.service.pocket
import android.support.annotation.VisibleForTesting
import android.support.annotation.VisibleForTesting.PRIVATE
import mozilla.components.service.pocket.data.PocketGlobalVideoRecommendation
import mozilla.components.service.pocket.ext.mapObjNotNull
import mozilla.components.support.ktx.android.org.json.mapNotNull
import org.json.JSONArray
import org.json.JSONException
import org.json.JSONObject
......@@ -22,7 +23,7 @@ internal class PocketJSONParser {
fun jsonToGlobalVideoRecommendations(jsonStr: String?): List<PocketGlobalVideoRecommendation>? = try {
val rawJSON = JSONObject(jsonStr)
val videosJSON = rawJSON.getJSONArray(KEY_VIDEO_RECOMMENDATIONS_INNER)
val videos = videosJSON.mapObjNotNull { jsonToGlobalVideoRecommendation(it) }
val videos = videosJSON.mapNotNull(JSONArray::getJSONObject) { jsonToGlobalVideoRecommendation(it) }
// We return null, rather than the empty list, because devs might forget to check an empty list.
if (videos.isNotEmpty()) videos else null
......
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package mozilla.components.service.pocket.ext
import org.json.JSONArray
import org.json.JSONException
import org.json.JSONObject
/**
* Map over the JSONArray, removing null values after [transform] and ignoring data that throws a [JSONException].
* An empty List will be returned if all the values are null or invalid.
*
* We don't expose this function publicly because it's not generic enough yet: JSONObject should be T. We don't
* want to expose this temporarily because then we'll break the public API.
*/
internal inline fun <R : Any> JSONArray.mapObjNotNull(transform: (JSONObject) -> R?): List<R> {
val transformedResults = mutableListOf<R>()
for (i in 0 until this.length()) {
try {
val transformed = transform(this.getJSONObject(i))
if (transformed != null) { transformedResults.add(transformed) }
} catch (e: JSONException) { /* Do nothing: we skip bad data. */ }
}
return transformedResults
}
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package mozilla.components.service.pocket.ext
import org.json.JSONArray
import org.json.JSONObject
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
@RunWith(RobolectricTestRunner::class)
class JSONKtTest {
@Test
fun `WHEN flatMapObj on an empty jsonArray THEN an empty list is returned`() {
assertEquals(emptyList<Int>(), JSONArray().mapObjNotNull { 1 })
}
@Test
fun `WHEN flatMapObj transform throws a JSONException THEN that item is ignored`() {
val input = JSONArray().apply {
put(JSONObject("""{"a": 1}"""))
put(JSONObject("""{"b": 2}"""))
}
val actual = input.mapObjNotNull {
it.get("b") // key not found for first item: throws an exception.
}
assertEquals(1, actual.size)
assertEquals(2, actual[0])
}
@Test
fun `WHEN flatMapObj on a list THEN nulls are removed and data is mapped`() {
val expected = listOf(
"a" to 1,
"b" to 2,
"c" to 3
)
// Convert expected input: [JSONObject("a" to 1), null, JSONObject("b" to 2), null, ...]
val input = JSONArray()
expected.forEach {
val obj = JSONObject().apply { put(it.first, it.second) }
input.put(obj)
input.put(null)
}
val actual = input.mapObjNotNull {
val keys = it.keys().asSequence().toList()
assertEquals(it.toString(), 1, keys.size)
val key = keys.first()
val value = it.get(key) as Int
Pair(key, value)
}
assertEquals(expected, actual)
}
}
......@@ -5,6 +5,7 @@
package mozilla.components.support.ktx.android.org.json
import org.json.JSONArray
import org.json.JSONException
/*
* Convenience method to convert a JSONArray into a sequence.
......@@ -37,3 +38,25 @@ fun <T> JSONArray?.toList(): List<T> {
}
return listOf()
}
/**
* Returns a list containing only the non-null results of applying the given [transform] function
* to each element in the original collection as returned by [getFromArray]. If [getFromArray]
* or [transform] throws a [JSONException], these elements will also be omitted.
*
* Here's an example call:
* ```kotlin
* jsonArray.mapNotNull(JSONArray::getJSONObject) { jsonObj -> jsonObj.getString("author") }
* ```
*/
inline fun <T, R : Any> JSONArray.mapNotNull(getFromArray: JSONArray.(index: Int) -> T, transform: (T) -> R?): List<R> {
val transformedResults = mutableListOf<R>()
for (i in 0 until this.length()) {
try {
val transformed = transform(getFromArray(i))
if (transformed != null) { transformedResults.add(transformed) }
} catch (e: JSONException) { /* Do nothing: we skip bad data. */ }
}
return transformedResults
}
......@@ -5,8 +5,10 @@
package mozilla.components.support.ktx.android.org.json
import org.json.JSONArray
import org.json.JSONObject
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
......@@ -14,6 +16,16 @@ import org.robolectric.RobolectricTestRunner
@RunWith(RobolectricTestRunner::class)
class JSONArrayTest {
private lateinit var testData2Elements: JSONArray
@Before
fun setUp() {
testData2Elements = JSONArray().apply {
put(JSONObject("""{"a": 1}"""))
put(JSONObject("""{"b": 2}"""))
}
}
@Test
fun itCanBeIterated() {
val array = JSONArray("[1, 2, 3]")
......@@ -49,4 +61,77 @@ class JSONArrayTest {
assertTrue(list.contains("value"))
assertTrue(list.contains("another-value"))
}
@Test
fun `WHEN mapNotNull on an empty jsonArray THEN an empty list is returned`() {
assertEquals(emptyList<Int>(), JSONArray().mapNotNull(JSONArray::getJSONObject) { 1 })
}
@Test
fun `WHEN mapNotNull getFromArray throws a JSONException THEN that item is ignored`() {
val expected = listOf("a", "b")
testData2Elements.put(404)
val actual = testData2Elements.mapNotNull(JSONArray::getJSONObject) { it.keys().asSequence().first() }
assertEquals(expected, actual)
}
@Test
fun `WHEN mapNotNull transform throws a JSONException THEN that item is ignored`() {
val actual = testData2Elements.mapNotNull(JSONArray::getJSONObject) {
it.get("b") // key not found for first item: throws an exception.
}
assertEquals(1, actual.size)
assertEquals(2, actual[0])
}
@Test
fun `WHEN mapNotNull getFromArray uses casted classes THEN data is mapped`() {
val expected = listOf(JSONArrayTest())
val input = JSONArray().apply { put(expected[0]) }
val actual = input.mapNotNull(getFromArray = { i -> get(i) as JSONArrayTest }) { it }
assertEquals(expected, actual)
}
@Test
fun `WHEN mapNotNull on an array of Int THEN nulls are removed and data is mapped`() {
val expected = listOf(2, 4, 6, 8, 10)
// Convert expected to input: [2, null, 4, null, ...]
val input = JSONArray()
expected.forEach {
input.put(it)
input.put(null)
}
val actual = input.mapNotNull(JSONArray::getInt) { it }
assertEquals(expected, actual)
}
@Test
fun `WHEN mapNotNull on an array of JSONObject THEN nulls are removed and data is mapped`() {
val expected = listOf(
"a" to 1,
"b" to 2,
"c" to 3
)
// Convert expected to input: [JSONObject("a" to 1), null, JSONObject("b" to 2), null, ...]
val input = JSONArray()
expected.forEach {
val obj = JSONObject().apply { put(it.first, it.second) }
input.put(obj)
input.put(null)
}
val actual = input.mapNotNull(JSONArray::getJSONObject) {
val keys = it.keys().asSequence().toList()
assertEquals(it.toString(), 1, keys.size)
val key = keys.first()
val value = it.get(key) as Int
Pair(key, value)
}
assertEquals(expected, actual)
}
}
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment