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:
Fraser Waters 2021-10-29 17:35:17 +01:00 committed by GitHub
parent e71fd81fe8
commit d39a14432f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 201 additions and 123 deletions

View file

@ -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

View file

@ -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

View file

@ -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);
}

View file

@ -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);

View file

@ -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);
}
}
}

View file

@ -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");
}
}
}

View file

@ -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);
}
}
}

View file

@ -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
{

View file

@ -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());
}

View file

@ -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);

View file

@ -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
{

View file

@ -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);
}
}
}

View file

@ -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);
}

View file

@ -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;
}

View file

@ -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);
}

View file

@ -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 });
}