docs
Loading...
Searching...
No Matches
Value.h
Go to the documentation of this file.
1///
2/// Copyright (C) 2026 Ultralight, Inc. All rights reserved.
3/// A license is required for commercial use. https://ultralig.ht
4///
5#pragma once
6#include <Ultralight/CAPI/CAPI_DOMData.h>
9
10#include <cstdint>
11#include <optional>
12#include <string>
13#include <string_view>
14#include <type_traits>
15#include <utility>
16#include <vector>
17
18namespace ultralight {
19namespace dom {
20namespace data {
21
22/// \cond INTERNAL
23namespace detail {
24template <typename T>
25inline constexpr bool kNoValueConversion = false;
26}
27/// \endcond
28
29///
30/// Get a Result's value or a default-constructed T if it holds an error (see dom::OrEmpty()).
31///
33
34///
35/// A generic value passed to data-binding formatters and handlers.
36///
37/// A Value represents incoming data in formatters and in action and change handlers that take a
38/// generic parameter instead of a concrete C++ type.
39///
40/// DefineFormat() passes the bound field to your formatter as a Value:
41///
42/// ```
43/// // Formats 12500 as "12,500".
44/// ctx.DefineFormat("comma", [](dd::Value v) {
45/// std::string digits = std::to_string(v.Or(int64_t(0)));
46/// for (int i = int(digits.size()) - 3; i > 0; i -= 3)
47/// digits.insert(i, ",");
48/// return digits;
49/// });
50/// ```
51///
52/// ## What a Value Holds
53///
54/// An instance represents one of several data shapes:
55///
56/// - **An empty state holds no data.** Actions dispatched without a payload arrive empty, including
57/// every action fired from markup.
58/// - **A leaf holds a single value.** Formatters, change handlers, and scalar action payloads
59/// provide one boolean, numeric, or string value.
60/// - **An object holds named members.** Actions dispatched with a payload struct provide fields
61/// that you look up by name.
62///
63/// Brackets look up an object's members by name:
64///
65/// ```
66/// binding.OnAction<"moveItem">([&](dd::Value request) {
67/// int from = request["from"].Or(0);
68/// int to = request["to"].Or(0);
69/// bag.MoveItem(from, to);
70/// });
71/// ```
72///
73/// ## Converting Values
74///
75/// Choose the extraction method that matches your error-handling style:
76///
77/// - **To() returns a Result.** Failure yields a TypeError explaining the mismatch.
78/// - **Maybe() returns an optional.** Failure yields `std::nullopt` without an error.
79/// - **Or() returns a fallback.** Failure returns the default value you supplied.
80///
81/// Conversions target bool, integer types, floating-point types, String, std::string, and reflected
82/// enums. Integer values convert to floating-point types, but floating-point values don't convert
83/// to integers. Reflected enums convert from string values using their enumerator names.
84///
85/// Indexing a missing member or an empty Value returns an empty Value. Because conversions fail
86/// safely on empty instances, you don't need to check Contains() or IsEmpty() before reading a
87/// member with Or() or Maybe().
88///
89/// @note dom::data::Value and js::Value are unrelated. A dom::data::Value holds an owned copy of
90/// native data rather than a handle into a page's JavaScript context.
91///
92/// @see dom::data::Binding::OnAction(), dom::data::Binding::OnChange(),
93/// dom::data::Context::DefineFormat(), dom::data::ActionInfo
94///
95class Value {
96 public:
97 ///
98 /// Create an empty Value.
99 ///
100 Value() = default;
101
102 ///
103 /// Whether or not this Value holds nothing.
104 ///
105 bool IsEmpty() const { return state_ == State::kEmpty; }
106
107 ///
108 /// Whether or not this Value holds something (the opposite of IsEmpty()).
109 ///
110 explicit operator bool() const { return !IsEmpty(); }
111
112 ///
113 /// Whether or not this Value is a boolean leaf.
114 ///
115 bool IsBoolean() const { return state_ == State::kLeaf && kind_ == ValueKind::Bool; }
116
117 ///
118 /// Whether or not this Value is an integer leaf.
119 ///
120 bool IsInt64() const { return state_ == State::kLeaf && kind_ == ValueKind::Int64; }
121
122 ///
123 /// Whether or not this Value is a floating-point leaf.
124 ///
125 bool IsDouble() const { return state_ == State::kLeaf && kind_ == ValueKind::Double; }
126
127 ///
128 /// Whether or not this Value is a string leaf.
129 ///
130 bool IsString() const { return state_ == State::kLeaf && kind_ == ValueKind::String; }
131
132 ///
133 /// Whether or not this Value is an object (the members of an action's payload struct).
134 ///
135 bool IsObject() const { return state_ == State::kObject; }
136
137 ///
138 /// Get a member of an object.
139 ///
140 /// @param name The member's name.
141 ///
142 /// @return Returns a copy of the member's value or an empty Value if this Value isn't an
143 /// object or has no such member.
144 ///
145 Value operator[](const char* name) const {
146 if (state_ == State::kObject && name) {
147 for (const auto& member : members_) {
148 if (member.first == name)
149 return member.second;
150 }
151 }
152 return Value();
153 }
154
155 ///
156 /// Whether or not this Value is an object with a member of the given name.
157 ///
158 /// @param name The member's name.
159 ///
160 bool Contains(const char* name) const {
161 if (state_ != State::kObject || !name)
162 return false;
163 for (const auto& member : members_) {
164 if (member.first == name)
165 return true;
166 }
167 return false;
168 }
169
170 ///
171 /// Get the number of members (0 unless this Value is an object).
172 ///
173 size_t member_count() const { return members_.size(); }
174
175 ///
176 /// Convert to a C++ type (see "Converting Values" in the class description).
177 ///
178 /// @return Returns the converted value. Fails with a TypeError if this Value is empty or
179 /// isn't a leaf of the requested kind.
180 ///
181 template <typename T>
182 [[nodiscard]] Result<T> To() const {
183 std::optional<T> value = Maybe<T>();
184 if (value)
185 return std::move(*value);
186 return Unexpected<Error>(Error::Make(ErrorType::TypeError, MismatchMessage<T>()));
187 }
188
189 ///
190 /// Convert to a C++ type like To() but without the failure reason.
191 ///
192 /// @return Returns the converted value or nullopt on any failure.
193 ///
194 template <typename T>
195 [[nodiscard]] std::optional<T> Maybe() const {
196 static_assert(!std::is_same_v<std::remove_cvref_t<T>, std::string_view>
197 && !std::is_same_v<std::remove_cvref_t<T>, const char*>,
198 "a Value conversion must produce an OWNING type; read strings as "
199 "String or std::string");
200 using Plain = std::remove_cvref_t<T>;
201 if (state_ != State::kLeaf)
202 return std::nullopt;
203 if constexpr (std::is_same_v<Plain, bool>) {
204 if (kind_ == ValueKind::Bool)
205 return bool_;
206 } else if constexpr (std::is_integral_v<Plain>) {
207 if (kind_ == ValueKind::Int64)
208 return static_cast<Plain>(int_);
209 } else if constexpr (std::is_floating_point_v<Plain>) {
210 if (kind_ == ValueKind::Double)
211 return static_cast<Plain>(double_);
212 if (kind_ == ValueKind::Int64)
213 return static_cast<Plain>(int_);
214 } else if constexpr (std::is_same_v<Plain, ultralight::String>) {
215 if (kind_ == ValueKind::String)
216 return string_;
217 } else if constexpr (std::is_same_v<Plain, std::string>) {
218 if (kind_ == ValueKind::String) {
219 const ultralight::String8& utf8 = const_cast<ultralight::String&>(string_).utf8();
220 return std::string(utf8.data(), utf8.length());
221 }
222#if ULTRALIGHT_REFLECTION
223 } else if constexpr (std::is_enum_v<Plain>) {
224 if (kind_ == ValueKind::String) {
225 const ultralight::String8& utf8 = const_cast<ultralight::String&>(string_).utf8();
226 const std::string_view name(utf8.data(), utf8.length());
227 for (const auto& entry : detail::EnumInfo<Plain>::kEntries) {
228 if (entry.name == name)
229 return static_cast<Plain>(entry.value);
230 }
231 }
232#endif
233 } else {
234 static_assert(detail::kNoValueConversion<Plain>,
235 "unsupported Value conversion target: expected bool, an integral or "
236 "floating-point type, String, std::string, or a reflected enum");
237 }
238 return std::nullopt;
239 }
240
241 ///
242 /// Convert to a C++ type like To() but with a fallback.
243 ///
244 /// @param fallback The value to return if the conversion fails.
245 ///
246 /// @return Returns the converted value or `fallback` on any failure.
247 ///
248 template <typename T>
249 [[nodiscard]] T Or(T fallback) const {
250 std::optional<T> value = Maybe<T>();
251 return value ? std::move(*value) : std::move(fallback);
252 }
253
254 ///
255 /// Convert to a std::string like To() but with a string-literal fallback.
256 ///
257 /// @param fallback The text to return if the conversion fails (nullptr returns an empty
258 /// string).
259 ///
260 /// @return Returns the converted string or `fallback` on any failure.
261 ///
262 [[nodiscard]] std::string Or(const char* fallback) const {
263 std::optional<std::string> value = Maybe<std::string>();
264 return value ? std::move(*value) : std::string(fallback ? fallback : "");
265 }
266
267 // --- Interop with the C API (most embedders never touch raw handles) -------------------
268
269 ///
270 /// Create a leaf Value from a C leaf value.
271 ///
272 /// You only need this when you receive values through the C API (eg, in a change callback
273 /// you set with ulDOMDataBindingSetChangeCallback()).
274 ///
275 /// @param value The C value. Its string is copied (exactly `string_length` bytes).
276 ///
277 /// @return Returns the leaf Value. A color, a StyleValue, or a kind that isn't a leaf gives
278 /// an empty Value.
279 ///
280 static Value FromLeaf(const ULDOMDataLeafValue& value) {
281 Value result;
282 switch (static_cast<ValueKind>(value.kind)) {
283 case ValueKind::Bool:
284 result.state_ = State::kLeaf;
285 result.kind_ = ValueKind::Bool;
286 result.bool_ = value.bool_value;
287 break;
288 case ValueKind::Int64:
289 result.state_ = State::kLeaf;
290 result.kind_ = ValueKind::Int64;
291 result.int_ = value.int64_value;
292 break;
294 result.state_ = State::kLeaf;
295 result.kind_ = ValueKind::Double;
296 result.double_ = value.double_value;
297 break;
298 case ValueKind::String: {
299 result.state_ = State::kLeaf;
300 result.kind_ = ValueKind::String;
301 result.string_ = value.string_value
302 ? ultralight::String(value.string_value, value.string_length)
304 break;
305 }
306 default:
307 // Non-leaf kinds, and the StyleValue/Color leaves, have no Value form; the
308 // result stays empty (typed delivery hands those kinds over in their own types).
309 break;
310 }
311 return result;
312 }
313
314 ///
315 /// Create an object Value from a C action payload.
316 ///
317 /// @param payload The payload's members (members with a null name are skipped).
318 ///
319 /// @param payload_count The number of members in `payload`.
320 ///
321 /// @return Returns the object Value or an empty Value if `payload` is nullptr or
322 /// `payload_count` is 0.
323 ///
324 /// @note A scalar payload arrives as one member named `value`, so this gives an object with
325 /// that one member (the typed handlers deliver a leaf instead).
326 ///
327 static Value FromPayload(const ULDOMDataPayloadEntry* payload, size_t payload_count) {
328 Value result;
329 if (!payload || payload_count == 0)
330 return result;
331 result.state_ = State::kObject;
332 result.members_.reserve(payload_count);
333 for (size_t i = 0; i < payload_count; i++) {
334 if (!payload[i].name)
335 continue;
336 result.members_.emplace_back(payload[i].name, FromLeaf(payload[i].value));
337 }
338 return result;
339 }
340
341 private:
342 enum class State : uint8_t { kEmpty, kLeaf, kObject };
343
344 template <typename T>
345 const char* MismatchMessage() const {
346 if (IsEmpty())
347 return "cannot convert an empty Value";
348 if constexpr (std::is_same_v<std::remove_cvref_t<T>, bool>)
349 return "expected a boolean Value";
350 else if constexpr (std::is_integral_v<std::remove_cvref_t<T>>)
351 return "expected an integer Value";
352 else if constexpr (std::is_floating_point_v<std::remove_cvref_t<T>>)
353 return "expected a numeric Value";
354 else
355 return "expected a string Value";
356 }
357
358 State state_ = State::kEmpty;
360 bool bool_ = false;
361 int64_t int_ = 0;
362 double double_ = 0;
363 ultralight::String string_;
364 std::vector<std::pair<std::string, Value>> members_;
365};
366
367} // namespace data
368} // namespace dom
369} // namespace ultralight
A null-terminated UTF-8 string container.
Definition String8.h:17
char * data()
Get raw UTF-8 data.
Definition String8.h:53
size_t length() const
Get length in bytes (not including null terminator).
Definition String8.h:59
Unicode string container with conversions for UTF-8, UTF-16, and UTF-32.
Definition String.h:31
static Error Make(ErrorType type, std::string message)
Create a native error.
Definition Error.h:151
static Value FromLeaf(const ULDOMDataLeafValue &value)
Create a leaf Value from a C leaf value.
Definition Value.h:280
std::string Or(const char *fallback) const
Convert to a std::string like To() but with a string-literal fallback.
Definition Value.h:262
Value operator[](const char *name) const
Get a member of an object.
Definition Value.h:145
bool IsInt64() const
Whether or not this Value is an integer leaf.
Definition Value.h:120
bool IsDouble() const
Whether or not this Value is a floating-point leaf.
Definition Value.h:125
T Or(T fallback) const
Convert to a C++ type like To() but with a fallback.
Definition Value.h:249
bool Contains(const char *name) const
Whether or not this Value is an object with a member of the given name.
Definition Value.h:160
bool IsObject() const
Whether or not this Value is an object (the members of an action's payload struct).
Definition Value.h:135
bool IsString() const
Whether or not this Value is a string leaf.
Definition Value.h:130
size_t member_count() const
Get the number of members (0 unless this Value is an object).
Definition Value.h:173
bool IsEmpty() const
Whether or not this Value holds nothing.
Definition Value.h:105
Result< T > To() const
Convert to a C++ type (see "Converting Values" in the class description).
Definition Value.h:182
Value()=default
Create an empty Value.
bool IsBoolean() const
Whether or not this Value is a boolean leaf.
Definition Value.h:115
static Value FromPayload(const ULDOMDataPayloadEntry *payload, size_t payload_count)
Create an object Value from a C action payload.
Definition Value.h:327
std::optional< T > Maybe() const
Convert to a C++ type like To() but without the failure reason.
Definition Value.h:195
Data-binding API that connects native C++ data to HTML and CSS markup.
Definition ActionInfo.h:13
ValueKind
The kinds of value a schema entry holds.
Definition ValueTraits.h:27
@ String
UTF-8 text (from strings and enums).
Definition ValueTraits.h:31
@ Bool
A boolean.
Definition ValueTraits.h:28
@ Double
A double-precision float (from any floating-point type).
Definition ValueTraits.h:30
@ Int64
A signed 64-bit integer (from any integer type).
Definition ValueTraits.h:29
Direct C++ access to modify page elements and handle events.
T OrEmpty(Result< T > result)
Get a Result's value, or a default-constructed T if it holds an error.
Definition Error.h:320
@ TypeError
An argument has the wrong type (eg, a submitter passed to HTMLFormElement::requestSubmit() that isn't...
Definition Error.h:54
Expected< T, Error > Result
The result of a DOM operation that can fail (either a T or a dom::Error).
Definition Error.h:277
Root namespace for every public Ultralight type, function, and enumeration.