Skip to content

Commit

Permalink
introduce output interpolation
Browse files Browse the repository at this point in the history
Signed-off-by: Julia Plewa <jplewa@virtuslab.com>
  • Loading branch information
jplewa committed Sep 23, 2023
1 parent 252878a commit f774aea
Show file tree
Hide file tree
Showing 3 changed files with 271 additions and 0 deletions.
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
83 changes: 83 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,83 @@
@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()
return try {
val output = Output.of(context.format())
if (context.isSecret) {
output.asSecret()
} else {
output
}
} catch (e: UnknownOutputError) {
if (context.isSecret) {
unknownOutput<String>().asSecret()
} else {
unknownOutput()
}
}
}
}

/**
* 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) {
/**
* 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
}

return value ?: throw UnknownOutputError()
}
}

private class UnknownOutputError : RuntimeException("Cannot interpolate an output whose value is unknown")

private fun <T> unknownOutput(): Output<T> {
return OutputInternal(CompletableFuture.completedFuture(OutputData.unknown()))
}
186 changes: 186 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,186 @@
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.assertNotEquals
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 = Output.of("value2")
val output3 = OutputInternal(CompletableFuture.completedFuture(OutputData.unknown<String>()))

// 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.of("value2")
val output3 = Output.ofSecret("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 = Output.of("value1")
val output2 = Output.ofSecret("value2")
val output3 = OutputInternal(CompletableFuture.completedFuture(OutputData.unknown<String>()))

// 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 unknown and secret outputs as not secret if an unknown output appears before a secret output`() {
// given
val output1 = OutputInternal(CompletableFuture.completedFuture(OutputData.unknown<String>()))
val output2 = Output.ofSecret("value2")
val output3 = Output.of("value1")

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

// then
assertEquals(null, result.getValue())
assertFalse(result.isKnown())
assertFalse(result.isSecret()) // note: this isn't the desired behavior

val javaResult = Output.format("output1: %s, output2: %s, output3: %s", output1, output2, output3)
assertEquals(javaResult.getValue(), result.getValue())
assertEquals(javaResult.isKnown(), result.isKnown())
assertNotEquals(javaResult.isSecret(), result.isSecret()) // note: this isn't the desired behavior
}

@Test
fun `interpolates outputs that are both unknown and secret`() {
// given
val output1 = Output.of("value1")
val output2 = Output.ofSecret("value2")
val output3 =
(OutputInternal(CompletableFuture.completedFuture(OutputData.unknown<String>())) as Output<String>).asSecret()

// 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 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()
}
}

0 comments on commit f774aea

Please sign in to comment.