Resolve #785 - improve API for StringValuesModel

This means that the model:
* Implements IConvertible so that Jint may implicitly convert
  it to other types as if it were just a string.
* The Values & Values properties are perhaps a little less confusing
  now, and present a more consistent set of state, regardless of
  the inner model.
* There's a minor simplification to ToString, since String.Join already
  did what we wanted for one or many values.

Notable though is - because this class implements IConvertible,
it is no longer CLS compliant.  We do not currently mark this assembly
as CLS complient so as things stand that's OK.  I do not know if there
are any plans/expectations for CLS compliance in Elsa.

If we do plan to mark the assemblies as compliant then we must mark this
class [CLSCompliant(false)].
This commit is contained in:
Craig Fowler 2021-03-25 21:10:25 +00:00
parent a4ad46c51d
commit a81d47afff
2 changed files with 169 additions and 22 deletions

View file

@ -1,33 +1,84 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Extensions.Primitives;
namespace Elsa.Activities.Http.Models
{
public class StringValuesModel
/// <summary>
/// Represents a collection of zero or more string values. It is used for values such as
/// HTTP header/query string values. These are often thought-of as key/value pairs but might
/// have more than one value.
/// </summary>
public class StringValuesModel : IConvertible
{
public string? Value { get; set; }
public string[]? Values { get; set; }
/// <summary>
/// Gets either the first value of <see cref="Values"/> or a <see langword="null"/> reference
/// if the values are null or empty.
/// </summary>
/// <returns>The first value</returns>
public string? Value => Values?.FirstOrDefault();
public StringValuesModel()
{
}
/// <summary>
/// Gets a collection of the string values for the current instance.
/// </summary>
/// <value>The values</value>
public string[] Values { get; set; }
public StringValuesModel(StringValues value)
{
Value = value.Count == 1 ? value.ToString() : default;
Values = value.Count != 1 ? value.ToArray() : default;
}
/// <summary>
/// Initializes an instance of <see cref="StringValuesModel"/> with an empty collection of values.
/// </summary>
public StringValuesModel() => Values = new string[0];
public override string? ToString()
{
if (Values == null)
return Value;
/// <summary>
/// Initializes an instance of <see cref="StringValuesModel"/> from a <see cref="StringValues"/>.
/// </summary>
/// <param name="value">A model of zero or more string values.</param>
public StringValuesModel(StringValues value) => Values = value.ToArray();
return Values.Length switch
{
0 => default,
1 => Values[0],
_ => string.Join(",", Values)
};
}
/// <summary>
/// Gets a string representation of the current instance. Either null (for no, or an empty-collection of <see cref="Values"/>),
/// a single string (for a collection of one <see cref="Values"/>) or a comma-separated string of many values.
/// </summary>
/// <returns>The string representation of the <see cref="Values"/>.</returns>
public override string? ToString() => (Values?.Any() == true) ? string.Join(",", Values) : null;
#region IConvertible implementation
TypeCode IConvertible.GetTypeCode() => TypeCode.Object;
bool IConvertible.ToBoolean(IFormatProvider provider) => Convert.ToBoolean(ToString(), provider);
byte IConvertible.ToByte(IFormatProvider provider) => Convert.ToByte(ToString(), provider);
char IConvertible.ToChar(IFormatProvider provider) => Convert.ToChar(ToString(), provider);
DateTime IConvertible.ToDateTime(IFormatProvider provider) => Convert.ToDateTime(ToString(), provider);
decimal IConvertible.ToDecimal(IFormatProvider provider) => Convert.ToDecimal(ToString(), provider);
double IConvertible.ToDouble(IFormatProvider provider) => Convert.ToDouble(ToString(), provider);
short IConvertible.ToInt16(IFormatProvider provider) => Convert.ToInt16(ToString(), provider);
int IConvertible.ToInt32(IFormatProvider provider) => Convert.ToInt32(ToString(), provider);
long IConvertible.ToInt64(IFormatProvider provider) => Convert.ToInt64(ToString(), provider);
sbyte IConvertible.ToSByte(IFormatProvider provider) => Convert.ToSByte(ToString(), provider);
float IConvertible.ToSingle(IFormatProvider provider) => Convert.ToSingle(ToString(), provider);
string IConvertible.ToString(IFormatProvider provider) => ToString()!;
object IConvertible.ToType(Type conversionType, IFormatProvider provider) => Convert.ChangeType(ToString(), conversionType, provider);
ushort IConvertible.ToUInt16(IFormatProvider provider) => Convert.ToUInt16(ToString(), provider);
uint IConvertible.ToUInt32(IFormatProvider provider) => Convert.ToUInt32(ToString(), provider);
ulong IConvertible.ToUInt64(IFormatProvider provider) => Convert.ToUInt64(ToString(), provider);
#endregion
}
}

View file

@ -0,0 +1,96 @@
using System;
using Elsa.Activities.Http.Models;
using Microsoft.Extensions.Primitives;
using Xunit;
namespace Elsa.Activities.Http
{
public class StringValuesModelTests
{
[Theory(DisplayName = "It should be possible to use Convert.ChangeType to convert an instance to string. This is primarily to support usage via Jint."), AutoMoqData]
public void ItShouldBePossibleToConvertAStringValuesModelToString(string[] values)
{
var sut = new StringValuesModel(new StringValues(values));
var result = Convert.ChangeType(sut, typeof(string));
Assert.Equal(sut.ToString(), result);
}
[Theory(DisplayName = "It should be possible to use Convert.ToInt32 to convert an instance to a number if the value is a valid string representation of a number. This is primarily to support usage via Jint."), AutoMoqData]
public void ItShouldBePossibleToConvertAStringValuesModelToInt32IfValid()
{
var sut = new StringValuesModel(new StringValues("3"));
var result = Convert.ChangeType(sut, typeof(int));
Assert.Equal(3, result);
}
[Theory(DisplayName = "The Value property should return the first value if there is more than one."), AutoMoqData]
public void ValueShouldReturnFirstItemIfThereAreMoreThanOne(string firstValue, string secondValue)
{
var sut = new StringValuesModel(new StringValues(new [] { firstValue, secondValue }));
Assert.Equal(firstValue, sut.Value);
}
[Theory(DisplayName = "The Value property should always return the first value if there is only one."), AutoMoqData]
public void ValueShouldReturnFirstItemIfThereIsOne(string firstValue)
{
var sut = new StringValuesModel(new StringValues(new [] { firstValue }));
Assert.Equal(firstValue, sut.Value);
}
[Fact(DisplayName = "The Value property should return null if there are no values.")]
public void ValueShouldReturnNullIfThereAreNone()
{
var sut = new StringValuesModel(new StringValues(new string[0]));
Assert.Null(sut.Value);
}
[Theory(DisplayName = "The Values property should return a length-one collection if there is a single value."), AutoMoqData]
public void ValuesShouldReturnLengthOneCollectionIfThereIsOneValue(string firstValue)
{
var sut = new StringValuesModel(new StringValues(new [] { firstValue }));
Assert.Equal(new [] {firstValue}, sut.Values);
}
[Theory(DisplayName = "The Values property should return a length-two collection if there are two values."), AutoMoqData]
public void ValuesShouldReturnLengthTwoCollectionIfThereAreTwoValues(string firstValue, string secondValue)
{
var sut = new StringValuesModel(new StringValues(new [] { firstValue, secondValue }));
Assert.Equal(new [] {firstValue, secondValue}, sut.Values);
}
[Fact(DisplayName = "The Values property should return an empty collection if there are no values.")]
public void ValuesShouldReturnEmptyCollectionIfThereAreNone()
{
var sut = new StringValuesModel(new StringValues(new string[0]));
Assert.Empty(sut.Values);
}
[Fact(DisplayName = "The ToString method should return null if there are no values")]
public void ToStringShouldReturnNullIfNotValues()
{
var sut = new StringValuesModel(new StringValues(new string[0]));
Assert.Null(sut.ToString());
}
[Theory(DisplayName = "The ToString method should return the first value if there is only one"), AutoMoqData]
public void ToStringShouldReturnFirstValueIfThereIsOnlyOne(string firstValue)
{
var sut = new StringValuesModel(new StringValues(new [] { firstValue }));
Assert.Equal(firstValue, sut.ToString());
}
[Theory(DisplayName = "The ToString method should return comma-separated values if there are more than one"), AutoMoqData]
public void ToStringShouldReturnCommaSeparatedValuesIfThereIsMoreThanOne(string firstValue, string secondValue)
{
var sut = new StringValuesModel(new StringValues(new [] { firstValue, secondValue }));
Assert.Equal($"{firstValue},{secondValue}", sut.ToString());
}
[Fact(DisplayName = "The parameterless constructor should initialize Values to an empty collection")]
public void ParameterlessCtorShouldInitializeWithEmptyCollection()
{
var sut = new StringValuesModel();
Assert.Empty(sut.Values);
}
}
}