All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.github.freya022.botcommands.internal.commands.application.diff.NewApplicationCommandDiffEngine.kt Maven / Gradle / Ivy

Go to download

A Kotlin-first (and Java) framework that makes creating Discord bots a piece of cake, using the JDA library.

There is a newer version: 3.0.0-alpha.18
Show newest version
package io.github.freya022.botcommands.internal.commands.application.diff

import net.dv8tion.jda.api.interactions.commands.OptionType

private typealias Command = Map
private typealias Option = Map
private typealias Choice = Map
private typealias Object = Map
private typealias ObjectName = String

internal object NewApplicationCommandDiffEngine : ApplicationCommandDiffEngine {
    context(DiffLogger)
    override fun checkCommands(oldCommands: List, newCommands: List): Boolean {
        val addedCommands = newCommands.toNames() - oldCommands.toNames()
        if (addedCommands.isNotEmpty()) log { "Added top-level commands: ${addedCommands.joinToString()}" }

        val removedCommands = oldCommands.toNames() - newCommands.toNames()
        if (removedCommands.isNotEmpty()) log { "Removed top-level commands: ${removedCommands.joinToString()}" }

        val isSame = forEachByName(oldCommands, newCommands) { commandName, oldCommand, newCommand ->
            checkProperties(oldCommand, newCommand, "top-level command '$commandName'") &&
                    checkOptions(commandName, oldCommand, newCommand) &&
                    checkSubcommands(commandName, oldCommand, newCommand) &&
                    checkSubcommandGroups(commandName, oldCommand, newCommand)
        }

        return addedCommands.isEmpty() && removedCommands.isEmpty() && isSame
    }

    context(DiffLogger)
    private fun checkSubcommandGroups(
        topLevelName: ObjectName,
        oldCommand: Command,
        newCommand: Command,
    ): Boolean {
        val oldSubcommandGroups = oldCommand.subcommandGroups
        val newSubcommandGroups = newCommand.subcommandGroups

        val addedSubcommandGroups = newSubcommandGroups.toNames() - oldSubcommandGroups.toNames()
        if (addedSubcommandGroups.isNotEmpty()) log { "Added subcommand groups to '$topLevelName': ${addedSubcommandGroups.joinToString()}" }

        val removedSubcommandGroups = oldSubcommandGroups.toNames() - newSubcommandGroups.toNames()
        if (removedSubcommandGroups.isNotEmpty()) log { "Removed subcommand groups from '$topLevelName': ${removedSubcommandGroups.joinToString()}" }

        val isSame = forEachByName(oldSubcommandGroups, newSubcommandGroups) { subcommandGroupName, oldSubcommandGroup, newSubcommandGroup ->
            checkProperties(oldSubcommandGroup, newSubcommandGroup, "subcommand group '$topLevelName $subcommandGroupName'") &&
                    checkSubcommands("$topLevelName $subcommandGroupName", oldSubcommandGroup, newSubcommandGroup)
        }

        return addedSubcommandGroups.isEmpty() && removedSubcommandGroups.isEmpty() && isSame
    }

    context(DiffLogger)
    private fun checkSubcommands(
        parentName: ObjectName,
        oldCommand: Command,
        newCommand: Command,
    ): Boolean {
        val oldSubcommands = oldCommand.subcommands
        val newSubcommands = newCommand.subcommands

        val addedSubcommands = newSubcommands.toNames() - oldSubcommands.toNames()
        if (addedSubcommands.isNotEmpty()) log { "Added subcommands to '$parentName': ${addedSubcommands.joinToString()}" }

        val removedSubcommands = oldSubcommands.toNames() - newSubcommands.toNames()
        if (removedSubcommands.isNotEmpty()) log { "Removed subcommands from '$parentName': ${removedSubcommands.joinToString()}" }

        val isSame = forEachByName(oldSubcommands, newSubcommands) { subcommandName, oldSubcommand, newSubcommand ->
            checkProperties(oldSubcommand, newSubcommand, "subcommand '$parentName $subcommandName'") &&
                    checkOptions("$parentName $subcommandName", oldSubcommand, newSubcommand)
        }

        return addedSubcommands.isEmpty() && removedSubcommands.isEmpty() && isSame
    }

    context(DiffLogger)
    private fun checkOptions(cmdName: ObjectName, oldCommand: Command, newCommand: Command): Boolean {
        val oldOptions = oldCommand.options
        val newOptions = newCommand.options

        val addedOptions = newOptions.toNames() - oldOptions.toNames()
        if (addedOptions.isNotEmpty()) log { "Added options to '$cmdName': ${addedOptions.joinToString()}" }

        val removedOptions = oldOptions.toNames() - newOptions.toNames()
        if (removedOptions.isNotEmpty()) log { "Removed options from '$cmdName': ${removedOptions.joinToString()}" }

        val isSame = forEachOption(oldOptions, newOptions) { optionName, oldOptionIndex, oldOption, newOptionIndex, newOption ->
            var isSame = true
            if (oldOptionIndex != newOptionIndex) {
                log { "Option '$optionName' from '$cmdName' moved from #$oldOptionIndex to #$newOptionIndex" }
                isSame = false
            }

            isSame &&
                    checkProperties(oldOption, newOption, "option '$optionName' in '$cmdName'") &&
                    checkChoices(cmdName, optionName, oldOption, newOption)
        }

        return addedOptions.isEmpty() && removedOptions.isEmpty() && isSame
    }

    context(DiffLogger)
    private fun checkChoices(cmdName: ObjectName, optionName: ObjectName, oldOption: Option, newOption: Option): Boolean {
        val oldChoices = oldOption.choices
        val newChoices = newOption.choices

        val addedChoices = newChoices.toNames() - oldChoices.toNames()
        if (addedChoices.isNotEmpty()) log { "Added choices to option '$optionName' of '$cmdName': ${addedChoices.joinToString()}" }

        val removedChoices = oldChoices.toNames() - newChoices.toNames()
        if (removedChoices.isNotEmpty()) log { "Removed choices from option '$optionName' of '$cmdName': ${removedChoices.joinToString()}" }

        val isUnmodified = forEachByName(oldChoices, newChoices) { choiceName, oldChoice, newChoice ->
            checkProperties(oldChoice, newChoice, "choice '$choiceName' in option '$optionName' of '$cmdName'")
        }

        return addedChoices.isEmpty() && removedChoices.isEmpty() && isUnmodified
    }

    context(DiffLogger)
    private fun checkProperties(oldCommand: Command, newCommand: Command, cmdName: ObjectName): Boolean {
        val oldPropertyNames = oldCommand.keys
        val newPropertyNames = newCommand.keys

        val addedPropertyNames = newPropertyNames - oldPropertyNames
        if (addedPropertyNames.isNotEmpty()) log { "Added properties to $cmdName: ${addedPropertyNames.joinToString()}" }

        val removedPropertyNames = oldPropertyNames - newPropertyNames
        if (removedPropertyNames.isNotEmpty()) log { "Removed properties from $cmdName: ${removedPropertyNames.joinToString()}" }

        var hasModifications = false
        // Don't check order-sensitive properties, they're done manually
        val allProperties = (oldPropertyNames + newPropertyNames) - "options" - "choices"
        allProperties.forEach { propertyName ->
            val oldProperty = oldCommand[propertyName]
            val newProperty = newCommand[propertyName]
            if (oldProperty?.javaClass != newProperty?.javaClass || oldProperty != newProperty) {
                fun Any?.q() = if (this == null) "null" else "'$this'"
                log { "Property '$propertyName' from $cmdName changed from ${oldProperty.q()} -> ${newProperty.q()}" }
                hasModifications = true
            }
        }
        return !hasModifications
    }

    private fun List.toNames(): Set = mapTo(hashSetOf()) { it["name"] as ObjectName }

    private fun  forEachByName(old: List, new: List, block: (name: ObjectName, old: T, new: T) -> Boolean): Boolean {
        var isSame = true
        old.toNames().intersect(new.toNames()).forEach { objectName ->
            val oldObject = old.first { it["name"] == objectName }
            val newObject = new.first { it["name"] == objectName }
            isSame = isSame && block(objectName, oldObject, newObject)
        }
        return isSame
    }

    private fun forEachOption(old: List