Clean Wizard is a KSP Processor
that processes annotations and generates classes for
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 motherboard: MotherboardDTO,
@SerialName("cpu")
public val cpu: CpuDTO,
@SerialName("isWorking")
public val isWorking: Boolean,
)
public fun ComputerDTO.toDomain(): ComputerModel = ComputerModel(
motherboard.toDomain(),
cpu.toDomain(), isWorking
)
public data class ComputerModel(
public val motherboard: MotherboardModel,
public val cpu: CpuModel,
public val isWorking: Boolean,
)
public data class ComputerUI(
public val motherboard: MotherboardUI,
public val cpu: CpuUI,
public val isWorking: Boolean,
)
public fun ComputerModel.toUI(): ComputerUI = ComputerUI(
motherboard.toUI(), cpu.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 motherboard: MotherboardDTO,
@SerialName("cpu")
public val cpu: CpuDTO,
@SerialName("isWorking")
public val isWorking: Boolean,
)
public fun ComputerDTO.toDomain(): ComputerModel = ComputerModel(
motherboard.toDomain(),
cpuDTO.toDomain(), isWorking
)
public data class ComputerModel(
public val motherboard: MotherboardModel,
public val cpu: CpuModel,
public val isWorking: Boolean,
)
public data class ComputerUI(
public val motherboard: MotherboardUI,
public val cpu: CpuUI,
public val isWorking: Boolean,
)
public fun ComputerModel.toUI(): ComputerUI = ComputerUI(
motherboard.toUI(), cpu.toUI(),
isWorking
)
Generated classes can be found under build package:
build/
└── generated/
└── ksp/
└── main/
└── corp/
└── tbm/
└── cleanwizard/
├── 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.cleanwizard.computer.model.ComputerModel
import corp.tbm.cleanwizard.cpu.ui.CpuUI
import corp.tbm.cleanwizard.cpu.ui.toUI
import corp.tbm.cleanwizard.motherboard.ui.MotherboardUI
import corp.tbm.cleanwizard.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 = false)
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 motherboard: MotherboardDTO,
@SerialName("cpu")
public val cpu: CpuDTO,
@SerialName("isWorking")
public val isWorking: Boolean,
) : DTOMapper<ComputerModel> {
override fun toDomain(): ComputerModel = ComputerModel(
motherboard.toDomain(),
cpu.toDomain(), isWorking
)
}
2.1 If your schema has lists, don't worry everything will be mapped
@DTO
data class ComputerDTOSchema(
@SerialName("motherboard")
val motherboard: MotherboardDTOSchema,
@SerialName("cpu")
val cpu: CpuDTOSchema,
@SerialName("ram")
val ram: List<RamDTOSchema>,
@SerialName("isWorking")
val isWorking: Boolean
)
@DTO
data class MotherboardDTOSchema(
@SerialName("name")
val name: String,
)
@DTO
data class CpuDTOSchema(
@SerialName("name")
val name: String,
)
@DTO
data class RamDTOSchema(
@SerialName("name")
val name: String,
val capacity: Int
)
public data class ComputerDTO(
@SerialName("motherboard")
public val motherboard: MotherboardDTO,
@SerialName("cpu")
public val cpu: CpuDTO,
@SerialName("ram")
public val ram: List<RamDTO>,
@SerialName("isWorking")
public val isWorking: Boolean,
)
public fun ComputerDTO.toDomain(): ComputerModel = ComputerModel(
motherboard.toDomain(),
cpu.toDomain(),
ram.map { ramDTO -> ramDTO.toDomain() },
isWorking
)
...
- You are able to generate enums, however, with only one parameter due to Kotlin annotations limitations. You are not able to use your custom predefined enum, see this issue for details.
@DTO
data class ComputerDTOSchema(
@SerialName("motherboard")
val motherboard: MotherboardDTOSchema,
@SerialName("cpu")
val cpu: CpuDTOSchema,
@SerialName("ram")
val ram: List<RamDTOSchema>,
@SerialName("isWorking")
@IntEnum(
enumName = "ComputerStatus",
parameterName = "status",
enumEntries = ["NO_POWER", "DISPLAY_NOT_WORKING", "WORKING", "CPU_PROBLEMS"],
enumEntryValues = [1, 2, 3, 4]
)
val isWorking: Int
)
build/generated/org.orgname/projectname/computer/model/enums/ComputerStatus.kt
public enum class ComputerStatus(
public val status: Int,
) {
NO_POWER(status = 1),
DISPLAY_NOT_WORKING(status = 2),
WORKING(status = 3),
CPU_PROBLEMS(status = 4),
;
}
public data class ComputerDTO(
@SerialName("motherboard")
public val motherboard: MotherboardDTO,
@SerialName("cpu")
public val cpu: CpuDTO,
@SerialName("ram")
public val ram: List<RamDTO>,
@SerialName("isWorking")
public val isWorking: ComputerStatus,
)
...
public data class ComputerModel(
@SerialName("motherboard")
public val motherboard: MotherboardModel,
@SerialName("cpu")
public val cpu: CpuModel,
@SerialName("ram")
public val ram: List<RamModel>,
@SerialName("isWorking")
public val isWorking: ComputerStatus,
)
public data class ComputerUI(
@SerialName("motherboard")
public val motherboard: MotherboardUI,
@SerialName("cpu")
public val cpu: CpuUI,
@SerialName("ram")
public val ram: List<RamUI>,
@SerialName("isWorking")
public val isWorking: ComputerStatus,
)
1.1 enumName
and parameterName
properties can be omitted. Property name will be used instead
ComputerDTOSchema.kt
@DTO
data class ComputerDTOSchema(
@SerialName("motherboard")
val motherboard: MotherboardDTOSchema,
@SerialName("cpu")
val cpu: CpuDTOSchema,
@SerialName("ram")
val ram: List<RamDTOSchema>,
@SerialName("isWorking")
@IntEnum(
enumEntries = ["NO_POWER", "DISPLAY_NOT_WORKING", "WORKING", "CPU_PROBLEMS"],
enumEntryValues = [1, 2, 3, 4]
)
val isComputerWorking: Int
)
build/generated/org.orgname/projectname/computer/model/enums/ComputerStatus
public enum class IsComputerWorking(
public val isComputerWorking: Int,
) {
NO_POWER(isComputerWorking = 1),
DISPLAY_NOT_WORKING(isComputerWorking = 2),
WORKING(isComputerWorking = 3),
CPU_PROBLEMS(isComputerWorking = 4),
;
}
You can see all the available enums available for generation here
2.0 Let's imagine that you want to change the suffix of the DTO classes from DTO
to Dto
.
Using ksp
extension's arg("KEY", "value")
is not type-safe and map-based,
so making mistake in a key is not uncommon.
For this case, clean-wizard
introduces the custom extension for passing processor options.
You need to apply clean-wizard
plugin to your root build.gradle.kts
Gradle (Groovy) - build.gradle(:project-name)
plugins {
id 'io.github.timbermir.clean-wizard' version '1.0.0'
}
Gradle (Kotlin) - build.gradle.kts(:project-name)
plugins {
id("io.github.timbermir.clean-wizard") version "1.0.0"
}
2.1
Use `clean-wizard`
extension in your root build.gradle.kts
and change the suffix
`clean-wizard` {
data {
classSuffix = "DTO"
}
}
2.2 See the result
build/generated/org.orgname/projectname/computer/dto/ComputerDto.kt
public data class ComputerDto(
@SerialName("motherboard")
public val motherboard: MotherboardDto,
@SerialName("cpu")
public val cpu: CpuDto,
@SerialName("ram")
public val ram: List<RamDto>,
@SerialName("isComputerWorking")
public val isComputerWorking: IsComputerWorking,
)
public fun ComputerDto.toModel(): ComputerDomain = ComputerDomain(
motherboard.toModel(),
cpu.toModel(),
ram.map { ramDto -> ramDto.toModel() },
isComputerWorking
)
...
Ready-to-use block with all the fields needed
`clean-wizard` {
jsonSerializer {
kotlinXSerialization {
json {
encodeDefaults = true
prettyPrint = true
explicitNulls = false
@OptIn(ExperimentalSerializationApi::class)
namingStrategy = JsonNamingStrategy.KebabCase
}
}
}
dataClassGenerationPattern = CleanWizardDataClassGenerationPattern.LAYER
dependencyInjection {
kodein {
useSimpleFunctions = true
binding = CleanWizardDependencyInjectionFramework.Kodein.KodeinBinding.Multiton()
}
}
data {
classSuffix = "DTO"
packageName = "dtos"
toDomainMapFunctionName = "toModel"
interfaceMapper {
className = "DTOMapper"
pathToModuleToGenerateInterfaceMapper = projects.workloads.core.dependencyProject.name
}
}
domain {
classSuffix = "Domain"
packageName = "models"
toDTOMapFunctionName = "fromDomain"
toUIMapFunctionName = "toUI"
useCase {
packageName = "useCase"
useCaseFunctionType = CleanWizardUseCaseFunctionType.CustomFunctionName("execute")
classSuffix = "UseCase"
}
}
presentation {
moduleName = "ui"
classSuffix = "Ui"
packageName = "uis"
shouldGenerate = true
toDomainMapFunctionName = "fromUI"
}
}
You can see the list of available options here
Clean Wizard 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-wizard:clean-wizard:1.0.0'
ksp 'io.github.timbermir.clean-wizard:data-class-compiler:1.0.0'
}
Gradle (Kotlin) - build.gradle.kts(:module-name)
dependencies {
implementation("io.github.timbermir.clean-wizard:clean-wizard:1.0.0")
ksp("io.github.timbermir.clean-wizard:data-class-compiler:1.0.0")
}
- (Optional) Apply
clean-wizard
plugin for custom processor options
Gradle (Groovy) - build.gradle(:project-name)
plugins {
id 'io.github.timbermir.clean-wizard' version '1.0.0'
}
Gradle (Kotlin) - build.gradle.kts(:project-name)
plugins {
id("io.github.timbermir.clean-wizard") version "1.0.0"
}
SUPPORTS data class generation only in a single module, in other words you can't generateDTO
s fordata
module, orModel
s fordomain module
, they are generated in module whereDTOSchema
is locatedSUPPORTS only kotlinx-serialization-jsonDOES NOT supportenums
,collections
or any custom type but the source onesDOES NOT support inheriting other annotationsDOES NOT support inheriting@SerialName
value if present, generated@SerialName
value is derived from field's nameDOES NOT support backwards mapping, i.e., frommodel
toDTO
DOES NOT support custom processor options, i.e., changeDTO
classes suffix toDto
- DOES NOT support multiplatform
DOES NOT support Room entity generation, therefore noTypeConverters
generation- DOES NOT utilize Incremental processing
DOES NOT utilize Multiple round processing
It is recommended to use the latest released version of IntelliJ IDEA (Community or Ultimate Edition). You can download IntelliJ IDEA here.
The project relies on Gradle
as its main build tool.
Currently used version is 8.9
.
IntelliJ will try to find it among the installed Gradle Versions or download it automatically if it
couldn't be found.
The project requires JDK 19 to build classes and to run tests. Gradle will try to find it among the installed JDKs or provision it automatically if it couldn't be found.
For local builds, you can use an earlier or later version of JDK if you don't have that version installed.
Specify
the version of this JDK with the jdk
property
in project-config.versions.toml
.
After that, Gradle
will download all dependencies the project depends on.
Run the processor via Main.kt
On Windows, you might need to add long paths setting to the repository:
git config core.longpaths true
The errors related to inline properties usage in build.gradle.kts
files can occur when IntelliJ IDEA cannot
resolve Target JVM Version
for Kotlin Compiler, causing it to fall back
to the default 1.8.
To resolve the errors, follow these steps
- Navigate to
Settings -> Build, Execution, Deployment -> Compiler -> Kotlin Compiler
- Set the
Target JVM Version
to match thejdk
property specified inproject-config.versions.toml
.
clean-wizard is distributed under the terms of the Apache License (Version 2.0). See the license for more information.