Skip to content

Commit b917d7c

Browse files
committed
Fix conversion for nested Output<T>
1 parent 6b7bc12 commit b917d7c

File tree

6 files changed

+250
-6
lines changed

6 files changed

+250
-6
lines changed

integration_tests/provider_construct/dotnet/Component.cs

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

84+
85+
[Output("nestedOutput")]
86+
public Output<NestedOutputType> NestedOutput { get; set; }
87+
88+
8489
[OutputConstructor]
85-
public ComplexType(string name, int intValue, string inheritOutputAttribute)
90+
public ComplexType(string name, int intValue, string inheritOutputAttribute, Output<NestedOutputType> nestedOutput)
8691
{
8792
Name = name;
8893
IntValue = intValue;
8994
InheritOutputAttribute = inheritOutputAttribute;
95+
NestedOutput = nestedOutput;
96+
}
97+
}
98+
99+
100+
[OutputType]
101+
public sealed class NestedOutputType
102+
{
103+
[Output("value")]
104+
public string Value { get; set; }
105+
106+
[OutputConstructor]
107+
public NestedOutputType(string value)
108+
{
109+
Value = value;
90110
}
91111
}

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

+25-2
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ public abstract class ComplexTypeBase
4343
public abstract string InheritOutputAttribute { get; set; }
4444
}
4545

46+
47+
4648
[OutputType]
4749
public sealed class ComplexType : ComplexTypeBase
4850
{
@@ -55,12 +57,32 @@ public sealed class ComplexType : ComplexTypeBase
5557
// the Output attribute is inherited from the base class
5658
public override string InheritOutputAttribute { get; set; }
5759

60+
61+
[Output("nestedOutput")]
62+
public Output<NestedOutputType> NestedOutput { get; set; }
63+
64+
5865
[OutputConstructor]
59-
public ComplexType(string name, int intValue, string inheritOutputAttribute)
66+
public ComplexType(string name, int intValue, string inheritOutputAttribute, Output<NestedOutputType> nestedOutput)
6067
{
6168
Name = name;
6269
IntValue = intValue;
6370
InheritOutputAttribute = inheritOutputAttribute;
71+
NestedOutput = nestedOutput;
72+
}
73+
}
74+
75+
76+
[OutputType]
77+
public sealed class NestedOutputType
78+
{
79+
[Output("value")]
80+
public string Value { get; set; }
81+
82+
[OutputConstructor]
83+
public NestedOutputType(string value)
84+
{
85+
Value = value;
6486
}
6587
}
6688

@@ -93,7 +115,8 @@ public Component(string name, ComponentArgs args, ComponentResourceOptions? opts
93115
: base("test:index:Test", name, args, opts)
94116
{
95117
PasswordResult = args.PasswordLength.Apply(GenerateRandomString);
96-
ComplexResult = args.Complex.Apply(complex => Output.Create(AsTask(new ComplexType(complex.Name, complex.IntValue, complex.InheritInputAttribute))));
118+
var nestedOutput = args.Complex.Apply(c => AsTask(new NestedOutputType(c.Name)));
119+
ComplexResult = args.Complex.Apply(complex => Output.Create(AsTask(new ComplexType(complex.Name, complex.IntValue, complex.InheritInputAttribute, nestedOutput))));
97120
InheritOutputAttribute = args.InheritInputAttribute;
98121
}
99122

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

+25
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,18 @@ 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 = ConvertObject(warn, fieldName, val, typeof(T));
348+
349+
return (Output.Create((T)element!), null);
350+
}
351+
332352
private static (object?, string?) TryConvertDictionary(
333353
Action<string> warn,
334354
string fieldName, object val, Type targetType)
@@ -471,6 +491,11 @@ static bool CheckEnumType(Type targetType, Type underlyingType)
471491
CheckTargetType(context, dictTypeArgs[1], seenTypes);
472492
return;
473493
}
494+
if (targetType.GetGenericTypeDefinition() == typeof(Output<>))
495+
{
496+
CheckTargetType(context, targetType.GenericTypeArguments.Single(), seenTypes);
497+
return;
498+
}
474499
throw new InvalidOperationException($@"{context} contains invalid type {targetType.FullName}:
475500
The only generic types allowed are ImmutableArray<...> and ImmutableDictionary<string, ...>");
476501
}

0 commit comments

Comments
 (0)