Are you tired of creating three similar data classes just for the sake of following Clean Architecture principles? Are you not fed up with creating those mapping functions and clearing JSON annotations? Then you have to give a shot to this thing:
A KSP Processor that processes annotations and generates data classes for other two Clean Architecture layers using Kotlinpoet.
- Define your
DTOSchema
that you want to generate classes from and annotate it with@DTO
@DTO
data class ComputerDTOSchema(
@SerialName("motherboard")
val motherboard: MotherboardDTOSchema,
@SerialName("cpu")
val cpu: CpuDTOSchema,
@SerialName("isWorking")
val isWorking: Boolean
)
@DTO
data class MotherboardDTOSchema(
@SerialName("name")
val name: String,
)
@DTO
data class CpuDTOSchema(
@SerialName("name")
val name: String,
)
- See the result
public data class ComputerDTO(
@SerialName("motherboard")
public val motherboardDTO: MotherboardDTO,
@SerialName("cpu")
public val cpuDTO: CpuDTO,
@SerialName("isWorking")
public val isWorking: Boolean,
)
public fun ComputerDTO.toDomain(): ComputerModel = ComputerModel(motherboardDTO.toDomain(),
cpuDTO.toDomain(), isWorking
)
public data class ComputerModel(
public val motherboardModel: MotherboardModel,
public val cpuModel: CpuModel,
public val isWorking: Boolean,
)
public data class ComputerUI(
public val motherboardUI: MotherboardUI,
public val cpuUI: CpuUI,
public val isWorking: Boolean,
)
public fun ComputerModel.toUI(): ComputerUI = ComputerUI(motherboardModel.toUI(), cpuModel.toUI(),
isWorking
)
Tip
In case your @SerialName annotation value is the same as field name you can just skip adding @SerialName, processor will do it for you, so
@DTO
data class ComputerDTOSchema(
val motherboard: MotherboardDTOSchema,
val cpu: CpuDTOSchema,
val isWorking: Boolean
)
@DTO
data class MotherboardDTOSchema(
val name: String,
)
@DTO
data class CpuDTOSchema(
val name: String,
)
will produce the same:
public data class ComputerDTO(
@SerialName("motherboard")
public val motherboardDTO: MotherboardDTO,
@SerialName("cpu")
public val cpuDTO: CpuDTO,
@SerialName("isWorking")
public val isWorking: Boolean,
)
public fun ComputerDTO.toDomain(): ComputerModel = ComputerModel(motherboardDTO.toDomain(),
cpuDTO.toDomain(), isWorking
)
public data class ComputerModel(
public val motherboardModel: MotherboardModel,
public val cpuModel: CpuModel,
public val isWorking: Boolean,
)
public data class ComputerUI(
public val motherboardUI: MotherboardUI,
public val cpuUI: CpuUI,
public val isWorking: Boolean,
)
public fun ComputerModel.toUI(): ComputerUI = ComputerUI(motherboardModel.toUI(), cpuModel.toUI(),
isWorking
)
Generated classes can be found under build package:
build/
└── generated/
└── ksp/
└── main/
└── corp/
└── tbm/
└── cleanarchitecturemapper/
├── computer/
│ ├── dto/
│ │ └── ComputerDTO.kt
│ ├── model/
│ │ └── ComputerModel.kt
│ └── ui/
│ └── ComputerUI.kt
├── motherboard/
│ ├── dto/
│ │ └── MotherboardDTO.kt
│ ├── model/
│ │ └── MotherboardModel.kt
│ └── ui/
│ └── MotherboardUI.kt
└── cpu/
├── dto/
│ └── CpuDTO.kt
├── model/
│ └── CpuModel.kt
└── ui/
└── CpuUI.kt
Don't worry, top-level extension functions to map
are imported!
import corp.tbm.cleanarchitecturemapper.computer.model.ComputerModel
import corp.tbm.cleanarchitecturemapper.cpu.ui.CpuUI
import corp.tbm.cleanarchitecturemapper.cpu.ui.toUI
import corp.tbm.cleanarchitecturemapper.motherboard.ui.MotherboardUI
import corp.tbm.cleanarchitecturemapper.motherboard.ui.toUI
import kotlin.Boolean
public data class ComputerUI(
public val motherboardUI: MotherboardUI,
public val cpuUI: CpuUI,
public val isWorking: Boolean,
)
public fun ComputerModel.toUI(): ComputerUI = ComputerUI(motherboardModel.toUI(), cpuModel.toUI(),
isWorking
)
- If you would like to map to domain using some kind of interface, I got you:
@DTO(toDomainAsTopLevel = true)
data class ComputerDTOSchema(
val motherboard: MotherboardDTOSchema,
val cpu: CpuDTOSchema,
val isWorking: Boolean
)
It will produce the following output:
public data class ComputerDTO(
@SerialName("motherboard")
public val motherboardDTO: MotherboardDTO,
@SerialName("cpu")
public val cpuDTO: CpuDTO,
@SerialName("isWorking")
public val isWorking: Boolean,
) : DTOMapper<ComputerModel> {
override fun toDomain(): ComputerModel = ComputerModel(motherboardDTO.toDomain(),
cpuDTO.toDomain(), isWorking)
}
Clean Architecture Mapper is available via Maven Central
- Add the KSP Plugin
Note: The KSP version you choose directly depends on the Kotlin version your project utilize
You can check https://github.com/google/ksp/releases for the list of KSP versions, then select the latest release that is compatible with your Kotlin version. Example: If you're using1.9.22
Kotlin version, then the latest KSP version is1.9.22-1.0.17
.
Gradle (Groovy) - build.gradle(:module-name)
plugins {
id 'com.google.devtools.ksp' version '1.9.22-1.0.17'
}
Gradle (Kotlin) - build.gradle.kts(:module-name)
plugins {
id("com.google.devtools.ksp") version "1.9.22-1.0.17"
}
- Add dependencies
Gradle (Groovy) - build.gradle(:module-name)
dependencies {
implementation 'io.github.timbermir:clean-architecture-mapper:1.0.0-snapshot'
ksp 'io.github.timbermir:clean-architecture-mapper:1.0.0-snapshot'
}
Gradle (Kotlin) - build.gradle.kts(:module-name)
dependencies {
implementation("io.github.timbermir:clean-architecture-mapper:1.0.0-snapshot")
ksp("io.github.timbermir:clean-architecture-mapper:1.0.0-snapshot")
}
- SUPPORTS data class generation only in a single module, in other words you can't generate
DTO
s fordata
module, orModel
s fordomain module
, they are generated in module whereDTOSchema
is located - SUPPORTS only kotlinx-serialization-json
- DOES NOT support
enums
,collections
or any custom type but the source ones - DOES NOT support inheriting other annotations
- DOES NOT support inheriting
@SerialName
value if present, generated@SerialName
value is derived from field's name - DOES NOT support backwards mapping, i.e., from
model
toDTO
- DOES NOT
support custom processor options,
i.e.,
change
DTO
classes suffix toDto
- DOES NOT support multiplatform
- DOES NOT support Room entity generation, therefore
no
TypeConverters
generation - DOES NOT utilize Incremental processing
- DOES NOT utilize Multiple round processing