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

software.amazon.smithy.kotlin.codegen.rendering.endpoints.EndpointParametersGenerator.kt Maven / Gradle / Ivy

/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 */
package software.amazon.smithy.kotlin.codegen.rendering.endpoints

import software.amazon.smithy.codegen.core.Symbol
import software.amazon.smithy.kotlin.codegen.KotlinSettings
import software.amazon.smithy.kotlin.codegen.core.KotlinWriter
import software.amazon.smithy.kotlin.codegen.core.clientName
import software.amazon.smithy.kotlin.codegen.core.withBlock
import software.amazon.smithy.kotlin.codegen.model.*
import software.amazon.smithy.kotlin.codegen.rendering.protocol.ProtocolGenerator
import software.amazon.smithy.kotlin.codegen.utils.getOrNull
import software.amazon.smithy.rulesengine.language.EndpointRuleSet
import software.amazon.smithy.rulesengine.language.syntax.parameters.Deprecated

private const val DEFAULT_DEPRECATED_MESSAGE =
    "This field is deprecated and no longer recommended for use."

/**
 * Renders the struct of parameters to be passed to the endpoint provider for resolution.
 */
class EndpointParametersGenerator(
    private val ctx: ProtocolGenerator.GenerationContext,
    rules: EndpointRuleSet?,
    private val writer: KotlinWriter,
) {
    companion object {

        fun getSymbol(settings: KotlinSettings): Symbol =
            buildSymbol {
                val prefix = clientName(settings.sdkId)
                name = "${prefix}EndpointParameters"
                namespace = "${settings.pkg.name}.endpoints"
            }
    }

    private val params: List = (rules?.parameters?.toList() ?: emptyList())
        .sortedBy { it.defaultName() }
        .map {
            KotlinEndpointParameter(
                it.defaultName(),
                it.type.toSymbol(),
                it.isRequired,
                it.default.getOrNull()?.toLiteral() ?: "null",
                it.documentation.getOrNull(),
                it.deprecated.getOrNull(),
            )
        }

    private val paramsSymbol = getSymbol(ctx.settings)

    fun render() {
        renderDocumentation()
        writer.withBlock(
            "#L class #T private constructor(builder: Builder) {",
            "}",
            ctx.settings.api.visibility,
            paramsSymbol,
        ) {
            renderFields()
            renderCompanionObject()
            write("")
            renderEquals()
            write("")
            renderHashCode()
            write("")
            renderToString()
            write("")
            renderCopy()
            write("")
            renderBuilder()
        }
    }

    private fun renderFields() {
        params.forEach {
            val initialValueExpr = if (it.isRequired) {
                """requireNotNull(builder.${it.name}) { "endpoint provider parameter #${it.name} is required" }"""
            } else {
                "builder.${it.name}"
            }

            writer.ensureSuppressDeprecation(it)
            it.renderDeclaration(writer, initialValueExpr)
            writer.write("")
        }
    }

    private fun renderDocumentation() {
        writer.dokka {
            write("The set of values necessary for endpoint resolution.")
        }
    }

    private fun renderCompanionObject() {
        writer.withBlock("public companion object {", "}") {
            write("public inline operator fun invoke(block: Builder.() -> Unit): #T = Builder().apply(block).build()", paramsSymbol)
        }
    }

    private fun renderEquals() {
        writer.withBlock("public override fun equals(other: Any?): Boolean {", "}") {
            write("if (this === other) return true")
            write("if (other !is #T) return false", paramsSymbol)
            params.forEach {
                ensureSuppressDeprecation(it)
                write("if (this.#1L != other.#1L) return false", it.name)
            }
            write("return true")
        }
    }

    private fun renderHashCode() {
        writer.withBlock("public override fun hashCode(): Int {", "}") {
            if (params.isEmpty()) {
                write("return this::class.hashCode()")
                return@withBlock
            }

            ensureSuppressDeprecation(params[0])
            write("var result = #L?.hashCode() ?: 0", params[0].name)
            params.drop(1).forEach {
                ensureSuppressDeprecation(it)
                write("result = 31 * result + (#L?.hashCode() ?: 0)", it.name)
            }
            write("return result")
        }
    }

    private fun renderToString() {
        writer.withBlock("public override fun toString(): String = buildString {", "}") {
            write("append(\"#L(\")", paramsSymbol.name)
            params.forEachIndexed { index, it ->
                ensureSuppressDeprecation(it)
                write("""append("#1L=$#1L#2L")""", it.name, if (index < params.size - 1) "," else ")")
            }
        }
    }

    private fun renderCopy() {
        writer.withBlock("public fun copy(block: Builder.() -> Unit = {}): #T {", "}", paramsSymbol) {
            withBlock("return Builder().apply {", "}") {
                params.forEach {
                    ensureSuppressDeprecation(it)
                    write("#1L = this@#2L.#1L", it.name, paramsSymbol.name)
                }
                write("block()")
            }
            write(".build()")
        }
    }

    private fun renderBuilder() {
        writer.withBlock("public class Builder {", "}") {
            params.forEach {
                it.renderDeclaration(writer, it.defaultLiteral, isMutable = true)
            }
            write("")
            write("public fun build(): #1T = #1T(this)", paramsSymbol)
        }
    }
}

// kotlin-mapped representation of an endpoint parameter
private data class KotlinEndpointParameter(
    val name: String,
    val type: Symbol,
    val isRequired: Boolean,
    // All endpoint params are nullable. Standard symbol rendering suppresses the default value for boxed types, but
    // endpoint params do not, so we must store the default ourselves to render.
    val defaultLiteral: String,
    val documentation: String?,
    val deprecated: Deprecated?,
)

private fun KotlinEndpointParameter.renderDeclaration(writer: KotlinWriter, initialValueLiteral: String, isMutable: Boolean = false) {
    documentation?.let {
        writer.dokka { write("#L", it) }
    }
    deprecated?.run { writeKotlinAnnotation(writer) }
    writer.write("public #L #L: #P = #L", if (isMutable) "var" else "val", name, type, initialValueLiteral)
    writer.write("")
}

private fun Deprecated.writeKotlinAnnotation(writer: KotlinWriter) =
    writer.write("@Deprecated(#S)", message.getOrNull() ?: DEFAULT_DEPRECATED_MESSAGE)

private fun KotlinWriter.ensureSuppressDeprecation(param: KotlinEndpointParameter) =
    param.deprecated?.let { write("@Suppress(\"DEPRECATION\")") }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy