morph 0.1.0
Asynchronous bridge between GUI thread and business models
Loading...
Searching...
No Matches
rules.hpp
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
2
3#pragma once
4
5/// @file forms/engine/rules.hpp
6/// @brief `x-rules` read into `RuleExpr` trees, translated into the UI document's expression language (`expr/1`)
7/// and evaluated by its one evaluator, three-valued and exactly.
8///
9/// Specified in `docs/spec/forms/engine.md`, "Rules".
10
11#include <algorithm>
12#include <array>
13#include <cstddef>
14#include <cstdint>
15#include <expected>
16#include <functional>
17#include <limits>
18#include <optional>
19#include <span>
20#include <string>
21#include <string_view>
22#include <utility>
23#include <vector>
24
27#include "../../ui/value.hpp"
29#include "detail/text.hpp"
30
31namespace morph::forms {
32
33/// @brief Why a schema does not read as a form: where, and why.
35 /// @brief Where in the schema: a dotted path such as `properties.amount` or `x-rules[2].when`.
36 std::string path;
37 /// @brief Why, in one sentence.
38 std::string message;
39 /// @brief Field-wise equality.
40 /// @param other The error to compare with.
41 /// @return Whether both members are equal.
42 bool operator==(SchemaError const& other) const = default;
43};
44
45/// @brief A three-valued verdict: a half-typed form has fields with no value yet, so a rule can be undecided.
46enum class Tri : std::uint8_t {
47 False, ///< Definitely does not hold.
48 True, ///< Definitely holds.
49 Unknown ///< Cannot be decided: an operand does not encode, or the kind is not one this engine knows.
50};
51
52/// @brief Kleene negation.
53/// @param value The verdict.
54/// @return `True` for `False`, `False` for `True`, `Unknown` for `Unknown`.
55[[nodiscard]] constexpr Tri triNot(Tri value) noexcept {
56 switch (value) {
57 case Tri::False:
58 return Tri::True;
59 case Tri::True:
60 return Tri::False;
61 case Tri::Unknown:
62 default:
63 return Tri::Unknown;
64 }
65}
66
67/// @brief What a rule reads of one field.
69 /// @brief Whether the field has a value a rule can use.
70 enum class State : std::uint8_t {
71 Blank, ///< Nothing entered.
72 Invalid, ///< Something entered that does not encode.
73 Valid ///< Entered and encodes; `value` holds the exact value.
74 };
75 /// @brief The state.
77 /// @brief The encoded value while `Valid`: an `int`, `decimal`, `bool`, `string` or `timestamp`; `null`
78 /// otherwise, and for a value the expression language cannot hold exactly.
80};
81
82/// @brief Reads one field by wire name, for a rule. The session's lookup is tracked, so a verdict computed inside a
83/// reactive computation recomputes when a field it read changes.
84using RuleLookup = std::function<RuleOperand(std::string_view field)>;
85
86namespace detail::engine {
87
88/// @brief The operands of one rule tree: each field it names, numbered in first-seen order.
89class RuleOperands {
90public:
91 /// @brief The number of a field, adding it when it is new.
92 /// @param field The wire name.
93 /// @return Its index.
94 std::size_t indexOf(std::string const& field) {
95 auto const found = std::ranges::find(_fields, field);
96 if (found != _fields.end()) {
97 return static_cast<std::size_t>(found - _fields.begin());
98 }
99 _fields.push_back(field);
100 return _fields.size() - 1;
101 }
102
103 /// @brief The fields, by index.
104 /// @return The fields.
105 [[nodiscard]] std::vector<std::string> take() && { return std::move(_fields); }
106
107private:
108 std::vector<std::string> _fields;
109};
110
111/// @brief A string node.
112/// @param value The text.
113/// @return The node.
114[[nodiscard]] inline Json jsonText(std::string value) {
115 Json node{};
116 node.data = std::move(value);
117 return node;
118}
119
120/// @brief `{"fn": name, "args": [...]}`.
121/// @param name The function.
122/// @param args Its arguments.
123/// @return The call's JSON.
124[[nodiscard]] inline Json call(std::string_view name, std::vector<Json> args) {
125 Json node = emptyObject();
126 node.get_object().insert_or_assign("fn", jsonText(std::string{name}));
127 Json list{};
128 list.data.emplace<Json::array_t>(std::move(args));
129 node.get_object().insert_or_assign("args", std::move(list));
130 return node;
131}
132
133/// @brief A one-key object `{key: value}`.
134/// @param key The key.
135/// @param value The value.
136/// @return The object.
137[[nodiscard]] inline Json single(std::string_view key, Json value) {
138 Json node = emptyObject();
139 node.get_object().insert_or_assign(std::string{key}, std::move(value));
140 return node;
141}
142
143/// @brief `{"if": [condition, then, otherwise]}`.
144/// @param condition The condition.
145/// @param then The value when it is true.
146/// @param otherwise The value when it is false.
147/// @return The expression.
148[[nodiscard]] inline Json choose(Json condition, Json then, Json otherwise) {
149 Json list{};
150 list.data.emplace<Json::array_t>(Json::array_t{std::move(condition), std::move(then), std::move(otherwise)});
151 return single("if", std::move(list));
152}
153
154/// @brief A boolean literal.
155/// @param flag The value.
156/// @return The literal.
157[[nodiscard]] inline Json literal(bool flag) {
158 Json node{};
159 node.data = flag;
160 return node;
161}
162
163/// @brief The `null` literal: an undecided verdict.
164/// @return The literal.
165[[nodiscard]] inline Json nullLiteral() {
166 Json node{};
167 node.data = nullptr;
168 return node;
169}
170
171/// @brief An integer literal.
172/// @param number The value.
173/// @return The literal.
174[[nodiscard]] inline Json integer(std::int64_t number) {
175 Json node{};
176 node.data = number;
177 return node;
178}
179
180} // namespace detail::engine
181
182/// @brief One `x-rules` entry, or a condition inside one, and its translation into `expr/1`.
183///
184/// The 16 kinds the emitters write are read; any other kind parses and is `Unknown`, so an older client never
185/// refuses a newer server's form. A rule's operands are numbered `f0`, `f1`, … in its translation, and each
186/// operand reads as two names: `fN.engaged` (`true`, `false`, or `null` when its draft does not encode) and
187/// `fN.value` (the encoded value, `null` unless it is valid). The two are needed because a blank operand and an
188/// invalid one are both "no value" to the expression language, and the rules tell them apart: a comparison with a
189/// blank operand holds vacuously, one with an invalid operand is `Unknown`.
190// NOLINTNEXTLINE(misc-no-recursion): a rule's conditions are rules, so copying one copies them.
191class RuleExpr {
192public:
193 /// @brief The rule kinds.
194 enum class Kind : std::uint8_t {
195 Engaged, ///< `engaged`: the field has a value.
196 NotEngaged, ///< `notEngaged`.
197 Equals, ///< `equals`: the field equals the literal `value` (`valueText` for wide integers).
198 Greater, ///< `greater`: the first field is greater than the second, when both are engaged.
199 GreaterOrEqual, ///< `greaterOrEqual`.
200 Less, ///< `less`.
201 LessOrEqual, ///< `lessOrEqual`.
202 RequiredWhen, ///< `requiredWhen`: the fields are required while `when` holds.
203 ExactlyOneOf, ///< `exactlyOneOf`: exactly one field is engaged.
204 AtLeastOneOf, ///< `atLeastOneOf`.
205 MutuallyExclusive, ///< `mutuallyExclusive`: at most one field is engaged.
206 VisibleWhen, ///< `visibleWhen`: presentation; the fields are shown while `when` holds.
207 ReadonlyWhen, ///< `readonlyWhen`: presentation; the fields are frozen while `when` holds.
208 And, ///< `and` over `conditions`.
209 Or, ///< `or` over `conditions`.
210 Not, ///< `not` of `condition`.
211 Unknown ///< A kind this engine does not know.
212 };
213
214 /// @brief Reads one rule or condition node and translates it.
215 /// @param node The node.
216 /// @param path Where it is, for errors.
217 /// @return The rule, or why it does not read: a node that is not an object, a missing `kind`, a malformed
218 /// `fields`, a known kind without the operands it needs, or a translation larger than `expr/1` admits.
219 [[nodiscard]] static std::expected<RuleExpr, SchemaError> parse(detail::engine::Json const& node,
220 std::string const& path) {
221 auto parsed = parseNode(node, path);
222 if (!parsed) {
223 return parsed;
224 }
225 detail::engine::RuleOperands operands;
226 detail::engine::Json const translation = parsed->verdictExpression(operands);
227 auto compiled = ui::parseExpression(translation);
228 if (!compiled) {
229 return std::unexpected(
230 SchemaError{.path = path,
231 .message = "the rule does not translate into an expression: " + compiled.error().reason});
232 }
233 parsed->_expression = translation;
234 parsed->_compiled = *std::move(compiled);
235 parsed->_operands = std::move(operands).take();
236 return parsed;
237 }
238
239 /// @brief The kind.
240 /// @return The kind; `Unknown` for a kind this engine does not know.
241 [[nodiscard]] Kind kind() const noexcept { return _kind; }
242
243 /// @brief The kind as the schema spells it.
244 /// @return The `kind` string.
245 [[nodiscard]] std::string_view kindName() const noexcept { return _kindName; }
246
247 /// @brief The fields this node names directly (`fields`).
248 /// @return The wire names.
249 [[nodiscard]] std::span<std::string const> fields() const noexcept { return _fields; }
250
251 /// @brief Whether this node names @p field directly. A presentation rule applies to every field it names.
252 /// @param field The wire name.
253 /// @return Whether it is among `fields()`.
254 [[nodiscard]] bool names(std::string_view field) const noexcept {
255 return std::ranges::find(_fields, field) != _fields.end();
256 }
257
258 /// @brief The `when` clause of `requiredWhen`, `visibleWhen` and `readonlyWhen`.
259 /// @return The clause, or null for any other kind.
260 [[nodiscard]] RuleExpr const* when() const noexcept {
261 bool const hasWhen = _kind == Kind::RequiredWhen || _kind == Kind::VisibleWhen || _kind == Kind::ReadonlyWhen;
262 return hasWhen && !_children.empty() ? &_children.front() : nullptr;
263 }
264
265 /// @brief The operands of `and` and `or`, or the one operand of `not`.
266 /// @return The child conditions; empty for any other kind.
267 [[nodiscard]] std::span<RuleExpr const> conditions() const noexcept {
268 bool const composite = _kind == Kind::And || _kind == Kind::Or || _kind == Kind::Not;
269 return composite ? std::span<RuleExpr const>{_children} : std::span<RuleExpr const>{};
270 }
271
272 /// @brief Whether the rule decides readiness. `visibleWhen` and `readonlyWhen` only present.
273 /// @return `false` for the presentation kinds.
274 [[nodiscard]] bool gates() const noexcept { return _kind != Kind::VisibleWhen && _kind != Kind::ReadonlyWhen; }
275
276 /// @brief Whether the rule is a presentation rule of @p kind that names @p field.
277 /// @param kind `RequiredWhen`, `VisibleWhen` or `ReadonlyWhen`.
278 /// @param field The wire name.
279 /// @return Whether it applies to the field's presentation.
280 [[nodiscard]] bool presents(Kind kind, std::string_view field) const noexcept {
281 return _kind == kind && names(field);
282 }
283
284 /// @brief The translation, as `expr/1` JSON: for a gating rule its test, for a presentation rule its `when`
285 /// clause. Set on a node `parse` returned; a child node holds none.
286 /// @return The expression's JSON.
287 [[nodiscard]] detail::engine::Json const& expression() const noexcept { return _expression; }
288
289 /// @brief The wire names the translation reads, by operand index (`f0` is the first).
290 /// @return The operands.
291 [[nodiscard]] std::span<std::string const> operands() const noexcept { return _operands; }
292
293 /// @brief Evaluates the translation with the UI document's evaluator.
294 ///
295 /// For a gating rule this is its test; for `visibleWhen`, `readonlyWhen` and the presentation side of
296 /// `requiredWhen` use `whenHolds`.
297 /// @param lookup Reads each operand.
298 /// @return The verdict: `null` is `Unknown`.
299 [[nodiscard]] Tri evaluate(RuleLookup const& lookup) const {
300 if (_compiled == nullptr) {
301 return Tri::Unknown;
302 }
303 Environment const env{_operands, lookup};
304 return verdictOf(ui::evaluate(*_compiled, env));
305 }
306
307 /// @brief Evaluates the `when` clause of a `requiredWhen`, `visibleWhen` or `readonlyWhen`.
308 /// @param lookup Reads each operand.
309 /// @return The clause's verdict; `Unknown` for any other kind.
310 [[nodiscard]] Tri whenHolds(RuleLookup const& lookup) const {
311 if (_whenCompiled == nullptr) {
312 return Tri::Unknown;
313 }
314 Environment const env{_operands, lookup};
315 return verdictOf(ui::evaluate(*_whenCompiled, env));
316 }
317
318private:
319 /// @brief Answers `fN.engaged` and `fN.value` from a lookup; every other name reads `null`.
320 class Environment final : public ui::Environment {
321 public:
322 Environment(std::span<std::string const> operands, RuleLookup const& lookup) noexcept
323 : _operands{operands}, _lookup{&lookup} {}
324
325 [[nodiscard]] ui::Value ref(std::string_view head, std::span<std::string const> path) const override {
326 if (head.size() < 2 || head.front() != 'f' || path.size() != 1) {
327 return {};
328 }
329 std::size_t index = 0;
330 for (char const chr : head.substr(1)) {
331 if (chr < '0' || chr > '9' || index > (std::numeric_limits<std::size_t>::max() / 10U) - 1U) {
332 return {};
333 }
334 index = (index * 10U) + static_cast<std::size_t>(chr - '0');
335 }
336 if (index >= _operands.size()) {
337 return {};
338 }
339 RuleOperand const operand = (*_lookup)(*std::next(_operands.begin(), static_cast<std::ptrdiff_t>(index)));
340 if (path.front() == "engaged") {
341 switch (operand.state) {
343 return false;
345 return true;
347 default:
348 return {};
349 }
350 }
351 if (path.front() == "value" && operand.state == RuleOperand::State::Valid) {
352 return operand.value;
353 }
354 return {};
355 }
356
357 [[nodiscard]] ui::Value row(std::span<std::string const> path) const override {
358 static_cast<void>(path);
359 return {};
360 }
361
362 [[nodiscard]] ui::Value control(ui::ExprKind form, std::string_view name) const override {
363 static_cast<void>(form);
364 static_cast<void>(name);
365 return {};
366 }
367
368 [[nodiscard]] ui::Value pref(std::string_view point, std::span<std::string const> path) const override {
369 static_cast<void>(point);
370 static_cast<void>(path);
371 return {};
372 }
373
374 [[nodiscard]] ui::Value client(ui::ExprKind form, std::string_view name) const override {
375 static_cast<void>(form);
376 static_cast<void>(name);
377 return {};
378 }
379
380 [[nodiscard]] ui::FormatContext const& formatting() const override {
381 static ui::FormatContext const kContext{};
382 return kContext;
383 }
384
385 // A comparison of two kinds that do not order (a string field against a number field) is `null`, which the
386 // verdict already reads as `Unknown`; there is nothing further to report.
387 // NOLINTNEXTLINE(bugprone-easily-swappable-parameters): the evaluator's interface.
388 void report(std::string_view path, std::string_view reason) const override {
389 static_cast<void>(path);
390 static_cast<void>(reason);
391 }
392
393 private:
394 std::span<std::string const> _operands;
395 RuleLookup const* _lookup;
396 };
397
398 [[nodiscard]] static Tri verdictOf(ui::Value const& value) noexcept {
399 bool const* const flag = value.asBool();
400 if (flag == nullptr) {
401 return Tri::Unknown;
402 }
403 return *flag ? Tri::True : Tri::False;
404 }
405
406 // A condition tree is read by the same function, node by node; one case per rule kind.
407 // NOLINTNEXTLINE(misc-no-recursion,readability-function-cognitive-complexity)
408 [[nodiscard]] static std::expected<RuleExpr, SchemaError> parseNode(detail::engine::Json const& node,
409 std::string const& path) {
410 using detail::engine::Json;
411 if (!node.is_object()) {
412 return std::unexpected(SchemaError{.path = path, .message = "a rule must be a JSON object"});
413 }
414 auto const* const kind = detail::engine::stringAt(node, "kind");
415 if (kind == nullptr) {
416 return std::unexpected(SchemaError{.path = path, .message = "a rule needs a string \"kind\""});
417 }
418 RuleExpr expr{};
419 expr._kindName = *kind;
420 expr._kind = kindOf(*kind);
421 if (Json const* const fields = detail::engine::member(node, "fields")) {
422 if (!fields->is_array()) {
423 return std::unexpected(SchemaError{.path = path, .message = "\"fields\" must be an array of strings"});
424 }
425 for (Json const& name : fields->get_array()) {
426 auto const* const text = name.get_if<std::string>();
427 if (text == nullptr) {
428 return std::unexpected(
429 SchemaError{.path = path, .message = "\"fields\" must be an array of strings"});
430 }
431 expr._fields.push_back(*text);
432 }
433 }
434 // NOLINTNEXTLINE(misc-no-recursion): a child is a condition tree read by parseNode.
435 auto const child = [&](std::string_view key) -> std::expected<void, SchemaError> {
436 Json const* const nested = detail::engine::member(node, key);
437 if (nested == nullptr) {
438 return std::unexpected(
439 SchemaError{.path = path, .message = expr._kindName + " needs \"" + std::string{key} + "\""});
440 }
441 auto parsed = parseNode(*nested, path + "." + std::string{key});
442 if (!parsed) {
443 return std::unexpected(parsed.error());
444 }
445 expr._children.push_back(*std::move(parsed));
446 return {};
447 };
448 auto const needFields = [&](std::size_t count, bool exact) -> std::expected<void, SchemaError> {
449 if (exact ? expr._fields.size() != count : expr._fields.size() < count) {
450 return std::unexpected(SchemaError{.path = path,
451 .message = "\"" + expr._kindName + "\" needs " +
452 (exact ? "exactly " : "at least ") +
453 std::to_string(count) + " field(s)"});
454 }
455 return {};
456 };
457 std::expected<void, SchemaError> shape{};
458 switch (expr._kind) {
462 shape = needFields(1, false);
463 if (shape) {
464 shape = child("when");
465 }
466 break;
467 case Kind::Not:
468 shape = child("condition");
469 break;
470 case Kind::And:
471 case Kind::Or: {
472 Json const* const list = detail::engine::member(node, "conditions");
473 if (list == nullptr || !list->is_array()) {
474 shape = std::unexpected(SchemaError{
475 .path = path, .message = "\"" + expr._kindName + R"(" needs a "conditions" array)"});
476 break;
477 }
478 std::size_t index = 0;
479 for (Json const& nested : list->get_array()) {
480 auto parsed = parseNode(nested, path + ".conditions[" + std::to_string(index++) + "]");
481 if (!parsed) {
482 return std::unexpected(parsed.error());
483 }
484 expr._children.push_back(*std::move(parsed));
485 }
486 break;
487 }
488 case Kind::Greater:
490 case Kind::Less:
492 shape = needFields(2, true);
493 break;
494 case Kind::Equals:
495 shape = needFields(1, false);
496 expr._literal = literalOf(node);
497 break;
498 case Kind::Engaged:
499 case Kind::NotEngaged:
503 shape = needFields(1, false);
504 break;
505 case Kind::Unknown:
506 default:
507 break;
508 }
509 if (!shape) {
510 return std::unexpected(shape.error());
511 }
512 return expr;
513 }
514
515 [[nodiscard]] static Kind kindOf(std::string_view name) noexcept {
516 constexpr std::array<std::pair<std::string_view, Kind>, 16> kNames{{
517 {"engaged", Kind::Engaged},
518 {"notEngaged", Kind::NotEngaged},
519 {"equals", Kind::Equals},
520 {"greater", Kind::Greater},
521 {"greaterOrEqual", Kind::GreaterOrEqual},
522 {"less", Kind::Less},
523 {"lessOrEqual", Kind::LessOrEqual},
524 {"requiredWhen", Kind::RequiredWhen},
525 {"exactlyOneOf", Kind::ExactlyOneOf},
526 {"atLeastOneOf", Kind::AtLeastOneOf},
527 {"mutuallyExclusive", Kind::MutuallyExclusive},
528 {"visibleWhen", Kind::VisibleWhen},
529 {"readonlyWhen", Kind::ReadonlyWhen},
530 {"and", Kind::And},
531 {"or", Kind::Or},
532 {"not", Kind::Not},
533 }};
534 for (auto const& [text, kind] : kNames) {
535 if (text == name) {
536 return kind;
537 }
538 }
539 return Kind::Unknown;
540 }
541
542 /// The literal `equals` compares against, as an `expr/1` literal: a bool, a string, an `int` (from `valueText`
543 /// when the emitter wrote the digits, so nothing passes through a double), or a decimal. A literal the language
544 /// cannot hold exactly (an integer beyond int64) is `null`, so the comparison is `Unknown`.
545 [[nodiscard]] static detail::engine::Json literalOf(detail::engine::Json const& node) {
546 using detail::engine::Json;
547 auto const wide = [](std::string_view digits) -> Json {
548 auto const parsed = render::parseDecimal(digits, render::NumericLocale{});
549 if (!parsed || !parsed->isInteger()) {
550 return detail::engine::nullLiteral();
551 }
552 return detail::engine::integer(parsed->numerator);
553 };
554 if (auto const* const digits = detail::engine::stringAt(node, "valueText");
555 digits != nullptr && detail::engine::isCanonicalInteger(*digits)) {
556 return wide(*digits);
557 }
558 Json const* const value = detail::engine::member(node, "value");
559 if (value == nullptr) {
560 return detail::engine::nullLiteral();
561 }
562 if (auto const* const flag = value->get_if<bool>()) {
563 return detail::engine::literal(*flag);
564 }
565 if (auto const* const text = value->get_if<std::string>()) {
566 return detail::engine::jsonText(*text);
567 }
568 if (value->get_if<std::int64_t>() != nullptr || value->get_if<std::uint64_t>() != nullptr) {
569 return wide(detail::engine::numberText(*value).value_or(std::string{}));
570 }
571 std::optional<math::Rational> exact;
572 if (auto const number = detail::engine::numberText(*value)) {
573 exact = render::parseDecimal(*number, render::NumericLocale{});
574 } else if (value->is_object()) {
575 auto const decoded = ui::valueFromJson(*value);
576 if (decoded && decoded->asDecimal() != nullptr) {
577 exact = decoded->asDecimal()->value;
578 }
579 }
580 if (!exact) {
581 return detail::engine::nullLiteral();
582 }
583 Json decimal = detail::engine::emptyObject();
584 decimal.get_object().insert_or_assign("num", detail::engine::integer(exact->numerator));
585 decimal.get_object().insert_or_assign("den", detail::engine::integer(exact->denominator));
586 decimal.get_object().insert_or_assign(
587 "dp", detail::engine::integer(static_cast<std::int64_t>(exact->decimalPlaces.value)));
588 return detail::engine::single("lit", std::move(decimal));
589 }
590
591 [[nodiscard]] static detail::engine::Json engagedRef(detail::engine::RuleOperands& operands,
592 std::string const& field) {
593 return detail::engine::single(
594 "ref", detail::engine::jsonText("f" + std::to_string(operands.indexOf(field)) + ".engaged"));
595 }
596
597 [[nodiscard]] static detail::engine::Json valueRef(detail::engine::RuleOperands& operands,
598 std::string const& field) {
599 return detail::engine::single(
600 "ref", detail::engine::jsonText("f" + std::to_string(operands.indexOf(field)) + ".value"));
601 }
602
603 /// The rule's whole verdict: a gating rule's test, a presentation rule's `when` clause.
604 [[nodiscard]] detail::engine::Json verdictExpression(detail::engine::RuleOperands& operands) {
605 using detail::engine::call;
606 using detail::engine::choose;
607 using detail::engine::Json;
608 if (_kind == Kind::RequiredWhen) {
609 Json const condition = _children.front().holds(operands);
610 std::vector<Json> engaged;
611 engaged.reserve(_fields.size());
612 for (std::string const& field : _fields) {
613 engaged.push_back(engagedRef(operands, field));
614 }
615 compileWhen(condition);
616 // Not required unless the condition is definitely true: an undecided condition does not demand
617 // the fields.
618 return choose(call("coalesce", {condition, detail::engine::literal(false)}),
619 call("and", std::move(engaged)), detail::engine::literal(true));
620 }
621 if (_kind == Kind::VisibleWhen || _kind == Kind::ReadonlyWhen) {
622 Json condition = _children.front().holds(operands);
623 compileWhen(condition);
624 return condition;
625 }
626 return holds(operands);
627 }
628
629 void compileWhen(detail::engine::Json const& condition) {
630 if (auto compiled = ui::parseExpression(condition)) {
631 _whenCompiled = *std::move(compiled);
632 }
633 }
634
635 /// Whether the node holds, as a condition. Presentation kinds and unknown kinds are not conditions: `null`.
636 // One case per kind; and, or and not translate their operands with the same function.
637 // NOLINTNEXTLINE(misc-no-recursion,readability-function-cognitive-complexity)
638 [[nodiscard]] detail::engine::Json holds(detail::engine::RuleOperands& operands) const {
639 using detail::engine::call;
640 using detail::engine::choose;
641 using detail::engine::integer;
642 using detail::engine::Json;
643 using detail::engine::literal;
644 using detail::engine::single;
645 switch (_kind) {
646 case Kind::Engaged:
647 return engagedRef(operands, _fields.front());
648 case Kind::NotEngaged:
649 return call("not", {engagedRef(operands, _fields.front())});
650 case Kind::Equals:
651 // Blank is false, invalid is unknown (the `if` of a null condition), valid compares.
652 return choose(engagedRef(operands, _fields.front()),
653 call("eq", {valueRef(operands, _fields.front()), _literal}), literal(false));
654 case Kind::Greater:
656 case Kind::Less:
657 case Kind::LessOrEqual: {
658 constexpr std::array<std::string_view, 4> kOps = {"gt", "ge", "lt", "le"};
659 auto const op = kOps.at(static_cast<std::size_t>(_kind) - static_cast<std::size_t>(Kind::Greater));
660 std::string const& lhs = _fields.front();
661 std::string const& rhs = _fields.back();
662 // A blank operand makes the comparison hold vacuously; an invalid one leaves it undecided.
663 return choose(call("and", {engagedRef(operands, lhs), engagedRef(operands, rhs)}),
664 call(op, {valueRef(operands, lhs), valueRef(operands, rhs)}), literal(true));
665 }
669 std::vector<Json> engaged;
670 engaged.reserve(_fields.size());
671 for (std::string const& field : _fields) {
672 engaged.push_back(engagedRef(operands, field));
673 }
674 Json list{};
675 list.data.emplace<Json::array_t>(std::move(engaged));
676 Json const all = single("list", list);
677 auto const counted = [&](Json where) {
678 Json comprehension = detail::engine::emptyObject();
679 comprehension.get_object().insert_or_assign("filter", all);
680 comprehension.get_object().insert_or_assign("as", detail::engine::jsonText("x"));
681 comprehension.get_object().insert_or_assign("where", std::move(where));
682 return call("count", {std::move(comprehension)});
683 };
684 Json const x = single("ref", detail::engine::jsonText("x"));
685 Json const definite = counted(x);
686 Json const open = counted(call("not", {call("engaged", {x})}));
687 Json const undecided = detail::engine::nullLiteral();
688 if (_kind == Kind::ExactlyOneOf) {
689 return choose(
690 call("ge", {definite, integer(2)}), literal(false),
691 choose(call("eq", {open, integer(0)}), call("eq", {definite, integer(1)}), undecided));
692 }
693 if (_kind == Kind::AtLeastOneOf) {
694 return choose(call("ge", {definite, integer(1)}), literal(true),
695 choose(call("eq", {open, integer(0)}), literal(false), undecided));
696 }
697 return choose(
698 call("ge", {definite, integer(2)}), literal(false),
699 choose(call("le", {call("add", {definite, open}), integer(1)}), literal(true), undecided));
700 }
701 case Kind::And:
702 case Kind::Or: {
703 if (_children.empty()) {
704 return literal(_kind == Kind::And);
705 }
706 std::vector<Json> parts;
707 parts.reserve(_children.size());
708 for (RuleExpr const& operand : _children) {
709 parts.push_back(operand.holds(operands));
710 }
711 return call(_kind == Kind::And ? "and" : "or", std::move(parts));
712 }
713 case Kind::Not:
714 return call("not", {_children.front().holds(operands)});
718 case Kind::Unknown:
719 default:
720 return detail::engine::nullLiteral();
721 }
722 }
723
724 Kind _kind = Kind::Unknown;
725 std::string _kindName;
726 std::vector<std::string> _fields;
727 std::vector<RuleExpr> _children;
728 detail::engine::Json _literal;
729 detail::engine::Json _expression;
730 std::vector<std::string> _operands;
731 ui::ExprPtr _compiled;
732 ui::ExprPtr _whenCompiled;
733};
734
735/// @brief Whether the gating rules let the form submit.
736///
737/// Only a `False` rule blocks: an `Unknown` one is waiting on a field that is required or does not encode, and that
738/// field blocks readiness itself; and a rule kind this engine does not know must not lock an older client out of a
739/// newer server's forms.
740/// @param rules The rules.
741/// @param lookup Reads each operand.
742/// @return `False` when some gating rule is false, `Unknown` when none is false and some is undecided, else `True`.
743[[nodiscard]] inline Tri gatingVerdict(std::span<RuleExpr const> rules, RuleLookup const& lookup) {
744 Tri verdict = Tri::True;
745 for (RuleExpr const& entry : rules) {
746 if (!entry.gates()) {
747 continue;
748 }
749 Tri const one = entry.evaluate(lookup);
750 if (one == Tri::False) {
751 return Tri::False;
752 }
753 if (one == Tri::Unknown) {
754 verdict = Tri::Unknown;
755 }
756 }
757 return verdict;
758}
759
760namespace detail::engine {
761
762/// @brief Combines every presentation rule of @p kind that names @p field.
763/// @param rules The rules.
764/// @param kind The presentation kind.
765/// @param field The wire name.
766/// @param lookup Reads each operand.
767/// @param decisive The `when` verdict that decides the presentation as soon as one rule has it.
768/// @param absent The verdict when no rule names the field.
769/// @return `decisive` when some rule's clause has it, `Unknown` when some is undecided, otherwise its negation.
770[[nodiscard]] inline Tri presentation(std::span<RuleExpr const> rules, RuleExpr::Kind kind, std::string_view field,
771 RuleLookup const& lookup, Tri decisive, Tri absent) {
772 bool named = false;
773 bool unknown = false;
774 for (RuleExpr const& entry : rules) {
775 if (!entry.presents(kind, field)) {
776 continue;
777 }
778 named = true;
779 Tri const when = entry.whenHolds(lookup);
780 if (when == decisive) {
781 return decisive;
782 }
783 unknown = unknown || when == Tri::Unknown;
784 }
785 if (!named) {
786 return absent;
787 }
788 return unknown ? Tri::Unknown : triNot(decisive);
789}
790
791} // namespace detail::engine
792
793/// @brief Whether @p field is shown: hidden as soon as one `visibleWhen` naming it is false.
794/// @param rules The rules.
795/// @param field The wire name.
796/// @param lookup Reads each operand.
797/// @return `False` when hidden; `Unknown` presents as shown.
798[[nodiscard]] inline Tri visibleVerdict(std::span<RuleExpr const> rules, std::string_view field,
799 RuleLookup const& lookup) {
800 return detail::engine::presentation(rules, RuleExpr::Kind::VisibleWhen, field, lookup, Tri::False, Tri::True);
801}
802
803/// @brief Whether @p field is frozen: as soon as one `readonlyWhen` naming it is true.
804/// @param rules The rules.
805/// @param field The wire name.
806/// @param lookup Reads each operand.
807/// @return `True` when frozen; `Unknown` presents as editable.
808[[nodiscard]] inline Tri readonlyVerdict(std::span<RuleExpr const> rules, std::string_view field,
809 RuleLookup const& lookup) {
810 return detail::engine::presentation(rules, RuleExpr::Kind::ReadonlyWhen, field, lookup, Tri::True, Tri::False);
811}
812
813/// @brief Whether @p field is required by a rule: as soon as one `requiredWhen` naming it is true.
814/// @param rules The rules.
815/// @param field The wire name.
816/// @param lookup Reads each operand.
817/// @return `True` when required; `Unknown` presents as not required.
818[[nodiscard]] inline Tri requiredVerdict(std::span<RuleExpr const> rules, std::string_view field,
819 RuleLookup const& lookup) {
820 return detail::engine::presentation(rules, RuleExpr::Kind::RequiredWhen, field, lookup, Tri::True, Tri::False);
821}
822
823} // namespace morph::forms
Node text(TextOptions const &options)
A text node.
Definition author.hpp:3126
AnyHandle list(std::vector< AnyHandle > const &items)
{"list": [...]}: a list built from expressions.
Definition author.hpp:1123
Command all(Commands const &steps)
all: steps started together.
Definition author.hpp:2928
Command open(Element const &dialog)
open: opens a dialog or drawer.
Definition author.hpp:2779
Handle< std::int64_t > count(AnyHandle const &list)
count: a list's length.
Definition author.hpp:1000
Node form(ModelHandle< Model > const &model, FormOptions< Action > const &options={})
A form node: a schema form of the forms engine over an action (spec 2); each field takes its label fr...
Definition author.hpp:4271
One x-rules entry, or a condition inside one, and its translation into expr/1.
Definition rules.hpp:191
Tri whenHolds(RuleLookup const &lookup) const
Evaluates the when clause of a requiredWhen, visibleWhen or readonlyWhen.
Definition rules.hpp:310
detail::engine::Json const & expression() const noexcept
The translation, as expr/1 JSON: for a gating rule its test, for a presentation rule its when clause....
Definition rules.hpp:287
std::string_view kindName() const noexcept
The kind as the schema spells it.
Definition rules.hpp:245
std::span< RuleExpr const > conditions() const noexcept
The operands of and and or, or the one operand of not.
Definition rules.hpp:267
Kind
The rule kinds.
Definition rules.hpp:194
@ MutuallyExclusive
mutuallyExclusive: at most one field is engaged.
Definition rules.hpp:205
@ Equals
equals: the field equals the literal value (valueText for wide integers).
Definition rules.hpp:197
@ ExactlyOneOf
exactlyOneOf: exactly one field is engaged.
Definition rules.hpp:203
@ Or
or over conditions.
Definition rules.hpp:209
@ GreaterOrEqual
greaterOrEqual.
Definition rules.hpp:199
@ RequiredWhen
requiredWhen: the fields are required while when holds.
Definition rules.hpp:202
@ Greater
greater: the first field is greater than the second, when both are engaged.
Definition rules.hpp:198
@ Unknown
A kind this engine does not know.
Definition rules.hpp:211
@ LessOrEqual
lessOrEqual.
Definition rules.hpp:201
@ Not
not of condition.
Definition rules.hpp:210
@ AtLeastOneOf
atLeastOneOf.
Definition rules.hpp:204
@ And
and over conditions.
Definition rules.hpp:208
@ VisibleWhen
visibleWhen: presentation; the fields are shown while when holds.
Definition rules.hpp:206
@ ReadonlyWhen
readonlyWhen: presentation; the fields are frozen while when holds.
Definition rules.hpp:207
@ Engaged
engaged: the field has a value.
Definition rules.hpp:195
bool gates() const noexcept
Whether the rule decides readiness. visibleWhen and readonlyWhen only present.
Definition rules.hpp:274
bool presents(Kind kind, std::string_view field) const noexcept
Whether the rule is a presentation rule of kind that names field.
Definition rules.hpp:280
Kind kind() const noexcept
The kind.
Definition rules.hpp:241
RuleExpr const * when() const noexcept
The when clause of requiredWhen, visibleWhen and readonlyWhen.
Definition rules.hpp:260
bool names(std::string_view field) const noexcept
Whether this node names field directly. A presentation rule applies to every field it names.
Definition rules.hpp:254
std::span< std::string const > operands() const noexcept
The wire names the translation reads, by operand index (f0 is the first).
Definition rules.hpp:291
Tri evaluate(RuleLookup const &lookup) const
Evaluates the translation with the UI document's evaluator.
Definition rules.hpp:299
static std::expected< RuleExpr, SchemaError > parse(detail::engine::Json const &node, std::string const &path)
Reads one rule or condition node and translates it.
Definition rules.hpp:219
std::span< std::string const > fields() const noexcept
The fields this node names directly (fields).
Definition rules.hpp:249
What an evaluated expression reads: the interpreter implements it over its reactive values.
virtual FormatContext const & formatting() const =0
The formatting facts.
The document's typed value (spec 5 §3): what every expression evaluates to.
Definition value.hpp:157
The expr/1 expression language of the UI document: parsing a JSON expression into an Expr tree,...
std::expected< ExprPtr, Refusal > parseExpression(Json const &json, std::string const &path="")
Parses one expression.
Value evaluate(Expr const &expr, Environment const &env)
Evaluates an expression.
constexpr auto engaged(V A::*field)
Builds an Engaged<V, A> condition testing whether field is engaged.
Definition forms.hpp:954
Locale numeric-entry normalisation for the control edge.
Exact rational arithmetic for morph model/action values.
std::function< RuleOperand(std::string_view field)> RuleLookup
Reads one field by wire name, for a rule. The session's lookup is tracked, so a verdict computed insi...
Definition rules.hpp:84
Tri readonlyVerdict(std::span< RuleExpr const > rules, std::string_view field, RuleLookup const &lookup)
Whether field is frozen: as soon as one readonlyWhen naming it is true.
Definition rules.hpp:808
Tri
A three-valued verdict: a half-typed form has fields with no value yet, so a rule can be undecided.
Definition rules.hpp:46
@ Unknown
Cannot be decided: an operand does not encode, or the kind is not one this engine knows.
Definition rules.hpp:49
@ True
Definitely holds.
Definition rules.hpp:48
@ False
Definitely does not hold.
Definition rules.hpp:47
Tri requiredVerdict(std::span< RuleExpr const > rules, std::string_view field, RuleLookup const &lookup)
Whether field is required by a rule: as soon as one requiredWhen naming it is true.
Definition rules.hpp:818
Tri visibleVerdict(std::span< RuleExpr const > rules, std::string_view field, RuleLookup const &lookup)
Whether field is shown: hidden as soon as one visibleWhen naming it is false.
Definition rules.hpp:798
constexpr Tri triNot(Tri value) noexcept
Kleene negation.
Definition rules.hpp:55
Tri gatingVerdict(std::span< RuleExpr const > rules, RuleLookup const &lookup)
Whether the gating rules let the form submit.
Definition rules.hpp:743
Compound condition: all of Conds... hold. Nests to any depth — each Cond may itself be a leaf (Engage...
Definition forms.hpp:1749
Rule: at least one of the listed fields is engaged.
Definition forms.hpp:1531
Condition: field is engaged (has a value). One of the closed condition kinds a requiredWhen / visible...
Definition forms.hpp:919
Condition: field's engaged value equals literal. An unengaged field is not vacuously satisfied here (...
Definition forms.hpp:1315
Rule: exactly one of the listed fields is engaged.
Definition forms.hpp:1480
Rule/condition: *lhs >= *rhs when both operands are engaged; vacuously satisfied when either is uneng...
Definition forms.hpp:1072
Rule/condition: *lhs > *rhs when both operands are engaged; vacuously satisfied when either is unenga...
Definition forms.hpp:1013
Rule/condition: *lhs <= *rhs when both operands are engaged; vacuously satisfied when either is uneng...
Definition forms.hpp:1190
Rule/condition: *lhs < *rhs when both operands are engaged; vacuously satisfied when either is unenga...
Definition forms.hpp:1131
Rule: at most one of the listed fields is engaged.
Definition forms.hpp:1582
Condition: field is not engaged. The complement of engaged.
Definition forms.hpp:963
Compound condition: the nested condition does not hold. Nests to any depth, and is usable directly as...
Definition forms.hpp:1864
Compound condition: at least one of Conds... holds. Nests to any depth, and is usable directly as a t...
Definition forms.hpp:1809
Presentation rule: the listed field is editable only while Cond does not hold (the field is read-only...
Definition forms.hpp:1691
Rule: field must be engaged whenever Cond holds; vacuously satisfied (not required) while the conditi...
Definition forms.hpp:1427
What a rule reads of one field.
Definition rules.hpp:68
ui::Value value
The encoded value while Valid: an int, decimal, bool, string or timestamp; null otherwise,...
Definition rules.hpp:79
State
Whether the field has a value a rule can use.
Definition rules.hpp:70
@ Valid
Entered and encodes; value holds the exact value.
Definition rules.hpp:73
@ Invalid
Something entered that does not encode.
Definition rules.hpp:72
@ Blank
Nothing entered.
Definition rules.hpp:71
State state
The state.
Definition rules.hpp:76
Why a schema does not read as a form: where, and why.
Definition rules.hpp:34
std::string path
Where in the schema: a dotted path such as properties.amount or x-rules[2].when.
Definition rules.hpp:36
std::string message
Why, in one sentence.
Definition rules.hpp:38
bool operator==(SchemaError const &other) const =default
Field-wise equality.
Presentation rule: the listed field is shown only while Cond holds. Never gates allRulesSatisfied — w...
Definition forms.hpp:1636
Exact text helpers the forms engine shares: JSON quoting and reading, canonical decimal comparison an...
The UI document's typed values (ui::Value), its types (ui::Type), their JSON form,...