21#include <glaze/glaze.hpp>
27#include <unordered_map>
31#include "../core/registry.hpp"
34namespace morph::views {
67 std::span<const BindEntry>
bind{};
86template <
typename Action>
88 ActionScope scope = ActionScope::Row,
89 std::span<const BindEntry> bind = {},
bool confirm =
false) {
90 return ActionDescriptor{
128inline constexpr bool isStdVector =
false;
131template <
typename T,
typename Alloc>
132inline constexpr bool isStdVector<std::vector<T, Alloc>> =
true;
135template <
typename Kind>
136struct ViewKindNameImpl;
140 static constexpr std::string_view value =
"collection";
144struct ViewKindNameImpl<MasterDetailView> {
145 static constexpr std::string_view value =
"master-detail";
149template <
typename Kind>
150inline constexpr std::string_view viewKindName = ViewKindNameImpl<Kind>::value;
154concept HasViewTitle =
requires { V::title; };
158concept HasRowKey =
requires { V::rowKey; };
162concept HasColumnOverrides =
requires {
163 std::begin(V::columns);
164 std::end(V::columns);
169concept HasRowActionDescriptor =
requires { V::rowAction; };
173concept HasViewActions =
requires {
174 std::begin(V::actions);
175 std::end(V::actions);
185[[nodiscard]]
inline glz::generic_u64
const* resolveSchemaRef(glz::generic_u64
const& rowDom,
186 glz::generic_u64
const& prop) {
187 using ::morph::forms::detail::findMember;
188 auto const* node = &
prop;
189 if (
auto const*
const anyOf = findMember(prop,
"anyOf"); anyOf !=
nullptr && anyOf->is_array()) {
190 for (
auto const& alternative : anyOf->get_array()) {
191 auto const*
const type = findMember(alternative,
"type");
192 if (type ==
nullptr || !type->is_string() || type->get_string() !=
"null") {
198 auto const*
const ref = findMember(*node,
"$ref");
199 if (ref ==
nullptr || !
ref->is_string()) {
202 auto const& refText =
ref->get_string();
203 auto const defName = refText.substr(refText.find_last_of(
'/') + 1);
204 auto const*
const defs = findMember(rowDom,
"$defs");
205 auto const*
const def = (defs ==
nullptr) ?
nullptr : findMember(*defs, defName);
206 return def ==
nullptr ? node : def;
212[[nodiscard]]
inline std::string schemaType(glz::generic_u64
const& node) {
213 auto const*
const type = ::morph::forms::detail::findMember(node,
"type");
214 if (type ==
nullptr) {
217 if (type->is_string()) {
218 return type->get_string();
220 if (type->is_array()) {
221 for (
auto const& alternative : type->get_array()) {
222 if (alternative.is_string() && alternative.get_string() !=
"null") {
223 return alternative.get_string();
234[[nodiscard]]
inline std::string schemaText(glz::generic_u64
const& node, std::string_view key) {
235 auto const*
const member = ::morph::forms::detail::findMember(node, key);
236 return (member !=
nullptr && member->is_string()) ? member->get_string() : std::string{};
251[[nodiscard]]
inline std::string_view inferColumnKind(glz::generic_u64
const& rowDom, glz::generic_u64
const& prop) {
252 using ::morph::forms::detail::findMember;
253 if (findMember(prop,
"x-comparator") !=
nullptr) {
256 auto const& node = *resolveSchemaRef(rowDom, prop);
257 auto const*
const properties = findMember(node,
"properties");
258 if (properties !=
nullptr && findMember(*properties,
"num") !=
nullptr &&
259 findMember(*properties,
"den") !=
nullptr) {
260 bool const units = findMember(prop,
"ExtUnits") !=
nullptr || findMember(node,
"ExtUnits") !=
nullptr;
261 return units ?
"quantity" :
"decimal";
263 auto const type = schemaType(node);
264 auto const widget = schemaText(prop,
"x-widget");
265 auto const format = schemaText(node,
"format");
266 if (type ==
"integer") {
267 if (widget ==
"epochDays") {
270 return widget ==
"epochSeconds" ?
"dateTime" :
"integer";
272 if (type ==
"number") {
275 if (type ==
"boolean") {
278 if (format ==
"date") {
281 if (format ==
"date-time") {
297[[nodiscard]]
inline glz::generic_u64 buildColumnEntry(glz::generic_u64
const& rowDom, std::string
const& field,
298 std::string
const& label,
bool hidden) {
299 glz::generic_u64 entry{};
300 entry[
"field"] = field;
301 entry[
"label"] = label;
303 entry[
"v-hidden"] =
true;
315 auto const& propsObj = rowDom[
"properties"].get_object();
316 auto iter = propsObj.find(field);
317 if (iter == propsObj.end()) {
320 auto const&
prop = iter->second;
321 if (
auto const*
const decimals = ::morph::forms::detail::findMember(prop,
"x-decimalPlaces")) {
329 entry[
"x-decimalPlaces"] = *decimals;
344 entry[
"kind"] = std::string{inferColumnKind(rowDom, prop)};
345 for (std::string_view
const key : {
"title",
"x-unitAlternatives",
"x-comparator"}) {
346 if (
auto const*
const value = ::morph::forms::detail::findMember(prop, key)) {
348 entry[std::string{key}] = *value;
351 auto const&
resolved = *resolveSchemaRef(rowDom, prop);
352 if (
auto const*
const values = ::morph::forms::detail::findMember(resolved,
"enum")) {
354 entry[
"enum"] = *values;
355 }
else if (
auto const*
const oneOf = ::morph::forms::detail::findMember(resolved,
"oneOf");
357 glz::generic_u64::array_t constants;
358 for (
auto const& option :
oneOf->get_array()) {
359 if (
auto const*
const constant = ::morph::forms::detail::findMember(option,
"const")) {
360 constants.push_back(*constant);
363 if (!constants.empty()) {
365 entry[
"enum"] = constants;
368 if (
auto const*
const units = ::morph::forms::detail::findMember(prop,
"ExtUnits")) {
371 entry[
"ExtUnits"] = *units;
372 }
else if (
auto const*
const defUnits = ::morph::forms::detail::findMember(resolved,
"ExtUnits")) {
375 entry[
"ExtUnits"] = *defUnits;
389template <
typename V,
typename Row>
390[[nodiscard]] std::string deriveColumns() {
391 glz::generic_u64 rowDom{};
412 if (glz::read_json(rowDom, ::morph::forms::schemaJson<Row>()) || !rowDom.contains(
"properties")) {
415 glz::generic_u64::array_t columns{};
416 if constexpr (HasColumnOverrides<V>) {
417 for (
auto const& colOverride : V::columns) {
419 colOverride.label.empty() ? std::string{colOverride.field} : std::string{colOverride.label};
420 columns.emplace_back(buildColumnEntry(rowDom, std::string{colOverride.field}, label, colOverride.hidden));
425 auto const& propsObj = rowDom[
"properties"].get_object();
426 std::vector<std::pair<std::string, std::uint64_t>> ordered;
427 ordered.reserve(propsObj.size());
428 for (
auto const& [name, prop] : propsObj) {
431 if (
auto const*
const hiddenNode = ::morph::forms::detail::findMember(prop,
"x-hidden");
432 hiddenNode !=
nullptr && hiddenNode->is_boolean() && hiddenNode->get_boolean()) {
435 auto const*
const orderNode = ::morph::forms::detail::findMember(prop,
"x-order");
436 auto const order = (orderNode ==
nullptr) ? 0 : orderNode->template get<std::uint64_t>();
437 ordered.emplace_back(name, order);
439 std::ranges::sort(ordered, [](
auto const& a,
auto const& b) {
return a.second < b.second; });
440 for (
auto const& [name, order] : ordered) {
441 static_cast<void>(order);
442 columns.emplace_back(buildColumnEntry(rowDom, name, name,
false));
445 return glz::write_json(columns).value_or(
"[]");
455[[nodiscard]]
inline glz::generic_u64 buildActionNode(ActionDescriptor
const& descriptor,
bool includeButtonFields) {
456 glz::generic_u64 node{};
457 node[
"action"] = std::string{descriptor.actionTypeId};
458 if (includeButtonFields) {
459 node[
"label"] = std::string{descriptor.label.empty() ? descriptor.actionTypeId : descriptor.label};
460 node[
"scope"] = std::string{descriptor.scope == ActionScope::Row ?
"row" :
"collection"};
461 if (descriptor.confirm) {
462 node[
"confirm"] =
true;
465 if (!descriptor.bind.empty()) {
466 glz::generic_u64 bindNode{};
467 for (
auto const& entry : descriptor.bind) {
468 bindNode[std::string{entry.actionField}] = std::string{entry.rowField};
470 node[
"bind"] = bindNode;
480[[nodiscard]] std::string buildViewSchema() {
481 using Query =
typename V::query;
482 using Result = typename ::morph::model::ActionTraits<Query>::Result;
484 glz::generic_u64 dom{};
485 dom[
"v-kind"] = std::string{viewKindName<typename V::kind>};
486 dom[
"v-query"] = std::string{::morph::model::ActionTraits<Query>::typeId()};
487 if constexpr (HasViewTitle<V>) {
488 dom[
"v-title"] = std::string{V::title};
490 dom[
"v-title"] = std::string{::morph::model::ActionTraits<Query>::typeId()};
492 if constexpr (HasRowKey<V>) {
493 dom[
"v-rowKey"] = std::string{V::rowKey};
495 dom[
"v-rowKey"] = std::string{
"id"};
501 std::string columnsJson =
"[]";
502 if constexpr (isStdVector<Result>) {
503 using Row =
typename std::remove_cvref_t<Result>::value_type;
504 columnsJson = deriveColumns<V, Row>();
508 ::morph::forms::detail::forEachNamedMember(probe,
509 [&]<std::size_t I>(std::string_view name,
const auto& member) {
510 static_cast<void>(name);
511 static_cast<void>(I);
512 using Member = std::remove_cvref_t<
decltype(member)>;
513 if constexpr (isStdVector<Member>) {
515 using Row =
typename Member::value_type;
516 columnsJson = deriveColumns<V, Row>();
522 glz::generic_u64 columnsDom{};
523 if (!glz::read_json(columnsDom, columnsJson)) {
524 dom[
"v-columns"] = columnsDom;
527 if constexpr (HasRowActionDescriptor<V>) {
528 dom[
"v-rowAction"] = buildActionNode(V::rowAction,
false);
530 if constexpr (HasViewActions<V>) {
531 glz::generic_u64::array_t actionsArray{};
532 for (
auto const& descriptor : V::actions) {
533 actionsArray.emplace_back(buildActionNode(descriptor,
true));
535 dom[
"v-actions"] = actionsArray;
538 return glz::write_json(dom).value_or(std::string{});
561 static const std::string cached = detail::buildViewSchema<V>();
584 template <
typename V>
589 _providers.insert_or_assign(std::string{viewId}, []() ->
const std::string& {
return viewSchemaJson<V>(); });
601 [[nodiscard]]
const std::string&
schemaJson(std::string_view viewId)
const {
602 auto iter = _providers.find(std::string{viewId});
603 if (iter == _providers.end()) {
604 throw std::runtime_error(
"unknown view: " + std::string{viewId});
606 return iter->second();
611 [[nodiscard]] std::vector<std::string>
viewIds()
const {
612 std::vector<std::string> ids;
613 ids.reserve(_providers.size());
614 for (
auto const& entry : _providers) {
615 ids.push_back(entry.first);
628 std::unordered_map<std::string, std::function<
const std::string&()>> _providers;
639inline bool registerViewOnce(std::string_view viewId)
noexcept {
664#define BRIDGE_REGISTER_VIEW(V, NAME) \
666 struct morph::views::ViewTraits<V> { \
667 static constexpr std::string_view typeId() noexcept { return NAME; } \
670 [[maybe_unused]] const bool bridge_view_reg_##V = morph::views::detail::registerViewOnce<V>(NAME); \
Handle< T > ref(std::string const &path)
A named read, {"ref": "name.path"}: a param, state, let, query value, element value or bound name.
Handle< std::string > format(AnyHandle const &pattern, std::vector< AnyHandle > args)
format: a template with {0}, {1}, … placeholders.
AnyHandle prop(std::string const &name)
A prop of a component, inside the component's fallback.
Process-level registry mapping a view's string type-id to its viewSchemaJson<V>() provider,...
void registerView(std::string_view viewId)
Registers V's schema provider under viewId. A second registration for the same viewId silently replac...
std::vector< std::string > viewIds() const
Returns every registered view id, in unspecified order.
const std::string & schemaJson(std::string_view viewId) const
Returns the cached viewSchemaJson<V>() for viewId.
static ViewRegistry & instance()
Returns the process-level singleton registry.
@ Row
On every row; the body is built from the row's bind entries.
Resolved const & resolved()
The process registries, matched and validated on the first call.
constexpr bool oneOf(std::array< std::string_view, N > const &names, std::string_view name) noexcept
Whether a name is one of a closed set.
@ Result
{"result": "m"}: a mutation's last result.
Traits specialisation that maps an action type to its id, JSON codec, and result type.
One action a view screen can fire — the row opener (v-rowAction) or an entry of v-actions.
std::span< const BindEntry > bind
Field-name mapping applied to prefill the target action's draft from the activating row....
ActionScope scope
Row button vs. collection-wide button.
std::string_view actionTypeId
Registered type id of the target action.
std::string_view label
Button label. Empty means "use the action type id as-is".
bool confirm
Whether a renderer must confirm before firing this action.
Maps one target-action wire field to a row wire field, so a row's current value prefills the action's...
std::string_view rowField
Wire field name on the query action's row type.
std::string_view actionField
Wire field name on the target action.
Tag type: V::kind selects a plain list/table screen.
Declare-to-override entry for one derived column of a collection view.
std::string_view field
Wire field name on the query action's row type.
bool hidden
Column present in the row model but not displayed.
std::string_view label
Column header. Empty means "use `field` as-is".
Tag type: V::kind selects a list + inline/side editor screen. Introduces no JSON keys beyond Collecti...
Traits specialisation that maps a view descriptor type to its string type-id. Specialise via BRIDGE_R...
consteval ActionDescriptor describeAction(std::string_view label={}, ActionScope scope=ActionScope::Row, std::span< const BindEntry > bind={}, bool confirm=false)
Builds an ActionDescriptor for Action, resolving its wire type id from the real, registered ActionTra...
ActionScope
Where a view action button appears: opened per row, or once for the whole collection.
const std::string & viewSchemaJson()
Generates the view-schema JSON document for view descriptor V.