kmapx

Mapper en tiempo de compilación: errores de mapeo son errores de compilación; transforma tipos con converters calificados, colecciones, value classes, PATCH, modos embedded/contract, diagnósticos detallados.

JVM
iOS
macOS
Linux
Windows
Wasm
JS
GitHub stars0
Authorskuroxbyte
Dependents0
LicenseApache License 2.0
Creation date4 months ago

Last activity3 months ago
Latest release0.4.0 (3 months ago)

KMapX

Mapper Kotlin-first en compile-time: todo mapeo inseguro es un error de compilación. Core de dominio puro (hexagonal) + frontend KSP2 + backend KotlinPoet.

📚 Documentación completa: kuroxbyte.github.io/kmapx

Estado

Publicado en Maven Central (io.github.kuroxbyte:kmapx-*, versión 0.3.0, Apache-2.0). Motor completo: dos modos (embedded/contract), converters (globales, calificados, inyectados), colecciones, value classes, sealed y enums paralelos, PATCH por forma, bidireccional validado, rutas anidadas, profiles con herencia, integraciones Spring/Koin/serialization, reporte de cobertura y Kotlin Multiplatform (JVM/Android, JS, WasmJS, Native). Diagnósticos KMX001–KMX047, cada uno con factory y contrato verificado.

Novedad en 0.3.0: el modelo neutral de tipos (antes MType/TypeKind internos) se extrajo a genkit — base hexagonal reutilizable publicada aparte (io.github.kuroxbyte:genkit-model, kspkit). Ver CHANGELOG.md.

Instalación (JVM)

// build.gradle.kts
plugins {
    kotlin("jvm") version "2.1.21"
    id("com.google.devtools.ksp") version "2.1.21-2.0.1"
}
dependencies {
    implementation("io.github.kuroxbyte:kmapx-annotations:0.4.0")
    implementation("io.github.kuroxbyte:kmapx-runtime:0.4.0")   // solo para Converts<A,B> / Patch<T>
    ksp("io.github.kuroxbyte:kmapx-frontend-ksp:0.4.0")
}
# gradle.properties
ksp.useKSP2=true

Para Kotlin Multiplatform, KSP se declara por target — ver la guía multiplataforma.

Compilar desde el fuente

./gradlew build

Requisitos: JDK 17+.

KMapX se apoya en genkit — la base hexagonal compartida (modelo neutral de tipos + frontend KSP), publicada en Maven Central (io.github.kuroxbyte:genkit-model, kspkit). Se resuelve como dependencia normal; no necesitas clonarla para compilar. Para co-desarrollarla junto a KMapX, clónala como hermano (../genkit) y arranca con -Pkmapx.useGenkitSource=true: el composite build la sustituye por el código local. Por defecto está apagado, así genkit no se importa dentro de KMapX en el IDE.

Módulos

Módulo Rol
kmapx-annotations API pública del usuario. KMP, cero dependencias.
kmapx-runtime KMP, mínimo: la interfaz Converts (solo si se usa el aspecto converter de @MapField).
kmapx-core Modelo (MType, MappingPlan), motor, diagnósticos. Kotlin puro.
kmapx-spi SPI de extensión (experimental): puntos de extensión del processor, vía ServiceLoader.
kmapx-ext-jvm Pack de converters JVM (java.time, UUID, BigDecimal, URI…). Consumidor del SPI.
kmapx-adapter-reflect mclassOf<T>() para testear el core sin compilador. Solo tests.
kmapx-backend-codegen Materializa MappingPlan → .kt (KotlinPoet).
kmapx-frontend-ksp Processor KSP2: traduce símbolos, invoca al motor, emite o reporta KMX.
kmapx-architecture-tests Konsist: la frontera del core como test.
kmapx-integration-tests Proyecto KMP consumidor: consumo end-to-end en vivo.
kmapx-incremental-tests Gradle TestKit: incrementalidad de KSP contra un consumidor real.
kmapx-demo App JVM CRUD que reemplaza a MapStruct (./gradlew :kmapx-demo:run). Ver kmapx-demo/README.
kmapx-gradle-plugin Plugin id("io.github.kuroxbyte.kmapx"): aplica KSP y cablea todo. Ver kmapx-gradle-plugin/README.
kmapx-intellij-plugin Plugin de IntelliJ: el motor real en el editor (diagnósticos KMX, quick-fixes, navegación, materializar). Build STANDALONE: ./gradlew -p kmapx-intellij-plugin buildPlugin.

Documentación

  • Alcance actual (qué se puede y qué no, con el porqué): capacidades-y-limitaciones.md.
  • Ejemplos que COMBINAN funcionalidades, por modo (compilados por el harness): ejemplos-avanzados.md.
  • Guía de referencia por temas (estilo MapStruct): referencia.md · catálogo de diagnósticos GENERADO desde el código: diagnosticos.md.
  • Sitio: mkdocs serve (MkDocs Material; deploy automático a Pages) · API docs: ./gradlew :kmapx-annotations:dokkaGeneratePublicationHtml.
  • Desarrollo: regenerar snapshots con ./gradlew :kmapx-backend-codegen:test -Dkmapx.updateSnapshots=true y revisar el diff.

Uso (modo embedded)

@MapTo(PersonDto::class)                      // genera fun Person.toPersonDto(): PersonDto
@MapTo(PersonSummary::class, name = "asSummary") // repeatable; nombre custom opcional
data class Person(val name: String, val age: Int)

Todas las funciones de una clase van a <Source>Mappings.kt, en su mismo paquete, replicando su visibilidad (internal → internal). Colisión de nombres de función → error KMX013.

Construcción

Orden determinista: @MapConstructor → @MapFactory (companion o top-level) → constructor primario visible. Ambigüedad → KMX006; nada utilizable → KMX005. Las vars públicas de cuerpo se asignan post-construcción vía .also { }.

Modo contract — la interfaz @Mapper

@Mapper
interface PersonMapper { fun toDto(p: Person): PersonDto }
// genera: object PersonMapperImpl : PersonMapper — delega en Person.toPersonDto() si existe,
// o materializa el plan inline si no. Post-función opcional: afterToDto(source, result): result.

Null-safety

T? → T es error de compilación (KMX003) salvo estrategia explícita en el target:

data class Dto(
    @MapField(onNull = OnNull.LITERAL, default = "N/A") val nickname: String, // nickname ?: "N/A" (parseo tipado)
    @MapField(onNull = OnNull.THROW) val email: String,               // ?: throw IllegalArgumentException("email must not be null mapping Src -> Dto")
    @MapField(onNull = OnNull.UNSAFE) val legacy: String,          // legacy!! — el ÚNICO !! posible (test global lo garantiza)
)

Dos estrategias para el mismo campo son inexpresables (onNull es UN valor); @MapField duplicada → KMX037; LITERAL sin default → KMX038; default no parseable/tipo no soportado → KMX017; estrategia sin violación → warning KMX018.

Errores

Todos los errores de una pasada (nunca "arregla uno, descubre el siguiente"), señalando la línea del parámetro target, con "did you mean" (distancia ≤ 2, case-insensitive, hasta 2 candidatos) y formato canónico [KMXnnn] <ubicación> <problema>. Fix: <acción>. verificado por test de contrato.

Conversiones implícitas

Lista CERRADA: idéntico (genéricos incluidos), T → T?, wrap/unwrap de value class, y colecciones elemento a elemento en una sola pasada (addresses.map { it.toAddressDto() }, Set vía mapTo(mutableSetOf()), anidamiento con una lambda por nivel). Nada más: Long→Int (narrowing), enum→String, List→Set… → KMX004 con fix sugiriendo @Converter. El widening numérico SIN PÉRDIDA es automático (Int→Long, Float→Double…) y stdConverters = true suma las estándar (String↔UUID, String↔BigDecimal, Long↔Instant…).

Converters

@Converter fun instantToIso(value: Instant): String = value.toString()
// createdAt = instantToIso(createdAt) — función Kotlin normal, refactor-safe.
// Gana sobre toda otra resolución. Dos converters del mismo par → KMX009.
// Firma inválida (2+ params, Unit, suspend, receiver, no top-level) → KMX019.

Converters calificados

object ShortDate : Converts<LocalDate, String> { override fun convert(v: LocalDate) = v.format(SHORT) }
// elección POR CAMPO, por KClass (reemplaza @Named/qualifiedBy de MapStruct, sin strings):
data class EventDto(@MapField(converter = ShortDate::class) val startDate: String)
// emite ShortDate.convert(startDate); gana sobre @Converter global y mapper (paso 0).
// A/B no encajan → KMX027; el object no implementa Converts → KMX029; A == B → warning KMX031.

Dos sedes de configuración — dominio limpio

La config por campo (@MapField: from/converter/onNull) también vive sobre el método de un @Mapper, nombrando el destino con target = "...". Así el dominio y el DTO quedan sin una sola anotación (hexagonal/DDD). Ambas sedes producen el mismo plan; duplicar campo+método → warning KMX032.

@Mapper interface CustomerMapper {
    @MapField(target = "name", from = "firstName")
    @MapField(target = "registeredAt", converter = IsoInstant::class)
    fun toDto(c: Customer): CustomerDto     // Customer y CustomerDto: CERO anotaciones KMapX
}

Renombrado

data class Dto(@MapField(from = "firstname") val name: String) // name = firstname
// from inexistente → KMX011 con "did you mean"; rutas anidadas ("a.b") → KMX020.
// Fan-out válido; el renombre explícito pisa el match homónimo sin warning.

Target defaults

data class Dto(val name: String, val nickname: String = "N/A")
@MapTo(Dto::class, onNull = OnNull.TARGET_DEFAULT)   // opt-in doble: default + política
data class Src(val name: String, val nickname: String?)
// genera: if (nickname != null) Dto(name, nickname) else Dto(name) — la omisión aplica
// el default (KSP no expone su valor). Hasta 2 campos (when de 4 ramas); más → KMX022.
// Fuente ausente + default → compila con warning KMX021 (nunca en silencio).

KMP

Generación POR target (guía completa). Targets de annotations v1: JVM (sirve a Android), JS(IR), WasmJS, LinuxX64, MingwX64, macOS x64/arm64, iOS arm64/simArm64 — metadata Gradle verificada publicando a repo local. El processor es JVM-only (corre en el build). Los tests de runtime viven en commonTest y corren por target (JVM/JS/Linux en CI Linux; macOS/iOS en el job macOS con -Pkmapx.ci.apple=true). expect class anotada → KMX025.

Funcionalidades

Enums (@MapEntry), anidados con detección de ciclos, Map/Array/Result y Iterable/Sequence como fuente, @BiMapTo validado, rutas anidadas con nulabilidad por segmento (en ambas sedes), converters calificados (@MapField(converter=)) —incluidos beans inyectados en modo contract—, dos sedes de configuración, PATCH set-null con Patch<T>, config global en el bloque KMapX { }, plugin de Gradle id("io.github.kuroxbyte.kmapx"), integraciones Spring/Koin/serialization (guía de migración, patrones de mapeo) y reporte de cobertura. Diagnósticos KMX001–KMX047, todos con factory + contrato. Hay una demo CRUD (módulo demo, reemplaza MapStruct) y un playground documentado.

JVM
iOS
macOS
Linux
Windows
Wasm
JS
GitHub stars0
Authorskuroxbyte
Dependents0
LicenseApache License 2.0
Creation date4 months ago

Last activity3 months ago
Latest release0.4.0 (3 months ago)

KMapX

Mapper Kotlin-first en compile-time: todo mapeo inseguro es un error de compilación. Core de dominio puro (hexagonal) + frontend KSP2 + backend KotlinPoet.

📚 Documentación completa: kuroxbyte.github.io/kmapx

Estado

Publicado en Maven Central (io.github.kuroxbyte:kmapx-*, versión 0.3.0, Apache-2.0). Motor completo: dos modos (embedded/contract), converters (globales, calificados, inyectados), colecciones, value classes, sealed y enums paralelos, PATCH por forma, bidireccional validado, rutas anidadas, profiles con herencia, integraciones Spring/Koin/serialization, reporte de cobertura y Kotlin Multiplatform (JVM/Android, JS, WasmJS, Native). Diagnósticos KMX001–KMX047, cada uno con factory y contrato verificado.

Novedad en 0.3.0: el modelo neutral de tipos (antes MType/TypeKind internos) se extrajo a genkit — base hexagonal reutilizable publicada aparte (io.github.kuroxbyte:genkit-model, kspkit). Ver CHANGELOG.md.

Instalación (JVM)

// build.gradle.kts
plugins {
    kotlin("jvm") version "2.1.21"
    id("com.google.devtools.ksp") version "2.1.21-2.0.1"
}
dependencies {
    implementation("io.github.kuroxbyte:kmapx-annotations:0.4.0")
    implementation("io.github.kuroxbyte:kmapx-runtime:0.4.0")   // solo para Converts<A,B> / Patch<T>
    ksp("io.github.kuroxbyte:kmapx-frontend-ksp:0.4.0")
}
# gradle.properties
ksp.useKSP2=true

Para Kotlin Multiplatform, KSP se declara por target — ver la guía multiplataforma.

Compilar desde el fuente

./gradlew build

Requisitos: JDK 17+.

KMapX se apoya en genkit — la base hexagonal compartida (modelo neutral de tipos + frontend KSP), publicada en Maven Central (io.github.kuroxbyte:genkit-model, kspkit). Se resuelve como dependencia normal; no necesitas clonarla para compilar. Para co-desarrollarla junto a KMapX, clónala como hermano (../genkit) y arranca con -Pkmapx.useGenkitSource=true: el composite build la sustituye por el código local. Por defecto está apagado, así genkit no se importa dentro de KMapX en el IDE.

Módulos

Módulo Rol
kmapx-annotations API pública del usuario. KMP, cero dependencias.
kmapx-runtime KMP, mínimo: la interfaz Converts (solo si se usa el aspecto converter de @MapField).
kmapx-core Modelo (MType, MappingPlan), motor, diagnósticos. Kotlin puro.
kmapx-spi SPI de extensión (experimental): puntos de extensión del processor, vía ServiceLoader.
kmapx-ext-jvm Pack de converters JVM (java.time, UUID, BigDecimal, URI…). Consumidor del SPI.
kmapx-adapter-reflect mclassOf<T>() para testear el core sin compilador. Solo tests.
kmapx-backend-codegen Materializa MappingPlan → .kt (KotlinPoet).
kmapx-frontend-ksp Processor KSP2: traduce símbolos, invoca al motor, emite o reporta KMX.
kmapx-architecture-tests Konsist: la frontera del core como test.
kmapx-integration-tests Proyecto KMP consumidor: consumo end-to-end en vivo.
kmapx-incremental-tests Gradle TestKit: incrementalidad de KSP contra un consumidor real.
kmapx-demo App JVM CRUD que reemplaza a MapStruct (./gradlew :kmapx-demo:run). Ver kmapx-demo/README.
kmapx-gradle-plugin Plugin id("io.github.kuroxbyte.kmapx"): aplica KSP y cablea todo. Ver kmapx-gradle-plugin/README.
kmapx-intellij-plugin Plugin de IntelliJ: el motor real en el editor (diagnósticos KMX, quick-fixes, navegación, materializar). Build STANDALONE: ./gradlew -p kmapx-intellij-plugin buildPlugin.

Documentación

  • Alcance actual (qué se puede y qué no, con el porqué): capacidades-y-limitaciones.md.
  • Ejemplos que COMBINAN funcionalidades, por modo (compilados por el harness): ejemplos-avanzados.md.
  • Guía de referencia por temas (estilo MapStruct): referencia.md · catálogo de diagnósticos GENERADO desde el código: diagnosticos.md.
  • Sitio: mkdocs serve (MkDocs Material; deploy automático a Pages) · API docs: ./gradlew :kmapx-annotations:dokkaGeneratePublicationHtml.
  • Desarrollo: regenerar snapshots con ./gradlew :kmapx-backend-codegen:test -Dkmapx.updateSnapshots=true y revisar el diff.

Uso (modo embedded)

@MapTo(PersonDto::class)                      // genera fun Person.toPersonDto(): PersonDto
@MapTo(PersonSummary::class, name = "asSummary") // repeatable; nombre custom opcional
data class Person(val name: String, val age: Int)

Todas las funciones de una clase van a <Source>Mappings.kt, en su mismo paquete, replicando su visibilidad (internal → internal). Colisión de nombres de función → error KMX013.

Construcción

Orden determinista: @MapConstructor → @MapFactory (companion o top-level) → constructor primario visible. Ambigüedad → KMX006; nada utilizable → KMX005. Las vars públicas de cuerpo se asignan post-construcción vía .also { }.

Modo contract — la interfaz @Mapper

@Mapper
interface PersonMapper { fun toDto(p: Person): PersonDto }
// genera: object PersonMapperImpl : PersonMapper — delega en Person.toPersonDto() si existe,
// o materializa el plan inline si no. Post-función opcional: afterToDto(source, result): result.

Null-safety

T? → T es error de compilación (KMX003) salvo estrategia explícita en el target:

data class Dto(
    @MapField(onNull = OnNull.LITERAL, default = "N/A") val nickname: String, // nickname ?: "N/A" (parseo tipado)
    @MapField(onNull = OnNull.THROW) val email: String,               // ?: throw IllegalArgumentException("email must not be null mapping Src -> Dto")
    @MapField(onNull = OnNull.UNSAFE) val legacy: String,          // legacy!! — el ÚNICO !! posible (test global lo garantiza)
)

Dos estrategias para el mismo campo son inexpresables (onNull es UN valor); @MapField duplicada → KMX037; LITERAL sin default → KMX038; default no parseable/tipo no soportado → KMX017; estrategia sin violación → warning KMX018.

Errores

Todos los errores de una pasada (nunca "arregla uno, descubre el siguiente"), señalando la línea del parámetro target, con "did you mean" (distancia ≤ 2, case-insensitive, hasta 2 candidatos) y formato canónico [KMXnnn] <ubicación> <problema>. Fix: <acción>. verificado por test de contrato.

Conversiones implícitas

Lista CERRADA: idéntico (genéricos incluidos), T → T?, wrap/unwrap de value class, y colecciones elemento a elemento en una sola pasada (addresses.map { it.toAddressDto() }, Set vía mapTo(mutableSetOf()), anidamiento con una lambda por nivel). Nada más: Long→Int (narrowing), enum→String, List→Set… → KMX004 con fix sugiriendo @Converter. El widening numérico SIN PÉRDIDA es automático (Int→Long, Float→Double…) y stdConverters = true suma las estándar (String↔UUID, String↔BigDecimal, Long↔Instant…).

Converters

@Converter fun instantToIso(value: Instant): String = value.toString()
// createdAt = instantToIso(createdAt) — función Kotlin normal, refactor-safe.
// Gana sobre toda otra resolución. Dos converters del mismo par → KMX009.
// Firma inválida (2+ params, Unit, suspend, receiver, no top-level) → KMX019.

Converters calificados

object ShortDate : Converts<LocalDate, String> { override fun convert(v: LocalDate) = v.format(SHORT) }
// elección POR CAMPO, por KClass (reemplaza @Named/qualifiedBy de MapStruct, sin strings):
data class EventDto(@MapField(converter = ShortDate::class) val startDate: String)
// emite ShortDate.convert(startDate); gana sobre @Converter global y mapper (paso 0).
// A/B no encajan → KMX027; el object no implementa Converts → KMX029; A == B → warning KMX031.

Dos sedes de configuración — dominio limpio

La config por campo (@MapField: from/converter/onNull) también vive sobre el método de un @Mapper, nombrando el destino con target = "...". Así el dominio y el DTO quedan sin una sola anotación (hexagonal/DDD). Ambas sedes producen el mismo plan; duplicar campo+método → warning KMX032.

@Mapper interface CustomerMapper {
    @MapField(target = "name", from = "firstName")
    @MapField(target = "registeredAt", converter = IsoInstant::class)
    fun toDto(c: Customer): CustomerDto     // Customer y CustomerDto: CERO anotaciones KMapX
}

Renombrado

data class Dto(@MapField(from = "firstname") val name: String) // name = firstname
// from inexistente → KMX011 con "did you mean"; rutas anidadas ("a.b") → KMX020.
// Fan-out válido; el renombre explícito pisa el match homónimo sin warning.

Target defaults

data class Dto(val name: String, val nickname: String = "N/A")
@MapTo(Dto::class, onNull = OnNull.TARGET_DEFAULT)   // opt-in doble: default + política
data class Src(val name: String, val nickname: String?)
// genera: if (nickname != null) Dto(name, nickname) else Dto(name) — la omisión aplica
// el default (KSP no expone su valor). Hasta 2 campos (when de 4 ramas); más → KMX022.
// Fuente ausente + default → compila con warning KMX021 (nunca en silencio).

KMP

Generación POR target (guía completa). Targets de annotations v1: JVM (sirve a Android), JS(IR), WasmJS, LinuxX64, MingwX64, macOS x64/arm64, iOS arm64/simArm64 — metadata Gradle verificada publicando a repo local. El processor es JVM-only (corre en el build). Los tests de runtime viven en commonTest y corren por target (JVM/JS/Linux en CI Linux; macOS/iOS en el job macOS con -Pkmapx.ci.apple=true). expect class anotada → KMX025.

Funcionalidades

Enums (@MapEntry), anidados con detección de ciclos, Map/Array/Result y Iterable/Sequence como fuente, @BiMapTo validado, rutas anidadas con nulabilidad por segmento (en ambas sedes), converters calificados (@MapField(converter=)) —incluidos beans inyectados en modo contract—, dos sedes de configuración, PATCH set-null con Patch<T>, config global en el bloque KMapX { }, plugin de Gradle id("io.github.kuroxbyte.kmapx"), integraciones Spring/Koin/serialization (guía de migración, patrones de mapeo) y reporte de cobertura. Diagnósticos KMX001–KMX047, todos con factory + contrato. Hay una demo CRUD (módulo demo, reemplaza MapStruct) y un playground documentado.