Closes #4
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace FinalFactory.Rendezvous.Contracts;
|
||||
|
||||
[JsonConverter(typeof(SessionListingIdJsonConverter))]
|
||||
public readonly struct SessionListingId : IEquatable<SessionListingId>
|
||||
{
|
||||
public SessionListingId(Guid value) => Value = GuidIdentifier.RequireNonEmpty(value, nameof(value));
|
||||
public Guid Value { get; }
|
||||
public static bool TryParse(string? value, out SessionListingId id) =>
|
||||
GuidIdentifier.TryParse(value, static guid => new SessionListingId(guid), out id);
|
||||
public bool Equals(SessionListingId other) => Value.Equals(other.Value);
|
||||
public override bool Equals(object? obj) => obj is SessionListingId other && Equals(other);
|
||||
public override int GetHashCode() => Value.GetHashCode();
|
||||
public override string ToString() => Value.ToString("D");
|
||||
public static bool operator ==(SessionListingId left, SessionListingId right) => left.Equals(right);
|
||||
public static bool operator !=(SessionListingId left, SessionListingId right) => !left.Equals(right);
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(LeaseIdJsonConverter))]
|
||||
public readonly struct LeaseId : IEquatable<LeaseId>
|
||||
{
|
||||
public LeaseId(Guid value) => Value = GuidIdentifier.RequireNonEmpty(value, nameof(value));
|
||||
public Guid Value { get; }
|
||||
public static bool TryParse(string? value, out LeaseId id) =>
|
||||
GuidIdentifier.TryParse(value, static guid => new LeaseId(guid), out id);
|
||||
public bool Equals(LeaseId other) => Value.Equals(other.Value);
|
||||
public override bool Equals(object? obj) => obj is LeaseId other && Equals(other);
|
||||
public override int GetHashCode() => Value.GetHashCode();
|
||||
public override string ToString() => Value.ToString("D");
|
||||
public static bool operator ==(LeaseId left, LeaseId right) => left.Equals(right);
|
||||
public static bool operator !=(LeaseId left, LeaseId right) => !left.Equals(right);
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(JoinAttemptIdJsonConverter))]
|
||||
public readonly struct JoinAttemptId : IEquatable<JoinAttemptId>
|
||||
{
|
||||
public JoinAttemptId(Guid value) => Value = GuidIdentifier.RequireNonEmpty(value, nameof(value));
|
||||
public Guid Value { get; }
|
||||
public static bool TryParse(string? value, out JoinAttemptId id) =>
|
||||
GuidIdentifier.TryParse(value, static guid => new JoinAttemptId(guid), out id);
|
||||
public bool Equals(JoinAttemptId other) => Value.Equals(other.Value);
|
||||
public override bool Equals(object? obj) => obj is JoinAttemptId other && Equals(other);
|
||||
public override int GetHashCode() => Value.GetHashCode();
|
||||
public override string ToString() => Value.ToString("D");
|
||||
public static bool operator ==(JoinAttemptId left, JoinAttemptId right) => left.Equals(right);
|
||||
public static bool operator !=(JoinAttemptId left, JoinAttemptId right) => !left.Equals(right);
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(MediationHandleJsonConverter))]
|
||||
public readonly struct MediationHandle : IEquatable<MediationHandle>
|
||||
{
|
||||
public MediationHandle(Guid value) => Value = GuidIdentifier.RequireNonEmpty(value, nameof(value));
|
||||
public Guid Value { get; }
|
||||
public static bool TryParse(string? value, out MediationHandle id) =>
|
||||
GuidIdentifier.TryParse(value, static guid => new MediationHandle(guid), out id);
|
||||
public bool Equals(MediationHandle other) => Value.Equals(other.Value);
|
||||
public override bool Equals(object? obj) => obj is MediationHandle other && Equals(other);
|
||||
public override int GetHashCode() => Value.GetHashCode();
|
||||
public override string ToString() => Value.ToString("D");
|
||||
public static bool operator ==(MediationHandle left, MediationHandle right) => left.Equals(right);
|
||||
public static bool operator !=(MediationHandle left, MediationHandle right) => !left.Equals(right);
|
||||
}
|
||||
|
||||
internal static class GuidIdentifier
|
||||
{
|
||||
public static Guid RequireNonEmpty(Guid value, string parameterName) =>
|
||||
value != Guid.Empty
|
||||
? value
|
||||
: throw new ArgumentException("Opaque identifiers cannot be empty.", parameterName);
|
||||
|
||||
public static bool TryParse<TIdentifier>(
|
||||
string? value,
|
||||
Func<Guid, TIdentifier> factory,
|
||||
out TIdentifier identifier)
|
||||
{
|
||||
if (Guid.TryParseExact(value, "D", out Guid guid) && guid != Guid.Empty)
|
||||
{
|
||||
identifier = factory(guid);
|
||||
return true;
|
||||
}
|
||||
|
||||
identifier = default!;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
internal abstract class GuidIdentifierJsonConverter<TIdentifier> :
|
||||
StringIdentifierJsonConverter<TIdentifier>
|
||||
{
|
||||
protected sealed override string Format(TIdentifier value) => value?.ToString() ?? string.Empty;
|
||||
}
|
||||
|
||||
internal sealed class SessionListingIdJsonConverter : GuidIdentifierJsonConverter<SessionListingId>
|
||||
{
|
||||
protected override SessionListingId Parse(string value) => new(Guid.ParseExact(value, "D"));
|
||||
}
|
||||
|
||||
internal sealed class LeaseIdJsonConverter : GuidIdentifierJsonConverter<LeaseId>
|
||||
{
|
||||
protected override LeaseId Parse(string value) => new(Guid.ParseExact(value, "D"));
|
||||
}
|
||||
|
||||
internal sealed class JoinAttemptIdJsonConverter : GuidIdentifierJsonConverter<JoinAttemptId>
|
||||
{
|
||||
protected override JoinAttemptId Parse(string value) => new(Guid.ParseExact(value, "D"));
|
||||
}
|
||||
|
||||
internal sealed class MediationHandleJsonConverter : GuidIdentifierJsonConverter<MediationHandle>
|
||||
{
|
||||
protected override MediationHandle Parse(string value) => new(Guid.ParseExact(value, "D"));
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace FinalFactory.Rendezvous.Contracts;
|
||||
|
||||
[JsonConverter(typeof(GameIdJsonConverter))]
|
||||
public readonly struct GameId : IEquatable<GameId>
|
||||
{
|
||||
public GameId(string value)
|
||||
{
|
||||
if (!ContractValidation.IsSlug(value, ContractLimits.GameIdMaxCharacters))
|
||||
{
|
||||
throw new ArgumentException("Game IDs must be lowercase URL-safe slugs.", nameof(value));
|
||||
}
|
||||
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public string Value { get; }
|
||||
|
||||
public static bool TryParse(string? value, out GameId gameId)
|
||||
{
|
||||
if (ContractValidation.IsSlug(value, ContractLimits.GameIdMaxCharacters))
|
||||
{
|
||||
gameId = new GameId(value!);
|
||||
return true;
|
||||
}
|
||||
|
||||
gameId = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool Equals(GameId other) => string.Equals(Value, other.Value, StringComparison.Ordinal);
|
||||
public override bool Equals(object? obj) => obj is GameId other && Equals(other);
|
||||
public override int GetHashCode() => StringComparer.Ordinal.GetHashCode(Value ?? string.Empty);
|
||||
public override string ToString() => Value ?? string.Empty;
|
||||
public static bool operator ==(GameId left, GameId right) => left.Equals(right);
|
||||
public static bool operator !=(GameId left, GameId right) => !left.Equals(right);
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(EnvironmentIdJsonConverter))]
|
||||
public readonly struct EnvironmentId : IEquatable<EnvironmentId>
|
||||
{
|
||||
public EnvironmentId(string value)
|
||||
{
|
||||
if (!ContractValidation.IsSlug(value, ContractLimits.EnvironmentIdMaxCharacters))
|
||||
{
|
||||
throw new ArgumentException("Environment IDs must be lowercase URL-safe slugs.", nameof(value));
|
||||
}
|
||||
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public string Value { get; }
|
||||
|
||||
public static bool TryParse(string? value, out EnvironmentId environmentId)
|
||||
{
|
||||
if (ContractValidation.IsSlug(value, ContractLimits.EnvironmentIdMaxCharacters))
|
||||
{
|
||||
environmentId = new EnvironmentId(value!);
|
||||
return true;
|
||||
}
|
||||
|
||||
environmentId = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool Equals(EnvironmentId other) => string.Equals(Value, other.Value, StringComparison.Ordinal);
|
||||
public override bool Equals(object? obj) => obj is EnvironmentId other && Equals(other);
|
||||
public override int GetHashCode() => StringComparer.Ordinal.GetHashCode(Value ?? string.Empty);
|
||||
public override string ToString() => Value ?? string.Empty;
|
||||
public static bool operator ==(EnvironmentId left, EnvironmentId right) => left.Equals(right);
|
||||
public static bool operator !=(EnvironmentId left, EnvironmentId right) => !left.Equals(right);
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(RegionIdJsonConverter))]
|
||||
public readonly struct RegionId : IEquatable<RegionId>
|
||||
{
|
||||
public RegionId(string value)
|
||||
{
|
||||
if (!ContractValidation.IsSlug(value, ContractLimits.RegionIdMaxCharacters))
|
||||
{
|
||||
throw new ArgumentException("Region IDs must be lowercase URL-safe slugs.", nameof(value));
|
||||
}
|
||||
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public string Value { get; }
|
||||
|
||||
public static bool TryParse(string? value, out RegionId regionId)
|
||||
{
|
||||
if (ContractValidation.IsSlug(value, ContractLimits.RegionIdMaxCharacters))
|
||||
{
|
||||
regionId = new RegionId(value!);
|
||||
return true;
|
||||
}
|
||||
|
||||
regionId = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool Equals(RegionId other) => string.Equals(Value, other.Value, StringComparison.Ordinal);
|
||||
public override bool Equals(object? obj) => obj is RegionId other && Equals(other);
|
||||
public override int GetHashCode() => StringComparer.Ordinal.GetHashCode(Value ?? string.Empty);
|
||||
public override string ToString() => Value ?? string.Empty;
|
||||
public static bool operator ==(RegionId left, RegionId right) => left.Equals(right);
|
||||
public static bool operator !=(RegionId left, RegionId right) => !left.Equals(right);
|
||||
}
|
||||
|
||||
internal sealed class GameIdJsonConverter : StringIdentifierJsonConverter<GameId>
|
||||
{
|
||||
protected override GameId Parse(string value) => new(value);
|
||||
protected override string Format(GameId value) => value.Value;
|
||||
}
|
||||
|
||||
internal sealed class EnvironmentIdJsonConverter : StringIdentifierJsonConverter<EnvironmentId>
|
||||
{
|
||||
protected override EnvironmentId Parse(string value) => new(value);
|
||||
protected override string Format(EnvironmentId value) => value.Value;
|
||||
}
|
||||
|
||||
internal sealed class RegionIdJsonConverter : StringIdentifierJsonConverter<RegionId>
|
||||
{
|
||||
protected override RegionId Parse(string value) => new(value);
|
||||
protected override string Format(RegionId value) => value.Value;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace FinalFactory.Rendezvous.Contracts;
|
||||
|
||||
internal abstract class StringIdentifierJsonConverter<TIdentifier> : JsonConverter<TIdentifier>
|
||||
{
|
||||
public sealed override TIdentifier Read(
|
||||
ref Utf8JsonReader reader,
|
||||
Type typeToConvert,
|
||||
JsonSerializerOptions options)
|
||||
{
|
||||
if (reader.TokenType != JsonTokenType.String)
|
||||
{
|
||||
throw new JsonException($"{typeof(TIdentifier).Name} must be a JSON string.");
|
||||
}
|
||||
|
||||
string value = reader.GetString() ?? string.Empty;
|
||||
|
||||
try
|
||||
{
|
||||
return Parse(value);
|
||||
}
|
||||
catch (Exception exception) when (exception is ArgumentException or FormatException)
|
||||
{
|
||||
throw new JsonException($"Invalid {typeof(TIdentifier).Name}.", exception);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed override void Write(
|
||||
Utf8JsonWriter writer,
|
||||
TIdentifier value,
|
||||
JsonSerializerOptions options) => writer.WriteStringValue(Format(value));
|
||||
|
||||
protected abstract TIdentifier Parse(string value);
|
||||
protected abstract string Format(TIdentifier value);
|
||||
}
|
||||
Reference in New Issue
Block a user