morph 0.1.0
Asynchronous bridge between GUI thread and business models
Loading...
Searching...
No Matches
field_errors.hpp
1// SPDX-License-Identifier: Apache-2.0
2#pragma once
3
4#include <concepts>
5#include <cstddef>
6#include <cstdint>
7#include <functional>
8#include <map>
9#include <memory>
10#include <string>
11#include <string_view>
12#include <type_traits>
13
14#include "forms.hpp"
15
16namespace morph::forms {
17
18/// @brief The message `fieldErrors` gives a required field with no value.
19inline constexpr std::string_view kFieldRequired = "required";
20/// @brief The message `fieldErrors` gives a `Quantity` outside its unit's declared bounds.
21inline constexpr std::string_view kFieldOutOfRange = "out of range";
22/// @brief The message `fieldErrors` gives a value violating its `fieldMetadata` `minimum`, `maximum` or `multipleOf`.
23inline constexpr std::string_view kFieldOutOfBounds = "out of bounds";
24
25namespace detail::field_errors {
26
27/// Whether a member is a required empty-capable field with no value.
28template <typename A, typename Member>
29[[nodiscard]] bool missing(A const& action, std::string_view name, Member const& member) {
30 if constexpr (EmptyCapableField<Member>) {
31 bool const isComputed =
32 detail::isComputedDestinationMember(action, static_cast<void const*>(std::addressof(member)));
33 return !detail::declaredOptional<A>(name) && !isComputed && !member.hasValue();
34 } else {
35 static_cast<void>(action);
36 static_cast<void>(name);
37 static_cast<void>(member);
38 return false;
39 }
40}
41
42/// Whether a member is a `Quantity` outside its unit's declared bounds.
43template <typename Member>
44[[nodiscard]] bool outOfRange(Member const& member) {
45 if constexpr (units::isQuantity<Member>) {
46 return !member.withinDeclaredBounds();
47 } else {
48 static_cast<void>(member);
49 return false;
50 }
51}
52
53/// Whether a member's value violates the bounds its `fieldMetadata` entry declares; an unengaged one never does.
54template <typename A, typename Member>
55[[nodiscard]] bool outOfBounds(std::string_view name, Member const& member) {
56 if constexpr (detail::HasFieldMetadata<A>) {
57 FieldMeta const* const meta = detail::findFieldMeta<A>(name);
58 if (meta == nullptr || !detail::declaresAnyBound(*meta)) {
59 return false;
60 }
61 if constexpr (units::isQuantity<Member>) {
62 return member.hasValue() && !detail::satisfiesDeclaredBounds(*meta, *member);
63 } else if constexpr (std::same_as<Member, ::morph::math::Rational>) {
64 return !detail::satisfiesDeclaredBounds(*meta, member);
65 } else if constexpr (detail::BoundCheckableInteger<Member>) {
66 return !detail::satisfiesDeclaredBounds(
67 *meta, ::morph::math::Rational{static_cast<std::int64_t>(member), ::morph::math::DecimalPlaces{0}});
68 } else {
69 return false;
70 }
71 } else {
72 static_cast<void>(name);
73 static_cast<void>(member);
74 return false;
75 }
76}
77
78} // namespace detail::field_errors
79
80/// @brief The per-field verdicts of the checks `allRequiredEngaged`, `checkQuantityBounds` and
81/// `allFieldBoundsSatisfied` make, keyed by field name.
82///
83/// The same three checks, made field by field instead of answering one boolean, so a caller can attach each failure
84/// to the element that shows the field. A field fails at most one check, reported in this order: `required` (a
85/// required empty-capable field with no value), `out of range` (a `Quantity` outside its unit's bounds), `out of
86/// bounds` (a declared `minimum`, `maximum` or `multipleOf` violated). The walk is as flat as theirs: only the
87/// action's own top-level members.
88///
89/// The action's own `validate()` is not consulted: it answers for the whole action, not for a field.
90/// @tparam A Action type (a reflectable aggregate).
91/// @param action The action, after `recomputeAll`, so a computed field is judged on the value it will have.
92/// @return Field name to message; empty when every check passes.
93template <typename A>
94[[nodiscard]] std::map<std::string, std::string, std::less<>> fieldErrors(A const& action) {
95 using Plain = std::remove_cvref_t<A>;
96 std::map<std::string, std::string, std::less<>> errors;
97 if constexpr (glz::reflectable<Plain> || glz::glaze_object_t<Plain>) {
98 detail::forEachNamedMember(action, [&]<std::size_t I>(std::string_view name, auto const& member) {
99 if (detail::field_errors::missing<Plain>(action, name, member)) {
100 errors.emplace(name, kFieldRequired);
101 } else if (detail::field_errors::outOfRange(member)) {
102 errors.emplace(name, kFieldOutOfRange);
103 } else if (detail::field_errors::outOfBounds<Plain>(name, member)) {
104 errors.emplace(name, kFieldOutOfBounds);
105 }
106 });
107 } else {
108 static_cast<void>(action);
109 }
110 return errors;
111}
112
113} // namespace morph::forms
JSON-Forms-style schema generation for morph actions.