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

Introduce Kotlin-style output interpolation #353

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
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
2 changes: 2 additions & 0 deletions sdk/src/main/kotlin/com/pulumi/kotlin/Common.kt
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
@file:Suppress("unused")

package com.pulumi.kotlin

import com.pulumi.Context
Expand Down
76 changes: 76 additions & 0 deletions sdk/src/main/kotlin/com/pulumi/kotlin/Output.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
@file:Suppress("unused")

package com.pulumi.kotlin

import com.pulumi.core.Output
import com.pulumi.core.internal.OutputData
import com.pulumi.core.internal.OutputInternal
import kotlinx.coroutines.future.await
import java.util.concurrent.CompletableFuture

object Output {

/**
* Use this method to open an [OutputInterpolationContext] in which the unary plus operator can be used on outputs
* to interpolate their asynchronous values into an asynchronous string wrapped in an output.
* ```kotlin
* val myOutput = Output.of("value")
* val interpolatedString = interpolate { "The value of this output is: ${+myOutput}" }
* ```
*
* This method is an alternative to the [Output.format] method from Java.
* ```java
* Output<String> myOutput = Output.of("value");
* Output<String> interpolatedString = String.format("The value of this output is: %s", myOutput);
* ```
* @param format a function returning a Kotlin-style interpolated string with [Output] instances marked with [OutputInterpolationContext.unaryPlus]
* @return an asynchronous [Output]-wrapped string with Kotlin-style interpolated values
*/
suspend fun interpolation(format: suspend OutputInterpolationContext.() -> String): Output<String> {
val context = OutputInterpolationContext()
val value = context.format()
val output = if (context.isKnown) Output.of(value) else unknownOutput()
return if (context.isSecret) output.asSecret() else output
}
}

/**
* The context for Kotlin-style [Output] interpolation.
*
* @property isSecret denotes whether any of the interpolated values contain a secret
*/
class OutputInterpolationContext internal constructor(
var isSecret: Boolean = false,
var isKnown: Boolean = true,
) {
/**
* The unary plus operator that can be used on [Output] instances within [OutputInterpolationContext].
*
* @return an asynchronous value of the [Output] in question converted to a string with [toString]
*/
suspend operator fun <T> Output<T>.unaryPlus(): String? {
return interpolate()
}

// TODO: decide if we prefer this or the unary plus
suspend fun <T> Output<T>.interpolate(): String? {
val outputData = (this as OutputInternal<T>)
.dataAsync
.await()

val value = outputData.valueNullable?.toString()

if (outputData.isSecret) {
this@OutputInterpolationContext.isSecret = true
}
if (!outputData.isKnown) {
this@OutputInterpolationContext.isKnown = false
}

return value
}
}

private fun <T> unknownOutput(): Output<T> {
return OutputInternal(CompletableFuture.completedFuture(OutputData.unknown()))
}
163 changes: 163 additions & 0 deletions sdk/src/test/kotlin/com/pulumi/kotlin/OutputTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
package com.pulumi.kotlin

import com.pulumi.core.Output
import com.pulumi.core.internal.OutputData
import com.pulumi.core.internal.OutputInternal
import com.pulumi.kotlin.Output.interpolation
import kotlinx.coroutines.runBlocking
import org.junit.jupiter.api.Test
import java.util.concurrent.CompletableFuture
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue

class OutputTest {

@Test
fun `interpolates known outputs`() {
// given
val output1 = Output.of("value1")
val output2 = Output.of("value2")
val output3 = Output.of("value3")

// when
val result = runBlocking {
interpolation {
"output1: ${+output1}, output2: ${+output2}, output3: ${+output3}"
}
}

// then
assertEquals("output1: value1, output2: value2, output3: value3", result.getValue())
assertFalse(result.isSecret())
assertTrue(result.isKnown())

val javaResult = Output.format("output1: %s, output2: %s, output3: %s", output1, output2, output3)
assertEquals(javaResult.getValue(), result.getValue())
assertEquals(javaResult.isKnown(), result.isKnown())
assertEquals(javaResult.isSecret(), result.isSecret())
}

@Test
fun `interpolates unknown outputs`() {
// given
val output1 = Output.of("value1")
val output2 = unknownOutput()
val output3 = Output.of("value3")

// when
val result = runBlocking {
interpolation {
"output1: ${+output1}, output2: ${+output2}, output3: ${+output3}"
}
}

// then
assertEquals(null, result.getValue())
assertFalse(result.isKnown())
assertFalse(result.isSecret())

val javaResult = Output.format("output1: %s, output2: %s, output3: %s", output1, output2, output3)
assertEquals(javaResult.getValue(), result.getValue())
assertEquals(javaResult.isKnown(), result.isKnown())
assertEquals(javaResult.isSecret(), result.isSecret())
}

@Test
fun `interpolates secret outputs`() {
// given
val output1 = Output.of("value1")
val output2 = Output.ofSecret("value2")
val output3 = Output.of("value3")

// when
val result = runBlocking {
interpolation {
"output1: ${+output1}, output2: ${+output2}, output3: ${+output3}"
}
}

// then
assertEquals("output1: value1, output2: value2, output3: value3", result.getValue())
assertTrue(result.isSecret())
assertTrue(result.isKnown())

val javaResult = Output.format("output1: %s, output2: %s, output3: %s", output1, output2, output3)
assertEquals(javaResult.getValue(), result.getValue())
assertEquals(javaResult.isKnown(), result.isKnown())
assertEquals(javaResult.isSecret(), result.isSecret())
}

@Test
fun `interpolates unknown and secret outputs`() {
// given
val output1 = unknownOutput()
val output2 = Output.ofSecret("value2")
val output3 = unknownOutput()

// when
val result = runBlocking {
interpolation {
"output1: ${+output1}, output2: ${+output2}, output3: ${+output3}"
}
}

// then
assertEquals(null, result.getValue())
assertFalse(result.isKnown())
assertTrue(result.isSecret())

val javaResult = Output.format("output1: %s, output2: %s, output3: %s", output1, output2, output3)
assertEquals(javaResult.getValue(), result.getValue())
assertEquals(javaResult.isKnown(), result.isKnown())
assertEquals(javaResult.isSecret(), result.isSecret())
}

@Test
fun `interpolates outputs that are both unknown and secret`() {
// given
val output1 = Output.of("value1")
val output2 = unknownOutput().asSecret()
val output3 = Output.of("value3")

// when
val result = runBlocking {
interpolation {
"output1: ${+output1}, output2: ${+output2}, output3: ${+output3}"
}
}

// then
assertEquals(null, result.getValue())
assertFalse(result.isKnown())
assertTrue(result.isSecret())

val javaResult = Output.format("output1: %s, output2: %s, output3: %s", output1, output2, output3)
assertEquals(javaResult.getValue(), result.getValue())
assertEquals(javaResult.isKnown(), result.isKnown())
assertEquals(javaResult.isSecret(), result.isSecret())
}

private fun unknownOutput(): Output<String> {
return OutputInternal(CompletableFuture.completedFuture(OutputData.unknown()))
}

private fun Output<String>.getValue(): String? {
return (this as OutputInternal<String>)
.dataAsync
.get()
.valueNullable
}

private fun Output<String>.isKnown(): Boolean {
return (this as OutputInternal<String>)
.isKnown
.get()
}

private fun Output<String>.isSecret(): Boolean {
return (this as OutputInternal<String>)
.isSecret
.get()
}
}