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

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

import software.amazon.smithy.codegen.core.CodegenException
import software.amazon.smithy.kotlin.codegen.*
import software.amazon.smithy.kotlin.codegen.core.KotlinSymbolProvider
import software.amazon.smithy.kotlin.codegen.model.traits.*
import software.amazon.smithy.kotlin.codegen.utils.getOrNull
import software.amazon.smithy.model.Model
import software.amazon.smithy.model.knowledge.TopDownIndex
import software.amazon.smithy.model.neighbor.Walker
import software.amazon.smithy.model.shapes.*
import software.amazon.smithy.model.traits.Trait

/**
 * Generate synthetic input and output shapes for a operations as needed and normalize the names.
 *
 * The normalization process leaves the cloned shape(s) in the model. Nothing is generated for these
 * though if they aren't in the service shape's closure anymore (assuming you are only walking shapes for said
 * closure which [software.amazon.smithy.kotlin.codegen.CodegenVisitor] does).
 */
object OperationNormalizer {
    private const val REQUEST_SUFFIX: String = "Request"
    private const val RESPONSE_SUFFIX: String = "Response"

    /**
     * Add synthetic input & output shapes to every Operation in the model. The generated shapes will be marked
     * with [SyntheticClone] trait. If an operation does not have an input or output an empty one will be added.
     * Existing inputs/outputs will be modified to have uniform names.
     *
     * @param model The model to transform
     * @param service The service shape ID used to determine the closure of operations to work on
     */
    fun transform(model: Model, service: ShapeId): Model {
        // smithy implicitly loads all models found on the classpath we have to be careful to only deal with
        // shapes in the closure of the service we care about
        val topDownIndex = TopDownIndex.of(model)
        val operations = topDownIndex.getContainedOperations(service)

        validateTransform(model, service, operations)

        val builder = model.toBuilder()
        operations.forEach { operation ->
            val newInputShape: StructureShape = operation.input
                .map { cloneOperationIOShape(operation.id, model.expectShape(it), REQUEST_SUFFIX) }
                .orElseGet { emptyOperationIOStruct(operation.id, REQUEST_SUFFIX) }

            val newOutputShape: StructureShape = operation.output
                .map { cloneOperationIOShape(operation.id, model.expectShape(it), RESPONSE_SUFFIX) }
                .orElseGet { emptyOperationIOStruct(operation.id, RESPONSE_SUFFIX) }

            builder.addShapes(newInputShape, newOutputShape)
            // update model operation with the input/output shapes
            builder.addShape(
                operation.toBuilder()
                    .input(newInputShape)
                    .output(newOutputShape)
                    .build(),
            )
        }
        return builder.build()
    }

    private fun validateTransform(model: Model, service: ShapeId, operations: Set) {
        // list of all renamed shapes
        val newNames = operations.flatMap {
            listOf(it.id.name + REQUEST_SUFFIX, it.id.name + RESPONSE_SUFFIX)
        }.toSet()

        val shapes = Walker(model).iterateShapes(model.expectShape(service))
        val shapesResultingInType = shapes.asSequence().filter {
            // remove trait definitions (which are also structures)
            !it.hasTrait() && KotlinSymbolProvider.isTypeGeneratedForShape(it)
        }.toList()

        val possibleConflicts = shapesResultingInType.filter { it.id.name in newNames }
        if (possibleConflicts.isEmpty()) return

        val operationInputIds = operations.mapNotNull { it.input.getOrNull() }.toSet()
        val operationOutputIds = operations.mapNotNull { it.output.getOrNull() }.toSet()
        val allIds = operationInputIds + operationOutputIds

        // a type that has the same name as a rename is a possible candidate for conflict
        // we have to check if the type is an operational input or not. If it's an operational
        // input already then a rename is effectively a no-op, if it isn't then it's going to conflict
        val realConflicts = possibleConflicts.filterNot { it.id in allIds }
        if (realConflicts.isNotEmpty()) {
            val formatted = realConflicts.joinToString(separator = "\n", prefix = " * ") { it.id.toString() }
            throw CodegenException(
                """renaming operation inputs or outputs will result in a conflict for:
                |$formatted
                |Fix by supplying a manual rename customization for the shapes listed.
                """.trimMargin(),
            )
        }
    }

    private fun syntheticShapeId(opShapeId: ShapeId, suffix: String): ShapeId {
        // see ABNF: https://awslabs.github.io/smithy/1.0/spec/core/model.html#shape-id
        // take the last part of the namespace and clone shapes into the synthetic namespace using the trailing
        // part of the original namespace as a suffix. e.g. "com.foo#Bar" -> "smithy.kotlin.synthetic.foo#Bar"
        val lastNs = opShapeId.namespace.split(".").last()
        return ShapeId.fromParts("$SYNTHETIC_NAMESPACE.$lastNs", opShapeId.name + suffix)
    }

    private fun emptyOperationIOStruct(opShapeId: ShapeId, suffix: String): StructureShape =
        StructureShape
            .builder()
            .id(syntheticShapeId(opShapeId, suffix))
            .addTrait(SyntheticClone.build { archetype = opShapeId })
            .addTrait(if (suffix == REQUEST_SUFFIX) OperationInput() else OperationOutput())
            .build()

    private fun cloneOperationIOShape(opShapeId: ShapeId, structure: StructureShape, suffix: String): StructureShape =
        structure
            .toBuilder()
            .id(syntheticShapeId(opShapeId, suffix))
            .addTrait(SyntheticClone.build { archetype = structure.id })
            .addTrait(if (suffix == REQUEST_SUFFIX) OperationInput() else OperationOutput())
            .build()
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy