I've been trying to create a Kotlin DSL for creating GSON JsonObjects with a JSON-like syntax. My builder looks like this
import com.google.gson.JsonArray
import com.google.gson.JsonElement
import com.google.gson.JsonObject
import com.google.gson.JsonPrimitive
class JsonBuilder(builder: JsonBuilder.() -> Unit) {
init {
builder()
}
val result = JsonObject()
infix fun String.to(property: Number) = result.addProperty(this, property)
infix fun String.to(property: Char) = result.addProperty(this, property)
infix fun String.to(property: Boolean) = result.addProperty(this, property)
infix fun String.to(property: String) = result.addProperty(this, property)
infix fun String.to(property: JsonElement) = result.add(this, property)
infix fun String.to(properties: Collection<JsonElement>) {
val arr = JsonArray()
properties.forEach(arr::add)
result.add(this, arr)
}
operator fun String.invoke(builder: JsonObject.() -> Unit) {
val obj = JsonObject()
obj.builder()
result.add(this, obj)
}
}
fun json(builder: JsonBuilder.() -> Unit) = JsonBuilder(builder).result
And my test looks like this
fun main() {
val json = json {
"name" to "value"
"obj" {
"int" to 1
}
"true" to true
}
println(json)
}
However, upon execution it causes a NullPointerException pointing to the first String extension function used, which I don't find very descriptive as I don't see anything being nullable up to that point. Moreover, I don't see how it really differs from the regular execution which of course doesn't cause a NPE.
val json = JsonObject()
json.addProperty("name", "value")
val obj = JsonObject()
obj.addProperty("int", 1)
json.add("obj", obj)
json.addProperty("true", true)
My question is what's exactly causing the exception (and how to prevent it).
The issue is that you've specified the initialiser block earlier than the result object, causing it to be null when you come to use it - this can be visualised by the following (decompiled output of your code).
public JsonBuilder(#NotNull Function1 builder) {
Intrinsics.checkParameterIsNotNull(builder, "builder");
super();
builder.invoke(this);
this.result = new JsonObject();
}
Therefore, the solution is to move the declaration and initialisation of result earlier than the initialiser block.
class JsonBuilder(builder: JsonBuilder.() -> Unit) {
val result = JsonObject()
init {
builder()
}
// ...
}
And the result is now...
{"name":"value","int":1,"obj":{},"true":true}
EDIT: You'll also want to allow chaining with your DSL, and fix a bug you currently have.
operator fun String.invoke(builder: JsonBuilder.() -> Unit) {
val obj = JsonBuilder(builder).result
result.add(this, obj)
}
Which produces the correct result of
{"name":"value","obj":{"int":1},"true":true}
Related
We have a relatively simple builder pattern we use for test data generator in Kotlin.
The builders follow the pattern:
class ThingBuilder private constructor(
var param1: Int = 1,
var param2: Boolean = true
) {
private constructor(vararg inits: ThingBuilder.(ThingBuilder) -> Unit) : this() {
inits.forEach { it(this) }
}
fun build(): Thing {
return Thing(
param1,
param2
)
}
companion object {
fun asDefaultCase(init: ThingBuilder.(ThingBuilder) -> Unit = {}): ThingBuilder {
return ThingBuilder(init)
}
fun asSomethingElseCase(init: ThingBuilder.(ThingBuilder) -> Unit = {}): ThingBuilder {
return ThingBuilder({ b -> b.param2 = false }, init)
}
}
}
Here the Kotlin compiler reports a warning:
The expression is unused
which references the line:
inits.forEach { it(this) }
I've tried turning that into an Array<T> rather than varags but same warning occurs.
What would be the more correct way to make this structure where the consumers can pass in lambdas to configure the builder data?
(for reference, the code works correctly and the loop functions as expected)
This seems to be a rather old bug KT-21282 False positive UNUSED_EXPRESSION compiler warning with object and lambda with receiver / extension function type.
The fix is simple - just specify the explicit receiver and do this.it(this). I also don't see why you would need to pass this as both the receiver and the formal parameter to the block. I would just do this instead:
private constructor(vararg inits: ThingBuilder.() -> Unit) : this() {
inits.forEach { this.it() }
}
or:
private constructor(vararg inits: ThingBuilder.() -> Unit) : this() {
inits.forEach { it(this) }
}
Then you don't even need to write the b parameter in asSomethingElseCase:
fun asSomethingElseCase(init: ThingBuilder.() -> Unit = {}): ThingBuilder {
return ThingBuilder({ param2 = false }, init)
}
I've created a Kotlin equivalent of TypeReference<T> like so:
abstract class TypeReference<T> : Comparable<T> {
val type: Type get() = getGenericType()
val arguments: List<Type> get() = getTypeArguments()
final override fun compareTo(other: T): Int {
return 0
}
private fun getGenericType(): Type {
val superClass = javaClass.genericSuperclass
check(superClass !is Class<*>) {
"TypeReference constructed without actual type information."
}
return (superClass as ParameterizedType).actualTypeArguments[0]
}
private fun getTypeArguments(): List<Type> {
val type = getGenericType()
return if (type is ParameterizedType) {
type.actualTypeArguments.toList()
} else emptyList()
}
}
In order to obtain Class<*> of the generic type and its arguments, I've also created the following extension function (and this is where I believe the problem lies, since this is where the stack trace fails).
fun Type.toClass(): Class<*> = when (this) {
is ParameterizedType -> rawType.toClass()
is Class<*> -> this
else -> Class.forName(typeName)
}
I'm unit testing this like so:
#Test
fun `TypeReference should correctly identify the List of BigDecimal type`() {
// Arrange
val expected = List::class.java
val expectedParameter1 = BigDecimal::class.java
val typeReference = object : TypeReference<List<BigDecimal>>() {}
// Act
val actual = typeReference.type.toClass()
val actualParameter1 = typeReference.arguments[0].toClass()
// Assert
assertEquals(expected, actual)
assertEquals(expectedParameter1, actualParameter1)
}
The problem I think, lies in the extension function else -> Class.forName(typeName) as it throws:
java.lang.ClassNotFoundException: ? extends java.math.BigDecimal
Is there a better way to obtain the Class<*> of a Type, even when they're generic type parameters?
You need to add is WildcardType -> ... branch to your when-expression to handle types like ? extends java.math.BigDecimal (Kotlin equivalent is out java.math.BigDecimal), ?(Kotlin equivalent is *), ? super Integer(Kotlin equivalent is in java.math.Integer):
fun Type.toClass(): Class<*> = when (this) {
is ParameterizedType -> rawType.toClass()
is Class<*> -> this
is WildcardType -> upperBounds.singleOrNull()?.toClass() ?: Any::class.java
else -> Class.forName(typeName)
}
Note that in this implementation single upper bound types will be resolved as its upper bound, but all other wildcard types (including multiple upper bounds types) will be resolved as Class<Object>
https://github.com/pluses/ktypes
val typeReference = object : TypeReference<List<BigDecimal>>() {}
val superType = typeReference::class.createType().findSuperType(TypeReference::class)!!
println(superType.arguments.first())// List<java.math.BigDecimal>
println(superType.arguments.first().type?.arguments?.first())// java.math.BigDecimal
In Kotlin JVM 1.2.x, I use to be able to do the following:
inline fun <R> Logger.logStuff(
crossinline f: () -> R
): R {
val methodName = object {}.javaClass.enclosingMethod.name
try {
this.debug("$methodName : Begin")
f()
this.debug("$methodName : End")
} catch (ex: Exception) {
this.error("$methodName : Threw exception : $ex")
throw ex
}
}
class Foo {
fun doStuff() = log.logStuff {
1 + 3
}
}
This would give me logs like:
Foo : doStuff : Begin
Foo : doStuff : End
But, after upgrading to Kotlin 1.3.50 (from 1.2.x), I got logs like the following:
Foo : logStuff : Begin
Foo : logStuff : End
I am aware of currentThread().stackTrace[1].methodName to get the enclosing method name, but I was hoping to avoid that.
Is there another way to get the current function name?
You may convert your logStuff() fun to extension and make your R type reified, like this:
inline fun <reified R : Any> R.logStuff() {
val methodName = object {}.javaClass.enclosingMethod.name
}
Update 08/12/22:
What is the difference and what this solution gives us?
We may improve it just a little bit more to visualise it better:
inline fun <reified R : Any> R?.log(msg: String?) = this?.run {
val objectId: String = this::class.simpleName ?: this::class.hashCode().toString()
val methodName = object {}.javaClass.enclosingMethod?.name
Log.d("TAG", "$objectId.$methodName() {\n $msg\n}")
}
Now we may use it just like:
class ExampleClass {
fun exampleFun() {
log("test")
}
}
And it will give us an output like:
ExampleClass.exampleFun() {
test
}
Because log function is inlined, the code will be executed inside an "exampleFun()".
As you may see, there would be a "Current Function Name" in the output. Please also note, that in some cases class may not have a simpleName, that's why there is a fallback to hashCode.
Update 30/01/22:
If above doesn't work for you, please try this:
inline fun <reified R : Any> R?.log(msg: String?) = this?.run {
val objectId: String = this::class.simpleName ?: this::class.hashCode().toString()
val methodName = StackWalker.getInstance().walk { frames ->
frames.findFirst().map { it.methodName }.orElse(null)
}
println("$objectId.$methodName() {\n $msg\n}")
}
In javascript we can do something like this
function putritanjungsari(data){
console.log(data.name)
}
let data = {
name:"putri",
div:"m4th"
}
putritanjungsari(data)
In kotlin, i'am creating a function that accept an object as parameter then read it's properties later, how to do that in kotlin that targeting JVM?
If I understood your question correct, you are trying to have a variable that associates keys with some value or undefined(null in kt) if none are found. You are searching for a Map
If you don't know what types you want, you can make a map of type Any? So
Map<String, Any?>
Which is also nullable
Map<String, Any>
If you don't want nullables
Your code for example:
fun putritanjungsari(data: Map<String, Any?>){
print(data["name"])
}
val data: Map<String, Any?> =mapOf(
"name" to "putri",
"div" to "m4th"
)
putritanjungsari(data)
Note that you can't add new keys or edit any data here, the default map is immutable. There is MutableMap (which is implemented the same, only it has a method to put new data)
You can apply the property design pattern to solve your problem.
Here is its implementation in Kotlin:
interface DynamicProperty<T> {
fun cast(value: Any?): T
fun default(): T
companion object {
inline fun <reified T> fromDefaultSupplier(crossinline default: () -> T) =
object : DynamicProperty<T> {
override fun cast(value: Any?): T = value as T
override fun default(): T = default()
}
inline operator fun <reified T> invoke(default: T) = fromDefaultSupplier { default }
inline fun <reified T> required() = fromDefaultSupplier<T> {
throw IllegalStateException("DynamicProperty isn't initialized")
}
inline fun <reified T> nullable() = DynamicProperty<T?>(null)
}
}
operator fun <T> DynamicProperty<T>.invoke(value: T) = DynamicPropertyValue(this, value)
data class DynamicPropertyValue<T>(val property: DynamicProperty<T>, val value: T)
class DynamicObject(vararg properties: DynamicPropertyValue<*>) {
private val properties = HashMap<DynamicProperty<*>, Any?>().apply {
properties.forEach { put(it.property, it.value) }
}
operator fun <T> get(property: DynamicProperty<T>) =
if (properties.containsKey(property)) property.cast(properties[property])
else property.default()
operator fun <T> set(property: DynamicProperty<T>, value: T) = properties.put(property, value)
operator fun <T> DynamicProperty<T>.minus(value: T) = set(this, value)
}
fun dynamicObj(init: DynamicObject.() -> Unit) = DynamicObject().apply(init)
You can define your properties these ways:
val NAME = DynamicProperty.required<String>() // throws exceptions on usage before initialization
val DIV = DynamicProperty.nullable<String>() // has nullable type String?
val IS_ENABLED = DynamicProperty(true) // true by default
Now you can use them:
fun printObjName(obj: DynamicObject) {
println(obj[NAME])
}
val data = dynamicObj {
NAME - "putri"
DIV - "m4th"
}
printObjName(data)
// throws exception because name isn't initialized
printObjName(DynamicObject(DIV("m4th"), IS_ENABLED(false)))
Reasons to use DynamicObject instead of Map<String, Any?>:
Type-safety (NAME - 3 and NAME(true) will not compile)
No casting is required on properties usage
You can define what the program should do when a property isn't initialized
Kotlin is statically typed language, so it required a param type to be precisely defined or unambiguously inferred (Groovy, for instance, addresses the case by at least two ways). But for JS interoperability Kotlin offers dynamic type.
Meanwhile, in your particular case you can type data structure to kt's Map and do not argue with strict typing.
You have to use Any and after that, you have to cast your object, like this
private fun putritanjungsari(data : Any){
if(data is Mydata){
var data = data as? Mydata
data.name
}
}
Just for the sake of inspiration. In Kotlin, you can create ad hoc objects:
val adHoc = object {
var x = 1
var y = 2
}
println(adHoc.x + adHoc.y)
following situation: I try to implement a generic function, which checks if a list of variables are all not null and executes a lambda, which requires non-nullable variables.
I can chain multiple let-calls or implement multiple 'safeLet'-Function, with 2,3,4... arguments, but I still hope one generic function with a list is possible.
Here the current code, with chained let-calls:
val parameters = call.receiveParameters()
val firstName = parameters["firstName"]
val lastName = parameters["lastName"]
firstName?.let {
lastName?.let { userService.add(UserDTO(firstName = firstName, lastName = lastName)) }
}
Here is my current 'safeLet' function:
fun <T> List<Any?>.safeLet(block: () -> T) {
if(this.contains(null)) return
block()
}
But following still doesn't compile (because parameters of UserDTO are String and not String?):
listOf(firstName, lastName).safeLet {
userService.add(UserDTO(firstName = firstName, lastName = lastName))
}
I can add !! after firstName and lastName to avoid the nullcheck, but that's ugly.
My idea is to use kotlin contracts. Is something possible like this:
#ExperimentalContracts
fun <T> List<Any?>.safeLet(block: () -> T) {
contract {
returnsNotNull() implies {ALL ELEMENTS ARE NOT NULLABLE}
}
if(this.contains(null)) return
block()
}
Thanks in advance.
In relation to the "filterNotNull" comment i now tried this. Still not ideal, because I don't like to use this[0] and this[1] here, but it works:
allNotNull(firstName, lastName)?.apply {
userService.add(UserDTO(firstName = this[0], lastName = this[1]))
}
fun <T : Any> allNotNull(vararg elements: T?): List<T>? = if(elements.contains(null)) null else elements.filterNotNull()
You can use a binding function. It accepts another function inside of which you can use bind to transform nullable reference to not-null one.
If you pass a not-null argument to bind, it returns it. Otherwise, it suspends the execution of the binding block.
If the execution is suspended, binding returns null, otherwise it returns a result of the binding block.
Here is how you can use binding:
binding { userService.add(UserDTO(firstName = firstName.bind(), lastName = lastName.bind())) }
One more example:
fun sumOrNull(a: Int?, b: Int?): Int? = binding { a.bind() + b.bind() }
Here is my binding implementation:
// startCoroutineUninterceptedOrReturn returns either COROUTINE_SUSPENDED or R
#Suppress("UNCHECKED_CAST")
fun <R> binding(block: suspend Binder.() -> R): R? =
when (val result = block.startCoroutineUninterceptedOrReturn(Binder, BinderContinuation)) {
COROUTINE_SUSPENDED -> null
else -> result as R
}
#RestrictsSuspension
object Binder {
suspend fun <T> T?.bind(): T {
if (this != null) return this
suspendCoroutine<Nothing> {}
}
}
suspend fun <T> Binder.bind(obj: T?): T {
contract {
returns() implies (obj != null)
}
return obj.bind()
}
private object BinderContinuation : Continuation<Any?> {
override val context: CoroutineContext
get() = EmptyCoroutineContext
override fun resumeWith(result: Result<Any?>) {
result.getOrThrow()
}
}