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

Dev #7

Merged
merged 5 commits into from
Aug 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
20 changes: 15 additions & 5 deletions .github/workflows/android.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
name: CI

on:
pull_request: { }
workflow_dispatch: { }
pull_request: {}
workflow_dispatch: {}
push:
branches:
- master
Expand All @@ -13,7 +13,7 @@ concurrency:
cancel-in-progress: true

jobs:
build:
build-and-test:
runs-on: ubuntu-latest

steps:
Expand All @@ -23,11 +23,21 @@ jobs:
- name: Set up JDK
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'corretto'
java-version: "17"
distribution: "corretto"

- name: Grant execute permission for gradlew
run: chmod +x gradlew

- name: Build with Gradle
run: ./gradlew build

- name: Run tests
run: ./gradlew test

- name: Upload test results
uses: actions/upload-artifact@v2
if: always()
with:
name: test-results
path: "**/build/test-results/test"
2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ appcompat = "1.7.0"
junit = "4.13.2"
junitVersion = "1.2.1"
espressoCore = "3.6.1"
robolectric = "4.8"

[libraries]
androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "coreKtx" }
Expand All @@ -18,6 +19,7 @@ androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "a
junit = { module = "junit:junit", version.ref = "junit" }
androidx-junit = { module = "androidx.test.ext:junit", version.ref = "junitVersion" }
androidx-espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "espressoCore" }
robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" }

ktlint = "com.pinterest.ktlint:ktlint-cli:1.3.1"

Expand Down
1 change: 1 addition & 0 deletions library/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ dependencies {
compileOnly(libs.androidx.appcompat)
// Testing dependencies
testImplementation(libs.junit)
testImplementation(libs.robolectric)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
}
115 changes: 83 additions & 32 deletions library/src/main/java/com/ding1ding/jsbridge/JsonUtils.kt
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
@file:Suppress("ktlint:standard:max-line-length")

package com.ding1ding.jsbridge

import java.math.BigInteger
Expand All @@ -16,61 +18,85 @@ object JsonUtils {
fun toJson(any: Any?): String = when (any) {
null -> "null"
is JSONObject, is JSONArray -> any.toString()
is String -> JSONObject.quote(any)
is String -> JSONObject.quote(any) // Use JSONObject.quote to handle special characters correctly
is Boolean, is Number -> any.toString()
is Date -> JSONObject.quote(isoDateFormat.format(any))
is Map<*, *> -> any.toJsonObject().toString()
is Collection<*> -> any.toJsonArray().toString()
is Map<*, *> -> mapToJson(any)
is Collection<*> -> collectionToJson(any)
is Enum<*> -> JSONObject.quote(any.name)
else -> try {
any::class.java.declaredFields
.filter { !it.isSynthetic }
.associate { field ->
field.isAccessible = true
field.name to field.get(any)
}.toJsonObject().toString()
objectToJson(any)
} catch (e: Exception) {
Logger.e(e) { "Failed to serialize object of type ${any::class.java.simpleName}" }
JSONObject.quote(any.toString())
}
}

private fun Map<*, *>.toJsonObject() = JSONObject().apply {
forEach { (key, value) ->
put(key.toString(), value?.let { toJsonValue(it) } ?: JSONObject.NULL)
private fun mapToJson(map: Map<*, *>): String {
val jsonObject = JSONObject()
for ((key, value) in map) {
jsonObject.put(key.toString(), toJsonValue(value))
}
return jsonObject.toString()
}

private fun Collection<*>.toJsonArray() = JSONArray(
map {
it?.let { toJsonValue(it) }
?: JSONObject.NULL
},
)
private fun collectionToJson(collection: Collection<*>): String {
val jsonArray = JSONArray()
for (item in collection) {
jsonArray.put(toJsonValue(item))
}
return jsonArray.toString()
}

private fun toJsonValue(value: Any): Any = when (value) {
is JSONObject, is JSONArray, is String, is Boolean, is Number -> value
private fun objectToJson(obj: Any): String {
val jsonObject = JSONObject()
obj::class.java.declaredFields
.filter { !it.isSynthetic }
.forEach { field ->
field.isAccessible = true
jsonObject.put(field.name, toJsonValue(field.get(obj)))
}
return jsonObject.toString()
}

private fun toJsonValue(value: Any?): Any? = when (value) {
null -> JSONObject.NULL
is JSONObject, is JSONArray, is String, is Boolean, is Number, is Char -> value
is Map<*, *> -> JSONObject(mapToJson(value))
is Collection<*> -> JSONArray(collectionToJson(value))
is Date -> isoDateFormat.format(value)
is Enum<*> -> value.name
else -> toJson(value)
}

fun fromJson(json: String): Any? = try {
when {
json == "null" -> null
json.startsWith("{") && json.endsWith("}") -> JSONObject(json).toMap()
json.startsWith("[") && json.endsWith("]") -> JSONArray(json).toList()
json.startsWith("\"") && json.endsWith("\"") -> json.unquote().let { unquoted ->
tryParseDate(unquoted) ?: unquoted
json.startsWith("{") && json.endsWith("}") -> parseJsonObject(json)
json.startsWith("[") && json.endsWith("]") -> parseJsonArray(json)
json.startsWith("\"") && json.endsWith("\"") -> {
val unquoted = json.substring(1, json.length - 1)
tryParseDate(unquoted) ?: unescapeString(unquoted)
}

json == "true" -> true
json == "false" -> false
else -> parseNumber(json)
else -> parseNumber(json) ?: json
}
} catch (e: Exception) {
Logger.e(e) { "Error parsing JSON: $json" }
json // Return the original string if parsing fails
}

private fun parseJsonObject(json: String): Map<String, Any?> =
JSONObject(json).keys().asSequence().associateWith { key ->
fromJson(JSONObject(json).get(key).toString())
}

private fun parseJsonArray(json: String): List<Any?> = JSONArray(json).let { array ->
(0 until array.length()).map { fromJson(array.get(it).toString()) }
}

private fun JSONObject.toMap(): Map<String, Any?> = keys().asSequence().associateWith { key ->
when (val value = get(key)) {
is JSONObject -> value.toMap()
Expand All @@ -89,7 +115,7 @@ object JsonUtils {
}
}

private fun parseNumber(value: String): Any = when {
private fun parseNumber(value: String): Any? = when {
value.contains(".") || value.lowercase(Locale.US).contains("e") -> {
try {
val doubleValue = value.toDouble()
Expand All @@ -99,7 +125,7 @@ object JsonUtils {
else -> doubleValue
}
} catch (e: NumberFormatException) {
value
null
}
}

Expand All @@ -114,7 +140,7 @@ object JsonUtils {
try {
BigInteger(value)
} catch (e: NumberFormatException) {
value
null
}
}
}
Expand All @@ -126,9 +152,34 @@ object JsonUtils {
null
}

private fun String.unquote(): String = if (length >= 2 && startsWith('"') && endsWith('"')) {
substring(1, length - 1).replace("\\\"", "\"").replace("\\\\", "\\")
} else {
this
private fun unescapeString(s: String): String {
val sb = StringBuilder(s.length)
var i = 0
while (i < s.length) {
var ch = s[i]
if (ch == '\\' && i + 1 < s.length) {
ch = s[++i]
when (ch) {
'b' -> sb.append('\b')
'f' -> sb.append('\u000C')
'n' -> sb.append('\n')
'r' -> sb.append('\r')
't' -> sb.append('\t')
'u' -> {
if (i + 4 < s.length) {
val hex = s.substring(i + 1, i + 5)
sb.append(hex.toInt(16).toChar())
i += 4
}
}

else -> sb.append(ch)
}
} else {
sb.append(ch)
}
i++
}
return sb.toString()
}
}
Loading
Loading