From c411be9ca4d796244b19f38646955d66b39d4e13 Mon Sep 17 00:00:00 2001 From: Stephane Janel Date: Sun, 12 Nov 2023 10:15:11 +0100 Subject: [PATCH] Easier usage as library - parseExchanges does not require to be last anymore --- src/engine/include/coincentercommands.hpp | 2 - .../include/commandlineoptionsparser.hpp | 1 + .../commandlineoptionsparseriterator.hpp | 0 src/engine/include/parseoptions.hpp | 54 +++++++++++++++++++ src/engine/include/stringoptionparser.hpp | 7 ++- src/engine/src/coincentercommandfactory.cpp | 4 -- src/engine/src/coincentercommands.cpp | 50 ----------------- src/engine/src/stringoptionparser.cpp | 32 ++++++++--- .../test/coincentercommandfactory_test.cpp | 21 +++++++- src/engine/test/stringoptionparser_test.cpp | 17 ++++++ src/main/src/main.cpp | 12 +++-- src/objects/include/exchangename.hpp | 5 +- src/objects/src/exchangename.cpp | 3 ++ 13 files changed, 134 insertions(+), 74 deletions(-) rename src/engine/{src => include}/commandlineoptionsparseriterator.hpp (100%) create mode 100644 src/engine/include/parseoptions.hpp diff --git a/src/engine/include/coincentercommands.hpp b/src/engine/include/coincentercommands.hpp index d4b9bf8e..d223a02d 100644 --- a/src/engine/include/coincentercommands.hpp +++ b/src/engine/include/coincentercommands.hpp @@ -17,8 +17,6 @@ class CoincenterCommands { // Builds a CoincenterCommands and add commands from given command line options span. explicit CoincenterCommands(std::span cmdLineOptionsSpan); - static vector ParseOptions(int argc, const char *argv[]); - /// @brief Set this CoincenterCommands from given command line options. void addOption(const CoincenterCmdLineOptions &cmdLineOptions, const CoincenterCommand *pPreviousCommand); diff --git a/src/engine/include/commandlineoptionsparser.hpp b/src/engine/include/commandlineoptionsparser.hpp index e3fa1398..2bdee22a 100644 --- a/src/engine/include/commandlineoptionsparser.hpp +++ b/src/engine/include/commandlineoptionsparser.hpp @@ -39,6 +39,7 @@ class CommandLineOptionsParser { public: using CommandLineOptionType = AllowedCommandLineOptionsBase::CommandLineOptionType; using CommandLineOptionWithValue = AllowedCommandLineOptionsBase::CommandLineOptionWithValue; + using value_type = OptValueType; template explicit CommandLineOptionsParser(const CommandLineOptionWithValue (&init)[N]) { diff --git a/src/engine/src/commandlineoptionsparseriterator.hpp b/src/engine/include/commandlineoptionsparseriterator.hpp similarity index 100% rename from src/engine/src/commandlineoptionsparseriterator.hpp rename to src/engine/include/commandlineoptionsparseriterator.hpp diff --git a/src/engine/include/parseoptions.hpp b/src/engine/include/parseoptions.hpp new file mode 100644 index 00000000..30325f3e --- /dev/null +++ b/src/engine/include/parseoptions.hpp @@ -0,0 +1,54 @@ +#pragma once + +#include +#include +#include +#include + +#include "cct_vector.hpp" +#include "coincenteroptions.hpp" +#include "commandlineoptionsparseriterator.hpp" + +namespace cct { +template +auto ParseOptions(ParserType &parser, int argc, const char *argv[]) { + auto programName = std::filesystem::path(argv[0]).filename().string(); + + std::span allArguments(argv, argc); + + // skip first argument which is program name + CommandLineOptionsParserIterator parserIt(parser, allArguments.last(allArguments.size() - 1U)); + + using OptValueType = ParserType::value_type; + OptValueType globalOptions; + + vector parsedOptions; + + // Support for command line multiple commands. Only full name flags are supported for multi command line commands. + while (parserIt.hasNext()) { + auto groupedArguments = parserIt.next(); + + auto groupParsedOptions = parser.parse(groupedArguments); + globalOptions.mergeGlobalWith(groupParsedOptions); + + if (groupedArguments.empty()) { + groupParsedOptions.help = true; + } + if (groupParsedOptions.help) { + parser.displayHelp(programName, std::cout); + } else if (groupParsedOptions.version) { + CoincenterCmdLineOptions::PrintVersion(programName, std::cout); + } else { + // Only store commands if they are not 'help' nor 'version' + parsedOptions.push_back(std::move(groupParsedOptions)); + } + } + + // Apply global options to all parsed options containing commands + for (auto &groupParsedOptions : parsedOptions) { + groupParsedOptions.mergeGlobalWith(globalOptions); + } + + return parsedOptions; +} +} // namespace cct \ No newline at end of file diff --git a/src/engine/include/stringoptionparser.hpp b/src/engine/include/stringoptionparser.hpp index 869ef8bd..e3a468f4 100644 --- a/src/engine/include/stringoptionparser.hpp +++ b/src/engine/include/stringoptionparser.hpp @@ -18,8 +18,10 @@ class StringOptionParser { enum class AmountType : int8_t { kAbsolute, kPercentage, kNotPresent }; enum class FieldIs : int8_t { kMandatory, kOptional }; + /// Constructs an empty StringOptionParser that will not be able to parse anything. StringOptionParser() noexcept = default; + /// Constructs a StringOptionParser from a full option string. explicit StringOptionParser(std::string_view optFullStr) : _opt(optFullStr) {} /// If FieldIs is kOptional and there is no currency, default currency code will be returned. @@ -39,8 +41,11 @@ class StringOptionParser { /// Parse exchanges. /// Exception will be raised for any invalid exchange name - but an empty list of exchanges is accepted. - ExchangeNames parseExchanges(char sep = ','); + /// 'exchangesSep' and 'endExchangesSep' should be different, otherwise parsing would not be possible + ExchangeNames parseExchanges(char exchangesSep = ',', char endExchangesSep = '\0'); + /// Call this method when the end of parsing of this option is expected. + /// If the option has not been fully parsed at this step, exception 'invalid_argument' will be raised. void checkEndParsing() const; private: diff --git a/src/engine/src/coincentercommandfactory.cpp b/src/engine/src/coincentercommandfactory.cpp index 80529f9f..7120b364 100644 --- a/src/engine/src/coincentercommandfactory.cpp +++ b/src/engine/src/coincentercommandfactory.cpp @@ -43,10 +43,6 @@ CoincenterCommand CoincenterCommandFactory::createOrderCommand(CoincenterCommand CoincenterCommand CoincenterCommandFactory::createTradeCommand(CoincenterCommandType type, StringOptionParser &optionParser) { - if (!_cmdLineOptions.tradeStrategy.empty() && !_cmdLineOptions.tradePrice.empty()) { - throw invalid_argument("Trade price and trade strategy cannot be set together"); - } - CoincenterCommand command(type); command.setTradeOptions(_cmdLineOptions.computeTradeOptions()); diff --git a/src/engine/src/coincentercommands.cpp b/src/engine/src/coincentercommands.cpp index e2ac50a2..aa7697fb 100644 --- a/src/engine/src/coincentercommands.cpp +++ b/src/engine/src/coincentercommands.cpp @@ -1,20 +1,14 @@ #include "coincentercommands.hpp" #include -#include -#include #include #include #include -#include "cct_vector.hpp" #include "coincentercommand.hpp" #include "coincentercommandfactory.hpp" #include "coincentercommandtype.hpp" #include "coincenteroptions.hpp" -#include "coincenteroptionsdef.hpp" -#include "commandlineoptionsparser.hpp" -#include "commandlineoptionsparseriterator.hpp" #include "currencycode.hpp" #include "depositsconstraints.hpp" #include "stringoptionparser.hpp" @@ -23,50 +17,6 @@ namespace cct { -vector CoincenterCommands::ParseOptions(int argc, const char *argv[]) { - using OptValueType = CoincenterCmdLineOptions; - - auto parser = CommandLineOptionsParser(CoincenterAllowedOptions::value); - - auto programName = std::filesystem::path(argv[0]).filename().string(); - - vector parsedOptions; - - std::span allArguments(argv, argc); - allArguments = allArguments.last(allArguments.size() - 1U); // skip first argument which is program name - - CommandLineOptionsParserIterator parserIt(parser, allArguments); - - CoincenterCmdLineOptions globalOptions; - - // Support for command line multiple commands. Only full name flags are supported for multi command line commands. - while (parserIt.hasNext()) { - std::span groupedArguments = parserIt.next(); - - CoincenterCmdLineOptions groupParsedOptions = parser.parse(groupedArguments); - globalOptions.mergeGlobalWith(groupParsedOptions); - - if (groupedArguments.empty()) { - groupParsedOptions.help = true; - } - if (groupParsedOptions.help) { - parser.displayHelp(programName, std::cout); - } else if (groupParsedOptions.version) { - CoincenterCmdLineOptions::PrintVersion(programName, std::cout); - } else { - // Only store commands if they are not 'help' nor 'version' - parsedOptions.push_back(std::move(groupParsedOptions)); - } - } - - // Apply global options to all parsed options containing commands - for (CoincenterCmdLineOptions &groupParsedOptions : parsedOptions) { - groupParsedOptions.mergeGlobalWith(globalOptions); - } - - return parsedOptions; -} - CoincenterCommands::CoincenterCommands(std::span cmdLineOptionsSpan) { _commands.reserve(cmdLineOptionsSpan.size()); const CoincenterCommand *pPreviousCommand = nullptr; diff --git a/src/engine/src/stringoptionparser.cpp b/src/engine/src/stringoptionparser.cpp index c6742abc..ba3c22ae 100644 --- a/src/engine/src/stringoptionparser.cpp +++ b/src/engine/src/stringoptionparser.cpp @@ -148,7 +148,7 @@ std::pair StringOptionParser::pa vector StringOptionParser::getCSVValues() { vector ret; if (!_opt.empty()) { - do { + while (true) { auto nextCommaPos = _opt.find(',', _pos); if (nextCommaPos == std::string_view::npos) { nextCommaPos = _opt.size(); @@ -160,27 +160,43 @@ vector StringOptionParser::getCSVValues() { break; } _pos = nextCommaPos + 1; - } while (true); + } } return ret; } -ExchangeNames StringOptionParser::parseExchanges(char sep) { - std::string_view str(_opt.begin() + _pos, _opt.end()); +ExchangeNames StringOptionParser::parseExchanges(char exchangesSep, char endExchangesSep) { + if (exchangesSep == endExchangesSep) { + throw invalid_argument("Exchanges separator cannot be the same as end exchanges separator"); + } + auto endPos = _opt.find(endExchangesSep, _pos); + if (endPos == std::string_view::npos) { + endPos = _opt.size(); + } + std::string_view str(_opt.begin() + _pos, _opt.begin() + endPos); ExchangeNames exchanges; if (!str.empty()) { - std::size_t first; - std::size_t last; - for (first = 0, last = str.find(sep); last != std::string_view::npos; last = str.find(sep, last + 1)) { + std::size_t first = 0; + std::size_t last = str.find(exchangesSep); + for (; last != std::string_view::npos; last = str.find(exchangesSep, last + 1)) { std::string_view exchangeNameStr(str.begin() + first, str.begin() + last); + if (!ExchangeName::IsValid(exchangeNameStr)) { + return exchanges; + } exchanges.emplace_back(exchangeNameStr); first = last + 1; _pos += exchangeNameStr.size() + 1U; } - // Add the last one as well + // Add the last one as well, if it is an exchange name std::string_view exchangeNameStr(str.begin() + first, str.end()); + if (!ExchangeName::IsValid(exchangeNameStr)) { + return exchanges; + } exchanges.emplace_back(exchangeNameStr); _pos += exchangeNameStr.size(); + if (_pos < _opt.size() && _opt[_pos] == endExchangesSep) { + ++_pos; + } } return exchanges; } diff --git a/src/engine/test/coincentercommandfactory_test.cpp b/src/engine/test/coincentercommandfactory_test.cpp index 0cebd086..5ce1fcd9 100644 --- a/src/engine/test/coincentercommandfactory_test.cpp +++ b/src/engine/test/coincentercommandfactory_test.cpp @@ -33,6 +33,7 @@ TEST_F(CoincenterCommandFactoryTest, CreateMarketCommandInvalidInputTest) { TEST_F(CoincenterCommandFactoryTest, CreateMarketCommandMarketTest) { EXPECT_EQ(CoincenterCommandFactory::CreateMarketCommand(inputStr("eth-usdt")), CoincenterCommand(CoincenterCommandType::kMarkets).setCur1("ETH").setCur2("USDT")); + EXPECT_NO_THROW(optionParser.checkEndParsing()); } TEST_F(CoincenterCommandFactoryTest, CreateMarketCommandSingleCurTest) { @@ -40,17 +41,20 @@ TEST_F(CoincenterCommandFactoryTest, CreateMarketCommandSingleCurTest) { CoincenterCommand(CoincenterCommandType::kMarkets) .setCur1("XLM") .setExchangeNames(ExchangeNames({ExchangeName("kraken"), ExchangeName("binance", "user1")}))); + EXPECT_NO_THROW(optionParser.checkEndParsing()); } TEST_F(CoincenterCommandFactoryTest, CreateOrderCommandAll) { CoincenterCommandType type = CoincenterCommandType::kOrdersOpened; EXPECT_EQ(commandFactory.createOrderCommand(type, inputStr("")), CoincenterCommand(type)); + EXPECT_NO_THROW(optionParser.checkEndParsing()); } TEST_F(CoincenterCommandFactoryTest, CreateOrderCommandSingleCur) { CoincenterCommandType type = CoincenterCommandType::kOrdersOpened; EXPECT_EQ(commandFactory.createOrderCommand(type, inputStr("AVAX")), CoincenterCommand(type).setOrdersConstraints(OrdersConstraints("AVAX"))); + EXPECT_NO_THROW(optionParser.checkEndParsing()); } TEST_F(CoincenterCommandFactoryTest, CreateOrderCommandMarketWithExchange) { @@ -59,6 +63,7 @@ TEST_F(CoincenterCommandFactoryTest, CreateOrderCommandMarketWithExchange) { CoincenterCommand(type) .setOrdersConstraints(OrdersConstraints("AVAX", "BTC")) .setExchangeNames(ExchangeNames({ExchangeName("huobi")}))); + EXPECT_NO_THROW(optionParser.checkEndParsing()); } TEST_F(CoincenterCommandFactoryTest, CreateTradeInvalidNegativeAmount) { @@ -68,8 +73,10 @@ TEST_F(CoincenterCommandFactoryTest, CreateTradeInvalidNegativeAmount) { TEST_F(CoincenterCommandFactoryTest, CreateTradeInvalidSeveralTrades) { CoincenterCommandType type = CoincenterCommandType::kTrade; - cmdLineOptions.buy = "100%USDT"; - EXPECT_THROW(commandFactory.createTradeCommand(type, inputStr("13XRP-BTC,binance_user2")), invalid_argument); + cmdLineOptions.buy = "100%USDT"; // to set isSmartTrade to true, such that currency will not be parsed + commandFactory.createTradeCommand(type, inputStr("13XRP-BTC,binance_user2")); + + EXPECT_THROW(optionParser.checkEndParsing(), invalid_argument); } TEST_F(CoincenterCommandFactoryTest, CreateTradeAbsolute) { @@ -81,6 +88,7 @@ TEST_F(CoincenterCommandFactoryTest, CreateTradeAbsolute) { .setPercentageAmount(false) .setCur1("BTC") .setExchangeNames(ExchangeNames({ExchangeName("binance", "user2")}))); + EXPECT_NO_THROW(optionParser.checkEndParsing()); } TEST_F(CoincenterCommandFactoryTest, CreateTradePercentage) { @@ -92,6 +100,7 @@ TEST_F(CoincenterCommandFactoryTest, CreateTradePercentage) { .setPercentageAmount(true) .setCur1("USDT") .setExchangeNames(ExchangeNames({ExchangeName("huobi"), ExchangeName("upbit", "user1")}))); + EXPECT_NO_THROW(optionParser.checkEndParsing()); } TEST_F(CoincenterCommandFactoryTest, CreateBuyCommand) { @@ -101,6 +110,7 @@ TEST_F(CoincenterCommandFactoryTest, CreateBuyCommand) { CoincenterCommand(type) .setTradeOptions(cmdLineOptions.computeTradeOptions()) .setAmount(MonetaryAmount("804XLM"))); + EXPECT_NO_THROW(optionParser.checkEndParsing()); } TEST_F(CoincenterCommandFactoryTest, CreateSellCommand) { @@ -111,6 +121,7 @@ TEST_F(CoincenterCommandFactoryTest, CreateSellCommand) { .setTradeOptions(cmdLineOptions.computeTradeOptions()) .setAmount(MonetaryAmount("0.76BTC")) .setExchangeNames(ExchangeNames({ExchangeName("bithumb")}))); + EXPECT_NO_THROW(optionParser.checkEndParsing()); } TEST_F(CoincenterCommandFactoryTest, CreateSellWithPreviousInvalidCommand) { @@ -127,6 +138,7 @@ TEST_F(CoincenterCommandFactoryTest, CreateSellAllCommand) { .setTradeOptions(cmdLineOptions.computeTradeOptions()) .setPercentageAmount(true) .setAmount(MonetaryAmount(100, "DOGE"))); + EXPECT_NO_THROW(optionParser.checkEndParsing()); } TEST_F(CoincenterCommandFactoryTest, CreateWithdrawInvalidNoPrevious) { @@ -147,6 +159,7 @@ TEST_F(CoincenterCommandFactoryTest, CreateWithdrawAbsoluteValid) { .setWithdrawOptions(cmdLineOptions.computeWithdrawOptions()) .setAmount(MonetaryAmount("5000XRP")) .setExchangeNames(ExchangeNames({ExchangeName("binance", "user1"), ExchangeName("kucoin", "user2")}))); + EXPECT_NO_THROW(optionParser.checkEndParsing()); } TEST_F(CoincenterCommandFactoryTest, CreateWithdrawPercentageValid) { @@ -156,6 +169,7 @@ TEST_F(CoincenterCommandFactoryTest, CreateWithdrawPercentageValid) { .setAmount(MonetaryAmount("43.25LTC")) .setPercentageAmount(true) .setExchangeNames(ExchangeNames({ExchangeName("bithumb"), ExchangeName("kraken")}))); + EXPECT_NO_THROW(optionParser.checkEndParsing()); } TEST_F(CoincenterCommandFactoryTest, CreateWithdrawAllNoCurrencyInvalid) { @@ -177,6 +191,7 @@ TEST_F(CoincenterCommandFactoryTest, CreateWithdrawAllValid) { .setAmount(MonetaryAmount(100, "SOL")) .setPercentageAmount(true) .setExchangeNames(ExchangeNames({ExchangeName("upbit"), ExchangeName("kraken")}))); + EXPECT_NO_THROW(optionParser.checkEndParsing()); } class CoincenterCommandFactoryWithPreviousTest : public ::testing::Test { @@ -197,6 +212,7 @@ TEST_F(CoincenterCommandFactoryWithPreviousTest, CreateSellWithPreviousCommand) cmdLineOptions.sell = "whatever"; EXPECT_EQ(commandFactory.createTradeCommand(type, inputStr("")), CoincenterCommand(type).setTradeOptions(cmdLineOptions.computeTradeOptions())); + EXPECT_NO_THROW(optionParser.checkEndParsing()); } TEST_F(CoincenterCommandFactoryWithPreviousTest, CreateWithdrawInvalidNoExchange) { @@ -212,6 +228,7 @@ TEST_F(CoincenterCommandFactoryWithPreviousTest, CreateWithdrawWithPreviousValid CoincenterCommand(CoincenterCommandType::kWithdrawApply) .setWithdrawOptions(cmdLineOptions.computeWithdrawOptions()) .setExchangeNames(ExchangeNames({ExchangeName("kraken", "user1")}))); + EXPECT_NO_THROW(optionParser.checkEndParsing()); } } // namespace cct \ No newline at end of file diff --git a/src/engine/test/stringoptionparser_test.cpp b/src/engine/test/stringoptionparser_test.cpp index 3ed0023f..72552477 100644 --- a/src/engine/test/stringoptionparser_test.cpp +++ b/src/engine/test/stringoptionparser_test.cpp @@ -193,4 +193,21 @@ TEST(StringOptionParserTest, SeveralAmountCurrencyExchangesFlow) { EXPECT_NO_THROW(parser.checkEndParsing()); } +TEST(StringOptionParserTest, ExchangesNotLast) { + StringOptionParser parser("jst,34.78966544ETH,kucoin_user1-binance-kraken,krw"); + + EXPECT_EQ(parser.parseCurrency(StringOptionParser::FieldIs::kOptional), CurrencyCode("JST")); + EXPECT_EQ(parser.parseNonZeroAmount(StringOptionParser::FieldIs::kMandatory), + std::make_pair(MonetaryAmount("34.78966544ETH"), StringOptionParser::AmountType::kAbsolute)); + EXPECT_EQ(parser.parseNonZeroAmount(StringOptionParser::FieldIs::kOptional), + std::make_pair(MonetaryAmount(), StringOptionParser::AmountType::kNotPresent)); + EXPECT_EQ(parser.parseCurrency(StringOptionParser::FieldIs::kOptional), CurrencyCode()); + + EXPECT_EQ(parser.parseExchanges('-', ','), + ExchangeNames({ExchangeName("kucoin", "user1"), ExchangeName("binance"), ExchangeName("kraken")})); + EXPECT_EQ(parser.parseCurrency(StringOptionParser::FieldIs::kMandatory), CurrencyCode("KRW")); + + EXPECT_NO_THROW(parser.checkEndParsing()); +} + } // namespace cct \ No newline at end of file diff --git a/src/main/src/main.cpp b/src/main/src/main.cpp index 20ae1fcf..21ee8aaa 100644 --- a/src/main/src/main.cpp +++ b/src/main/src/main.cpp @@ -5,19 +5,23 @@ #include "cct_invalid_argument_exception.hpp" #include "coincentercommands.hpp" +#include "commandlineoptionsparser.hpp" +#include "parseoptions.hpp" #include "processcommandsfromcli.hpp" #include "runmodes.hpp" int main(int argc, const char* argv[]) { try { - const auto cmdLineOptionsVector = cct::CoincenterCommands::ParseOptions(argc, argv); + using namespace cct; + auto parser = + CommandLineOptionsParser(CoincenterAllowedOptions::value); + const auto cmdLineOptionsVector = ParseOptions(parser, argc, argv); if (!cmdLineOptionsVector.empty()) { - const cct::CoincenterCommands coincenterCommands(cmdLineOptionsVector); + const CoincenterCommands coincenterCommands(cmdLineOptionsVector); const auto programName = std::filesystem::path(argv[0]).filename().string(); - cct::ProcessCommandsFromCLI(programName, coincenterCommands, cmdLineOptionsVector.front(), - cct::settings::RunMode::kProd); + ProcessCommandsFromCLI(programName, coincenterCommands, cmdLineOptionsVector.front(), settings::RunMode::kProd); } } catch (const cct::invalid_argument& e) { std::cerr << "Invalid argument: " << e.what() << '\n'; diff --git a/src/objects/include/exchangename.hpp b/src/objects/include/exchangename.hpp index 40876539..0a34da4d 100644 --- a/src/objects/include/exchangename.hpp +++ b/src/objects/include/exchangename.hpp @@ -55,9 +55,8 @@ class ExchangeName { private: static constexpr std::size_t kMinExchangeNameLength = - std::ranges::min_element(kSupportedExchanges, [](std::string_view lhs, std::string_view rhs) { - return lhs.size() < rhs.size(); - })->size(); + std::ranges::min_element(kSupportedExchanges, [](auto lhs, auto rhs) { return lhs.size() < rhs.size(); }) + -> size(); std::size_t underscorePos() const { return _nameWithKey.find('_', kMinExchangeNameLength); } diff --git a/src/objects/src/exchangename.cpp b/src/objects/src/exchangename.cpp index 293e9b01..364a4164 100644 --- a/src/objects/src/exchangename.cpp +++ b/src/objects/src/exchangename.cpp @@ -12,6 +12,9 @@ namespace cct { bool ExchangeName::IsValid(std::string_view str) { + if (str.size() < kMinExchangeNameLength) { + return false; + } return std::ranges::any_of(kSupportedExchanges, [lowerStr = ToLower(str)](std::string_view ex) { return lowerStr.starts_with(ex) && (lowerStr.size() == ex.size() || lowerStr[ex.size()] == '_'); });