morph 0.1.0
Asynchronous bridge between GUI thread and business models
Loading...
Searching...
No Matches
forms.hpp File Reference

JSON-Forms-style schema generation for morph actions. More...

#include <algorithm>
#include <array>
#include <cctype>
#include <compare>
#include <concepts>
#include <cstddef>
#include <cstdint>
#include <glaze/glaze.hpp>
#include <limits>
#include <memory>
#include <morph/detail/fixed_string.hpp>
#include <morph/detail/reflected_member.hpp>
#include <optional>
#include <stdexcept>
#include <string>
#include <string_view>
#include <tuple>
#include <type_traits>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
#include "../util/quantity.hpp"
#include "../util/rational.hpp"
#include "blank_as.hpp"
#include "choice.hpp"
#include "layout.hpp"

Go to the source code of this file.

Classes

struct  morph::forms::FieldMeta
 Per-field presentation overrides and scalar bounds: label, help, placeholder, read-only, hidden, minimum/maximum/multipleOf, and a plain member's display unit/decimals (docs/spec/forms/forms.md, "Field metadata"). More...
struct  morph::forms::Engaged< V, A >
 Condition: field is engaged (has a value). One of the closed condition kinds a requiredWhen / visibleWhen / readonlyWhen rule's when clause accepts. More...
struct  morph::forms::NotEngaged< V, A >
 Condition: field is not engaged. The complement of engaged. More...
struct  morph::forms::Greater< V, A >
 Rule/condition: *lhs > *rhs when both operands are engaged; vacuously satisfied when either is unengaged (a form still being filled in must not fail this comparison prematurely — see forms.md). Compares the operands' engaged values (operator*()) directly — e.g. the underlying math::Rational for Quantity, or DateTime for Timestamp — never the field type's own (possibly throwing, for Quantity) operator<=>. More...
struct  morph::forms::GreaterOrEqual< V, A >
 Rule/condition: *lhs >= *rhs when both operands are engaged; vacuously satisfied when either is unengaged. See Greater for the exact-value / vacuous-operand rationale. More...
struct  morph::forms::Less< V, A >
 Rule/condition: *lhs < *rhs when both operands are engaged; vacuously satisfied when either is unengaged. See Greater for the exact-value / vacuous-operand rationale. More...
struct  morph::forms::LessOrEqual< V, A >
 Rule/condition: *lhs <= *rhs when both operands are engaged; vacuously satisfied when either is unengaged. See Greater for the exact-value / vacuous-operand rationale. More...
struct  morph::forms::Equals< V, A, L >
 Condition: field's engaged value equals literal. An unengaged field is not vacuously satisfied here (unlike the comparison kinds) — a field with no value cannot equal anything, so equals returns false while the field is unengaged. More...
struct  morph::forms::RequiredWhen< V, A, Cond >
 Rule: field must be engaged whenever Cond holds; vacuously satisfied (not required) while the condition does not hold. More...
struct  morph::forms::ExactlyOneOf< A, Vs >
 Rule: exactly one of the listed fields is engaged. More...
struct  morph::forms::AtLeastOneOf< A, Vs >
 Rule: at least one of the listed fields is engaged. More...
struct  morph::forms::MutuallyExclusive< A, Vs >
 Rule: at most one of the listed fields is engaged. More...
struct  morph::forms::VisibleWhen< V, A, Cond >
 Presentation rule: the listed field is shown only while Cond holds. Never gates allRulesSatisfied — while hidden, the field's current draft value still travels in the payload (hiding never clears it), exactly like a static x-hidden field. More...
struct  morph::forms::ReadonlyWhen< V, A, Cond >
 Presentation rule: the listed field is editable only while Cond does not hold (the field is read-only while when holds). Never gates allRulesSatisfied, exactly like VisibleWhen. More...
struct  morph::forms::And< A, Conds >
 Compound condition: all of Conds... hold. Nests to any depth — each Cond may itself be a leaf (Engaged, Equals, a comparison, …) or another And/Or/Not. Usable both as a nested when clause and directly as a top-level formRules entry (it declares isPresentation and test() exactly like every other validation rule), which is what lets a single rule carry a compound condition tree instead of factoring the composition into multiple single-condition rules. More...
struct  morph::forms::Or< A, Conds >
 Compound condition: at least one of Conds... holds. Nests to any depth, and is usable directly as a top-level formRules entry, exactly like And. More...
struct  morph::forms::Not< A, Cond >
 Compound condition: the nested condition does not hold. Nests to any depth, and is usable directly as a top-level formRules entry, exactly like And/Or. More...
struct  morph::forms::RuleList< Rules >
 Composed list of an action's declared cross-field rules — the value of A::formRules. Built by ruleList(...); never constructed directly. More...
struct  morph::forms::UnsatisfiableFormError
 Thrown by schemaJson<A>() when A's declared formRules contradict A's own derived required array so completely that no submission could satisfy both. More...
struct  morph::forms::ConflictingOptionsError
 Thrown by schema generation when a Choice<…> member's FieldMeta also sets optionsAction: the member's type already names where its options come from, and two sources cannot both hold. More...
struct  morph::forms::IncompleteOptionsError
 Thrown by schema generation when a FieldMeta sets optionsAction without both valueField and labelField, or either of those without optionsAction: options need the action, the value field and the label field together. More...
struct  morph::forms::QuantityDecodeError
 Thrown when a decoded action has a Quantity field whose engaged value falls outside its unit's declared bounds (UnitTraits<E>::bounds). More...

Concepts

concept  morph::forms::EmptyCapableField
 Concept: a field type with an internal empty state (Quantity, Choice, Timestamp, or any user type exposing hasValue()).
concept  morph::forms::EngageableField
 Broader than EmptyCapableField: also covers a plain std::optional<T> member (e.g. std::optional<std::string> email), which does not satisfy EmptyCapableField — it exposes has_value(), not hasValue() (see forms.md's allRequiredEngaged "two exclusions" note). The cross-field rule vocabulary's engagement checks (engaged, notEngaged, requiredWhen, and the membership rules exactlyOneOf/atLeastOneOf/mutuallyExclusive) accept either kind of field, since docs/spec/forms/forms.md's worked example ranges an exactlyOneOf over two plain std::optional<std::string> fields.
concept  morph::forms::Condition
 Concept: Cond may be used as a nested condition — inside an andOf/orOf/notOf tree, or as the when clause of a requiredWhen / visibleWhen / readonlyWhen rule.
concept  morph::forms::RuleLiteral
 Constraint: literal types equals(...) accepts — the closed, JSON-representable scalar set a field can hold. Restricting the literal type keeps equals losslessly serialisable into x-rules's value key (see forms.md): a numeric literal is always the exact math::Rational, never a double.
concept  morph::forms::ComparableAgainstLiteral
 Constraint: a field of type V can be compared against a literal of type L at all.
concept  morph::forms::HasFormRules
 Concept: action A declares a static constexpr formRules member (a RuleList<...>). Mirrors detail::HasOptionalFields.
concept  morph::forms::HasExplicitSubmit
 Concept: action A declares a static constexpr bool explicitSubmit, opting its form out of auto-submit-on-validity.

Functions

template<typename V, typename A>
requires EngageableField<V>
constexpr auto morph::forms::engaged (V A::*field)
 Builds an Engaged<V, A> condition testing whether field is engaged.
template<typename V, typename A>
requires EngageableField<V>
constexpr auto morph::forms::notEngaged (V A::*field)
 Builds a NotEngaged<V, A> condition testing whether field is not engaged.
template<typename V, typename A>
requires detail::ComparableField<V>
constexpr auto morph::forms::greater (V A::*lhs, V A::*rhs)
 Builds a Greater<V, A> rule/condition: *lhs > *rhs.
template<typename V, typename A>
requires detail::ComparableField<V>
constexpr auto morph::forms::greaterOrEqual (V A::*lhs, V A::*rhs)
 Builds a GreaterOrEqual<V, A> rule/condition: *lhs >= *rhs.
template<typename V, typename A>
requires detail::ComparableField<V>
constexpr auto morph::forms::less (V A::*lhs, V A::*rhs)
 Builds a Less<V, A> rule/condition: *lhs < *rhs.
template<typename V, typename A>
requires detail::ComparableField<V>
constexpr auto morph::forms::lessOrEqual (V A::*lhs, V A::*rhs)
 Builds a LessOrEqual<V, A> rule/condition: *lhs <= *rhs.
template<typename V, typename A, RuleLiteral L>
requires ComparableAgainstLiteral<V, L>
constexpr auto morph::forms::equals (V A::*field, L literal)
 Builds an Equals<V, A, L> condition: field's engaged value equals literal.
template<typename V, typename A, std::size_t N>
requires ComparableAgainstLiteral<V, detail::LiteralString<N>>
constexpr auto morph::forms::equals (V A::*field, const char(&literal)[N])
 equals overload for a string-literal argument (equals(&A::code, "X")), so callers do not have to spell std::string{"X"} explicitly.
template<typename V, typename A, Condition Cond>
requires EngageableField<V> && std::same_as<detail::ConditionActionType<Cond>, A>
constexpr auto morph::forms::requiredWhen (V A::*field, Cond when)
 Builds a RequiredWhen<V, A, Cond> rule: field becomes required exactly when when holds.
template<typename A, typename... Vs>
requires (EngageableField<Vs> && ...)
constexpr auto morph::forms::exactlyOneOf (Vs A::*... fields)
 Builds an ExactlyOneOf<A, Vs...> rule over fields.
template<typename A, typename... Vs>
requires (EngageableField<Vs> && ...)
constexpr auto morph::forms::atLeastOneOf (Vs A::*... fields)
 Builds an AtLeastOneOf<A, Vs...> rule over fields.
template<typename A, typename... Vs>
requires (EngageableField<Vs> && ...)
constexpr auto morph::forms::mutuallyExclusive (Vs A::*... fields)
 Builds a MutuallyExclusive<A, Vs...> rule over fields.
template<typename V, typename A, Condition Cond>
requires std::same_as<detail::ConditionActionType<Cond>, A>
constexpr auto morph::forms::visibleWhen (V A::*field, Cond when)
 Builds a VisibleWhen<V, A, Cond> presentation rule: field is shown only while when holds.
template<typename V, typename A, Condition Cond>
requires std::same_as<detail::ConditionActionType<Cond>, A>
constexpr auto morph::forms::readonlyWhen (V A::*field, Cond when)
 Builds a ReadonlyWhen<V, A, Cond> presentation rule: field is editable only while when does not hold.
template<Condition Cond0, Condition... Conds>
requires (std::same_as<detail::ConditionActionType<Cond0>, detail::ConditionActionType<Conds>> && ...)
constexpr auto morph::forms::andOf (Cond0 condition0, Conds... conditions)
 Builds an And<A, Conds...> condition: every listed condition must hold.
template<Condition Cond0, Condition... Conds>
requires (std::same_as<detail::ConditionActionType<Cond0>, detail::ConditionActionType<Conds>> && ...)
constexpr auto morph::forms::orOf (Cond0 condition0, Conds... conditions)
 Builds an Or<A, Conds...> condition: at least one listed condition must hold.
template<Condition Cond>
constexpr auto morph::forms::notOf (Cond condition)
 Builds a Not<A, Cond> condition: negates condition.
template<typename... Rules>
constexpr auto morph::forms::ruleList (Rules... rules)
 Composes rules into the RuleList an action assigns to its static constexpr formRules member.
template<typename A>
constexpr bool morph::forms::allRulesSatisfied (const A &action) noexcept
 Whether every validation rule in A::formRules holds for action. Presentation rules (visibleWhen / readonlyWhen) are skipped — they can never fail this check. Returns true unconditionally for an action with no formRules (safe to call from every action's validate() regardless of whether it declares rules).
template<auto Dst, auto... Inputs, typename Fn>
consteval auto morph::forms::computed (Fn fn) noexcept
 Builds one computed-field declaration binding a destination member to its declared input members and a pure derivation function.
template<typename... Fields>
consteval auto morph::forms::computeList (Fields... fields) noexcept
 Builds a ComputeList from one or more computed(...) declarations.
template<typename A>
constexpr void morph::forms::recomputeAll (A &action)
 Recomputes every entry of A::computedFields in place on action.
template<auto MemberPtr>
FieldMeta morph::forms::describe (std::string_view label={}, std::string_view help={}) noexcept
 Builds a FieldMeta for the member named by MemberPtr, so the wire key is never restated as a string.
template<typename A>
constexpr void morph::forms::reconcileDeclaredPrecision (A &action)
 Rounds every Quantity member of action to its declared precision, so the stored value matches the precision the schema advertises via x-decimalPlaces.
template<typename A>
std::optional< std::string > morph::forms::checkQuantityBounds (const A &action)
 Checks every Quantity member of action against its unit's declared bounds (morph::units::Quantity::withinDeclaredBounds, driven by the optional UnitTraits<E>::bounds(E) customisation point).
template<typename A>
void morph::forms::enforceQuantityBounds (const A &action)
 Runs checkQuantityBounds<A>(action) and throws QuantityDecodeError naming the first out-of-bounds field, if any. The throwing counterpart used directly on the decode path (registry.hpp/bridge.hpp call sites); a caller that wants the field name without an exception uses checkQuantityBounds itself.
template<typename A>
constexpr bool morph::forms::allRequiredEngaged (const A &action) noexcept
 Whether every required empty-capable member of action is engaged (has a value).
template<typename A>
bool morph::forms::allFieldBoundsSatisfied (const A &action) noexcept
 Whether every numeric member of action satisfies the minimum/maximum/multipleOf its A::fieldMetadata entry declares.
template<typename A>
const std::string & morph::forms::schemaJson ()
 Generates the JSON Schema for action type A, ready for a client-side form renderer.

Variables

constexpr std::uint64_t morph::forms::kExactDoubleLimit = 9007199254740992ULL
 The largest N such that every integer in [0, N] is exactly representable as an IEEE-754 double: 2^53.
constexpr std::int64_t morph::forms::kExactDoubleLimitSigned = 9007199254740992LL
 Signed spelling of kExactDoubleLimit, for the negative bound.

Detailed Description

JSON-Forms-style schema generation for morph actions.

Given an action type A (a plain aggregate, as registered with BRIDGE_REGISTER_ACTION), this header produces a standard JSON Schema a client can render a form from at runtime — the "describe" half of an auto-generated GUI. It builds on glaze's write_json_schema (which already contributes types, $defs, per-field metadata declared via glz::json_schema<A>, and the ExtUnits stamped by morph::units::Quantity) and closes the gaps glaze leaves open. The list below is the main surface, not an exhaustive enumeration of every key emitted:

  • required — under the options schemaJson<A>() uses, glaze derives no required entries from member types: glz::requires_key only returns true for a member when meta<T>::requires_key says so or when Opts.error_on_missing_keys is set, and morph sets neither. (A type declaring meta<V>::required, and a tagged variant's discriminator, do still get one.) schemaJson<A>() always writes its own, overwriting whatever the schema writer did or did not produce: a member is required unless it is a std::optional<...> or its name is listed in the action's opt-out list (see below).
  • x-decimalPlaces — for Quantity members, the field's declared precision (Quantity<U, Dec>::declaredDecimals, which defaults to the unit's UnitTraits default but is overridable per member), so a client knows the input step without hardcoding unit knowledge.
  • x-order — the member's declaration index on every property, so a renderer can lay fields out in declaration order (JSON object key order is not reliable once schemas pass through DOMs/maps).
  • x-unitAlternatives — for Quantity members whose unit system declares convertible units (UnitTraits<E>::relations): an array of {id, display, decimals, num, den} entries, where num/den is the exact alternative-to-canonical ratio. Renderers offer a unit selector and recalculate entered values exactly on switch; payloads always carry the canonical unit.
  • x-optionsAction / x-optionValue / x-optionLabel — for morph::forms::Choice members: which registered action serves the options, and which result-row fields carry the submitted value and the display label. Renderers turn these into combo boxes populated by executing the named action.
  • x-optionsDependsOn — for a Choice whose options are parameterised by sibling field values (Choice's DependsOn pack): the wire names of those sibling fields. Renderers send {name: value, ...} as the options-action request body instead of an empty one, and re-fetch when a named field changes. Omitted when the Choice declares no dependency.
  • x-layout / x-group / x-section / x-colspan — for actions declaring a static constexpr formLayout and/or fieldSpans (morph::forms::FieldGroup / FieldSpan, forms/layout.hpp): visual structure (sections, tabs, an accordion) over the flat field list, and per-field grid column spans. Absent either declaration, none of these keys are emitted and a renderer lays fields out exactly as it does today.
  • x-widget — for a field whose type declares a noexcept static constexpr widget() (Multiline, Ranged — widget_hints.hpp), or any field named in a fieldMetadata-shaped override: the renderer's preferred control id ("textarea", "slider", "radio", …).
  • x-min / x-max / x-step — for a field whose type additionally declares min()/max()/step() (the Ranged shape): the slider's control-track bounds and increment — advisory, not a validation bound.
  • x-rules — for an action declaring a static constexpr formRules (morph::forms::ruleList(...)): a closed, typed cross-field rule vocabulary (requiredWhen, comparisons, membership, presentation, and the compound andOf/orOf/notOf conditions that nest a condition tree to any depth) evaluated identically by the schema, the client, and the server. See morph::forms::allRulesSatisfied below and docs/spec/forms/forms.md.
  • minimum / maximum / multipleOf — for a field whose fieldMetadata entry declares them (morph::forms::FieldMeta): a per-field scalar bound, stamped on the property node as the standard JSON-Schema keys of those names. multipleOf: 1 is how "whole number" is spelled. morph::forms::allFieldBoundsSatisfied evaluates the same declaration in C++, so an action's validate() enforces exactly what the client was served. Bounds are per field, unlike UnitTraits::bounds, which is per unit.
  • ExtUnits / x-displayDecimals — for a non-Quantity field whose FieldMeta declares unit / decimals: the display unit (in the same ExtUnits shape a Quantity carries) and the fraction digits a renderer shows and accepts. Presentation only — the member keeps its plain JSON encoding.
  • x-computed / x-readonly — for a member listed as the destination of an action's computedFields declaration: the field is derived from sibling inputs (named in x-computed.inputs) and must not be rendered as an editable control (x-readonly: true). See morph::forms::computed, morph::forms::computeList, and morph::forms::recomputeAll.

morph::time::Timestamp members need no extension keys: their schema carries the standard "format": "date-time" annotation.

Nested aggregates (recursive, cycle-safe). A member whose type is itself a reflectable aggregate — a plain nested struct, or std::vector<Sub> — gets its own members annotated too: x-order, title/FieldMeta, required, and the Quantity/Choice/widget/ ranged-bounds rules above, applied against the nested type's own reflection. Unlike the top level, this recurses into the type graph, to whatever depth it has: there is no depth limit, and a self- or mutually-referential type is described rather than rejected — glaze emits a finite $ref-cyclic schema for it, and the runtime $defs visited set walks that once. Computed fields/formLayout/fieldSpans/formRules remain top-level-only regardless of depth. See docs/spec/forms/forms.md, "Nested aggregates (recursive, cycle-safe)", and detail::annotateNestedAggregateRef.

Declaring optional fields
Required is the default. An action opts individual fields out with a static member list:
struct RecordMeasurement {
std::int64_t sampleId = 0;
Density density{};
Moisture moisture{}; // may stay empty
static constexpr std::array optionalFields{std::string_view{"moisture"}};
[[nodiscard]] bool validate() const { return morph::forms::allRequiredEngaged(*this); }
};
constexpr bool allRequiredEngaged(const A &action) noexcept
Whether every required empty-capable member of action is engaged (has a value).
Definition forms.hpp:3589
std::expected< TableQuery, TableError > validate(TableQuery query, std::span< ColumnInfo const > columns, QueryLimits const &limits={}, std::function< bool(std::string_view)> const &mayRead={})
Checks query against the table's columns and limits.
Definition query.hpp:220
Declaring computed fields
A derived, read-only field is declared with a static constexpr map from a destination member to its declared input members and a pure derivation:
struct LineItem {
Quantity<Units, 2> qty;
Quantity<Units, 2> price;
Quantity<Units, 2> total; // computed -- not user-entered
// A generic (auto) lambda parameter: this initializer runs while
// LineItem is still an incomplete type, so the body's member access
// must stay dependent until first use, after the class is complete.
static constexpr auto computedFields = morph::forms::computeList(
[](const auto& s) { return s.qty * s.price; }));
};
consteval auto computed(Fn fn) noexcept
Builds one computed-field declaration binding a destination member to its declared input members and ...
Definition forms.hpp:3367
consteval auto computeList(Fields... fields) noexcept
Builds a ComputeList from one or more computed(...) declarations.
Definition forms.hpp:3380
schemaJson<A>() then emits x-computed/x-readonly on total and excludes it from required; recomputeAll<A>(action) is the single evaluator the reactive set<> path and every dispatch path call to overwrite it authoritatively – see bridge.md/registry.md.
Readiness helper
allRequiredEngaged(action) returns true when every required empty-capable member (Quantity, Choice, Timestamp — anything with a hasValue()) is engaged. Wire it up as the action's validate() (the existing ActionValidator machinery picks that method up automatically) so the same declaration drives the schema's required 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 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.

Definition in file forms.hpp.

Function Documentation

◆ engaged()

template<typename V, typename A>
requires EngageableField<V>
auto morph::forms::engaged ( V A::* field)
nodiscardconstexpr

Builds an Engaged<V, A> condition testing whether field is engaged.

Template Parameters
VField member type (deduced; must satisfy EngageableField).
AAction type (deduced).
Parameters
fieldPointer to the member to test.
Returns
The condition node.

Definition at line 954 of file forms.hpp.

◆ notEngaged()

template<typename V, typename A>
requires EngageableField<V>
auto morph::forms::notEngaged ( V A::* field)
nodiscardconstexpr

Builds a NotEngaged<V, A> condition testing whether field is not engaged.

Template Parameters
VField member type (deduced; must satisfy EngageableField).
AAction type (deduced).
Parameters
fieldPointer to the member to test.
Returns
The condition node.

Definition at line 998 of file forms.hpp.

◆ greater()

template<typename V, typename A>
requires detail::ComparableField<V>
auto morph::forms::greater ( V A::* lhs,
V A::* rhs )
nodiscardconstexpr

Builds a Greater<V, A> rule/condition: *lhs > *rhs.

Template Parameters
VField member type shared by both operands (deduced; must satisfy detail::ComparableField).
AAction type (deduced).
Parameters
lhsPointer to the left-hand member.
rhsPointer to the right-hand member.
Returns
The rule/condition node.

Definition at line 1061 of file forms.hpp.

◆ greaterOrEqual()

template<typename V, typename A>
requires detail::ComparableField<V>
auto morph::forms::greaterOrEqual ( V A::* lhs,
V A::* rhs )
nodiscardconstexpr

Builds a GreaterOrEqual<V, A> rule/condition: *lhs >= *rhs.

Template Parameters
VField member type shared by both operands (deduced; must satisfy detail::ComparableField).
AAction type (deduced).
Parameters
lhsPointer to the left-hand member.
rhsPointer to the right-hand member.
Returns
The rule/condition node.

Definition at line 1120 of file forms.hpp.

◆ less()

template<typename V, typename A>
requires detail::ComparableField<V>
auto morph::forms::less ( V A::* lhs,
V A::* rhs )
nodiscardconstexpr

Builds a Less<V, A> rule/condition: *lhs < *rhs.

Template Parameters
VField member type shared by both operands (deduced; must satisfy detail::ComparableField).
AAction type (deduced).
Parameters
lhsPointer to the left-hand member.
rhsPointer to the right-hand member.
Returns
The rule/condition node.

Definition at line 1179 of file forms.hpp.

◆ lessOrEqual()

template<typename V, typename A>
requires detail::ComparableField<V>
auto morph::forms::lessOrEqual ( V A::* lhs,
V A::* rhs )
nodiscardconstexpr

Builds a LessOrEqual<V, A> rule/condition: *lhs <= *rhs.

Template Parameters
VField member type shared by both operands (deduced; must satisfy detail::ComparableField).
AAction type (deduced).
Parameters
lhsPointer to the left-hand member.
rhsPointer to the right-hand member.
Returns
The rule/condition node.

Definition at line 1238 of file forms.hpp.

◆ equals() [1/2]

template<typename V, typename A, RuleLiteral L>
requires ComparableAgainstLiteral<V, L>
auto morph::forms::equals ( V A::* field,
L literal )
nodiscardconstexpr

Builds an Equals<V, A, L> condition: field's engaged value equals literal.

Template Parameters
VField member type (deduced).
AAction type (deduced).
LLiteral type (deduced; must satisfy RuleLiteral).
Parameters
fieldPointer to the member to test.
literalThe value to compare against.
Returns
The condition node.

Definition at line 1393 of file forms.hpp.

◆ equals() [2/2]

template<typename V, typename A, std::size_t N>
requires ComparableAgainstLiteral<V, detail::LiteralString<N>>
auto morph::forms::equals ( V A::* field,
const char(&) literal[N] )
nodiscardconstexpr

equals overload for a string-literal argument (equals(&A::code, "X")), so callers do not have to spell std::string{"X"} explicitly.

The literal is captured inline as a detail::LiteralString, not copied into a std::string, so the resulting node stays a literal type and the documented static constexpr auto formRules = ruleList(...) form works for a literal of any length. Stored as a std::string, it only worked while the text fit the standard library's small-string buffer — see detail::LiteralString. Serialisation is unaffected: emitNode() emits the same JSON string either way.

Template Parameters
VField member type (deduced).
AAction type (deduced).
NString literal length (deduced), including the trailing '\0'.
Parameters
fieldPointer to the member to test.
literalThe string literal to compare against.
Returns
The condition node, with the literal stored inline.

Definition at line 1417 of file forms.hpp.

◆ requiredWhen()

template<typename V, typename A, Condition Cond>
requires EngageableField<V> && std::same_as<detail::ConditionActionType<Cond>, A>
auto morph::forms::requiredWhen ( V A::* field,
Cond when )
nodiscardconstexpr

Builds a RequiredWhen<V, A, Cond> rule: field becomes required exactly when when holds.

Template Parameters
VField member type (deduced; must satisfy EngageableField).
AAction type (deduced).
CondCondition node type (deduced).
Parameters
fieldPointer to the member that becomes conditionally required.
whenThe condition node (engaged(...), notEngaged(...), a comparison, or equals(...)).
Returns
The rule node.

Definition at line 1471 of file forms.hpp.

◆ exactlyOneOf()

template<typename A, typename... Vs>
requires (EngageableField<Vs> && ...)
auto morph::forms::exactlyOneOf ( Vs A::*... fields)
nodiscardconstexpr

Builds an ExactlyOneOf<A, Vs...> rule over fields.

Template Parameters
AAction type (deduced).
VsField member types (deduced; each must satisfy EngageableField).
Parameters
fieldsPointers to the member fields, at least two.
Returns
The rule node.

Definition at line 1522 of file forms.hpp.

◆ atLeastOneOf()

template<typename A, typename... Vs>
requires (EngageableField<Vs> && ...)
auto morph::forms::atLeastOneOf ( Vs A::*... fields)
nodiscardconstexpr

Builds an AtLeastOneOf<A, Vs...> rule over fields.

Template Parameters
AAction type (deduced).
VsField member types (deduced; each must satisfy EngageableField).
Parameters
fieldsPointers to the member fields, at least two.
Returns
The rule node.

Definition at line 1573 of file forms.hpp.

◆ mutuallyExclusive()

template<typename A, typename... Vs>
requires (EngageableField<Vs> && ...)
auto morph::forms::mutuallyExclusive ( Vs A::*... fields)
nodiscardconstexpr

Builds a MutuallyExclusive<A, Vs...> rule over fields.

Template Parameters
AAction type (deduced).
VsField member types (deduced; each must satisfy EngageableField).
Parameters
fieldsPointers to the member fields, at least two.
Returns
The rule node.

Definition at line 1624 of file forms.hpp.

◆ visibleWhen()

template<typename V, typename A, Condition Cond>
requires std::same_as<detail::ConditionActionType<Cond>, A>
auto morph::forms::visibleWhen ( V A::* field,
Cond when )
nodiscardconstexpr

Builds a VisibleWhen<V, A, Cond> presentation rule: field is shown only while when holds.

Template Parameters
VField member type (deduced).
AAction type (deduced).
CondCondition node type (deduced).
Parameters
fieldPointer to the member whose visibility is controlled.
whenThe condition node.
Returns
The rule node.

Definition at line 1680 of file forms.hpp.

◆ readonlyWhen()

template<typename V, typename A, Condition Cond>
requires std::same_as<detail::ConditionActionType<Cond>, A>
auto morph::forms::readonlyWhen ( V A::* field,
Cond when )
nodiscardconstexpr

Builds a ReadonlyWhen<V, A, Cond> presentation rule: field is editable only while when does not hold.

Template Parameters
VField member type (deduced).
AAction type (deduced).
CondCondition node type (deduced).
Parameters
fieldPointer to the member whose editability is controlled.
whenThe condition node.
Returns
The rule node.

Definition at line 1735 of file forms.hpp.

◆ andOf()

template<Condition Cond0, Condition... Conds>
requires (std::same_as<detail::ConditionActionType<Cond0>, detail::ConditionActionType<Conds>> && ...)
auto morph::forms::andOf ( Cond0 condition0,
Conds... conditions )
nodiscardconstexpr

Builds an And<A, Conds...> condition: every listed condition must hold.

Template Parameters
Cond0First condition's node type (deduced); its action type A is recovered from test() and shared by every other node.
CondsRemaining nested condition node types (deduced).
Parameters
condition0The first nested condition.
conditionsThe remaining nested conditions, at least one more.
Returns
The compound condition node.

The action type is recovered from Cond0 alone, so every other operand is required to agree with it here rather than failing deep inside std::tuple's instantiation: andOf(engaged(&C::x), engaged(&B::y)) produced 153 lines whose first error was inside <type_traits> and which named the caller's own formRules line only near the end.

Definition at line 1797 of file forms.hpp.

◆ orOf()

template<Condition Cond0, Condition... Conds>
requires (std::same_as<detail::ConditionActionType<Cond0>, detail::ConditionActionType<Conds>> && ...)
auto morph::forms::orOf ( Cond0 condition0,
Conds... conditions )
nodiscardconstexpr

Builds an Or<A, Conds...> condition: at least one listed condition must hold.

Template Parameters
Cond0First condition's node type (deduced); its action type A is recovered from test() and shared by every other node.
CondsRemaining nested condition node types (deduced).
Parameters
condition0The first nested condition.
conditionsThe remaining nested conditions, at least one more.
Returns
The compound condition node. Constrained exactly as andOf is, and for the same reasons.

Definition at line 1852 of file forms.hpp.

◆ notOf()

template<Condition Cond>
auto morph::forms::notOf ( Cond condition)
nodiscardconstexpr

Builds a Not<A, Cond> condition: negates condition.

Template Parameters
CondNested condition node type (deduced); its action type A is recovered from test().
Parameters
conditionThe condition to negate.
Returns
The compound condition node.

Definition at line 1898 of file forms.hpp.

◆ ruleList()

template<typename... Rules>
auto morph::forms::ruleList ( Rules... rules)
nodiscardconstexpr

Composes rules into the RuleList an action assigns to its static constexpr formRules member.

Template Parameters
RulesRule node types (deduced).
Parameters
rulesThe rule nodes, in the order they should be evaluated and emitted.
Returns
The composed RuleList<Rules...>.

Definition at line 1919 of file forms.hpp.

◆ allRulesSatisfied()

template<typename A>
bool morph::forms::allRulesSatisfied ( const A & action)
nodiscardconstexprnoexcept

Whether every validation rule in A::formRules holds for action. Presentation rules (visibleWhen / readonlyWhen) are skipped — they can never fail this check. Returns true unconditionally for an action with no formRules (safe to call from every action's validate() regardless of whether it declares rules).

Template Parameters
AAction type.
Parameters
actionAction snapshot to check.
Returns
true when every validation rule holds (or there are none).

Definition at line 1979 of file forms.hpp.

◆ computed()

template<auto Dst, auto... Inputs, typename Fn>
auto morph::forms::computed ( Fn fn)
nodiscardconstevalnoexcept

Builds one computed-field declaration binding a destination member to its declared input members and a pure derivation function.

static constexpr auto computedFields = morph::forms::computeList(
[](const auto& s) { return s.qty * s.price; })); // auto: LineItem is incomplete here
Template Parameters
DstPointer-to-data-member of the derived (destination) field.
InputsPointer-to-data-members of the fields the derivation reads, in declaration order.
FnDeduced callable type: ValueOfDst(const A&).
Parameters
fnPure function computing the destination value from the action. Must have no side effects and read nothing beyond fn's own argument – the framework cannot check this; it is the author's contract.
Returns
A detail::ComputedField<Dst, Fn, Inputs...> value.

Definition at line 3367 of file forms.hpp.

◆ computeList()

template<typename... Fields>
auto morph::forms::computeList ( Fields... fields)
nodiscardconstevalnoexcept

Builds a ComputeList from one or more computed(...) declarations.

Assign the result to a static constexpr auto computedFields member on the action type; recomputeAll<A> and schemaJson<A>() detect it via the detail::HasComputedFields<A> concept.

Template Parameters
FieldsDeduced detail::ComputedField<...> types.
Parameters
fieldsThe computed-field declarations, in declaration order.
Returns
A detail::ComputeList<Fields...> value.

Definition at line 3380 of file forms.hpp.

◆ recomputeAll()

template<typename A>
void morph::forms::recomputeAll ( A & action)
constexpr

Recomputes every entry of A::computedFields in place on action.

A no-op for actions with no computedFields declaration – backward compatible with every existing action type. For an action that does declare computedFields, every entry is evaluated in declaration order via detail::recomputeOne (see that function for the per-entry semantics: empty-input propagation and declared-precision retagging). Called from the reactive set<> path (bridge.hpp, live/non-authoritative) and from every dispatch site (bridge.hpp, registry.hpp, authoritative) so the value the client displays and the value the server stores are derived from the identical function over identically-reconciled inputs.

Template Parameters
AAction type (a reflectable aggregate).
Parameters
actionDraft action whose computed members are overwritten in place.

Definition at line 3398 of file forms.hpp.

◆ describe()

template<auto MemberPtr>
FieldMeta morph::forms::describe ( std::string_view label = {},
std::string_view help = {} )
nodiscardnoexcept

Builds a FieldMeta for the member named by MemberPtr, so the wire key is never restated as a string.

Warning
Because this resolves MemberPtr via runtime reflection on a probe instance of its own containing type, a fieldMetadata array built from describe<>() cannot be a single in-class static constexpr initializer (the type is still incomplete at that point, and glaze's reflection for it is not constexpr either — see docs/spec/forms/forms.md, "deriving the field name from the member", for the two compile errors this produces). Declare the member in the class and define it just after the closing brace instead:
struct RecordMeasurement {
Choice<std::int64_t, "ListSamples"> sampleId;
Density density{};
Moisture moisture{};
static const std::array<morph::forms::FieldMeta, 2> fieldMetadata;
};
inline const std::array<morph::forms::FieldMeta, 2> RecordMeasurement::fieldMetadata{
morph::forms::describe<&RecordMeasurement::sampleId>("Sample", "Which logged sample…"),
};
FieldMeta describe(std::string_view label={}, std::string_view help={}) noexcept
Builds a FieldMeta for the member named by MemberPtr, so the wire key is never restated as a string.
Definition forms.hpp:3438
The plain FieldMeta{.field = "sampleId", ...} literal form has no such restriction and stays a single in-class static constexpr array.
Template Parameters
MemberPtrPointer to the member, e.g. &RecordMeasurement::sampleId.
Parameters
labelDisplay label; empty infers one from the member name.
helpHelp text; empty omits description.
Returns
A FieldMeta naming MemberPtr's wire key, with label and help set.

Definition at line 3438 of file forms.hpp.

◆ reconcileDeclaredPrecision()

template<typename A>
void morph::forms::reconcileDeclaredPrecision ( A & action)
constexpr

Rounds every Quantity member of action to its declared precision, so the stored value matches the precision the schema advertises via x-decimalPlaces.

A wire payload carries each Quantity with its own runtime dp, which a client may set to anything. Left alone, the field is stored at the client's dp, silently contradicting the schema's x-decimalPlaces (which is the field's compile-time declared precision, Quantity<U, Dec>::declaredDecimals). Calling this on the decode path — right after ActionTraits<A>::fromJson and before dispatch — rounds each Quantity to declaredPrecision() so the two agree. atDeclaredPrecision() performs an exact Rational re-rounding (half away from zero, the rule the decimal formatter uses), so the value a handler stores is the value the form displays — not a finer one hidden behind a coarser tag. An empty Quantity is left empty; non-Quantity members are untouched.

This is the enforcement half of the x-decimalPlaces contract: the schema advertises the declared precision and the dispatch path stores at that precision, rather than honouring whatever dp the client sent. Precision beyond the declared amount is therefore discarded, not hidden — that is the point, and it is why the operation normalises rather than rejects: the same call also lands on server-derived Quantity values (see recomputeOne), which routinely carry more decimals than the destination field declares.

Template Parameters
AAction type (a reflectable aggregate).
Parameters
actionDraft action whose Quantity members are rounded in place.

Definition at line 3468 of file forms.hpp.

◆ checkQuantityBounds()

template<typename A>
std::optional< std::string > morph::forms::checkQuantityBounds ( const A & action)
inlinenodiscard

Checks every Quantity member of action against its unit's declared bounds (morph::units::Quantity::withinDeclaredBounds, driven by the optional UnitTraits<E>::bounds(E) customisation point).

This is the pre-decode wire validation seam: called on the decode path — right after ActionTraits<A>::fromJson and reconcileDeclaredPrecision, before recomputeAll/ActionValidator<A>::ready — so a wire payload carrying a value outside a field's declared physical/unit bounds (e.g. a percentage above 100, a mass below zero) is rejected uniformly at the framework level, before an action's own validate() (a business-rule check, not a decode-level one) ever runs. No-op — always returns std::nullopt — for actions with no Quantity members, or whose Quantity members' units declare no bounds(): zero behaviour change, backward compatible, exactly like reconcileDeclaredPrecision.

Template Parameters
AAction type (a reflectable aggregate).
Parameters
actionDecoded action to check.
Returns
The wire name of the first out-of-bounds Quantity member encountered (in declaration order), or std::nullopt when every Quantity member is within its declared bounds (or the unit declares none).

Definition at line 3531 of file forms.hpp.

◆ enforceQuantityBounds()

template<typename A>
void morph::forms::enforceQuantityBounds ( const A & action)
inline

Runs checkQuantityBounds<A>(action) and throws QuantityDecodeError naming the first out-of-bounds field, if any. The throwing counterpart used directly on the decode path (registry.hpp/bridge.hpp call sites); a caller that wants the field name without an exception uses checkQuantityBounds itself.

Template Parameters
AAction type (a reflectable aggregate).
Parameters
actionDecoded action to check.
Exceptions
QuantityDecodeErrorif any Quantity member is outside its unit's declared bounds.

Definition at line 3563 of file forms.hpp.

◆ allRequiredEngaged()

template<typename A>
bool morph::forms::allRequiredEngaged ( const A & action)
nodiscardconstexprnoexcept

Whether every required empty-capable member of action is engaged (has a value).

Empty-capable covers Quantity, Choice, Timestamp, and any user type satisfying EmptyCapableField. Required means: not a std::optional<...> member, not listed in A::optionalFields, and not the destination of a 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 action's own top-level members are inspected, never a nested aggregate's, a std::vector<Sub>'s, or the value inside a std::optional<Sub>. 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.

Template Parameters
AAction type (a reflectable aggregate).
Parameters
actionDraft whose fields are checked.
Returns
true when no required empty-capable field is empty.

Definition at line 3589 of file forms.hpp.

◆ allFieldBoundsSatisfied()

template<typename A>
bool morph::forms::allFieldBoundsSatisfied ( const A & action)
nodiscardnoexcept

Whether every numeric member of action satisfies the minimum/maximum/multipleOf its A::fieldMetadata entry declares.

The evaluating half of the per-field bounds vocabulary: schemaJson<A>() serves the same three declarations as JSON-Schema keys, so a client can gate on them, and this is what the server checks — one declaration, two consumers, no second source of truth (examples/IMPLEMENTATION.md rule 3). Intended as (part of) the action's validate(), which puts it on every dispatch path ActionValidator<A>::ready already guards.

An unengaged EmptyCapableField is vacuously satisfied, exactly as the formRules comparison kinds are: a form still being filled in must not fail a bound on a field that has no value yet, and whether the field has to be filled at all is required/allRequiredEngaged's question. A field with no empty state is always checked.

Bounds are read from these member kinds and no others: Quantity (its engaged math::Rational, in the canonical unit), a bare math::Rational, and an integral member satisfying detail::BoundCheckableInteger. A declaration on a member of any other type — a std::string, a bool, a std::uint64_t — is inert here; the schema still advertises it.

Template Parameters
AAction type (a reflectable aggregate).
Parameters
actionDraft whose fields are checked.
Returns
true when no declared bound is violated, and trivially true for an action that declares no fieldMetadata.

Definition at line 3634 of file forms.hpp.

◆ schemaJson()

template<typename A>
const std::string & morph::forms::schemaJson ( )
nodiscard

Generates the JSON Schema for action type A, ready for a client-side form renderer.

glaze's write_json_schema<A>() output, post-processed with:

  • a top-level required array (see file docs for the rule),
  • x-decimalPlaces on every Quantity property (the field's declared precision — see reconcileDeclaredPrecision),
  • x-order (declaration index) on every property.

The result is fixed per type, so it is computed once and cached. On any internal failure the unmerged glaze schema (or an empty string if even that failed) is returned rather than throwing — schema generation is a description facility, never worth crashing a server over.

The single exception is a self-contradicting declaration. When an A::formRules entry caps engagement over two or more fields that A also makes required, the pair describes a form no submission can satisfy, and UnsatisfiableFormError is thrown. That is an author error in A, decidable at generation time, and every client would otherwise inherit an unsubmittable form (see detail::rejectUnsatisfiableRules). Because the cache is a function-local static, the throw leaves it uninitialised and a later call re-runs the check rather than serving a half-built schema.

Returned by reference, like model::payloadFingerprint<A>() and model::payloadShapeString<A>() (core/payload_schema.hpp), which cache the same way: the cached string is created once per type per process, is never mutated afterwards, and lives until the process exits, so the reference stays valid for as long as any caller could hold it. Returning it by value made every call — including the per-request ones on a server's descriptor path — an allocation plus a copy of the whole schema for a string the caller almost always only reads. A caller that genuinely needs its own mutable copy asks for one (std::string mine = schemaJson<A>();), which is what the by-value signature used to do unconditionally.

Template Parameters
AAction type (a reflectable aggregate).
Returns
Reference to the process-lifetime merged schema JSON for A.
Exceptions
UnsatisfiableFormErrorif A::formRules declares a capping rule (exactlyOneOf / mutuallyExclusive) over two or more fields that are also in A's derived required array.

Definition at line 3705 of file forms.hpp.

Variable Documentation

◆ kExactDoubleLimit

std::uint64_t morph::forms::kExactDoubleLimit = 9007199254740992ULL
inlineconstexpr

The largest N such that every integer in [0, N] is exactly representable as an IEEE-754 double: 2^53.

Not "the largest value a double holds exactly" — 2^60 is exact too. What stops at 2^53 is the contiguous range: past it, consecutive integers start sharing a representation, so an integer bound above it cannot be relied on to survive JSON.parse intact and needs an exact companion the renderer can read instead.

Definition at line 1302 of file forms.hpp.

◆ kExactDoubleLimitSigned

std::int64_t morph::forms::kExactDoubleLimitSigned = 9007199254740992LL
inlineconstexpr

Signed spelling of kExactDoubleLimit, for the negative bound.

Definition at line 1305 of file forms.hpp.