diff --git a/besom-cats/src/main/scala/runtime.scala b/besom-cats/src/main/scala/runtime.scala index b2ff7450..b4e98454 100644 --- a/besom-cats/src/main/scala/runtime.scala +++ b/besom-cats/src/main/scala/runtime.scala @@ -50,4 +50,30 @@ trait CatsEffectModule extends BesomModule: // override def run(program: Context ?=> Output[Exports]): Future[Unit] = ??? object Pulumi extends CatsEffectModule -export Pulumi.{*, given} +export Pulumi.{component => _, *, given} + +import scala.reflect.Typeable + +// this proxy is only necessary due to https://github.com/scala/scala3/issues/17930 +/** Creates a new component resource. + * + * @param name + * The unique name of the resource. + * @param typ + * The type of the resource. + * @param opts + * A bag of options that control this resource's behavior. + * @param f + * The function that will create the component resource. + * @tparam A + * The type of the component resource. + * @return + * The component resource. + */ +def component[A <: ComponentResource & Product: RegistersOutputs: Typeable](using ctx: Context)( + name: NonEmptyString, + typ: ResourceType, + opts: ComponentResourceOptions = ComponentResourceOptions() +)( + f: Context ?=> ComponentBase ?=> A +): Output[A] = Pulumi.component(name, typ, opts)(f) diff --git a/besom-zio/src/main/scala/runtime.scala b/besom-zio/src/main/scala/runtime.scala index 96f7589a..248d8b9a 100644 --- a/besom-zio/src/main/scala/runtime.scala +++ b/besom-zio/src/main/scala/runtime.scala @@ -52,4 +52,30 @@ trait ZIOModule extends BesomModule: // override def run(program: Context ?=> Output[Exports]): Future[Unit] = ??? object Pulumi extends ZIOModule -export Pulumi.{*, given} +export Pulumi.{component => _, *, given} + +import scala.reflect.Typeable + +// this proxy is only necessary due to https://github.com/scala/scala3/issues/17930 +/** Creates a new component resource. + * + * @param name + * The unique name of the resource. + * @param typ + * The type of the resource. + * @param opts + * A bag of options that control this resource's behavior. + * @param f + * The function that will create the component resource. + * @tparam A + * The type of the component resource. + * @return + * The component resource. + */ +def component[A <: ComponentResource & Product: RegistersOutputs: Typeable](using ctx: Context)( + name: NonEmptyString, + typ: ResourceType, + opts: ComponentResourceOptions = ComponentResourceOptions() +)( + f: Context ?=> ComponentBase ?=> A +): Output[A] = Pulumi.component(name, typ, opts)(f) diff --git a/core/src/main/scala/besom/future.scala b/core/src/main/scala/besom/future.scala index b83d7352..1730ede0 100644 --- a/core/src/main/scala/besom/future.scala +++ b/core/src/main/scala/besom/future.scala @@ -44,4 +44,30 @@ trait FutureMonadModule extends BesomModule: * }}} */ object Pulumi extends FutureMonadModule -export Pulumi.{*, given} +export Pulumi.{component => _, *, given} + +import scala.reflect.Typeable + +// this proxy is only necessary due to https://github.com/scala/scala3/issues/17930 +/** Creates a new component resource. + * + * @param name + * The unique name of the resource. + * @param typ + * The type of the resource. + * @param opts + * A bag of options that control this resource's behavior. + * @param f + * The function that will create the component resource. + * @tparam A + * The type of the component resource. + * @return + * The component resource. + */ +def component[A <: ComponentResource & Product: RegistersOutputs: Typeable](using ctx: Context)( + name: NonEmptyString, + typ: ResourceType, + opts: ComponentResourceOptions = ComponentResourceOptions() +)( + f: Context ?=> ComponentBase ?=> A +): Output[A] = Pulumi.component(name, typ, opts)(f) diff --git a/core/src/main/scala/besom/internal/BesomSyntax.scala b/core/src/main/scala/besom/internal/BesomSyntax.scala index 422d5b88..775f1f83 100644 --- a/core/src/main/scala/besom/internal/BesomSyntax.scala +++ b/core/src/main/scala/besom/internal/BesomSyntax.scala @@ -91,7 +91,7 @@ trait BesomSyntax: typ: ResourceType, opts: ComponentResourceOptions = ComponentResourceOptions() )( - f: Context ?=> ComponentBase ?=> A | Output[A] + f: Context ?=> ComponentBase ?=> A ): Output[A] = Output.ofData { ctx @@ -103,10 +103,7 @@ trait BesomSyntax: val componentContext = ComponentContext(ctx, urnRes) val componentOutput = - try - f(using componentContext)(using componentBase) match - case output: Output[A] @unchecked => output - case a: A => Output(Result.pure(a)) + try Output(Result.pure(f(using componentContext)(using componentBase))) catch case e: Exception => Output(Result.fail(e)) val componentResult = componentOutput.getValueOrFail { diff --git a/core/src/main/scala/besom/internal/Output.scala b/core/src/main/scala/besom/internal/Output.scala index f7536c5f..0d436666 100644 --- a/core/src/main/scala/besom/internal/Output.scala +++ b/core/src/main/scala/besom/internal/Output.scala @@ -80,14 +80,18 @@ trait OutputFactory: )(using BuildFrom[CC[Output[B]], B, To], Context): Output[To] = sequence(coll.map(f).asInstanceOf[CC[Output[B]]]) def fail(t: Throwable)(using Context): Output[Nothing] = Output.fail(t) +end OutputFactory + trait OutputExtensionsFactory: - implicit final class OutputSequenceOps[A, CC[X] <: Iterable[X], To](coll: CC[Output[A]]): - def sequence(using BuildFrom[CC[Output[A]], A, To], Context): Output[To] = - Output.sequence(coll) + implicit object OutputSequenceOps: + extension [A, CC[X] <: Iterable[X]](coll: CC[Output[A]]) + def sequence[To](using BuildFrom[CC[Output[A]], A, To], Context): Output[To] = + Output.sequence(coll) - implicit final class OutputTraverseOps[A, CC[X] <: Iterable[X]](coll: CC[A]): - def traverse[B, To](f: A => Output[B])(using BuildFrom[CC[Output[B]], B, To], Context): Output[To] = - coll.map(f).asInstanceOf[CC[Output[B]]].sequence + implicit object OutputTraverseOps: + extension [A, CC[X] <: Iterable[X]](coll: CC[A]) + def traverse[B, To](f: A => Output[B])(using BuildFrom[CC[Output[B]], B, To], Context): Output[To] = + Output.sequence(coll.map(f).asInstanceOf[CC[Output[B]]]) implicit final class OutputOptionOps[A](output: Output[Option[A]]): def getOrElse[B >: A: Typeable](default: => B | Output[B])(using ctx: Context): Output[B] = @@ -108,6 +112,7 @@ trait OutputExtensionsFactory: case b: Output[Option[B]] => b case b: Option[B] => Output(b) } +end OutputExtensionsFactory object Output: // should be NonEmptyString diff --git a/core/src/test/scala/besom/internal/OutputTest.scala b/core/src/test/scala/besom/internal/OutputTest.scala index 4881d41c..606e251a 100644 --- a/core/src/test/scala/besom/internal/OutputTest.scala +++ b/core/src/test/scala/besom/internal/OutputTest.scala @@ -138,4 +138,60 @@ class OutputTest extends munit.FunSuite: Context().waitForAllTasks.unsafeRunSync() } + test("Output.sequence works with all kinds of collections") { + given Context = DummyContext().unsafeRunSync() + + assertEquals(Output.sequence(List(Output("value"), Output("value2"))).getData.unsafeRunSync(), OutputData(List("value", "value2"))) + assertEquals(Output.sequence(Vector(Output("value"), Output("value2"))).getData.unsafeRunSync(), OutputData(Vector("value", "value2"))) + assertEquals(Output.sequence(Set(Output("value"), Output("value2"))).getData.unsafeRunSync(), OutputData(Set("value", "value2"))) + assertEquals( + Output.sequence(Array(Output("value"), Output("value2")).toList).getData.unsafeRunSync(), + OutputData(List("value", "value2")) + ) + val iter: Iterable[String] = List("value", "value2") + assertEquals(Output.sequence(iter.map(Output(_))).getData.unsafeRunSync(), OutputData(List("value", "value2"))) + + Context().waitForAllTasks.unsafeRunSync() + } + + test("extensions for sequence and traverse work will all kinds of collections") { + import besom.* // test global import + given Context = DummyContext().unsafeRunSync() + + assertEquals(List(Output("value"), Output("value2")).sequence.getData.unsafeRunSync(), OutputData(List("value", "value2"))) + assertEquals(Vector(Output("value"), Output("value2")).sequence.getData.unsafeRunSync(), OutputData(Vector("value", "value2"))) + assertEquals(Set(Output("value"), Output("value2")).sequence.getData.unsafeRunSync(), OutputData(Set("value", "value2"))) + assertEquals( + Array("value", "value2").toList.traverse(x => Output(x)).getData.unsafeRunSync(), + OutputData(List("value", "value2")) + ) + + Context().waitForAllTasks.unsafeRunSync() + } + + test("issue 430") { + import java.io.File + import besom.* + object s3: + def BucketObject(name: NonEmptyString)(using Context): Output[Unit] = Output(()) + + given Context = DummyContext().unsafeRunSync() + + val uploads = File(".").listFiles().toList.traverse { file => + val name = NonEmptyString(file.getName) match + case Some(name) => Output(name) + case None => Output(None).map(_ => throw new RuntimeException("Unexpected empty file name")) + + name.flatMap { + s3.BucketObject( + _ + ) + } + } + + uploads.getData.unsafeRunSync() + + Context().waitForAllTasks.unsafeRunSync() + } + end OutputTest