Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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<Stay>.Failure(LeafPortError()));
.AsOptionalReference(_ => Outcome<Stay>.Failure(LeafPortError()));

Outcome<string> outcome = bind.New(_ => "never");
Error wrapped = outcome.Error!.InnerErrors.Single();
Expand Down Expand Up @@ -271,7 +271,7 @@ public void GuardClauses() {

Check.ThatCode(() => bind.ComplexProperty(r => r.Stay).FailWith(null!)).Throws<ArgumentNullException>();
Check.ThatCode(() => bind.ComplexProperty(r => r.Stay).FailWith(BookingEnvelopeError.StayInvalid).AsRequired<Stay>(null!)).Throws<ArgumentNullException>();
Check.ThatCode(() => bind.ComplexProperty(r => r.Stay).FailWith(BookingEnvelopeError.StayInvalid).AsOptional<Stay>(null!)).Throws<ArgumentNullException>();
Check.ThatCode(() => bind.ComplexProperty(r => r.Stay).FailWith(BookingEnvelopeError.StayInvalid).AsOptionalReference<Stay>(null!)).Throws<ArgumentNullException>();

Check.ThatCode(() => bind.ListOfSimpleProperties(r => r.Tags).AsRequired<Tag>(null!)).Throws<ArgumentNullException>();
Check.ThatCode(() => bind.ListOfSimpleProperties(r => r.Tags).AsOptional<Tag>(null!)).Throws<ArgumentNullException>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ private static Outcome<BookingCommand> BindCommand(BookingRequest request) {
RequiredField<string> reference = bind.SimpleProperty(r => r.Reference).AsRequired();
RequiredField<Currency> currency = bind.SimpleProperty(r => r.Currency).AsOptional(Currency.Parse, "EUR");
OptionalValueField<int> maxNights = bind.SimpleProperty(r => r.MaxNights).AsOptionalValue(PositiveInt.Parse);
OptionalReferenceField<Stay> stay = bind.ComplexProperty(r => r.Stay).FailWith(BookingEnvelopeError.StayInvalid).AsOptional(BindStay);
OptionalReferenceField<Stay> stay = bind.ComplexProperty(r => r.Stay).FailWith(BookingEnvelopeError.StayInvalid).AsOptionalReference(BindStay);
RequiredField<IReadOnlyList<Tag>> tags = bind.ListOfSimpleProperties(r => r.Tags).AsOptional(Tag.Parse);
RequiredField<IReadOnlyList<Guest>> guests = bind.ListOfComplexProperties(r => r.Guests).FailWith(BookingEnvelopeError.GuestInvalid).AsRequired(BindGuest);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,19 +65,19 @@ 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<Stay> none = absent.ComplexProperty(r => r.Stay).FailWith(BookingEnvelopeError.StayInvalid).AsOptional(BindStay);
OptionalReferenceField<Stay> 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<Stay> some = present.ComplexProperty(r => r.Stay).FailWith(BookingEnvelopeError.StayInvalid).AsOptional(BindStay);
OptionalReferenceField<Stay> 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));
}

[Fact(DisplayName = "An optional complex property that is present but invalid records its envelope.")]
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<string> outcome = bind.New(_ => "never");
Check.That(outcome.IsFailure).IsTrue();
Expand Down
10 changes: 6 additions & 4 deletions FirstClassErrors.RequestBinder/ComplexPropertyConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,15 +65,17 @@ public RequiredField<TProperty> AsRequired<TProperty>(Func<RequestBinder<TArgume
}

/// <summary>
/// Binds an optional complex argument: absent yields a <c>null</c>
/// <see cref="OptionalReferenceField{TProperty}" /> value and records nothing; a present-but-invalid nested
/// binding records its envelope.
/// Binds an optional complex argument whose bound type is a <b>reference type</b>: absent yields a
/// <c>null</c> <see cref="OptionalReferenceField{TProperty}" /> value and records nothing; a
/// present-but-invalid nested binding records its envelope. The complex counterpart of the scalar
/// <c>AsOptionalReference</c>; the split name keeps <c>AsOptionalValue</c> free for a future value-type
/// nested object.
/// </summary>
/// <typeparam name="TProperty">The reference type the nested binding produces.</typeparam>
/// <param name="bindNested">The nested binding function (typically a method group such as <c>BindAddress</c>).</param>
/// <returns>The bound field token.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="bindNested" /> is <c>null</c>.</exception>
public OptionalReferenceField<TProperty> AsOptional<TProperty>(Func<RequestBinder<TArgument>, Outcome<TProperty>> bindNested) where TProperty : class {
public OptionalReferenceField<TProperty> AsOptionalReference<TProperty>(Func<RequestBinder<TArgument>, Outcome<TProperty>> bindNested) where TProperty : class {
if (bindNested is null) { throw new ArgumentNullException(nameof(bindNested)); }

if (_isMissing) { return new OptionalReferenceField<TProperty>(_binder, value: null); }
Expand Down
5 changes: 3 additions & 2 deletions doc/handwritten/for-users/RequestBinder.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
5 changes: 3 additions & 2 deletions doc/handwritten/for-users/RequestBinder.fr.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down