Hildebrandt Tobias
03/07/2024, 11:54 AMCHANGE_PROFILE
and ADD_PROFILE
. The first needs id, from, to
and the latter just needs id, name
.
Somewhere up in the code an Audit(user, auditAction)
is created and passed down.
It eventuelle lands at the point where it should be written to the databse.
So I have imagine something that looks into the `AuditAction`and depending if it's change or add a different function(signature) is presented, they all return a Map<AuditDetail, String>
.
So in pseudo code something like this:
enum class AuditDetail {
ID,
NAME,
FROM,
TO,
}
object AuditPresets {
fun profileChange(id: String, from: String, to: String) = mapOf(
AuditDetail.ID to id,
AuditDetail.FROM to from,
AuditDetail.TO to to,
)
fun profileAdd(id: String, name: String) = mapOf(
AuditDetail.ID to id,
AuditDetail.NAME to name,
)
}
enum class AuditAction(val transformer: ???) {
PROFILE_CHANGE(AuditPresets::profileChange),
PROFILE_ADD(AuditPresets::profileAdd)
}
Now this obviously doesn't work since calling audit.auditAction()
could be any of the AuditAction
and the compiler doesn't know the signature ahead of time.
I just want a way to connect the AuditAction
enum(or sealed class or whatever) to a specific function even when the result is something like:
return when (audit.auditAction) {
AuditAction.PROFILE_CHANGE -> AuditAction.PROFILE_CHANGE("0","oldValue", "newValue")
AuditAction.PROFILE_ADD -> AuditAction.PROFILE_ADD("1","newName")
}
I feel like the solution might be simple and obvious and I just can't see the forest for the trees.Hildebrandt Tobias
03/07/2024, 12:03 PMsealed class AuditAction {
object ProfileAdd : AuditAction()
data class ProfileAdd(
val id: String,
val name: String,
val details: Map<AuditDetail, String> = mapOf(
AuditDetail.ID to id,
AuditDetail.NAME to name
)
) : AuditAction()
}
Hildebrandt Tobias
03/07/2024, 12:24 PMobject AuditPresets {
fun profileAdd(name: String, id: String) = mapOf(
AuditDetail.ID to id,
AuditDetail.NAME to name,
)
fun profileChange(id: String, from: String, to: String) = mapOf(
AuditDetail.ID to id,
AuditDetail.FROM to from,
AuditDetail.TO to to
)
}
sealed class AuditAction {
object ProfileAdd : AuditAction() {
operator fun invoke() = AuditPresets::profileAdd
}
object ProfileChange : AuditAction() {
operator fun invoke() = AuditPresets::profileChange
}
}
Usage:
when (audit.action) {
AuditAction.ProfileAdd -> AuditAction.ProfileAdd()("0", "1")
AuditAction.ProfileChange -> AuditAction.ProfileChange()("0", "old", "new")
}
Is there a way to omit the empty ()
?CLOVIS
03/12/2024, 10:05 AMsealed class AuditAction {
object ProfileAdd : AuditAction() {
val create get() = AuditPresets::profileAdd
}
object ProfileChange : AuditAction() {
val create get() = AuditPresets::profileChange
}
}
Usage:
when (audit.action) {
AuditAction.ProfileAdd -> AuditAction.ProfileAdd.create("0", "1")
AuditAction.ProfileChange -> AuditAction.ProfileChange.create("0", "old", "new")
}
CLOVIS
03/12/2024, 10:08 AMsealed interface AuditDetail {
sealed interface WithId : AuditDetail {
val id: String
}
sealed interface WithName : AuditDetail {
val name: String
}
// …
data class ProfileChange(
override val id: String,
override val name: String,
override val from: String,
) : AuditDetail, WithId, WithName, WithFrom
data class ProfileAdd(
override val id: String,
override val name: String,
) : AuditDetail, WithId, WithName
}
CLOVIS
03/12/2024, 10:12 AMsealed class AuditAction {
object ProfileAdd : AuditAction() {
operator fun invoke(name: String, id: String) = mapOf(
AuditDetail.ID to id,
AuditDetail.NAME to name,
)
}
// …
}
I'm not a fan that the invoke
operator is abused to construct a data type, it should be used for operations.CLOVIS
03/12/2024, 10:17 AMabstract class AbstractAuditAction {
private val data = HashMap<AuditDetail, String>()
protected fun register(id: AuditDetail, value: String) {
data[id] = value
}
fun asMap() = HashMap(data)
override fun equals(…) { … }
override fun hashCode() { … }
override fun toString() = "${this::class}($data)"
}
sealed class AuditAction {
class ProfileChange(name: String, id: String) : AuditAction(), AbstractAuditAction() {
init {
register(NAME, name)
register(ID, id)
}
}
}
but I'm not sure it's simpler 😅Hildebrandt Tobias
03/15/2024, 12:37 AMobject AuditPresets {
fun profileAddRemove(id: String, name: String) = mapOf(
AuditDetail.ID to id,
AuditDetail.NAME to name,
)
fun profileChange(id: String, from: String, to: String) = mapOf(
AuditDetail.ID to id,
AuditDetail.FROM to from,
<http://AuditDetail.TO|AuditDetail.TO> to to
)
}
sealed class AuditType(open val action: AuditAction) {
sealed class Ibis(override val action: AuditAction) : AuditType(action) {
data object ProfileAdd : Ibis(AuditAction.IBIS_PROFILE_ADD) { val details = AuditPresets::profileAddRemove }
data object ProfileRemove : Ibis(AuditAction.IBIS_PROFILE_REMOVE) { val details = AuditPresets::profileAddRemove }
data object ProfileChange : Ibis(AuditAction.IBIS_PROFILE_CHANGE) { val details = AuditPresets::profileChange }
}
data object None : AuditType(AuditAction.NONE)
}
enum class AuditAction {
NONE,
IBIS_PROFILE_ADD,
IBIS_PROFILE_REMOVE,
IBIS_PROFILE_CHANGE,
}
enum class AuditDetail {
COMPANY,
SUBJECT,
PARENT,
KEY,
VALUE,
ID,
NAME,
FROM,
TO
}
data class Audit(
val user: String,
val action: AuditAction,
val timestamp: Instant = Clock.System.now(),
val details: Map<AuditDetail, String>
)
And here an actual example where I use it:
private fun audit(request: Request) {
val new = message(request)
val from = settingsRepository.getSettingsByTargetAndType(new.target, new.type).value
val to = new.value
auditor.writeAudit(Audit(
user = security.tokenLens(request).id,
action = AuditType.Settings.AuditSet.action,
details = AuditType.Settings.AuditSet.details(new.target, from, to)
))
}
The only thing I am not happy with is that currently all AuditType
are in one file/sealed class.
When I have time I'd like to see if I can break that down so that each kind of AuditType
can be in its own file.CLOVIS
03/15/2024, 8:54 AMHildebrandt Tobias
03/15/2024, 12:34 PM