From 9ca2d941d82fdf932cb4ffa2a25eac33192bdb60 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 18 Jul 2026 11:57:33 +0000 Subject: [PATCH] refactor(binder): rename complex AsOptional to AsOptionalReference MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ComplexPropertyConverter.AsOptional was constrained `where TProperty : class` and returned an OptionalReferenceField — the reference-optional semantics the scalar side already exposes as AsOptionalReference. Under the name AsOptional it was a dead end: a future value-type variant could not reuse the name (two methods differing only by a class vs struct constraint do not overload, CS0111), forcing an inconsistent third name or a breaking rename after the API freeze. Rename it to AsOptionalReference, matching the scalar AsOptionalReference / AsOptionalValue split and keeping AsOptionalValue free for a future value-type nested object. The three list converters' AsOptional are intentionally left alone: they are `notnull` and mean "absent list binds an empty list", with no reference/value split to disambiguate. The library is 0.1.0-preview.1 with no published consumers, so the rename is free now and needs no ADR. Updated the call sites (all in tests) and the binder reference docs (EN + FR). Refs: #146 Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_015HzQtszd156kmTp86rfsQR --- .../BindingContractTests.cs | 4 ++-- .../BookingEndToEndTests.cs | 2 +- .../ComplexPropertyBindingTests.cs | 6 +++--- .../ComplexPropertyConverter.cs | 10 ++++++---- doc/handwritten/for-users/RequestBinder.en.md | 5 +++-- doc/handwritten/for-users/RequestBinder.fr.md | 5 +++-- 6 files changed, 18 insertions(+), 14 deletions(-) diff --git a/FirstClassErrors.RequestBinder.UnitTests/BindingContractTests.cs b/FirstClassErrors.RequestBinder.UnitTests/BindingContractTests.cs index c7bad911..a62f6d96 100644 --- a/FirstClassErrors.RequestBinder.UnitTests/BindingContractTests.cs +++ b/FirstClassErrors.RequestBinder.UnitTests/BindingContractTests.cs @@ -115,7 +115,7 @@ public void BareOptionalNestedPrimaryPortErrorLeafIsWrapped() { var bind = Bind.PropertiesOf(Request()).FailWith(BookingEnvelopeError.CommandInvalid); bind.ComplexProperty(r => r.Stay).FailWith(BookingEnvelopeError.StayInvalid) - .AsOptional(_ => Outcome.Failure(LeafPortError())); + .AsOptionalReference(_ => Outcome.Failure(LeafPortError())); Outcome outcome = bind.New(_ => "never"); Error wrapped = outcome.Error!.InnerErrors.Single(); @@ -271,7 +271,7 @@ public void GuardClauses() { Check.ThatCode(() => bind.ComplexProperty(r => r.Stay).FailWith(null!)).Throws(); Check.ThatCode(() => bind.ComplexProperty(r => r.Stay).FailWith(BookingEnvelopeError.StayInvalid).AsRequired(null!)).Throws(); - Check.ThatCode(() => bind.ComplexProperty(r => r.Stay).FailWith(BookingEnvelopeError.StayInvalid).AsOptional(null!)).Throws(); + Check.ThatCode(() => bind.ComplexProperty(r => r.Stay).FailWith(BookingEnvelopeError.StayInvalid).AsOptionalReference(null!)).Throws(); Check.ThatCode(() => bind.ListOfSimpleProperties(r => r.Tags).AsRequired(null!)).Throws(); Check.ThatCode(() => bind.ListOfSimpleProperties(r => r.Tags).AsOptional(null!)).Throws(); diff --git a/FirstClassErrors.RequestBinder.UnitTests/BookingEndToEndTests.cs b/FirstClassErrors.RequestBinder.UnitTests/BookingEndToEndTests.cs index fda6d04e..7e2d3719 100644 --- a/FirstClassErrors.RequestBinder.UnitTests/BookingEndToEndTests.cs +++ b/FirstClassErrors.RequestBinder.UnitTests/BookingEndToEndTests.cs @@ -33,7 +33,7 @@ private static Outcome BindCommand(BookingRequest request) { RequiredField reference = bind.SimpleProperty(r => r.Reference).AsRequired(); RequiredField currency = bind.SimpleProperty(r => r.Currency).AsOptional(Currency.Parse, "EUR"); OptionalValueField maxNights = bind.SimpleProperty(r => r.MaxNights).AsOptionalValue(PositiveInt.Parse); - OptionalReferenceField stay = bind.ComplexProperty(r => r.Stay).FailWith(BookingEnvelopeError.StayInvalid).AsOptional(BindStay); + OptionalReferenceField stay = bind.ComplexProperty(r => r.Stay).FailWith(BookingEnvelopeError.StayInvalid).AsOptionalReference(BindStay); RequiredField> tags = bind.ListOfSimpleProperties(r => r.Tags).AsOptional(Tag.Parse); RequiredField> guests = bind.ListOfComplexProperties(r => r.Guests).FailWith(BookingEnvelopeError.GuestInvalid).AsRequired(BindGuest); diff --git a/FirstClassErrors.RequestBinder.UnitTests/ComplexPropertyBindingTests.cs b/FirstClassErrors.RequestBinder.UnitTests/ComplexPropertyBindingTests.cs index f7328832..b255e098 100644 --- a/FirstClassErrors.RequestBinder.UnitTests/ComplexPropertyBindingTests.cs +++ b/FirstClassErrors.RequestBinder.UnitTests/ComplexPropertyBindingTests.cs @@ -65,11 +65,11 @@ public void RequiredComplexMissing() { [Fact(DisplayName = "An optional complex property yields null when absent — recording nothing — and binds when present.")] public void OptionalComplex() { var absent = Bind.PropertiesOf(RequestWith(stay: null)).FailWith(BookingEnvelopeError.CommandInvalid); - OptionalReferenceField none = absent.ComplexProperty(r => r.Stay).FailWith(BookingEnvelopeError.StayInvalid).AsOptional(BindStay); + OptionalReferenceField none = absent.ComplexProperty(r => r.Stay).FailWith(BookingEnvelopeError.StayInvalid).AsOptionalReference(BindStay); Check.That(absent.New(s => s.Get(none) is null).GetResultOrThrow()).IsTrue(); var present = Bind.PropertiesOf(RequestWith(new StayDto("2026-08-10", "2026-08-14"))).FailWith(BookingEnvelopeError.CommandInvalid); - OptionalReferenceField some = present.ComplexProperty(r => r.Stay).FailWith(BookingEnvelopeError.StayInvalid).AsOptional(BindStay); + OptionalReferenceField some = present.ComplexProperty(r => r.Stay).FailWith(BookingEnvelopeError.StayInvalid).AsOptionalReference(BindStay); Check.That(present.New(s => s.Get(some)!.CheckOut.Value).GetResultOrThrow()).IsEqualTo(new DateOnly(2026, 8, 14)); } @@ -77,7 +77,7 @@ public void OptionalComplex() { public void OptionalComplexPresentButInvalidRecords() { var bind = Bind.PropertiesOf(RequestWith(new StayDto(null, null))).FailWith(BookingEnvelopeError.CommandInvalid); - bind.ComplexProperty(r => r.Stay).FailWith(BookingEnvelopeError.StayInvalid).AsOptional(BindStay); + bind.ComplexProperty(r => r.Stay).FailWith(BookingEnvelopeError.StayInvalid).AsOptionalReference(BindStay); Outcome outcome = bind.New(_ => "never"); Check.That(outcome.IsFailure).IsTrue(); diff --git a/FirstClassErrors.RequestBinder/ComplexPropertyConverter.cs b/FirstClassErrors.RequestBinder/ComplexPropertyConverter.cs index 53abfce2..f94eef9e 100644 --- a/FirstClassErrors.RequestBinder/ComplexPropertyConverter.cs +++ b/FirstClassErrors.RequestBinder/ComplexPropertyConverter.cs @@ -65,15 +65,17 @@ public RequiredField AsRequired(Func - /// Binds an optional complex argument: absent yields a null - /// value and records nothing; a present-but-invalid nested - /// binding records its envelope. + /// Binds an optional complex argument whose bound type is a reference type: absent yields a + /// null value and records nothing; a + /// present-but-invalid nested binding records its envelope. The complex counterpart of the scalar + /// AsOptionalReference; the split name keeps AsOptionalValue free for a future value-type + /// nested object. /// /// The reference type the nested binding produces. /// The nested binding function (typically a method group such as BindAddress). /// The bound field token. /// Thrown when is null. - public OptionalReferenceField AsOptional(Func, Outcome> bindNested) where TProperty : class { + public OptionalReferenceField AsOptionalReference(Func, Outcome> bindNested) where TProperty : class { if (bindNested is null) { throw new ArgumentNullException(nameof(bindNested)); } if (_isMissing) { return new OptionalReferenceField(_binder, value: null); } diff --git a/doc/handwritten/for-users/RequestBinder.en.md b/doc/handwritten/for-users/RequestBinder.en.md index 2e9af29f..7d4844f9 100644 --- a/doc/handwritten/for-users/RequestBinder.en.md +++ b/doc/handwritten/for-users/RequestBinder.en.md @@ -327,8 +327,9 @@ The nested binder inherits the parent's options and **prefixes** its argument paths, so a failure inside `Stay` reports `Stay.CheckIn`, not just `CheckIn`. A missing complex property records `REQUEST_ARGUMENT_REQUIRED`; a nested binding that fails contributes its own envelope, whose inner errors already carry the -prefixed paths. Use `AsOptional` instead of `AsRequired` for a nullable nested -object: absent yields `null` and records nothing. +prefixed paths. Use `AsOptionalReference` instead of `AsRequired` for a nullable +nested object: absent yields `null` and records nothing — the same +`AsOptionalReference` name the scalar side uses for a nullable reference value. ## Lists diff --git a/doc/handwritten/for-users/RequestBinder.fr.md b/doc/handwritten/for-users/RequestBinder.fr.md index 6cae81e2..b6d7072f 100644 --- a/doc/handwritten/for-users/RequestBinder.fr.md +++ b/doc/handwritten/for-users/RequestBinder.fr.md @@ -335,9 +335,10 @@ Le binder imbriqué hérite des options du parent et **préfixe** ses chemins d’argument : un échec dans `Stay` rapporte `Stay.CheckIn`, pas seulement `CheckIn`. Une propriété complexe manquante enregistre `REQUEST_ARGUMENT_REQUIRED` ; une liaison imbriquée qui échoue contribue sa propre enveloppe, dont les erreurs -internes portent déjà les chemins préfixés. Utilisez `AsOptional` plutôt +internes portent déjà les chemins préfixés. Utilisez `AsOptionalReference` plutôt qu’`AsRequired` pour un objet imbriqué nullable : l’absence donne `null` et -n’enregistre rien. +n’enregistre rien — le même nom `AsOptionalReference` que le côté scalaire utilise +pour une valeur de référence nullable. ## Listes