Saltar a contenido

KAudit — guía de referencia

Referencia por temas del comportamiento y del código generado. Para la visión general, ver el README.

La función generada

Por cada clase @Auditable se genera <Simple>Diff.kt en su paquete, con:

public fun Account.diff(other: Account): List<FieldChange>

Recorre los campos en orden de declaración, acumula un FieldChange por diferencia y devuelve la lista (vacía si no hubo cambios).

Estrategias por campo

El builder clasifica cada propiedad (tras excluir las @AuditIgnore) en una estrategia:

Estrategia Cuándo Emisión
Direct escalar u objeto anidado no @Auditable if (this.f != other.f) changes += FieldChange("f", this.f, other.f)
Redact @Sensitive if (this.f != other.f) changes += FieldChange("f", null, null, redacted = true)
Recurse tipo @Auditable changes += this.f.diff(other.f).map { it.copy(path = "f." + it.path) }
OpaqueCollection colección sin elemento @AuditKey un FieldChange con la lista entera
KeyedCollection colección cuyo elemento tiene @AuditKey diff por clave (ver abajo)
KeyedMap Map<K, V> diff por la clave natural del mapa (sin @AuditKey)
SealedRecurse propiedad de tipo sealed @Auditable mismo subtipo → recorre; subtipo distinto → objeto entero (is, sin reflexión)

Recurse con propiedad nullable

Si el campo @Auditable es nullable, se compara la presencia antes de recursar:

run {
    val a = this.owner; val b = other.owner
    if (a == null || b == null) { if (a != b) changes += FieldChange("owner", a, b) }
    else changes += a.diff(b).map { it.copy(path = "owner." + it.path) }
}

KeyedCollection

run {
    val oldByKey = this.members.associateBy { it.id }
    val newByKey = other.members.associateBy { it.id }
    for (k in oldByKey.keys - newByKey.keys) changes += FieldChange("members[" + k + "]", oldByKey.getValue(k), null, kind = ChangeKind.REMOVED)
    for (k in newByKey.keys - oldByKey.keys) changes += FieldChange("members[" + k + "]", null, newByKey.getValue(k), kind = ChangeKind.ADDED)
    for (k in oldByKey.keys intersect newByKey.keys) {
        val a = oldByKey.getValue(k); val b = newByKey.getValue(k)
        // elemento @Auditable → se recorre; si no, un MODIFIED por equals:
        changes += a.diff(b).map { it.copy(path = "members[" + k + "]." + it.path) }
    }
}
  • ADDED: kind = ADDED, old = null, new = <elemento>.
  • REMOVED: kind = REMOVED, old = <elemento>, new = null.
  • MODIFIED: solo para elementos que persisten con la misma clave. Si el elemento es @Auditable, se recorre campo a campo (members[42].role); si no, un único FieldChange con el elemento entero.
  • Reordenar sin cambiar contenido → sin cambios (la clave, no el índice, manda).

Recursión y ciclos

El generado delega (owner.diff(other.owner)): cada tipo genera su propio diff y se llama entre sí. No se expande el modelo anidado, así que un tipo autorreferencial (Node(next: Node?)) produce una función recursiva correcta sin detección de ciclos en build-time. Un grafo de datos cíclico en runtime (p. ej. a.next === a) haría bucle infinito; es una limitación documentada, no un caso de auditoría normal.

Decisiones cerradas

  • Igualdad: equals estándar. BigDecimal("1.0") != BigDecimal("1.00") (scale). Override por comparador de tipo → extensión futura, no v1.
  • @AuditKey: tipo con identidad clara (primitivos, String, UUID, value class, enum); nullable prohibido. Ambos son errores de compilación.
  • Array: prohibido como propiedad auditada (equals = identidad de referencia). Usa List o @AuditIgnore.
  • @Sensitive: nunca lleva valor (estructural, no configurable).
  • Anidado no anotado: opaco (equals sobre el objeto entero).

Diagnósticos

Código Severidad Fix sugerido
kaudit.key.nullable error hacer la propiedad no-nullable.
kaudit.key.type error usar un tipo con identidad clara.
kaudit.array.property error usar List, o excluir con @AuditIgnore.

Todos se acumulan en una pasada y apuntan al símbolo de origen (KSPLogger sobre el KSNode).

Incrementalidad

Gracias a la delegación, el archivo generado de un tipo depende solo de su propia fuente: si un tipo anidado cambia, se regenera su propio diff, no el de quien lo usa. El CodeWriter declara ese origen para KSP (originatingKSFiles).