Skip to content

Commit

Permalink
Easier usage as library - parseExchanges does not require to be last …
Browse files Browse the repository at this point in the history
…anymore
  • Loading branch information
sjanel committed Nov 19, 2023
1 parent 54f60b2 commit 7f2bcbd
Show file tree
Hide file tree
Showing 13 changed files with 134 additions and 74 deletions.
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

0 comments on commit 7f2bcbd

Please sign in to comment.