From 9b6daea743d2539094436db7e3b320e977c65716 Mon Sep 17 00:00:00 2001 From: Yaraslau Tamashevich Date: Wed, 23 Sep 2026 20:57:20 +0200 Subject: [PATCH] forms: say at the example that allRequiredEngaged is flat and string-blind (refs #793) `forms.hpp`'s file comment offers `return morph::forms::allRequiredEngaged(*this);` as the model `validate()` body, thirty lines under a heading that reads "Nested aggregates (recursive, cycle-safe)". That heading is about `schemaJson()`. The readiness helper is flat, and its "non-quantity members are not checked" sentence does not say which ordinary member types that sweeps up. Measured on this revision, with a probe compiled against the real header (clang 22.1.8, glaze v7.4.0): EmptyCapableField = 0 EmptyCapableField = 0 EmptyCapableField> = 0 [id set, empty name] hand-written `id.hasValue() && !name.empty()` = 0 allRequiredEngaged = 1 [optional engaged, Sub's own required id empty] hand-written `!b.has_value() || b->validate()` = 0 allRequiredEngaged = 1 [Sub by value + vector, their ids empty] allRequiredEngaged = 1 Both facts are already stated precisely in `docs/spec/forms/forms.md`; neither was stated where an action author meets the helper. This adds them to the `@par Readiness helper` block and to the function's own Doxygen, and changes no code. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01VptDWG2fKr2vBnLSJcgzgW --- include/morph/forms/forms.hpp | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/include/morph/forms/forms.hpp b/include/morph/forms/forms.hpp index 97bc6a23b..fa4f29e17 100644 --- a/include/morph/forms/forms.hpp +++ b/include/morph/forms/forms.hpp @@ -146,7 +146,17 @@ /// array, the client-side submit gate, and the fielded-action readiness /// check. Non-quantity members are not checked — a plain `int64_t` cannot /// express "not filled in"; use a `Quantity` (or a custom `validate()`) when -/// that distinction matters. +/// that distinction matters. That exclusion covers `std::string` too: a +/// `std::string` member exposes `empty()`, not `hasValue()`, so a "this text +/// field must be non-empty" rule is a custom `validate()` body, never +/// something this helper reports on. +/// +/// Its walk is **flat**, unlike the schema generation described above: it +/// inspects the action's own top-level members and does not descend into a +/// nested aggregate, a `std::vector` of one, or a `std::optional` holding +/// one. A nested type's own required fields are therefore *not* gated by an +/// outer `allRequiredEngaged` — delegating to the nested value's `validate()` +/// is the caller's to write. #include #include @@ -3324,6 +3334,13 @@ inline void enforceQuantityBounds(const A& action) { /// `A::computedFields` entry (a computed field is never something the user /// must fill -- see `morph::forms::recomputeAll`). Intended as the body of the /// action's `validate()`. +/// +/// The walk is flat: only @p action's own top-level members are inspected, +/// never a nested aggregate's, a `std::vector`'s, or the value inside a +/// `std::optional`. A member whose type has no `hasValue()` -- a +/// `std::string`, a plain integer, a nested struct -- is skipped entirely, so +/// an action needing "this string is non-empty" or "this nested value is +/// itself valid" writes that in its own `validate()` alongside this call. /// @tparam A Action type (a reflectable aggregate). /// @param action Draft whose fields are checked. /// @return `true` when no required empty-capable field is empty.