MayaFlux 1.0.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 "Serializer.hpp"
4
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 and std::unordered_map / std::map
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> or std::unordered_map
50 * @param indent Spaces per indentation level (-1 for compact).
51 */
52 template <typename T>
53 [[nodiscard]] std::string encode(const T& value, int indent = 2)
54 {
55 return to_json(value).dump(indent);
56 }
57
58 /**
59 * @brief Encode @p value and write to @p path (created or truncated).
60 * @return True on success. On failure call last_error().
61 */
62 template <typename T>
63 [[nodiscard]] bool write(const std::string& path, const T& value, int indent = 2)
64 {
65 m_last_error.clear();
66 const auto resolved = resolve_write_path(path);
67 std::ofstream file(resolved, std::ios::out | std::ios::trunc);
68 if (!file.is_open()) {
69 m_last_error = "Failed to open for writing: " + resolved;
70 return false;
71 }
72 file << encode(value, indent);
73 if (!file.good()) {
74 m_last_error = "Write failed: " + resolved;
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 = 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 [[nodiscard]] static std::string resolve_path(const std::string& filepath)
139 {
140 namespace fs = std::filesystem;
141 auto normalized = std::string(filepath);
142 std::ranges::replace(normalized, '\\', '/');
143 if (fs::path(normalized).is_absolute())
144 return normalized;
145 if (fs::exists(normalized))
146 return normalized;
147 auto from_cwd = fs::current_path() / normalized;
148 if (fs::exists(from_cwd))
149 return from_cwd.string();
150 auto from_root = fs::path(Config::project_root()) / normalized;
151 if (fs::exists(from_root))
152 return from_root.string();
153 return normalized;
154 }
155
156 // -----------------------------------------------------------------------
157 // Encoding engine
158 // -----------------------------------------------------------------------
159
160 template <typename T>
161 static nlohmann::json to_json(const T& val)
162 {
163 if constexpr (Reflect::Reflectable<T>) {
164 nlohmann::json j = nlohmann::json::object();
165 std::apply(
166 [&](const auto&... props) {
167 (encode_property(j, val, props), ...);
168 },
169 T::describe());
170 return j;
171 } else if constexpr (Reflect::is_optional_v<T>) {
172 if (!val.has_value()) {
173 return nullptr;
174 }
175 return to_json(*val);
176 } else if constexpr (Reflect::is_vector_v<T>) {
177 auto arr = nlohmann::json::array();
178 for (const auto& item : val) {
179 arr.push_back(to_json(item));
180 }
181 return arr;
182 } else if constexpr (Reflect::is_string_map_v<T>) {
183 nlohmann::json j = nlohmann::json::object();
184 for (const auto& [k, v] : val) {
185 j[k] = to_json(v);
186 }
187 return j;
188 } else if constexpr (std::is_enum_v<T>) {
189 return static_cast<std::underlying_type_t<T>>(val);
190 } else if constexpr (std::is_same_v<T, nlohmann::json>) {
191 return val;
192 } else {
193 /// Extension point — specialize Serializer<T> to handle custom types
194 return Serializer<T>::to_json(val);
195 }
196 }
197
198 template <typename Class, typename T>
199 static void encode_property(
200 nlohmann::json& j,
201 const Class& obj,
202 const Reflect::Property<Class, T>& prop)
203 {
204 j[prop.key] = to_json(obj.*prop.member);
205 }
206
207 template <typename Class, typename T>
208 static void encode_property(
209 nlohmann::json& j,
210 const Class& obj,
212 {
213 const auto& opt = obj.*prop.member;
214 if (opt.has_value()) {
215 j[prop.key] = to_json(*opt);
216 }
217 }
218
219 // -----------------------------------------------------------------------
220 // Decoding engine
221 // -----------------------------------------------------------------------
222
223 template <typename T>
224 static void from_json(const nlohmann::json& j, T& out)
225 {
226 if constexpr (Reflect::Reflectable<T>) {
227 if (!j.is_object()) {
228 throw nlohmann::json::type_error::create(302, "expected object for Reflectable type", &j);
229 }
230 std::apply(
231 [&](const auto&... props) {
232 (decode_property(j, out, props), ...);
233 },
234 T::describe());
235 } else if constexpr (Reflect::is_optional_v<T>) {
236 using Inner = typename Reflect::is_optional<T>::inner;
237 if (j.is_null()) {
238 out = std::nullopt;
239 } else {
240 Inner inner {};
241 from_json(j, inner);
242 out = std::move(inner);
243 }
244 } else if constexpr (Reflect::is_vector_v<T>) {
245 using V = typename Reflect::is_vector<T>::element;
246 if (!j.is_array())
247 throw nlohmann::json::type_error::create(302, "expected array", &j);
248 out.clear();
249 out.reserve(j.size());
250 for (const auto& item : j) {
251 V element {};
252 from_json(item, element);
253 out.push_back(std::move(element));
254 }
255
256 } else if constexpr (Reflect::is_string_map_v<T>) {
257 using V = typename Reflect::is_string_map<T>::element;
258 if (!j.is_object())
259 throw nlohmann::json::type_error::create(302, "expected object for map", &j);
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 (std::is_enum_v<T>) {
267 out = static_cast<T>(j.get<std::underlying_type_t<T>>());
268
269 } else if constexpr (std::is_same_v<T, nlohmann::json>) {
270 out = j;
271
272 } else {
274 }
275 }
276
277 template <typename Class, typename T>
278 static void decode_property(
279 const nlohmann::json& j,
280 Class& obj,
281 const Reflect::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,
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} // namespace MayaFlux::IO
std::vector< float > * out
glm::vec3 value
std::string encode(const T &value, int indent=2)
Serialize value to a JSON string.
JSONSerializer & operator=(const JSONSerializer &)=delete
static void encode_property(nlohmann::json &j, const Class &obj, const Reflect::OptionalProperty< Class, T > &prop)
std::optional< T > read(const std::string &path)
Read path and deserialize into T.
JSONSerializer(const JSONSerializer &)=delete
static void from_json(const nlohmann::json &j, T &out)
std::optional< T > decode(const std::string &str)
Parse str and deserialize into T.
const std::string & last_error() const
Last error message, empty if no error.
static void encode_property(nlohmann::json &j, const Class &obj, const Reflect::Property< Class, T > &prop)
static std::string resolve_path(const std::string &filepath)
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 Reflect::Property< Class, T > &prop)
static void decode_property(const nlohmann::json &j, Class &obj, const Reflect::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::string resolve_write_path(const std::string &filepath, std::string_view default_directory=Config::OUT_DIR)
Resolve a writable path against the configured output directory.
Extension point for JSONSerializer.
std::optional< T > Class::* member
Definition Mirror.hpp:28
Binds a string key to a std::optional<T> member pointer.
Definition Mirror.hpp:26
std::string_view key
Definition Mirror.hpp:15
Binds a string key to a required member pointer.
Definition Mirror.hpp:14