Skip to content
Open
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
27 changes: 27 additions & 0 deletions src/TransparentValueObjects.Augments/IHasRandomValueGenerator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using System;

namespace TransparentValueObjects.Augments;

/// <summary>
/// Augment to enable support for random value generation via <see cref="Random"/>.
/// </summary>
/// <typeparam name="TValueObject"></typeparam>
/// <typeparam name="TValue"></typeparam>
/// <typeparam name="TRandom"></typeparam>
public interface IHasRandomValueGenerator<out TValueObject, out TValue, out TRandom>
where TValueObject : IValueObject<TValue>
where TValue : notnull
where TRandom : Random
{
/// <summary>
/// Gets the random source. <see cref="Random.Shared"/> can be used for better performance.
/// </summary>
/// <returns></returns>
public static abstract TRandom GetRandom();

/// <summary>
/// Gets the random object.
/// </summary>
/// <returns></returns>
public static abstract TValueObject NewRandomValue();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using System;

namespace TransparentValueObjects.Augments;

/// <summary>
/// Augment to extend support for random value generation via <see cref="Random"/>.
/// Provides a high performance implementation for <see langword="unmanaged"/> structs.
/// </summary>
/// <typeparam name="TValueObject"></typeparam>
/// <typeparam name="TValue"></typeparam>
/// <typeparam name="TRandom"></typeparam>
public interface IHasUnmanagedRandomValueGenerator<out TValueObject, out TValue, out TRandom> : IHasRandomValueGenerator<TValueObject, TValue, TRandom>
where TValueObject : IValueObject<TValue>
where TValue : unmanaged
where TRandom : Random
{

}
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ private SampleValueObjectGuid(global::System.Guid value)
public override string ToString() => Value.ToString();

public bool Equals(SampleValueObjectGuid other) => Equals(other.Value);
public bool Equals(global::System.Guid other) => Value.Equals(other);
public bool Equals(global::System.Guid other) => InnerValueDefaultEqualityComparer.Equals(Value, other);
public bool Equals(SampleValueObjectGuid other, global::System.Collections.Generic.IEqualityComparer<global::System.Guid> comparer) => comparer.Equals(Value, other.Value);
public override bool Equals(object? obj)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ private SampleValueObjectInt(global::System.Int32 value)
public override string ToString() => Value.ToString();

public bool Equals(SampleValueObjectInt other) => Equals(other.Value);
public bool Equals(global::System.Int32 other) => Value.Equals(other);
public bool Equals(global::System.Int32 other) => InnerValueDefaultEqualityComparer.Equals(Value, other);
public bool Equals(SampleValueObjectInt other, global::System.Collections.Generic.IEqualityComparer<global::System.Int32> comparer) => comparer.Equals(Value, other.Value);
public override bool Equals(object? obj)
{
Expand Down Expand Up @@ -92,6 +92,17 @@ public override void WriteAsPropertyName(global::System.Text.Json.Utf8JsonWriter

}

public static global::System.Random GetRandom() => new global::System.Random();
public static SampleValueObjectInt NewRandomValue()
{
var random = GetRandom();
var size = global::System.Runtime.CompilerServices.Unsafe.SizeOf<global::System.Int32>();
global::System.Span<byte> bytes = stackalloc byte[size];
random.NextBytes(bytes);
var id = global::System.Runtime.InteropServices.MemoryMarshal.Cast<byte, global::System.Int32>(bytes)[0];
return SampleValueObjectInt.From(id);
}

public global::System.Int32 CompareTo(SampleValueObjectInt other) => Value.CompareTo(other);
public static bool operator <(SampleValueObjectInt left, SampleValueObjectInt right) => left.Value.CompareTo(right.Value) < 0;
public static bool operator >(SampleValueObjectInt left, SampleValueObjectInt right) => left.Value.CompareTo(right.Value) > 0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ public override void WriteAsPropertyName(global::System.Text.Json.Utf8JsonWriter

}

public static global::System.Random GetRandom() => new global::System.Random();
public global::System.Int32 CompareTo(SampleValueObjectString other) => Value.CompareTo(other);
public static bool operator <(SampleValueObjectString left, SampleValueObjectString right) => left.Value.CompareTo(right.Value) < 0;
public static bool operator >(SampleValueObjectString left, SampleValueObjectString right) => left.Value.CompareTo(right.Value) > 0;
Expand Down
8 changes: 7 additions & 1 deletion src/TransparentValueObjects.Sample/SampleValueObjectGuid.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using TransparentValueObjects.Augments;
using TransparentValueObjects.Generated;

Expand All @@ -7,7 +8,12 @@ namespace TransparentValueObjects.Sample;
[ValueObject<Guid>]
public readonly partial struct SampleValueObjectGuid :
IHasDefaultValue<SampleValueObjectGuid, Guid>,
IHasSystemTextJsonConverter
IHasDefaultEqualityComparer<SampleValueObjectGuid, Guid>,
IHasSystemTextJsonConverter,
IHasRandomValueGenerator<SampleValueObjectGuid, Guid, Random>
{
public static SampleValueObjectGuid DefaultValue => From(Guid.Empty);
public static IEqualityComparer<Guid> InnerValueDefaultEqualityComparer => EqualityComparer<Guid>.Default;
public static Random GetRandom() => Random.Shared;
public static SampleValueObjectGuid NewRandomValue() => From(Guid.NewGuid());
}
9 changes: 7 additions & 2 deletions src/TransparentValueObjects.Sample/SampleValueObjectInt.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
using TransparentValueObjects.Augments;
using System;
using System.Collections.Generic;
using TransparentValueObjects.Augments;
using TransparentValueObjects.Generated;

namespace TransparentValueObjects.Sample;

[ValueObject<int>]
public readonly partial struct SampleValueObjectInt :
IHasDefaultValue<SampleValueObjectInt, int>,
IHasSystemTextJsonConverter
IHasDefaultEqualityComparer<SampleValueObjectInt, int>,
IHasSystemTextJsonConverter,
IHasUnmanagedRandomValueGenerator<SampleValueObjectInt, int, Random>
{
public static SampleValueObjectInt DefaultValue => From(0);
public static IEqualityComparer<int> InnerValueDefaultEqualityComparer => EqualityComparer<int>.Default;
}
12 changes: 11 additions & 1 deletion src/TransparentValueObjects.Sample/SampleValueObjectString.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,18 @@ namespace TransparentValueObjects.Sample;
public readonly partial struct SampleValueObjectString :
IHasDefaultValue<SampleValueObjectString, string>,
IHasDefaultEqualityComparer<SampleValueObjectString, string>,
IHasSystemTextJsonConverter
IHasSystemTextJsonConverter,
IHasRandomValueGenerator<SampleValueObjectString, string, Random>
{
public static SampleValueObjectString DefaultValue => From("Hello World!");
public static IEqualityComparer<string> InnerValueDefaultEqualityComparer => StringComparer.OrdinalIgnoreCase;
public static SampleValueObjectString NewRandomValue()
{
const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
return From(string.Create(10, GetRandom(), static (span, random) =>
{
for (var i = 0; i < span.Length; i++)
span[i] = chars[random.Next(0, chars.Length)];
}));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ public class ValueObjectIncrementalSourceGenerator : IIncrementalGenerator
private const string HasDefaultValueInterfaceName = "IHasDefaultValue";
private const string HasDefaultEqualityComparerInterfaceName = "IHasDefaultEqualityComparer";
private const string HasSystemTextJsonConverterInterfaceName = "IHasSystemTextJsonConverter";
private const string HasRandomValueGeneratorInterfaceName = "IHasRandomValueGenerator";
private const string HasUnmanagedRandomValueGeneratorInterfaceName = "IHasUnmanagedRandomValueGenerator";

private const string AttributeSourceCode =
$$"""
Expand Down Expand Up @@ -173,6 +175,24 @@ private static void Generate(SourceProductionContext context, Compilation compil
if (hasSystemTextJsonConverter && !hasSystemTextJsonConverterOverride)
AddSystemTextJsonClasses(cw, valueObjectTypeName, innerValueTypeName, hasDefaultValue);

// The NewRandomValue
if (GetAugment(valueObjectInterfaces, HasRandomValueGeneratorInterfaceName) is { TypeArguments.Length: 3 } randomAugmentTypeSymbol)
{
var randomType = randomAugmentTypeSymbol.TypeArguments[2];
var randomTypeName = $"global::{randomType.ContainingNamespace.ToDisplayString()}.{randomType.Name}";
var hasGetRandomOverride = valueObjectNamedTypeSymbol.GetMembers("GetRandom")
.Any(x => x is IMethodSymbol { ReturnType: var ret, Parameters.Length: 0 } && SymbolEqualityComparer.Default.Equals(ret, randomType));
AddRandomValueMethod(cw, randomTypeName, hasGetRandomOverride);
}
if (GetAugment(valueObjectInterfaces, HasUnmanagedRandomValueGeneratorInterfaceName) is { TypeArguments.Length: 3 } uRandomAugmentTypeSymbol)
{
var randomType = uRandomAugmentTypeSymbol.TypeArguments[2];
var randomTypeName = $"global::{randomType.ContainingNamespace.ToDisplayString()}.{randomType.Name}";
var hasGetRandomOverride = valueObjectNamedTypeSymbol.GetMembers("GetRandom")
.Any(x => x is IMethodSymbol { ReturnType: var ret, Parameters.Length: 0 } && SymbolEqualityComparer.Default.Equals(ret, randomType));
AddUnmanagedRandomValueMethod(cw, valueObjectTypeName, innerValueTypeName, randomTypeName, hasGetRandomOverride);
}

if (comparableInterfaceTypeSymbol is not null)
{
ForwardInterface(cw, valueObjectTypeName, comparableInterfaceTypeSymbol);
Expand Down Expand Up @@ -200,6 +220,11 @@ private static void Generate(SourceProductionContext context, Compilation compil
);
}

private static INamedTypeSymbol? GetAugment(ImmutableArray<INamedTypeSymbol> existingInterfaces, string augmentName)
{
return existingInterfaces.FirstOrDefault(x => x.Name == augmentName && x.ContainingNamespace.ToDisplayString() == AugmentedNamespace);
}

private static bool HasAugment(ImmutableArray<INamedTypeSymbol> existingInterfaces, string augmentName)
{
return existingInterfaces.Any(x =>
Expand Down Expand Up @@ -442,6 +467,41 @@ public static void AddSystemTextJsonClasses(CodeWriter cw, string valueObjectTyp
}
}

public static void AddRandomValueMethod(
CodeWriter cw,
string randomTypeName,
bool hasGetRandomOverride)
{
if (!hasGetRandomOverride)
{
cw.AppendLine($"public static {randomTypeName} GetRandom() => new {randomTypeName}();");
}
}

public static void AddUnmanagedRandomValueMethod(
CodeWriter cw,
string valueObjectTypeName,
string innerValueTypeName,
string randomTypeName,
bool hasGetRandomOverride)
{
if (!hasGetRandomOverride)
{
cw.AppendLine($"public static {randomTypeName} GetRandom() => new {randomTypeName}();");
}

cw.AppendLine($"public static {valueObjectTypeName} NewRandomValue()");
using (cw.AddBlock())
{
cw.AppendLine("var random = GetRandom();");
cw.AppendLine($"var size = global::System.Runtime.CompilerServices.Unsafe.SizeOf<{innerValueTypeName}>();");
cw.AppendLine("global::System.Span<byte> bytes = stackalloc byte[size];");
cw.AppendLine("random.NextBytes(bytes);");
cw.AppendLine($"var id = global::System.Runtime.InteropServices.MemoryMarshal.Cast<byte, {innerValueTypeName}>(bytes)[0];");
cw.AppendLine($"return {valueObjectTypeName}.From(id);");
}
}

private readonly struct Target : IEquatable<Target>
{
public readonly StructDeclarationSyntax Syntax;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
using Xunit;

namespace TransparentValueObjects.Tests.ValueObjectIncrementalSourceGeneratorTests.Augments;

public class HasRandomValueGenerator
{
private const string Input =
"""
using TransparentValueObjects.Generated;
using TransparentValueObjects.Augments;

namespace TestNamespace;

[ValueObject<string>]
public readonly partial struct StringValueObject : IHasRandomValueGenerator<SampleValueObjectGuid, Guid, Random>
{
public static StringValueObject DefaultValue => From("Hello World!");
}
""";

private const string Output =
"""
// <auto-generated/>
#nullable enable
namespace TestNamespace;

[global::System.Diagnostics.DebuggerDisplay("{Value}")]
[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage(Justification = "Auto-generated.")]
readonly partial struct StringValueObject :
global::TransparentValueObjects.Augments.IValueObject<global::System.String>,
global::System.IEquatable<StringValueObject>,
global::System.IEquatable<global::System.String>,
global::System.IComparable<StringValueObject>
{
public readonly global::System.String Value;

public static global::System.Type InnerValueType => typeof(global::System.String);

[global::System.Obsolete($"Use StringValueObject.{nameof(From)} instead.", error: true)]
public StringValueObject()
{
throw new global::System.InvalidOperationException($"Use StringValueObject.{nameof(From)} instead.");
}

private StringValueObject(global::System.String value)
{
Value = value;
}

public static StringValueObject From(global::System.String value) => new(value);

public override int GetHashCode() => Value.GetHashCode();

public override string ToString() => Value.ToString();

public bool Equals(StringValueObject other) => Equals(other.Value);
public bool Equals(global::System.String? other) => Value.Equals(other);
public bool Equals(StringValueObject other, global::System.Collections.Generic.IEqualityComparer<global::System.String> comparer) => comparer.Equals(Value, other.Value);
public override bool Equals(object? obj)
{
if (obj is null) return false;
if (obj is StringValueObject value) return Equals(value);
if (obj is global::System.String innerValue) return Equals(innerValue);
return false;
}

public static bool operator ==(StringValueObject left, StringValueObject right) => left.Equals(right);
public static bool operator !=(StringValueObject left, StringValueObject right) => !left.Equals(right);

public static bool operator ==(StringValueObject left, global::System.String right) => left.Equals(right);
public static bool operator !=(StringValueObject left, global::System.String right) => !left.Equals(right);

public static bool operator ==(global::System.String left, StringValueObject right) => right.Equals(left);
public static bool operator !=(global::System.String left, StringValueObject right) => !right.Equals(left);

public static explicit operator StringValueObject(global::System.String value) => From(value);
public static explicit operator global::System.String(StringValueObject value) => value.Value;

public static global::<global namespace>.Random GetRandom() => new global::<global namespace>.Random();
public global::System.Int32 CompareTo(StringValueObject other) => Value.CompareTo(other);
public static bool operator <(StringValueObject left, StringValueObject right) => left.Value.CompareTo(right.Value) < 0;
public static bool operator >(StringValueObject left, StringValueObject right) => left.Value.CompareTo(right.Value) > 0;
public static bool operator <=(StringValueObject left, StringValueObject right) => left.Value.CompareTo(right.Value) <= 0;
public static bool operator >=(StringValueObject left, StringValueObject right) => left.Value.CompareTo(right.Value) >= 0;

public static bool operator <(global::System.String left, StringValueObject right) => left.CompareTo(right.Value) < 0;
public static bool operator >(global::System.String left, StringValueObject right) => left.CompareTo(right.Value) > 0;
public static bool operator <=(global::System.String left, StringValueObject right) => left.CompareTo(right.Value) <= 0;
public static bool operator >=(global::System.String left, StringValueObject right) => left.CompareTo(right.Value) >= 0;

public static bool operator <(StringValueObject left, global::System.String right) => left.Value.CompareTo(right) < 0;
public static bool operator >(StringValueObject left, global::System.String right) => left.Value.CompareTo(right) > 0;
public static bool operator <=(StringValueObject left, global::System.String right) => left.Value.CompareTo(right) <= 0;
public static bool operator >=(StringValueObject left, global::System.String right) => left.Value.CompareTo(right) >= 0;

}
""";

[Fact]
public void TestAugment()
{
TestHelpers.TestGenerator(Input, "StringValueObject.g.cs", Output);
}
}
Loading