From 2b39034e7d706faaaaf02893d70d07847826f6de Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 21 Jul 2026 21:38:52 +0000 Subject: [PATCH 1/3] feat(dummies): add top-level OneOf/ElementOf choice combinator Picking an arbitrary element from a caller-held set was the one common dummy need with no seed-aware answer: users hand-rolled pool[new Random().Next(...)], which ignores the ambient source and silently breaks Reproducibly for that draw. The typed builders' OneOf only narrows within a scalar's own domain; there was no way to draw from a pool of arbitrary domain objects. Add AnyOneOf, a terminal generator drawing one value uniformly from an explicit pool, and the top-level factories Any.OneOf(params T[]), Any.ElementOf(IReadOnlyList) and Any.ElementOf(IEnumerable) (the sequence overload materializes once), mirrored on AnyContext so the seeded surface carries them too. Two names avoid the params-generic inference footgun: OneOf for inline literals, ElementOf for a held collection. The pool is deduplicated, sized and membership-tested under EqualityComparer.Default - the type-agnostic analogue of the string generator's ordinal dedup, and a sound upper bound for the distinct- collection cardinality gate (ADR-0013). An empty pool throws eagerly; a null element is rejected and the message points at OrNull(), keeping nullability orthogonal and symmetric with Any.String().OneOf (ADR-0030). The Any<->AnyContext mirror is enforced by SurfaceParityTests; the generic, parameterized factories are excluded from the CLR-naming and cross-engine guards by construction. Covered by AnyOneOfTests. Record the decision as ADR-0032 (Proposed): draw arbitrary values from an explicit, top-level choice pool. Refs: #223 Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01LQ15y5om7Gagin9ZpYXDWf --- Dummies.UnitTests/AnyOneOfTests.cs | 190 ++++++++++++++++++ Dummies/Any.cs | 58 ++++++ Dummies/AnyContext.cs | 46 +++++ Dummies/AnyOneOf.cs | 75 +++++++ ...lues-from-an-explicit-top-level-pool.fr.md | 165 +++++++++++++++ ...-values-from-an-explicit-top-level-pool.md | 152 ++++++++++++++ doc/handwritten/for-maintainers/adr/README.md | 1 + 7 files changed, 687 insertions(+) create mode 100644 Dummies.UnitTests/AnyOneOfTests.cs create mode 100644 Dummies/AnyOneOf.cs create mode 100644 doc/handwritten/for-maintainers/adr/0032-draw-arbitrary-values-from-an-explicit-top-level-pool.fr.md create mode 100644 doc/handwritten/for-maintainers/adr/0032-draw-arbitrary-values-from-an-explicit-top-level-pool.md diff --git a/Dummies.UnitTests/AnyOneOfTests.cs b/Dummies.UnitTests/AnyOneOfTests.cs new file mode 100644 index 00000000..65ea6d33 --- /dev/null +++ b/Dummies.UnitTests/AnyOneOfTests.cs @@ -0,0 +1,190 @@ +#region Usings declarations + +using JetBrains.Annotations; + +using NFluent; + +#endregion + +namespace Dummies.UnitTests; + +[TestSubject(typeof(AnyOneOf<>))] +public sealed class AnyOneOfTests { + + private const int SampleCount = 200; + + #region Statics members declarations + + private static IEnumerable Samples(IAny generator) { + for (int i = 0; i < SampleCount; i++) { + yield return generator.Generate(); + } + } + + #endregion + + [Fact(DisplayName = "OneOf draws only the supplied values, including domain objects.")] + public void DrawsOnlyTheSuppliedValues() { + Percentage ten = Percentage.Create(10); + Percentage twenty = Percentage.Create(20); + Percentage thirty = Percentage.Create(30); + Percentage[] allowed = [ten, twenty, thirty]; + + foreach (Percentage value in Samples(Any.OneOf(ten, twenty, thirty))) { + Check.That(allowed.Contains(value)).IsTrue(); + } + } + + [Fact(DisplayName = "OneOf eventually reaches every supplied value.")] + public void ReachesEverySuppliedValue() { + HashSet seen = new(Samples(Any.OneOf(1, 2, 3))); + + Check.That(seen).Contains(1, 2, 3); + } + + [Fact(DisplayName = "A single value pins the generated value.")] + public void SingleValueIsPinned() { + foreach (int value in Samples(Any.OneOf(42))) { + Check.That(value).IsEqualTo(42); + } + } + + [Fact(DisplayName = "OneOf varies from draw to draw when the pool holds more than one value.")] + public void VariesAcrossDraws() { + HashSet seen = new(Samples(Any.OneOf(1, 2, 3, 4))); + + Check.That(seen.Count).IsStrictlyGreaterThan(1); + } + + [Fact(DisplayName = "Duplicate values are collapsed under the default comparer: both distinct values are still drawn, nothing else.")] + public void DuplicatesAreCollapsed() { + HashSet seen = new(Samples(Any.OneOf(1, 1, 2))); + + Check.That(seen).IsOnlyMadeOf(1, 2); + Check.That(seen).Contains(1, 2); + } + + [Fact(DisplayName = "OneOf is reproducible under a seed.")] + public void ReproducibleUnderASeed() { + string first = string.Join("|", Enumerable.Range(0, 20).Select(_ => Any.WithSeed(7).OneOf("a", "b", "c", "d").Generate())); + string second = string.Join("|", Enumerable.Range(0, 20).Select(_ => Any.WithSeed(7).OneOf("a", "b", "c", "d").Generate())); + + Check.That(second).IsEqualTo(first); + } + + [Fact(DisplayName = "OneOf composes into a value object through As.")] + public void ComposesThroughAs() { + IAny generator = Any.OneOf("ORD-12345678", "ORD-87654321").As(OrderReference.Create); + + for (int i = 0; i < SampleCount; i++) { + OrderReference reference = generator.Generate(); + Check.That(reference.Value).StartsWith("ORD-"); + Check.That(reference.Value.Length).IsEqualTo(12); + } + } + + [Fact(DisplayName = "OrNull makes the pool generator null about half the time, otherwise a member of the pool.")] + public void OrNullIsSometimesNull() { + Percentage one = Percentage.Create(1); + Percentage two = Percentage.Create(2); + IAny generator = Any.WithSeed(20260721).OneOf(one, two).OrNull(); + + List values = new(); + for (int i = 0; i < SampleCount; i++) { + values.Add(generator.Generate()); + } + + Check.That(values.Any(value => value is null)).IsTrue(); + Check.That(values.Where(value => value is not null)).IsOnlyMadeOf(one, two); + } + + [Fact(DisplayName = "A distinct set over OneOf is gated by the pool's cardinality, both ways.")] + public void CardinalityGatesDistinctCollections() { + // Two distinct values cannot fill a set of three: caught eagerly, like any cardinality conflict. + Check.ThatCode(() => Any.SetOf(Any.OneOf(1, 2)).WithCount(3)).Throws(); + + // Within the domain it fills the set with the requested distinct values. + HashSet set = Any.SetOf(Any.OneOf(1, 2, 3)).WithCount(3).Generate(); + Check.That(set.Count).IsEqualTo(3); + Check.That(set).IsOnlyMadeOf(1, 2, 3); + } + + [Fact(DisplayName = "Reference identity keeps equal-valued but distinct instances as separate pool members.")] + public void ReferenceIdentityKeepsDistinctInstancesDistinct() { + // Percentage has no value equality, so two instances of the same percentage are distinct under the default + // comparer — the pool's cardinality is two, and a set of two is fillable. + Percentage first = Percentage.Create(50); + Percentage second = Percentage.Create(50); + + HashSet set = Any.SetOf(Any.OneOf(first, second)).WithCount(2).Generate(); + + Check.That(set.Count).IsEqualTo(2); + Check.That(set).IsOnlyMadeOf(first, second); + } + + [Fact(DisplayName = "OneOf rejects empty, null, or null-containing pools as arguments — null goes through OrNull.")] + public void RejectsInvalidPools() { + Check.ThatCode(() => Any.OneOf()).Throws(); + Check.ThatCode(() => Any.OneOf((string[])null!)).Throws(); + Check.ThatCode(() => Any.OneOf("a", null!)).Throws(); + } + + [Fact(DisplayName = "The null-element message points the caller at OrNull().")] + public void NullElementMessagePointsAtOrNull() { + ArgumentException error = Assert.Throws(() => Any.OneOf("a", null!)); + + Check.That(error.Message).Contains("OrNull"); + } + + [Fact(DisplayName = "ElementOf draws only from the list it is given.")] + public void ElementOfDrawsFromTheList() { + IReadOnlyList pool = new List { 1, 2, 3 }; + + HashSet seen = new(Samples(Any.ElementOf(pool))); + + Check.That(seen).IsOnlyMadeOf(1, 2, 3); + Check.That(seen.Count).IsStrictlyGreaterThan(1); + } + + [Fact(DisplayName = "ElementOf materializes a lazy sequence once, not once per draw.")] + public void ElementOfMaterializesTheSequenceOnce() { + int enumerations = 0; + + IEnumerable Source() { + enumerations++; + yield return 1; + yield return 2; + yield return 3; + } + + AnyOneOf generator = Any.ElementOf(Source()); + for (int i = 0; i < SampleCount; i++) { + generator.Generate(); + } + + Check.That(enumerations).IsEqualTo(1); + } + + [Fact(DisplayName = "ElementOf validates null, empty and null elements like OneOf, for both the list and the sequence overload.")] + public void ElementOfValidatesItsPool() { + Check.ThatCode(() => Any.ElementOf((IReadOnlyList)null!)).Throws(); + Check.ThatCode(() => Any.ElementOf((IEnumerable)null!)).Throws(); + Check.ThatCode(() => Any.ElementOf(new List())).Throws(); + Check.ThatCode(() => Any.ElementOf(Enumerable.Empty())).Throws(); + Check.ThatCode(() => Any.ElementOf(new List { "a", null! })).Throws(); + } + + [Fact(DisplayName = "A seeded context makes OneOf and ElementOf deterministic — the mirrored surface draws from the context's seed.")] + public void SeededContextIsDeterministic() { + List pool = new() { 10, 20, 30, 40 }; + + string oneOfFirst = string.Join("|", Samples(Any.WithSeed(11).OneOf(10, 20, 30, 40)).Take(20)); + string oneOfSecond = string.Join("|", Samples(Any.WithSeed(11).OneOf(10, 20, 30, 40)).Take(20)); + Check.That(oneOfSecond).IsEqualTo(oneOfFirst); + + string elementFirst = string.Join("|", Samples(Any.WithSeed(11).ElementOf(pool)).Take(20)); + string elementSecond = string.Join("|", Samples(Any.WithSeed(11).ElementOf(pool)).Take(20)); + Check.That(elementSecond).IsEqualTo(elementFirst); + } + +} diff --git a/Dummies/Any.cs b/Dummies/Any.cs index bcd758ed..5f0eb8b0 100644 --- a/Dummies/Any.cs +++ b/Dummies/Any.cs @@ -317,6 +317,64 @@ public static AnyHalf Half() { } #endif + /// + /// Draws an arbitrary value from an explicit pool of caller-supplied , over the + /// ambient random context — the top-level choice combinator for a value whose domain is a closed set the test + /// does not assert on ("one of the configured currencies", "one of the states in this table"). The pool is the + /// whole specification, so the returned generator is terminal, yet it still composes through As(...), + /// OrNull(), Combine(...) and the collection generators like any other. To draw from a pool + /// already held as a collection, use . + /// + /// + /// The draw is uniform and reproducible under a seed; duplicates collapse under + /// so no value is implicitly weighted, and the distinct count is + /// advertised so a distinct collection over the pool gates eagerly. A null element is rejected — make the + /// whole generator nullable with OrNull() instead of placing null in the pool. + /// + /// The pool the generated value is drawn from; duplicates are ignored. + /// The type of the pooled values. + /// A terminal generator drawing uniformly from . + /// Thrown when is null. + /// Thrown when is empty or contains a null element. + public static AnyOneOf OneOf(params T[] values) { + if (values is null) { throw new ArgumentNullException(nameof(values)); } + + return AnyOneOf.FromPool(AmbientRandomSource.Instance, values); + } + + /// + /// Draws an arbitrary value from an explicit pool held as a list — the + /// counterpart of , for a pool already materialized (a configuration list, a + /// fixture, a lookup table of domain objects). Same terminal contract as : the pool + /// is the whole specification, duplicates collapse, and the draw is uniform and reproducible under a seed. + /// + /// The pool the generated value is drawn from; duplicates are ignored. + /// The type of the pooled values. + /// A terminal generator drawing uniformly from . + /// Thrown when is null. + /// Thrown when is empty or contains a null element. + public static AnyOneOf ElementOf(IReadOnlyList values) { + if (values is null) { throw new ArgumentNullException(nameof(values)); } + + return AnyOneOf.FromPool(AmbientRandomSource.Instance, values); + } + + /// + /// Draws an arbitrary value from an explicit pool held as a sequence — the + /// counterpart of (a LINQ result, values loaded at setup). The + /// sequence is materialized once, so a lazy query is never re-enumerated per draw. + /// + /// The pool the generated value is drawn from; duplicates are ignored. + /// The type of the pooled values. + /// A terminal generator drawing uniformly from . + /// Thrown when is null. + /// Thrown when is empty or contains a null element. + public static AnyOneOf ElementOf(IEnumerable values) { + if (values is null) { throw new ArgumentNullException(nameof(values)); } + + return AnyOneOf.FromPool(AmbientRandomSource.Instance, values as IReadOnlyList ?? values.ToArray()); + } + /// /// Creates an isolated, deterministic generation context: every generator created from it draws from a /// dedicated source seeded with , independent of the ambient context. Two contexts diff --git a/Dummies/AnyContext.cs b/Dummies/AnyContext.cs index ea3b9641..e144c2ae 100644 --- a/Dummies/AnyContext.cs +++ b/Dummies/AnyContext.cs @@ -295,4 +295,50 @@ public AnyHalf Half() { } #endif + /// + /// Draws an arbitrary value from an explicit pool of caller-supplied drawing from + /// this context — same surface as , deterministic under this context's seed. + /// + /// The pool the generated value is drawn from; duplicates are ignored. + /// The type of the pooled values. + /// A terminal generator drawing uniformly from . + /// Thrown when is null. + /// Thrown when is empty or contains a null element. + public AnyOneOf OneOf(params T[] values) { + if (values is null) { throw new ArgumentNullException(nameof(values)); } + + return AnyOneOf.FromPool(_source, values); + } + + /// + /// Draws an arbitrary value from an explicit pool held as a list drawing from this context — same surface as + /// , deterministic under this context's seed. + /// + /// The pool the generated value is drawn from; duplicates are ignored. + /// The type of the pooled values. + /// A terminal generator drawing uniformly from . + /// Thrown when is null. + /// Thrown when is empty or contains a null element. + public AnyOneOf ElementOf(IReadOnlyList values) { + if (values is null) { throw new ArgumentNullException(nameof(values)); } + + return AnyOneOf.FromPool(_source, values); + } + + /// + /// Draws an arbitrary value from an explicit pool held as a sequence drawing from this context — same surface + /// as , deterministic under this context's seed. The sequence is + /// materialized once. + /// + /// The pool the generated value is drawn from; duplicates are ignored. + /// The type of the pooled values. + /// A terminal generator drawing uniformly from . + /// Thrown when is null. + /// Thrown when is empty or contains a null element. + public AnyOneOf ElementOf(IEnumerable values) { + if (values is null) { throw new ArgumentNullException(nameof(values)); } + + return AnyOneOf.FromPool(_source, values as IReadOnlyList ?? values.ToArray()); + } + } diff --git a/Dummies/AnyOneOf.cs b/Dummies/AnyOneOf.cs new file mode 100644 index 00000000..cf9476ca --- /dev/null +++ b/Dummies/AnyOneOf.cs @@ -0,0 +1,75 @@ +namespace Dummies; + +/// +/// A generator that draws an arbitrary value from an explicit, fixed pool supplied by the caller — the +/// dummy for a value whose domain is a closed set a test does not assert on (one of the currencies a context is +/// configured with, one of the orders already in a fixture, one of a handful of domain states). Unlike the typed +/// builders' OneOf, which narrows within a scalar's own domain, this draws from values the library +/// could never synthesize on its own, so the pool is the whole specification: it is a terminal generator +/// exposing no further constraints. It still composes like any other generator — pipe it through As(...) +/// into a value object, make it optional with OrNull(), or fold it into Combine(...) and the +/// collection generators. +/// +/// +/// +/// Each draws one value uniformly from the pool, from the generator's random context — +/// so a run is reproducible under a seed, exactly like every other generator. Duplicate values are collapsed +/// under , so no value carries a heavier weight for being listed +/// twice, and the number of distinct values is the exact size of the domain a distinct collection +/// (SetOf, a dictionary's keys) gates against. +/// +/// +/// A null element is rejected at construction: nullability is an orthogonal concern expressed by +/// OrNull(), never smuggled into the pool — the same rule the string value-set generator applies. +/// +/// +/// +/// Currency currency = Any.OneOf(eur, usd, gbp).Generate(); +/// Order order = Any.ElementOf(existingOrders).Generate(); +/// +/// +/// +/// The type of the pooled values. +public sealed class AnyOneOf : IAny, IHasRandomSource, ICardinalityHint { + + #region Statics members declarations + + // Validates and deduplicates the caller's pool, then builds the generator. The array-null check belongs to the + // public factories (they own the parameter name); by the time we get here the pool is non-null and materialized. + internal static AnyOneOf FromPool(RandomSource source, IReadOnlyList values) { + if (values.Count == 0) { throw new ArgumentException("At least one value is required.", nameof(values)); } + if (values.Any(value => value is null)) { throw new ArgumentException("The values must not contain a null element; use OrNull() to make the whole generator nullable.", nameof(values)); } + + return new AnyOneOf(source, values.Distinct().ToArray()); + } + + #endregion + + #region Fields declarations + + private readonly RandomSource _source; + private readonly IReadOnlyList _values; + + #endregion + + private AnyOneOf(RandomSource source, IReadOnlyList values) { + _source = source; + _values = values; + } + + RandomSource? IHasRandomSource.Source => _source; + + // The pool is fixed and deduplicated at construction under the default comparer, so its count is the exact number + // of distinct values drawable, and membership is a direct lookup under that same comparer. Both deliberately ignore + // any custom comparer a downstream distinct collection carries: such a comparer can only merge values, never create + // new ones, so the advertised size stays a sound upper bound and membership never claims a value the pool lacks. + long? ICardinalityHint.DistinctCardinality => _values.Count; + + bool ICardinalityHint.Contains(T value) => _values.Contains(value); + + /// + public T Generate() { + return _values[_source.Current.Random.Next(_values.Count)]; + } + +} diff --git a/doc/handwritten/for-maintainers/adr/0032-draw-arbitrary-values-from-an-explicit-top-level-pool.fr.md b/doc/handwritten/for-maintainers/adr/0032-draw-arbitrary-values-from-an-explicit-top-level-pool.fr.md new file mode 100644 index 00000000..55f006e4 --- /dev/null +++ b/doc/handwritten/for-maintainers/adr/0032-draw-arbitrary-values-from-an-explicit-top-level-pool.fr.md @@ -0,0 +1,165 @@ +# ADR-0032 | Tirer des valeurs arbitraires depuis un pool de choix explicite et de premier niveau + +🌍 🇬🇧 [English](0032-draw-arbitrary-values-from-an-explicit-top-level-pool.md) · 🇫🇷 Français (ce fichier) + +**Statut :** Proposé +**Date :** 2026-07-21 +**Décideurs :** Reefact + +## Contexte + +`Dummies` fournit des valeurs arbitraires mais valides depuis une source unique et seedable, de sorte que tout run est +reproductible à partir d'un seed reporté (ADR-0006). Un besoin récurrent est une valeur dont le domaine est un +**ensemble fermé que l'appelant détient déjà** — l'une des devises pour lesquelles un contexte est configuré, l'une des +commandes déjà présentes dans une fixture, l'un d'une poignée d'états métier sur lesquels le test ne porte aucune +assertion. La bibliothèque génère des formes structurelles (une longueur, un intervalle, un motif) ; elle ne peut pas +synthétiser un tel ensemble du monde réel, et c'est l'appelant qui possède les valeurs. + +Plusieurs faits existants cadrent le choix : + +* La façon la plus courante de tirer aujourd'hui dans un ensemble détenu par l'appelant est écrite à la main — + `pool[new Random().Next(pool.Count)]` — ce qui tire d'un `Random` neuf, ignore la source seedée ambiante et ne peut + donc pas être rejoué sous `Any.Reproducibly(...)` : précisément le piège que la bibliothèque existe pour supprimer. +* Les builders scalaires et d'enum exposent `OneOf(params T[])`, mais uniquement **au sein de leur propre domaine** — + il restreint l'intervalle ou le pool d'un scalaire et se contrôle vis-à-vis des autres contraintes. Il n'existe aucun + combinateur de premier niveau pour tirer dans un pool d'objets métier arbitraires. +* L'ADR-0030 a ajouté `Any.String().OneOf(...)` comme générateur d'ensemble de valeurs **terminal** chaîné sur le point + d'entrée des chaînes : il implémente `ICardinalityHint`, déduplique sous une comparaison ordinale, tire + uniformément et de façon reproductible, et **rejette un élément `null`**, en orientant l'appelant vers `OrNull()`. Il + est spécifique aux chaînes ; un pool d'objets métier agnostique au type n'a aucun builder typé sur lequel se chaîner. +* Les collections distinctes se gardent, au moment de la déclaration, sur la cardinalité annoncée par le générateur + d'éléments (ADR-0013), à travers l'interface interne `ICardinalityHint` ; un générateur qui annonce une + cardinalité répond aussi à l'appartenance. +* `OrNull()` est le décorateur orthogonal de nullabilité de la bibliothèque, pour les types valeur comme référence. +* Avec une méthode `params T[]` seule, passer une unique collection détenue lie le paramètre de type au **type de la + collection**, non à ses éléments ; et lorsque le type d'élément est lui-même énumérable, une surcharge acceptant + aussi `IEnumerable` rend `OneOf(collection)` ambigu entre « un pool contenant la collection » et « un pool de ses + éléments ». +* Une factory qui prend des valeurs brutes plutôt qu'un opérande `IAny<>` n'hérite d'aucun contexte aléatoire depuis un + opérande, donc — contrairement à `Combine`/`ListOf`/`SetOf` — elle doit exister à la fois sur `Any` (ambiant) et + `AnyContext` (seedé) ; la garde de parité de surface traite une telle factory comme une factory scalaire et exige le + miroir. +* L'audit d'architecture et de conception de Dummies du 2026-07-20 (§10) classe cet ajout comme le Must-Have à plus + fort levier : chaque consommateur, presque chaque semaine. + +## Décision + +`Any.OneOf(params T[])` et `Any.ElementOf(IReadOnlyList)`/`Any.ElementOf(IEnumerable)` — reflétées sur +`AnyContext` — tirent une valeur uniformément dans un pool explicite fourni par l'appelant, en tant que générateur +terminal, en rejetant un pool vide et tout élément `null`, et en dédupliquant, dimensionnant et testant l'appartenance +du pool sous `EqualityComparer.Default`. + +## Justification + +* **Cela ferme le piège de reproductibilité qui est la raison d'être de la bibliothèque.** Un tirage de pool conscient + du seed remplace le `Random` écrit à la main, si bien que le choix se rejoue sous `Any.Reproducibly(...)` et + `Any.WithSeed(...)` comme tout autre tirage (ADR-0006). L'audit désigne cet ajout comme celui au plus fort levier. +* **Rejeter `null` garde la nullabilité orthogonale et la surface symétrique.** `OrNull()` est l'unique manière + d'exprimer une valeur optionnelle, aussi un membre de pool `null` réintroduirait-il l'ambiguïté « `null` est-il une + valeur ou une absence » que ce décorateur existe pour supprimer. Cela s'aligne aussi sur le générateur de chaînes + livré (ADR-0030) : les deux combinateurs d'ensemble de valeurs restent symétriques sur leur contrat `null` au lieu de + diverger — le type d'asymétrie contre lequel l'audit met en garde. Un appelant qui veut un `null` occasionnel écrit + toujours `OneOf(...).OrNull()`. +* **`EqualityComparer.Default` est l'analogue agnostique au type de la déduplication ordinale du générateur de + chaînes, et c'est le choix *sound* pour le contrat de cardinalité.** Une collection distincte en aval portant un + comparateur personnalisé plus grossier ne peut que *fusionner* des valeurs du pool, jamais en créer de nouvelles ; le + nombre distinct annoncé reste donc une borne supérieure conservatrice et l'appartenance ne revendique jamais une + valeur absente du pool — la collection continue de se garder correctement (ADR-0013). +* **Un générateur terminal qui annonce sa cardinalité compose gratuitement.** Le pool est la spécification tout entière + — il n'y a aucun domaine scalaire à restreindre — de sorte que le générateur n'expose aucune autre contrainte, tout + en circulant à travers `As(...)`, `OrNull()`, `Combine(...)` et les générateurs de collections comme tout `IAny`, + et une collection distincte au-dessus de lui se garde tôt comme les autres générateurs à domaine dénombrable. +* **Deux noms valent mieux qu'un seul nom surchargé.** `OneOf` prend des littéraux en ligne ; `ElementOf` prend une + collection détenue. La séparation supprime le piège d'inférence générique : les valeurs en ligne ne lient jamais le + paramètre de type à un conteneur, et une collection détenue n'est jamais confondue avec ses propres éléments. La + surcharge séquence d'`ElementOf` matérialise une fois, de sorte qu'une requête paresseuse n'est pas ré-énumérée à + chaque tirage. +* **Le miroir `AnyContext` est requis, pas optionnel.** Le pool ne porte aucun opérande d'où hériter d'un contexte + seedé, donc sans miroir la surface seedée présenterait un trou silencieux ; la garde de parité fait de l'omission un + test qui échoue. + +## Alternatives considérées + +### Autoriser `null` comme membre de pool + +Considérée parce qu'un objet métier `null` est sans doute un choix arbitraire valide, et parce qu'un membre `null` +(poids `1/n`) diffère, sur le plan de la distribution, du tirage au sort indépendant d'`OrNull()` — la direction que +l'issue déposée proposait d'abord. + +Rejetée parce qu'elle contredirait le `Any.String().OneOf(...)` livré (ADR-0030), réintroduirait l'ambiguïté +valeur-contre-absence qu'`OrNull()` supprime, et scinderait les deux combinateurs d'ensemble de valeurs sur leur +contrat `null` — exactement l'asymétrie de surface que l'audit signale. Le cas du `null` occasionnel reste servi par +`OneOf(...).OrNull()`. + +### Un unique `OneOf(params T[])` surchargé plus `OneOf(IEnumerable)`, sans `ElementOf` + +Considérée pour l'économie de surface, en miroir des deux surcharges du builder de chaînes. + +Rejetée parce que l'inférence générique en fait un piège : passer une collection détenue à la forme `params` met le +conteneur lui-même en pool, et lorsque le type d'élément est énumérable les deux surcharges rendent `OneOf(collection)` +ambigu entre le conteneur et ses éléments. Un nom `ElementOf` distinct rend l'intention non ambiguë au site d'appel. + +### Ajouter une surcharge `IEqualityComparer`, comme `SetOf` + +Considérée parce qu'un appelant pourrait vouloir que l'identité du pool soit décidée par un comparateur personnalisé. + +Rejetée comme inutile pour la v1 : le comparateur par défaut fournit déjà une borne de cardinalité *sound* sous +n'importe quel comparateur aval, et un comparateur spécifique au pool pourra être ajouté plus tard sur preuve de besoin +sans changer le contrat par défaut. + +### La chaîner sur un point d'entrée typé, comme le `OneOf` des chaînes + +Considérée pour la cohérence avec `Any.String().OneOf(...)`. + +Rejetée parce qu'un objet métier arbitraire n'a aucun builder `Any.X()` sur lequel se chaîner — tout l'intérêt est une +factory de premier niveau, agnostique au type — de sorte qu'une factory statique sur `Any`/`AnyContext` est la seule +forme qui convienne. + +## Conséquences + +### Positives + +* La lacune classée première par l'audit est comblée : tirer dans un ensemble détenu par l'appelant devient un dummy + d'une ligne, reproductible par seed, qui compose vers des objets valeur (`As`), des optionnels (`OrNull`) et des + collections comme tout autre générateur. +* Les combinateurs d'ensemble de valeurs des chaînes et génériques partagent désormais un seul contrat `null` (rejeté, + via `OrNull()`), supprimant une asymétrie au lieu d'en ajouter une. +* Une collection distincte au-dessus du pool est gardée tôt par sa cardinalité, en cohérence avec les autres + générateurs à domaine dénombrable. + +### Négatives + +* Un nouveau type public (`AnyOneOf`) et deux noms de point d'entrée (`OneOf`/`ElementOf`) à maintenir, documenter + et garder reflétés sur `AnyContext`. +* La bibliothèque ne vérifie pas que les valeurs du pool respectent un format externe : ce sont le contenu de + l'appelant, et un objet valeur a toujours besoin d'`As(...)` pour faire respecter son invariant. + +### Risques + +* Un appelant peut s'attendre à ce que `null` soit un membre de pool légal et être surpris qu'il soit refusé. Atténué + par le message d'exception qui pointe vers `OrNull()` — le même conseil que donne le générateur de chaînes. +* Un appelant peut passer une collection détenue à `OneOf` et obtenir un pool d'un seul élément. Atténué par `ElementOf` + qui est le chemin documenté pour une collection détenue, et par le résumé d'`OneOf` qui y oriente. + +## Actions de suivi + +* Documenter `OneOf`/`ElementOf` dans le guide utilisateur de Dummies (`ArbitraryTestValues.en.md`) et sa traduction + française, ainsi que dans le README du package (`README.nuget.md`), avec un exemple. +* Garder le miroir `Any`↔`AnyContext` au vert (imposé par `SurfaceParityTests`). +* Envisager d'aligner les messages d'élément `null` du générateur de chaînes et du générateur générique lors de la + prochaine révision de la surface des chaînes. + +## Références + +* ADR-0030 — Tirer des chaînes arbitraires depuis un ensemble de valeurs explicite et terminal (le frère « chaînes » ; + le précédent générateur terminal et rejet du `null`). +* ADR-0013 — Garder les collections distinctes par cardinalité, sinon par un tirage borné (le contrat + `ICardinalityHint`). +* ADR-0006 — Fournir les valeurs de test arbitraires depuis une source unique et seedable (reproductibilité). +* ADR-0031 — Nommer les factories scalaires d'Any d'après leur type CLR (pourquoi `OneOf`/`ElementOf`, en tant que + combinateurs, sont exemptés). +* ADR-0020 — Ne matérialiser les dummies qu'à travers `Generate()`. +* Issue [#223](https://github.com/Reefact/first-class-errors/issues/223) et l'audit d'architecture et de conception de + Dummies du 2026-07-20 (§10 Must-Have). +* Le type `AnyOneOf`, les factories `Any.OneOf`/`Any.ElementOf` et `AnyContext.OneOf`/`AnyContext.ElementOf`, et + leurs tests dans le projet `Dummies` et `Dummies.UnitTests`. diff --git a/doc/handwritten/for-maintainers/adr/0032-draw-arbitrary-values-from-an-explicit-top-level-pool.md b/doc/handwritten/for-maintainers/adr/0032-draw-arbitrary-values-from-an-explicit-top-level-pool.md new file mode 100644 index 00000000..e78378f8 --- /dev/null +++ b/doc/handwritten/for-maintainers/adr/0032-draw-arbitrary-values-from-an-explicit-top-level-pool.md @@ -0,0 +1,152 @@ +# ADR-0032 | Draw arbitrary values from an explicit, top-level choice pool + +🌍 🇬🇧 English (this file) · 🇫🇷 [Français](0032-draw-arbitrary-values-from-an-explicit-top-level-pool.fr.md) + +**Status:** Proposed +**Date:** 2026-07-21 +**Decision Makers:** Reefact + +## Context + +`Dummies` supplies arbitrary yet valid values from a single seedable source, so any run is reproducible from a +reported seed (ADR-0006). A recurring need is a value whose domain is a **closed set the caller already holds** — one +of the currencies a context is configured with, one of the orders already in a fixture, one of a handful of domain +states the test does not assert on. The library generates structural shapes (a length, an interval, a pattern); it +cannot synthesize such a real-world set, and the caller owns the values. + +Several existing facts frame the choice: + +* The most common way to pick from a caller-held set today is hand-rolled — `pool[new Random().Next(pool.Count)]` — + which draws from a fresh `Random`, ignores the ambient seeded source, and therefore cannot replay under + `Any.Reproducibly(...)`: exactly the trap the library exists to remove. +* The scalar and enum builders expose `OneOf(params T[])`, but only **within their own domain** — it narrows a + scalar's interval or pool and cross-validates against the other constraints. There is no top-level combinator to + draw from a pool of arbitrary domain objects. +* ADR-0030 added `Any.String().OneOf(...)` as a **terminal** value-set generator chained off the string entry point: + it implements `ICardinalityHint`, deduplicates under an ordinal comparison, draws uniformly and + reproducibly, and **rejects a `null` element**, directing the caller to `OrNull()`. It is string-specific; a + type-agnostic pool of domain objects has no typed builder to chain off. +* Distinct collections gate on an element generator's advertised cardinality at declaration time (ADR-0013), through + the internal `ICardinalityHint`; a generator that advertises a cardinality also answers membership. +* `OrNull()` is the library's orthogonal decorator for nullability, for both value and reference types. +* With a `params T[]`-only method, passing a single held collection binds the type parameter to the **collection + type**, not its elements; and when the element type is itself enumerable, an overload that also accepts + `IEnumerable` makes `OneOf(collection)` ambiguous between "a pool holding the collection" and "a pool of its + elements". +* A factory that takes raw values rather than an `IAny<>` operand does not inherit a random context from an operand, + so — unlike `Combine`/`ListOf`/`SetOf` — it must exist on both `Any` (ambient) and `AnyContext` (seeded); the + surface-parity guard treats such a factory as a scalar factory and requires the mirror. +* The 2026-07-20 Dummies architecture & design audit (§10) ranks this the highest-leverage Must-Have: every consumer, + most weeks. + +## Decision + +`Any.OneOf(params T[])` and `Any.ElementOf(IReadOnlyList)`/`Any.ElementOf(IEnumerable)` — mirrored on +`AnyContext` — draw one value uniformly from an explicit, caller-supplied pool as a terminal generator, rejecting an +empty pool and any `null` element, and deduplicating, sizing and testing membership of the pool under +`EqualityComparer.Default`. + +## Rationale + +* **It closes the reproducibility trap that is the library's reason to exist.** A seed-aware pool draw replaces the + hand-rolled `Random`, so the choice replays under `Any.Reproducibly(...)` and `Any.WithSeed(...)` like every other + draw (ADR-0006). The audit names this the single highest-leverage addition. +* **Rejecting `null` keeps nullability orthogonal and the surface symmetric.** `OrNull()` is the one way to express an + optional value, so a `null` pool member would reintroduce the "is `null` a value or an absence" ambiguity that + decorator exists to remove. It also matches the shipped string generator (ADR-0030): the two value-set combinators + stay symmetric on their `null` contract instead of diverging — the kind of asymmetry the audit warns against. A + caller who wants an occasional `null` still writes `OneOf(...).OrNull()`. +* **`EqualityComparer.Default` is the type-agnostic analogue of the string generator's ordinal dedup, and it is the + sound choice for the cardinality contract.** A downstream distinct collection carrying a coarser custom comparer can + only *merge* pool values, never create new ones, so the advertised distinct count stays a conservative upper bound + and membership never claims a value the pool lacks — the collection keeps gating correctly (ADR-0013). +* **A terminal generator that advertises its cardinality composes for free.** The pool is the whole specification — + there is no scalar domain to narrow — so the generator exposes no further constraints, yet it flows through + `As(...)`, `OrNull()`, `Combine(...)` and the collection generators as any `IAny` does, and a distinct collection + over it gates eagerly like the other countable-domain generators. +* **Two names beat one overloaded name.** `OneOf` takes inline literals; `ElementOf` takes a held collection. The + split removes the generic-inference footgun: inline values never bind the type parameter to a container, and a held + collection is never confused with its own elements. `ElementOf`'s sequence overload materializes once so a lazy + query is not re-enumerated per draw. +* **The `AnyContext` mirror is required, not optional.** The pool carries no operand from which to inherit a seeded + context, so without a mirror the seeded surface would have a silent hole; the parity guard makes the omission a + failing test. + +## Alternatives Considered + +### Allow `null` as a pool member + +Considered because a `null` domain object is arguably a valid arbitrary choice, and a `null` member (weight `1/n`) is +distributionally different from `OrNull()`'s independent coin flip — the direction the filed issue first proposed. + +Rejected because it would contradict the shipped `Any.String().OneOf(...)` (ADR-0030), reintroduce the +value-versus-absence ambiguity `OrNull()` removes, and split the two value-set combinators on their `null` contract — +exactly the surface asymmetry the audit flags. The occasional-`null` case is still served by `OneOf(...).OrNull()`. + +### A single overloaded `OneOf(params T[])` plus `OneOf(IEnumerable)`, no `ElementOf` + +Considered for surface economy, mirroring the string builder's two overloads. + +Rejected because generic inference turns it into a footgun: passing a held collection to the `params` form pools the +container itself, and when the element type is enumerable the two overloads make `OneOf(collection)` ambiguous between +the container and its elements. A distinct `ElementOf` name makes the intent unambiguous at the call site. + +### Add an `IEqualityComparer` overload, as `SetOf` has + +Considered because a caller might want pool identity decided by a custom comparer. + +Rejected as unneeded for v1: the default comparer already yields a sound cardinality bound under any downstream +comparer, and a pool-specific comparer can be added later on evidence of need without changing the default contract. + +### Chain it off a typed entry point, like the string `OneOf` + +Considered for consistency with `Any.String().OneOf(...)`. + +Rejected because an arbitrary domain object has no `Any.X()` builder to chain from — the whole point is a +type-agnostic, top-level factory — so a static factory on `Any`/`AnyContext` is the only shape that fits. + +## Consequences + +### Positive + +* The audit's first-ranked gap is closed: picking from a caller-held set becomes a one-line, seed-reproducible dummy + that composes into value objects (`As`), optionals (`OrNull`) and collections like every other generator. +* The string and generic value-set combinators now share one `null` contract (rejected, via `OrNull()`), removing an + asymmetry rather than adding one. +* A distinct collection over the pool is gated eagerly by its cardinality, consistent with the other + countable-domain generators. + +### Negative + +* A new public type (`AnyOneOf`) and two entry-point names (`OneOf`/`ElementOf`) to maintain, document, and keep + mirrored on `AnyContext`. +* The library does not check that the pooled values meet any external format: they are the caller's content, and a + value object still needs `As(...)` to enforce its invariant. + +### Risks + +* A caller may expect `null` to be a legal pool member and be surprised it is refused. Mitigated by the exception + message pointing at `OrNull()` — the same guidance the string generator gives. +* A caller may pass a held collection to `OneOf` and get a pool of one element. Mitigated by `ElementOf` being the + documented path for a held collection, and by `OneOf`'s summary directing there. + +## Follow-up Actions + +* Document `OneOf`/`ElementOf` in the Dummies user guide (`ArbitraryTestValues.en.md`) and its French translation, and + in the package README (`README.nuget.md`), with an example. +* Keep the `Any`↔`AnyContext` mirror green (enforced by `SurfaceParityTests`). +* Consider aligning the string generator's and the generic generator's `null`-element messages when the string + surface is next revised. + +## References + +* ADR-0030 — Draw arbitrary strings from an explicit, terminal value set (the string sibling; the terminal-generator + and `null`-rejection precedent). +* ADR-0013 — Gate distinct collections by cardinality, otherwise by a bounded draw (the `ICardinalityHint` contract). +* ADR-0006 — Supply arbitrary test values from a single seedable source (reproducibility). +* ADR-0031 — Name Any's scalar factories after their CLR type (why `OneOf`/`ElementOf`, as combinators, are exempt). +* ADR-0020 — Materialize dummies only through `Generate()`. +* Issue [#223](https://github.com/Reefact/first-class-errors/issues/223) and the 2026-07-20 Dummies architecture & + design audit (§10 Must-Have). +* The `AnyOneOf` type, the `Any.OneOf`/`Any.ElementOf` and `AnyContext.OneOf`/`AnyContext.ElementOf` factories, and + their tests in the `Dummies` project and `Dummies.UnitTests`. diff --git a/doc/handwritten/for-maintainers/adr/README.md b/doc/handwritten/for-maintainers/adr/README.md index 9bfe7217..78db4c09 100644 --- a/doc/handwritten/for-maintainers/adr/README.md +++ b/doc/handwritten/for-maintainers/adr/README.md @@ -214,3 +214,4 @@ Optional supporting material: | [ADR-0029](0029-complete-the-outcome-try-async-surface-with-token-less-overloads.md) | Complete the Outcome.Try async surface with token-less overloads | Accepted | | [ADR-0030](0030-draw-arbitrary-strings-from-an-explicit-terminal-set.md) | Draw arbitrary strings from an explicit, terminal value set | Accepted | | [ADR-0031](0031-name-any-factories-after-their-clr-type.md) | Name Any's scalar factories after their CLR type | Accepted | +| [ADR-0032](0032-draw-arbitrary-values-from-an-explicit-top-level-pool.md) | Draw arbitrary values from an explicit, top-level choice pool | Proposed | From ee20e2c9eb9ad862c1592006cfbd76a024a45dc6 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 21 Jul 2026 21:43:38 +0000 Subject: [PATCH 2/3] docs(dummies): document the choice combinator, accept ADR-0032 Document the top-level choice combinator in the Dummies package README: Any.OneOf(...) and Any.ElementOf(...) as the seed-aware answer to "any of these", mirroring where the string value-set generator is documented. Record the maintainer's acceptance of ADR-0032 (draw arbitrary values from an explicit, top-level choice pool): flip its status from Proposed to Accepted in the English and French records and in the ADR index. The Dummies CHANGELOG [Unreleased] section is drafted automatically from merged pull requests (changelog.yml), so it is left untouched here. Refs: #223 Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01LQ15y5om7Gagin9ZpYXDWf --- Dummies/README.nuget.md | 8 ++++++++ ...arbitrary-values-from-an-explicit-top-level-pool.fr.md | 2 +- ...aw-arbitrary-values-from-an-explicit-top-level-pool.md | 2 +- doc/handwritten/for-maintainers/adr/README.md | 2 +- 4 files changed, 11 insertions(+), 3 deletions(-) diff --git a/Dummies/README.nuget.md b/Dummies/README.nuget.md index a26b2907..25486ae9 100644 --- a/Dummies/README.nuget.md +++ b/Dummies/README.nuget.md @@ -40,6 +40,14 @@ matter — and that is the point. currency code, a well-known name). A *terminal* generator, like `StringMatching`: the set is the whole specification, duplicates collapse, and the draw is uniform and reproducible under a seed. +- **Any value from an explicit pool**: `Any.OneOf(eur, usd, gbp)` draws one value from a + caller-supplied set of arbitrary values or domain objects, and `Any.ElementOf(orders)` + does the same from a collection already held (a list, a LINQ result). This is the + seed-aware answer to "any of these" — replacing a hand-rolled + `pool[new Random().Next(...)]` that would ignore the seed and break `Reproducibly`. + Terminal and uniform like the string set: duplicates collapse under the default + comparer, the pool's distinct count gates distinct collections, and a `null` element is + refused — make the whole draw optional with `.OrNull()` instead. - **Domain vocabulary where it belongs**: dates constrain with `After`/`Before`/`Between`, quantities with `Positive`/`Between`/`NonZero`, identities with `NonEmpty`/`DifferentFrom` — and deliberately no clock-relative diff --git a/doc/handwritten/for-maintainers/adr/0032-draw-arbitrary-values-from-an-explicit-top-level-pool.fr.md b/doc/handwritten/for-maintainers/adr/0032-draw-arbitrary-values-from-an-explicit-top-level-pool.fr.md index 55f006e4..6a6c5a35 100644 --- a/doc/handwritten/for-maintainers/adr/0032-draw-arbitrary-values-from-an-explicit-top-level-pool.fr.md +++ b/doc/handwritten/for-maintainers/adr/0032-draw-arbitrary-values-from-an-explicit-top-level-pool.fr.md @@ -2,7 +2,7 @@ 🌍 🇬🇧 [English](0032-draw-arbitrary-values-from-an-explicit-top-level-pool.md) · 🇫🇷 Français (ce fichier) -**Statut :** Proposé +**Statut :** Accepté **Date :** 2026-07-21 **Décideurs :** Reefact diff --git a/doc/handwritten/for-maintainers/adr/0032-draw-arbitrary-values-from-an-explicit-top-level-pool.md b/doc/handwritten/for-maintainers/adr/0032-draw-arbitrary-values-from-an-explicit-top-level-pool.md index e78378f8..72f58331 100644 --- a/doc/handwritten/for-maintainers/adr/0032-draw-arbitrary-values-from-an-explicit-top-level-pool.md +++ b/doc/handwritten/for-maintainers/adr/0032-draw-arbitrary-values-from-an-explicit-top-level-pool.md @@ -2,7 +2,7 @@ 🌍 🇬🇧 English (this file) · 🇫🇷 [Français](0032-draw-arbitrary-values-from-an-explicit-top-level-pool.fr.md) -**Status:** Proposed +**Status:** Accepted **Date:** 2026-07-21 **Decision Makers:** Reefact diff --git a/doc/handwritten/for-maintainers/adr/README.md b/doc/handwritten/for-maintainers/adr/README.md index 78db4c09..dcec90c0 100644 --- a/doc/handwritten/for-maintainers/adr/README.md +++ b/doc/handwritten/for-maintainers/adr/README.md @@ -214,4 +214,4 @@ Optional supporting material: | [ADR-0029](0029-complete-the-outcome-try-async-surface-with-token-less-overloads.md) | Complete the Outcome.Try async surface with token-less overloads | Accepted | | [ADR-0030](0030-draw-arbitrary-strings-from-an-explicit-terminal-set.md) | Draw arbitrary strings from an explicit, terminal value set | Accepted | | [ADR-0031](0031-name-any-factories-after-their-clr-type.md) | Name Any's scalar factories after their CLR type | Accepted | -| [ADR-0032](0032-draw-arbitrary-values-from-an-explicit-top-level-pool.md) | Draw arbitrary values from an explicit, top-level choice pool | Proposed | +| [ADR-0032](0032-draw-arbitrary-values-from-an-explicit-top-level-pool.md) | Draw arbitrary values from an explicit, top-level choice pool | Accepted | From f01c782ff8cdfaa4e13f52f2d746163966afc7fd Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 21 Jul 2026 21:59:30 +0000 Subject: [PATCH 3/3] build(dummies): declare OneOf/ElementOf in the public API baseline Merging main brought in the per-target-framework public API baselines (#221): PublicApiAnalyzers now raises RS0016 for any public symbol absent from PublicAPI.Unshipped.txt, a warning locally and an error in CI. The new top-level choice combinator was therefore undeclared and reddened the build, pack and packaged-asset jobs. Declare the eight new public symbols - AnyOneOf, its Generate(), and the OneOf/ElementOf factories on Any and AnyContext - in both the net8.0 and netstandard2.0 baselines. The entries are identical across the two assets because the combinator is not gated by #if NET8_0_OR_GREATER. Verified locally: dotnet pack -c Release is clean (no RS0016, package validation passes) and the full solution builds warning-free. Refs: #223 Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01LQ15y5om7Gagin9ZpYXDWf --- Dummies/PublicAPI/net8.0/PublicAPI.Unshipped.txt | 8 ++++++++ Dummies/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/Dummies/PublicAPI/net8.0/PublicAPI.Unshipped.txt b/Dummies/PublicAPI/net8.0/PublicAPI.Unshipped.txt index 46f7533c..51f00166 100644 --- a/Dummies/PublicAPI/net8.0/PublicAPI.Unshipped.txt +++ b/Dummies/PublicAPI/net8.0/PublicAPI.Unshipped.txt @@ -49,6 +49,8 @@ Dummies.AnyContext.DateTime() -> Dummies.AnyDateTime! Dummies.AnyContext.DateTimeOffset() -> Dummies.AnyDateTimeOffset! Dummies.AnyContext.Decimal() -> Dummies.AnyDecimal! Dummies.AnyContext.Double() -> Dummies.AnyDouble! +Dummies.AnyContext.ElementOf(System.Collections.Generic.IEnumerable! values) -> Dummies.AnyOneOf! +Dummies.AnyContext.ElementOf(System.Collections.Generic.IReadOnlyList! values) -> Dummies.AnyOneOf! Dummies.AnyContext.Enum() -> Dummies.AnyEnum! Dummies.AnyContext.Guid() -> Dummies.AnyGuid! Dummies.AnyContext.Half() -> Dummies.AnyHalf! @@ -56,6 +58,7 @@ Dummies.AnyContext.Int128() -> Dummies.AnyInt128! Dummies.AnyContext.Int16() -> Dummies.AnyInt16! Dummies.AnyContext.Int32() -> Dummies.AnyInt32! Dummies.AnyContext.Int64() -> Dummies.AnyInt64! +Dummies.AnyContext.OneOf(params T[]! values) -> Dummies.AnyOneOf! Dummies.AnyContext.SByte() -> Dummies.AnySByte! Dummies.AnyContext.Seed.get -> int Dummies.AnyContext.Single() -> Dummies.AnySingle! @@ -227,6 +230,8 @@ Dummies.AnyInt64.Zero() -> Dummies.AnyInt64! Dummies.AnyList Dummies.AnyList.Distinct() -> Dummies.AnyList! Dummies.AnyList.Distinct(System.Collections.Generic.IEqualityComparer! comparer) -> Dummies.AnyList! +Dummies.AnyOneOf +Dummies.AnyOneOf.Generate() -> T Dummies.AnyPattern Dummies.AnyPattern.Generate() -> string! Dummies.AnySByte @@ -378,6 +383,8 @@ static Dummies.Any.Decimal() -> Dummies.AnyDecimal! static Dummies.Any.DictionaryOf(Dummies.IAny! keys, Dummies.IAny! values) -> Dummies.AnyDictionary! static Dummies.Any.DictionaryOf(Dummies.IAny! keys, Dummies.IAny! values, System.Collections.Generic.IEqualityComparer! keyComparer) -> Dummies.AnyDictionary! static Dummies.Any.Double() -> Dummies.AnyDouble! +static Dummies.Any.ElementOf(System.Collections.Generic.IEnumerable! values) -> Dummies.AnyOneOf! +static Dummies.Any.ElementOf(System.Collections.Generic.IReadOnlyList! values) -> Dummies.AnyOneOf! static Dummies.Any.Enum() -> Dummies.AnyEnum! static Dummies.Any.Guid() -> Dummies.AnyGuid! static Dummies.Any.Half() -> Dummies.AnyHalf! @@ -386,6 +393,7 @@ static Dummies.Any.Int16() -> Dummies.AnyInt16! static Dummies.Any.Int32() -> Dummies.AnyInt32! static Dummies.Any.Int64() -> Dummies.AnyInt64! static Dummies.Any.ListOf(Dummies.IAny! item) -> Dummies.AnyList! +static Dummies.Any.OneOf(params T[]! values) -> Dummies.AnyOneOf! static Dummies.Any.PairOf(Dummies.IAny! first, Dummies.IAny! second) -> Dummies.IAny<(T1, T2)>! static Dummies.Any.Reproducibly(int seed, System.Action! body, System.Action? report = null) -> void static Dummies.Any.Reproducibly(int seed, System.Func! body, System.Action? report = null) -> System.Threading.Tasks.Task! diff --git a/Dummies/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt b/Dummies/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt index 3b612113..847752a5 100644 --- a/Dummies/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt +++ b/Dummies/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt @@ -48,11 +48,14 @@ Dummies.AnyContext.DateTime() -> Dummies.AnyDateTime! Dummies.AnyContext.DateTimeOffset() -> Dummies.AnyDateTimeOffset! Dummies.AnyContext.Decimal() -> Dummies.AnyDecimal! Dummies.AnyContext.Double() -> Dummies.AnyDouble! +Dummies.AnyContext.ElementOf(System.Collections.Generic.IEnumerable! values) -> Dummies.AnyOneOf! +Dummies.AnyContext.ElementOf(System.Collections.Generic.IReadOnlyList! values) -> Dummies.AnyOneOf! Dummies.AnyContext.Enum() -> Dummies.AnyEnum! Dummies.AnyContext.Guid() -> Dummies.AnyGuid! Dummies.AnyContext.Int16() -> Dummies.AnyInt16! Dummies.AnyContext.Int32() -> Dummies.AnyInt32! Dummies.AnyContext.Int64() -> Dummies.AnyInt64! +Dummies.AnyContext.OneOf(params T[]! values) -> Dummies.AnyOneOf! Dummies.AnyContext.SByte() -> Dummies.AnySByte! Dummies.AnyContext.Seed.get -> int Dummies.AnyContext.Single() -> Dummies.AnySingle! @@ -184,6 +187,8 @@ Dummies.AnyInt64.Zero() -> Dummies.AnyInt64! Dummies.AnyList Dummies.AnyList.Distinct() -> Dummies.AnyList! Dummies.AnyList.Distinct(System.Collections.Generic.IEqualityComparer! comparer) -> Dummies.AnyList! +Dummies.AnyOneOf +Dummies.AnyOneOf.Generate() -> T Dummies.AnyPattern Dummies.AnyPattern.Generate() -> string! Dummies.AnySByte @@ -312,12 +317,15 @@ static Dummies.Any.Decimal() -> Dummies.AnyDecimal! static Dummies.Any.DictionaryOf(Dummies.IAny! keys, Dummies.IAny! values) -> Dummies.AnyDictionary! static Dummies.Any.DictionaryOf(Dummies.IAny! keys, Dummies.IAny! values, System.Collections.Generic.IEqualityComparer! keyComparer) -> Dummies.AnyDictionary! static Dummies.Any.Double() -> Dummies.AnyDouble! +static Dummies.Any.ElementOf(System.Collections.Generic.IEnumerable! values) -> Dummies.AnyOneOf! +static Dummies.Any.ElementOf(System.Collections.Generic.IReadOnlyList! values) -> Dummies.AnyOneOf! static Dummies.Any.Enum() -> Dummies.AnyEnum! static Dummies.Any.Guid() -> Dummies.AnyGuid! static Dummies.Any.Int16() -> Dummies.AnyInt16! static Dummies.Any.Int32() -> Dummies.AnyInt32! static Dummies.Any.Int64() -> Dummies.AnyInt64! static Dummies.Any.ListOf(Dummies.IAny! item) -> Dummies.AnyList! +static Dummies.Any.OneOf(params T[]! values) -> Dummies.AnyOneOf! static Dummies.Any.PairOf(Dummies.IAny! first, Dummies.IAny! second) -> Dummies.IAny<(T1, T2)>! static Dummies.Any.Reproducibly(int seed, System.Action! body, System.Action? report = null) -> void static Dummies.Any.Reproducibly(int seed, System.Func! body, System.Action? report = null) -> System.Threading.Tasks.Task!