# encoding: ascii
# api: csharp
# title: Union types
# description: This module is a hack to add union types to PowerShell.
# version: 0.1
# type: script
# author: Public Domain
# license: CC0
# x-poshcode-id: 5314
# x-archived: 2016-05-19T10:32:00
# x-published: 2016-07-17T03:10:00
#
# It also is useful for overriding the type system of PowerShell to use a more traditional C# style type system.
#
<#

After running this code, use it like:

[or([int],[float])]$lhs = rhs
# only [int]s or [float]s can be assigned to $lhs

##########
# Notes: #
##########

####
1. If rhs is $null, it will examine all of the types in the or() to determine if they can accept $null.
If all the types are value types, $null wont be accepted as a valid value. If you want to allow nulls in this scenario, use [Nullable[some_valuetype]] or [Nullable`1] as at least one of the types.


####
2. or() uses $Type.IsAssignableFrom which checks the inheritence hierarchy (i.e. it's similar to -is) rather than the fancier PowerShell algorithm which considers casts, constructors, conversion through strings or a numeric tower, etc. e.g.

[or([long])]$a = 1
# will fail because 1 is an [int] and not a [long]

[or([string])]$a = $null
# $a is $null instead of '' so you don't have to do the [NullString]::Value hack

[or([int])]$a = $null
# errors instead of setting $a to 0
# similar to [ValidateNotNull()][Nullable[int]]$a = $null but without the type conversion

In other words, [or([foo])] works similar to [ValidateScript({$_ -is [foo]})]


####
3. or() allows generic type definitions unlike powershell, e.g.

[or([System.Collections.Generic.ISet`1])]$set = [System.Collections.Generic.HashSet[int]]@(1, 2, 3)
# works

However, it only works on the top level type. e.g.

[or([dictionary[int,dictionary`2]])]
# wont match any value


####
4. [or()] without a types listed will always fail when you try to assign to it.


#>


Add-Type -TypeDefinition @'
using System;
using System.Management.Automation;

[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
public class OrAttribute : ArgumentTransformationAttribute {
	private Type[] _types;

	public OrAttribute(params Type[] types) {
		this._types = types ?? System.Type.EmptyTypes;
	}

	private static bool matches(Type wanted, Type inputDataType) {
		if (wanted.IsInterface) {
			foreach (Type ti in inputDataType.GetInterfaces()) {
				if (ti.IsGenericType && wanted == ti.GetGenericTypeDefinition())
					return true;
			}
		} else {
			do {
				if (inputDataType.IsGenericType && wanted == inputDataType.GetGenericTypeDefinition())
					return true;
			} while ((inputDataType = inputDataType.BaseType) != null);
		}
		return false;
	}

	public override Object Transform(EngineIntrinsics engineIntrinsics, Object inputData) {
		Object o = inputData;
		PSObject pso = inputData as PSObject;
		if (pso != null)
			o = (pso != System.Management.Automation.Internal.AutomationNull.Value) ? pso.BaseObject : null;

		if (o == null) {
			foreach (Type t in this._types) {
				if (!t.IsValueType || (t.IsGenericType && t.GetGenericTypeDefinition() == typeof(Nullable<>)))
					return inputData;
			}
		} else {
			Type inputDataType = inputData.GetType();
			foreach (Type t in this._types) {
				if ((t.IsGenericTypeDefinition && matches(t, inputDataType)) || t.IsAssignableFrom(inputDataType))
					return inputData;
			}
		}
		throw new ArgumentException("Value doesn't match any union types");
	}
}
'@
