Skip to content
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
190 changes: 190 additions & 0 deletions Dummies.UnitTests/AnyOneOfTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
#region Usings declarations

using JetBrains.Annotations;

using NFluent;

#endregion

namespace Dummies.UnitTests;

[TestSubject(typeof(AnyOneOf<>))]
public sealed class AnyOneOfTests {

private const int SampleCount = 200;

#region Statics members declarations

private static IEnumerable<T> Samples<T>(IAny<T> generator) {
for (int i = 0; i < SampleCount; i++) {
yield return generator.Generate();
}
}

#endregion

[Fact(DisplayName = "OneOf draws only the supplied values, including domain objects.")]
public void DrawsOnlyTheSuppliedValues() {
Percentage ten = Percentage.Create(10);
Percentage twenty = Percentage.Create(20);
Percentage thirty = Percentage.Create(30);
Percentage[] allowed = [ten, twenty, thirty];

foreach (Percentage value in Samples(Any.OneOf(ten, twenty, thirty))) {
Check.That(allowed.Contains(value)).IsTrue();
}
}

[Fact(DisplayName = "OneOf eventually reaches every supplied value.")]
public void ReachesEverySuppliedValue() {
HashSet<int> seen = new(Samples(Any.OneOf(1, 2, 3)));

Check.That(seen).Contains(1, 2, 3);
}

[Fact(DisplayName = "A single value pins the generated value.")]
public void SingleValueIsPinned() {
foreach (int value in Samples(Any.OneOf(42))) {
Check.That(value).IsEqualTo(42);
}
}

[Fact(DisplayName = "OneOf varies from draw to draw when the pool holds more than one value.")]
public void VariesAcrossDraws() {
HashSet<int> seen = new(Samples(Any.OneOf(1, 2, 3, 4)));

Check.That(seen.Count).IsStrictlyGreaterThan(1);
}

[Fact(DisplayName = "Duplicate values are collapsed under the default comparer: both distinct values are still drawn, nothing else.")]
public void DuplicatesAreCollapsed() {
HashSet<int> seen = new(Samples(Any.OneOf(1, 1, 2)));

Check.That(seen).IsOnlyMadeOf(1, 2);
Check.That(seen).Contains(1, 2);
}

[Fact(DisplayName = "OneOf is reproducible under a seed.")]
public void ReproducibleUnderASeed() {
string first = string.Join("|", Enumerable.Range(0, 20).Select(_ => Any.WithSeed(7).OneOf("a", "b", "c", "d").Generate()));
string second = string.Join("|", Enumerable.Range(0, 20).Select(_ => Any.WithSeed(7).OneOf("a", "b", "c", "d").Generate()));

Check.That(second).IsEqualTo(first);
}

[Fact(DisplayName = "OneOf composes into a value object through As.")]
public void ComposesThroughAs() {
IAny<OrderReference> generator = Any.OneOf("ORD-12345678", "ORD-87654321").As(OrderReference.Create);

for (int i = 0; i < SampleCount; i++) {
OrderReference reference = generator.Generate();
Check.That(reference.Value).StartsWith("ORD-");
Check.That(reference.Value.Length).IsEqualTo(12);
}
}

[Fact(DisplayName = "OrNull makes the pool generator null about half the time, otherwise a member of the pool.")]
public void OrNullIsSometimesNull() {
Percentage one = Percentage.Create(1);
Percentage two = Percentage.Create(2);
IAny<Percentage?> generator = Any.WithSeed(20260721).OneOf(one, two).OrNull();

List<Percentage?> values = new();
for (int i = 0; i < SampleCount; i++) {
values.Add(generator.Generate());
}

Check.That(values.Any(value => value is null)).IsTrue();
Check.That(values.Where(value => value is not null)).IsOnlyMadeOf(one, two);
}

[Fact(DisplayName = "A distinct set over OneOf is gated by the pool's cardinality, both ways.")]
public void CardinalityGatesDistinctCollections() {
// Two distinct values cannot fill a set of three: caught eagerly, like any cardinality conflict.
Check.ThatCode(() => Any.SetOf(Any.OneOf(1, 2)).WithCount(3)).Throws<ConflictingAnyConstraintException>();

// Within the domain it fills the set with the requested distinct values.
HashSet<int> set = Any.SetOf(Any.OneOf(1, 2, 3)).WithCount(3).Generate();
Check.That(set.Count).IsEqualTo(3);
Check.That(set).IsOnlyMadeOf(1, 2, 3);
}

[Fact(DisplayName = "Reference identity keeps equal-valued but distinct instances as separate pool members.")]
public void ReferenceIdentityKeepsDistinctInstancesDistinct() {
// Percentage has no value equality, so two instances of the same percentage are distinct under the default
// comparer — the pool's cardinality is two, and a set of two is fillable.
Percentage first = Percentage.Create(50);
Percentage second = Percentage.Create(50);

HashSet<Percentage> set = Any.SetOf(Any.OneOf(first, second)).WithCount(2).Generate();

Check.That(set.Count).IsEqualTo(2);
Check.That(set).IsOnlyMadeOf(first, second);
}

[Fact(DisplayName = "OneOf rejects empty, null, or null-containing pools as arguments — null goes through OrNull.")]
public void RejectsInvalidPools() {
Check.ThatCode(() => Any.OneOf<int>()).Throws<ArgumentException>();
Check.ThatCode(() => Any.OneOf((string[])null!)).Throws<ArgumentNullException>();
Check.ThatCode(() => Any.OneOf("a", null!)).Throws<ArgumentException>();
}

[Fact(DisplayName = "The null-element message points the caller at OrNull().")]
public void NullElementMessagePointsAtOrNull() {
ArgumentException error = Assert.Throws<ArgumentException>(() => Any.OneOf("a", null!));

Check.That(error.Message).Contains("OrNull");
}

[Fact(DisplayName = "ElementOf draws only from the list it is given.")]
public void ElementOfDrawsFromTheList() {
IReadOnlyList<int> pool = new List<int> { 1, 2, 3 };

HashSet<int> seen = new(Samples(Any.ElementOf(pool)));

Check.That(seen).IsOnlyMadeOf(1, 2, 3);
Check.That(seen.Count).IsStrictlyGreaterThan(1);
}

[Fact(DisplayName = "ElementOf materializes a lazy sequence once, not once per draw.")]
public void ElementOfMaterializesTheSequenceOnce() {
int enumerations = 0;

IEnumerable<int> Source() {
enumerations++;
yield return 1;
yield return 2;
yield return 3;
}

AnyOneOf<int> generator = Any.ElementOf(Source());
for (int i = 0; i < SampleCount; i++) {
generator.Generate();
}

Check.That(enumerations).IsEqualTo(1);
}

[Fact(DisplayName = "ElementOf validates null, empty and null elements like OneOf, for both the list and the sequence overload.")]
public void ElementOfValidatesItsPool() {
Check.ThatCode(() => Any.ElementOf((IReadOnlyList<int>)null!)).Throws<ArgumentNullException>();
Check.ThatCode(() => Any.ElementOf((IEnumerable<int>)null!)).Throws<ArgumentNullException>();
Check.ThatCode(() => Any.ElementOf(new List<int>())).Throws<ArgumentException>();
Check.ThatCode(() => Any.ElementOf(Enumerable.Empty<int>())).Throws<ArgumentException>();
Check.ThatCode(() => Any.ElementOf(new List<string> { "a", null! })).Throws<ArgumentException>();
}

[Fact(DisplayName = "A seeded context makes OneOf and ElementOf deterministic — the mirrored surface draws from the context's seed.")]
public void SeededContextIsDeterministic() {
List<int> pool = new() { 10, 20, 30, 40 };

string oneOfFirst = string.Join("|", Samples(Any.WithSeed(11).OneOf(10, 20, 30, 40)).Take(20));
string oneOfSecond = string.Join("|", Samples(Any.WithSeed(11).OneOf(10, 20, 30, 40)).Take(20));
Check.That(oneOfSecond).IsEqualTo(oneOfFirst);

string elementFirst = string.Join("|", Samples(Any.WithSeed(11).ElementOf(pool)).Take(20));
string elementSecond = string.Join("|", Samples(Any.WithSeed(11).ElementOf(pool)).Take(20));
Check.That(elementSecond).IsEqualTo(elementFirst);
}

}
58 changes: 58 additions & 0 deletions Dummies/Any.cs
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,64 @@
}
#endif

/// <summary>
/// Draws an arbitrary value from an explicit pool of caller-supplied <paramref name="values" />, over the
/// ambient random context — the top-level choice combinator for a value whose domain is a closed set the test
/// does not assert on ("one of the configured currencies", "one of the states in this table"). The pool is the
/// whole specification, so the returned generator is terminal, yet it still composes through <c>As(...)</c>,
/// <c>OrNull()</c>, <c>Combine(...)</c> and the collection generators like any other. To draw from a pool
/// already held as a collection, use <see cref="ElementOf{T}(IReadOnlyList{T})" />.
/// </summary>
/// <remarks>
/// The draw is uniform and reproducible under a seed; duplicates collapse under
/// <see cref="EqualityComparer{T}.Default" /> so no value is implicitly weighted, and the distinct count is
/// advertised so a distinct collection over the pool gates eagerly. A <c>null</c> element is rejected — make the
/// whole generator nullable with <c>OrNull()</c> instead of placing <c>null</c> in the pool.
/// </remarks>
/// <param name="values">The pool the generated value is drawn from; duplicates are ignored.</param>
/// <typeparam name="T">The type of the pooled values.</typeparam>
/// <returns>A terminal generator drawing uniformly from <paramref name="values" />.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="values" /> is <c>null</c>.</exception>
/// <exception cref="ArgumentException">Thrown when <paramref name="values" /> is empty or contains a <c>null</c> element.</exception>
public static AnyOneOf<T> OneOf<T>(params T[] values) {
if (values is null) { throw new ArgumentNullException(nameof(values)); }

return AnyOneOf<T>.FromPool(AmbientRandomSource.Instance, values);
}

/// <summary>
/// Draws an arbitrary value from an explicit pool held as a list — the <see cref="IReadOnlyList{T}" />
/// counterpart of <see cref="OneOf{T}(T[])" />, for a pool already materialized (a configuration list, a
/// fixture, a lookup table of domain objects). Same terminal contract as <see cref="OneOf{T}(T[])" />: the pool
/// is the whole specification, duplicates collapse, and the draw is uniform and reproducible under a seed.
/// </summary>
/// <param name="values">The pool the generated value is drawn from; duplicates are ignored.</param>
/// <typeparam name="T">The type of the pooled values.</typeparam>
/// <returns>A terminal generator drawing uniformly from <paramref name="values" />.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="values" /> is <c>null</c>.</exception>
/// <exception cref="ArgumentException">Thrown when <paramref name="values" /> is empty or contains a <c>null</c> element.</exception>
public static AnyOneOf<T> ElementOf<T>(IReadOnlyList<T> values) {
if (values is null) { throw new ArgumentNullException(nameof(values)); }

return AnyOneOf<T>.FromPool(AmbientRandomSource.Instance, values);
}

/// <summary>
/// Draws an arbitrary value from an explicit pool held as a sequence — the <see cref="IEnumerable{T}" />
/// counterpart of <see cref="ElementOf{T}(IReadOnlyList{T})" /> (a LINQ result, values loaded at setup). The
/// sequence is materialized once, so a lazy query is never re-enumerated per draw.
/// </summary>
/// <param name="values">The pool the generated value is drawn from; duplicates are ignored.</param>
/// <typeparam name="T">The type of the pooled values.</typeparam>
/// <returns>A terminal generator drawing uniformly from <paramref name="values" />.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="values" /> is <c>null</c>.</exception>
/// <exception cref="ArgumentException">Thrown when <paramref name="values" /> is empty or contains a <c>null</c> element.</exception>
public static AnyOneOf<T> ElementOf<T>(IEnumerable<T> values) {
if (values is null) { throw new ArgumentNullException(nameof(values)); }

return AnyOneOf<T>.FromPool(AmbientRandomSource.Instance, values as IReadOnlyList<T> ?? values.ToArray());
}

/// <summary>
/// Creates an isolated, deterministic generation context: every generator created from it draws from a
/// dedicated source seeded with <paramref name="seed" />, independent of the ambient context. Two contexts
Expand Down Expand Up @@ -468,7 +526,7 @@
/// <typeparam name="TResult">The type of the composed value.</typeparam>
/// <returns>A generator of the composed value.</returns>
/// <exception cref="ArgumentNullException">Thrown when any argument is <c>null</c>.</exception>
public static IAny<TResult> Combine<T1, T2, T3, TResult>(IAny<T1> first, IAny<T2> second, IAny<T3> third, Func<T1, T2, T3, TResult> compose) {

Check warning on line 529 in Dummies/Any.cs

View workflow job for this annotation

GitHub Actions / SonarQube Cloud analysis

Reduce the number of generic parameters in the 'Any.Combine' method to no more than the 3 authorized.

Check warning on line 529 in Dummies/Any.cs

View workflow job for this annotation

GitHub Actions / SonarQube Cloud analysis

Reduce the number of generic parameters in the 'Any.Combine' method to no more than the 3 authorized.
if (first is null) { throw new ArgumentNullException(nameof(first)); }
if (second is null) { throw new ArgumentNullException(nameof(second)); }
if (third is null) { throw new ArgumentNullException(nameof(third)); }
Expand Down Expand Up @@ -501,7 +559,7 @@
/// <typeparam name="TResult">The type of the composed value.</typeparam>
/// <returns>A generator of the composed value.</returns>
/// <exception cref="ArgumentNullException">Thrown when any argument is <c>null</c>.</exception>
public static IAny<TResult> Combine<T1, T2, T3, T4, TResult>(IAny<T1> first, IAny<T2> second, IAny<T3> third, IAny<T4> fourth, Func<T1, T2, T3, T4, TResult> compose) {

Check warning on line 562 in Dummies/Any.cs

View workflow job for this annotation

GitHub Actions / SonarQube Cloud analysis

Reduce the number of generic parameters in the 'Any.Combine' method to no more than the 3 authorized.

Check warning on line 562 in Dummies/Any.cs

View workflow job for this annotation

GitHub Actions / SonarQube Cloud analysis

Reduce the number of generic parameters in the 'Any.Combine' method to no more than the 3 authorized.
if (first is null) { throw new ArgumentNullException(nameof(first)); }
if (second is null) { throw new ArgumentNullException(nameof(second)); }
if (third is null) { throw new ArgumentNullException(nameof(third)); }
Expand Down Expand Up @@ -538,7 +596,7 @@
/// <typeparam name="TResult">The type of the composed value.</typeparam>
/// <returns>A generator of the composed value.</returns>
/// <exception cref="ArgumentNullException">Thrown when any argument is <c>null</c>.</exception>
public static IAny<TResult> Combine<T1, T2, T3, T4, T5, TResult>(IAny<T1> first, IAny<T2> second, IAny<T3> third, IAny<T4> fourth, IAny<T5> fifth, Func<T1, T2, T3, T4, T5, TResult> compose) {

Check warning on line 599 in Dummies/Any.cs

View workflow job for this annotation

GitHub Actions / SonarQube Cloud analysis

Reduce the number of generic parameters in the 'Any.Combine' method to no more than the 3 authorized.

Check warning on line 599 in Dummies/Any.cs

View workflow job for this annotation

GitHub Actions / SonarQube Cloud analysis

Reduce the number of generic parameters in the 'Any.Combine' method to no more than the 3 authorized.
if (first is null) { throw new ArgumentNullException(nameof(first)); }
if (second is null) { throw new ArgumentNullException(nameof(second)); }
if (third is null) { throw new ArgumentNullException(nameof(third)); }
Expand Down Expand Up @@ -579,7 +637,7 @@
/// <typeparam name="TResult">The type of the composed value.</typeparam>
/// <returns>A generator of the composed value.</returns>
/// <exception cref="ArgumentNullException">Thrown when any argument is <c>null</c>.</exception>
public static IAny<TResult> Combine<T1, T2, T3, T4, T5, T6, TResult>(IAny<T1> first, IAny<T2> second, IAny<T3> third, IAny<T4> fourth, IAny<T5> fifth, IAny<T6> sixth, Func<T1, T2, T3, T4, T5, T6, TResult> compose) {

Check warning on line 640 in Dummies/Any.cs

View workflow job for this annotation

GitHub Actions / SonarQube Cloud analysis

Reduce the number of generic parameters in the 'Any.Combine' method to no more than the 3 authorized.

Check warning on line 640 in Dummies/Any.cs

View workflow job for this annotation

GitHub Actions / SonarQube Cloud analysis

Reduce the number of generic parameters in the 'Any.Combine' method to no more than the 3 authorized.
if (first is null) { throw new ArgumentNullException(nameof(first)); }
if (second is null) { throw new ArgumentNullException(nameof(second)); }
if (third is null) { throw new ArgumentNullException(nameof(third)); }
Expand Down Expand Up @@ -626,7 +684,7 @@
/// <exception cref="ArgumentNullException">Thrown when any argument is <c>null</c>.</exception>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Major Code Smell", "S107:Methods should not have too many parameters",
Justification = "Heterogeneous composition needs one generator parameter per part; the arity-8 ceiling is a deliberate ergonomic decision (ADR-0015), and a flat parameter list reads better at the call site than nested Combine calls.")]
public static IAny<TResult> Combine<T1, T2, T3, T4, T5, T6, T7, TResult>(IAny<T1> first, IAny<T2> second, IAny<T3> third, IAny<T4> fourth, IAny<T5> fifth, IAny<T6> sixth, IAny<T7> seventh, Func<T1, T2, T3, T4, T5, T6, T7, TResult> compose) {

Check warning on line 687 in Dummies/Any.cs

View workflow job for this annotation

GitHub Actions / SonarQube Cloud analysis

Reduce the number of generic parameters in the 'Any.Combine' method to no more than the 3 authorized.
if (first is null) { throw new ArgumentNullException(nameof(first)); }
if (second is null) { throw new ArgumentNullException(nameof(second)); }
if (third is null) { throw new ArgumentNullException(nameof(third)); }
Expand Down Expand Up @@ -678,7 +736,7 @@
/// <exception cref="ArgumentNullException">Thrown when any argument is <c>null</c>.</exception>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Major Code Smell", "S107:Methods should not have too many parameters",
Justification = "Heterogeneous composition needs one generator parameter per part; the arity-8 ceiling is a deliberate ergonomic decision (ADR-0015), and a flat parameter list reads better at the call site than nested Combine calls.")]
public static IAny<TResult> Combine<T1, T2, T3, T4, T5, T6, T7, T8, TResult>(IAny<T1> first, IAny<T2> second, IAny<T3> third, IAny<T4> fourth, IAny<T5> fifth, IAny<T6> sixth, IAny<T7> seventh, IAny<T8> eighth, Func<T1, T2, T3, T4, T5, T6, T7, T8, TResult> compose) {

Check warning on line 739 in Dummies/Any.cs

View workflow job for this annotation

GitHub Actions / SonarQube Cloud analysis

Reduce the number of generic parameters in the 'Any.Combine' method to no more than the 3 authorized.
if (first is null) { throw new ArgumentNullException(nameof(first)); }
if (second is null) { throw new ArgumentNullException(nameof(second)); }
if (third is null) { throw new ArgumentNullException(nameof(third)); }
Expand Down
46 changes: 46 additions & 0 deletions Dummies/AnyContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -295,4 +295,50 @@ public AnyHalf Half() {
}
#endif

/// <summary>
/// Draws an arbitrary value from an explicit pool of caller-supplied <paramref name="values" /> drawing from
/// this context — same surface as <see cref="Any.OneOf{T}(T[])" />, deterministic under this context's seed.
/// </summary>
/// <param name="values">The pool the generated value is drawn from; duplicates are ignored.</param>
/// <typeparam name="T">The type of the pooled values.</typeparam>
/// <returns>A terminal generator drawing uniformly from <paramref name="values" />.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="values" /> is <c>null</c>.</exception>
/// <exception cref="ArgumentException">Thrown when <paramref name="values" /> is empty or contains a <c>null</c> element.</exception>
public AnyOneOf<T> OneOf<T>(params T[] values) {
if (values is null) { throw new ArgumentNullException(nameof(values)); }

return AnyOneOf<T>.FromPool(_source, values);
}

/// <summary>
/// Draws an arbitrary value from an explicit pool held as a list drawing from this context — same surface as
/// <see cref="Any.ElementOf{T}(IReadOnlyList{T})" />, deterministic under this context's seed.
/// </summary>
/// <param name="values">The pool the generated value is drawn from; duplicates are ignored.</param>
/// <typeparam name="T">The type of the pooled values.</typeparam>
/// <returns>A terminal generator drawing uniformly from <paramref name="values" />.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="values" /> is <c>null</c>.</exception>
/// <exception cref="ArgumentException">Thrown when <paramref name="values" /> is empty or contains a <c>null</c> element.</exception>
public AnyOneOf<T> ElementOf<T>(IReadOnlyList<T> values) {
if (values is null) { throw new ArgumentNullException(nameof(values)); }

return AnyOneOf<T>.FromPool(_source, values);
}

/// <summary>
/// Draws an arbitrary value from an explicit pool held as a sequence drawing from this context — same surface
/// as <see cref="Any.ElementOf{T}(IEnumerable{T})" />, deterministic under this context's seed. The sequence is
/// materialized once.
/// </summary>
/// <param name="values">The pool the generated value is drawn from; duplicates are ignored.</param>
/// <typeparam name="T">The type of the pooled values.</typeparam>
/// <returns>A terminal generator drawing uniformly from <paramref name="values" />.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="values" /> is <c>null</c>.</exception>
/// <exception cref="ArgumentException">Thrown when <paramref name="values" /> is empty or contains a <c>null</c> element.</exception>
public AnyOneOf<T> ElementOf<T>(IEnumerable<T> values) {
if (values is null) { throw new ArgumentNullException(nameof(values)); }

return AnyOneOf<T>.FromPool(_source, values as IReadOnlyList<T> ?? values.ToArray());
}

}
75 changes: 75 additions & 0 deletions Dummies/AnyOneOf.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
namespace Dummies;

/// <summary>
/// A generator that draws an arbitrary value from an <b>explicit, fixed pool</b> supplied by the caller — the
/// dummy for a value whose domain is a closed set a test does not assert on (one of the currencies a context is
/// configured with, one of the orders already in a fixture, one of a handful of domain states). Unlike the typed
/// builders' <c>OneOf</c>, which narrows <i>within</i> a scalar's own domain, this draws from values the library
/// could never synthesize on its own, so the pool is the whole specification: it is a <i>terminal</i> generator
/// exposing no further constraints. It still composes like any other generator — pipe it through <c>As(...)</c>
/// into a value object, make it optional with <c>OrNull()</c>, or fold it into <c>Combine(...)</c> and the
/// collection generators.
/// </summary>
/// <remarks>
/// <para>
/// Each <see cref="Generate" /> draws one value uniformly from the pool, from the generator's random context —
/// so a run is reproducible under a seed, exactly like every other generator. Duplicate values are collapsed
/// under <see cref="EqualityComparer{T}.Default" />, so no value carries a heavier weight for being listed
/// twice, and the number of distinct values is the exact size of the domain a distinct collection
/// (<c>SetOf</c>, a dictionary's keys) gates against.
/// </para>
/// <para>
/// A <c>null</c> element is rejected at construction: nullability is an orthogonal concern expressed by
/// <c>OrNull()</c>, never smuggled into the pool — the same rule the string value-set generator applies.
/// </para>
/// <example>
/// <code>
/// Currency currency = Any.OneOf(eur, usd, gbp).Generate();
/// Order order = Any.ElementOf(existingOrders).Generate();
/// </code>
/// </example>
/// </remarks>
/// <typeparam name="T">The type of the pooled values.</typeparam>
public sealed class AnyOneOf<T> : IAny<T>, IHasRandomSource, ICardinalityHint<T> {

#region Statics members declarations

// Validates and deduplicates the caller's pool, then builds the generator. The array-null check belongs to the
// public factories (they own the parameter name); by the time we get here the pool is non-null and materialized.
internal static AnyOneOf<T> FromPool(RandomSource source, IReadOnlyList<T> values) {
if (values.Count == 0) { throw new ArgumentException("At least one value is required.", nameof(values)); }
if (values.Any(value => value is null)) { throw new ArgumentException("The values must not contain a null element; use OrNull() to make the whole generator nullable.", nameof(values)); }

return new AnyOneOf<T>(source, values.Distinct().ToArray());
}

#endregion

#region Fields declarations

private readonly RandomSource _source;
private readonly IReadOnlyList<T> _values;

#endregion

private AnyOneOf(RandomSource source, IReadOnlyList<T> values) {
_source = source;
_values = values;
}

RandomSource? IHasRandomSource.Source => _source;

// The pool is fixed and deduplicated at construction under the default comparer, so its count is the exact number
// of distinct values drawable, and membership is a direct lookup under that same comparer. Both deliberately ignore
// any custom comparer a downstream distinct collection carries: such a comparer can only merge values, never create
// new ones, so the advertised size stays a sound upper bound and membership never claims a value the pool lacks.
long? ICardinalityHint<T>.DistinctCardinality => _values.Count;

bool ICardinalityHint<T>.Contains(T value) => _values.Contains(value);

/// <inheritdoc />
public T Generate() {
return _values[_source.Current.Random.Next(_values.Count)];
}

}
Loading