|
@@ -0,0 +1,119 @@
|
|
|
|
|
+#include "config_loader.hpp"
|
|
|
|
|
+
|
|
|
|
|
+#include <algorithm>
|
|
|
|
|
+#include <fstream>
|
|
|
|
|
+#include <spdlog/spdlog.h>
|
|
|
|
|
+#include <stdexcept>
|
|
|
|
|
+#include <system_error>
|
|
|
|
|
+
|
|
|
|
|
+namespace fs = std::filesystem;
|
|
|
|
|
+
|
|
|
|
|
+namespace smartbotic::database {
|
|
|
|
|
+
|
|
|
|
|
+namespace {
|
|
|
|
|
+
|
|
|
|
|
+nlohmann::json parseJsonFile(const fs::path& path) {
|
|
|
|
|
+ std::ifstream f(path);
|
|
|
|
|
+ if (!f) {
|
|
|
|
|
+ throw std::runtime_error("cannot open config file: " + path.string());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ nlohmann::json out;
|
|
|
|
|
+ try {
|
|
|
|
|
+ f >> out;
|
|
|
|
|
+ } catch (const nlohmann::json::parse_error& e) {
|
|
|
|
|
+ // nlohmann carries the byte offset in e.what(); include the filename
|
|
|
|
|
+ // so the operator can jump straight to the bad file.
|
|
|
|
|
+ throw std::runtime_error("JSON syntax error in " + path.string() +
|
|
|
|
|
+ ": " + e.what());
|
|
|
|
|
+ }
|
|
|
|
|
+ return out;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+std::vector<fs::path> listJsonDropins(const fs::path& dir) {
|
|
|
|
|
+ std::vector<fs::path> out;
|
|
|
|
|
+ std::error_code ec;
|
|
|
|
|
+ if (!fs::exists(dir, ec) || !fs::is_directory(dir, ec)) {
|
|
|
|
|
+ return out;
|
|
|
|
|
+ }
|
|
|
|
|
+ for (const auto& entry : fs::directory_iterator(dir, ec)) {
|
|
|
|
|
+ if (!entry.is_regular_file(ec)) continue;
|
|
|
|
|
+ if (entry.path().extension() != ".json") continue;
|
|
|
|
|
+ out.push_back(entry.path());
|
|
|
|
|
+ }
|
|
|
|
|
+ // Sort by filename (not full path) — matches operator expectation of
|
|
|
|
|
+ // "00-…, 50-…, 99-…" ordering regardless of the parent directory layout.
|
|
|
|
|
+ std::sort(out.begin(), out.end(), [](const fs::path& a, const fs::path& b) {
|
|
|
|
|
+ return a.filename().string() < b.filename().string();
|
|
|
|
|
+ });
|
|
|
|
|
+ return out;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// Shallow list of top-level keys, for a non-secret-leaking debug summary.
|
|
|
|
|
+std::string topLevelKeysSummary(const nlohmann::json& config) {
|
|
|
|
|
+ if (!config.is_object()) return "<non-object>";
|
|
|
|
|
+ std::string out;
|
|
|
|
|
+ bool first = true;
|
|
|
|
|
+ for (auto it = config.begin(); it != config.end(); ++it) {
|
|
|
|
|
+ if (!first) out += ", ";
|
|
|
|
|
+ out += it.key();
|
|
|
|
|
+ first = false;
|
|
|
|
|
+ }
|
|
|
|
|
+ return out;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+} // anonymous namespace
|
|
|
|
|
+
|
|
|
|
|
+nlohmann::json loadLayeredConfig(const fs::path& baseFile,
|
|
|
|
|
+ const fs::path& dropinDir,
|
|
|
|
|
+ std::vector<fs::path>* mergedFiles) {
|
|
|
|
|
+ nlohmann::json config = nlohmann::json::object();
|
|
|
|
|
+
|
|
|
|
|
+ // Base file (optional).
|
|
|
|
|
+ std::error_code ec;
|
|
|
|
|
+ const bool baseExists = fs::exists(baseFile, ec);
|
|
|
|
|
+ if (baseExists) {
|
|
|
|
|
+ spdlog::info("Loading base config: {}", baseFile.string());
|
|
|
|
|
+ config = parseJsonFile(baseFile);
|
|
|
|
|
+ if (!config.is_object()) {
|
|
|
|
|
+ throw std::runtime_error("base config must be a JSON object: " +
|
|
|
|
|
+ baseFile.string());
|
|
|
|
|
+ }
|
|
|
|
|
+ if (mergedFiles) mergedFiles->push_back(baseFile);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ spdlog::info("No base config at {}, starting with empty config",
|
|
|
|
|
+ baseFile.string());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Drop-ins.
|
|
|
|
|
+ auto dropins = listJsonDropins(dropinDir);
|
|
|
|
|
+ for (const auto& p : dropins) {
|
|
|
|
|
+ spdlog::info("Merging drop-in: {}", p.string());
|
|
|
|
|
+ nlohmann::json patch = parseJsonFile(p);
|
|
|
|
|
+ if (!patch.is_object()) {
|
|
|
|
|
+ throw std::runtime_error("drop-in config must be a JSON object: " +
|
|
|
|
|
+ p.string());
|
|
|
|
|
+ }
|
|
|
|
|
+ // RFC 7396: arrays and scalars replace; objects merge recursively.
|
|
|
|
|
+ config.merge_patch(patch);
|
|
|
|
|
+ if (mergedFiles) mergedFiles->push_back(p);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (dropins.empty()) {
|
|
|
|
|
+ spdlog::info("No drop-ins found in {}", dropinDir.string());
|
|
|
|
|
+ } else {
|
|
|
|
|
+ spdlog::info("Config assembled from {} file(s) ({} base + {} drop-in(s))",
|
|
|
|
|
+ (baseExists ? 1u : 0u) + dropins.size(),
|
|
|
|
|
+ baseExists ? 1 : 0,
|
|
|
|
|
+ dropins.size());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Log the top-level key set at DEBUG — values may contain secrets, keys
|
|
|
|
|
+ // are safe and give operators enough to diagnose "did my drop-in land?".
|
|
|
|
|
+ spdlog::debug("Effective config top-level keys: {}",
|
|
|
|
|
+ topLevelKeysSummary(config));
|
|
|
|
|
+
|
|
|
|
|
+ return config;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+} // namespace smartbotic::database
|