BaseAction.kt

/**
 * This file is part of the pl.wrzasq.cform.
 *
 * @license http://mit-license.org/ The MIT license
 * @copyright 2021 - 2022, 2024 © by Rafał Wrzeszcz - Wrzasq.pl.
 */

package pl.wrzasq.cform.macro.pipeline.types

import pl.wrzasq.cform.macro.pipeline.PipelineAction
import pl.wrzasq.cform.macro.pipeline.PipelineManager
import pl.wrzasq.cform.macro.pipeline.conditional
import pl.wrzasq.cform.macro.template.asMap
import pl.wrzasq.cform.macro.template.asMapAlways
import pl.wrzasq.cform.macro.template.mapSelected
import pl.wrzasq.cform.macro.template.mapValuesOnly

// action and stage names can contain `.` so we need to match last one
private val REFERENCE = Regex("#\\{([^:]+):([^.]+).([^}]+)}")
private const val OPTION_INPUTARTIFACTS = "InputArtifacts"
private const val OPTION_OUTPUTARTIFACTS = "OutputArtifacts"

private const val PROPERTY_CONFIGURATION = "Configuration"
private const val PROPERTY_COMMANDS = "Commands"
private const val PROPERTY_NAMESPACE = "Namespace"
private const val PROPERTY_RUNORDER = "RunOrder"

/**
 * Common setup for action types.
 *
 * @param name Action name.
 * @param input Input properties.
 * @param condition Condition name.
 */
abstract class BaseAction(
    override val name: String,
    input: Map<String, Any>,
    private val condition: String?,
) : PipelineAction {
    protected val properties = input.toMutableMap()
    override val inputs = mutableSetOf<String>()
    override val outputs = mutableSetOf<String>()
    override val dependencies = mutableSetOf<String>()

    // we compute these stuff but might have been defined already in the template

    override var namespace = properties[PROPERTY_NAMESPACE]?.toString()
    override var runOrder: Int? = properties[PROPERTY_RUNORDER]?.toString()?.toInt()

    init {
        properties[OPTION_INPUTARTIFACTS]?.let { readArtifacts(it, inputs) }
        properties[OPTION_OUTPUTARTIFACTS]?.let { readArtifacts(it, outputs) }
    }

    override fun compile(manager: PipelineManager) {
        properties[PROPERTY_CONFIGURATION]?.let {
            properties[PROPERTY_CONFIGURATION] = asMap(it).mapValuesOnly { value ->
                processReference(value, manager)
            }
        }
        properties[PROPERTY_COMMANDS]?.let {
            properties[PROPERTY_COMMANDS] = if (it is List<*>) {
                it.filterNotNull().map { value ->
                    processReference(value, manager)
                }
            } else {
                it
            }
        }
    }

    /**
     * Generates `ActionTypeId` clause.
     *
     * @return Template fragment.
     */
    abstract fun buildActionTypeId(): Map<String, Any>

    /**
     * Action configuration hook.
     *
     * @param configuration Initial configuration structure.
     */
    open fun buildConfiguration(configuration: MutableMap<String, Any>) {}

    override fun buildDefinition(): Map<String, Any> {
        val input = mutableMapOf(
            "Name" to name,
            "ActionTypeId" to buildActionTypeId(),
        )

        val configuration = asMapAlways(properties[PROPERTY_CONFIGURATION]).toMutableMap()
        buildConfiguration(configuration)

        // we do these as conditional `.put()` calls as these stuff might be in `.properties` when defined by hand
        namespace?.let { input[PROPERTY_NAMESPACE] = it }
        runOrder?.let { input[PROPERTY_RUNORDER] = it }
        if (inputs.isNotEmpty()) {
            input[OPTION_INPUTARTIFACTS] = inputs.sorted()
        }
        if (outputs.isNotEmpty()) {
            input[OPTION_OUTPUTARTIFACTS] = outputs.sorted()
        }
        if (configuration.isNotEmpty()) {
            input[PROPERTY_CONFIGURATION] = configuration
        }

        val definition = (properties + input).mapSelected(
            OPTION_INPUTARTIFACTS to ::buildArtifacts,
            OPTION_OUTPUTARTIFACTS to ::buildArtifacts,
        )

        return conditional(definition, condition)
    }

    protected fun processReference(value: Any, manager: PipelineManager) = if (value is String) {
        value.replace(REFERENCE) { match ->
            val group = match.groupValues
            val reference = "${group[1]}:${group[2]}"

            dependencies.add(reference)

            "#{${manager.resolveNamespace(reference)}.${group[3]}}"
        }
    } else {
        value
    }
}

private fun readArtifacts(input: Any, target: MutableSet<String>) {
    if (input is List<*>) {
        for (artifact in input) {
            if (artifact is String) {
                target.add(artifact)
            } else if (artifact is Map<*, *> && "Name" in artifact) {
                target.add(artifact["Name"].toString())
            }
        }
    }
}

private fun buildArtifacts(input: Any) = if (input is List<*>) {
    input.map { if (it is String) mapOf("Name" to it) else it }
} else {
    input
}

/**
 * Builds action type definition for default AWS actions.
 *
 * @param category Category of action.
 * @param provider Providing service.
 * @param owner Action vendor.
 * @param version Definition version.
 */
fun buildAwsActionTypeId(category: String, provider: String, owner: String = "AWS", version: String = "1") = mapOf(
    "Category" to category,
    "Owner" to owner,
    "Provider" to provider,
    "Version" to version,
)