From 0061813e8c1f2be30e702a0805610af2b0f5214e Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 19 Jul 2026 09:17:59 +0000 Subject: [PATCH 1/3] docs(dummies): draft ADR-0020 for Generate-only materialization Record the decision to drop the implicit generator conversions and make Generate() the single materialization, taken before the standalone Dummies package reaches a stable 1.0. Drafted as Proposed for the maintainer to accept; the implementation lands separately. Refs: #190 Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_011NjuZBGDpqfLRvqzVPt34T --- ...ialize-dummies-only-through-generate.fr.md | 160 ++++++++++++++++++ ...terialize-dummies-only-through-generate.md | 148 ++++++++++++++++ doc/handwritten/for-maintainers/adr/README.md | 1 + 3 files changed, 309 insertions(+) create mode 100644 doc/handwritten/for-maintainers/adr/0020-materialize-dummies-only-through-generate.fr.md create mode 100644 doc/handwritten/for-maintainers/adr/0020-materialize-dummies-only-through-generate.md 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..49e2869a --- /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 :** Proposé +**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..38de32dd --- /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:** Proposed +**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..7c5fc922 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() | Proposed | From a688a9e7c1768c8c85f240080a93d2b4e772e523 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 19 Jul 2026 09:17:59 +0000 Subject: [PATCH 2/3] refactor(dummies): remove implicit generator conversions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The concrete generators defined an implicit conversion to their generated type. That conversion draws randomness, can throw, and yields a fresh value on every run — an effectful, non-idempotent call disguised behind an assignment, which contradicts the recipe-versus-value model the library teaches, and which fires only in explicitly-typed contexts while silently passing the generator under var, object, params object[], generic inference and overload resolution. Generate() already materializes uniformly in every context and is the path the As/Combine seams use, so removing the operators loses no capability. The package is pre-1.0, standalone and referenced only by its own tests, so this is the cheapest moment to settle the contract. Documentation and tests are updated to present Generate() as the sole materialization. See ADR-0020. Refs: #190 Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_011NjuZBGDpqfLRvqzVPt34T --- Dummies.UnitTests/AnyCollectionTests.cs | 34 +++++----- Dummies.UnitTests/AnyContinuousTests.cs | 6 +- Dummies.UnitTests/AnySetTypeTests.cs | 2 +- Dummies.UnitTests/AnySignedIntegerTests.cs | 6 +- Dummies.UnitTests/AnyUnsignedIntegerTests.cs | 8 +-- Dummies.UnitTests/ImplicitConversionTests.cs | 52 -------------- Dummies.UnitTests/MaterializationTests.cs | 68 +++++++++++++++++++ Dummies.UnitTests/SeedReproducibilityTests.cs | 44 ++++++------ Dummies/Any.cs | 2 +- Dummies/AnyArray.cs | 14 ---- Dummies/AnyBool.cs | 10 --- Dummies/AnyByte.cs | 10 --- Dummies/AnyChar.cs | 10 --- Dummies/AnyCollection.cs | 4 +- Dummies/AnyDateOnly.cs | 10 --- Dummies/AnyDateTime.cs | 10 --- Dummies/AnyDateTimeOffset.cs | 10 --- Dummies/AnyDecimal.cs | 10 --- Dummies/AnyDictionary.cs | 10 --- Dummies/AnyDouble.cs | 10 --- Dummies/AnyEnum.cs | 10 --- Dummies/AnyGuid.cs | 10 --- Dummies/AnyHalf.cs | 10 --- Dummies/AnyInt128.cs | 10 --- Dummies/AnyInt16.cs | 10 --- Dummies/AnyInt32.cs | 16 +---- Dummies/AnyInt64.cs | 10 --- Dummies/AnyList.cs | 14 ---- Dummies/AnySByte.cs | 10 --- Dummies/AnySequence.cs | 6 +- Dummies/AnySet.cs | 14 ---- Dummies/AnySingle.cs | 10 --- Dummies/AnyString.cs | 14 +--- Dummies/AnyTimeOnly.cs | 10 --- Dummies/AnyTimeSpan.cs | 10 --- Dummies/AnyUInt128.cs | 10 --- Dummies/AnyUInt16.cs | 10 --- Dummies/AnyUInt32.cs | 10 --- Dummies/AnyUInt64.cs | 10 --- Dummies/IAny.cs | 9 ++- Dummies/README.nuget.md | 7 +- 41 files changed, 138 insertions(+), 412 deletions(-) delete mode 100644 Dummies.UnitTests/ImplicitConversionTests.cs create mode 100644 Dummies.UnitTests/MaterializationTests.cs 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) From 8ea5c5fbd6520e55a6701d0900861eb6ea770a7a Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 19 Jul 2026 15:10:00 +0000 Subject: [PATCH 3/3] docs(dummies): accept ADR-0020 for Generate-only materialization The maintainer accepts the decision to drop the implicit generator conversions and materialize dummies only through Generate(). Flip the status from Proposed to Accepted in both language versions and the index. Refs: #190 Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_011NjuZBGDpqfLRvqzVPt34T --- .../adr/0020-materialize-dummies-only-through-generate.fr.md | 2 +- .../adr/0020-materialize-dummies-only-through-generate.md | 2 +- doc/handwritten/for-maintainers/adr/README.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) 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 index 49e2869a..af6f327a 100644 --- 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 @@ -2,7 +2,7 @@ 🌍 🇬🇧 [English](0020-materialize-dummies-only-through-generate.md) · 🇫🇷 Français (ce fichier) -**Statut :** Proposé +**Statut :** Accepté **Date :** 2026-07-19 **Décideurs :** Reefact 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 index 38de32dd..aa56ac94 100644 --- 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 @@ -2,7 +2,7 @@ 🌍 🇬🇧 English (this file) · 🇫🇷 [Français](0020-materialize-dummies-only-through-generate.fr.md) -**Status:** Proposed +**Status:** Accepted **Date:** 2026-07-19 **Decision Makers:** Reefact diff --git a/doc/handwritten/for-maintainers/adr/README.md b/doc/handwritten/for-maintainers/adr/README.md index 7c5fc922..88341100 100644 --- a/doc/handwritten/for-maintainers/adr/README.md +++ b/doc/handwritten/for-maintainers/adr/README.md @@ -193,4 +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() | Proposed | +| [ADR-0020](0020-materialize-dummies-only-through-generate.md) | Materialize dummies only through Generate() | Accepted |