Skip to content

Commit

Permalink
Add support for ByteString data type (#371)
Browse files Browse the repository at this point in the history
`ByteString` represents an immutable sequence of bytes.

This commit adds support for creating `ByteString` instances from `byte[]` or `ReadOnlyMemory<byte>`, automatic conversion to/from `Variant`, and JSON serializer support.
  • Loading branch information
wazzamatazz authored Jan 17, 2024
1 parent 8947c6a commit 97ecec8
Show file tree
Hide file tree
Showing 12 changed files with 338 additions and 11 deletions.
148 changes: 148 additions & 0 deletions src/DataCore.Adapter.Core/Common/ByteString.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
using System;
using System.Runtime.InteropServices;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace DataCore.Adapter.Common {

/// <summary>
/// <see cref="ByteString"/> represents an immutable sequence of bytes.
/// </summary>
[JsonConverter(typeof(ByteStringConverter))]
public readonly struct ByteString : IEquatable<ByteString> {

/// <summary>
/// An empty <see cref="ByteString"/> instance.
/// </summary>
public static ByteString Empty => default;

/// <summary>
/// The underlying byte sequence.
/// </summary>
public ReadOnlyMemory<byte> Bytes { get; }

/// <summary>
/// The length of the byte sequence.
/// </summary>
public int Length => Bytes.Length;

/// <summary>
/// Specifies if the byte sequence is empty.
/// </summary>
public bool IsEmpty => Bytes.IsEmpty;


/// <summary>
/// Creates a new <see cref="ByteString"/> instance.
/// </summary>
/// <param name="bytes">
/// The byte sequence.
/// </param>
public ByteString(ReadOnlyMemory<byte> bytes) {
Bytes = bytes;
}


/// <summary>
/// Creates a new <see cref="ByteString"/> instance.
/// </summary>
/// <param name="bytes">
/// The byte sequence.
/// </param>
public ByteString(byte[] bytes) {
Bytes = bytes ?? Array.Empty<byte>();
}


/// <inheritdoc/>
public override string ToString() {
if (Bytes.IsEmpty) {
return string.Empty;
}

if (MemoryMarshal.TryGetArray(Bytes, out ArraySegment<byte> segment)) {
return Convert.ToBase64String(segment.Array!, segment.Offset, segment.Count);
}
else {
return Convert.ToBase64String(Bytes.ToArray());
}
}


/// <inheritdoc/>
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]);
}


/// <inheritdoc/>
public override bool Equals(object obj) {
return obj is ByteString other && Equals(other);
}


/// <inheritdoc/>
public bool Equals(ByteString other) {
return Length == other.Length && Bytes.Span.SequenceEqual(other.Bytes.Span);
}


/// <inheritdoc/>
public static implicit operator ByteString(ReadOnlyMemory<byte> bytes) => new ByteString(bytes);

/// <inheritdoc/>
public static implicit operator ByteString(byte[] bytes) => new ByteString(bytes);

/// <inheritdoc/>
public static implicit operator ReadOnlyMemory<byte>(ByteString bytes) => bytes.Bytes;

/// <inheritdoc/>
public static implicit operator byte[](ByteString bytes) => bytes.Bytes.ToArray();

}


/// <summary>
/// JSON converter for <see cref="ByteString"/>.
/// </summary>
internal sealed class ByteStringConverter : JsonConverter<ByteString> {

/// <inheritdoc/>
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());
}


/// <inheritdoc/>
public override void Write(Utf8JsonWriter writer, ByteString value, JsonSerializerOptions options) {
if (value.IsEmpty) {
writer.WriteNullValue();
return;
}
writer.WriteBase64StringValue(value.Bytes.Span);
}

}

}
13 changes: 13 additions & 0 deletions src/DataCore.Adapter.Core/Common/Variant.Operators.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,19 @@ partial struct Variant {
public static explicit operator byte[]?(Variant val) => (byte[]?) val.Value;


/// <inheritdoc/>
public static implicit operator Variant(ByteString val) => new Variant(val);

/// <inheritdoc/>
public static explicit operator ByteString(Variant val) => val.Value == null ? default : (ByteString) val.Value;

/// <inheritdoc/>
public static implicit operator Variant(ByteString[]? val) => new Variant(val);

/// <inheritdoc/>
public static explicit operator ByteString[]?(Variant val) => (ByteString[]?) val.Value;


/// <inheritdoc/>
public static implicit operator Variant(short val) => new Variant(val);

Expand Down
43 changes: 42 additions & 1 deletion src/DataCore.Adapter.Core/Common/Variant.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public partial struct Variant : IEquatable<Variant>, IFormattable {
public static IReadOnlyDictionary<Type, VariantType> VariantTypeMap { get; } = new System.Collections.ObjectModel.ReadOnlyDictionary<Type, VariantType>(new Dictionary<Type, VariantType>() {
[typeof(bool)] = VariantType.Boolean,
[typeof(byte)] = VariantType.Byte,
[typeof(ByteString)] = VariantType.ByteString,
[typeof(DateTime)] = VariantType.DateTime,
[typeof(double)] = VariantType.Double,
[typeof(EncodedObject)] = VariantType.ExtensionObject,
Expand Down Expand Up @@ -283,6 +284,43 @@ public Variant(byte[]? value) {
}


/// <summary>
/// Creates a new <see cref="Variant"/> instance with the specified value.
/// </summary>
/// <param name="value">
/// The value.
/// </param>
public Variant(ByteString value) {
Value = value;
Type = VariantType.ByteString;
ArrayDimensions = null;
}


/// <summary>
/// Creates a new <see cref="Variant"/> instance with the specified array value.
/// </summary>
/// <param name="value">
/// The array value.
/// </param>
/// <remarks>
/// If <paramref name="value"/> is <see langword="null"/>, the <see cref="Variant"/>
/// will be equal to <see cref="Null"/>.
/// </remarks>
public Variant(ByteString[]? value) {
if (value == null) {
Value = null;
Type = VariantType.Null;
ArrayDimensions = null;
return;
}

Value = value;
Type = VariantType.ByteString;
ArrayDimensions = GetArrayDimensions(value);
}


/// <summary>
/// Creates a new <see cref="Variant"/> instance with the specified value.
/// </summary>
Expand Down Expand Up @@ -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();
}
Expand Down Expand Up @@ -1189,6 +1226,10 @@ public override Variant Read(ref Utf8JsonReader reader, Type typeToConvert, Json
return isArray
? new Variant(JsonExtensions.ReadArray<byte>(valueElement, arrayDimensions!, options))
: valueElement.Deserialize<byte>(options);
case VariantType.ByteString:
return isArray
? new Variant(JsonExtensions.ReadArray<ByteString>(valueElement, arrayDimensions!, options))
: valueElement.Deserialize<ByteString>(options);
case VariantType.DateTime:
return isArray
? new Variant(JsonExtensions.ReadArray<DateTime>(valueElement, arrayDimensions!, options))
Expand Down
8 changes: 6 additions & 2 deletions src/DataCore.Adapter.Core/Common/VariantType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ namespace DataCore.Adapter.Common {
/// <summary>
/// Describes the type of a variant value.
/// </summary>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1720:Identifier contains type name", Justification = "Enum members all refer to data types")]
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum VariantType {

Expand Down Expand Up @@ -102,7 +101,12 @@ public enum VariantType {
/// <summary>
/// JSON
/// </summary>
Json = 18
Json = 18,

/// <summary>
/// An immutable byte sequence.
/// </summary>
ByteString = 19,

}

Expand Down
7 changes: 0 additions & 7 deletions src/DataCore.Adapter.Core/Json/AdapterJsonConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,6 @@ namespace DataCore.Adapter.Json {
/// </typeparam>
internal abstract class AdapterJsonConverter<T> : JsonConverter<T> {

/// <summary>
/// A flag indicating if <typeparamref name="T"/> is serliazed/deserialized as a JSON
/// object.
/// </summary>
protected virtual bool SerializeAsObject { get; set; } = true;


/// <summary>
/// Throws a <see cref="JsonException"/> to indicate that the JSON structure is invalid.
/// </summary>
Expand Down
23 changes: 23 additions & 0 deletions src/DataCore.Adapter.Core/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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<byte>
DataCore.Adapter.Common.ByteString.ByteString() -> void
DataCore.Adapter.Common.ByteString.ByteString(byte[]! bytes) -> void
DataCore.Adapter.Common.ByteString.ByteString(System.ReadOnlyMemory<byte> 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
Expand All @@ -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<System.ComponentModel.DataAnnotations.ValidationResult!>!
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<byte> bytes) -> DataCore.Adapter.Common.ByteString
static DataCore.Adapter.Common.ByteString.implicit operator System.ReadOnlyMemory<byte>(DataCore.Adapter.Common.ByteString bytes) -> System.ReadOnlyMemory<byte>
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
Expand Down
39 changes: 39 additions & 0 deletions src/DataCore.Adapter.Json.Newtonsoft/ByteStringConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using System;

using DataCore.Adapter.Common;

using Newtonsoft.Json;

namespace DataCore.Adapter.NewtonsoftJson {

/// <summary>
/// JSON converter for <see cref="ByteString"/>.
/// </summary>
public class ByteStringConverter : JsonConverter<ByteString> {

/// <inheritdoc/>
public override void WriteJson(JsonWriter writer, ByteString value, JsonSerializer serializer) {
if (value.IsEmpty) {
writer.WriteNull();
return;
}

writer.WriteValue(value.Bytes.ToArray());
}


/// <inheritdoc/>
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()!);
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ public static void AddDataCoreAdapterConverters(this ICollection<JsonConverter>
converters.Add(new JsonElementConverter(jsonElementConverterOptions));
converters.Add(new NullableJsonElementConverter(jsonElementConverterOptions));
converters.Add(new VariantConverter());
converters.Add(new ByteStringConverter());
}

}
Expand Down
4 changes: 4 additions & 0 deletions src/DataCore.Adapter.Json.Newtonsoft/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -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<Newtonsoft.Json.JsonConverter!>! converters, System.Text.Json.JsonSerializerOptions? jsonElementConverterOptions) -> void
4 changes: 4 additions & 0 deletions src/DataCore.Adapter.Json.Newtonsoft/VariantConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,10 @@ public override Variant ReadJson(JsonReader reader, Type objectType, Variant exi
return isArray
? new Variant(ReadArray<byte>(valueToken, arrayDimensions!, serializer))
: new Variant(valueToken.ToObject<byte>());
case VariantType.ByteString:
return isArray
? new Variant(ReadArray<ByteString>(valueToken, arrayDimensions!, serializer))
: new Variant(valueToken.ToObject<ByteString>());
case VariantType.DateTime:
return isArray
? new Variant(ReadArray<DateTime>(valueToken, arrayDimensions!, serializer))
Expand Down
Loading

0 comments on commit 97ecec8

Please sign in to comment.