KStarbound/src/main/kotlin/ru/dbotthepony/kstarbound/json/JsonPatch.kt

151 lines
5.1 KiB
Kotlin

package ru.dbotthepony.kstarbound.json
import com.google.gson.JsonArray
import com.google.gson.JsonElement
import com.google.gson.JsonNull
import com.google.gson.JsonObject
import com.google.gson.JsonSyntaxException
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.gson.get
import ru.dbotthepony.kstarbound.IStarboundFile
import ru.dbotthepony.kstarbound.Starbound
enum class JsonPatch(val key: String) {
TEST("test") {
override fun apply(base: JsonElement, data: JsonObject): JsonElement {
val path = JsonPath.pointer(data.get("path").asString)
val value = data["value"] ?: JsonNull.INSTANCE
val inverse = data.get("inverse", false)
val get = path.find(base) ?: JsonNull.INSTANCE
if (inverse) {
if (value == JsonNull.INSTANCE && get != JsonNull.INSTANCE) {
throw JsonTestException("Expected $path to not contain anything")
} else if (value != JsonNull.INSTANCE && value == get) {
throw JsonTestException("Expected $path to not contain $value")
}
} else {
if (value == JsonNull.INSTANCE && get == JsonNull.INSTANCE) {
throw JsonTestException("Expected $path to contain anything")
} else if (value != JsonNull.INSTANCE && get == JsonNull.INSTANCE) {
throw JsonTestException("Expected $path to contain '$value', but found nothing")
} else if (value != JsonNull.INSTANCE && get != JsonNull.INSTANCE && value != get) {
var text = get.toString()
if (text.length > 40) {
text = text.substring(0, 40) + "..."
}
throw JsonTestException("Expected $path to contain $value, but found $text")
}
}
return base
}
},
REMOVE("remove") {
override fun apply(base: JsonElement, data: JsonObject): JsonElement {
return JsonPath.pointer(data.get("path").asString).remove(base).first
}
},
ADD("add") {
override fun apply(base: JsonElement, data: JsonObject): JsonElement {
val value = data["value"] ?: throw JsonSyntaxException("Missing 'value' to 'add' operation")
return JsonPath.pointer(data.get("path").asString).add(base, value)
}
},
REPLACE("replace") {
override fun apply(base: JsonElement, data: JsonObject): JsonElement {
val value = data["value"] ?: throw JsonSyntaxException("Missing 'value' to 'add' operation")
val path = JsonPath.pointer(data.get("path").asString)
return path.add(path.remove(base).first, value)
}
},
MOVE("move") {
override fun apply(base: JsonElement, data: JsonObject): JsonElement {
val from = JsonPath.pointer(data.get("from").asString)
val path = JsonPath.pointer(data.get("path").asString)
val (newBase, removed) = from.remove(base)
return path.add(newBase, removed)
}
},
COPY("copy") {
override fun apply(base: JsonElement, data: JsonObject): JsonElement {
val from = JsonPath.pointer(data.get("from").asString)
val path = JsonPath.pointer(data.get("path").asString)
return path.add(base, from.get(base))
}
};
abstract fun apply(base: JsonElement, data: JsonObject): JsonElement
class JsonTestException(message: String) : IllegalStateException(message)
companion object {
private val LOGGER = LogManager.getLogger()
fun apply(base: JsonElement, data: JsonObject): JsonElement {
val op = data["op"]?.asString ?: throw JsonSyntaxException("Missing 'op' in json patch operation")
val operation = entries.firstOrNull { it.key == op } ?: throw JsonSyntaxException("Unknown patch operation $op!")
return operation.apply(base, data)
}
@Suppress("NAME_SHADOWING")
fun apply(base: JsonElement, data: JsonArray, source: IStarboundFile?): JsonElement {
// original engine decides what to do based on first element encountered
// in patch array... let's not.
var base = base
for ((i, element) in data.withIndex()) {
if (element is JsonObject) {
try {
base = apply(base, element)
} catch (err: JsonTestException) {
LOGGER.debug("Test condition failed in {} at index {}: {}", source, i, err.message)
} catch (err: Throwable) {
LOGGER.error("Error while applying JSON patch from $source at index $i: ${err.message}")
}
} else if (element is JsonArray) {
for ((i2, sub) in element.withIndex()) {
try {
if (sub !is JsonObject)
throw JsonSyntaxException("Expected JSON Object as patch data, got $sub")
base = apply(base, sub)
} catch (err: JsonTestException) {
LOGGER.debug("Test condition failed in {} at index {} -> {}: {}", source, i, i2, err.message)
break
} catch (err: Throwable) {
LOGGER.error("Error while applying JSON patch from $source at index $i -> $i2: ${err.message}")
break
}
}
} else {
LOGGER.error("Unknown data in $source at index $i")
}
}
return base
}
@Suppress("NAME_SHADOWING")
fun apply(base: JsonElement, source: Collection<IStarboundFile>?): JsonElement {
source ?: return base
var base = base
for (patch in source) {
val read = Starbound.ELEMENTS_ADAPTER.read(patch.jsonReader())
if (read !is JsonArray) {
LOGGER.error("$patch root element is not an array")
} else {
base = apply(base, read, patch)
}
}
return base
}
}
}