Skip to content

Commit

Permalink
Add color to request and response body when content-type is json (#930)
Browse files Browse the repository at this point in the history
Co-authored-by: AmirHosein Hoseini <a.hoseini@snapp.cab>
  • Loading branch information
Amirhy and AmirHosein Hoseini authored Mar 4, 2023
1 parent ac4fcb0 commit 3ae4d08
Show file tree
Hide file tree
Showing 10 changed files with 369 additions and 39 deletions.
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ buildscript {
mockkVersion = '1.13.4'
robolectricVersion = '4.9.2'
truthVersion = '1.1.3'
androidXTestRunner = '1.5.1'
androidXTestRules = '1.5.0'
androidXTestExt = '1.1.4'

// Publishing
nexusStagingPlugin = '0.30.0'
Expand Down
7 changes: 7 additions & 0 deletions library/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ android {
minSdk rootProject.minSdkVersion
consumerProguardFiles 'proguard-rules.pro'
resValue("string", "chucker_version", "$VERSION_NAME")
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}

compileOptions {
Expand Down Expand Up @@ -96,6 +97,12 @@ dependencies {
testImplementation "androidx.arch.core:core-testing:$androidXCoreVersion"
testImplementation "com.google.truth:truth:$truthVersion"
testImplementation "org.robolectric:robolectric:$robolectricVersion"

androidTestImplementation "junit:junit:$junit4Version"
androidTestImplementation "androidx.test:runner:$androidXTestRunner"
androidTestImplementation "androidx.test:rules:$androidXTestRules"
androidTestImplementation "com.google.truth:truth:$truthVersion"
androidTestImplementation "androidx.test.ext:junit:$androidXTestExt"
}

apply from: rootProject.file('gradle/gradle-mvn-push.gradle')
Expand Down
101 changes: 101 additions & 0 deletions library/src/androidTest/kotlin/com/chuckerteam/chucker/SpanUtilTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package com.chuckerteam.chucker

import android.annotation.SuppressLint
import android.content.Context
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.chuckerteam.chucker.internal.support.SpanTextUtil
import com.google.common.truth.Truth
import org.junit.Assert
import org.junit.Before
import org.junit.BeforeClass
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
public class SpanUtilTest {
private lateinit var context: Context

@Before
public fun init() {
context = InstrumentationRegistry.getInstrumentation().context
}
@SuppressLint("CheckResult")
@Test
public fun json_can_have_null_value() {
val parsedJson = SpanTextUtil(context).spanJson(
"""{ "field": null }"""
)
Assert.assertEquals(
parsedJson.toString(),
"""
{
"field": null
}
""".trimIndent()
)
}
@Test
public fun json_can_have_empty_fields() {
val parsedJson = SpanTextUtil(context).spanJson(
"""{ "field": "" }"""
)

Truth.assertThat(parsedJson.toString()).isEqualTo(
"""
{
"field": ""
}
""".trimIndent()
)
}

@Test
public fun json_can_be_invalid() {
val parsedJson = SpanTextUtil(context).spanJson(
"""[{ "field": null }"""
)

Truth.assertThat(parsedJson.toString()).isEqualTo(
"""[{ "field": null }"""
)
}

@Test
public fun json_object_is_pretty_printed() {
val parsedJson = SpanTextUtil(context).spanJson(
"""{ "field1": "something", "field2": "else" }"""
)

Truth.assertThat(parsedJson.toString()).isEqualTo(
"""
{
"field1": "something",
"field2": "else"
}
""".trimIndent()
)
}

@Test
public fun json_array_is_pretty_printed() {
val parsedJson = SpanTextUtil(context).spanJson(
"""[{ "field1": "something1", "field2": "else1" }, { "field1": "something2", "field2": "else2" }]"""
)

Truth.assertThat(parsedJson.toString()).isEqualTo(
"""
[
{
"field1": "something1",
"field2": "else1"
},
{
"field1": "something2",
"field2": "else2"
}
]
""".trimIndent()
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,19 @@

package com.chuckerteam.chucker.internal.data.entity

import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.text.SpannableStringBuilder
import android.text.style.ForegroundColorSpan
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.Ignore
import androidx.room.PrimaryKey
import com.chuckerteam.chucker.internal.support.FormatUtils
import com.chuckerteam.chucker.internal.support.FormattedUrl
import com.chuckerteam.chucker.internal.support.JsonConverter
import com.chuckerteam.chucker.internal.support.SpanTextUtil
import com.google.gson.reflect.TypeToken
import okhttp3.Headers
import okhttp3.HttpUrl
Expand Down Expand Up @@ -215,6 +219,25 @@ internal class HttpTransaction(
}
}

/**
* This method creates [android.text.SpannableString] from body
* and add [ForegroundColorSpan] to text with different colors for better contrast between
* keys and values and etc in the body.
*
* This method just works with json content-type yet, and calls [formatBody]
* for other content-type until parser function will be developed for other content-types.
*/
private fun spanBody(body: CharSequence, contentType: String?, context: Context?): CharSequence {
return when {
//TODO Implement Other Content Types
contentType.isNullOrBlank() -> body
contentType.contains("json", ignoreCase = true) && context != null -> {
SpanTextUtil(context).spanJson(body)
}
else -> formatBody(body.toString(), contentType)
}
}

private fun formatBytes(bytes: Long): String {
return FormatUtils.formatByteCount(bytes, true)
}
Expand All @@ -223,10 +246,21 @@ internal class HttpTransaction(
return requestBody?.let { formatBody(it, requestContentType) } ?: ""
}

fun getSpannedRequestBody(context: Context?): CharSequence {
return requestBody?.let { spanBody(it, requestContentType,context) }
?: SpannableStringBuilder.valueOf("")
}

fun getFormattedResponseBody(): String {
return responseBody?.let { formatBody(it, responseContentType) } ?: ""
}

fun getSpannedResponseBody(context: Context?): CharSequence {
return responseBody?.let {
spanBody(it, responseContentType, context)
} ?: SpannableStringBuilder.valueOf("")
}

fun populateUrl(httpUrl: HttpUrl): HttpTransaction {
val formattedUrl = FormattedUrl.fromHttpUrl(httpUrl, encoded = false)
url = formattedUrl.url
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ import android.text.style.UnderlineSpan
*
* @param search the text to highlight
*/
internal fun String.highlightWithDefinedColors(
internal fun SpannableStringBuilder.highlightWithDefinedColors(
search: String,
backgroundColor: Int,
foregroundColor: Int
): SpannableStringBuilder {
val startIndexes = indexesOf(this, search)
val startIndexes = indexesOf(this.toString(), search)
return applyColoredSpannable(this, startIndexes, search.length, backgroundColor, foregroundColor)
}

Expand All @@ -31,14 +31,14 @@ private fun indexesOf(text: String, search: String): List<Int> {
}

private fun applyColoredSpannable(
text: String,
text: SpannableStringBuilder,
indexes: List<Int>,
length: Int,
backgroundColor: Int,
foregroundColor: Int
): SpannableStringBuilder {
return indexes
.fold(SpannableStringBuilder(text)) { builder, position ->
.fold(text) { builder, position ->
builder.setSpan(
UnderlineSpan(),
position,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
package com.chuckerteam.chucker.internal.support

import android.content.Context
import android.text.SpannableStringBuilder
import android.text.Spanned
import android.text.style.ForegroundColorSpan
import androidx.core.content.ContextCompat
import androidx.core.text.isDigitsOnly
import com.chuckerteam.chucker.R
import com.google.gson.JsonElement
import com.google.gson.JsonParser
import com.google.gson.JsonSyntaxException


public class SpanTextUtil(context: Context) {
private val jsonKeyColor: Int
private val jsonValueColor: Int
private val jsonDigitsAndNullValueColor: Int
private val jsonSignElementsColor: Int

init {
jsonKeyColor = ContextCompat.getColor(context, R.color.chucker_json_key_color)
jsonValueColor = ContextCompat.getColor(context, R.color.chucker_json_value_color)
jsonDigitsAndNullValueColor =
ContextCompat.getColor(context, R.color.chucker_json_digit_and_null_value_color)
jsonSignElementsColor = ContextCompat.getColor(context, R.color.chucker_json_elements_color)
}

public fun spanJson(input: CharSequence): SpannableStringBuilder {
val jsonElement = try {
JsonParser.parseString(input.toString())
} catch (e: JsonSyntaxException) {
Logger.warn("Json structure is invalid so it can not be formatted", e)
return SpannableStringBuilder.valueOf(input)
}
return SpannableStringBuilder().also {
printifyRecursive(it, StringBuilder(""), jsonElement)
}
}
private fun printifyRecursive(
sb: SpannableStringBuilder,
currentIndent: StringBuilder,
transformedJson: JsonElement
) {
val indent = StringBuilder(currentIndent)
if (transformedJson.isJsonArray) {
printifyJsonArray(sb, indent, transformedJson)
}
if (transformedJson.isJsonObject) {
printifyJsonObject(sb, indent, transformedJson)
}
}

private fun printifyJsonArray(
sb: SpannableStringBuilder,
indent: StringBuilder,
transformedJson: JsonElement
) {
if (transformedJson.asJsonArray.isEmpty) {
sb.appendWithColor(
"[]",
jsonSignElementsColor
)
return
}
sb.appendWithColor("${indent}[\n", jsonSignElementsColor)
indent.append(" ")
for (index in 0 until transformedJson.asJsonArray.size()) {
val item = transformedJson.asJsonArray[index]
if (item.isJsonObject || item.isJsonArray)
printifyRecursive(sb, indent, item)
else {
sb.append(indent)
sb.appendJsonValue(item)
}
if (index != transformedJson.asJsonArray.size() - 1)
sb.appendWithColor(",", jsonSignElementsColor).append("\n")
}
val finalIndent = StringBuilder(indent.dropLast(2))
sb.appendWithColor("\n${finalIndent}]", jsonSignElementsColor)
}

private fun printifyJsonObject(
sb: SpannableStringBuilder,
indentBuilder: StringBuilder,
transformedJson: JsonElement
) {
if (transformedJson.asJsonObject.size() == 0) {
sb.appendWithColor(
"{}",
jsonSignElementsColor
)
return
}
sb.appendWithColor("${indentBuilder}{\n", jsonSignElementsColor)
indentBuilder.append(" ")
var index = 0
for (item in transformedJson.asJsonObject.entrySet()) {
sb.append(indentBuilder)
index++
sb.appendWithColor("\"${item.key}\"", jsonKeyColor)
.appendWithColor(":", jsonSignElementsColor)
if (item.value.isJsonObject || item.value.isJsonArray) {
sb.append(" ")
printifyRecursive(sb, indentBuilder, item.value)
} else sb.appendJsonValue(item.value)
if (index != transformedJson.asJsonObject.size())
sb.appendWithColor(",", jsonSignElementsColor).append("\n")
}
sb.appendWithColor("\n${indentBuilder.dropLast(2)}}", jsonSignElementsColor)
}

private fun SpannableStringBuilder.appendWithColor(text: CharSequence, color: Int):
SpannableStringBuilder {
this.append(
text, ChuckerForegroundColorSpan(color),
Spanned.SPAN_INCLUSIVE_INCLUSIVE
)
return this
}

private fun SpannableStringBuilder.appendJsonValue(jsonValue: JsonElement):
SpannableStringBuilder {
val isDigit = jsonValue.isJsonNull.not() &&
jsonValue.asString.isNotEmpty() &&
jsonValue.isJsonPrimitive &&
jsonValue.asString.isDigitsOnly()
val value = if (isDigit) jsonValue.asString else jsonValue.toString()
val color = if (isDigit || jsonValue.isJsonNull) jsonDigitsAndNullValueColor
else jsonValueColor
return this.appendWithColor(
" $value",
color
)
}

public class ChuckerForegroundColorSpan(color: Int) : ForegroundColorSpan(color)
}

Loading

0 comments on commit 3ae4d08

Please sign in to comment.