From 970f0a7035842ce45ce7c22a5b3d6cc49eb29d87 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 22 Jul 2026 09:00:35 +0000 Subject: [PATCH] refactor(dummies): share the collection count facade across generators MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AnyDictionary re-implemented the entire count facade — NonEmpty, Empty, WithCount, WithMinCount, WithMaxCount, WithCountBetween plus the RequireNonNegative/V helpers — verbatim from AnyCollection, so any fix or message change to the count algebra had to be mirrored by hand and could silently drift between the two surfaces. Extract that logic once into an internal CountConstraints helper over CollectionState: each generator now delegates its count methods to the helper and wraps the returned state in its own immutable type. The AnyCollection builders (list, array, sequence, set) and AnyDictionary now share a single definition. Behaviour is unchanged: public signatures, exception types and parameter names, and the constraint labels that surface in conflict messages are all preserved, and the labels stay out of the label-agnostic CollectionState. Refs: #225 Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_018RduXUaM3V6tk6MuKxzVzJ --- Dummies/AnyCollection.cs | 44 +++----------------- Dummies/AnyDictionary.cs | 44 +++----------------- Dummies/CountConstraints.cs | 82 +++++++++++++++++++++++++++++++++++++ 3 files changed, 94 insertions(+), 76 deletions(-) create mode 100644 Dummies/CountConstraints.cs diff --git a/Dummies/AnyCollection.cs b/Dummies/AnyCollection.cs index 76b0dee2..ff2e2cd2 100644 --- a/Dummies/AnyCollection.cs +++ b/Dummies/AnyCollection.cs @@ -1,9 +1,3 @@ -#region Usings declarations - -using System.Globalization; - -#endregion - namespace Dummies; /// @@ -25,20 +19,6 @@ namespace Dummies; public abstract class AnyCollection : IAny, IHasRandomSource where TSelf : AnyCollection { - #region Statics members declarations - - private static string V(int value) { - return value.ToString(CultureInfo.InvariantCulture); - } - - private static int RequireNonNegative(int count, string parameterName) { - if (count < 0) { throw new ArgumentOutOfRangeException(parameterName, count, "The count must not be negative."); } - - return count; - } - - #endregion - #region Fields declarations private protected readonly RandomSource? SourceOrNull; @@ -57,14 +37,14 @@ private protected AnyCollection(RandomSource? source, CollectionState sta /// A new generator carrying the added constraint. /// Thrown when the constraint contradicts a constraint already declared. public TSelf NonEmpty() { - return With(State.WithMinCount(1, "NonEmpty()")); + return With(CountConstraints.NonEmpty(State)); } /// Fixes the collection to no elements. /// A new generator carrying the added constraint. /// Thrown when the constraint contradicts a constraint already declared. public TSelf Empty() { - return With(State.WithExactCount(0, "Empty()")); + return With(CountConstraints.Empty(State)); } /// Fixes the exact number of elements. Declared once per generator. @@ -73,9 +53,7 @@ public TSelf Empty() { /// Thrown when is negative. /// Thrown when the constraint contradicts a constraint already declared. public TSelf WithCount(int count) { - RequireNonNegative(count, nameof(count)); - - return With(State.WithExactCount(count, $"WithCount({V(count)})")); + return With(CountConstraints.WithCount(State, count)); } /// Requires at least elements. @@ -84,9 +62,7 @@ public TSelf WithCount(int count) { /// Thrown when is negative. /// Thrown when the constraint contradicts a constraint already declared. public TSelf WithMinCount(int count) { - RequireNonNegative(count, nameof(count)); - - return With(State.WithMinCount(count, $"WithMinCount({V(count)})")); + return With(CountConstraints.WithMinCount(State, count)); } /// Requires at most elements. @@ -95,9 +71,7 @@ public TSelf WithMinCount(int count) { /// Thrown when is negative. /// Thrown when the constraint contradicts a constraint already declared. public TSelf WithMaxCount(int count) { - RequireNonNegative(count, nameof(count)); - - return With(State.WithMaxCount(count, $"WithMaxCount({V(count)})")); + return With(CountConstraints.WithMaxCount(State, count)); } /// Requires a number of elements within the inclusive range [, ]. @@ -108,13 +82,7 @@ public TSelf WithMaxCount(int count) { /// Thrown when is greater than . /// Thrown when the constraint contradicts a constraint already declared. public TSelf WithCountBetween(int minimum, int maximum) { - RequireNonNegative(minimum, nameof(minimum)); - RequireNonNegative(maximum, nameof(maximum)); - if (minimum > maximum) { throw new ArgumentException($"The minimum ({V(minimum)}) must be less than or equal to the maximum ({V(maximum)}).", nameof(minimum)); } - - string constraint = $"WithCountBetween({V(minimum)}, {V(maximum)})"; - - return With(State.WithMinCount(minimum, constraint).WithMaxCount(maximum, constraint)); + return With(CountConstraints.WithCountBetween(State, minimum, maximum)); } /// diff --git a/Dummies/AnyDictionary.cs b/Dummies/AnyDictionary.cs index 1ddf499d..c2f16240 100644 --- a/Dummies/AnyDictionary.cs +++ b/Dummies/AnyDictionary.cs @@ -1,9 +1,3 @@ -#region Usings declarations - -using System.Globalization; - -#endregion - namespace Dummies; /// @@ -18,20 +12,6 @@ namespace Dummies; public sealed class AnyDictionary : IAny>, IHasRandomSource where TKey : notnull { - #region Statics members declarations - - private static string V(int value) { - return value.ToString(CultureInfo.InvariantCulture); - } - - private static int RequireNonNegative(int count, string parameterName) { - if (count < 0) { throw new ArgumentOutOfRangeException(parameterName, count, "The count must not be negative."); } - - return count; - } - - #endregion - #region Fields declarations private readonly CollectionState _keys; @@ -52,14 +32,14 @@ internal AnyDictionary(RandomSource? source, CollectionState keys, IAnyA new generator carrying the added constraint. /// Thrown when the constraint contradicts a constraint already declared. public AnyDictionary NonEmpty() { - return With(_keys.WithMinCount(1, "NonEmpty()")); + return With(CountConstraints.NonEmpty(_keys)); } /// Fixes the dictionary to no entries. /// A new generator carrying the added constraint. /// Thrown when the constraint contradicts a constraint already declared. public AnyDictionary Empty() { - return With(_keys.WithExactCount(0, "Empty()")); + return With(CountConstraints.Empty(_keys)); } /// Fixes the exact number of entries. Declared once per generator. @@ -68,9 +48,7 @@ public AnyDictionary Empty() { /// Thrown when is negative. /// Thrown when the constraint contradicts a constraint already declared. public AnyDictionary WithCount(int count) { - RequireNonNegative(count, nameof(count)); - - return With(_keys.WithExactCount(count, $"WithCount({V(count)})")); + return With(CountConstraints.WithCount(_keys, count)); } /// Requires at least entries. @@ -79,9 +57,7 @@ public AnyDictionary WithCount(int count) { /// Thrown when is negative. /// Thrown when the constraint contradicts a constraint already declared. public AnyDictionary WithMinCount(int count) { - RequireNonNegative(count, nameof(count)); - - return With(_keys.WithMinCount(count, $"WithMinCount({V(count)})")); + return With(CountConstraints.WithMinCount(_keys, count)); } /// Requires at most entries. @@ -90,9 +66,7 @@ public AnyDictionary WithMinCount(int count) { /// Thrown when is negative. /// Thrown when the constraint contradicts a constraint already declared. public AnyDictionary WithMaxCount(int count) { - RequireNonNegative(count, nameof(count)); - - return With(_keys.WithMaxCount(count, $"WithMaxCount({V(count)})")); + return With(CountConstraints.WithMaxCount(_keys, count)); } /// Requires a number of entries within the inclusive range [, ]. @@ -103,13 +77,7 @@ public AnyDictionary WithMaxCount(int count) { /// Thrown when is greater than . /// Thrown when the constraint contradicts a constraint already declared. public AnyDictionary WithCountBetween(int minimum, int maximum) { - RequireNonNegative(minimum, nameof(minimum)); - RequireNonNegative(maximum, nameof(maximum)); - if (minimum > maximum) { throw new ArgumentException($"The minimum ({V(minimum)}) must be less than or equal to the maximum ({V(maximum)}).", nameof(minimum)); } - - string constraint = $"WithCountBetween({V(minimum)}, {V(maximum)})"; - - return With(_keys.WithMinCount(minimum, constraint).WithMaxCount(maximum, constraint)); + return With(CountConstraints.WithCountBetween(_keys, minimum, maximum)); } /// diff --git a/Dummies/CountConstraints.cs b/Dummies/CountConstraints.cs new file mode 100644 index 00000000..ed7f8232 --- /dev/null +++ b/Dummies/CountConstraints.cs @@ -0,0 +1,82 @@ +#region Usings declarations + +using System.Globalization; + +#endregion + +namespace Dummies; + +/// +/// The count-constraint facade shared by every collection generator, defined once over +/// . The builders +/// (a list, an array, a sequence, a set) and — whose keys run through +/// the very same — each expose NonEmpty/Empty/WithCount/ +/// WithMinCount/WithMaxCount/WithCountBetween by delegating here, so the argument +/// validation and the constraint labels that surface in a live +/// in exactly one place and cannot drift between the two surfaces. +/// +/// +/// Each method takes a state and returns the tightened state; the caller wraps that state back into its own +/// immutable generator. The label strings ("NonEmpty()", "WithCount(3)", ...) are part of the +/// user-facing conflict messages, so they are produced here rather than in the label-agnostic +/// , which only records whichever label its caller hands it. +/// +internal static class CountConstraints { + + #region Statics members declarations + + private static string V(int value) { + return value.ToString(CultureInfo.InvariantCulture); + } + + private static int RequireNonNegative(int count, string parameterName) { + if (count < 0) { throw new ArgumentOutOfRangeException(parameterName, count, "The count must not be negative."); } + + return count; + } + + #endregion + + /// Requires at least one element. + internal static CollectionState NonEmpty(CollectionState state) { + return state.WithMinCount(1, "NonEmpty()"); + } + + /// Fixes the collection to no elements. + internal static CollectionState Empty(CollectionState state) { + return state.WithExactCount(0, "Empty()"); + } + + /// Fixes the exact number of elements. + internal static CollectionState WithCount(CollectionState state, int count) { + RequireNonNegative(count, nameof(count)); + + return state.WithExactCount(count, $"WithCount({V(count)})"); + } + + /// Requires at least elements. + internal static CollectionState WithMinCount(CollectionState state, int count) { + RequireNonNegative(count, nameof(count)); + + return state.WithMinCount(count, $"WithMinCount({V(count)})"); + } + + /// Requires at most elements. + internal static CollectionState WithMaxCount(CollectionState state, int count) { + RequireNonNegative(count, nameof(count)); + + return state.WithMaxCount(count, $"WithMaxCount({V(count)})"); + } + + /// Requires a number of elements within the inclusive range [, ]. + internal static CollectionState WithCountBetween(CollectionState state, int minimum, int maximum) { + RequireNonNegative(minimum, nameof(minimum)); + RequireNonNegative(maximum, nameof(maximum)); + if (minimum > maximum) { throw new ArgumentException($"The minimum ({V(minimum)}) must be less than or equal to the maximum ({V(maximum)}).", nameof(minimum)); } + + string constraint = $"WithCountBetween({V(minimum)}, {V(maximum)})"; + + return state.WithMinCount(minimum, constraint).WithMaxCount(maximum, constraint); + } + +}