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

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

import software.amazon.smithy.codegen.core.Symbol
import software.amazon.smithy.kotlin.codegen.core.*
import software.amazon.smithy.kotlin.codegen.model.asNullable
import software.amazon.smithy.kotlin.codegen.model.buildSymbol
import software.amazon.smithy.kotlin.codegen.model.defaultValue
import software.amazon.smithy.kotlin.codegen.model.nullable

typealias CustomPropertyRenderer = (ConfigProperty, KotlinWriter) -> Unit

/**
 * Represents a configuration property (e.g. service client config)
 *
 * e.g.
 *
 * ```kotlin
 * val myProp = ConfigProperty {
 *     symbol = buildSymbol { ... }
 *     documentation = "my property documentation"
 *     addBaseClass(myBaseClass)
 * }
 * ```
 */
class ConfigProperty private constructor(builder: Builder) {
    init {
        if (builder.builderSymbol != null && builder.symbol != builder.builderSymbol) {
            requireNotNull(builder.toBuilderExpression) { "an expression mapping the immutable property back to the mutable builder is required when the builder symbol differs" }
        }
    }

    /**
     * The symbol (type) for the property
     *
     * NOTE: Use the extension properties on Symbol.Builder to set additional properties:
     * e.g.
     * ```
     * val symbol = Symbol.builder()
     *    .nullable = true // mark the symbol as nullable
     *    .defaultValue("foo") // set the default value for the property
     *    .build()
     * ```
     */
    val symbol: Symbol = requireNotNull(builder.symbol)

    /**
     * The symbol (type) for the builder implementation. Usually this is the same as the underlying symbol.
     * This can be used to generate a different builder symbol (e.g. `MutableList` vs `List`).
     */
    val builderSymbol: Symbol = builder.builderSymbol ?: symbol

    /**
     * The expression used to map immutable config back to a builder instance. This is usually not required
     * when the symbol types are the same as a simple assignment will work.
     *
     * e.g. `toBuilderExpression = ".toMutableList()"`
     */
    val toBuilderExpression: String? = builder.toBuilderExpression

    /**
     * Help text to be rendered for the property
     */
    val documentation: String? = builder.documentation

    /**
     * The name of the property to render to config
     */
    val propertyName: String = builder.name ?: symbol.name.replaceFirstChar { c -> c.lowercaseChar() }

    /**
     * Additional base classes config should inherit from
     *
     * NOTE: Adding 1 or more base classes will implicitly render the property with an `override` modifier
     */
    val baseClass: Symbol? = builder.baseClass

    /**
     * A delegate the config object should use as the backing implementation of this property's base class. This enables
     * class specifications such as:
     *
     * ```kotlin
     * class Config : BaseClass by BaseClassDelegate(...)
     * ```
     */
    val baseClassDelegate: Delegate? = builder.baseClassDelegate

    /**
     * Additional base classes the config builder should inherit from
     *
     * NOTE: Adding 1 or more base classes will implicitly render the property with an `override` modifier
     */
    val builderBaseClass: Symbol? = builder.builderBaseClass

    /**
     * A delegate the config builder object should use as the backing implementation of this property's builder base
     * class. This enables class specifications such as:
     *
     * ```kotlin
     * class Builder : BuilderBaseClass by BuilderBaseClassDelegate(...)
     * ```
     */
    val builderBaseClassDelegate: Delegate? = builder.builderBaseClassDelegate

    /**
     * The configuration property type. This controls how the property is constructed and rendered
     */
    val propertyType: ConfigPropertyType = builder.propertyType

    /**
     * Additional symbols that should be imported when this property is generated. This is useful for
     * example when the [symbol] type has is an interface and has a default or constant value that
     * implements that type. The default value symbol also needs imported.
     */
    val additionalImports: List = builder.additionalImports

    /**
     * The priority order of rendering the property. Used to manage dependencies between configuration properties.
     */
    val order: Int = builder.order

    /**
     * Flag indicating if this property stems from some base class and needs an override modifier when rendered
     */
    internal val requiresOverride: Boolean
        get() = baseClass != null

    /**
     * Flag indicating if builder property stems from some base class and needs an override modifier when rendered
     */
    internal val builderRequiresOverride: Boolean
        get() = builderBaseClass != null

    companion object {
        operator fun invoke(block: Builder.() -> Unit): ConfigProperty =
            Builder().apply(block).build()

        /**
         * Convenience init for an integer symbol.
         * @param name The property name
         * @param defaultValue The default value the config property should have if not set (if not specified the
         * parameter is assumed nullable)
         * @param documentation Help text to render as documentation for the property
         * @param baseClass Base class the config class should inherit from (assumes this property
         * stems from this type)
         */
        fun Int(
            name: String,
            defaultValue: Int? = null,
            documentation: String? = null,
            baseClass: Symbol? = null,
        ): ConfigProperty =
            builtInProperty(name, builtInSymbol("Int", defaultValue?.toString()), documentation, baseClass)

        /**
         * Convenience init for a boolean symbol.
         * @param name The property name
         * @param defaultValue The default value the config property should have if not set (if not specified the
         * parameter is assumed nullable)
         * @param documentation Help text to render as documentation for the property
         * @param baseClass Base class the config class should inherit from (assumes this property
         * stems from this type)
         */
        fun Boolean(
            name: String,
            defaultValue: Boolean? = null,
            documentation: String? = null,
            baseClass: Symbol? = null,
        ): ConfigProperty =
            builtInProperty(name, builtInSymbol("Boolean", defaultValue?.toString()), documentation, baseClass)

        /**
         * Convenience init for a string symbol.
         * @param name The property name
         * @param defaultValue The default value the config property should have if not set (if not specified the
         * parameter is assumed nullable)
         * @param documentation Help text to render as documentation for the property
         * @param baseClass Base class the config class should inherit from (assumes this property
         * stems from this type)
         */
        fun String(
            name: String,
            defaultValue: String? = null,
            documentation: String? = null,
            baseClass: Symbol? = null,
        ): ConfigProperty =
            builtInProperty(name, builtInSymbol("String", defaultValue), documentation, baseClass)
    }

    fun toBuilder(): Builder = Builder().apply {
        symbol = [email protected]
        builderSymbol = [email protected] { it == [email protected] }
        toBuilderExpression = [email protected]
        name = [email protected]
        documentation = [email protected]
        baseClass = [email protected]
        builderBaseClass = [email protected]
        propertyType = [email protected]
        additionalImports = [email protected]
        order = [email protected]
    }

    class Builder {
        var symbol: Symbol? = null
        var builderSymbol: Symbol? = null
        var toBuilderExpression: String? = null

        // override the property name (defaults to symbol name)
        var name: String? = null
        var documentation: String? = null

        var baseClass: Symbol? = null
        var baseClassDelegate: Delegate? = null
        var builderBaseClass: Symbol? = null
        var builderBaseClassDelegate: Delegate? = null

        var propertyType: ConfigPropertyType = ConfigPropertyType.SymbolDefault

        var additionalImports: List = emptyList()

        var order: Int = 0

        /**
         * Convenience function to set the [builderBaseClass] symbol to a `Builder` class nested in the base
         * class interface itself (common in the runtime)
         */
        fun useNestedBuilderBaseClass() {
            val base = checkNotNull(baseClass) { "must set baseClass before calling" }
            builderBaseClass = buildSymbol {
                name = "${base.name}.Builder"
                namespace = base.namespace
            }
        }

        /**
         * Shorthand to declare that the property has a [symbol] where the builder version is the same, but nullable.
         */
        fun useSymbolWithNullableBuilder(symbol: Symbol, defaultValue: String? = null) {
            this.symbol = symbol
            this.builderSymbol = symbol.asNullable()
            this.toBuilderExpression = "" // nothing needed to convert back, T fits into T?
            this.propertyType = if (defaultValue == null) {
                ConfigPropertyType.Required()
            } else {
                ConfigPropertyType.RequiredWithDefault(defaultValue)
            }
        }

        fun build(): ConfigProperty = ConfigProperty(this)
    }
}

/**
 * Represents a type which may be used for [inheritance delegation](https://kotlinlang.org/docs/delegation.html).
 * @param symbol The symbol for the delegate target. Codegenerators should import this symbol where appropriate so that
 * the [delegationExpression] has the correct symbol in scope.
 * @param delegationExpression The code expression for how to delegate
 */
data class Delegate(val symbol: Symbol?, val delegationExpression: String)

private fun builtInSymbol(symbolName: String, defaultValue: String?): Symbol {
    val builder = Symbol.builder()
        .name(symbolName)

    if (defaultValue != null) {
        builder.defaultValue(defaultValue)
    } else {
        builder.nullable()
    }
    return builder.build()
}

private fun builtInProperty(
    name: String,
    symbol: Symbol,
    documentation: String?,
    baseClass: Symbol?,
): ConfigProperty =
    ConfigProperty {
        this.symbol = symbol
        this.name = name
        this.documentation = documentation
        this.baseClass = baseClass
    }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy