2019-10-26 01:59:50 +02:00
// Copyright 2016-2019, Pulumi Corporation
using System ;
2019-11-22 00:36:01 +01:00
using System.Collections.Generic ;
2019-10-26 01:59:50 +02:00
using System.Collections.Immutable ;
2019-11-26 05:50:05 +01:00
using System.IO ;
2019-10-26 01:59:50 +02:00
using System.Linq ;
using System.Reflection ;
2019-11-26 05:50:05 +01:00
using System.Text.Json ;
2019-10-26 01:59:50 +02:00
using Google.Protobuf.WellKnownTypes ;
namespace Pulumi.Serialization
{
internal static class Converter
{
public static OutputData < T > ConvertValue < T > ( string context , Value value )
{
var ( data , isKnown , isSecret ) = ConvertValue ( context , value , typeof ( T ) ) ;
2019-12-17 23:11:45 +01:00
return new OutputData < T > ( ImmutableHashSet < Resource > . Empty , ( T ) data ! , isKnown , isSecret ) ;
2019-10-26 01:59:50 +02:00
}
public static OutputData < object? > ConvertValue ( string context , Value value , System . Type targetType )
{
2019-11-22 00:36:01 +01:00
CheckTargetType ( context , targetType , new HashSet < System . Type > ( ) ) ;
2019-10-26 01:59:50 +02:00
var ( deserialized , isKnown , isSecret ) = Deserializer . Deserialize ( value ) ;
var converted = ConvertObject ( context , deserialized , targetType ) ;
2019-12-17 23:11:45 +01:00
return new OutputData < object? > (
ImmutableHashSet < Resource > . Empty , converted , isKnown , isSecret ) ;
2019-10-26 01:59:50 +02:00
}
private static object? ConvertObject ( string context , object? val , System . Type targetType )
2019-11-21 20:51:45 +01:00
{
var ( result , exception ) = TryConvertObject ( context , val , targetType ) ;
if ( exception ! = null )
throw exception ;
return result ;
}
private static ( object? , InvalidOperationException ? ) TryConvertObject ( string context , object? val , System . Type targetType )
2019-10-26 01:59:50 +02:00
{
var targetIsNullable = targetType . IsGenericType & & targetType . GetGenericTypeDefinition ( ) = = typeof ( Nullable < > ) ;
// Note: 'null's can enter the system as the representation of an 'unknown' value.
// Before calling 'Convert' we will have already lifted the 'IsKnown' bit out, but we
// will be passing null around as a value.
if ( val = = null )
{
if ( targetIsNullable )
2019-11-21 20:51:45 +01:00
{
2019-10-26 01:59:50 +02:00
// A 'null' value coerces to a nullable null.
2019-11-21 20:51:45 +01:00
return ( null , null ) ;
}
2019-10-26 01:59:50 +02:00
if ( targetType . IsValueType )
2019-11-21 20:51:45 +01:00
{
return ( Activator . CreateInstance ( targetType ) , null ) ;
}
2019-10-26 01:59:50 +02:00
// for all other types, can just return the null value right back out as a legal
// reference type value.
2019-11-21 20:51:45 +01:00
return ( null , null ) ;
2019-10-26 01:59:50 +02:00
}
// We're not null and we're converting to Nullable<T>, just convert our value to be a T.
if ( targetIsNullable )
2019-11-21 20:51:45 +01:00
return TryConvertObject ( context , val , targetType . GenericTypeArguments . Single ( ) ) ;
2019-10-26 01:59:50 +02:00
if ( targetType = = typeof ( string ) )
2019-11-21 20:51:45 +01:00
return TryEnsureType < string > ( context , val ) ;
2019-10-26 01:59:50 +02:00
if ( targetType = = typeof ( bool ) )
2019-11-21 20:51:45 +01:00
return TryEnsureType < bool > ( context , val ) ;
2019-10-26 01:59:50 +02:00
if ( targetType = = typeof ( double ) )
2019-11-21 20:51:45 +01:00
return TryEnsureType < double > ( context , val ) ;
2019-10-26 01:59:50 +02:00
if ( targetType = = typeof ( int ) )
2019-11-21 20:51:45 +01:00
{
var ( d , exception ) = TryEnsureType < double > ( context , val ) ;
if ( exception ! = null )
return ( null , exception ) ;
return ( ( int ) d , exception ) ;
}
2019-10-26 01:59:50 +02:00
if ( targetType = = typeof ( Asset ) )
2019-11-21 20:51:45 +01:00
return TryEnsureType < Asset > ( context , val ) ;
2019-10-26 01:59:50 +02:00
if ( targetType = = typeof ( Archive ) )
2019-11-21 20:51:45 +01:00
return TryEnsureType < Archive > ( context , val ) ;
2019-10-26 01:59:50 +02:00
if ( targetType = = typeof ( AssetOrArchive ) )
2019-11-21 20:51:45 +01:00
return TryEnsureType < AssetOrArchive > ( context , val ) ;
2019-10-26 01:59:50 +02:00
2019-11-26 05:50:05 +01:00
if ( targetType = = typeof ( JsonElement ) )
return TryConvertJsonElement ( context , val ) ;
2019-10-26 01:59:50 +02:00
if ( targetType . IsConstructedGenericType )
{
2019-11-21 20:51:45 +01:00
if ( targetType . GetGenericTypeDefinition ( ) = = typeof ( Union < , > ) )
return TryConvertOneOf ( context , val , targetType ) ;
2019-10-26 01:59:50 +02:00
if ( targetType . GetGenericTypeDefinition ( ) = = typeof ( ImmutableArray < > ) )
2019-11-21 20:51:45 +01:00
return TryConvertArray ( context , val , targetType ) ;
2019-10-26 01:59:50 +02:00
if ( targetType . GetGenericTypeDefinition ( ) = = typeof ( ImmutableDictionary < , > ) )
2019-11-21 20:51:45 +01:00
return TryConvertDictionary ( context , val , targetType ) ;
2019-10-26 01:59:50 +02:00
throw new InvalidOperationException (
$"Unexpected generic target type {targetType.FullName} when deserializing {context}" ) ;
}
2020-02-11 11:40:14 +01:00
if ( targetType . GetCustomAttribute < Pulumi . OutputTypeAttribute > ( ) = = null
#pragma warning disable 618
& & targetType . GetCustomAttribute < Pulumi . Serialization . OutputTypeAttribute > ( ) = = null )
#pragma warning restore 618
2019-11-21 20:51:45 +01:00
return ( null , new InvalidOperationException (
$"Unexpected target type {targetType.FullName} when deserializing {context}" ) ) ;
2019-10-26 01:59:50 +02:00
var constructor = GetPropertyConstructor ( targetType ) ;
if ( constructor = = null )
2019-11-21 20:51:45 +01:00
return ( null , new InvalidOperationException (
2020-02-11 11:40:14 +01:00
$"Expected target type {targetType.FullName} to have [{nameof(Pulumi.OutputConstructorAttribute)}] constructor when deserializing {context}" ) ) ;
2019-10-26 01:59:50 +02:00
2019-11-21 20:51:45 +01:00
var ( dictionary , tempException ) = TryEnsureType < ImmutableDictionary < string , object > > ( context , val ) ;
if ( tempException ! = null )
return ( null , tempException ) ;
2019-10-26 01:59:50 +02:00
var constructorParameters = constructor . GetParameters ( ) ;
var arguments = new object? [ constructorParameters . Length ] ;
for ( int i = 0 , n = constructorParameters . Length ; i < n ; i + + )
{
var parameter = constructorParameters [ i ] ;
// Note: TryGetValue may not find a value here. That can happen for things like
// unknown vals. That's ok. We'll pass that through to 'Convert' and will get the
// default value needed for the parameter type.
2019-11-21 20:51:45 +01:00
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 ;
2019-10-26 01:59:50 +02:00
}
2019-11-21 20:51:45 +01:00
return ( constructor . Invoke ( arguments ) , null ) ;
2019-10-26 01:59:50 +02:00
}
2019-11-26 05:50:05 +01:00
private static ( object? , InvalidOperationException ? ) 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 ) ;
}
stream . Position = 0 ;
var document = JsonDocument . Parse ( stream ) ;
var element = document . RootElement ;
return ( element , null ) ;
}
}
private static InvalidOperationException ? TryWriteJson ( string context , Utf8JsonWriter writer , object? val )
{
switch ( val )
{
case string v :
writer . WriteStringValue ( v ) ;
return null ;
case double v :
writer . WriteNumberValue ( v ) ;
return null ;
case bool v :
writer . WriteBooleanValue ( v ) ;
return null ;
case null :
writer . WriteNullValue ( ) ;
return null ;
case ImmutableArray < object? > v :
writer . WriteStartArray ( ) ;
foreach ( var element in v )
{
var exception = TryWriteJson ( context , writer , element ) ;
if ( exception ! = null )
return exception ;
}
writer . WriteEndArray ( ) ;
return null ;
case ImmutableDictionary < string , object? > v :
writer . WriteStartObject ( ) ;
foreach ( var ( key , element ) in v )
{
writer . WritePropertyName ( key ) ;
var exception = TryWriteJson ( context , writer , element ) ;
if ( exception ! = null )
return exception ;
}
writer . WriteEndObject ( ) ;
return null ;
default :
return new InvalidOperationException ( $"Unexpected type {val.GetType().FullName} when converting {context} to {nameof(JsonElement)}" ) ;
}
}
2019-11-21 20:51:45 +01:00
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}" ) ) ;
2019-10-26 01:59:50 +02:00
2019-11-21 20:51:45 +01:00
private static ( object? , InvalidOperationException ? ) TryConvertOneOf ( string context , object val , System . Type oneOfType )
2019-10-26 01:59:50 +02:00
{
2019-11-21 20:51:45 +01:00
var firstType = oneOfType . GenericTypeArguments [ 0 ] ;
var secondType = oneOfType . GenericTypeArguments [ 1 ] ;
var ( val1 , exception1 ) = TryConvertObject ( $"{context}.AsT0" , val , firstType ) ;
if ( exception1 = = null )
2019-10-26 01:59:50 +02:00
{
2019-11-21 20:51:45 +01:00
var fromT0Method = oneOfType . GetMethod ( nameof ( Union < int , int > . FromT0 ) , BindingFlags . Public | BindingFlags . Static ) ;
return ( fromT0Method . Invoke ( null , new [ ] { val1 } ) , null ) ;
2019-10-26 01:59:50 +02:00
}
2019-11-21 20:51:45 +01:00
var ( val2 , exception2 ) = TryConvertObject ( $"{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}" ) ) ;
}
private static ( object? , InvalidOperationException ? ) TryConvertArray (
string fieldName , object val , System . Type targetType )
{
if ( ! ( val is ImmutableArray < object > array ) )
return ( null , new InvalidOperationException (
$"Expected {typeof(ImmutableArray<object>).FullName} but got {val.GetType().FullName} deserializing {fieldName}" ) ) ;
2019-10-26 01:59:50 +02:00
var builder =
typeof ( ImmutableArray ) . GetMethod ( nameof ( ImmutableArray . CreateBuilder ) , Array . Empty < System . Type > ( ) ) !
. MakeGenericMethod ( targetType . GenericTypeArguments )
. Invoke ( obj : null , parameters : null ) ! ;
var builderAdd = builder . GetType ( ) . GetMethod ( nameof ( ImmutableArray < int > . Builder . Add ) ) ! ;
var builderToImmutable = builder . GetType ( ) . GetMethod ( nameof ( ImmutableArray < int > . Builder . ToImmutable ) ) ! ;
var elementType = targetType . GenericTypeArguments . Single ( ) ;
foreach ( var element in array )
{
2019-11-21 20:51:45 +01:00
var ( e , exception ) = TryConvertObject ( fieldName , element , elementType ) ;
if ( exception ! = null )
return ( null , exception ) ;
builderAdd . Invoke ( builder , new [ ] { e } ) ;
2019-10-26 01:59:50 +02:00
}
2019-11-21 20:51:45 +01:00
return ( builderToImmutable . Invoke ( builder , null ) , null ) ;
2019-10-26 01:59:50 +02:00
}
2019-11-21 20:51:45 +01:00
private static ( object? , InvalidOperationException ? ) TryConvertDictionary (
string fieldName , object val , System . Type targetType )
2019-10-26 01:59:50 +02:00
{
if ( ! ( val is ImmutableDictionary < string , object > dictionary ) )
2019-11-21 20:51:45 +01:00
return ( null , new InvalidOperationException (
$"Expected {typeof(ImmutableDictionary<string, object>).FullName} but got {val.GetType().FullName} deserializing {fieldName}" ) ) ;
2019-10-26 01:59:50 +02:00
2019-11-21 20:51:45 +01:00
// check if already in the form we need. no need to convert anything.
2019-10-26 01:59:50 +02:00
if ( targetType = = typeof ( ImmutableDictionary < string , object > ) )
2019-11-21 20:51:45 +01:00
return ( val , null ) ;
2019-10-26 01:59:50 +02:00
var keyType = targetType . GenericTypeArguments [ 0 ] ;
if ( keyType ! = typeof ( string ) )
2019-11-21 20:51:45 +01:00
return ( null , new InvalidOperationException (
$"Unexpected type {targetType.FullName} when deserializing {fieldName}. ImmutableDictionary's TKey type was not {typeof(string).FullName}" ) ) ;
2019-10-26 01:59:50 +02:00
var builder =
typeof ( ImmutableDictionary ) . GetMethod ( nameof ( ImmutableDictionary . CreateBuilder ) , Array . Empty < System . 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 )
{
2019-11-21 20:51:45 +01:00
var ( e , exception ) = TryConvertObject ( fieldName , element , elementType ) ;
if ( exception ! = null )
return ( null , exception ) ;
builderAdd . Invoke ( builder , new [ ] { key , e } ) ;
2019-10-26 01:59:50 +02:00
}
2019-11-21 20:51:45 +01:00
return ( builderToImmutable . Invoke ( builder , null ) , null ) ;
2019-10-26 01:59:50 +02:00
}
2019-11-22 00:36:01 +01:00
public static void CheckTargetType ( string context , System . Type targetType , HashSet < System . Type > seenTypes )
2019-10-26 01:59:50 +02:00
{
2019-11-22 00:36:01 +01:00
// types can be recursive. So only dive into a type if it's the first time we're seeing it.
if ( ! seenTypes . Add ( targetType ) )
return ;
2019-10-26 01:59:50 +02:00
if ( targetType = = typeof ( bool ) | |
targetType = = typeof ( int ) | |
targetType = = typeof ( double ) | |
targetType = = typeof ( string ) | |
targetType = = typeof ( Asset ) | |
targetType = = typeof ( Archive ) | |
2019-11-26 05:50:05 +01:00
targetType = = typeof ( AssetOrArchive ) | |
targetType = = typeof ( JsonElement ) )
2019-10-26 01:59:50 +02:00
{
return ;
}
if ( targetType = = typeof ( ImmutableDictionary < string , object > ) )
{
// This type is what is generated for things like azure/aws tags. It's an untyped
// map in our original schema. This is the only place that `object` should appear
// as a legal value.
return ;
}
if ( targetType . IsConstructedGenericType )
{
if ( targetType . GetGenericTypeDefinition ( ) = = typeof ( Nullable < > ) )
{
2019-11-22 00:36:01 +01:00
CheckTargetType ( context , targetType . GenericTypeArguments . Single ( ) , seenTypes ) ;
2019-10-26 01:59:50 +02:00
return ;
}
2019-11-21 20:51:45 +01:00
else if ( targetType . GetGenericTypeDefinition ( ) = = typeof ( Union < , > ) )
{
2019-11-22 00:36:01 +01:00
CheckTargetType ( context , targetType . GenericTypeArguments [ 0 ] , seenTypes ) ;
CheckTargetType ( context , targetType . GenericTypeArguments [ 1 ] , seenTypes ) ;
2019-11-21 20:51:45 +01:00
return ;
}
2019-10-26 01:59:50 +02:00
else if ( targetType . GetGenericTypeDefinition ( ) = = typeof ( ImmutableArray < > ) )
{
2019-11-22 00:36:01 +01:00
CheckTargetType ( context , targetType . GenericTypeArguments . Single ( ) , seenTypes ) ;
2019-10-26 01:59:50 +02:00
return ;
}
else if ( targetType . GetGenericTypeDefinition ( ) = = typeof ( ImmutableDictionary < , > ) )
{
var dictTypeArgs = targetType . GenericTypeArguments ;
if ( dictTypeArgs [ 0 ] ! = typeof ( string ) )
{
throw new InvalidOperationException (
$ @ "{context} contains invalid type {targetType.FullName}:
The only allowed ImmutableDictionary ' TKey ' type is ' String ' . ");
}
2019-11-22 00:36:01 +01:00
CheckTargetType ( context , dictTypeArgs [ 1 ] , seenTypes ) ;
2019-10-26 01:59:50 +02:00
return ;
}
else
{
throw new InvalidOperationException (
$ @ "{context} contains invalid type {targetType.FullName}:
The only generic types allowed are ImmutableArray < . . . > and ImmutableDictionary < string , . . . > ");
}
}
2020-02-11 11:40:14 +01:00
var propertyTypeAttribute = ( Attribute ? ) targetType . GetCustomAttribute < Pulumi . OutputTypeAttribute > ( )
#pragma warning disable 618
? ? targetType . GetCustomAttribute < Pulumi . Serialization . OutputTypeAttribute > ( ) ;
#pragma warning restore 618
2019-10-26 01:59:50 +02:00
if ( propertyTypeAttribute = = null )
{
throw new InvalidOperationException (
$ @ "{context} contains invalid type {targetType.FullName}. Allowed types are:
String , Boolean , Int32 , Double ,
Nullable < . . . > , ImmutableArray < . . . > and ImmutableDictionary < string , . . . > or
2020-02-11 11:40:14 +01:00
a class explicitly marked with the [ { nameof ( Pulumi . OutputTypeAttribute ) } ] . ");
2019-10-26 01:59:50 +02:00
}
var constructor = GetPropertyConstructor ( targetType ) ;
if ( constructor = = null )
{
throw new InvalidOperationException (
2020-02-11 11:40:14 +01:00
$@"{targetType.FullName} had [{nameof(Pulumi.OutputTypeAttribute)}], but did not contain constructor marked with [{nameof(Pulumi.OutputConstructorAttribute)}]." ) ;
2019-10-26 01:59:50 +02:00
}
foreach ( var param in constructor . GetParameters ( ) )
{
2019-11-22 00:36:01 +01:00
CheckTargetType ( $@"{targetType.FullName}({param.Name})" , param . ParameterType , seenTypes ) ;
2019-10-26 01:59:50 +02:00
}
}
private static ConstructorInfo GetPropertyConstructor ( System . Type outputTypeArg )
= > outputTypeArg . GetConstructors ( BindingFlags . NonPublic | BindingFlags . Public | BindingFlags . Instance ) . FirstOrDefault (
2020-02-11 11:40:14 +01:00
c = > c . GetCustomAttributes < Pulumi . OutputConstructorAttribute > ( ) ! = null
#pragma warning disable 618
| | c . GetCustomAttributes < Pulumi . Serialization . OutputConstructorAttribute > ( ) ! = null ) ;
#pragma warning restore 618
2019-10-26 01:59:50 +02:00
}
}