pl.wrzasq.cform.macro.apigateway.ApiGateway.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of cform-macro Show documentation
Show all versions of cform-macro Show documentation
AWS CloudFormation template processing macro.
/**
* This file is part of the pl.wrzasq.cform.
*
* @license http://mit-license.org/ The MIT license
* @copyright 2021 © by Rafał Wrzeszcz - Wrzasq.pl.
*/
package pl.wrzasq.cform.macro.apigateway
import pl.wrzasq.cform.macro.model.ResourceDefinition
import pl.wrzasq.cform.macro.template.Fn
import pl.wrzasq.cform.macro.template.asMap
import pl.wrzasq.cform.macro.template.popProperty
private val SLUG_FILTER = Regex("\\W")
/**
* API Gateway definition.
*
* @param id Resource logical ID.
* @param input Resource specification.
*/
class ApiGateway(
val id: String,
input: Map
) : ApiTemplateResource {
private val properties: Map
/**
* Map of authorizers registered in current API.
*/
val authorizers: MutableMap = mutableMapOf()
private val models: MutableMap = mutableMapOf()
private val validators: MutableMap = mutableMapOf()
private val resources: MutableMap = mutableMapOf()
private val methods: MutableMap = mutableMapOf()
private val resourceIds: MutableSet = mutableSetOf()
private lateinit var deploymentHash: String
init {
properties = input
.popProperty("Authorizers", {
for ((authorizerId, definition) in asMap(it)) {
authorizers[authorizerId] = ApiAuthorizer(this, authorizerId, asMap(definition))
}
})
.popProperty("Models", {
for ((modelId, definition) in asMap(it)) {
models[modelId] = ApiModel(this, modelId, asMap(definition))
}
})
.popProperty("Resources", {
initTree(Fn.getAtt(resourceId, "RootResourceId"), asMap(it))
})
}
override val resourceId
get() = "ApiGateway$id"
/**
* Returns validator of given type.
*
* @param validatorType Type of validation.
* @return Validator resource.
*/
fun getValidator(validatorType: String) = validators.computeIfAbsent(validatorType) {
when (it) {
"BODY_ONLY" -> ApiRequestValidator(this, "BodyOnly", true, false)
"PARAMETERS_ONLY" -> ApiRequestValidator(this, "ParametersOnly", false, true)
"BODY_AND_PARAMETERS" -> ApiRequestValidator(this, "BodyAndParameters", true, true)
else -> throw IllegalArgumentException("Unsupported validator type `$it`")
}
}
/**
* Builds resources definitions.
*
* @return List of resource entries.
*/
fun generateResources(): List {
val definitions = mutableListOf()
// api resource
definitions.add(
ResourceDefinition(
id = resourceId,
type = "AWS::ApiGateway::RestApi",
properties = properties
)
)
// sorted resources to ensure consistent order for hash computation
definitions.addAll(authorizers.toSortedMap().values.map(ApiAuthorizer::generateResource))
definitions.addAll(models.toSortedMap().values.map(ApiModel::generateResource))
definitions.addAll(validators.toSortedMap().values.map(ApiRequestValidator::generateResource))
definitions.addAll(resources.toSortedMap().values.map(ApiResource::generateResource))
definitions.addAll(methods.toSortedMap().values.map(ApiMethod::generateResource))
// deployment
definitions.add(
ResourceDefinition(
id = "${resourceId}Deployment${computeDeploymentHash(definitions)}",
type = "AWS::ApiGateway::Deployment",
dependsOn = methods.values.map(ApiMethod::resourceId).sorted(),
properties = mapOf("RestApiId" to ref())
)
)
return definitions
}
private fun computeDeploymentHash(definitions: List): String {
deploymentHash = definitions.hashCode().toString().replace("-", "0")
return deploymentHash
}
private fun initTree(
parent: Any,
input: Map,
scopePath: String = "",
scopeId: String = ""
) {
for ((key, value) in input) {
when (key[0]) {
// sub-resource
'/' -> {
val part = key.substring(1)
val path = "$scopePath$key"
val id = generateResourceId(scopeId, part)
val resource = ApiResource(this, id, parent, part)
resources[path] = resource
resourceIds.add(id)
// https://knowyourmeme.com/memes/we-need-to-go-deeper
initTree(resource.ref(), asMap(value), path, id)
}
// method
'@' -> {
val method = key.substring(1)
methods["$method$scopePath"] = ApiMethod(this, "$method$scopeId", parent, method, asMap(value))
}
else -> throw IllegalArgumentException("`$key` is not a valid API structure entry")
}
}
}
private fun generateResourceId(scope: String, part: String): String {
val slug = SLUG_FILTER.replace(part, "").replaceFirstChar { it.uppercase() }
var id = "$scope$slug"
while (id in resourceIds) {
id += "X"
}
return id
}
/**
* Resolves resource reference.
*
* @param path Local resource identifier.
* @return CloudFormation resource identifier.
*/
fun resolve(path: List): String {
if (path.isEmpty()) {
// root element reference
return resourceId
} else if (path.first() == "Deployment") {
// resolving takes place after resources construction so hash will be available
return "${resourceId}Deployment${deploymentHash}"
}
val lookupId = path[1]
return when (path.first()) {
"Authorizer" -> authorizers[lookupId] ?: throw IllegalArgumentException("Unknown authorizer $lookupId")
"Validator" -> validators[lookupId] ?: throw IllegalArgumentException("Unknown validator $lookupId")
"Model" -> models[lookupId] ?: throw IllegalArgumentException("Unknown model $lookupId")
"Resource" -> {
val key = unescapeReference(lookupId)
resources[key] ?: throw IllegalArgumentException("Unknown resource $key")
}
"Method" -> {
val key = unescapeReference(lookupId)
methods[key] ?: throw IllegalArgumentException("Unknown resource method $key")
}
else -> throw IllegalArgumentException("Unknown resource type reference ${path.first()}")
}.resourceId
}
}
// since in API gateway only full path parts are allowed as fragments this is only pattern possibility
// %/ won't work for top-level placeholder resources, but after first replacement only trailing `%` will be left
private fun unescapeReference(reference: String) = reference.replace("/%", "/{").replace("%", "}")