52#include <glaze/glaze.hpp>
58#include "../attributes.hpp"
63namespace morph::forms {
97 return "belowMinimum";
99 return "aboveMaximum";
101 return "precisionExceeded";
139[[nodiscard]]
constexpr std::int64_t powerOfTenForConstraint(std::uint32_t places)
noexcept {
140 std::int64_t result = 1;
141 for (std::uint32_t i = 0; i < places; ++i) {
155[[nodiscard]]
constexpr bool exactAtDecimals(
const math::Rational& value, std::uint32_t places)
noexcept {
156 if (places > math::kMaxDecimalPlaces) {
159 return powerOfTenForConstraint(places) % value.denominator == 0;
167[[nodiscard]]
inline glz::generic_u64 boundNode(
const math::Rational& bound) {
168 glz::generic_u64 node{};
170 node[
"num"] = bound.numerator;
171 node[
"den"] = bound.denominator;
172 node[
"dp"] =
static_cast<std::uint64_t
>(bound.decimalPlaces.value);
204 for (
auto& existing : _fields) {
205 if (existing.field == constraint.field) {
206 existing = std::move(constraint);
210 _fields.push_back(std::move(constraint));
220 for (
const auto& entry : _fields) {
221 if (entry.field == field) {
230 [[nodiscard]]
bool empty() const noexcept {
return _fields.empty(); }
235 [[nodiscard]]
const std::vector<FieldConstraint>&
fields() const noexcept MORPH_LIFETIMEBOUND {
return _fields; }
254 [[nodiscard]] std::string
decorate(std::string schema)
const {
255 if (_fields.empty()) {
262 glz::generic_u64 dom{};
263 if (glz::read_json(dom, schema)) {
270 auto*
const properties = detail::findMember(dom,
"properties");
271 if (properties ==
nullptr) {
274 glz::generic_u64::array_t decorated{};
275 for (
const auto& entry : _fields) {
276 auto*
const property = detail::findMember(*properties, entry.field);
277 if (property ==
nullptr) {
280 if (entry.decimalPlaces.has_value()) {
281 (*property)[
"x-decimalPlaces"] =
282 static_cast<std::uint64_t
>(math::detail::clampWireDecimalPlaces(*entry.decimalPlaces));
284 if (entry.minimum.has_value()) {
285 (*property)[
"x-minimum"] = detail::boundNode(*entry.minimum);
287 if (entry.maximum.has_value()) {
288 (*property)[
"x-maximum"] = detail::boundNode(*entry.maximum);
290 decorated.emplace_back(entry.field);
292 if (decorated.empty()) {
295 dom[
"x-instanceConstraints"] = decorated;
297 return glz::write_json(dom).value_or(schema);
313 [[nodiscard]] std::vector<ConstraintViolation>
checkValue(std::string_view field,
315 std::vector<ConstraintViolation> violations{};
316 const auto* entry =
forField(field);
317 if (entry ==
nullptr) {
320 if (entry->minimum.has_value() && (value <=> *entry->minimum) == std::strong_ordering::less) {
323 if (entry->maximum.has_value() && (value <=> *entry->maximum) == std::strong_ordering::greater) {
326 if (entry->decimalPlaces.has_value() && !detail::exactAtDecimals(value, *entry->
decimalPlaces)) {
345 template <
typename A>
346 [[nodiscard]] std::vector<ConstraintViolation>
checkAction(
const A& action)
const {
347 using Plain = std::remove_cvref_t<A>;
348 std::vector<ConstraintViolation> violations{};
349 if constexpr (glz::reflectable<Plain> || glz::glaze_object_t<Plain>) {
350 forms::detail::forEachNamedMember(action, [&]<std::size_t I>(std::string_view name,
const auto& member) {
351 static_cast<void>(I);
352 using Member = std::remove_cvref_t<
decltype(member)>;
354 if (member.hasValue()) {
355 for (
auto& violation :
checkValue(name, *member)) {
356 violations.push_back(std::move(violation));
360 static_cast<void>(name);
361 static_cast<void>(member);
365 static_cast<void>(action);
371 std::vector<FieldConstraint> _fields{};
std::string_view violationKindName(ConstraintViolationKind kind) noexcept
A stable, machine-readable name for a violation kind — for log lines and model-level error messages.
std::string instanceSchemaJson(const InstanceConstraints &constraints)
schemaJson<A>() decorated with constraints — the served form for one instance.
ConstraintViolationKind
Which framework-meaningful key a submitted value fell foul of.
@ BelowMinimum
The value is strictly below the declared inclusive minimum.
@ AboveMaximum
The value is strictly above the declared inclusive maximum.
@ PrecisionExceeded
The value is not exactly representable at the declared decimalPlaces — it carries more precision than...
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.
Exact rational number with a runtime decimal-precision tag.
DecimalPlaces decimalPlaces
Decimal-precision tag. Invariant: 0 <= value <= kMaxDecimalPlaces.