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