This commit is contained in:
Mars 2025-05-06 01:12:37 -04:00
parent 816619a162
commit d2fab41864
4 changed files with 298 additions and 206 deletions

View file

@ -63,6 +63,7 @@
#endif #endif
#include "src/util/defs.hpp" #include "src/util/defs.hpp"
#include "src/util/error.hpp" // Added for Result type
#include "src/util/types.hpp" #include "src/util/types.hpp"
#ifndef ARGPARSE_CUSTOM_STRTOF #ifndef ARGPARSE_CUSTOM_STRTOF
@ -231,7 +232,7 @@ namespace argparse {
} }
template <class T, auto Param> template <class T, auto Param>
fn do_from_chars(const StringView s) -> T { fn do_from_chars(const StringView s) -> Result<T> {
T x { 0 }; T x { 0 };
auto [first, last] = pointer_range(s); auto [first, last] = pointer_range(s);
auto [ptr, ec] = std::from_chars(first, last, x, Param); auto [ptr, ec] = std::from_chars(first, last, x, Param);
@ -239,102 +240,94 @@ namespace argparse {
if (ec == std::errc()) { if (ec == std::errc()) {
if (ptr == last) if (ptr == last)
return x; return x;
throw std::invalid_argument { "pattern '" + String(s) + "' does not match to the end" };
return Err(util::error::DracError(util::error::DracErrorCode::ParseError, std::format("pattern '{}' does not match to the end", String(s))));
} }
if (ec == std::errc::invalid_argument) if (ec == std::errc::invalid_argument)
throw std::invalid_argument { "pattern '" + String(s) + "' not found" }; return Err(util::error::DracError(util::error::DracErrorCode::InvalidArgument, std::format("pattern '{}' not found", String(s))));
if (ec == std::errc::result_out_of_range) if (ec == std::errc::result_out_of_range)
throw std::range_error { "'" + String(s) + "' not representable" }; return Err(util::error::DracError(util::error::DracErrorCode::ParseError, std::format("'{}' not representable", String(s))));
return x; // unreachable // Should be unreachable, but handle potential unknown error codes
return Err(util::error::DracError(util::error::DracErrorCode::InternalError, std::format("Unknown parsing error for '{}'", String(s))));
} }
template <class T, auto Param = 0> template <class T, auto Param = 0>
struct parse_number { struct parse_number {
static fn operator()(const StringView s)->T { static fn operator()(const StringView s)->Result<T> {
return do_from_chars<T, Param>(s); return do_from_chars<T, Param>(s);
} }
}; };
template <class T> template <class T>
struct parse_number<T, radix_2> { struct parse_number<T, radix_2> {
static fn operator()(const StringView s)->T { static fn operator()(const StringView s)->Result<T> {
if (auto [ok, rest] = consume_binary_prefix(s); ok) if (auto [ok, rest] = consume_binary_prefix(s); ok)
return do_from_chars<T, radix_2>(rest); return do_from_chars<T, radix_2>(rest);
throw std::invalid_argument { "pattern not found" }; return Err(util::error::DracError(util::error::DracErrorCode::InvalidArgument, "pattern not found"));
} }
}; };
template <class T> template <class T>
struct parse_number<T, radix_16> { struct parse_number<T, radix_16> {
static fn operator()(const StringView s)->T { static fn operator()(const StringView s)->Result<T> {
Result<T> result;
if (starts_with("0x"sv, s) || starts_with("0X"sv, s)) { if (starts_with("0x"sv, s) || starts_with("0X"sv, s)) {
if (auto [ok, rest] = consume_hex_prefix(s); ok) if (auto [ok, rest] = consume_hex_prefix(s); ok)
try { result = do_from_chars<T, radix_16>(rest);
return do_from_chars<T, radix_16>(rest); else
} catch (const std::invalid_argument& err) { return Err(util::error::DracError(util::error::DracErrorCode::InternalError, std::format("Inconsistent hex prefix detection for '{}'", String(s))));
throw std::invalid_argument("Failed to parse '" + String(s) + "' as hexadecimal: " + err.what());
} catch (const std::range_error& err) {
throw std::range_error("Failed to parse '" + String(s) + "' as hexadecimal: " + err.what());
}
} else } else
// Allow passing hex numbers without prefix result = do_from_chars<T, radix_16>(s);
// Shape 'x' already has to be specified
try {
return do_from_chars<T, radix_16>(s);
} catch (const std::invalid_argument& err) {
throw std::invalid_argument("Failed to parse '" + String(s) + "' as hexadecimal: " + err.what());
} catch (const std::range_error& err) {
throw std::range_error("Failed to parse '" + String(s) + "' as hexadecimal: " + err.what());
}
throw std::invalid_argument { "pattern '" + String(s) + if (!result)
"' not identified as hexadecimal" }; return Err(util::error::DracError(result.error().code, std::format("Failed to parse '{}' as hexadecimal: {}", String(s), result.error().message)));
return result;
} }
}; };
template <class T> template <class T>
struct parse_number<T> { struct parse_number<T> {
static fn operator()(const StringView s)->T { static fn operator()(const StringView s)->Result<T> {
auto [ok, rest] = consume_hex_prefix(s); if (auto [ok, rest] = consume_hex_prefix(s); ok) {
Result<T> result = do_from_chars<T, radix_16>(rest);
if (ok) if (!result)
try { return Err(util::error::DracError(result.error().code, std::format("Failed to parse '{}' as hexadecimal: {}", String(s), result.error().message)));
return do_from_chars<T, radix_16>(rest);
} catch (const std::invalid_argument& err) { return result;
throw std::invalid_argument("Failed to parse '" + String(s) + "' as hexadecimal: " + err.what());
} catch (const std::range_error& err) {
throw std::range_error("Failed to parse '" + String(s) + "' as hexadecimal: " + err.what());
} }
if (auto [ok_binary, rest_binary] = consume_binary_prefix(s); ok_binary) if (auto [ok_binary, rest_binary] = consume_binary_prefix(s); ok_binary) {
try { Result<T> result = do_from_chars<T, radix_2>(rest_binary);
return do_from_chars<T, radix_2>(rest_binary);
} catch (const std::invalid_argument& err) { if (!result)
throw std::invalid_argument("Failed to parse '" + String(s) + "' as binary: " + err.what()); return Err(util::error::DracError(result.error().code, std::format("Failed to parse '{}' as binary: {}", String(s), result.error().message)));
} catch (const std::range_error& err) {
throw std::range_error("Failed to parse '" + String(s) + "' as binary: " + err.what()); return result;
} }
if (starts_with("0"sv, s)) // Note: consume_hex_prefix already removed the prefix if present, so 'rest' is correct here for octal/decimal check.
try { if (starts_with("0"sv, s)) { // Check original string for octal prefix
return do_from_chars<T, radix_8>(rest); Result<T> result = do_from_chars<T, radix_8>(s); // Pass original string for octal
} catch (const std::invalid_argument& err) {
throw std::invalid_argument("Failed to parse '" + String(s) + "' as octal: " + err.what()); if (!result)
} catch (const std::range_error& err) { return Err(util::error::DracError(result.error().code, std::format("Failed to parse '{}' as octal: {}", String(s), result.error().message)));
throw std::range_error("Failed to parse '" + String(s) + "' as octal: " + err.what());
return result;
} }
try { Result<T> result = do_from_chars<T, radix_10>(s); // Pass original string for decimal
return do_from_chars<T, radix_10>(rest);
} catch (const std::invalid_argument& err) { if (!result)
throw std::invalid_argument("Failed to parse '" + String(s) + "' as decimal integer: " + err.what()); return Err(util::error::DracError(result.error().code, std::format("Failed to parse '{}' as decimal integer: {}", String(s), result.error().message)));
} catch (const std::range_error& err) {
throw std::range_error("Failed to parse '" + String(s) + "' as decimal integer: " + err.what()); return result;
}
} }
}; };
@ -348,9 +341,9 @@ namespace argparse {
inline const auto generic_strtod<long double> = ARGPARSE_CUSTOM_STRTOLD; inline const auto generic_strtod<long double> = ARGPARSE_CUSTOM_STRTOLD;
template <class T> template <class T>
fn do_strtod(const String& s) -> T { fn do_strtod(const String& s) -> Result<T> {
if (isspace(static_cast<unsigned char>(s[0])) || s[0] == '+') if (isspace(static_cast<unsigned char>(s[0])) || s[0] == '+')
throw std::invalid_argument { "pattern '" + s + "' not found" }; return Err(util::error::DracError(util::error::DracErrorCode::InvalidArgument, std::format("pattern '{}' not found", s)));
auto [first, last] = pointer_range(s); auto [first, last] = pointer_range(s);
@ -364,126 +357,103 @@ namespace argparse {
if (ptr == last) if (ptr == last)
return x; return x;
throw std::invalid_argument { "pattern '" + s + return Err(util::error::DracError(util::error::DracErrorCode::ParseError, std::format("pattern '{}' does not match to the end", s)));
"' does not match to the end" };
} }
if (errno == ERANGE) if (errno == ERANGE)
throw std::range_error { "'" + s + "' not representable" }; return Err(util::error::DracError(util::error::DracErrorCode::ParseError, std::format("'{}' not representable", s)));
return x; // unreachable // Handle other potential errno values
return Err(util::error::DracError(std::error_code(errno, std::system_category())));
} }
template <class T> template <class T>
struct parse_number<T, chars_format::general> { struct parse_number<T, chars_format::general> {
fn operator()(const String& s)->T { fn operator()(const String& s)->Result<T> {
if (auto [is_hex, rest] = consume_hex_prefix(s); is_hex) if (auto [is_hex, rest] = consume_hex_prefix(s); is_hex)
throw std::invalid_argument { return Err(util::error::DracError(util::error::DracErrorCode::InvalidArgument, "chars_format::general does not parse hexfloat"));
"chars_format::general does not parse hexfloat"
};
if (auto [is_bin, rest] = consume_binary_prefix(s); is_bin) if (auto [is_bin, rest] = consume_binary_prefix(s); is_bin)
throw std::invalid_argument { return Err(util::error::DracError(util::error::DracErrorCode::InvalidArgument, "chars_format::general does not parse binfloat"));
"chars_format::general does not parse binfloat"
};
try { Result<T> result = do_strtod<T>(s);
return do_strtod<T>(s); if (!result)
} catch (const std::invalid_argument& err) { return Err(util::error::DracError(result.error().code, std::format("Failed to parse '{}' as number: {}", s, result.error().message)));
throw std::invalid_argument("Failed to parse '" + s + "' as number: " + err.what()); return result;
} catch (const std::range_error& err) {
throw std::range_error("Failed to parse '" + s + "' as number: " + err.what());
}
} }
}; };
template <class T> template <class T>
struct parse_number<T, chars_format::hex> { struct parse_number<T, chars_format::hex> {
fn operator()(const String& s)->T { fn operator()(const String& s)->Result<T> {
if (auto [is_hex, rest] = consume_hex_prefix(s); !is_hex) if (auto [is_hex, rest] = consume_hex_prefix(s); !is_hex)
throw std::invalid_argument { "chars_format::hex parses hexfloat" }; return Err(util::error::DracError(util::error::DracErrorCode::InvalidArgument, "chars_format::hex requires hexfloat format (e.g., 0x1.2p3)"));
if (auto [is_bin, rest] = consume_binary_prefix(s); is_bin) if (auto [is_bin, rest] = consume_binary_prefix(s); is_bin)
throw std::invalid_argument { "chars_format::hex does not parse binfloat" }; return Err(util::error::DracError(util::error::DracErrorCode::InvalidArgument, "chars_format::hex does not parse binfloat"));
try { Result<T> result = do_strtod<T>(s);
return do_strtod<T>(s); if (!result)
} catch (const std::invalid_argument& err) { return Err(util::error::DracError(result.error().code, std::format("Failed to parse '{}' as hexadecimal float: {}", s, result.error().message)));
throw std::invalid_argument("Failed to parse '" + s + "' as hexadecimal: " + err.what()); return result;
} catch (const std::range_error& err) {
throw std::range_error("Failed to parse '" + s + "' as hexadecimal: " + err.what());
}
} }
}; };
template <class T> template <class T>
struct parse_number<T, chars_format::binary> { struct parse_number<T, chars_format::binary> {
fn operator()(const String& s)->T { fn operator()(const String& s)->Result<T> {
if (auto [is_hex, rest] = consume_hex_prefix(s); is_hex) if (auto [is_hex, rest] = consume_hex_prefix(s); is_hex)
throw std::invalid_argument { return Err(util::error::DracError(util::error::DracErrorCode::InvalidArgument, "chars_format::binary does not parse hexfloat"));
"chars_format::binary does not parse hexfloat"
};
if (auto [is_bin, rest] = consume_binary_prefix(s); !is_bin) if (auto [is_bin, rest] = consume_binary_prefix(s); !is_bin)
throw std::invalid_argument { "chars_format::binary parses binfloat" }; return Err(util::error::DracError(util::error::DracErrorCode::InvalidArgument, "chars_format::binary requires binfloat format (e.g., 0b1.01p2)"));
return do_strtod<T>(s); Result<T> result = do_strtod<T>(s);
if (!result)
return Err(util::error::DracError(result.error().code, std::format("Failed to parse '{}' as binary float: {}", s, result.error().message)));
return result;
} }
}; };
template <class T> template <class T>
struct parse_number<T, chars_format::scientific> { struct parse_number<T, chars_format::scientific> {
fn operator()(const String& s)->T { fn operator()(const String& s)->Result<T> {
if (const auto [is_hex, rest] = consume_hex_prefix(s); is_hex) if (const auto [is_hex, rest] = consume_hex_prefix(s); is_hex)
throw std::invalid_argument { return Err(util::error::DracError(util::error::DracErrorCode::InvalidArgument, "chars_format::scientific does not parse hexfloat"));
"chars_format::scientific does not parse hexfloat"
};
if (const auto [is_bin, rest] = consume_binary_prefix(s); is_bin) if (const auto [is_bin, rest] = consume_binary_prefix(s); is_bin)
throw std::invalid_argument { return Err(util::error::DracError(util::error::DracErrorCode::InvalidArgument, "chars_format::scientific does not parse binfloat"));
"chars_format::scientific does not parse binfloat"
};
if (s.find_first_of("eE") == String::npos) if (s.find_first_of("eE") == String::npos)
throw std::invalid_argument { return Err(util::error::DracError(util::error::DracErrorCode::InvalidArgument, "chars_format::scientific requires exponent part"));
"chars_format::scientific requires exponent part"
};
try { Result<T> result = do_strtod<T>(s);
return do_strtod<T>(s);
} catch (const std::invalid_argument& err) { if (!result)
throw std::invalid_argument("Failed to parse '" + s + "' as scientific notation: " + err.what()); return Err(util::error::DracError(result.error().code, std::format("Failed to parse '{}' as scientific notation: {}", s, result.error().message)));
} catch (const std::range_error& err) {
throw std::range_error("Failed to parse '" + s + "' as scientific notation: " + err.what()); return result;
}
} }
}; };
template <class T> template <class T>
struct parse_number<T, chars_format::fixed> { struct parse_number<T, chars_format::fixed> {
fn operator()(const String& s)->T { fn operator()(const String& s)->Result<T> {
if (const auto [is_hex, rest] = consume_hex_prefix(s); is_hex) if (const auto [is_hex, rest] = consume_hex_prefix(s); is_hex)
throw std::invalid_argument { return Err(util::error::DracError(util::error::DracErrorCode::InvalidArgument, "chars_format::fixed does not parse hexfloat"));
"chars_format::fixed does not parse hexfloat"
};
if (const auto [is_bin, rest] = consume_binary_prefix(s); is_bin) if (const auto [is_bin, rest] = consume_binary_prefix(s); is_bin)
throw std::invalid_argument { return Err(util::error::DracError(util::error::DracErrorCode::InvalidArgument, "chars_format::fixed does not parse binfloat"));
"chars_format::fixed does not parse binfloat"
};
if (s.find_first_of("eE") != String::npos) if (s.find_first_of("eE") != String::npos)
throw std::invalid_argument { return Err(util::error::DracError(util::error::DracErrorCode::InvalidArgument, "chars_format::fixed does not parse exponent part"));
"chars_format::fixed does not parse exponent part"
};
try { Result<T> result = do_strtod<T>(s);
return do_strtod<T>(s);
} catch (const std::invalid_argument& err) { if (!result)
throw std::invalid_argument("Failed to parse '" + s + "' as fixed notation: " + err.what()); return Err(util::error::DracError(result.error().code, std::format("Failed to parse '{}' as fixed notation: {}", s, result.error().message)));
} catch (const std::range_error& err) {
throw std::range_error("Failed to parse '" + s + "' as fixed notation: " + err.what()); return result;
}
} }
}; };
@ -707,7 +677,12 @@ namespace argparse {
var = std::any_cast<T>(m_default_value); var = std::any_cast<T>(m_default_value);
action([&var](const auto& s) { action([&var](const auto& s) {
var = details::parse_number<T, details::radix_10>()(s); Result<T> result = details::parse_number<T, details::radix_10>()(s);
if (!result)
throw std::runtime_error(std::format("Failed to parse '{}' as decimal integer: {}", s, result.error().message));
var = *result;
return var; return var;
}); });
@ -720,7 +695,12 @@ namespace argparse {
var = std::any_cast<T>(m_default_value); var = std::any_cast<T>(m_default_value);
action([&var](const auto& s) { action([&var](const auto& s) {
var = details::parse_number<T, details::chars_format::general>()(s); Result<T> result = details::parse_number<T, details::chars_format::general>()(s);
if (!result)
throw std::runtime_error(std::format("Failed to parse '{}' as number: {}", s, result.error().message));
var = *result;
return var; return var;
}); });
@ -774,7 +754,13 @@ namespace argparse {
var.clear(); var.clear();
m_is_used = true; m_is_used = true;
var.push_back(details::parse_number<int, details::radix_10>()(s));
Result<int> result = details::parse_number<int, details::radix_10>()(s);
if (!result)
throw std::runtime_error(std::format("Failed to parse '{}' as decimal integer for vector: {}", s, result.error().message));
var.push_back(*result);
return var; return var;
}); });
@ -806,7 +792,13 @@ namespace argparse {
var.clear(); var.clear();
m_is_used = true; m_is_used = true;
var.insert(details::parse_number<int, details::radix_10>()(s));
Result<int> result = details::parse_number<int, details::radix_10>()(s);
if (!result)
throw std::runtime_error(std::format("Failed to parse '{}' as decimal integer for set: {}", s, result.error().message));
var.insert(*result);
return var; return var;
}); });
@ -833,25 +825,75 @@ namespace argparse {
}; };
if constexpr (Shape == 'd' && std::is_integral_v<T>) if constexpr (Shape == 'd' && std::is_integral_v<T>)
action(details::parse_number<T, details::radix_10>()); action([](const String& s) -> T {
Result<T> result = details::parse_number<T, details::radix_10>()(s);
if (!result)
throw std::runtime_error(std::format("Failed to parse '{}' as decimal integer (scan 'd'): {}", s, result.error().message));
return *result;
});
else if constexpr (Shape == 'i' && std::is_integral_v<T>) else if constexpr (Shape == 'i' && std::is_integral_v<T>)
action(details::parse_number<T>()); action([](const String& s) -> T {
Result<T> result = details::parse_number<T>()(s);
if (!result)
throw std::runtime_error(std::format("Failed to parse '{}' as integer (scan 'i'): {}", s, result.error().message));
return *result;
});
else if constexpr (Shape == 'u' && (std::is_integral_v<T> && std::is_unsigned_v<T>)) else if constexpr (Shape == 'u' && (std::is_integral_v<T> && std::is_unsigned_v<T>))
action(details::parse_number<T, details::radix_10>()); action([](const String& s) -> T {
Result<T> result = details::parse_number<T, details::radix_10>()(s);
if (!result)
throw std::runtime_error(std::format("Failed to parse '{}' as unsigned decimal integer (scan 'u'): {}", s, result.error().message));
return *result;
});
else if constexpr (Shape == 'b' && (std::is_integral_v<T> && std::is_unsigned_v<T>)) else if constexpr (Shape == 'b' && (std::is_integral_v<T> && std::is_unsigned_v<T>))
action(details::parse_number<T, details::radix_2>()); action([](const String& s) -> T {
Result<T> result = details::parse_number<T, details::radix_2>()(s);
if (!result)
throw std::runtime_error(std::format("Failed to parse '{}' as binary integer (scan 'b'): {}", s, result.error().message));
return *result;
});
else if constexpr (Shape == 'o' && (std::is_integral_v<T> && std::is_unsigned_v<T>)) else if constexpr (Shape == 'o' && (std::is_integral_v<T> && std::is_unsigned_v<T>))
action(details::parse_number<T, details::radix_8>()); action([](const String& s) -> T {
Result<T> result = details::parse_number<T, details::radix_8>()(s);
if (!result)
throw std::runtime_error(std::format("Failed to parse '{}' as octal integer (scan 'o'): {}", s, result.error().message));
return *result;
});
else if constexpr (is_one_of(Shape, 'x', 'X') && (std::is_integral_v<T> && std::is_unsigned_v<T>)) else if constexpr (is_one_of(Shape, 'x', 'X') && (std::is_integral_v<T> && std::is_unsigned_v<T>))
action(details::parse_number<T, details::radix_16>()); action([](const String& s) -> T {
Result<T> result = details::parse_number<T, details::radix_16>()(s);
if (!result)
throw std::runtime_error(std::format("Failed to parse '{}' as hexadecimal integer (scan '{}'): {}", s, Shape, result.error().message));
return *result;
});
else if constexpr (is_one_of(Shape, 'a', 'A') && std::is_floating_point_v<T>) else if constexpr (is_one_of(Shape, 'a', 'A') && std::is_floating_point_v<T>)
action(details::parse_number<T, details::chars_format::hex>()); action([](const String& s) -> T {
Result<T> result = details::parse_number<T, details::chars_format::hex>()(s);
if (!result)
throw std::runtime_error(std::format("Failed to parse '{}' as hexadecimal float (scan '{}'): {}", s, Shape, result.error().message));
return *result;
});
else if constexpr (is_one_of(Shape, 'e', 'E') && std::is_floating_point_v<T>) else if constexpr (is_one_of(Shape, 'e', 'E') && std::is_floating_point_v<T>)
action(details::parse_number<T, details::chars_format::scientific>()); action([](const String& s) -> T {
Result<T> result = details::parse_number<T, details::chars_format::scientific>()(s);
if (!result)
throw std::runtime_error(std::format("Failed to parse '{}' as scientific float (scan '{}'): {}", s, Shape, result.error().message));
return *result;
});
else if constexpr (is_one_of(Shape, 'f', 'F') && std::is_floating_point_v<T>) else if constexpr (is_one_of(Shape, 'f', 'F') && std::is_floating_point_v<T>)
action(details::parse_number<T, details::chars_format::fixed>()); action([](const String& s) -> T {
Result<T> result = details::parse_number<T, details::chars_format::fixed>()(s);
if (!result)
throw std::runtime_error(std::format("Failed to parse '{}' as fixed float (scan '{}'): {}", s, Shape, result.error().message));
return *result;
});
else if constexpr (is_one_of(Shape, 'g', 'G') && std::is_floating_point_v<T>) else if constexpr (is_one_of(Shape, 'g', 'G') && std::is_floating_point_v<T>)
action(details::parse_number<T, details::chars_format::general>()); action([](const String& s) -> T {
Result<T> result = details::parse_number<T, details::chars_format::general>()(s);
if (!result)
throw std::runtime_error(std::format("Failed to parse '{}' as general float (scan '{}'): {}", s, Shape, result.error().message));
return *result;
});
else else
static_assert(false, "No scan specification for T"); static_assert(false, "No scan specification for T");
@ -1070,26 +1112,72 @@ namespace argparse {
} }
/* /*
* @throws std::runtime_error if argument values are not valid * @returns Result<void> indicating success or failure
*/ */
fn validate() const -> void { [[nodiscard]] fn validate() const -> Result<void> {
if (m_is_optional) { if (m_is_optional) {
// TODO: check if an implicit value was programmed for this argument // TODO: check if an implicit value was programmed for this argument
if (!m_is_used && !m_default_value.has_value() && m_is_required) if (!m_is_used && !m_default_value.has_value() && m_is_required)
throw_required_arg_not_used_error(); return Err(util::error::DracError(util::error::DracErrorCode::InvalidArgument, std::format("Required argument '{}' was not provided", m_names[0])));
if (m_is_used && m_is_required && m_values.empty()) if (m_is_used && m_is_required && m_values.empty())
throw_required_arg_no_value_provided_error(); return Err(util::error::DracError(util::error::DracErrorCode::InvalidArgument, std::format("Required argument '{}' requires a value, but none was provided", m_names[0])));
if (m_is_used && m_num_args_range.get_min() > m_values.size())
return Err(util::error::DracError(util::error::DracErrorCode::InvalidArgument, std::format("Too few arguments for optional argument '{}'. Expected at least {}, got {}.", m_names[0], m_num_args_range.get_min(), m_values.size())));
} else { } else {
if (!m_num_args_range.contains(m_values.size()) && if (!m_num_args_range.contains(m_values.size()) && !m_default_value.has_value()) {
!m_default_value.has_value()) String expected_str;
throw_nargs_range_validation_error();
if (m_num_args_range.is_exact())
expected_str = std::to_string(m_num_args_range.get_min());
else if (!m_num_args_range.is_right_bounded())
expected_str = std::format("at least {}", m_num_args_range.get_min());
else
expected_str = std::format("{} to {}", m_num_args_range.get_min(), m_num_args_range.get_max());
return Err(DracError(util::error::DracErrorCode::InvalidArgument, std::format("Incorrect number of arguments for positional argument '{}'. Expected {}, got {}.", (m_metavar.empty() ? m_names[0] : m_metavar), expected_str, m_values.size())));
} }
if (m_choices.has_value()) if (m_num_args_range.get_min() > m_values.size())
// Make sure the default value (if provided) return Err(DracError(util::error::DracErrorCode::InvalidArgument, std::format("Too few arguments for positional argument '{}'. Expected at least {}, got {}.", (m_metavar.empty() ? m_names[0] : m_metavar), m_num_args_range.get_min(), m_values.size())));
// is in the list of choices }
find_default_value_in_choices_or_throw();
if (m_num_args_range.get_max() < m_values.size()) {
if (m_is_optional)
return Err(DracError(util::error::DracErrorCode::InvalidArgument, std::format("Too many arguments for optional argument '{}'. Expected at most {}, got {}.", m_names[0], m_num_args_range.get_max(), m_values.size())));
return Err(DracError(util::error::DracErrorCode::InvalidArgument, std::format("Too many arguments for positional argument '{}'. Expected at most {}, got {}.", (m_metavar.empty() ? m_names[0] : m_metavar), m_num_args_range.get_max(), m_values.size())));
}
if (m_choices.has_value()) {
const Vec<String>& choices = m_choices.value();
// Check default value
if (m_default_value.has_value())
if (const String& default_val_str = m_default_value_str.value(); std::ranges::find(choices, default_val_str) == choices.end()) {
const String choices_as_csv = std::accumulate(
choices.begin(), choices.end(), String(), [](const String& option_a, const String& option_b) -> String { return option_a + (option_a.empty() ? "" : ", ") + option_b; }
);
return Err(DracError(util::error::DracErrorCode::InvalidArgument, std::format("Default value '{}' is not in the allowed choices: {{{}}}", default_val_str, choices_as_csv)));
}
// Check provided values
for (const auto& value_any : m_values) {
if (value_any.type() != typeid(String))
return Err(DracError(util::error::DracErrorCode::InvalidArgument, std::format("Invalid argument type for choice validation - expected string, got '{}'", value_any.type().name())));
if (const String& value = std::any_cast<const String&>(value_any); std::ranges::find(choices, value) == choices.end()) {
const String choices_as_csv = std::accumulate(
choices.begin(), choices.end(), String(), [](const String& option_a, const String& option_b) -> String { return std::format("{}{}{}", option_a, option_a.empty() ? "" : ", ", option_b); }
);
return Err(DracError(util::error::DracErrorCode::InvalidArgument, std::format("Invalid argument '{}' - allowed options: {{{}}}", value, choices_as_csv)));
}
}
}
return {};
} }
[[nodiscard]] fn get_names_csv(const char separator = ',') const -> String { [[nodiscard]] fn get_names_csv(const char separator = ',') const -> String {
@ -1432,7 +1520,8 @@ namespace argparse {
// precondition: we have consumed or will consume at least one digit // precondition: we have consumed or will consume at least one digit
fn consume_digits = [=](StringView sd) -> StringView { fn consume_digits = [=](StringView sd) -> StringView {
const char* const it = std::ranges::find_if_not(sd, is_digit); const auto it = std::ranges::find_if_not(sd, is_digit);
return sd.substr(static_cast<usize>(it - std::begin(sd))); return sd.substr(static_cast<usize>(it - std::begin(sd)));
}; };
@ -1848,7 +1937,8 @@ namespace argparse {
parse_args_internal(arguments); parse_args_internal(arguments);
// Check if all arguments are parsed // Check if all arguments are parsed
for (const auto& argument : m_argument_map | std::views::values) for (const auto& argument : m_argument_map | std::views::values)
argument->validate(); if (Result<> validation_result = argument->validate(); !validation_result)
throw std::runtime_error(validation_result.error().message);
// Check each mutually exclusive group and make sure // Check each mutually exclusive group and make sure
// there are no constraint violations // there are no constraint violations
@ -1896,8 +1986,10 @@ namespace argparse {
fn parse_known_args(const Vec<String>& arguments) -> Vec<String> { fn parse_known_args(const Vec<String>& arguments) -> Vec<String> {
Vec<String> unknown_arguments = parse_known_args_internal(arguments); Vec<String> unknown_arguments = parse_known_args_internal(arguments);
for (const auto& argument : m_argument_map | std::views::values) for (const auto& argument : m_argument_map | std::views::values) {
argument->validate(); if (Result<> validation_result = argument->validate(); !validation_result)
throw std::runtime_error(validation_result.error().message);
}
return unknown_arguments; return unknown_arguments;
} }

View file

@ -1,3 +1,4 @@
#define NOMINMAX
#include "weather.hpp" #include "weather.hpp"
#include <chrono> // std::chrono::{duration, operator-} #include <chrono> // std::chrono::{duration, operator-}

View file

@ -24,7 +24,10 @@ using util::types::i32;
fn main(const i32 argc, char* argv[]) -> i32 { fn main(const i32 argc, char* argv[]) -> i32 {
using namespace ftxui; using namespace ftxui;
using os::SystemData; using os::SystemData;
using util::types::Exception;
// TODO: don't wrap all of this in a try-catch
try {
#ifdef _WIN32 #ifdef _WIN32
winrt::init_apartment(); winrt::init_apartment();
#endif #endif
@ -42,19 +45,7 @@ fn main(const i32 argc, char* argv[]) -> i32 {
.help("Enable verbose logging. Alias for --log-level=debug") .help("Enable verbose logging. Alias for --log-level=debug")
.flag(); .flag();
try {
parser.parse_args(argc, argv); parser.parse_args(argc, argv);
} catch (const util::types::Exception& err) {
#ifdef __cpp_lib_print
std::println(stderr, "{}", err.what());
#else
std::cerr << err.what() << '\n';
#endif
std::cerr << parser;
return 1;
}
if (parser["--verbose"] == true || parser["-v"] == true) if (parser["--verbose"] == true || parser["-v"] == true)
info_log("Verbose logging enabled"); info_log("Verbose logging enabled");
@ -73,6 +64,13 @@ fn main(const i32 argc, char* argv[]) -> i32 {
#else #else
std::cout << '\n'; std::cout << '\n';
#endif #endif
} catch (const Exception& e) {
error_log("Exception: {}", e.what());
return 1;
} catch (...) {
error_log("Unknown exception");
return 1;
}
return 0; return 0;
} }

View file

@ -6,6 +6,7 @@
#include <system_error> // std::error_code #include <system_error> // std::error_code
#ifdef _WIN32 #ifdef _WIN32
#include <guiddef.h> // GUID
#include <winerror.h> // error values #include <winerror.h> // error values
#include <winrt/base.h> // winrt::hresult_error #include <winrt/base.h> // winrt::hresult_error
#endif #endif
@ -77,14 +78,14 @@ namespace util {
} }
#ifdef _WIN32 #ifdef _WIN32
explicit DracError(const winrt::hresult_error& e) explicit DracError(const winrt::hresult_error& err)
: message(winrt::to_string(e.message())) { : message(winrt::to_string(err.message())) {
using namespace matchit; using namespace matchit;
using enum DracErrorCode; using enum DracErrorCode;
fn fromWin32 = [](const types::u32 x) -> HRESULT { return HRESULT_FROM_WIN32(x); }; fn fromWin32 = [](const types::u32 win32) -> HRESULT { return HRESULT_FROM_WIN32(win32); };
code = match(e.code())( code = match(err.code())(
is | or_(E_ACCESSDENIED, fromWin32(ERROR_ACCESS_DENIED)) = PermissionDenied, is | or_(E_ACCESSDENIED, fromWin32(ERROR_ACCESS_DENIED)) = PermissionDenied,
is | fromWin32(ERROR_FILE_NOT_FOUND) = NotFound, is | fromWin32(ERROR_FILE_NOT_FOUND) = NotFound,
is | fromWin32(ERROR_PATH_NOT_FOUND) = NotFound, is | fromWin32(ERROR_PATH_NOT_FOUND) = NotFound,