Skip to content

[Question] How do I modify json continuously? #69

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

Open
SMFDrummer opened this issue Feb 11, 2025 · 5 comments
Open

[Question] How do I modify json continuously? #69

SMFDrummer opened this issue Feb 11, 2025 · 5 comments

Comments

@SMFDrummer
Copy link

I use this to modify my json, but I have a problem. I had never known Arrow before, how do I modify my json without declare so many variables?
Here is the issue I sended before. I don't want to declare so many variables, nor do I want to use let for multi-layer nesting. Is parZip works better? Please give me some choice.

@SMFDrummer
Copy link
Author

Maybe I can try this? And recently I want to learn how to use Arrow, Please give me some advice!

val result = data.run {
    JsonPath.path("sd.n").modify(this) { 
        JsonUnquotedLiteral(it.jsonPrimitive.content.ensureAscii().escaped().addQuotes())
    }.run {
        JsonPath.path("sd.lmz").modify(this) { 
            JsonUnquotedLiteral(it.jsonPrimitive.content.toBigDecimal().setScale(6).toString())
        }
    }.run {
        JsonPath.path("sd.rsd.wr").modify(this) { 
            JsonUnquotedLiteral(it.jsonPrimitive.content.toBigDecimal().setScale(6).toString())
        }
    }.run {
        JsonPath.path("sd.rsd.lr").modify(this) { 
            JsonUnquotedLiteral(it.jsonPrimitive.content.toBigDecimal().setScale(6).toString())
        }
    }
}

or

val result = data
    .apply { 
        JsonPath.path("sd.n").modify(this) { 
            JsonUnquotedLiteral(it.jsonPrimitive.content.ensureAscii().escaped().addQuotes())
        } 
    }
    .apply { 
        JsonPath.path("sd.lmz").modify(this) { 
            JsonUnquotedLiteral(it.jsonPrimitive.content.toBigDecimal().setScale(6).toString())
        } 
    }
    .apply { 
        JsonPath.path("sd.rsd.wr").modify(this) { 
            JsonUnquotedLiteral(it.jsonPrimitive.content.toBigDecimal().setScale(6).toString())
        } 
    }
    .apply { 
        JsonPath.path("sd.rsd.lr").modify(this) { 
            JsonUnquotedLiteral(it.jsonPrimitive.content.toBigDecimal().setScale(6).toString())
        } 
    }

println(Json.encodeToString(result) == origin) // true

But it still feels quite troublesome lol...

@nomisRev
Copy link
Owner

nomisRev commented May 7, 2025

Sorry for the late reply.

Have you tried using the copy { } block? API Docs copy.

val result = data.copy {
    JsonPath.path("sd.n").transform { JsonUnquotedLiteral(it.jsonPrimitive.content.ensureAscii().escaped().addQuotes()) } 
    JsonPath.path("sd.lmz").transform {  JsonUnquotedLiteral(it.jsonPrimitive.content.toBigDecimal().setScale(6).toString()) }
    JsonPath.path("sd.rsd.wr").transform { JsonUnquotedLiteral(it.jsonPrimitive.content.toBigDecimal().setScale(6).toString()) } 
    JsonPath.path("sd.rsd.lr").transform { JsonUnquotedLiteral(it.jsonPrimitive.content.toBigDecimal().setScale(6).toString()) } 
}

@SMFDrummer
Copy link
Author

Sorry for the late reply.

Have you tried using the copy { } block? API Docs copy* .

val result = data.copy {
JsonPath.path("sd.n").transform { JsonUnquotedLiteral(it.jsonPrimitive.content.ensureAscii().escaped().addQuotes()) }
JsonPath.path("sd.lmz").transform { JsonUnquotedLiteral(it.jsonPrimitive.content.toBigDecimal().setScale(6).toString()) }
JsonPath.path("sd.rsd.wr").transform { JsonUnquotedLiteral(it.jsonPrimitive.content.toBigDecimal().setScale(6).toString()) }
JsonPath.path("sd.rsd.lr").transform { JsonUnquotedLiteral(it.jsonPrimitive.content.toBigDecimal().setScale(6).toString()) }
}

Thanks for your reply, I've written these for a new DSL. Hope you enjoy it!

/**
 * Converts the current [JsonElement] into a [JsonUnquotedLiteral] decimal string with the specified precision.
 *
 * @param scale Number of decimal places to retain, defaults to 6.
 * @return A [JsonPrimitive] representing the unquoted JSON string.
 *
 * Example: `"3.1415926535"` -> `3.141593`
 */
fun JsonElement.toScaledLiteral(scale: Int = 6): JsonPrimitive =
    JsonUnquotedLiteral(this.jsonPrimitive.content.toBigDecimal().setScale(scale).toString())

/**
 * Wraps the current string with double quotes (`"`).
 */
private fun String.addQuotes(): String = "\"$this\""

/**
 * Encodes each character in the current string as `\\uXXXX`, ensuring it is ASCII-safe.
 */
private fun String.ensureAscii(): String = this.toCharArray().joinToString("") {
    "\\u" + it.code.toString(16).padStart(4, '0')
}

/**
 * Converts the current [JsonElement] into a fully ASCII-safe [JsonUnquotedLiteral] string and wraps it in quotes.
 *
 * Typically used for safely displaying or transmitting JSON field values over the network.
 */
fun JsonElement.toAsciiLiteral(): JsonPrimitive =
    JsonUnquotedLiteral(this.jsonPrimitive.content.ensureAscii().escaped().addQuotes())

/**
 * Modifies values at specified paths within the current [JsonElement] using a DSL-style block.
 *
 * Example:
 * ```kotlin
 * val modified = json.modify {
 *     path("a.b") { it.toScaledLiteral() }
 *     path("x.y") { it.toAsciiLiteral() }
 * }
 * ```
 *
 * @param block A configuration block that defines the paths and transformation logic.
 * @return A new [JsonElement] with the specified modifications applied.
 */
inline fun JsonElement.modify(block: JsonCopyBuilder.() -> Unit): JsonElement {
    val builder = JsonCopyBuilder(this)
    builder.block()
    return builder.build()
}

/**
 * A builder class for composing multiple JSON path-based transformations.
 *
 * @property original The initial [JsonElement] to transform.
 */
class JsonCopyBuilder(original: JsonElement) {
    private var current = original

    /**
     * Applies a transformation function to the value at the specified JSON [path].
     *
     * @param path A JSON path string (e.g., `"a.b[0].c"`).
     * @param transformer A function to transform the [JsonElement] located at the path.
     */
    fun path(path: String, transformer: (JsonElement) -> JsonElement) {
        current = JsonPath.path(path).modify(current, transformer)
    }

    /**
     * Returns the final [JsonElement] after all transformations have been applied.
     */
    fun build(): JsonElement = current
}

and use it for this:

val result = data.modify { // data must be a jsonElement
        path("sd.n") { it.toAsciiLiteral() }
        path("sd.lmz") { it.toScaledLiteral() }
        path("sd.rsd.wr") { it.toScaledLiteral() }
        path("sd.rsd.lr") { it.toScaledLiteral() }
    }

If you see this reply, you could close this issue. I have no permission.

@nomisRev
Copy link
Owner

Oh, I really do like your builder... Curious if we can enable it within the Copy block from Arrow-optics itself. I think something like this might work. This could replace your modify and JsonCopyBuilder boilerplate. Perhaps interesting to add in this library directly as syntactic sugar for integrating more easily in copy { }.

fun Copy.path(path: String, transformer: (JsonElement) -> JsonElement) {
   JsonPath.path(path).transform(transformer)
}

@SMFDrummer
Copy link
Author

Yeah, you're right, but Copy needs a recevier, so you need write it such like:

fun Copy<JsonElement>.path(path: String, transformer: (JsonElement) -> JsonElement) {
    JsonPath.path(path).transform(transformer)
}

then can use like:

val result = data.copy { // data must be a jsonElement
        path("sd.n") { it.toAsciiLiteral() }
        path("sd.lmz") { it.toScaledLiteral() }
        path("sd.rsd.wr") { it.toScaledLiteral() }
        path("sd.rsd.lr") { it.toScaledLiteral() }
    }

JsonObject is needed, but I think JsonElement is all right.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants