Skip to content

Commit c400493

Browse files
Fix conversion for nested Output<T> (#527)
1 parent 9b585c8 commit c400493

File tree

7 files changed

+253
-6
lines changed

7 files changed

+253
-6
lines changed
+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
component: sdk/converter
2+
kind: bug-fixes
3+
body: Fix conversion for nested Output<T>
4+
time: 2025-03-06T22:07:47.3405356+01:00
5+
custom:
6+
PR: "527"

integration_tests/provider_construct/dotnet/Component.cs

+18-1
Original file line numberDiff line numberDiff line change
@@ -81,11 +81,28 @@ public sealed class ComplexType : ComplexTypeBase
8181
// the Output attribute is inherited from the base class
8282
public override string InheritOutputAttribute { get; set; }
8383

84+
[Output("nestedOutput")]
85+
public Output<NestedOutputType> NestedOutput { get; set; }
86+
8487
[OutputConstructor]
85-
public ComplexType(string name, int intValue, string inheritOutputAttribute)
88+
public ComplexType(string name, int intValue, string inheritOutputAttribute, Output<NestedOutputType> nestedOutput)
8689
{
8790
Name = name;
8891
IntValue = intValue;
8992
InheritOutputAttribute = inheritOutputAttribute;
93+
NestedOutput = nestedOutput;
94+
}
95+
}
96+
97+
[OutputType]
98+
public sealed class NestedOutputType
99+
{
100+
[Output("value")]
101+
public string Value { get; set; }
102+
103+
[OutputConstructor]
104+
public NestedOutputType(string value)
105+
{
106+
Value = value;
90107
}
91108
}

integration_tests/provider_construct/dotnet/Program.cs

+5-3
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,8 @@ static async Task<int> Main(string[] args)
3737

3838
var complexResult10 = await OutputUtilities.GetValueAsync(component10.ComplexResult);
3939
var complexResult20 = await OutputUtilities.GetValueAsync(component20.ComplexResult);
40-
ValidateComplexResult(complexResult10, complexArgs10);
41-
ValidateComplexResult(complexResult20, complexArgs20);
40+
await ValidateComplexResult(complexResult10, complexArgs10);
41+
await ValidateComplexResult(complexResult20, complexArgs20);
4242

4343
var inheritedOutputResult10 = await OutputUtilities.GetValueAsync(component10.InheritOutputAttribute);
4444
var inheritedOutputResult20 = await OutputUtilities.GetValueAsync(component20.InheritOutputAttribute);
@@ -49,10 +49,12 @@ static async Task<int> Main(string[] args)
4949
return returnCode;
5050
}
5151

52-
private static void ValidateComplexResult(ComplexType result, ComplexTypeArgs expected)
52+
private static async Task ValidateComplexResult(ComplexType result, ComplexTypeArgs expected)
5353
{
5454
result.Name.Should().Be(expected.Name);
5555
result.IntValue.Should().Be(expected.IntValue);
5656
result.InheritOutputAttribute.Should().Be(expected.InheritInputAttribute);
57+
var nestedValue = await OutputUtilities.GetValueAsync(result.NestedOutput);
58+
nestedValue.Value.Should().Be(expected.Name);
5759
}
5860
}

integration_tests/provider_construct/testcomponent-dotnet/Component.cs

+20-2
Original file line numberDiff line numberDiff line change
@@ -55,12 +55,29 @@ public sealed class ComplexType : ComplexTypeBase
5555
// the Output attribute is inherited from the base class
5656
public override string InheritOutputAttribute { get; set; }
5757

58+
[Output("nestedOutput")]
59+
public Output<NestedOutputType> NestedOutput { get; set; }
60+
5861
[OutputConstructor]
59-
public ComplexType(string name, int intValue, string inheritOutputAttribute)
62+
public ComplexType(string name, int intValue, string inheritOutputAttribute, Output<NestedOutputType> nestedOutput)
6063
{
6164
Name = name;
6265
IntValue = intValue;
6366
InheritOutputAttribute = inheritOutputAttribute;
67+
NestedOutput = nestedOutput;
68+
}
69+
}
70+
71+
[OutputType]
72+
public sealed class NestedOutputType
73+
{
74+
[Output("value")]
75+
public string Value { get; set; }
76+
77+
[OutputConstructor]
78+
public NestedOutputType(string value)
79+
{
80+
Value = value;
6481
}
6582
}
6683

@@ -93,7 +110,8 @@ public Component(string name, ComponentArgs args, ComponentResourceOptions? opts
93110
: base("test:index:Test", name, args, opts)
94111
{
95112
PasswordResult = args.PasswordLength.Apply(GenerateRandomString);
96-
ComplexResult = args.Complex.Apply(complex => Output.Create(AsTask(new ComplexType(complex.Name, complex.IntValue, complex.InheritInputAttribute))));
113+
var nestedOutput = args.Complex.Apply(c => AsTask(new NestedOutputType(c.Name)));
114+
ComplexResult = args.Complex.Apply(complex => Output.Create(AsTask(new ComplexType(complex.Name, complex.IntValue, complex.InheritInputAttribute, nestedOutput))));
97115
InheritOutputAttribute = args.InheritInputAttribute;
98116
}
99117

sdk/Pulumi.Tests/Provider/PropertyValueTests.cs

+47
Original file line numberDiff line numberDiff line change
@@ -736,4 +736,51 @@ public async Task DeserializingClassWithMultipleConstructorsWorks()
736736
Assert.Equal(5, typeWithOptionalParameterCtor.First);
737737
Assert.Equal(1, typeWithOptionalParameterCtor.Second);
738738
}
739+
740+
[OutputType]
741+
public class OuterOutputType
742+
{
743+
[Output("nestedOutput")]
744+
public required Output<NestedOutputType> NestedOutput { get; init; }
745+
}
746+
747+
[OutputType]
748+
public class NestedOutputType
749+
{
750+
[Output("stringOutput")]
751+
public required Output<string> StringOutput { get; init; }
752+
}
753+
754+
[Fact]
755+
public async Task SerializingNestedOutputWorks()
756+
{
757+
var serializer = CreateSerializer();
758+
759+
var serialized = await serializer.Serialize(new OuterOutputType()
760+
{
761+
NestedOutput = Output.Create(new NestedOutputType
762+
{
763+
StringOutput = Output.Create("hello")
764+
})
765+
});
766+
767+
var expected = Object(
768+
Pair("nestedOutput", Object(Pair("stringOutput", new PropertyValue("hello")))));
769+
770+
Assert.Equal(expected, serialized);
771+
}
772+
773+
[Fact]
774+
public async Task DeerializingNestedOutputWorks()
775+
{
776+
var serializer = CreateSerializer();
777+
var value = Object(
778+
Pair("nestedOutput", Object(Pair("stringOutput", new PropertyValue("hello")))));
779+
780+
var serialized = await serializer.Deserialize<OuterOutputType>(value);
781+
782+
var nestedValueOutput = await serialized.NestedOutput.DataTask;
783+
var stringValueOutput = await nestedValueOutput.Value.StringOutput.DataTask;
784+
Assert.Equal("hello", stringValueOutput.Value);
785+
}
739786
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
// Copyright 2016-2025, Pulumi Corporation
2+
3+
using System.Collections.Immutable;
4+
using System.Text.Json;
5+
using System.Threading.Tasks;
6+
using Google.Protobuf.WellKnownTypes;
7+
using Pulumi.Serialization;
8+
using Xunit;
9+
10+
namespace Pulumi.Tests.Serialization
11+
{
12+
public class NestedOutputTest : ConverterTests
13+
{
14+
[OutputType]
15+
public class NestedOutputType
16+
{
17+
[OutputConstructor]
18+
public NestedOutputType(Output<string> output)
19+
{
20+
Output = output;
21+
}
22+
23+
public Output<string> Output { get; }
24+
}
25+
26+
[OutputType]
27+
public class DeepNestedOutputType
28+
{
29+
[OutputConstructor]
30+
public DeepNestedOutputType(Output<Output<ImmutableArray<string>>> output)
31+
{
32+
Output = output;
33+
}
34+
35+
public Output<Output<ImmutableArray<string>>> Output { get; }
36+
}
37+
38+
[Fact]
39+
public async Task NestedCase()
40+
{
41+
var stringValue = "Hello World";
42+
var data = Converter.ConvertValue<NestedOutputType>(NoWarn, "", new Value
43+
{
44+
StructValue = new Struct
45+
{
46+
Fields =
47+
{
48+
{
49+
"output", new Value
50+
{
51+
StringValue = stringValue
52+
}
53+
},
54+
}
55+
}
56+
});
57+
Assert.True(data.IsKnown);
58+
Assert.Equal(stringValue, await data.Value.Output.GetValueAsync(""));
59+
}
60+
61+
[Fact]
62+
public async Task DeepNestedCase()
63+
{
64+
var stringValue = "Hello World";
65+
var data = Converter.ConvertValue<DeepNestedOutputType>(NoWarn, "", new Value
66+
{
67+
StructValue = new Struct
68+
{
69+
Fields =
70+
{
71+
{
72+
"output", new Value
73+
{
74+
ListValue = new ListValue
75+
{
76+
Values =
77+
{
78+
new Value
79+
{
80+
StringValue = stringValue
81+
}
82+
}
83+
}
84+
}
85+
},
86+
}
87+
}
88+
});
89+
Assert.True(data.IsKnown);
90+
var innerOutput = await data.Value.Output.GetValueAsync(Output.Create(ImmutableArray<string>.Empty));
91+
var listValue = await innerOutput.GetValueAsync(ImmutableArray<string>.Empty);
92+
Assert.Equivalent(ImmutableArray.Create(stringValue), listValue);
93+
}
94+
95+
[Fact]
96+
public void JsonSerialize()
97+
{
98+
var stringValue = "Hello World";
99+
var data = Converter.ConvertValue<JsonElement>(NoWarn, "", new Value
100+
{
101+
StructValue = new Struct
102+
{
103+
Fields =
104+
{
105+
{
106+
"output", new Value
107+
{
108+
ListValue = new ListValue
109+
{
110+
Values =
111+
{
112+
new Value
113+
{
114+
StringValue = stringValue
115+
}
116+
}
117+
}
118+
}
119+
},
120+
}
121+
}
122+
});
123+
Assert.True(data.IsKnown);
124+
Assert.Equal($$"""{"output":["{{stringValue}}"]}""", data.Value.ToString());
125+
}
126+
}
127+
}

sdk/Pulumi/Serialization/Converter.cs

+30
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,11 @@ private static (object?, string?) TryConvertObject(Action<string> warn, string c
178178
if (targetType.GetGenericTypeDefinition() == typeof(ImmutableDictionary<,>))
179179
return TryConvertDictionary(warn, context, val, targetType);
180180

181+
if (targetType.GetGenericTypeDefinition() == typeof(Output<>))
182+
return ((object?, string?))TryConvertOutputMethod
183+
.MakeGenericMethod(targetType.GetGenericArguments().Single())
184+
.Invoke(null, new object?[] { warn, context, val, targetType })!;
185+
181186
throw new InvalidOperationException(
182187
$"Unexpected generic target type {targetType.FullName} when deserializing {context}");
183188
}
@@ -272,6 +277,9 @@ private static (object?, string?) TryConvertJsonElement(
272277
}
273278
writer.WriteEndObject();
274279
return null;
280+
case IOutput output:
281+
var outputData = output.GetDataAsync().GetAwaiter().GetResult();
282+
return TryWriteJson(context, writer, outputData.Value);
275283
default:
276284
return $"Unexpected type {val.GetType().FullName} when converting {context} to {nameof(JsonElement)}";
277285
}
@@ -329,6 +337,23 @@ private static (object?, string?) TryConvertArray(
329337
return (builderToImmutable.Invoke(builder, null), null);
330338
}
331339

340+
private static readonly MethodInfo TryConvertOutputMethod = typeof(Converter).GetMethod(nameof(TryConvertOutput), BindingFlags.NonPublic | BindingFlags.Static)
341+
?? throw new MissingMethodException($"Method {nameof(TryConvertOutput)} not found.");
342+
343+
private static (object?, string?) TryConvertOutput<T>(
344+
Action<string> warn,
345+
string fieldName, object val, Type targetType)
346+
{
347+
var (element, error) = TryConvertObject(warn, fieldName, val, typeof(T));
348+
349+
if (error != null)
350+
{
351+
return (element, error);
352+
}
353+
354+
return (Output.Create((T)element!), null);
355+
}
356+
332357
private static (object?, string?) TryConvertDictionary(
333358
Action<string> warn,
334359
string fieldName, object val, Type targetType)
@@ -471,6 +496,11 @@ static bool CheckEnumType(Type targetType, Type underlyingType)
471496
CheckTargetType(context, dictTypeArgs[1], seenTypes);
472497
return;
473498
}
499+
if (targetType.GetGenericTypeDefinition() == typeof(Output<>))
500+
{
501+
CheckTargetType(context, targetType.GenericTypeArguments.Single(), seenTypes);
502+
return;
503+
}
474504
throw new InvalidOperationException($@"{context} contains invalid type {targetType.FullName}:
475505
The only generic types allowed are ImmutableArray<...> and ImmutableDictionary<string, ...>");
476506
}

0 commit comments

Comments
 (0)