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

software.amazon.smithy.kotlin.codegen.KotlinSettings.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.codegen.core.CodegenException
import software.amazon.smithy.kotlin.codegen.lang.isValidPackageName
import software.amazon.smithy.model.Model
import software.amazon.smithy.model.knowledge.ServiceIndex
import software.amazon.smithy.model.node.ObjectNode
import software.amazon.smithy.model.node.StringNode
import software.amazon.smithy.model.shapes.ServiceShape
import software.amazon.smithy.model.shapes.Shape
import software.amazon.smithy.model.shapes.ShapeId
import java.util.Optional
import java.util.logging.Logger
import kotlin.streams.toList

// shapeId of service from which to generate an SDK
private const val SERVICE = "service"
private const val PACKAGE_SETTINGS = "package"
private const val PACKAGE_NAME = "name"
private const val PACKAGE_VERSION = "version"
private const val PACKAGE_DESCRIPTION = "description"
private const val BUILD_SETTINGS = "build"
// Optional specification of sdkId for models that provide them, otherwise Service's shape id name is used
private const val SDK_ID = "sdkId"

/**
 * Settings used by [KotlinCodegenPlugin]
 */
data class KotlinSettings(
    val service: ShapeId,
    val pkg: PackageSettings,
    val sdkId: String,
    val build: BuildSettings = BuildSettings.Default
) {

    /**
     * Configuration elements specific to the service's package namespace, version, and description.
     */
    data class PackageSettings(val name: String, val version: String, val description: String? = null)

    /**
     * Get the corresponding [ServiceShape] from a model.
     * @return Returns the found `Service`
     * @throws CodegenException if the service is invalid or not found
     */
    fun getService(model: Model): ServiceShape = model
        .getShape(service)
        .orElseThrow { CodegenException("Service shape not found: $service") }
        .asServiceShape()
        .orElseThrow { CodegenException("Shape is not a service: $service") }

    companion object {
        private val LOGGER: Logger = Logger.getLogger(KotlinSettings::class.java.name)

        /**
         * Create settings from a configuration object node.
         *
         * @param model Model to infer the service from (if not explicitly set in config)
         * @param config Config object to load
         * @throws software.amazon.smithy.model.node.ExpectationNotMetException
         * @return Returns the extracted settings
         */
        fun from(model: Model, config: ObjectNode): KotlinSettings {
            config.warnIfAdditionalProperties(listOf(SERVICE, PACKAGE_SETTINGS, BUILD_SETTINGS, SDK_ID))

            val serviceId = config.getStringMember(SERVICE)
                .map(StringNode::expectShapeId)
                .orElseGet { inferService(model) }

            val packageNode = config.expectObjectMember(PACKAGE_SETTINGS)

            val packageName = packageNode.expectStringMember(PACKAGE_NAME).value
            if (!packageName.isValidPackageName()) {
                throw CodegenException("Invalid package name, is empty or has invalid characters: '$packageName'")
            }

            val version = packageNode.expectStringMember(PACKAGE_VERSION).value
            val desc = packageNode.getStringMemberOrDefault(PACKAGE_DESCRIPTION, "$packageName client")

            // Load the sdk id from configurations that define it, fall back to service name for those that don't.
            val sdkId = config.getStringMemberOrDefault(SDK_ID, serviceId.name)
            val build = config.getObjectMember(BUILD_SETTINGS)
            return KotlinSettings(serviceId, PackageSettings(packageName, version, desc), sdkId, BuildSettings.fromNode(build))
        }

        // infer the service to generate from a model
        internal fun inferService(model: Model): ShapeId {
            val services = model.shapes(ServiceShape::class.java)
                .map(Shape::getId)
                .sorted()
                .toList()

            when {
                services.isEmpty() -> {
                    throw CodegenException(
                        "Cannot infer a service to generate because the model does not " +
                            "contain any service shapes"
                    )
                }
                services.size > 1 -> {
                    throw CodegenException(
                        "Cannot infer service to generate because the model contains " +
                            "multiple service shapes: " + services
                    )
                }
                else -> {
                    val service = services[0]
                    LOGGER.info("Inferring service to generate as: $service")
                    return service
                }
            }
        }
    }

    /**
     * Resolves the highest priority protocol from a service shape that is
     * supported by the generator.
     *
     * @param serviceIndex Service index containing the support
     * @param service Service to get the protocols from if "protocols" is not set.
     * @param supportedProtocolTraits The set of protocol traits supported by the generator.
     * @return Returns the resolved protocol name.
     * @throws UnresolvableProtocolException if no protocol could be resolved.
     */
    fun resolveServiceProtocol(
        serviceIndex: ServiceIndex,
        service: ServiceShape,
        supportedProtocolTraits: Set
    ): ShapeId {
        val resolvedProtocols: Set = serviceIndex.getProtocols(service).keys
        val protocol = resolvedProtocols.firstOrNull(supportedProtocolTraits::contains)
        return protocol ?: throw UnresolvableProtocolException(
            "The ${service.id} service supports the following unsupported protocols $resolvedProtocols. " +
                "The following protocol generators were found on the class path: $supportedProtocolTraits"
        )
    }
}

data class BuildSettings(
    /**
     * Flag indicating to generate a full project that will exist independent of other projects
     */
    val generateFullProject: Boolean = false,

    /**
     * Flag indicating if (Gradle) build files should be spit out. This can be used to turn off generated gradle
     * files by default in-favor of e.g. spitting out your own custom Gradle file as part of an integration.
     */
    val generateDefaultBuildFiles: Boolean = true,

    /**
     * Kotlin opt-in annotations
     * See: https://kotlinlang.org/docs/reference/opt-in-requirements.html
     */
    val optInAnnotations: List? = null
) {
    companion object {
        private const val ROOT_PROJECT = "rootProject"
        private const val GENERATE_DEFAULT_BUILD_FILES = "generateDefaultBuildFiles"
        private const val ANNOTATIONS = "optInAnnotations"

        fun fromNode(node: Optional): BuildSettings = if (node.isPresent) {
            val generateFullProject = node.get().getBooleanMemberOrDefault(ROOT_PROJECT, false)
            val generateBuildFiles = node.get().getBooleanMemberOrDefault(GENERATE_DEFAULT_BUILD_FILES, true)
            val annotations = node.get().getArrayMember(ANNOTATIONS).map {
                it.elements.mapNotNull {
                    it.asStringNode().map { it.value }.orNull()
                }
            }.orNull()

            BuildSettings(generateFullProject, generateBuildFiles, annotations)
        } else {
            Default
        }

        /**
         * Default build settings
         */
        val Default: BuildSettings = BuildSettings()
    }
}

class UnresolvableProtocolException(message: String) : CodegenException(message)

private fun  Optional.orNull(): T? = if (isPresent) get() else null




© 2015 - 2025 Weber Informatics LLC | Privacy Policy