# cream.kt - Cross-Class Copy Function Generator cream.kt is a Kotlin Symbol Processing (KSP) plugin that automatically generates extension functions for copying objects between similar classes. It eliminates the boilerplate of manually copying shared properties during state transitions, making code more maintainable and less error-prone. The plugin is particularly valuable in scenarios like ViewModel state management (Loading → Success → Error) and cross-layer data model transformations (Data ↔ Domain ↔ Presentation). The library provides four core annotations: `@CopyTo` for forward copying, `@CopyFrom` for reverse copying, `@CopyToChildren` for sealed hierarchies, and `@CopyMapping` for third-party library classes. Each annotation triggers compile-time code generation that creates type-safe copy functions with automatic property matching. Properties with matching names are set as default parameters, while additional properties must be explicitly provided. ## Setup and Installation **Adding cream.kt to your Gradle project** ```kotlin // module/build.gradle.kts plugins { id("com.google.devtools.ksp") version "1.9.20-1.0.14" } dependencies { implementation("me.tbsten.cream:cream-runtime:1.0.0") ksp("me.tbsten.cream:cream-ksp:1.0.0") } ``` ## @CopyTo - Forward State Transitions **Generate copy functions from source class to specified targets** ```kotlin import me.tbsten.cream.CopyTo @CopyTo(UiState.Success::class, UiState.Error::class) sealed interface UiState { val userId: String val timestamp: Long data class Loading( override val userId: String, override val timestamp: Long, ) : UiState data class Success( override val userId: String, override val timestamp: Long, val data: List, ) : UiState data class Error( override val userId: String, override val timestamp: Long, val message: String, ) : UiState } // Generated functions: // fun UiState.Loading.copyToUiStateSuccess( // userId: String = this.userId, // timestamp: Long = this.timestamp, // data: List // ): UiState.Success // fun UiState.Loading.copyToUiStateError( // userId: String = this.userId, // timestamp: Long = this.timestamp, // message: String // ): UiState.Error // Usage in ViewModel: class MyViewModel : ViewModel() { private val _state = MutableStateFlow( UiState.Loading(userId = "user123", timestamp = System.currentTimeMillis()) ) val state: StateFlow = _state.asStateFlow() fun loadData() { viewModelScope.launch { try { val data = repository.fetchData() _state.update { currentState -> // Before cream.kt: // UiState.Success(userId = currentState.userId, timestamp = currentState.timestamp, data = data) // With cream.kt - shared properties automatically copied: (currentState as UiState.Loading).copyToUiStateSuccess(data = data) } } catch (e: Exception) { _state.update { currentState -> (currentState as UiState.Loading).copyToUiStateError(message = e.message ?: "Unknown error") } } } } } ``` ## @CopyFrom - Reverse Mapping **Generate copy functions from specified sources to target class** ```kotlin import me.tbsten.cream.CopyFrom // Data layer model (from repository/network) data class UserApiResponse( val id: String, val name: String, val email: String, val createdAt: Long, ) // Domain layer model @CopyFrom(UserApiResponse::class) data class User( val id: String, val name: String, val email: String, val createdAt: Long, ) // Generated function: // fun UserApiResponse.copyToUser( // id: String = this.id, // name: String = this.name, // email: String = this.email, // createdAt: Long = this.createdAt // ): User // Usage in Repository: class UserRepository(private val api: UserApi) { suspend fun getUser(userId: String): User { val response: UserApiResponse = api.fetchUser(userId) // Before cream.kt: // return User(id = response.id, name = response.name, email = response.email, createdAt = response.createdAt) // With cream.kt - clean transformation: return response.copyToUser() } suspend fun getUserWithOverride(userId: String, customName: String): User { val response: UserApiResponse = api.fetchUser(userId) // Override specific properties while keeping others: return response.copyToUser(name = customName) } } // Example with @CopyFrom.Exclude: Exclude specific source classes from mapping a property data class LegacyUser( val userId: String, val userName: String, val userEmail: String, ) data class ModernUser( val userId: String, val userName: String, val userEmail: String, ) @CopyFrom(LegacyUser::class, ModernUser::class) data class DomainUser( val userId: String, val userName: String, // This property won't have a default value from LegacyUser, but will from ModernUser @CopyFrom.Exclude("LegacyUser") val userEmail: String, ) // Generated functions: // fun LegacyUser.copyToDomainUser( // userId: String = this.userId, // userName: String = this.userName, // userEmail: String // No default from LegacyUser due to @Exclude // ): DomainUser // // fun ModernUser.copyToDomainUser( // userId: String = this.userId, // userName: String = this.userName, // userEmail: String = this.userEmail // Has default from ModernUser // ): DomainUser ``` ## @CopyToChildren - Sealed Class Hierarchy **Automatically generate copy functions to all direct children of a sealed interface** ```kotlin import me.tbsten.cream.CopyToChildren @CopyToChildren sealed interface PaymentState { val orderId: String val amount: Double data class Pending( override val orderId: String, override val amount: Double, ) : PaymentState data class Processing( override val orderId: String, override val amount: Double, val processorId: String, ) : PaymentState data class Completed( override val orderId: String, override val amount: Double, val transactionId: String, val completedAt: Long, ) : PaymentState data class Failed( override val orderId: String, override val amount: Double, val errorCode: String, val errorMessage: String, ) : PaymentState } // Generated functions: // fun PaymentState.copyToPaymentStatePending(...): PaymentState.Pending // fun PaymentState.copyToPaymentStateProcessing(...): PaymentState.Processing // fun PaymentState.copyToPaymentStateCompleted(...): PaymentState.Completed // fun PaymentState.copyToPaymentStateFailed(...): PaymentState.Failed // Usage in payment processor: class PaymentProcessor { fun processPayment(state: PaymentState.Pending): PaymentState { return try { val processorId = startProcessing() state.copyToPaymentStateProcessing(processorId = processorId) } catch (e: Exception) { state.copyToPaymentStateFailed( errorCode = "PROC_ERROR", errorMessage = e.message ?: "Processing failed" ) } } fun completePayment(state: PaymentState.Processing): PaymentState { val transactionId = finalizeTransaction() return state.copyToPaymentStateCompleted( transactionId = transactionId, completedAt = System.currentTimeMillis() ) } } ``` ## @CopyTo.Map and @CopyFrom.Map - Property Name Mapping **Map properties with different names between source and target** ```kotlin import me.tbsten.cream.CopyTo import me.tbsten.cream.CopyFrom // Example 1: Using @CopyTo.Map for forward mapping @CopyTo(DatabaseUser::class) data class DomainUser( @CopyTo.Map("user_id") val id: String, @CopyTo.Map("user_name") val name: String, @CopyTo.Map("email_address") val email: String, ) data class DatabaseUser( val user_id: String, val user_name: String, val email_address: String, ) // Generated function: // fun DomainUser.copyToDatabaseUser( // user_id: String = this.id, // id mapped to user_id // user_name: String = this.name, // name mapped to user_name // email_address: String = this.email // email mapped to email_address // ): DatabaseUser // Example 2: Using @CopyFrom.Map for reverse mapping data class ApiProduct( val product_id: String, val product_name: String, val price_cents: Int, ) @CopyFrom(ApiProduct::class) data class Product( @CopyFrom.Map("product_id") val id: String, @CopyFrom.Map("product_name") val name: String, @CopyFrom.Map("price_cents") val priceInCents: Int, ) // Generated function: // fun ApiProduct.copyToProduct( // id: String = this.product_id, // name: String = this.product_name, // priceInCents: Int = this.price_cents // ): Product // Usage in data layer: class ProductRepository(private val api: ProductApi, private val db: ProductDatabase) { suspend fun saveProduct(product: Product) { // Map domain model to database model with snake_case columns: val dbUser = product.copyToDatabaseUser() db.insert(dbUser) } suspend fun getProduct(id: String): Product { // Map API response to domain model: val apiProduct = api.fetchProduct(id) return apiProduct.copyToProduct() } } ``` ## @CopyMapping - Third-Party Library Integration **Generate copy functions between external library classes without modifying them** ```kotlin import me.tbsten.cream.CopyMapping // Library X model (cannot be modified - from external library) data class RetrofitUserResponse( val userId: String, val displayName: String, val avatarUrl: String, ) // Library Y model (cannot be modified - from another external library) data class RoomUserEntity( val userId: String, val displayName: String, val avatarUrl: String, val cachedAt: Long, ) // Your domain model data class User( val userId: String, val displayName: String, val avatarUrl: String, ) // Declare mappings in your own code: @CopyMapping(source = RetrofitUserResponse::class, target = User::class) @CopyMapping(source = User::class, target = RoomUserEntity::class) private object UserMappings // Generated functions: // fun RetrofitUserResponse.copyToUser(...): User // fun User.copyToRoomUserEntity(...): RoomUserEntity // Usage in repository with multiple libraries: class UserRepository( private val api: RetrofitApiService, private val db: RoomDatabase ) { suspend fun fetchAndCacheUser(userId: String): User { // Fetch from network (Retrofit library): val networkResponse: RetrofitUserResponse = api.getUser(userId) // Convert to domain model: val user: User = networkResponse.copyToUser() // Convert to database entity (Room library) and cache: val entity: RoomUserEntity = user.copyToRoomUserEntity( cachedAt = System.currentTimeMillis() ) db.userDao().insert(entity) return user } } // Example 2: Bidirectional mapping with canReverse parameter data class LibraryAModel(val data: String, val propA: Int) data class LibraryBModel(val data: String, val propB: String) @CopyMapping(source = LibraryAModel::class, target = LibraryBModel::class, canReverse = true) private object BidirectionalMapping // Generated functions (both directions): // fun LibraryAModel.copyToLibraryBModel(data: String = this.data, propB: String): LibraryBModel // fun LibraryBModel.copyToLibraryAModel(data: String = this.data, propA: Int): LibraryAModel class DataSynchronizer { fun syncAtoB(modelA: LibraryAModel): LibraryBModel { return modelA.copyToLibraryBModel(propB = modelA.propA.toString()) } fun syncBtoA(modelB: LibraryBModel): LibraryAModel { return modelB.copyToLibraryAModel(propA = modelB.propB.toIntOrNull() ?: 0) } } // Example 3: Property name mapping with properties parameter data class PersonDto( val fullName: String, val emailAddr: String, val age: Int, ) data class UserEntity( val name: String, val email: String, val age: Int, ) @CopyMapping( source = PersonDto::class, target = UserEntity::class, properties = [ CopyMapping.Map(source = "fullName", target = "name"), CopyMapping.Map(source = "emailAddr", target = "email") ] ) private object PersonUserMapping // Generated function: // fun PersonDto.copyToUserEntity( // name: String = this.fullName, // fullName mapped to name // email: String = this.emailAddr, // emailAddr mapped to email // age: Int = this.age // ): UserEntity // Usage with property mapping: class UserService { fun convertPerson(dto: PersonDto): UserEntity { // Properties are automatically mapped according to CopyMapping.Map annotations: return dto.copyToUserEntity() } fun convertPersonWithOverride(dto: PersonDto, customEmail: String): UserEntity { return dto.copyToUserEntity(email = customEmail) } } // Example 4: Bidirectional mapping with property mappings @CopyMapping( source = PersonDto::class, target = UserEntity::class, canReverse = true, properties = [ CopyMapping.Map(source = "fullName", target = "name"), CopyMapping.Map(source = "emailAddr", target = "email") ] ) private object BidirectionalPersonMapping // Generated functions with reversed property mappings: // Forward: fun PersonDto.copyToUserEntity(name: String = this.fullName, email: String = this.emailAddr, age: Int = this.age): UserEntity // Reverse: fun UserEntity.copyToPersonDto(fullName: String = this.name, emailAddr: String = this.email, age: Int = this.age): PersonDto ``` ## Configuration Options **Customize generated function naming conventions** ```kotlin // module/build.gradle.kts ksp { // Prefix for generated functions (default: "copyTo") arg("cream.copyFunNamePrefix", "to") // Naming strategy (default: "under-package") // Options: "under-package", "diff-parent", "simple-name", "full-name", "inner-name" arg("cream.copyFunNamingStrategy", "simple-name") // How to escape dots in class names (default: "lower-camel-case") // Options: "lower-camel-case", "replace-to-underscore", "pascal-case", "backquote" arg("cream.escapeDot", "replace-to-underscore") // Skip generating copy functions to objects (default: "false") arg("cream.notCopyToObject", "false") } // Example class to demonstrate naming strategies: package com.example.app @CopyTo(com.example.app.Feature.Module.Result::class) data class Source(val value: String) // With default settings (copyFunNamePrefix="copyTo", copyFunNamingStrategy="under-package", escapeDot="lower-camel-case"): // fun Source.copyToComExampleAppFeatureModuleResult(value: String = this.value): Result // With copyFunNamePrefix="to": // fun Source.toComExampleAppFeatureModuleResult(value: String = this.value): Result // With copyFunNamingStrategy="simple-name": // fun Source.copyToResult(value: String = this.value): Result // With copyFunNamingStrategy="diff-parent" (difference from parent package): // fun Source.copyToFeatureModuleResult(value: String = this.value): Result // With escapeDot="replace-to-underscore": // fun Source.copyTo_com_example_app_Feature_Module_Result(value: String = this.value): Result ``` ## Summary cream.kt streamlines Kotlin development by eliminating repetitive property copying code across similar classes. The primary use cases include managing complex state transitions in ViewModels (particularly with sealed interfaces representing Loading/Success/Error states), transforming data models across architectural layers (Data/Domain/Presentation), and bridging between third-party library models without modifying external code. Integration is straightforward: add the KSP plugin and dependencies, annotate your classes with `@CopyTo`, `@CopyFrom`, `@CopyToChildren`, or `@CopyMapping`, and the generated extension functions become available at compile time. The generated functions provide type safety, automatic property matching via default parameters, and support for property name mapping and customizable naming conventions. This approach reduces maintenance burden when adding or removing properties, improves code readability by highlighting only the changed properties, and prevents bugs from forgotten property copies during refactoring.