268 lines
9.2 KiB
C++
268 lines
9.2 KiB
C++
#include "yaml_parser.h"
|
|
#include <algorithm>
|
|
#include <cctype>
|
|
#include <sstream>
|
|
#include <vector>
|
|
|
|
namespace radiantwave::config {
|
|
|
|
namespace {
|
|
|
|
std::string trim(const std::string& str) {
|
|
auto start = str.find_first_not_of(" \t\r\n");
|
|
if (start == std::string::npos) return "";
|
|
auto end = str.find_last_not_of(" \t\r\n");
|
|
return str.substr(start, end - start + 1);
|
|
}
|
|
|
|
std::string unquote(const std::string& str) {
|
|
std::string s = trim(str);
|
|
if (s.size() >= 2) {
|
|
if ((s.front() == '"' && s.back() == '"') ||
|
|
(s.front() == '\'' && s.back() == '\'')) {
|
|
return s.substr(1, s.size() - 2);
|
|
}
|
|
}
|
|
return s;
|
|
}
|
|
|
|
std::string removeComments(const std::string& line) {
|
|
bool in_single_quote = false;
|
|
bool in_double_quote = false;
|
|
|
|
for (size_t i = 0; i < line.size(); ++i) {
|
|
char c = line[i];
|
|
if (c == '\'' && !in_double_quote) {
|
|
in_single_quote = !in_single_quote;
|
|
} else if (c == '"' && !in_single_quote) {
|
|
in_double_quote = !in_double_quote;
|
|
} else if (c == '#' && !in_single_quote && !in_double_quote) {
|
|
return line.substr(0, i);
|
|
}
|
|
}
|
|
return line;
|
|
}
|
|
|
|
int countIndent(const std::string& line) {
|
|
int indent = 0;
|
|
for (char c : line) {
|
|
if (c == ' ') indent++;
|
|
else if (c == '\t') indent += 2;
|
|
else break;
|
|
}
|
|
return indent;
|
|
}
|
|
|
|
} // anonymous namespace
|
|
|
|
// ─── YamlNode Implementation ───────────────────────────────────────────
|
|
|
|
std::string YamlNode::asString(const std::string& defaultValue) const {
|
|
if (type == Type::Scalar && !scalar.empty()) {
|
|
return scalar;
|
|
}
|
|
return defaultValue;
|
|
}
|
|
|
|
int YamlNode::asInt(int defaultValue) const {
|
|
if (type == Type::Scalar && !scalar.empty()) {
|
|
try {
|
|
return std::stoi(scalar);
|
|
} catch (...) {}
|
|
}
|
|
return defaultValue;
|
|
}
|
|
|
|
float YamlNode::asFloat(float defaultValue) const {
|
|
if (type == Type::Scalar && !scalar.empty()) {
|
|
try {
|
|
return std::stof(scalar);
|
|
} catch (...) {}
|
|
}
|
|
return defaultValue;
|
|
}
|
|
|
|
bool YamlNode::asBool(bool defaultValue) const {
|
|
if (type == Type::Scalar && !scalar.empty()) {
|
|
std::string s = scalar;
|
|
std::transform(s.begin(), s.end(), s.begin(), [](unsigned char c){ return std::tolower(c); });
|
|
if (s == "true" || s == "yes" || s == "1" || s == "on") return true;
|
|
if (s == "false" || s == "no" || s == "0" || s == "off") return false;
|
|
}
|
|
return defaultValue;
|
|
}
|
|
|
|
bool YamlNode::hasChild(const std::string& key) const {
|
|
if (type != Type::Mapping) return false;
|
|
return children.find(key) != children.end();
|
|
}
|
|
|
|
static const YamlNode EMPTY_NODE;
|
|
|
|
const YamlNode& YamlNode::getChild(const std::string& key) const {
|
|
if (type == Type::Mapping) {
|
|
auto it = children.find(key);
|
|
if (it != children.end()) {
|
|
return it->second;
|
|
}
|
|
}
|
|
return EMPTY_NODE;
|
|
}
|
|
|
|
// ─── Parser ─────────────────────────────────────────────────────────────
|
|
|
|
YamlNode parseYaml(const std::string& content) {
|
|
YamlNode root;
|
|
root.type = YamlNode::Type::Mapping;
|
|
|
|
std::stringstream ss(content);
|
|
std::string raw_line;
|
|
|
|
std::string current_section = "";
|
|
|
|
while (std::getline(ss, raw_line)) {
|
|
std::string line_no_comment = removeComments(raw_line);
|
|
std::string line = trim(line_no_comment);
|
|
|
|
if (line.empty()) continue;
|
|
|
|
int indent = countIndent(line_no_comment);
|
|
|
|
auto colon_pos = line.find(':');
|
|
if (colon_pos == std::string::npos) continue;
|
|
|
|
std::string key = trim(line.substr(0, colon_pos));
|
|
std::string val = trim(line.substr(colon_pos + 1));
|
|
|
|
if (indent == 0) {
|
|
// Top-level key
|
|
if (val.empty()) {
|
|
// Start a section mapping
|
|
current_section = key;
|
|
YamlNode section_node;
|
|
section_node.type = YamlNode::Type::Mapping;
|
|
root.children[key] = section_node;
|
|
} else {
|
|
current_section = "";
|
|
YamlNode scalar_node;
|
|
scalar_node.type = YamlNode::Type::Scalar;
|
|
scalar_node.scalar = unquote(val);
|
|
root.children[key] = scalar_node;
|
|
}
|
|
} else {
|
|
// Indented key (child of current_section)
|
|
if (!current_section.empty() && root.children.find(current_section) != root.children.end()) {
|
|
YamlNode scalar_node;
|
|
scalar_node.type = YamlNode::Type::Scalar;
|
|
scalar_node.scalar = unquote(val);
|
|
root.children[current_section].children[key] = scalar_node;
|
|
}
|
|
}
|
|
}
|
|
|
|
return root;
|
|
}
|
|
|
|
std::string serializeYaml(const YamlNode& root) {
|
|
std::stringstream ss;
|
|
|
|
if (root.type == YamlNode::Type::Mapping) {
|
|
for (const auto& [key, node] : root.children) {
|
|
if (node.isScalar()) {
|
|
ss << key << ": \"" << node.scalar << "\"\n";
|
|
} else if (node.isMapping()) {
|
|
ss << "\n" << key << ":\n";
|
|
for (const auto& [child_key, child_node] : node.children) {
|
|
ss << " " << child_key << ": \"" << child_node.scalar << "\"\n";
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return ss.str();
|
|
}
|
|
|
|
// ─── High-Level AppConfig Conversions ──────────────────────────────────
|
|
|
|
AppConfig parseConfigFromYaml(const std::string& yamlContent) {
|
|
AppConfig config = AppConfig::defaults();
|
|
YamlNode root = parseYaml(yamlContent);
|
|
|
|
if (root.hasChild("license_key")) {
|
|
config.license_key = root.getChild("license_key").asString(config.license_key);
|
|
}
|
|
if (root.hasChild("email_address")) {
|
|
config.email_address = root.getChild("email_address").asString(config.email_address);
|
|
}
|
|
if (root.hasChild("provisioning_status")) {
|
|
config.provisioning_status = root.getChild("provisioning_status").asString(config.provisioning_status);
|
|
}
|
|
if (root.hasChild("timezone")) {
|
|
config.timezone = root.getChild("timezone").asString(config.timezone);
|
|
}
|
|
|
|
if (root.hasChild("audio")) {
|
|
const auto& audio_node = root.getChild("audio");
|
|
if (audio_node.hasChild("enabled")) {
|
|
config.audio.enabled = audio_node.getChild("enabled").asBool(config.audio.enabled);
|
|
}
|
|
if (audio_node.hasChild("volume")) {
|
|
config.audio.volume = audio_node.getChild("volume").asInt(config.audio.volume);
|
|
}
|
|
if (audio_node.hasChild("output_device")) {
|
|
config.audio.output_device = audio_node.getChild("output_device").asString(config.audio.output_device);
|
|
}
|
|
if (audio_node.hasChild("current_track")) {
|
|
config.audio.current_track = audio_node.getChild("current_track").asString(config.audio.current_track);
|
|
}
|
|
if (audio_node.hasChild("dynamic_audio")) {
|
|
config.audio.dynamic_audio = audio_node.getChild("dynamic_audio").asBool(config.audio.dynamic_audio);
|
|
}
|
|
}
|
|
|
|
if (root.hasChild("proclamation")) {
|
|
const auto& proc_node = root.getChild("proclamation");
|
|
if (proc_node.hasChild("scroll_speed")) {
|
|
config.proclamation.scroll_speed = proc_node.getChild("scroll_speed").asFloat(config.proclamation.scroll_speed);
|
|
}
|
|
if (proc_node.hasChild("font_size")) {
|
|
config.proclamation.font_size = proc_node.getChild("font_size").asInt(config.proclamation.font_size);
|
|
}
|
|
if (proc_node.hasChild("current_id")) {
|
|
config.proclamation.current_id = proc_node.getChild("current_id").asString(config.proclamation.current_id);
|
|
}
|
|
if (proc_node.hasChild("schedule_active")) {
|
|
config.proclamation.schedule_active = proc_node.getChild("schedule_active").asBool(config.proclamation.schedule_active);
|
|
}
|
|
}
|
|
|
|
return config;
|
|
}
|
|
|
|
std::string serializeConfigToYaml(const AppConfig& config) {
|
|
std::stringstream ss;
|
|
|
|
ss << "# RadiantWave Kiosk Configuration\n\n";
|
|
ss << "license_key: \"" << config.license_key << "\"\n";
|
|
ss << "email_address: \"" << config.email_address << "\"\n";
|
|
ss << "provisioning_status: \"" << config.provisioning_status << "\"\n";
|
|
ss << "timezone: \"" << config.timezone << "\"\n\n";
|
|
|
|
ss << "audio:\n";
|
|
ss << " enabled: " << (config.audio.enabled ? "true" : "false") << "\n";
|
|
ss << " volume: " << config.audio.volume << "\n";
|
|
ss << " output_device: \"" << config.audio.output_device << "\"\n";
|
|
ss << " current_track: \"" << config.audio.current_track << "\"\n";
|
|
ss << " dynamic_audio: " << (config.audio.dynamic_audio ? "true" : "false") << "\n\n";
|
|
|
|
ss << "proclamation:\n";
|
|
ss << " scroll_speed: " << config.proclamation.scroll_speed << "\n";
|
|
ss << " font_size: " << config.proclamation.font_size << "\n";
|
|
ss << " current_id: \"" << config.proclamation.current_id << "\"\n";
|
|
ss << " schedule_active: " << (config.proclamation.schedule_active ? "true" : "false") << "\n";
|
|
|
|
return ss.str();
|
|
}
|
|
|
|
} // namespace radiantwave::config
|