mirror of
https://github.com/latentPrion/libspinscale.git
synced 2026-08-12 23:48:22 +00:00
EnvKvStore: find/get split, typed int accessors, nest DotenvParser.
Rename optional lookup to find(), add throwing get(), and provide getInt/getPositiveInt/getPositiveNonZeroInt with optional defaults so callers own missing-key policy without baking domain period semantics into spinscale. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+256
-176
@@ -3,197 +3,186 @@
|
||||
#include <cstdlib>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <limits>
|
||||
#include <ranges>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
#include <utility>
|
||||
|
||||
#include <spinscale/envKvStore.h>
|
||||
|
||||
namespace sscl {
|
||||
namespace {
|
||||
|
||||
std::string trim(std::string_view value)
|
||||
class EnvKvStore::DotenvParser
|
||||
{
|
||||
auto begin = std::ranges::find_if_not(
|
||||
value, [](unsigned char c) { return std::isspace(c); });
|
||||
auto rbegin = std::ranges::find_if_not(
|
||||
value | std::views::reverse,
|
||||
[](unsigned char c) { return std::isspace(c); });
|
||||
auto end = rbegin.base();
|
||||
if (begin >= end)
|
||||
public:
|
||||
static bool lineIsBlankOrComment(const std::string &line)
|
||||
{
|
||||
return {};
|
||||
std::string trimmed = trim(line);
|
||||
return trimmed.empty() || trimmed.front() == '#';
|
||||
}
|
||||
return std::string(begin, end);
|
||||
}
|
||||
|
||||
bool characterIsValidNameStart(char c)
|
||||
{
|
||||
return std::isalpha(static_cast<unsigned char>(c)) || c == '_';
|
||||
}
|
||||
|
||||
bool characterIsValidNameBody(char c)
|
||||
{
|
||||
return std::isalnum(static_cast<unsigned char>(c)) || c == '_';
|
||||
}
|
||||
|
||||
bool nameIsValid(std::string_view name)
|
||||
{
|
||||
if (name.empty() || !characterIsValidNameStart(name.front()))
|
||||
static std::pair<std::string, std::string> parseAssignment(
|
||||
const std::filesystem::path &envFilePath,
|
||||
std::size_t lineNumber,
|
||||
const std::string &line)
|
||||
{
|
||||
return false;
|
||||
std::size_t separator = line.find('=');
|
||||
if (separator == std::string::npos)
|
||||
{
|
||||
throw makeParseError(
|
||||
envFilePath, lineNumber, "Expected KEY=value.");
|
||||
}
|
||||
|
||||
std::string name = trim(std::string_view(line).substr(0, separator));
|
||||
if (!nameIsValid(name))
|
||||
{
|
||||
throw makeParseError(
|
||||
envFilePath, lineNumber, "Invalid variable name.");
|
||||
}
|
||||
|
||||
return {
|
||||
std::move(name),
|
||||
parseValue(
|
||||
envFilePath,
|
||||
lineNumber,
|
||||
std::string_view(line).substr(separator + 1))};
|
||||
}
|
||||
return std::ranges::all_of(name.substr(1), characterIsValidNameBody);
|
||||
}
|
||||
|
||||
std::runtime_error makeParseError(
|
||||
const std::filesystem::path &envFilePath,
|
||||
std::size_t lineNumber,
|
||||
const std::string &message)
|
||||
{
|
||||
std::ostringstream stream;
|
||||
stream << envFilePath << ":" << lineNumber << ": " << message;
|
||||
return std::runtime_error(stream.str());
|
||||
}
|
||||
|
||||
bool lineIsBlankOrComment(const std::string &line)
|
||||
{
|
||||
std::string trimmed = trim(line);
|
||||
return trimmed.empty() || trimmed.front() == '#';
|
||||
}
|
||||
|
||||
std::size_t findClosingQuote(
|
||||
const std::filesystem::path &envFilePath,
|
||||
std::size_t lineNumber,
|
||||
std::string_view value)
|
||||
{
|
||||
char quote = value.front();
|
||||
bool escapeNext = false;
|
||||
for (std::size_t i = 1; i < value.size(); ++i)
|
||||
private:
|
||||
static std::string trim(std::string_view value)
|
||||
{
|
||||
if (escapeNext)
|
||||
{
|
||||
escapeNext = false;
|
||||
continue;
|
||||
}
|
||||
if (quote == '"' && value[i] == '\\')
|
||||
{
|
||||
escapeNext = true;
|
||||
continue;
|
||||
}
|
||||
if (value[i] == quote)
|
||||
{
|
||||
return i;
|
||||
}
|
||||
auto begin = std::ranges::find_if_not(
|
||||
value, [](unsigned char c) { return std::isspace(c); });
|
||||
auto rbegin = std::ranges::find_if_not(
|
||||
value | std::views::reverse,
|
||||
[](unsigned char c) { return std::isspace(c); });
|
||||
auto end = rbegin.base();
|
||||
if (begin >= end) { return {}; }
|
||||
return std::string(begin, end);
|
||||
}
|
||||
throw makeParseError(envFilePath, lineNumber, "Unterminated quoted value.");
|
||||
}
|
||||
|
||||
std::string decodeDoubleQuotedValue(std::string_view value)
|
||||
{
|
||||
std::string decoded;
|
||||
decoded.reserve(value.size());
|
||||
bool escapeNext = false;
|
||||
for (char c : value)
|
||||
static bool characterIsValidNameStart(char c)
|
||||
{
|
||||
if (!escapeNext && c == '\\')
|
||||
return std::isalpha(static_cast<unsigned char>(c)) || c == '_';
|
||||
}
|
||||
|
||||
static bool characterIsValidNameBody(char c)
|
||||
{
|
||||
return std::isalnum(static_cast<unsigned char>(c)) || c == '_';
|
||||
}
|
||||
|
||||
static bool nameIsValid(std::string_view name)
|
||||
{
|
||||
if (name.empty() || !characterIsValidNameStart(name.front())) { return false; }
|
||||
return std::ranges::all_of(name.substr(1), characterIsValidNameBody);
|
||||
}
|
||||
|
||||
static std::runtime_error makeParseError(
|
||||
const std::filesystem::path &envFilePath,
|
||||
std::size_t lineNumber,
|
||||
const std::string &message)
|
||||
{
|
||||
std::ostringstream stream;
|
||||
stream << envFilePath << ":" << lineNumber << ": " << message;
|
||||
return std::runtime_error(stream.str());
|
||||
}
|
||||
|
||||
static std::size_t findClosingQuote(
|
||||
const std::filesystem::path &envFilePath,
|
||||
std::size_t lineNumber,
|
||||
std::string_view value)
|
||||
{
|
||||
char quote = value.front();
|
||||
bool escapeNext = false;
|
||||
for (std::size_t i = 1; i < value.size(); ++i)
|
||||
{
|
||||
escapeNext = true;
|
||||
continue;
|
||||
}
|
||||
if (escapeNext)
|
||||
{
|
||||
switch (c)
|
||||
if (escapeNext)
|
||||
{
|
||||
case 'n':
|
||||
decoded.push_back('\n');
|
||||
break;
|
||||
case 'r':
|
||||
decoded.push_back('\r');
|
||||
break;
|
||||
case 't':
|
||||
decoded.push_back('\t');
|
||||
break;
|
||||
default:
|
||||
decoded.push_back(c);
|
||||
break;
|
||||
escapeNext = false;
|
||||
continue;
|
||||
}
|
||||
escapeNext = false;
|
||||
continue;
|
||||
if (quote == '"' && value[i] == '\\')
|
||||
{
|
||||
escapeNext = true;
|
||||
continue;
|
||||
}
|
||||
if (value[i] == quote) { return i; }
|
||||
}
|
||||
decoded.push_back(c);
|
||||
}
|
||||
if (escapeNext)
|
||||
{
|
||||
decoded.push_back('\\');
|
||||
}
|
||||
return decoded;
|
||||
}
|
||||
|
||||
std::string parseQuotedValue(
|
||||
const std::filesystem::path &envFilePath,
|
||||
std::size_t lineNumber,
|
||||
std::string_view value)
|
||||
{
|
||||
char quote = value.front();
|
||||
std::size_t closingQuote = findClosingQuote(envFilePath, lineNumber, value);
|
||||
std::string trailing = trim(value.substr(closingQuote + 1));
|
||||
if (!trailing.empty() && trailing.front() != '#')
|
||||
{
|
||||
throw makeParseError(
|
||||
envFilePath, lineNumber, "Unexpected text after quoted value.");
|
||||
}
|
||||
std::string_view quotedBody = value.substr(1, closingQuote - 1);
|
||||
if (quote == '"')
|
||||
{
|
||||
return decodeDoubleQuotedValue(quotedBody);
|
||||
}
|
||||
return std::string(quotedBody);
|
||||
}
|
||||
|
||||
std::string parseValue(
|
||||
const std::filesystem::path &envFilePath,
|
||||
std::size_t lineNumber,
|
||||
std::string_view rawValue)
|
||||
{
|
||||
std::string value = trim(rawValue);
|
||||
if (value.empty())
|
||||
{
|
||||
return {};
|
||||
}
|
||||
if (value.front() == '\'' || value.front() == '"')
|
||||
{
|
||||
return parseQuotedValue(envFilePath, lineNumber, value);
|
||||
}
|
||||
return trim(value.substr(0, value.find('#')));
|
||||
}
|
||||
|
||||
std::pair<std::string, std::string> parseAssignment(
|
||||
const std::filesystem::path &envFilePath,
|
||||
std::size_t lineNumber,
|
||||
const std::string &line)
|
||||
{
|
||||
std::size_t separator = line.find('=');
|
||||
if (separator == std::string::npos)
|
||||
{
|
||||
throw makeParseError(envFilePath, lineNumber, "Expected KEY=value.");
|
||||
envFilePath, lineNumber, "Unterminated quoted value.");
|
||||
}
|
||||
|
||||
std::string name = trim(std::string_view(line).substr(0, separator));
|
||||
if (!nameIsValid(name))
|
||||
static std::string decodeDoubleQuotedValue(std::string_view value)
|
||||
{
|
||||
throw makeParseError(envFilePath, lineNumber, "Invalid variable name.");
|
||||
std::string decoded;
|
||||
decoded.reserve(value.size());
|
||||
bool escapeNext = false;
|
||||
for (char c : value)
|
||||
{
|
||||
if (!escapeNext && c == '\\')
|
||||
{
|
||||
escapeNext = true;
|
||||
continue;
|
||||
}
|
||||
if (escapeNext)
|
||||
{
|
||||
switch (c)
|
||||
{
|
||||
case 'n':
|
||||
decoded.push_back('\n');
|
||||
break;
|
||||
case 'r':
|
||||
decoded.push_back('\r');
|
||||
break;
|
||||
case 't':
|
||||
decoded.push_back('\t');
|
||||
break;
|
||||
default:
|
||||
decoded.push_back(c);
|
||||
break;
|
||||
}
|
||||
escapeNext = false;
|
||||
continue;
|
||||
}
|
||||
decoded.push_back(c);
|
||||
}
|
||||
if (escapeNext) { decoded.push_back('\\'); }
|
||||
return decoded;
|
||||
}
|
||||
|
||||
return {
|
||||
std::move(name),
|
||||
parseValue(
|
||||
envFilePath,
|
||||
lineNumber,
|
||||
std::string_view(line).substr(separator + 1))};
|
||||
}
|
||||
static std::string parseQuotedValue(
|
||||
const std::filesystem::path &envFilePath,
|
||||
std::size_t lineNumber,
|
||||
std::string_view value)
|
||||
{
|
||||
char quote = value.front();
|
||||
std::size_t closingQuote =
|
||||
findClosingQuote(envFilePath, lineNumber, value);
|
||||
std::string trailing = trim(value.substr(closingQuote + 1));
|
||||
if (!trailing.empty() && trailing.front() != '#')
|
||||
{
|
||||
throw makeParseError(
|
||||
envFilePath,
|
||||
lineNumber,
|
||||
"Unexpected text after quoted value.");
|
||||
}
|
||||
std::string_view quotedBody = value.substr(1, closingQuote - 1);
|
||||
if (quote == '"') { return decodeDoubleQuotedValue(quotedBody); }
|
||||
return std::string(quotedBody);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
static std::string parseValue(
|
||||
const std::filesystem::path &envFilePath,
|
||||
std::size_t lineNumber,
|
||||
std::string_view rawValue)
|
||||
{
|
||||
std::string value = trim(rawValue);
|
||||
if (value.empty()) { return {}; }
|
||||
if (value.front() == '\'' || value.front() == '"') { return parseQuotedValue(envFilePath, lineNumber, value); }
|
||||
return trim(value.substr(0, value.find('#')));
|
||||
}
|
||||
};
|
||||
|
||||
EnvKvStore::EnvKvStore(
|
||||
const std::vector<std::filesystem::path> &envFilePaths,
|
||||
@@ -205,8 +194,7 @@ EnvKvStore::EnvKvStore(
|
||||
EnvKvStore::EnvKvStore(
|
||||
const std::vector<std::filesystem::path> &envFilePaths)
|
||||
: EnvKvStore(envFilePaths, std::cerr)
|
||||
{
|
||||
}
|
||||
{}
|
||||
|
||||
void EnvKvStore::loadFiles(
|
||||
const std::vector<std::filesystem::path> &envFilePaths,
|
||||
@@ -218,21 +206,115 @@ void EnvKvStore::loadFiles(
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<std::string> EnvKvStore::get(std::string_view name) const
|
||||
std::optional<std::string> EnvKvStore::find(
|
||||
std::string_view name,
|
||||
bool bypassProcessEnvironment) const
|
||||
{
|
||||
std::string ownedName(name);
|
||||
if (const char *value = std::getenv(ownedName.c_str()))
|
||||
if (!bypassProcessEnvironment)
|
||||
{
|
||||
return std::string(value);
|
||||
std::string ownedName(name);
|
||||
if (const char *value = std::getenv(ownedName.c_str())) {
|
||||
return std::string(value);
|
||||
}
|
||||
}
|
||||
|
||||
auto value = values.find(std::string(name));
|
||||
if (value == values.end())
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
if (value == values.end()) { return std::nullopt; }
|
||||
return value->second;
|
||||
}
|
||||
|
||||
std::string EnvKvStore::get(
|
||||
std::string_view name,
|
||||
bool bypassProcessEnvironment) const
|
||||
{
|
||||
std::optional<std::string> value = find(name, bypassProcessEnvironment);
|
||||
if (!value.has_value())
|
||||
{
|
||||
throw std::runtime_error(
|
||||
std::string("EnvKvStore: missing key '")
|
||||
+ std::string(name)
|
||||
+ "'");
|
||||
}
|
||||
return *value;
|
||||
}
|
||||
|
||||
int EnvKvStore::parseInt(std::string_view name, const std::string &raw)
|
||||
{
|
||||
try
|
||||
{
|
||||
std::size_t consumed = 0;
|
||||
const long parsed = std::stol(raw, &consumed);
|
||||
if (consumed != raw.size())
|
||||
{
|
||||
throw std::runtime_error(
|
||||
std::string("EnvKvStore: '")
|
||||
+ std::string(name)
|
||||
+ "' must be an integer, got: "
|
||||
+ raw);
|
||||
}
|
||||
if (parsed < std::numeric_limits<int>::min()
|
||||
|| parsed > std::numeric_limits<int>::max())
|
||||
{
|
||||
throw std::runtime_error(
|
||||
std::string("EnvKvStore: '")
|
||||
+ std::string(name)
|
||||
+ "' is out of int range, got: "
|
||||
+ raw);
|
||||
}
|
||||
return static_cast<int>(parsed);
|
||||
}
|
||||
catch (const std::runtime_error &)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (const std::exception &)
|
||||
{
|
||||
throw std::runtime_error(
|
||||
std::string("EnvKvStore: failed to parse '")
|
||||
+ std::string(name)
|
||||
+ "' as an integer, got: "
|
||||
+ raw);
|
||||
}
|
||||
}
|
||||
|
||||
int EnvKvStore::getIntWithConstraint(
|
||||
std::string_view name,
|
||||
std::optional<int> defaultValue,
|
||||
IntConstraint constraint) const
|
||||
{
|
||||
const std::optional<std::string> raw = find(name);
|
||||
if (!raw.has_value())
|
||||
{
|
||||
if (!defaultValue.has_value())
|
||||
{
|
||||
throw std::runtime_error(
|
||||
std::string("EnvKvStore: missing key '")
|
||||
+ std::string(name)
|
||||
+ "'");
|
||||
}
|
||||
return *defaultValue;
|
||||
}
|
||||
|
||||
const int parsed = parseInt(name, *raw);
|
||||
if (constraint == IntConstraint::NonNegative && parsed < 0)
|
||||
{
|
||||
throw std::runtime_error(
|
||||
std::string("EnvKvStore: '")
|
||||
+ std::string(name)
|
||||
+ "' must be a non-negative integer, got: "
|
||||
+ *raw);
|
||||
}
|
||||
if (constraint == IntConstraint::PositiveNonZero && parsed <= 0)
|
||||
{
|
||||
throw std::runtime_error(
|
||||
std::string("EnvKvStore: '")
|
||||
+ std::string(name)
|
||||
+ "' must be a positive non-zero integer, got: "
|
||||
+ *raw);
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
void EnvKvStore::loadFile(
|
||||
const std::filesystem::path &envFilePath,
|
||||
std::ostream &warningStream)
|
||||
@@ -249,11 +331,9 @@ void EnvKvStore::loadFile(
|
||||
while (std::getline(file, line))
|
||||
{
|
||||
++lineNumber;
|
||||
if (lineIsBlankOrComment(line))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
auto [name, value] = parseAssignment(envFilePath, lineNumber, line);
|
||||
if (DotenvParser::lineIsBlankOrComment(line)) { continue; }
|
||||
auto [name, value] =
|
||||
DotenvParser::parseAssignment(envFilePath, lineNumber, line);
|
||||
storeValue(envFilePath, name, value, warningStream);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user