Don't throw on type mismatches in the dotnet sdk (#8286)
* Don't throw on type mismatches in the dotnet sdk Fixes #7329 The converter will no longer throw if resource providers return data that does not match the expected type declared in the dotnet sdk. Instead a warning will be logged for the resource and the value will be set to `default(T)`.
This commit is contained in:
parent
e71fd81fe8
commit
d39a14432f
|
@ -8,10 +8,8 @@
|
|||
- [sdk/go] - Respect implicit parents in alias resolution
|
||||
[#8288](https://github.com/pulumi/pulumi/pull/8288)
|
||||
|
||||
- [sdk/dotnet] - Fix a race condition when detecting exceptions in stack creation
|
||||
[#8294](https://github.com/pulumi/pulumi/pull/8294)
|
||||
- Clarify error message string in `sdk/go/common/diag/errors.go`
|
||||
[#8284](https://github.com/pulumi/pulumi/pull/8284)
|
||||
- Clarify error message string in `sdk/go/common/diag/errors.go`
|
||||
[#8284](https://github.com/pulumi/pulumi/pull/8284)
|
||||
|
||||
- [cli] Add `--json` flag to `up`, `destroy` and `refresh`.
|
||||
|
||||
|
@ -30,6 +28,9 @@
|
|||
- [sdk/go] - Fix regression marshaling assets/archives.
|
||||
[#8290](https://github.com/pulumi/pulumi/pull/8290)
|
||||
|
||||
- [sdk/dotnet] - Don't panic on schema mismatches
|
||||
[#8286](https://github.com/pulumi/pulumi/pull/8286)
|
||||
|
||||
### Miscellaneous
|
||||
|
||||
- [sdk/python] - Drop support for python 3.6
|
||||
|
|
|
@ -36,6 +36,22 @@ namespace Pulumi.Tests.Mocks
|
|||
public Task<(string? id, object state)> NewResourceAsync(MockResourceArgs args) => throw new Exception("Not used");
|
||||
}
|
||||
|
||||
class MyInvalidMocks : IMocks
|
||||
{
|
||||
public Task<object> CallAsync(MockCallArgs args)
|
||||
{
|
||||
return Task.FromResult<object>(args);
|
||||
}
|
||||
|
||||
public Task<(string? id, object state)> NewResourceAsync(MockResourceArgs args) =>
|
||||
args.Type switch
|
||||
{
|
||||
"aws:ec2/instance:Instance" => Task.FromResult<(string?, object)>(("i-1234567890abcdef0", new Dictionary<string, object> { { "publicIp", unchecked((int)0xcb00710c) }, })),
|
||||
"pkg:index:MyCustom" => Task.FromResult<(string?, object)>((args.Name + "_id", args.Inputs)),
|
||||
_ => throw new Exception($"Unknown resource {args.Type}")
|
||||
};
|
||||
}
|
||||
|
||||
public class MocksTests
|
||||
{
|
||||
[Fact]
|
||||
|
@ -108,6 +124,20 @@ namespace Pulumi.Tests.Mocks
|
|||
Assert.Contains("' failed with an unhandled exception:", exception!.Message);
|
||||
Assert.Contains("Grpc.Core.RpcException: Status(StatusCode=\"Unknown\", Detail=\"error code 404\")", exception!.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TestStackWithInvalidSchema()
|
||||
{
|
||||
var resources = await Deployment.TestAsync<MyStack>(new MyInvalidMocks(), new TestOptions { IsPreview = false });
|
||||
|
||||
var stack = resources.OfType<MyStack>().FirstOrDefault();
|
||||
Assert.NotNull(stack);
|
||||
|
||||
var ip = await stack!.PublicIp.GetValueAsync(whenUnknown: default!);
|
||||
Assert.Null(ip);
|
||||
|
||||
// TODO: It would be good to assert that a warning was logged to the engine but getting hold of warnings requires re-plumbing what TestAsync returns.
|
||||
}
|
||||
}
|
||||
|
||||
public static class Testing
|
||||
|
|
|
@ -36,7 +36,7 @@ namespace Pulumi.Tests.Serialization
|
|||
private async Task Test(object args, string expected)
|
||||
{
|
||||
var serialized = await SerializeToValueAsync(args);
|
||||
var converted = Converter.ConvertValue<JsonElement>("", serialized);
|
||||
var converted = Converter.ConvertValue<JsonElement>(NoWarn, "", serialized);
|
||||
var value = converted.Value.GetProperty("v").GetProperty("s").GetString();
|
||||
Assert.Equal(expected, value);
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ namespace Pulumi.Tests.Serialization
|
|||
[Fact]
|
||||
public void True()
|
||||
{
|
||||
var data = Converter.ConvertValue<bool>("", new Value { BoolValue = true });
|
||||
var data = Converter.ConvertValue<bool>(NoWarn, "", new Value { BoolValue = true });
|
||||
Assert.True(data.Value);
|
||||
Assert.True(data.IsKnown);
|
||||
}
|
||||
|
@ -21,7 +21,7 @@ namespace Pulumi.Tests.Serialization
|
|||
[Fact]
|
||||
public void False()
|
||||
{
|
||||
var data = Converter.ConvertValue<bool>("", new Value { BoolValue = false });
|
||||
var data = Converter.ConvertValue<bool>(NoWarn, "", new Value { BoolValue = false });
|
||||
|
||||
Assert.False(data.Value);
|
||||
Assert.True(data.IsKnown);
|
||||
|
@ -30,7 +30,7 @@ namespace Pulumi.Tests.Serialization
|
|||
[Fact]
|
||||
public void SecretTrue()
|
||||
{
|
||||
var data = Converter.ConvertValue<bool>("", CreateSecretValue(new Value { BoolValue = true }));
|
||||
var data = Converter.ConvertValue<bool>(NoWarn, "", CreateSecretValue(new Value { BoolValue = true }));
|
||||
|
||||
Assert.True(data.Value);
|
||||
Assert.True(data.IsKnown);
|
||||
|
@ -40,7 +40,7 @@ namespace Pulumi.Tests.Serialization
|
|||
[Fact]
|
||||
public void SecretFalse()
|
||||
{
|
||||
var data = Converter.ConvertValue<bool>("", CreateSecretValue(new Value { BoolValue = false }));
|
||||
var data = Converter.ConvertValue<bool>(NoWarn, "", CreateSecretValue(new Value { BoolValue = false }));
|
||||
|
||||
Assert.False(data.Value);
|
||||
Assert.True(data.IsKnown);
|
||||
|
@ -48,12 +48,16 @@ namespace Pulumi.Tests.Serialization
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void NonBooleanThrows()
|
||||
public void NonBooleanLogs()
|
||||
{
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
{
|
||||
Converter.ConvertValue<bool>("", new Value { StringValue = "" });
|
||||
});
|
||||
string? loggedError = null;
|
||||
Action<string> warn = error => loggedError = error;
|
||||
var data = Converter.ConvertValue<bool>(warn, "", new Value { StringValue = "" });
|
||||
|
||||
Assert.False(data.Value);
|
||||
Assert.True(data.IsKnown);
|
||||
|
||||
Assert.Equal("Expected System.Boolean but got System.String deserializing ", loggedError);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
@ -61,7 +65,7 @@ namespace Pulumi.Tests.Serialization
|
|||
{
|
||||
return RunInPreview(() =>
|
||||
{
|
||||
var data = Converter.ConvertValue<bool>("", new Value { NullValue = NullValue.NullValue });
|
||||
var data = Converter.ConvertValue<bool>(NoWarn, "", new Value { NullValue = NullValue.NullValue });
|
||||
|
||||
Assert.False(data.Value);
|
||||
Assert.True(data.IsKnown);
|
||||
|
@ -73,7 +77,7 @@ namespace Pulumi.Tests.Serialization
|
|||
{
|
||||
return RunInNormal(() =>
|
||||
{
|
||||
var data = Converter.ConvertValue<bool>("", new Value { NullValue = NullValue.NullValue });
|
||||
var data = Converter.ConvertValue<bool>(NoWarn, "", new Value { NullValue = NullValue.NullValue });
|
||||
|
||||
Assert.False(data.Value);
|
||||
Assert.True(data.IsKnown);
|
||||
|
@ -83,25 +87,16 @@ namespace Pulumi.Tests.Serialization
|
|||
[Fact]
|
||||
public void UnknownProducesFalseUnknown()
|
||||
{
|
||||
var data = Converter.ConvertValue<bool>("", UnknownValue);
|
||||
var data = Converter.ConvertValue<bool>(NoWarn, "", UnknownValue);
|
||||
|
||||
Assert.False(data.Value);
|
||||
Assert.False(data.IsKnown);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StringTest()
|
||||
{
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
{
|
||||
Converter.ConvertValue<bool>("", new Value { StringValue = "" });
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NullableTrue()
|
||||
{
|
||||
var data = Converter.ConvertValue<bool?>("", new Value { BoolValue = true });
|
||||
var data = Converter.ConvertValue<bool?>(NoWarn, "", new Value { BoolValue = true });
|
||||
Assert.True(data.Value);
|
||||
Assert.True(data.IsKnown);
|
||||
}
|
||||
|
@ -109,7 +104,7 @@ namespace Pulumi.Tests.Serialization
|
|||
[Fact]
|
||||
public void NullableFalse()
|
||||
{
|
||||
var data = Converter.ConvertValue<bool?>("", new Value { BoolValue = false });
|
||||
var data = Converter.ConvertValue<bool?>(NoWarn, "", new Value { BoolValue = false });
|
||||
|
||||
Assert.False(data.Value);
|
||||
Assert.True(data.IsKnown);
|
||||
|
@ -118,7 +113,7 @@ namespace Pulumi.Tests.Serialization
|
|||
[Fact]
|
||||
public void NullableNull()
|
||||
{
|
||||
var data = Converter.ConvertValue<bool?>("", new Value { NullValue = NullValue.NullValue });
|
||||
var data = Converter.ConvertValue<bool?>(NoWarn, "", new Value { NullValue = NullValue.NullValue });
|
||||
|
||||
Assert.Null(data.Value);
|
||||
Assert.True(data.IsKnown);
|
||||
|
|
|
@ -81,7 +81,7 @@ namespace Pulumi.Tests.Serialization
|
|||
[Fact]
|
||||
public async Task TestComplexType1()
|
||||
{
|
||||
var data = Converter.ConvertValue<ComplexType1>("", await SerializeToValueAsync(new Dictionary<string, object>
|
||||
var data = Converter.ConvertValue<ComplexType1>(NoWarn, "", await SerializeToValueAsync(new Dictionary<string, object>
|
||||
{
|
||||
{ "s", "str" },
|
||||
{ "b", true },
|
||||
|
@ -133,10 +133,10 @@ namespace Pulumi.Tests.Serialization
|
|||
[Fact]
|
||||
public async Task TestComplexType2()
|
||||
{
|
||||
var data = Converter.ConvertValue<ComplexType2>("", await SerializeToValueAsync(new Dictionary<string, object>
|
||||
var data = Converter.ConvertValue<ComplexType2>(NoWarn, "", await SerializeToValueAsync(new Dictionary<string, object>
|
||||
{
|
||||
{
|
||||
"c",
|
||||
"c",
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
{ "s", "str1" },
|
||||
|
@ -168,7 +168,7 @@ namespace Pulumi.Tests.Serialization
|
|||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
{
|
||||
"c2Map",
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
|
@ -231,5 +231,48 @@ namespace Pulumi.Tests.Serialization
|
|||
}
|
||||
|
||||
#endregion
|
||||
|
||||
[Fact]
|
||||
public async Task TestComplexTypeTypeMismatches()
|
||||
{
|
||||
var warnings = new List<string>();
|
||||
|
||||
var data = Converter.ConvertValue<ComplexType1>(warnings.Add, "", await SerializeToValueAsync(new Dictionary<string, object>
|
||||
{
|
||||
{ "s", 24 },
|
||||
{ "b", "hi" },
|
||||
{ "i", "string" },
|
||||
{ "d", true },
|
||||
{ "array", new List<object> { false, 99, true, "hello" } },
|
||||
{ "dict", new Dictionary<object, object> { { "k", 10 }, { "v", "hello" } } },
|
||||
{ "obj", "test" },
|
||||
{ "size", "bigger" },
|
||||
{ "color", true },
|
||||
}));
|
||||
|
||||
Assert.Null(data.Value.S);
|
||||
Assert.False(data.Value.B);
|
||||
Assert.Equal(0, data.Value.I);
|
||||
Assert.Equal(0.0, data.Value.D);
|
||||
AssertEx.SequenceEqual(ImmutableArray<bool>.Empty.Add(false).Add(false).Add(true).Add(false), data.Value.Array);
|
||||
AssertEx.MapEqual(ImmutableDictionary<string, int>.Empty.Add("k", 10).Add("v", 0), data.Value.Dict);
|
||||
Assert.Equal("test", data.Value.Obj);
|
||||
Assert.Equal(default(ContainerSize), data.Value.Size);
|
||||
Assert.Equal(default(ContainerColor), data.Value.Color);
|
||||
|
||||
Assert.True(data.IsKnown);
|
||||
|
||||
AssertEx.SequenceEqual(new string[] {
|
||||
"Expected System.String but got System.Double deserializing Pulumi.Tests.Serialization.ComplexTypeConverterTests+ComplexType1(s)",
|
||||
"Expected System.Boolean but got System.String deserializing Pulumi.Tests.Serialization.ComplexTypeConverterTests+ComplexType1(b)",
|
||||
"Expected System.Double but got System.String deserializing Pulumi.Tests.Serialization.ComplexTypeConverterTests+ComplexType1(i)",
|
||||
"Expected System.Double but got System.Boolean deserializing Pulumi.Tests.Serialization.ComplexTypeConverterTests+ComplexType1(d)",
|
||||
"Expected System.Boolean but got System.Double deserializing Pulumi.Tests.Serialization.ComplexTypeConverterTests+ComplexType1(array)",
|
||||
"Expected System.Boolean but got System.String deserializing Pulumi.Tests.Serialization.ComplexTypeConverterTests+ComplexType1(array)",
|
||||
"Expected System.Double but got System.String deserializing Pulumi.Tests.Serialization.ComplexTypeConverterTests+ComplexType1(dict)",
|
||||
"Expected System.Double but got System.String deserializing Pulumi.Tests.Serialization.ComplexTypeConverterTests+ComplexType1(size)",
|
||||
"Expected System.String or System.Double but got System.Boolean deserializing Pulumi.Tests.Serialization.ComplexTypeConverterTests+ComplexType1(color)",
|
||||
}, warnings);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
// Copyright 2016-2019, Pulumi Corporation
|
||||
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Google.Protobuf.WellKnownTypes;
|
||||
using Pulumi.Serialization;
|
||||
|
@ -35,5 +36,10 @@ namespace Pulumi.Tests.Serialization
|
|||
var v = Deserializer.Deserialize(value).Value;
|
||||
return v == null ? default! : (T)v;
|
||||
}
|
||||
|
||||
protected static void NoWarn(string error)
|
||||
{
|
||||
throw new Exception("Test did not expect warn to be called");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -91,7 +91,7 @@ namespace Pulumi.Tests.Serialization
|
|||
[MemberData(nameof(StringEnums))]
|
||||
public async Task StringEnum(ContainerColor input)
|
||||
{
|
||||
var data = Converter.ConvertValue<ContainerColor>("", await SerializeToValueAsync(input));
|
||||
var data = Converter.ConvertValue<ContainerColor>(NoWarn, "", await SerializeToValueAsync(input));
|
||||
|
||||
Assert.Equal(input, data.Value);
|
||||
Assert.True(data.IsKnown);
|
||||
|
@ -108,7 +108,7 @@ namespace Pulumi.Tests.Serialization
|
|||
[MemberData(nameof(DoubleEnums))]
|
||||
public async Task DoubleEnum(ContainerBrightness input)
|
||||
{
|
||||
var data = Converter.ConvertValue<ContainerBrightness>("", await SerializeToValueAsync(input));
|
||||
var data = Converter.ConvertValue<ContainerBrightness>(NoWarn, "", await SerializeToValueAsync(input));
|
||||
|
||||
Assert.Equal(input, data.Value);
|
||||
Assert.True(data.IsKnown);
|
||||
|
@ -123,7 +123,7 @@ namespace Pulumi.Tests.Serialization
|
|||
[InlineData((ContainerSize)int.MaxValue)]
|
||||
public async Task Int32Enum(ContainerSize input)
|
||||
{
|
||||
var data = Converter.ConvertValue<ContainerSize>("", await SerializeToValueAsync(input));
|
||||
var data = Converter.ConvertValue<ContainerSize>(NoWarn, "", await SerializeToValueAsync(input));
|
||||
|
||||
Assert.Equal(input, data.Value);
|
||||
Assert.True(data.IsKnown);
|
||||
|
@ -200,19 +200,23 @@ namespace Pulumi.Tests.Serialization
|
|||
public static IEnumerable<object[]> EnumsWithUnconvertibleValues()
|
||||
=> new[]
|
||||
{
|
||||
new object[] { typeof(ContainerColor), new Value { NumberValue = 1.0 } },
|
||||
new object[] { typeof(ContainerBrightness), new Value { StringValue = "hello" } },
|
||||
new object[] { typeof(ContainerSize), new Value { StringValue = "hello" } },
|
||||
new object[] { typeof(ContainerColor), new Value { NumberValue = 1.0 }, "Expected target type Pulumi.Tests.Serialization.EnumConverterTests+ContainerColor to have a constructor with a single System.Double parameter." },
|
||||
new object[] { typeof(ContainerBrightness), new Value { StringValue = "hello" }, "Expected target type Pulumi.Tests.Serialization.EnumConverterTests+ContainerBrightness to have a constructor with a single System.String parameter." },
|
||||
new object[] { typeof(ContainerSize), new Value { StringValue = "hello" }, "Expected System.Double but got System.String deserializing " },
|
||||
};
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(EnumsWithUnconvertibleValues))]
|
||||
public void ConvertingUnconvertibleValuesThrows(Type targetType, Value value)
|
||||
public void ConvertingUnconvertibleValuesLogs(Type targetType, Value value, string expectedError)
|
||||
{
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
{
|
||||
Converter.ConvertValue("", value, targetType);
|
||||
});
|
||||
string? loggedError = null;
|
||||
Action<string> warn = error => loggedError = error;
|
||||
var data = Converter.ConvertValue(warn, "", value, targetType);
|
||||
|
||||
Assert.Null(data.Value);
|
||||
Assert.True(data.IsKnown);
|
||||
|
||||
Assert.Equal(expectedError, loggedError);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ namespace Pulumi.Tests.Serialization
|
|||
[Fact]
|
||||
public void IgnoreInternalProperty()
|
||||
{
|
||||
var data = Converter.ConvertValue<ImmutableDictionary<string, string>>("", new Value
|
||||
var data = Converter.ConvertValue<ImmutableDictionary<string, string>>(NoWarn, "", new Value
|
||||
{
|
||||
StructValue = new Struct
|
||||
{
|
||||
|
|
|
@ -13,7 +13,7 @@ namespace Pulumi.Tests.Serialization
|
|||
{
|
||||
var element = JsonDocument.Parse(json).RootElement;
|
||||
var serialized = await SerializeToValueAsync(element);
|
||||
var converted = Converter.ConvertValue<JsonElement>("", serialized);
|
||||
var converted = Converter.ConvertValue<JsonElement>(NoWarn, "", serialized);
|
||||
|
||||
Assert.Equal(expected, converted.Value.ToString());
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ namespace Pulumi.Tests.Serialization
|
|||
[Fact]
|
||||
public async Task EmptyList()
|
||||
{
|
||||
var data = Converter.ConvertValue<ImmutableArray<bool>>("", await SerializeToValueAsync(new List<bool>()));
|
||||
var data = Converter.ConvertValue<ImmutableArray<bool>>(NoWarn, "", await SerializeToValueAsync(new List<bool>()));
|
||||
|
||||
Assert.Equal(ImmutableArray<bool>.Empty, data.Value);
|
||||
Assert.True(data.IsKnown);
|
||||
|
@ -22,7 +22,7 @@ namespace Pulumi.Tests.Serialization
|
|||
[Fact]
|
||||
public async Task ListWithElement()
|
||||
{
|
||||
var data = Converter.ConvertValue<ImmutableArray<bool>>("", await SerializeToValueAsync(new List<bool> { true }));
|
||||
var data = Converter.ConvertValue<ImmutableArray<bool>>(NoWarn, "", await SerializeToValueAsync(new List<bool> { true }));
|
||||
|
||||
AssertEx.SequenceEqual(ImmutableArray<bool>.Empty.Add(true), data.Value);
|
||||
Assert.True(data.IsKnown);
|
||||
|
@ -31,7 +31,7 @@ namespace Pulumi.Tests.Serialization
|
|||
[Fact]
|
||||
public async Task SecretListWithElement()
|
||||
{
|
||||
var data = Converter.ConvertValue<ImmutableArray<bool>>("", await SerializeToValueAsync(Output.CreateSecret(new List<object> { true })));
|
||||
var data = Converter.ConvertValue<ImmutableArray<bool>>(NoWarn, "", await SerializeToValueAsync(Output.CreateSecret(new List<object> { true })));
|
||||
|
||||
AssertEx.SequenceEqual(ImmutableArray<bool>.Empty.Add(true), data.Value);
|
||||
Assert.True(data.IsKnown);
|
||||
|
@ -41,7 +41,7 @@ namespace Pulumi.Tests.Serialization
|
|||
[Fact]
|
||||
public async Task ListWithSecretElement()
|
||||
{
|
||||
var data = Converter.ConvertValue<ImmutableArray<bool>>("", await SerializeToValueAsync(new List<object> { Output.CreateSecret(true) }));
|
||||
var data = Converter.ConvertValue<ImmutableArray<bool>>(NoWarn, "", await SerializeToValueAsync(new List<object> { Output.CreateSecret(true) }));
|
||||
|
||||
AssertEx.SequenceEqual(ImmutableArray<bool>.Empty.Add(true), data.Value);
|
||||
Assert.True(data.IsKnown);
|
||||
|
@ -51,7 +51,7 @@ namespace Pulumi.Tests.Serialization
|
|||
[Fact]
|
||||
public async Task ListWithUnknownElement()
|
||||
{
|
||||
var data = Converter.ConvertValue<ImmutableArray<bool>>("", await SerializeToValueAsync(new List<object> { Output<bool>.CreateUnknown(true) }));
|
||||
var data = Converter.ConvertValue<ImmutableArray<bool>>(NoWarn, "", await SerializeToValueAsync(new List<object> { Output<bool>.CreateUnknown(true) }));
|
||||
|
||||
AssertEx.SequenceEqual(ImmutableArray<bool>.Empty.Add(false), data.Value);
|
||||
Assert.False(data.IsKnown);
|
||||
|
|
|
@ -26,7 +26,7 @@ namespace Pulumi.Tests.Serialization
|
|||
[Fact]
|
||||
public void SimpleCase()
|
||||
{
|
||||
var data = Converter.ConvertValue<RecursiveType>("", new Value
|
||||
var data = Converter.ConvertValue<RecursiveType>(NoWarn, "", new Value
|
||||
{
|
||||
StructValue = new Struct
|
||||
{
|
||||
|
|
|
@ -15,7 +15,7 @@ namespace Pulumi.Tests.Serialization
|
|||
[Fact]
|
||||
public void T0()
|
||||
{
|
||||
var data = Converter.ConvertValue<Union<int, string>>("", new Value { NumberValue = 1 });
|
||||
var data = Converter.ConvertValue<Union<int, string>>(NoWarn, "", new Value { NumberValue = 1 });
|
||||
Assert.True(data.Value.IsT0);
|
||||
Assert.True(data.IsKnown);
|
||||
Assert.Equal(1, data.Value.AsT0);
|
||||
|
@ -24,7 +24,7 @@ namespace Pulumi.Tests.Serialization
|
|||
[Fact]
|
||||
public void T1()
|
||||
{
|
||||
var data = Converter.ConvertValue<Union<int, string>>("", new Value { StringValue = "foo" });
|
||||
var data = Converter.ConvertValue<Union<int, string>>(NoWarn, "", new Value { StringValue = "foo" });
|
||||
Assert.True(data.Value.IsT1);
|
||||
Assert.True(data.IsKnown);
|
||||
Assert.Equal("foo", data.Value.AsT1);
|
||||
|
@ -33,7 +33,7 @@ namespace Pulumi.Tests.Serialization
|
|||
[Fact]
|
||||
public async Task MixedList()
|
||||
{
|
||||
var data = Converter.ConvertValue<ImmutableArray<Union<int, string>>>("",
|
||||
var data = Converter.ConvertValue<ImmutableArray<Union<int, string>>>(NoWarn, "",
|
||||
await SerializeToValueAsync(new List<object> { 1, "foo" }));
|
||||
Assert.True(data.IsKnown);
|
||||
Assert.Equal(2, data.Value.Length);
|
||||
|
@ -46,12 +46,16 @@ namespace Pulumi.Tests.Serialization
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void WrongTypeThrows()
|
||||
public void WrongTypeLogs()
|
||||
{
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
{
|
||||
Converter.ConvertValue<Union<int, string>>("", new Value { BoolValue = true });
|
||||
});
|
||||
string? loggedError = null;
|
||||
Action<string> warn = error => loggedError = error;
|
||||
var data = Converter.ConvertValue<Union<int, string>>(warn, "", new Value { BoolValue = true });
|
||||
|
||||
Assert.Equal(default(Union<int, string>), data.Value);
|
||||
Assert.True(data.IsKnown);
|
||||
|
||||
Assert.Equal("Expected System.Int32 or System.String but got System.Boolean deserializing ", loggedError);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,7 +26,7 @@ namespace Pulumi
|
|||
var (result, deps) = await CallRawAsync(token, args, self, options).ConfigureAwait(false);
|
||||
if (convertResult)
|
||||
{
|
||||
var converted = Converter.ConvertValue<T>($"{token} result", new Value { StructValue = result });
|
||||
var converted = Converter.ConvertValue<T>(err => Log.Warn(err, self), $"{token} result", new Value { StructValue = result });
|
||||
return new OutputData<T>(deps, converted.Value, converted.IsKnown, converted.IsSecret);
|
||||
}
|
||||
|
||||
|
|
|
@ -56,7 +56,7 @@ namespace Pulumi
|
|||
// tracking, which is a good future direction also for
|
||||
// `Invoke`.
|
||||
var result = await InvokeRawAsync(token, args, options).ConfigureAwait(false);
|
||||
var data = Converter.ConvertValue<T>($"{token} result",
|
||||
var data = Converter.ConvertValue<T>(err => Log.Warn(err), $"{token} result",
|
||||
new Value { StructValue = result.Serialized });
|
||||
var resources = ImmutableHashSet.CreateRange(
|
||||
result.PropertyToDependentResources.Values.SelectMany(r => r)
|
||||
|
@ -77,7 +77,7 @@ namespace Pulumi
|
|||
return default!;
|
||||
}
|
||||
|
||||
var data = Converter.ConvertValue<T>($"{token} result", new Value { StructValue = result.Serialized });
|
||||
var data = Converter.ConvertValue<T>(err => Log.Warn(err), $"{token} result", new Value { StructValue = result.Serialized });
|
||||
return data.Value;
|
||||
}
|
||||
|
||||
|
|
|
@ -119,7 +119,7 @@ namespace Pulumi
|
|||
dependencies = ImmutableHashSet<Resource>.Empty;
|
||||
}
|
||||
|
||||
var converted = Converter.ConvertValue($"{resource.GetType().FullName}.{fieldName}", value,
|
||||
var converted = Converter.ConvertValue(err => Log.Warn(err, resource), $"{resource.GetType().FullName}.{fieldName}", value,
|
||||
completionSource.TargetType, dependencies);
|
||||
completionSource.SetValue(converted);
|
||||
}
|
||||
|
|
|
@ -17,38 +17,41 @@ namespace Pulumi.Serialization
|
|||
{
|
||||
internal static class Converter
|
||||
{
|
||||
public static OutputData<T> ConvertValue<T>(string context, Value value)
|
||||
public static OutputData<T> ConvertValue<T>(Action<string> warn, string context, Value value)
|
||||
{
|
||||
var (data, isKnown, isSecret) = ConvertValue(context, value, typeof(T));
|
||||
return new OutputData<T>(ImmutableHashSet<Resource>.Empty, (T)data!, isKnown, isSecret);
|
||||
var (data, isKnown, isSecret) = ConvertValue(warn, context, value, typeof(T));
|
||||
var result = data == null ? default(T)! : (T)data;
|
||||
return new OutputData<T>(ImmutableHashSet<Resource>.Empty, result!, isKnown, isSecret);
|
||||
}
|
||||
|
||||
public static OutputData<object?> ConvertValue(string context, Value value, Type targetType)
|
||||
public static OutputData<object?> ConvertValue(Action<string> warn, string context, Value value, Type targetType)
|
||||
{
|
||||
return ConvertValue(context, value, targetType, ImmutableHashSet<Resource>.Empty);
|
||||
return ConvertValue(warn, context, value, targetType, ImmutableHashSet<Resource>.Empty);
|
||||
}
|
||||
|
||||
public static OutputData<object?> ConvertValue(
|
||||
string context, Value value, Type targetType, ImmutableHashSet<Resource> resources)
|
||||
Action<string> warn, string context, Value value, Type targetType, ImmutableHashSet<Resource> resources)
|
||||
{
|
||||
CheckTargetType(context, targetType, new HashSet<Type>());
|
||||
|
||||
var (deserialized, isKnown, isSecret) = Deserializer.Deserialize(value);
|
||||
var converted = ConvertObject(context, deserialized, targetType);
|
||||
var converted = ConvertObject(warn, context, deserialized, targetType);
|
||||
|
||||
return new OutputData<object?>(resources, converted, isKnown, isSecret);
|
||||
}
|
||||
|
||||
private static object? ConvertObject(string context, object? val, Type targetType)
|
||||
private static object? ConvertObject(Action<string> warn, string context, object? val, Type targetType)
|
||||
{
|
||||
var (result, exception) = TryConvertObject(context, val, targetType);
|
||||
if (exception != null)
|
||||
throw exception;
|
||||
var (result, error) = TryConvertObject(warn, context, val, targetType);
|
||||
if (error != null)
|
||||
{
|
||||
warn(error);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static (object?, InvalidOperationException?) TryConvertObject(string context, object? val, Type targetType)
|
||||
private static (object?, string?) TryConvertObject(Action<string> warn, string context, object? val, Type targetType)
|
||||
{
|
||||
var targetIsNullable = targetType.IsGenericType && targetType.GetGenericTypeDefinition() == typeof(Nullable<>);
|
||||
|
||||
|
@ -75,7 +78,7 @@ namespace Pulumi.Serialization
|
|||
|
||||
// We're not null and we're converting to Nullable<T>, just convert our value to be a T.
|
||||
if (targetIsNullable)
|
||||
return TryConvertObject(context, val, targetType.GenericTypeArguments.Single());
|
||||
return TryConvertObject(warn, context, val, targetType.GenericTypeArguments.Single());
|
||||
|
||||
if (targetType == typeof(string))
|
||||
return TryEnsureType<string>(context, val);
|
||||
|
@ -131,7 +134,7 @@ namespace Pulumi.Serialization
|
|||
if (targetType.IsEnum)
|
||||
{
|
||||
var underlyingType = targetType.GetEnumUnderlyingType();
|
||||
var (value, exception) = TryConvertObject(context, val, underlyingType);
|
||||
var (value, exception) = TryConvertObject(warn, context, val, underlyingType);
|
||||
if (exception != null || value is null)
|
||||
return (null, exception);
|
||||
|
||||
|
@ -144,16 +147,16 @@ namespace Pulumi.Serialization
|
|||
if (valType != typeof(string) &&
|
||||
valType != typeof(double))
|
||||
{
|
||||
return (null, new InvalidOperationException(
|
||||
$"Expected {typeof(string).FullName} or {typeof(double).FullName} but got {valType.FullName} deserializing {context}"));
|
||||
return (null,
|
||||
$"Expected {typeof(string).FullName} or {typeof(double).FullName} but got {valType.FullName} deserializing {context}");
|
||||
}
|
||||
|
||||
var enumTypeConstructor = targetType.GetConstructor(
|
||||
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, null, new[] { valType }, null);
|
||||
if (enumTypeConstructor == null)
|
||||
{
|
||||
return (null, new InvalidOperationException(
|
||||
$"Expected target type {targetType.FullName} to have a constructor with a single {valType.FullName} parameter."));
|
||||
return (null,
|
||||
$"Expected target type {targetType.FullName} to have a constructor with a single {valType.FullName} parameter.");
|
||||
}
|
||||
return (enumTypeConstructor.Invoke(new[] { val }), null);
|
||||
}
|
||||
|
@ -161,26 +164,25 @@ namespace Pulumi.Serialization
|
|||
if (targetType.IsConstructedGenericType)
|
||||
{
|
||||
if (targetType.GetGenericTypeDefinition() == typeof(Union<,>))
|
||||
return TryConvertOneOf(context, val, targetType);
|
||||
return TryConvertOneOf(warn, context, val, targetType);
|
||||
|
||||
if (targetType.GetGenericTypeDefinition() == typeof(ImmutableArray<>))
|
||||
return TryConvertArray(context, val, targetType);
|
||||
|
||||
return TryConvertArray(warn, context, val, targetType);
|
||||
|
||||
if (targetType.GetGenericTypeDefinition() == typeof(ImmutableDictionary<,>))
|
||||
return TryConvertDictionary(context, val, targetType);
|
||||
|
||||
return TryConvertDictionary(warn, context, val, targetType);
|
||||
|
||||
throw new InvalidOperationException(
|
||||
$"Unexpected generic target type {targetType.FullName} when deserializing {context}");
|
||||
}
|
||||
|
||||
if (targetType.GetCustomAttribute<OutputTypeAttribute>() == null)
|
||||
return (null, new InvalidOperationException(
|
||||
$"Unexpected target type {targetType.FullName} when deserializing {context}"));
|
||||
return (null, $"Unexpected target type {targetType.FullName} when deserializing {context}");
|
||||
|
||||
var constructor = GetPropertyConstructor(targetType);
|
||||
if (constructor == null)
|
||||
return (null, new InvalidOperationException(
|
||||
$"Expected target type {targetType.FullName} to have [{nameof(OutputConstructorAttribute)}] constructor when deserializing {context}"));
|
||||
return (null,
|
||||
$"Expected target type {targetType.FullName} to have [{nameof(OutputConstructorAttribute)}] constructor when deserializing {context}");
|
||||
|
||||
var (dictionary, tempException) = TryEnsureType<ImmutableDictionary<string, object>>(context, val);
|
||||
if (tempException != null)
|
||||
|
@ -197,25 +199,22 @@ namespace Pulumi.Serialization
|
|||
// unknown vals. That's ok. We'll pass that through to 'Convert' and will get the
|
||||
// default value needed for the parameter type.
|
||||
dictionary!.TryGetValue(parameter.Name!, out var argValue);
|
||||
var (temp, tempException1) = TryConvertObject($"{targetType.FullName}({parameter.Name})", argValue, parameter.ParameterType);
|
||||
if (tempException1 != null)
|
||||
return (null, tempException1);
|
||||
|
||||
arguments[i] = temp;
|
||||
arguments[i] = ConvertObject(warn, $"{targetType.FullName}({parameter.Name})", argValue, parameter.ParameterType);
|
||||
}
|
||||
|
||||
return (constructor.Invoke(arguments), null);
|
||||
}
|
||||
|
||||
private static (object?, InvalidOperationException?) TryConvertJsonElement(
|
||||
private static (object?, string?) TryConvertJsonElement(
|
||||
string context, object val)
|
||||
{
|
||||
using var stream = new MemoryStream();
|
||||
using (var writer = new Utf8JsonWriter(stream))
|
||||
{
|
||||
var exception = TryWriteJson(context, writer, val);
|
||||
if (exception != null)
|
||||
return (null, exception);
|
||||
var error = TryWriteJson(context, writer, val);
|
||||
if (error != null)
|
||||
return (null, error);
|
||||
}
|
||||
|
||||
stream.Position = 0;
|
||||
|
@ -224,7 +223,7 @@ namespace Pulumi.Serialization
|
|||
return (element, null);
|
||||
}
|
||||
|
||||
private static InvalidOperationException? TryWriteJson(string context, Utf8JsonWriter writer, object? val)
|
||||
private static string? TryWriteJson(string context, Utf8JsonWriter writer, object? val)
|
||||
{
|
||||
switch (val)
|
||||
{
|
||||
|
@ -262,41 +261,42 @@ namespace Pulumi.Serialization
|
|||
writer.WriteEndObject();
|
||||
return null;
|
||||
default:
|
||||
return new InvalidOperationException($"Unexpected type {val.GetType().FullName} when converting {context} to {nameof(JsonElement)}");
|
||||
return $"Unexpected type {val.GetType().FullName} when converting {context} to {nameof(JsonElement)}";
|
||||
}
|
||||
}
|
||||
|
||||
private static (T, InvalidOperationException?) TryEnsureType<T>(string context, object val)
|
||||
=> val is T t ? (t, null) : (default(T)!, new InvalidOperationException($"Expected {typeof(T).FullName} but got {val.GetType().FullName} deserializing {context}"));
|
||||
private static (T, string?) TryEnsureType<T>(string context, object val)
|
||||
=> val is T t ? (t, null) : (default(T)!, $"Expected {typeof(T).FullName} but got {val.GetType().FullName} deserializing {context}");
|
||||
|
||||
private static (object?, InvalidOperationException?) TryConvertOneOf(string context, object val, Type oneOfType)
|
||||
private static (object?, string?) TryConvertOneOf(Action<string> warn, string context, object val, Type oneOfType)
|
||||
{
|
||||
var firstType = oneOfType.GenericTypeArguments[0];
|
||||
var secondType = oneOfType.GenericTypeArguments[1];
|
||||
|
||||
var (val1, exception1) = TryConvertObject($"{context}.AsT0", val, firstType);
|
||||
var (val1, exception1) = TryConvertObject(warn, $"{context}.AsT0", val, firstType);
|
||||
if (exception1 == null)
|
||||
{
|
||||
var fromT0Method = oneOfType.GetMethod(nameof(Union<int, int>.FromT0), BindingFlags.Public | BindingFlags.Static);
|
||||
return (fromT0Method?.Invoke(null, new[] { val1 }), null);
|
||||
}
|
||||
|
||||
var (val2, exception2) = TryConvertObject($"{context}.AsT1", val, secondType);
|
||||
var (val2, exception2) = TryConvertObject(warn, $"{context}.AsT1", val, secondType);
|
||||
if (exception2 == null)
|
||||
{
|
||||
var fromT1Method = oneOfType.GetMethod(nameof(Union<int, int>.FromT1), BindingFlags.Public | BindingFlags.Static);
|
||||
return (fromT1Method?.Invoke(null, new[] { val2 }), null);
|
||||
}
|
||||
|
||||
return (null, new InvalidOperationException($"Expected {firstType.FullName} or {secondType.FullName} but got {val.GetType().FullName} deserializing {context}"));
|
||||
return (null, $"Expected {firstType.FullName} or {secondType.FullName} but got {val.GetType().FullName} deserializing {context}");
|
||||
}
|
||||
|
||||
private static (object?, InvalidOperationException?) TryConvertArray(
|
||||
private static (object?, string?) TryConvertArray(
|
||||
Action<string> warn,
|
||||
string fieldName, object val, Type targetType)
|
||||
{
|
||||
if (!(val is ImmutableArray<object> array))
|
||||
return (null, new InvalidOperationException(
|
||||
$"Expected {typeof(ImmutableArray<object>).FullName} but got {val.GetType().FullName} deserializing {fieldName}"));
|
||||
return (null,
|
||||
$"Expected {typeof(ImmutableArray<object>).FullName} but got {val.GetType().FullName} deserializing {fieldName}");
|
||||
|
||||
var builder =
|
||||
typeof(ImmutableArray).GetMethod(nameof(ImmutableArray.CreateBuilder), Array.Empty<Type>())!
|
||||
|
@ -309,9 +309,7 @@ namespace Pulumi.Serialization
|
|||
var elementType = targetType.GenericTypeArguments.Single();
|
||||
foreach (var element in array)
|
||||
{
|
||||
var (e, exception) = TryConvertObject(fieldName, element, elementType);
|
||||
if (exception != null)
|
||||
return (null, exception);
|
||||
var e = ConvertObject(warn, fieldName, element, elementType);
|
||||
|
||||
builderAdd.Invoke(builder, new[] { e });
|
||||
}
|
||||
|
@ -319,12 +317,13 @@ namespace Pulumi.Serialization
|
|||
return (builderToImmutable.Invoke(builder, null), null);
|
||||
}
|
||||
|
||||
private static (object?, InvalidOperationException?) TryConvertDictionary(
|
||||
private static (object?, string?) TryConvertDictionary(
|
||||
Action<string> warn,
|
||||
string fieldName, object val, Type targetType)
|
||||
{
|
||||
if (!(val is ImmutableDictionary<string, object> dictionary))
|
||||
return (null, new InvalidOperationException(
|
||||
$"Expected {typeof(ImmutableDictionary<string, object>).FullName} but got {val.GetType().FullName} deserializing {fieldName}"));
|
||||
return (null,
|
||||
$"Expected {typeof(ImmutableDictionary<string, object>).FullName} but got {val.GetType().FullName} deserializing {fieldName}");
|
||||
|
||||
// check if already in the form we need. no need to convert anything.
|
||||
if (targetType == typeof(ImmutableDictionary<string, object>))
|
||||
|
@ -332,25 +331,21 @@ namespace Pulumi.Serialization
|
|||
|
||||
var keyType = targetType.GenericTypeArguments[0];
|
||||
if (keyType != typeof(string))
|
||||
return (null, new InvalidOperationException(
|
||||
$"Unexpected type {targetType.FullName} when deserializing {fieldName}. ImmutableDictionary's TKey type was not {typeof(string).FullName}"));
|
||||
return (null,
|
||||
$"Unexpected type {targetType.FullName} when deserializing {fieldName}. ImmutableDictionary's TKey type was not {typeof(string).FullName}");
|
||||
|
||||
var builder =
|
||||
typeof(ImmutableDictionary).GetMethod(nameof(ImmutableDictionary.CreateBuilder), Array.Empty<Type>())!
|
||||
.MakeGenericMethod(targetType.GenericTypeArguments)
|
||||
.Invoke(obj: null, parameters: null)!;
|
||||
|
||||
// var b = ImmutableDictionary.CreateBuilder<string, object>().Add()
|
||||
|
||||
var builderAdd = builder.GetType().GetMethod(nameof(ImmutableDictionary<string, object>.Builder.Add), targetType.GenericTypeArguments)!;
|
||||
var builderToImmutable = builder.GetType().GetMethod(nameof(ImmutableDictionary<string, object>.Builder.ToImmutable))!;
|
||||
|
||||
var elementType = targetType.GenericTypeArguments[1];
|
||||
foreach (var (key, element) in dictionary)
|
||||
{
|
||||
var (e, exception) = TryConvertObject(fieldName, element, elementType);
|
||||
if (exception != null)
|
||||
return (null, exception);
|
||||
var e = ConvertObject(warn, fieldName, element, elementType);
|
||||
|
||||
builderAdd.Invoke(builder, new[] { key, e });
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue