Skip to content

Instantly share code, notes, and snippets.

@TWiStErRob
Last active June 12, 2023 00:21
Show Gist options
  • Star 8 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save TWiStErRob/507a44b2a746b1cd9cab8c2169658f59 to your computer and use it in GitHub Desktop.
Save TWiStErRob/507a44b2a746b1cd9cab8c2169658f59 to your computer and use it in GitHub Desktop.
Kotlin Script (kts) 1.3.21 with dependencies to call a JSON web endpoint, including tests

Notes:

  • The file name has to end in .main.kts and kotlin-main-kts.jar has to be on classpath. See https://youtrack.jetbrains.com/issue/KT-27853
  • Transitive dependencies have to be explicitly listed, and version conflicts manually resolved by hard-coding the right versions.

kotlinc: https://kotlinlang.org/docs/tutorials/command-line.html

To edit this in IntelliJ IDEA:

  • create and idea folder next to the .kts file
  • save build.gradle to that folder
  • import the project from idea/build.gradle. Note: @DependsOn dependencies have to be duplicated in build.gradle

Debugging: https://youtrack.jetbrains.com/issue/KT-30211

java -agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5005 -noverify -cp "%KOTLIN_HOME%\lib\kotlin-compiler.jar" org.jetbrains.kotlin.cli.jvm.K2JVMCompiler -cp "%KOTLIN_HOME%/lib/kotlin-main-kts.jar" -script github.main.kts twisterrob

Note: default encoding is not UTF-8 on Windows, so adding java -Dfile.encoding=UTF-8 fixes the output issue of ó.

TODO:

  • rx -> coroutines (may need to use Retrofit 2.6-SNAPSHOT)
plugins {
id "org.jetbrains.kotlin.jvm" version "1.3.21"
}
repositories {
jcenter()
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib"
implementation "org.jetbrains.kotlin:kotlin-script-runtime"
implementation "org.jetbrains.kotlin:kotlin-main-kts"
//implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.1.1"
implementation "com.squareup.retrofit2:retrofit:2.5.0"
implementation "com.squareup.retrofit2:converter-gson:2.5.0"
implementation "com.squareup.retrofit2:adapter-rxjava:2.5.0"
//implementation "com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:0.9.2"
implementation "junit:junit:4.13-beta-2"
implementation "org.hamcrest:hamcrest-all:1.3"
implementation "com.flextrade.jfixture:jfixture:2.7.2"
implementation "org.mockito:mockito-core:2.24.5"
implementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.1.0"
}
sourceSets.main.java.srcDirs new File(rootDir, "..")
// Try "%KOTLIN_HOME%\bin\kotlinc" -cp "%KOTLIN_HOME%/lib/kotlin-main-kts.jar" -script github.main.kts twisterrob
@file:Suppress("RedundantSuspendModifier")
@file:Repository("https://jcenter.bintray.com")
// prod
// These coroutines don't work because kotlin-main-kts has embedded coroutines
// see https://youtrack.jetbrains.com/issue/KT-30210
//@file:DependsOn("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.1.1")
//@file:DependsOn("org.jetbrains.kotlinx:kotlinx-coroutines-core-common:1.1.1")
@file:DependsOn("com.google.code.gson:gson:2.8.0")
@file:DependsOn("io.reactivex:rxjava:1.3.8")
@file:DependsOn("com.squareup.okhttp3:okhttp:3.12.0")
@file:DependsOn("com.squareup.okio:okio:1.15.0")
@file:DependsOn("com.squareup.retrofit2:retrofit:2.5.0")
@file:DependsOn("com.squareup.retrofit2:converter-gson:2.5.0")
@file:DependsOn("com.squareup.retrofit2:adapter-rxjava:2.5.0")
// TODO https://github.com/square/retrofit/pull/2886 merged 2019-02, will be in Retrofit 2.6
//@file:DependsOn("com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:0.9.2")
// test
@file:DependsOn("junit:junit:4.13-beta-2")
@file:DependsOn("org.hamcrest:hamcrest-all:1.3")
@file:DependsOn("com.flextrade.jfixture:jfixture:2.7.2")
@file:DependsOn("org.mockito:mockito-core:2.24.5")
@file:DependsOn("com.nhaarman.mockitokotlin2:mockito-kotlin:2.1.0")
@file:DependsOn("net.bytebuddy:byte-buddy:1.9.7")
@file:DependsOn("net.bytebuddy:byte-buddy-agent:1.9.7")
@file:DependsOn("org.objenesis:objenesis:2.6")
import com.flextrade.jfixture.FixtureAnnotations
import com.flextrade.jfixture.JFixture
import com.flextrade.jfixture.annotations.Fixture
import com.google.gson.annotations.SerializedName
import com.nhaarman.mockitokotlin2.whenever
import kotlinx.coroutines.runBlocking
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.containsString
import org.hamcrest.Matchers.hasSize
import org.junit.Before
import org.junit.Test
import org.junit.runner.JUnitCore
import org.mockito.Mock
import org.mockito.MockitoAnnotations
import retrofit2.Retrofit
import retrofit2.adapter.rxjava.RxJavaCallAdapterFactory
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.GET
import retrofit2.http.Path
import rx.Single
import rx.schedulers.Schedulers
import kotlin.system.exitProcess
//Class.forName("kotlinx.coroutines.BuildersKt").declaredMethods.forEach(::println)
main(*args)
/**
* @see https://youtrack.jetbrains.com/issue/KT-27853
* @see https://github.com/Kotlin/KEEP/blob/scripting/proposals/scripting-support.md
*/
@Suppress("KDocUnresolvedReference")
fun main(vararg args: String) = runBlocking {
test()
if (args.size != 1) {
help()
exitProcess(1)
}
Script.di(args[0])
}
class Script(
private val github: GitHub,
private val output: (String) -> Unit
) {
companion object {
fun di(userName: String) {
val retrofit = Retrofit.Builder()
.baseUrl("https://api.github.com/")
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
//.addCallAdapterFactory(CoroutineCallAdapterFactory())
.build()
val github = retrofit.create(GitHub::class.java)
Script(github, ::println).start(userName)
}
}
fun start(userName: String) {
val user = github.user(userName)
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.trampoline())
.toBlocking() // because background threads are daemon, need to keep alive
.value()
handleUser(user)
}
private fun handleUser(user: GitHub.User) {
output("${user.login}: ${user.name} works at ${user.company}")
}
@Suppress("FunctionName", "unused")
class ScriptTest {
@Mock lateinit var mockGithub: GitHub
@Fixture lateinit var fixtUser: GitHub.User
private lateinit var fixture: JFixture
private lateinit var sut: Script
@Before fun setUp() {
fixture = JFixture()
FixtureAnnotations.initFixtures(this, fixture)
MockitoAnnotations.initMocks(this)
sut = Script(mockGithub, ::capture)
}
@Test fun `github user gets printed`() = runBlocking {
val fixtUserName = fixture.create(String::class.java)
whenever(mockGithub.user(fixtUserName)).thenReturn(Single.just(fixtUser))
//whenever(mockGithub.userCo(fixtUserName)).thenReturn(async { fixtUser })
sut.start(fixtUserName)
assertThat(captured, hasSize(1))
val output = captured[0]
assertThat(output, containsString(fixtUser.login))
assertThat(output, containsString(fixtUser.name))
assertThat(output, containsString(fixtUser.company))
}
private val captured = mutableListOf<String>()
private fun capture(output: String) {
captured.add(output)
}
}
}
interface GitHub {
@GET("users/{user}")
fun user(@Path("user") userName: String): Single<User>
data class User(
@SerializedName("login")
val login: String,
@SerializedName("id")
val id: String,
@SerializedName("node_id")
val nodeId: String,
@SerializedName("avatar_url")
val avatarUrl: String,
@SerializedName("gravatar_id")
val gravatarId: String,
@SerializedName("url")
val url: String,
@SerializedName("html_url")
val htmlUrl: String,
@SerializedName("followers_url")
val followersUrl: String,
@SerializedName("following_url")
val followingUrl: String,
@SerializedName("gists_url")
val gistsUrl: String,
@SerializedName("starred_url")
val starredUrl: String,
@SerializedName("subscriptions_url")
val subscriptionsUrl: String,
@SerializedName("organizations_url")
val organizationsUrl: String,
@SerializedName("repos_url")
val reposUrl: String,
@SerializedName("events_url")
val eventsUrl: String,
@SerializedName("received_events_url")
val receivedEventsUrl: String,
@SerializedName("type")
val type: String,
@SerializedName("site_admin")
val siteAdmin: Boolean,
@SerializedName("name")
val name: String,
@SerializedName("company")
val company: String,
@SerializedName("blog")
val blog: String,
@SerializedName("location")
val location: String,
@SerializedName("email")
val email: String,
@SerializedName("hireable")
val hireable: Boolean,
@SerializedName("bio")
val bio: String,
@SerializedName("public_repos")
val publicRepos: Int,
@SerializedName("public_gists")
val publicGists: Int,
@SerializedName("followers")
val followers: Int,
@SerializedName("following")
val following: Int,
@SerializedName("created_at")
val createdAt: String,
@SerializedName("updated_at")
val updatedAt: String
)
}
fun help() {
val dollar = '$'
println(
"""
Usage
* Windows: "%KOTLIN_HOME%\bin\kotlinc" -cp "%KOTLIN_HOME%/lib/kotlin-main-kts.jar" -script github.main.kts <username>
* Unix: "${dollar}{KOTLIN_HOME}/bin/kotlinc" -cp "${dollar}{KOTLIN_HOME}/lib/kotlin-main-kts.jar" -script github.main.kts <username>
""".trimIndent()
)
}
fun test() {
JUnitCore.runClasses(
Script.ScriptTest::class.java
).run {
val stats = "in ${runTime}ms: ran ${runCount} (ignored ${ignoreCount}), failed ${failureCount}"
if (wasSuccessful()) {
println("Self test complete $stats")
} else {
println("Self test failed $stats")
failures.forEach(::println)
}
}
}
@yschimke
Copy link

Drive by review - most of this just isn't correct.

  • You can drop a main.kts script into Intellij and if you have main.kts activated in your Intellij preferences it will let you edit with completion etc
  • It runs/debugs straight out of Intellij with a right click and run from the project view or inside the file
  • It's executable from the command line like a bash script

See https://github.com/yschimke/okurl-scripts/blob/master/commands/rsocketTcpProxy.main.kts

A web example https://github.com/yschimke/okurl-scripts/blob/master/commands/showList.main.kts

A github graphql example https://github.com/yschimke/okurl-scripts/blob/master/commands/github-pull-requests.main.kts

gmail https://github.com/yschimke/okurl-scripts/blob/master/commands/gmail-inbox.main.kts

@yazmnh87
Copy link

Do you know if you have to use runBlocking? Can you run simultaneous requests?

@TWiStErRob
Copy link
Author

@yschimke * at the time of writing. If you look at the dates, I wrote this in 2019, and you reviewed end of 2020, between the two they fixed https://youtrack.jetbrains.com/issue/KT-30211 in 1.4.0, you can clearly see 1.3.21 is used in my build.gradle. Since then I've also created a lot of .main.kts files with better code: https://github.com/search?q=user%3ATWiStErRob+filename%3Amain.kts

@TWiStErRob
Copy link
Author

@yazmnh87 you can do parallel work, runBlocking is just the entry point, it provides the root context/scope. Note that it's important which Dispatcher you pass in, you can test with delay()s if your setup is correct.

Here's an example where I do parallel operations on the file system:
https://github.com/TWiStErRob/TWiStErRob-env/blob/master/git/merged-branches/find-merged-branches.main.kts

@yschimke
Copy link

Fair play, my comment was also from 2020. It's 2023 now, I promise to stop reviews your gists :)

@TWiStErRob
Copy link
Author

Haha, feel free to comment / review, just look at the whole context before doing so.
I'm sure your links have helped people, so 👏!

Btw, looking at the date of your comment, it's exactly the date of our anniversary with my girlfriend. I think I was preoccupied. 🥰

@yazmnh87
Copy link

@TWiStErRob Thanks for your reply. I like your work, and really appreciate the code you've made available for me to reference and the time you spent to send me the additional links:)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment