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.kotlin.codegen.utils.getOrNull
import software.amazon.smithy.kotlin.codegen.utils.toCamelCase
import software.amazon.smithy.model.Model
import software.amazon.smithy.model.knowledge.NullableIndex.CheckMode
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.IllegalArgumentException
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"
private const val API_SETTINGS = "api"

// 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,
    val api: ApiSettings = ApiSettings.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) {
        /**
         * Derive a subpackage namespace from the root package name
         */
        fun subpackage(subpackageName: String): String = "$name.$subpackageName"

        /**
         * The package namespace for generated serialization/deserialization support
         */
        val serde: String
            get() = subpackage("serde")
    }

    /**
     * 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, API_SETTINGS))

            val serviceId = config.getStringMember(SERVICE)
                .map(StringNode::expectShapeId)
                .orElseGet { model.inferService().also { LOGGER.info("Inferring service to generate as $it") } }

            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)
            val api = config.getObjectMember(API_SETTINGS)
            return KotlinSettings(
                serviceId,
                PackageSettings(packageName, version, desc),
                sdkId,
                BuildSettings.fromNode(build),
                ApiSettings.fromNode(api),
            )
        }
    }

    /**
     * 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",
        )
    }
}

fun Model.inferService(): ShapeId {
    val services = shapes(ServiceShape::class.java)
        .map(Shape::getId)
        .sorted()
        .toList()

    return 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 -> services.single()
    }
}

/**
 * Contains Gradle build settings for a Kotlin project
 * @param generateFullProject Flag indicating to generate a full project that will exist independent of other projects
 * @param generateDefaultBuildFiles 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.
 * @param optInAnnotations Kotlin opt-in annotations. See:
 * https://kotlinlang.org/docs/reference/opt-in-requirements.html
 * @param generateMultiplatformProject Flag indicating to generate a Kotlin multiplatform or JVM project
 */
data class BuildSettings(
    val generateFullProject: Boolean = false,
    val generateDefaultBuildFiles: Boolean = true,
    val optInAnnotations: List? = null,
    val generateMultiplatformProject: Boolean = false,
) {
    companion object {
        const val ROOT_PROJECT = "rootProject"
        const val GENERATE_DEFAULT_BUILD_FILES = "generateDefaultBuildFiles"
        const val ANNOTATIONS = "optInAnnotations"
        const val GENERATE_MULTIPLATFORM_MODULE = "multiplatform"

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

            BuildSettings(generateFullProject, generateBuildFiles, annotations, generateMultiplatformProject)
        }.orElse(Default)

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

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

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

/**
 * The visibility of code-generated classes, objects, interfaces, etc.
 * Valid values are `public` and `internal`. `private` not supported because codegen would not compile with private classes.
 */
enum class Visibility(val value: String) {
    PUBLIC("public"),
    INTERNAL("internal"),
    ;

    override fun toString(): String = value

    companion object {
        public fun fromValue(value: String): Visibility = when (value.lowercase()) {
            "public" -> PUBLIC
            "internal" -> INTERNAL
            else -> throw IllegalArgumentException("$value is not a valid Visibility value, expected $PUBLIC or $INTERNAL")
        }
    }
}

private fun checkModefromValue(value: String): CheckMode {
    val camelCaseToMode = CheckMode.values().associateBy { it.toString().toCamelCase() }
    return requireNotNull(camelCaseToMode[value]) { "$value is not a valid CheckMode, expected one of ${camelCaseToMode.keys}" }
}

/**
 * Get the plugin setting for this check mode
 */
val CheckMode.kotlinPluginSetting: String
    get() = toString().toCamelCase()

enum class DefaultValueSerializationMode(val value: String) {
    /**
     * Always serialize values even if they are set to the modeled default
     */
    ALWAYS("always"),

    /**
     * Only serialize values when they differ from the modeled default or are marked `@required`
     */
    WHEN_DIFFERENT("whenDifferent"),
    ;
    override fun toString(): String = value
    companion object {
        fun fromValue(value: String): DefaultValueSerializationMode =
            values().find {
                it.value == value
            } ?: throw IllegalArgumentException("$value is not a valid DefaultValueSerializationMode, expected one of ${values().map { it.value }}")
    }
}

/**
 * Contains API settings for a Kotlin project
 * @param visibility Enum representing the visibility of code-generated classes, objects, interfaces, etc.
 * @param nullabilityCheckMode Enum representing the nullability check mode to use
 * @param defaultValueSerializationMode Enum representing when default values should be serialized
 * @param enableEndpointAuthProvider flag indicating that endpoint resolution should be enabled as part of resolving
 * an auth scheme. This is an advanced option that only a select few service clients like S3 and EventBridge require.
 */
data class ApiSettings(
    val visibility: Visibility = Visibility.PUBLIC,
    val nullabilityCheckMode: CheckMode = CheckMode.CLIENT_CAREFUL,
    val defaultValueSerializationMode: DefaultValueSerializationMode = DefaultValueSerializationMode.WHEN_DIFFERENT,
    val enableEndpointAuthProvider: Boolean = false,
) {
    companion object {
        const val VISIBILITY = "visibility"
        const val NULLABILITY_CHECK_MODE = "nullabilityCheckMode"
        const val DEFAULT_VALUE_SERIALIZATION_MODE = "defaultValueSerializationMode"
        const val ENABLE_ENDPOINT_AUTH_PROVIDER = "enableEndpointAuthProvider"

        fun fromNode(node: Optional): ApiSettings = node.map {
            val visibility = node.get()
                .getStringMember(VISIBILITY)
                .map { Visibility.fromValue(it.value) }
                .getOrNull() ?: Visibility.PUBLIC
            val checkMode = node.get()
                .getStringMember(NULLABILITY_CHECK_MODE)
                .map { checkModefromValue(it.value) }
                .getOrNull() ?: CheckMode.CLIENT_CAREFUL
            val defaultValueSerializationMode = DefaultValueSerializationMode.fromValue(
                node.get()
                    .getStringMemberOrDefault(
                        DEFAULT_VALUE_SERIALIZATION_MODE,
                        DefaultValueSerializationMode.WHEN_DIFFERENT.value,
                    ),
            )
            val enableEndpointAuthProvider = node.get().getBooleanMemberOrDefault(ENABLE_ENDPOINT_AUTH_PROVIDER, false)
            ApiSettings(visibility, checkMode, defaultValueSerializationMode, enableEndpointAuthProvider)
        }.orElse(Default)

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




© 2015 - 2025 Weber Informatics LLC | Privacy Policy