2019-10-26 01:59:50 +02:00
// Copyright 2016-2019, Pulumi Corporation
using System ;
using System.Collections.Immutable ;
using System.Linq ;
using System.Reflection ;
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 ) ) ;
return new OutputData < T > ( ( T ) data ! , isKnown , isSecret ) ;
}
public static OutputData < object? > ConvertValue ( string context , Value value , System . Type targetType )
{
CheckTargetType ( context , targetType ) ;
var ( deserialized , isKnown , isSecret ) = Deserializer . Deserialize ( value ) ;
var converted = ConvertObject ( context , deserialized , targetType ) ;
return new OutputData < object? > ( converted , isKnown , isSecret ) ;
}
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
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}" ) ;
}
if ( targetType . GetCustomAttribute < OutputTypeAttribute > ( ) = = null )
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 (
$"Expected target type {targetType.FullName} to have [{nameof(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-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
}
public static void CheckTargetType ( string context , System . Type targetType )
{
if ( targetType = = typeof ( bool ) | |
targetType = = typeof ( int ) | |
targetType = = typeof ( double ) | |
targetType = = typeof ( string ) | |
targetType = = typeof ( Asset ) | |
targetType = = typeof ( Archive ) | |
targetType = = typeof ( AssetOrArchive ) )
{
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 < > ) )
{
CheckTargetType ( context , targetType . GenericTypeArguments . Single ( ) ) ;
return ;
}
2019-11-21 20:51:45 +01:00
else if ( targetType . GetGenericTypeDefinition ( ) = = typeof ( Union < , > ) )
{
CheckTargetType ( context , targetType . GenericTypeArguments [ 0 ] ) ;
CheckTargetType ( context , targetType . GenericTypeArguments [ 1 ] ) ;
return ;
}
2019-10-26 01:59:50 +02:00
else if ( targetType . GetGenericTypeDefinition ( ) = = typeof ( ImmutableArray < > ) )
{
CheckTargetType ( context , targetType . GenericTypeArguments . Single ( ) ) ;
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 ' . ");
}
CheckTargetType ( context , dictTypeArgs [ 1 ] ) ;
return ;
}
else
{
throw new InvalidOperationException (
$ @ "{context} contains invalid type {targetType.FullName}:
The only generic types allowed are ImmutableArray < . . . > and ImmutableDictionary < string , . . . > ");
}
}
var propertyTypeAttribute = targetType . GetCustomAttribute < OutputTypeAttribute > ( ) ;
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
a class explicitly marked with the [ { nameof ( OutputTypeAttribute ) } ] . ");
}
var constructor = GetPropertyConstructor ( targetType ) ;
if ( constructor = = null )
{
throw new InvalidOperationException (
$@"{targetType.FullName} had [{nameof(OutputTypeAttribute)}], but did not contain constructor marked with [{nameof(OutputConstructorAttribute)}]." ) ;
}
foreach ( var param in constructor . GetParameters ( ) )
{
CheckTargetType ( $@"{targetType.FullName}({param.Name})" , param . ParameterType ) ;
}
}
private static ConstructorInfo GetPropertyConstructor ( System . Type outputTypeArg )
= > outputTypeArg . GetConstructors ( BindingFlags . NonPublic | BindingFlags . Public | BindingFlags . Instance ) . FirstOrDefault (
c = > c . GetCustomAttributes < OutputConstructorAttribute > ( ) ! = null ) ;
}
}