Skip to content

Commit 15bca19

Browse files
authored
feat: Kotlin MP gradle file generation (#577)
1 parent 304cc82 commit 15bca19

File tree

22 files changed

+729
-285
lines changed

22 files changed

+729
-285
lines changed

runtime/protocol/http-client-engines/http-client-engine-ktor/build.gradle.kts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,13 @@ val ktorVersion: String by project
1111

1212
kotlin {
1313
sourceSets {
14-
jvmMain {
14+
commonMain {
1515
dependencies {
1616
api(project(":runtime:protocol:http"))
17+
}
18+
}
19+
jvmMain {
20+
dependencies {
1721
implementation(project(":runtime:io"))
1822
// okhttp works on both JVM and Android
1923
implementation("io.ktor:ktor-client-okhttp:$ktorVersion")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0.
4+
*/
5+
package aws.smithy.kotlin.runtime.http.engine.ktor
6+
7+
import aws.smithy.kotlin.runtime.http.engine.HttpClientEngine
8+
import aws.smithy.kotlin.runtime.http.engine.HttpClientEngineBase
9+
import aws.smithy.kotlin.runtime.http.engine.HttpClientEngineConfig
10+
11+
/**
12+
* Specifies the ktor http client of which platform specific [HttpClientEngine]'s actualize
13+
*
14+
* @param config Provides configuration for the DefaultHttpClientEngine
15+
*/
16+
expect class KtorEngine(config: HttpClientEngineConfig = HttpClientEngineConfig.Default) : HttpClientEngineBase {
17+
val config: HttpClientEngineConfig
18+
}

runtime/protocol/http-client-engines/http-client-engine-ktor/jvm/src/aws/smithy/kotlin/runtime/http/engine/ktor/KtorEngine.kt

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,16 @@ import aws.smithy.kotlin.runtime.http.response.HttpResponse as SdkHttpResponse
3232
/**
3333
* JVM [HttpClientEngine] backed by Ktor
3434
*/
35-
class KtorEngine(
36-
private val config: HttpClientEngineConfig = HttpClientEngineConfig.Default
35+
actual class KtorEngine actual constructor(
36+
config: HttpClientEngineConfig
3737
) : HttpClientEngineBase("ktor-okhttp") {
38+
39+
actual val config: HttpClientEngineConfig
40+
41+
init {
42+
this.config = config
43+
}
44+
3845
@OptIn(ExperimentalTime::class)
3946
val client: HttpClient = HttpClient(OkHttp) {
4047
engine {

smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/KotlinSettings.kt

Lines changed: 31 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,12 @@ data class KotlinSettings(
8585
// Load the sdk id from configurations that define it, fall back to service name for those that don't.
8686
val sdkId = config.getStringMemberOrDefault(SDK_ID, serviceId.name)
8787
val build = config.getObjectMember(BUILD_SETTINGS)
88-
return KotlinSettings(serviceId, PackageSettings(packageName, version, desc), sdkId, BuildSettings.fromNode(build))
88+
return KotlinSettings(
89+
serviceId,
90+
PackageSettings(packageName, version, desc),
91+
sdkId,
92+
BuildSettings.fromNode(build)
93+
)
8994
}
9095

9196
// infer the service to generate from a model
@@ -141,42 +146,43 @@ data class KotlinSettings(
141146
}
142147
}
143148

149+
/**
150+
* Contains Gradle build settings for a Kotlin project
151+
* @param generateFullProject Flag indicating to generate a full project that will exist independent of other projects
152+
* @param generateDefaultBuildFiles Flag indicating if (Gradle) build files should be spit out. This can be used to
153+
* turn off generated gradle files by default in-favor of e.g. spitting out your own custom Gradle file as part of an
154+
* integration.
155+
* @param optInAnnotations Kotlin opt-in annotations. See:
156+
* https://kotlinlang.org/docs/reference/opt-in-requirements.html
157+
* @param generateMultiplatformProject Flag indicating to generate a Kotlin multiplatform or JVM project
158+
*/
144159
data class BuildSettings(
145-
/**
146-
* Flag indicating to generate a full project that will exist independent of other projects
147-
*/
148160
val generateFullProject: Boolean = false,
149-
150-
/**
151-
* Flag indicating if (Gradle) build files should be spit out. This can be used to turn off generated gradle
152-
* files by default in-favor of e.g. spitting out your own custom Gradle file as part of an integration.
153-
*/
154161
val generateDefaultBuildFiles: Boolean = true,
155-
156-
/**
157-
* Kotlin opt-in annotations
158-
* See: https://kotlinlang.org/docs/reference/opt-in-requirements.html
159-
*/
160-
val optInAnnotations: List<String>? = null
162+
val optInAnnotations: List<String>? = null,
163+
val generateMultiplatformProject: Boolean = false,
161164
) {
162165
companion object {
163-
private const val ROOT_PROJECT = "rootProject"
164-
private const val GENERATE_DEFAULT_BUILD_FILES = "generateDefaultBuildFiles"
165-
private const val ANNOTATIONS = "optInAnnotations"
166+
const val ROOT_PROJECT = "rootProject"
167+
const val GENERATE_DEFAULT_BUILD_FILES = "generateDefaultBuildFiles"
168+
const val ANNOTATIONS = "optInAnnotations"
169+
const val GENERATE_MULTIPLATFORM_MODULE = "multiplatform"
166170

167-
fun fromNode(node: Optional<ObjectNode>): BuildSettings = if (node.isPresent) {
171+
fun fromNode(node: Optional<ObjectNode>): BuildSettings = node.map {
168172
val generateFullProject = node.get().getBooleanMemberOrDefault(ROOT_PROJECT, false)
169173
val generateBuildFiles = node.get().getBooleanMemberOrDefault(GENERATE_DEFAULT_BUILD_FILES, true)
174+
val generateMultiplatformProject =
175+
node.get().getBooleanMemberOrDefault(GENERATE_MULTIPLATFORM_MODULE, false)
170176
val annotations = node.get().getArrayMember(ANNOTATIONS).map {
171-
it.elements.mapNotNull {
172-
it.asStringNode().map { it.value }.orNull()
177+
it.elements.mapNotNull { node ->
178+
node.asStringNode().map { stringNode ->
179+
stringNode.value
180+
}.orNull()
173181
}
174182
}.orNull()
175183

176-
BuildSettings(generateFullProject, generateBuildFiles, annotations)
177-
} else {
178-
Default
179-
}
184+
BuildSettings(generateFullProject, generateBuildFiles, annotations, generateMultiplatformProject)
185+
}.orElse(Default)
180186

181187
/**
182188
* Default build settings
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0.
4+
*/
5+
package software.amazon.smithy.kotlin.codegen.core
6+
7+
import software.amazon.smithy.kotlin.codegen.integration.SectionId
8+
import software.amazon.smithy.utils.CodeWriter
9+
import java.util.function.BiFunction
10+
11+
/**
12+
* Extension function that is more idiomatic Kotlin that is roughly the same purpose as
13+
* the provided function `openBlock(String textBeforeNewline, String textAfterNewline, Runnable r)`
14+
*
15+
* Example:
16+
* ```
17+
* writer.withBlock("{", "}") {
18+
* write("foo")
19+
* }
20+
* ```
21+
*
22+
* Equivalent to:
23+
* ```
24+
* writer.openBlock("{")
25+
* writer.write("foo")
26+
* writer.closeBlock("}")
27+
* ```
28+
*/
29+
fun <T : CodeWriter> T.withBlock(
30+
textBeforeNewLine: String,
31+
textAfterNewLine: String,
32+
vararg args: Any,
33+
block: T.() -> Unit
34+
): T = wrapBlockIf(true, textBeforeNewLine, textAfterNewLine, *args) { block(this) }
35+
36+
/**
37+
* Extension function that is more idiomatic Kotlin that is roughly the same purpose as an if block wrapped around
38+
* the provided function `openBlock(String textBeforeNewline, String textAfterNewline, Runnable r)`
39+
*
40+
* Example:
41+
* ```
42+
* writer.wrapBlockIf(foo == bar, "{", "}") {
43+
* write("foo")
44+
* }
45+
* ```
46+
*
47+
* Equivalent to:
48+
* ```
49+
* if (foo == bar) writer.openBlock("{")
50+
* writer.write("foo")
51+
* if (foo == bar) writer.closeBlock("}")
52+
* ```
53+
*/
54+
fun <T : CodeWriter> T.wrapBlockIf(
55+
condition: Boolean,
56+
textBeforeNewLine: String,
57+
textAfterNewLine: String,
58+
vararg args: Any,
59+
block: T.() -> Unit,
60+
): T {
61+
if (condition) openBlock(textBeforeNewLine, *args)
62+
block(this)
63+
if (condition) closeBlock(textAfterNewLine)
64+
return this
65+
}
66+
67+
/**
68+
* Extension function that closes the previous block, dedents, opens a new block with [textBeforeNewLine], and indents.
69+
*
70+
* This is useful for chaining if-if-else-else branches.
71+
*
72+
* Example:
73+
* ```
74+
* writer.openBlock("if (foo) {")
75+
* .write("foo()")
76+
* .closeAndOpenBlock("} else {")
77+
* .write("bar()")
78+
* .closeBlock("}")
79+
* ```
80+
*/
81+
fun <T : CodeWriter> T.closeAndOpenBlock(
82+
textBeforeNewLine: String,
83+
vararg args: Any,
84+
): T = apply {
85+
dedent()
86+
openBlock(textBeforeNewLine, *args)
87+
}
88+
89+
/**
90+
* Declares a section for extension in codegen. The [SectionId] should be specified as a child
91+
* of the type housing the codegen associated with the section. This keeps [SectionId]s closely
92+
* associated with their targets.
93+
*/
94+
fun <T : CodeWriter> T.declareSection(id: SectionId, context: Map<String, Any?> = emptyMap(), block: T.() -> Unit = {}): T {
95+
putContext(context)
96+
pushState(id.javaClass.canonicalName)
97+
block(this)
98+
popState()
99+
removeContext(context)
100+
return this
101+
}
102+
103+
private fun <T : CodeWriter> T.removeContext(context: Map<String, Any?>): Unit =
104+
context.keys.forEach { key -> removeContext(key) }
105+
106+
/**
107+
* Convenience function to get a typed value out of the context or throw if the key doesn't exist
108+
* or the type is wrong
109+
*/
110+
inline fun <reified T> CodeWriter.getContextValue(key: String): T = checkNotNull(getContext(key) as? T) {
111+
"Expected `$key` in CodeWriter context"
112+
}
113+
114+
// Specifies a function that receives a [CodeWriter]
115+
typealias InlineCodeWriter = CodeWriter.() -> Unit
116+
/**
117+
* Formatter to enable passing a writing function
118+
* @param codeWriterCreator function that creates a new [CodeWriter] instance used to generate output of inline content
119+
*/
120+
class InlineCodeWriterFormatter(
121+
private val codeWriterCreator: () -> CodeWriter = { CodeWriter() }
122+
) : BiFunction<Any, String, String> {
123+
@Suppress("UNCHECKED_CAST")
124+
override fun apply(t: Any, u: String): String {
125+
val func = t as? InlineCodeWriter ?: error("Invalid parameter type of ${t::class}")
126+
val innerWriter = codeWriterCreator()
127+
func(innerWriter)
128+
return innerWriter.toString().trimEnd()
129+
}
130+
}
131+
132+
/**
133+
* Optionally call the [Runnable] if [test] is true, otherwise do nothing and return the instance without
134+
* running the block
135+
*/
136+
fun CodeWriter.callIf(test: Boolean, runnable: Runnable): CodeWriter {
137+
if (test) {
138+
runnable.run()
139+
}
140+
return this
141+
}

smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/core/KotlinDependency.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,12 @@ enum class GradleConfiguration {
5454
TestRuntimeOnly;
5555

5656
override fun toString(): String = StringUtils.uncapitalize(this.name)
57+
58+
/**
59+
* Return true if the dependency is in the test scope
60+
*/
61+
val isTestScope
62+
get() = this == TestImplementation || this == TestCompileOnly || this == TestRuntimeOnly
5763
}
5864

5965
data class KotlinDependency(

0 commit comments

Comments
 (0)