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

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

import software.amazon.smithy.build.FileManifest
import software.amazon.smithy.build.PluginContext
import software.amazon.smithy.codegen.core.SymbolProvider
import software.amazon.smithy.kotlin.codegen.core.*
import software.amazon.smithy.kotlin.codegen.integration.KotlinIntegration
import software.amazon.smithy.kotlin.codegen.model.*
import software.amazon.smithy.kotlin.codegen.rendering.*
import software.amazon.smithy.kotlin.codegen.rendering.protocol.ApplicationProtocol
import software.amazon.smithy.kotlin.codegen.rendering.protocol.ProtocolGenerator
import software.amazon.smithy.model.Model
import software.amazon.smithy.model.knowledge.ServiceIndex
import software.amazon.smithy.model.neighbor.Walker
import software.amazon.smithy.model.shapes.*
import software.amazon.smithy.model.transform.ModelTransformer
import java.util.*
import java.util.logging.Logger

/**
 * Orchestrates Kotlin code generation
 */
class CodegenVisitor(context: PluginContext) : ShapeVisitor.Default() {

    private val LOGGER = Logger.getLogger(javaClass.name)
    private val model: Model
    private val settings = KotlinSettings.from(context.model, context.settings)
    private val service: ServiceShape
    private val fileManifest: FileManifest = context.fileManifest
    private val symbolProvider: SymbolProvider
    private val writers: KotlinDelegator
    private val integrations: List
    private val protocolGenerator: ProtocolGenerator?
    private val applicationProtocol: ApplicationProtocol
    private val baseGenerationContext: GenerationContext

    init {
        val classLoader = context.pluginClassLoader.orElse(javaClass.classLoader)
        LOGGER.info("Discovering KotlinIntegration providers...")
        integrations = ServiceLoader.load(KotlinIntegration::class.java, classLoader)
            .onEach { integration -> LOGGER.info("Loaded KotlinIntegration: ${integration.javaClass.name}") }
            .filter { integration -> integration.enabledForService(context.model, settings) }
            .onEach { integration -> LOGGER.info("Enabled KotlinIntegration: ${integration.javaClass.name}") }
            .sortedBy(KotlinIntegration::order)
            .toList()

        LOGGER.info("Preprocessing model")
        // Model pre-processing:
        // 1. Start with the model from the plugin context
        // 2. Apply integrations
        // 3. Flatten error shapes (see: https://github.com/awslabs/smithy/pull/919)
        // 4. Normalize the operations
        var resolvedModel = context.model
        for (integration in integrations) {
            resolvedModel = integration.preprocessModel(resolvedModel, settings)
        }

        resolvedModel = ModelTransformer.create()
            .copyServiceErrorsToOperations(resolvedModel, settings.getService(resolvedModel))

        // normalize operations
        model = OperationNormalizer.transform(resolvedModel, settings.service)

        service = settings.getService(model)

        symbolProvider = integrations.fold(
            KotlinCodegenPlugin.createSymbolProvider(model, settings),
        ) { provider, integration ->
            integration.decorateSymbolProvider(settings, model, provider)
        }

        writers = KotlinDelegator(settings, model, fileManifest, symbolProvider, integrations)

        protocolGenerator = resolveProtocolGenerator(integrations, model, service, settings)
        applicationProtocol = protocolGenerator?.applicationProtocol ?: ApplicationProtocol.createDefaultHttpApplicationProtocol()

        baseGenerationContext = GenerationContext(model, symbolProvider, settings, protocolGenerator, integrations)
    }

    private fun resolveProtocolGenerator(
        integrations: List,
        model: Model,
        service: ServiceShape,
        settings: KotlinSettings,
    ): ProtocolGenerator? {
        val generators = integrations.flatMap { it.protocolGenerators }.associateBy { it.protocol }
        val serviceIndex = ServiceIndex.of(model)

        try {
            val protocolTrait = settings.resolveServiceProtocol(serviceIndex, service, generators.keys)
            return generators[protocolTrait]
        } catch (ex: UnresolvableProtocolException) {
            LOGGER.warning("Unable to find protocol generator for ${service.id}: ${ex.message}")
        }
        return null
    }

    fun execute() {
        LOGGER.info("Generating Kotlin client for service ${settings.service}")

        LOGGER.info("Walking shapes from ${settings.service} to find shapes to generate")
        val modelWithoutTraits = ModelTransformer.create().getModelWithoutTraitShapes(model)
        val serviceShapes = Walker(modelWithoutTraits).walkShapes(service)
        serviceShapes.forEach { it.accept(this) }

        protocolGenerator?.apply {
            val ctx = ProtocolGenerator.GenerationContext(
                settings,
                model,
                service,
                symbolProvider,
                integrations,
                protocol,
                writers,
            )

            LOGGER.info("[${service.id}] Generating unit tests for protocol $protocol")
            generateProtocolUnitTests(ctx)

            LOGGER.info("[${service.id}] Generating service client for protocol $protocol")
            generateProtocolClient(ctx)

            LOGGER.info("[${service.id}] Generating endpoint provider for protocol $protocol")
            generateEndpointsSources(ctx)

            LOGGER.info("[${service.id}] Generating auth scheme provider for protocol $protocol")
            generateAuthSchemeProvider(ctx)
        }

        writers.finalize()

        if (settings.build.generateDefaultBuildFiles) {
            val dependencies = writers.dependencies
                .mapNotNull { it.properties["dependency"] as? KotlinDependency }
                .distinct()
            writeGradleBuild(settings, fileManifest, dependencies)
        }

        // write files defined by integrations
        integrations.forEach { it.writeAdditionalFiles(baseGenerationContext, writers) }

        writers.flushWriters()
    }

    override fun getDefault(shape: Shape?) { }

    override fun structureShape(shape: StructureShape) {
        writers.useShapeWriter(shape) {
            val renderingContext = baseGenerationContext.toRenderingContext(it, shape)
            StructureGenerator(renderingContext).render()
        }
    }

    override fun stringShape(shape: StringShape) {
        // smithy will present both strings with legacy enum trait AND explicit (non-int) enum shapes in this manner
        if (shape.hasTrait<@Suppress("DEPRECATION") software.amazon.smithy.model.traits.EnumTrait>()) {
            writers.useShapeWriter(shape) { EnumGenerator(shape, symbolProvider.toSymbol(shape), it).render() }
        }
    }

    override fun intEnumShape(shape: IntEnumShape) {
        writers.useShapeWriter(shape) { EnumGenerator(shape, symbolProvider.toSymbol(shape), it).render() }
    }

    override fun unionShape(shape: UnionShape) {
        writers.useShapeWriter(shape) { UnionGenerator(model, symbolProvider, it, shape).render() }
    }

    override fun serviceShape(shape: ServiceShape) {
        if (service != shape) {
            LOGGER.fine("Skipping `${shape.id}` because it is not `${service.id}`")
            return
        }

        writers.useShapeWriter(shape) {
            val renderingCtx = baseGenerationContext.toRenderingContext(it, shape)
            ServiceClientGenerator(renderingCtx).render()
        }

        // render the service (client) base exception type
        val baseExceptionSymbol = ExceptionBaseClassGenerator.baseExceptionSymbol(baseGenerationContext.settings)
        writers.useFileWriter("${baseExceptionSymbol.name}.kt", baseExceptionSymbol.namespace) { writer ->
            ExceptionBaseClassGenerator.render(baseGenerationContext, writer)
        }
    }
}

// delegate generation of endpoints-related modules
// the following are generated regardless of whether the model has endpoint rules:
// - typed EndpointProvider interface
// - EndpointParameters struct (will just be empty if no rules/params)
// - ResolveEndpointMiddleware
// the actual default implementation and test cases will only be generated if there's an actual rule set from which to
// derive them
private fun ProtocolGenerator.generateEndpointsSources(ctx: ProtocolGenerator.GenerationContext) {
    with(endpointDelegator(ctx)) {
        val rules = ctx.service.getEndpointRules()
        generateEndpointProvider(ctx, rules)
        generateEndpointParameters(ctx, rules)
        generateEndpointResolverAdapter(ctx)
        if (rules != null) {
            generateEndpointProviderTests(ctx, ctx.service.getEndpointTests(), rules)
        }
    }
}

private fun ProtocolGenerator.generateAuthSchemeProvider(ctx: ProtocolGenerator.GenerationContext) {
    with(authSchemeDelegator(ctx)) {
        identityProviderGenerator().render(ctx)
        authSchemeParametersGenerator().render(ctx)
        authSchemeProviderGenerator().render(ctx)
        authSchemeProviderAdapterGenerator().render(ctx)
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy