Gaia-ECS v1.0.0
A simple and powerful entity component system
Loading...
Searching...
No Matches
world_json_patch.h
1#pragma once
2#include "gaia/config/config.h"
3
4#if GAIA_JSON_ENABLED
5
6 #include <cstdint>
7 #include <cstring>
8
9namespace gaia {
10 namespace ecs {
12 namespace detail {
17 inline bool runtime_patch_decode_token(ser::json_str_view encoded, util::str& token) {
18 token.clear();
19
20 GAIA_FOR(encoded.size()) {
21 const auto ch = encoded.data()[i];
22 if (ch != '~') {
23 token.append(ch);
24 continue;
25 }
26
27 if (i + 1 >= encoded.size())
28 return false;
29
30 const auto escaped = encoded.data()[++i];
31 if (escaped == '0')
32 token.append('~');
33 else if (escaped == '1')
34 token.append('/');
35 else
36 return false;
37 }
38
39 return true;
40 }
41
46 inline bool runtime_patch_parse_index(util::str_view token, uint32_t& index) {
47 if (token.empty())
48 return false;
49
50 uint64_t value = 0;
51 GAIA_FOR(token.size()) {
52 const auto ch = token.data()[i];
53 if (ch < '0' || ch > '9')
54 return false;
55 value = value * 10 + (uint32_t)(ch - '0');
56 if (value > UINT32_MAX)
57 return false;
58 }
59
60 index = (uint32_t)value;
61 return true;
62 }
63
65 struct RuntimeJsonPointerTarget final {
67 const ComponentCacheItem* pType = nullptr;
69 const RuntimeFieldDesc* pField = nullptr;
71 bool endsAtField = false;
73 ser::JsonDiagReason reason = ser::JsonDiagReason::None;
75 const char* message = nullptr;
76 };
77
86 inline bool walk_runtime_json_pointer(
87 const ComponentCache& cache, const ComponentCacheItem& root, ComponentCursor& cursor,
88 ser::json_str_view pointer, bool forWrite, RuntimeJsonPointerTarget& target) {
89 auto fail = [&](ser::JsonDiagReason reason, const char* message) {
90 target.reason = reason;
91 target.message = message;
92 return false;
93 };
94
95 if (!pointer.empty() && pointer.data()[0] != '/')
96 return fail(ser::JsonDiagReason::InvalidPatchPath, "Component path must be an RFC 6901 JSON Pointer.");
97 if (!pointer.empty() && (pointer.size() == 1 || pointer.data()[pointer.size() - 1] == '/'))
98 return fail(ser::JsonDiagReason::InvalidPatchPath, "Component path contains an empty token.");
99
100 target.pType = &root;
101 uint32_t pos = pointer.empty() ? pointer.size() : 1;
102 while (pos < pointer.size()) {
103 uint32_t end = pos;
104 while (end < pointer.size() && pointer.data()[end] != '/')
105 ++end;
106
107 util::str token;
108 if (!runtime_patch_decode_token(ser::json_str_view(pointer.data() + pos, end - pos), token) || token.empty())
109 return fail(ser::JsonDiagReason::InvalidPatchPath, "Component path contains an invalid token.");
110
111 const ComponentCacheItem* pFieldOwner = target.pType;
112 if (pFieldOwner != nullptr && pFieldOwner->typeKind == RuntimeTypeKind::Opaque)
113 pFieldOwner = cache.find(pFieldOwner->opaque_as_type());
114
115 if (pFieldOwner != nullptr && pFieldOwner->typeKind == RuntimeTypeKind::Struct) {
116 const auto* pField = pFieldOwner->field(util::str_view(token.data(), token.size()));
117 if (pField == nullptr)
118 return fail(ser::JsonDiagReason::InvalidPatchPath, "Component path field does not exist.");
119 if (forWrite && (pField->flags & RuntimeFieldFlag_ReadOnly) != 0)
120 return fail(ser::JsonDiagReason::ReadOnlyField, "Component path field is read-only.");
121 if (forWrite && (pField->flags & RuntimeFieldFlag_Hidden) != 0)
122 return fail(ser::JsonDiagReason::HiddenField, "Component path field is hidden.");
123 if (!cursor.field(util::str_view(token.data(), token.size())))
124 return fail(ser::JsonDiagReason::InvalidPatchPath, "Component path field cannot be traversed.");
125
126 target.pField = pField;
127 target.endsAtField = true;
128 target.pType = cache.find(pField->type);
129 } else {
130 uint32_t index = 0;
131 if (!runtime_patch_parse_index(util::str_view(token.data(), token.size()), index) || !cursor.elem(index))
132 return fail(ser::JsonDiagReason::InvalidPatchPath, "Component path sequence index is invalid.");
133
134 target.endsAtField = false;
135 target.pType = cache.find(cursor.type());
136 }
137
138 pos = end + 1;
139 }
140
141 return true;
142 }
143
144 } // namespace detail
146
158 inline bool component_to_json_at(
159 const ComponentCacheItem& item, const void* pComponentData, ser::json_str_view pointer, ser::ser_json& writer,
160 ser::JsonDiagnostics& diagnostics, const ser::RuntimeJsonPolicy& policy = {}) {
161 auto error = [&](ser::JsonDiagReason reason, const char* message) {
162 diagnostics.add(ser::JsonDiagSeverity::Error, reason, pointer, message);
163 return false;
164 };
165
166 const auto* pCache = item.owner_cache();
167 if (pComponentData == nullptr || pCache == nullptr || item.comp.size() == 0)
168 return error(ser::JsonDiagReason::MissingComponentStorage, "Component value is unavailable.");
169
170 auto cursor = ComponentCursor::from_raw(
171 *pCache, item.entity, ComponentRawView{pComponentData, item.comp.size(), ComponentRawViewFlag_Valid});
172 detail::RuntimeJsonPointerTarget target;
173 if (!detail::walk_runtime_json_pointer(*pCache, item, cursor, pointer, false, target))
174 return error(target.reason, target.message);
175
176 // The cursor resolves sequence elements and opaque values as a patch sees them.
177 const auto* pData = (const uint8_t*)cursor.ptr();
178 const auto size = cursor.size();
179 if (pData == nullptr || size == 0)
180 return error(ser::JsonDiagReason::UnsupportedPatchValue, "Component path endpoint cannot be read.");
181
182 bool ok = false;
183 if (target.endsAtField) {
184 // A field keeps its own layout: element count and string encoding.
185 detail::RuntimeJsonFieldLayout layout{};
186 if (!detail::resolve_runtime_json_field_layout(pCache, *target.pField, layout) ||
187 (uint64_t)layout.elemSize * layout.elemCount != size)
188 return error(ser::JsonDiagReason::UnsupportedPatchValue, "Component path field layout is invalid.");
189 ok = detail::write_runtime_json_field_data(pCache, *target.pField, layout, pData, writer, policy, 0);
190 } else {
191 ok = detail::write_runtime_json_value(pCache, target.pType, cursor.type(), pData, size, writer, policy, 0);
192 }
193 if (!ok)
194 return error(ser::JsonDiagReason::UnsupportedPatchValue, "Component path endpoint cannot be written as JSON.");
195 return true;
196 }
197
198 inline bool World::patch_comp_json(
199 Entity entity, Entity component, ser::json_str_view pointer, ser::json_str_view value,
200 ser::JsonDiagnostics& diagnostics, const ser::RuntimeJsonPolicy& policy, uint64_t expectedRuntimeSchemaHash) {
201 auto error = [&](ser::JsonDiagReason reason, const char* message) {
202 diagnostics.add(ser::JsonDiagSeverity::Error, reason, pointer, message);
203 return false;
204 };
205
206 if (expectedRuntimeSchemaHash != 0 && expectedRuntimeSchemaHash != runtime_schema_hash())
207 return error(ser::JsonDiagReason::StaleSchema, "Runtime schema hash does not match the current manifest.");
208
209 const auto* pRoot = component.pair() ? m_compCache.find_pair_payload(component) : m_compCache.find(component);
210 if (pRoot == nullptr)
211 return error(ser::JsonDiagReason::UnknownComponent, "Component patch target is not registered.");
212
213 auto cursor = cursor_mut(entity, component);
214 if (!cursor.valid() || cursor.size() == 0)
215 return error(ser::JsonDiagReason::MissingComponentStorage, "Component patch payload is unavailable.");
216
217 detail::RuntimeJsonPointerTarget target;
218 if (!detail::walk_runtime_json_pointer(m_compCache, *pRoot, cursor, pointer, true, target))
219 return error(target.reason, target.message);
220
221 const ComponentCacheItem* pCurrent = target.pType;
222 const RuntimeFieldDesc* pSelectedField = target.pField;
223
224 const auto count = cursor.count();
225 const bool charBuffer = pSelectedField != nullptr && cursor.type() == Char8 && count.ok() && count.value > 1;
226
227 if (pCurrent == nullptr || !detail::runtime_json_leaf_editable(*pCurrent) ||
228 (count.ok() && count.value > 1 && !charBuffer))
229 return error(
230 ser::JsonDiagReason::UnsupportedPatchValue, "Component patch endpoint must be a supported reflected leaf.");
231
232 // Work on a copy so a failed patch does not change the component.
233 cnt::darray<uint8_t> original;
234 original.resize(cursor.size());
235 if (!cursor.get_raw(original.data(), (uint32_t)original.size()))
236 return error(ser::JsonDiagReason::UnsupportedPatchValue, "Component patch endpoint cannot be read.");
237
238 cnt::darray<uint8_t> bytes;
239 bytes.resize(original.size());
240 memcpy(bytes.data(), original.data(), original.size());
241
242 ser::ser_json reader(value.data(), value.size());
243 bool valueOk = true;
244 detail::RuntimeJsonReadContext ctx{&m_compCache, reader, diagnostics, policy, valueOk};
245
246 if (cursor.type() == Char8 && cursor.size() == sizeof(char)) {
247 // One Char8 value is written as a one-character JSON string.
248 ser::json_str_view text;
249 if (!reader.parse_string_view(text) || text.size() != 1)
250 return error(ser::JsonDiagReason::InvalidJson, "Component patch character value must contain one character.");
251
252 bytes[0] = (uint8_t)text.data()[0];
253 } else if (!detail::read_runtime_json_value(
254 ctx, pCurrent, cursor.type(), bytes.data(), (uint32_t)bytes.size(), pointer, 0)) {
255 return error(
256 ser::JsonDiagReason::InvalidJson, "Component patch value is not valid JSON for the selected field.");
257 }
258
259 reader.ws();
260 if (!reader.eof())
261 return error(ser::JsonDiagReason::InvalidJson, "Component patch value contains trailing JSON.");
262
263 if (!valueOk)
264 return error(
265 ser::JsonDiagReason::UnsupportedPatchValue,
266 "Component patch value is incompatible with the selected field.");
267
268 if (pSelectedField != nullptr &&
269 (pSelectedField->flags & (RuntimeFieldFlag_HasMinimum | RuntimeFieldFlag_HasMaximum)) != 0) {
270 double number = 0.0;
271 if (!detail::runtime_patch_numeric_value(pCurrent, cursor.type(), bytes.data(), cursor.size(), number))
272 return error(
273 ser::JsonDiagReason::UnsupportedPatchValue, "Component patch range applies to a non-numeric field.");
274
275 if (((pSelectedField->flags & RuntimeFieldFlag_HasMinimum) != 0 && number < pSelectedField->minimum) ||
276 ((pSelectedField->flags & RuntimeFieldFlag_HasMaximum) != 0 && number > pSelectedField->maximum))
277 return error(
278 ser::JsonDiagReason::RangeViolation, "Component patch value is outside the authored field range.");
279 }
280
281 if (!cursor.set_raw(bytes.data(), (uint32_t)bytes.size())) {
282 // The write may be incomplete, so put the original value back.
283 (void)cursor.set_raw(original.data(), (uint32_t)original.size(), false);
284 return error(
285 ser::JsonDiagReason::UnsupportedPatchValue, "Component patch could not commit the selected field.");
286 }
287
288 return true;
289 }
290
291 } // namespace ecs
292} // namespace gaia
293
294#endif
GAIA_NODISCARD ComponentCursor cursor(Entity entity, Entity component) const
Creates a read-only cursor over a runtime component on entity. Inherited ids resolve like get_raw()....
Definition world.h:15096
GAIA_NODISCARD ComponentCursor cursor_mut(Entity entity, Entity component)
Creates a mutable cursor over a directly owned runtime component on entity. Direct writes through mut...
Definition world.h:15112
GAIA_NODISCARD Entity target(Entity entity, Entity relation) const
Returns the first relationship target for the relation entity on entity.
Definition world.h:8788
GAIA_NODISCARD uint32_t bytes(const T &data)
Calculates how many bytes data would need when serialized via ser::save. Useful when a destination st...
Definition ser_ct.h:88
GAIA_NODISCARD CursorResult< uint32_t > get_raw(void *data, uint32_t byteCount) const noexcept
Copies exact bytes from the current cursor position.
Definition component_cursor.h:798
static GAIA_NODISCARD ComponentCursor from_raw(const ComponentCache &components, Entity component, ComponentRawView view)
Creates a read-only cursor from a raw component view.
Definition component_cursor.h:198
GAIA_NODISCARD uint32_t size() const noexcept
Returns the current payload or field size in bytes.
Definition component_cursor.h:317
GAIA_NODISCARD bool valid() const noexcept
Definition component_cursor.h:280
GAIA_NODISCARD Entity type() const noexcept
Definition component_cursor.h:290
GAIA_NODISCARD CursorResult< uint32_t > count() const noexcept
Returns the element count for the current fixed or adapted sequence scope.
Definition component_cursor.h:347
CursorResult< void > set_raw(const void *data, uint32_t byteCount) noexcept
Writes exact bytes to the current cursor position.
Definition component_cursor.h:828
GAIA_NODISCARD constexpr auto value() const noexcept
Raw identifier value.
Definition id.h:325