How to create compile-time constant in Kotlin from enum? - kotlin

I have an annotation that requires defaultValue to be compile-time constant. I take defaultValue from enum below:
enum class RaceType {
MARATHON,
SPRINT;
companion object {
fun apply(type: RaceType): RaceDto {
return when (type) {
MARATHON -> MarathonDto()
SPRINT -> SprintDto()
}
}
}
}
My dtos are the following:
interface RaceDto {
}
data class MarathonDto: RaceDto
data class SprintDto: RaceDto
when I use annotation #QraphQLArgument(defaultValue = RaceType.SPRINT.name) Kotlin requires RaceType.SPRINT.name to be compile-time constant.
Annotation implementation itself:
#Retention(RetentionPolicy.RUNTIME)
#Target({ElementType.PARAMETER})
public #interface GraphQLArgument {
String NONE = "\n\t\t\n\t\t\n\ue000\ue001\ue002\n\t\t\t\t\n";
String NULL = "\n\t\t\n\t\t\n\ue000\ue001\ue002\ue003\n\t\t\t\t\n";
String name();
String description() default "";
String defaultValue() default "\n\t\t\n\t\t\n\ue000\ue001\ue002\n\t\t\t\t\n";
Class<? extends DefaultValueProvider> defaultValueProvider() default JsonDefaultValueProvider.class;
}
I looked through similar questions but don't see a way how it can be resolved. I also found article related to the topic but nothing worked so far.
Side note: I cannot change annotation since it is from the library and I cannot change the library as well.
To summarize, is there a way to make from enum compile-time constant in Kotlin to use in an annotation?

is there a way to make from enum compile-time constant in Kotlin to use in an annotation?
No, because formally enums aren't compile-time constants in Java.
However please consider the sealed classes:
sealed class RaceType {
object MARATHON: RaceType() {
const val name = "MARATHON" // copy-paste is required here until https://youtrack.jetbrains.com/issue/KT-16304
}
object SPRINT: RaceType()
companion object {
fun apply(type: RaceType): RaceDto {
return when (type) { // the check is in compile time, because of sealed class
MARATHON -> MarathonDto()
SPRINT -> SprintDto()
}
}
}
}
A little part of copy-paste is still required. Please vote on kotlin compiler bug or follow this thread.
However, as I understand, this doesn't solve your issue with #QraphQLArgument(defaultValue = RaceType.SPRINT.name) unfortunately, because the name of class is not the same with value. In the other words, with sealed classes you need to write code to convert input strings to them.

Related

Having trouble with type erasure

I have something like this :
import kotlin.reflect.KClass
class Quantity<T> {
/* ... */
}
class Field<T : Any> {
val type: KClass<T> get() = TODO("This is initialized, don't worry about implentation details, just know that fields know their type.")
fun initValue(value: T) {
/* Do something very useful */
}
/* Other methods */
class Template<T : Any> {
fun initFieldWithValue(value: T): Field<T> {
return Field<T>().apply {
this.initValue(value)
}
}
}
}
class ComponentClass(
val fieldsTemplates: Map<String, Field.Template<*>>
) {
inner class Instance(field: Map<String, Field<*>>)
fun new(fieldValues: Map<String, Quantity<*>>): Instance {
val fields = mutableMapOf<String, Field<*>>()
for ((fieldName, template) in fieldsTemplates) {
fields[fieldName] = fieldsTemplates
.getValue(fieldName)
.initFieldWithValue(fieldValues.getValue(fieldName) /* Here a type error */)
}
return Instance(fields)
}
}
As you might guess, this is intended to work as a 'runtime way' of creating classes that own fields (Field<T> class), each one possessing a typed value (represented by a Quantity<T>).
The problem is that this code won't compile due to the fact that the quantity retrieved from fieldValues when creating the different fields of the future Instance in the new method isn't guaranteed to be of the required type for the field it is stuffed into.
The problem is that I would need a check since filling a Field<Quantity<String>> with a Quantity<Int> is obviously not a good idea, but because of the type erasure I cannot ensure that the quantities passed in are of the good type.
Any idea ? One more thought : Fields know what their type is thanks to their type attribute, but unfortunately I can't do the same for the Quantity class...
Your initFieldWithValue function is enforcing the type of the parameter to match the type known by the Template/Field. But inside your new function, your Template is a Template<*> since you retrieve it from a collection where the values are of this type.
The point of generics is to enforce compile time checks so casting can be done safely and automatically under the hood. This is only useful when your type is known at compile time. In this case, the type is not known at compile time, so the generics are preventing your code from compiling. This is what generics are supposed to do: prevent code from compiling if the compiler cannot check that they types match.
If you want this code to compile, you should change initFieldWithValue so it doesn't enforce generics. You can instead manually check the type and throw an error or exit early if it's incorrect. It will be up to your code elsewhere to ensure you aren't mixing and matching types.
Here's an example of a version that would work. The type check it does requires the Kotlin reflection library. If you're targeting JVM only, you can use the Java Class.isAssignableFrom method instead to do this check.
class Template<T : Any> {
val type: KClass<T> get() = TODO()
/**
* #throws IllegalStateException if [value] is not of the same type
* as this Template's [type].
*/
fun initFieldWithValue(value: Any): Field<T> {
if (!value::class.isSubclassOf(type)) {
error("Invalid value type for Field type of $type")
}
return Field<T>().apply {
#Suppress("UNCHECKED_CAST") // we manually checked it above
initValue(value as T)
}
}
}

GSON Deserialization of subtypes in Kotlin

I'm not sure if this is a limitation, a bug or just bad use of GSON. I need to have a hierarchy of Kotlin objects (parent with various subtypes) and I need to deserialize them with GSON. The deserialized object has correct subtype but its field enumField is actually null.
First I thought this is because the field is passed to the "super" constructor but then I found out that "super" works well for string, just enum is broken.
See this example:
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.typeadapters.RuntimeTypeAdapterFactory
open class Parent(val stringField: String,
val enumField: EnumField) {
enum class EnumField {
SUBTYPE1,
SUBTYPE2,
SUBTYPE3
}
}
class Subtype1() : Parent("s1", EnumField.SUBTYPE1)
class Subtype2(stringField: String) : Parent(stringField, EnumField.SUBTYPE2)
class Subtype3(stringField: String, type: EnumField) : Parent(stringField, type)
val subtypeRAF = RuntimeTypeAdapterFactory.of(Parent::class.java, "enumField")
.registerSubtype(Subtype1::class.java, Parent.EnumField.SUBTYPE1.name)
.registerSubtype(Subtype2::class.java, Parent.EnumField.SUBTYPE2.name)
.registerSubtype(Subtype3::class.java, Parent.EnumField.SUBTYPE3.name)
fun main() {
val gson = GsonBuilder()
.registerTypeAdapterFactory(subtypeRAF)
.create()
serializeAndDeserialize(gson, Subtype1()) // this works (but not suitable)
serializeAndDeserialize(gson, Subtype2("s2")) // broken
serializeAndDeserialize(gson, Subtype3("s3", Parent.EnumField.SUBTYPE3)) // broken
}
private fun serializeAndDeserialize(gson: Gson, obj: Parent) {
println("-----------------------------------------")
val json = gson.toJson(obj)
println(json)
val obj = gson.fromJson(json, Parent::class.java)
println("stringField=${obj.stringField}, enumField=${obj.enumField}")
}
Any ideas how to achieve to deserialization of enumField?
(deps: com.google.code.gson:gson:2.8.5, org.danilopianini:gson-extras:0.2.1)
P.S.: Note that I have to use RuntimeAdapterFactory because I have subtypes with different set of fields (I did not do it in the example so it is easier to understand).
Gson requires constructors without arguments to work properly (see deep-dive into Gson code below). Gson constructs raw objects and then use reflection to populate fields with values.
So if you just add some argument-less dummy constructors to your classes that miss them, like this:
class Subtype1() : Parent("s1", EnumField.SUBTYPE1)
class Subtype2(stringField: String) : Parent(stringField, EnumField.SUBTYPE2) {
constructor() : this("")
}
class Subtype3(stringField: String, type: EnumField) : Parent(stringField, type) {
constructor() : this("", EnumField.SUBTYPE3)
}
you will get the expected output:
-----------------------------------------
{"stringField":"s1","enumField":"SUBTYPE1"}
stringField=s1, enumField=SUBTYPE1
-----------------------------------------
{"stringField":"s2","enumField":"SUBTYPE2"}
stringField=s2, enumField=SUBTYPE2
-----------------------------------------
{"stringField":"s3","enumField":"SUBTYPE3"}
stringField=s3, enumField=SUBTYPE3
Gson deep-dive
If you want to investigate the internals of Gson, a tip is to add an init { } block to Subtype1 since it works and then set a breakpoint there. After it is hit you can move up the call stack, step through code, set more breakpoints etc, to reveal the details of how Gson constructs objects.
By using this method, you can find the Gson internal class com.google.gson.internal.ConstructorConstructor and its method newDefaultConstructor(Class<? super T>) that has code like this (I have simplified for brevity):
final Constructor<? super T> constructor = rawType.getDeclaredConstructor(); // rawType is e.g. 'class Subtype3'
Object[] args = null;
return (T) constructor.newInstance(args);
i.e. it tries to construct an object via a constructor without arguments. In your case for Subtype2 and Subtype3, the code will result in a caught exception:
} catch (NoSuchMethodException e) { // java.lang.NoSuchMethodException: Subtype3.<init>()
return null; // set breakpoint here to see
}
i.e. your original code fails since Gson can't find constructors without arguments for Subtype2 and Subtype3.
In simple cases, the problem with missing argument-less constructors is worked around with the newUnsafeAllocator(Type, final Class<? super T>)-method in ConstructorConstructor, but with RuntimeTypeAdapterFactory that does not work correctly.
I may be missing something in what you're trying to achieve, but is it necessary to use the RuntimeTypeAdapterFactory? If we take out the line where we register that in the Gson builder, so that it reads
val gson = GsonBuilder()
.create()
Then the output returns the enum we would expect, which looks to be serialising / deserialising correctly. I.e. the output is:
-----------------------------------------
{"stringField":"s1","enumField":"SUBTYPE1"}
stringField=s1, enumField=SUBTYPE1
-----------------------------------------
{"stringField":"s2","enumField":"SUBTYPE2"}
stringField=s2, enumField=SUBTYPE2
-----------------------------------------
{"stringField":"s3","enumField":"SUBTYPE3"}
stringField=s3, enumField=SUBTYPE3
It also may be an idea to implement Serializable in Parent. i.e.
open class Parent(val stringField: String, val enumField: EnumField) : Serializable {
enum class EnumField {
SUBTYPE1,
SUBTYPE2,
SUBTYPE3
}
}
Try adding #SerializedName annotation to each enum.
enum class EnumField {
#SerializedName("subtype1")
SUBTYPE1,
#SerializedName("subtype2")
SUBTYPE2,
#SerializedName("subtype3")
SUBTYPE3
}

How to handle nullable generics with Java interop

I have a Java class that is out of my control, defined as:
public #interface ValueSource {
String[] strings() default {}
}
I am trying to use this class from a Kotlin file I control, like so:
class Thing {
#ValueSource(string = ["non-null", null])
fun performAction(value: String?) {
// Do stuff
}
}
I get a compiler error
Kotlin: Type inference failed. Expected type mismatch: inferred type is Array<String?> but Array<String> was expected.
I understand why the inferred type is Array<String?>, but why is the expected type not the same? Why is Kotlin interpreting the Java generic as String! rather than String?? And finally, is there a way to suppress the error?
Kotlin 1.2.61
This isn't a Kotlin issue - this code isn't valid either, because Java simply doesn't allow null values in annotation parameters:
public class Thing {
#ValueSource(strings = {"non-null", null}) // Error: Attribute value must be constant
void performAction(String value) {
// Do stuff
}
}
See this article and this question for more discussion on this.

Strange "Val cannot be reassigned" error when setting a property in Kotlin of a Java object

The strange thing is that my Kotlin code compiled fine before, when it looked like this in the java class Allocator:
public void setAllocMethod(#NotNull AllocMethod allocMethod) {
this.allocMethod = allocMethod;
}
but when I changed the java class' setter to this:
public void setAllocMethod(#Nullable AllocMethod allocMethod) {
this.allocMethod= allocMethod;
}
then when I compile the project, I get this Kotlin error in the kt file that calls the java object:
Val Cannot be Reassigned
allocator.allocMethod = DefaultAllocMethod() // kotlin code
also here is the java getter:
public #NotNull AllocMethod getAllocMethod() {
if (allocMethod == null) allocMethod = DefaultAllocMethod.newDefault();
return allocMethod;
}
DefaultAllocMethod is a java subclass of AllocMethod
allocator is of type Allocator, which is a java class that has the getter and setter described above.
Can anyone explain what is happening? thanks
Your setters's type #Nullable AllocMethod, which is Kotlin's AllocMethod?, does not match the getters type #NotNull AllocMethod, which is Kotlin's AllocMethod
What the error message means is that since the types do not match, only the getter is considered as a property. So from the Kotlin's point of view instead of a var allocMethod you have val allocMethod and fun setAllocMethod(...)
Remember that an AllocMethod? is an Any? and that an AllocMethod is an Any. That helps to understand why these getters and setters don't match up.

How to write a package-level static initializer in Kotlin?

A previous question shows how to put a static initializer inside a class using its companion object. I'm trying to find a way to add a static initializer at the package level, but it seems packages have no companion object.
// compiler error: Modifier 'companion' is not applicable inside 'file'
companion object { init { println("Loaded!") } }
fun main(args: Array<String>) { println("run!") }
I've tried other variations that might've made sense (init on its own, static), and I know as a workaround I can use a throwaway val as in
val static_init = {
println("ugly workaround")
}()
but is there a clean, official way to achieve the same result?
Edit: As #mfulton26's answer mentions, there is no such thing as a package-level function really in the JVM. Behind the scenes, the kotlin compiler is wrapping any free functions, including main in a class. I'm trying to add a static initializer to that class -- the class being generated by kotlin for the free functions declared in the file.
Currently there is no way to add code to the static constructor generated for Kotlin file classes, only top-level property initializers are getting there. This sounds like a feature request, so now there is an issue to track this: KT-13486 Package-level 'init' blocks
Another workaround is to place initialization in top-level private/internal object and reference that object in those functions that depend on the effect of that initialization. Objects are initialized lazily, when they are referenced first time.
fun dependsOnState(arg: Int) = State.run {
arg + value
}
private object State {
val value: Int
init {
value = 42
println("State was initialized")
}
}
As you mentioned, you need a property with something that would run on initialisation:
val x = run {
println("The package class has loaded")
}
I got around it by using a Backing Property on the top-level, under the Kotlin file. Kotlin Docs: Backing Properties
private var _table: Map<String, Int>? = null
public val table: Map<String, Int>
get() {
if (_table == null) {
_table = HashMap() // Type parameters are inferred
// .... some other initialising code here
}
return _table ?: throw AssertionError("Set to null by another thread")
}