RyanHub – file viewer
filename: src/utils/config/Config.cpp
branch: feature/world
back to repo
/*
 *
 *               _____  _                 _
 *              /  ___|| |               | |
 *              \ `--. | |_  _ __   __ _ | |_   ___   ___
 *               `--. \| __|| '__| / _` || __| / _ \ / __|
 *              /\__/ /| |_ | |   | (_| || |_ | (_) |\__ \
 *              \____/  \__||_|    \__,_| \__| \___/ |___/
 *
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Copyright (C) 2025 Armen Deroian
 *
 */

#include "Config.h"

#include "utils/StringUtils.h"

#include <ranges>
#include <regex>

namespace fs = std::filesystem;

namespace stratos {
Config::Config(const Path& file) : configPath(file) {
    configFile = std::fstream(file.toString(), std::ios::in | std::ios::out);
    if (!configFile.is_open()) throw std::runtime_error("Failed to open config file: " + file.toString());
}
Config::Config(const Path& file, const std::string& default_) : configPath(file) {
    if (!file.exists() || !file.isFile()) {
        configFile = std::fstream(file.toString(), std::ios::in | std::ios::out | std::ios::trunc);
        configFile << default_;
        configFile.flush();
        configFile.seekg(0);
    } else {
        configFile = std::fstream(file.toString(), std::ios::in | std::ios::out);
    }
}
void Config::close() {
    save();
    if (configFile.is_open())
        configFile.close();
}
int PropertiesConfig::Property::asInt() const { return std::stoi(value); }
long PropertiesConfig::Property::asLong() const { return std::stol(value); }
float PropertiesConfig::Property::asFloat() const { return std::stof(value); }
double PropertiesConfig::Property::asDouble() const { return std::stod(value); }
bool PropertiesConfig::Property::asBool() const { return value == "true" || value == "1"; }
std::string PropertiesConfig::Property::asString() const { return value; }
bool PropertiesConfig::Property::operator==(const Property& other) const { return key == other.key && value == other.value; }
void PropertiesConfig::load() {
    int         lineNumber = 1;
    std::string line;
    while (std::getline(configFile, line)) {
        if (auto property = parseLine(line, lineNumber); property.has_value()) {
            propertiesKeys.push_back(property->key);
            properties[property->key] = std::move(*property);
        }
        lineNumber++;
    }
}
void PropertiesConfig::save() {
    if (dirty) {
        configFile.clear();
        configFile.seekp(0, std::ios::beg);
        for (const auto& property : propertiesKeys) {
            if (auto it = properties.find(property); it != properties.end())
                configFile << it->first << "=" << it->second.value << "\n";
        }
        dirty = configFile.tellp() < fs::file_size(configPath.getNativePath()); // If the current position is less than the file size, we have removed some content
    }
    configFile.flush();
}
void PropertiesConfig::close() {
    if (dirty && configFile.is_open()) {
        save();
        const std::streampos endPos = configFile.tellp();
        configFile.close();
        // Resize the file to the current position if we have removed some content
        if (dirty) fs::resize_file(configPath.getNativePath(), endPos);
    } else {
        if (configFile.is_open()) configFile.close();
    }
}
std::optional<std::reference_wrapper<const PropertiesConfig::Property>> PropertiesConfig::getProperty(const std::string& key) const {
    if (key.empty()) throw std::runtime_error("Cannot get property with empty key.");
    const auto it = properties.find(key);
    return it != properties.end() ? std::make_optional(std::cref(it->second)) : std::nullopt;
}
void PropertiesConfig::removeProperty(const std::string& key) {
    if (key.empty()) throw std::runtime_error("Cannot remove property with empty key.");
    if (const auto it = properties.find(key); it != properties.end()) {
        properties.erase(it);
        dirty = true;
        std::erase(propertiesKeys, it->first);
    } else {
        throw std::runtime_error("Property with key '" + key + "' does not exist.");
    }
}
void PropertiesConfig::writeProperty(const Property& property) {
    if (property.key.empty()) throw std::runtime_error("Cannot write property with empty key.");
    properties[property.key] = property;
    if (std::ranges::find(propertiesKeys, property.key) == propertiesKeys.end())
        propertiesKeys.push_back(property.key);
    dirty = true;
}
std::optional<PropertiesConfig::Property> PropertiesConfig::parseLine(const std::string& line, const int& lineNumber) {
    if (line.empty() || line[0] == '#' || line[0] == '!') return std::nullopt;
    const size_t pos = line.find('=');
    if (pos == std::string::npos) throw std::runtime_error("Properties Syntax Error: Missing '=' in line " + std::to_string(lineNumber) + ": " + line);

    const std::string key   = trim(line.substr(0, pos));
    const std::string value = trim(line.substr(pos + 1));
    if (key.empty()) throw std::runtime_error("Properties Syntax Error: Empty key in line " + std::to_string(lineNumber) + ": " + line);

    return Property{key, value};
}
} // namespace stratos