diff --git a/src/DataCore.Adapter.Core/Common/ByteString.cs b/src/DataCore.Adapter.Core/Common/ByteString.cs new file mode 100644 index 00000000..d59110a2 --- /dev/null +++ b/src/DataCore.Adapter.Core/Common/ByteString.cs @@ -0,0 +1,148 @@ +using System; +using System.Runtime.InteropServices; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace DataCore.Adapter.Common { + + /// + /// represents an immutable sequence of bytes. + /// + [JsonConverter(typeof(ByteStringConverter))] + public readonly struct ByteString : IEquatable { + + /// + /// An empty instance. + /// + public static ByteString Empty => default; + + /// + /// The underlying byte sequence. + /// + public ReadOnlyMemory Bytes { get; } + + /// + /// The length of the byte sequence. + /// + public int Length => Bytes.Length; + + /// + /// Specifies if the byte sequence is empty. + /// + public bool IsEmpty => Bytes.IsEmpty; + + + /// + /// Creates a new instance. + /// + /// + /// The byte sequence. + /// + public ByteString(ReadOnlyMemory bytes) { + Bytes = bytes; + } + + + /// + /// Creates a new instance. + /// + /// + /// The byte sequence. + /// + public ByteString(byte[] bytes) { + Bytes = bytes ?? Array.Empty(); + } + + + /// + public override string ToString() { + if (Bytes.IsEmpty) { + return string.Empty; + } + + if (MemoryMarshal.TryGetArray(Bytes, out ArraySegment segment)) { + return Convert.ToBase64String(segment.Array!, segment.Offset, segment.Count); + } + else { + return Convert.ToBase64String(Bytes.ToArray()); + } + } + + + /// + public override int GetHashCode() { + // We need to calculate a hash code that distributes evenly across a hash space, but + // we don't want to have to iterate over the entire byte sequence to do so. Therefore, + // we will compute a hash code based on the following criteria: + // + // * Length of the byte sequence + // * First byte (non-empty byte sequences only) + // * Middle byte (non-empty byte sequences only) + // * Last byte (non-empty byte sequences only) + + if (IsEmpty) { + return HashCode.Combine(0); + } + + return HashCode.Combine(Length, Bytes.Span[0], Bytes.Span[(Bytes.Span.Length - 1) / 2], Bytes.Span[Bytes.Span.Length - 1]); + } + + + /// + public override bool Equals(object obj) { + return obj is ByteString other && Equals(other); + } + + + /// + public bool Equals(ByteString other) { + return Length == other.Length && Bytes.Span.SequenceEqual(other.Bytes.Span); + } + + + /// + public static implicit operator ByteString(ReadOnlyMemory bytes) => new ByteString(bytes); + + /// + public static implicit operator ByteString(byte[] bytes) => new ByteString(bytes); + + /// + public static implicit operator ReadOnlyMemory(ByteString bytes) => bytes.Bytes; + + /// + public static implicit operator byte[](ByteString bytes) => bytes.Bytes.ToArray(); + + } + + + /// + /// JSON converter for . + /// + internal sealed class ByteStringConverter : JsonConverter { + + /// + public override ByteString Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { + if (reader.TokenType == JsonTokenType.Null) { + return ByteString.Empty; + } + + if (reader.TokenType != JsonTokenType.String) { + throw new JsonException(); + } + + return new ByteString(reader.GetBytesFromBase64()); + } + + + /// + public override void Write(Utf8JsonWriter writer, ByteString value, JsonSerializerOptions options) { + if (value.IsEmpty) { + writer.WriteNullValue(); + return; + } + writer.WriteBase64StringValue(value.Bytes.Span); + } + + } + +} diff --git a/src/DataCore.Adapter.Core/Common/Variant.Operators.cs b/src/DataCore.Adapter.Core/Common/Variant.Operators.cs index 9e9187f5..e35db444 100644 --- a/src/DataCore.Adapter.Core/Common/Variant.Operators.cs +++ b/src/DataCore.Adapter.Core/Common/Variant.Operators.cs @@ -56,6 +56,19 @@ partial struct Variant { public static explicit operator byte[]?(Variant val) => (byte[]?) val.Value; + /// + public static implicit operator Variant(ByteString val) => new Variant(val); + + /// + public static explicit operator ByteString(Variant val) => val.Value == null ? default : (ByteString) val.Value; + + /// + public static implicit operator Variant(ByteString[]? val) => new Variant(val); + + /// + public static explicit operator ByteString[]?(Variant val) => (ByteString[]?) val.Value; + + /// public static implicit operator Variant(short val) => new Variant(val); diff --git a/src/DataCore.Adapter.Core/Common/Variant.cs b/src/DataCore.Adapter.Core/Common/Variant.cs index 2d87037a..3130e2e6 100644 --- a/src/DataCore.Adapter.Core/Common/Variant.cs +++ b/src/DataCore.Adapter.Core/Common/Variant.cs @@ -19,6 +19,7 @@ public partial struct Variant : IEquatable, IFormattable { public static IReadOnlyDictionary VariantTypeMap { get; } = new System.Collections.ObjectModel.ReadOnlyDictionary(new Dictionary() { [typeof(bool)] = VariantType.Boolean, [typeof(byte)] = VariantType.Byte, + [typeof(ByteString)] = VariantType.ByteString, [typeof(DateTime)] = VariantType.DateTime, [typeof(double)] = VariantType.Double, [typeof(EncodedObject)] = VariantType.ExtensionObject, @@ -283,6 +284,43 @@ public Variant(byte[]? value) { } + /// + /// Creates a new instance with the specified value. + /// + /// + /// The value. + /// + public Variant(ByteString value) { + Value = value; + Type = VariantType.ByteString; + ArrayDimensions = null; + } + + + /// + /// Creates a new instance with the specified array value. + /// + /// + /// The array value. + /// + /// + /// If is , the + /// will be equal to . + /// + public Variant(ByteString[]? value) { + if (value == null) { + Value = null; + Type = VariantType.Null; + ArrayDimensions = null; + return; + } + + Value = value; + Type = VariantType.ByteString; + ArrayDimensions = GetArrayDimensions(value); + } + + /// /// Creates a new instance with the specified value. /// @@ -1085,7 +1123,6 @@ public string ToString(string? format, IFormatProvider? formatProvider) { if (Value is string s) { return s; } - if (Value is Array a) { return a.ToString(); } @@ -1189,6 +1226,10 @@ public override Variant Read(ref Utf8JsonReader reader, Type typeToConvert, Json return isArray ? new Variant(JsonExtensions.ReadArray(valueElement, arrayDimensions!, options)) : valueElement.Deserialize(options); + case VariantType.ByteString: + return isArray + ? new Variant(JsonExtensions.ReadArray(valueElement, arrayDimensions!, options)) + : valueElement.Deserialize(options); case VariantType.DateTime: return isArray ? new Variant(JsonExtensions.ReadArray(valueElement, arrayDimensions!, options)) diff --git a/src/DataCore.Adapter.Core/Common/VariantType.cs b/src/DataCore.Adapter.Core/Common/VariantType.cs index 060de858..b9cb966b 100644 --- a/src/DataCore.Adapter.Core/Common/VariantType.cs +++ b/src/DataCore.Adapter.Core/Common/VariantType.cs @@ -5,7 +5,6 @@ namespace DataCore.Adapter.Common { /// /// Describes the type of a variant value. /// - [System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1720:Identifier contains type name", Justification = "Enum members all refer to data types")] [JsonConverter(typeof(JsonStringEnumConverter))] public enum VariantType { @@ -102,7 +101,12 @@ public enum VariantType { /// /// JSON /// - Json = 18 + Json = 18, + + /// + /// An immutable byte sequence. + /// + ByteString = 19, } diff --git a/src/DataCore.Adapter.Core/Json/AdapterJsonConverter.cs b/src/DataCore.Adapter.Core/Json/AdapterJsonConverter.cs index 8c63b710..bb9e5a5e 100644 --- a/src/DataCore.Adapter.Core/Json/AdapterJsonConverter.cs +++ b/src/DataCore.Adapter.Core/Json/AdapterJsonConverter.cs @@ -13,13 +13,6 @@ namespace DataCore.Adapter.Json { /// internal abstract class AdapterJsonConverter : JsonConverter { - /// - /// A flag indicating if is serliazed/deserialized as a JSON - /// object. - /// - protected virtual bool SerializeAsObject { get; set; } = true; - - /// /// Throws a to indicate that the JSON structure is invalid. /// diff --git a/src/DataCore.Adapter.Core/PublicAPI.Unshipped.txt b/src/DataCore.Adapter.Core/PublicAPI.Unshipped.txt index 85f172af..924c4d29 100644 --- a/src/DataCore.Adapter.Core/PublicAPI.Unshipped.txt +++ b/src/DataCore.Adapter.Core/PublicAPI.Unshipped.txt @@ -12,12 +12,23 @@ DataCore.Adapter.AssetModel.FindAssetModelNodesRequest.Page.get -> int DataCore.Adapter.AssetModel.FindAssetModelNodesRequest.Page.set -> void DataCore.Adapter.AssetModel.FindAssetModelNodesRequest.PageSize.get -> int DataCore.Adapter.AssetModel.FindAssetModelNodesRequest.PageSize.set -> void +DataCore.Adapter.Common.ByteString +DataCore.Adapter.Common.ByteString.Bytes.get -> System.ReadOnlyMemory +DataCore.Adapter.Common.ByteString.ByteString() -> void +DataCore.Adapter.Common.ByteString.ByteString(byte[]! bytes) -> void +DataCore.Adapter.Common.ByteString.ByteString(System.ReadOnlyMemory bytes) -> void +DataCore.Adapter.Common.ByteString.Equals(DataCore.Adapter.Common.ByteString other) -> bool +DataCore.Adapter.Common.ByteString.IsEmpty.get -> bool +DataCore.Adapter.Common.ByteString.Length.get -> int DataCore.Adapter.Common.FindAdaptersRequest.Page.get -> int DataCore.Adapter.Common.FindAdaptersRequest.Page.set -> void DataCore.Adapter.Common.FindAdaptersRequest.PageSize.get -> int DataCore.Adapter.Common.FindAdaptersRequest.PageSize.set -> void +DataCore.Adapter.Common.Variant.Variant(DataCore.Adapter.Common.ByteString value) -> void +DataCore.Adapter.Common.Variant.Variant(DataCore.Adapter.Common.ByteString[]? value) -> void DataCore.Adapter.Common.Variant.Variant(System.Text.Json.JsonElement value) -> void DataCore.Adapter.Common.Variant.Variant(System.Text.Json.JsonElement[]? value) -> void +DataCore.Adapter.Common.VariantType.ByteString = 19 -> DataCore.Adapter.Common.VariantType DataCore.Adapter.DataValidation.MaxUriLengthAttribute DataCore.Adapter.DataValidation.MaxUriLengthAttribute.Length.get -> int DataCore.Adapter.DataValidation.MaxUriLengthAttribute.MaxUriLengthAttribute(int length) -> void @@ -33,8 +44,20 @@ DataCore.Adapter.Tags.GetTagPropertiesRequest.Page.get -> int DataCore.Adapter.Tags.GetTagPropertiesRequest.Page.set -> void DataCore.Adapter.Tags.GetTagPropertiesRequest.PageSize.get -> int DataCore.Adapter.Tags.GetTagPropertiesRequest.PageSize.set -> void +override DataCore.Adapter.Common.ByteString.Equals(object! obj) -> bool +override DataCore.Adapter.Common.ByteString.GetHashCode() -> int +override DataCore.Adapter.Common.ByteString.ToString() -> string! override DataCore.Adapter.Common.FindAdaptersRequest.Validate(System.ComponentModel.DataAnnotations.ValidationContext! validationContext) -> System.Collections.Generic.IEnumerable! override DataCore.Adapter.DataValidation.MaxUriLengthAttribute.FormatErrorMessage(string! name) -> string! +static DataCore.Adapter.Common.ByteString.Empty.get -> DataCore.Adapter.Common.ByteString +static DataCore.Adapter.Common.ByteString.implicit operator byte[]!(DataCore.Adapter.Common.ByteString bytes) -> byte[]! +static DataCore.Adapter.Common.ByteString.implicit operator DataCore.Adapter.Common.ByteString(byte[]! bytes) -> DataCore.Adapter.Common.ByteString +static DataCore.Adapter.Common.ByteString.implicit operator DataCore.Adapter.Common.ByteString(System.ReadOnlyMemory bytes) -> DataCore.Adapter.Common.ByteString +static DataCore.Adapter.Common.ByteString.implicit operator System.ReadOnlyMemory(DataCore.Adapter.Common.ByteString bytes) -> System.ReadOnlyMemory +static DataCore.Adapter.Common.Variant.explicit operator DataCore.Adapter.Common.ByteString(DataCore.Adapter.Common.Variant val) -> DataCore.Adapter.Common.ByteString +static DataCore.Adapter.Common.Variant.explicit operator DataCore.Adapter.Common.ByteString[]?(DataCore.Adapter.Common.Variant val) -> DataCore.Adapter.Common.ByteString[]? +static DataCore.Adapter.Common.Variant.implicit operator DataCore.Adapter.Common.Variant(DataCore.Adapter.Common.ByteString val) -> DataCore.Adapter.Common.Variant +static DataCore.Adapter.Common.Variant.implicit operator DataCore.Adapter.Common.Variant(DataCore.Adapter.Common.ByteString[]? val) -> DataCore.Adapter.Common.Variant static DataCore.Adapter.Common.VariantExtensions.IsFloatingPointNumericType(this DataCore.Adapter.Common.Variant variant) -> bool static DataCore.Adapter.Common.VariantExtensions.IsFloatingPointNumericType(this DataCore.Adapter.Common.VariantType variantType) -> bool static DataCore.Adapter.RealTimeData.TagValueExtensions.IsFloatingPointNumericType(this DataCore.Adapter.RealTimeData.TagValue! value) -> bool diff --git a/src/DataCore.Adapter.Json.Newtonsoft/ByteStringConverter.cs b/src/DataCore.Adapter.Json.Newtonsoft/ByteStringConverter.cs new file mode 100644 index 00000000..0021f927 --- /dev/null +++ b/src/DataCore.Adapter.Json.Newtonsoft/ByteStringConverter.cs @@ -0,0 +1,39 @@ +using System; + +using DataCore.Adapter.Common; + +using Newtonsoft.Json; + +namespace DataCore.Adapter.NewtonsoftJson { + + /// + /// JSON converter for . + /// + public class ByteStringConverter : JsonConverter { + + /// + public override void WriteJson(JsonWriter writer, ByteString value, JsonSerializer serializer) { + if (value.IsEmpty) { + writer.WriteNull(); + return; + } + + writer.WriteValue(value.Bytes.ToArray()); + } + + + /// + public override ByteString ReadJson(JsonReader reader, Type objectType, ByteString existingValue, bool hasExistingValue, JsonSerializer serializer) { + if (reader.TokenType == JsonToken.Null) { + return ByteString.Empty; + } + + if (reader.TokenType != JsonToken.Bytes) { + throw new JsonException(); + } + + return new ByteString(reader.ReadAsBytes()!); + } + + } +} diff --git a/src/DataCore.Adapter.Json.Newtonsoft/JsonSerializerSettingsExtensions.cs b/src/DataCore.Adapter.Json.Newtonsoft/JsonSerializerSettingsExtensions.cs index 0ed167f4..21e8a811 100644 --- a/src/DataCore.Adapter.Json.Newtonsoft/JsonSerializerSettingsExtensions.cs +++ b/src/DataCore.Adapter.Json.Newtonsoft/JsonSerializerSettingsExtensions.cs @@ -79,6 +79,7 @@ public static void AddDataCoreAdapterConverters(this ICollection converters.Add(new JsonElementConverter(jsonElementConverterOptions)); converters.Add(new NullableJsonElementConverter(jsonElementConverterOptions)); converters.Add(new VariantConverter()); + converters.Add(new ByteStringConverter()); } } diff --git a/src/DataCore.Adapter.Json.Newtonsoft/PublicAPI.Unshipped.txt b/src/DataCore.Adapter.Json.Newtonsoft/PublicAPI.Unshipped.txt index 957af52f..74a348b8 100644 --- a/src/DataCore.Adapter.Json.Newtonsoft/PublicAPI.Unshipped.txt +++ b/src/DataCore.Adapter.Json.Newtonsoft/PublicAPI.Unshipped.txt @@ -1,5 +1,9 @@ #nullable enable +DataCore.Adapter.NewtonsoftJson.ByteStringConverter +DataCore.Adapter.NewtonsoftJson.ByteStringConverter.ByteStringConverter() -> void DataCore.Adapter.NewtonsoftJson.JsonElementConverter.JsonElementConverter(System.Text.Json.JsonSerializerOptions? options) -> void DataCore.Adapter.NewtonsoftJson.NullableJsonElementConverter.NullableJsonElementConverter(System.Text.Json.JsonSerializerOptions? options) -> void +override DataCore.Adapter.NewtonsoftJson.ByteStringConverter.ReadJson(Newtonsoft.Json.JsonReader! reader, System.Type! objectType, DataCore.Adapter.Common.ByteString existingValue, bool hasExistingValue, Newtonsoft.Json.JsonSerializer! serializer) -> DataCore.Adapter.Common.ByteString +override DataCore.Adapter.NewtonsoftJson.ByteStringConverter.WriteJson(Newtonsoft.Json.JsonWriter! writer, DataCore.Adapter.Common.ByteString value, Newtonsoft.Json.JsonSerializer! serializer) -> void static DataCore.Adapter.NewtonsoftJson.JsonSerializerSettingsExtensions.AddDataCoreAdapterConverters(this Newtonsoft.Json.JsonSerializerSettings! settings, System.Text.Json.JsonSerializerOptions? jsonElementConverterOptions) -> void static DataCore.Adapter.NewtonsoftJson.JsonSerializerSettingsExtensions.AddDataCoreAdapterConverters(this System.Collections.Generic.ICollection! converters, System.Text.Json.JsonSerializerOptions? jsonElementConverterOptions) -> void diff --git a/src/DataCore.Adapter.Json.Newtonsoft/VariantConverter.cs b/src/DataCore.Adapter.Json.Newtonsoft/VariantConverter.cs index efa30cae..df56e3de 100644 --- a/src/DataCore.Adapter.Json.Newtonsoft/VariantConverter.cs +++ b/src/DataCore.Adapter.Json.Newtonsoft/VariantConverter.cs @@ -82,6 +82,10 @@ public override Variant ReadJson(JsonReader reader, Type objectType, Variant exi return isArray ? new Variant(ReadArray(valueToken, arrayDimensions!, serializer)) : new Variant(valueToken.ToObject()); + case VariantType.ByteString: + return isArray + ? new Variant(ReadArray(valueToken, arrayDimensions!, serializer)) + : new Variant(valueToken.ToObject()); case VariantType.DateTime: return isArray ? new Variant(ReadArray(valueToken, arrayDimensions!, serializer)) diff --git a/test/DataCore.Adapter.Tests/JsonTests.cs b/test/DataCore.Adapter.Tests/JsonTests.cs index 7a508330..2745de50 100644 --- a/test/DataCore.Adapter.Tests/JsonTests.cs +++ b/test/DataCore.Adapter.Tests/JsonTests.cs @@ -101,6 +101,22 @@ public void Variant_ByteShouldRoundTrip(params byte[] values) { } + [DataTestMethod] + [DataRow(false, byte.MinValue)] + [DataRow(false, byte.MaxValue)] + [DataRow(false, byte.MinValue, byte.MaxValue)] + [DataRow(true, byte.MinValue, byte.MaxValue)] + public void Variant_ByteStringShouldRoundTrip(bool arrayTest, params byte[] values) { + var options = GetOptions(); + if (arrayTest) { + VariantRoundTripTest(values.Select(x => new ByteString(new[] { x })).ToArray(), options); + } + else { + VariantRoundTripTest((ByteString) values, options); + } + } + + [TestMethod] public void Variant_DateTimeShouldRoundTrip() { var now = DateTime.UtcNow; diff --git a/test/DataCore.Adapter.Tests/VariantTests.cs b/test/DataCore.Adapter.Tests/VariantTests.cs index 59996554..9823c082 100644 --- a/test/DataCore.Adapter.Tests/VariantTests.cs +++ b/test/DataCore.Adapter.Tests/VariantTests.cs @@ -16,6 +16,7 @@ public class VariantTests : TestsBase { [DataTestMethod] [DataRow(typeof(bool), typeof(bool[]), typeof(bool[,]), typeof(bool[,,]))] [DataRow(typeof(byte), typeof(byte[]), typeof(byte[,]), typeof(byte[,,]))] + [DataRow(typeof(ByteString), typeof(ByteString[]), typeof(ByteString[,]), typeof(ByteString[,,]))] [DataRow(typeof(DateTime), typeof(DateTime[]), typeof(DateTime[,]), typeof(DateTime[,,]))] [DataRow(typeof(double), typeof(double[]), typeof(double[,]), typeof(double[,,]))] [DataRow(typeof(EncodedObject), typeof(EncodedObject[]), typeof(EncodedObject[,]), typeof(EncodedObject[,,]))] @@ -102,7 +103,7 @@ public void VariantShouldAllowImplicitConversionFromByte() { [TestMethod] public void VariantShouldAllowImplicitConversionFromByteArray() { - byte[] value = new byte [] { 255, 254 }; + byte[] value = new byte[] { 255, 254 }; Variant variant = value; ValidateVariant(variant, VariantType.Byte, value, new[] { value.Length }); } @@ -126,6 +127,46 @@ public void VariantShouldAllowExplicitConversionToByteArray() { } + [TestMethod] + public void VariantShouldAllowImplicitConversionFromByteString() { + ByteString value = new byte[] { 255, 254 }; + Variant variant = value; + ValidateVariant(variant, VariantType.ByteString, value, null); + } + + + [TestMethod] + public void VariantShouldAllowImplicitConversionFromByteStringArray() { + ByteString[] value = { + (ByteString) new byte[] { 255, 254 }, + (ByteString) new byte[] { 128, 127 } + }; + Variant variant = value; + ValidateVariant(variant, VariantType.ByteString, value, new[] { value.Length }); + } + + + [TestMethod] + public void VariantShouldAllowExplicitConversionToByteString() { + ByteString value = new byte[] { 255, 254 }; + Variant variant = value; + var actualValue = (ByteString) variant; + Assert.AreEqual(value, actualValue); + } + + + [TestMethod] + public void VariantShouldAllowExplicitConversionToByteStringArray() { + ByteString[] value = { + (ByteString) new byte[] { 255, 254 }, + (ByteString) new byte[] { 128, 127 } + }; + Variant variant = value; + var actualValue = (ByteString[]) variant; + Assert.IsTrue(value.SequenceEqual(actualValue)); + } + + [TestMethod] public void VariantShouldAllowImplicitConversionFromDateTime() { var value = DateTime.UtcNow;