-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Signed-off-by: Julia Plewa <jplewa@virtuslab.com>
- Loading branch information
Showing
3 changed files
with
271 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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())) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
} | ||
} |