morph 0.1.0
Asynchronous bridge between GUI thread and business models
Loading...
Searching...
No Matches
result_walker.hpp
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
2
3#pragma once
4
5/// @file core/detail/result_walker.hpp
6/// @brief Where an action's result holds which reflected types.
7///
8/// Every action registration walks its result type once and stores, under the
9/// action id, each reflected aggregate the result can hold and the JSON path to
10/// it. The walk needs no record to be registered: it reports every aggregate,
11/// and matching those types against the record registry happens when that
12/// registry is first read.
13
14#include <algorithm>
15#include <cstddef>
16#include <functional>
17#include <glaze/glaze.hpp>
18#include <map>
19#include <optional>
20#include <string>
21#include <string_view>
22#include <type_traits>
23#include <typeindex>
24#include <utility>
25#include <vector>
26
28
29namespace morph::model::detail {
30
31/// @brief One place an action's result holds a reflected aggregate.
32struct ResultSlot {
33 /// The aggregate's C++ type.
34 std::type_index type;
35 /// The JSON path to it: member names joined with `.`, empty for the result itself.
36 /// A `std::vector`, `std::optional` or `std::map` value adds no segment.
37 std::string path;
38};
39
40/// @brief What one action id's registration recorded about its result.
41///
42/// The action type is kept beside the slots because an action id is unique per
43/// model, not per process: two action types may register under one id. The
44/// first registration's slots are kept, and a later registration with a
45/// different action type is listed in `actionResultConflicts()` instead.
46struct ActionResult {
47 /// The action type that registered under the id first.
48 std::type_index action;
49 /// The places that action's result holds a reflected aggregate.
50 std::vector<ResultSlot> resultSlots;
51};
52
53/// @brief Every action's result, by action id.
54using ActionResultSlotMap = std::map<std::string, ActionResult, std::less<>>;
55
56namespace result_walker {
57
58/// @brief The value type a container the walker looks through holds; `void` for any other type.
59/// @tparam T The type being walked.
60template <typename T>
61struct Element {
62 /// The contained value type.
63 using type = void;
64};
65template <typename T, typename Alloc>
66struct Element<std::vector<T, Alloc>> {
67 /// The contained value type.
68 using type = T;
69};
70template <typename T>
71struct Element<std::optional<T>> {
72 /// The contained value type.
73 using type = T;
74};
75template <typename K, typename V, typename Compare, typename Alloc>
76struct Element<std::map<K, V, Compare, Alloc>> {
77 /// The contained value type.
78 using type = V;
79};
80
81/// @brief Whether a type's name places it in an anonymous namespace.
82///
83/// Glaze reads a pure-reflection type's member names through an
84/// external-linkage object, which a type without linkage cannot have, so such a
85/// type cannot be reflected at all.
86/// @tparam T A class type.
87/// @return True when the type, or a template argument of it, has no linkage.
88template <typename T>
89consteval bool hasAnonymousName() {
90 constexpr std::string_view name = glz::type_name<T>;
91 return name.contains("(anonymous namespace)") || name.contains("{anonymous}") ||
92 name.contains("`anonymous namespace'") || name.contains("`anonymous-namespace'");
93}
94
95/// @brief A type whose members the walker visits: one glaze writes as an object, member by member.
96///
97/// A `glz::meta` that replaces the codec (`custom_read`/`custom_write`, or a
98/// `value` mapping) leaves the JSON shape unknown, so such a type is skipped. A
99/// `glz::meta` object mapping, renamed members included, is visited.
100template <typename T>
101concept Object = std::is_class_v<T> && !glz::custom_read<T> && !glz::custom_write<T> &&
102 (glz::glaze_object_t<T> || (glz::reflectable<T> && !hasAnonymousName<T>()));
103
104/// @brief Whether walking a type can yield a slot, so members that cannot are not walked at all.
105/// @tparam T The type being walked.
106/// @return True for an `Object`, or a container the walker looks through whose value type can.
107template <typename T>
108consteval bool mayHoldSlots() {
109 if constexpr (!std::is_void_v<typename Element<T>::type>) {
110 return mayHoldSlots<std::remove_cvref_t<typename Element<T>::type>>();
111 } else {
112 return Object<T>;
113 }
114}
115
116/// @brief The state of one walk.
117struct Walk {
118 /// The slots found so far.
119 std::vector<ResultSlot> found;
120 /// The aggregates on the current path, which end the walk where they recur.
121 std::vector<std::type_index> onPath;
122};
123
124template <typename T>
125void visit(Walk& walk, std::string const& path);
126
127/// @brief Visits each member of an `Object` that can yield a slot.
128/// @tparam T The aggregate.
129/// @tparam I Its reflected member indices.
130/// @param walk The walk's state.
131/// @param path The aggregate's own path.
132template <typename T, std::size_t... I>
133void visitMembers(Walk& walk, std::string const& path, std::index_sequence<I...> /*indices*/) {
134 // Unused for an aggregate with no members: the fold below is then empty.
135 [[maybe_unused]] auto const member = [&]<std::size_t Index>() {
136 using Member = std::remove_cvref_t<decltype(morph::detail::reflectedMember<Index>(std::declval<T&>()))>;
137 if constexpr (mayHoldSlots<Member>()) {
138 constexpr std::string_view key = glz::reflect<T>::keys[Index];
139 result_walker::visit<Member>(walk, path.empty() ? std::string{key} : path + "." + std::string{key});
140 }
141 };
142 (member.template operator()<I>(), ...);
143}
144
145/// @brief Visits a value of type @p T at @p path.
146/// @tparam T The type being walked.
147/// @param walk The walk's state.
148/// @param path Where the value sits in the result.
149template <typename T>
150void visit(Walk& walk, std::string const& path) {
151 if constexpr (!std::is_void_v<typename Element<T>::type>) {
152 result_walker::visit<std::remove_cvref_t<typename Element<T>::type>>(walk, path);
153 } else if constexpr (Object<T>) {
154 std::type_index const self{typeid(T)};
155 if (std::ranges::find(walk.onPath, self) != walk.onPath.end()) {
156 return;
157 }
158 walk.found.push_back(ResultSlot{.type = self, .path = path});
159 walk.onPath.push_back(self);
160 visitMembers<T>(walk, path, std::make_index_sequence<glz::reflect<T>::size>{});
161 walk.onPath.pop_back();
162 }
163}
164
165} // namespace result_walker
166
167/// @brief Every reflected aggregate a value of type @p R can hold, and the path to each.
168///
169/// The result itself, a `std::vector`, `std::optional` or `std::map` value of a
170/// visited type, and the members of a reflected aggregate are visited,
171/// depth-first. Member names are glaze's reflected keys, which are the names
172/// the codec writes. `void`, scalars, strings, types whose `glz::meta` replaces
173/// the codec, and pure-reflection types in an anonymous namespace are skipped.
174/// The walk stops at a type already on its path, so recursive types end.
175/// @tparam R The result type, `void` included.
176/// @return The slots in visiting order.
177template <typename R>
178[[nodiscard]] std::vector<ResultSlot> walkResult() {
179 result_walker::Walk walk;
180 if constexpr (!std::is_void_v<R>) {
181 if constexpr (result_walker::mayHoldSlots<std::remove_cvref_t<R>>()) {
182 result_walker::visit<std::remove_cvref_t<R>>(walk, std::string{});
183 }
184 }
185 return std::move(walk.found);
186}
187
188/// @brief The process-level map of action result slots, filled by static registrars.
189/// @return The mutable map.
190inline ActionResultSlotMap& mutableActionResultSlots() {
191 static ActionResultSlotMap entries;
192 return entries;
193}
194
195/// @brief Every registered action's result slots, by action id.
196/// @return The map, unmatched against records.
197inline ActionResultSlotMap const& actionResultSlots() { return mutableActionResultSlots(); }
198
199/// @brief The process-level list of action ids registered by more than one action type.
200/// @return The mutable list.
201inline std::vector<std::string>& mutableActionResultConflicts() {
202 static std::vector<std::string> ids;
203 return ids;
204}
205
206/// @brief The action ids that a second, different action type registered under.
207///
208/// Each id appears once. Its entry in `actionResultSlots()` holds the first
209/// type's slots; matching refuses the id, since which result a cached query
210/// holds would depend on link order.
211/// @return The ids in the order the conflicts were found.
212inline std::vector<std::string> const& actionResultConflicts() { return mutableActionResultConflicts(); }
213
214} // namespace morph::model::detail
AnyHandle map(AnyHandle const &source, std::string const &name, std::type_identity_t< std::function< AnyHandle(Handle< E > const &)> > const &body)
A comprehension over a list, {"map": e, "as": "x", "to": e}.
Definition author.hpp:1171
constexpr Self self
The dialog or drawer a command list runs in.
Definition author.hpp:1295
Value walk(Value const &value, std::span< std::string const > path)
Reads a path out of a value: a record's field by name, a list's element by index.
@ Object
A nested object.
The value glaze reads and writes under a reflected key.