1//! @file configure.cpp
2//! @author ryftchen
3//! @brief The definitions (configure) in the application module.
4//! @version 0.1.0
5//! @copyright Copyright (c) 2022-2026 ryftchen. All rights reserved.
6
7#include "configure.hpp"
8#include "log.hpp"
9#include "view.hpp"
10
11#ifndef _PRECOMPILED_HEADER
12#include <filesystem>
13#include <iterator>
14#else
15#include "application/pch/precompiled_header.hpp"
16#endif
17
18namespace application::configure
19{
20//! @brief Anonymous namespace.
21inline namespace
22{
23//! @brief The semaphore that controls the maximum access limit.
24std::counting_semaphore<maxAccessLimit> configSem( // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
25 maxAccessLimit);
26} // namespace
27
28std::string getFullConfigPath(const std::string_view filename)
29{
30 const char* const processHome = std::getenv(name: "FOO_HOME"); // NOLINT(concurrency-mt-unsafe)
31 if (!processHome)
32 {
33 throw std::runtime_error{"The environment variable FOO_HOME is not set."};
34 }
35 return std::string{processHome} + '/' + filename.data();
36}
37
38//! @brief Get the Configure instance.
39//! @param filename - configure file path
40//! @return const reference of the Configure object
41const Configure& getInstance(const std::string_view filename = defaultConfigFile)
42{
43 static const Configure configurator(filename);
44 return configurator;
45}
46
47utility::json::JSON Configure::parseConfigFile(const std::string_view configFile)
48{
49 if (!std::filesystem::is_regular_file(p: configFile))
50 {
51 throw std::runtime_error{"Configuration file " + std::string{configFile} + " is missing."};
52 }
53
54 using utility::json::JSON;
55 const auto configRows = utility::io::readFileLines(filename: configFile, lock: true);
56 std::ostringstream transfer{};
57 std::ranges::copy(configRows, std::ostream_iterator<std::string>(transfer, ""));
58 auto preprocessedData = JSON::load(fmt: transfer.str());
59 verifyConfigData(configData: preprocessedData);
60 return preprocessedData;
61}
62
63//! @brief Check the "logger" object in the helper list.
64//! @param helper - object of helper
65template <>
66void Configure::checkObjectInHelperList<log::Log>(const utility::json::JSON& helper)
67{
68 if (!helper.hasKey(key: field::properties) || !helper.hasKey(key: field::required))
69 {
70 throw std::runtime_error{
71 "Incomplete 3rd level configuration in \"" + std::string{field::logger} + "\" field in \""
72 + std::string{field::helperList} + "\" field (" + helper.asUnescapedString() + ")."};
73 }
74
75 bool isVerified = helper.isObjectType();
76 const auto& loggerProperties = helper.at(key: field::properties);
77 const auto& loggerRequired = helper.at(key: field::required);
78 isVerified &= loggerProperties.isObjectType() && loggerRequired.isArrayType()
79 && (loggerProperties.size() == loggerRequired.length());
80 for (const auto& item : loggerRequired.arrayRange())
81 {
82 isVerified &= item.isStringType() && loggerProperties.hasKey(key: item.asString());
83 }
84 for (const auto& [key, item] : loggerProperties.objectRange())
85 {
86 switch (utility::common::bkdrHash(str: key.c_str()))
87 {
88 using utility::common::operator""_bkdrHash, utility::common::EnumCheck;
89 case operator""_bkdrHash(str: field::filePath):
90 isVerified &= item.isStringType();
91 break;
92 case operator""_bkdrHash(str: field::priorityLevel):
93 using OutputLevel = log::Log::OutputLevel;
94 isVerified &= item.isIntegralType()
95 && EnumCheck<OutputLevel,
96 OutputLevel::debug,
97 OutputLevel::info,
98 OutputLevel::warning,
99 OutputLevel::error>::has(val: item.asIntegral());
100 break;
101 case operator""_bkdrHash(str: field::targetType):
102 using OutputType = log::Log::OutputType;
103 isVerified &= item.isIntegralType()
104 && EnumCheck<OutputType, OutputType::file, OutputType::terminal, OutputType::all>::has(
105 val: item.asIntegral());
106 break;
107 case operator""_bkdrHash(str: field::writeMode):
108 using OutputMode = log::Log::OutputMode;
109 isVerified &= item.isIntegralType()
110 && EnumCheck<OutputMode, OutputMode::append, OutputMode::overwrite>::has(val: item.asIntegral());
111 break;
112 default:
113 isVerified &= false;
114 break;
115 }
116 }
117
118 if (!isVerified)
119 {
120 throw std::runtime_error{
121 "Illegal 3rd level configuration in \"" + std::string{field::logger} + "\" field in \""
122 + std::string{field::helperList} + "\" field (" + helper.asUnescapedString() + ")."};
123 }
124}
125
126//! @brief Check the "viewer" object in the helper list.
127//! @param helper - object of helper
128template <>
129void Configure::checkObjectInHelperList<view::View>(const utility::json::JSON& helper)
130{
131 if (!helper.hasKey(key: field::properties) || !helper.hasKey(key: field::required))
132 {
133 throw std::runtime_error{
134 "Incomplete 3rd level configuration in \"" + std::string{field::viewer} + "\" field in \""
135 + std::string{field::helperList} + "\" field (" + helper.asUnescapedString() + ")."};
136 }
137
138 bool isVerified = helper.isObjectType();
139 const auto& viewerProperties = helper.at(key: field::properties);
140 const auto& viewerRequired = helper.at(key: field::required);
141 isVerified &= viewerProperties.isObjectType() && viewerRequired.isArrayType()
142 && (viewerProperties.size() == viewerRequired.length());
143 for (const auto& item : viewerRequired.arrayRange())
144 {
145 isVerified &= item.isStringType() && viewerProperties.hasKey(key: item.asString());
146 }
147 for (const auto& [key, item] : viewerProperties.objectRange())
148 {
149 switch (utility::common::bkdrHash(str: key.c_str()))
150 {
151 using utility::common::operator""_bkdrHash;
152 case operator""_bkdrHash(str: field::tcpHost):
153 isVerified &= item.isStringType();
154 break;
155 case operator""_bkdrHash(str: field::tcpPort):
156 isVerified &= item.isIntegralType()
157 && ((item.asIntegral() >= view::minPortNumber) && (item.asIntegral() <= view::maxPortNumber));
158 break;
159 case operator""_bkdrHash(str: field::udpHost):
160 isVerified &= item.isStringType();
161 break;
162 case operator""_bkdrHash(str: field::udpPort):
163 isVerified &= item.isIntegralType()
164 && ((item.asIntegral() >= view::minPortNumber) && (item.asIntegral() <= view::maxPortNumber));
165 break;
166 default:
167 isVerified &= false;
168 break;
169 }
170 }
171
172 if (!isVerified)
173 {
174 throw std::runtime_error{
175 "Illegal 3rd level configuration in \"" + std::string{field::viewer} + "\" field in \""
176 + std::string{field::helperList} + "\" field (" + helper.asUnescapedString() + ")."};
177 }
178}
179
180void Configure::verifyConfigData(const utility::json::JSON& configData)
181{
182 if (!configData.hasKey(key: field::activateHelper) || !configData.hasKey(key: field::helperList)
183 || !configData.hasKey(key: field::helperTimeout))
184 {
185 throw std::runtime_error{"Incomplete 1st level configuration (" + configData.asUnescapedString() + ")."};
186 }
187
188 if (!configData.at(key: field::activateHelper).isBooleanType() || !configData.at(key: field::helperList).isObjectType()
189 || !configData.at(key: field::helperTimeout).isIntegralType()
190 || configData.at(key: field::helperTimeout).asIntegral() < 0)
191 {
192 throw std::runtime_error{"Illegal 1st level configuration (" + configData.asUnescapedString() + ")."};
193 }
194
195 const auto& helperListObject = configData.at(key: field::helperList);
196 if (!helperListObject.hasKey(key: field::logger) || !helperListObject.hasKey(key: field::viewer))
197 {
198 throw std::runtime_error{
199 "Incomplete 2nd level configuration in \"" + std::string{field::helperList} + "\" field ("
200 + helperListObject.asUnescapedString() + ")."};
201 }
202 for (const auto& [key, item] : helperListObject.objectRange())
203 {
204 switch (utility::common::bkdrHash(str: key.c_str()))
205 {
206 using utility::common::operator""_bkdrHash;
207 case operator""_bkdrHash(str: field::logger):
208 checkObjectInHelperList<log::Log>(helper: item);
209 break;
210 case operator""_bkdrHash(str: field::viewer):
211 checkObjectInHelperList<view::View>(helper: item);
212 break;
213 default:
214 throw std::runtime_error{
215 "Illegal 2nd level configuration in \"" + std::string{field::helperList} + "\" field ("
216 + helperListObject.asUnescapedString() + ")."};
217 }
218 }
219}
220
221Retrieve::Retrieve(std::counting_semaphore<maxAccessLimit>& sem) : sem{sem}
222{
223 sem.acquire();
224}
225
226Retrieve::~Retrieve()
227{
228 sem.release();
229}
230
231const utility::json::JSON& Retrieve::operator/(const std::string& field) const
232{
233 return getInstance().dataRepo.at(key: field);
234}
235
236Retrieve::operator const utility::json::JSON&() const
237{
238 return getInstance().dataRepo;
239}
240
241//! @brief Safely retrieve the configuration data repository.
242//! @return current configuration data repository
243Retrieve retrieveDataRepo()
244{
245 return Retrieve(configSem);
246}
247
248// NOLINTBEGIN(readability-magic-numbers)
249//! @brief Dump the default configuration.
250//! @return default configuration
251utility::json::JSON dumpDefaultConfig()
252{
253 using log::Log;
254 auto loggerProperties = utility::json::object();
255 loggerProperties.at(key: field::filePath) = "log/foo.log";
256 loggerProperties.at(key: field::priorityLevel) =
257 static_cast<std::underlying_type_t<Log::OutputLevel>>(Log::OutputLevel::debug);
258 loggerProperties.at(key: field::targetType) = static_cast<std::underlying_type_t<Log::OutputType>>(Log::OutputType::all);
259 loggerProperties.at(key: field::writeMode) =
260 static_cast<std::underlying_type_t<Log::OutputMode>>(Log::OutputMode::append);
261 auto loggerRequired = utility::json::array();
262 loggerRequired.append(item0: field::filePath, items: field::priorityLevel, items: field::targetType, items: field::writeMode);
263 MACRO_ASSERT(loggerProperties.size() == loggerRequired.length());
264
265 auto viewerProperties = utility::json::object();
266 viewerProperties.at(key: field::tcpHost) = "localhost";
267 viewerProperties.at(key: field::tcpPort) = 61501;
268 viewerProperties.at(key: field::udpHost) = "localhost";
269 viewerProperties.at(key: field::udpPort) = 61502;
270 auto viewerRequired = utility::json::array();
271 viewerRequired.append(item0: field::tcpHost, items: field::tcpPort, items: field::udpHost, items: field::udpPort);
272 MACRO_ASSERT(viewerProperties.size() == viewerRequired.length());
273 // clang-format off
274 return utility::json::JSON
275 (
276 {
277 field::activateHelper, true,
278 field::helperList, {
279 field::logger, {
280 field::properties, std::move(loggerProperties),
281 field::required, std::move(loggerRequired)
282 },
283 field::viewer, {
284 field::properties, std::move(viewerProperties),
285 field::required, std::move(viewerRequired)
286 }
287 },
288 field::helperTimeout, 1000
289 }
290 );
291 // clang-format on
292}
293// NOLINTEND(readability-magic-numbers)
294
295//! @brief Forced configuration update by default.
296//! @param filePath - full path to the configuration file
297static void forcedConfigUpdateByDefault(const std::string_view filePath)
298{
299 utility::io::FileWriter configWriter(filePath);
300 configWriter.open(overwrite: true);
301 configWriter.lock();
302 configWriter.stream() << configure::dumpDefaultConfig() << std::endl;
303 configWriter.unlock();
304 configWriter.close();
305}
306
307//! @brief Initialize the configuration.
308//! @param filePath - full path to the configuration file
309static void initializeConfig(const std::string_view filePath)
310{
311 const auto configFolderPath = std::filesystem::absolute(p: filePath).parent_path();
312 std::filesystem::create_directories(p: configFolderPath);
313 std::filesystem::permissions(
314 p: configFolderPath, prms: std::filesystem::perms::owner_all, opts: std::filesystem::perm_options::add);
315
316 forcedConfigUpdateByDefault(filePath);
317}
318
319//! @brief Show prompt and wait for input on configuration exception.
320//! @param filePath - full path to the configuration file
321//! @return whether to continue throwing exception
322static bool handleConfigException(const std::string_view filePath)
323{
324 constexpr std::string_view prompt = "Type y to force an update to the default configuration, n to exit: ";
325 constexpr std::string_view escapeClear = "\033[1A\033[2K\r";
326 constexpr std::string_view escapeMoveUp = "\n\033[1A\033[";
327 std::cout << prompt << escapeMoveUp << prompt.length() << 'C' << std::flush;
328
329 bool keepThrowing = true;
330 constexpr std::uint16_t timeout = 5000;
331 utility::io::waitForUserInput(
332 operation: utility::common::wrapClosure(
333 closure: [&](const std::string& input)
334 {
335 switch (utility::common::bkdrHash(str: input.c_str()))
336 {
337 using utility::common::operator""_bkdrHash;
338 case "y"_bkdrHash:
339 forcedConfigUpdateByDefault(filePath);
340 [[fallthrough]];
341 case "n"_bkdrHash:
342 keepThrowing = false;
343 break;
344 default:
345 std::cout << escapeClear << prompt << std::flush;
346 return false;
347 }
348 return true;
349 }),
350 timeout);
351 return keepThrowing;
352}
353
354//! @brief Load the settings.
355//! @param filename - configure file path
356//! @return successful or failed to load
357bool loadSettings(const std::string_view filename)
358{
359 const auto filePath = getFullConfigPath(filename);
360 try
361 {
362 if (!std::filesystem::is_regular_file(p: filePath))
363 {
364 initializeConfig(filePath);
365 }
366 getInstance(filename);
367 return true;
368 }
369 catch (...)
370 {
371 std::cerr << "Configuration load exception ..." << std::endl;
372 if (handleConfigException(filePath))
373 {
374 std::cout << '\n' << std::endl;
375 throw;
376 }
377 std::cout << std::endl;
378 }
379 return false;
380}
381
382namespace task
383{
384//! @brief Get memory pool for task when making multi-threading.
385//! @return reference of the ResourcePool object
386ResourcePool& resourcePool()
387{
388 static ResourcePool pooling{};
389 return pooling;
390}
391} // namespace task
392} // namespace application::configure
393