org.http4k.format.ConfigurableMoshi.kt Maven / Gradle / Ivy
package org.http4k.format
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.JsonReader
import com.squareup.moshi.JsonWriter
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import okio.buffer
import okio.source
import org.http4k.core.Body
import org.http4k.core.ContentType
import org.http4k.core.ContentType.Companion.APPLICATION_JSON
import org.http4k.core.HttpMessage
import org.http4k.core.with
import org.http4k.format.StrictnessMode.FailOnUnknown
import org.http4k.format.StrictnessMode.Lenient
import org.http4k.lens.BiDiBodyLensSpec
import org.http4k.lens.BiDiMapping
import org.http4k.lens.BiDiWsMessageLensSpec
import org.http4k.lens.ContentNegotiation
import org.http4k.lens.ContentNegotiation.Companion.None
import org.http4k.lens.string
import org.http4k.websocket.WsMessage
import java.io.InputStream
import java.math.BigDecimal
import java.math.BigInteger
import kotlin.reflect.KClass
open class ConfigurableMoshi(
builder: Moshi.Builder,
override val defaultContentType: ContentType = APPLICATION_JSON,
private val strictness: StrictnessMode = Lenient
) : AutoMarshallingJson() {
private val moshi = builder.build()
private val objectAdapter = moshi.adapter(Any::class.java)
override fun MoshiNode.asPrettyJsonString(): String = objectAdapter.indent(" ").toJson(unwrap())
override fun MoshiNode.asCompactJsonString(): String = objectAdapter.toJson(unwrap())
override fun String.asJsonObject() = MoshiNode.wrap(objectAdapter.fromJson(this))
override fun >> LIST.asJsonObject() = MoshiObject(toMap())
override fun String?.asJsonValue() = if (this == null) MoshiNull else MoshiString(this)
override fun Int?.asJsonValue() = if (this == null) MoshiNull else MoshiInteger(toLong())
override fun Double?.asJsonValue() = if (this == null) MoshiNull else MoshiDecimal(this)
override fun Long?.asJsonValue() = if (this == null) MoshiNull else MoshiInteger(this)
override fun BigDecimal?.asJsonValue() = if (this == null) MoshiNull else MoshiDecimal(toDouble())
override fun BigInteger?.asJsonValue() = if (this == null) MoshiNull else MoshiInteger(toLong())
override fun Boolean?.asJsonValue() = if (this == null) MoshiNull else MoshiBoolean(this)
override fun > T.asJsonArray() = MoshiArray(toList())
override fun textValueOf(node: MoshiNode, name: String): String? = (node as? MoshiObject)
?.attributes?.get(name)
?.unwrap()?.toString()
override fun decimal(value: MoshiNode) = (value as MoshiDecimal).value.toBigDecimal()
override fun integer(value: MoshiNode) = ((value as MoshiInteger).value)
override fun bool(value: MoshiNode) = (value as MoshiBoolean).value
override fun text(value: MoshiNode) = when (value) {
is MoshiString -> value.value
is MoshiBoolean -> value.value.toString()
is MoshiInteger -> value.value.toString()
is MoshiDecimal -> value.value.toString()
is MoshiArray -> ""
is MoshiObject -> ""
MoshiNull -> "null"
}
override fun elements(value: MoshiNode) = (value as MoshiArray).elements
override fun fields(node: MoshiNode) = (node as? MoshiObject)
?.attributes
?.map { it.key to it.value }
?: emptyList()
override fun typeOf(value: MoshiNode) = when (value) {
is MoshiNull -> JsonType.Null
is MoshiObject -> JsonType.Object
is MoshiArray -> JsonType.Array
is MoshiInteger -> JsonType.Integer
is MoshiDecimal -> JsonType.Number
is MoshiString -> JsonType.String
is MoshiBoolean -> JsonType.Boolean
}
override fun asFormatString(input: Any): String = moshi.adapter(input.javaClass).toJson(input)
fun asJsonString(t: T, c: KClass): String = moshi.adapter(c.java).toJson(t)
override fun asA(input: String, target: KClass): T = adapterFor(target).fromJson(input)!!
private fun adapterFor(target: KClass) = when (strictness) {
Lenient -> moshi.adapter(target.java)
FailOnUnknown -> moshi.adapter(target.java).failOnUnknown()
}
override fun asA(input: InputStream, target: KClass): T = adapterFor(target).fromJson(
input.source().buffer()
)!!
/**
* Convenience function to write the object as JSON to the message body and set the content type.
*/
inline fun R.json(t: T): R = with(Body.auto().toLens() of t)
/**
* Convenience function to read an object as JSON from the message body.
*/
inline fun HttpMessage.json(): T = Body.auto().toLens()(this)
override fun asJsonObject(input: Any): MoshiNode = MoshiNode.wrap(objectAdapter.toJsonValue(input))
override fun asA(j: MoshiNode, target: KClass): T = adapterFor(target)
.fromJsonValue(j.unwrap())!!
inline fun asBiDiMapping() =
BiDiMapping({ asA(it, T::class) }, { asFormatString(it) })
inline fun Body.Companion.auto(
description: String? = null,
contentNegotiation: ContentNegotiation = None,
contentType: ContentType = defaultContentType
): BiDiBodyLensSpec =
autoBody(description, contentNegotiation, contentType)
inline fun autoBody(
description: String? = null,
contentNegotiation: ContentNegotiation = None,
contentType: ContentType = defaultContentType
): BiDiBodyLensSpec =
Body.string(contentType, description, contentNegotiation).map({ asA(it, T::class) }, {
asFormatString(it)
})
inline fun WsMessage.Companion.auto(): BiDiWsMessageLensSpec =
WsMessage.string().map({ it.asA(T::class) }, { asFormatString(it) })
}
fun Moshi.Builder.asConfigurable(
kotlinFactory: JsonAdapter.Factory = KotlinJsonAdapterFactory()
) = object : AutoMappingConfiguration {
override fun int(mapping: BiDiMapping) = adapter(mapping, { value(it) }, { nextInt() })
override fun long(mapping: BiDiMapping) =
adapter(mapping, { value(it) }, { nextLong() })
override fun double(mapping: BiDiMapping) =
adapter(mapping, { value(it) }, { nextDouble() })
override fun bigInteger(mapping: BiDiMapping) =
adapter(mapping, { value(it) }, { nextLong().toBigInteger() })
override fun bigDecimal(mapping: BiDiMapping) =
adapter(mapping, { value(it) }, { nextDouble().toBigDecimal() })
override fun boolean(mapping: BiDiMapping) =
adapter(mapping, { value(it) }, { nextBoolean() })
override fun text(mapping: BiDiMapping) =
adapter(mapping, { value(it) }, { nextString() })
private fun adapter(
mapping: BiDiMapping,
write: JsonWriter.(IN) -> Unit,
read: JsonReader.() -> IN
) =
apply {
add(mapping.clazz, object : JsonAdapter() {
override fun fromJson(reader: JsonReader) = mapping.invoke(reader.read())
override fun toJson(writer: JsonWriter, value: OUT?) {
value?.let { writer.write(mapping(it)) } ?: writer.nullValue()
}
}.nullSafe())
}
// add the Kotlin adapter last, as it will hjiack our custom mappings otherwise
override fun done() = this@asConfigurable
.add(Unit::class.java, UnitAdapter)
.addLast(kotlinFactory)
}
private object UnitAdapter : JsonAdapter() {
override fun fromJson(reader: JsonReader) {
reader.readJsonValue(); Unit
}
override fun toJson(writer: JsonWriter, value: Unit?) {
value?.let { writer.beginObject().endObject() } ?: writer.nullValue()
}
}
inline operator fun ConfigurableMoshi.invoke(msg: HttpMessage): T = autoBody().toLens()(msg)
inline operator fun ConfigurableMoshi.invoke(item: T) = autoBody().toLens().of(item)
© 2015 - 2024 Weber Informatics LLC | Privacy Policy