diff --git a/Dummies.UnitTests/AnyCollectionTests.cs b/Dummies.UnitTests/AnyCollectionTests.cs index 7c40fdd3..f776fd74 100644 --- a/Dummies.UnitTests/AnyCollectionTests.cs +++ b/Dummies.UnitTests/AnyCollectionTests.cs @@ -39,13 +39,13 @@ public void ListOfUnconstrained() { [Fact(DisplayName = "ListOf: the count family fixes, floors, caps and ranges the size.")] public void ListOfCountFamily() { for (int i = 0; i < SampleCount; i++) { - Check.That(((List)Any.ListOf(Any.Int32()).WithCount(5)).Count).IsEqualTo(5); - Check.That(((List)Any.ListOf(Any.Int32()).Empty()).Count).IsEqualTo(0); - Check.That(((List)Any.ListOf(Any.Int32()).NonEmpty()).Count).IsStrictlyGreaterThan(0); - Check.That(((List)Any.ListOf(Any.Int32()).WithMinCount(3)).Count).IsGreaterOrEqualThan(3); - Check.That(((List)Any.ListOf(Any.Int32()).WithMaxCount(2)).Count).IsLessOrEqualThan(2); + Check.That(Any.ListOf(Any.Int32()).WithCount(5).Generate().Count).IsEqualTo(5); + Check.That(Any.ListOf(Any.Int32()).Empty().Generate().Count).IsEqualTo(0); + Check.That(Any.ListOf(Any.Int32()).NonEmpty().Generate().Count).IsStrictlyGreaterThan(0); + Check.That(Any.ListOf(Any.Int32()).WithMinCount(3).Generate().Count).IsGreaterOrEqualThan(3); + Check.That(Any.ListOf(Any.Int32()).WithMaxCount(2).Generate().Count).IsLessOrEqualThan(2); - int ranged = ((List)Any.ListOf(Any.Int32()).WithCountBetween(4, 6)).Count; + int ranged = Any.ListOf(Any.Int32()).WithCountBetween(4, 6).Generate().Count; Check.That(ranged is >= 4 and <= 6).IsTrue(); } } @@ -71,7 +71,7 @@ public void ListOfCountValidation() { [Fact(DisplayName = "Distinct: a wide-domain distinct list holds only distinct elements.")] public void DistinctOverAWideDomain() { for (int i = 0; i < SampleCount; i++) { - List list = Any.ListOf(Any.Int32().Between(1, 1000)).WithCount(20).Distinct(); + List list = Any.ListOf(Any.Int32().Between(1, 1000)).WithCount(20).Distinct().Generate(); Check.That(list.Count).IsEqualTo(20); Check.That(new HashSet(list).Count).IsEqualTo(20); } @@ -102,7 +102,7 @@ public void DistinctFallbackThrowsAtGeneration() { [Fact(DisplayName = "SetOf: elements are always distinct and drawn from the item generator.")] public void SetOfIsDistinct() { for (int i = 0; i < SampleCount; i++) { - HashSet set = Any.SetOf(Any.Int32().Between(1, 500)).WithCount(10); + HashSet set = Any.SetOf(Any.Int32().Between(1, 500)).WithCount(10).Generate(); Check.That(set.Count).IsEqualTo(10); Check.That(set).ContainsOnlyElementsThatMatch(value => value is >= 1 and <= 500); } @@ -113,7 +113,7 @@ public void SetOfHonoursAComparer() { IEqualityComparer modTen = new ModuloComparer(10); for (int i = 0; i < SampleCount; i++) { - HashSet set = Any.SetOf(Any.Int32().Between(0, 999), modTen).WithCount(5); + HashSet set = Any.SetOf(Any.Int32().Between(0, 999), modTen).WithCount(5).Generate(); Check.That(set.Count).IsEqualTo(5); List classes = set.Select(value => value % 10).ToList(); Check.That(classes.Count).IsEqualTo(new HashSet(classes).Count); @@ -127,7 +127,7 @@ public void SetOfHonoursAComparer() { [Fact(DisplayName = "Containing: a required value is present, and a distinct duplicate requirement conflicts.")] public void ContainingPlacesValues() { for (int i = 0; i < SampleCount; i++) { - List list = Any.ListOf(Any.Int32().Between(1, 9)).WithCount(5).Containing(777); + List list = Any.ListOf(Any.Int32().Between(1, 9)).WithCount(5).Containing(777).Generate(); Check.That(list).Contains(777); Check.That(list.Count).IsEqualTo(5); } @@ -142,7 +142,7 @@ public void ContainingPlacesValues() { [Fact(DisplayName = "Containing: a value drawn from a generator is forced into the collection.")] public void ContainingFromAGenerator() { for (int i = 0; i < SampleCount; i++) { - List list = Any.ListOf(Any.Int32().Between(1, 9)).NonEmpty().ContainingAny(Any.Int32().OneOf(4242)); + List list = Any.ListOf(Any.Int32().Between(1, 9)).NonEmpty().ContainingAny(Any.Int32().OneOf(4242)).Generate(); Check.That(list).Contains(4242); } } @@ -150,7 +150,7 @@ public void ContainingFromAGenerator() { [Fact(DisplayName = "ArrayOf: produces an array of the requested size, distinct when asked.")] public void ArrayOfProducesArrays() { for (int i = 0; i < SampleCount; i++) { - int[] array = Any.ArrayOf(Any.Int32().Between(1, 100)).WithCount(6).Distinct(); + int[] array = Any.ArrayOf(Any.Int32().Between(1, 100)).WithCount(6).Distinct().Generate(); Check.That(array.Length).IsEqualTo(6); Check.That(new HashSet(array).Count).IsEqualTo(6); } @@ -169,7 +169,7 @@ public void SequenceOfIsMaterialized() { [Fact(DisplayName = "DictionaryOf: builds unique-keyed dictionaries and gates the count by the key domain.")] public void DictionaryOfBehaves() { for (int i = 0; i < SampleCount; i++) { - Dictionary dictionary = Any.DictionaryOf(Any.Int32().Between(1, 1000), Any.String().NonEmpty()).WithCount(8); + Dictionary dictionary = Any.DictionaryOf(Any.Int32().Between(1, 1000), Any.String().NonEmpty()).WithCount(8).Generate(); Check.That(dictionary.Count).IsEqualTo(8); Check.That(dictionary.Values).ContainsOnlyElementsThatMatch(value => value.Length > 0); } @@ -194,13 +194,13 @@ public void PairAndTriple() { [Fact(DisplayName = "Collections are reproducible when their element generator draws from a seeded context.")] public void CollectionsAreReproducible() { - HashSet first = Any.SetOf(Any.WithSeed(4242).Int32()).WithCount(6); - HashSet second = Any.SetOf(Any.WithSeed(4242).Int32()).WithCount(6); + HashSet first = Any.SetOf(Any.WithSeed(4242).Int32()).WithCount(6).Generate(); + HashSet second = Any.SetOf(Any.WithSeed(4242).Int32()).WithCount(6).Generate(); Check.That(second.OrderBy(value => value)).ContainsExactly(first.OrderBy(value => value)); - List listOne = Any.ListOf(Any.WithSeed(7).Int32().Between(0, 99)).WithCount(5); - List listTwo = Any.ListOf(Any.WithSeed(7).Int32().Between(0, 99)).WithCount(5); + List listOne = Any.ListOf(Any.WithSeed(7).Int32().Between(0, 99)).WithCount(5).Generate(); + List listTwo = Any.ListOf(Any.WithSeed(7).Int32().Between(0, 99)).WithCount(5).Generate(); Check.That(listTwo).ContainsExactly(listOne); } diff --git a/Dummies.UnitTests/AnyContinuousTests.cs b/Dummies.UnitTests/AnyContinuousTests.cs index 731fd747..6bc50ba4 100644 --- a/Dummies.UnitTests/AnyContinuousTests.cs +++ b/Dummies.UnitTests/AnyContinuousTests.cs @@ -107,9 +107,9 @@ public void DecimalBehaves() { [Fact(DisplayName = "Continuous generators convert implicitly to their value type.")] public void ImplicitConversions() { - double d = Any.Double().Between(1d, 2d); - float f = Any.Single().Between(1f, 2f); - decimal m = Any.Decimal().Between(1m, 2m); + double d = Any.Double().Between(1d, 2d).Generate(); + float f = Any.Single().Between(1f, 2f).Generate(); + decimal m = Any.Decimal().Between(1m, 2m).Generate(); Check.That(d).IsGreaterOrEqualThan(1d); Check.That(f).IsGreaterOrEqualThan(1f); diff --git a/Dummies.UnitTests/AnySetTypeTests.cs b/Dummies.UnitTests/AnySetTypeTests.cs index 8b49d306..a2b6dfac 100644 --- a/Dummies.UnitTests/AnySetTypeTests.cs +++ b/Dummies.UnitTests/AnySetTypeTests.cs @@ -35,7 +35,7 @@ public void BoolBehaves() { Check.That(conflict.Message).Contains("False()"); Check.That(conflict.Message).Contains("True()"); - bool value = Any.Bool().True(); + bool value = Any.Bool().True().Generate(); Check.That(value).IsTrue(); } diff --git a/Dummies.UnitTests/AnySignedIntegerTests.cs b/Dummies.UnitTests/AnySignedIntegerTests.cs index 1956f199..5b31c3f4 100644 --- a/Dummies.UnitTests/AnySignedIntegerTests.cs +++ b/Dummies.UnitTests/AnySignedIntegerTests.cs @@ -86,9 +86,9 @@ public void Int64ExtremesAndArguments() { [Fact(DisplayName = "Signed integers convert implicitly to their value type.")] public void ImplicitConversions() { - sbyte small = Any.SByte().Positive(); - short mid = Any.Int16().Negative(); - long wide = Any.Int64().Between(1L, 10L); + sbyte small = Any.SByte().Positive().Generate(); + short mid = Any.Int16().Negative().Generate(); + long wide = Any.Int64().Between(1L, 10L).Generate(); Check.That((int)small).IsStrictlyGreaterThan(0); Check.That((int)mid).IsStrictlyLessThan(0); diff --git a/Dummies.UnitTests/AnyUnsignedIntegerTests.cs b/Dummies.UnitTests/AnyUnsignedIntegerTests.cs index b4271b6d..c3cbe2a4 100644 --- a/Dummies.UnitTests/AnyUnsignedIntegerTests.cs +++ b/Dummies.UnitTests/AnyUnsignedIntegerTests.cs @@ -75,10 +75,10 @@ public void UInt64ExtremesAndSets() { [Fact(DisplayName = "Unsigned integers convert implicitly to their value type.")] public void ImplicitConversions() { - byte tiny = Any.Byte().Between(1, 10); - ushort mid = Any.UInt16().NonZero(); - uint wide = Any.UInt32().Between(1u, 10u); - ulong huge = Any.UInt64().Between(1UL, 10UL); + byte tiny = Any.Byte().Between(1, 10).Generate(); + ushort mid = Any.UInt16().NonZero().Generate(); + uint wide = Any.UInt32().Between(1u, 10u).Generate(); + ulong huge = Any.UInt64().Between(1UL, 10UL).Generate(); Check.That((int)tiny).IsGreaterOrEqualThan(1); Check.That((int)mid).IsStrictlyGreaterThan(0); diff --git a/Dummies.UnitTests/ImplicitConversionTests.cs b/Dummies.UnitTests/ImplicitConversionTests.cs deleted file mode 100644 index b5a2def5..00000000 --- a/Dummies.UnitTests/ImplicitConversionTests.cs +++ /dev/null @@ -1,52 +0,0 @@ -#region Usings declarations - -using JetBrains.Annotations; - -using NFluent; - -#endregion - -namespace Dummies.UnitTests; - -[TestSubject(typeof(Any))] -public sealed class ImplicitConversionTests { - - [Fact(DisplayName = "An AnyString flows implicitly into a string variable.")] - public void AnyStringConvertsImplicitly() { - string value = Any.String().NonEmpty(); - - Check.That(value).IsNotEmpty(); - } - - [Fact(DisplayName = "An AnyInt32 flows implicitly into an int variable.")] - public void AnyInt32ConvertsImplicitly() { - int value = Any.Int32().Positive(); - - Check.That(value).IsStrictlyGreaterThan(0); - } - - [Fact(DisplayName = "An AnyString flows implicitly into a method expecting a string.")] - public void AnyStringConvertsImplicitlyAtACallSite() { - static int Measure(string text) { - return text.Length; - } - - int length = Measure(Any.String().WithLength(9)); - - Check.That(length).IsEqualTo(9); - } - - [Fact(DisplayName = "Each implicit conversion draws a fresh value.")] - public void EachConversionDrawsAFreshValue() { - AnyInt32 generator = Any.Int32().Between(0, int.MaxValue); - - HashSet seen = new(); - for (int i = 0; i < 20; i++) { - int value = generator; - seen.Add(value); - } - - Check.That(seen.Count).IsStrictlyGreaterThan(1); - } - -} diff --git a/Dummies.UnitTests/MaterializationTests.cs b/Dummies.UnitTests/MaterializationTests.cs new file mode 100644 index 00000000..955a9d50 --- /dev/null +++ b/Dummies.UnitTests/MaterializationTests.cs @@ -0,0 +1,68 @@ +#region Usings declarations + +using JetBrains.Annotations; + +using NFluent; + +#endregion + +namespace Dummies.UnitTests; + +[TestSubject(typeof(Any))] +public sealed class MaterializationTests { + + #region Statics members declarations + + private static T Materialize(IAny generator) { + return generator.Generate(); + } + + #endregion + + [Fact(DisplayName = "Generate materializes a valid string value.")] + public void GenerateMaterializesAString() { + string value = Any.String().NonEmpty().Generate(); + + Check.That(value).IsNotEmpty(); + } + + [Fact(DisplayName = "Generate materializes a valid int value.")] + public void GenerateMaterializesAnInt() { + int value = Any.Int32().Positive().Generate(); + + Check.That(value).IsStrictlyGreaterThan(0); + } + + [Fact(DisplayName = "A materialized value flows into a method expecting the generated type.")] + public void GeneratedValueFlowsIntoACallSite() { + static int Measure(string text) { + return text.Length; + } + + int length = Measure(Any.String().WithLength(9).Generate()); + + Check.That(length).IsEqualTo(9); + } + + [Fact(DisplayName = "Each Generate call draws a fresh value.")] + public void EachGenerateDrawsAFreshValue() { + AnyInt32 generator = Any.Int32().Between(0, int.MaxValue); + + HashSet seen = new(); + for (int i = 0; i < 20; i++) { + seen.Add(generator.Generate()); + } + + Check.That(seen.Count).IsStrictlyGreaterThan(1); + } + + [Fact(DisplayName = "Generic inference flows through IAny, materializing without any implicit conversion.")] + public void GenericInferenceMaterializesThroughIAny() { + string text = Materialize(Any.String().NonEmpty()); + int value = Materialize(Any.Int32().Positive()); + + Check.That(text).IsNotEmpty(); + Check.That(value).IsStrictlyGreaterThan(0); + } + +} diff --git a/Dummies.UnitTests/SeedReproducibilityTests.cs b/Dummies.UnitTests/SeedReproducibilityTests.cs index caac4214..f60535f0 100644 --- a/Dummies.UnitTests/SeedReproducibilityTests.cs +++ b/Dummies.UnitTests/SeedReproducibilityTests.cs @@ -16,24 +16,24 @@ public sealed class SeedReproducibilityTests { private static string Batch() { // Explicitly typed locals: string.Join(params object[]) would otherwise box the generators and // call their ToString() instead of triggering the implicit conversions. - int full = Any.Int32(); - int bounded = Any.Int32().Between(1, 1000); - string free = Any.String(); - string capped = Any.String().NonEmpty().WithMaxLength(50); - string shaped = Any.String().StartingWith("ORD-").WithLength(12); - long wide = Any.Int64(); - ulong unsigned = Any.UInt64(); - double real = Any.Double().Between(0d, 1000d); - decimal exact = Any.Decimal().Between(0m, 1000m); - bool flag = Any.Bool(); - Guid id = Any.Guid(); - char letter = Any.Char(); - TimeSpan span = Any.TimeSpan(); - DateTime instant = Any.DateTime(); - Int128 huge = Any.Int128(); - Half tiny = Any.Half(); - List list = Any.ListOf(Any.Int32().Between(0, 9)).WithCount(4); - HashSet set = Any.SetOf(Any.Int32().Between(0, 99)).WithCount(3); + int full = Any.Int32().Generate(); + int bounded = Any.Int32().Between(1, 1000).Generate(); + string free = Any.String().Generate(); + string capped = Any.String().NonEmpty().WithMaxLength(50).Generate(); + string shaped = Any.String().StartingWith("ORD-").WithLength(12).Generate(); + long wide = Any.Int64().Generate(); + ulong unsigned = Any.UInt64().Generate(); + double real = Any.Double().Between(0d, 1000d).Generate(); + decimal exact = Any.Decimal().Between(0m, 1000m).Generate(); + bool flag = Any.Bool().Generate(); + Guid id = Any.Guid().Generate(); + char letter = Any.Char().Generate(); + TimeSpan span = Any.TimeSpan().Generate(); + DateTime instant = Any.DateTime().Generate(); + Int128 huge = Any.Int128().Generate(); + Half tiny = Any.Half().Generate(); + List list = Any.ListOf(Any.Int32().Between(0, 9)).WithCount(4).Generate(); + HashSet set = Any.SetOf(Any.Int32().Between(0, 99)).WithCount(3).Generate(); int? maybe = Any.Int32().Between(0, 9).OrNull().Generate(); return string.Join("|", full, bounded, free, capped, shaped, @@ -50,8 +50,8 @@ public void SameSeedContextsAgree() { AnyContext any1 = Any.WithSeed(12345); AnyContext any2 = Any.WithSeed(12345); - string value1 = any1.String().NonEmpty(); - string value2 = any2.String().NonEmpty(); + string value1 = any1.String().NonEmpty().Generate(); + string value2 = any2.String().NonEmpty().Generate(); Check.That(value2).IsEqualTo(value1); } @@ -78,12 +78,12 @@ public void DifferentSeedContextsDiverge() { [Fact(DisplayName = "A context is isolated from the ambient source: interleaved ambient draws do not shift it.")] public void ContextIsIsolatedFromAmbientDraws() { AnyContext quiet = Any.WithSeed(31415); - string undisturbed = quiet.String().WithLength(10); + string undisturbed = quiet.String().WithLength(10).Generate(); AnyContext interleaved = Any.WithSeed(31415); Any.String().Generate(); Any.Int32().Generate(); - string disturbed = interleaved.String().WithLength(10); + string disturbed = interleaved.String().WithLength(10).Generate(); Check.That(disturbed).IsEqualTo(undisturbed); } diff --git a/Dummies/Any.cs b/Dummies/Any.cs index 91dff493..b4d4a61c 100644 --- a/Dummies/Any.cs +++ b/Dummies/Any.cs @@ -23,7 +23,7 @@ namespace Dummies; /// /// /// // The reference format is the invariant; the exact value is irrelevant — so it is Any. -/// string reference = Any.String().StartingWith("ORD-").WithLength(12); +/// string reference = Any.String().StartingWith("ORD-").WithLength(12).Generate(); /// /// // Turn a constrained primitive into a value object, without reflection: /// OrderReference order = Any.String().StartingWith("ORD-").WithLength(12) diff --git a/Dummies/AnyArray.cs b/Dummies/AnyArray.cs index 8eafc1e5..7f0a3e53 100644 --- a/Dummies/AnyArray.cs +++ b/Dummies/AnyArray.cs @@ -8,20 +8,6 @@ namespace Dummies; /// The element type. public sealed class AnyArray : AnyCollection> { - #region Statics members declarations - - /// - /// Generates the value — an can be used wherever a T[] is expected. Each - /// conversion draws a fresh array. - /// - /// The generator to draw from. - /// An arbitrary array satisfying the generator's constraints. - public static implicit operator T[](AnyArray generator) { - return generator.Generate(); - } - - #endregion - internal AnyArray(RandomSource? source, CollectionState state) : base(source, state) { } /// Requires the elements to be pairwise distinct (default equality). diff --git a/Dummies/AnyBool.cs b/Dummies/AnyBool.cs index cecf0359..f1c3913c 100644 --- a/Dummies/AnyBool.cs +++ b/Dummies/AnyBool.cs @@ -9,16 +9,6 @@ public sealed class AnyBool : IAny, IHasRandomSource, ICardinalityHint { #region Statics members declarations - /// - /// Generates the value — an can be used wherever a is expected. - /// Each conversion draws a fresh value. - /// - /// The generator to draw from. - /// An arbitrary value satisfying the generator's constraints. - public static implicit operator bool(AnyBool generator) { - return generator.Generate(); - } - internal static AnyBool Create(RandomSource source) { return new AnyBool(source, null, null); } diff --git a/Dummies/AnyByte.cs b/Dummies/AnyByte.cs index 6b3a28f2..5f75b1f2 100644 --- a/Dummies/AnyByte.cs +++ b/Dummies/AnyByte.cs @@ -16,16 +16,6 @@ public sealed class AnyByte : IAny, IHasRandomSource, ICardinalityHint { #region Statics members declarations - /// - /// Generates the value — an can be used wherever a is expected. - /// Each conversion draws a fresh value. - /// - /// The generator to draw from. - /// An arbitrary value satisfying the generator's constraints. - public static implicit operator byte(AnyByte generator) { - return generator.Generate(); - } - internal static AnyByte Create(RandomSource source) { return new AnyByte(source, OrdinalIntervalSpec.Unconstrained("Byte", ordinal => V(Val(ordinal)), Ord(byte.MinValue), Ord(byte.MaxValue))); } diff --git a/Dummies/AnyChar.cs b/Dummies/AnyChar.cs index 9d0c5cd0..56ce746b 100644 --- a/Dummies/AnyChar.cs +++ b/Dummies/AnyChar.cs @@ -12,16 +12,6 @@ public sealed class AnyChar : IAny, IHasRandomSource, ICardinalityHint { #region Statics members declarations - /// - /// Generates the value — an can be used wherever a is expected. - /// Each conversion draws a fresh value. - /// - /// The generator to draw from. - /// An arbitrary value satisfying the generator's constraints. - public static implicit operator char(AnyChar generator) { - return generator.Generate(); - } - internal static AnyChar Create(RandomSource source) { return new AnyChar(source, null, null, null, null, null, null, []); } diff --git a/Dummies/AnyCollection.cs b/Dummies/AnyCollection.cs index ffb7be92..76b0dee2 100644 --- a/Dummies/AnyCollection.cs +++ b/Dummies/AnyCollection.cs @@ -131,8 +131,8 @@ public TSelf Containing(TItem value) { /// /// Requires the collection to contain a value drawn from at generation time — /// useful to force a particular shape of element into an otherwise arbitrary collection. Named apart from - /// because a library generator both is an and converts - /// implicitly to its value, which would make a single overloaded name ambiguous. + /// to keep the two cases legible: pins a concrete value + /// known now, whereas this method draws one from a generator when the collection is built. /// /// The generator whose drawn value the collection must contain. /// A new generator carrying the added constraint. diff --git a/Dummies/AnyDateOnly.cs b/Dummies/AnyDateOnly.cs index 82bc4abc..a8c8f449 100644 --- a/Dummies/AnyDateOnly.cs +++ b/Dummies/AnyDateOnly.cs @@ -20,16 +20,6 @@ public sealed class AnyDateOnly : IAny, IHasRandomSource, ICardinality #region Statics members declarations - /// - /// Generates the value — an can be used wherever a is expected. - /// Each conversion draws a fresh value. - /// - /// The generator to draw from. - /// An arbitrary value satisfying the generator's constraints. - public static implicit operator DateOnly(AnyDateOnly generator) { - return generator.Generate(); - } - internal static AnyDateOnly Create(RandomSource source) { return new AnyDateOnly(source, OrdinalIntervalSpec.Unconstrained("DateOnly", ordinal => V(Val(ordinal)), Ord(DateOnly.MinValue), Ord(DateOnly.MaxValue))); } diff --git a/Dummies/AnyDateTime.cs b/Dummies/AnyDateTime.cs index 1e657101..80402520 100644 --- a/Dummies/AnyDateTime.cs +++ b/Dummies/AnyDateTime.cs @@ -23,16 +23,6 @@ public sealed class AnyDateTime : IAny, IHasRandomSource, ICardinality #region Statics members declarations - /// - /// Generates the value — an can be used wherever a is - /// expected. Each conversion draws a fresh value. - /// - /// The generator to draw from. - /// An arbitrary value satisfying the generator's constraints. - public static implicit operator DateTime(AnyDateTime generator) { - return generator.Generate(); - } - internal static AnyDateTime Create(RandomSource source) { return new AnyDateTime(source, OrdinalIntervalSpec.Unconstrained("DateTime", ordinal => V(Val(ordinal)), Ord(DateTime.MinValue), Ord(DateTime.MaxValue))); } diff --git a/Dummies/AnyDateTimeOffset.cs b/Dummies/AnyDateTimeOffset.cs index 146758cb..0859094d 100644 --- a/Dummies/AnyDateTimeOffset.cs +++ b/Dummies/AnyDateTimeOffset.cs @@ -25,16 +25,6 @@ public sealed class AnyDateTimeOffset : IAny, IHasRandomSource, #region Statics members declarations - /// - /// Generates the value — an can be used wherever a - /// is expected. Each conversion draws a fresh value. - /// - /// The generator to draw from. - /// An arbitrary value satisfying the generator's constraints. - public static implicit operator DateTimeOffset(AnyDateTimeOffset generator) { - return generator.Generate(); - } - internal static AnyDateTimeOffset Create(RandomSource source) { return new AnyDateTimeOffset(source, OrdinalIntervalSpec.Unconstrained("DateTimeOffset", ordinal => V(Val(ordinal)), Ord(DateTimeOffset.MinValue), Ord(DateTimeOffset.MaxValue)), null); } diff --git a/Dummies/AnyDecimal.cs b/Dummies/AnyDecimal.cs index 948adfcc..b8d93293 100644 --- a/Dummies/AnyDecimal.cs +++ b/Dummies/AnyDecimal.cs @@ -17,16 +17,6 @@ public sealed class AnyDecimal : IAny, IHasRandomSource { #region Statics members declarations - /// - /// Generates the value — an can be used wherever a is - /// expected. Each conversion draws a fresh value. - /// - /// The generator to draw from. - /// An arbitrary value satisfying the generator's constraints. - public static implicit operator decimal(AnyDecimal generator) { - return generator.Generate(); - } - internal static AnyDecimal Create(RandomSource source) { return new AnyDecimal(source, DecimalIntervalSpec.Unconstrained("Decimal", V)); } diff --git a/Dummies/AnyDictionary.cs b/Dummies/AnyDictionary.cs index 591144a9..1ddf499d 100644 --- a/Dummies/AnyDictionary.cs +++ b/Dummies/AnyDictionary.cs @@ -20,16 +20,6 @@ public sealed class AnyDictionary : IAny> #region Statics members declarations - /// - /// Generates the value — an can be used wherever a - /// is expected. Each conversion draws a fresh dictionary. - /// - /// The generator to draw from. - /// An arbitrary dictionary satisfying the generator's constraints. - public static implicit operator Dictionary(AnyDictionary generator) { - return generator.Generate(); - } - private static string V(int value) { return value.ToString(CultureInfo.InvariantCulture); } diff --git a/Dummies/AnyDouble.cs b/Dummies/AnyDouble.cs index 16f93810..3c8f3586 100644 --- a/Dummies/AnyDouble.cs +++ b/Dummies/AnyDouble.cs @@ -16,16 +16,6 @@ public sealed class AnyDouble : IAny, IHasRandomSource { #region Statics members declarations - /// - /// Generates the value — an can be used wherever a is expected. - /// Each conversion draws a fresh value. - /// - /// The generator to draw from. - /// An arbitrary value satisfying the generator's constraints. - public static implicit operator double(AnyDouble generator) { - return generator.Generate(); - } - internal static AnyDouble Create(RandomSource source) { return new AnyDouble(source, ContinuousIntervalSpec.Unconstrained("Double", V, value => value, ContinuousIntervalSpec.NextUp, -double.MaxValue, double.MaxValue)); } diff --git a/Dummies/AnyEnum.cs b/Dummies/AnyEnum.cs index 0c3e21cc..4f0d0df1 100644 --- a/Dummies/AnyEnum.cs +++ b/Dummies/AnyEnum.cs @@ -12,16 +12,6 @@ public sealed class AnyEnum : IAny, IHasRandomSource, ICardinality #region Statics members declarations - /// - /// Generates the value — an can be used wherever a - /// is expected. Each conversion draws a fresh value. - /// - /// The generator to draw from. - /// An arbitrary value satisfying the generator's constraints. - public static implicit operator TEnum(AnyEnum generator) { - return generator.Generate(); - } - // The declared-members set of an enum type is a process constant; cached once per closed generic type // instead of reflecting on every Any.Enum() call. private static readonly TEnum[] Declared = ((TEnum[])Enum.GetValues(typeof(TEnum))).Distinct().ToArray(); diff --git a/Dummies/AnyGuid.cs b/Dummies/AnyGuid.cs index 687c2eef..a0a507ba 100644 --- a/Dummies/AnyGuid.cs +++ b/Dummies/AnyGuid.cs @@ -12,16 +12,6 @@ public sealed class AnyGuid : IAny, IHasRandomSource, ICardinalityHint { #region Statics members declarations - /// - /// Generates the value — an can be used wherever a is expected. - /// Each conversion draws a fresh value. - /// - /// The generator to draw from. - /// An arbitrary value satisfying the generator's constraints. - public static implicit operator Guid(AnyGuid generator) { - return generator.Generate(); - } - internal static AnyGuid Create(RandomSource source) { return new AnyGuid(source, null, null, null, null, []); } diff --git a/Dummies/AnyHalf.cs b/Dummies/AnyHalf.cs index 2f27efc2..21fc1cc3 100644 --- a/Dummies/AnyHalf.cs +++ b/Dummies/AnyHalf.cs @@ -18,16 +18,6 @@ public sealed class AnyHalf : IAny, IHasRandomSource { #region Statics members declarations - /// - /// Generates the value — an can be used wherever a is expected. - /// Each conversion draws a fresh value. - /// - /// The generator to draw from. - /// An arbitrary value satisfying the generator's constraints. - public static implicit operator Half(AnyHalf generator) { - return generator.Generate(); - } - internal static AnyHalf Create(RandomSource source) { return new AnyHalf(source, ContinuousIntervalSpec.Unconstrained("Half", value => V((Half)value), value => (double)(Half)value, value => NextUp((Half)value), -(double)Half.MaxValue, (double)Half.MaxValue)); } diff --git a/Dummies/AnyInt128.cs b/Dummies/AnyInt128.cs index b40047b0..1f040036 100644 --- a/Dummies/AnyInt128.cs +++ b/Dummies/AnyInt128.cs @@ -18,16 +18,6 @@ public sealed class AnyInt128 : IAny, IHasRandomSource { #region Statics members declarations - /// - /// Generates the value — an can be used wherever a is expected. - /// Each conversion draws a fresh value. - /// - /// The generator to draw from. - /// An arbitrary value satisfying the generator's constraints. - public static implicit operator Int128(AnyInt128 generator) { - return generator.Generate(); - } - internal static AnyInt128 Create(RandomSource source) { return new AnyInt128(source, WideIntervalSpec.Unconstrained("Int128", ordinal => V(Val(ordinal)), Ord(Int128.MinValue), Ord(Int128.MaxValue))); } diff --git a/Dummies/AnyInt16.cs b/Dummies/AnyInt16.cs index 48008939..1cc212ee 100644 --- a/Dummies/AnyInt16.cs +++ b/Dummies/AnyInt16.cs @@ -16,16 +16,6 @@ public sealed class AnyInt16 : IAny, IHasRandomSource, ICardinalityHint { #region Statics members declarations - /// - /// Generates the value — an can be used wherever a is expected. - /// Each conversion draws a fresh value. - /// - /// The generator to draw from. - /// An arbitrary value satisfying the generator's constraints. - public static implicit operator short(AnyInt16 generator) { - return generator.Generate(); - } - internal static AnyInt16 Create(RandomSource source) { return new AnyInt16(source, OrdinalIntervalSpec.Unconstrained("Int16", ordinal => V(Val(ordinal)), Ord(short.MinValue), Ord(short.MaxValue))); } diff --git a/Dummies/AnyInt32.cs b/Dummies/AnyInt32.cs index 9551a6d4..c8169f90 100644 --- a/Dummies/AnyInt32.cs +++ b/Dummies/AnyInt32.cs @@ -16,14 +16,14 @@ namespace Dummies; /// /// /// Instances are immutable recipes: every method returns a new generator, and the value is drawn only when -/// runs (or when the generator is implicitly converted to ), from +/// runs, from /// the random context the generator was created with. Values are built to satisfy the constraints in /// one draw — the library never generates candidates and retries. /// /// /// -/// int quantity = Any.Int32().Positive(); -/// int percentage = Any.Int32().Between(0, 100); +/// int quantity = Any.Int32().Positive().Generate(); +/// int percentage = Any.Int32().Between(0, 100).Generate(); /// Any.Int32().GreaterThan(100).LessThan(10); // throws ConflictingAnyConstraintException /// /// @@ -32,16 +32,6 @@ public sealed class AnyInt32 : IAny, IHasRandomSource, ICardinalityHint { #region Statics members declarations - /// - /// Generates the value — an can be used wherever an is expected. - /// Each conversion draws a fresh value. - /// - /// The generator to draw from. - /// An arbitrary value satisfying the generator's constraints. - public static implicit operator int(AnyInt32 generator) { - return generator.Generate(); - } - internal static AnyInt32 Create(RandomSource source) { return new AnyInt32(source, OrdinalIntervalSpec.Unconstrained("Int32", ordinal => V(Val(ordinal)), Ord(int.MinValue), Ord(int.MaxValue))); } diff --git a/Dummies/AnyInt64.cs b/Dummies/AnyInt64.cs index 548317b6..a64bd925 100644 --- a/Dummies/AnyInt64.cs +++ b/Dummies/AnyInt64.cs @@ -16,16 +16,6 @@ public sealed class AnyInt64 : IAny, IHasRandomSource, ICardinalityHint { #region Statics members declarations - /// - /// Generates the value — an can be used wherever a is expected. - /// Each conversion draws a fresh value. - /// - /// The generator to draw from. - /// An arbitrary value satisfying the generator's constraints. - public static implicit operator long(AnyInt64 generator) { - return generator.Generate(); - } - internal static AnyInt64 Create(RandomSource source) { return new AnyInt64(source, OrdinalIntervalSpec.Unconstrained("Int64", ordinal => V(Val(ordinal)), Ord(long.MinValue), Ord(long.MaxValue))); } diff --git a/Dummies/AnyList.cs b/Dummies/AnyList.cs index df85f0f5..a1259e8a 100644 --- a/Dummies/AnyList.cs +++ b/Dummies/AnyList.cs @@ -8,20 +8,6 @@ namespace Dummies; /// The element type. public sealed class AnyList : AnyCollection, AnyList> { - #region Statics members declarations - - /// - /// Generates the value — an can be used wherever a is expected. - /// Each conversion draws a fresh list. - /// - /// The generator to draw from. - /// An arbitrary list satisfying the generator's constraints. - public static implicit operator List(AnyList generator) { - return generator.Generate(); - } - - #endregion - internal AnyList(RandomSource? source, CollectionState state) : base(source, state) { } /// Requires the elements to be pairwise distinct (default equality). diff --git a/Dummies/AnySByte.cs b/Dummies/AnySByte.cs index ca39c444..9647ad62 100644 --- a/Dummies/AnySByte.cs +++ b/Dummies/AnySByte.cs @@ -16,16 +16,6 @@ public sealed class AnySByte : IAny, IHasRandomSource, ICardinalityHint { #region Statics members declarations - /// - /// Generates the value — an can be used wherever a is expected. - /// Each conversion draws a fresh value. - /// - /// The generator to draw from. - /// An arbitrary value satisfying the generator's constraints. - public static implicit operator sbyte(AnySByte generator) { - return generator.Generate(); - } - internal static AnySByte Create(RandomSource source) { return new AnySByte(source, OrdinalIntervalSpec.Unconstrained("SByte", ordinal => V(Val(ordinal)), Ord(sbyte.MinValue), Ord(sbyte.MaxValue))); } diff --git a/Dummies/AnySequence.cs b/Dummies/AnySequence.cs index 6d828cd2..8a347947 100644 --- a/Dummies/AnySequence.cs +++ b/Dummies/AnySequence.cs @@ -8,10 +8,8 @@ namespace Dummies; /// to require pairwise-distinct elements. /// /// -/// Unlike the other collection generators, an exposes no implicit conversion: -/// C# forbids a user-defined conversion whose target is an interface such as . Call -/// , or use the generator through -/// . +/// Materialize the sequence with , or use the generator +/// through . /// /// The element type. public sealed class AnySequence : AnyCollection, AnySequence> { diff --git a/Dummies/AnySet.cs b/Dummies/AnySet.cs index 99d29d2b..c890ad6d 100644 --- a/Dummies/AnySet.cs +++ b/Dummies/AnySet.cs @@ -11,20 +11,6 @@ namespace Dummies; /// The element type. public sealed class AnySet : AnyCollection, AnySet> { - #region Statics members declarations - - /// - /// Generates the value — an can be used wherever a is - /// expected. Each conversion draws a fresh set. - /// - /// The generator to draw from. - /// An arbitrary set satisfying the generator's constraints. - public static implicit operator HashSet(AnySet generator) { - return generator.Generate(); - } - - #endregion - internal AnySet(RandomSource? source, CollectionState state) : base(source, state) { } private protected override AnySet With(CollectionState state) { diff --git a/Dummies/AnySingle.cs b/Dummies/AnySingle.cs index 7bd1a9ec..499384ec 100644 --- a/Dummies/AnySingle.cs +++ b/Dummies/AnySingle.cs @@ -16,16 +16,6 @@ public sealed class AnySingle : IAny, IHasRandomSource { #region Statics members declarations - /// - /// Generates the value — an can be used wherever a is expected. - /// Each conversion draws a fresh value. - /// - /// The generator to draw from. - /// An arbitrary value satisfying the generator's constraints. - public static implicit operator float(AnySingle generator) { - return generator.Generate(); - } - internal static AnySingle Create(RandomSource source) { return new AnySingle(source, ContinuousIntervalSpec.Unconstrained("Single", value => V((float)value), value => (float)value, value => NextUp((float)value), -float.MaxValue, float.MaxValue)); } diff --git a/Dummies/AnyString.cs b/Dummies/AnyString.cs index 05e1eb4e..2a67357d 100644 --- a/Dummies/AnyString.cs +++ b/Dummies/AnyString.cs @@ -16,7 +16,7 @@ namespace Dummies; /// /// /// Instances are immutable recipes: every method returns a new generator, and the value is drawn only when -/// runs (or when the generator is implicitly converted to ), +/// runs, /// from the random context the generator was created with. Strings are built to satisfy the /// constraints — laid out as prefix + filler + contained values + filler + suffix — never generated /// and filtered. That layout means fragments never overlap: the length budget they require is the plain sum @@ -28,7 +28,7 @@ namespace Dummies; /// /// /// -/// string code = Any.String().NonEmpty().WithMaxLength(50).StartingWith("ORD-"); +/// string code = Any.String().NonEmpty().WithMaxLength(50).StartingWith("ORD-").Generate(); /// Any.String().WithLength(3).StartingWith("ORD-"); // throws ConflictingAnyConstraintException /// Any.String().Numeric().StartingWith("ORD-"); // throws ConflictingAnyConstraintException /// @@ -38,16 +38,6 @@ public sealed class AnyString : IAny, IHasRandomSource { #region Statics members declarations - /// - /// Generates the value — an can be used wherever a is expected. - /// Each conversion draws a fresh value. - /// - /// The generator to draw from. - /// An arbitrary string satisfying the generator's constraints. - public static implicit operator string(AnyString generator) { - return generator.Generate(); - } - private static string V(int value) { return value.ToString(CultureInfo.InvariantCulture); } diff --git a/Dummies/AnyTimeOnly.cs b/Dummies/AnyTimeOnly.cs index 9e5fa3cc..d72960f4 100644 --- a/Dummies/AnyTimeOnly.cs +++ b/Dummies/AnyTimeOnly.cs @@ -20,16 +20,6 @@ public sealed class AnyTimeOnly : IAny, IHasRandomSource, ICardinality #region Statics members declarations - /// - /// Generates the value — an can be used wherever a is expected. - /// Each conversion draws a fresh value. - /// - /// The generator to draw from. - /// An arbitrary value satisfying the generator's constraints. - public static implicit operator TimeOnly(AnyTimeOnly generator) { - return generator.Generate(); - } - internal static AnyTimeOnly Create(RandomSource source) { return new AnyTimeOnly(source, OrdinalIntervalSpec.Unconstrained("TimeOnly", ordinal => V(Val(ordinal)), Ord(TimeOnly.MinValue), Ord(TimeOnly.MaxValue))); } diff --git a/Dummies/AnyTimeSpan.cs b/Dummies/AnyTimeSpan.cs index 455cee8f..b6edb664 100644 --- a/Dummies/AnyTimeSpan.cs +++ b/Dummies/AnyTimeSpan.cs @@ -17,16 +17,6 @@ public sealed class AnyTimeSpan : IAny, IHasRandomSource, ICardinality #region Statics members declarations - /// - /// Generates the value — an can be used wherever a is - /// expected. Each conversion draws a fresh value. - /// - /// The generator to draw from. - /// An arbitrary value satisfying the generator's constraints. - public static implicit operator TimeSpan(AnyTimeSpan generator) { - return generator.Generate(); - } - internal static AnyTimeSpan Create(RandomSource source) { return new AnyTimeSpan(source, OrdinalIntervalSpec.Unconstrained("TimeSpan", ordinal => V(Val(ordinal)), Ord(TimeSpan.MinValue), Ord(TimeSpan.MaxValue))); } diff --git a/Dummies/AnyUInt128.cs b/Dummies/AnyUInt128.cs index 20346c15..d1617c1b 100644 --- a/Dummies/AnyUInt128.cs +++ b/Dummies/AnyUInt128.cs @@ -18,16 +18,6 @@ public sealed class AnyUInt128 : IAny, IHasRandomSource { #region Statics members declarations - /// - /// Generates the value — an can be used wherever a is expected. - /// Each conversion draws a fresh value. - /// - /// The generator to draw from. - /// An arbitrary value satisfying the generator's constraints. - public static implicit operator UInt128(AnyUInt128 generator) { - return generator.Generate(); - } - internal static AnyUInt128 Create(RandomSource source) { return new AnyUInt128(source, WideIntervalSpec.Unconstrained("UInt128", ordinal => V(Val(ordinal)), Ord(UInt128.MinValue), Ord(UInt128.MaxValue))); } diff --git a/Dummies/AnyUInt16.cs b/Dummies/AnyUInt16.cs index 41fe70f5..2586729b 100644 --- a/Dummies/AnyUInt16.cs +++ b/Dummies/AnyUInt16.cs @@ -16,16 +16,6 @@ public sealed class AnyUInt16 : IAny, IHasRandomSource, ICardinalityHint #region Statics members declarations - /// - /// Generates the value — an can be used wherever a is expected. - /// Each conversion draws a fresh value. - /// - /// The generator to draw from. - /// An arbitrary value satisfying the generator's constraints. - public static implicit operator ushort(AnyUInt16 generator) { - return generator.Generate(); - } - internal static AnyUInt16 Create(RandomSource source) { return new AnyUInt16(source, OrdinalIntervalSpec.Unconstrained("UInt16", ordinal => V(Val(ordinal)), Ord(ushort.MinValue), Ord(ushort.MaxValue))); } diff --git a/Dummies/AnyUInt32.cs b/Dummies/AnyUInt32.cs index d745d192..c1268f7a 100644 --- a/Dummies/AnyUInt32.cs +++ b/Dummies/AnyUInt32.cs @@ -16,16 +16,6 @@ public sealed class AnyUInt32 : IAny, IHasRandomSource, ICardinalityHint { #region Statics members declarations - /// - /// Generates the value — an can be used wherever a is expected. - /// Each conversion draws a fresh value. - /// - /// The generator to draw from. - /// An arbitrary value satisfying the generator's constraints. - public static implicit operator uint(AnyUInt32 generator) { - return generator.Generate(); - } - internal static AnyUInt32 Create(RandomSource source) { return new AnyUInt32(source, OrdinalIntervalSpec.Unconstrained("UInt32", ordinal => V(Val(ordinal)), Ord(uint.MinValue), Ord(uint.MaxValue))); } diff --git a/Dummies/AnyUInt64.cs b/Dummies/AnyUInt64.cs index e3311ca6..fec29a3b 100644 --- a/Dummies/AnyUInt64.cs +++ b/Dummies/AnyUInt64.cs @@ -16,16 +16,6 @@ public sealed class AnyUInt64 : IAny, IHasRandomSource, ICardinalityHint #region Statics members declarations - /// - /// Generates the value — an can be used wherever a is expected. - /// Each conversion draws a fresh value. - /// - /// The generator to draw from. - /// An arbitrary value satisfying the generator's constraints. - public static implicit operator ulong(AnyUInt64 generator) { - return generator.Generate(); - } - internal static AnyUInt64 Create(RandomSource source) { return new AnyUInt64(source, OrdinalIntervalSpec.Unconstrained("UInt64", ordinal => V(Val(ordinal)), Ord(ulong.MinValue), Ord(ulong.MaxValue))); } diff --git a/Dummies/IAny.cs b/Dummies/IAny.cs index 115cb982..b857d60c 100644 --- a/Dummies/IAny.cs +++ b/Dummies/IAny.cs @@ -17,9 +17,12 @@ namespace Dummies; /// fresh value each time. /// /// -/// Generic inference flows through this interface — Materialize(Any.String().NonEmpty()) infers -/// T = string — whereas the implicit conversions the concrete generators offer are an ergonomic -/// convenience only. +/// is the single operation that materializes a value: the concrete generators expose +/// no implicit conversion to their generated type, so a value is produced only by an explicit +/// call — directly, or through the composition seams +/// and , which call +/// it internally. Generic inference likewise flows through this interface — Materialize(Any.String().NonEmpty()) +/// infers T = string. /// /// /// The type of the generated values. diff --git a/Dummies/README.nuget.md b/Dummies/README.nuget.md index 73bda8bc..368079cd 100644 --- a/Dummies/README.nuget.md +++ b/Dummies/README.nuget.md @@ -14,15 +14,16 @@ invariant*, never what the test asserts: string code = Any.String() .NonEmpty() .WithMaxLength(50) - .StartingWith("ORD-"); + .StartingWith("ORD-") + .Generate(); Read it as: *any* string that satisfies these constraints. The exact value does not matter — and that is the point. ## What's inside -- **Fluent, typed generators** implementing `IAny`, with implicit conversion to - the generated type, across the .NET simple types: `String`, `Char`, every integer +- **Fluent, typed generators** implementing `IAny`, materialized through + `.Generate()`, across the .NET simple types: `String`, `Char`, every integer width (`SByte`/`Byte`/`Int16`/`UInt16`/`Int32`/`UInt32`/`Int64`/`UInt64`), `Double`/`Single`/`Decimal` (finite values only — never NaN or infinities), `Bool`, `Guid`, `Enum` (declared members only), `TimeSpan`, `DateTime` (UTC) diff --git a/doc/handwritten/for-maintainers/adr/0020-materialize-dummies-only-through-generate.fr.md b/doc/handwritten/for-maintainers/adr/0020-materialize-dummies-only-through-generate.fr.md new file mode 100644 index 00000000..af6f327a --- /dev/null +++ b/doc/handwritten/for-maintainers/adr/0020-materialize-dummies-only-through-generate.fr.md @@ -0,0 +1,160 @@ +# ADR-0020 | Matérialiser les dummies uniquement via Generate() + +🌍 🇬🇧 [English](0020-materialize-dummies-only-through-generate.md) · 🇫🇷 Français (ce fichier) + +**Statut :** Accepté +**Date :** 2026-07-19 +**Décideurs :** Reefact + +## Contexte + +`Dummies` est une DSL fluide de générateurs typés porteurs de contraintes. Chaque +générateur implémente `IAny`, dont l'unique membre `Generate()` tire une valeur +satisfaisant les contraintes déclarées ; les points de composition `As` et +`Combine` construisent des générateurs plus larges et matérialisent leurs parties +en appelant `Generate()`. Le modèle affiché de la bibliothèque est qu'un générateur +est une **recette immuable, pas une valeur** : l'aléatoire n'est tiré que lorsque +`Generate()` s'exécute, et une même recette peut être générée plusieurs fois, en +produisant une valeur fraîche à chaque fois. + +Jusqu'ici, chaque générateur concret définissait aussi une **conversion +implicite** vers son type généré — 28 opérateurs au total, un par type simple et +un par type de collection (`List`, `T[]`, `HashSet`, +`Dictionary`). Cette conversion rendait une affectation à type +explicite concise, par exemple une variable locale `string` affectée directement +depuis un générateur de chaînes. + +Plusieurs faits sur cette conversion ont émergé lors d'une revue ciblée de la +bibliothèque (issue #190) : + +* La conversion a des **effets de bord** : elle tire de l'aléatoire, ce n'est donc + pas un élargissement ; elle peut **lever une exception** + (`AnyGenerationException`, `ConflictingAnyConstraintException`) sur un site qui + se lit comme une simple affectation ; et elle n'est **pas idempotente** — chaque + conversion tire une valeur fraîche, si bien que lire deux fois la « même » + variable donne deux valeurs. +* La conversion ne se déclenche que dans **une** forme syntaxique — une variable + locale ou un paramètre à type explicite. Dans les formes voisines, elle fait + silencieusement autre chose : `var` lie le générateur, `object` et + `params object[]` boxent le générateur, l'inférence générique passe le + générateur, et des surcharges concurrentes peuvent se résoudre vers le + générateur plutôt que la valeur. La suite de tests devait déjà utiliser des + locales à type explicite autour d'une API `params object[]` pour cette raison. +* `Generate()` fonctionne déjà de manière uniforme dans chacun de ces contextes, + est le membre par lequel passe l'inférence générique, et est l'opération + qu'utilisent les points de composition. C'est l'idiome dominant dans la suite de + tests. + +Deux contraintes bornent le calendrier. `Dummies` est un package autonome +pré-1.0 dont l'API évoluera le plus dans ses premières itérations, et il n'est +référencé que par son propre projet de test (ADR-0011) ; retirer une surface +d'opérateurs publique est donc peu coûteux maintenant et deviendrait un +changement cassant une fois un `1.0` stable publié. L'issue #190 exige aussi que +le contrat de ces conversions soit décidé et consigné avant ce `1.0`. + +## Décision + +Les générateurs concrets de `Dummies` n'exposent aucune conversion implicite vers +leur type généré : une valeur n'est matérialisée que par `Generate()`, appelé +directement ou par les points de composition `As` et `Combine` qui l'appellent en +interne. + +## Justification + +* **Une conversion implicite devrait être bon marché, totale et + référentiellement transparente ; celle-ci n'est aucune des trois.** Parce + qu'elle tire de l'aléatoire, peut lever une exception et renvoie une valeur + différente à chaque exécution, c'est un appel de méthode à effet de bord + déguisé derrière une affectation. Cela contredit directement le modèle que la + bibliothèque enseigne — un générateur est une recette, et la valeur n'est tirée + qu'à `Generate()` — en fournissant l'unique chemin qui laisse l'appelant oublier + que le tirage a lieu. +* **La commodité est une abstraction partielle et surprenante.** Elle se comporte + comme annoncé dans une seule forme syntaxique et se comporte silencieusement mal + dans les formes voisines. La garder en documentant le piège décrirait une + complexité accidentelle au lieu de la retirer ; la complexité est accidentelle + précisément parce que `Generate()` couvre déjà tous les contextes de manière + uniforme. +* **Le retrait ne coûte aucune capacité.** `Generate()` est déjà le chemin + canonique — au niveau de l'interface, cible de l'inférence générique, opération + qu'appellent les points de composition, et idiome dominant dans la suite. Ce qui + est perdu est un raccourci qui économisait un appel dans un seul contexte, pas + une quelconque expressivité. +* **C'est le moment le moins coûteux pour décider.** Le package est pré-1.0, + autonome et auto-consommé (ADR-0011), donc le changement ne touche aujourd'hui + que ses propres tests ; le même retrait après un `1.0` stable casserait chaque + consommateur qui affectait un générateur à une locale typée. L'issue #190 exige + que la décision soit consignée avant cette publication. + +## Alternatives considérées + +### Garder les conversions et documenter le contrat + +La direction que privilégie l'issue #190. Envisagée parce qu'elle préserve le site +d'appel vedette concis et indique, en documentation, où la conversion s'exécute ou +non. Rejetée parce qu'elle documente un piège au lieu d'en retirer un, et conserve +une conversion à effet de bord, non idempotente et pouvant lever une exception qui +contredit le modèle recette-contre-valeur au centre de la bibliothèque. + +### Garder les conversions et ajouter un analyzer pour les contextes trompeurs + +Envisagée parce qu'un analyzer signalant les usages `var`, `object` et par +inférence générique pourrait préserver l'ergonomie tout en attrapant les pièges. +Rejetée parce que c'est une surface large et permanente — 28 opérateurs plus un +analyzer et ses tests — pour préserver un raccourci d'un appel, et parce qu'un +contrat « convertit, sauf là où l'analyzer dit que non » est lui-même +schizophrène. Retirer les opérateurs rend l'analyzer sans objet, ce pourquoi +l'issue #190 le liste comme optionnel. + +### Retirer les conversions de certains types seulement + +Envisagée comme compromis — par exemple les garder sur les types simples immuables +et ne les retirer que des collections. Rejetée parce qu'une règle par type est +plus difficile à expliquer que l'un ou l'autre choix uniforme, et laisse quand +même la surprise de l'affectation à effet de bord sur les types qui les gardent. + +## Conséquences + +### Positives + +* Il existe une seule façon évidente et uniforme de matérialiser une valeur, et la + distinction recette-contre-valeur que la bibliothèque enseigne n'est plus + contredite par une fonctionnalité qui masque le tirage. +* Un générateur ne se substitue jamais silencieusement à sa valeur sous `var`, + `object`, `params object[]`, inférence générique ou résolution de surcharge ; + ces sites échouent désormais à la compilation au lieu de mal se comporter. + +### Négatives + +* Le site d'appel vedette est plus verbeux : une affectation à type explicite gagne + un `.Generate()`. +* Vingt-huit opérateurs, ainsi que leurs tests et exemples de documentation, sont + retirés ; la surface publique pré-1.0 change — acceptable maintenant, et la + raison pour laquelle la décision est prise avant le `1.0`. + +### Risques + +* Un utilisateur portant un modèle mental de conversion implicite pourrait au + début omettre `.Generate()`. Le risque est borné : l'omission est une erreur de + compilation au message actionnable (affecter via `IAny` ou appeler + `Generate()`), jamais une valeur fausse silencieuse. + +## Actions de suivi + +* Mettre à jour la documentation pour que `.Generate()` soit présenté comme + l'unique matérialisation — le README du package et les docs XML — fait dans le + même changement que cette décision. +* Ne pas poursuivre l'analyzer optionnel suggéré par l'issue #190 ; le retrait le + rend inutile. +* À revisiter seulement si un futur consommateur hors tests démontre un besoin + ergonomique que la forme `Generate()` ne peut satisfaire. + +## Références + +* Issue #190 — Définir et documenter le contrat des conversions implicites de + générateurs. +* ADR-0011 — Héberger Dummies comme package autonome (churn pré-1.0, + auto-consommé). +* ADR-0006 — Fournir des valeurs de test arbitraires depuis une source unique à + graine. +* `Dummies/IAny.cs` — le contrat `Generate()` par lequel passent ces générateurs. diff --git a/doc/handwritten/for-maintainers/adr/0020-materialize-dummies-only-through-generate.md b/doc/handwritten/for-maintainers/adr/0020-materialize-dummies-only-through-generate.md new file mode 100644 index 00000000..aa56ac94 --- /dev/null +++ b/doc/handwritten/for-maintainers/adr/0020-materialize-dummies-only-through-generate.md @@ -0,0 +1,148 @@ +# ADR-0020 | Materialize dummies only through Generate() + +🌍 🇬🇧 English (this file) · 🇫🇷 [Français](0020-materialize-dummies-only-through-generate.fr.md) + +**Status:** Accepted +**Date:** 2026-07-19 +**Decision Makers:** Reefact + +## Context + +`Dummies` is a fluent DSL of typed, constraint-carrying generators. Every +generator implements `IAny`, whose single member `Generate()` draws one value +satisfying the declared constraints; the composition seams `As` and `Combine` +build larger generators and materialize their parts by calling `Generate()`. The +library's stated model is that a generator is an **immutable recipe, not a +value**: randomness is drawn only when `Generate()` runs, and the same recipe can +be generated from several times, yielding a fresh value each time. + +Until now, each concrete generator also defined an **implicit conversion** to its +generated type — 28 operators in total, one per simple type and one per +collection type (`List`, `T[]`, `HashSet`, `Dictionary`). That +conversion let an explicitly-typed assignment read tersely, for example a +`string` local assigned directly from a string generator. + +Several facts about that conversion were surfaced during a focused review of the +library (issue #190): + +* The conversion has **side effects**: it draws randomness, so it is not a + widening; it can **throw** (`AnyGenerationException`, + `ConflictingAnyConstraintException`) at a site that reads like a plain + assignment; and it is **not idempotent** — each conversion draws a fresh value, + so reading the "same" variable twice yields two values. +* The conversion fires in only **one** syntactic shape — an explicitly-typed + local or parameter. In the adjacent shapes it silently does something else: + `var` binds the generator, `object` and `params object[]` box the generator, + generic inference passes the generator, and competing overloads can resolve to + the generator rather than the value. The test suite already had to use + explicitly-typed locals around a `params object[]` API for this reason. +* `Generate()` already works uniformly in every one of those contexts, is the + member generic inference flows through, and is the operation the composition + seams use. It is the dominant idiom across the test suite. + +Two constraints bound the timing. `Dummies` is a pre-1.0, standalone package +whose API is expected to churn most in its early iterations, and it is referenced +only by its own test project (ADR-0011); removing a public operator surface is +therefore cheap now and a breaking change once a stable `1.0` is published. Issue +#190 also requires the contract of these conversions to be decided and recorded +before that `1.0`. + +## Decision + +Concrete `Dummies` generators expose no implicit conversion to their generated +type: a value is materialized only by `Generate()`, called directly or by the +`As` and `Combine` composition seams that call it internally. + +## Rationale + +* **An implicit conversion should be cheap, total, and referentially + transparent; this one is none of those.** Because it draws randomness, can + throw, and returns a different value on each run, it is an effectful method + call disguised behind an assignment. That directly contradicts the model the + library teaches — a generator is a recipe, and the value is drawn only at + `Generate()` — by providing the one path that lets a caller forget the draw is + happening at all. +* **The convenience is a partial, surprising abstraction.** It behaves as + advertised in a single syntactic shape and silently misbehaves in the shapes + next to it. Keeping it and documenting the hazard would describe an accidental + complexity rather than remove it; the complexity is accidental precisely + because `Generate()` already covers every context uniformly. +* **Removal costs no capability.** `Generate()` is already the canonical path — + interface-level, the target of generic inference, the operation the + composition seams call, and the dominant idiom in the suite. What is lost is a + shorthand that saved one call in one context, not any expressiveness. +* **This is the cheapest moment to decide.** The package is pre-1.0, standalone, + and self-consumed (ADR-0011), so the change touches only its own tests today; + the same removal after a stable `1.0` would break every consumer that assigned + a generator to a typed local. Issue #190 requires the decision to be recorded + before that release. + +## Alternatives Considered + +### Keep the conversions and document the contract + +The direction issue #190 leads with. Considered because it preserves the terse +headline call site and states, in documentation, where the conversion does and +does not run. Rejected because it documents a hazard instead of removing one, and +keeps an effectful, non-idempotent, throwing conversion that contradicts the +recipe-versus-value model at the center of the library. + +### Keep the conversions and add an analyzer for the misleading contexts + +Considered because an analyzer flagging `var`, `object`, and generic-inference +uses could preserve the ergonomics while catching the traps. Rejected because it +is a large, permanent surface — 28 operators plus an analyzer and its tests — to +preserve a one-call shorthand, and a "converts, except where the analyzer says it +does not" contract is itself split-brained. Removing the operators makes the +analyzer moot, which is why issue #190 lists it as optional. + +### Remove the conversions only from some types + +Considered as a compromise — for example keeping them on immutable simple types +and dropping them only on collections. Rejected because a per-type rule is harder +to explain than either uniform choice and still leaves the effectful-assignment +surprise on the types that keep it. + +## Consequences + +### Positive + +* There is one obvious, uniform way to materialize a value, and the + recipe-versus-value distinction the library teaches is no longer contradicted + by a feature that hides the draw. +* A generator never silently stands in for its value under `var`, `object`, + `params object[]`, generic inference, or overload resolution; those sites now + fail to compile instead of misbehaving. + +### Negative + +* The headline call site is more verbose: an explicitly-typed assignment gains a + `.Generate()`. +* Twenty-eight operators, along with their tests and documentation examples, are + removed; the pre-1.0 public surface changes — acceptable now, and the reason + the decision is taken before `1.0`. + +### Risks + +* A user carrying an implicit-conversion mental model may at first omit + `.Generate()`. The risk is bounded: the omission is a compile-time error with + an actionable message (assign through `IAny` or call `Generate()`), never a + silent wrong value. + +## Follow-up Actions + +* Update the documentation so `.Generate()` is presented as the sole + materialization — the package README and the XML docs — done in the same + change as this decision. +* Do not pursue the optional analyzer suggested in issue #190; the removal makes + it unnecessary. +* Revisit only if a future, non-test consumer demonstrates an ergonomic need the + `Generate()` form cannot meet. + +## References + +* Issue #190 — Define and document the contract of implicit generator + conversions. +* ADR-0011 — Host Dummies as a standalone package (pre-1.0 churn, self-consumed). +* ADR-0006 — Supply arbitrary test values from a single seedable source. +* `Dummies/IAny.cs` — the `Generate()` contract these generators flow through. diff --git a/doc/handwritten/for-maintainers/adr/README.md b/doc/handwritten/for-maintainers/adr/README.md index e19c71c3..88341100 100644 --- a/doc/handwritten/for-maintainers/adr/README.md +++ b/doc/handwritten/for-maintainers/adr/README.md @@ -193,3 +193,4 @@ Optional supporting material: | [ADR-0017](0017-provide-a-configurable-application-wide-default-for-the-binder-options.md) | Provide a configurable application-wide default for the binder options | Accepted | | [ADR-0018](0018-bundle-the-binders-structural-error-code-and-messages.md) | Bundle the binder's structural error code and messages in one definition | Accepted | | [ADR-0019](0019-document-overridden-binder-errors-in-the-consumers-catalog.md) | Document overridden binder errors in the consumer's own catalog | Accepted | +| [ADR-0020](0020-materialize-dummies-only-through-generate.md) | Materialize dummies only through Generate() | Accepted |