Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Easier usage as library - parseExchanges does not require to be last anymore #482

Merged
merged 1 commit into from
Nov 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions src/engine/include/coincentercommands.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@ class CoincenterCommands {
// Builds a CoincenterCommands and add commands from given command line options span.
explicit CoincenterCommands(std::span<const CoincenterCmdLineOptions> cmdLineOptionsSpan);

static vector<CoincenterCmdLineOptions> ParseOptions(int argc, const char *argv[]);

/// @brief Set this CoincenterCommands from given command line options.
void addOption(const CoincenterCmdLineOptions &cmdLineOptions, const CoincenterCommand *pPreviousCommand);

Expand Down
1 change: 1 addition & 0 deletions src/engine/include/commandlineoptionsparser.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ class CommandLineOptionsParser {
public:
using CommandLineOptionType = AllowedCommandLineOptionsBase<OptValueType>::CommandLineOptionType;
using CommandLineOptionWithValue = AllowedCommandLineOptionsBase<OptValueType>::CommandLineOptionWithValue;
using value_type = OptValueType;

template <unsigned N>
explicit CommandLineOptionsParser(const CommandLineOptionWithValue (&init)[N]) {
Expand Down
54 changes: 54 additions & 0 deletions src/engine/include/parseoptions.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
#pragma once

#include <filesystem>
#include <iostream>
#include <span>
#include <utility>

#include "cct_vector.hpp"
#include "coincenteroptions.hpp"
#include "commandlineoptionsparseriterator.hpp"

namespace cct {
template <class ParserType>
auto ParseOptions(ParserType &parser, int argc, const char *argv[]) {
auto programName = std::filesystem::path(argv[0]).filename().string();

std::span<const char *> 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<OptValueType> 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
7 changes: 6 additions & 1 deletion src/engine/include/stringoptionparser.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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:
Expand Down
4 changes: 0 additions & 4 deletions src/engine/src/coincentercommandfactory.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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());

Expand Down
50 changes: 0 additions & 50 deletions src/engine/src/coincentercommands.cpp
Original file line number Diff line number Diff line change
@@ -1,20 +1,14 @@
#include "coincentercommands.hpp"

#include <chrono>
#include <filesystem>
#include <iostream>
#include <span>
#include <string_view>
#include <utility>

#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"
Expand All @@ -23,50 +17,6 @@

namespace cct {

vector<CoincenterCmdLineOptions> CoincenterCommands::ParseOptions(int argc, const char *argv[]) {
using OptValueType = CoincenterCmdLineOptions;

auto parser = CommandLineOptionsParser<OptValueType>(CoincenterAllowedOptions<OptValueType>::value);

auto programName = std::filesystem::path(argv[0]).filename().string();

vector<CoincenterCmdLineOptions> parsedOptions;

std::span<const char *> 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<const char *> 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<const CoincenterCmdLineOptions> cmdLineOptionsSpan) {
_commands.reserve(cmdLineOptionsSpan.size());
const CoincenterCommand *pPreviousCommand = nullptr;
Expand Down
32 changes: 24 additions & 8 deletions src/engine/src/stringoptionparser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ std::pair<MonetaryAmount, StringOptionParser::AmountType> StringOptionParser::pa
vector<string> StringOptionParser::getCSVValues() {
vector<string> ret;
if (!_opt.empty()) {
do {
while (true) {
auto nextCommaPos = _opt.find(',', _pos);
if (nextCommaPos == std::string_view::npos) {
nextCommaPos = _opt.size();
Expand All @@ -160,27 +160,43 @@ vector<string> 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;
}
Expand Down
21 changes: 19 additions & 2 deletions src/engine/test/coincentercommandfactory_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -33,24 +33,28 @@ 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) {
EXPECT_EQ(CoincenterCommandFactory::CreateMarketCommand(inputStr("XLM,kraken,binance_user1")),
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) {
Expand All @@ -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) {
Expand All @@ -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) {
Expand All @@ -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) {
Expand All @@ -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) {
Expand All @@ -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) {
Expand All @@ -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) {
Expand All @@ -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) {
Expand All @@ -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) {
Expand All @@ -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) {
Expand All @@ -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 {
Expand All @@ -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) {
Expand All @@ -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
17 changes: 17 additions & 0 deletions src/engine/test/stringoptionparser_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading
Loading