Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: docs
on:
push:
branches:
- main
workflow_dispatch:

jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v6
with:
python-version: 3.x

- name: Deploy docs
run: |
git config user.name 'github-actions[bot]' && git config user.email 'github-actions[bot]@users.noreply.github.com'
pip install --upgrade pip && pip install mkdocs-material mkdocs-material-extensions mkdocs-minify-plugin
mkdocs gh-deploy --force
6 changes: 3 additions & 3 deletions .github/workflows/release-prepare-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,13 @@ jobs:
run: |
git checkout -b "release/${{ github.event.inputs.version }}"

# Updates version numbers in Gradle Build files and README.md
- name: Update version in build files and README
# Updates version numbers in Gradle Build files and docs
- name: Update version in build files and docs
run: |
VERSION=${{ github.event.inputs.version }}
sed -i "s/^version = \".*\"/version = \"$VERSION\"/" automapper/processor/build.gradle.kts
sed -i "s/^version = \".*\"/version = \"$VERSION\"/" automapper/annotation/build.gradle.kts
sed -i -r "s/(io\.github\.jacksever\.automapper:[^:]*:)[0-9]+\.[0-9]+\.[0-9]+(.*)/\1$VERSION\2/g" README.md
sed -i -r "s/(io\.github\.jacksever\.automapper:[^:]*:)[0-9]+\.[0-9]+\.[0-9]+(.*)/\1$VERSION\2/g" docs/setup.md

# Commits the version changes and pushes the new release branch
- name: Commit and Push changes
Expand Down
328 changes: 2 additions & 326 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ Effortless, type-safe object-to-object mapping in Kotlin. Tired of writing boile

Kotlin AutoMapper uses KSP (Kotlin Symbol Processing) to generate extension functions that automatically map your `data`, `enum`, and `sealed` classes. No reflection, no runtime magic - just pure, fast, and safe generated code for all your targets.

**➡️ [View the full documentation for Setup, How to Use, and Advanced Usage](https://jacksever.github.io/kotlin-automapper/) ⬅️**

## Features

- **Kotlin Multiplatform First:** Works seamlessly across `JVM`, `iOS`, `JS`, `Wasm`, and other Kotlin targets.
Expand All @@ -23,332 +25,6 @@ Kotlin AutoMapper uses KSP (Kotlin Symbol Processing) to generate extension func
- **Visibility Control:** Generated code automatically inherits the visibility (`public` or `internal`) of your `@AutoMapperModule`.
- **`@OptIn` Propagation:** Automatically propagates `@OptIn` annotations from your custom converters to the generated functions, ensuring compile-time safety for experimental APIs.

## Setup

Ensure you have the `ksp` plugin applied in your module's `build.gradle.kts` file.

### For a Kotlin Multiplatform Project

```kotlin
kotlin {
sourceSets {
commonMain.dependencies {
// 1. Add the annotation dependency to commonMain
implementation("io.github.jacksever.automapper:annotation:0.8.0")
}
}
}

// 2. Apply the processor to the targets you need
dependencies {
add("kspJs", "io.github.jacksever.automapper:processor:0.8.0")
add("kspJvm", "io.github.jacksever.automapper:processor:0.8.0")
add("kspIosX64", "io.github.jacksever.automapper:processor:0.8.0")
// etc. for your other targets
}
```

### For an Android-Only (or JVM) Project

In a standard Android or JVM module, you can add the dependencies directly.

```kotlin
dependencies {
// Annotation dependency
implementation("io.github.jacksever.automapper:annotation:0.8.0")

// KSP processor
ksp("io.github.jacksever.automapper:processor:0.8.0")
}
```

## How to Use

Using the library is a simple three-step process:

### Step 1: Define Your Models

For a KMP project, define your models in `commonMain` so they are accessible from all targets.

### Step 2: Declare Your Mapping Intent

Create an interface (also in `commonMain`) and annotate it with `@AutoMapperModule`. Inside, define functions that describe *what* you want to map.

`src/commonMain/kotlin/com/example/mapper/MapperModule.kt`
```kotlin
import io.github.jacksever.automapper.annotation.AutoMapper
import io.github.jacksever.automapper.annotation.AutoMapperModule

// Making this interface internal will make the generated code internal
@AutoMapperModule
internal interface MapperModule {

// Data class mapping
@AutoMapper
fun userMapper(user: User): UserEntity

// Sealed class mapping
@AutoMapper
fun shapeMapper(shape: Shape): ShapeEntity

// Enum mapping
@AutoMapper
fun statusMapper(status: Status): StatusEntity
}
```

### Step 3: Build Your Project

Build your project (`./gradlew build`). KSP will automatically find your `MapperModule` and generate the necessary extension functions for each target.

### What Happens Under the Hood?

KSP generates extension functions for each mapping you defined. Here is what the generated code looks like:

**Data Class Mapping:**

`UserMapper.kt` (Generated)
```kotlin
package com.example.mapper

// Generated code is internal because the MapperModule is internal

/**
* Converts [User] to [UserEntity]
*/
internal fun User.asUserEntity(): UserEntity = UserEntity(
id = id,
name = name,
age = age,
)

/**
* Converts [UserEntity] to [User]
*/
internal fun UserEntity.asUser(): User = User(
id = id,
name = name,
age = age,
)
```

**Enum Mapping:**

`StatusMapper.kt` (Generated)
```kotlin
package com.example.mapper

/**
* Converts [Status] to [StatusEntity]
*/
internal fun Status.asStatusEntity(): StatusEntity = when (this) {
Status.ACTIVE -> StatusEntity.ACTIVE
Status.PENDING -> StatusEntity.PENDING
Status.INACTIVE -> StatusEntity.INACTIVE
}

/**
* Converts [StatusEntity] to [Status]
*/
internal fun StatusEntity.asStatus(): Status = when (this) {
StatusEntity.ACTIVE -> Status.ACTIVE
StatusEntity.PENDING -> Status.PENDING
StatusEntity.INACTIVE -> Status.INACTIVE
}
```

**Sealed Class Mapping:**

`ShapeMapper.kt` (Generated)
```kotlin
package com.example.mapper

/**
* Converts [Shape] to [ShapeEntity]
*/
internal fun Shape.asShapeEntity(): ShapeEntity = when (this) {
Shape.NoShape -> ShapeEntity.NoShape
is Shape.Square -> ShapeEntity.Square(side = side)
is Shape.Circle -> ShapeEntity.Circle(radius = radius)
}

/**
* Converts [ShapeEntity] to [Shape]
*/
internal fun ShapeEntity.asShape(): Shape = when (this) {
ShapeEntity.NoShape -> Shape.NoShape
is ShapeEntity.Square -> Shape.Square(side = side)
is ShapeEntity.Circle -> Shape.Circle(radius = radius)
}
```

### Step 4: Use the Generated Functions

You can now call the generated functions directly from your common or platform-specific code.

```kotlin
import com.example.mapper.asUserEntity

fun main() {
val domainUser = User(id = 1, name = "Jane Doe", age = 28)

// Convert to entity
val entity = domainUser.asUserEntity()
println("Entity: $entity")
}
```

## Advanced Usage

### Unidirectional Mapping

If you only need a one-way mapping (e.g., from domain to UI), use the `reversible = false` flag.

```kotlin
@AutoMapperModule
interface MapperModule {

// Only `User.asUiUser()` will be generated
@AutoMapper(reversible = false)
fun userUiMapper(user: User): UiUser
}
```

### Customizing Mappings

AutoMapper provides powerful annotations to handle mismatches in property names or to provide default values when a source property is missing or `null`.

#### Renaming Properties with `@PropertyMapping`

Use the `propertyMappings` array to map properties that have different names between the source and target. Both `from` and `to` parameters are mandatory.

*Models:*
```kotlin
// Domain
data class User(val id: Long, val fullName: String)

// Data Layer
data class UserEntity(val userId: Long, val name: String)
```

*Mapping Definition:*
```kotlin
import io.github.jacksever.automapper.annotation.PropertyMapping

@AutoMapper(
propertyMappings = [
PropertyMapping(from = "id", to = "userId"),
PropertyMapping(from = "fullName", to = "name")
]
)
fun userMapper(user: User): UserEntity
```

This also works for renaming `enum` constants or `sealed` class implementations.

#### Providing Default Values with `@DefaultValue`

Use the `defaultValues` array to provide a fallback value for a target property. This is useful in two scenarios:
1. The source class does not have a corresponding property.
2. The source property is `nullable`, but the target property is not.

The processor will generate code that uses the provided value if the source property is missing or `null`.

*Models:*
```kotlin
enum class Role { ADMIN, GUEST }

// Domain: nullable address, no role
data class User(val id: Long, val address: String?)

// Data Layer: non-nullable address, has a role
data class UserEntity(val id: Long, val address: String, val role: Role)
```

*Mapping Definition:*
```kotlin
import io.github.jacksever.automapper.annotation.DefaultValue

@AutoMapper(
defaultValues = [
// Provides a value for 'role' (an enum) which is missing in the source
DefaultValue(property = "role", value = "Role.GUEST"),
// Provides a value for 'address' if it's null
DefaultValue(property = "address", value = "Unknown")
]
)
fun userMapper(user: User): UserEntity
```
The generated code will safely use the Elvis operator for `address` (`address = address ?: "Unknown"`) and will assign the enum constant for `role` (`role = Role.GUEST`).

The same approach works for `sealed class` objects. It's best to use the simple class name (e.g., `TaskState.Idle`), as the processor will automatically add the necessary imports.

### Custom Type Conversion with `@AutoConverter`

While AutoMapper handles primitive conversions (e.g., `Int` to `String`), you often need custom logic for complex types, like converting a `kotlin.uuid.Uuid` to a `String` or a `kotlin.time.Instant` to a `Long`. The `@AutoConverter` annotation is designed for this.

#### How It Works

1. **Define Converter Functions:** Create an `object` or `class` to hold your converter logic. Inside, create functions that take one parameter and return the converted type. Annotate these functions with `@AutoConverter`.

2. **Register the Converter Class:** Add your converter class to the `converters` array in either `@AutoMapperModule` (to make it available globally to all mappers in that module) or `@AutoMapper` (for a specific mapping).

*Models:*
```kotlin
import kotlin.uuid.Uuid
import kotlin.uuid.ExperimentalUuidApi

// Domain
@OptIn(ExperimentalUuidApi::class)
data class User(val id: Uuid)

// Data Layer
data class UserEntity(val id: String)
```

*Converter Definition:*
```kotlin
package com.example.converter

import io.github.jacksever.automapper.annotation.AutoConverter
import kotlin.uuid.ExperimentalUuidApi
import kotlin.uuid.Uuid

@OptIn(ExperimentalUuidApi::class)
object UuidConverter {

@AutoConverter
fun fromUuid(value: Uuid): String = value.toString()

@AutoConverter
fun toUuid(value: String): Uuid = Uuid.parse(uuidString = value)
}
```

*Mapping Definition:*
```kotlin
import com.example.converter.UuidConverter
import io.github.jacksever.automapper.annotation.AutoMapper
import io.github.jacksever.automapper.annotation.AutoMapperModule

@AutoMapperModule(
// Register the class containing the converters globally
converters = [UuidConverter::class]
)
interface MapperModule {

// The processor will find the correct converters for both directions
@AutoMapper(reversible = true)
fun userMapper(user: User): UserEntity
}
```

**Key Points:**

- **Reversible Mappings:** If you use `reversible = true`, you must provide converters for both directions (e.g., `Uuid -> String` and `String -> Uuid`) for the generated code to compile.
- **Converter Priority:** If you register a converter for the same type pair both globally (`@AutoMapperModule`) and locally (`@AutoMapper`), the **local converter will be used**, and the processor will issue a compile-time warning.
- **`@OptIn` Propagation:** If your converter function, or the class containing it, is marked with an `@OptIn` annotation (like `@OptIn(ExperimentalUuidApi::class)` in the example), the processor will automatically apply that same `@OptIn` annotation to the generated mapping function. This preserves compile-time safety and suppresses warnings in the generated code.

## License

This project is licensed under the Apache License, Version 2.0. See the [LICENSE](LICENSE) file for details.
Loading