Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ISpanParsable on to GrainId #8565

Merged
merged 3 commits into from
Aug 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 60 additions & 11 deletions src/Orleans.Core.Abstractions/IDs/GrainId.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ namespace Orleans.Runtime
/// </summary>
[Serializable, GenerateSerializer, Immutable]
[JsonConverter(typeof(GrainIdJsonConverter))]
public readonly struct GrainId : IEquatable<GrainId>, IComparable<GrainId>, ISerializable, ISpanFormattable
public readonly struct GrainId : IEquatable<GrainId>, IComparable<GrainId>, ISerializable, ISpanFormattable, ISpanParsable<GrainId>
{
[Id(0)]
private readonly GrainType _type;
Expand Down Expand Up @@ -64,36 +64,71 @@ private GrainId(SerializationInfo info, StreamingContext context)
public static GrainId Create(GrainType type, IdSpan key) => new GrainId(type, key);

/// <summary>
/// Creates a new <see cref="GrainType"/> instance.
/// Parses a <see cref="GrainId"/> from the span.
/// </summary>
public static GrainId Parse(string value)
Romanx marked this conversation as resolved.
Show resolved Hide resolved
public static GrainId Parse(ReadOnlySpan<char> value, IFormatProvider? provider = null)
{
if (!TryParse(value, out var result))
if (!TryParse(value, provider, out var result))
{
ThrowInvalidGrainId(value);

static void ThrowInvalidGrainId(string value) => throw new ArgumentException($"Unable to parse \"{value}\" as a grain id");
static void ThrowInvalidGrainId(ReadOnlySpan<char> value) => throw new ArgumentException($"Unable to parse \"{value}\" as a grain id");
}

return result;
}

/// <summary>
/// Creates a new <see cref="GrainType"/> instance.
/// Tries to parse a <see cref="GrainId"/> from the span.
/// </summary>
public static bool TryParse(string? value, out GrainId grainId)
/// <returns><see langword="true"/> if a valid <see cref="GrainId"/> was parsed. <see langword="false"/> otherwise</returns>
public static bool TryParse(ReadOnlySpan<char> value, IFormatProvider? provider, out GrainId result)
{
int i;
if (value is null || (i = value.IndexOf('/')) < 0)
if ((i = value.IndexOf('/')) < 0)
{
grainId = default;
result = default;
return false;
}

grainId = new(new GrainType(Encoding.UTF8.GetBytes(value, 0, i)), new IdSpan(Encoding.UTF8.GetBytes(value, i + 1, value.Length - i - 1)));
var typeSpan = value[0..i];
var type = new byte[Encoding.UTF8.GetByteCount(typeSpan)];
Encoding.UTF8.GetBytes(typeSpan, type);

var idSpan = value[(i + 1)..];
var id = new byte[Encoding.UTF8.GetByteCount(idSpan)];
Encoding.UTF8.GetBytes(idSpan, id);

result = new(new GrainType(type), new IdSpan(id));
return true;
}

/// <summary>
/// Parses a <see cref="GrainId"/> from the string.
/// </summary>
public static GrainId Parse(string value)
=> Parse(value.AsSpan(), null);

/// <summary>
/// Parses a <see cref="GrainId"/> from the string.
/// </summary>
public static GrainId Parse(string value, IFormatProvider? provider = null)
=> Parse(value.AsSpan(), provider);

/// <summary>
/// Tries to parse a <see cref="GrainId"/> from the string.
/// </summary>
/// <returns><see langword="true"/> if a valid <see cref="GrainId"/> was parsed. <see langword="false"/> otherwise</returns>
public static bool TryParse(string? value, out GrainId result)
=> TryParse(value.AsSpan(), null, out result);

/// <summary>
/// Tries to parse a <see cref="GrainId"/> from the string.
/// </summary>
/// <returns><see langword="true"/> if a valid <see cref="GrainId"/> was parsed. <see langword="false"/> otherwise</returns>
public static bool TryParse(string? value, IFormatProvider? provider, out GrainId result)
=> TryParse(value.AsSpan(), provider, out result);

/// <summary>
/// <see langword="true"/> if this instance is the default value, <see langword="false"/> if it is not.
/// </summary>
Expand Down Expand Up @@ -167,7 +202,21 @@ bool ISpanFormattable.TryFormat(Span<char> destination, out int charsWritten, Re
public sealed class GrainIdJsonConverter : JsonConverter<GrainId>
{
/// <inheritdoc />
public override GrainId Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => GrainId.Parse(reader.GetString()!);
public override GrainId Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var valueLength = reader.HasValueSequence
? checked((int)reader.ValueSequence.Length)
: reader.ValueSpan.Length;

Span<char> buf = valueLength <= 128
? (stackalloc char[128])[..valueLength]
: new char[valueLength];

var written = reader.CopyString(buf);
buf = buf[..written];

return GrainId.Parse(buf);
}

/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, GrainId value, JsonSerializerOptions options)
Expand Down
82 changes: 47 additions & 35 deletions test/NonSilo.Tests/General/Identifiertests.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Net;
using System.Text.Json;
using Microsoft.Extensions.DependencyInjection;
using Orleans;
using Orleans.GrainReferences;
Expand Down Expand Up @@ -97,45 +98,56 @@ public void GrainIdShouldEncodeAndDecodePrimaryKeyGuidCorrectly()
}
}

[Fact, TestCategory("SlowBVT"), TestCategory("Identifiers")]
public void GrainId_ToFromPrintableString()
[Theory, TestCategory("SlowBVT"), TestCategory("Identifiers")]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder why we put these (obviously quick) tests in SlowBVT

[MemberData(nameof(TestGrainIds))]
public void GrainId_ToFromPrintableString(GrainId grainId)
{
Guid guid = Guid.NewGuid();
GrainId grainId = GrainId.Create(GrainType.Create("test"), GrainIdKeyExtensions.CreateGuidKey(guid));
GrainId roundTripped = RoundTripGrainIdToParsable(grainId);
Assert.Equal(grainId, roundTripped); // GrainId.ToPrintableString -- Guid key

string extKey = "Guid-ExtKey-1";
guid = Guid.NewGuid();
grainId = GrainId.Create(GrainType.Create("test"), GrainIdKeyExtensions.CreateGuidKey(guid, extKey));
roundTripped = RoundTripGrainIdToParsable(grainId);
Assert.Equal(grainId, roundTripped); // GrainId.ToPrintableString -- Guid key + Extended Key

grainId = GrainId.Create(GrainType.Create("test"), GrainIdKeyExtensions.CreateGuidKey(guid, (string)null));
roundTripped = RoundTripGrainIdToParsable(grainId);
Assert.Equal(grainId, roundTripped); // GrainId.ToPrintableString -- Guid key + null Extended Key

long key = random.Next();
grainId = GrainId.Create(GrainType.Create("test"), GrainIdKeyExtensions.CreateIntegerKey(key));
roundTripped = RoundTripGrainIdToParsable(grainId);
Assert.Equal(grainId, roundTripped); // GrainId.ToPrintableString -- Int64 key

extKey = "Long-ExtKey-2";
key = random.Next();
grainId = GrainId.Create(GrainType.Create("test"), GrainIdKeyExtensions.CreateIntegerKey(key, extKey));
roundTripped = RoundTripGrainIdToParsable(grainId);
Assert.Equal(grainId, roundTripped); // GrainId.ToPrintableString -- Int64 key + Extended Key

key = UniqueKey.NewKey(key).PrimaryKeyToLong();
grainId = GrainId.Create(GrainType.Create("test"), GrainIdKeyExtensions.CreateIntegerKey(key, extKey));
roundTripped = RoundTripGrainIdToParsable(grainId);
Assert.Equal(grainId, roundTripped); // GrainId.ToPrintableString -- Int64 key + null Extended Key
string str = grainId.ToString();
var roundTripped = GrainId.Parse(str);

Assert.Equal(grainId, roundTripped);
}

[Theory, TestCategory("SlowBVT"), TestCategory("Identifiers")]
[MemberData(nameof(TestGrainIds))]
public void GrainId_TryParseFromPrintableString(GrainId grainId)
{
string str = grainId.ToString();
var success = GrainId.TryParse(str, out var roundTripped);

Assert.True(success);
Assert.Equal(grainId, roundTripped);
}

private GrainId RoundTripGrainIdToParsable(GrainId input)
[Theory, TestCategory("SlowBVT"), TestCategory("Identifiers")]
[MemberData(nameof(TestGrainIds))]
public void GrainId_RoundTripJsonConverter(GrainId grainId)
{
string str = input.ToString();
return GrainId.Parse(str);
var serialized = JsonSerializer.Serialize(grainId);
var deserialized = JsonSerializer.Deserialize<GrainId>(serialized);

Assert.Equal(grainId, deserialized);
}

public static TheoryData<GrainId> TestGrainIds
{
get
{
var td = new TheoryData<GrainId>();
var grainType = GrainType.Create("test");
var guid = Guid.NewGuid();
var integer = Random.Shared.NextInt64();

td.Add(GrainId.Create(grainType, GrainIdKeyExtensions.CreateGuidKey(guid)));
td.Add(GrainId.Create(grainType, GrainIdKeyExtensions.CreateGuidKey(guid, "Guid-ExtKey-1")));
td.Add(GrainId.Create(grainType, GrainIdKeyExtensions.CreateGuidKey(guid, (string)null)));
td.Add(GrainId.Create(grainType, GrainIdKeyExtensions.CreateIntegerKey(integer)));
td.Add(GrainId.Create(grainType, GrainIdKeyExtensions.CreateIntegerKey(integer, "Long-ExtKey-2")));
td.Add(GrainId.Create(grainType, GrainIdKeyExtensions.CreateIntegerKey(integer, (string)null)));
td.Add(GrainId.Create(grainType, GrainIdKeyExtensions.CreateIntegerKey(UniqueKey.NewKey(integer).PrimaryKeyToLong(), "Long-ExtKey-2")));

return td;
}
}

[Fact, TestCategory("BVT"), TestCategory("Identifiers")]
Expand Down
Loading