Aria 2.0.0
C++23 MVVM framework (C++20 minimum) — reactive, coroutine-first, ABI-layered
Loading...
Searching...
No Matches
validator.hpp
Go to the documentation of this file.
1#pragma once
2
3#include "aria/concepts.hpp"
5#include "aria/error.hpp"
6#include "aria/property.hpp"
9
10#include <functional>
11#include <initializer_list>
12#include <memory>
13#include <optional>
14#include <string>
15#include <utility>
16#include <vector>
17
18namespace aria {
19
20namespace async {
21template<class T> class AsyncValidator;
22}
23
24// ---------------------------------------------------------------------------
25// ValidationResult -- legacy "valid + errors" projection
26//
27// Kept for callers that just want a quick boolean + list. New code should
28// bind to `Validator::state()` directly: it carries the same data plus
29// `touched / dirty / pending`.
30//
31// Per docs/error-model.md the entries are unified `aria::Error`s with
32// `kind == ErrorKind::Validation` and a populated `key`.
33// ---------------------------------------------------------------------------
35 bool valid = true;
36 std::vector<Error> errors;
37
38 explicit operator bool() const noexcept { return valid; }
39
43 [[nodiscard]] std::vector<std::string> error_messages() const {
44 std::vector<std::string> out;
45 out.reserve(errors.size());
46 for (const auto& e : errors) out.push_back(e.message);
47 return out;
48 }
49};
50
51inline bool operator==(const ValidationResult& a, const ValidationResult& b) noexcept {
52 return a.valid == b.valid && a.errors == b.errors;
53}
54inline bool operator!=(const ValidationResult& a, const ValidationResult& b) noexcept {
55 return !(a == b);
56}
57
58// ---------------------------------------------------------------------------
59// ValidationState -- the richer form-state record
60// ---------------------------------------------------------------------------
64 bool valid = true;
65
67 bool pending = false;
68
71 bool touched = false;
72
74 bool dirty = false;
75
78 std::vector<Error> errors;
79
82 std::vector<Error> warnings;
83
84 // ── Convenience queries (dominant UI patterns) ────────────────────
85
87 [[nodiscard]] std::optional<Error> first_error() const {
88 if (errors.empty()) return std::nullopt;
89 return errors.front();
90 }
91
93 [[nodiscard]] std::optional<std::string> first_error_message() const {
94 if (errors.empty()) return std::nullopt;
95 return errors.front().message;
96 }
97
99 [[nodiscard]] std::vector<Error>
100 errors_for(std::string_view field_path) const {
101 std::vector<Error> out;
102 for (const auto& e : errors) {
103 if (e.key.field_path == field_path) out.push_back(e);
104 }
105 return out;
106 }
107
109 [[nodiscard]] bool has_error_with_rule(std::string_view rule_id) const noexcept {
110 for (const auto& e : errors) {
111 if (e.key.rule_id == rule_id) return true;
112 }
113 return false;
114 }
115
116 explicit operator bool() const noexcept { return valid; }
117};
118
119inline bool operator==(const ValidationState& a, const ValidationState& b) noexcept {
120 return a.valid == b.valid
121 && a.pending == b.pending
122 && a.touched == b.touched
123 && a.dirty == b.dirty
124 && a.errors == b.errors
125 && a.warnings == b.warnings;
126}
127inline bool operator!=(const ValidationState& a, const ValidationState& b) noexcept {
128 return !(a == b);
129}
130
131// ---------------------------------------------------------------------------
132// Validator<T>
133// ---------------------------------------------------------------------------
134template<PropertyValue T>
136public:
142 using Rule = std::function<std::optional<std::string>(const T&)>;
143
144 explicit Validator(Property<T>& source, std::string field_path = {})
145 : source_(&source),
146 field_path_(std::move(field_path)),
147 baseline_(source.get()),
148 result_(ValidationResult{true, {}}),
149 state_(ValidationState{}) {
150 sub_ = source_->bind([this](const T& v) {
151 const bool new_dirty = !(v == baseline_);
152 if (new_dirty != state_.peek().dirty) {
153 auto s = state_.peek();
154 s.dirty = new_dirty;
155 state_.set(s);
156 }
157 run_(v);
158 });
159 }
160
162 // Async drivers must stop using this object before any member starts
163 // destruction. Synchronous-only validators never allocate this token.
164 lifetime_.reset();
165 }
166
167 [[nodiscard]] const std::string& field_path() const noexcept {
168 return field_path_;
169 }
170
171 // ── Rule composition ──────────────────────────────────────────────
172
173 Validator& rule(Rule r, std::string rule_id_value = {}) {
174 const std::size_t auto_id = auto_id_counter_++;
175 if (rule_id_value.empty()) {
176 rule_id_value = "rule_" + std::to_string(auto_id);
177 }
178 rules_.push_back(RuleEntry{std::move(r), std::move(rule_id_value)});
179 if (!suspend_) run_(source_->get());
180 return *this;
181 }
182
183 template<std::predicate<const T&> P>
184 Validator& must(P&& predicate, std::string message,
185 std::string rule_id_value = {}) {
186 return rule(
187 [p = std::forward<P>(predicate), m = std::move(message)](
188 const T& v) -> std::optional<std::string> {
189 if (p(v)) return std::nullopt;
190 return m;
191 },
192 std::move(rule_id_value));
193 }
194
195 Validator& warning(Rule r, std::string rule_id_value = {}) {
196 const std::size_t auto_id = auto_id_counter_++;
197 if (rule_id_value.empty()) {
198 rule_id_value = "rule_" + std::to_string(auto_id);
199 }
200 warnings_.push_back(RuleEntry{std::move(r), std::move(rule_id_value)});
201 if (!suspend_) run_(source_->get());
202 return *this;
203 }
204
205 template<std::predicate<const T&> P>
206 Validator& should(P&& predicate, std::string message,
207 std::string rule_id_value = {}) {
208 return warning(
209 [p = std::forward<P>(predicate), m = std::move(message)](
210 const T& v) -> std::optional<std::string> {
211 if (p(v)) return std::nullopt;
212 return m;
213 },
214 std::move(rule_id_value));
215 }
216
217 Validator& rules(std::initializer_list<Rule> rs) {
218 suspend_ = true;
219 for (const auto& r : rs) {
220 const std::size_t auto_id = auto_id_counter_++;
221 rules_.push_back(RuleEntry{r, "rule_" + std::to_string(auto_id)});
222 }
223 suspend_ = false;
224 run_(source_->get());
225 return *this;
226 }
227
228 // ── Form-state transitions ────────────────────────────────────────
229
230 void touch() {
231 if (state_.peek().touched) return;
232 auto s = state_.peek();
233 s.touched = true;
234 state_.set(s);
235 }
236
238 if (!state_.peek().touched) return;
239 auto s = state_.peek();
240 s.touched = false;
241 state_.set(s);
242 }
243
244 void reset_dirty() {
245 // `peek_ref()` instead of `get()`/`get_ref()` so that calling
246 // reset_dirty() from inside a Computed/Effect doesn't make that
247 // Derivation accidentally depend on the source — `reset_dirty`
248 // is administrative and should never establish a reactive edge.
249 baseline_ = source_->peek_ref();
250 if (!state_.peek_ref().dirty) return;
251 auto s = state_.peek_ref();
252 s.dirty = false;
253 state_.set(std::move(s));
254 }
255
257 if (state_.peek().pending) return;
258 auto s = state_.peek();
259 s.pending = true;
260 state_.set(s);
264 "begin_pending",
265 ::aria::ValidationKey{field_path_, std::string{}},
266 std::string{},
267 });
268 }
269 }
270
276 void end_pending(std::vector<std::string> extra_messages) {
277 async_errors_.clear();
278 async_errors_.reserve(extra_messages.size());
279 std::size_t i = 0;
280 for (auto& msg : extra_messages) {
281 async_errors_.push_back(Error::validation(
282 ValidationKey{field_path_, "async_" + std::to_string(i++)},
283 std::move(msg)));
284 }
285 finish_pending_();
286 }
287
290 void end_pending(std::vector<Error> extra) {
291 async_errors_ = std::move(extra);
292 finish_pending_();
293 }
294
296 void end_pending() {
297 async_errors_.clear();
298 finish_pending_();
299 }
300
304 if (!state_.peek_ref().pending) return;
305 auto s = state_.peek_ref();
306 s.pending = false;
307 state_.set(std::move(s));
308 }
309
310 // ── Accessors ─────────────────────────────────────────────────────
311
312 [[nodiscard]] Property<ValidationState>& state() noexcept { return state_; }
313 [[nodiscard]] const Property<ValidationState>& state() const noexcept { return state_; }
314
315 [[nodiscard]] Property<ValidationResult>& result() noexcept { return result_; }
316 [[nodiscard]] const Property<ValidationResult>& result() const noexcept { return result_; }
317
318private:
319 template<class U> friend class async::AsyncValidator;
320
321 std::weak_ptr<void> lifetime_token_() {
322 if (!lifetime_) lifetime_ = std::make_shared<char>();
323 return lifetime_;
324 }
325
326 struct RuleEntry {
327 Rule body;
328 std::string rule_id;
329 };
330
331 void finish_pending_() {
332 {
333 auto s = state_.peek();
334 s.pending = false;
335 state_.set(s);
336 }
339 ::aria::trace::Validation{
340 "end_pending",
341 ::aria::ValidationKey{field_path_, std::string{}},
342 std::string{},
343 });
344 }
345 run_(source_->get());
346 }
347
348 void run_(const T& v) {
349 const bool tracing = ::aria::has_trace_sink();
350 auto s = state_.peek_ref();
351
352 s.errors.clear();
353 for (auto& entry : rules_) {
354 if (auto msg = entry.body(v)) {
355 if (tracing) {
357 ::aria::trace::Validation{
358 "rule_fail",
359 ::aria::ValidationKey{field_path_, entry.rule_id},
360 *msg,
361 });
362 }
363 s.errors.push_back(Error::validation(
364 ValidationKey{field_path_, entry.rule_id},
365 std::move(*msg)));
366 } else if (tracing) {
368 ::aria::trace::Validation{
369 "rule_pass",
370 ::aria::ValidationKey{field_path_, entry.rule_id},
371 std::string{},
372 });
373 }
374 }
375 s.warnings.clear();
376 for (auto& entry : warnings_) {
377 if (auto msg = entry.body(v)) {
378 if (tracing) {
380 ::aria::trace::Validation{
381 "warning_fail",
382 ::aria::ValidationKey{field_path_, entry.rule_id},
383 *msg,
384 });
385 }
386 s.warnings.push_back(Error::validation_warning(
387 ValidationKey{field_path_, entry.rule_id},
388 std::move(*msg)));
389 } else if (tracing) {
391 ::aria::trace::Validation{
392 "warning_pass",
393 ::aria::ValidationKey{field_path_, entry.rule_id},
394 std::string{},
395 });
396 }
397 }
398
399 for (const auto& e : async_errors_) {
400 // Preserve severity while backfilling the owning field and
401 // validation defaults for both failures and soft advisories.
402 Error fixed = e;
404 if (fixed.key.field_path.empty()) fixed.key.field_path = field_path_;
405 if (fixed.source.empty()) fixed.source = "Validator";
406 if (fixed.is_warning()) s.warnings.push_back(std::move(fixed));
407 else s.errors.push_back(std::move(fixed));
408 }
409
410 s.valid = s.errors.empty();
411
412 // Coalesce the two writes into a single graph flush. Without
413 // the batch, observers wired to BOTH `state()` and `result()`
414 // would re-render twice for one validation pass — and
415 // intermediate observers might briefly see `state.valid=true`
416 // while `result.valid=false` (or vice versa). The batch makes
417 // the two updates atomic from the observer's point of view.
418 ValidationResult new_result{s.valid, s.errors};
420 state_.set(std::move(s));
421 result_.set(std::move(new_result));
422 });
423 }
424
425 Property<T>* source_;
426 std::string field_path_;
427 T baseline_;
428 std::vector<RuleEntry> rules_;
429 std::vector<RuleEntry> warnings_;
430 std::vector<Error> async_errors_;
433 Subscription sub_;
434 std::size_t auto_id_counter_ = 0;
435 bool suspend_ = false;
436 std::shared_ptr<void> lifetime_;
437};
438
439} // namespace aria
RAII handle to a single subscription.
Definition subscription.hpp:44
const Property< ValidationResult > & result() const noexcept
Definition validator.hpp:316
std::function< std::optional< std::string >(const T &)> Rule
User-supplied rule body: returns a message when the value fails, std::nullopt when it passes.
Definition validator.hpp:142
void touch()
Definition validator.hpp:230
Validator & rules(std::initializer_list< Rule > rs)
Definition validator.hpp:217
void reset_touched()
Definition validator.hpp:237
Validator(Property< T > &source, std::string field_path={})
Definition validator.hpp:144
Validator & warning(Rule r, std::string rule_id_value={})
Definition validator.hpp:195
void begin_pending()
Definition validator.hpp:256
const std::string & field_path() const noexcept
Definition validator.hpp:167
~Validator()
Definition validator.hpp:161
Validator & should(P &&predicate, std::string message, std::string rule_id_value={})
Definition validator.hpp:206
void cancel_pending()
Cancel pending work while preserving the previous validation result, including async errors and warni...
Definition validator.hpp:303
const Property< ValidationState > & state() const noexcept
Definition validator.hpp:313
Validator & rule(Rule r, std::string rule_id_value={})
Definition validator.hpp:173
void end_pending(std::vector< Error > extra)
Settle pending with caller-shaped error records.
Definition validator.hpp:290
Property< ValidationState > & state() noexcept
Definition validator.hpp:312
void end_pending(std::vector< std::string > extra_messages)
Settle the async validation: clears pending and stores extra_messages as additional Error-severity en...
Definition validator.hpp:276
Validator & must(P &&predicate, std::string message, std::string rule_id_value={})
Definition validator.hpp:184
Property< ValidationResult > & result() noexcept
Definition validator.hpp:315
void end_pending()
Settle pending without any extra errors.
Definition validator.hpp:296
void reset_dirty()
Definition validator.hpp:244
Driver that turns a coroutine factory into a latest-wins async validation rule attached to a Validato...
Definition async_validator.hpp:210
Definition property.hpp:103
Definition async_command.hpp:118
auto batch(Fn &&fn) -> decltype(fn())
Sugar: batch([&]{ firstName = "..."; lastName = "..."; }).
Definition graph.hpp:369
Definition signal.hpp:12
@ Validation
A validator rule failed.
Definition error.hpp:74
void publish_trace_unchecked(const TraceEvent &event) noexcept
Publish an already-built event using one owning sink snapshot.
Definition diagnostics.hpp:293
validation_dsl::RulePart rule_id(std::string id) noexcept
Definition validation_key.hpp:96
bool has_trace_sink() noexcept
True iff a sink is currently installed.
Definition diagnostics.hpp:286
@ Validation
Validator / FormValidator rule runs.
Definition diagnostics.hpp:71
bool operator!=(const Error &a, const Error &b) noexcept
Definition error.hpp:271
bool operator==(const Error &a, const Error &b) noexcept
Definition error.hpp:264
Definition validation_key.hpp:110
One uniform error record.
Definition error.hpp:129
static Error validation(ValidationKey k, std::string msg)
Hard validation error.
Definition error.hpp:170
ErrorKind kind
Definition error.hpp:130
static Error validation_warning(ValidationKey k, std::string msg)
Soft validation advisory (severity = Warning).
Definition error.hpp:176
(field_path, rule_id) locator for a validation message.
Definition validation_key.hpp:52
Definition validator.hpp:34
std::vector< Error > errors
Definition validator.hpp:36
std::vector< std::string > error_messages() const
Project the rich error list down to plain message strings, in the order they were produced.
Definition validator.hpp:43
bool valid
Definition validator.hpp:35
Definition validator.hpp:61
std::optional< Error > first_error() const
First Error-severity entry, if any.
Definition validator.hpp:87
bool pending
True while an async validator is running.
Definition validator.hpp:67
std::vector< Error > errors
Hard failures (kind == Validation, severity == Error).
Definition validator.hpp:78
std::optional< std::string > first_error_message() const
First Error-severity message text, if any.
Definition validator.hpp:93
bool valid
True iff there are no Error-severity entries.
Definition validator.hpp:64
bool touched
True once the user has interacted with the field (typically on focus-out).
Definition validator.hpp:71
std::vector< Error > errors_for(std::string_view field_path) const
All Error-severity entries that target a given field path.
Definition validator.hpp:100
std::vector< Error > warnings
Soft advisories (kind == Validation, severity == Warning).
Definition validator.hpp:82
bool dirty
True once the field's value has moved away from its baseline.
Definition validator.hpp:74
bool has_error_with_rule(std::string_view rule_id) const noexcept
True if any Error-severity entry has the given rule_id.
Definition validator.hpp:109
Validation events.
Definition diagnostics.hpp:160