MayaFlux 0.5.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
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 and std::unordered_map / std::map
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> or std::unordered_map
48 * @param indent Spaces per indentation level (-1 for compact).
49 */
50 template <typename T>
51 [[nodiscard]] std::string encode(const T& value, int indent = 2)
52 {
53 return to_json(value).dump(indent);
54 }
55
56 /**
57 * @brief Encode @p value and write to @p path (created or truncated).
58 * @return True on success. On failure call last_error().
59 */
60 template <typename T>
61 [[nodiscard]] bool write(const std::string& path, const T& value, int indent = 2)
62 {
63 m_last_error.clear();
64 std::ofstream file(path, std::ios::out | std::ios::trunc);
65 if (!file.is_open()) {
66 m_last_error = "Failed to open for writing: " + path;
67 return false;
68 }
69 file << encode(value, indent);
70 if (!file.good()) {
71 m_last_error = "Write failed: " + path;
72 return false;
73 }
74 return true;
75 }
76
77 // -----------------------------------------------------------------------
78 // Decode
79 // -----------------------------------------------------------------------
80
81 /**
82 * @brief Parse @p str and deserialize into T.
83 * @return Populated instance, or nullopt on any parse or structural error.
84 * On failure call last_error().
85 */
86 template <typename T>
87 [[nodiscard]] std::optional<T> decode(const std::string& str)
88 {
89 m_last_error.clear();
90 try {
91 auto j = nlohmann::json::parse(str);
92 T out {};
93 from_json(j, out);
94 return out;
95 } catch (const std::exception& e) {
96 m_last_error = std::string("decode error: ") + e.what();
97 return std::nullopt;
98 }
99 }
100
101 /**
102 * @brief Read @p path and deserialize into T.
103 * @return Populated instance, or nullopt on file or parse error.
104 * On failure call last_error().
105 */
106 template <typename T>
107 [[nodiscard]] std::optional<T> read(const std::string& path)
108 {
109 m_last_error.clear();
110 const auto resolved = resolve_path(path);
111 std::ifstream file(resolved);
112 if (!file.is_open()) {
113 m_last_error = "Failed to open for reading: " + resolved;
114 return std::nullopt;
115 }
116 try {
117 auto j = nlohmann::json::parse(file);
118 T out {};
119 from_json(j, out);
120 return out;
121 } catch (const std::exception& e) {
122 m_last_error = std::string("read error in ") + resolved + ": " + e.what();
123 return std::nullopt;
124 }
125 }
126
127 /**
128 * @brief Last error message, empty if no error.
129 */
130 [[nodiscard]] const std::string& last_error() const { return m_last_error; }
131
132private:
133 std::string m_last_error;
134
135 [[nodiscard]] static std::string resolve_path(const std::string& filepath)
136 {
137 namespace fs = std::filesystem;
138 auto normalized = std::string(filepath);
139 std::ranges::replace(normalized, '\\', '/');
140 if (fs::path(normalized).is_absolute())
141 return normalized;
142 if (fs::exists(normalized))
143 return normalized;
144 auto from_cwd = fs::current_path() / normalized;
145 if (fs::exists(from_cwd))
146 return from_cwd.string();
147 auto from_root = fs::path(Config::SOURCE_DIR) / normalized;
148 if (fs::exists(from_root))
149 return from_root.string();
150 return normalized;
151 }
152
153 // -----------------------------------------------------------------------
154 // Encoding engine
155 // -----------------------------------------------------------------------
156
157 template <typename T>
158 static nlohmann::json to_json(const T& val)
159 {
160 if constexpr (Reflect::Reflectable<T>) {
161 nlohmann::json j = nlohmann::json::object();
162 std::apply(
163 [&](const auto&... props) {
164 (encode_property(j, val, props), ...);
165 },
166 T::describe());
167 return j;
168 } else if constexpr (Reflect::is_optional_v<T>) {
169 if (!val.has_value()) {
170 return nullptr;
171 }
172 return to_json(*val);
173 } else if constexpr (Reflect::is_vector_v<T>) {
174 auto arr = nlohmann::json::array();
175 for (const auto& item : val) {
176 arr.push_back(to_json(item));
177 }
178 return arr;
179 } else if constexpr (Reflect::is_string_map_v<T>) {
180 nlohmann::json j = nlohmann::json::object();
181 for (const auto& [k, v] : val) {
182 j[k] = to_json(v);
183 }
184 return j;
185 } else if constexpr (std::is_enum_v<T>) {
186 return static_cast<std::underlying_type_t<T>>(val);
187 } else if constexpr (std::is_same_v<T, nlohmann::json>) {
188 return val;
189 } else {
190 /// Extension point — specialize Serializer<T> to handle custom types
191 return Serializer<T>::to_json(val);
192 }
193 }
194
195 template <typename Class, typename T>
196 static void encode_property(
197 nlohmann::json& j,
198 const Class& obj,
199 const Reflect::Property<Class, T>& prop)
200 {
201 j[prop.key] = to_json(obj.*prop.member);
202 }
203
204 template <typename Class, typename T>
205 static void encode_property(
206 nlohmann::json& j,
207 const Class& obj,
209 {
210 const auto& opt = obj.*prop.member;
211 if (opt.has_value()) {
212 j[prop.key] = to_json(*opt);
213 }
214 }
215
216 // -----------------------------------------------------------------------
217 // Decoding engine
218 // -----------------------------------------------------------------------
219
220 template <typename T>
221 static void from_json(const nlohmann::json& j, T& out)
222 {
223 if constexpr (Reflect::Reflectable<T>) {
224 if (!j.is_object()) {
225 throw nlohmann::json::type_error::create(302, "expected object for Reflectable type", &j);
226 }
227 std::apply(
228 [&](const auto&... props) {
229 (decode_property(j, out, props), ...);
230 },
231 T::describe());
232 } else if constexpr (Reflect::is_optional_v<T>) {
233 using Inner = typename Reflect::is_optional<T>::inner;
234 if (j.is_null()) {
235 out = std::nullopt;
236 } else {
237 Inner inner {};
238 from_json(j, inner);
239 out = std::move(inner);
240 }
241 } else if constexpr (Reflect::is_vector_v<T>) {
242 using V = typename Reflect::is_vector<T>::element;
243 if (!j.is_array())
244 throw nlohmann::json::type_error::create(302, "expected array", &j);
245 out.clear();
246 out.reserve(j.size());
247 for (const auto& item : j) {
248 V element {};
249 from_json(item, element);
250 out.push_back(std::move(element));
251 }
252
253 } else if constexpr (Reflect::is_string_map_v<T>) {
254 using V = typename Reflect::is_string_map<T>::element;
255 if (!j.is_object())
256 throw nlohmann::json::type_error::create(302, "expected object for map", &j);
257 out.clear();
258 for (const auto& [k, v] : j.items()) {
259 V val {};
260 from_json(v, val);
261 out.emplace(k, std::move(val));
262 }
263 } else if constexpr (std::is_enum_v<T>) {
264 out = static_cast<T>(j.get<std::underlying_type_t<T>>());
265
266 } else if constexpr (std::is_same_v<T, nlohmann::json>) {
267 out = j;
268
269 } else {
271 }
272 }
273
274 template <typename Class, typename T>
275 static void decode_property(
276 const nlohmann::json& j,
277 Class& obj,
278 const Reflect::Property<Class, T>& prop)
279 {
280 if (j.contains(prop.key)) {
281 from_json(j.at(prop.key), obj.*prop.member);
282 }
283 }
284
285 template <typename Class, typename T>
286 static void decode_property(
287 const nlohmann::json& j,
288 Class& obj,
290 {
291 if (!j.contains(prop.key) || j.at(prop.key).is_null()) {
292 obj.*prop.member = std::nullopt;
293 } else {
294 T inner {};
295 from_json(j.at(prop.key), inner);
296 obj.*prop.member = std::move(inner);
297 }
298 }
299};
300
301} // namespace MayaFlux::IO
float value
float k
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::vector< double > normalized(const std::vector< double > &data, double target_peak)
Normalize single-channel data (non-destructive)
Definition Yantra.cpp:588
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