Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add color to request and response body when content-type is json #930

Merged
merged 24 commits into from
Mar 4, 2023
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
bc92e2c
implement Spannable Body For Response
Nov 23, 2022
079b152
add some test case
Nov 27, 2022
cab7bfc
fix color issue
Nov 27, 2022
30ee6ed
rename test function
Nov 27, 2022
7cf8087
add spanned request body
Nov 27, 2022
3df30f5
Merge branch 'develop' of https://github.com/Amirhy/chucker into feat…
Nov 27, 2022
2d16c8d
handle adding and removing highlighting spans when search a word
Nov 28, 2022
9d56bd8
fix a bug when highlighting searched text and there is no header item
Nov 28, 2022
689f7f0
try to resolve detekt errors
Nov 29, 2022
86e0221
resolve detekt errors
Nov 29, 2022
a1b459e
resolve detekt errors
Nov 29, 2022
9cbe545
Merge branch 'ChuckerTeam:develop' into feat_colorful_body
Amirhy Dec 24, 2022
2e2bf8c
Merge branch 'develop' of https://github.com/Amirhy/chucker into feat…
Dec 24, 2022
ec23c91
refactor SpanTextUtil.kt
Dec 25, 2022
1943db4
remove bug fix codes from this branch
Dec 25, 2022
f39612f
fix indent issue
Dec 25, 2022
7e96474
Merge remote-tracking branch 'origin/feat_colorful_body' into feat_co…
Dec 25, 2022
3b77798
fix merge request comments
Feb 7, 2023
5596e2f
Merge branch 'develop' of https://github.com/Amirhy/chucker into feat…
Feb 15, 2023
aefcb9f
some improvement
Feb 15, 2023
bee1044
change test method's name
Feb 15, 2023
1982e83
Merge branch 'develop' of https://github.com/Amirhy/chucker into feat…
Feb 26, 2023
edb81b1
handle dark mode in json colors
Feb 28, 2023
c747060
Merge branch 'develop' of https://github.com/Amirhy/chucker into feat…
Feb 28, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -16,6 +16,7 @@ android {
minSdkVersion rootProject.minSdkVersion
consumerProguardFiles 'proguard-rules.pro'
resValue("string", "chucker_version", "$VERSION_NAME")
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}

kotlinOptions {
Expand Down Expand Up @@ -90,6 +91,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
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package com.chuckerteam.chucker

import android.annotation.SuppressLint
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.chuckerteam.chucker.internal.support.SpanTextUtil
import com.google.common.truth.Truth
import org.junit.Assert
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
public class SpanUtilTest {
@SuppressLint("CheckResult")
@Test
public fun json_can_have_null_value() {
Amirhy marked this conversation as resolved.
Show resolved Hide resolved
val parsedJson = SpanTextUtil.spanJson(
"""{ "field": null }"""
)
Assert.assertEquals(
parsedJson.toString(),
"""
{
"field": null
}
""".trimIndent()
)
}
@Test
public fun json_can_have_empty_fields() {
Amirhy marked this conversation as resolved.
Show resolved Hide resolved
val parsedJson = SpanTextUtil.spanJson(
"""{ "field": "" }"""
)

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

@Test
public fun json_can_be_invalid() {
Amirhy marked this conversation as resolved.
Show resolved Hide resolved
val parsedJson = SpanTextUtil.spanJson(
"""[{ "field": null }"""
)

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

@Test
public fun json_object_is_pretty_printed() {
Amirhy marked this conversation as resolved.
Show resolved Hide resolved
val parsedJson = SpanTextUtil.spanJson(
"""{ "field1": "something", "field2": "else" }"""
)

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

@Test
public fun json_array_is_pretty_printed() {
Amirhy marked this conversation as resolved.
Show resolved Hide resolved
val parsedJson = SpanTextUtil.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 @@ -4,13 +4,16 @@ package com.chuckerteam.chucker.internal.data.entity

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 +218,23 @@ 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?): CharSequence {
return when {
//TODO Implement Other Content Types
contentType.isNullOrBlank() -> body
contentType.contains("json", ignoreCase = true) -> SpanTextUtil.spanJson(body)
else -> formatBody(body.toString(), contentType)
}
}

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

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

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

fun getSpannedResponseBody(): CharSequence {
return responseBody?.let {
spanBody(it, responseContentType)
} ?: 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,132 @@
package com.chuckerteam.chucker.internal.support

import android.graphics.Color
import android.text.SpannableStringBuilder
import android.text.Spanned
import android.text.style.ForegroundColorSpan
import androidx.core.text.isDigitsOnly
import com.google.gson.JsonElement
import com.google.gson.JsonParser
import com.google.gson.JsonSyntaxException


public class SpanTextUtil {
public companion object {
private val JSON_KEY_COLOR = Color.parseColor("#8B0057")
private val JSON_STRING_VALUE_COLOR = Color.parseColor("#2F00FF")
private val JSON_DIGIT_AND_NULL_VALUE_COLOR = Color.parseColor("#E84B31")
private val JSON_SIGN_ELEMENTS_COLOR = Color.parseColor("#474747")
Amirhy marked this conversation as resolved.
Show resolved Hide resolved

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)
}
val sb = SpannableStringBuilder()
printifyRecursive(sb, StringBuilder(""), jsonElement)
return sb
Amirhy marked this conversation as resolved.
Show resolved Hide resolved
}

private fun printifyRecursive(
Amirhy marked this conversation as resolved.
Show resolved Hide resolved
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(
"[]",
JSON_SIGN_ELEMENTS_COLOR
)
return
}
sb.appendWithColor("${indent}[\n", JSON_SIGN_ELEMENTS_COLOR)
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(",", JSON_SIGN_ELEMENTS_COLOR).append("\n")
}
val finalIndent = StringBuilder(indent.dropLast(2))
sb.appendWithColor("\n${finalIndent}]", JSON_SIGN_ELEMENTS_COLOR)
}

private fun printifyJsonObject(
sb: SpannableStringBuilder,
indentBuilder: StringBuilder,
transformedJson: JsonElement
) {
if (transformedJson.asJsonObject.size() == 0) {
sb.appendWithColor(
"{}",
JSON_SIGN_ELEMENTS_COLOR
)
return
}
sb.appendWithColor("${indentBuilder}{\n", JSON_SIGN_ELEMENTS_COLOR)
indentBuilder.append(" ")
var index = 0
for (item in transformedJson.asJsonObject.entrySet()) {
sb.append(indentBuilder)
index++
sb.appendWithColor("\"${item.key}\"", JSON_KEY_COLOR)
.appendWithColor(":", JSON_SIGN_ELEMENTS_COLOR)
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(",", JSON_SIGN_ELEMENTS_COLOR).append("\n")
}
sb.appendWithColor("\n${indentBuilder.dropLast(2)}}", JSON_SIGN_ELEMENTS_COLOR)
}

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) JSON_DIGIT_AND_NULL_VALUE_COLOR
else JSON_STRING_VALUE_COLOR
return this.appendWithColor(
" $value",
color
)
}
}

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

Loading