From a955e126a55733d1fbe165da49c2cadeeab041fc Mon Sep 17 00:00:00 2001 From: Thomas Farr Date: Wed, 4 Jan 2023 13:29:48 +1300 Subject: [PATCH 1/2] Smithy generator experiment --- java-codegen/build.gradle.kts | 41 ++ .../client/codegen/CodegenVisitor.kt | 80 ++++ .../codegen/OpenSearchJavaCodegenPlugin.kt | 12 + .../client/codegen/OpenSearchJavaSettings.kt | 70 +++ .../client/codegen/core/CodegenContext.kt | 38 ++ .../client/codegen/core/ImportStatements.kt | 30 ++ .../client/codegen/core/JavaDelegator.kt | 27 ++ .../client/codegen/core/JavaSymbolProvider.kt | 176 +++++++ .../client/codegen/core/JavaVisibility.kt | 11 + .../client/codegen/core/JavaWriter.kt | 94 ++++ .../opensearch/client/codegen/core/Naming.kt | 18 + .../client/codegen/core/ReservedWords.kt | 12 + .../client/codegen/core/RuntimeTypes.kt | 73 +++ .../core/traits/SyntheticInputTrait.kt | 14 + .../client/codegen/model/ModelExt.kt | 8 + .../codegen/model/OperationNormalizer.kt | 64 +++ .../client/codegen/model/ShapeExt.kt | 25 + .../client/codegen/model/SymbolExt.kt | 60 +++ .../client/codegen/render/ServiceGenerator.kt | 91 ++++ .../codegen/render/StructureGenerator.kt | 450 ++++++++++++++++++ .../client/codegen/utils/Strings.kt | 12 + ...ware.amazon.smithy.build.SmithyBuildPlugin | 1 + settings.gradle.kts | 1 + 23 files changed, 1408 insertions(+) create mode 100644 java-codegen/build.gradle.kts create mode 100644 java-codegen/src/main/kotlin/org/opensearch/client/codegen/CodegenVisitor.kt create mode 100644 java-codegen/src/main/kotlin/org/opensearch/client/codegen/OpenSearchJavaCodegenPlugin.kt create mode 100644 java-codegen/src/main/kotlin/org/opensearch/client/codegen/OpenSearchJavaSettings.kt create mode 100644 java-codegen/src/main/kotlin/org/opensearch/client/codegen/core/CodegenContext.kt create mode 100644 java-codegen/src/main/kotlin/org/opensearch/client/codegen/core/ImportStatements.kt create mode 100644 java-codegen/src/main/kotlin/org/opensearch/client/codegen/core/JavaDelegator.kt create mode 100644 java-codegen/src/main/kotlin/org/opensearch/client/codegen/core/JavaSymbolProvider.kt create mode 100644 java-codegen/src/main/kotlin/org/opensearch/client/codegen/core/JavaVisibility.kt create mode 100644 java-codegen/src/main/kotlin/org/opensearch/client/codegen/core/JavaWriter.kt create mode 100644 java-codegen/src/main/kotlin/org/opensearch/client/codegen/core/Naming.kt create mode 100644 java-codegen/src/main/kotlin/org/opensearch/client/codegen/core/ReservedWords.kt create mode 100644 java-codegen/src/main/kotlin/org/opensearch/client/codegen/core/RuntimeTypes.kt create mode 100644 java-codegen/src/main/kotlin/org/opensearch/client/codegen/core/traits/SyntheticInputTrait.kt create mode 100644 java-codegen/src/main/kotlin/org/opensearch/client/codegen/model/ModelExt.kt create mode 100644 java-codegen/src/main/kotlin/org/opensearch/client/codegen/model/OperationNormalizer.kt create mode 100644 java-codegen/src/main/kotlin/org/opensearch/client/codegen/model/ShapeExt.kt create mode 100644 java-codegen/src/main/kotlin/org/opensearch/client/codegen/model/SymbolExt.kt create mode 100644 java-codegen/src/main/kotlin/org/opensearch/client/codegen/render/ServiceGenerator.kt create mode 100644 java-codegen/src/main/kotlin/org/opensearch/client/codegen/render/StructureGenerator.kt create mode 100644 java-codegen/src/main/kotlin/org/opensearch/client/codegen/utils/Strings.kt create mode 100644 java-codegen/src/main/resources/META-INF/services/software.amazon.smithy.build.SmithyBuildPlugin diff --git a/java-codegen/build.gradle.kts b/java-codegen/build.gradle.kts new file mode 100644 index 0000000000..baac5e030e --- /dev/null +++ b/java-codegen/build.gradle.kts @@ -0,0 +1,41 @@ +plugins { + kotlin("jvm") version "1.7.10" + id("com.github.johnrengelman.shadow") version "7.1.2" +} + +group = "org.opensearch.client" +version = "2.1.1-SNAPSHOT" + +repositories { + mavenCentral() +} + +dependencies { + val smithyVersion = "1.26.1" + val kotlinVersion = "1.7.10" + val junitVersion = "5.8.2" + val kotestVersion = "5.4.1" + + implementation(kotlin("stdlib-jdk8")) + implementation("software.amazon.smithy:smithy-codegen-core:$smithyVersion") + implementation("software.amazon.smithy:smithy-waiters:$smithyVersion") + implementation("software.amazon.smithy:smithy-rules-engine:$smithyVersion") + implementation("software.amazon.smithy:smithy-aws-traits:$smithyVersion") + implementation("software.amazon.smithy:smithy-protocol-test-traits:$smithyVersion") + + // Test dependencies + testImplementation("org.junit.jupiter:junit-jupiter:$junitVersion") + testImplementation("io.kotest:kotest-assertions-core-jvm:$kotestVersion") + testImplementation("org.jetbrains.kotlin:kotlin-test:$kotlinVersion") + testImplementation("org.jetbrains.kotlin:kotlin-test-junit5:$kotlinVersion") +} + +kotlin { + jvmToolchain { + languageVersion.set(JavaLanguageVersion.of(8)) + } +} + +tasks.getByName("test") { + useJUnitPlatform() +} \ No newline at end of file diff --git a/java-codegen/src/main/kotlin/org/opensearch/client/codegen/CodegenVisitor.kt b/java-codegen/src/main/kotlin/org/opensearch/client/codegen/CodegenVisitor.kt new file mode 100644 index 0000000000..de62c29201 --- /dev/null +++ b/java-codegen/src/main/kotlin/org/opensearch/client/codegen/CodegenVisitor.kt @@ -0,0 +1,80 @@ +package org.opensearch.client.codegen + +import org.opensearch.client.codegen.core.GenerationContext +import org.opensearch.client.codegen.core.JavaDelegator +import org.opensearch.client.codegen.core.JavaSymbolProvider +import org.opensearch.client.codegen.core.toRenderingContext +import org.opensearch.client.codegen.model.OperationNormalizer +import org.opensearch.client.codegen.render.ServiceGenerator +import org.opensearch.client.codegen.render.StructureGenerator +import software.amazon.smithy.build.FileManifest +import software.amazon.smithy.build.PluginContext +import software.amazon.smithy.codegen.core.SymbolProvider +import software.amazon.smithy.model.Model +import software.amazon.smithy.model.knowledge.HttpBindingIndex +import software.amazon.smithy.model.neighbor.Walker +import software.amazon.smithy.model.shapes.ServiceShape +import software.amazon.smithy.model.shapes.Shape +import software.amazon.smithy.model.shapes.ShapeVisitor +import software.amazon.smithy.model.shapes.StructureShape +import software.amazon.smithy.model.transform.ModelTransformer +import java.util.logging.Logger + +class CodegenVisitor( + context: PluginContext +) : ShapeVisitor.Default() { + private val logger = Logger.getLogger(javaClass.name) + private val settings = OpenSearchJavaSettings.from(context.model, context.settings) + + private val model: Model + private val service: ServiceShape + private val fileManifest: FileManifest = context.fileManifest + private val symbolProvider: JavaSymbolProvider + private val httpBindingIndex: HttpBindingIndex + private val writers: JavaDelegator + private val generationContext: GenerationContext + + init { + model = baselineTransform(context.model) + service = settings.getService(model) + symbolProvider = JavaSymbolProvider(model, settings) + httpBindingIndex = HttpBindingIndex.of(model) + writers = JavaDelegator(fileManifest, symbolProvider, settings) + generationContext = GenerationContext(model, symbolProvider, httpBindingIndex, settings) + } + + private fun baselineTransform(model: Model) = + model + .let { ModelTransformer.create().flattenAndRemoveMixins(it) } + .let { ModelTransformer.create().copyServiceErrorsToOperations(it, settings.getService(it)) } + .let { OperationNormalizer.transform(it) } + + fun execute() { + val service = settings.getService(model) + val serviceShapes = Walker(model).walkShapes(service) + serviceShapes.forEach { it.accept(this) } + writers.finalize() + } + + override fun getDefault(shape: Shape?) {} + + override fun serviceShape(shape: ServiceShape) { + logger.info("Generating structure ${shape.id.name}") + writers.useShapeWriter(shape) { + val ctx = generationContext.toRenderingContext(it, shape) + ServiceGenerator(ctx).render() + } + val asyncSymbol = symbolProvider.serviceAsyncSymbol(shape) + writers.useSymbolWriter(asyncSymbol) { + val ctx = generationContext.toRenderingContext(it, shape, asyncSymbol) + ServiceGenerator(ctx).render() + } + } + + override fun structureShape(shape: StructureShape) { + writers.useShapeWriter(shape) { + val ctx = generationContext.toRenderingContext(it, shape) + StructureGenerator(ctx).render() + } + } +} \ No newline at end of file diff --git a/java-codegen/src/main/kotlin/org/opensearch/client/codegen/OpenSearchJavaCodegenPlugin.kt b/java-codegen/src/main/kotlin/org/opensearch/client/codegen/OpenSearchJavaCodegenPlugin.kt new file mode 100644 index 0000000000..f9b8b2cff7 --- /dev/null +++ b/java-codegen/src/main/kotlin/org/opensearch/client/codegen/OpenSearchJavaCodegenPlugin.kt @@ -0,0 +1,12 @@ +package org.opensearch.client.codegen + +import software.amazon.smithy.build.PluginContext +import software.amazon.smithy.build.SmithyBuildPlugin + +class OpenSearchJavaCodegenPlugin : SmithyBuildPlugin { + override fun getName(): String = "opensearch-java" + + override fun execute(context: PluginContext) { + CodegenVisitor(context).execute() + } +} \ No newline at end of file diff --git a/java-codegen/src/main/kotlin/org/opensearch/client/codegen/OpenSearchJavaSettings.kt b/java-codegen/src/main/kotlin/org/opensearch/client/codegen/OpenSearchJavaSettings.kt new file mode 100644 index 0000000000..9e4dd8146e --- /dev/null +++ b/java-codegen/src/main/kotlin/org/opensearch/client/codegen/OpenSearchJavaSettings.kt @@ -0,0 +1,70 @@ +package org.opensearch.client.codegen + +import org.opensearch.client.codegen.model.expectShape +import software.amazon.smithy.codegen.core.CodegenException +import software.amazon.smithy.model.Model +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.logging.Logger +import kotlin.streams.toList + +const val SERVICE: String = "service" + +class OpenSearchJavaSettings( + val service: ShapeId +) { + fun getService(model: Model): ServiceShape { + return model.expectShape(service) + } + + companion object { + private val LOGGER: Logger = Logger.getLogger(OpenSearchJavaSettings::class.java.name) + + fun from(model: Model, config: ObjectNode): OpenSearchJavaSettings { + config.warnIfAdditionalProperties( + arrayListOf( + SERVICE + ) + ) + + val service = config.getStringMember(SERVICE) + .map(StringNode::expectShapeId) + .orElseGet { inferService(model) } + + return OpenSearchJavaSettings(service) + } + + @JvmStatic + private 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 + } + } + } + } +} \ No newline at end of file diff --git a/java-codegen/src/main/kotlin/org/opensearch/client/codegen/core/CodegenContext.kt b/java-codegen/src/main/kotlin/org/opensearch/client/codegen/core/CodegenContext.kt new file mode 100644 index 0000000000..7317d32134 --- /dev/null +++ b/java-codegen/src/main/kotlin/org/opensearch/client/codegen/core/CodegenContext.kt @@ -0,0 +1,38 @@ +package org.opensearch.client.codegen.core + +import org.opensearch.client.codegen.OpenSearchJavaSettings +import software.amazon.smithy.codegen.core.Symbol +import software.amazon.smithy.codegen.core.SymbolProvider +import software.amazon.smithy.model.Model +import software.amazon.smithy.model.knowledge.HttpBindingIndex +import software.amazon.smithy.model.shapes.Shape + +interface CodegenContext { + val model: Model + val symbolProvider: SymbolProvider + val httpBindingIndex: HttpBindingIndex + val settings: OpenSearchJavaSettings +} + +data class GenerationContext( + override val model: Model, + override val symbolProvider: SymbolProvider, + override val httpBindingIndex: HttpBindingIndex, + override val settings: OpenSearchJavaSettings +) : CodegenContext + +fun CodegenContext.toRenderingContext(writer: JavaWriter, forShape: T? = null, symbol: Symbol? = null) = + RenderingContext(this, writer, forShape, symbol) + +data class RenderingContext( + val writer: JavaWriter, + val shape: T?, + val symbol: Symbol, + override val model: Model, + override val symbolProvider: SymbolProvider, + override val httpBindingIndex: HttpBindingIndex, + override val settings: OpenSearchJavaSettings +) : CodegenContext { + constructor(other: CodegenContext, writer: JavaWriter, shape: T?, symbol: Symbol? = null) : + this(writer, shape, symbol ?: other.symbolProvider.toSymbol(shape), other.model, other.symbolProvider, other.httpBindingIndex, other.settings) +} \ No newline at end of file diff --git a/java-codegen/src/main/kotlin/org/opensearch/client/codegen/core/ImportStatements.kt b/java-codegen/src/main/kotlin/org/opensearch/client/codegen/core/ImportStatements.kt new file mode 100644 index 0000000000..bfff96e3b5 --- /dev/null +++ b/java-codegen/src/main/kotlin/org/opensearch/client/codegen/core/ImportStatements.kt @@ -0,0 +1,30 @@ +package org.opensearch.client.codegen.core + +import software.amazon.smithy.codegen.core.CodegenException +import software.amazon.smithy.codegen.core.ImportContainer +import software.amazon.smithy.codegen.core.Symbol + +class ImportStatements(private val packageName: String) : ImportContainer { + private val imports: MutableSet = mutableSetOf() + + override fun importSymbol(symbol: Symbol, alias: String?) { + if (alias != symbol.name) { + throw CodegenException("Java doesn't allow import aliasing") + } + + if (symbol.namespace.isNotEmpty() && symbol.namespace != packageName) { + imports.add(ImportStatement(symbol.namespace, symbol.name)) + } + } + + override fun toString(): String { + return imports.map { it.toString() }.sorted().joinToString(separator = "\n") + } +} + +private data class ImportStatement(val packageName: String, val symbolName: String) { + val rendered: String + get() = "import $packageName.$symbolName;" + + override fun toString(): String = rendered +} \ No newline at end of file diff --git a/java-codegen/src/main/kotlin/org/opensearch/client/codegen/core/JavaDelegator.kt b/java-codegen/src/main/kotlin/org/opensearch/client/codegen/core/JavaDelegator.kt new file mode 100644 index 0000000000..42410a3a6d --- /dev/null +++ b/java-codegen/src/main/kotlin/org/opensearch/client/codegen/core/JavaDelegator.kt @@ -0,0 +1,27 @@ +package org.opensearch.client.codegen.core + +import org.opensearch.client.codegen.OpenSearchJavaSettings +import software.amazon.smithy.build.FileManifest +import software.amazon.smithy.codegen.core.* +import software.amazon.smithy.model.shapes.Shape +import java.util.function.Consumer + +class JavaDelegator( + fileManifest: FileManifest, + symbolProvider: SymbolProvider, + settings: OpenSearchJavaSettings +) { + private val inner = WriterDelegator(fileManifest, symbolProvider, JavaWriter.factory()) + + fun useShapeWriter(shape: Shape, writerConsumer: Consumer) { + inner.useShapeWriter(shape, writerConsumer) + } + + fun useSymbolWriter(symbol: Symbol, writerConsumer: Consumer) { + inner.useFileWriter(symbol.definitionFile, symbol.namespace, writerConsumer) + } + + fun finalize() { + inner.flushWriters() + } +} \ No newline at end of file diff --git a/java-codegen/src/main/kotlin/org/opensearch/client/codegen/core/JavaSymbolProvider.kt b/java-codegen/src/main/kotlin/org/opensearch/client/codegen/core/JavaSymbolProvider.kt new file mode 100644 index 0000000000..d97b523374 --- /dev/null +++ b/java-codegen/src/main/kotlin/org/opensearch/client/codegen/core/JavaSymbolProvider.kt @@ -0,0 +1,176 @@ +package org.opensearch.client.codegen.core + +import org.opensearch.client.codegen.OpenSearchJavaSettings +import org.opensearch.client.codegen.model.SymbolProperty +import org.opensearch.client.codegen.model.boxed +import org.opensearch.client.codegen.model.nullable +import software.amazon.smithy.codegen.core.ReservedWordSymbolProvider +import software.amazon.smithy.codegen.core.Symbol +import software.amazon.smithy.codegen.core.SymbolProvider +import software.amazon.smithy.model.Model +import software.amazon.smithy.model.knowledge.NullableIndex +import software.amazon.smithy.model.shapes.* +import java.util.logging.Logger + +class JavaSymbolProvider( + private val model: Model, + private val settings: OpenSearchJavaSettings +) : SymbolProvider, ShapeVisitor { + private val rootNamespace = "org.opensearch.client" + private val service = settings.getService(model) + private val logger = Logger.getLogger(javaClass.name) + private val escaper: ReservedWordSymbolProvider.Escaper + private val nullableIndex = NullableIndex(model) + + private var depth = 0 + + init { + val reservedWords = javaReservedWords() + escaper = ReservedWordSymbolProvider.builder() + .nameReservedWords(reservedWords) + .memberReservedWords(reservedWords) + .escapePredicate { _, symbol -> symbol.definitionFile.isNotEmpty() } + .buildEscaper() + } + + companion object { + fun isTypeGeneratedForShape(shape: Shape): Boolean = when { + shape.isEnumShape || shape.isIntEnumShape || shape.isStructureShape || shape.isUnionShape -> true + else -> false + } + } + + override fun toSymbol(shape: Shape): Symbol { + depth++ + val symbol: Symbol = shape.accept(this) + depth-- + logger.fine("creating symbol from $shape: $symbol") + return escaper.escapeSymbol(shape, symbol) + } + + override fun toMemberName(shape: MemberShape): String = escaper.escapeMemberName(shape.defaultName()) + + override fun blobShape(shape: BlobShape?): Symbol { + TODO("Not yet implemented") + } + + override fun booleanShape(shape: BooleanShape?): Symbol = + createSymbolBuilder(shape, "boolean").build() + + override fun listShape(shape: ListShape): Symbol { + val reference = toSymbol(shape.member).toBuilder().boxed().build() + return createSymbolBuilder(shape, "List<${reference.name}>") + .namespace("java.util", ".") + .addReference(reference) + .build() + } + + override fun mapShape(shape: MapShape): Symbol { + val reference = toSymbol(shape.value).toBuilder().boxed().build() + return createSymbolBuilder(shape, "Map") + .namespace("java.util", ".") + .addReference(reference) + .build() + } + + override fun byteShape(shape: ByteShape?): Symbol { + TODO("Not yet implemented") + } + + override fun shortShape(shape: ShortShape?): Symbol { + TODO("Not yet implemented") + } + + override fun integerShape(shape: IntegerShape?): Symbol = + createSymbolBuilder(shape, "int").build() + + override fun longShape(shape: LongShape?): Symbol { + TODO("Not yet implemented") + } + + override fun floatShape(shape: FloatShape?): Symbol { + TODO("Not yet implemented") + } + + override fun documentShape(shape: DocumentShape?): Symbol { + TODO("Not yet implemented") + } + + override fun doubleShape(shape: DoubleShape?): Symbol { + TODO("Not yet implemented") + } + + override fun bigIntegerShape(shape: BigIntegerShape?): Symbol { + TODO("Not yet implemented") + } + + override fun bigDecimalShape(shape: BigDecimalShape?): Symbol { + TODO("Not yet implemented") + } + + override fun operationShape(shape: OperationShape): Symbol { + val name = shape.defaultName() + return createSymbolBuilder(shape, name) + .namespace(rootNamespace, ".") + .definitionFile("$name.java") + .build() + } + + override fun resourceShape(shape: ResourceShape?): Symbol { + TODO("Not yet implemented") + } + + override fun serviceShape(shape: ServiceShape): Symbol { + val name = shape.defaultName() + return createSymbolBuilder(shape, name) + .namespace(rootNamespace, ".") + .definitionFile("$name.java") + .build() + } + + public fun serviceAsyncSymbol(shape: ServiceShape): Symbol { + val name = shape.asyncName() + return createSymbolBuilder(shape, name) + .namespace(rootNamespace, ".") + .definitionFile("$name.java") + .build() + } + + override fun stringShape(shape: StringShape?): Symbol = + createSymbolBuilder(shape, "String").build() + + override fun structureShape(shape: StructureShape): Symbol { + val name = shape.defaultName() + return createSymbolBuilder(shape, name) + .namespace(rootNamespace, ".") + .definitionFile("$name.java") + .build() + } + + override fun unionShape(shape: UnionShape?): Symbol { + TODO("Not yet implemented") + } + + override fun memberShape(shape: MemberShape): Symbol { + val targetShape = model.expectShape(shape.target) + + val targetSymbol = if (nullableIndex.isMemberNullable(shape, NullableIndex.CheckMode.CLIENT)) { + toSymbol(targetShape).toBuilder().nullable().build() + } else { + toSymbol(targetShape) + } + + return targetSymbol + } + + override fun timestampShape(shape: TimestampShape?): Symbol { + TODO("Not yet implemented") + } + + private fun createSymbolBuilder(shape: Shape?, typeName: String): Symbol.Builder { + val builder = Symbol.builder() + .putProperty(SymbolProperty.SHAPE_KEY, shape) + .name(typeName) + return builder + } +} \ No newline at end of file diff --git a/java-codegen/src/main/kotlin/org/opensearch/client/codegen/core/JavaVisibility.kt b/java-codegen/src/main/kotlin/org/opensearch/client/codegen/core/JavaVisibility.kt new file mode 100644 index 0000000000..cdf8454358 --- /dev/null +++ b/java-codegen/src/main/kotlin/org/opensearch/client/codegen/core/JavaVisibility.kt @@ -0,0 +1,11 @@ +package org.opensearch.client.codegen.core + +enum class JavaVisibility { + PUBLIC, + PROTECTED, + PRIVATE; + + override fun toString(): String { + return super.toString().lowercase() + } +} \ No newline at end of file diff --git a/java-codegen/src/main/kotlin/org/opensearch/client/codegen/core/JavaWriter.kt b/java-codegen/src/main/kotlin/org/opensearch/client/codegen/core/JavaWriter.kt new file mode 100644 index 0000000000..1a128c24eb --- /dev/null +++ b/java-codegen/src/main/kotlin/org/opensearch/client/codegen/core/JavaWriter.kt @@ -0,0 +1,94 @@ +package org.opensearch.client.codegen.core + +import software.amazon.smithy.codegen.core.CodegenException +import software.amazon.smithy.codegen.core.Symbol +import software.amazon.smithy.codegen.core.SymbolWriter +import software.amazon.smithy.codegen.core.SymbolWriter.Factory +import software.amazon.smithy.utils.AbstractCodeWriter +import java.util.function.BiFunction + +class JavaWriter private constructor( + private val filename: String, + val packageName: String +) : SymbolWriter(ImportStatements(packageName)) { + companion object { + fun factory(): Factory = + Factory { fileName: String, packageName: String -> JavaWriter(fileName, packageName) } + } + + init { + trimBlankLines() + trimTrailingSpaces() + + indentText = " " + expressionStart = '#' + + putFormatter('T', JavaSymbolFormatter(this)) + } + + fun addImport(symbol: Symbol, alias: String = symbol.name): JavaWriter { + return super.addImport(symbol, alias) + } + + override fun toString(): String { + val contents = super.toString() + val pkgDecl = "package $packageName;\n\n" + val imports = "${importContainer}\n\n" + return pkgDecl + imports + contents + } +} + +private class JavaSymbolFormatter( + private val writer: JavaWriter +) : BiFunction { + override fun apply(type: Any, indent: String): String { + when (type) { + is Symbol -> { + writer.addImport(type) + return type.name + } + + else -> throw CodegenException("Invalid type provided for #T. Expected a Symbol, but found `$type`") + } + } +} + +fun > T.block( + header: String, + vararg args: Any, + block: T.() -> Unit +): T { + openBlock("$header {", *args) + block(this) + closeBlock("}") + return this +} + +fun > T.javaClass( + visibility: JavaVisibility, + name: String, + annotations: List? = null, + extends: Symbol? = null, + implements: List? = null, + body: T.() -> Unit +): T { + annotations?.forEach { + write("@#T", it) + } + var header = format( + "#L #L", + visibility, + name + ) + if (extends != null) { + header = format("#L extends #T", header, extends) + } + if (!implements.isNullOrEmpty()) { + header = format( + "#L implements #L", + header, + implements.joinToString(separator = ", ") { format("#T", it) } + ) + } + return block(header, block = body) +} \ No newline at end of file diff --git a/java-codegen/src/main/kotlin/org/opensearch/client/codegen/core/Naming.kt b/java-codegen/src/main/kotlin/org/opensearch/client/codegen/core/Naming.kt new file mode 100644 index 0000000000..0f2bd04aa7 --- /dev/null +++ b/java-codegen/src/main/kotlin/org/opensearch/client/codegen/core/Naming.kt @@ -0,0 +1,18 @@ +package org.opensearch.client.codegen.core + +import org.opensearch.client.codegen.utils.toCamelCase +import org.opensearch.client.codegen.utils.toPascalCase +import software.amazon.smithy.model.shapes.MemberShape +import software.amazon.smithy.model.shapes.OperationShape +import software.amazon.smithy.model.shapes.ServiceShape +import software.amazon.smithy.model.shapes.StructureShape + +fun MemberShape.defaultName(): String = memberName.toCamelCase() + +fun OperationShape.defaultName(): String = id.name.toPascalCase() + +fun ServiceShape.defaultName(): String = id.name.toPascalCase() + "Client" + +fun ServiceShape.asyncName(): String = id.name.toPascalCase() + "AsyncClient" + +fun StructureShape.defaultName(): String = id.name.toPascalCase() \ No newline at end of file diff --git a/java-codegen/src/main/kotlin/org/opensearch/client/codegen/core/ReservedWords.kt b/java-codegen/src/main/kotlin/org/opensearch/client/codegen/core/ReservedWords.kt new file mode 100644 index 0000000000..cef3dde00d --- /dev/null +++ b/java-codegen/src/main/kotlin/org/opensearch/client/codegen/core/ReservedWords.kt @@ -0,0 +1,12 @@ +package org.opensearch.client.codegen.core + +import software.amazon.smithy.codegen.core.ReservedWords +import software.amazon.smithy.codegen.core.ReservedWordsBuilder + +fun javaReservedWords(): ReservedWords = ReservedWordsBuilder().apply { + hardReservedWords.forEach { put(it, "_$it") } +}.build() + +val hardReservedWords = listOf( + "null" +) \ No newline at end of file diff --git a/java-codegen/src/main/kotlin/org/opensearch/client/codegen/core/RuntimeTypes.kt b/java-codegen/src/main/kotlin/org/opensearch/client/codegen/core/RuntimeTypes.kt new file mode 100644 index 0000000000..a26d6dc5e2 --- /dev/null +++ b/java-codegen/src/main/kotlin/org/opensearch/client/codegen/core/RuntimeTypes.kt @@ -0,0 +1,73 @@ +package org.opensearch.client.codegen.core + +import org.opensearch.client.codegen.model.toSymbol + +object RuntimeTypes { + private const val ClientPkg = "org.opensearch.client" + + val ApiClient = "$ClientPkg.ApiClient".toSymbol() + + object Json { + private const val JsonPkg = "$ClientPkg.json" + + val JsonpDeserializable = "$JsonPkg.JsonpDeserializable".toSymbol() + val JsonpDeserializer = "$JsonPkg.JsonpDeserializer".toSymbol() + val JsonpMapper = "$JsonPkg.JsonpMapper".toSymbol() + val JsonpSerializable = "$JsonPkg.JsonpSerializable".toSymbol() + val ObjectBuilderDeserializer = "$JsonPkg.ObjectBuilderDeserializer".toSymbol() + val ObjectDeserializer = "$JsonPkg.ObjectDeserializer".toSymbol() + } + + object OpenSearch { + private const val OpenSearchPkg = "$ClientPkg.opensearch" + + object Types { + private const val TypesPkg = "$OpenSearchPkg._types" + + val ErrorResponse = "$TypesPkg.ErrorResponse".toSymbol() + val OpenSearchException = "$TypesPkg.OpenSearchException".toSymbol() + val RequestBase = "$TypesPkg.RequestBase".toSymbol() + } + } + + object Transport { + private const val TransportPkg = "$ClientPkg.transport" + + val Endpoint = "$TransportPkg.Endpoint".toSymbol() + val JsonEndpoint = "$TransportPkg.JsonEndpoint".toSymbol() + val OpenSearchTransport = "$TransportPkg.OpenSearchTransport".toSymbol() + val TransportOptions = "$TransportPkg.TransportOptions".toSymbol() + + object Endpoints { + private const val EndpointsPkg = "$TransportPkg.endpoints" + + val SimpleEndpoint = "$EndpointsPkg.SimpleEndpoint".toSymbol() + } + } + + object Util { + private const val UtilPkg = "$ClientPkg.util" + + val ApiTypeHelper = "$UtilPkg.ApiTypeHelper".toSymbol() + val ObjectBuilder = "$UtilPkg.ObjectBuilder".toSymbol() + val ObjectBuilderBase = "$UtilPkg.ObjectBuilderBase".toSymbol() + } + + object Jakarta { + val JsonGenerator = "jakarta.json.stream.JsonGenerator".toSymbol() + } + + object JavaIo { + val IOException = "java.io.IOException".toSymbol() + } + + object JavaUtil { + val Function = "java.util.function.Function".toSymbol() + val HashMap = "java.util.HashMap".toSymbol() + val Map = "java.util.Map".toSymbol() + } + + object Javax { + val Nullable = "javax.annotation.Nullable".toSymbol() + } +} \ No newline at end of file diff --git a/java-codegen/src/main/kotlin/org/opensearch/client/codegen/core/traits/SyntheticInputTrait.kt b/java-codegen/src/main/kotlin/org/opensearch/client/codegen/core/traits/SyntheticInputTrait.kt new file mode 100644 index 0000000000..b3f7cb7fd9 --- /dev/null +++ b/java-codegen/src/main/kotlin/org/opensearch/client/codegen/core/traits/SyntheticInputTrait.kt @@ -0,0 +1,14 @@ +package org.opensearch.client.codegen.core.traits + +import software.amazon.smithy.model.node.Node +import software.amazon.smithy.model.shapes.ShapeId +import software.amazon.smithy.model.traits.AnnotationTrait + +class SyntheticInputTrait( + val operation: ShapeId, + val originalId: ShapeId?, +) : AnnotationTrait(ID, Node.objectNode()) { + companion object { + val ID: ShapeId = ShapeId.from("smithy.api.internal#syntheticInput") + } +} diff --git a/java-codegen/src/main/kotlin/org/opensearch/client/codegen/model/ModelExt.kt b/java-codegen/src/main/kotlin/org/opensearch/client/codegen/model/ModelExt.kt new file mode 100644 index 0000000000..aa89a19287 --- /dev/null +++ b/java-codegen/src/main/kotlin/org/opensearch/client/codegen/model/ModelExt.kt @@ -0,0 +1,8 @@ +package org.opensearch.client.codegen.model + +import software.amazon.smithy.model.Model +import software.amazon.smithy.model.shapes.Shape +import software.amazon.smithy.model.shapes.ShapeId + +@Suppress("EXTENSION_SHADOWED_BY_MEMBER") +inline fun Model.expectShape(id: ShapeId): T = expectShape(id, T::class.java) \ No newline at end of file diff --git a/java-codegen/src/main/kotlin/org/opensearch/client/codegen/model/OperationNormalizer.kt b/java-codegen/src/main/kotlin/org/opensearch/client/codegen/model/OperationNormalizer.kt new file mode 100644 index 0000000000..d6b414d499 --- /dev/null +++ b/java-codegen/src/main/kotlin/org/opensearch/client/codegen/model/OperationNormalizer.kt @@ -0,0 +1,64 @@ +package org.opensearch.client.codegen.model + +import org.opensearch.client.codegen.core.traits.SyntheticInputTrait +import software.amazon.smithy.model.Model +import software.amazon.smithy.model.shapes.OperationShape +import software.amazon.smithy.model.shapes.Shape +import software.amazon.smithy.model.shapes.ShapeId +import software.amazon.smithy.model.shapes.StructureShape +import software.amazon.smithy.model.transform.ModelTransformer +import java.util.* +import kotlin.streams.toList + +object OperationNormalizer { + private fun OperationShape.syntheticInputId() = + ShapeId.fromParts(this.id.namespace + ".synthetic", "${this.id.name}Request") + + private fun OperationShape.syntheticOutputId() = + ShapeId.fromParts(this.id.namespace + ".synthetic", "${this.id.name}Response") + + fun transform(model: Model): Model { + val transformer = ModelTransformer.create() + val operations = model.shapes(OperationShape::class.java).toList() + val newShapes = operations.flatMap { operation -> + // Generate or modify the input and output of the given `Operation` to be a unique shape + listOf(syntheticInputShape(model, operation), syntheticOutputShape(model, operation)) + } + val shapeConflict = newShapes.firstOrNull { shape -> model.getShape(shape.id).isPresent } + check( + shapeConflict == null, + ) { "shape $shapeConflict conflicted with an existing shape in the model (${model.getShape(shapeConflict!!.id)}. This is a bug." } + val modelWithOperationInputs = model.toBuilder().addShapes(newShapes).build() + return transformer.mapShapes(modelWithOperationInputs) { + // Update all operations to point to their new input/output shapes + val transformed: Optional = it.asOperationShape().map { operation -> + modelWithOperationInputs.expectShape(operation.syntheticInputId()) + operation.toBuilder() + .input(operation.syntheticInputId()) + .output(operation.syntheticOutputId()) + .build() + } + transformed.orElse(it) + } + } + + private fun syntheticOutputShape(model: Model, operation: OperationShape): StructureShape { + val outputId = operation.syntheticOutputId() + val outputShapeBuilder = operation.output.map { shapeId -> + model.expectShape(shapeId, StructureShape::class.java).toBuilder().rename(outputId) + }.orElse(empty(outputId)) + return outputShapeBuilder.build() + } + + private fun syntheticInputShape(model: Model, operation: OperationShape): StructureShape { + val inputId = operation.syntheticInputId() + val inputShapeBuilder = operation.input.map { shapeId -> + model.expectShape(shapeId, StructureShape::class.java).toBuilder().rename(inputId) + }.orElse(empty(inputId)) + return inputShapeBuilder.addTrait( + SyntheticInputTrait(operation.id, operation.input.orElse(null)) + ).build() + } + + private fun empty(id: ShapeId): StructureShape.Builder = StructureShape.builder().id(id) +} \ No newline at end of file diff --git a/java-codegen/src/main/kotlin/org/opensearch/client/codegen/model/ShapeExt.kt b/java-codegen/src/main/kotlin/org/opensearch/client/codegen/model/ShapeExt.kt new file mode 100644 index 0000000000..02b1921707 --- /dev/null +++ b/java-codegen/src/main/kotlin/org/opensearch/client/codegen/model/ShapeExt.kt @@ -0,0 +1,25 @@ +package org.opensearch.client.codegen.model + +import software.amazon.smithy.model.Model +import software.amazon.smithy.model.shapes.OperationShape +import software.amazon.smithy.model.shapes.Shape +import software.amazon.smithy.model.shapes.ShapeId +import software.amazon.smithy.model.shapes.StructureShape +import software.amazon.smithy.model.traits.Trait + +inline fun Shape.hasTrait(): Boolean = hasTrait(T::class.java) +inline fun Shape.getTrait(): T? = getTrait(T::class.java).orElse(null) +inline fun Shape.expectTrait(): T = expectTrait(T::class.java) + +fun OperationShape.inputShape(model: Model): StructureShape = model.expectShape(this.inputShape) +fun OperationShape.outputShape(model: Model): StructureShape = model.expectShape(this.outputShape) + +fun StructureShape.Builder.rename(newId: ShapeId): StructureShape.Builder { + val renamedMembers = this.build().members().map { + it.toBuilder().id(newId.withMember(it.memberName)).build() + } + return this.id(newId).members(renamedMembers) +} + +val Shape.isListOrMap: Boolean + get() = isListShape || isMapShape \ No newline at end of file diff --git a/java-codegen/src/main/kotlin/org/opensearch/client/codegen/model/SymbolExt.kt b/java-codegen/src/main/kotlin/org/opensearch/client/codegen/model/SymbolExt.kt new file mode 100644 index 0000000000..6f48e93456 --- /dev/null +++ b/java-codegen/src/main/kotlin/org/opensearch/client/codegen/model/SymbolExt.kt @@ -0,0 +1,60 @@ +package org.opensearch.client.codegen.model + +import software.amazon.smithy.codegen.core.Symbol +import software.amazon.smithy.model.shapes.Shape + +object SymbolProperty { + const val SHAPE_KEY: String = "shape" + const val NULLABLE_KEY: String = "nullable" +} + +fun String.toSymbol(): Symbol { + require(isNotBlank()) { "Invalid string to convert to symbol" } + val segments = split(".") + val name = segments.last() + val namespace = segments.dropLast(1).joinToString(separator = ".") { it } + return Symbol.builder() + .name(name) + .namespace(namespace, ".") + .build() +} + +fun Symbol.Builder.boxed(): Symbol.Builder { + val symbol = build() + val newName = if (symbol.namespace.isNullOrEmpty()) { + when (symbol.name) { + "byte" -> "Byte" + "short" -> "Short" + "int" -> "Integer" + "long" -> "Long" + "float" -> "Float" + "double" -> "Double" + "boolean" -> "Boolean" + "char" -> "Character" + else -> null + } + } else { + null + } + + return if (newName != null) { + this.name(newName) + } else { + this + } +} + +fun Symbol.Builder.nullable(): Symbol.Builder = + boxed() + .putProperty(SymbolProperty.NULLABLE_KEY, true) + +val Symbol.isNullable: Boolean + get() = getProperty(SymbolProperty.NULLABLE_KEY).map { + when (it) { + is Boolean -> it + else -> false + } + }.orElse(false) + +val Symbol.shape: Shape + get() = getProperty(SymbolProperty.SHAPE_KEY, Shape::class.java).get() \ No newline at end of file diff --git a/java-codegen/src/main/kotlin/org/opensearch/client/codegen/render/ServiceGenerator.kt b/java-codegen/src/main/kotlin/org/opensearch/client/codegen/render/ServiceGenerator.kt new file mode 100644 index 0000000000..d8e5265e7d --- /dev/null +++ b/java-codegen/src/main/kotlin/org/opensearch/client/codegen/render/ServiceGenerator.kt @@ -0,0 +1,91 @@ +package org.opensearch.client.codegen.render + +import org.opensearch.client.codegen.core.* +import org.opensearch.client.codegen.model.inputShape +import org.opensearch.client.codegen.model.outputShape +import org.opensearch.client.codegen.utils.toCamelCase +import software.amazon.smithy.codegen.core.SymbolProvider +import software.amazon.smithy.model.Model +import software.amazon.smithy.model.knowledge.TopDownIndex +import software.amazon.smithy.model.shapes.OperationShape +import software.amazon.smithy.model.shapes.ServiceShape + +class ServiceGenerator( + private val ctx: RenderingContext +) { + private val model = ctx.model + private val shape = ctx.shape + private val symbol = ctx.symbolProvider.toSymbol(ctx.shape) + private val symbolProvider = ctx.symbolProvider + private val writer = ctx.writer + private val index = TopDownIndex.of(model) + + fun render() { + writer.block( + "public class #1T extends #2T<#3T, #1T>", + symbol, RuntimeTypes.ApiClient, RuntimeTypes.Transport.OpenSearchTransport + ) { + renderConstructors() + + val operations = index.getContainedOperations(shape).sortedBy { it.id } + operations.forEach(::renderOperation) + } + } + + private fun renderConstructors() { + writer.block( + "public #T(#T transport)", + symbol, RuntimeTypes.Transport.OpenSearchTransport + ) { + write("super(transport);") + } + writer.write("") + + writer.block( + "public #T(#T transport, @#T #T transportOptions)", + symbol, RuntimeTypes.Transport.OpenSearchTransport, RuntimeTypes.Javax.Nullable, RuntimeTypes.Transport.TransportOptions + ) { + write("super(transport, transportOptions);") + } + writer.write("") + + writer.block( + "@Override public #T withTransportOptions(@#T #T transportOptions)", + symbol, RuntimeTypes.Javax.Nullable, RuntimeTypes.Transport.TransportOptions + ) { + write("return new #T(this.transport, transportOptions);", symbol) + } + writer.write("") + } + + private fun renderOperation(operation: OperationShape) { + val inputShape = operation.inputShape(model) + val inputSymbol = symbolProvider.toSymbol(inputShape) + val outputShape = operation.outputShape(model) + val outputSymbol = symbolProvider.toSymbol(outputShape) + val operationName = operation.id.name.toCamelCase() + + writer.block( + "public #T #L(#T request) throws #T, #T", + outputSymbol, operationName, inputSymbol, RuntimeTypes.JavaIo.IOException, RuntimeTypes.OpenSearch.Types.OpenSearchException + ) { + val endpointType = format("#T<#T, #T, #T>", RuntimeTypes.Transport.JsonEndpoint, inputSymbol, outputSymbol, RuntimeTypes.OpenSearch.Types.ErrorResponse) + + write(""" + @SuppressWarnings("unchecked") + #1L endpoint = (#1L) #2T._ENDPOINT; + + return this.transport.performRequest(request, endpoint, this.transportOptions); + """.trimIndent(), endpointType, inputSymbol) + } + writer.write("") + + writer.block( + "public final #1T #2L(#3T<#4T.Builder, #5T<#4T>> fn) throws #6T, #7T", + outputSymbol, operationName, RuntimeTypes.JavaUtil.Function, inputSymbol, RuntimeTypes.Util.ObjectBuilder, RuntimeTypes.JavaIo.IOException, RuntimeTypes.OpenSearch.Types.OpenSearchException + ) { + write("return #L(fn.apply(new #T.Builder()).build());", operationName, inputSymbol) + } + writer.write("") + } +} \ No newline at end of file diff --git a/java-codegen/src/main/kotlin/org/opensearch/client/codegen/render/StructureGenerator.kt b/java-codegen/src/main/kotlin/org/opensearch/client/codegen/render/StructureGenerator.kt new file mode 100644 index 0000000000..e39316d772 --- /dev/null +++ b/java-codegen/src/main/kotlin/org/opensearch/client/codegen/render/StructureGenerator.kt @@ -0,0 +1,450 @@ +package org.opensearch.client.codegen.render + +import org.opensearch.client.codegen.core.* +import org.opensearch.client.codegen.core.traits.SyntheticInputTrait +import org.opensearch.client.codegen.model.* +import org.opensearch.client.codegen.utils.dq +import software.amazon.smithy.codegen.core.Symbol +import software.amazon.smithy.model.knowledge.HttpBinding +import software.amazon.smithy.model.shapes.* +import software.amazon.smithy.model.traits.HttpQueryTrait +import software.amazon.smithy.model.traits.HttpTrait + +class StructureGenerator( + private val ctx: RenderingContext +) { + private val shape = requireNotNull(ctx.shape) + private val writer = ctx.writer + private val symbolProvider = ctx.symbolProvider + private val model = ctx.model + private val symbol = ctx.symbolProvider.toSymbol(ctx.shape) + private val isInput = shape.hasTrait() + + private val sortedMembers: List = shape.allMembers.values.sortedBy { it.defaultName() } + private val memberNameSymbolIndex: Map> = + sortedMembers.associateWith { member -> + Pair(symbolProvider.toMemberName(member), symbolProvider.toSymbol(member)) + } + + fun render() { + writer.javaClass( + JavaVisibility.PUBLIC, + symbol.name, + annotations = listOf(RuntimeTypes.Json.JsonpDeserializable), + extends = if (isInput) RuntimeTypes.OpenSearch.Types.RequestBase else null, + implements = listOf(RuntimeTypes.Json.JsonpSerializable) + ) { + renderFields() + renderConstructor() + renderGetters() + renderSerialize() + renderBuilder() + renderDeserialize() + if (isInput) { + renderEndpoint() + } + } + } + + private fun forEachMember(op: (memberShape: MemberShape, memberName: String, memberSymbol: Symbol) -> Unit) = + forEachMember(sortedMembers, op) + + private fun forEachMember( + members: List, + op: (memberShape: MemberShape, memberName: String, memberSymbol: Symbol) -> Unit + ) = + members.forEach { + val (memberName, memberSymbol) = memberNameSymbolIndex[it]!! + + op(it, memberName, memberSymbol) + } + + private fun renderFields(final: Boolean = true) { + forEachMember { _, memberName, memberSymbol -> + if (memberSymbol.isNullable && !(final && memberSymbol.shape.isListOrMap)) { + writer.write("@#T", RuntimeTypes.Javax.Nullable) + } + writer.write( + "private #L#T #L;", + if (final) "final " else "", + memberSymbol, memberName + ) + writer.write("") + } + } + + private fun renderConstructor() { + writer.block("private #T(Builder builder)", symbol) { + forEachMember { _, memberName, memberSymbol -> + var builderField = format("builder.#L", memberName) + + if (memberSymbol.shape.isListOrMap) { + builderField = if (memberSymbol.isNullable) { + format("#T.unmodifiable(#L)", RuntimeTypes.Util.ApiTypeHelper, builderField) + } else { + format( + "#T.unmodifiableRequired(#L, this, #L)", + RuntimeTypes.Util.ApiTypeHelper, builderField, memberName.dq() + ) + } + } else if (!memberSymbol.isNullable) { + builderField = format( + "#T.requireNonNull(#L, this, #L)", + RuntimeTypes.Util.ApiTypeHelper, builderField, memberName.dq() + ) + } + + write("this.#L = #L;", memberName, builderField) + } + } + writer.write("") + + writer.block( + "public static #1T of(#2T> fn)", + symbol, RuntimeTypes.JavaUtil.Function, RuntimeTypes.Util.ObjectBuilder + ) { + write("return fn.apply(new Builder()).build();") + } + writer.write("") + } + + private fun renderGetters() { + forEachMember { _, memberName, memberSymbol -> + if (memberSymbol.isNullable && !memberSymbol.shape.isListOrMap) { + writer.write("@#T", RuntimeTypes.Javax.Nullable) + } + + writer.block("public final #T #L()", memberSymbol, memberName) { + write("return this.#L;", memberName) + } + writer.write("") + } + } + + private fun renderSerialize() { + writer.block( + "public void serialize(#T generator, #T mapper)", + RuntimeTypes.Jakarta.JsonGenerator, RuntimeTypes.Json.JsonpMapper + ) { + write( + """ + generator.writeStartObject(); + serializeInternal(generator, mapper); + generator.writeEnd(); + """.trimIndent() + ) + } + writer.write("") + + writer.block( + "protected void serializeInternal(#T generator, #T mapper)", + RuntimeTypes.Jakarta.JsonGenerator, RuntimeTypes.Json.JsonpMapper + ) { + forEachMember(getDocumentFields(), ::renderSerializeField) + } + writer.write("") + } + + private fun getDocumentFields(): List = + if (!isInput) { + sortedMembers + } else { + ctx.httpBindingIndex.getRequestBindings( + shape.expectTrait().operation, + HttpBinding.Location.DOCUMENT + ) + .map { it.member } + .sortedBy { it.memberName } + } + + private fun renderSerializeField(memberShape: MemberShape, memberName: String, memberSymbol: Symbol) { + writer.ifValueDefined("this.${memberName}", memberSymbol) { + write("generator.writeKey(${memberShape.memberName.dq()});") + renderSerializeValue("this.${memberName}", memberSymbol.shape) + } + } + + private fun renderSerializeValue(value: String, valueShape: Shape) { + when (valueShape) { + is StructureShape -> writer.write("${value}.serialize(generator, mapper);") + is ListShape -> { + val elementSymbol = symbolProvider.toSymbol(valueShape.elementShape).toBuilder().boxed().build() + writer.write("generator.writeStartArray();") + writer.block("for (#T item : #L)", elementSymbol, value) { + renderSerializeValue("item", valueShape.elementShape) + } + writer.write("generator.writeEnd();") + } + + is MapShape -> { + val elementSymbol = symbolProvider.toSymbol(valueShape.elementShape).toBuilder().boxed().build() + writer.write("generator.writeStartObject();") + writer.block( + "for (#T.Entry item : #L.entrySet())", + RuntimeTypes.JavaUtil.Map, + elementSymbol, + value + ) { + write("generator.writeKey(item.getKey());") + renderSerializeValue("item.getValue()", valueShape.elementShape) + } + writer.write("generator.writeEnd();") + } + + else -> writer.write("generator.write(${value});") + } + } + + private fun JavaWriter.ifValueDefined(value: String, valueSymbol: Symbol, block: JavaWriter.() -> Unit) { + if (!valueSymbol.isNullable) { + this.block() + return + } + + val check = if (valueSymbol.shape.isListOrMap) { + format("#T.isDefined($value)", RuntimeTypes.Util.ApiTypeHelper) + } else { + "$value != null" + } + + this.block("if (${check})", block = block) + } + + private fun renderBuilder() { + writer.block( + "public static class Builder extends #T implements #T<#T>", + RuntimeTypes.Util.ObjectBuilderBase, RuntimeTypes.Util.ObjectBuilder, symbol + ) { + renderFields(false) + + forEachMember { memberShape, memberName, memberSymbol -> + when (val valueShape = memberShape.targetShape) { + is ListShape -> { + block("public final Builder #L(#T list)", memberName, memberSymbol) { + write( + """ + this.#1L = _listAddAll(this.#1L, list); + return this; + """.trimIndent(), memberName + ) + } + write("") + + val elementSymbol = symbolProvider.toSymbol(valueShape.elementShape) + block("public final Builder #1L(#2T value, #2T... values)", memberName, elementSymbol) { + write( + """ + this.#1L = _listAdd(this.#1L, value, values); + return this; + """.trimIndent(), memberName + ) + } + } + + is MapShape -> { + block("public final Builder #L(#T map)", memberName, memberSymbol) { + write( + """ + this.#1L = _mapPutAll(this.#1L, map); + return this; + """.trimIndent(), memberName + ) + } + write("") + + val elementSymbol = symbolProvider.toSymbol(valueShape.elementShape) + block("public final Builder #L(String key, #T value)", memberName, elementSymbol) { + write( + """ + this.#1L = _mapPut(this.#1L, key, value); + return this; + """.trimIndent(), memberName + ) + } + } + + else -> { + block("public final Builder #L(#T value)", memberName, memberSymbol) { + write( + """ + this.#L = value; + return this; + """.trimIndent(), memberName + ) + } + } + } + + write("") + } + + block("public #T build()", symbol) { + write( + """ + _checkSingleUse(); + return new #T(this); + """.trimIndent(), symbol + ) + } + } + writer.write("") + } + + private fun renderDeserialize() { + val setupDeserializer = writer.format("setup#LDeserializer", symbol.name) + + writer.write( + "public static final #1T<#2T> _DESERIALIZER = #3T.lazy(Builder::new, #2T::#4L);", + RuntimeTypes.Json.JsonpDeserializer, + symbol, + RuntimeTypes.Json.ObjectBuilderDeserializer, + setupDeserializer + ) + writer.write("") + + writer.block( + "protected static void #L(#T<#T.Builder> op)", + setupDeserializer, RuntimeTypes.Json.ObjectDeserializer, symbol + ) { + forEachMember(getDocumentFields()) { memberShape, memberName, memberSymbol -> + write("op.add(Builder::${memberName}, ${valueDeserializer(memberSymbol.shape)}, ${memberShape.memberName.dq()});") + } + } + writer.write("") + } + + private fun valueDeserializer(valueShape: Shape): String = + when (valueShape) { + is StructureShape -> writer.format("#T._DESERIALIZER", symbolProvider.toSymbol(valueShape)) + is ListShape -> { + writer.format( + "#T.arrayDeserializer(#L)", + RuntimeTypes.Json.JsonpDeserializer, + valueDeserializer(valueShape.elementShape) + ) + } + + is MapShape -> { + writer.format( + "#T.stringMapDeserializer(#L)", + RuntimeTypes.Json.JsonpDeserializer, + valueDeserializer(valueShape.elementShape) + ) + } + + is BooleanShape -> writer.format("#T.booleanDeserializer()", RuntimeTypes.Json.JsonpDeserializer) + is StringShape -> writer.format("#T.stringDeserializer()", RuntimeTypes.Json.JsonpDeserializer) + is IntegerShape -> writer.format("#T.integerDeserializer()", RuntimeTypes.Json.JsonpDeserializer) + else -> TODO("Output correct deserializer for $valueShape") + } + + private val MemberShape.targetShape: Shape + get() = model.expectShape(target) + + private val ListShape.elementShape: Shape + get() = member.targetShape + + private val MapShape.elementShape: Shape + get() = value.targetShape + + private fun renderEndpoint() { + val inputTrait = shape.expectTrait() + val operation = model.expectShape(inputTrait.operation) + val httpTrait = operation.expectTrait() + val outputShape = model.expectShape(operation.outputShape) + val outputSymbol = symbolProvider.toSymbol(outputShape) + + writer.openBlock( + "public static final #T<#T, #T, #T> _ENDPOINT = new #T<>(", + RuntimeTypes.Transport.Endpoint, + symbol, + outputSymbol, + RuntimeTypes.OpenSearch.Types.ErrorResponse, + RuntimeTypes.Transport.Endpoints.SimpleEndpoint + ) + + writer.write( + """ + // Request method + request -> ${httpTrait.method.dq()}, + + """.trimIndent() + ) + + writer.write("// Request path") + writer.openBlock("request -> {") + writer.write("StringBuilder buf = new StringBuilder();") + val labelMembers = ctx.httpBindingIndex.getRequestBindings(operation, HttpBinding.Location.LABEL) + .map { it.member } + .associateBy { it.memberName } + httpTrait.uri.segments.forEach { segment -> + writer.write("buf.append('/');") + if (segment.isLabel || segment.isGreedyLabel) { + writer.write( + "#T.pathEncode(request.#L, buf);", + RuntimeTypes.Transport.Endpoints.SimpleEndpoint, + symbolProvider.toMemberName(labelMembers[segment.content]) + ) + } else { + writer.write("buf.append(${segment.content.dq()});") + } + } + writer.write("return buf.toString();") + writer.closeBlock("},\n") + + writer.write("// Request parameters") + writer.openBlock("request -> {") + writer.write( + "#T params = new #T<>();", + RuntimeTypes.JavaUtil.Map, + RuntimeTypes.JavaUtil.HashMap + ) + val paramMembers = ctx.httpBindingIndex.getRequestBindings(operation, HttpBinding.Location.QUERY) + .map { it.member } + .sortedBy { it.memberName } + forEachMember(paramMembers, ::renderQueryParam) + writer.write("return params;") + writer.closeBlock("},\n") + + // TODO: headers? + writer.write( + """ + // Request headers + #T.emptyMap(), + """.trimIndent(), RuntimeTypes.Transport.Endpoints.SimpleEndpoint + ) + + // TODO: maybe no requestbody? + writer.write( + """ + // Has request body + true, + """.trimIndent() + ) + + writer.write( + """ + // Response deserializer + #T._DESERIALIZER + """.trimIndent(), outputSymbol + ) + + writer.closeBlock(");") + } + + private fun renderQueryParam(memberShape: MemberShape, memberName: String, memberSymbol: Symbol) { + val queryTrait = memberShape.expectTrait() + + val value = "request.${memberName}" + writer.ifValueDefined(value, memberSymbol) { + write("params.put(${queryTrait.value.dq()}, ${queryParamValueOf(value, memberSymbol.shape)});") + } + } + + private fun queryParamValueOf(value: String, valueShape: Shape): String = + when (valueShape) { + is StringShape -> value + is BooleanShape -> "String.valueOf($value)" + else -> TODO("Output query param value getter for $valueShape") + } +} \ No newline at end of file diff --git a/java-codegen/src/main/kotlin/org/opensearch/client/codegen/utils/Strings.kt b/java-codegen/src/main/kotlin/org/opensearch/client/codegen/utils/Strings.kt new file mode 100644 index 0000000000..7d0a4c0b5e --- /dev/null +++ b/java-codegen/src/main/kotlin/org/opensearch/client/codegen/utils/Strings.kt @@ -0,0 +1,12 @@ +package org.opensearch.client.codegen.utils + +import software.amazon.smithy.utils.CaseUtils +import software.amazon.smithy.utils.StringUtils + +fun String.dq(): String = StringUtils.escapeJavaString(this, "") + +fun String.toSnakeCase(): String = CaseUtils.toSnakeCase(this) + +fun String.toPascalCase(): String = CaseUtils.toPascalCase(toSnakeCase()) + +fun String.toCamelCase(): String = CaseUtils.toCamelCase(toSnakeCase()) diff --git a/java-codegen/src/main/resources/META-INF/services/software.amazon.smithy.build.SmithyBuildPlugin b/java-codegen/src/main/resources/META-INF/services/software.amazon.smithy.build.SmithyBuildPlugin new file mode 100644 index 0000000000..b582707e72 --- /dev/null +++ b/java-codegen/src/main/resources/META-INF/services/software.amazon.smithy.build.SmithyBuildPlugin @@ -0,0 +1 @@ +org.opensearch.client.codegen.OpenSearchJavaCodegenPlugin \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 2f1f60b93b..8288e72240 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -32,3 +32,4 @@ rootProject.name = "opensearch-java" include("java-client") +include("java-codegen") From 430b86bf7196dfa52a14d560abfe2ca73fb3c3f7 Mon Sep 17 00:00:00 2001 From: Thomas Farr Date: Tue, 31 Jan 2023 17:19:51 +1300 Subject: [PATCH 2/2] Rewrite from Kotlin to Java Signed-off-by: Thomas Farr --- java-codegen/build.gradle.kts | 16 +- .../client/codegen/CodegenVisitor.java | 91 ++++ .../codegen/OpenSearchJavaCodegenPlugin.java | 16 + .../codegen/OpenSearchJavaSettings.java | 51 ++ .../client/codegen/core/CodegenContext.java | 16 + .../codegen/core/GenerationContext.java | 40 ++ .../client/codegen/core/ImportStatements.java | 69 +++ .../client/codegen/core/JavaDelegator.java | 29 ++ .../codegen/core/JavaReservedWords.java | 23 + .../codegen/core/JavaSymbolProvider.java | 182 +++++++ .../client/codegen/core/JavaVisibility.java | 12 + .../client/codegen/core/JavaWriter.java | 84 ++++ .../client/codegen/core/Naming.java | 29 ++ .../client/codegen/core/RenderingContext.java | 67 +++ .../client/codegen/core/RuntimeTypes.java | 75 +++ .../core/traits/SyntheticInputTrait.java | 19 + .../codegen/model/OperationNormalizer.java | 77 +++ .../client/codegen/model/Shapes.java | 30 ++ .../client/codegen/model/Symbols.java | 82 ++++ .../codegen/render/ServiceGenerator.java | 109 +++++ .../codegen/render/StructureGenerator.java | 439 +++++++++++++++++ .../client/codegen/utils/Strings.java | 22 + .../client/codegen/CodegenVisitor.kt | 80 ---- .../codegen/OpenSearchJavaCodegenPlugin.kt | 12 - .../client/codegen/OpenSearchJavaSettings.kt | 70 --- .../client/codegen/core/CodegenContext.kt | 38 -- .../client/codegen/core/ImportStatements.kt | 30 -- .../client/codegen/core/JavaDelegator.kt | 27 -- .../client/codegen/core/JavaSymbolProvider.kt | 176 ------- .../client/codegen/core/JavaVisibility.kt | 11 - .../client/codegen/core/JavaWriter.kt | 94 ---- .../opensearch/client/codegen/core/Naming.kt | 18 - .../client/codegen/core/ReservedWords.kt | 12 - .../client/codegen/core/RuntimeTypes.kt | 73 --- .../core/traits/SyntheticInputTrait.kt | 14 - .../client/codegen/model/ModelExt.kt | 8 - .../codegen/model/OperationNormalizer.kt | 64 --- .../client/codegen/model/ShapeExt.kt | 25 - .../client/codegen/model/SymbolExt.kt | 60 --- .../client/codegen/render/ServiceGenerator.kt | 91 ---- .../codegen/render/StructureGenerator.kt | 450 ------------------ .../client/codegen/utils/Strings.kt | 12 - 42 files changed, 1565 insertions(+), 1378 deletions(-) create mode 100644 java-codegen/src/main/java/org/opensearch/client/codegen/CodegenVisitor.java create mode 100644 java-codegen/src/main/java/org/opensearch/client/codegen/OpenSearchJavaCodegenPlugin.java create mode 100644 java-codegen/src/main/java/org/opensearch/client/codegen/OpenSearchJavaSettings.java create mode 100644 java-codegen/src/main/java/org/opensearch/client/codegen/core/CodegenContext.java create mode 100644 java-codegen/src/main/java/org/opensearch/client/codegen/core/GenerationContext.java create mode 100644 java-codegen/src/main/java/org/opensearch/client/codegen/core/ImportStatements.java create mode 100644 java-codegen/src/main/java/org/opensearch/client/codegen/core/JavaDelegator.java create mode 100644 java-codegen/src/main/java/org/opensearch/client/codegen/core/JavaReservedWords.java create mode 100644 java-codegen/src/main/java/org/opensearch/client/codegen/core/JavaSymbolProvider.java create mode 100644 java-codegen/src/main/java/org/opensearch/client/codegen/core/JavaVisibility.java create mode 100644 java-codegen/src/main/java/org/opensearch/client/codegen/core/JavaWriter.java create mode 100644 java-codegen/src/main/java/org/opensearch/client/codegen/core/Naming.java create mode 100644 java-codegen/src/main/java/org/opensearch/client/codegen/core/RenderingContext.java create mode 100644 java-codegen/src/main/java/org/opensearch/client/codegen/core/RuntimeTypes.java create mode 100644 java-codegen/src/main/java/org/opensearch/client/codegen/core/traits/SyntheticInputTrait.java create mode 100644 java-codegen/src/main/java/org/opensearch/client/codegen/model/OperationNormalizer.java create mode 100644 java-codegen/src/main/java/org/opensearch/client/codegen/model/Shapes.java create mode 100644 java-codegen/src/main/java/org/opensearch/client/codegen/model/Symbols.java create mode 100644 java-codegen/src/main/java/org/opensearch/client/codegen/render/ServiceGenerator.java create mode 100644 java-codegen/src/main/java/org/opensearch/client/codegen/render/StructureGenerator.java create mode 100644 java-codegen/src/main/java/org/opensearch/client/codegen/utils/Strings.java delete mode 100644 java-codegen/src/main/kotlin/org/opensearch/client/codegen/CodegenVisitor.kt delete mode 100644 java-codegen/src/main/kotlin/org/opensearch/client/codegen/OpenSearchJavaCodegenPlugin.kt delete mode 100644 java-codegen/src/main/kotlin/org/opensearch/client/codegen/OpenSearchJavaSettings.kt delete mode 100644 java-codegen/src/main/kotlin/org/opensearch/client/codegen/core/CodegenContext.kt delete mode 100644 java-codegen/src/main/kotlin/org/opensearch/client/codegen/core/ImportStatements.kt delete mode 100644 java-codegen/src/main/kotlin/org/opensearch/client/codegen/core/JavaDelegator.kt delete mode 100644 java-codegen/src/main/kotlin/org/opensearch/client/codegen/core/JavaSymbolProvider.kt delete mode 100644 java-codegen/src/main/kotlin/org/opensearch/client/codegen/core/JavaVisibility.kt delete mode 100644 java-codegen/src/main/kotlin/org/opensearch/client/codegen/core/JavaWriter.kt delete mode 100644 java-codegen/src/main/kotlin/org/opensearch/client/codegen/core/Naming.kt delete mode 100644 java-codegen/src/main/kotlin/org/opensearch/client/codegen/core/ReservedWords.kt delete mode 100644 java-codegen/src/main/kotlin/org/opensearch/client/codegen/core/RuntimeTypes.kt delete mode 100644 java-codegen/src/main/kotlin/org/opensearch/client/codegen/core/traits/SyntheticInputTrait.kt delete mode 100644 java-codegen/src/main/kotlin/org/opensearch/client/codegen/model/ModelExt.kt delete mode 100644 java-codegen/src/main/kotlin/org/opensearch/client/codegen/model/OperationNormalizer.kt delete mode 100644 java-codegen/src/main/kotlin/org/opensearch/client/codegen/model/ShapeExt.kt delete mode 100644 java-codegen/src/main/kotlin/org/opensearch/client/codegen/model/SymbolExt.kt delete mode 100644 java-codegen/src/main/kotlin/org/opensearch/client/codegen/render/ServiceGenerator.kt delete mode 100644 java-codegen/src/main/kotlin/org/opensearch/client/codegen/render/StructureGenerator.kt delete mode 100644 java-codegen/src/main/kotlin/org/opensearch/client/codegen/utils/Strings.kt diff --git a/java-codegen/build.gradle.kts b/java-codegen/build.gradle.kts index baac5e030e..a4e4dcde34 100644 --- a/java-codegen/build.gradle.kts +++ b/java-codegen/build.gradle.kts @@ -1,5 +1,5 @@ plugins { - kotlin("jvm") version "1.7.10" + java id("com.github.johnrengelman.shadow") version "7.1.2" } @@ -12,11 +12,10 @@ repositories { dependencies { val smithyVersion = "1.26.1" - val kotlinVersion = "1.7.10" val junitVersion = "5.8.2" - val kotestVersion = "5.4.1" - implementation(kotlin("stdlib-jdk8")) + implementation("org.jetbrains:annotations:24.0.0") + implementation("software.amazon.smithy:smithy-codegen-core:$smithyVersion") implementation("software.amazon.smithy:smithy-waiters:$smithyVersion") implementation("software.amazon.smithy:smithy-rules-engine:$smithyVersion") @@ -25,15 +24,6 @@ dependencies { // Test dependencies testImplementation("org.junit.jupiter:junit-jupiter:$junitVersion") - testImplementation("io.kotest:kotest-assertions-core-jvm:$kotestVersion") - testImplementation("org.jetbrains.kotlin:kotlin-test:$kotlinVersion") - testImplementation("org.jetbrains.kotlin:kotlin-test-junit5:$kotlinVersion") -} - -kotlin { - jvmToolchain { - languageVersion.set(JavaLanguageVersion.of(8)) - } } tasks.getByName("test") { diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/CodegenVisitor.java b/java-codegen/src/main/java/org/opensearch/client/codegen/CodegenVisitor.java new file mode 100644 index 0000000000..ac62e94fcb --- /dev/null +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/CodegenVisitor.java @@ -0,0 +1,91 @@ +package org.opensearch.client.codegen; + +import org.opensearch.client.codegen.core.GenerationContext; +import org.opensearch.client.codegen.core.JavaDelegator; +import org.opensearch.client.codegen.core.JavaSymbolProvider; +import org.opensearch.client.codegen.core.RenderingContext; +import org.opensearch.client.codegen.model.OperationNormalizer; +import org.opensearch.client.codegen.render.ServiceGenerator; +import org.opensearch.client.codegen.render.StructureGenerator; +import software.amazon.smithy.build.FileManifest; +import software.amazon.smithy.build.PluginContext; +import software.amazon.smithy.codegen.core.Symbol; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.knowledge.HttpBindingIndex; +import software.amazon.smithy.model.neighbor.Walker; +import software.amazon.smithy.model.shapes.ServiceShape; +import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.shapes.ShapeVisitor; +import software.amazon.smithy.model.shapes.StructureShape; +import software.amazon.smithy.model.transform.ModelTransformer; + +import java.util.logging.Logger; + +public class CodegenVisitor extends ShapeVisitor.Default { + private static final Logger LOGGER = Logger.getLogger(CodegenVisitor.class.getName()); + private final OpenSearchJavaSettings settings; + private final Model model; + private final ServiceShape service; + private final JavaSymbolProvider symbolProvider; + private final JavaDelegator writers; + private final GenerationContext generationContext; + + + public CodegenVisitor(PluginContext context) { + settings = OpenSearchJavaSettings.from(context.getModel(), context.getSettings()); + model = baselineTransform(context.getModel()); + service = settings.getService(model); + FileManifest fileManifest = context.getFileManifest(); + symbolProvider = new JavaSymbolProvider(model); + HttpBindingIndex httpBindingIndex = HttpBindingIndex.of(model); + writers = new JavaDelegator(fileManifest, symbolProvider); + generationContext = new GenerationContext(model, symbolProvider, httpBindingIndex, settings); + } + + private Model baselineTransform(Model model) { + model = ModelTransformer.create().flattenAndRemoveMixins(model); + model = ModelTransformer.create().copyServiceErrorsToOperations(model, settings.getService(model)); + model = OperationNormalizer.transform(model); + return model; + } + + public void execute() { + new Walker(model) + .walkShapes(service) + .forEach(s -> s.accept(this)); + writers.flushWriters(); + } + + @Override + protected Void getDefault(Shape shape) { + return null; + } + + @Override + public Void serviceShape(ServiceShape shape) { + LOGGER.info("Generating structure " + shape.getId().getName()); + + writers.useShapeWriter(shape, w -> { + RenderingContext ctx = new RenderingContext<>(generationContext, w, shape); + new ServiceGenerator(ctx).render(); + }); + + Symbol asyncSymbol = symbolProvider.serviceAsyncSymbol(shape); + writers.useSymbolWriter(asyncSymbol, w -> { + RenderingContext ctx = new RenderingContext<>(generationContext, w, shape, asyncSymbol); + new ServiceGenerator(ctx).render(); + }); + + return null; + } + + @Override + public Void structureShape(StructureShape shape) { + writers.useShapeWriter(shape, w -> { + RenderingContext ctx = new RenderingContext<>(generationContext, w, shape); + new StructureGenerator(ctx).render(); + }); + + return null; + } +} diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/OpenSearchJavaCodegenPlugin.java b/java-codegen/src/main/java/org/opensearch/client/codegen/OpenSearchJavaCodegenPlugin.java new file mode 100644 index 0000000000..364708b9d3 --- /dev/null +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/OpenSearchJavaCodegenPlugin.java @@ -0,0 +1,16 @@ +package org.opensearch.client.codegen; + +import software.amazon.smithy.build.PluginContext; +import software.amazon.smithy.build.SmithyBuildPlugin; + +public class OpenSearchJavaCodegenPlugin implements SmithyBuildPlugin { + @Override + public String getName() { + return "opensearch-java"; + } + + @Override + public void execute(PluginContext context) { + new CodegenVisitor(context).execute(); + } +} diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/OpenSearchJavaSettings.java b/java-codegen/src/main/java/org/opensearch/client/codegen/OpenSearchJavaSettings.java new file mode 100644 index 0000000000..7912a3bdcb --- /dev/null +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/OpenSearchJavaSettings.java @@ -0,0 +1,51 @@ +package org.opensearch.client.codegen; + +import software.amazon.smithy.codegen.core.CodegenException; +import software.amazon.smithy.model.Model; +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.Collections; +import java.util.List; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +public class OpenSearchJavaSettings { + private static final Logger LOGGER = Logger.getLogger(OpenSearchJavaSettings.class.getName()); + private static final String SERVICE = "service"; + private final ShapeId service; + + private OpenSearchJavaSettings(ShapeId service) { + this.service = service; + } + + public ServiceShape getService(Model model) { + return model.expectShape(service, ServiceShape.class); + } + + public static OpenSearchJavaSettings from(Model model, ObjectNode config) { + config.warnIfAdditionalProperties(Collections.singletonList(SERVICE)); + + ShapeId service = config.getStringMember(SERVICE).map(StringNode::expectShapeId).orElseGet(() -> inferService(model)); + + return new OpenSearchJavaSettings(service); + } + + private static ShapeId inferService(Model model) { + List services = model.shapes(ServiceShape.class).map(Shape::getId).sorted().collect(Collectors.toList()); + + if (services.isEmpty()) { + throw new CodegenException("Cannot infer a service to generate because the model does not contain any service shapes"); + } + if (services.size() > 1) { + throw new CodegenException("Cannot infer a service to generate because the model contains multiple service shapes: " + services); + } + + ShapeId service = services.get(0); + LOGGER.info("Inferring service to generate as: " + service); + return service; + } +} diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/core/CodegenContext.java b/java-codegen/src/main/java/org/opensearch/client/codegen/core/CodegenContext.java new file mode 100644 index 0000000000..e58ec59df2 --- /dev/null +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/core/CodegenContext.java @@ -0,0 +1,16 @@ +package org.opensearch.client.codegen.core; + +import org.opensearch.client.codegen.OpenSearchJavaSettings; +import software.amazon.smithy.codegen.core.SymbolProvider; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.knowledge.HttpBindingIndex; + +public interface CodegenContext { + Model getModel(); + + SymbolProvider getSymbolProvider(); + + HttpBindingIndex getHttpBindingIndex(); + + OpenSearchJavaSettings getSettings(); +} diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/core/GenerationContext.java b/java-codegen/src/main/java/org/opensearch/client/codegen/core/GenerationContext.java new file mode 100644 index 0000000000..21fdbc8248 --- /dev/null +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/core/GenerationContext.java @@ -0,0 +1,40 @@ +package org.opensearch.client.codegen.core; + +import org.opensearch.client.codegen.OpenSearchJavaSettings; +import software.amazon.smithy.codegen.core.SymbolProvider; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.knowledge.HttpBindingIndex; + +public class GenerationContext implements CodegenContext { + private final Model model; + private final SymbolProvider symbolProvider; + private final HttpBindingIndex httpBindingIndex; + private final OpenSearchJavaSettings settings; + + public GenerationContext(Model model, SymbolProvider symbolProvider, HttpBindingIndex httpBindingIndex, OpenSearchJavaSettings settings) { + this.model = model; + this.symbolProvider = symbolProvider; + this.httpBindingIndex = httpBindingIndex; + this.settings = settings; + } + + @Override + public Model getModel() { + return model; + } + + @Override + public SymbolProvider getSymbolProvider() { + return symbolProvider; + } + + @Override + public HttpBindingIndex getHttpBindingIndex() { + return httpBindingIndex; + } + + @Override + public OpenSearchJavaSettings getSettings() { + return settings; + } +} diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/core/ImportStatements.java b/java-codegen/src/main/java/org/opensearch/client/codegen/core/ImportStatements.java new file mode 100644 index 0000000000..91a0095147 --- /dev/null +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/core/ImportStatements.java @@ -0,0 +1,69 @@ +package org.opensearch.client.codegen.core; + +import software.amazon.smithy.codegen.core.CodegenException; +import software.amazon.smithy.codegen.core.ImportContainer; +import software.amazon.smithy.codegen.core.Symbol; + +import java.util.HashSet; +import java.util.Objects; +import java.util.stream.Collectors; + +public class ImportStatements implements ImportContainer { + private final String packageName; + private final HashSet imports = new HashSet<>(); + + public ImportStatements(String packageName) { + this.packageName = packageName; + } + + @Override + public void importSymbol(Symbol symbol, String alias) { + String name = symbol.getName(); + String namespace = symbol.getNamespace(); + + if (!name.equals(alias)) { + throw new CodegenException("Java does not allow import aliasing"); + } + + if (!namespace.isEmpty() && !namespace.equals(packageName)) { + imports.add(new ImportStatement(namespace, name)); + } + } + + @Override + public String toString() { + return imports + .stream() + .map(ImportStatement::toString) + .sorted() + .collect(Collectors.joining("\n")); + } + + private static class ImportStatement { + private final String packageName; + private final String symbolName; + + public ImportStatement(String packageName, String symbolName) { + this.packageName = packageName; + this.symbolName = symbolName; + } + + @Override + public String toString() { + return "import " + packageName + "." + symbolName + ";"; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ImportStatement that = (ImportStatement) o; + return Objects.equals(packageName, that.packageName) && Objects.equals(symbolName, that.symbolName); + } + + @Override + public int hashCode() { + return Objects.hash(packageName, symbolName); + } + } +} diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/core/JavaDelegator.java b/java-codegen/src/main/java/org/opensearch/client/codegen/core/JavaDelegator.java new file mode 100644 index 0000000000..5b6555ec9e --- /dev/null +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/core/JavaDelegator.java @@ -0,0 +1,29 @@ +package org.opensearch.client.codegen.core; + +import software.amazon.smithy.build.FileManifest; +import software.amazon.smithy.codegen.core.Symbol; +import software.amazon.smithy.codegen.core.SymbolProvider; +import software.amazon.smithy.codegen.core.WriterDelegator; +import software.amazon.smithy.model.shapes.Shape; + +import java.util.function.Consumer; + +public class JavaDelegator { + private final WriterDelegator inner; + + public JavaDelegator(FileManifest fileManifest, SymbolProvider symbolProvider) { + inner = new WriterDelegator<>(fileManifest, symbolProvider, JavaWriter.factory()); + } + + public void useShapeWriter(Shape shape, Consumer writerConsumer) { + inner.useShapeWriter(shape, writerConsumer); + } + + public void useSymbolWriter(Symbol symbol, Consumer writerConsumer) { + inner.useFileWriter(symbol.getDefinitionFile(), symbol.getNamespace(), writerConsumer); + } + + public void flushWriters() { + inner.flushWriters(); + } +} diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/core/JavaReservedWords.java b/java-codegen/src/main/java/org/opensearch/client/codegen/core/JavaReservedWords.java new file mode 100644 index 0000000000..3d5087ca56 --- /dev/null +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/core/JavaReservedWords.java @@ -0,0 +1,23 @@ +package org.opensearch.client.codegen.core; + +import software.amazon.smithy.codegen.core.ReservedWords; +import software.amazon.smithy.codegen.core.ReservedWordsBuilder; + +public class JavaReservedWords { + private static final String[] HARD_RESERVED_WORDS = { + "null" + }; + private static final ReservedWords INSTANCE; + + static { + ReservedWordsBuilder builder = new ReservedWordsBuilder(); + for (String word : HARD_RESERVED_WORDS) { + builder.put(word, "_" + word); + } + INSTANCE = builder.build(); + } + + public static ReservedWords instance() { + return INSTANCE; + } +} diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/core/JavaSymbolProvider.java b/java-codegen/src/main/java/org/opensearch/client/codegen/core/JavaSymbolProvider.java new file mode 100644 index 0000000000..5862ff1d36 --- /dev/null +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/core/JavaSymbolProvider.java @@ -0,0 +1,182 @@ +package org.opensearch.client.codegen.core; + +import org.opensearch.client.codegen.model.Symbols; +import software.amazon.smithy.codegen.core.ReservedWordSymbolProvider; +import software.amazon.smithy.codegen.core.Symbol; +import software.amazon.smithy.codegen.core.SymbolProvider; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.knowledge.NullableIndex; +import software.amazon.smithy.model.shapes.*; + +import java.util.logging.Logger; + +public class JavaSymbolProvider implements ShapeVisitor, SymbolProvider { + private static final String ROOT_NAMESPACE = "org.opensearch.client"; + private static final Logger LOGGER = Logger.getLogger(JavaSymbolProvider.class.getName()); + private final Model model; + private final ReservedWordSymbolProvider.Escaper escaper; + private final NullableIndex nullableIndex; + private int depth = 0; + + public JavaSymbolProvider(Model model) { + this.model = model; + this.escaper = ReservedWordSymbolProvider + .builder() + .nameReservedWords(JavaReservedWords.instance()) + .memberReservedWords(JavaReservedWords.instance()) + .escapePredicate((shape, symbol) -> !symbol.getDefinitionFile().isEmpty()) + .buildEscaper(); + this.nullableIndex = new NullableIndex(model); + } + + @Override + public Symbol toSymbol(Shape shape) { + ++depth; + Symbol symbol = shape.accept(this); + --depth; + LOGGER.fine("Creating symbol from " + shape + ": " + symbol); + return escaper.escapeSymbol(shape, symbol); + } + + @Override + public String toMemberName(MemberShape shape) { + return escaper.escapeMemberName(Naming.defaultName(shape)); + } + + @Override + public Symbol blobShape(BlobShape shape) { + return null; + } + + @Override + public Symbol booleanShape(BooleanShape shape) { + return Symbols.forShape(shape, "boolean").build(); + } + + @Override + public Symbol listShape(ListShape shape) { + Symbol reference = Symbols.boxed(toSymbol(shape.getMember()).toBuilder()).build(); + return Symbols.forShape(shape, "List<" + reference.getName() + ">") + .addReference(RuntimeTypes.JavaUtil.List) + .addReference(reference) + .build(); + } + + @Override + public Symbol mapShape(MapShape shape) { + Symbol reference = Symbols.boxed(toSymbol(shape.getValue()).toBuilder()).build(); + return Symbols.forShape(shape, "Map") + .addReference(RuntimeTypes.JavaUtil.Map) + .addReference(reference) + .build(); + } + + @Override + public Symbol byteShape(ByteShape shape) { + return null; + } + + @Override + public Symbol shortShape(ShortShape shape) { + return null; + } + + @Override + public Symbol integerShape(IntegerShape shape) { + return Symbols.forShape(shape, "int").build(); + } + + @Override + public Symbol longShape(LongShape shape) { + return null; + } + + @Override + public Symbol floatShape(FloatShape shape) { + return null; + } + + @Override + public Symbol documentShape(DocumentShape shape) { + return null; + } + + @Override + public Symbol doubleShape(DoubleShape shape) { + return null; + } + + @Override + public Symbol bigIntegerShape(BigIntegerShape shape) { + return null; + } + + @Override + public Symbol bigDecimalShape(BigDecimalShape shape) { + return null; + } + + @Override + public Symbol operationShape(OperationShape shape) { + String name = Naming.defaultName(shape); + return Symbols.forShape(shape, name) + .namespace(ROOT_NAMESPACE, ".") + .definitionFile(name + ".java") + .build(); + } + + @Override + public Symbol resourceShape(ResourceShape shape) { + return null; + } + + @Override + public Symbol serviceShape(ServiceShape shape) { + String name = Naming.defaultName(shape); + return Symbols.forShape(shape, name) + .namespace(ROOT_NAMESPACE, ".") + .definitionFile(name + ".java") + .build(); + } + + public Symbol serviceAsyncSymbol(ServiceShape shape) { + String name = Naming.asyncName(shape); + return Symbols.forShape(shape, name) + .namespace(ROOT_NAMESPACE, ".") + .definitionFile(name + ".java") + .build(); + } + + @Override + public Symbol stringShape(StringShape shape) { + return Symbols.forShape(shape, "String").build(); + } + + @Override + public Symbol structureShape(StructureShape shape) { + String name = Naming.defaultName(shape); + return Symbols.forShape(shape, name) + .namespace(ROOT_NAMESPACE, ".") + .definitionFile(name + ".java") + .build(); + } + + @Override + public Symbol unionShape(UnionShape shape) { + return null; + } + + @Override + public Symbol memberShape(MemberShape shape) { + Shape targetShape = model.expectShape(shape.getTarget()); + Symbol targetSymbol = toSymbol(targetShape); + return nullableIndex.isMemberNullable(shape, NullableIndex.CheckMode.CLIENT) + ? Symbols.nullable(targetSymbol.toBuilder()).build() + : targetSymbol; + } + + @Override + public Symbol timestampShape(TimestampShape shape) { + return null; + } +} diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/core/JavaVisibility.java b/java-codegen/src/main/java/org/opensearch/client/codegen/core/JavaVisibility.java new file mode 100644 index 0000000000..542dee71c2 --- /dev/null +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/core/JavaVisibility.java @@ -0,0 +1,12 @@ +package org.opensearch.client.codegen.core; + +public enum JavaVisibility { + PUBLIC, + PROTECTED, + PRIVATE; + + @Override + public String toString() { + return super.toString().toLowerCase(); + } +} diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/core/JavaWriter.java b/java-codegen/src/main/java/org/opensearch/client/codegen/core/JavaWriter.java new file mode 100644 index 0000000000..dfdbda286c --- /dev/null +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/core/JavaWriter.java @@ -0,0 +1,84 @@ +package org.opensearch.client.codegen.core; + +import software.amazon.smithy.codegen.core.CodegenException; +import software.amazon.smithy.codegen.core.Symbol; +import software.amazon.smithy.codegen.core.SymbolWriter; +import software.amazon.smithy.rulesengine.language.eval.Type; + +import java.util.List; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +public class JavaWriter extends SymbolWriter { + private final String filename; + private final String namespace; + + public static Factory factory() { + return JavaWriter::new; + } + + private JavaWriter(String filename, String namespace) { + super(new ImportStatements(namespace)); + this.filename = filename; + this.namespace = namespace; + + trimBlankLines(); + trimTrailingSpaces(); + setIndentText(" "); + setExpressionStart('#'); + putFormatter('T', this::formatSymbol); + } + + public JavaWriter addImport(Symbol symbol) { + return super.addImport(symbol, symbol.getName()); + } + + public JavaWriter block(String header, Object[] args, Consumer block) { + openBlock(header + " {", args); + block.accept(this); + closeBlock("}"); + return this; + } + + public JavaWriter javaClass(JavaVisibility visibility, String name, List annotations, Symbol extendz, List implementz, Consumer body) { + if (annotations != null) { + annotations.forEach(s -> { + write("@#T", s); + }); + } + + String header = format("#L #L", visibility, name); + if (extendz != null) { + header = format("#L extends #T", header, extendz); + } + if (implementz != null && !implementz.isEmpty()) { + header = format( + "#L implements #L", + header, + implementz + .stream() + .map(s -> format("#T", s)) + .collect(Collectors.joining(", ")) + ); + } + + return block(header, new Object[0], body); + } + + @Override + public String toString() { + return "package " + namespace + ";\n\n" + + getImportContainer() + "\n\n" + + super.toString(); + } + + private String formatSymbol(Object o, String indent) { + if (o instanceof Symbol) { + Symbol type = (Symbol) o; + addImport(type); + return type.getName(); + } + + throw new CodegenException("Invalid type provided for #T. Expected a Symbol, but found `" + o + "`"); + } +} diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/core/Naming.java b/java-codegen/src/main/java/org/opensearch/client/codegen/core/Naming.java new file mode 100644 index 0000000000..452d1eb0bf --- /dev/null +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/core/Naming.java @@ -0,0 +1,29 @@ +package org.opensearch.client.codegen.core; + +import org.opensearch.client.codegen.utils.Strings; +import software.amazon.smithy.model.shapes.MemberShape; +import software.amazon.smithy.model.shapes.OperationShape; +import software.amazon.smithy.model.shapes.ServiceShape; +import software.amazon.smithy.model.shapes.StructureShape; + +public class Naming { + public static String defaultName(MemberShape shape) { + return Strings.toCamelCase(shape.getMemberName()); + } + + public static String defaultName(OperationShape shape) { + return Strings.toCamelCase(shape.getId().getName()); + } + + public static String defaultName(ServiceShape shape) { + return Strings.toPascalCase(shape.getId().getName()) + "Client"; + } + + public static String asyncName(ServiceShape shape) { + return Strings.toPascalCase(shape.getId().getName()) + "AsyncClient"; + } + + public static String defaultName(StructureShape shape) { + return Strings.toPascalCase(shape.getId().getName()); + } +} diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/core/RenderingContext.java b/java-codegen/src/main/java/org/opensearch/client/codegen/core/RenderingContext.java new file mode 100644 index 0000000000..9d5075fa67 --- /dev/null +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/core/RenderingContext.java @@ -0,0 +1,67 @@ +package org.opensearch.client.codegen.core; + +import org.jetbrains.annotations.Nullable; +import org.opensearch.client.codegen.OpenSearchJavaSettings; +import software.amazon.smithy.codegen.core.Symbol; +import software.amazon.smithy.codegen.core.SymbolProvider; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.knowledge.HttpBindingIndex; +import software.amazon.smithy.model.shapes.Shape; + +import java.util.Optional; + +public class RenderingContext implements CodegenContext { + private final Model model; + private final SymbolProvider symbolProvider; + private final HttpBindingIndex httpBindingIndex; + private final OpenSearchJavaSettings settings; + private final JavaWriter writer; + private final T shape; + private final Symbol symbol; + + public RenderingContext(CodegenContext other, JavaWriter writer, @Nullable T shape) { + this(other, writer, shape, null); + } + + public RenderingContext(CodegenContext other, JavaWriter writer, @Nullable T shape, Symbol symbol) { + this.model = other.getModel(); + this.symbolProvider = other.getSymbolProvider(); + this.httpBindingIndex = other.getHttpBindingIndex(); + this.settings = other.getSettings(); + this.writer = writer; + this.shape = shape; + this.symbol = symbol != null ? symbol : this.symbolProvider.toSymbol(shape); + } + + @Override + public Model getModel() { + return model; + } + + @Override + public SymbolProvider getSymbolProvider() { + return symbolProvider; + } + + @Override + public HttpBindingIndex getHttpBindingIndex() { + return httpBindingIndex; + } + + @Override + public OpenSearchJavaSettings getSettings() { + return settings; + } + + public JavaWriter getWriter() { + return writer; + } + + public Optional getShape() { + return Optional.ofNullable(shape); + } + + public Symbol getSymbol() { + return symbol; + } +} diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/core/RuntimeTypes.java b/java-codegen/src/main/java/org/opensearch/client/codegen/core/RuntimeTypes.java new file mode 100644 index 0000000000..610ec053a5 --- /dev/null +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/core/RuntimeTypes.java @@ -0,0 +1,75 @@ +package org.opensearch.client.codegen.core; + +import org.opensearch.client.codegen.model.Symbols; +import software.amazon.smithy.codegen.core.Symbol; + +public class RuntimeTypes { + private static final String CLIENT_PKG = "org.opensearch.client"; + + public static final Symbol ApiClient = Symbols.from(CLIENT_PKG, "ApiClient"); + + public static class Json { + private static final String JSON_PKG = CLIENT_PKG + ".json"; + + public static final Symbol JsonpDeserializable = Symbols.from(JSON_PKG, "JsonpDeserializable"); + public static final Symbol JsonpDeserializer = Symbols.from(JSON_PKG, "JsonpDeserializer"); + public static final Symbol JsonpMapper = Symbols.from(JSON_PKG, "JsonpMapper"); + public static final Symbol JsonpSerializable = Symbols.from(JSON_PKG, "JsonpSerializable"); + public static final Symbol ObjectBuilderDeserializer = Symbols.from(JSON_PKG, "ObjectBuilderDeserializer"); + public static final Symbol ObjectDeserializer = Symbols.from(JSON_PKG, "ObjectDeserializer"); + } + + public static class OpenSearch { + private static final String OPENSEARCH_PKG = CLIENT_PKG + ".opensearch"; + + public static class Types { + private static final String TYPES_PKG = OPENSEARCH_PKG + "._types"; + + public static final Symbol ErrorResponse = Symbols.from(TYPES_PKG, "ErrorResponse"); + public static final Symbol OpenSearchException = Symbols.from(TYPES_PKG, "OpenSearchException"); + public static final Symbol RequestBase = Symbols.from(TYPES_PKG, "RequestBase"); + } + } + + public static class Transport { + private static final String TRANSPORT_PKG = CLIENT_PKG + ".transport"; + + public static final Symbol Endpoint = Symbols.from(TRANSPORT_PKG, "Endpoint"); + public static final Symbol JsonEndpoint = Symbols.from(TRANSPORT_PKG, "JsonEndpoint"); + public static final Symbol OpenSearchTransport = Symbols.from(TRANSPORT_PKG, "OpenSearchTransport"); + public static final Symbol TransportOptions = Symbols.from(TRANSPORT_PKG, "TransportOptions"); + + public static class Endpoints { + private static final String ENDPOINTS_PKG = TRANSPORT_PKG + ".endpoints"; + + public static final Symbol SimpleEndpoint = Symbols.from(ENDPOINTS_PKG, "SimpleEndpoint"); + } + } + + public static class Util { + private static final String UTIL_PKG = CLIENT_PKG + ".util"; + + public static final Symbol ApiTypeHelper = Symbols.from(UTIL_PKG, "ApiTypeHelper"); + public static final Symbol ObjectBuilder = Symbols.from(UTIL_PKG, "ObjectBuilder"); + public static final Symbol ObjectBuilderBase = Symbols.from(UTIL_PKG, "ObjectBuilderBase"); + } + + public static class Jakarta { + public static final Symbol JsonGenerator = Symbols.from("jakarta.json.stream", "JsonGenerator"); + } + + public static class JavaIo { + public static final Symbol IOException = Symbols.from(java.io.IOException.class); + } + + public static class JavaUtil { + public static final Symbol Function = Symbols.from(java.util.function.Function.class); + public static final Symbol HashMap = Symbols.from(java.util.HashMap.class); + public static final Symbol List = Symbols.from(java.util.List.class); + public static final Symbol Map = Symbols.from(java.util.Map.class); + } + + public static class Javax { + public static final Symbol Nullable = Symbols.from("javax.annotation", "Nullable"); + } +} diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/core/traits/SyntheticInputTrait.java b/java-codegen/src/main/java/org/opensearch/client/codegen/core/traits/SyntheticInputTrait.java new file mode 100644 index 0000000000..667d69c22b --- /dev/null +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/core/traits/SyntheticInputTrait.java @@ -0,0 +1,19 @@ +package org.opensearch.client.codegen.core.traits; + +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.traits.AnnotationTrait; + +public class SyntheticInputTrait extends AnnotationTrait { + private static final ShapeId ID = ShapeId.from("smithy.api.internal#syntheticInput"); + private final ShapeId operation; + + public SyntheticInputTrait(ShapeId operation) { + super(ID, Node.objectNode()); + this.operation = operation; + } + + public ShapeId getOperation() { + return operation; + } +} diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/model/OperationNormalizer.java b/java-codegen/src/main/java/org/opensearch/client/codegen/model/OperationNormalizer.java new file mode 100644 index 0000000000..ad4bb381a9 --- /dev/null +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/model/OperationNormalizer.java @@ -0,0 +1,77 @@ +package org.opensearch.client.codegen.model; + +import org.opensearch.client.codegen.core.traits.SyntheticInputTrait; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.shapes.OperationShape; +import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.shapes.StructureShape; +import software.amazon.smithy.model.transform.ModelTransformer; + +import java.util.List; +import java.util.Optional; +import java.util.function.Supplier; +import java.util.logging.Logger; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class OperationNormalizer { + private static final Logger LOGGER = Logger.getLogger(OperationNormalizer.class.getName()); + + public static Model transform(Model model) { + ModelTransformer transformer = ModelTransformer.create(); + + List operations = model.shapes(OperationShape.class).collect(Collectors.toList()); + List newShapes = operations + .stream() + .flatMap(o -> Stream.of(syntheticInputShape(model, o), syntheticOutputShape(model, o))) + .collect(Collectors.toList()); + + Optional firstConflict = newShapes.stream().filter(s -> model.getShape(s.getId()).isPresent()).findFirst(); + if (firstConflict.isPresent()) { + throw new IllegalStateException("Shape " + firstConflict + " conflicted with an existing shape in the model (" + model.getShape(firstConflict.get().getId()) + "). This is a bug."); + } + + Model modelWithOperationInputs = model.toBuilder().addShapes(newShapes).build(); + + return transformer.mapShapes(modelWithOperationInputs, s -> s + .asOperationShape() + .map(o -> { + ShapeId id = o.getId(); + ShapeId inputId = syntheticInputId(id); + modelWithOperationInputs.expectShape(inputId); + return (Shape) o.toBuilder() + .input(inputId) + .output(syntheticOutputId(id)) + .build(); + }) + .orElse(s)); + } + + private static ShapeId syntheticInputId(ShapeId operationId) { + return ShapeId.fromParts(operationId.getNamespace() + ".synthetic", operationId.getName() + "Request"); + } + + private static StructureShape syntheticInputShape(Model model, OperationShape operation) { + ShapeId operationId = operation.getId(); + return renamedOrEmpty(model, operation::getInput, syntheticInputId(operationId)) + .addTrait(new SyntheticInputTrait(operationId)) + .build(); + } + + private static ShapeId syntheticOutputId(ShapeId operationId) { + return ShapeId.fromParts(operationId.getNamespace() + ".synthetic", operationId.getName() + "Response"); + } + + private static StructureShape syntheticOutputShape(Model model, OperationShape operation) { + return renamedOrEmpty(model, operation::getOutput, syntheticOutputId(operation.getId())).build(); + } + + private static StructureShape.Builder renamedOrEmpty(Model model, Supplier> getExistingShape, ShapeId newId) { + return getExistingShape + .get() + .map(shapeId -> model.expectShape(shapeId, StructureShape.class).toBuilder()) + .orElseGet(StructureShape::builder) + .id(newId); + } +} diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/model/Shapes.java b/java-codegen/src/main/java/org/opensearch/client/codegen/model/Shapes.java new file mode 100644 index 0000000000..ef14b5e532 --- /dev/null +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/model/Shapes.java @@ -0,0 +1,30 @@ +package org.opensearch.client.codegen.model; + +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.shapes.*; + +public class Shapes { + public static boolean isListOrMap(Shape shape) { + return shape.isListShape() || shape.isMapShape(); + } + + public static Shape getElementShape(ListShape shape, Model model) { + return getTargetShape(shape.getMember(), model); + } + + public static Shape getElementShape(MapShape shape, Model model) { + return getTargetShape(shape.getValue(), model); + } + + public static Shape getTargetShape(MemberShape shape, Model model) { + return model.expectShape(shape.getTarget()); + } + + public static StructureShape getInputShape(OperationShape operation, Model model) { + return model.expectShape(operation.getInputShape(), StructureShape.class); + } + + public static StructureShape getOutputShape(OperationShape operation, Model model) { + return model.expectShape(operation.getOutputShape(), StructureShape.class); + } +} diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/model/Symbols.java b/java-codegen/src/main/java/org/opensearch/client/codegen/model/Symbols.java new file mode 100644 index 0000000000..1c53ac67b6 --- /dev/null +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/model/Symbols.java @@ -0,0 +1,82 @@ +package org.opensearch.client.codegen.model; + +import software.amazon.smithy.codegen.core.Symbol; +import software.amazon.smithy.model.shapes.Shape; + +import java.util.HashMap; +import java.util.Map; +import java.util.NoSuchElementException; + +public class Symbols { + private static final Map BOXED_PRIMITIVES = new HashMap() {{ + put("byte", "Byte"); + put("short", "Short"); + put("int", "Integer"); + put("long", "Long"); + put("float", "Float"); + put("double", "Double"); + put("boolean", "Boolean"); + put("char", "Character"); + }}; + private static final String SHAPE_KEY = "shape"; + private static final String NULLABLE_KEY = "nullable"; + + public static Symbol boxed(Symbol symbol) { + return boxed(symbol.toBuilder()).build(); + } + + public static Symbol.Builder boxed(Symbol.Builder builder) { + Symbol symbol = builder.build(); + String namespace = symbol.getNamespace(); + + if (namespace == null || namespace.isEmpty()) { + String newName = BOXED_PRIMITIVES.get(symbol.getName()); + if (newName != null) { + builder.name(newName); + } + } + + return builder; + } + + public static Symbol.Builder nullable(Symbol.Builder builder) { + return Symbols.boxed(builder).putProperty(NULLABLE_KEY, true); + } + + public static boolean isNullable(Symbol symbol) { + return symbol + .getProperty(NULLABLE_KEY) + .map(it -> { + if (it instanceof Boolean) { + return (boolean) it; + } + return false; + }) + .orElse(false); + } + + public static Shape getShape(Symbol symbol) { + return symbol + .getProperty(SHAPE_KEY, Shape.class) + .orElseThrow(() -> new NoSuchElementException("Symbol is missing shape property")); + } + + public static Symbol.Builder forShape(Shape shape, String name) { + return Symbol + .builder() + .name(name) + .putProperty(SHAPE_KEY, shape); + } + + public static Symbol from(String namespace, String name) { + return Symbol + .builder() + .name(name) + .namespace(namespace, ".") + .build(); + } + + public static Symbol from(Class clazz) { + return from(clazz.getPackage().getName(), clazz.getSimpleName()); + } +} diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/render/ServiceGenerator.java b/java-codegen/src/main/java/org/opensearch/client/codegen/render/ServiceGenerator.java new file mode 100644 index 0000000000..8b01ef570b --- /dev/null +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/render/ServiceGenerator.java @@ -0,0 +1,109 @@ +package org.opensearch.client.codegen.render; + +import org.opensearch.client.codegen.core.JavaWriter; +import org.opensearch.client.codegen.core.RenderingContext; +import org.opensearch.client.codegen.core.RuntimeTypes; +import org.opensearch.client.codegen.model.Shapes; +import org.opensearch.client.codegen.utils.Strings; +import software.amazon.smithy.codegen.core.Symbol; +import software.amazon.smithy.codegen.core.SymbolProvider; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.knowledge.TopDownIndex; +import software.amazon.smithy.model.shapes.OperationShape; +import software.amazon.smithy.model.shapes.ServiceShape; +import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.shapes.StructureShape; + +import java.util.Comparator; +import java.util.NoSuchElementException; + +public class ServiceGenerator { + private final Model model; + private final Shape shape; + private final SymbolProvider symbolProvider; + private final Symbol symbol; + private final JavaWriter writer; + private final TopDownIndex index; + + public ServiceGenerator(RenderingContext ctx) { + model = ctx.getModel(); + shape = ctx.getShape().orElseThrow(() -> new NoSuchElementException("Missing shape on rendering context")); + symbolProvider = ctx.getSymbolProvider(); + symbol = symbolProvider.toSymbol(shape); + writer = ctx.getWriter(); + index = TopDownIndex.of(model); + } + + public void render() { + writer.block( + "public class #1T extends #2T<#3T, #1T>", + new Object[]{symbol, RuntimeTypes.ApiClient, RuntimeTypes.Transport.OpenSearchTransport}, + w -> { + renderConstructors(); + + index.getContainedOperations(shape) + .stream() + .sorted(Comparator.comparing(Shape::getId)) + .forEach(this::renderOperation); + } + ); + } + + private void renderConstructors() { + writer.block( + "public #T(#T transport)", + new Object[]{symbol, RuntimeTypes.Transport.OpenSearchTransport}, + w -> w.write("super(transport);") + ); + writer.write(""); + + writer.block( + "public #T(#T transport, @#T #T transportOptions)", + new Object[]{symbol, RuntimeTypes.Transport.OpenSearchTransport, RuntimeTypes.Javax.Nullable, RuntimeTypes.Transport.TransportOptions}, + w -> w.write("super(transport, transportOptions);") + ); + writer.write(""); + + writer.block( + "@Override public #T withTransportOptions(@#T #T transportOptions)", + new Object[]{symbol, RuntimeTypes.Javax.Nullable, RuntimeTypes.Transport.TransportOptions}, + w -> w.write("return new #T(this.transport, transportOptions);", symbol) + ); + writer.write(""); + } + + private void renderOperation(OperationShape operation) { + StructureShape inputShape = Shapes.getInputShape(operation, model); + Symbol inputSymbol = symbolProvider.toSymbol(inputShape); + StructureShape outputShape = Shapes.getOutputShape(operation, model); + Symbol outputSymbol = symbolProvider.toSymbol(outputShape); + String operationName = Strings.toCamelCase(operation.getId().getName()); + + writer.block( + "public #T #L(#T request) throws #T, #T", + new Object[]{outputSymbol, operationName, inputSymbol, RuntimeTypes.JavaIo.IOException, RuntimeTypes.OpenSearch.Types.OpenSearchException}, + w -> { + String endpointType = w.format( + "#T<#T, #T, #T>", + RuntimeTypes.Transport.JsonEndpoint, + inputSymbol, + outputSymbol, + RuntimeTypes.OpenSearch.Types.ErrorResponse + ); + + w.write("@SuppressWarnings(\"unchecked\")"); + w.write("#1L endpoint = (#1L) #2T._ENDPOINT;", endpointType, inputSymbol); + w.write(""); + w.write("return this.transport.performRequest(request, endpoint, this.transportOptions);"); + } + ); + writer.write(""); + + writer.block( + "public final #1T #2L(#3T<#4T.Builder, #5T<#4T>> fn) throws #6T, #7T", + new Object[]{outputSymbol, operationName, RuntimeTypes.JavaUtil.Function, inputSymbol, RuntimeTypes.Util.ObjectBuilder, RuntimeTypes.JavaIo.IOException, RuntimeTypes.OpenSearch.Types.OpenSearchException}, + w -> w.write("return #L(fn.apply(new #T.Builder()).build());", operationName, inputSymbol) + ); + writer.write(""); + } +} diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/render/StructureGenerator.java b/java-codegen/src/main/java/org/opensearch/client/codegen/render/StructureGenerator.java new file mode 100644 index 0000000000..0c2e0dd95d --- /dev/null +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/render/StructureGenerator.java @@ -0,0 +1,439 @@ +package org.opensearch.client.codegen.render; + +import org.opensearch.client.codegen.core.*; +import org.opensearch.client.codegen.core.traits.SyntheticInputTrait; +import org.opensearch.client.codegen.model.Shapes; +import org.opensearch.client.codegen.model.Symbols; +import org.opensearch.client.codegen.utils.Strings; +import software.amazon.smithy.codegen.core.Symbol; +import software.amazon.smithy.codegen.core.SymbolProvider; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.knowledge.HttpBinding; +import software.amazon.smithy.model.knowledge.HttpBindingIndex; +import software.amazon.smithy.model.shapes.*; +import software.amazon.smithy.model.traits.HttpQueryTrait; +import software.amazon.smithy.model.traits.HttpTrait; +import software.amazon.smithy.utils.Pair; +import software.amazon.smithy.utils.TriConsumer; + +import java.util.*; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +public class StructureGenerator { + private final Shape shape; + private final JavaWriter writer; + private final SymbolProvider symbolProvider; + private final Model model; + private final Symbol symbol; + private final HttpBindingIndex httpBindingIndex; + private final boolean isInput; + private final List sortedMembers; + private final Map> memberNameSymbolIndex; + + public StructureGenerator(RenderingContext ctx) { + shape = ctx.getShape().orElseThrow(() -> new NoSuchElementException("Missing shape on rendering context")); + writer = ctx.getWriter(); + symbolProvider = ctx.getSymbolProvider(); + model = ctx.getModel(); + symbol = symbolProvider.toSymbol(shape); + httpBindingIndex = ctx.getHttpBindingIndex(); + isInput = shape.hasTrait(SyntheticInputTrait.class); + sortedMembers = shape + .getAllMembers() + .values() + .stream() + .sorted(Comparator.comparing(Naming::defaultName)) + .collect(Collectors.toList()); + memberNameSymbolIndex = new HashMap<>(); + sortedMembers.forEach(m -> memberNameSymbolIndex.put(m, new Pair<>(symbolProvider.toMemberName(m), symbolProvider.toSymbol(m)))); + } + + public void render() { + writer.javaClass( + JavaVisibility.PUBLIC, + symbol.getName(), + Collections.singletonList(RuntimeTypes.Json.JsonpDeserializable), + isInput ? RuntimeTypes.OpenSearch.Types.RequestBase : null, + Collections.singletonList(RuntimeTypes.Json.JsonpSerializable), + w -> { + renderFields(); + renderConstructor(); + renderGetters(); + renderSerialize(); + renderBuilder(); + renderDeserialize(); + if (isInput) { + renderEndpoint(); + } + } + ); + } + + private void renderFields() { + renderFields(true); + } + + private void renderFields(boolean _final) { + forEachMember((member, memberName, memberSymbol) -> { + if (Symbols.isNullable(memberSymbol) && !(_final && Shapes.isListOrMap(Symbols.getShape(memberSymbol)))) { + writer.write("@#T", RuntimeTypes.Javax.Nullable); + } + writer.write( + "private #L#T #L;\n", + _final ? "final " : "", + memberSymbol, + memberName + ); + }); + } + + private void renderConstructor() { + writer.block( + "private #T(Builder builder)", + new Object[]{symbol}, + w -> forEachMember((member, memberName, memberSymbol) -> { + String builderField = w.format("builder.#L", memberName); + + if (Shapes.isListOrMap(Symbols.getShape(memberSymbol))) { + builderField = Symbols.isNullable(memberSymbol) + ? w.format( + "#T.unmodifiable(#L)", + RuntimeTypes.Util.ApiTypeHelper, builderField) + : w.format( + "#T.unmodifiableRequired(#L, this, #L)", + RuntimeTypes.Util.ApiTypeHelper, builderField, Strings.dq(memberName)); + } else if (!Symbols.isNullable(memberSymbol)) { + builderField = w.format( + "#T.requireNonNull(#L, this, #L)", + RuntimeTypes.Util.ApiTypeHelper, builderField, Strings.dq(memberName) + ); + } + + w.write("this.#L = #L;", memberName, builderField); + })); + writer.write(""); + + writer.block( + "public static #1T of(#2T> fn)", + new Object[]{symbol, RuntimeTypes.JavaUtil.Function, RuntimeTypes.Util.ObjectBuilder}, + w -> w.write("return fn.apply(new Builder()).build();")); + writer.write(""); + } + + private void renderGetters() { + forEachMember((member, memberName, memberSymbol) -> { + if (Symbols.isNullable(memberSymbol) && !Shapes.isListOrMap(Symbols.getShape(memberSymbol))) { + writer.write("@#T", RuntimeTypes.Javax.Nullable); + } + + writer.block( + "public final #T #L()", + new Object[]{memberSymbol, memberName}, + w -> w.write("return this.#L;", memberName)); + writer.write(""); + }); + } + + private void renderSerialize() { + writer.block( + "public void serialize(#T generator, #T mapper)", + new Object[]{RuntimeTypes.Jakarta.JsonGenerator, RuntimeTypes.Json.JsonpMapper}, + w -> { + w.write("generator.writeStartObject();"); + w.write("serializeInternal(generator, mapper);"); + w.write("generator.writeEnd();"); + }); + writer.write(""); + + writer.block( + "protected void serializeInternal(#T generator, #T mapper)", + new Object[]{RuntimeTypes.Jakarta.JsonGenerator, RuntimeTypes.Json.JsonpMapper}, + w -> forEachMember(getDocumentFields(), this::renderSerializeField)); + writer.write(""); + } + + private Iterable getDocumentFields() { + if (!isInput) { + return sortedMembers; + } + + ShapeId operation = shape.expectTrait(SyntheticInputTrait.class).getOperation(); + + return httpBindingIndex.getRequestBindings(operation, HttpBinding.Location.DOCUMENT) + .stream() + .map(HttpBinding::getMember) + .sorted(Comparator.comparing(MemberShape::getMemberName)) + .collect(Collectors.toList()); + } + + private void renderSerializeField(MemberShape member, String memberName, Symbol memberSymbol) { + ifValueDefined("this." + memberName, memberSymbol, w -> { + w.write("generator.writeKey(#L);", Strings.dq(member.getMemberName())); + renderSerializeValue("this." + memberName, Symbols.getShape(memberSymbol), 0); + }); + } + + private void renderSerializeValue(String value, Shape valueShape, int depth) { + if (valueShape.isStructureShape()) { + writer.write("#L.serialize(generator, mapper);", value); + } else if (valueShape.isListShape()) { + Shape elementShape = Shapes.getElementShape((ListShape) valueShape, model); + Symbol elementSymbol = Symbols.boxed(symbolProvider.toSymbol(elementShape)); + String item = "item" + depth; + writer.write("generator.writeStartArray();"); + writer.block( + "for (#T #L : #L)", + new Object[]{elementSymbol, item, value}, + w -> renderSerializeValue(item, elementShape, depth + 1)); + writer.write("generator.writeEnd();"); + } else if (valueShape.isMapShape()) { + Shape elementShape = Shapes.getElementShape((MapShape) valueShape, model); + Symbol elementSymbol = Symbols.boxed(symbolProvider.toSymbol(elementShape)); + String item = "item" + depth; + writer.write("generator.writeStartObject();"); + writer.block( + "for (#T.Entry #L : #L.entrySet())", + new Object[]{RuntimeTypes.JavaUtil.Map, elementSymbol, item, value}, + w -> { + w.write("generator.writeKey(#L.getKey());", item); + renderSerializeValue(item + ".getValue()", elementShape, depth + 1); + }); + writer.write("generator.writeEnd();"); + } else { + writer.write("generator.write(#L);", value); + } + } + + private void renderBuilder() { + writer.block( + "public static class Builder extends #T implements #T<#T>", + new Object[]{RuntimeTypes.Util.ObjectBuilderBase, RuntimeTypes.Util.ObjectBuilder, symbol}, + w -> { + renderFields(false); + + forEachMember(this::renderBuilderSetter); + + w.block( + "public #T build()", + new Object[]{symbol}, + w2 -> { + w2.write("_checkSingleUse();"); + w2.write("return new #T(this);", symbol); + }); + }); + writer.write(""); + } + + private void renderBuilderSetter(MemberShape member, String memberName, Symbol memberSymbol) { + Shape valueShape = Shapes.getTargetShape(member, model); + + if (valueShape.isListShape()) { + writer.block( + "public final Builder #L(#T list)", + new Object[]{memberName, memberSymbol}, + w -> { + w.write("this.#1L = _listAddAll(this.#1L, list);", memberName); + w.write("return this;"); + }); + writer.write(""); + + Symbol elementSymbol = symbolProvider.toSymbol(Shapes.getElementShape((ListShape) valueShape, model)); + writer.block( + "public final Builder #1L(#2T value, #2T... values)", + new Object[]{memberName, elementSymbol}, + w -> { + w.write("this.#1L = _listAdd(this.#1L, value, values);", memberName); + w.write("return this;"); + }); + } else if (valueShape.isMapShape()) { + writer.block( + "public final Builder #L(#T map)", + new Object[]{memberName, memberSymbol}, + w -> { + w.write("this.#1L = _mapPutAll(this.#1L, map);", memberName); + w.write("return this;"); + }); + writer.write(""); + + Symbol elementSymbol = symbolProvider.toSymbol(Shapes.getElementShape((MapShape) valueShape, model)); + writer.block( + "public final Builder #L(String key, #T value)", + new Object[]{memberName, elementSymbol}, + w -> { + w.write("this.#1L = _mapPut(this.#1L, key, value);", memberName); + w.write("return this;"); + }); + } else { + writer.block( + "public final Builder #L(#T value)", + new Object[]{memberName, memberSymbol}, + w -> { + w.write("this.#L = value;", memberName); + w.write("return this;"); + }); + } + + writer.write(""); + } + + private void renderDeserialize() { + String setupDeserialize = writer.format("setup#LDeserializer", symbol.getName()); + + writer.write( + "public static final #1T<#2T> _DESERIALIZER = #3T.lazy(Builder::new, #2T::#4L);", + RuntimeTypes.Json.JsonpDeserializer, symbol, RuntimeTypes.Json.ObjectBuilderDeserializer, setupDeserialize); + writer.write(""); + + writer.block( + "protected static void #L(#T<#T.Builder> op)", + new Object[]{setupDeserialize, RuntimeTypes.Json.ObjectDeserializer, symbol}, + w -> forEachMember(getDocumentFields(), (member, memberName, memberSymbol) -> { + w.write( + "op.add(Builder::#L, #L, #L);", + memberName, valueDeserializer(Symbols.getShape(memberSymbol)), Strings.dq(member.getMemberName())); + })); + writer.write(""); + } + + private String valueDeserializer(Shape valueShape) { + if (valueShape.isStructureShape()) { + return writer.format("#T._DESERIALIZER", symbolProvider.toSymbol(valueShape)); + } else if (valueShape.isListShape()) { + return writer.format("#T.arrayDeserializer(#L)", RuntimeTypes.Json.JsonpDeserializer, valueDeserializer(Shapes.getElementShape((ListShape) valueShape, model))); + } else if (valueShape.isMapShape()) { + return writer.format("#T.stringMapDeserializer(#L)", RuntimeTypes.Json.JsonpDeserializer, valueDeserializer(Shapes.getElementShape((MapShape) valueShape, model))); + } else if (valueShape.isBooleanShape()) { + return writer.format("#T.booleanDeserializer()", RuntimeTypes.Json.JsonpDeserializer); + } else if (valueShape.isStringShape()) { + return writer.format("#T.stringDeserializer()", RuntimeTypes.Json.JsonpDeserializer); + } else if (valueShape.isIntegerShape()) { + return writer.format("#T.integerDeserializer()", RuntimeTypes.Json.JsonpDeserializer); + } else { + throw new UnsupportedOperationException("TODO: Output correct deserializer for " + valueShape); + } + } + + private void renderEndpoint() { + SyntheticInputTrait inputTrait = shape.expectTrait(SyntheticInputTrait.class); + OperationShape operation = model.expectShape(inputTrait.getOperation(), OperationShape.class); + HttpTrait httpTrait = operation.expectTrait(HttpTrait.class); + Shape ouputShape = model.expectShape(operation.getOutputShape()); + Symbol outputSymbol = symbolProvider.toSymbol(ouputShape); + + writer.openBlock( + "public static final #T<#T, #T, #T> _ENDPOINT = new #T<>(", + RuntimeTypes.Transport.Endpoint, + symbol, + outputSymbol, + RuntimeTypes.OpenSearch.Types.ErrorResponse, + RuntimeTypes.Transport.Endpoints.SimpleEndpoint); + + writer.write("// Request method\nrequest -> #L,\n", Strings.dq(httpTrait.getMethod())); + + renderPathBuilder(operation, httpTrait); + + renderQueryParamsBuilder(operation); + + // TODO: headers? + writer.write("// Request headers\n#T.emptyMap(),\n", RuntimeTypes.Transport.Endpoints.SimpleEndpoint); + + // TODO: maybe no requestbody? + writer.write("// Has request body\ntrue,\n"); + + writer.write("// Response deserializer\n#T._DESERIALIZER", outputSymbol); + + writer.closeBlock(");"); + } + + private void renderPathBuilder(OperationShape operation, HttpTrait httpTrait) { + writer.openBlock("// Request path\nrequest -> {"); + writer.write("StringBuilder buf = new StringBuilder();"); + + Map labelMembers = new HashMap<>(); + httpBindingIndex.getRequestBindings(operation, HttpBinding.Location.LABEL) + .stream() + .map(HttpBinding::getMember) + .forEach(m -> labelMembers.put(m.getMemberName(), m)); + + httpTrait.getUri().getSegments().forEach(segment -> { + writer.write("buf.append('/');"); + if (segment.isLabel() || segment.isGreedyLabel()) { + writer.write( + "#T.pathEncode(request.#L, buf);", + RuntimeTypes.Transport.Endpoints.SimpleEndpoint, + symbolProvider.toMemberName(labelMembers.get(segment.getContent()))); + } else { + writer.write("buf.append(#L);", Strings.dq(segment.getContent())); + } + }); + + writer.write("return buf.toString();"); + writer.closeBlock("},\n"); + } + + private void renderQueryParamsBuilder(OperationShape operation) { + writer.openBlock("// Request parameters\nrequest -> {"); + writer.write( + "#T params = new #T<>();", + RuntimeTypes.JavaUtil.Map, + RuntimeTypes.JavaUtil.HashMap + ); + + List paramMembers = httpBindingIndex + .getRequestBindings(operation, HttpBinding.Location.QUERY) + .stream() + .map(HttpBinding::getMember) + .sorted(Comparator.comparing(MemberShape::getMemberName)) + .collect(Collectors.toList()); + + forEachMember(paramMembers, (member, memberName, memberSymbol) -> { + HttpQueryTrait queryTrait = member.expectTrait(HttpQueryTrait.class); + String value = "request." + memberName; + + ifValueDefined(value, memberSymbol, w -> { + w.write( + "params.put(#L, #L);", + Strings.dq(queryTrait.getValue()), + queryParamValueOf(value, Symbols.getShape(memberSymbol))); + }); + }); + + writer.write("return params;"); + writer.closeBlock("},\n"); + } + + private String queryParamValueOf(String value, Shape valueShape) { + if (valueShape.isStringShape()) { + return value; + } else if (valueShape.isBooleanShape()) { + return "String.valueOf(" + value + ")"; + } else { + throw new UnsupportedOperationException("TODO: Output query param value getter for " + valueShape); + } + } + + private void ifValueDefined(String value, Symbol valueSymbol, Consumer block) { + if (!Symbols.isNullable(valueSymbol)) { + block.accept(writer); + return; + } + + String check = Shapes.isListOrMap(Symbols.getShape(valueSymbol)) + ? writer.format("#T.isDefined(#L)", RuntimeTypes.Util.ApiTypeHelper, value) + : writer.format("#L != null", value); + + writer.block("if (#L)", new Object[]{check}, block); + } + + private void forEachMember(TriConsumer op) { + forEachMember(sortedMembers, op); + } + + private void forEachMember(Iterable members, TriConsumer op) { + members.forEach(m -> { + Pair nameAndSymbol = memberNameSymbolIndex.get(m); + op.accept(m, nameAndSymbol.left, nameAndSymbol.right); + }); + } +} diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/utils/Strings.java b/java-codegen/src/main/java/org/opensearch/client/codegen/utils/Strings.java new file mode 100644 index 0000000000..274a1df4fa --- /dev/null +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/utils/Strings.java @@ -0,0 +1,22 @@ +package org.opensearch.client.codegen.utils; + +import software.amazon.smithy.utils.CaseUtils; +import software.amazon.smithy.utils.StringUtils; + +public class Strings { + public static String dq(String str) { + return StringUtils.escapeJavaString(str, ""); + } + + public static String toSnakeCase(String str) { + return CaseUtils.toSnakeCase(str); + } + + public static String toPascalCase(String str) { + return CaseUtils.toPascalCase(toSnakeCase(str)); + } + + public static String toCamelCase(String str) { + return CaseUtils.toCamelCase(toSnakeCase(str)); + } +} diff --git a/java-codegen/src/main/kotlin/org/opensearch/client/codegen/CodegenVisitor.kt b/java-codegen/src/main/kotlin/org/opensearch/client/codegen/CodegenVisitor.kt deleted file mode 100644 index de62c29201..0000000000 --- a/java-codegen/src/main/kotlin/org/opensearch/client/codegen/CodegenVisitor.kt +++ /dev/null @@ -1,80 +0,0 @@ -package org.opensearch.client.codegen - -import org.opensearch.client.codegen.core.GenerationContext -import org.opensearch.client.codegen.core.JavaDelegator -import org.opensearch.client.codegen.core.JavaSymbolProvider -import org.opensearch.client.codegen.core.toRenderingContext -import org.opensearch.client.codegen.model.OperationNormalizer -import org.opensearch.client.codegen.render.ServiceGenerator -import org.opensearch.client.codegen.render.StructureGenerator -import software.amazon.smithy.build.FileManifest -import software.amazon.smithy.build.PluginContext -import software.amazon.smithy.codegen.core.SymbolProvider -import software.amazon.smithy.model.Model -import software.amazon.smithy.model.knowledge.HttpBindingIndex -import software.amazon.smithy.model.neighbor.Walker -import software.amazon.smithy.model.shapes.ServiceShape -import software.amazon.smithy.model.shapes.Shape -import software.amazon.smithy.model.shapes.ShapeVisitor -import software.amazon.smithy.model.shapes.StructureShape -import software.amazon.smithy.model.transform.ModelTransformer -import java.util.logging.Logger - -class CodegenVisitor( - context: PluginContext -) : ShapeVisitor.Default() { - private val logger = Logger.getLogger(javaClass.name) - private val settings = OpenSearchJavaSettings.from(context.model, context.settings) - - private val model: Model - private val service: ServiceShape - private val fileManifest: FileManifest = context.fileManifest - private val symbolProvider: JavaSymbolProvider - private val httpBindingIndex: HttpBindingIndex - private val writers: JavaDelegator - private val generationContext: GenerationContext - - init { - model = baselineTransform(context.model) - service = settings.getService(model) - symbolProvider = JavaSymbolProvider(model, settings) - httpBindingIndex = HttpBindingIndex.of(model) - writers = JavaDelegator(fileManifest, symbolProvider, settings) - generationContext = GenerationContext(model, symbolProvider, httpBindingIndex, settings) - } - - private fun baselineTransform(model: Model) = - model - .let { ModelTransformer.create().flattenAndRemoveMixins(it) } - .let { ModelTransformer.create().copyServiceErrorsToOperations(it, settings.getService(it)) } - .let { OperationNormalizer.transform(it) } - - fun execute() { - val service = settings.getService(model) - val serviceShapes = Walker(model).walkShapes(service) - serviceShapes.forEach { it.accept(this) } - writers.finalize() - } - - override fun getDefault(shape: Shape?) {} - - override fun serviceShape(shape: ServiceShape) { - logger.info("Generating structure ${shape.id.name}") - writers.useShapeWriter(shape) { - val ctx = generationContext.toRenderingContext(it, shape) - ServiceGenerator(ctx).render() - } - val asyncSymbol = symbolProvider.serviceAsyncSymbol(shape) - writers.useSymbolWriter(asyncSymbol) { - val ctx = generationContext.toRenderingContext(it, shape, asyncSymbol) - ServiceGenerator(ctx).render() - } - } - - override fun structureShape(shape: StructureShape) { - writers.useShapeWriter(shape) { - val ctx = generationContext.toRenderingContext(it, shape) - StructureGenerator(ctx).render() - } - } -} \ No newline at end of file diff --git a/java-codegen/src/main/kotlin/org/opensearch/client/codegen/OpenSearchJavaCodegenPlugin.kt b/java-codegen/src/main/kotlin/org/opensearch/client/codegen/OpenSearchJavaCodegenPlugin.kt deleted file mode 100644 index f9b8b2cff7..0000000000 --- a/java-codegen/src/main/kotlin/org/opensearch/client/codegen/OpenSearchJavaCodegenPlugin.kt +++ /dev/null @@ -1,12 +0,0 @@ -package org.opensearch.client.codegen - -import software.amazon.smithy.build.PluginContext -import software.amazon.smithy.build.SmithyBuildPlugin - -class OpenSearchJavaCodegenPlugin : SmithyBuildPlugin { - override fun getName(): String = "opensearch-java" - - override fun execute(context: PluginContext) { - CodegenVisitor(context).execute() - } -} \ No newline at end of file diff --git a/java-codegen/src/main/kotlin/org/opensearch/client/codegen/OpenSearchJavaSettings.kt b/java-codegen/src/main/kotlin/org/opensearch/client/codegen/OpenSearchJavaSettings.kt deleted file mode 100644 index 9e4dd8146e..0000000000 --- a/java-codegen/src/main/kotlin/org/opensearch/client/codegen/OpenSearchJavaSettings.kt +++ /dev/null @@ -1,70 +0,0 @@ -package org.opensearch.client.codegen - -import org.opensearch.client.codegen.model.expectShape -import software.amazon.smithy.codegen.core.CodegenException -import software.amazon.smithy.model.Model -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.logging.Logger -import kotlin.streams.toList - -const val SERVICE: String = "service" - -class OpenSearchJavaSettings( - val service: ShapeId -) { - fun getService(model: Model): ServiceShape { - return model.expectShape(service) - } - - companion object { - private val LOGGER: Logger = Logger.getLogger(OpenSearchJavaSettings::class.java.name) - - fun from(model: Model, config: ObjectNode): OpenSearchJavaSettings { - config.warnIfAdditionalProperties( - arrayListOf( - SERVICE - ) - ) - - val service = config.getStringMember(SERVICE) - .map(StringNode::expectShapeId) - .orElseGet { inferService(model) } - - return OpenSearchJavaSettings(service) - } - - @JvmStatic - private 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 - } - } - } - } -} \ No newline at end of file diff --git a/java-codegen/src/main/kotlin/org/opensearch/client/codegen/core/CodegenContext.kt b/java-codegen/src/main/kotlin/org/opensearch/client/codegen/core/CodegenContext.kt deleted file mode 100644 index 7317d32134..0000000000 --- a/java-codegen/src/main/kotlin/org/opensearch/client/codegen/core/CodegenContext.kt +++ /dev/null @@ -1,38 +0,0 @@ -package org.opensearch.client.codegen.core - -import org.opensearch.client.codegen.OpenSearchJavaSettings -import software.amazon.smithy.codegen.core.Symbol -import software.amazon.smithy.codegen.core.SymbolProvider -import software.amazon.smithy.model.Model -import software.amazon.smithy.model.knowledge.HttpBindingIndex -import software.amazon.smithy.model.shapes.Shape - -interface CodegenContext { - val model: Model - val symbolProvider: SymbolProvider - val httpBindingIndex: HttpBindingIndex - val settings: OpenSearchJavaSettings -} - -data class GenerationContext( - override val model: Model, - override val symbolProvider: SymbolProvider, - override val httpBindingIndex: HttpBindingIndex, - override val settings: OpenSearchJavaSettings -) : CodegenContext - -fun CodegenContext.toRenderingContext(writer: JavaWriter, forShape: T? = null, symbol: Symbol? = null) = - RenderingContext(this, writer, forShape, symbol) - -data class RenderingContext( - val writer: JavaWriter, - val shape: T?, - val symbol: Symbol, - override val model: Model, - override val symbolProvider: SymbolProvider, - override val httpBindingIndex: HttpBindingIndex, - override val settings: OpenSearchJavaSettings -) : CodegenContext { - constructor(other: CodegenContext, writer: JavaWriter, shape: T?, symbol: Symbol? = null) : - this(writer, shape, symbol ?: other.symbolProvider.toSymbol(shape), other.model, other.symbolProvider, other.httpBindingIndex, other.settings) -} \ No newline at end of file diff --git a/java-codegen/src/main/kotlin/org/opensearch/client/codegen/core/ImportStatements.kt b/java-codegen/src/main/kotlin/org/opensearch/client/codegen/core/ImportStatements.kt deleted file mode 100644 index bfff96e3b5..0000000000 --- a/java-codegen/src/main/kotlin/org/opensearch/client/codegen/core/ImportStatements.kt +++ /dev/null @@ -1,30 +0,0 @@ -package org.opensearch.client.codegen.core - -import software.amazon.smithy.codegen.core.CodegenException -import software.amazon.smithy.codegen.core.ImportContainer -import software.amazon.smithy.codegen.core.Symbol - -class ImportStatements(private val packageName: String) : ImportContainer { - private val imports: MutableSet = mutableSetOf() - - override fun importSymbol(symbol: Symbol, alias: String?) { - if (alias != symbol.name) { - throw CodegenException("Java doesn't allow import aliasing") - } - - if (symbol.namespace.isNotEmpty() && symbol.namespace != packageName) { - imports.add(ImportStatement(symbol.namespace, symbol.name)) - } - } - - override fun toString(): String { - return imports.map { it.toString() }.sorted().joinToString(separator = "\n") - } -} - -private data class ImportStatement(val packageName: String, val symbolName: String) { - val rendered: String - get() = "import $packageName.$symbolName;" - - override fun toString(): String = rendered -} \ No newline at end of file diff --git a/java-codegen/src/main/kotlin/org/opensearch/client/codegen/core/JavaDelegator.kt b/java-codegen/src/main/kotlin/org/opensearch/client/codegen/core/JavaDelegator.kt deleted file mode 100644 index 42410a3a6d..0000000000 --- a/java-codegen/src/main/kotlin/org/opensearch/client/codegen/core/JavaDelegator.kt +++ /dev/null @@ -1,27 +0,0 @@ -package org.opensearch.client.codegen.core - -import org.opensearch.client.codegen.OpenSearchJavaSettings -import software.amazon.smithy.build.FileManifest -import software.amazon.smithy.codegen.core.* -import software.amazon.smithy.model.shapes.Shape -import java.util.function.Consumer - -class JavaDelegator( - fileManifest: FileManifest, - symbolProvider: SymbolProvider, - settings: OpenSearchJavaSettings -) { - private val inner = WriterDelegator(fileManifest, symbolProvider, JavaWriter.factory()) - - fun useShapeWriter(shape: Shape, writerConsumer: Consumer) { - inner.useShapeWriter(shape, writerConsumer) - } - - fun useSymbolWriter(symbol: Symbol, writerConsumer: Consumer) { - inner.useFileWriter(symbol.definitionFile, symbol.namespace, writerConsumer) - } - - fun finalize() { - inner.flushWriters() - } -} \ No newline at end of file diff --git a/java-codegen/src/main/kotlin/org/opensearch/client/codegen/core/JavaSymbolProvider.kt b/java-codegen/src/main/kotlin/org/opensearch/client/codegen/core/JavaSymbolProvider.kt deleted file mode 100644 index d97b523374..0000000000 --- a/java-codegen/src/main/kotlin/org/opensearch/client/codegen/core/JavaSymbolProvider.kt +++ /dev/null @@ -1,176 +0,0 @@ -package org.opensearch.client.codegen.core - -import org.opensearch.client.codegen.OpenSearchJavaSettings -import org.opensearch.client.codegen.model.SymbolProperty -import org.opensearch.client.codegen.model.boxed -import org.opensearch.client.codegen.model.nullable -import software.amazon.smithy.codegen.core.ReservedWordSymbolProvider -import software.amazon.smithy.codegen.core.Symbol -import software.amazon.smithy.codegen.core.SymbolProvider -import software.amazon.smithy.model.Model -import software.amazon.smithy.model.knowledge.NullableIndex -import software.amazon.smithy.model.shapes.* -import java.util.logging.Logger - -class JavaSymbolProvider( - private val model: Model, - private val settings: OpenSearchJavaSettings -) : SymbolProvider, ShapeVisitor { - private val rootNamespace = "org.opensearch.client" - private val service = settings.getService(model) - private val logger = Logger.getLogger(javaClass.name) - private val escaper: ReservedWordSymbolProvider.Escaper - private val nullableIndex = NullableIndex(model) - - private var depth = 0 - - init { - val reservedWords = javaReservedWords() - escaper = ReservedWordSymbolProvider.builder() - .nameReservedWords(reservedWords) - .memberReservedWords(reservedWords) - .escapePredicate { _, symbol -> symbol.definitionFile.isNotEmpty() } - .buildEscaper() - } - - companion object { - fun isTypeGeneratedForShape(shape: Shape): Boolean = when { - shape.isEnumShape || shape.isIntEnumShape || shape.isStructureShape || shape.isUnionShape -> true - else -> false - } - } - - override fun toSymbol(shape: Shape): Symbol { - depth++ - val symbol: Symbol = shape.accept(this) - depth-- - logger.fine("creating symbol from $shape: $symbol") - return escaper.escapeSymbol(shape, symbol) - } - - override fun toMemberName(shape: MemberShape): String = escaper.escapeMemberName(shape.defaultName()) - - override fun blobShape(shape: BlobShape?): Symbol { - TODO("Not yet implemented") - } - - override fun booleanShape(shape: BooleanShape?): Symbol = - createSymbolBuilder(shape, "boolean").build() - - override fun listShape(shape: ListShape): Symbol { - val reference = toSymbol(shape.member).toBuilder().boxed().build() - return createSymbolBuilder(shape, "List<${reference.name}>") - .namespace("java.util", ".") - .addReference(reference) - .build() - } - - override fun mapShape(shape: MapShape): Symbol { - val reference = toSymbol(shape.value).toBuilder().boxed().build() - return createSymbolBuilder(shape, "Map") - .namespace("java.util", ".") - .addReference(reference) - .build() - } - - override fun byteShape(shape: ByteShape?): Symbol { - TODO("Not yet implemented") - } - - override fun shortShape(shape: ShortShape?): Symbol { - TODO("Not yet implemented") - } - - override fun integerShape(shape: IntegerShape?): Symbol = - createSymbolBuilder(shape, "int").build() - - override fun longShape(shape: LongShape?): Symbol { - TODO("Not yet implemented") - } - - override fun floatShape(shape: FloatShape?): Symbol { - TODO("Not yet implemented") - } - - override fun documentShape(shape: DocumentShape?): Symbol { - TODO("Not yet implemented") - } - - override fun doubleShape(shape: DoubleShape?): Symbol { - TODO("Not yet implemented") - } - - override fun bigIntegerShape(shape: BigIntegerShape?): Symbol { - TODO("Not yet implemented") - } - - override fun bigDecimalShape(shape: BigDecimalShape?): Symbol { - TODO("Not yet implemented") - } - - override fun operationShape(shape: OperationShape): Symbol { - val name = shape.defaultName() - return createSymbolBuilder(shape, name) - .namespace(rootNamespace, ".") - .definitionFile("$name.java") - .build() - } - - override fun resourceShape(shape: ResourceShape?): Symbol { - TODO("Not yet implemented") - } - - override fun serviceShape(shape: ServiceShape): Symbol { - val name = shape.defaultName() - return createSymbolBuilder(shape, name) - .namespace(rootNamespace, ".") - .definitionFile("$name.java") - .build() - } - - public fun serviceAsyncSymbol(shape: ServiceShape): Symbol { - val name = shape.asyncName() - return createSymbolBuilder(shape, name) - .namespace(rootNamespace, ".") - .definitionFile("$name.java") - .build() - } - - override fun stringShape(shape: StringShape?): Symbol = - createSymbolBuilder(shape, "String").build() - - override fun structureShape(shape: StructureShape): Symbol { - val name = shape.defaultName() - return createSymbolBuilder(shape, name) - .namespace(rootNamespace, ".") - .definitionFile("$name.java") - .build() - } - - override fun unionShape(shape: UnionShape?): Symbol { - TODO("Not yet implemented") - } - - override fun memberShape(shape: MemberShape): Symbol { - val targetShape = model.expectShape(shape.target) - - val targetSymbol = if (nullableIndex.isMemberNullable(shape, NullableIndex.CheckMode.CLIENT)) { - toSymbol(targetShape).toBuilder().nullable().build() - } else { - toSymbol(targetShape) - } - - return targetSymbol - } - - override fun timestampShape(shape: TimestampShape?): Symbol { - TODO("Not yet implemented") - } - - private fun createSymbolBuilder(shape: Shape?, typeName: String): Symbol.Builder { - val builder = Symbol.builder() - .putProperty(SymbolProperty.SHAPE_KEY, shape) - .name(typeName) - return builder - } -} \ No newline at end of file diff --git a/java-codegen/src/main/kotlin/org/opensearch/client/codegen/core/JavaVisibility.kt b/java-codegen/src/main/kotlin/org/opensearch/client/codegen/core/JavaVisibility.kt deleted file mode 100644 index cdf8454358..0000000000 --- a/java-codegen/src/main/kotlin/org/opensearch/client/codegen/core/JavaVisibility.kt +++ /dev/null @@ -1,11 +0,0 @@ -package org.opensearch.client.codegen.core - -enum class JavaVisibility { - PUBLIC, - PROTECTED, - PRIVATE; - - override fun toString(): String { - return super.toString().lowercase() - } -} \ No newline at end of file diff --git a/java-codegen/src/main/kotlin/org/opensearch/client/codegen/core/JavaWriter.kt b/java-codegen/src/main/kotlin/org/opensearch/client/codegen/core/JavaWriter.kt deleted file mode 100644 index 1a128c24eb..0000000000 --- a/java-codegen/src/main/kotlin/org/opensearch/client/codegen/core/JavaWriter.kt +++ /dev/null @@ -1,94 +0,0 @@ -package org.opensearch.client.codegen.core - -import software.amazon.smithy.codegen.core.CodegenException -import software.amazon.smithy.codegen.core.Symbol -import software.amazon.smithy.codegen.core.SymbolWriter -import software.amazon.smithy.codegen.core.SymbolWriter.Factory -import software.amazon.smithy.utils.AbstractCodeWriter -import java.util.function.BiFunction - -class JavaWriter private constructor( - private val filename: String, - val packageName: String -) : SymbolWriter(ImportStatements(packageName)) { - companion object { - fun factory(): Factory = - Factory { fileName: String, packageName: String -> JavaWriter(fileName, packageName) } - } - - init { - trimBlankLines() - trimTrailingSpaces() - - indentText = " " - expressionStart = '#' - - putFormatter('T', JavaSymbolFormatter(this)) - } - - fun addImport(symbol: Symbol, alias: String = symbol.name): JavaWriter { - return super.addImport(symbol, alias) - } - - override fun toString(): String { - val contents = super.toString() - val pkgDecl = "package $packageName;\n\n" - val imports = "${importContainer}\n\n" - return pkgDecl + imports + contents - } -} - -private class JavaSymbolFormatter( - private val writer: JavaWriter -) : BiFunction { - override fun apply(type: Any, indent: String): String { - when (type) { - is Symbol -> { - writer.addImport(type) - return type.name - } - - else -> throw CodegenException("Invalid type provided for #T. Expected a Symbol, but found `$type`") - } - } -} - -fun > T.block( - header: String, - vararg args: Any, - block: T.() -> Unit -): T { - openBlock("$header {", *args) - block(this) - closeBlock("}") - return this -} - -fun > T.javaClass( - visibility: JavaVisibility, - name: String, - annotations: List? = null, - extends: Symbol? = null, - implements: List? = null, - body: T.() -> Unit -): T { - annotations?.forEach { - write("@#T", it) - } - var header = format( - "#L #L", - visibility, - name - ) - if (extends != null) { - header = format("#L extends #T", header, extends) - } - if (!implements.isNullOrEmpty()) { - header = format( - "#L implements #L", - header, - implements.joinToString(separator = ", ") { format("#T", it) } - ) - } - return block(header, block = body) -} \ No newline at end of file diff --git a/java-codegen/src/main/kotlin/org/opensearch/client/codegen/core/Naming.kt b/java-codegen/src/main/kotlin/org/opensearch/client/codegen/core/Naming.kt deleted file mode 100644 index 0f2bd04aa7..0000000000 --- a/java-codegen/src/main/kotlin/org/opensearch/client/codegen/core/Naming.kt +++ /dev/null @@ -1,18 +0,0 @@ -package org.opensearch.client.codegen.core - -import org.opensearch.client.codegen.utils.toCamelCase -import org.opensearch.client.codegen.utils.toPascalCase -import software.amazon.smithy.model.shapes.MemberShape -import software.amazon.smithy.model.shapes.OperationShape -import software.amazon.smithy.model.shapes.ServiceShape -import software.amazon.smithy.model.shapes.StructureShape - -fun MemberShape.defaultName(): String = memberName.toCamelCase() - -fun OperationShape.defaultName(): String = id.name.toPascalCase() - -fun ServiceShape.defaultName(): String = id.name.toPascalCase() + "Client" - -fun ServiceShape.asyncName(): String = id.name.toPascalCase() + "AsyncClient" - -fun StructureShape.defaultName(): String = id.name.toPascalCase() \ No newline at end of file diff --git a/java-codegen/src/main/kotlin/org/opensearch/client/codegen/core/ReservedWords.kt b/java-codegen/src/main/kotlin/org/opensearch/client/codegen/core/ReservedWords.kt deleted file mode 100644 index cef3dde00d..0000000000 --- a/java-codegen/src/main/kotlin/org/opensearch/client/codegen/core/ReservedWords.kt +++ /dev/null @@ -1,12 +0,0 @@ -package org.opensearch.client.codegen.core - -import software.amazon.smithy.codegen.core.ReservedWords -import software.amazon.smithy.codegen.core.ReservedWordsBuilder - -fun javaReservedWords(): ReservedWords = ReservedWordsBuilder().apply { - hardReservedWords.forEach { put(it, "_$it") } -}.build() - -val hardReservedWords = listOf( - "null" -) \ No newline at end of file diff --git a/java-codegen/src/main/kotlin/org/opensearch/client/codegen/core/RuntimeTypes.kt b/java-codegen/src/main/kotlin/org/opensearch/client/codegen/core/RuntimeTypes.kt deleted file mode 100644 index a26d6dc5e2..0000000000 --- a/java-codegen/src/main/kotlin/org/opensearch/client/codegen/core/RuntimeTypes.kt +++ /dev/null @@ -1,73 +0,0 @@ -package org.opensearch.client.codegen.core - -import org.opensearch.client.codegen.model.toSymbol - -object RuntimeTypes { - private const val ClientPkg = "org.opensearch.client" - - val ApiClient = "$ClientPkg.ApiClient".toSymbol() - - object Json { - private const val JsonPkg = "$ClientPkg.json" - - val JsonpDeserializable = "$JsonPkg.JsonpDeserializable".toSymbol() - val JsonpDeserializer = "$JsonPkg.JsonpDeserializer".toSymbol() - val JsonpMapper = "$JsonPkg.JsonpMapper".toSymbol() - val JsonpSerializable = "$JsonPkg.JsonpSerializable".toSymbol() - val ObjectBuilderDeserializer = "$JsonPkg.ObjectBuilderDeserializer".toSymbol() - val ObjectDeserializer = "$JsonPkg.ObjectDeserializer".toSymbol() - } - - object OpenSearch { - private const val OpenSearchPkg = "$ClientPkg.opensearch" - - object Types { - private const val TypesPkg = "$OpenSearchPkg._types" - - val ErrorResponse = "$TypesPkg.ErrorResponse".toSymbol() - val OpenSearchException = "$TypesPkg.OpenSearchException".toSymbol() - val RequestBase = "$TypesPkg.RequestBase".toSymbol() - } - } - - object Transport { - private const val TransportPkg = "$ClientPkg.transport" - - val Endpoint = "$TransportPkg.Endpoint".toSymbol() - val JsonEndpoint = "$TransportPkg.JsonEndpoint".toSymbol() - val OpenSearchTransport = "$TransportPkg.OpenSearchTransport".toSymbol() - val TransportOptions = "$TransportPkg.TransportOptions".toSymbol() - - object Endpoints { - private const val EndpointsPkg = "$TransportPkg.endpoints" - - val SimpleEndpoint = "$EndpointsPkg.SimpleEndpoint".toSymbol() - } - } - - object Util { - private const val UtilPkg = "$ClientPkg.util" - - val ApiTypeHelper = "$UtilPkg.ApiTypeHelper".toSymbol() - val ObjectBuilder = "$UtilPkg.ObjectBuilder".toSymbol() - val ObjectBuilderBase = "$UtilPkg.ObjectBuilderBase".toSymbol() - } - - object Jakarta { - val JsonGenerator = "jakarta.json.stream.JsonGenerator".toSymbol() - } - - object JavaIo { - val IOException = "java.io.IOException".toSymbol() - } - - object JavaUtil { - val Function = "java.util.function.Function".toSymbol() - val HashMap = "java.util.HashMap".toSymbol() - val Map = "java.util.Map".toSymbol() - } - - object Javax { - val Nullable = "javax.annotation.Nullable".toSymbol() - } -} \ No newline at end of file diff --git a/java-codegen/src/main/kotlin/org/opensearch/client/codegen/core/traits/SyntheticInputTrait.kt b/java-codegen/src/main/kotlin/org/opensearch/client/codegen/core/traits/SyntheticInputTrait.kt deleted file mode 100644 index b3f7cb7fd9..0000000000 --- a/java-codegen/src/main/kotlin/org/opensearch/client/codegen/core/traits/SyntheticInputTrait.kt +++ /dev/null @@ -1,14 +0,0 @@ -package org.opensearch.client.codegen.core.traits - -import software.amazon.smithy.model.node.Node -import software.amazon.smithy.model.shapes.ShapeId -import software.amazon.smithy.model.traits.AnnotationTrait - -class SyntheticInputTrait( - val operation: ShapeId, - val originalId: ShapeId?, -) : AnnotationTrait(ID, Node.objectNode()) { - companion object { - val ID: ShapeId = ShapeId.from("smithy.api.internal#syntheticInput") - } -} diff --git a/java-codegen/src/main/kotlin/org/opensearch/client/codegen/model/ModelExt.kt b/java-codegen/src/main/kotlin/org/opensearch/client/codegen/model/ModelExt.kt deleted file mode 100644 index aa89a19287..0000000000 --- a/java-codegen/src/main/kotlin/org/opensearch/client/codegen/model/ModelExt.kt +++ /dev/null @@ -1,8 +0,0 @@ -package org.opensearch.client.codegen.model - -import software.amazon.smithy.model.Model -import software.amazon.smithy.model.shapes.Shape -import software.amazon.smithy.model.shapes.ShapeId - -@Suppress("EXTENSION_SHADOWED_BY_MEMBER") -inline fun Model.expectShape(id: ShapeId): T = expectShape(id, T::class.java) \ No newline at end of file diff --git a/java-codegen/src/main/kotlin/org/opensearch/client/codegen/model/OperationNormalizer.kt b/java-codegen/src/main/kotlin/org/opensearch/client/codegen/model/OperationNormalizer.kt deleted file mode 100644 index d6b414d499..0000000000 --- a/java-codegen/src/main/kotlin/org/opensearch/client/codegen/model/OperationNormalizer.kt +++ /dev/null @@ -1,64 +0,0 @@ -package org.opensearch.client.codegen.model - -import org.opensearch.client.codegen.core.traits.SyntheticInputTrait -import software.amazon.smithy.model.Model -import software.amazon.smithy.model.shapes.OperationShape -import software.amazon.smithy.model.shapes.Shape -import software.amazon.smithy.model.shapes.ShapeId -import software.amazon.smithy.model.shapes.StructureShape -import software.amazon.smithy.model.transform.ModelTransformer -import java.util.* -import kotlin.streams.toList - -object OperationNormalizer { - private fun OperationShape.syntheticInputId() = - ShapeId.fromParts(this.id.namespace + ".synthetic", "${this.id.name}Request") - - private fun OperationShape.syntheticOutputId() = - ShapeId.fromParts(this.id.namespace + ".synthetic", "${this.id.name}Response") - - fun transform(model: Model): Model { - val transformer = ModelTransformer.create() - val operations = model.shapes(OperationShape::class.java).toList() - val newShapes = operations.flatMap { operation -> - // Generate or modify the input and output of the given `Operation` to be a unique shape - listOf(syntheticInputShape(model, operation), syntheticOutputShape(model, operation)) - } - val shapeConflict = newShapes.firstOrNull { shape -> model.getShape(shape.id).isPresent } - check( - shapeConflict == null, - ) { "shape $shapeConflict conflicted with an existing shape in the model (${model.getShape(shapeConflict!!.id)}. This is a bug." } - val modelWithOperationInputs = model.toBuilder().addShapes(newShapes).build() - return transformer.mapShapes(modelWithOperationInputs) { - // Update all operations to point to their new input/output shapes - val transformed: Optional = it.asOperationShape().map { operation -> - modelWithOperationInputs.expectShape(operation.syntheticInputId()) - operation.toBuilder() - .input(operation.syntheticInputId()) - .output(operation.syntheticOutputId()) - .build() - } - transformed.orElse(it) - } - } - - private fun syntheticOutputShape(model: Model, operation: OperationShape): StructureShape { - val outputId = operation.syntheticOutputId() - val outputShapeBuilder = operation.output.map { shapeId -> - model.expectShape(shapeId, StructureShape::class.java).toBuilder().rename(outputId) - }.orElse(empty(outputId)) - return outputShapeBuilder.build() - } - - private fun syntheticInputShape(model: Model, operation: OperationShape): StructureShape { - val inputId = operation.syntheticInputId() - val inputShapeBuilder = operation.input.map { shapeId -> - model.expectShape(shapeId, StructureShape::class.java).toBuilder().rename(inputId) - }.orElse(empty(inputId)) - return inputShapeBuilder.addTrait( - SyntheticInputTrait(operation.id, operation.input.orElse(null)) - ).build() - } - - private fun empty(id: ShapeId): StructureShape.Builder = StructureShape.builder().id(id) -} \ No newline at end of file diff --git a/java-codegen/src/main/kotlin/org/opensearch/client/codegen/model/ShapeExt.kt b/java-codegen/src/main/kotlin/org/opensearch/client/codegen/model/ShapeExt.kt deleted file mode 100644 index 02b1921707..0000000000 --- a/java-codegen/src/main/kotlin/org/opensearch/client/codegen/model/ShapeExt.kt +++ /dev/null @@ -1,25 +0,0 @@ -package org.opensearch.client.codegen.model - -import software.amazon.smithy.model.Model -import software.amazon.smithy.model.shapes.OperationShape -import software.amazon.smithy.model.shapes.Shape -import software.amazon.smithy.model.shapes.ShapeId -import software.amazon.smithy.model.shapes.StructureShape -import software.amazon.smithy.model.traits.Trait - -inline fun Shape.hasTrait(): Boolean = hasTrait(T::class.java) -inline fun Shape.getTrait(): T? = getTrait(T::class.java).orElse(null) -inline fun Shape.expectTrait(): T = expectTrait(T::class.java) - -fun OperationShape.inputShape(model: Model): StructureShape = model.expectShape(this.inputShape) -fun OperationShape.outputShape(model: Model): StructureShape = model.expectShape(this.outputShape) - -fun StructureShape.Builder.rename(newId: ShapeId): StructureShape.Builder { - val renamedMembers = this.build().members().map { - it.toBuilder().id(newId.withMember(it.memberName)).build() - } - return this.id(newId).members(renamedMembers) -} - -val Shape.isListOrMap: Boolean - get() = isListShape || isMapShape \ No newline at end of file diff --git a/java-codegen/src/main/kotlin/org/opensearch/client/codegen/model/SymbolExt.kt b/java-codegen/src/main/kotlin/org/opensearch/client/codegen/model/SymbolExt.kt deleted file mode 100644 index 6f48e93456..0000000000 --- a/java-codegen/src/main/kotlin/org/opensearch/client/codegen/model/SymbolExt.kt +++ /dev/null @@ -1,60 +0,0 @@ -package org.opensearch.client.codegen.model - -import software.amazon.smithy.codegen.core.Symbol -import software.amazon.smithy.model.shapes.Shape - -object SymbolProperty { - const val SHAPE_KEY: String = "shape" - const val NULLABLE_KEY: String = "nullable" -} - -fun String.toSymbol(): Symbol { - require(isNotBlank()) { "Invalid string to convert to symbol" } - val segments = split(".") - val name = segments.last() - val namespace = segments.dropLast(1).joinToString(separator = ".") { it } - return Symbol.builder() - .name(name) - .namespace(namespace, ".") - .build() -} - -fun Symbol.Builder.boxed(): Symbol.Builder { - val symbol = build() - val newName = if (symbol.namespace.isNullOrEmpty()) { - when (symbol.name) { - "byte" -> "Byte" - "short" -> "Short" - "int" -> "Integer" - "long" -> "Long" - "float" -> "Float" - "double" -> "Double" - "boolean" -> "Boolean" - "char" -> "Character" - else -> null - } - } else { - null - } - - return if (newName != null) { - this.name(newName) - } else { - this - } -} - -fun Symbol.Builder.nullable(): Symbol.Builder = - boxed() - .putProperty(SymbolProperty.NULLABLE_KEY, true) - -val Symbol.isNullable: Boolean - get() = getProperty(SymbolProperty.NULLABLE_KEY).map { - when (it) { - is Boolean -> it - else -> false - } - }.orElse(false) - -val Symbol.shape: Shape - get() = getProperty(SymbolProperty.SHAPE_KEY, Shape::class.java).get() \ No newline at end of file diff --git a/java-codegen/src/main/kotlin/org/opensearch/client/codegen/render/ServiceGenerator.kt b/java-codegen/src/main/kotlin/org/opensearch/client/codegen/render/ServiceGenerator.kt deleted file mode 100644 index d8e5265e7d..0000000000 --- a/java-codegen/src/main/kotlin/org/opensearch/client/codegen/render/ServiceGenerator.kt +++ /dev/null @@ -1,91 +0,0 @@ -package org.opensearch.client.codegen.render - -import org.opensearch.client.codegen.core.* -import org.opensearch.client.codegen.model.inputShape -import org.opensearch.client.codegen.model.outputShape -import org.opensearch.client.codegen.utils.toCamelCase -import software.amazon.smithy.codegen.core.SymbolProvider -import software.amazon.smithy.model.Model -import software.amazon.smithy.model.knowledge.TopDownIndex -import software.amazon.smithy.model.shapes.OperationShape -import software.amazon.smithy.model.shapes.ServiceShape - -class ServiceGenerator( - private val ctx: RenderingContext -) { - private val model = ctx.model - private val shape = ctx.shape - private val symbol = ctx.symbolProvider.toSymbol(ctx.shape) - private val symbolProvider = ctx.symbolProvider - private val writer = ctx.writer - private val index = TopDownIndex.of(model) - - fun render() { - writer.block( - "public class #1T extends #2T<#3T, #1T>", - symbol, RuntimeTypes.ApiClient, RuntimeTypes.Transport.OpenSearchTransport - ) { - renderConstructors() - - val operations = index.getContainedOperations(shape).sortedBy { it.id } - operations.forEach(::renderOperation) - } - } - - private fun renderConstructors() { - writer.block( - "public #T(#T transport)", - symbol, RuntimeTypes.Transport.OpenSearchTransport - ) { - write("super(transport);") - } - writer.write("") - - writer.block( - "public #T(#T transport, @#T #T transportOptions)", - symbol, RuntimeTypes.Transport.OpenSearchTransport, RuntimeTypes.Javax.Nullable, RuntimeTypes.Transport.TransportOptions - ) { - write("super(transport, transportOptions);") - } - writer.write("") - - writer.block( - "@Override public #T withTransportOptions(@#T #T transportOptions)", - symbol, RuntimeTypes.Javax.Nullable, RuntimeTypes.Transport.TransportOptions - ) { - write("return new #T(this.transport, transportOptions);", symbol) - } - writer.write("") - } - - private fun renderOperation(operation: OperationShape) { - val inputShape = operation.inputShape(model) - val inputSymbol = symbolProvider.toSymbol(inputShape) - val outputShape = operation.outputShape(model) - val outputSymbol = symbolProvider.toSymbol(outputShape) - val operationName = operation.id.name.toCamelCase() - - writer.block( - "public #T #L(#T request) throws #T, #T", - outputSymbol, operationName, inputSymbol, RuntimeTypes.JavaIo.IOException, RuntimeTypes.OpenSearch.Types.OpenSearchException - ) { - val endpointType = format("#T<#T, #T, #T>", RuntimeTypes.Transport.JsonEndpoint, inputSymbol, outputSymbol, RuntimeTypes.OpenSearch.Types.ErrorResponse) - - write(""" - @SuppressWarnings("unchecked") - #1L endpoint = (#1L) #2T._ENDPOINT; - - return this.transport.performRequest(request, endpoint, this.transportOptions); - """.trimIndent(), endpointType, inputSymbol) - } - writer.write("") - - writer.block( - "public final #1T #2L(#3T<#4T.Builder, #5T<#4T>> fn) throws #6T, #7T", - outputSymbol, operationName, RuntimeTypes.JavaUtil.Function, inputSymbol, RuntimeTypes.Util.ObjectBuilder, RuntimeTypes.JavaIo.IOException, RuntimeTypes.OpenSearch.Types.OpenSearchException - ) { - write("return #L(fn.apply(new #T.Builder()).build());", operationName, inputSymbol) - } - writer.write("") - } -} \ No newline at end of file diff --git a/java-codegen/src/main/kotlin/org/opensearch/client/codegen/render/StructureGenerator.kt b/java-codegen/src/main/kotlin/org/opensearch/client/codegen/render/StructureGenerator.kt deleted file mode 100644 index e39316d772..0000000000 --- a/java-codegen/src/main/kotlin/org/opensearch/client/codegen/render/StructureGenerator.kt +++ /dev/null @@ -1,450 +0,0 @@ -package org.opensearch.client.codegen.render - -import org.opensearch.client.codegen.core.* -import org.opensearch.client.codegen.core.traits.SyntheticInputTrait -import org.opensearch.client.codegen.model.* -import org.opensearch.client.codegen.utils.dq -import software.amazon.smithy.codegen.core.Symbol -import software.amazon.smithy.model.knowledge.HttpBinding -import software.amazon.smithy.model.shapes.* -import software.amazon.smithy.model.traits.HttpQueryTrait -import software.amazon.smithy.model.traits.HttpTrait - -class StructureGenerator( - private val ctx: RenderingContext -) { - private val shape = requireNotNull(ctx.shape) - private val writer = ctx.writer - private val symbolProvider = ctx.symbolProvider - private val model = ctx.model - private val symbol = ctx.symbolProvider.toSymbol(ctx.shape) - private val isInput = shape.hasTrait() - - private val sortedMembers: List = shape.allMembers.values.sortedBy { it.defaultName() } - private val memberNameSymbolIndex: Map> = - sortedMembers.associateWith { member -> - Pair(symbolProvider.toMemberName(member), symbolProvider.toSymbol(member)) - } - - fun render() { - writer.javaClass( - JavaVisibility.PUBLIC, - symbol.name, - annotations = listOf(RuntimeTypes.Json.JsonpDeserializable), - extends = if (isInput) RuntimeTypes.OpenSearch.Types.RequestBase else null, - implements = listOf(RuntimeTypes.Json.JsonpSerializable) - ) { - renderFields() - renderConstructor() - renderGetters() - renderSerialize() - renderBuilder() - renderDeserialize() - if (isInput) { - renderEndpoint() - } - } - } - - private fun forEachMember(op: (memberShape: MemberShape, memberName: String, memberSymbol: Symbol) -> Unit) = - forEachMember(sortedMembers, op) - - private fun forEachMember( - members: List, - op: (memberShape: MemberShape, memberName: String, memberSymbol: Symbol) -> Unit - ) = - members.forEach { - val (memberName, memberSymbol) = memberNameSymbolIndex[it]!! - - op(it, memberName, memberSymbol) - } - - private fun renderFields(final: Boolean = true) { - forEachMember { _, memberName, memberSymbol -> - if (memberSymbol.isNullable && !(final && memberSymbol.shape.isListOrMap)) { - writer.write("@#T", RuntimeTypes.Javax.Nullable) - } - writer.write( - "private #L#T #L;", - if (final) "final " else "", - memberSymbol, memberName - ) - writer.write("") - } - } - - private fun renderConstructor() { - writer.block("private #T(Builder builder)", symbol) { - forEachMember { _, memberName, memberSymbol -> - var builderField = format("builder.#L", memberName) - - if (memberSymbol.shape.isListOrMap) { - builderField = if (memberSymbol.isNullable) { - format("#T.unmodifiable(#L)", RuntimeTypes.Util.ApiTypeHelper, builderField) - } else { - format( - "#T.unmodifiableRequired(#L, this, #L)", - RuntimeTypes.Util.ApiTypeHelper, builderField, memberName.dq() - ) - } - } else if (!memberSymbol.isNullable) { - builderField = format( - "#T.requireNonNull(#L, this, #L)", - RuntimeTypes.Util.ApiTypeHelper, builderField, memberName.dq() - ) - } - - write("this.#L = #L;", memberName, builderField) - } - } - writer.write("") - - writer.block( - "public static #1T of(#2T> fn)", - symbol, RuntimeTypes.JavaUtil.Function, RuntimeTypes.Util.ObjectBuilder - ) { - write("return fn.apply(new Builder()).build();") - } - writer.write("") - } - - private fun renderGetters() { - forEachMember { _, memberName, memberSymbol -> - if (memberSymbol.isNullable && !memberSymbol.shape.isListOrMap) { - writer.write("@#T", RuntimeTypes.Javax.Nullable) - } - - writer.block("public final #T #L()", memberSymbol, memberName) { - write("return this.#L;", memberName) - } - writer.write("") - } - } - - private fun renderSerialize() { - writer.block( - "public void serialize(#T generator, #T mapper)", - RuntimeTypes.Jakarta.JsonGenerator, RuntimeTypes.Json.JsonpMapper - ) { - write( - """ - generator.writeStartObject(); - serializeInternal(generator, mapper); - generator.writeEnd(); - """.trimIndent() - ) - } - writer.write("") - - writer.block( - "protected void serializeInternal(#T generator, #T mapper)", - RuntimeTypes.Jakarta.JsonGenerator, RuntimeTypes.Json.JsonpMapper - ) { - forEachMember(getDocumentFields(), ::renderSerializeField) - } - writer.write("") - } - - private fun getDocumentFields(): List = - if (!isInput) { - sortedMembers - } else { - ctx.httpBindingIndex.getRequestBindings( - shape.expectTrait().operation, - HttpBinding.Location.DOCUMENT - ) - .map { it.member } - .sortedBy { it.memberName } - } - - private fun renderSerializeField(memberShape: MemberShape, memberName: String, memberSymbol: Symbol) { - writer.ifValueDefined("this.${memberName}", memberSymbol) { - write("generator.writeKey(${memberShape.memberName.dq()});") - renderSerializeValue("this.${memberName}", memberSymbol.shape) - } - } - - private fun renderSerializeValue(value: String, valueShape: Shape) { - when (valueShape) { - is StructureShape -> writer.write("${value}.serialize(generator, mapper);") - is ListShape -> { - val elementSymbol = symbolProvider.toSymbol(valueShape.elementShape).toBuilder().boxed().build() - writer.write("generator.writeStartArray();") - writer.block("for (#T item : #L)", elementSymbol, value) { - renderSerializeValue("item", valueShape.elementShape) - } - writer.write("generator.writeEnd();") - } - - is MapShape -> { - val elementSymbol = symbolProvider.toSymbol(valueShape.elementShape).toBuilder().boxed().build() - writer.write("generator.writeStartObject();") - writer.block( - "for (#T.Entry item : #L.entrySet())", - RuntimeTypes.JavaUtil.Map, - elementSymbol, - value - ) { - write("generator.writeKey(item.getKey());") - renderSerializeValue("item.getValue()", valueShape.elementShape) - } - writer.write("generator.writeEnd();") - } - - else -> writer.write("generator.write(${value});") - } - } - - private fun JavaWriter.ifValueDefined(value: String, valueSymbol: Symbol, block: JavaWriter.() -> Unit) { - if (!valueSymbol.isNullable) { - this.block() - return - } - - val check = if (valueSymbol.shape.isListOrMap) { - format("#T.isDefined($value)", RuntimeTypes.Util.ApiTypeHelper) - } else { - "$value != null" - } - - this.block("if (${check})", block = block) - } - - private fun renderBuilder() { - writer.block( - "public static class Builder extends #T implements #T<#T>", - RuntimeTypes.Util.ObjectBuilderBase, RuntimeTypes.Util.ObjectBuilder, symbol - ) { - renderFields(false) - - forEachMember { memberShape, memberName, memberSymbol -> - when (val valueShape = memberShape.targetShape) { - is ListShape -> { - block("public final Builder #L(#T list)", memberName, memberSymbol) { - write( - """ - this.#1L = _listAddAll(this.#1L, list); - return this; - """.trimIndent(), memberName - ) - } - write("") - - val elementSymbol = symbolProvider.toSymbol(valueShape.elementShape) - block("public final Builder #1L(#2T value, #2T... values)", memberName, elementSymbol) { - write( - """ - this.#1L = _listAdd(this.#1L, value, values); - return this; - """.trimIndent(), memberName - ) - } - } - - is MapShape -> { - block("public final Builder #L(#T map)", memberName, memberSymbol) { - write( - """ - this.#1L = _mapPutAll(this.#1L, map); - return this; - """.trimIndent(), memberName - ) - } - write("") - - val elementSymbol = symbolProvider.toSymbol(valueShape.elementShape) - block("public final Builder #L(String key, #T value)", memberName, elementSymbol) { - write( - """ - this.#1L = _mapPut(this.#1L, key, value); - return this; - """.trimIndent(), memberName - ) - } - } - - else -> { - block("public final Builder #L(#T value)", memberName, memberSymbol) { - write( - """ - this.#L = value; - return this; - """.trimIndent(), memberName - ) - } - } - } - - write("") - } - - block("public #T build()", symbol) { - write( - """ - _checkSingleUse(); - return new #T(this); - """.trimIndent(), symbol - ) - } - } - writer.write("") - } - - private fun renderDeserialize() { - val setupDeserializer = writer.format("setup#LDeserializer", symbol.name) - - writer.write( - "public static final #1T<#2T> _DESERIALIZER = #3T.lazy(Builder::new, #2T::#4L);", - RuntimeTypes.Json.JsonpDeserializer, - symbol, - RuntimeTypes.Json.ObjectBuilderDeserializer, - setupDeserializer - ) - writer.write("") - - writer.block( - "protected static void #L(#T<#T.Builder> op)", - setupDeserializer, RuntimeTypes.Json.ObjectDeserializer, symbol - ) { - forEachMember(getDocumentFields()) { memberShape, memberName, memberSymbol -> - write("op.add(Builder::${memberName}, ${valueDeserializer(memberSymbol.shape)}, ${memberShape.memberName.dq()});") - } - } - writer.write("") - } - - private fun valueDeserializer(valueShape: Shape): String = - when (valueShape) { - is StructureShape -> writer.format("#T._DESERIALIZER", symbolProvider.toSymbol(valueShape)) - is ListShape -> { - writer.format( - "#T.arrayDeserializer(#L)", - RuntimeTypes.Json.JsonpDeserializer, - valueDeserializer(valueShape.elementShape) - ) - } - - is MapShape -> { - writer.format( - "#T.stringMapDeserializer(#L)", - RuntimeTypes.Json.JsonpDeserializer, - valueDeserializer(valueShape.elementShape) - ) - } - - is BooleanShape -> writer.format("#T.booleanDeserializer()", RuntimeTypes.Json.JsonpDeserializer) - is StringShape -> writer.format("#T.stringDeserializer()", RuntimeTypes.Json.JsonpDeserializer) - is IntegerShape -> writer.format("#T.integerDeserializer()", RuntimeTypes.Json.JsonpDeserializer) - else -> TODO("Output correct deserializer for $valueShape") - } - - private val MemberShape.targetShape: Shape - get() = model.expectShape(target) - - private val ListShape.elementShape: Shape - get() = member.targetShape - - private val MapShape.elementShape: Shape - get() = value.targetShape - - private fun renderEndpoint() { - val inputTrait = shape.expectTrait() - val operation = model.expectShape(inputTrait.operation) - val httpTrait = operation.expectTrait() - val outputShape = model.expectShape(operation.outputShape) - val outputSymbol = symbolProvider.toSymbol(outputShape) - - writer.openBlock( - "public static final #T<#T, #T, #T> _ENDPOINT = new #T<>(", - RuntimeTypes.Transport.Endpoint, - symbol, - outputSymbol, - RuntimeTypes.OpenSearch.Types.ErrorResponse, - RuntimeTypes.Transport.Endpoints.SimpleEndpoint - ) - - writer.write( - """ - // Request method - request -> ${httpTrait.method.dq()}, - - """.trimIndent() - ) - - writer.write("// Request path") - writer.openBlock("request -> {") - writer.write("StringBuilder buf = new StringBuilder();") - val labelMembers = ctx.httpBindingIndex.getRequestBindings(operation, HttpBinding.Location.LABEL) - .map { it.member } - .associateBy { it.memberName } - httpTrait.uri.segments.forEach { segment -> - writer.write("buf.append('/');") - if (segment.isLabel || segment.isGreedyLabel) { - writer.write( - "#T.pathEncode(request.#L, buf);", - RuntimeTypes.Transport.Endpoints.SimpleEndpoint, - symbolProvider.toMemberName(labelMembers[segment.content]) - ) - } else { - writer.write("buf.append(${segment.content.dq()});") - } - } - writer.write("return buf.toString();") - writer.closeBlock("},\n") - - writer.write("// Request parameters") - writer.openBlock("request -> {") - writer.write( - "#T params = new #T<>();", - RuntimeTypes.JavaUtil.Map, - RuntimeTypes.JavaUtil.HashMap - ) - val paramMembers = ctx.httpBindingIndex.getRequestBindings(operation, HttpBinding.Location.QUERY) - .map { it.member } - .sortedBy { it.memberName } - forEachMember(paramMembers, ::renderQueryParam) - writer.write("return params;") - writer.closeBlock("},\n") - - // TODO: headers? - writer.write( - """ - // Request headers - #T.emptyMap(), - """.trimIndent(), RuntimeTypes.Transport.Endpoints.SimpleEndpoint - ) - - // TODO: maybe no requestbody? - writer.write( - """ - // Has request body - true, - """.trimIndent() - ) - - writer.write( - """ - // Response deserializer - #T._DESERIALIZER - """.trimIndent(), outputSymbol - ) - - writer.closeBlock(");") - } - - private fun renderQueryParam(memberShape: MemberShape, memberName: String, memberSymbol: Symbol) { - val queryTrait = memberShape.expectTrait() - - val value = "request.${memberName}" - writer.ifValueDefined(value, memberSymbol) { - write("params.put(${queryTrait.value.dq()}, ${queryParamValueOf(value, memberSymbol.shape)});") - } - } - - private fun queryParamValueOf(value: String, valueShape: Shape): String = - when (valueShape) { - is StringShape -> value - is BooleanShape -> "String.valueOf($value)" - else -> TODO("Output query param value getter for $valueShape") - } -} \ No newline at end of file diff --git a/java-codegen/src/main/kotlin/org/opensearch/client/codegen/utils/Strings.kt b/java-codegen/src/main/kotlin/org/opensearch/client/codegen/utils/Strings.kt deleted file mode 100644 index 7d0a4c0b5e..0000000000 --- a/java-codegen/src/main/kotlin/org/opensearch/client/codegen/utils/Strings.kt +++ /dev/null @@ -1,12 +0,0 @@ -package org.opensearch.client.codegen.utils - -import software.amazon.smithy.utils.CaseUtils -import software.amazon.smithy.utils.StringUtils - -fun String.dq(): String = StringUtils.escapeJavaString(this, "") - -fun String.toSnakeCase(): String = CaseUtils.toSnakeCase(this) - -fun String.toPascalCase(): String = CaseUtils.toPascalCase(toSnakeCase()) - -fun String.toCamelCase(): String = CaseUtils.toCamelCase(toSnakeCase())