#include "configuration.h"
#include <algorithm>
#include <cctype>
#include <exception>
#include <fstream>
#include <iterator>
#include <limits>
#include <string_view>
#include <unordered_set>
#include <utility>
#include <ryml.hpp>
#include <ryml_std.hpp>
#include <mw/utils.hpp>
namespace
{
using Node = ryml::ConstNodeRef;
Node child(Node node, std::string_view key)
{
return node.find_child(c4::csubstr(key.data(), key.size()));
}
mw::Error configError(std::string_view context, std::string_view message)
{
return mw::runtimeError(std::string(context) + ": " +
std::string(message));
}
mw::E<std::vector<char>> readFile(const std::filesystem::path& path)
{
std::ifstream file(path, std::ios::binary);
if(!file)
{
return std::unexpected(configError(
path.string(), "failed to open configuration file"));
}
std::vector<char> content(
(std::istreambuf_iterator<char>(file)),
std::istreambuf_iterator<char>());
if(file.bad())
{
return std::unexpected(configError(
path.string(), "failed to read configuration file"));
}
return content;
}
mw::E<std::string> requiredString(Node node, std::string_view key,
std::string_view context)
{
auto value = child(node, key);
if(!value.readable() || !value.has_val())
{
return std::unexpected(configError(
context, std::string("missing string field ") + std::string(key)));
}
std::string result;
value.load(&result);
return result;
}
mw::E<std::optional<std::string>> optionalString(
Node node, std::string_view key, std::string_view context)
{
auto value = child(node, key);
if(!value.readable())
{
return std::optional<std::string>{};
}
if(!value.has_val())
{
return std::unexpected(configError(
context, std::string("invalid string field ") + std::string(key)));
}
std::string result;
value.load(&result);
return std::optional<std::string>{std::move(result)};
}
template<typename Integer>
mw::E<Integer> requiredInteger(Node node, std::string_view key,
std::string_view context)
{
auto value = child(node, key);
if(!value.readable() || !value.has_val())
{
return std::unexpected(configError(
context, std::string("missing integer field ") + std::string(key)));
}
Integer result{};
if(!value.deserialize(&result))
{
return std::unexpected(configError(
context, std::string("invalid integer field ") + std::string(key)));
}
return result;
}
template<typename Integer>
mw::E<std::optional<Integer>> optionalInteger(
Node node, std::string_view key, std::string_view context)
{
auto value = child(node, key);
if(!value.readable())
{
return std::optional<Integer>{};
}
if(!value.has_val())
{
return std::unexpected(configError(
context, std::string("invalid integer field ") + std::string(key)));
}
Integer result{};
if(!value.deserialize(&result))
{
return std::unexpected(configError(
context, std::string("invalid integer field ") + std::string(key)));
}
return std::optional<Integer>{result};
}
std::string lower(std::string value)
{
std::transform(value.begin(), value.end(), value.begin(),
[](unsigned char character) {
return static_cast<char>(std::tolower(character));
});
return value;
}
mw::E<std::chrono::steady_clock::duration> parseInterval(
Node service, std::string_view context)
{
auto interval = child(service, "interval");
if(!interval.readable() || !interval.is_map())
{
return std::unexpected(configError(
context, "missing interval map"));
}
ASSIGN_OR_RETURN(const auto value,
requiredInteger<std::int64_t>(interval, "value", context));
ASSIGN_OR_RETURN(auto unit, requiredString(interval, "unit", context));
if(value <= 0)
{
return std::unexpected(configError(
context, "interval value must be positive"));
}
const auto normalized_unit = lower(std::move(unit));
std::int64_t multiplier = 0;
if(normalized_unit == "second" || normalized_unit == "seconds")
{
multiplier = 1;
}
else if(normalized_unit == "minute" || normalized_unit == "minutes")
{
multiplier = 60;
}
else if(normalized_unit == "hour" || normalized_unit == "hours")
{
multiplier = 60 * 60;
}
else if(normalized_unit == "day" || normalized_unit == "days")
{
multiplier = 24 * 60 * 60;
}
else
{
return std::unexpected(configError(
context, "interval unit must be second, minute, hour, or day"));
}
const auto max_seconds = std::chrono::duration_cast<std::chrono::seconds>(
std::chrono::steady_clock::duration::max()).count();
if(value > max_seconds / multiplier)
{
return std::unexpected(configError(context, "interval is too large"));
}
return std::chrono::duration_cast<std::chrono::steady_clock::duration>(
std::chrono::seconds(value * multiplier));
}
mw::E<EndpointConfig> parseEndpoint(Node endpoint, std::string_view context)
{
ASSIGN_OR_RETURN(auto protocol,
requiredString(endpoint, "protocol", context));
protocol = lower(std::move(protocol));
if(protocol == "http" || protocol == "https")
{
ASSIGN_OR_RETURN(auto url,
optionalString(endpoint, "url", context));
return EndpointConfig{HttpEndpoint{url.value_or("")}};
}
if(protocol == "tcp")
{
ASSIGN_OR_RETURN(auto host,
requiredString(endpoint, "host", context));
ASSIGN_OR_RETURN(const auto port,
requiredInteger<std::uint64_t>(
endpoint, "port", context));
if(port == 0 || port > std::numeric_limits<std::uint16_t>::max())
{
return std::unexpected(configError(
context, "TCP port must be between 1 and 65535"));
}
return EndpointConfig{TcpEndpoint{
std::move(host), static_cast<std::uint16_t>(port)}};
}
if(protocol == "udp")
{
ASSIGN_OR_RETURN(auto host,
requiredString(endpoint, "host", context));
ASSIGN_OR_RETURN(const auto port,
requiredInteger<std::uint64_t>(
endpoint, "port", context));
if(port == 0 || port > std::numeric_limits<std::uint16_t>::max())
{
return std::unexpected(configError(
context, "UDP port must be between 1 and 65535"));
}
ASSIGN_OR_RETURN(auto payload,
optionalString(endpoint, "payload", context));
return EndpointConfig{UdpEndpoint{
std::move(host), static_cast<std::uint16_t>(port),
payload.value_or("")}};
}
if(protocol == "icmp")
{
ASSIGN_OR_RETURN(auto host,
requiredString(endpoint, "host", context));
return EndpointConfig{IcmpEndpoint{std::move(host)}};
}
return std::unexpected(configError(
context, "protocol must be HTTP, TCP, UDP, or ICMP"));
}
mw::E<ServiceConfig> parseService(Node service, std::string id,
std::string_view context)
{
if(!service.is_map())
{
return std::unexpected(configError(context, "service must be a map"));
}
ServiceConfig result;
result.id = std::move(id);
ASSIGN_OR_RETURN(result.name, requiredString(service, "name", context));
ASSIGN_OR_RETURN(result.description,
requiredString(service, "description", context));
ASSIGN_OR_RETURN(result.url, optionalString(service, "url", context));
auto endpoint = child(service, "endpoint");
if(!endpoint.readable() || !endpoint.is_map())
{
return std::unexpected(configError(
context, "missing endpoint map"));
}
ASSIGN_OR_RETURN(result.endpoint,
parseEndpoint(endpoint, context));
auto timeout = optionalInteger<std::int64_t>(
service, "timeout_second", context);
if(!timeout)
{
return std::unexpected(timeout.error());
}
if(!*timeout)
{
timeout = optionalInteger<std::int64_t>(
endpoint, "timeout_second", context);
if(!timeout)
{
return std::unexpected(timeout.error());
}
}
const auto timeout_value = timeout->value_or(5);
if(timeout_value <= 0)
{
return std::unexpected(configError(
context, "timeout_second must be positive"));
}
result.timeout = std::chrono::seconds(timeout_value);
ASSIGN_OR_RETURN(result.interval, parseInterval(service, context));
return result;
}
mw::E<Configuration> parseTree(ryml::Tree& tree)
{
const auto root = tree.rootref();
if(!root.is_map())
{
return std::unexpected(configError(
"configuration", "root must be a map"));
}
Configuration result;
ASSIGN_OR_RETURN(const auto worker_count,
requiredInteger<std::uint64_t>(
root, "worker_count", "configuration"));
if(worker_count == 0 ||
worker_count > std::numeric_limits<std::size_t>::max())
{
return std::unexpected(configError(
"configuration", "worker_count must be positive"));
}
result.worker_count = static_cast<std::size_t>(worker_count);
ASSIGN_OR_RETURN(result.database_path,
requiredString(root, "database_path", "configuration"));
if(result.database_path.empty() ||
result.database_path.find('\0') != std::string::npos)
{
return std::unexpected(configError(
"configuration", "database_path must not be empty"));
}
ASSIGN_OR_RETURN(auto unix_socket,
optionalString(root, "unix_socket", "configuration"));
if(unix_socket && unix_socket->find('\0') != std::string::npos)
{
return std::unexpected(configError(
"configuration", "unix_socket contains a null character"));
}
if(unix_socket)
{
result.unix_socket = std::move(*unix_socket);
}
ASSIGN_OR_RETURN(auto socket_permission,
optionalInteger<unsigned int>(
root, "socket_permission", "configuration"));
if(socket_permission && *socket_permission > 0777)
{
return std::unexpected(configError(
"configuration",
"socket_permission must be an octal mode from 0o000 through "
"0o777"));
}
result.socket_permission = std::move(socket_permission);
auto groups = child(root, "groups");
if(!groups.readable() || !groups.is_seq())
{
return std::unexpected(configError(
"configuration", "groups must be a sequence"));
}
std::unordered_set<std::string> service_ids;
std::unordered_set<std::string> group_names;
std::size_t group_index = 0;
for(const auto& group : groups)
{
const auto group_location =
"groups[" + std::to_string(group_index) + "]";
++group_index;
if(!group.is_map())
{
return std::unexpected(configError(
group_location, "group must be a map"));
}
ASSIGN_OR_RETURN(auto group_name,
requiredString(group, "name", group_location));
if(group_name.empty())
{
return std::unexpected(configError(
group_location, "group name must not be empty"));
}
if(!group_names.insert(group_name).second)
{
return std::unexpected(configError(
group_location, "group name must be unique"));
}
const auto group_context = group_location + "." + group_name;
ServiceGroupConfig group_config;
group_config.name = std::move(group_name);
auto services = child(group, "services");
if(!services.readable() || !services.is_map())
{
return std::unexpected(configError(
group_context, "services must be a map"));
}
for(const auto& service : services)
{
if(!service.has_key() || !service.is_map())
{
return std::unexpected(configError(
group_context, "service must be a map"));
}
const std::string service_id(
service.key().begin(), service.key().end());
if(service_id.empty())
{
return std::unexpected(configError(
group_context, "service id must not be empty"));
}
const auto context = group_context + "." + service_id;
ASSIGN_OR_RETURN(auto service_config,
parseService(service, service_id, context));
if(!service_ids.insert(service_config.id).second)
{
return std::unexpected(configError(
context, "service id must be unique"));
}
group_config.services.push_back(std::move(service_config));
}
result.groups.push_back(std::move(group_config));
}
return result;
}
}
mw::E<Configuration> Configuration::fromYaml(
const std::filesystem::path& path)
{
ASSIGN_OR_RETURN(auto buffer, readFile(path));
try
{
auto tree = ryml::parse_in_place(ryml::to_substr(buffer));
return parseTree(tree);
}
catch(const std::exception& error)
{
return std::unexpected(configError(
path.string(), std::string("invalid YAML: ") + error.what()));
}
}