using System; using System.Collections.Generic; using System.ComponentModel; using System.Reflection; using System.Runtime.Serialization; using GitHub.Services.WebApi; using Newtonsoft.Json; namespace GitHub.DistributedTask.Pipelines { /// /// Provides a mechanism for performing delayed evaluation of a value based on the environment context as runtime. /// [EditorBrowsable(EditorBrowsableState.Never)] public abstract class ExpressionValue { public static Boolean IsExpression(String value) { return !String.IsNullOrEmpty(value) && value.Length > 3 && value.StartsWith("$[", StringComparison.Ordinal) && value.EndsWith("]", StringComparison.Ordinal); } /// /// Attempts to parse the specified string as an expression value. /// /// The expected type of the expression result /// The expression string /// The value which was parsed, if any /// True if the value was successfully parsed; otherwise, false public static Boolean TryParse( String expression, out ExpressionValue value) { if (IsExpression(expression)) { value = new ExpressionValue(expression, isExpression: true); } else { value = null; } return value != null; } /// /// Creates an ExpressionValue from expression string. /// Returns null if argument is not an expression /// public static ExpressionValue FromExpression(String expression) { return new ExpressionValue(expression, isExpression: true); } /// /// Creates an ExpressionValue from literal. /// public static ExpressionValue FromLiteral(T literal) { return new ExpressionValue(literal); } /// /// When T is String, we cannot distiguish between literals and expressions solely by type. /// Use this function when parsing and you want to err on the side of expressions. /// public static ExpressionValue FromToken(String token) { if (ExpressionValue.IsExpression(token)) { return ExpressionValue.FromExpression(token); } return ExpressionValue.FromLiteral(token); } internal static String TrimExpression(String value) { var expression = value.Substring(2, value.Length - 3).Trim(); if (String.IsNullOrEmpty(expression)) { throw new ArgumentException(PipelineStrings.ExpressionInvalid(value)); } return expression; } } /// /// Provides a mechanism for performing delayed evaluation of a value based on the environment context at runtime. /// /// The type of value [DataContract] [EditorBrowsable(EditorBrowsableState.Never)] public sealed class ExpressionValue : ExpressionValue, IEquatable> { /// /// Initializes a new ExpressionValue instance with the specified literal value. /// /// The literal value which should be used public ExpressionValue(T literalValue) { m_literalValue = literalValue; } /// /// Initializes a new ExpressionValue with the given expression. /// Throws if expression is invalid. /// /// The expression to be used /// This parameter is unused other than to discriminate this constructor from the literal constructor internal ExpressionValue( String expression, Boolean isExpression) { if (!IsExpression(expression)) { throw new ArgumentException(PipelineStrings.ExpressionInvalid(expression)); } m_expression = ExpressionValue.TrimExpression(expression); } [JsonConstructor] private ExpressionValue() { } internal T Literal { get { return m_literalValue; } } internal String Expression { get { return m_expression; } } /// /// Gets a value indicating whether or not the expression is backed by a literal value. /// internal Boolean IsLiteral => String.IsNullOrEmpty(m_expression); /// /// Converts the value to a string representation. /// /// A string representation of the current value public override String ToString() { if (!String.IsNullOrEmpty(m_expression)) { return String.Concat("$[ ", m_expression, " ]"); } else { return m_literalValue?.ToString(); } } /// /// Provides automatic conversion of a literal value into a pipeline value for convenience. /// /// The value which the pipeline value represents public static implicit operator ExpressionValue(T value) { return new ExpressionValue(value); } public Boolean Equals(ExpressionValue rhs) { if (rhs is null) { return false; } if (ReferenceEquals(this, rhs)) { return true; } if (IsLiteral) { return EqualityComparer.Default.Equals(this.Literal, rhs.Literal); } else { return this.Expression == rhs.Expression; } } public override Boolean Equals(object obj) { return Equals(obj as ExpressionValue); } public static Boolean operator ==(ExpressionValue lhs, ExpressionValue rhs) { if (lhs is null) { return rhs is null; } return lhs.Equals(rhs); } public static Boolean operator !=(ExpressionValue lhs, ExpressionValue rhs) { return !(lhs == rhs); } public override Int32 GetHashCode() { if (IsLiteral) { if (Literal != null) { return Literal.GetHashCode(); } } else if (Expression != null) { return Expression.GetHashCode(); } return 0; // unspecified expression values are all the same. } [DataMember(Name = "LiteralValue", EmitDefaultValue = false)] private readonly T m_literalValue; [DataMember(Name = "VariableValue", EmitDefaultValue = false)] private readonly String m_expression; } internal class ExpressionValueJsonConverter : VssSecureJsonConverter { public override Boolean CanConvert(Type objectType) { return objectType.GetTypeInfo().Equals(typeof(String).GetTypeInfo()) || typeof(T).GetTypeInfo().IsAssignableFrom(objectType.GetTypeInfo()); } public override Object ReadJson( JsonReader reader, Type objectType, Object existingValue, JsonSerializer serializer) { if (reader.TokenType == JsonToken.String) { // string types are either expressions of any type T, or literals of type String var s = (String)(Object)reader.Value; if (ExpressionValue.IsExpression(s)) { return ExpressionValue.FromExpression(s); } else { return new ExpressionValue(s); } } else { var parsedValue = serializer.Deserialize(reader); return new ExpressionValue(parsedValue); } } public override void WriteJson( JsonWriter writer, Object value, JsonSerializer serializer) { base.WriteJson(writer, value, serializer); if (value is ExpressionValue expressionValue) { if (!String.IsNullOrEmpty(expressionValue.Expression)) { serializer.Serialize(writer, $"$[ {expressionValue.Expression} ]"); } else { serializer.Serialize(writer, expressionValue.Literal); } } } } }