#include "yaml_parser.h" #include #include #include #include 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