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

software.amazon.smithy.kotlin.codegen.rendering.ServiceGenerator.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

import software.amazon.smithy.codegen.core.Symbol
import software.amazon.smithy.kotlin.codegen.core.*
import software.amazon.smithy.kotlin.codegen.integration.SectionId
import software.amazon.smithy.kotlin.codegen.model.hasStreamingMember
import software.amazon.smithy.kotlin.codegen.model.operationSignature
import software.amazon.smithy.model.knowledge.OperationIndex
import software.amazon.smithy.model.knowledge.TopDownIndex
import software.amazon.smithy.model.shapes.OperationShape
import software.amazon.smithy.model.shapes.ServiceShape

// FIXME - rename file and class to ServiceClientGenerator

/**
 * Renders just the service client interfaces. The actual implementation is handled by protocol generators
 */
class ServiceGenerator(private val ctx: RenderingContext) {
    /**
     * SectionId used when rendering the service interface companion object
     */
    object SectionServiceCompanionObject : SectionId {
        /**
         * Context key for the service symbol
         */
        const val ServiceSymbol = "ServiceSymbol"
    }

    /**
     * SectionId used when rendering the service configuration object
     */
    object SectionServiceConfig : SectionId {
        /**
         * The current rendering context for the service generator
         */
        const val RenderingContext = "RenderingContext"
    }

    init {
        require(ctx.shape is ServiceShape) { "ServiceShape is required for generating a service interface; was: ${ctx.shape}" }
    }

    private val service: ServiceShape =
        requireNotNull(ctx.shape) { "ServiceShape is required to render a service client" }
    private val serviceSymbol = ctx.symbolProvider.toSymbol(service)
    private val writer = ctx.writer

    fun render() {

        importExternalSymbols()

        val topDownIndex = TopDownIndex.of(ctx.model)
        val operations = topDownIndex.getContainedOperations(service).sortedBy { it.defaultName() }
        val operationsIndex = OperationIndex.of(ctx.model)

        writer.renderDocumentation(service)
        writer.renderAnnotations(service)
        writer.openBlock("interface ${serviceSymbol.name} : SdkClient {")
            .call { overrideServiceName() }
            .call {
                // allow access to client's Config
                writer.dokka("${serviceSymbol.name}'s configuration")
                writer.write("val config: Config")
            }
            .call {
                // allow integrations to add additional fields to companion object or configuration
                writer.write("")
                writer.declareSection(
                    SectionServiceCompanionObject,
                    context = mapOf(SectionServiceCompanionObject.ServiceSymbol to serviceSymbol)
                ) {
                    renderCompanionObject()
                }
                writer.write("")
                renderServiceConfig()
            }
            .call {
                operations.forEach { op ->
                    renderOperation(operationsIndex, op)
                }
            }
            .closeBlock("}")
            .write("")
    }

    private fun renderServiceConfig() {
        writer.declareSection(
            SectionServiceConfig,
            context = mapOf(SectionServiceConfig.RenderingContext to ctx)
        ) {
            ClientConfigGenerator(ctx).render()
        }
    }

    /**
     * Render the service interface companion object which is the main entry point for most consumers
     *
     * e.g.
     * ```
     * companion object {
     *     operator fun invoke(block: Config.Builder.() -> Unit = {}): LambdaClient {
     *         val config = Config.Builder().apply(block).build()
     *         return DefaultLambdaClient(config)
     *     }
     *
     *     operator fun invoke(config: Config): LambdaClient = DefaultLambdaClient(config)
     * }
     * ```
     */
    private fun renderCompanionObject() {
        writer.withBlock("companion object {", "}") {
            val hasProtocolGenerator = ctx.protocolGenerator != null
            // If there is no ProtocolGenerator, do not codegen references to the non-existent default client.
            callIf(hasProtocolGenerator) {
                withBlock("operator fun invoke(block: Config.Builder.() -> Unit = {}): ${serviceSymbol.name} {", "}") {
                    write("val config = Config.Builder().apply(block).build()")
                    write("return Default${serviceSymbol.name}(config)")
                }
                write("")
                write("operator fun invoke(config: Config): ${serviceSymbol.name} = Default${serviceSymbol.name}(config)")
            }
        }
    }

    private fun importExternalSymbols() {
        // base client interface
        val sdkInterfaceSymbol = Symbol.builder()
            .name("SdkClient")
            .namespace(RUNTIME_ROOT_NS, ".")
            .addDependency(KotlinDependency.CORE)
            .build()

        writer.addImport(sdkInterfaceSymbol)

        // import all the models generated for use in input/output shapes
        writer.addImport("${ctx.settings.pkg.name}.model", "*")
    }

    private fun overrideServiceName() {
        writer.write("")
            .write("override val serviceName: String")
            .indent()
            .write("get() = #S", ctx.settings.sdkId)
            .dedent()
    }

    private fun renderOperation(opIndex: OperationIndex, op: OperationShape) {
        writer.write("")
        writer.renderDocumentation(op)
        writer.renderAnnotations(op)
        writer.write(opIndex.operationSignature(ctx.model, ctx.symbolProvider, op))

        // Add DSL overload (if appropriate)
        opIndex.getInput(op).ifPresent { inputShape ->
            val outputShape = opIndex.getOutput(op)
            val hasOutputStream = outputShape.map { it.hasStreamingMember(ctx.model) }.orElse(false)

            if (!hasOutputStream) {
                val input = ctx.symbolProvider.toSymbol(inputShape).name
                val operationName = op.defaultName()

                writer.write("")
                writer.renderDocumentation(op)
                writer.renderAnnotations(op)
                val signature = "suspend fun $operationName(block: $input.Builder.() -> Unit)"
                val impl = "$operationName($input.Builder().apply(block).build())"
                writer.write("$signature = $impl")
            }
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy