MayaFlux 0.4.0
Digital-First Multimedia Processing Framework
Loading...
Searching...
No Matches
JSONSerializer.hpp
Go to the documentation of this file.
1#pragma once
2
3#include "Reflection.hpp"
4
5#include "FileReader.hpp"
6
8
9#include <nlohmann/json.hpp>
10
11#include "fstream"
12
13namespace MayaFlux::IO {
14
15/**
16 * @class JSONSerializer
17 * @brief Converts arbitrary C++ types to/from JSON strings and disk files.
18 *
19 * Encoding and decoding are driven by the Reflectable concept: any type that
20 * provides a static constexpr describe() returning a tuple of Property /
21 * OptionalProperty descriptors is handled recursively. Types without a
22 * describe() fall through to nlohmann's native converters (arithmetic,
23 * std::string, bool) or to the built-in dispatchers for std::vector,
24 * std::optional, std::unordered_map / std::map, and glm vec/mat types.
25 *
26 * Callers never touch nlohmann::json directly. JSONSerializer owns all
27 * knowledge of the wire format.
28 *
29 * File operations are built on top of the in-memory encode / decode pair.
30 * last_error() is set on any failure; it is cleared at the start of every
31 * fallible call.
32 */
33class MAYAFLUX_API JSONSerializer {
34public:
35 JSONSerializer() = default;
36
41
42 // -----------------------------------------------------------------------
43 // Encode
44 // -----------------------------------------------------------------------
45
46 /**
47 * @brief Serialize @p value to a JSON string.
48 * @tparam T Any Reflectable struct, std::vector<Reflectable>, arithmetic
49 * type, std::string, std::optional<T>, std::unordered_map, or
50 * glm vec/mat type.
51 * @param indent Spaces per indentation level (-1 for compact).
52 */
53 template <typename T>
54 [[nodiscard]] std::string encode(const T& value, int indent = 2)
55 {
56 return to_json(value).dump(indent);
57 }
58
59 /**
60 * @brief Encode @p value and write to @p path (created or truncated).
61 * @return True on success. On failure call last_error().
62 */
63 template <typename T>
64 [[nodiscard]] bool write(const std::string& path, const T& value, int indent = 2)
65 {
66 m_last_error.clear();
67 std::ofstream file(path, std::ios::out | std::ios::trunc);
68 if (!file.is_open()) {
69 m_last_error = "Failed to open for writing: " + path;
70 return false;
71 }
72 file << encode(value, indent);
73 if (!file.good()) {
74 m_last_error = "Write failed: " + path;
75 return false;
76 }
77 return true;
78 }
79
80 // -----------------------------------------------------------------------
81 // Decode
82 // -----------------------------------------------------------------------
83
84 /**
85 * @brief Parse @p str and deserialize into T.
86 * @return Populated instance, or nullopt on any parse or structural error.
87 * On failure call last_error().
88 */
89 template <typename T>
90 [[nodiscard]] std::optional<T> decode(const std::string& str)
91 {
92 m_last_error.clear();
93 try {
94 auto j = nlohmann::json::parse(str);
95 T out {};
96 from_json(j, out);
97 return out;
98 } catch (const std::exception& e) {
99 m_last_error = std::string("decode error: ") + e.what();
100 return std::nullopt;
101 }
102 }
103
104 /**
105 * @brief Read @p path and deserialize into T.
106 * @return Populated instance, or nullopt on file or parse error.
107 * On failure call last_error().
108 */
109 template <typename T>
110 [[nodiscard]] std::optional<T> read(const std::string& path)
111 {
112 m_last_error.clear();
113 const auto resolved = FileReader::resolve_path(path);
114 std::ifstream file(resolved);
115 if (!file.is_open()) {
116 m_last_error = "Failed to open for reading: " + resolved;
117 return std::nullopt;
118 }
119 try {
120 auto j = nlohmann::json::parse(file);
121 T out {};
122 from_json(j, out);
123 return out;
124 } catch (const std::exception& e) {
125 m_last_error = std::string("read error in ") + resolved + ": " + e.what();
126 return std::nullopt;
127 }
128 }
129
130 /**
131 * @brief Last error message, empty if no error.
132 */
133 [[nodiscard]] const std::string& last_error() const { return m_last_error; }
134
135private:
136 std::string m_last_error;
137
138 // -----------------------------------------------------------------------
139 // Encoding engine
140 // -----------------------------------------------------------------------
141
142 template <typename T>
143 static nlohmann::json to_json(const T& val)
144 {
145 if constexpr (Reflectable<T>) {
146 nlohmann::json j = nlohmann::json::object();
147 std::apply(
148 [&](const auto&... props) {
149 (encode_property(j, val, props), ...);
150 },
151 T::describe());
152 return j;
153 } else if constexpr (is_optional_v<T>) {
154 if (!val.has_value()) {
155 return nullptr;
156 }
157 return to_json(*val);
158 } else if constexpr (is_vector_v<T>) {
159 auto arr = nlohmann::json::array();
160 for (const auto& item : val) {
161 arr.push_back(to_json(item));
162 }
163 return arr;
164 } else if constexpr (is_string_map_v<T>) {
165 nlohmann::json j = nlohmann::json::object();
166 for (const auto& [k, v] : val) {
167 j[k] = to_json(v);
168 }
169 return j;
170 } else if constexpr (GlmSerializable<T>) {
171 return encode_glm(val);
172 } else if constexpr (std::is_enum_v<T>) {
173 return static_cast<std::underlying_type_t<T>>(val);
174 } else if constexpr (std::is_same_v<T, nlohmann::json>) {
175 return val;
176 } else {
177 return val;
178 }
179 }
180
181 template <typename Class, typename T>
182 static void encode_property(
183 nlohmann::json& j,
184 const Class& obj,
185 const Property<Class, T>& prop)
186 {
187 j[prop.key] = to_json(obj.*prop.member);
188 }
189
190 template <typename Class, typename T>
191 static void encode_property(
192 nlohmann::json& j,
193 const Class& obj,
194 const OptionalProperty<Class, T>& prop)
195 {
196 const auto& opt = obj.*prop.member;
197 if (opt.has_value()) {
198 j[prop.key] = to_json(*opt);
199 }
200 }
201
202 // -----------------------------------------------------------------------
203 // Decoding engine
204 // -----------------------------------------------------------------------
205
206 template <typename T>
207 static void from_json(const nlohmann::json& j, T& out)
208 {
209 if constexpr (Reflectable<T>) {
210 if (!j.is_object()) {
211 auto e = nlohmann::json::type_error::create(
212 302, "expected object for Reflectable type", &j);
213 error<std::runtime_error>(
214 Journal::Component::IO,
215 Journal::Context::Runtime,
216 std::source_location::current(),
217 "JSON type error: {}", e.what());
218 }
219 std::apply(
220 [&](const auto&... props) {
221 (decode_property(j, out, props), ...);
222 },
223 T::describe());
224 } else if constexpr (is_optional_v<T>) {
225 using Inner = typename is_optional<T>::inner;
226 if (j.is_null()) {
227 out = std::nullopt;
228 } else {
229 Inner inner {};
230 from_json(j, inner);
231 out = std::move(inner);
232 }
233 } else if constexpr (is_vector_v<T>) {
234 using V = typename is_vector<T>::element;
235 if (!j.is_array()) {
236 auto e = nlohmann::json::type_error::create(302, "expected array", &j);
237 error<std::runtime_error>(
238 Journal::Component::IO,
239 Journal::Context::Runtime,
240 std::source_location::current(),
241 "JSON type error: {}", e.what());
242 }
243 out.clear();
244 out.reserve(j.size());
245 for (const auto& item : j) {
246 V element {};
247 from_json(item, element);
248 out.push_back(std::move(element));
249 }
250 } else if constexpr (is_string_map_v<T>) {
251 using V = typename is_string_map<T>::element;
252 if (!j.is_object()) {
253 auto e = nlohmann::json::type_error::create(302, "expected object for map", &j);
254 error<std::runtime_error>(
255 Journal::Component::IO,
256 Journal::Context::Runtime,
257 std::source_location::current(),
258 "JSON type error: {}", e.what());
259 }
260 out.clear();
261 for (const auto& [k, v] : j.items()) {
262 V val {};
263 from_json(v, val);
264 out.emplace(k, std::move(val));
265 }
266 } else if constexpr (GlmSerializable<T>) {
267 decode_glm(j, out);
268 } else if constexpr (std::is_enum_v<T>) {
269 out = static_cast<T>(j.get<std::underlying_type_t<T>>());
270 } else if constexpr (std::is_same_v<T, nlohmann::json>) {
271 out = j;
272 } else {
273 out = j.get<T>();
274 }
275 }
276
277 template <typename Class, typename T>
278 static void decode_property(
279 const nlohmann::json& j,
280 Class& obj,
281 const Property<Class, T>& prop)
282 {
283 if (j.contains(prop.key)) {
284 from_json(j.at(prop.key), obj.*prop.member);
285 }
286 }
287
288 template <typename Class, typename T>
289 static void decode_property(
290 const nlohmann::json& j,
291 Class& obj,
292 const OptionalProperty<Class, T>& prop)
293 {
294 if (!j.contains(prop.key) || j.at(prop.key).is_null()) {
295 obj.*prop.member = std::nullopt;
296 } else {
297 T inner {};
298 from_json(j.at(prop.key), inner);
299 obj.*prop.member = std::move(inner);
300 }
301 }
302
303 // -----------------------------------------------------------------------
304 // GLM encode / decode
305 // -----------------------------------------------------------------------
306
307 template <typename T>
308 requires GlmSerializable<T>
309 static nlohmann::json encode_glm(const T& v)
310 {
311 constexpr auto n = glm_component_count<T>();
312 using Comp = glm_component_type<T>;
313 auto arr = nlohmann::json::array();
314 const Comp* ptr = &v[0];
315 for (size_t i = 0; i < n; ++i) {
316 arr.push_back(ptr[i]);
317 }
318 return arr;
319 }
320
321 template <typename T>
322 requires GlmSerializable<T>
323 static void decode_glm(const nlohmann::json& j, T& out)
324 {
325 if (!j.is_array()) {
326 auto e = nlohmann::json::type_error::create(302, "expected array for glm type", &j);
327 error<std::runtime_error>(
328 Journal::Component::IO,
329 Journal::Context::Runtime,
330 std::source_location::current(),
331 "JSON type error: {}", e.what());
332 }
333 constexpr auto n = glm_component_count<T>();
334 if (j.size() != n) {
335 auto e = nlohmann::json::other_error::create(
336 501, "glm component count mismatch", &j);
337 error<std::runtime_error>(
338 Journal::Component::IO,
339 Journal::Context::Runtime,
340 std::source_location::current(),
341 "JSON error: expected {} components for type {}, got {}: {}",
342 n, typeid(T).name(), j.size(), e.what());
343 }
344 using Comp = glm_component_type<T>;
345 Comp* ptr = &out[0];
346 for (size_t i = 0; i < n; ++i) {
347 ptr[i] = j[i].get<Comp>();
348 }
349 }
350};
351
352} // namespace MayaFlux::IO
const uint8_t * ptr
std::string encode(const T &value, int indent=2)
Serialize value to a JSON string.
static nlohmann::json encode_glm(const T &v)
JSONSerializer & operator=(const JSONSerializer &)=delete
std::optional< T > read(const std::string &path)
Read path and deserialize into T.
JSONSerializer(const JSONSerializer &)=delete
static void encode_property(nlohmann::json &j, const Class &obj, const OptionalProperty< Class, T > &prop)
static void from_json(const nlohmann::json &j, T &out)
std::optional< T > decode(const std::string &str)
Parse str and deserialize into T.
static void encode_property(nlohmann::json &j, const Class &obj, const Property< Class, T > &prop)
static void decode_glm(const nlohmann::json &j, T &out)
const std::string & last_error() const
Last error message, empty if no error.
static void decode_property(const nlohmann::json &j, Class &obj, const Property< Class, T > &prop)
bool write(const std::string &path, const T &value, int indent=2)
Encode value and write to path (created or truncated).
JSONSerializer(JSONSerializer &&)=default
static void decode_property(const nlohmann::json &j, Class &obj, const OptionalProperty< Class, T > &prop)
static nlohmann::json to_json(const T &val)
JSONSerializer & operator=(JSONSerializer &&)=default
Converts arbitrary C++ types to/from JSON strings and disk files.
std::optional< T > Class::* member
Binds a string key to a std::optional<T> member pointer.
std::string_view key
Binds a string key to a required member pointer.