Skip to content

Commit

Permalink
Kotlin Coroutines Support (#29)
Browse files Browse the repository at this point in the history
Kotlin Coroutines Support
  • Loading branch information
cortinico committed Jan 6, 2020
2 parents e9b4a14 + 3789d33 commit 9d9edce
Show file tree
Hide file tree
Showing 13 changed files with 283 additions and 9 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ Here the list of the supported platforms:
| Platform | Description |
| -------- | ------------------------------------------ |
| `kotlin` | Generates Kotlin code and Retrofit interfaces, with RxJava2 for async calls, Moshi for serialization and ThreeTenABP for Data management |
| `kotlin-coroutines` | Generates Kotlin code and Retrofit interfaces, with [Kotlin Coroutines](https://kotlinlang.org/docs/reference/coroutines-overview.html) for async calls, Moshi for serialization and ThreeTenABP for Data management |

We're looking forward to more platforms to support in the future. Contributions are more than welcome.

Expand All @@ -81,6 +82,8 @@ You can find some **examples** in this repository to help you set up your genera

* [samples/kotlin-android](/samples/kotlin-android) Contains an example of an Android Library configured with a `build.gradle.kts` file, using Kotlin as scripting language.

* [samples/kotlin-coroutines](/samples/kotlin-coroutines) Contains an example of an Android Library configured to output Kotlin Coroutines capable code.

* [samples/junit-tests](/samples/junit-tests) This sample contains specs used to test edge cases and scenarios that have been reported in the issue tracker or that are worth testing.

## How the generated code will look like
Expand Down
35 changes: 35 additions & 0 deletions plugin/src/main/java/com/yelp/codegen/KotlinCoroutineGenerator.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.yelp.codegen

import io.swagger.codegen.CodegenOperation
import io.swagger.models.Model
import io.swagger.models.Operation
import io.swagger.models.Swagger

open class KotlinCoroutineGenerator : KotlinGenerator() {

init {
templateDir = "kotlin"
}

override fun getName() = "kotlin-coroutines"

override fun wrapResponseType(imports: MutableSet<String>, responsePrimitiveType: String) = responsePrimitiveType

override fun getNoResponseType(imports: MutableSet<String>) = "Unit"

/**
* Overriding the behavior of [KotlinGenerator] to make sure operations are rendered as `suspend fun` rather
* than just plain `fun`.
*/
override fun fromOperation(
path: String?,
httpMethod: String?,
operation: Operation?,
definitions: MutableMap<String, Model>?,
swagger: Swagger?
): CodegenOperation {
val codegenOperation = super.fromOperation(path, httpMethod, operation, definitions, swagger)
codegenOperation.vendorExtensions[X_FUNCTION_QUALIFIERS] = "suspend"
return codegenOperation
}
}
31 changes: 23 additions & 8 deletions plugin/src/main/java/com/yelp/codegen/KotlinGenerator.kt
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import io.swagger.models.Swagger
import io.swagger.models.properties.Property
import java.io.File

class KotlinGenerator : SharedCodegen() {
open class KotlinGenerator : SharedCodegen() {

companion object {
/**
Expand Down Expand Up @@ -406,19 +406,16 @@ class KotlinGenerator : SharedCodegen() {
}
}

when {
codegenOperation.returnType = when {
codegenOperation.isResponseFile -> {
codegenOperation.returnType = "Single<ResponseBody>"
codegenOperation.imports.add("okhttp3.ResponseBody")
codegenOperation.imports.add("io.reactivex.Single")
wrapResponseType(codegenOperation.imports, "ResponseBody")
}
codegenOperation.returnType == null -> {
codegenOperation.returnType = "Completable"
codegenOperation.imports.add("io.reactivex.Completable")
getNoResponseType(codegenOperation.imports)
}
else -> {
codegenOperation.returnType = "Single<${codegenOperation.returnType}>"
codegenOperation.imports.add("io.reactivex.Single")
wrapResponseType(codegenOperation.imports, codegenOperation.returnType)
}
}

Expand Down Expand Up @@ -509,4 +506,22 @@ class KotlinGenerator : SharedCodegen() {
operation.vendorExtensions[HAS_OPERATION_HEADERS] = topLevelHeaders.isNotEmpty()
operation.vendorExtensions[OPERATION_HEADERS] = topLevelHeaders
}

/**
* Wraps the return type of an operation with the proper type (e.g. Single, Observable, Future, etc.)
* Use this method to eventually add imports if needed in the [imports] param.
*/
protected open fun wrapResponseType(imports: MutableSet<String>, responsePrimitiveType: String): String {
imports.add("io.reactivex.Single")
return "Single<$responsePrimitiveType>"
}

/**
* Get the return type of operations with no ResponseType set (void, Unit, Completables).
* Use this method to eventually add imports if needed in the [imports] param.
*/
protected open fun getNoResponseType(imports: MutableSet<String>): String {
imports.add("io.reactivex.Completable")
return "Completable"
}
}
1 change: 1 addition & 0 deletions plugin/src/main/java/com/yelp/codegen/Main.kt
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ fun main(args: Array<String>) {
configurator.outputDir = parsed['o']

configurator.addAdditionalProperty(LANGUAGE, parsed['p'])

configurator.addAdditionalProperty(SPEC_VERSION, specVersion)
configurator.addAdditionalProperty(SERVICE_NAME, parsed['s'])
configurator.addAdditionalProperty(GROUP_ID, parsed['g'])
Expand Down
1 change: 1 addition & 0 deletions plugin/src/main/java/com/yelp/codegen/SharedCodegen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ const val HEADERS_TO_IGNORE = "headers_to_ignore"
internal const val X_NULLABLE = "x-nullable"
internal const val X_MODEL = "x-model"
internal const val X_OPERATION_ID = "x-operation-id"
internal const val X_FUNCTION_QUALIFIERS = "x-function-qualifiers"
internal const val X_UNSAFE_OPERATION = "x-unsafe-operation"

// Headers Names
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
com.yelp.codegen.KotlinGenerator
com.yelp.codegen.KotlinCoroutineGenerator
2 changes: 1 addition & 1 deletion plugin/src/main/resources/kotlin/retrofit2/api.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ interface {{classname}} {
{{/vendorExtensions.x-unsafe-operation}}{{^vendorExtensions.x-unsafe-operation}}{{#isDeprecated}}
@Deprecated(message = "Deprecated"){{/isDeprecated}}
{{/vendorExtensions.x-unsafe-operation}}
fun {{operationId}}({{^allParams}}){{/allParams}}
{{vendorExtensions.x-function-qualifiers}} fun {{operationId}}({{^allParams}}){{/allParams}}
{{#allParams}}{{>retrofit2/queryParams}}{{>retrofit2/pathParams}}{{>retrofit2/headerParams}}{{>retrofit2/bodyParams}}{{>retrofit2/formParams}}{{#hasMore}},
{{/hasMore}}{{^hasMore}}
){{/hasMore}}{{/allParams}}: {{{returnType}}}
Expand Down
71 changes: 71 additions & 0 deletions samples/kotlin-coroutines/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
buildscript {
repositories {
mavenLocal()
gradlePluginPortal()
google()
mavenCentral()
jcenter()
}

dependencies {
classpath "com.android.tools.build:gradle:3.5.0"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.50"
classpath "com.yelp.codegen:plugin:1.2.0"
classpath "io.gitlab.arturbosch.detekt:detekt-gradle-plugin:1.0.1"
}
}

apply plugin: "com.android.library"
apply plugin: "kotlin-android"
apply plugin: "com.yelp.codegen.plugin"
apply plugin: "io.gitlab.arturbosch.detekt"

android {
compileSdkVersion = 28
defaultConfig {
minSdkVersion 21
targetSdkVersion 28
versionCode = 1
versionName = "1.0"
}
}

dependencies {
// Kotlin
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.3.41"
implementation "org.jetbrains.kotlin:kotlin-reflect:1.3.41"

// Moshi + OkHttp + Retrofit
implementation "com.squareup.moshi:moshi:1.8.0"
implementation "com.squareup.moshi:moshi-adapters:1.8.0"
implementation "com.squareup.moshi:moshi-kotlin:1.8.0"
implementation "com.squareup.okhttp3:okhttp:3.12.3"
implementation "com.squareup.retrofit2:retrofit:2.6.1"
implementation "com.squareup.retrofit2:converter-moshi:2.6.1"

// Date Support
implementation "com.jakewharton.threetenabp:threetenabp:1.2.1"

// Testing Dependencies
testImplementation "junit:junit:4.12"
testImplementation "com.squareup.okhttp3:mockwebserver:3.12.3"
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.3.3"
}

generateSwagger {
platform = "kotlin-coroutines"
packageName = "com.yelp.codegen.samples.kotlincoroutines"
specName = "kotlincoroutines"
inputFile = file("../sample_specs.json")
outputDir = file("./src/main/java/")
}

repositories {
mavenCentral()
}

detekt {
toolVersion = "1.0.0-RC16"
input = files("src/test")
filters = ".*/resources/.*,.*/build/.*"
}
2 changes: 2 additions & 0 deletions samples/kotlin-coroutines/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.yelp.samplelibrary"/>
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package com.yelp.codegen.samples.kotlincoroutines

import com.yelp.codegen.samples.kotlincoroutines.apis.ResourceApi
import com.yelp.codegen.samples.kotlincoroutines.models.PropertyModel
import com.yelp.codegen.samples.kotlincoroutines.tools.CoroutineDispatcherRule
import com.yelp.codegen.samples.kotlincoroutines.tools.MockServerApiRule
import kotlinx.coroutines.ExperimentalCoroutinesApi
import okhttp3.mockwebserver.MockResponse
import org.junit.Assert.*
import org.junit.Rule
import org.junit.Test

@ExperimentalCoroutinesApi
class CoroutinesEndpointsTest {

@get:Rule
val mockServerRule = MockServerApiRule()

@get:Rule
val coroutinesRule = CoroutineDispatcherRule()

@Test
fun emptyEndpointTest() {
mockServerRule.server.enqueue(MockResponse().setBody("{}"))

coroutinesRule.runBlockingTest {
val returned = mockServerRule.getApi<ResourceApi>().getEmptyEndpoint()
assertNotNull(returned)
}
}

@Test
fun propertyEndpointTest_withStringProperty() {
mockServerRule.server.enqueue(MockResponse().setBody("""
{
"string_property": "string"
}
""".trimIndent()))

coroutinesRule.runBlockingTest {
val returned = mockServerRule.getApi<ResourceApi>().getPropertyEndpoint("string")
assertEquals("string", returned.stringProperty)
assertNull(returned.enumProperty)
}
}

@Test
fun propertyEndpointTest_withEnumProperty() {
mockServerRule.server.enqueue(MockResponse().setBody("""
{
"enum_property": "VALUE1"
}
""".trimIndent()))

coroutinesRule.runBlockingTest {
val returned = mockServerRule.getApi<ResourceApi>().getPropertyEndpoint("string")
assertEquals(PropertyModel.EnumPropertyEnum.VALUE1, returned.enumProperty)
assertNull(returned.stringProperty)
}
}

@Test
fun propertyEndpointTest_withEmptyObject() {
mockServerRule.server.enqueue(MockResponse().setBody("{}"))

coroutinesRule.runBlockingTest {
val returned = mockServerRule.getApi<ResourceApi>().getPropertyEndpoint("string")
assertNull(returned.stringProperty)
assertNull(returned.enumProperty)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.yelp.codegen.samples.kotlincoroutines.tools

import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.TestCoroutineDispatcher
import kotlinx.coroutines.test.TestCoroutineScope
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.setMain
import okhttp3.mockwebserver.MockWebServer
import org.junit.rules.ExternalResource

/**
* JUnit rule to create a Retrofit instance able to interact with the [MockWebServer] and to create Retrofit instances
* on the fly as needed.
*/
@ExperimentalCoroutinesApi
class CoroutineDispatcherRule : ExternalResource() {

private val testCoroutineDispatcher = TestCoroutineDispatcher()
private val testCoroutineScope = TestCoroutineScope(testCoroutineDispatcher)

override fun before() {
super.before()
Dispatchers.setMain(testCoroutineDispatcher)
}

override fun after() {
Dispatchers.resetMain()
testCoroutineScope.cleanupTestCoroutines()
super.after()
}

fun runBlockingTest(block: suspend CoroutineScope.() -> Unit) =
runBlocking(Dispatchers.Main, block)

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.yelp.codegen.samples.kotlincoroutines.tools

import okhttp3.mockwebserver.MockWebServer
import org.junit.rules.ExternalResource
import retrofit2.Retrofit

/**
* JUnit rule to create a Retrofit instance able to interact with the [MockWebServer] and to create Retrofit instances
* on the fly as needed.
*/
class MockServerApiRule : ExternalResource() {

lateinit var retrofit: Retrofit
val server = MockWebServer()

override fun before() {
super.before()
server.start()

retrofit = Retrofit.Builder()
.addConverterFactory(GeneratedCodeConverters.converterFactory())
.baseUrl(server.url("/"))
.build()
}

override fun after() {
server.shutdown()
super.after()
}

inline fun <reified T> getApi(): T {
return retrofit.create(T::class.java)
}
}
1 change: 1 addition & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ include(":plugin")
if (pluginIsInstalled()) {
include(":samples:junit-tests",
":samples:kotlin-android",
":samples:kotlin-coroutines",
":samples:groovy-android",
":samples:generated-code")
}
Expand Down

0 comments on commit 9d9edce

Please sign in to comment.