173#include <glaze/glaze.hpp>
181#include <string_view>
183#include <type_traits>
184#include <unordered_map>
185#include <unordered_set>
195namespace morph::forms {
266 std::optional<::morph::math::Rational>
minimum{};
271 std::optional<::morph::math::Rational>
maximum{};
314 std::optional<::morph::math::DecimalPlaces>
decimals{};
432 { field.hasValue() }
noexcept -> std::convertible_to<bool>;
439struct IsStdOptional : std::false_type {};
442struct IsStdOptional<std::optional<T>> : std::true_type {};
445inline constexpr bool isStdOptional = IsStdOptional<std::remove_cvref_t<T>>::value;
450concept HasOptionalFields =
requires {
451 std::begin(A::optionalFields);
452 std::end(A::optionalFields);
457[[nodiscard]]
constexpr bool declaredOptional(std::string_view fieldName)
noexcept {
458 if constexpr (HasOptionalFields<A>) {
459 for (std::string_view
const candidate : A::optionalFields) {
460 if (candidate == fieldName) {
465 static_cast<void>(fieldName);
481concept HasFieldMetadata =
requires {
482 { *std::begin(A::fieldMetadata) } -> std::convertible_to<const FieldMeta&>;
483 std::end(A::fieldMetadata);
490[[nodiscard]]
const FieldMeta* findFieldMeta(std::string_view fieldName)
noexcept {
491 if constexpr (HasFieldMetadata<A>) {
492 for (
auto const& candidate : A::fieldMetadata) {
493 if (candidate.field == fieldName) {
498 static_cast<void>(fieldName);
506inline std::string inferTitle(std::string_view fieldName) {
508 bool startOfWord =
true;
509 for (std::size_t i = 0; i < fieldName.size(); ++i) {
510 char const c = fieldName[i];
515 bool const isUpper = std::isupper(
static_cast<unsigned char>(c)) != 0;
516 bool const prevUpper = i > 0 && std::isupper(
static_cast<unsigned char>(fieldName[i - 1])) != 0;
517 if (i > 0 && isUpper && !prevUpper) {
520 if (startOfWord && !result.empty()) {
523 result += startOfWord ?
static_cast<char>(std::toupper(
static_cast<unsigned char>(c)))
524 : static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
535concept DeclaresWidget =
requires {
536 { std::remove_cvref_t<T>::widget() }
noexcept -> std::convertible_to<std::string_view>;
543concept DeclaresSchemaMarker =
requires {
544 { std::remove_cvref_t<T>::schemaMarker() }
noexcept -> std::convertible_to<std::string_view>;
551template <
typename Member>
552void annotateSchemaMarker(glz::generic_u64& property) {
553 if constexpr (DeclaresSchemaMarker<Member>) {
555 property[std::string{Member::schemaMarker()}] =
true;
562concept DeclaresRangedBounds =
requires {
563 { std::remove_cvref_t<T>::min() }
noexcept;
564 { std::remove_cvref_t<T>::max() }
noexcept;
565 { std::remove_cvref_t<T>::step() }
noexcept;
571concept HasFieldMetadataEntries =
requires {
572 std::begin(A::fieldMetadata);
573 std::end(A::fieldMetadata);
583concept HasFieldMetadataWidgets =
584 HasFieldMetadataEntries<A> &&
requires(std::remove_cvref_t<
decltype(*std::begin(A::fieldMetadata))> entry) {
585 { entry.field } -> std::convertible_to<std::string_view>;
586 { entry.widget } -> std::convertible_to<std::string_view>;
593[[nodiscard]]
constexpr std::string_view widgetOverride(std::string_view fieldName)
noexcept {
594 if constexpr (HasFieldMetadataWidgets<A>) {
595 for (
auto const& entry : A::fieldMetadata) {
596 if (std::string_view{entry.field} == fieldName) {
597 return std::string_view{entry.widget};
601 static_cast<void>(fieldName);
611template <
typename A,
typename Visitor>
622constexpr void forEachNamedMember(A&& action, Visitor&& visitor) {
623 using Plain = std::remove_cvref_t<A>;
624 constexpr auto memberCount = glz::reflect<Plain>::size;
625 [&]<std::size_t... I>(std::index_sequence<I...>) {
631 (visitor.template operator()<I>(glz::reflect<Plain>::keys[I], morph::detail::reflectedMember<I>(action)), ...);
632 }(std::make_index_sequence<memberCount>{});
637struct MemberPointerClass;
640template <
typename C,
typename M>
641struct MemberPointerClass<M C::*> {
648using MemberPointerClassT =
typename MemberPointerClass<T>::type;
658template <auto MemberPtr>
659[[nodiscard]] std::string_view memberWireName() noexcept {
660 using A = MemberPointerClassT<
decltype(MemberPtr)>;
662 std::string_view found{};
663 forEachNamedMember(probe, [&]<std::size_t I>(std::string_view name,
auto& member) {
664 using MemberT = std::remove_reference_t<
decltype(member)>;
665 using TargetT = std::remove_reference_t<
decltype(probe.*MemberPtr)>;
666 if constexpr (std::is_same_v<MemberT, TargetT>) {
667 if (std::addressof(member) == std::addressof(probe.*MemberPtr)) {
682template <
typename TestMemberPtr>
683struct ConditionActionTypeFromTest;
688template <
typename Cond,
typename A>
689struct ConditionActionTypeFromTest<bool (Cond::*)(const A&) const noexcept> {
696template <
typename Cond>
697using ConditionActionType =
typename ConditionActionTypeFromTest<
decltype(&Cond::test)>::type;
705enum class RuleKind : std::uint8_t {
726[[nodiscard]]
constexpr std::string_view ruleKindName(RuleKind kind)
noexcept {
728 case RuleKind::Engaged:
730 case RuleKind::NotEngaged:
732 case RuleKind::Equals:
734 case RuleKind::Greater:
736 case RuleKind::GreaterOrEqual:
737 return "greaterOrEqual";
740 case RuleKind::LessOrEqual:
741 return "lessOrEqual";
742 case RuleKind::RequiredWhen:
743 return "requiredWhen";
744 case RuleKind::ExactlyOneOf:
745 return "exactlyOneOf";
746 case RuleKind::AtLeastOneOf:
747 return "atLeastOneOf";
748 case RuleKind::MutuallyExclusive:
749 return "mutuallyExclusive";
750 case RuleKind::VisibleWhen:
751 return "visibleWhen";
752 case RuleKind::ReadonlyWhen:
753 return "readonlyWhen";
781[[nodiscard]]
constexpr bool isEngaged(
const T& value)
noexcept {
782 if constexpr (::morph::forms::EmptyCapableField<T>) {
783 return value.hasValue();
785 return value.has_value();
795template <
typename V,
typename A>
796[[nodiscard]]
inline std::string resolveFieldName(V A::* field) {
799 forEachNamedMember(probe, [&]<std::size_t I>(std::string_view name,
const auto& member) {
800 static_cast<void>(I);
801 using Member = std::remove_cvref_t<
decltype(member)>;
802 if constexpr (std::is_same_v<Member, V>) {
803 if (
static_cast<const void*
>(&member) ==
static_cast<const void*
>(&(probe.*field))) {
804 found = std::string{name};
819concept ComparableField = ::morph::forms::EmptyCapableField<V> &&
requires(
const V& value) {
820 { *value <=> *value };
845template <std::
size_t N>
846using LiteralString = ::morph::detail::FixedString<N>;
851inline constexpr bool isLiteralString =
false;
855template <std::
size_t N>
856inline constexpr bool isLiteralString<LiteralString<N>> =
true;
908template <
typename Cond>
910 { Cond::isCondition } -> std::convertible_to<bool>;
911} && Cond::isCondition;
918template <
typename V,
typename A>
923 static constexpr detail::RuleKind
kind = detail::RuleKind::Engaged;
932 [[nodiscard]]
constexpr bool test(
const A& action)
const noexcept {
return detail::isEngaged(action.*
field); }
937 glz::generic_u64 node{};
938 node[
"kind"] = std::string{detail::ruleKindName(
kind)};
939 glz::generic_u64::array_t fields{};
940 fields.emplace_back(detail::resolveFieldName(
field));
941 node[
"fields"] = fields;
952template <
typename V,
typename A>
953 requires EngageableField<V>
954[[nodiscard]]
constexpr auto engaged(V A::* field) {
962template <
typename V,
typename A>
967 static constexpr detail::RuleKind
kind = detail::RuleKind::NotEngaged;
976 [[nodiscard]]
constexpr bool test(
const A& action)
const noexcept {
return !detail::isEngaged(action.*
field); }
981 glz::generic_u64 node{};
982 node[
"kind"] = std::string{detail::ruleKindName(
kind)};
983 glz::generic_u64::array_t fields{};
984 fields.emplace_back(detail::resolveFieldName(
field));
985 node[
"fields"] = fields;
996template <
typename V,
typename A>
997 requires EngageableField<V>
1012template <
typename V,
typename A>
1019 static constexpr detail::RuleKind
kind = detail::RuleKind::Greater;
1030 [[nodiscard]]
constexpr bool test(
const A& action)
const noexcept {
1031 const auto& lv = action.*
lhs;
1032 const auto& rv = action.*
rhs;
1033 if (!lv.hasValue() || !rv.hasValue()) {
1036 return (*lv <=> *rv) == std::strong_ordering::greater;
1042 glz::generic_u64 node{};
1043 node[
"kind"] = std::string{detail::ruleKindName(
kind)};
1044 glz::generic_u64::array_t fields{};
1045 fields.emplace_back(detail::resolveFieldName(
lhs));
1046 fields.emplace_back(detail::resolveFieldName(
rhs));
1047 node[
"fields"] = fields;
1059template <
typename V,
typename A>
1060 requires detail::ComparableField<V>
1061[[nodiscard]]
constexpr auto greater(V A::* lhs, V A::* rhs) {
1071template <
typename V,
typename A>
1078 static constexpr detail::RuleKind
kind = detail::RuleKind::GreaterOrEqual;
1089 [[nodiscard]]
constexpr bool test(
const A& action)
const noexcept {
1090 const auto& lv = action.*
lhs;
1091 const auto& rv = action.*
rhs;
1092 if (!lv.hasValue() || !rv.hasValue()) {
1095 return std::is_gteq(*lv <=> *rv);
1101 glz::generic_u64 node{};
1102 node[
"kind"] = std::string{detail::ruleKindName(
kind)};
1103 glz::generic_u64::array_t fields{};
1104 fields.emplace_back(detail::resolveFieldName(
lhs));
1105 fields.emplace_back(detail::resolveFieldName(
rhs));
1106 node[
"fields"] = fields;
1118template <
typename V,
typename A>
1119 requires detail::ComparableField<V>
1130template <
typename V,
typename A>
1137 static constexpr detail::RuleKind
kind = detail::RuleKind::Less;
1148 [[nodiscard]]
constexpr bool test(
const A& action)
const noexcept {
1149 const auto& lv = action.*
lhs;
1150 const auto& rv = action.*
rhs;
1151 if (!lv.hasValue() || !rv.hasValue()) {
1154 return std::is_lt(*lv <=> *rv);
1160 glz::generic_u64 node{};
1161 node[
"kind"] = std::string{detail::ruleKindName(
kind)};
1162 glz::generic_u64::array_t fields{};
1163 fields.emplace_back(detail::resolveFieldName(
lhs));
1164 fields.emplace_back(detail::resolveFieldName(
rhs));
1165 node[
"fields"] = fields;
1177template <
typename V,
typename A>
1178 requires detail::ComparableField<V>
1179[[nodiscard]]
constexpr auto less(V A::* lhs, V A::* rhs) {
1189template <
typename V,
typename A>
1196 static constexpr detail::RuleKind
kind = detail::RuleKind::LessOrEqual;
1207 [[nodiscard]]
constexpr bool test(
const A& action)
const noexcept {
1208 const auto& lv = action.*
lhs;
1209 const auto& rv = action.*
rhs;
1210 if (!lv.hasValue() || !rv.hasValue()) {
1213 return std::is_lteq(*lv <=> *rv);
1219 glz::generic_u64 node{};
1220 node[
"kind"] = std::string{detail::ruleKindName(
kind)};
1221 glz::generic_u64::array_t fields{};
1222 fields.emplace_back(detail::resolveFieldName(
lhs));
1223 fields.emplace_back(detail::resolveFieldName(
rhs));
1224 node[
"fields"] = fields;
1236template <
typename V,
typename A>
1237 requires detail::ComparableField<V>
1247template <
typename T>
1248concept RuleLiteral = std::same_as<T, std::int64_t> || std::same_as<T, bool> || std::same_as<T, std::string> ||
1249 std::same_as<T, ::morph::math::Rational> || detail::isLiteralString<T>;
1258template <
typename V>
1259[[nodiscard]]
constexpr decltype(
auto) equalsOperand(
const V& fieldValue)
noexcept {
1269 return (*fieldValue);
1272 return (fieldValue);
1289template <
typename V,
typename L>
1291 { detail::equalsOperand(fieldValue) == literal } -> std::convertible_to<bool>;
1314template <
typename V,
typename A,
typename L>
1321 static constexpr detail::RuleKind
kind = detail::RuleKind::Equals;
1331 [[nodiscard]]
constexpr bool test(
const A& action)
const noexcept {
1332 const auto& fieldValue = action.*
field;
1334 if (!detail::isEngaged(fieldValue)) {
1337 return static_cast<bool>(*fieldValue ==
literal);
1339 return static_cast<bool>(fieldValue ==
literal);
1348 glz::generic_u64 node{};
1349 node[
"kind"] = std::string{detail::ruleKindName(
kind)};
1350 glz::generic_u64::array_t fields{};
1351 fields.emplace_back(detail::resolveFieldName(
field));
1352 node[
"fields"] = fields;
1353 if constexpr (std::is_same_v<L, ::morph::math::Rational>) {
1354 glz::generic_u64 value{};
1355 value[
"num"] =
literal.numerator;
1356 value[
"den"] =
literal.denominator;
1357 node[
"value"] = value;
1358 }
else if constexpr (detail::isLiteralString<L>) {
1363 node[
"value"] = std::string{
literal.view()};
1371 if constexpr (std::is_integral_v<L> && !std::is_same_v<L, bool>) {
1375 node[
"valueText"] = std::to_string(
literal);
1391template <
typename V,
typename A, RuleLiteral L>
1392 requires ComparableAgainstLiteral<V, L>
1393[[nodiscard]]
constexpr auto equals(V A::* field, L literal) {
1415template <
typename V,
typename A, std::
size_t N>
1416 requires ComparableAgainstLiteral<V, detail::LiteralString<N>>
1417[[nodiscard]]
constexpr auto equals(V A::* field,
const char (&literal)[N]) {
1426template <
typename V,
typename A,
typename Cond>
1433 static constexpr detail::RuleKind
kind = detail::RuleKind::RequiredWhen;
1440 [[nodiscard]]
constexpr bool test(
const A& action)
const noexcept {
1441 if (!
when.test(action)) {
1444 return detail::isEngaged(action.*
field);
1450 glz::generic_u64 node{};
1451 node[
"kind"] = std::string{detail::ruleKindName(
kind)};
1452 glz::generic_u64::array_t fields{};
1453 fields.emplace_back(detail::resolveFieldName(
field));
1454 node[
"fields"] = fields;
1455 node[
"when"] =
when.emitNode();
1469template <
typename V,
typename A, Condition Cond>
1470 requires EngageableField<V> && std::same_as<detail::ConditionActionType<Cond>, A>
1479template <
typename A,
typename... Vs>
1484 static constexpr detail::RuleKind
kind = detail::RuleKind::ExactlyOneOf;
1495 [[nodiscard]]
constexpr bool test(
const A& action)
const noexcept {
1496 int engagedCount = 0;
1497 std::apply([&](
auto... field) { ((engagedCount += (detail::isEngaged(action.*field) ? 1 : 0)), ...); },
1499 return engagedCount == 1;
1505 glz::generic_u64 node{};
1506 node[
"kind"] = std::string{detail::ruleKindName(
kind)};
1507 glz::generic_u64::array_t names{};
1508 std::apply([&](
auto... field) { (names.emplace_back(detail::resolveFieldName(field)), ...); },
fields);
1509 node[
"fields"] = names;
1520template <
typename A,
typename... Vs>
1521 requires(EngageableField<Vs> && ...)
1523 return ExactlyOneOf<A, Vs...>{std::tuple<Vs A::*...>{fields...}};
1530template <
typename A,
typename... Vs>
1535 static constexpr detail::RuleKind
kind = detail::RuleKind::AtLeastOneOf;
1546 [[nodiscard]]
constexpr bool test(
const A& action)
const noexcept {
1547 int engagedCount = 0;
1548 std::apply([&](
auto... field) { ((engagedCount += (detail::isEngaged(action.*field) ? 1 : 0)), ...); },
1550 return engagedCount >= 1;
1556 glz::generic_u64 node{};
1557 node[
"kind"] = std::string{detail::ruleKindName(
kind)};
1558 glz::generic_u64::array_t names{};
1559 std::apply([&](
auto... field) { (names.emplace_back(detail::resolveFieldName(field)), ...); },
fields);
1560 node[
"fields"] = names;
1571template <
typename A,
typename... Vs>
1572 requires(EngageableField<Vs> && ...)
1574 return AtLeastOneOf<A, Vs...>{std::tuple<Vs A::*...>{fields...}};
1581template <
typename A,
typename... Vs>
1586 static constexpr detail::RuleKind
kind = detail::RuleKind::MutuallyExclusive;
1597 [[nodiscard]]
constexpr bool test(
const A& action)
const noexcept {
1598 int engagedCount = 0;
1599 std::apply([&](
auto... field) { ((engagedCount += (detail::isEngaged(action.*field) ? 1 : 0)), ...); },
1601 return engagedCount <= 1;
1607 glz::generic_u64 node{};
1608 node[
"kind"] = std::string{detail::ruleKindName(
kind)};
1609 glz::generic_u64::array_t names{};
1610 std::apply([&](
auto... field) { (names.emplace_back(detail::resolveFieldName(field)), ...); },
fields);
1611 node[
"fields"] = names;
1622template <
typename A,
typename... Vs>
1623 requires(EngageableField<Vs> && ...)
1635template <
typename V,
typename A,
typename Cond>
1642 static constexpr detail::RuleKind
kind = detail::RuleKind::VisibleWhen;
1652 [[nodiscard]]
constexpr bool test(
const A& action)
const noexcept {
1653 static_cast<void>(action);
1660 glz::generic_u64 node{};
1661 node[
"kind"] = std::string{detail::ruleKindName(
kind)};
1662 glz::generic_u64::array_t fields{};
1663 fields.emplace_back(detail::resolveFieldName(
field));
1664 node[
"fields"] = fields;
1665 node[
"when"] =
when.emitNode();
1678template <
typename V,
typename A, Condition Cond>
1679 requires std::same_as<detail::ConditionActionType<Cond>, A>
1690template <
typename V,
typename A,
typename Cond>
1697 static constexpr detail::RuleKind
kind = detail::RuleKind::ReadonlyWhen;
1707 [[nodiscard]]
constexpr bool test(
const A& action)
const noexcept {
1708 static_cast<void>(action);
1715 glz::generic_u64 node{};
1716 node[
"kind"] = std::string{detail::ruleKindName(
kind)};
1717 glz::generic_u64::array_t fields{};
1718 fields.emplace_back(detail::resolveFieldName(
field));
1719 node[
"fields"] = fields;
1720 node[
"when"] =
when.emitNode();
1733template <
typename V,
typename A, Condition Cond>
1734 requires std::same_as<detail::ConditionActionType<Cond>, A>
1748template <
typename A,
typename... Conds>
1753 static constexpr detail::RuleKind
kind = detail::RuleKind::And;
1765 [[nodiscard]]
constexpr bool test(
const A& action)
const noexcept {
1766 return std::apply([&](
const auto&... cond) {
return (cond.test(action) && ...); },
conditions);
1772 glz::generic_u64 node{};
1773 node[
"kind"] = std::string{detail::ruleKindName(
kind)};
1774 glz::generic_u64::array_t nested{};
1775 std::apply([&](
const auto&... cond) { (nested.emplace_back(cond.emitNode()), ...); },
conditions);
1776 node[
"conditions"] = nested;
1795template <Condition Cond0, Condition... Conds>
1796 requires(std::same_as<detail::ConditionActionType<Cond0>, detail::ConditionActionType<Conds>> && ...)
1797[[nodiscard]]
constexpr auto andOf(Cond0 condition0, Conds... conditions) {
1798 static_assert(
sizeof...(Conds) >= 1,
"andOf: needs at least two conditions; one operand is the condition itself");
1800 std::tuple<Cond0, Conds...>{std::move(condition0), std::move(conditions)...}};
1808template <
typename A,
typename... Conds>
1813 static constexpr detail::RuleKind
kind = detail::RuleKind::Or;
1825 [[nodiscard]]
constexpr bool test(
const A& action)
const noexcept {
1826 return std::apply([&](
const auto&... cond) {
return (cond.test(action) || ...); },
conditions);
1832 glz::generic_u64 node{};
1833 node[
"kind"] = std::string{detail::ruleKindName(
kind)};
1834 glz::generic_u64::array_t nested{};
1835 std::apply([&](
const auto&... cond) { (nested.emplace_back(cond.emitNode()), ...); },
conditions);
1836 node[
"conditions"] = nested;
1850template <Condition Cond0, Condition... Conds>
1851 requires(std::same_as<detail::ConditionActionType<Cond0>, detail::ConditionActionType<Conds>> && ...)
1852[[nodiscard]]
constexpr auto orOf(Cond0 condition0, Conds... conditions) {
1853 static_assert(
sizeof...(Conds) >= 1,
"orOf: needs at least two conditions; one operand is the condition itself");
1855 std::tuple<Cond0, Conds...>{std::move(condition0), std::move(conditions)...}};
1863template <
typename A,
typename Cond>
1868 static constexpr detail::RuleKind
kind = detail::RuleKind::Not;
1880 [[nodiscard]]
constexpr bool test(
const A& action)
const noexcept {
return !
condition.test(action); }
1885 glz::generic_u64 node{};
1886 node[
"kind"] = std::string{detail::ruleKindName(
kind)};
1887 node[
"condition"] =
condition.emitNode();
1897template <Condition Cond>
1898[[nodiscard]]
constexpr auto notOf(Cond condition) {
1906template <
typename... Rules>
1918template <
typename... Rules>
1920 return RuleList<Rules...>{std::tuple<Rules...>{std::move(rules)...}};
1925template <
typename A>
1947template <
typename A>
1949 { A::explicitSubmit } -> std::convertible_to<bool>;
1957template <
typename Rule,
typename A>
1958[[nodiscard]]
constexpr bool evaluateGatingRule(
const Rule& rule,
const A& action)
noexcept {
1959 if constexpr (Rule::isPresentation) {
1960 static_cast<void>(rule);
1961 static_cast<void>(action);
1964 return rule.test(action);
1978template <
typename A>
1981 return std::apply([&](
const auto&... rule) {
return (detail::evaluateGatingRule(rule, action) && ...); },
1982 A::formRules.rules);
1984 static_cast<void>(action);
2016 : std::logic_error(
"morph::forms: unsatisfiable form for action '" + std::string{actionName} +
"': rule '" +
2017 std::string{ruleKind} +
2018 "' caps how many of its fields may be engaged, but these are also "
2019 "in the schema's required array: " +
2020 std::string{requiredFields} +
2021 ". No submission can satisfy both. List them in the action's optionalFields, or drop the "
2036 : std::logic_error(
"morph::forms: '" + std::string{actionName} +
"." + std::string{field} +
2037 "' is a Choice and its FieldMeta also sets optionsAction; the Choice already names its "
2038 "options action. Drop optionsAction from the FieldMeta, or make the member a plain type.") {
2054 : std::logic_error(
"morph::forms: '" + std::string{actionName} +
"." + std::string{field} +
2055 "' declares options in its FieldMeta without " + std::string{missing} +
2056 "; set optionsAction, valueField and labelField together.") {}
2071template <
auto Dst,
typename Fn,
auto... Inputs>
2072struct ComputedField {
2079template <
typename A>
2080concept HasComputedFields =
requires { A::computedFields; };
2086template <
typename... Fields>
2089 std::tuple<Fields...> fields;
2101template <
typename A,
auto Dst,
typename Fn,
auto... Inputs>
2102[[nodiscard]]
constexpr bool isDestinationOf(
const A& action,
const void* memberAddr,
2103 const ComputedField<Dst, Fn, Inputs...>& field)
noexcept {
2104 static_cast<void>(field);
2105 return memberAddr ==
static_cast<const void*
>(std::addressof(action.*Dst));
2122template <
typename A>
2123[[nodiscard]]
constexpr bool isComputedDestinationMember(
const A& action,
const void* memberAddr)
noexcept {
2124 if constexpr (HasComputedFields<A>) {
2126 std::apply([&](
const auto&... field) { ((found = found || isDestinationOf(action, memberAddr, field)), ...); },
2127 action.computedFields.fields);
2130 static_cast<void>(action);
2131 static_cast<void>(memberAddr);
2159template <
typename A,
auto Dst,
typename Fn,
auto... Inputs>
2160constexpr void recomputeOne(A& action,
const ComputedField<Dst, Fn, Inputs...>& field) {
2161 bool allEngaged =
true;
2162 [[maybe_unused]]
auto checkInput = [&]<
auto InputPtr>() {
2163 using InputMember = std::remove_cvref_t<
decltype(action.*InputPtr)>;
2164 if constexpr (EmptyCapableField<InputMember>) {
2165 if (!(action.*InputPtr).hasValue()) {
2170 (checkInput.template operator()<Inputs>(), ...);
2172 using DstMember = std::remove_cvref_t<
decltype(action.*Dst)>;
2174 action.*Dst = DstMember{};
2177 auto result = field.fn(action);
2178 if constexpr (units::isQuantity<DstMember>) {
2179 DstMember
const converted = result;
2180 action.*Dst = converted.atDeclaredPrecision();
2182 action.*Dst = result;
2199template <
typename A,
typename MemberPtr>
2200[[nodiscard]] std::string_view resolveMemberName(
const A& probe, MemberPtr memberPtr) {
2201 std::string_view result;
2202 forEachNamedMember(probe, [&]<std::size_t I>(std::string_view name,
const auto& member) {
2203 static_cast<void>(I);
2204 if (
static_cast<const void*
>(std::addressof(member)) ==
2205 static_cast<const void*
>(std::addressof(probe.*memberPtr))) {
2221template <
typename A,
auto Dst,
typename Fn,
auto... Inputs>
2222void collectComputedInputs(
const A& probe,
const ComputedField<Dst, Fn, Inputs...>& field,
2223 std::unordered_map<std::string_view, std::vector<std::string_view>>& out) {
2224 static_cast<void>(field);
2225 out.emplace(resolveMemberName(probe, Dst), std::vector<std::string_view>{resolveMemberName(probe, Inputs)...});
2230template <
typename T>
2231struct IsStdVector : std::false_type {
2232 using ValueType = void;
2235template <
typename T,
typename Alloc>
2236struct IsStdVector<std::vector<T, Alloc>> : std::true_type {
2237 using ValueType = T;
2244template <
typename T>
2245concept ReflectableAggregate = glz::reflectable<T> || glz::glaze_object_t<T>;
2258template <
typename T>
2259concept BoundCheckableInteger =
2260 std::integral<T> && !std::same_as<T, bool> && (std::signed_integral<T> || std::numeric_limits<T>::digits < 64);
2276inline void emitDeclaredBound(glz::generic_u64& property,
const std::string& key,
2277 const ::morph::math::Rational& bound) {
2278 if (bound.isInteger()) {
2279 property[key] = bound.numerator;
2281 property[key] =
static_cast<double>(bound.numerator) /
static_cast<double>(bound.denominator);
2289[[nodiscard]]
constexpr bool declaresAnyBound(
const FieldMeta& meta)
noexcept {
2290 return meta.minimum.has_value() || meta.maximum.has_value() || meta.multipleOf.has_value();
2297[[nodiscard]]
constexpr bool hasUsableMultipleOf(
const FieldMeta& meta)
noexcept {
2298 return meta.multipleOf.has_value() && !meta.multipleOf->isZero() && !meta.multipleOf->isNegative();
2310inline void annotateDeclaredBounds(glz::generic_u64& property,
const FieldMeta& meta) {
2311 if (meta.minimum.has_value()) {
2312 emitDeclaredBound(property,
"minimum", *meta.minimum);
2314 if (meta.maximum.has_value()) {
2315 emitDeclaredBound(property,
"maximum", *meta.maximum);
2317 if (hasUsableMultipleOf(meta)) {
2318 emitDeclaredBound(property,
"multipleOf", *meta.multipleOf);
2324template <
typename T>
2325inline constexpr bool isRationalMember = std::is_same_v<std::remove_cvref_t<T>, ::morph::math::Rational>;
2327template <
typename T>
2328inline constexpr bool isRationalMember<std::optional<T>> = isRationalMember<T>;
2343template <
typename Member>
2344void annotateDisplayUnit(glz::generic_u64& property,
const FieldMeta& meta) {
2345 if constexpr (!units::isQuantity<Member>) {
2346 if (!meta.unit.empty()) {
2347 glz::generic_u64 units{};
2348 units[
"unitAscii"] = std::string{meta.unit};
2349 units[
"unitUnicode"] = std::string{meta.unit};
2350 property[
"ExtUnits"] = std::move(units);
2352 if (meta.decimals.has_value() && meta.decimals->value <= ::morph::math::kMaxDecimalPlaces) {
2354 property[isRationalMember<Member> ?
"x-decimalPlaces" :
"x-displayDecimals"] =
2355 std::uint64_t{meta.decimals->value};
2373[[nodiscard]]
constexpr bool satisfiesDeclaredBounds(
const FieldMeta& meta,
2374 const ::morph::math::Rational& value)
noexcept {
2375 if (meta.minimum.has_value() && std::is_lt(value <=> *meta.minimum)) {
2378 if (meta.maximum.has_value() && std::is_gt(value <=> *meta.maximum)) {
2381 if (hasUsableMultipleOf(meta)) {
2382 auto const quotient = ::morph::math::checkedDiv(value, *meta.multipleOf);
2383 if (!quotient.has_value() || !quotient->isInteger()) {
2399template <
typename Owner,
typename Member>
2400void annotateMetaOptions(glz::generic_u64& property, std::string_view name, FieldMeta
const& meta) {
2401 bool const action = !meta.optionsAction.empty();
2402 if (!action && meta.valueField.empty() && meta.labelField.empty()) {
2405 if constexpr (isChoice<Member>) {
2407 throw ConflictingOptionsError{glz::name_v<Owner>, name};
2410 std::string missing;
2411 auto const lacks = [&missing](
bool set, std::string_view label) {
2413 missing += (missing.empty() ?
"" :
", ") + std::string{label};
2416 lacks(action,
"optionsAction");
2417 lacks(!meta.valueField.empty(),
"valueField");
2418 lacks(!meta.labelField.empty(),
"labelField");
2419 if (!missing.empty()) {
2420 throw IncompleteOptionsError{glz::name_v<Owner>, name, missing};
2423 property[
"x-optionsAction"] = std::string{meta.optionsAction};
2424 property[
"x-optionValue"] = std::string{meta.valueField};
2425 property[
"x-optionLabel"] = std::string{meta.labelField};
2444template <
typename Owner,
typename Member>
2445void annotateBasicMemberProperty(glz::generic_u64& property, std::string_view name) {
2455 if constexpr (std::is_enum_v<Member>) {
2456 static_assert(glz::glaze_enum_t<Member>,
2457 "morph::forms: an enum class rendered as a form field must declare a glz::meta with "
2458 "glz::enumerate -- without one, schemaJson<A>() cannot describe its closed set of "
2459 "values (glaze emits a six-way wildcard type instead), which no renderer can draw as a "
2460 "choice. See docs/spec/forms/forms.md, \"Closed sets\", and examples/"
2461 "IMPLEMENTATION.md rule 3.");
2463 const FieldMeta* fieldMeta = findFieldMeta<Owner>(name);
2464 std::string_view
const declaredLabel = fieldMeta !=
nullptr ? fieldMeta->label : std::string_view{};
2465 property[
"title"] = declaredLabel.empty() ? inferTitle(name) : std::string{declaredLabel};
2466 if (fieldMeta !=
nullptr) {
2467 if (!fieldMeta->help.empty()) {
2468 property[
"description"] = std::string{fieldMeta->help};
2470 if (!fieldMeta->placeholder.empty()) {
2471 property[
"x-placeholder"] = std::string{fieldMeta->placeholder};
2473 if (fieldMeta->readOnly) {
2474 property[
"x-readonly"] =
true;
2476 if (fieldMeta->hidden) {
2477 property[
"x-hidden"] =
true;
2479 if (!fieldMeta->i18nKey.empty()) {
2480 property[
"x-i18nKey"] = std::string{fieldMeta->i18nKey};
2482 annotateDeclaredBounds(property, *fieldMeta);
2483 annotateDisplayUnit<Member>(property, *fieldMeta);
2484 annotateMetaOptions<Owner, Member>(property, name, *fieldMeta);
2485 if constexpr (std::is_same_v<Member, std::string> || std::is_same_v<Member, std::optional<std::string>>) {
2486 if (fieldMeta->blankAs == BlankAs::Empty) {
2487 property[
"x-blankAs"] = std::string{
"empty"};
2492 if constexpr (units::isQuantity<Member>) {
2493 property[
"x-decimalPlaces"] = std::uint64_t{Member::declaredDecimals};
2494 auto const alternatives = Member::unitAlternatives();
2495 if (!alternatives.empty()) {
2496 glz::generic_u64::array_t
list{};
2497 for (
auto const& alternative : alternatives) {
2499 units::UnitTraits<std::remove_const_t<
decltype(Member::unit)>>::meta(alternative.unit);
2500 glz::generic_u64 entry{};
2501 entry[
"id"] = std::string{meta.id};
2502 entry[
"display"] = std::string{meta.display};
2503 entry[
"decimals"] = std::uint64_t{meta.defaultDecimals};
2504 entry[
"num"] = alternative.num;
2505 entry[
"den"] = alternative.den;
2506 list.emplace_back(std::move(entry));
2508 property[
"x-unitAlternatives"] =
list;
2511 if constexpr (isChoice<Member>) {
2512 property[
"x-optionsAction"] = std::string{Member::optionsAction()};
2513 property[
"x-optionValue"] = std::string{Member::valueField()};
2514 property[
"x-optionLabel"] = std::string{Member::labelField()};
2515 if constexpr (!Member::optionsDependsOn().empty()) {
2516 glz::generic_u64::array_t dependsOn{};
2517 for (
auto const& parentName : Member::optionsDependsOn()) {
2518 dependsOn.emplace_back(std::string{parentName});
2520 property[
"x-optionsDependsOn"] = dependsOn;
2524 std::string_view widgetHint{};
2525 if constexpr (DeclaresWidget<Member>) {
2526 widgetHint = Member::widget();
2528 if constexpr (HasFieldMetadataWidgets<Owner>) {
2529 if (
auto const overrideWidget = widgetOverride<Owner>(name); !overrideWidget.empty()) {
2530 widgetHint = overrideWidget;
2533 if (!widgetHint.empty()) {
2534 property[
"x-widget"] = std::string{widgetHint};
2536 annotateSchemaMarker<Member>(property);
2537 if constexpr (DeclaresRangedBounds<Member>) {
2538 using Bound = std::remove_cvref_t<
decltype(Member::min())>;
2539 if constexpr (std::floating_point<Bound>) {
2540 property[
"x-min"] =
static_cast<double>(Member::min());
2541 property[
"x-max"] =
static_cast<double>(Member::max());
2542 property[
"x-step"] =
static_cast<double>(Member::step());
2544 property[
"x-min"] =
static_cast<std::int64_t
>(Member::min());
2545 property[
"x-max"] =
static_cast<std::int64_t
>(Member::max());
2546 property[
"x-step"] =
static_cast<std::int64_t
>(Member::step());
2591template <
typename Node>
2592 requires std::same_as<std::remove_const_t<Node>, glz::generic_u64>
2593[[nodiscard]]
Node* findMember(Node& node, std::string_view key) {
2594 auto*
const object = node.template get_if<glz::generic_u64::object_t>();
2595 if (
object ==
nullptr) {
2598 auto*
const entry =
object->find(key);
2599 return (entry == object->end()) ? nullptr : &entry->second;
2613using NestedDefsVisited = std::unordered_set<std::string>;
2636 explicit SchemaDomRef(glz::generic_u64& dom) noexcept : _dom(&dom) {}
2640 [[nodiscard]] glz::generic_u64& value() const noexcept {
return *_dom; }
2643 glz::generic_u64* _dom;
2651template <
typename Sub>
2652void annotateNestedAggregate(SchemaDomRef dom, glz::generic_u64& node, NestedDefsVisited& visited);
2654template <
typename Sub>
2655void annotateNestedAggregateRef(SchemaDomRef dom, glz::generic_u64& propertyOrItems, NestedDefsVisited& visited);
2657template <
typename Member>
2658void recurseIntoNestedAggregateIfAny(SchemaDomRef dom, glz::generic_u64& property, NestedDefsVisited& visited);
2704template <
typename Member>
2705void recurseIntoNestedAggregateIfAny(SchemaDomRef dom, glz::generic_u64& property, NestedDefsVisited& visited) {
2706 if constexpr (ReflectableAggregate<Member>) {
2707 annotateNestedAggregateRef<Member>(dom, property, visited);
2708 }
else if constexpr (IsStdVector<Member>::value && ReflectableAggregate<typename IsStdVector<Member>::ValueType>) {
2709 using ItemType =
typename IsStdVector<Member>::ValueType;
2710 if (
auto*
const items = findMember(property,
"items")) {
2716 annotateNestedAggregateRef<ItemType>(dom, *items, visited);
2718 }
else if constexpr (isStdOptional<Member>) {
2724 using ValueType =
typename std::remove_cvref_t<Member>::value_type;
2725 if (
auto*
const branches = findMember(property,
"anyOf")) {
2726 if (
auto*
const list = branches->get_if<glz::generic_u64::array_t>()) {
2727 for (
auto& branch : *list) {
2728 recurseIntoNestedAggregateIfAny<ValueType>(dom, branch, visited);
2767template <
typename Sub>
2768void annotateNestedAggregate(SchemaDomRef dom, glz::generic_u64& node, NestedDefsVisited& visited) {
2770 glz::generic_u64::array_t requiredNames{};
2771 forEachNamedMember(probe, [&]<std::size_t I>(std::string_view name,
const auto& member) {
2772 using Member = std::remove_cvref_t<
decltype(member)>;
2773 if (!(isStdOptional<Member> || declaredOptional<Sub>(name))) {
2774 requiredNames.emplace_back(std::string{name});
2776 auto&
property = node[
"properties"][std::string{name}];
2777 property[
"x-order"] = std::uint64_t{I};
2778 annotateBasicMemberProperty<Sub, Member>(property, name);
2779 recurseIntoNestedAggregateIfAny<Member>(dom, property, visited);
2786 node[
"required"] = requiredNames;
2812template <
typename Sub>
2813void annotateNestedAggregateRef(SchemaDomRef dom, glz::generic_u64& propertyOrItems, NestedDefsVisited& visited) {
2814 constexpr std::string_view kDefsPrefix =
"#/$defs/";
2815 if (
auto*
const refNode = findMember(propertyOrItems,
"$ref")) {
2816 if (
auto const* ref = refNode->get_if<std::string>()) {
2817 if (std::string_view{*
ref}.starts_with(kDefsPrefix)) {
2818 auto const key = std::string{
ref->substr(kDefsPrefix.size())};
2829 auto*
const defs = findMember(dom.value(),
"$defs");
2830 auto*
const entry = (defs ==
nullptr) ?
nullptr : findMember(*defs, key);
2836 if (entry !=
nullptr && visited.insert(key).second) {
2837 annotateNestedAggregate<Sub>(dom, *entry, visited);
2845 if (propertyOrItems.contains(
"properties")) {
2846 annotateNestedAggregate<Sub>(dom, propertyOrItems, visited);
2863[[nodiscard]]
inline bool capsEngagedCount(std::string_view kind)
noexcept {
2864 return kind == ruleKindName(RuleKind::ExactlyOneOf) || kind == ruleKindName(RuleKind::MutuallyExclusive);
2877template <
typename T>
2878[[nodiscard]]
inline const T* ruleNodeMember(
const glz::generic_u64::object_t& node, std::string_view key) {
2879 auto const*
const entry = node.find(key);
2880 return (entry == node.end()) ? nullptr : entry->second.template get_if<T>();
2893[[nodiscard]]
inline std::optional<std::string> requiredFieldsNamedBy(
2894 const glz::generic_u64::array_t& fields,
const std::vector<std::string_view>& requiredNames) {
2895 std::string offenders{};
2896 std::size_t offenderCount = 0;
2897 for (
auto const& fieldNode : fields) {
2898 auto const* fieldName = fieldNode.get_if<std::string>();
2904 if (fieldName ==
nullptr ||
2905 std::ranges::find(requiredNames, std::string_view{*fieldName}) == requiredNames.end()) {
2908 if (offenderCount > 0) {
2912 offenders += *fieldName;
2914 return (offenderCount >= 2) ? std::optional{std::move(offenders)} : std::nullopt;
2929[[nodiscard]]
inline std::optional<std::pair<std::string, std::string>> findUnsatisfiableConjunct(
2930 const glz::generic_u64::array_t& nodes,
const std::vector<std::string_view>& requiredNames) {
2931 for (
auto const& entry : nodes) {
2932 auto const* node = entry.get_if<glz::generic_u64::object_t>();
2933 if (node ==
nullptr) {
2936 auto const* kind = ruleNodeMember<std::string>(*node,
"kind");
2937 if (kind ==
nullptr) {
2941 if (*kind == ruleKindName(RuleKind::And)) {
2942 auto const* nested = ruleNodeMember<glz::generic_u64::array_t>(*node,
"conditions");
2943 if (
auto offender = (nested ==
nullptr) ? std::nullopt : findUnsatisfiableConjunct(*nested, requiredNames);
2944 offender.has_value()) {
2950 if (!capsEngagedCount(*kind)) {
2953 auto const* fields = ruleNodeMember<glz::generic_u64::array_t>(*node,
"fields");
2954 if (fields ==
nullptr) {
2957 if (
auto offenders = requiredFieldsNamedBy(*fields, requiredNames); offenders.has_value()) {
2958 return std::pair{*kind, *std::move(offenders)};
2961 return std::nullopt;
3021template <
typename A>
3022void rejectUnsatisfiableRules(
const glz::generic_u64::array_t& xRules,
3023 const std::vector<std::string_view>& requiredNames) {
3024 if (
auto const offender = findUnsatisfiableConjunct(xRules, requiredNames); offender.has_value()) {
3025 throw UnsatisfiableFormError{glz::name_v<A>, offender->first, offender->second};
3043enum class ExactBoundKind : std::uint8_t {
3065inline void annotateExactBound(glz::generic_u64& node, ExactBoundKind kind) {
3066 bool const isMinimum = (kind == ExactBoundKind::Minimum);
3067 std::string_view
const key = isMinimum ?
"minimum" :
"maximum";
3068 std::string_view
const textKey = isMinimum ?
"x-exactMinimum" :
"x-exactMaximum";
3074 auto const*
const bound = findMember(node, key);
3075 if (bound ==
nullptr) {
3084 if (bound->template holds<std::uint64_t>()) {
3085 auto const value = bound->template get<std::uint64_t>();
3086 if (std::cmp_greater(value, kExactDoubleLimit)) {
3087 node[textKey] = std::to_string(value);
3089 }
else if (bound->template holds<std::int64_t>()) {
3090 auto const value = bound->template get<std::int64_t>();
3091 if (std::cmp_greater(value, kExactDoubleLimit) || std::cmp_less(value, -kExactDoubleLimitSigned)) {
3092 node[textKey] = std::to_string(value);
3107inline void annotateExactNumericBounds(glz::generic_u64& node) {
3108 if (node.is_object()) {
3109 annotateExactBound(node, ExactBoundKind::Minimum);
3110 annotateExactBound(node, ExactBoundKind::Maximum);
3111 for (
auto& [childKey, child] : node.get_object()) {
3112 annotateExactNumericBounds(child);
3114 }
else if (node.is_array()) {
3115 for (
auto& child : node.get_array()) {
3116 annotateExactNumericBounds(child);
3136template <
typename A>
3137void annotateSubmitMode(glz::generic_u64& dom) {
3138 if constexpr (HasExplicitSubmit<A>) {
3139 if constexpr (A::explicitSubmit) {
3140 dom[
"x-submitMode"] =
"explicit";
3158template <
typename A>
3159[[nodiscard]] std::string mergeSchemaExtras(std::string rawSchema) {
3162 glz::generic_u64 dom{};
3163 if (glz::read_json(dom, rawSchema)) {
3167 glz::generic_u64::array_t requiredNames{};
3173 std::vector<std::string_view> memberNames{};
3177 std::vector<std::string_view> requiredMemberNames{};
3189 NestedDefsVisited nestedVisited{};
3195 std::unordered_map<std::string_view, std::vector<std::string_view>> computedInputs{};
3196 if constexpr (HasComputedFields<A>) {
3197 std::apply([&](
const auto&... field) { (collectComputedInputs(probe, field, computedInputs), ...); },
3198 A::computedFields.fields);
3201 forEachNamedMember(probe, [&]<std::size_t I>(std::string_view name,
const auto& member) {
3202 using Member = std::remove_cvref_t<
decltype(member)>;
3203 static_cast<void>(member);
3204 memberNames.push_back(name);
3205 const bool isComputed = computedInputs.contains(name);
3208 const bool isOptional = isStdOptional<Member> || declaredOptional<A>(name) || isComputed;
3210 requiredNames.emplace_back(std::string{name});
3211 requiredMemberNames.push_back(name);
3213 auto&
property = dom[
"properties"][std::string{name}];
3214 property[
"x-order"] = std::uint64_t{I};
3216 property[
"x-readonly"] =
true;
3217 glz::generic_u64 computedMeta{};
3218 glz::generic_u64::array_t inputsList{};
3219 for (
auto const& inputName : computedInputs.at(name)) {
3220 inputsList.emplace_back(std::string{inputName});
3222 computedMeta[
"inputs"] = inputsList;
3223 property[
"x-computed"] = computedMeta;
3229 annotateBasicMemberProperty<A, Member>(property, name);
3244 recurseIntoNestedAggregateIfAny<Member>(SchemaDomRef{dom}, property, nestedVisited);
3248 dom[
"required"] = requiredNames;
3253 if constexpr (detail::HasFormLayout<A>) {
3254 glz::generic_u64::array_t groupsJson{};
3259 std::vector<std::pair<std::string_view, std::size_t>> sectionOf{};
3260 std::size_t groupIndex = 0;
3261 for (
auto const& group : A::formLayout) {
3262 glz::generic_u64::array_t fieldsJson{};
3263 for (std::string_view fieldName : group.fields) {
3264 bool const isMember =
3265 std::find(memberNames.begin(), memberNames.end(), fieldName) != memberNames.end();
3269 bool alreadyPlaced =
false;
3270 for (
auto const& placed : sectionOf) {
3271 if (placed.first == fieldName) {
3272 alreadyPlaced =
true;
3276 if (alreadyPlaced) {
3279 fieldsJson.emplace_back(std::string{fieldName});
3280 sectionOf.emplace_back(fieldName, groupIndex);
3282 glz::generic_u64 groupJson{};
3283 groupJson[
"title"] = std::string{group.title};
3285 groupJson[
"fields"] = fieldsJson;
3286 groupsJson.emplace_back(std::move(groupJson));
3289 dom[
"x-layout"][
"groups"] = groupsJson;
3291 for (
auto const& placed : sectionOf) {
3292 auto&
property = dom[
"properties"][std::string{placed.first}];
3293 property[
"x-group"] = std::string{A::formLayout[placed.second].title};
3294 property[
"x-section"] = std::uint64_t{placed.second};
3300 if constexpr (detail::HasFieldSpans<A>) {
3301 for (
auto const& span : A::fieldSpans) {
3302 if (span.colspan <= 1) {
3305 bool const isMember = std::find(memberNames.begin(), memberNames.end(), span.field) != memberNames.end();
3309 auto&
property = dom[
"properties"][std::string{span.field}];
3310 property[
"x-colspan"] = span.colspan;
3323 if constexpr (HasFormRules<A>) {
3324 glz::generic_u64::array_t xRules{};
3325 std::apply([&](
const auto&... rule) { (xRules.emplace_back(rule.emitNode()), ...); }, A::formRules.rules);
3329 rejectUnsatisfiableRules<A>(xRules, requiredMemberNames);
3330 dom[
"x-rules"] = xRules;
3333 annotateSubmitMode<A>(dom);
3337 annotateExactNumericBounds(dom);
3342 return glz::write_json(dom).value_or(rawSchema);
3366template <
auto Dst,
auto... Inputs,
typename Fn>
3368 return detail::ComputedField<Dst, Fn, Inputs...>{fn};
3379template <
typename... Fields>
3381 return detail::ComputeList<Fields...>{std::tuple<Fields...>{fields...}};
3397template <
typename A>
3399 if constexpr (detail::HasComputedFields<A>) {
3400 std::apply([&](
const auto&... field) { (detail::recomputeOne(action, field), ...); },
3401 A::computedFields.fields);
3403 static_cast<void>(action);
3437template <auto MemberPtr>
3439 return FieldMeta{.field = detail::memberWireName<MemberPtr>(), .label = label, .help = help};
3467template <
typename A>
3469 using Plain = std::remove_cvref_t<A>;
3475 if constexpr (glz::reflectable<Plain> || glz::glaze_object_t<Plain>) {
3476 constexpr auto memberCount = glz::reflect<Plain>::size;
3477 [&]<std::size_t... I>(std::index_sequence<I...>) {
3478 [[maybe_unused]]
auto retag = [&]<std::size_t Idx>() {
3482 using Member = std::remove_cvref_t<
decltype(member)>;
3484 member = member.atDeclaredPrecision();
3487 (retag.template operator()<I>(), ...);
3488 }(std::make_index_sequence<memberCount>{});
3490 static_cast<void>(action);
3506 : std::runtime_error(
"quantity field out of declared bounds: " + std::string{fieldName}) {}
3530template <
typename A>
3532 using Plain = std::remove_cvref_t<A>;
3533 std::optional<std::string> offender;
3534 if constexpr (glz::reflectable<Plain> || glz::glaze_object_t<Plain>) {
3535 detail::forEachNamedMember(action, [&]<std::size_t I>(std::string_view name,
const auto& member) {
3536 static_cast<void>(I);
3537 if (offender.has_value()) {
3540 using Member = std::remove_cvref_t<
decltype(member)>;
3542 if (!member.withinDeclaredBounds()) {
3543 offender = std::string{name};
3548 static_cast<void>(action);
3562template <
typename A>
3588template <
typename A>
3590 bool allEngaged =
true;
3591 detail::forEachNamedMember(action, [&]<std::size_t I>(std::string_view name,
const auto& member) {
3592 using Member = std::remove_cvref_t<
decltype(member)>;
3594 const bool isComputed =
3595 detail::isComputedDestinationMember(action,
static_cast<const void*
>(std::addressof(member)));
3596 if (!detail::declaredOptional<A>(name) && !isComputed && !member.hasValue()) {
3600 static_cast<void>(name);
3601 static_cast<void>(member);
3633template <
typename A>
3635 if constexpr (!detail::HasFieldMetadata<A>) {
3636 static_cast<void>(action);
3639 bool satisfied =
true;
3640 detail::forEachNamedMember(action, [&]<std::size_t I>(std::string_view name,
const auto& member) {
3641 using Member = std::remove_cvref_t<
decltype(member)>;
3642 const FieldMeta* meta = detail::findFieldMeta<A>(name);
3643 if (meta ==
nullptr || !detail::declaresAnyBound(*meta)) {
3647 if (member.hasValue() && !detail::satisfiesDeclaredBounds(*meta, *member)) {
3650 }
else if constexpr (std::same_as<Member, ::morph::math::Rational>) {
3651 if (!detail::satisfiesDeclaredBounds(*meta, member)) {
3654 }
else if constexpr (detail::BoundCheckableInteger<Member>) {
3655 const ::morph::math::Rational value{
static_cast<std::int64_t
>(member),
3657 if (!detail::satisfiesDeclaredBounds(*meta, value)) {
3704template <
typename A>
3706 static const std::string cached =
3707 detail::mergeSchemaExtras<A>(glz::write_json_schema<A>().value_or(std::string{}));
AnyHandle list(std::vector< AnyHandle > const &items)
{"list": [...]}: a list built from expressions.
Handle< T > ref(std::string const &path)
A named read, {"ref": "name.path"}: a param, state, let, query value, element value or bound name.
Command set(AnyHandle const &target, AnyHandle const &value)
set: writes a state, an input's value or an app name.
morph::forms::BlankAs: what a blank string field submits.
BlankAs
What a renderer submits for a string field the user left blank (FieldMeta::blankAs).
@ Omit
Blank is "no value": the member is left out of the payload (the default).
Fields whose value is picked from a list served by another action.
Shared compile-time, NTTP-capable fixed string.
Visual structure (sections, tabs, accordions, column spans) layered over an action's flat field list.
constexpr std::string_view groupKindName(GroupKind kind) noexcept
Maps a GroupKind to the string mergeSchemaExtras emits for it in x-layout.groups[]....
Unit-tagged, optionally-empty exact values with a derivation trail.
constexpr bool isQuantity
true when T (cvref-stripped) is a morph::units::Quantity.
Exact rational arithmetic for morph model/action values.
The value glaze reads and writes under a reflected key.
constexpr decltype(auto) reflectedMember(T &object)
The value glaze reads and writes under glz::reflect<T>::keys[I].
Strong type for a decimal-precision count.
Exact rational number with a runtime decimal-precision tag.
std::shared_ptr< NodeData const > Node
A view-tree node: immutable, shared data.