How do I write Nested Generics in Kotlin's Sealed Classes - kotlin

I'm trying to come up with a data model that allows me to do the following:
Define a type of Task and change its Status
This Status can be either InProgress or Completed
In the case of a completed Status, I want to be able to add data, that is specific to the Task that was completed.
Initially, I came up with this data model:
sealed class Task<R : TaskResult> {
abstract val status: TaskStatus<R>
data class A(
val data: String,
override val status: TaskStatus<NoResult>,
) : Task<NoResult>()
data class B(
val data: String,
override val status: TaskStatus<TaskBResult>,
) : Task<TaskBResult>()
}
sealed class TaskStatus<R : TaskResult> {
object InProgress : TaskStatus<NoResult>()
data class Completed<R : TaskResult>(val result: R) : TaskStatus<R>()
}
sealed class TaskResult {
object NoResult : TaskResult()
data class TaskBResult(val resultData: String) : TaskResult()
}
Here you have Task.A and Task.B, where:
A completed Task.A only accepts NoResult
A completed Task.B only accepts TaskBResult
However, when I run this:
fun main() {
val taskA = Task.A(
data = "data",
status = TaskStatus.InProgress
).copy(
status = TaskStatus.Completed(
result = NoResult
)
)
val taskB = Task.B(
data = "data",
status = TaskStatus.InProgress
).copy(
status = TaskStatus.Completed(
result = TaskBResult(
resultData = "resultData"
)
)
)
}
I get the following compile error for setting the initial status of Task.B:
status = TaskStatus.InProgress
Type mismatch.
Required: TaskStatus<TaskResult.TaskBResult>
Found: TaskStatus.InProgress
Does anyone know how to change the data model so I'm allowed to run this (or a very similar) main function?

This could work with a very little change: just make TaskStatus a covariant generic class and make InProgress a TaskStatus<Nothing>. This is a typical strategy you can use when you have "special case" objects that represent no state. After this change, your code should compile:
sealed class Task<R : TaskResult> {
abstract val status: TaskStatus<R>
data class A(
val data: String,
override val status: TaskStatus<TaskResult.NoResult>,
) : Task<TaskResult.NoResult>()
data class B(
val data: String,
override val status: TaskStatus<TaskResult.TaskBResult>,
) : Task<TaskResult.TaskBResult>()
}
sealed class TaskStatus<out R : TaskResult> {
object InProgress : TaskStatus<Nothing>()
data class Completed<R : TaskResult>(val result: R) : TaskStatus<R>()
}
sealed class TaskResult {
object NoResult : TaskResult()
data class TaskBResult(val resultData: String) : TaskResult()
}
fun main() {
val taskA = Task.A(
data = "data",
status = TaskStatus.InProgress
).copy(
status = TaskStatus.Completed(
result = NoResult
)
)
val taskB = Task.B(
data = "data",
status = TaskStatus.InProgress
).copy(
status = TaskStatus.Completed(
result = TaskBResult(
resultData = "resultData"
)
)
)
}

Related

Kotlin annotation changing of target

I have a annotation AggregateId that could be set on method params and properties and that I will use to retrieve some id :
#Target(AnnotationTarget.PROPERTY, AnnotationTarget.VALUE_PARAMETER)
#Retention(AnnotationRetention.RUNTIME)
annotation class AggregateId
I wrote that test case :
data class Example(
#AggregateId
val id: UUID
)
class AggregateIdTests {
private val exUuid = UUID.fromString("bae30706-f949-4eb5-b091-d51a13ddc832")
#Test
fun test() {
val ex = Example(id = exUuid)
val id = resolve(ex)
Assertions.assertThat(id).isEqualTo(exUuid)
}
private fun resolve(target: Any): UUID? {
val prop = target::class.declaredMemberProperties.find {
it.findAnnotation<AggregateId>() != null
}
return prop?.getter?.call(target) as UUID?
}
}
That actually works.
But if I add this class in the code :
class TestClass {
fun aMethod(#AggregateId param: UUID) {
}
}
Suddently the AggregateId changes of target for the other class. Even though I didn't change the rest of the code. What is the explaination of this ?
(using kotlin 1.5)

How get data from dto?

I have this DTO in Kotlin
#JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
sealed class InspirationDTO {
abstract val id: Long
#JsonTypeName("DishOfTheDay")
data class DishOfTheDay(
#JsonProperty("dishOfTheDay")
val dishOfTheDay: DishOfTheDayDTO,
override val id: Long,
) : InspirationDTO()
#JsonTypeName("InspirationScreenContinuousSlider")
data class InspirationScreenContinuousSlider(
#JsonProperty("name")
val inspirationScreenContinuousSlider: InspirationScreenContinuousSliderDTO,
override val id: Long,
) : InspirationDTO()
#JsonTypeName("InspirationScreenLink")
data class InspirationScreenLink(
#JsonProperty("inspirationScreenLink")
val inspirationScreenLink: InspirationScreenLinkDTO,
override val id: Long,
) : InspirationDTO()
#JsonTypeName("InspirationScreenPagingSlider")
data class InspirationScreenPagingSlider(
#JsonProperty("inspirationScreenPagingSlider")
val inspirationScreenPagingSlider: InspirationScreenPagingSliderDTO,
override val id: Long,
) : InspirationDTO()
#JsonTypeName("InspirationScreenRecentlyViewedSlider")
data class InspirationScreenRecentlyViewedSlider(
#JsonProperty("inspirationScreenRecentlyViewedSlider")
val inspirationScreenRecentlyViewedSlider: InspirationScreenRecentlyViewedSliderDTO,
override val id: Long,
) : InspirationDTO()
#JsonTypeName("InspirationScreenTagsSlider")
data class InspirationScreenTagsSlider(
#JsonProperty("inspirationScreenTagsSlider")
val inspirationScreenTagsSlider: InspirationScreenTagsSliderDTO,
override val id: Long,
) : InspirationDTO()
}
in service I expect that DTO to save all data from it's classes
fun saveDishOfTheDay(inspirationDTO: InspirationDTO) = with(inspirationDTO) {
inspirationFacadeRepo.saveDish(DishOfTheDayEntity(id = id, title = inspirationDTO.DishofTheDay.title))
}
this part I need to get inspirationDTO.DishofTheDay.title
but inspirationDTO from this DTO can't see DishOfTheDay class and it's inner item title
like this
data class DishOfTheDayDTO(
val title: String,
)
so need to get all classes from that DTO and their fields to put in entity fields and save them.

Android Serialization Jackson deserialize using JsonTypeInfo

Good morning. I'm trying to figure out how to deserialize a parameter but I can't find the solution. In practice, the response JSON should populate or not a field based on the value I get from another type of field, which is numeric and can be 0 or 1.
In particular (seeing following serialization) in MyData class there is a property called "mutable_value" that can be 1 or 0. when is 1 I must serialize b_property, else I must serialzie c_property.
I'm trying to achieve it but I don't know where I can do my case if 0 or 1...
The class I serialize is the following:
data class ClassResponseData(
#JsonProperty("code")
val code: String,
#JsonProperty("data")
val data: MyData,
) {
#JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY,
visible = true,
property = , // <-------------what I must do here?
defaultImpl = MyData.DefaultValue::class
)
#JsonSubTypes(
JsonSubTypes.Type(
value = MyData.BType::class,
name = "new_domain"
),
JsonSubTypes.Type(
value = MyData.CType::class,
name = "new_domain"
)
)
sealed class MyData{
data class DefaultValue(
#JsonProperty("b_value")
val bValue: String,
#JsonProperty("c_value")
val cValue: String,
#JsonProperty("mutable_value")
val mutableValue: Int
)
data class BType(
#JsonProperty("b_value")
val bValue: String,
#JsonProperty("mutable_value")
val mutableValue: Int
) : MyData()
data class CType(
#JsonProperty("c_value")
val cValue: String,
#JsonProperty("mutable_value")
val mutableValue: Int
) : MyData()
}
}
What I'm doing wrong?

How to implement different states of object in Kotlin?

What's a compact and simple way to implement variants of object going through different states?
The complication is that for every state a new properties added to the same object.
A task executed and follow through states Created / Waiting / InProgress possible way would be
to define following classes
class Task(
val type: String,
val make_request: () -> Void
)
class TaskWaitingExecution(
val task: Task,
val retry_count: Int
)
class TaskInProgress(
val task: TaskWaitingExecution,
val request_id: Int
) {
val timer = bon.timer()
}
And progress with steps
val task = Task("some_request', {})
val waiting = TaskWaitingExecution(task, 1)
val in_progress = TaskInProgress(waiting, 10)
But it's not ideal as accessing say type property would end up in
in_progress.task.task.type
Sadly there's no way to use implements by inhreitance implement PreviousTask by task
as it's not working for classes.
Is there a better way to express such logic?
Turn it inside out and put the status as a property of the Task:
class Task(
val type: String,
val make_request: () -> Void,
var status: TaskStatus = null,
)
To store the status-specific fields, a sealed class is perfect:
sealed class TaskStatus;
class WaitingExecution : TaskStatus (
val retry_count: Int
)
class InProgress : TaskStatus (
val request_id: Int
) {
val timer = bon.timer()
}
Usage:
val task = Task("some_request', {})
task.status = WaitingExecution(1)
task.status = InProgress(10)
You can do it with val status as well, but then you'd need to create a new Task object each time its status changes.
It's been a while since I last Kotlin'ed so there might be syntax errors, but I hope the idea is clear.

Use Kotlin's data class in service-proxy of Vert.x

I'm trying to pass data class to the service-proxy of Vert.x like this:
data class Entity(val field: String)
#ProxyGen
#VertxGen
public interface DatabaseService {
DatabaseService createEntity(Entity entity, Handler<AsyncResult<Void>> resultHandler);
}
However, the service-proxy requires a DataObject as the parameter type.
Below are what I've tried so far.
First, I rewrite the data class as:
#DataObject
data class Entity(val field: String) {
constructor(json: JsonObject) : this(
json.getString("field")
)
fun toJson(): JsonObject = JsonObject.mapFrom(this)
}
Although this works, the code is redundant, so I tried the kapt with the following generator:
override fun process(annotations: Set<TypeElement>, roundEnv: RoundEnvironment): Boolean {
roundEnv.getElementsAnnotatedWith(ProxyDataObject::class.java).forEach { el ->
val className = el.simpleName.toString()
val pack = processingEnv.elementUtils.getPackageOf(el).toString()
val filename = "Proxy$className"
val classBuilder = TypeSpec.classBuilder(filename)
val primaryConstructorBuilder = FunSpec.constructorBuilder()
val secondaryConstructorBuilder = FunSpec.constructorBuilder().addParameter("json", JsonObject::class)
val secondaryConstructorCodeBlocks = mutableListOf<CodeBlock>()
el.enclosedElements.forEach {
if (it.kind == ElementKind.FIELD) {
val name = it.simpleName.toString()
val kClass = getClass(it) // get the corresponding Kotlin class
val jsonTypeName = getJsonTypeName(it) // get the corresponding type name in methods of JsonObject
classBuilder.addProperty(PropertySpec.builder(name, kClass).initializer(name).build())
primaryConstructorBuilder.addParameter(name, kClass)
secondaryConstructorCodeBlocks.add(CodeBlock.of("json.get$jsonTypeName(\"$name\")"))
}
}
secondaryConstructorBuilder.callThisConstructor(secondaryConstructorCodeBlocks)
classBuilder
.addAnnotation(DataObject::class)
.addModifiers(KModifier.DATA)
.primaryConstructor(primaryConstructorBuilder.build())
.addFunction(secondaryConstructorBuilder.build())
.addFunction(
FunSpec.builder("toJson").returns(JsonObject::class).addStatement("return JsonObject.mapFrom(this)").build()
)
val generatedFile = FileSpec.builder(pack, filename).addType(classBuilder.build()).build()
generatedFile.writeTo(processingEnv.filer)
}
return true
}
Then I can get the correct generated file by simply writing the original data class, but when I execute the building after cleaning, I still get the following error:
Could not generate model for DatabaseService#createEntity(ProxyEntity,io.vertx.core.Handler<io.vertx.core.AsyncResult<java.lang.Void>>): type ProxyEntity is not legal for use for a parameter in proxy
It seems that the generated annotation #DataObject is not processed.
So what should I do? Is there a better solution?