Program Listing for File config.cc¶

↰ Return to documentation for file (core/config.cc)

#include "lupnt/core/config.h"

#include <algorithm>
#include <fstream>

#include "lupnt/core/error.h"
#include "lupnt/core/file.h"

namespace lupnt {

  static std::vector<std::filesystem::path> config_search_dirs_;

  void AddConfigSearchDir(const std::string& dir) {
    std::filesystem::path p(dir);
    if (std::filesystem::exists(p) && std::filesystem::is_directory(p)
        && std::find(config_search_dirs_.begin(), config_search_dirs_.end(), p)
               == config_search_dirs_.end()) {
      config_search_dirs_.push_back(p);
    }
  }

  void InitDefaultConfigSearchDirs() { AddConfigSearchDir((GetBaseDir() / "configs").string()); }

  // Parse "path::key" into (path, key)
  static std::pair<std::string, std::string> ParsePathAndKey(const std::string& path_with_key) {
    size_t pos = path_with_key.find("::");
    return pos != std::string::npos
               ? std::make_pair(path_with_key.substr(0, pos), path_with_key.substr(pos + 2))
               : std::make_pair(path_with_key, "");
  }

  static bool IsConfigPath(const std::string& str) {
    return str.find(".yaml") != std::string::npos || str.find(".yml") != std::string::npos;
  }

  static std::filesystem::path FindConfigFile(const std::string& path);
  static void LoadRecursive(YAML::Node& node);
  static YAML::Node ProcessInheritance(const YAML::Node& node, const YAML::Node& root);

  static YAML::Node ProcessScalarValue(const YAML::Node& value) {
    if (!value.IsScalar()) return value;
    std::string str = value.as<std::string>();
    if (!IsConfigPath(str)) return value;

    try {
      auto [path, key] = ParsePathAndKey(str);
      auto abs_path = FindConfigFile(path);
      if (std::filesystem::exists(abs_path)) {
        return LoadConfig(abs_path.string(), key, true);
      }
    } catch (...) {
    }
    return value;
  }

  static void DeepMerge(YAML::Node& base, const std::string& key, const YAML::Node& value) {
    if (!(value.IsMap() && base[key] && base[key].IsMap())) {
      base[key] = ProcessScalarValue(value);
      return;
    }

    for (auto it = value.begin(); it != value.end(); ++it) {
      std::string sub_key = it->first.as<std::string>();
      if (base[key][sub_key] && base[key][sub_key].IsMap() && it->second.IsMap()) {
        for (auto sub_it = it->second.begin(); sub_it != it->second.end(); ++sub_it) {
          base[key][sub_key][sub_it->first.as<std::string>()] = ProcessScalarValue(sub_it->second);
        }
      } else {
        base[key][sub_key] = ProcessScalarValue(it->second);
      }
    }
  }

  static YAML::Node ProcessInheritance(const YAML::Node& node, const YAML::Node& root) {
    if (!node.IsMap()) return node;

    YAML::Node result;

    // First, handle inheritance at this level if present
    if (node["inherit_from"]) {
      std::string inherit_path = node["inherit_from"].as<std::string>();
      try {
        // Check if this is a simple key reference (no file path)
        if (!IsConfigPath(inherit_path) && inherit_path.find("::") == std::string::npos) {
          // Try to find the key in the root node of the current file
          if (root[inherit_path]) {
            result = YAML::Clone(root[inherit_path]);
            Logger::Debug(fmt::format("Inherited config from local key: {}", inherit_path),
                          "Config");
          } else {
            Logger::Warn(fmt::format("Local key '{}' not found in config", inherit_path), "Config");
            result = YAML::Node(YAML::NodeType::Map);
          }
        } else {
          // Load from external file
          auto [file, key] = ParsePathAndKey(inherit_path);
          result = LoadConfig(file, key, true);
        }

        for (auto it = node.begin(); it != node.end(); ++it) {
          if (it->first.as<std::string>() != "inherit_from") {
            DeepMerge(result, it->first.as<std::string>(), it->second);
          }
        }
      } catch (const std::exception& e) {
        Logger::Warn(fmt::format("Failed to inherit from {}: {}", inherit_path, e.what()),
                     "Config");
        result = YAML::Clone(node);
      }
    } else {
      result = YAML::Clone(node);
    }

    // Recursively process inheritance for all nested maps
    for (auto it = result.begin(); it != result.end(); ++it) {
      if (it->second.IsMap()) {
        it->second = ProcessInheritance(it->second, root);
      }
    }

    return result;
  }

  Config LoadConfig(const std::string& path_with_key, const std::string& key, bool recursive) {
    auto [parsed_path, parsed_key] = ParsePathAndKey(path_with_key);
    std::string actual_key = key.empty() ? parsed_key : key;

    auto abs_path = FindConfigFile(parsed_path);
    Logger::Info(fmt::format("Loading config file: {}", abs_path.string()), "Config");
    YAML::Node root = YAML::LoadFile(abs_path.string());

    // Add parent directory to search dirs
    auto parent = abs_path.parent_path();
    if (std::find(config_search_dirs_.begin(), config_search_dirs_.end(), parent)
        == config_search_dirs_.end()) {
      config_search_dirs_.push_back(parent);
    }

    // Extract key or auto-extract single-key map
    YAML::Node node = root;
    if (!actual_key.empty()) {
      node = root[actual_key];
    } else if (root.IsMap() && root.size() == 1) {
      node = root.begin()->second;
    }

    if (recursive) LoadRecursive(node);
    return ProcessInheritance(node, root);
  }

  Config LoadConfig(const Config& config) {
    // If config is just a string that looks like a file path, load that file
    if (config.IsScalar()) {
      std::string str = config.as<std::string>();
      if (IsConfigPath(str)) {
        return LoadConfig(str, "", true);
      }
    }
    YAML::Node cloned = YAML::Clone(config);
    return ProcessInheritance(cloned, cloned);
  }

  std::string ConfigToString(const Config& config) {
    YAML::Emitter out;
    out << config;
    return out.c_str();
  }

  void SaveConfig(const Config& config, const std::string& path) { std::ofstream(path) << config; }

  static bool PathEndsWith(const std::filesystem::path& rel_path,
                           const std::filesystem::path& req_path) {
    std::vector<std::filesystem::path> rel, req;
    for (const auto& c : rel_path) rel.push_back(c);
    for (const auto& c : req_path) req.push_back(c);

    if (req.size() > rel.size()) return false;
    for (size_t i = 0; i < req.size(); ++i) {
      if (rel[rel.size() - req.size() + i] != req[i]) return false;
    }
    return true;
  }

  static std::filesystem::path FindConfigFile(const std::string& path) {
    std::filesystem::path p(path);
    if (p.is_absolute() && std::filesystem::exists(p)) return p;

    static bool initialized = false;
    if (!initialized) {
      InitDefaultConfigSearchDirs();
      initialized = true;
    }

    Logger::Debug(fmt::format("Searching for config file: {}", path), "Config");

    for (auto it = config_search_dirs_.rbegin(); it != config_search_dirs_.rend(); ++it) {
      if (std::filesystem::exists(*it / p)) return *it / p;

      try {
        for (const auto& entry : std::filesystem::recursive_directory_iterator(*it)) {
          if (entry.is_regular_file() && PathEndsWith(entry.path().lexically_relative(*it), p)) {
            Logger::Debug(fmt::format("Found: {}", entry.path().string()), "Config");
            return entry.path();
          }
        }
      } catch (const std::filesystem::filesystem_error&) {
      }
    }

    if (std::filesystem::exists(p)) return p;
    LUPNT_CHECK(false, "Config file not found: " + path, "Config");
  }

  static void LoadRecursive(YAML::Node& node) {
    if (!node.IsMap()) return;

    for (auto it = node.begin(); it != node.end(); ++it) {
      if (it->second.IsScalar()) {
        std::string str = it->second.as<std::string>();
        if (IsConfigPath(str)) {
          try {
            auto [path, key] = ParsePathAndKey(str);
            auto abs_path = FindConfigFile(path);
            if (std::filesystem::exists(abs_path)) {
              it->second = LoadConfig(abs_path.string(), key, true);
            }
          } catch (...) {
          }
        }
      } else if (it->second.IsMap()) {
        LoadRecursive(it->second);
      }
    }
  }

  json ConfigToJson(const Config& config) {
    if (config.IsNull()) {
      return nullptr;
    } else if (config.IsScalar()) {
      if (config.Tag() == "!") {
        return config.as<std::string>();
      }
      // Try to parse as different types
      try {
        return config.as<bool>();
      } catch (...) {
      }
      try {
        return config.as<int>();
      } catch (...) {
      }
      try {
        return config.as<double>();
      } catch (...) {
      }
      return config.as<std::string>();
    } else if (config.IsSequence()) {
      json arr = json::array();
      for (const auto& item : config) {
        arr.push_back(ConfigToJson(item));
      }
      return arr;
    } else if (config.IsMap()) {
      json obj = json::object();
      for (const auto& pair : config) {
        obj[pair.first.as<std::string>()] = ConfigToJson(pair.second);
      }
      return obj;
    }
    return nullptr;
  }

  Config JsonToConfig(const json& json) { return YAML::Load(json.dump()); }
}  // namespace lupnt