software.amazon.smithy.kotlin.codegen.test.ModelTestUtils.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.test
import software.amazon.smithy.build.MockManifest
import software.amazon.smithy.codegen.core.SymbolProvider
import software.amazon.smithy.kotlin.codegen.KotlinCodegenPlugin
import software.amazon.smithy.kotlin.codegen.KotlinSettings
import software.amazon.smithy.kotlin.codegen.core.*
import software.amazon.smithy.kotlin.codegen.model.OperationNormalizer
import software.amazon.smithy.kotlin.codegen.model.shapes
import software.amazon.smithy.kotlin.codegen.rendering.protocol.ProtocolGenerator
import software.amazon.smithy.kotlin.codegen.utils.getOrNull
import software.amazon.smithy.model.Model
import software.amazon.smithy.model.node.Node
import software.amazon.smithy.model.shapes.ServiceShape
import software.amazon.smithy.model.shapes.ShapeId
import software.amazon.smithy.model.shapes.SmithyIdlModelSerializer
import software.amazon.smithy.model.validation.ValidatedResultException
import java.net.URL
/**
* This file houses classes and functions to help with testing with Smithy models.
*
* These functions should be relatively low-level and deal directly with types provided
* by smithy-codegen.
*/
/**
* Unless necessary to deviate for test reasons, the following literals should be used in test models:
* smithy version: "1"
* model version: "1.0.0"
* namespace: TestDefault.NAMESPACE
* service name: "Test"
*/
internal object TestModelDefault {
const val SMITHY_IDL_VERSION = "1"
const val MODEL_VERSION = "1.0.0"
const val NAMESPACE = "com.test"
const val SERVICE_NAME = "Test"
const val SDK_ID = "Test"
const val SERVICE_SHAPE_ID = "com.test#Test"
}
// attempt to replicate transforms that happen in CodegenVisitor such that tests
// more closely reflect reality
private fun Model.applyKotlinCodegenTransforms(serviceShapeId: String?): Model {
val serviceId = if (serviceShapeId != null) ShapeId.from(serviceShapeId) else {
// try to autodiscover the service so that tests "Just Work" (TM) without having to do anything
val services = this.shapes()
check(services.size <= 1) { "multiple services discovered in model; auto inference of service shape impossible for test. Fix by passing the service shape explicitly" }
if (services.isEmpty()) return this // no services defined, move along
services.first().id
}
val transforms = listOf(OperationNormalizer::transform)
return transforms.fold(this) { m, transform ->
transform(m, serviceId)
}
}
/**
* Load and initialize a model from a Java resource URL
*/
internal fun URL.toSmithyModel(serviceShapeId: String? = null): Model {
val model = Model.assembler()
.addImport(this)
.discoverModels()
.assemble()
.unwrap()
return model.applyKotlinCodegenTransforms(serviceShapeId)
}
/**
* Load and initialize a model from a String
*/
fun String.toSmithyModel(sourceLocation: String? = null, serviceShapeId: String? = null, applyDefaultTransforms: Boolean = true): Model {
val processed = if (this.trimStart().startsWith("\$version")) this else "\$version: \"1.0\"\n$this"
val model = try {
Model.assembler()
.discoverModels()
.addUnparsedModel(sourceLocation ?: "test.smithy", processed)
.assemble()
.unwrap()
} catch (e: ValidatedResultException) {
System.err.println("Model failed to parse:")
System.err.println(this)
throw e
}
return if (applyDefaultTransforms) model.applyKotlinCodegenTransforms(serviceShapeId) else model
}
/**
* Generate Smithy IDL from a model instance.
*
* NOTE: this is used for debugging / unit test generation, please don't remove.
*/
internal fun Model.toSmithyIDL(): String {
val builtInModelIds = setOf("smithy.test.smithy", "aws.auth.smithy", "aws.protocols.smithy", "aws.api.smithy")
val ms: SmithyIdlModelSerializer = SmithyIdlModelSerializer.builder().build()
val node = ms.serialize(this)
return node.filterNot { builtInModelIds.contains(it.key.toString()) }.values.first()
}
/**
* Initiate codegen for the model and produce a [TestContext].
*
* @param serviceName name of service without namespace
* @param packageName root namespace of model
* @param settings [KotlinSettings] associated w/ test context
* @param generator [ProtocolGenerator] associated w/ test context
*/
fun Model.newTestContext(
serviceName: String = TestModelDefault.SERVICE_NAME,
packageName: String = TestModelDefault.NAMESPACE,
settings: KotlinSettings = this.defaultSettings(serviceName, packageName),
generator: ProtocolGenerator = MockHttpProtocolGenerator()
): TestContext {
val manifest = MockManifest()
val provider: SymbolProvider = KotlinCodegenPlugin.createSymbolProvider(model = this, rootNamespace = packageName, serviceName = serviceName)
val service = this.getShape(ShapeId.from("$packageName#$serviceName")).get().asServiceShape().get()
val delegator = KotlinDelegator(settings, this, manifest, provider)
val ctx = ProtocolGenerator.GenerationContext(
settings,
this,
service,
provider,
listOf(),
generator.protocol,
delegator
)
return TestContext(ctx, manifest, generator)
}
/**
* Generate a KotlinSettings instance from a model.
* @param serviceName name of service without namespace or null to attempt to discover from model
* @param packageName name of module or DEFAULT_SERVICE_NAME if unspecified
* @param packageVersion version of module or DEFAULT_MODEL_VERSION if unspecified
* @param sdkId sdk id of settings
* @param generateDefaultBuildFiles flag used to determine what build files to generate
*
* Example:
* {
* "service": "com.amazonaws.lambda#AWSGirApiService",
* "package": {
* "name": "aws.sdk.kotlin.services.lambda",
* "version": "0.2.0-SNAPSHOT",
* "description": "AWS Lambda"
* },
* "sdkId": "Lambda",
* "build": {
* "generateDefaultBuildFiles": false
* }
* }
*/
fun Model.defaultSettings(
serviceName: String? = null,
packageName: String = TestModelDefault.NAMESPACE,
packageVersion: String = TestModelDefault.MODEL_VERSION,
sdkId: String = TestModelDefault.SDK_ID,
generateDefaultBuildFiles: Boolean = false
): KotlinSettings {
val serviceId = if (serviceName == null) {
KotlinSettings.inferService(this)
} else {
this.getShape(ShapeId.from("$packageName#$serviceName")).getOrNull()?.id
?: error("Unable to find service '$serviceName' in model.")
}
return KotlinSettings.from(
this,
Node.objectNodeBuilder()
.withMember("service", Node.from(serviceId.toString()))
.withMember(
"package",
Node.objectNode()
.withMember("name", Node.from(packageName))
.withMember("version", Node.from(packageVersion))
)
.withMember("sdkId", Node.from(sdkId))
.withMember(
"build",
Node.objectNode()
.withMember("generateDefaultBuildFiles", Node.from(generateDefaultBuildFiles))
)
.build()
)
}
// Generate a Smithy IDL model based on input parameters and source string
internal fun String.generateTestModel(
protocol: String,
namespace: String = TestModelDefault.NAMESPACE,
serviceName: String = TestModelDefault.SERVICE_NAME,
operations: List
): Model {
val completeModel = """
namespace $namespace
use aws.protocols#$protocol
@$protocol
service $serviceName {
version: "${TestModelDefault.MODEL_VERSION}",
operations: [
${operations.joinToString(separator = ", ")}
]
}
$this
""".trimIndent()
return completeModel.toSmithyModel()
}
// Specifies AWS protocols that can be set on test models.
enum class AwsProtocolModelDeclaration(val annotation: String, val import: String) {
RestJson("@restJson1", "aws.protocols#restJson1"),
AwsJson1_1("@awsJson1_1", "aws.protocols#awsJson1_1")
}
// Generates the model header which by default conforms to the conventions defined for test models.
fun String.prependNamespaceAndService(
version: String = TestModelDefault.SMITHY_IDL_VERSION,
namespace: String = TestModelDefault.NAMESPACE,
imports: List = emptyList(),
serviceName: String = TestModelDefault.SERVICE_NAME,
protocol: AwsProtocolModelDeclaration? = null,
operations: List = emptyList()
): String {
val versionExpr = "\$version: \"$version\""
val (modelProtocol, modelImports) = if (protocol == null) {
"" to imports
} else {
protocol.annotation to imports + listOf(protocol.import)
}
val importExpr = modelImports.map { "use $it" }.joinToString(separator = "\n") { it }
return (
"""
$versionExpr
namespace $namespace
$importExpr
$modelProtocol
service $serviceName {
version: "${TestModelDefault.MODEL_VERSION}",
operations: $operations
}
""".trimIndent() + this.trimIndent()
)
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy