Skip to content

Kotlin Symbol Processor that auto-generates DTO, Domain and UI models along with mapper functions for your Clean Architecture application

License

Notifications You must be signed in to change notification settings

Timbermir/clean-wizard

Repository files navigation

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:

Clean Architecture Mapper

A KSP Processor that processes annotations and generates data classes for other two Clean Architecture layers using Kotlinpoet.

Basic Usage

  1. 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,
)
  1. 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
)
  1. 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)
}

Setup 🧩

Clean Architecture Mapper is available via Maven Central

  1. 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 using 1.9.22 Kotlin version, then the latest KSP version is 1.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"
}
  1. 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")
}

Current Processor limitations 🚧

  • SUPPORTS data class generation only in a single module, in other words you can't generate DTOs for data module, or Models for domain module, they are generated in module where DTOSchema 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 to DTO
  • DOES NOT support custom processor options, i.e., change DTO classes suffix to Dto
  • 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