I try to implement a data driven test for a kotlin function that has several parameters with default values.
In my test definitions I'd like to be able to leave out any combination of arguments that have a default argument in the function declaration.
I don't see how that can work (without a separate branch for each combination of default values).
Maybe it is better explained by code:
import kotlin.test.assertEquals
fun foobalize(start: Int = 0, separator: String = "\t", convert: Boolean = false): Int {
return 0 // implementation omitted
}
data class TestSpec(
val start: Int? = null, // null should mean: Don't pass this argument to foobalize(), but use its default value
val separator: String? = null, // dito
val convert: Boolean? = null, // dito
val expectedResult: Int
)
fun testFoobalize(testSpec: TestSpec) {
// How to call foobalize here with values from TestSpec, but leave out parameters that are null,
// so that the defaults from the fopobalize() function declaration are used???
val actualResult = foobalize(start = testSpec.start)
assertEquals(testSpec.expectedResult, actualResult)
}
Is there some completely different way to do this?
Default parameters is a kotlin compiler feature, so there is no easy way to use it at runtime. It should be possible with reflection though.
IMO if you really want it this way, it's better to enhance the API with another method, which takes a data class as a parameter.
data classc Spec(val start: Int = 0, val separator: String = "\t", val convert: Boolean = false)
fun foobalize(start: Int, separator: String, convert: Boolean): Int {}
fun foobalize(spec: Spec) = foobalize(spec.start, spec.separator, spec.convert)
Related
I'm trying to create a function that returns any data class object setting it's property values with its property names (if all strings) without changing it's default values
I have an example on how it is:
Imagine this data class:
data class StudentProfile(
var fullName: String = "",
var mobilePhone: String = "",
var birthDate: String = "",
var email: String = ""
)
I want to keep this empty default values, but I wanted a generic function that should work for any class and returns (in this case) this:
return StudentProfile(
mobilePhone = "mobilePhone",
fullName = "fullName",
email = "email",
birthDate = "birthDate"
)
Is it possible?
This does sound like an X-Y problem (I can't imagine how it would be useful), but I thought it'd be fun to solve anyway.
I'm unclear about whether you want to replace default values or not (since you say you don't but your example does), so this example lets you choose.
Explanation: Make sure all of the constructor's parameters are Strings or optional (have defaults). Otherwise, this is impossible because non-String parameter values could not be specified. Then filter the parameters list to include only the ones we are setting to their own name, and associate them to their names to create a Map<KParameter, String> that we can pass to constructor.callBy.
fun <T: Any> produceWithPropertiesByOwnName(type: KClass<T>, overrideDefaults: Boolean): T {
val constructor = type.primaryConstructor!!
val parameters = constructor.parameters
if (!parameters.all { param -> param.type.classifier == String::class || param.isOptional }){
error("Class $type primary constructor has required non-String parameters.")
}
val valuesByParameter = parameters.filter { it.type.classifier == String::class && (!it.isOptional || overrideDefaults) }
.associateWith(KParameter::name)
return constructor.callBy(valuesByParameter)
}
I'm writing an annotation processor in Kotlin and I want to get types of constructor arguments.
Here is my annotated class
#SomeAnnotation
class MyClass(name: String, age: Int)
Can I extract the String and the Int types from annotatedElement.kotlinMetadata? Or should I just use the annotatedElement which is a TypeElement? How can I do this?
This is where I'm stucked:
val metadata = annotatedElement.kotlinMetadata as KotlinClassMetadata
val proto = metadata.data.classProto
val mainConstructor = proto.constructorList.find { it.isPrimary }
val parameters = annotatedElement.typeParameters
The typeParameters return 0 and I should have 2.
There are two mistakes in your code:
You are getting generic typeParameters instead of parameters.map { it.type }
You are trying to get constructor parameters, but you are using annotatedElement instead of mainConstructor
Here is how you can correct them:
mainConstructor?.parameters?.map { it.type }
Suppose I have two methods:
private fun method1(a: A): A {
return a.copy(v1 = null)
}
private fun method2(a: A): A {
return a.copy(v2 = null)
}
Can I write something like:
private fun commonMethod(a: A, variableToChange: String): A {
return a.copy($variableToChange = null)
}
Another words, can I use a variable to refer to a named argument?
If I understand correctly what you are trying to archive I would recommend to pass a setter to the method e.g.
fun <A> changer (a: A, setter: (a: A) -> Unit ) {
// do stuff
setter(a)
}
Is this what you are looking for?
A possible solution for this problem (with usage of reflection) is:
inline fun <reified T : Any> copyValues(a: T, values: Map<String, Any?>): T {
val function = a::class.functions.first { it.name == "copy" }
val parameters = function.parameters
return function.callBy(
values.map { (parameterName, value) ->
parameters.first { it.name == parameterName } to value
}.toMap() + (parameters.first() to a)
) as T
}
This works with all data classes and all classes that have a custom copy function with the same semantics (as long as the parameter names are not erased while compiling). In the first step the function reference of the copy method is searched (KFunction<*>). This object has two importent properties. The parameters property and the callBy function.
With the callBy function you can execute all function references with a map for the parameters. This map must contain a reference to the receiver object.
The parameters propery contains a collection of KProperty. They are needed as keys for the callBy map. The name can be used to find the right KProperty. If a function as a parameter that is not given in the map it uses the default value if available or throws an exception.
Be aware that this solution requires the full reflection library and therefore only works with Kotlin-JVM. It also ignores typechecking for the parameters and can easily lead to runtime exceptions.
You can use it like:
data class Person (
val name: String,
val age: Int,
val foo: Boolean
)
fun main() {
var p = Person("Bob", 18, false)
println(p)
p = copyValues(p, mapOf(
"name" to "Max",
"age" to 35,
"foo" to true
))
println(p)
}
// Person(name=Name, age=15, foo=false)
// Person(name=Max, age=35, foo=true)
I am trying to find a solution for a nice kotlin data class solution. I have already this:
data class Object(
var classMember: Boolean,
var otherClassMember: Boolean,
var example: Int = 0) {
fun set(block: Object.() -> kotlin.Unit): Object {
val copiedObject = this.copy()
copiedObject.apply {
block()
}
return copiedObject
}
fun touch(block: Object.() -> kotlin.Unit): Object {
return this.set {
classMember = true
otherClassMember = false
block() }
}
}
val test = Object(true,true,1)
val changedTest = test.touch { example = 2 }
the result of this method is that the changedTest object has classMember = true, otherClassMember = false and example = 2
The problem with this solution is, the class properties are not immutable with var declaration. Does somebody have an idea how to optimize my methods to change var to val?
val says that a variable can't change it's value after initialization at the definition point. Kotlin's generated copy method does not modify an existing copy after construction: this method actually uses retrieved values from an object, replaces these values with ones that provided in copy method (if any), and after that just constructs a new object using these values.
So, it is not possible to perform such an optimization if you are going to change object's state after construction.
If I understood what you want correctly, you can do
data class Object(
val classMember: Boolean,
val otherClassMember: Boolean,
val example: Int = 0) {
fun touch(example: Int = this.example): Object {
return copy(
classMember = true,
otherClassMember = false,
example = example)
}
}
val test = Object(true,true,1)
val changedTest = test.touch(example = 2)
Though you need to repeat parameters other than classMember and otherClassMember but without reflection you can't do better.
In java we can call a method aMethod(val1 , val2)
with classObject.aMethod(null_if_Val1_NotAvailable,val2)
And inside the Method i will check that if val1 is null
i will initialize that
How to do the same in Kotlin.
I tried but its saying Null Cannot be value of Non-null type
You can use default values:
fun aMethod(val1: String = "default value", val2: String)
You can now use the default method like so:
classObject.aMethod(val2 = "val2")
Try it this way
fun printHello(name: String?): Unit {
if (name != null)
println("Hello ${name}")
else
println("Hi there!")
// `return Unit` or `return` is optional
}
Return type as your wish.
Refrence from kotlin langauge function tutorial
? after the datatype allows you to have null. refer this link
Kotlin emphasis on null safety. If you want a property/return type/parameter/local variable to be null, you will have to use nullable types, by appending a question mark to the end of your type.
e.g.
Foo foo = null // doesn't compile
Foo? foo = null // compiles
fun foo(foo: Foo)
foo(null) // doesn't compile
fun bar(foo: Foo?)
bar(null) // compiles
fun foo(): Foo = null // doesn't compile
fun foo(): Foo? = null // compiles
So in order to make your code works, all you need is to specify that function aMethod takes a nullable type:
fun aMethod(val1: SomeType?, val2) {
//...
}
However, in kotlin, it's not suggested to use null here. We have a better way to deal with your situation. We use default parameters, as suggested by nhaarman.
fun aMethod(val1: SomeType = someDefaultValue, val2: OtherType) {
//...
}
aMethod(val2 = OtherType())