Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Kotlin Coroutines Support #29

Merged
merged 2 commits into from
Jan 6, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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_QUALIFIER] = "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_QUALIFIER = "x-function-qualifier"
cortinico marked this conversation as resolved.
Show resolved Hide resolved
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-qualifier}} 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