b3view/settings.cpp

487 lines
15 KiB
C++

#include "Utility.h"
// #include "Debug.h"
#include "settings.h"
#include <fstream>
#include <iostream>
#include <vector>
#include <algorithm> // std::find
#include <assert.h>
using namespace std;
void Settings::init_default_symbols() {
this->init(" = ", "# ");
}
void Settings::init(std::string assignmentOperatorAndSpacing, std::string commentMarkAndSpacing)
{
this->ao_and_spacing = assignmentOperatorAndSpacing;
this->cm_and_spacing = commentMarkAndSpacing;
this->enable_autosave = true;
}
Settings::Settings()
{
this->init_default_symbols();
}
Settings::Settings(std::string confPath)
{
this->init_default_symbols();
this->path = confPath;
this->load(confPath);
}
void Settings::clear()
{
this->section = "";
this->sections.clear();
this->table.clear();
}
void Settings::clear_types()
{
this->types.clear();
}
bool Settings::load(std::string path)
{
bool readable = false;
this->section = "";
this->path = path;
fstream newfile;
this->pre = "";
std::string ao = this->ao_trimmed();
std::string cm = this->cm_trimmed();
newfile.open(path, ios::in);
if (newfile.is_open()) {
std::string line;
int lineN = 0; // Set this to 1 before use.
while (getline(newfile, line)) {
lineN += 1;
this->pre = this->path + ":" + std::to_string(lineN) + ": "; // must end with space for outputinspector
line = Utility::trim(line);
std::size_t signPos = line.find(ao);
std::string typeStr = "string";
if ((line.length() >= cm.length()) && (line.substr(0, cm.length()) == cm)) {
// comment
}
else if (line.length() == 0) {
// blank
}
else if (signPos != std::string::npos) {
std::string name = Utility::trim(line.substr(0, signPos));
std::string value = Utility::trim(line.substr(signPos+1));
cerr << "parsing name=\"" << name << "\" value=\"" << value << "\"" << std::endl;
std::string::size_type iSz;
std::string::size_type fSz;
int valueI;
int valueF;
// See if it is a number (silently degrade to string if not).
try {
valueI = std::stoi(value, &iSz);
}
catch (const std::invalid_argument& ex) {
// cerr << "[Settings::load] invalid_argument \"" << value << "\" in stoi: " << ex.what() << std::endl;
valueI = 0;
iSz = 0;
}
catch (const std::exception& ex) {
cerr << "[Settings::load] undefined error in stoi: " << ex.what() << std::endl;
}
try {
valueF = std::stof(value, &iSz);
}
catch (const std::invalid_argument& ex) {
// cerr << "[Settings::load] invalid_argument \"" << value << "\" in stof: " << ex.what() << std::endl;
valueF = 0.0f;
fSz = 0;
}
catch (const std::exception& ex) {
cerr << "[Settings::load] undefined error in stof: " << ex.what() << std::endl;
}
// ^ radix (3rd param) default is 10 (base-10 number system)
// cerr << name << std::endl;
// cerr << " valueI length: " << iSz << std::endl;
// cerr << " valueF length: " << fSz << std::endl;
// Silently degrade (Assume the value is supposed to be a string).
if (fSz > iSz) {
typeStr = "float";
}
else if (iSz == value.length()) {
typeStr = "int";
}
else if (iSz > 0) {
cerr << this->pre << "WARNING: The value \"" << value
<< "\" starts with a number but is not a number."
<< std::endl;
}
this->types[name] = typeStr;
this->table[name] = value;
if (this->types.find(name) != this->types.end()) {
if (this->types[name] != typeStr) {
cerr << this->pre << "WARNING: The file has a "
<< typeStr << " for " << name << ", but "
<< this->types[name] << " was expected."
<< std::endl;
}
}
if (this->section.length() > 0) {
this->sections[name] = this->section;
}
}
else {
if (
(line.length() >= 3)
&& Utility::startsWith(line, "[")
&& Utility::endsWith(line, "]")
) {
std::string section = Utility::trim(line.substr(1, line.length()-2));
if (section.length() > 0) {
this->section = section;
}
else {
cerr << this->pre << "WARNING: The file has a blank section \""
<< line << "\" (expected section name)."
<< std::endl;
}
}
else {
cerr << this->pre << "WARNING: \""
<< line << "\" is not understood (expected comment with '"
<< cm <<"' (settable via this->setCM(commentMark)),"
<< " section enclosed in '[' and ']' or assignment containing '"
<< ao << "' [settable via this->setAO(assignmentOperator)])."
<< std::endl;
}
}
}
newfile.close();
readable = true;
cerr << "* load finished reading " << path << " (elements:" << this->table.size() << ")" << endl;
}
this->section = "";
this->pre = "";
return readable;
}
bool Settings::save(std::string path)
{
bool ok = true;
this->path = path;
ofstream myfile;
myfile.open(path, ios::out); // default is ios_base::out
std::map<std::string, std::string>::iterator it;
std::vector<std::string> sectionNames;
sectionNames.push_back(" ");
for (it = this->sections.begin(); it != this->sections.end(); it++) {
if (std::find(sectionNames.begin(), sectionNames.end(), it->second) == sectionNames.end())
sectionNames.push_back(it->second);
}
// Save each section consecutively, starting with variables that have no section.
for (std::vector<std::string>::iterator vIt = sectionNames.begin() ; vIt != sectionNames.end(); ++vIt) {
if (*vIt != " ") {
myfile << "[" << *vIt << "]" << std::endl;
}
for (it = table.begin(); it != table.end(); it++) {
if (this->sections.find(it->first) != this->sections.end()) {
if (*vIt == this->sections[it->first]) {
myfile << it->first << this->ao_and_spacing << it->second << std::endl;
}
}
else if (*vIt == " ") {
// ^ " " is the global section
// (The variable is not in a section).
myfile << it->first << this->ao_and_spacing << it->second << std::endl;
}
}
}
myfile.close();
return ok;
}
bool Settings::save()
{
if (this->path.length() == 0) {
throw std::runtime_error("There is no path during save().");
}
return this->save(this->path);
}
string Settings::ao_trimmed()
{
return Utility::trim(this->ao_and_spacing);
}
string Settings::cm_trimmed()
{
return Utility::trim(this->cm_and_spacing);
}
bool Settings::check_type(std::string typeStr, std::string name)
{
std::string currentTypeStr = get_type_str(name);
if ((currentTypeStr != "") && (typeStr != currentTypeStr)) {
cerr << this->pre << "WARNING: settings.set got a(n) "
<< typeStr << " (value \"" << this->table[name] << "\") for "
<< name << ", but expected a(n) "
<< currentTypeStr << "."
<< std::endl;
return false;
}
return true;
}
string Settings::get_type_str(string name)
{
std::string currentTypeStr = "";
if (this->types.find(name) != this->types.end()) {
currentTypeStr = this->types[name];
}
return currentTypeStr;
}
bool Settings::exists(string name)
{
return (this->table.find(name) != this->table.end());
}
void Settings::set_ao_and_spacing(std::string assignmentOperator)
{
this->ao_and_spacing = assignmentOperator;
}
void Settings::set_cm_and_spacing(std::string commentMark)
{
this->cm_and_spacing = commentMark;
}
void Settings::set_all_auto(std::map<std::string, std::string> table)
{
std::map<std::string, std::string>::iterator it;
for (it = table.begin(); it != table.end(); it++) {
this->set_auto(it->first, it->second);
}
}
void Settings::set_section(std::string sectionName)
{
this->section = sectionName;
}
void Settings::set_raw(std::string name, std::string value) {
std::string section = this->section;
// In any case below, never change the section of a variable
// that was already declared.
if (this->sections.find(name) != this->sections.end()) {
// Get use its section since it exists.
section = this->sections[name];
}
else if (this->exists(name)) {
section = "";
}
std::string previous = "";
if (this->table.find(name) != this->table.end()) {
previous = this->table[name];
}
this->table[name] = value;
if (section.length() > 0) {
this->sections[name] = section;
}
if (this->enable_autosave) {
if (previous != value) {
if (this->path.length() > 0) {
this->save();
}
}
}
}
bool Settings::set(std::string name, std::string value)
{
bool match = this->check_type("string", name);
set_raw(name, value);
if (this->types.find(name) == this->types.end()) {
this->types[name] = "string";
}
return match;
}
bool Settings::set_int(std::string name, int value)
{
bool match = this->check_type("int", name);
this->set_raw(name, std::to_string(value));
if (this->types.find(name) == this->types.end()) {
this->types[name] = "int";
}
return match;
}
bool Settings::set_float(std::string name, irr::f32 value)
{
bool match = this->check_type("float", name);
this->set_raw(name, std::to_string(value));
if (this->types.find(name) == this->types.end()) {
this->types[name] = "float";
}
return match;
}
void Settings::set_auto(std::string name, std::string value)
{
std::string typeStr = "string";
if (this->types.find(name) != this->types.end()) {
std::string typeStr = this->types[name];
}
if (typeStr == "int") {
std::string::size_type iSz;
int valueI = std::stoi(value, &iSz);
this->set_int(name, valueI);
}
else if (typeStr == "float") {
std::string::size_type fSz;
float valueF = std::stof(value, &fSz);
this->set_float(name, valueF);
}
else {
// Treat typeStr as string if blank
// (implement more types above to avoid errors in
// "set" which always assumes that string is the type).
this->set(name, value);
}
}
string Settings::get(string name, bool& found)
{
bool match = this->check_type("string", name);
if (this->table.find(name) == this->table.end()) {
found = false;
return "";
}
found = true;
return this->table[name];
}
float Settings::get_float(string name, bool &found)
{
bool match = this->check_type("float", name);
if (this->table.find(name) == this->table.end()) {
found = false;
return 0.0f;
}
found = true;
size_t sz;
std::string value = this->table[name];
float v = std::stof(value, &sz);
if (sz != name.length()) {
cerr << this->pre << "WARNING: only \""
<< value.substr(0, sz) << "\" part of \""
<< value << "\" for the variable named \""
<< name << "\" is a number."
<< endl;
}
return v;
}
int Settings::get_int(string name, bool &found)
{
bool match = this->check_type("int", name);
if (this->table.find(name) == this->table.end()) {
found = false;
return 0;
}
found = true;
size_t sz;
std::string value = this->table[name];
int v = std::stoi(value, &sz);
if (sz != name.length()) {
cerr << this->pre << "WARNING: only \""
<< value.substr(0, sz) << "\" part of \""
<< value << "\" for the variable named \""
<< name << "\" is a number."
<< endl;
}
return v;
}
///////////////////////// TESTS /////////////////////////
TestSettings::TestSettings() {
cerr << "TestSettings..." << std::flush;
assert_type_warning_str_then_int();
assert_set("what", "bluedragon");
assert_set("what", "greendragon");
assert_get("what", "greendragon");
assert_section_set_on_create();
cerr << "OK" << std::endl;
}
void TestSettings::assert_equal(const std::string subject, const std::string expectedResult)
{
if (subject != expectedResult) {
cerr << "The test expected \"" << expectedResult << "\" but got \"" << subject << "\"" << std::endl;
}
assert(subject == expectedResult);
}
void TestSettings::assert_set(const std::string &name, const std::string &value)
{
settings.set(name, value);
bool found = false;
std::string result = settings.get(name, found);
assert_equal(result, value);
assert(found);
}
void TestSettings::assert_get(const std::string &name, const std::string &expectedResult)
{
bool found = false;
std::string result = settings.get(name, found);
assert(found);
assert_equal(result, expectedResult);
}
void TestSettings::assert_type_warning_str_then_int()
{
settings.clear();
settings.set("username", "Poikilos");
bool match = settings.set_int("username", 1);
assert(!match);
cerr << "(The warning above is expected during the test: expected string, got int)" << endl;
}
void TestSettings::assert_section_set_on_create()
{
std::string tmpPath = "test.conf";
settings.clear();
settings.set_int("a", 1);
settings.set_section("more");
settings.set_int("b", 2);
settings.set_int("a", 3);
settings.save(tmpPath);
settings.clear();
ifstream myfile(tmpPath);
if (myfile.is_open())
{
std::string line;
assert(getline(myfile,line));
assert_equal(line, "a = 3");
assert(getline(myfile,line));
assert_equal(line, "[more]");
assert(getline(myfile,line));
assert_equal(line, "b = 2");
myfile.close();
}
}
#ifdef DEBUG
static TestSettings testsettings; // Run tests (Creating the first instance runs the static constructor).
#elif QT_DEBUG
static TestSettings testsettings; // Run tests (Creating the first instance runs the static constructor).
#endif