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