From a81d47affffbb6c0e7442d6ce264f1e43f19dda1 Mon Sep 17 00:00:00 2001 From: Craig Fowler Date: Thu, 25 Mar 2021 21:10:25 +0000 Subject: [PATCH] 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)]. --- .../Models/StringValuesModel.cs | 95 +++++++++++++----- .../Activities/Http/StringValuesModelTests.cs | 96 +++++++++++++++++++ 2 files changed, 169 insertions(+), 22 deletions(-) create mode 100644 test/unit/Elsa.UnitTests/Activities/Http/StringValuesModelTests.cs diff --git a/src/activities/Elsa.Activities.Http/Models/StringValuesModel.cs b/src/activities/Elsa.Activities.Http/Models/StringValuesModel.cs index 71fe2125b..c9fb296e4 100644 --- a/src/activities/Elsa.Activities.Http/Models/StringValuesModel.cs +++ b/src/activities/Elsa.Activities.Http/Models/StringValuesModel.cs @@ -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 + /// + /// 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. + /// + public class StringValuesModel : IConvertible { - public string? Value { get; set; } - public string[]? Values { get; set; } + /// + /// Gets either the first value of or a reference + /// if the values are null or empty. + /// + /// The first value + public string? Value => Values?.FirstOrDefault(); - public StringValuesModel() - { - } + /// + /// Gets a collection of the string values for the current instance. + /// + /// The values + public string[] Values { get; set; } - public StringValuesModel(StringValues value) - { - Value = value.Count == 1 ? value.ToString() : default; - Values = value.Count != 1 ? value.ToArray() : default; - } + /// + /// Initializes an instance of with an empty collection of values. + /// + public StringValuesModel() => Values = new string[0]; - public override string? ToString() - { - if (Values == null) - return Value; + /// + /// Initializes an instance of from a . + /// + /// A model of zero or more string values. + public StringValuesModel(StringValues value) => Values = value.ToArray(); - return Values.Length switch - { - 0 => default, - 1 => Values[0], - _ => string.Join(",", Values) - }; - } + /// + /// Gets a string representation of the current instance. Either null (for no, or an empty-collection of ), + /// a single string (for a collection of one ) or a comma-separated string of many values. + /// + /// The string representation of the . + 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 } } \ No newline at end of file diff --git a/test/unit/Elsa.UnitTests/Activities/Http/StringValuesModelTests.cs b/test/unit/Elsa.UnitTests/Activities/Http/StringValuesModelTests.cs new file mode 100644 index 000000000..c61ef147b --- /dev/null +++ b/test/unit/Elsa.UnitTests/Activities/Http/StringValuesModelTests.cs @@ -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); + } + } +} \ No newline at end of file