VoltMod
C++23 framework for CS2 server plugins
Loading...
Searching...
No Matches
Json.hpp
Go to the documentation of this file.
1#pragma once
2
7#include <cstdint>
8#include <format>
9#include <glaze/json.hpp>
10#include <string>
11#include <string_view>
12#include <utility>
13
14namespace VoltMod
15{
16
17/**
18 * @brief System.Text.Json-style helpers for mapping C++ types to and from JSON.
19 *
20 * Glaze reflects public aggregate members directly: the member name is the JSON key and a missing
21 * key keeps the member's C++ initializer. Unknown keys are ignored for compatibility with older
22 * settings files. No registration macro is needed.
23 *
24 * @code
25 * struct Cfg { std::string host = "localhost"; int port = 5432; };
26 * auto cfg = Json::ReadFile<Cfg>("addons/voltmod/plugins/my-plugin/config.jsonc");
27 * if (!cfg)
28 * Log::Error("{}", cfg.error().Detail);
29 * @endcode
30 */
31class Json
32{
33public:
34 /** JSONC with the tolerant unknown-key behavior used by existing settings files. */
35 static constexpr glz::opts ReadOptions{.comments = true, .error_on_unknown_keys = false};
36 /** JSONC for formats whose schema rejects unknown keys. */
37 static constexpr glz::opts StrictReadOptions{.comments = true};
38
39 /**
40 * @brief Parse @p path (resolved via ResolvePath) into T.
41 *
42 * @return ErrorCode::NotFound when the file cannot be opened; ErrorCode::Invalid with a
43 * position-aware message for a syntax error, wrong-typed value, or invalid UTF-8.
44 */
45 template <class T, auto Options = ReadOptions>
46 static Result<T> ReadFile(std::string_view path)
47 {
48 auto text = ReadAllText(path);
49 if (!text)
50 {
51 return std::unexpected(text.error());
52 }
53
55 if (!parsed)
56 {
57 return std::unexpected(Error::Invalid(std::format("{}: {}", path, parsed.error().Detail)));
58 }
59 return parsed;
60 }
61
62 /** @brief As @ref ReadFile, over text already in hand. */
63 template <class T, auto Options = ReadOptions>
64 static Result<T> Read(std::string_view text)
65 {
66 T value{};
67 if (auto ec = glz::read<Options>(value, text))
68 {
69 return std::unexpected(Error::Invalid(glz::format_error(ec, text)));
70 }
71 return value;
72 }
73
74 /** Glaze reads `indentation_width` off the options type only when it declares one, so the
75 * two-space indentation this repo's JSON files use needs its own options struct. */
76 struct PrettyOpts : glz::opts
77 {
79 };
80 static constexpr PrettyOpts PrettyOptions{{.prettify = true}, 2};
81
82 /**
83 * @brief Serialize @p value as prettified JSON.
84 *
85 * For a file a human reviews: one value per line keeps a `git diff` of a regenerated
86 * artifact down to the values that actually changed.
87 */
88 template <class T>
89 static std::string WritePretty(const T& value)
90 {
91 return Write<T, PrettyOptions>(value);
92 }
93
94 /** @brief Serialize @p value as compact JSON. */
95 template <class T, auto Options = glz::opts{}>
96 static std::string Write(const T& value)
97 {
98 auto written = glz::write<Options>(value);
99 if (!written)
100 {
101 Log::Error("Json: failed to serialize: {}", glz::format_error(written.error()));
102 return {};
103 }
104 return std::move(*written);
105 }
106
107 /** @brief Parse free-form JSON whose shape is not known at compile time. */
108 static Result<glz::generic> ParseDocument(std::string_view text)
109 {
110 glz::generic document{};
111 if (auto ec = glz::read_json(document, text))
112 {
113 return std::unexpected(Error::Invalid(glz::format_error(ec, text)));
114 }
115 return document;
116 }
117
118 /** @brief Recursively replace `{key}` tokens in every string value of @p node.
119 *
120 * Substituting inside the parsed document rather than in its text is what keeps a token
121 * value containing `"` or `\` (a player name, say) from producing invalid JSON. */
122 static void SubstituteTokens(glz::generic& node, const Tokens& tokens)
123 {
124 if (node.is_string())
125 {
126 node = Strings::SubstituteTokens(node.get_string(), tokens);
127 }
128 else if (node.is_object())
129 {
130 for (auto& [key, child] : node.get_object())
131 {
133 }
134 }
135 else if (node.is_array())
136 {
137 for (auto& child : node.get<glz::generic::array_t>())
138 {
140 }
141 }
142 }
143
144 /**
145 * @brief Descend a dot-separated path (e.g. "data.room.code") in @p jsonText.
146 *
147 * @return The leaf as a string, or "" when @p jsonText does not parse, the path is absent, or
148 * the leaf is neither a string nor a primitive. A numeric or boolean leaf reads as
149 * "42" / "true" without quotes.
150 */
151 static std::string GetStringByPath(std::string_view jsonText, std::string_view dotPath)
152 {
154 if (!document)
155 {
156 return {};
157 }
158 return GetString(*document, dotPath);
159 }
160
161 /** As above, for a node already parsed: saves callers a dump-and-reparse round trip. */
162 static std::string GetString(const glz::generic& root, std::string_view dotPath)
163 {
164 const glz::generic* node = &root;
165 for (size_t start = 0; start <= dotPath.size();)
166 {
167 const size_t dot = dotPath.find('.', start);
168 const std::string key(
169 dotPath.substr(start, dot == std::string_view::npos ? std::string_view::npos : dot - start));
170 if (!node->is_object() || !node->contains(key))
171 {
172 return {};
173 }
174 node = &(*node)[key];
175 if (dot == std::string_view::npos)
176 {
177 break;
178 }
179 start = dot + 1;
180 }
181
182 if (node->is_string())
183 {
184 return node->get_string();
185 }
186 // A numeric value (e.g. a room id) is valid; dump() yields "42"/"true" without quotes.
187 if (node->is_number() || node->is_boolean())
188 {
189 if (auto dumped = node->dump())
190 {
191 return std::move(*dumped);
192 }
193 }
194 return {};
195 }
196};
197
198} // namespace VoltMod
System.Text.Json-style helpers for mapping C++ types to and from JSON.
Definition Json.hpp:32
static std::string WritePretty(const T &value)
Serialize value as prettified JSON.
Definition Json.hpp:89
static constexpr PrettyOpts PrettyOptions
Definition Json.hpp:80
static std::string GetStringByPath(std::string_view jsonText, std::string_view dotPath)
Descend a dot-separated path (e.g. "data.room.code") in jsonText.
Definition Json.hpp:151
static std::string Write(const T &value)
Serialize value as compact JSON.
Definition Json.hpp:96
static Result< T > ReadFile(std::string_view path)
Parse path (resolved via ResolvePath) into T.
Definition Json.hpp:46
static Result< glz::generic > ParseDocument(std::string_view text)
Parse free-form JSON whose shape is not known at compile time.
Definition Json.hpp:108
static void SubstituteTokens(glz::generic &node, const Tokens &tokens)
Recursively replace {key} tokens in every string value of node.
Definition Json.hpp:122
static std::string GetString(const glz::generic &root, std::string_view dotPath)
Definition Json.hpp:162
static constexpr glz::opts ReadOptions
Definition Json.hpp:35
static Result< T > Read(std::string_view text)
As ReadFile, over text already in hand.
Definition Json.hpp:64
static constexpr glz::opts StrictReadOptions
Definition Json.hpp:37
Owns a plugin's settings and republishes them whole on every Load.
Definition Options.hpp:25
static std::string SubstituteTokens(std::string text, const Tokens &tokens)
Definition Strings.cpp:176
void Error(std::format_string< Args... > fmt, Args &&... args)
Definition Log.hpp:97
Result< std::string > ReadAllText(std::string_view path)
Read path (resolved via ResolvePath) into a string.
Definition File.cpp:12
std::map< std::string, std::string > Tokens
Definition Strings.hpp:14
std::expected< T, Error > Result
Definition Result.hpp:64
static Error Invalid(std::string detail)
Definition Result.hpp:51