Browse Source

Util+Test: Add code snapshot from the riyyi/manafiles project

Add all utility and unit test code from the riyyi/manafiles project at
commit 6f0e3d6063ab75ad81899135689569e440ddb813, link at:
github.com/riyyi/manafiles/tree/6f0e3d6063ab75ad81899135689569e440ddb813
master
Riyyi 2 years ago
parent
commit
e940c81670
  1. 568
      src/util/argparser.cpp
  2. 120
      src/util/argparser.h
  3. 89
      src/util/file.cpp
  4. 34
      src/util/file.h
  5. 6
      src/util/format/.clang-tidy
  6. 222
      src/util/format/builder.cpp
  7. 85
      src/util/format/builder.h
  8. 176
      src/util/format/color.cpp
  9. 156
      src/util/format/color.h
  10. 76
      src/util/format/format.cpp
  11. 119
      src/util/format/format.h
  12. 117
      src/util/format/formatter.cpp
  13. 325
      src/util/format/formatter.h
  14. 109
      src/util/format/log.cpp
  15. 97
      src/util/format/log.h
  16. 419
      src/util/format/parser.cpp
  17. 61
      src/util/format/parser.h
  18. 52
      src/util/format/print.cpp
  19. 69
      src/util/format/print.h
  20. 71
      src/util/genericlexer.cpp
  21. 41
      src/util/genericlexer.h
  22. 26
      src/util/json/array.cpp
  23. 56
      src/util/json/array.h
  24. 133
      src/util/json/fromjson.h
  25. 114
      src/util/json/job.cpp
  26. 41
      src/util/json/job.h
  27. 15
      src/util/json/json.h
  28. 217
      src/util/json/lexer.cpp
  29. 65
      src/util/json/lexer.h
  30. 26
      src/util/json/object.cpp
  31. 52
      src/util/json/object.h
  32. 497
      src/util/json/parser.cpp
  33. 45
      src/util/json/parser.h
  34. 155
      src/util/json/serializer.cpp
  35. 35
      src/util/json/serializer.h
  36. 153
      src/util/json/tojson.h
  37. 340
      src/util/json/value.cpp
  38. 165
      src/util/json/value.h
  39. 96
      src/util/meta/assert.h
  40. 27
      src/util/meta/compiler.h
  41. 22
      src/util/meta/concepts.h
  42. 20
      src/util/meta/odr.h
  43. 53
      src/util/shell.cpp
  44. 33
      src/util/shell.h
  45. 53
      src/util/singleton.h
  46. 328
      src/util/system.cpp
  47. 68
      src/util/system.h
  48. 131
      src/util/timer.cpp
  49. 51
      src/util/timer.h
  50. 79
      test/macro.h
  51. 8
      test/main.cpp
  52. 45
      test/testcase.h
  53. 76
      test/testsuite.cpp
  54. 36
      test/testsuite.h
  55. 949
      test/unit/testutilargparser.cpp
  56. 517
      test/unit/testutilformat.cpp
  57. 570
      test/unit/testutiljson.cpp
  58. 39
      test/unit/testutilshell.cpp
  59. 98
      test/unit/testutilsystem.cpp

568
src/util/argparser.cpp

@ -0,0 +1,568 @@
/*
* Copyright (C) 2021-2022 Riyyi
*
* SPDX-License-Identifier: MIT
*/
#include <algorithm> // find_if
#include <cstddef> // size_t
#include <cstdint> // uint8_t
#include <cstdio> // printf
#include <cstring> // strcmp
#include <limits> // numeric_limits
#include <string> // stod, stoi, stoul
#include <string_view>
#include <vector>
#include "util/argparser.h"
namespace Util {
ArgParser::ArgParser()
{
}
ArgParser::~ArgParser()
{
}
void ArgParser::printError(char parameter, Error error)
{
char tmp[] { parameter, '\0' };
printError(tmp, error, false);
}
void ArgParser::printError(const char* parameter, Error error, bool longName)
{
if (!m_errorReporting) {
return;
}
if (error == Error::OptionInvalid) {
printf("%s: invalid option -- '%s'\n", m_name, parameter);
}
else if (error == Error::OptionUnrecognized) {
printf("%s: unrecognized option -- '%s'\n", m_name, parameter);
}
else if (error == Error::OptionDoesntAllowArgument) {
printf("%s: option '--%s' doesn't allow an argument\n", m_name, parameter);
}
else if (error == Error::OptionRequiresArgument) {
if (longName) {
printf("%s: option '--%s' requires an argument\n", m_name, parameter);
}
else {
printf("%s: option requires an argument -- '%s'\n", m_name, parameter);
}
}
else if (error == Error::OptionInvalidArgumentType) {
if (longName) {
printf("%s: option '--%s' invalid type\n", m_name, parameter);
}
else {
printf("%s: option invalid type -- '%s'\n", m_name, parameter);
}
}
else if (error == Error::ArgumentExtraOperand) {
printf("%s: extra operand '%s'\n", m_name, parameter);
}
else if (error == Error::ArgumentRequired) {
printf("%s: missing required argument '%s'\n", m_name, parameter);
}
else if (error == Error::ArgumentInvalidType) {
printf("%s: invalid argument type '%s'\n", m_name, parameter);
}
// TODO: Print command usage, if it's enabled.
}
// Required: directly after || separated by space
// Optional: directly after
bool ArgParser::parseShortOption(std::string_view option, std::string_view next)
{
bool result = true;
#ifndef NDEBUG
printf("Parsing short option: '%s'\n", option.data());
#endif
char c;
std::string_view value;
for (size_t i = 0; i < option.size(); ++i) {
c = option.at(i);
#ifndef NDEBUG
printf("short '%c'\n", c);
#endif
auto foundOption = std::find_if(m_options.begin(), m_options.end(), [&c](Option& it) -> bool {
return it.shortName == c;
});
// Option does not exist
if (foundOption == m_options.cend()) {
printError(c, Error::OptionInvalid);
if (m_exitOnFirstError) {
return false;
}
}
else if (foundOption->requiresArgument == Required::No) {
// FIXME: Figure out why providing a nullptr breaks the lambda here.
result = foundOption->acceptValue("");
}
else if (foundOption->requiresArgument == Required::Yes) {
value = option.substr(i + 1);
if (value.empty() && next.empty()) {
foundOption->error = Error::OptionRequiresArgument;
printError(c, Error::OptionRequiresArgument);
result = false;
}
else if (!value.empty()) {
result = foundOption->acceptValue(value.data());
if (!result) {
printError(c, Error::OptionInvalidArgumentType);
}
}
else if (next[0] == '-') {
foundOption->error = Error::OptionRequiresArgument;
printError(c, Error::OptionRequiresArgument);
result = false;
}
else {
result = foundOption->acceptValue(next.data());
m_optionIndex++;
if (!result) {
printError(c, Error::OptionInvalidArgumentType);
}
}
break;
}
else if (foundOption->requiresArgument == Required::Optional) {
value = option.substr(i + 1);
if (!value.empty()) {
result = foundOption->acceptValue(value.data());
if (!result) {
printError(c, Error::OptionInvalidArgumentType);
}
break;
}
}
}
return result;
}
// Required: directly after, separated by '=' || separated by space
// Optional: directly after, separated by '='
bool ArgParser::parseLongOption(std::string_view option, std::string_view next)
{
std::string name = std::string(option.substr(0, option.find_first_of('=')));
std::string_view value = option.substr(option.find_first_of('=') + 1);
auto foundOption = std::find_if(m_options.begin(), m_options.end(), [&name](Option& it) -> bool {
return it.longName && it.longName == name;
});
if (foundOption == m_options.cend()) {
printError(name.data(), Error::OptionUnrecognized);
return false;
}
enum class ArgumentProvided : uint8_t {
No,
DirectlyAfter,
Seperated,
};
auto argument = ArgumentProvided::No;
if (name != value || option.find('=') != std::string_view::npos) {
argument = ArgumentProvided::DirectlyAfter;
}
else if (!next.empty() && next[0] != '-') {
argument = ArgumentProvided::Seperated;
value = next;
}
bool result = true;
if (foundOption->requiresArgument == Required::No) {
if (argument == ArgumentProvided::DirectlyAfter) {
foundOption->error = Error::OptionDoesntAllowArgument;
printError(name.data(), Error::OptionDoesntAllowArgument);
return false;
}
result = foundOption->acceptValue("");
}
else if (foundOption->requiresArgument == Required::Yes) {
if (argument == ArgumentProvided::No) {
foundOption->error = Error::OptionRequiresArgument;
printError(name.data(), Error::OptionRequiresArgument);
return false;
}
result = foundOption->acceptValue(value.data());
if (!result) {
printError(name.data(), Error::OptionInvalidArgumentType);
}
if (argument == ArgumentProvided::Seperated) {
m_optionIndex++;
}
}
else if (foundOption->requiresArgument == Required::Optional) {
if (argument == ArgumentProvided::DirectlyAfter) {
result = foundOption->acceptValue(value.data());
if (!result) {
printError(name.data(), Error::OptionInvalidArgumentType);
}
}
}
return result;
}
bool ArgParser::parseArgument(std::string_view argument)
{
bool result = true;
for (;;) {
// Run out of argument handlers
if (m_argumentIndex >= m_arguments.size()) {
printError(argument.data(), Error::ArgumentExtraOperand);
return false;
}
Argument& currentArgument = m_arguments.at(m_argumentIndex);
result = currentArgument.acceptValue(argument.data());
if (result) {
currentArgument.addedValues++;
if (currentArgument.addedValues >= currentArgument.maxValues) {
m_argumentIndex++;
}
}
else if (currentArgument.minValues == 0) {
m_argumentIndex++;
continue;
}
else {
printError(argument.data(), Error::ArgumentInvalidType);
}
break;
}
return result;
}
bool ArgParser::parse(int argc, const char* argv[])
{
bool result = true;
// Set looping indices
m_optionIndex = 1;
m_argumentIndex = 0;
// By default parse all '-' prefixed parameters as options
m_nonOptionMode = false;
// Get program name
m_name = argv[0] + std::string_view(argv[0]).find_last_of('/') + 1;
std::string_view argument;
std::string_view next;
for (; m_optionIndex < (size_t)argc; ++m_optionIndex) {
#ifndef NDEBUG
printf("argv[%zu]: %s\n", m_optionIndex, argv[m_optionIndex]);
#endif
// Get the current and next parameter
argument = argv[m_optionIndex];
if (m_optionIndex + 1 < (size_t)argc && argv[m_optionIndex + 1][0] != '-') {
next = argv[m_optionIndex + 1];
}
else {
next = {};
}
// Stop parsing '-' prefixed parameters as options afer '--'
if (argument.compare("--") == 0) {
m_nonOptionMode = true;
continue;
}
// Long Option
if (!m_nonOptionMode && argument[0] == '-' && argument[1] == '-') {
argument = argument.substr(argument.find_first_not_of('-'));
if (!parseLongOption(argument, next)) {
result = false;
}
}
// Short Option
else if (!m_nonOptionMode && argument[0] == '-') {
argument = argument.substr(argument.find_first_not_of('-'));
if (!parseShortOption(argument, next)) {
result = false;
}
}
// Argument
else {
if (m_stopParsingOnFirstNonOption) {
m_nonOptionMode = true;
}
if (!parseArgument(argument)) {
result = false;
}
}
if (m_exitOnFirstError && !result) {
break;
}
}
// Check any leftover arguments for required
for (; m_argumentIndex < m_arguments.size(); ++m_argumentIndex) {
Argument& currentArgument = m_arguments.at(m_argumentIndex);
if (currentArgument.minValues > currentArgument.addedValues) {
result = false;
printError(currentArgument.name ? currentArgument.name : "", Error::ArgumentRequired);
}
}
if (result) {
return true;
}
for (auto& option : m_options) {
if (option.longName && strcmp(option.longName, "help") == 0) {
printf("Try '%s --help' for more information.\n", m_name);
break;
}
if (option.shortName == 'h') {
printf("Try '%s -h' for more information.\n", m_name);
break;
}
}
return false;
}
// -----------------------------------------
void ArgParser::addOption(Option&& option)
{
m_options.push_back(option);
}
void ArgParser::addOption(bool& value, char shortName, const char* longName, const char* usageString, const char* manString)
{
addOption({ shortName, longName, nullptr, usageString, manString, Required::No, getAcceptFunction(value) });
}
void ArgParser::addOption(const char*& value, char shortName, const char* longName, const char* usageString, const char* manString, const char* argumentName, Required requiresArgument)
{
addOption({ shortName, longName, argumentName, usageString, manString, requiresArgument, getAcceptFunction(value) });
}
void ArgParser::addOption(std::string& value, char shortName, const char* longName, const char* usageString, const char* manString, const char* argumentName, Required requiresArgument)
{
addOption({ shortName, longName, argumentName, usageString, manString, requiresArgument, getAcceptFunction(value) });
}
void ArgParser::addOption(std::string_view& value, char shortName, const char* longName, const char* usageString, const char* manString, const char* argumentName, Required requiresArgument)
{
addOption({ shortName, longName, argumentName, usageString, manString, requiresArgument, getAcceptFunction(value) });
}
void ArgParser::addOption(int& value, char shortName, const char* longName, const char* usageString, const char* manString, const char* argumentName, Required requiresArgument)
{
addOption({ shortName, longName, argumentName, usageString, manString, requiresArgument, getAcceptFunction(value) });
}
void ArgParser::addOption(unsigned int& value, char shortName, const char* longName, const char* usageString, const char* manString, const char* argumentName, Required requiresArgument)
{
addOption({ shortName, longName, argumentName, usageString, manString, requiresArgument, getAcceptFunction(value) });
}
void ArgParser::addOption(double& value, char shortName, const char* longName, const char* usageString, const char* manString, const char* argumentName, Required requiresArgument)
{
addOption({ shortName, longName, argumentName, usageString, manString, requiresArgument, getAcceptFunction(value) });
}
void ArgParser::addOption(std::vector<std::string>& values, char shortName, const char* longName, const char* usageString, const char* manString, const char* argumentName, Required requiresArgument)
{
addOption({ shortName, longName, argumentName, usageString, manString, requiresArgument, getAcceptFunction(values) });
}
// -----------------------------------------
void ArgParser::addArgument(Argument&& argument)
{
m_arguments.push_back(argument);
}
void ArgParser::addArgument(bool& value, const char* name, const char* usageString, const char* manString, Required required)
{
size_t minValues = required == Required::Yes ? 1 : 0;
addArgument({ name, usageString, manString, minValues, 1, 0, getAcceptFunction(value) });
}
void ArgParser::addArgument(const char*& value, const char* name, const char* usageString, const char* manString, Required required)
{
size_t minValues = required == Required::Yes ? 1 : 0;
addArgument({ name, usageString, manString, minValues, 1, 0, getAcceptFunction(value) });
}
void ArgParser::addArgument(std::string& value, const char* name, const char* usageString, const char* manString, Required required)
{
size_t minValues = required == Required::Yes ? 1 : 0;
addArgument({ name, usageString, manString, minValues, 1, 0, getAcceptFunction(value) });
}
void ArgParser::addArgument(std::string_view& value, const char* name, const char* usageString, const char* manString, Required required)
{
size_t minValues = required == Required::Yes ? 1 : 0;
addArgument({ name, usageString, manString, minValues, 1, 0, getAcceptFunction(value) });
}
void ArgParser::addArgument(int& value, const char* name, const char* usageString, const char* manString, Required required)
{
size_t minValues = required == Required::Yes ? 1 : 0;
addArgument({ name, usageString, manString, minValues, 1, 0, getAcceptFunction(value) });
}
void ArgParser::addArgument(unsigned int& value, const char* name, const char* usageString, const char* manString, Required required)
{
size_t minValues = required == Required::Yes ? 1 : 0;
addArgument({ name, usageString, manString, minValues, 1, 0, getAcceptFunction(value) });
}
void ArgParser::addArgument(double& value, const char* name, const char* usageString, const char* manString, Required required)
{
size_t minValues = required == Required::Yes ? 1 : 0;
addArgument({ name, usageString, manString, minValues, 1, 0, getAcceptFunction(value) });
}
void ArgParser::addArgument(std::vector<std::string>& values, const char* name, const char* usageString, const char* manString, Required required)
{
size_t minValues = required == Required::Yes ? 1 : 0;
addArgument({ name, usageString, manString, minValues, values.max_size(), 0, getAcceptFunction(values) });
}
// -----------------------------------------
AcceptFunction ArgParser::getAcceptFunction(bool& value)
{
return [&value](const char*) -> bool {
value = true;
return true;
};
}
AcceptFunction ArgParser::getAcceptFunction(const char*& value)
{
return [&value](const char* input) -> bool {
value = input;
return true;
};
}
AcceptFunction ArgParser::getAcceptFunction(std::string& value)
{
return [&value](const char* input) -> bool {
value = input;
return true;
};
}
AcceptFunction ArgParser::getAcceptFunction(std::string_view& value)
{
return [&value](const char* input) -> bool {
value = input;
return true;
};
}
AcceptFunction ArgParser::getAcceptFunction(int& value)
{
return [&value](const char* input) -> bool {
const char* validate = input;
for (; *validate != '\0'; ++validate) {
// - [0-9]
if (*validate != 45 && (*validate < 48 || *validate > 57)) {
return false;
}
}
try {
value = std::stoi(input);
return true;
}
catch (...) {
return false;
}
};
}
AcceptFunction ArgParser::getAcceptFunction(unsigned int& value)
{
return [&value](const char* input) -> bool {
const char* validate = input;
for (; *validate != '\0'; ++validate) {
// [0-9]
if (*validate < 48 || *validate > 57) {
return false;
}
}
unsigned long convert = 0;
try {
convert = std::stoul(input);
}
catch (...) {
return false;
}
if (convert <= std::numeric_limits<unsigned int>::max()) {
value = static_cast<unsigned int>(convert);
return true;
}
return false;
};
}
AcceptFunction ArgParser::getAcceptFunction(double& value)
{
return [&value](const char* input) -> bool {
const char* validate = input;
for (; *validate != '\0'; ++validate) {
// . [0-9]
if (*validate != 46 && (*validate < 48 || *validate > 57)) {
return false;
}
}
try {
value = std::stod(input);
return true;
}
catch (...) {
return false;
}
};
}
AcceptFunction ArgParser::getAcceptFunction(std::vector<std::string>& value)
{
return [&value](const char* input) -> bool {
value.push_back(input);
return true;
};
}
} // namespace Util

120
src/util/argparser.h

@ -0,0 +1,120 @@
/*
* Copyright (C) 2022 Riyyi
*
* SPDX-License-Identifier: MIT
*/
#pragma once
#include <cstddef> // size_t
#include <cstdint> // uint8_t
#include <functional> // function
#include <string>
#include <string_view>
#include <vector>
namespace Util {
using AcceptFunction = std::function<bool(const char*)>;
class ArgParser final {
public:
ArgParser();
virtual ~ArgParser();
enum class Required : uint8_t {
No,
Yes,
Optional,
};
enum class Error : uint8_t {
None,
OptionInvalid, // For short options
OptionUnrecognized, // For long options
OptionDoesntAllowArgument,
OptionRequiresArgument,
OptionInvalidArgumentType,
ArgumentExtraOperand,
ArgumentRequired,
ArgumentInvalidType,
};
struct Option {
char shortName { 0 };
const char* longName { nullptr };
const char* argumentName { nullptr };
const char* usageString { nullptr };
const char* manString { nullptr };
Required requiresArgument;
AcceptFunction acceptValue;
Error error = Error::None;
};
struct Argument {
const char* name { nullptr };
const char* usageString { nullptr };
const char* manString { nullptr };
size_t minValues { 0 };
size_t maxValues { 1 };
size_t addedValues { 0 };
AcceptFunction acceptValue;
};
bool parse(int argc, const char* argv[]);
void addOption(Option&& option);
void addOption(bool& value, char shortName, const char* longName, const char* usageString, const char* manString);
void addOption(const char*& value, char shortName, const char* longName, const char* usageString, const char* manString, const char* argumentName = "", Required requiresArgument = Required::No);
void addOption(std::string& value, char shortName, const char* longName, const char* usageString, const char* manString, const char* argumentName = "", Required requiresArgument = Required::No);
void addOption(std::string_view& value, char shortName, const char* longName, const char* usageString, const char* manString, const char* argumentName = "", Required requiresArgument = Required::No);
void addOption(int& value, char shortName, const char* longName, const char* usageString, const char* manString, const char* argumentName = "", Required requiresArgument = Required::No);
void addOption(unsigned int& value, char shortName, const char* longName, const char* usageString, const char* manString, const char* argumentName = "", Required requiresArgument = Required::No);
void addOption(double& value, char shortName, const char* longName, const char* usageString, const char* manString, const char* argumentName = "", Required requiresArgument = Required::No);
void addOption(std::vector<std::string>& values, char shortName, const char* longName, const char* usageString, const char* manString, const char* argumentName = "", Required requiresArgument = Required::No);
void addArgument(Argument&& argument);
void addArgument(bool& value, const char* name, const char* usageString, const char* manString, Required required = Required::No);
void addArgument(const char*& value, const char* name, const char* usageString, const char* manString, Required required = Required::No);
void addArgument(std::string& value, const char* name, const char* usageString, const char* manString, Required required = Required::No);
void addArgument(std::string_view& value, const char* name, const char* usageString, const char* manString, Required required = Required::No);
void addArgument(int& value, const char* name, const char* usageString, const char* manString, Required required = Required::No);
void addArgument(unsigned int& value, const char* name, const char* usageString, const char* manString, Required required = Required::No);
void addArgument(double& value, const char* name, const char* usageString, const char* manString, Required required = Required::No);
void addArgument(std::vector<std::string>& values, const char* name, const char* usageString, const char* manString, Required required = Required::No);
void setErrorReporting(bool state) { m_errorReporting = state; }
void setExitOnFirstError(bool state) { m_exitOnFirstError = state; }
void setStopParsingOnFirstNonOption(bool state) { m_stopParsingOnFirstNonOption = state; }
private:
void printError(char parameter, Error error);
void printError(const char* parameter, Error error, bool longName = true);
bool parseShortOption(std::string_view option, std::string_view next);
bool parseLongOption(std::string_view option, std::string_view next);
bool parseArgument(std::string_view argument);
AcceptFunction getAcceptFunction(bool& value);
AcceptFunction getAcceptFunction(const char*& value);
AcceptFunction getAcceptFunction(std::string& value);
AcceptFunction getAcceptFunction(std::string_view& value);
AcceptFunction getAcceptFunction(int& value);
AcceptFunction getAcceptFunction(unsigned int& value);
AcceptFunction getAcceptFunction(double& value);
AcceptFunction getAcceptFunction(std::vector<std::string>& value);
bool m_errorReporting { true };
bool m_exitOnFirstError { true };
bool m_stopParsingOnFirstNonOption { false };
size_t m_optionIndex { 1 };
size_t m_argumentIndex { 0 };
bool m_nonOptionMode { false };
const char* m_name { nullptr };
std::vector<Option> m_options;
std::vector<Argument> m_arguments;
};
} // namespace Util

89
src/util/file.cpp

@ -0,0 +1,89 @@
/*
* Copyright (C) 2021-2022 Riyyi
*
* SPDX-License-Identifier: MIT
*/
#include <cstdint> // int32_t
#include <filesystem>
#include <fstream> // ifstream, ios, ofstream
#include <memory> // make_unique
#include <string>
#include "util/file.h"
#include "util/meta/assert.h"
namespace Util {
File::File(const std::string& path)
: m_path(path)
{
// Create input stream object and open file
std::ifstream file(path, std::ios::in);
VERIFY(file.is_open(), "failed to open file: '{}'", path);
// Get length of the file
file.seekg(0, std::ios::end);
int32_t size = file.tellg();
file.seekg(0, std::ios::beg);
VERIFY(size != -1, "failed to read file length: '{}', path");
// Allocate memory filled with zeros
auto buffer = std::make_unique<char[]>(size);
// Fill buffer with file contents
file.read(buffer.get(), size);
file.close();
m_data = std::string(buffer.get(), size);
}
File::~File()
{
}
// -----------------------------------------
File File::create(const std::string& path)
{
if (!std::filesystem::exists(path)) {
std::ofstream { path };
}
return Util::File(path);
}
// -----------------------------------------
void File::clear()
{
m_data.clear();
}
File& File::append(const std::string& data)
{
m_data.append(data);
return *this;
}
File& File::replace(size_t index, size_t length, const std::string& data)
{
m_data.replace(index, length, data);
return *this;
}
File& File::flush()
{
// Create output stream object and open file
std::ofstream file(m_path, std::ios::out | std::ios::trunc);
VERIFY(file.is_open(), "failed to open file: '{}'", m_path);
// Write data to disk
file.write(m_data.c_str(), m_data.size());
return *this;
}
} // namespace Util

34
src/util/file.h

@ -0,0 +1,34 @@
/*
* Copyright (C) 2021-2022 Riyyi
*
* SPDX-License-Identifier: MIT
*/
#pragma once
#include <string>
namespace Util {
class File {
public:
File(const std::string& path);
virtual ~File();
static File create(const std::string& path);
void clear();
File& append(const std::string& data);
File& replace(size_t index, size_t length, const std::string& data);
File& flush();
const char* c_str() const { return m_data.c_str(); }
const std::string& data() const { return m_data; }
const std::string& path() const { return m_path; }
private:
std::string m_path;
std::string m_data;
};
} // namespace Util

6
src/util/format/.clang-tidy

@ -0,0 +1,6 @@
# -*- yaml -*-
---
# FIXME: Figure out why NOLINTBEGIN/NOLINTEND doesnt work
Checks: -misc-unused-using-decls
...

222
src/util/format/builder.cpp

@ -0,0 +1,222 @@
/*
* Copyright (C) 2022 Riyyi
*
* SPDX-License-Identifier: MIT
*/
#include <algorithm> // min
#include <cstddef> // size_t
#include <cstdint> // uint8_t, uint16_t, uint32_t, uint64_t
#include <iomanip> // setprecision
#include <ios> // defaultfloat, fixed
#include <limits> // numeric_limits
#include <sstream> // stringstream
#include <string>
#include <string_view>
#include "util/format/builder.h"
#include "util/format/parser.h"
#include "util/meta/assert.h"
namespace Util::Format {
void Builder::putLiteral(std::string_view literal)
{
for (size_t i = 0; i < literal.length(); ++i) {
putCharacter(literal[i]);
if (literal[i] == '{' || literal[i] == '}') {
++i;
}
}
}
static constexpr std::string numberToString(size_t value, uint8_t base, bool uppercase)
{
std::string result;
if (value > std::numeric_limits<uint32_t>::max()) {
result.reserve(64);
}
else if (value > std::numeric_limits<uint16_t>::max()) {
result.reserve(32);
}
constexpr const auto& lookupLowercase = "0123456789abcdef";
constexpr const auto& lookupUppercase = "0123456789ABCDEF";
if (value == 0) {
result = '0';
}
else if (uppercase) {
while (value > 0) {
result.insert(0, 1, lookupUppercase[value % base]);
value /= base;
}
}
else {
while (value > 0) {
result.insert(0, 1, lookupLowercase[value % base]);
value /= base;
}
}
return result;
}
void Builder::putU64(size_t value,
uint8_t base,
bool uppercase,
char fill,
Align align,
Sign sign,
bool alternativeForm,
bool zeroPadding,
size_t width,
bool isNegative) const
{
std::string number = numberToString(value, base, uppercase);
// Sign
std::string prefix = "";
switch (sign) {
case Sign::None:
case Sign::Negative:
if (isNegative) {
prefix = '-';
}
break;
case Sign::Both:
prefix = (isNegative) ? '-' : '+';
break;
case Sign::Space:
prefix = (isNegative) ? '-' : ' ';
break;
default:
VERIFY_NOT_REACHED();
};
// Alternative form
if (alternativeForm) {
switch (base) {
case 2:
prefix += (uppercase) ? "0B" : "0b";
break;
case 8:
prefix += '0';
break;
case 10:
break;
case 16:
prefix += (uppercase) ? "0X" : "0x";
break;
default:
VERIFY_NOT_REACHED();
}
}
if (!zeroPadding) {
number.insert(0, prefix);
}
else {
if (align != Builder::Align::None) {
number.insert(0, prefix);
}
fill = '0';
}
size_t length = number.length();
if (width < length) {
m_builder.write(number.data(), length);
return;
}
switch (align) {
case Align::Left:
m_builder.write(number.data(), length);
m_builder << std::string(width - length, fill);
break;
case Align::Center: {
size_t half = (width - length) / 2;
m_builder << std::string(half, fill);
m_builder.write(number.data(), length);
m_builder << std::string(width - half - length, fill);
break;
}
case Align::Right:
m_builder << std::string(width - length, fill);
m_builder.write(number.data(), length);
break;
case Align::None:
if (zeroPadding) {
m_builder << prefix;
m_builder << std::string(width - length - prefix.length(), fill);
}
else {
m_builder << std::string(width - length, fill);
}
m_builder.write(number.data(), length);
break;
default:
VERIFY_NOT_REACHED();
};
}
void Builder::putI64(int64_t value,
uint8_t base,
bool uppercase,
char fill,
Align align,
Sign sign,
bool alternativeForm,
bool zeroPadding,
size_t width) const
{
bool isNegative = value < 0;
value = isNegative ? -value : value;
putU64(static_cast<uint64_t>(value), base, uppercase, fill, align, sign, alternativeForm, zeroPadding, width, isNegative);
}
void Builder::putF64(double number, uint8_t precision) const
{
precision = std::min(precision, static_cast<uint8_t>(std::numeric_limits<double>::digits10));
std::stringstream stream;
stream
<< std::fixed << std::setprecision(precision)
<< number
<< std::defaultfloat << std::setprecision(6);
std::string string = stream.str();
m_builder << string;
}
void Builder::putString(std::string_view string, char fill, Align align, size_t width) const
{
size_t length = string.length();
if (width < length) {
m_builder.write(string.data(), length);
return;
}
switch (align) {
case Align::None:
case Align::Left:
m_builder.write(string.data(), length);
m_builder << std::string(width - length, fill);
break;
case Align::Center: {
size_t half = (width - length) / 2;
m_builder << std::string(half, fill);
m_builder.write(string.data(), length);
m_builder << std::string(width - half - length, fill);
break;
}
case Align::Right:
m_builder << std::string(width - length, fill);
m_builder.write(string.data(), length);
break;
default:
VERIFY_NOT_REACHED();
};
}
} // namespace Util::Format

85
src/util/format/builder.h

@ -0,0 +1,85 @@
/*
* Copyright (C) 2022 Riyyi
*
* SPDX-License-Identifier: MIT
*/
#pragma once
#include <cstddef> // size_t
#include <cstdint> // int32_t, int64_t, uint8_t, uint32_t
#include <sstream> // stringstream
#include <string_view>
namespace Util::Format {
class Builder {
public:
enum class Align : uint8_t {
None,
Left = '<',
Right = '>',
Center = '^',
};
enum class Sign : uint8_t {
None,
Negative = '-',
Both = '+',
Space = ' ',
};
explicit Builder(std::stringstream& builder)
: m_builder(builder)
{
}
void putLiteral(std::string_view literal);
void putU64(size_t value,
uint8_t base = 10,
bool uppercase = false,
char fill = ' ',
Align align = Align::Right,
Sign sign = Sign::Negative,
bool alternativeForm = false,
bool zeroPadding = false,
size_t width = 0,
bool isNegative = false) const;
void putI64(int64_t value,
uint8_t base = 10,
bool uppercase = false,
char fill = ' ',
Align align = Align::Right,
Sign sign = Sign::Negative,
bool alternativeForm = false,
bool zeroPadding = false,
size_t width = 0) const;
void putF64(double number, uint8_t precision = 6) const;
void putCharacter(char character) const { m_builder.write(&character, 1); }
void putString(std::string_view string, char fill = ' ', Align align = Align::Left, size_t width = 0) const;
const std::stringstream& builder() const { return m_builder; }
std::stringstream& builder() { return m_builder; }
private:
std::stringstream& m_builder;
};
} // namespace Util::Format
#if 0
// Defaults based on type
switch (type) {
case SpecifierType::Integral:
specifier.align = Builder::Align::Right;
specifier.type = PresentationType::Decimal;
break;
case SpecifierType::FloatingPoint:
specifier.align = Builder::Align::Right;
specifier.type = PresentationType::General;
break;
}
#endif

176
src/util/format/color.cpp

@ -0,0 +1,176 @@
/*
* Copyright (C) 2022 Riyyi
*
* SPDX-License-Identifier: MIT
*/
#include <cstdint> // uint8_t
#include <cstdio> // FILE, fputs, stdout
#include <sstream> // stringstream
#include <string>
#include <string_view>
#include "util/format/color.h"
#include "util/format/format.h"
#include "util/meta/assert.h"
namespace Util::Format {
TextStyle::TextStyle(Emphasis emphasis)
: m_emphasis(emphasis)
{
}
TextStyle::TextStyle(bool isForeground, TerminalColor color)
{
if (isForeground) {
m_foregroundColor = color;
}
else {
m_backgroundColor = color;
}
}
// -----------------------------------------
TextStyle& TextStyle::operator|=(const TextStyle& rhs)
{
if (m_foregroundColor == TerminalColor::None) {
m_foregroundColor = rhs.m_foregroundColor;
}
else {
VERIFY(rhs.m_foregroundColor == TerminalColor::None, "can't OR a terminal color");
}
if (m_backgroundColor == TerminalColor::None) {
m_backgroundColor = rhs.m_backgroundColor;
}
else {
VERIFY(rhs.m_backgroundColor == TerminalColor::None, "can't OR a terminal color");
}
m_emphasis = static_cast<Emphasis>(static_cast<uint8_t>(m_emphasis) | static_cast<uint8_t>(rhs.m_emphasis));
return *this;
}
TextStyle fg(TerminalColor foreground)
{
return TextStyle(true, foreground);
}
TextStyle bg(TerminalColor background)
{
return TextStyle(false, background);
}
TextStyle operator|(TextStyle lhs, const TextStyle& rhs)
{
return lhs |= rhs;
}
TextStyle operator|(Emphasis lhs, Emphasis rhs)
{
return TextStyle { lhs } | rhs;
}
bool operator&(Emphasis lhs, Emphasis rhs)
{
return static_cast<uint8_t>(lhs) & static_cast<uint8_t>(rhs);
}
// -----------------------------------------
void setDisplayAttributes(std::stringstream& stream, const TextStyle& style)
{
bool hasForeground = style.foregroundColor() != TerminalColor::None;
bool hasBackground = style.backgroundColor() != TerminalColor::None;
bool hasEmphasis = style.emphasis() != Emphasis::None;
if (!hasForeground && !hasBackground && !hasEmphasis) {
return;
}
stream.write("\033[", 2);
if (hasForeground) {
stream << format("{}", static_cast<uint8_t>(style.foregroundColor()));
}
if (hasBackground) {
if (hasForeground) {
stream.write(";", 1);
}
stream << format("{}", static_cast<uint8_t>(style.backgroundColor()) + 10);
}
stream.write("m", 1);
if (hasEmphasis) {
#define ESCAPE_ATTRIBUTE(escape, attribute) \
if (style.emphasis() & escape) { \
stream.write("\033[", 2); \
stream.write(attribute, 1); \
stream.write("m", 1); \
}
ESCAPE_ATTRIBUTE(Emphasis::Bold, "1");
ESCAPE_ATTRIBUTE(Emphasis::Faint, "2");
ESCAPE_ATTRIBUTE(Emphasis::Italic, "3");
ESCAPE_ATTRIBUTE(Emphasis::Underline, "4");
ESCAPE_ATTRIBUTE(Emphasis::Blink, "5");
ESCAPE_ATTRIBUTE(Emphasis::Reverse, "7");
ESCAPE_ATTRIBUTE(Emphasis::Conceal, "8");
ESCAPE_ATTRIBUTE(Emphasis::Strikethrough, "9");
}
}
void coloredVariadicFormat(std::stringstream& stream, const TextStyle& style, std::string_view format, TypeErasedParameters& parameters)
{
setDisplayAttributes(stream, style);
variadicFormat(stream, format, parameters);
stream.write("\033[0m", 4);
}
// -----------------------------------------
void coloredVariadicPrint(FILE* file, const TextStyle& style, std::string_view format, TypeErasedParameters& parameters)
{
std::stringstream stream;
setDisplayAttributes(stream, style);
variadicFormat(stream, format, parameters);
stream.write("\033[0m", 4);
std::string string = stream.str();
fputs(string.c_str(), file);
}
// -----------------------------------------
ColorPrintOperatorStyle::ColorPrintOperatorStyle(FILE* file, const TextStyle& style)
: m_file(file)
, m_style(style)
, m_stream()
, m_builder(m_stream)
{
setDisplayAttributes(m_stream, style);
}
ColorPrintOperatorStyle::~ColorPrintOperatorStyle()
{
m_stream.write("\033[0m", 4);
std::string string = m_stream.str();
fputs(string.c_str(), m_file);
}
ColorPrintOperatorStyle print(const TextStyle& style)
{
return ColorPrintOperatorStyle(stdout, style);
}
ColorPrintOperatorStyle print(FILE* file, const TextStyle& style)
{
return ColorPrintOperatorStyle(file, style);
}
} // namespace Util::Format

156
src/util/format/color.h

@ -0,0 +1,156 @@
/*
* Copyright (C) 2022 Riyyi
*
* SPDX-License-Identifier: MIT
*/
#pragma once
#include <cstddef> // size_t
#include <cstdint> // uint8_t
#include <cstdio> // FILE, stdout
#include <sstream> // stringstream
#include <string_view>
#include "util/format/format.h"
namespace Util::Format {
enum class TerminalColor : uint8_t {
None = 0,
Black = 30,
Red,
Green,
Yellow,
Blue,
Magenta,
Cyan,
White,
BrightBlack = 90,
BrightRed,
BrightGreen,
BrightYellow,
BrightBlue,
BrightMagenta,
BrightCyan,
BrightWhite
};
// Bit field
enum class Emphasis : uint8_t {
None = 0, // Attribute 0
Bold = 1, // Attribute 1
Faint = 1 << 1, // Attribute 2
Italic = 1 << 2, // Attribute 3
Underline = 1 << 3, // Attribute 4
Blink = 1 << 4, // Attribute 5
Reverse = 1 << 5, // Attribute 7
Conceal = 1 << 6, // Attribute 8
Strikethrough = 1 << 7, // Attribute 9
};
class TextStyle {
private:
friend TextStyle fg(TerminalColor foreground);
friend TextStyle bg(TerminalColor background);
public:
TextStyle(Emphasis emphasis);
// Operator pipe equal, reads the same way as +=
TextStyle& operator|=(const TextStyle& rhs);
TerminalColor foregroundColor() const { return m_foregroundColor; }
TerminalColor backgroundColor() const { return m_backgroundColor; }
Emphasis emphasis() const { return m_emphasis; }
private:
TextStyle(bool isForeground, TerminalColor color);
TerminalColor m_foregroundColor { TerminalColor::None };
TerminalColor m_backgroundColor { TerminalColor::None };
Emphasis m_emphasis { 0 };
};
TextStyle fg(TerminalColor foreground);
TextStyle bg(TerminalColor background);
TextStyle operator|(TextStyle lhs, const TextStyle& rhs);
TextStyle operator|(Emphasis lhs, Emphasis rhs);
bool operator&(Emphasis lhs, Emphasis rhs);
// -----------------------------------------
void coloredVariadicFormat(std::stringstream& stream, const TextStyle& style, std::string_view format, TypeErasedParameters& parameters);
template<typename... Parameters>
std::string format(const TextStyle& style, std::string_view format, const Parameters&... parameters)
{
std::stringstream stream;
VariadicParameters variadicParameters { parameters... };
coloredVariadicFormat(stream, style, format, variadicParameters);
return stream.str();
}
template<typename... Parameters>
void formatTo(std::string& output, const TextStyle& style, std::string_view format, const Parameters&... parameters)
{
std::stringstream stream;
VariadicParameters variadicParameters { parameters... };
coloredVariadicFormat(stream, style, format, variadicParameters);
output += stream.str();
}
// -----------------------------------------
void coloredVariadicPrint(FILE* file, const TextStyle& style, std::string_view format, TypeErasedParameters& parameters);
template<size_t N, typename... Parameters>
void print(const TextStyle& style, const char (&format)[N], const Parameters&... parameters)
{
VariadicParameters variadicParameters { parameters... };
coloredVariadicPrint(stdout, style, { format, N - 1 }, variadicParameters);
}
template<size_t N, typename... Parameters>
void print(FILE* file, const TextStyle& style, const char (&format)[N], const Parameters&... parameters)
{
VariadicParameters variadicParameters { parameters... };
coloredVariadicPrint(file, style, { format, N - 1 }, variadicParameters);
}
// -----------------------------------------
class ColorPrintOperatorStyle {
public:
ColorPrintOperatorStyle(FILE* file, const TextStyle& style);
virtual ~ColorPrintOperatorStyle();
Builder& builder() { return m_builder; }
private:
FILE* m_file;
TextStyle m_style;
std::stringstream m_stream;
Builder m_builder;
};
template<typename T>
const ColorPrintOperatorStyle& operator<<(const ColorPrintOperatorStyle& colorPrintOperatorStyle, const T& value)
{
_format(const_cast<ColorPrintOperatorStyle&>(colorPrintOperatorStyle).builder(), value);
return colorPrintOperatorStyle;
}
ColorPrintOperatorStyle print(const TextStyle& style);
ColorPrintOperatorStyle print(FILE* file, const TextStyle& style);
} // namespace Util::Format
namespace Util {
using Util::Format::format;
using Util::Format::formatTo;
using Util::Format::print;
} // namespace Util

76
src/util/format/format.cpp

@ -0,0 +1,76 @@
/*
* Copyright (C) 2022 Riyyi
*
* SPDX-License-Identifier: MIT
*/
#include <sstream> // stringstream
#include <string>
#include <string_view>
#include "util/format/builder.h"
#include "util/format/format.h"
#include "util/format/parser.h"
#include "util/meta/assert.h"
namespace Util::Format {
void variadicFormatImpl(Builder& builder, Parser& parser, TypeErasedParameters& parameters)
{
// Consume everything until '{' or EOF
const auto literal = parser.consumeLiteral();
builder.putLiteral(literal);
// Reached end of format string
if (parser.isEOF()) {
return;
}
// Consume index + ':'
auto indexMaybe = parser.consumeIndex();
// Get parameter at index, or next
size_t index = indexMaybe.has_value() ? indexMaybe.value() : parameters.tell();
VERIFY(index < parameters.size(), "argument not found at index '%zu'", index);
auto& parameter = parameters.parameter(index);
// Format the parameter
parameter.format(builder, parser, parameter.value);
// Go to next parameter
parameters.ignore();
// Recurse
if (!parser.isEOF()) {
variadicFormatImpl(builder, parser, parameters);
}
}
void variadicFormat(std::stringstream& stream, std::string_view format, TypeErasedParameters& parameters)
{
Builder builder { stream };
Parser parser { format, parameters.size() };
variadicFormatImpl(builder, parser, parameters);
}
// -----------------------------------------
FormatOperatorStyle::FormatOperatorStyle(std::string& output)
: m_output(output)
, m_stream()
, m_builder(m_stream)
{
}
FormatOperatorStyle::~FormatOperatorStyle()
{
m_output = m_stream.str();
}
FormatOperatorStyle formatTo(std::string& output)
{
return FormatOperatorStyle(output);
}
} // namespace Util::Format

119
src/util/format/format.h

@ -0,0 +1,119 @@
/*
* Copyright (C) 2022 Riyyi
*
* SPDX-License-Identifier: MIT
*/
#pragma once
#include <cstddef> // size_t
#include <span>
#include <sstream> // stringstream
#include <string>
#include <string_view>
#include "util/format/formatter.h"
namespace Util::Format {
class Builder;
class Parser;
struct Parameter {
const void* value;
void (*format)(Builder& builder, Parser& parser, const void* value);
};
template<typename T>
void formatParameterValue(Builder& builder, Parser& parser, const void* value)
{
Formatter<T> formatter;
formatter.parse(parser);
formatter.format(builder, *static_cast<const T*>(value));
}
// Type erasure improves both compile time and binary size significantly
class TypeErasedParameters {
public:
const Parameter parameter(size_t index) { return m_parameters[index]; }
size_t tell() const { return m_index; }
size_t size() const { return m_parameters.size(); }
bool isEOF() const { return m_index >= m_parameters.size(); }
void ignore() { m_index++; }
protected:
size_t m_index { 0 };
std::span<const Parameter> m_parameters;
};
template<typename... Parameters>
class VariadicParameters final : public TypeErasedParameters {
public:
VariadicParameters(const Parameters&... parameters)
: m_templatedParameters({ { &parameters, formatParameterValue<Parameters> }... })
{
m_parameters = m_templatedParameters;
}
private:
std::array<Parameter, sizeof...(Parameters)> m_templatedParameters;
};
// -----------------------------------------
void variadicFormatImpl(Builder& builder, Parser& parser, TypeErasedParameters& parameters);
void variadicFormat(std::stringstream& stream, std::string_view format, TypeErasedParameters& parameters);
// -----------------------------------------
template<typename... Parameters>
std::string format(std::string_view format, const Parameters&... parameters)
{
std::stringstream stream;
VariadicParameters variadicParameters { parameters... };
variadicFormat(stream, format, variadicParameters);
return stream.str();
}
template<typename... Parameters>
void formatTo(std::string& output, std::string_view format, const Parameters&... parameters)
{
std::stringstream stream;
VariadicParameters variadicParameters { parameters... };
variadicFormat(stream, format, variadicParameters);
output += stream.str();
}
// -----------------------------------------
class FormatOperatorStyle {
public:
FormatOperatorStyle(std::string& output);
virtual ~FormatOperatorStyle();
Builder& builder() { return m_builder; }
private:
std::string& m_output;
std::stringstream m_stream;
Builder m_builder;
};
template<typename T>
const FormatOperatorStyle& operator<<(const FormatOperatorStyle& formatOperatorStyle, const T& value)
{
_format(const_cast<FormatOperatorStyle&>(formatOperatorStyle).builder(), value);
return formatOperatorStyle;
}
FormatOperatorStyle formatTo(std::string& output);
} // namespace Util::Format
namespace Util {
using Util::Format::format;
using Util::Format::formatTo;
} // namespace Util

117
src/util/format/formatter.cpp

@ -0,0 +1,117 @@
/*
* Copyright (C) 2022 Riyyi
*
* SPDX-License-Identifier: MIT
*/
#include <cstdint> // uint8_t
#include <cstring> // strlen
#include <string>
#include <string_view>
#include "util/format/builder.h"
#include "util/format/formatter.h"
#include "util/format/parser.h"
namespace Util::Format {
// Char
template<>
void Formatter<char>::format(Builder& builder, char value) const
{
if (specifier.type != PresentationType::None && specifier.type != PresentationType::Character) {
// "Type char is a distinct type that has an implementation-defined
// choice of “signed char” or “unsigned char” as its underlying type."
// http://eel.is/c++draft/basic#fundamental
Formatter<signed char> formatter { .specifier = specifier };
return formatter.format(builder, static_cast<signed char>(value));
}
Formatter<std::string_view> formatter { .specifier = specifier };
return formatter.format(builder, { &value, 1 });
}
template<>
void Formatter<bool>::format(Builder& builder, bool value) const
{
switch (specifier.type) {
case PresentationType::Binary:
case PresentationType::BinaryUppercase:
case PresentationType::Character:
case PresentationType::Decimal:
case PresentationType::Octal:
case PresentationType::Hex:
case PresentationType::HexUppercase: {
Formatter<uint8_t> formatter { .specifier = specifier };
return formatter.format(builder, static_cast<uint8_t>(value));
}
default:
break;
};
Formatter<std::string_view> formatter { .specifier = specifier };
formatter.format(builder, value ? "true" : "false");
}
// String
template<>
void Formatter<std::string_view>::format(Builder& builder, std::string_view value) const
{
builder.putString(value, specifier.fill, specifier.align, specifier.width);
}
void Formatter<const char*>::parse(Parser& parser)
{
parser.parseSpecifier(specifier, Parser::ParameterType::CString);
}
void Formatter<const char*>::format(Builder& builder, const char* value) const
{
if (specifier.type == PresentationType::Pointer) {
Formatter<uintptr_t> formatter { .specifier = specifier };
formatter.specifier.alternativeForm = true;
formatter.specifier.type = PresentationType::Hex;
return formatter.format(builder, reinterpret_cast<uintptr_t>(value));
}
Formatter<std::string_view>::format(
builder,
value != nullptr ? std::string_view { value, strlen(value) } : "nullptr");
}
// Pointer
void Formatter<std::nullptr_t>::format(Builder& builder, std::nullptr_t) const
{
Formatter<const void*>::format(builder, 0);
}
// For debugging
void Formatter<Specifier>::format(Builder& builder, Specifier value) const
{
char result[255];
sprintf(result, R"(
fill: {%c}
align: {%c}
sign: {%c}
alternativeForm: {%d}
zeroPadding: {%d}
width: {%zu}
precision: {%d}
type: {%c}
)",
value.fill,
static_cast<char>(value.align),
static_cast<char>(value.sign),
value.alternativeForm,
value.zeroPadding,
value.width,
value.precision,
static_cast<char>(value.type));
builder.putString(std::string_view(result));
}
} // namespace Util::Format

325
src/util/format/formatter.h

@ -0,0 +1,325 @@
/*
* Copyright (C) 2022 Riyyi
*
* SPDX-License-Identifier: MIT
*/
#pragma once
#include <cassert>
#include <cstddef> // size_t
#include <cstdint> // int8_t, uint8_t, uintptr_t
#include <map>
#include <string>
#include <string_view>
#include <type_traits> // is_integral_v, is_same
#include <unordered_map>
#include <vector>
#include "util/format/builder.h"
#include "util/format/parser.h"
#include "util/meta/concepts.h"
namespace Util::Format {
enum class PresentationType : uint8_t {
None,
// Interger
Binary = 'b',
BinaryUppercase = 'B',
Decimal = 'd',
Octal = 'o',
Hex = 'x',
HexUppercase = 'X',
// Floating-point
Hexfloat = 'a',
HexfloatUppercase = 'A',
Exponent = 'e',
ExponentUppercase = 'E',
FixedPoint = 'f',
FixedPointUppercase = 'F',
General = 'g',
GeneralUppercase = 'G',
// Character
Character = 'c',
// String
String = 's',
// Pointer
Pointer = 'p',
// Container
Container = 'C',
};
struct Specifier {
char fill = ' ';
Builder::Align align = Builder::Align::None;
Builder::Sign sign = Builder::Sign::None;
bool alternativeForm = false;
bool zeroPadding = false;
size_t width = 0;
int8_t precision = -1;
PresentationType type = PresentationType::None;
};
template<typename T>
struct Formatter {
Specifier specifier;
void parse(Parser& parser)
{
if constexpr (std::is_same_v<T, char>) {
parser.parseSpecifier(specifier, Parser::ParameterType::Char);
}
else if (std::is_same_v<T, bool>) {
parser.parseSpecifier(specifier, Parser::ParameterType::Char);
}
else if (std::is_same_v<T, std::string_view>) {
parser.parseSpecifier(specifier, Parser::ParameterType::String);
}
}
void format(Builder&, T) const {}
};
// Integral
template<Integral T>
struct Formatter<T> {
Specifier specifier;
void parse(Parser& parser)
{
parser.parseSpecifier(specifier, Parser::ParameterType::Integral);
}
void format(Builder& builder, T value) const
{
if (specifier.type == PresentationType::Character) {
assert(value >= 0 && value <= 127);
Formatter<std::string_view> formatter { .specifier = specifier };
formatter.specifier.type = PresentationType::String;
return formatter.format(builder, { reinterpret_cast<const char*>(&value), 1 });
}
uint8_t base = 0;
bool uppercase = false;
switch (specifier.type) {
case PresentationType::BinaryUppercase:
uppercase = true;
case PresentationType::Binary:
base = 2;
break;
case PresentationType::Octal:
base = 8;
break;
case PresentationType::None:
case PresentationType::Decimal:
base = 10;
break;
case PresentationType::HexUppercase:
uppercase = true;
case PresentationType::Hex:
base = 16;
break;
default:
assert(false);
};
if constexpr (std::is_unsigned_v<T>) {
builder.putU64(
value, base, uppercase, specifier.fill, specifier.align, specifier.sign,
specifier.alternativeForm, specifier.zeroPadding, specifier.width);
}
else {
builder.putI64(
value, base, uppercase, specifier.fill, specifier.align, specifier.sign,
specifier.alternativeForm, specifier.zeroPadding, specifier.width);
}
}
};
// Floating point
template<FloatingPoint T>
struct Formatter<T> {
Specifier specifier;
void parse(Parser& parser)
{
parser.parseSpecifier(specifier, Parser::ParameterType::FloatingPoint);
}
void format(Builder& builder, T value) const
{
if (specifier.precision < 0) {
builder.putF64(value);
return;
}
builder.putF64(value, specifier.precision);
}
};
// Char
template<>
void Formatter<char>::format(Builder& builder, char value) const;
template<>
void Formatter<bool>::format(Builder& builder, bool value) const;
// String
template<>
void Formatter<std::string_view>::format(Builder& builder, std::string_view value) const;
template<>
struct Formatter<std::string> : Formatter<std::string_view> {
};
template<>
struct Formatter<const char*> : Formatter<std::string_view> {
void parse(Parser& parser);
void format(Builder& builder, const char* value) const;
};
template<>
struct Formatter<char*> : Formatter<const char*> {
};
template<size_t N>
struct Formatter<char[N]> : Formatter<const char*> {
};
// Pointer
template<typename T>
struct Formatter<T*> : Formatter<uintptr_t> {
void parse(Parser& parser)
{
parser.parseSpecifier(specifier, Parser::ParameterType::Pointer);
specifier.alternativeForm = true;
specifier.type = PresentationType::Hex;
}
void format(Builder& builder, T* value) const
{
Formatter<uintptr_t>::format(builder, reinterpret_cast<uintptr_t>(value));
}
};
template<>
struct Formatter<std::nullptr_t> : Formatter<const void*> {
void format(Builder& builder, std::nullptr_t) const;
};
// Container
template<typename T>
struct Formatter<std::vector<T>> : Formatter<T> {
Specifier specifier;
void parse(Parser& parser)
{
parser.parseSpecifier(specifier, Parser::ParameterType::Container);
}
void format(Builder& builder, const std::vector<T>& value) const
{
std::string indent = std::string(specifier.width, specifier.fill);
builder.putCharacter('{');
if (specifier.alternativeForm) {
builder.putCharacter('\n');
}
for (auto it = value.cbegin(); it != value.cend(); ++it) {
builder.putString(indent);
Formatter<T>::format(builder, *it);
// Add comma, except after the last element
if (it != std::prev(value.end(), 1)) {
builder.putCharacter(',');
}
else if (!specifier.alternativeForm) {
builder.putString(indent);
}
if (specifier.alternativeForm) {
builder.putCharacter('\n');
}
}
builder.putCharacter('}');
}
};
#define UTIL_FORMAT_FORMAT_AS_MAP(type) \
template<typename K, typename V> \
struct Formatter<type<K, V>> \
: Formatter<K> \
, Formatter<V> { \
Specifier specifier; \
\
void parse(Parser& parser) \
{ \
parser.parseSpecifier(specifier, Parser::ParameterType::Container); \
} \
\
void format(Builder& builder, const type<K, V>& value) const \
{ \
std::string indent = std::string(specifier.width, specifier.fill); \
\
builder.putCharacter('{'); \
if (specifier.alternativeForm) { \
builder.putCharacter('\n'); \
} \
auto last = value.end(); \
for (auto it = value.begin(); it != last; ++it) { \
builder.putString(indent); \
builder.putCharacter('"'); \
Formatter<K>::format(builder, it->first); \
builder.putCharacter('"'); \
builder.putString((specifier.width > 0) ? ": " : ":"); \
Formatter<V>::format(builder, it->second); \
\
/* Add comma, except after the last element */ \
if (std::next(it) != last) { \
builder.putCharacter(','); \
} \
else if (!specifier.alternativeForm) { \
builder.putString(indent); \
} \
\
if (specifier.alternativeForm) { \
builder.putCharacter('\n'); \
} \
} \
builder.putCharacter('}'); \
} \
}
UTIL_FORMAT_FORMAT_AS_MAP(std::map);
UTIL_FORMAT_FORMAT_AS_MAP(std::unordered_map);
// For debugging
template<>
struct Formatter<Specifier> : Formatter<std::nullptr_t> {
void format(Builder& builder, Specifier value) const;
};
} // namespace Util::Format
#if 0
TODO:
- Split assert.h so it can work in format headers! (formatter.h)
v Add unit tests
- Expand Builder::putf60
- Add Builder::putf80 (?)
#endif

109
src/util/format/log.cpp

@ -0,0 +1,109 @@
/*
* Copyright (C) 2022 Riyyi
*
* SPDX-License-Identifier: MIT
*/
#include <cstdio> // FILE
#include <string>
#include "util/format/color.h"
#include "util/format/log.h"
namespace Util::Format {
std::string formatTimeElapsed()
{
return format("{:.3}s ", s_timer.elapsedNanoseconds() / 1000000000.0);
}
std::string formatType(LogType type)
{
std::string output;
formatTo(output, "[");
switch (type) {
case LogType::Trace:
formatTo(output, "trace");
break;
case LogType::Debug:
formatTo(output, fg(TerminalColor::Magenta), "debug");
break;
case LogType::Success:
formatTo(output, fg(TerminalColor::Green), "success");
break;
case LogType::Info:
formatTo(output, fg(TerminalColor::Blue), "info");
break;
case LogType::Warn:
formatTo(output, Emphasis::Bold | fg(TerminalColor::Yellow), "warn");
break;
case LogType::Error:
formatTo(output, Emphasis::Bold | fg(TerminalColor::Red), "error");
break;
case LogType::Critical:
formatTo(output, Emphasis::Bold | fg(TerminalColor::White) | bg(TerminalColor::Red), "critical");
break;
default:
break;
};
formatTo(output, "] ");
return output;
}
// -----------------------------------------
LogOperatorStyle::LogOperatorStyle(FILE* file, LogType type)
: m_file(file)
, m_type(type)
, m_stream()
, m_builder(m_stream)
{
m_stream << formatTimeElapsed();
m_stream << formatType(type);
}
LogOperatorStyle::~LogOperatorStyle()
{
m_stream.write("\n", 1);
std::string string = m_stream.str();
fputs(string.c_str(), m_file);
}
LogOperatorStyle trace()
{
return LogOperatorStyle(stdout, LogType::Trace);
}
LogOperatorStyle debug()
{
return LogOperatorStyle(stdout, LogType::Debug);
}
LogOperatorStyle success()
{
return LogOperatorStyle(stdout, LogType::Success);
}
LogOperatorStyle info()
{
return LogOperatorStyle(stdout, LogType::Info);
}
LogOperatorStyle warn()
{
return LogOperatorStyle(stderr, LogType::Warn);
}
LogOperatorStyle error()
{
return LogOperatorStyle(stderr, LogType::Error);
}
LogOperatorStyle critical()
{
return LogOperatorStyle(stderr, LogType::Critical);
}
} // namespace Util::Format

97
src/util/format/log.h

@ -0,0 +1,97 @@
/*
* Copyright (C) 2022 Riyyi
*
* SPDX-License-Identifier: MIT
*/
#pragma once
#include <cstdio> // FILE, stderr, stdout
#include <string>
#include <string_view>
#include "util/format/format.h"
#include "util/format/print.h"
#include "util/timer.h"
namespace Util::Format {
static Util::Timer s_timer;
enum class LogType : uint8_t {
Trace, // White
Debug, // Purple
Success, // Green
Info, // Blue
Warn, // Bold yellow
Error, // Bold red
Critical, // Bold on red
};
std::string formatTimeElapsed();
std::string formatType(LogType type);
#define LOG_FUNCTION(name, file, type) \
template<size_t N, typename... Parameters> \
void name(const char(&format)[N], const Parameters&... parameters) \
{ \
print(file, "{}", formatTimeElapsed()); \
print(file, "{}", formatType(type)); \
VariadicParameters variadicParameters { parameters... }; \
print(file, format, variadicParameters); \
print(file, "\n"); \
}
LOG_FUNCTION(trace, stdout, LogType::Trace);
LOG_FUNCTION(debug, stdout, LogType::Debug);
LOG_FUNCTION(success, stdout, LogType::Success);
LOG_FUNCTION(info, stdout, LogType::Info);
LOG_FUNCTION(warn, stderr, LogType::Warn);
LOG_FUNCTION(error, stderr, LogType::Error);
LOG_FUNCTION(critical, stderr, LogType::Critical);
// -----------------------------------------
class LogOperatorStyle {
public:
LogOperatorStyle(FILE* file, LogType type);
virtual ~LogOperatorStyle();
Builder& builder() { return m_builder; }
private:
FILE* m_file;
LogType m_type;
std::stringstream m_stream;
Builder m_builder;
};
template<typename T>
const LogOperatorStyle& operator<<(const LogOperatorStyle& logOperatorStyle, const T& value)
{
_format(const_cast<LogOperatorStyle&>(logOperatorStyle).builder(), value);
return logOperatorStyle;
}
LogOperatorStyle trace();
LogOperatorStyle debug();
LogOperatorStyle success();
LogOperatorStyle info();
LogOperatorStyle warn();
LogOperatorStyle error();
LogOperatorStyle critical();
} // namespace Util::Format
namespace Util {
using Util::Format::critical;
using Util::Format::debug;
using Util::Format::error;
using Util::Format::info;
using Util::Format::success;
using Util::Format::trace;
using Util::Format::warn;
} // namespace Util

419
src/util/format/parser.cpp

@ -0,0 +1,419 @@
/*
* Copyright (C) 2022 Riyyi
*
* SPDX-License-Identifier: MIT
*/
#include <algorithm> // replace
#include <cstddef> // size_t
#include <cstdint> // int8_t
#include <limits> // numeric_limits
#include <string>
#include <string_view>
#include "util/format/builder.h"
#include "util/format/formatter.h"
#include "util/format/parser.h"
#include "util/format/print.h"
#include "util/meta/assert.h"
namespace Util::Format {
Parser::Parser(std::string_view format, size_t parameterCount)
: GenericLexer(format)
, m_parameterCount(parameterCount)
{
checkFormatParameterConsistency();
}
Parser::~Parser()
{
}
// -----------------------------------------
void Parser::checkFormatParameterConsistency()
{
size_t braceOpen = 0;
size_t braceClose = 0;
while (!isEOF()) {
char peek0 = peek();
char peek1 = peek(1);
if (peek0 == '{' && peek1 == '{') {
ignore(2);
continue;
}
if (peek0 == '}' && peek1 == '}') {
ignore(2);
continue;
}
if (peek0 == '{') {
braceOpen++;
if (peek1 >= '0' && peek1 <= '9') {
m_mode = ArgumentIndexingMode::Manual;
}
}
if (peek0 == '}') {
braceClose++;
}
ignore();
}
m_index = 0;
VERIFY(!(braceOpen < braceClose), "extra open braces in format string");
VERIFY(!(braceOpen > braceClose), "extra closing braces in format string");
if (m_mode == ArgumentIndexingMode::Automatic) {
VERIFY(!(braceOpen < m_parameterCount), "format string does not reference all passed parameters");
VERIFY(!(braceOpen > m_parameterCount), "format string references nonexistent parameter");
}
}
size_t Parser::stringToNumber(std::string_view value)
{
size_t result = 0;
for (size_t i = 0; i < value.length(); ++i) {
VERIFY(value[i] >= '0' && value[i] <= '9', "unexpected '%c'", value[i]);
result *= 10;
result += value[i] - '0'; // Subtract ASCII 48 to get the number
}
return result;
}
std::string_view Parser::consumeLiteral()
{
const auto begin = tell();
while (!isEOF()) {
char peek0 = peek();
char peek1 = peek(1);
if (peek0 == '{' && peek1 == '{') {
ignore(2);
continue;
}
if (peek0 == '}' && peek1 == '}') {
ignore(2);
continue;
}
// Get literal before the specifier {}
if (peek0 == '{' || peek0 == '}') {
return m_input.substr(begin, tell() - begin);
}
ignore();
}
return m_input.substr(begin);
}
std::optional<size_t> Parser::consumeIndex()
{
if (!consumeSpecific('{')) {
VERIFY_NOT_REACHED();
return {};
}
switch (m_mode) {
case ArgumentIndexingMode::Automatic: {
VERIFY(consumeSpecific(':') || peek() == '}', "expecting ':' or '}', not '%c'", peek());
return {};
}
case ArgumentIndexingMode::Manual: {
const auto begin = tell();
while (!isEOF()) {
char peek0 = peek();
if (peek0 == '}' || peek0 == ':') {
break;
}
VERIFY(peek0 >= '0' && peek0 <= '9', "expecting number, not '%c'", peek0);
ignore();
}
size_t result = stringToNumber(m_input.substr(begin, tell() - begin));
if (peek() == ':') {
ignore();
}
return result;
}
};
VERIFY_NOT_REACHED();
}
void Parser::parseSpecifier(Specifier& specifier, ParameterType type)
{
if (consumeSpecific('}') || isEOF()) {
return;
}
// Alignment
char peek0 = peek();
char peek1 = peek(1);
if (peek1 == '<' || peek1 == '>' || peek1 == '^') {
specifier.fill = peek0;
specifier.align = static_cast<Builder::Align>(peek1);
ignore(2);
}
enum State {
AfterAlign,
AfterSign,
AfterAlternativeForm,
AfterZeroPadding,
AfterWidth,
AfterDot,
AfterPrecision,
AfterType,
} state { State::AfterAlign };
size_t widthBegin = std::numeric_limits<size_t>::max();
size_t precisionBegin = std::numeric_limits<size_t>::max();
size_t widthEnd = 0;
size_t precisionEnd = 0;
std::string_view acceptedTypes = "bdoxaefgscpBXAFG";
static int p = 1;
while (true) {
char peek0 = peek();
if (peek0 == '}') {
ignore();
break;
}
if (isEOF()) {
break;
}
// Sign is only valid for numeric types
if (peek0 == '+' || peek0 == '-' || peek0 == ' ') {
VERIFY(state < State::AfterSign, "unexpected '%c' at this position", peek0);
state = State::AfterSign;
specifier.sign = static_cast<Builder::Sign>(peek0);
}
// Alternative form is only valid for numeric types
if (peek0 == '#') {
VERIFY(state < State::AfterAlternativeForm, "unexpected '#' at this position");
state = State::AfterAlternativeForm;
specifier.alternativeForm = true;
}
// Sign aware zero padding is only valid for numeric types
if (peek0 == '0') {
if (state < State::AfterWidth) {
VERIFY(state < State::AfterZeroPadding, "unexpected '0' at this position");
state = State::AfterZeroPadding;
specifier.zeroPadding = true;
}
}
if (peek0 >= '0' && peek0 <= '9') {
if (widthBegin == std::numeric_limits<size_t>::max() && state < State::AfterDot) {
VERIFY(state < State::AfterWidth, "unexpected '%c' at this position", peek0);
state = State::AfterWidth;
widthBegin = tell();
}
if (precisionBegin == std::numeric_limits<size_t>::max() && state == State::AfterDot) {
state = State::AfterPrecision;
precisionBegin = tell();
}
}
if (peek0 == '.') {
if (state == State::AfterWidth) {
widthEnd = tell();
}
VERIFY(state < State::AfterDot, "unexpected '.' at this position");
state = State::AfterDot;
}
if ((peek0 >= 'a' && peek0 <= 'z')
|| (peek0 >= 'A' && peek0 <= 'Z')) {
if (state == State::AfterWidth) {
widthEnd = tell();
}
if (state == State::AfterPrecision) {
precisionEnd = tell();
}
VERIFY(state < State::AfterType, "unexpected '%c' at this position", peek0);
state = State::AfterType;
VERIFY(acceptedTypes.find(peek0) != std::string_view::npos, "unexpected '%c' at this position", peek0);
specifier.type = static_cast<PresentationType>(peek0);
}
ignore();
}
if (widthBegin != std::numeric_limits<size_t>::max()) {
if (widthEnd == 0) {
// We parse until after the closing '}', so take this into account
widthEnd = tell() - 1;
}
specifier.width = stringToNumber(m_input.substr(widthBegin, widthEnd - widthBegin));
}
if (precisionBegin != std::numeric_limits<size_t>::max()) {
if (precisionEnd == 0) {
// We parse until after the closing '}', so take this into account
precisionEnd = tell() - 1;
}
specifier.precision = static_cast<int8_t>(stringToNumber(m_input.substr(precisionBegin, precisionEnd - precisionBegin)));
}
checkSpecifierType(specifier, type);
if (!p) {
p++;
Util::Format::print("{}\n", specifier);
}
}
constexpr void Parser::checkSpecifierIntegralType(const Specifier& specifier)
{
switch (specifier.type) {
case PresentationType::None:
case PresentationType::Binary:
case PresentationType::BinaryUppercase:
case PresentationType::Character:
case PresentationType::Decimal:
case PresentationType::Octal:
case PresentationType::Hex:
case PresentationType::HexUppercase:
break;
default:
VERIFY(false, "invalid type specifier");
};
// Invalid: precision
VERIFY(specifier.precision == -1, "invalid specifier option");
}
constexpr void Parser::checkSpecifierFloatingPointType(const Specifier& specifier)
{
switch (specifier.type) {
case PresentationType::None:
case PresentationType::Hexfloat:
case PresentationType::HexfloatUppercase:
case PresentationType::Exponent:
case PresentationType::ExponentUppercase:
case PresentationType::FixedPoint:
case PresentationType::FixedPointUppercase:
case PresentationType::General:
case PresentationType::GeneralUppercase:
break;
default:
VERIFY(false, "invalid type specifier");
}
}
constexpr void Parser::checkSpecifierCharType(const Specifier& specifier)
{
checkSpecifierIntegralType(specifier);
// Valid: fill + align, width
// Invalid: sign, alternativeForm, zeroPadding, precision
if (specifier.type == PresentationType::None
|| specifier.type == PresentationType::Character) {
VERIFY(specifier.sign == Builder::Sign::None, "invalid specifier option");
VERIFY(specifier.alternativeForm == false, "invalid specifier option");
VERIFY(specifier.zeroPadding == false, "invalid specifier option");
}
// Precision checked in Integral
}
constexpr void Parser::checkSpecifierCStringType(const Specifier& specifier)
{
switch (specifier.type) {
case PresentationType::None:
case PresentationType::String:
case PresentationType::Pointer:
break;
default:
VERIFY(false, "invalid type specifier");
}
// Valid: fill + align, width
// Invalid: sign, alternativeForm, zeroPadding, precision
VERIFY(specifier.sign == Builder::Sign::None, "invalid specifier option");
VERIFY(specifier.alternativeForm == false, "invalid specifier option");
VERIFY(specifier.zeroPadding == false, "invalid specifier option");
VERIFY(specifier.precision == -1, "invalid specifier option");
}
constexpr void Parser::checkSpecifierStringType(const Specifier& specifier)
{
checkSpecifierCStringType(specifier);
VERIFY(specifier.type != PresentationType::Pointer, "invalid type specifier");
}
constexpr void Parser::checkSpecifierPointerType(const Specifier& specifier)
{
checkSpecifierCStringType(specifier);
VERIFY(specifier.type != PresentationType::String, "invalid type specifier");
}
constexpr void Parser::checkSpecifierContainerType(const Specifier& specifier)
{
switch (specifier.type) {
case PresentationType::None:
case PresentationType::Container:
break;
default:
VERIFY(false, "invalid type specifier");
}
// Valid: fill + align, alternativeForm, width
// Invalid: sign, zeroPadding, precision
VERIFY(specifier.sign == Builder::Sign::None, "invalid specifier option");
VERIFY(specifier.zeroPadding == false, "invalid specifier option");
VERIFY(specifier.precision == -1, "invalid specifier option");
}
constexpr void Parser::checkSpecifierType(const Specifier& specifier, ParameterType type)
{
switch (type) {
case ParameterType::Integral:
checkSpecifierIntegralType(specifier);
break;
case ParameterType::FloatingPoint:
checkSpecifierFloatingPointType(specifier);
break;
case ParameterType::Char:
checkSpecifierCharType(specifier);
break;
case ParameterType::CString:
checkSpecifierCStringType(specifier);
break;
case ParameterType::String:
checkSpecifierStringType(specifier);
break;
case ParameterType::Pointer:
checkSpecifierPointerType(specifier);
break;
case ParameterType::Container:
checkSpecifierContainerType(specifier);
break;
default:
VERIFY_NOT_REACHED();
}
}
} // namespace Util::Format

61
src/util/format/parser.h

@ -0,0 +1,61 @@
/*
* Copyright (C) 2022 Riyyi
*
* SPDX-License-Identifier: MIT
*/
#pragma once
#include <cstddef> // size_t
#include <optional>
#include <string_view>
#include "util/genericlexer.h"
namespace Util::Format {
class Builder;
struct Specifier;
class Parser final : public GenericLexer {
public:
enum class ArgumentIndexingMode {
Automatic, // {} ,{}
Manual, // {0},{1}
};
enum class ParameterType {
Integral,
FloatingPoint,
Char,
CString,
String,
Pointer,
Container,
};
Parser(std::string_view format, size_t parameterCount);
virtual ~Parser();
void checkFormatParameterConsistency();
size_t stringToNumber(std::string_view value);
std::string_view consumeLiteral();
std::optional<size_t> consumeIndex();
void parseSpecifier(Specifier& specifier, ParameterType type);
constexpr void checkSpecifierIntegralType(const Specifier& specifier);
constexpr void checkSpecifierFloatingPointType(const Specifier& specifier);
constexpr void checkSpecifierCharType(const Specifier& specifier);
constexpr void checkSpecifierCStringType(const Specifier& specifier);
constexpr void checkSpecifierStringType(const Specifier& specifier);
constexpr void checkSpecifierPointerType(const Specifier& specifier);
constexpr void checkSpecifierContainerType(const Specifier& specifier);
constexpr void checkSpecifierType(const Specifier& specifier, ParameterType type);
private:
ArgumentIndexingMode m_mode { ArgumentIndexingMode::Automatic };
size_t m_parameterCount { 0 };
};
} // namespace Util::Format

52
src/util/format/print.cpp

@ -0,0 +1,52 @@
/*
* Copyright (C) 2022 Riyyi
*
* SPDX-License-Identifier: MIT
*/
#include <cstdio> // FILE, fputs, stdout, stderr
#include <iomanip> // setprecision
#include <ios> // defaultfloat, fixed
#include <sstream> // stringstream
#include <string>
#include <string_view>
#include "util/format/print.h"
namespace Util::Format {
void variadicPrint(FILE* file, std::string_view format, TypeErasedParameters& parameters)
{
std::stringstream stream;
variadicFormat(stream, format, parameters);
std::string string = stream.str();
fputs(string.c_str(), file);
}
// -----------------------------------------
PrintOperatorStyle::PrintOperatorStyle(FILE* file)
: m_file(file)
, m_stream()
, m_builder(m_stream)
{
}
PrintOperatorStyle::~PrintOperatorStyle()
{
std::string string = m_stream.str();
fputs(string.c_str(), m_file);
}
PrintOperatorStyle print()
{
return PrintOperatorStyle(stdout);
}
PrintOperatorStyle print(FILE* file)
{
return PrintOperatorStyle(file);
}
} // namespace Util::Format

69
src/util/format/print.h

@ -0,0 +1,69 @@
/*
* Copyright (C) 2022 Riyyi
*
* SPDX-License-Identifier: MIT
*/
#pragma once
#include <cstdint> // uint8_t
#include <cstdio> // FILE, stdout
#include <sstream> // stringstream
#include <string_view>
#include "util/format/format.h"
#include "util/timer.h"
namespace Util::Format {
void variadicPrint(FILE* file, std::string_view format, TypeErasedParameters& parameters);
template<size_t N, typename... Parameters>
void print(const char (&format)[N], const Parameters&... parameters)
{
VariadicParameters variadicParameters { parameters... };
variadicPrint(stdout, { format, N - 1 }, variadicParameters);
}
template<size_t N, typename... Parameters>
void print(FILE* file, const char (&format)[N], const Parameters&... parameters)
{
VariadicParameters variadicParameters { parameters... };
variadicPrint(file, { format, N - 1 }, variadicParameters);
}
// -----------------------------------------
class PrintOperatorStyle {
public:
PrintOperatorStyle(FILE* file);
virtual ~PrintOperatorStyle();
Builder& builder() { return m_builder; }
private:
FILE* m_file;
std::stringstream m_stream;
Builder m_builder;
};
template<typename T>
const PrintOperatorStyle& operator<<(const PrintOperatorStyle& printOperatorStyle, const T& value)
{
_format(const_cast<PrintOperatorStyle&>(printOperatorStyle).builder(), value);
return printOperatorStyle;
}
PrintOperatorStyle print();
PrintOperatorStyle print(FILE* file);
// -----------------------------------------
} // namespace Util::Format
namespace Util {
using Util::Format::print;
} // namespace Util

71
src/util/genericlexer.cpp

@ -0,0 +1,71 @@
/*
* Copyright (C) 2022 Riyyi
*
* SPDX-License-Identifier: MIT
*/
#include <algorithm> // max, min
#include "util/genericlexer.h"
#include "util/meta/assert.h"
namespace Util {
GenericLexer::GenericLexer(std::string_view input)
: m_input(input)
{
}
GenericLexer::~GenericLexer()
{
}
// -----------------------------------------
size_t GenericLexer::tell() const
{
return m_index;
}
size_t GenericLexer::tellRemaining() const
{
return m_input.length() - m_index;
}
bool GenericLexer::isEOF() const
{
return m_index >= m_input.length();
}
char GenericLexer::peek(size_t offset) const
{
return (m_index + offset < m_input.length()) ? m_input[m_index + offset] : '\0';
}
void GenericLexer::ignore(size_t count)
{
m_index += std::min(count, m_input.length() - m_index);
}
void GenericLexer::retreat(size_t count)
{
m_index -= std::min(count, m_index);
}
char GenericLexer::consume()
{
VERIFY(!isEOF());
return m_input[m_index++];
}
bool GenericLexer::consumeSpecific(const char& character)
{
if (peek() != character) {
return false;
}
ignore();
return true;
}
} // namespace Util

41
src/util/genericlexer.h

@ -0,0 +1,41 @@
/*
* Copyright (C) 2022 Riyyi
*
* SPDX-License-Identifier: MIT
*/
#pragma once
#include <cstddef> // size_t
#include <string_view>
namespace Util {
class GenericLexer {
public:
GenericLexer(std::string_view input);
virtual ~GenericLexer();
// Position
size_t tell() const;
size_t tellRemaining() const;
bool isEOF() const;
// Access
char peek(size_t offset = 0) const;
// Modifiers
void ignore(size_t count = 1);
void retreat(size_t count = 1);
char consume();
bool consumeSpecific(const char& character);
protected:
size_t m_index { 0 };
std::string_view m_input;
};
} // namespace Util

26
src/util/json/array.cpp

@ -0,0 +1,26 @@
/*
* Copyright (C) 2022 Riyyi
*
* SPDX-License-Identifier: MIT
*/
#include "util/json/array.h"
#include "util/json/value.h"
namespace Util::JSON {
void Array::emplace_back(Value element)
{
m_elements.emplace_back(std::move(element));
}
Value& Array::operator[](size_t index)
{
if (index + 1 > m_elements.size()) {
m_elements.resize(index + 1);
}
return m_elements[index];
}
} // namespace Util::JSON

56
src/util/json/array.h

@ -0,0 +1,56 @@
/*
* Copyright (C) 2022 Riyyi
*
* SPDX-License-Identifier: MIT
*/
#pragma once
#include <utility> // move
#include <vector>
#include "util/json/parser.h"
namespace Util::JSON {
class Value;
class Array {
public:
Array() {}
virtual ~Array() {}
Array(const std::vector<Value>& elements)
: m_elements(elements)
{}
Array(const Array& other)
: m_elements(other.m_elements)
{
}
// Capacity
bool empty() const { return m_elements.empty(); }
size_t size() const { return m_elements.size(); }
void reserve(size_t size) { m_elements.reserve(size); }
// Element access
Value& operator[](size_t index);
Value& at(size_t index) { return m_elements.at(index); }
const Value& at(size_t index) const { return m_elements.at(index); }
const std::vector<Value>& elements() const { return m_elements; }
// Modifiers
void clear() { m_elements.clear(); }
void emplace_back(Value element);
private:
std::vector<Value> m_elements;
};
} // namespace Util::JSON

133
src/util/json/fromjson.h

@ -0,0 +1,133 @@
/*
* Copyright (C) 2022 Riyyi
*
* SPDX-License-Identifier: MIT
*/
#pragma once
#include <algorithm> // transform
#include <cstddef> // nullptr_t
#include <map>
#include <string>
#include <unordered_map>
#include <utility> // forward
#include <vector>
#include "util/json/array.h"
#include "util/json/object.h"
#include "util/meta/assert.h"
#include "util/meta/odr.h"
namespace Util::JSON {
namespace Detail {
// Required for containers with Json::Value type
template<typename Json>
void fromJson(const Json& json, Json& value)
{
value = json;
}
template<typename Json>
void fromJson(const Json& json, std::nullptr_t& null)
{
VERIFY(json.type() == Json::Type::Null);
null = nullptr;
}
template<typename Json>
void fromJson(const Json& json, bool& boolean)
{
VERIFY(json.type() == Json::Type::Bool);
boolean = json.asBool();
}
template<typename Json>
void fromJson(const Json& json, int& number)
{
VERIFY(json.type() == Json::Type::Number);
number = (int)json.asDouble();
}
template<typename Json>
void fromJson(const Json& json, double& number)
{
VERIFY(json.type() == Json::Type::Number);
number = json.asDouble();
}
template<typename Json>
void fromJson(const Json& json, std::string& string)
{
VERIFY(json.type() == Json::Type::String);
string = json.asString();
}
template<typename Json, typename T>
void fromJson(const Json& json, std::vector<T>& array)
{
VERIFY(json.type() == Json::Type::Array);
array.resize(json.size());
std::transform(
json.asArray().elements().begin(),
json.asArray().elements().end(),
array.begin(),
[](const Json& json) {
return json.template get<T>(); // (missing-dependent-template-keyword)
});
}
template<typename Json, typename T>
void fromJson(const Json& json, std::map<std::string, T>& object)
{
VERIFY(json.type() == Json::Type::Object);
object.clear();
for (const auto& [name, value] : json.asObject().members()) {
object.emplace(name, value.template get<T>());
}
}
template<typename Json, typename T>
void fromJson(const Json& json, std::unordered_map<std::string, T>& object)
{
VERIFY(json.type() == Json::Type::Object);
object.clear();
for (const auto& [name, value] : json.asObject().members()) {
object.emplace(name, value.template get<T>());
}
}
struct fromJsonFunction {
template<typename Json, typename T>
auto operator()(const Json& json, T&& value) const
{
return fromJson(json, std::forward<T>(value));
}
};
} // namespace Detail
// Anonymous namespace prevents multiple definition of the reference
namespace {
// Function object
constexpr const auto& fromJson = Util::Detail::staticConst<Detail::fromJsonFunction>; // NOLINT(misc-definitions-in-headers,clang-diagnostic-unused-variable)
} // namespace
} // namespace Util::JSON
// Customization Points
// https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2015/n4381.html
// Json::fromJson is a function object, the type of which is
// Json::Detail::fromJsonFunction. In the Json::Detail namespace are the
// fromJson free functions. The function call operator of fromJsonFunction makes
// an unqualified call to fromJson which, since it shares the Detail namespace
// with the fromJson free functions, will consider those in addition to any
// overloads that are found by argument-dependent lookup.
// Variable templates are linked externally, therefor every translation unit
// will see the same address for Detail::staticConst<Detail::fromJsonFunction>.
// Since Json::fromJson is a reference to the variable template, it too will
// have the same address in all translation units.

114
src/util/json/job.cpp

@ -0,0 +1,114 @@
/*
* Copyright (C) 2022 Riyyi
*
* SPDX-License-Identifier: MIT
*/
#include <algorithm> // count
#include <sstream> // istringstream
#include <string> // getline
#include "util/json/job.h"
#include "util/json/lexer.h"
#include "util/json/parser.h"
#include "util/json/value.h"
namespace Util::JSON {
Job::Job(std::string_view input)
: m_input(input)
{
// FIXME: Make this work for all newline types: \n, \r, \r\n
m_lineNumbersWidth = std::count(m_input.begin(), m_input.end(), '\n');
m_lineNumbersWidth += m_input.back() == '\n' ? 0 : 1;
m_lineNumbersWidth = std::to_string(m_lineNumbersWidth).length();
}
Job::~Job()
{
}
// ------------------------------------------
Value Job::fire()
{
Lexer lexer(this);
lexer.analyze();
if (!m_success) {
return nullptr;
}
Parser parser(this);
Value value = parser.parse();
if (!m_success) {
return nullptr;
}
return value;
}
void Job::printErrorLine(Token token, const char* message)
{
m_success = false;
// Error message
std::string errorFormat = "\033[;1m" // Bold
"JSON:%zu:%zu: "
"\033[31;1m" // Bold red
"error: "
"\033[0m" // Reset
"%s"
"\n";
fprintf(stderr,
errorFormat.c_str(),
token.line + 1,
token.column + 1,
message);
// Get the JSON line that caused the error
std::istringstream input(m_input.data());
std::string line;
for (size_t i = 0; std::getline(input, line); ++i) {
if (i == token.line) {
break;
}
}
// Replace tab indentation with spaces
size_t oldLineLength = line.length();
size_t tabs = line.find_first_not_of('\t');
if (tabs > 0 && tabs < line.size()) {
line = std::string(tabs * 4, ' ') + line.substr(tabs);
}
token.column += line.length() - oldLineLength;
// JSON line
std::string lineFormat = " %"
+ std::to_string(m_lineNumbersWidth)
+ "zu | "
"%s"
"\033[31;1m" // Bold red
"%s"
"\033[0m" // Reset
"\n";
fprintf(stderr,
lineFormat.c_str(),
token.line + 1,
line.substr(0, token.column).c_str(),
line.substr(token.column).c_str());
// Arrow pointer
std::string arrowFormat = " %s | "
"\033[31;1m" // Bold red
"%s^%s"
"\033[0m" // Reset
"\n";
fprintf(stderr,
arrowFormat.c_str(),
std::string(m_lineNumbersWidth, ' ').c_str(),
std::string(token.column, ' ').c_str(),
std::string(line.length() - token.column, '~').c_str());
}
} // namespace Util::JSON

41
src/util/json/job.h

@ -0,0 +1,41 @@
/*
* Copyright (C) 2022 Riyyi
*
* SPDX-License-Identifier: MIT
*/
#pragma once
#include <cstddef> // size_t
#include <string_view>
#include <vector>
#include "util/json/lexer.h"
namespace Util::JSON {
class Value;
class Job {
public:
Job(std::string_view input);
virtual ~Job();
Value fire();
void printErrorLine(Token token, const char* message);
bool success() const { return m_success; }
std::string_view input() const { return m_input; }
std::vector<Token>* tokens() { return &m_tokens; }
private:
bool m_success { true };
std::string_view m_input;
size_t m_lineNumbersWidth { 0 };
std::vector<Token> m_tokens;
};
} // namespace Util::JSON

15
src/util/json/json.h

@ -0,0 +1,15 @@
/*
* Copyright (C) 2022 Riyyi
*
* SPDX-License-Identifier: MIT
*/
#pragma once
#include "util/json/value.h"
namespace Util {
using Json = Util::JSON::Value;
} // namespace Util

217
src/util/json/lexer.cpp

@ -0,0 +1,217 @@
/*
* Copyright (C) 2022 Riyyi
*
* SPDX-License-Identifier: MIT
*/
#include <cstddef>
#include <string>
#include "util/json/job.h"
#include "util/json/lexer.h"
namespace Util::JSON {
Lexer::Lexer(Job* job)
: GenericLexer(job->input())
, m_job(job)
, m_tokens(job->tokens())
{
}
Lexer::~Lexer()
{
}
// -----------------------------------------
void Lexer::analyze()
{
while (m_index < m_input.length()) {
switch (peek()) {
case '{':
m_tokens->push_back({ Token::Type::BraceOpen, m_line, m_column, "{" });
break;
case '}':
m_tokens->push_back({ Token::Type::BraceClose, m_line, m_column, "}" });
break;
case '[':
m_tokens->push_back({ Token::Type::BracketOpen, m_line, m_column, "[" });
break;
case ']':
m_tokens->push_back({ Token::Type::BracketClose, m_line, m_column, "]" });
break;
case ':':
m_tokens->push_back({ Token::Type::Colon, m_line, m_column, ":" });
break;
case ',':
m_tokens->push_back({ Token::Type::Comma, m_line, m_column, "," });
break;
case '"':
if (!consumeString()) {
return;
}
break;
case '-':
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
if (!consumeNumber()) {
return;
}
break;
case 'a':
case 'b':
case 'c':
case 'd':
case 'e':
case 'f':
case 'g':
case 'h':
case 'i':
case 'j':
case 'k':
case 'l':
case 'm':
case 'n':
case 'o':
case 'p':
case 'q':
case 'r':
case 's':
case 't':
case 'u':
case 'v':
case 'w':
case 'x':
case 'y':
case 'z':
if (!consumeLiteral()) {
return;
}
break;
case ' ':
case '\t':
break;
case '\r':
if (peek(1) == '\n') { // CRLF \r\n
break;
}
m_column = -1;
m_line++;
break;
case '\n':
m_column = -1;
m_line++;
break;
default:
// Error!
m_tokens->push_back({ Token::Type::None, m_line, m_column, std::string(1, peek()) });
m_job->printErrorLine(m_tokens->back(),
(std::string() + "unexpected character '" + peek() + "'").c_str());
return;
break;
}
ignore();
m_column++;
}
}
// -----------------------------------------
bool Lexer::consumeString()
{
size_t column = m_column;
std::string symbol = "";
bool escape = false;
char character = consume();
for (;;) {
character = peek();
if (!escape && character == '\\') {
symbol += '\\';
ignore();
escape = true;
continue;
}
if (!escape
&& (character == '"'
|| character == '\r'
|| character == '\n'
|| character == '\0')) {
break;
}
symbol += character;
ignore();
if (escape) {
escape = false;
}
}
m_tokens->push_back({ Token::Type::String, m_line, column, symbol });
if (character != '"') {
m_job->printErrorLine(m_job->tokens()->back(), "strings should be wrapped in double quotes");
return false;
}
return true;
}
bool Lexer::consumeNumberOrLiteral(Token::Type type)
{
size_t index = m_index;
size_t column = m_column;
for (char character;;) {
character = peek();
if (character == '{'
|| character == '}'
|| character == '['
|| character == ']'
|| character == ':'
|| character == ','
|| character == '"'
|| character == ' '
|| character == '\t'
|| character == '\r'
|| character == '\n'
|| character == '\0') {
break;
}
ignore();
}
m_tokens->push_back({ type, m_line, column,
std::string(m_input.substr(index, m_index - index)) });
retreat();
return true;
}
bool Lexer::consumeNumber()
{
return consumeNumberOrLiteral(Token::Type::Number);
}
bool Lexer::consumeLiteral()
{
return consumeNumberOrLiteral(Token::Type::Literal);
}
} // namespace Util::JSON

65
src/util/json/lexer.h

@ -0,0 +1,65 @@
/*
* Copyright (C) 2022 Riyyi
*
* SPDX-License-Identifier: MIT
*/
#pragma once
// The JavaScript Object Notation (JSON) Data Interchange Format
// https://www.rfc-editor.org/rfc/pdfrfc/rfc8259.txt.pdf
#include <cstddef> // size_t
#include <cstdint> // uint8_t
#include <string>
#include <vector>
#include "util/genericlexer.h"
namespace Util::JSON {
class Job;
struct Token {
enum class Type : uint8_t {
None,
BraceOpen, // {
BraceClose, // }
BracketOpen, // [
BracketClose, // ]
Colon, // :
Comma, // ,
String, // "foobar"
Number, // 123.456
Literal, // false/null/true (case sensitive)
};
Type type { Type::None };
size_t line { 0 };
size_t column { 0 };
std::string symbol;
};
// Lexical analyzer
class Lexer final : public Util::GenericLexer {
public:
Lexer(Job* job);
virtual ~Lexer();
void analyze();
private:
bool consumeString();
bool consumeNumberOrLiteral(Token::Type type);
bool consumeNumber();
bool consumeLiteral();
Job* m_job { nullptr };
size_t m_column { 0 };
size_t m_line { 0 };
std::vector<Token>* m_tokens { nullptr };
};
} // namespace Util::JSON

26
src/util/json/object.cpp

@ -0,0 +1,26 @@
/*
* Copyright (C) 2022 Riyyi
*
* SPDX-License-Identifier: MIT
*/
#include "util/json/object.h"
#include "util/json/value.h"
namespace Util::JSON {
void Object::emplace(const std::string& name, Value value)
{
m_members.emplace(name, std::move(value));
}
Value& Object::operator[](const std::string& name)
{
if (m_members.find(name) == m_members.end()) {
emplace(name, {});
}
return m_members.at(name);
}
} // namespace Util::JSON

52
src/util/json/object.h

@ -0,0 +1,52 @@
/*
* Copyright (C) 2022 Riyyi
*
* SPDX-License-Identifier: MIT
*/
#pragma once
#include <map>
#include <string>
#include <utility> // move
#include "util/json/parser.h"
namespace Util::JSON {
class Value;
class Object {
public:
Object() {}
virtual ~Object() {}
Object(const Object& other)
: m_members(other.m_members)
{
}
// Capacity
bool empty() const { return m_members.empty(); }
size_t size() const { return m_members.size(); }
// Member access
Value& operator[](const std::string& name);
Value& at(const std::string& name) { return m_members.at(name); }
const Value& at(const std::string& name) const { return m_members.at(name); }
const std::map<std::string, Value>& members() const { return m_members; }
// Modifiers
void clear() { m_members.clear(); }
void emplace(const std::string& name, Value value);
private:
std::map<std::string, Value> m_members;
};
} // namespace Util::JSON

497
src/util/json/parser.cpp

@ -0,0 +1,497 @@
/*
* Copyright (C) 2022 Riyyi
*
* SPDX-License-Identifier: MIT
*/
#include <algorithm> // count
#include <cstddef> // size_t
#include <cstdint> // uint8_t
#include <cstdio> // printf
#include <map>
#include <string> // stod
#include "util/json/array.h"
#include "util/json/job.h"
#include "util/json/lexer.h"
#include "util/json/object.h"
#include "util/json/parser.h"
#include "util/json/value.h"
#include "util/meta/assert.h"
namespace Util::JSON {
Parser::Parser(Job* job)
: m_job(job)
, m_tokens(m_job->tokens())
{
}
Parser::~Parser()
{
}
// -----------------------------------------
Value Parser::parse()
{
Value result;
if (m_tokens->size() == 0) {
m_job->printErrorLine({}, "expecting token, not 'EOF'");
return result;
}
Token token = peek();
switch (token.type) {
case Token::Type::Literal:
result = consumeLiteral();
break;
case Token::Type::Number:
result = consumeNumber();
break;
case Token::Type::String:
result = consumeString();
break;
case Token::Type::BracketOpen:
result = consumeArray();
break;
case Token::Type::BraceOpen:
result = consumeObject();
break;
case Token::Type::BracketClose:
m_job->printErrorLine(token, "expecting value, not ']'");
m_index++;
break;
case Token::Type::BraceClose:
m_job->printErrorLine(token, "expecting string, not '}'");
m_index++;
break;
default:
m_job->printErrorLine(token, "multiple root elements");
m_index++;
break;
}
if (!isEOF()) {
m_job->printErrorLine(peek(), "multiple root elements");
}
return result;
}
// -----------------------------------------
bool Parser::isEOF()
{
return m_index >= m_tokens->size();
}
Token Parser::peek()
{
VERIFY(!isEOF());
return (*m_tokens)[m_index];
}
Token Parser::consume()
{
VERIFY(!isEOF());
return (*m_tokens)[m_index++];
}
void Parser::ignoreUntil(Token::Type type)
{
while (!isEOF() && peek().type != type) {
++m_index;
}
}
Value Parser::consumeLiteral()
{
Token token = consume();
if (token.symbol == "null") {
return nullptr;
}
else if (token.symbol == "true") {
return true;
}
else if (token.symbol == "false") {
return false;
}
m_job->printErrorLine(token, "invalid literal");
return nullptr;
}
Value Parser::consumeNumber()
{
Token token = consume();
auto reportError = [this](Token token, const std::string& message) -> void {
m_job->printErrorLine(token, message.c_str());
};
// Validation
// number = [ minus ] int [ frac ] [ exp ]
size_t minusPrefix = token.symbol[0] == '-' ? 1 : 0;
// Leading 0s
if (token.symbol[minusPrefix] == '0'
&& token.symbol[minusPrefix + 1] > '0' && token.symbol[minusPrefix + 1] < '9') {
reportError(token, "invalid leading zero");
return nullptr;
}
enum class State : uint8_t {
Int,
Fraction,
Exponent
};
State state = State::Int;
#define CHECK_IF_VALID_NUMBER \
if (character < 48 || character > 57) { \
reportError(token, std::string() + "invalid number, unexpected '" + character + '\''); \
return nullptr; \
}
size_t fractionPosition = 0;
size_t exponentPosition = 0;
size_t length = token.symbol.length();
for (size_t i = 0; i < length; ++i) {
char character = token.symbol[i];
// Int -> Fraction
if (character == '.' && state == State::Int) {
state = State::Fraction;
fractionPosition = i;
continue;
}
// Int/Fraction -> Exponent
else if ((character == 'e' || character == 'E') && state != State::Exponent) {
state = State::Exponent;
exponentPosition = i;
continue;
}
if (state == State::Int) {
if (character == '-') {
if (i == length - 1) {
reportError(token, "expected number after minus");
return nullptr;
}
if (i != 0) {
reportError(token, "invalid minus");
return nullptr;
}
}
else {
CHECK_IF_VALID_NUMBER;
}
}
else if (state == State::Fraction) {
CHECK_IF_VALID_NUMBER;
}
else if (state == State::Exponent) {
if (character == '-' || character == '+') {
if (i == length - 1) {
reportError(token, "expected number after plus/minus");
return nullptr;
}
if (i > exponentPosition + 1) {
reportError(token, "invalid plus/minus");
return nullptr;
}
}
else {
CHECK_IF_VALID_NUMBER;
}
}
}
if (fractionPosition != 0 || exponentPosition != 0) {
if (fractionPosition == exponentPosition - 1) {
reportError(token, "invalid exponent sign, expected number");
return nullptr;
}
if (fractionPosition == length - 1 || exponentPosition == length - 1) {
reportError(token, "invalid number");
return nullptr;
}
}
return std::stod(token.symbol);
}
Value Parser::consumeString()
{
Token token = consume();
auto reportError = [this](Token token, const std::string& message) -> void {
m_job->printErrorLine(token, message.c_str());
};
// FIXME: support \u Unicode character escape sequence
auto getPrintableString = [](char character) -> std::string {
if (character == '"' || character == '\\' || character == '/'
|| (character >= 0 && character <= 31)) {
switch (character) {
case '"':
return "\\\"";
break;
case '\\':
return "\\\\";
break;
case '/':
return "/";
break;
case '\b':
return "\\b";
break;
case '\f':
return "\\f";
break;
case '\n':
return "\\n";
break;
case '\r':
return "\\r";
break;
case '\t':
return "\\t";
break;
default:
char buffer[7];
sprintf(buffer, "\\u%0.4X", character);
return std::string(buffer);
break;
}
}
return std::string() + character;
};
std::string string;
bool escape = false;
for (char character : token.symbol) {
if (!escape) {
if (character == '\\') {
escape = true;
continue;
}
if (character == '"' || (character >= 0 && character <= 31)) {
reportError(token, "invalid string, unescaped character found");
return nullptr;
}
}
string += getPrintableString(character);
if (escape) {
escape = false;
}
}
return string;
}
Value Parser::consumeArray()
{
m_index++;
auto reportError = [this](Token token, const std::string& message) -> void {
m_job->printErrorLine(token, message.c_str());
// After an error, try to find the closing bracket
ignoreUntil(Token::Type::BracketClose);
m_index++;
};
Value array = Value::Type::Array;
Token token;
for (;;) {
// EOF
if (isEOF()) {
reportError(m_tokens->at(m_index - 1), "expecting closing ']' at end");
break;
}
token = peek();
if (token.type == Token::Type::Literal) {
array.emplace_back(consumeLiteral());
}
else if (token.type == Token::Type::Number) {
array.emplace_back(consumeNumber());
}
else if (token.type == Token::Type::String) {
array.emplace_back(consumeString());
}
else if (token.type == Token::Type::BracketOpen) {
array.emplace_back(consumeArray());
}
else if (token.type == Token::Type::BraceOpen) {
array.emplace_back(consumeObject());
}
else if (token.type == Token::Type::BracketClose) {
// Trailing comma
if (array.m_value.array->size() > 0) {
reportError(m_tokens->at(m_index - 1), "invalid comma, expecting ']'");
break;
}
}
else {
reportError(token, "expecting value or ']', not '" + token.symbol + "'");
break;
}
// EOF
if (isEOF()) {
reportError(token, "expecting closing ']' at end");
break;
}
// Find , or ]
token = consume();
if (token.type == Token::Type::Comma) {
continue;
}
else if (token.type == Token::Type::BracketClose) {
break;
}
else {
reportError(m_tokens->at(m_index - 1), "expecting comma or ']', not '" + token.symbol + "'");
break;
}
}
return array;
}
Value Parser::consumeObject()
{
m_index++;
auto reportError = [this](Token token, const std::string& message) -> void {
m_job->printErrorLine(token, message.c_str());
// After an error, try to find the closing brace
ignoreUntil(Token::Type::BraceClose);
m_index++;
};
Value object = Value::Type::Object;
Token token;
std::string name;
std::map<std::string, uint8_t> unique;
for (;;) {
// EOF
if (isEOF()) {
reportError(m_tokens->at(m_index - 1), "expecting closing '}' at end");
break;
}
token = consume();
if (token.type == Token::Type::BraceClose) {
// Trailing comma
if (object.m_value.object->size() > 0) {
reportError(m_tokens->at(m_index - 1), "invalid comma, expecting '}'");
}
// Empty object
break;
}
if (token.type != Token::Type::String) {
reportError(token, "expecting string or '}', not '" + token.symbol + "'");
break;
}
// Find member name
m_index--;
Value tmpName = consumeString();
if (tmpName.m_type != Value::Type::String) {
ignoreUntil(Token::Type::BraceClose);
m_index++;
break;
}
// Check if name exists in hashmap
name = *tmpName.m_value.string;
if (unique.find(name) != unique.end()) {
reportError(token, "duplicate name '" + token.symbol + "', names should be unique");
break;
}
// Add name to hashmap
unique.insert({ name, 0 });
// EOF
if (isEOF()) {
reportError(token, "expecting colon, not 'EOF'");
reportError(token, "expecting closing '}' at end");
break;
}
// Find :
token = consume();
if (token.type != Token::Type::Colon) {
reportError(token, "expecting colon, not '" + token.symbol + "'");
break;
}
// EOF
if (isEOF()) {
reportError(token, "expecting value, not 'EOF'");
reportError(token, "expecting closing '}' at end");
break;
}
// Add member (name:value pair) to object
token = peek();
if (token.type == Token::Type::Literal) {
object.emplace(name, consumeLiteral());
}
else if (token.type == Token::Type::Number) {
object.emplace(name, consumeNumber());
}
else if (token.type == Token::Type::String) {
object.emplace(name, consumeString());
}
else if (token.type == Token::Type::BracketOpen) {
object.emplace(name, consumeArray());
}
else if (token.type == Token::Type::BraceOpen) {
object.emplace(name, consumeObject());
}
else {
reportError(token, "expecting value, not '" + token.symbol + "'");
break;
}
// EOF
if (isEOF()) {
reportError(token, "expecting closing '}' at end");
break;
}
// Find , or }
token = consume();
if (token.type == Token::Type::Comma) {
continue;
}
else if (token.type == Token::Type::BraceClose) {
break;
}
else {
reportError(token, "expecting comma or '}', not '" + token.symbol + "'");
break;
}
}
return object;
}
} // namespace Util::JSON

45
src/util/json/parser.h

@ -0,0 +1,45 @@
/*
* Copyright (C) 2022 Riyyi
*
* SPDX-License-Identifier: MIT
*/
#pragma once
#include <cstddef> // size_t
#include <vector>
#include "util/json/lexer.h"
namespace Util::JSON {
class Job;
class Value;
class Parser {
public:
Parser(Job* job);
virtual ~Parser();
Value parse();
private:
bool isEOF();
Token peek();
Token consume();
void ignoreUntil(Token::Type type);
Value consumeLiteral();
Value consumeNumber();
Value consumeString();
Value consumeArray();
Value consumeObject();
Job* m_job { nullptr };
size_t m_index { 0 };
std::vector<Token>* m_tokens { nullptr };
};
} // namespace Util::JSON

155
src/util/json/serializer.cpp

@ -0,0 +1,155 @@
/*
* Copyright (C) 2022 Riyyi
*
* SPDX-License-Identifier: MIT
*/
#include <cstdint> // uint32_t
#include <iterator> // next
#include <sstream> // ostringstream
#include <string>
#include "util/json/array.h"
#include "util/json/lexer.h"
#include "util/json/object.h"
#include "util/json/serializer.h"
namespace Util::JSON {
Serializer::Serializer(const uint32_t indent, const char indentCharacter)
: m_indent(indent)
, m_indentCharacter(indentCharacter)
, m_compact(indent == 0)
{
}
Serializer::~Serializer()
{
}
// ------------------------------------------
std::string Serializer::dump(const Value& value)
{
dumpHelper(value);
return m_output;
}
// ------------------------------------------
void Serializer::dumpHelper(const Value& value, const uint32_t indentLevel)
{
switch (value.m_type) {
case Value::Type::Null:
m_output += "null";
break;
case Value::Type::Bool:
m_output += value.m_value.boolean ? "true" : "false";
break;
case Value::Type::Number: {
std::ostringstream os;
os << value.m_value.number;
m_output += os.str();
break;
}
case Value::Type::String:
m_output += "\"" + *value.m_value.string + "\"";
break;
case Value::Type::Array:
dumpArray(value, indentLevel);
break;
case Value::Type::Object:
dumpObject(value, indentLevel);
break;
default:
break;
}
}
void Serializer::dumpArray(const Value& value, const uint32_t indentLevel)
{
m_output += '[';
if (!m_compact) {
m_output += '\n';
}
// Empty Array early return
if (value.m_value.array->empty()) {
m_output += ']';
return;
}
size_t i = 0;
auto it = value.m_value.array->elements().cbegin();
if (!m_indent) {
for (; i < value.m_value.array->size() - 1; ++i, ++it) {
dumpHelper(*it, indentLevel + 1);
m_output += ',';
}
dumpHelper(*it, indentLevel + 1);
}
else {
std::string indentation = std::string(m_indent * (indentLevel + 1), m_indentCharacter);
for (; i < value.m_value.array->size() - 1; ++i, ++it) {
m_output += indentation;
dumpHelper(*it, indentLevel + 1);
m_output += ",\n";
}
m_output += indentation;
dumpHelper(*it, indentLevel + 1);
m_output += '\n';
// Append indentation
m_output += std::string(m_indent * indentLevel, m_indentCharacter);
}
m_output += "]";
}
void Serializer::dumpObject(const Value& value, const uint32_t indentLevel)
{
m_output += '{';
if (!m_compact) {
m_output += '\n';
}
// Empty Object early return
if (value.m_value.object->empty()) {
m_output += '}';
return;
}
size_t i = 0;
auto it = value.m_value.object->members().cbegin();
if (!m_indent) {
for (; i < value.m_value.object->size() - 1; ++i, ++it) {
m_output += '"' + it->first + "\":";
dumpHelper(it->second, indentLevel + 1);
m_output += ',';
}
m_output += '"' + it->first + "\":";
dumpHelper(it->second, indentLevel + 1);
}
else {
std::string indentation = std::string(m_indent * (indentLevel + 1), m_indentCharacter);
for (; i < value.m_value.object->size() - 1; ++i, ++it) {
m_output += indentation;
m_output += '"' + it->first + "\": ";
dumpHelper(it->second, indentLevel + 1);
m_output += ",\n";
}
m_output += indentation;
m_output += '"' + it->first + "\": ";
dumpHelper(it->second, indentLevel + 1);
m_output += '\n';
// Append indentation
m_output += std::string(m_indent * indentLevel, m_indentCharacter);
}
m_output += '}';
}
} // namespace Util::JSON

35
src/util/json/serializer.h

@ -0,0 +1,35 @@
/*
* Copyright (C) 2022 Riyyi
*
* SPDX-License-Identifier: MIT
*/
#pragma once
#include <cstdint> // uint32_t
#include <string>
#include "util/json/value.h"
namespace Util::JSON {
class Serializer {
public:
Serializer(const uint32_t indent = 0, const char indentCharacter = ' ');
virtual ~Serializer();
std::string dump(const Value& value);
private:
void dumpHelper(const Value& value, const uint32_t indentLevel = 0);
void dumpArray(const Value& value, const uint32_t indentLevel = 0);
void dumpObject(const Value& value, const uint32_t indentLevel = 0);
std::string m_output;
uint32_t m_indent { 0 };
char m_indentCharacter { ' ' };
bool m_compact { true };
};
} // namespace Util::JSON

153
src/util/json/tojson.h

@ -0,0 +1,153 @@
/*
* Copyright (C) 2022 Riyyi
*
* SPDX-License-Identifier: MIT
*/
#pragma once
#include <cassert> // assert
#include <cstddef> // nullptr_t
#include <map>
#include <string>
#include <unordered_map>
#include <utility> // forward
#include "util/json/array.h"
#include "util/json/object.h"
#include "util/meta/odr.h"
namespace Util::JSON {
namespace Detail {
struct jsonConstructor {
template<typename Json>
static void construct(Json& json, bool boolean)
{
json.destroy();
json.m_type = Json::Type::Bool;
json.m_value.boolean = boolean;
}
template<typename Json>
static void construct(Json& json, int number)
{
json.destroy();
json.m_type = Json::Type::Number;
json.m_value.number = (double)number;
}
template<typename Json>
static void construct(Json& json, double number)
{
json.destroy();
json.m_type = Json::Type::Number;
json.m_value.number = number;
}
template<typename Json>
static void construct(Json& json, const char* string)
{
json.destroy();
json.m_type = Json::Type::String;
json.m_value.string = new std::string(string);
}
template<typename Json>
static void construct(Json& json, const std::string& string)
{
json.destroy();
json.m_type = Json::Type::String;
json.m_value.string = new std::string(string);
}
template<typename Json>
static void construct(Json& json, const Array& array)
{
json.destroy();
json.m_type = Json::Type::Array;
json.m_value.array = new Array(array);
}
template<typename Json, typename T>
static void construct(Json& json, const std::vector<T>& array)
{
json.destroy();
json.m_type = Json::Type::Array;
json.m_value.array = new Array;
json.m_value.array->reserve(array.size());
for (const T& value : array) {
json.m_value.array->emplace_back(value);
}
}
template<typename Json>
static void construct(Json& json, const Object& object)
{
json.destroy();
json.m_type = Json::Type::Object;
json.m_value.object = new Object(object);
}
template<typename Json, typename T>
static void construct(Json& json, const std::map<std::string, T>& object)
{
json.destroy();
json.m_type = Json::Type::Object;
json.m_value.object = new Object;
for (const auto& [name, value] : object) {
json.m_value.object->emplace(name, value);
}
}
template<typename Json, typename T>
static void construct(Json& json, const std::unordered_map<std::string, T>& object)
{
json.destroy();
json.m_type = Json::Type::Object;
json.m_value.object = new Object;
for (const auto& [name, value] : object) {
json.m_value.object->emplace(name, value);
}
}
};
template<typename Json, typename T>
void toJson(Json& json, const T& value)
{
jsonConstructor::construct(json, value);
}
struct toJsonFunction {
template<typename Json, typename T>
auto operator()(Json& json, T&& value) const
{
return toJson(json, std::forward<T>(value));
}
};
} // namespace Detail
// Anonymous namespace prevents multiple definition of the reference
namespace {
// Function object
constexpr const auto& toJson = Util::Detail::staticConst<Detail::toJsonFunction>; // NOLINT(misc-definitions-in-headers,clang-diagnostic-unused-variable)
} // namespace
} // namespace Util::JSON
// Customization Points
// https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2015/n4381.html
// Json::toJson is a function object, the type of which is
// Json::Detail::toJsonFunction. In the Json::Detail namespace are the toJson
// free functions. The function call operator of toJsonFunction makes an
// unqualified call to toJson which, since it shares the Detail namespace with
// the toJson free functions, will consider those in addition to any overloads
// that are found by argument-dependent lookup.
// Variable templates are linked externally, therefor every translation unit
// will see the same address for Detail::staticConst<Detail::toJsonFunction>.
// Since Json::toJson is a reference to the variable template, it too will have
// the same address in all translation units.

340
src/util/json/value.cpp

@ -0,0 +1,340 @@
/*
* Copyright (C) 2022 Riyyi
*
* SPDX-License-Identifier: MIT
*/
#include <algorithm> // all_of
#include <cstdint> // uint32_t
#include <fstream> // >>
#include <iostream> // istream, ostream
#include <string>
#include <utility> // move, swap
#include "util/format/builder.h"
#include "util/json/array.h"
#include "util/json/job.h"
#include "util/json/object.h"
#include "util/json/serializer.h"
#include "util/json/value.h"
#include "util/meta/assert.h"
namespace Util::JSON {
Value::Value(std::nullptr_t)
: Value(Type::Null)
{
}
Value::Value(Type type)
: m_type(type)
{
switch (m_type) {
case Type::Bool:
m_value.boolean = false;
break;
case Type::Number:
m_value.number = 0.0;
break;
case Type::String:
m_value.string = new std::string;
break;
case Type::Array:
m_value.array = new Array;
break;
case Type::Object:
m_value.object = new Object;
break;
case Type::Null:
default:
break;
}
}
Value::Value(const std::initializer_list<Value>& values)
{
bool isObject = std::all_of(values.begin(), values.end(), [](const Value& value) {
return value.type() == Type::Array
&& value.size() == 2
&& value[0].m_type == Type::String;
});
if (!isObject) {
m_type = Type::Array;
m_value.array = new Array(values);
}
else {
m_type = Type::Object;
m_value.object = new Object;
for (auto& value : values) {
m_value.object->emplace(std::move(*value[0].m_value.string),
std::move(value[1]));
}
}
}
// Copy constructor
Value::Value(const Value& other)
: m_type(other.m_type)
{
switch (m_type) {
case Type::Bool:
m_value.boolean = other.m_value.boolean;
break;
case Type::Number:
m_value.number = other.m_value.number;
break;
case Type::String:
m_value.string = new std::string(*other.m_value.string);
break;
case Type::Array:
m_value.array = new Array(*other.m_value.array);
break;
case Type::Object:
m_value.object = new Object(*other.m_value.object);
break;
case Type::Null:
default:
break;
}
}
// Move constructor
Value::Value(Value&& other) noexcept
: Value(Type::Null) // Initialize via default construction
{
// Allow std::swap as a fallback on ADL failure
using std::swap;
// Unqualified call to swap, allow ADL to operate and find best match
swap(*this, other);
}
// Copy assignment
// Move assignment
Value& Value::operator=(Value other)
{
// Allow std::swap as a fallback on ADL failure
using std::swap;
// Unqualified call to swap, allow ADL to operate and find best match
swap(*this, other);
return *this;
}
void swap(Value& left, Value& right) noexcept
{
std::swap(left.m_type, right.m_type);
std::swap(left.m_value, right.m_value);
}
// ------------------------------------------
void Value::clear()
{
switch (m_type) {
case Type::Bool:
m_value.boolean = false;
break;
case Type::Number:
m_value.number = 0.0;
break;
case Type::String:
m_value.string->clear();
break;
case Type::Array:
m_value.array->clear();
break;
case Type::Object:
m_value.object->clear();
break;
case Type::Null:
default:
break;
}
}
Value Value::parse(std::string_view input)
{
return Job(input).fire();
}
Value Value::parse(std::ifstream& file)
{
Value value;
file >> value;
return value;
}
std::string Value::dump(const uint32_t indent, const char indentCharacter) const
{
Serializer serializer(indent, indentCharacter);
return serializer.dump(*this);
}
void Value::emplace_back(Value value)
{
// Implicitly convert null to an array
if (m_type == Type::Null) {
m_type = Type::Array;
m_value.array = new Array;
}
VERIFY(m_type == Type::Array);
m_value.array->emplace_back(value);
}
void Value::emplace(const std::string& key, Value value)
{
// Implicitly convert null to an object
if (m_type == Type::Null) {
m_type = Type::Object;
m_value.object = new Object;
}
VERIFY(m_type == Type::Object);
m_value.object->emplace(key, value);
}
bool Value::exists(size_t index) const
{
return index < size();
}
bool Value::exists(const std::string& key) const
{
VERIFY(m_type == Type::Object);
return m_value.object->members().find(key) != m_value.object->members().end();
}
// ------------------------------------------
Value& Value::operator[](size_t index)
{
// Implicitly convert null to an array
if (m_type == Type::Null) {
m_type = Type::Array;
m_value.array = new Array;
}
VERIFY(m_type == Type::Array);
return (*m_value.array)[index];
}
Value& Value::operator[](const std::string& key)
{
// Implicitly convert null to an object
if (m_type == Type::Null) {
m_type = Type::Object;
m_value.object = new Object;
}
VERIFY(m_type == Type::Object);
return (*m_value.object)[key];
}
const Value& Value::operator[](size_t index) const
{
VERIFY(m_type == Type::Array);
return (*m_value.array)[index];
}
const Value& Value::operator[](const std::string& key) const
{
VERIFY(m_type == Type::Object);
return (*m_value.object)[key];
}
Value& Value::at(size_t index)
{
VERIFY(m_type == Type::Array);
return m_value.array->at(index);
}
Value& Value::at(const std::string& key)
{
VERIFY(m_type == Type::Object);
return m_value.object->at(key);
}
const Value& Value::at(size_t index) const
{
VERIFY(m_type == Type::Array);
return m_value.array->at(index);
}
const Value& Value::at(const std::string& key) const
{
VERIFY(m_type == Type::Object);
return m_value.object->at(key);
}
// ------------------------------------------
size_t Value::size() const
{
switch (m_type) {
case Type::Null:
return 0;
case Type::Array:
return m_value.array->size();
case Type::Object:
return m_value.object->size();
case Type::Bool:
case Type::Number:
case Type::String:
default:
return 1;
}
}
// ------------------------------------------
void Value::destroy()
{
switch (m_type) {
case Type::String:
delete m_value.string;
break;
case Type::Array:
delete m_value.array;
break;
case Type::Object:
delete m_value.object;
break;
case Type::Null:
case Type::Bool:
case Type::Number:
default:
break;
}
}
// ------------------------------------------
std::istream& operator>>(std::istream& input, Value& value)
{
std::string inputString;
char buffer[4096];
while (input.read(buffer, sizeof(buffer))) {
inputString.append(buffer, sizeof(buffer));
}
inputString.append(buffer, input.gcount());
value = Job(inputString).fire();
return input;
}
std::ostream& operator<<(std::ostream& output, const Value& value)
{
return output << value.dump(4);
}
void format(Util::Format::Builder& builder, const Value& value)
{
builder.putString(value.dump(4));
}
} // namespace Util::JSON

165
src/util/json/value.h

@ -0,0 +1,165 @@
/*
* Copyright (C) 2022 Riyyi
*
* SPDX-License-Identifier: MIT
*/
#pragma once
#include <cstddef> // nullptr_t, size_t
#include <cstdint> // uint8_t, uint32_t
#include <initializer_list>
#include <iostream> // istream, ostream
#include <string>
#include <utility> // forward
#include "util/format/builder.h"
#include "util/json/fromjson.h"
#include "util/json/tojson.h"
namespace Util::JSON {
class Array;
class Object;
class Value {
private:
friend Detail::jsonConstructor;
friend class Parser;
friend class Serializer;
public:
enum class Type : uint8_t {
Null, // null (case sensitive!)
Bool, // true/false (case sensitive!)
Number, // 123
String, // ""
Array, // []
Object, // {}
};
// --------------------------------------
// Constructors
Value(std::nullptr_t = nullptr);
Value(Type type);
Value(const std::initializer_list<Value>& values);
template<typename T>
Value(T value)
{
toJson(*this, std::forward<T>(value));
}
// Rule of Five:
// Copy constructor
Value(const Value& other);
// Move constructor
Value(Value&& other) noexcept;
// Copy assignment
// Move assignment
Value& operator=(Value other);
// Destructor
virtual ~Value() { destroy(); }
friend void swap(Value& left, Value& right) noexcept;
// --------------------------------------
static Value parse(std::string_view input);
static Value parse(std::ifstream& file);
std::string dump(const uint32_t indent = 0, const char indentCharacter = ' ') const;
void clear();
void emplace_back(Value value);
void emplace(const std::string& key, Value value);
bool exists(size_t index) const;
bool exists(const std::string& key) const;
// --------------------------------------
// Array index operator
Value& operator[](size_t index);
Value& operator[](const std::string& key);
const Value& operator[](size_t index) const;
const Value& operator[](const std::string& key) const;
Value& at(size_t index);
Value& at(const std::string& key);
const Value& at(size_t index) const;
const Value& at(const std::string& key) const;
// --------------------------------------
template<typename T>
T get() const
{
T type;
fromJson(*this, type);
return type;
}
template<typename T>
void getTo(T& type) const
{
fromJson(*this, type);
}
// --------------------------------------
Type type() const { return m_type; }
size_t size() const;
bool asBool() const { return m_value.boolean; }
double asDouble() const { return m_value.number; }
const std::string& asString() const { return *m_value.string; }
const Array& asArray() const { return *m_value.array; }
const Object& asObject() const { return *m_value.object; }
private:
void destroy();
Type m_type { Type::Null };
union {
bool boolean;
double number;
std::string* string;
Array* array;
Object* object;
} m_value {};
};
std::istream& operator>>(std::istream& input, Value& value);
std::ostream& operator<<(std::ostream& output, const Value& value);
void format(Util::Format::Builder& builder, const Value& value);
} // namespace Util::JSON
/**
* User-defined string literal
*
* Example usage: auto json = "[ 3.14, true, null ]"_json;
*/
// inline Util::JSON::Value operator"" _json(const char* input, size_t length)
// {
// return Util::JSON::Value::parse(std::string(input, length));
// }
// TODO:
// - find
// - custom iterator
// - begin
// - end
// v parse function that accepts ifstream > redirect to operator>>
// - dump(-1), 0 inserts only newlines, -1 is full compact(?)
// v add timer pause and unpause functions
// v std::prev -> std::next in serializer
// v serializer: output as member, does this speed it up?
// - add Badge pattern, ex: Serializer constructor isnt needed publically
// v Rename namespace Json -> Util::Json, create easy include json.h
// - look into this for correctness
// https://www.json.org/json-en.html

96
src/util/meta/assert.h

@ -0,0 +1,96 @@
/*
* Copyright (C) 2022 Riyyi
*
* SPDX-License-Identifier: MIT
*/
#pragma once
#include <cstddef> // size_t
#include <cstdint> // uint32_t
#include <cstring> // strlen
#include <fstream> // ifstream
#include <string>
#include <string_view>
#include "util/format/format.h"
#include "util/meta/compiler.h"
#define CRASH() asm volatile("int $0x03");
#ifndef NDEBUG
#define VERIFY(expr, ...) (static_cast<bool>(expr) ? (void)0 : Util::__assertion_failed(#expr, __FILE__, __LINE__, FUNCTION_MACRO __VA_OPT__(, ) __VA_ARGS__))
#define VERIFY_NOT_REACHED() VERIFY(false)
#else
#define VERIFY(expr, ...) (static_cast<bool>(expr) ? (void)0 : CRASH()
#define VERIFY_NOT_REACHED() CRASH()
#endif
#ifndef NDEBUG
namespace Util {
template<typename... Parameters>
inline void __assertion_failed(const char* assertion, const char* file, uint32_t line, const char* function, const Parameters&... parameters)
{
// Get the line that caused the error
std::ifstream source(file);
std::string content;
if (source.is_open()) {
for (uint32_t i = 0; std::getline(source, content); ++i) {
if (i == line - 1) {
break;
}
}
}
// Replace tab indentation with spaces
size_t tabs = content.find_first_not_of('\t');
if (tabs > 0 && tabs < content.size()) {
content = std::string(tabs * 4, ' ') + content.substr(tabs);
}
// Find the assertion in the line
size_t column = content.find(assertion);
size_t assertionLength = strlen(assertion);
if (column == std::string::npos) {
column = content.find_first_not_of(' ');
assertionLength = content.length() - column;
}
// Error message
fprintf(stderr,
"\033[;1m%s:%u:%zu "
"\033[31;1merror: "
"\033[0massertion failed",
file, line, column + 1);
if constexpr (sizeof...(Parameters) > 0) {
fprintf(stderr, ": ");
// Cant use the formatting library to print asserts caused by the formatting library
std::string_view functionString = function;
if (functionString.find("Util::Format::") != std::string_view::npos
&& functionString.find("Util::GenericLexer::") != std::string_view::npos) {
std::string message;
formatTo(message, parameters...);
fprintf(stderr, "%s", message.c_str());
}
else {
fprintf(stderr, parameters...);
}
}
// Code line
fprintf(stderr, "\n %u | %s\033[31;1m%s\033[0m%s\n", line,
content.substr(0, column).c_str(), // Whitespace at front
content.substr(column, assertionLength).c_str(), // Error portion
content.substr(column + assertionLength).c_str()); // Rest of the line
// Arrow pointer
fprintf(stderr, " %s | %s\033[31;1m^%s\033[0m\n",
std::string(std::to_string(line).length(), ' ').c_str(), // Line number spacing
std::string(column, ' ').c_str(), // Content spacing
std::string(assertionLength - 1, '~').c_str()); // Arrow pointer
CRASH();
}
} // namespace Util
#endif

27
src/util/meta/compiler.h

@ -0,0 +1,27 @@
/*
* Copyright (C) 2022 Riyyi
*
* SPDX-License-Identifier: MIT
*/
#pragma once
// Compiler
#if defined(__clang__)
#define GCC
#elif defined(__GNUG__) || (defined(__GNUC__) && defined(__cplusplus))
#define GCC
#elif defined(__INTEL_COMPILER) // Supports some GCC extensions
#define GCC
#elif defined(_MSC_VER)
#define MSVC
#endif
// Non-standard function macro
#ifdef GCC
#define FUNCTION_MACRO __PRETTY_FUNCTION__ // GCC extension
#elif MSVC
#define FUNCTION_MACRO __FUNCSIG__
#else
#define FUNCTION_MACRO __func__ // C99
#endif

22
src/util/meta/concepts.h

@ -0,0 +1,22 @@
/*
* Copyright (C) 2022 Riyyi
*
* SPDX-License-Identifier: MIT
*/
#pragma once
#include <type_traits> // is_integral_v, floating_point_v
namespace Util::Concepts {
template<class T>
concept Integral = std::is_integral_v<T>;
template<typename T>
concept FloatingPoint = std::is_floating_point_v<T>;
} // namespace Util::Concepts
using Util::Concepts::FloatingPoint;
using Util::Concepts::Integral;

20
src/util/meta/odr.h

@ -0,0 +1,20 @@
/*
* Copyright (C) 2022 Riyyi
*
* SPDX-License-Identifier: MIT
*/
#pragma once
namespace Util {
namespace Detail {
// Avoid ODR (One Definition Rule) violations,
// variable templates are required to have external linkage
template<typename T>
constexpr T staticConst {};
} // namespace Detail
} // namespace Util

53
src/util/shell.cpp

@ -0,0 +1,53 @@
#include <cstdio> // pclose, perror, popen
#include <string>
#include <string_view>
#include "util/shell.h"
namespace Util {
Shell::Shell()
{
}
Shell::Shell(const std::string& output, int status)
: m_output(output)
, m_status(status)
{
}
Shell Shell::operator()(const char* command)
{
FILE* shell = popen(command, "r");
if (!shell) {
perror("\033[31;1mError:\033[0m popen");
return { "", -1 };
}
std::string output;
constexpr int bufferSize = 4096;
char buffer[bufferSize];
while (fgets(buffer, sizeof(buffer), shell)) {
output.append(buffer);
}
int status = pclose(shell);
if (status < 0) {
perror("\033[31;1mError:\033[0m pclose");
}
return { output, status };
}
Shell Shell::operator()(std::string command)
{
return operator()(command.c_str());
}
Shell Shell::operator()(std::string_view command)
{
return operator()(command.data());
}
} // namespace Util

33
src/util/shell.h

@ -0,0 +1,33 @@
/*
* Copyright (C) 2022 Riyyi
*
* SPDX-License-Identifier: MIT
*/
#pragma once
#include <string>
#include <string_view>
namespace Util {
class Shell {
public:
Shell();
virtual ~Shell() {}
Shell operator()(const char* command);
Shell operator()(std::string command);
Shell operator()(std::string_view command);
std::string output() const { return m_output; }
int status() const { return m_status; }
private:
Shell(const std::string& output, int status);
std::string m_output;
int m_status { 0 };
};
} // namespace Util

53
src/util/singleton.h

@ -0,0 +1,53 @@
/*
* Copyright (C) 2022 Riyyi
*
* SPDX-License-Identifier: MIT
*/
#pragma once
#include <cassert>
namespace Util {
template<typename T>
class Singleton {
public:
static inline T& the()
{
if (s_instance == nullptr) {
s_instance = new T { s {} };
}
return *s_instance;
}
static inline void destroy()
{
if (s_instance) {
delete s_instance;
}
s_instance = nullptr;
}
// Remove copy constructor and copy assignment operator
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
Singleton(Singleton&&) = delete;
Singleton& operator=(Singleton&&) = delete;
protected:
Singleton() {}
// Constructor token
struct s {};
private:
static T* s_instance;
};
template<typename T>
T* Singleton<T>::s_instance = nullptr;
} // namespace Util

328
src/util/system.cpp

@ -0,0 +1,328 @@
#include <cerrno> // errno, EAGAIN, EINTR
#include <cstddef> // size_t
#include <cstdio> // perror, ssize_t
#include <cstdlib> // exit, WEXITSTATUS
#include <cstring> // strcpy, strtok
#include <functional> // function
#include <sstream> // istringstream
#include <string>
#include <string_view>
#include <sys/wait.h> // waitpid
#include <unistd.h> // close, dup2, execvp, fork, pipe, read
#include <vector>
#include "util/system.h"
namespace Util {
System::System()
{
}
System::System(const std::vector<std::string>& arguments)
: m_arguments(arguments)
{
}
System System::operator()()
{
return exec();
}
System System::operator()(const char* command)
{
return operator()(std::string { command });
}
System System::operator()(std::string command)
{
std::vector<std::string> arguments;
size_t index = 0;
while (index != std::string::npos) {
index = command.find_first_of(" ");
arguments.push_back(command.substr(0, index));
command = command.substr(index + 1);
}
return { arguments };
}
System System::operator()(std::string_view command)
{
return operator()(std::string { command });
}
System System::operator()(const std::vector<const char*>& arguments)
{
std::vector<std::string> stringArguments(arguments.size(), "");
for (size_t i = 0; i < arguments.size(); ++i) {
stringArguments[i] = arguments[i];
}
return { stringArguments };
}
System System::operator()(const std::vector<std::string>& arguments)
{
return { arguments };
}
System System::operator()(const std::vector<std::string_view>& arguments)
{
std::vector<std::string> stringArguments(arguments.size(), "");
for (size_t i = 0; i < arguments.size(); ++i) {
stringArguments[i] = arguments[i];
}
return { stringArguments };
}
// Shell equivalent ;
System System::operator+(System rhs)
{
auto lhs = *this;
lhs.exec();
rhs.m_output.append(lhs.m_output);
rhs.m_error.append(lhs.m_error);
rhs.exec();
return rhs;
}
System System::operator|(System rhs)
{
auto lhs = *this;
lhs.exec();
rhs.exec(lhs.m_output);
return rhs;
}
System System::operator&&(System rhs)
{
auto lhs = *this;
lhs.exec();
if (lhs.m_status > 0) {
return lhs;
}
rhs.m_output.append(lhs.m_output);
rhs.m_error.append(lhs.m_error);
rhs.exec();
return rhs;
}
System System::operator||(System rhs)
{
auto lhs = *this;
lhs.exec();
if (lhs.m_status == 0) {
return lhs;
}
rhs.m_output.append(lhs.m_output);
rhs.m_error.append(lhs.m_error);
rhs.exec();
return rhs;
}
// cut -f -d
System& System::cut(uint32_t field, char delimiter)
{
exec();
return apply([&field, &delimiter](std::vector<std::string>& lines) {
for (auto& line : lines) {
size_t count = 1;
size_t index = 0;
while (index != std::string::npos) {
if (count == field) {
line = line.substr(0, line.find_first_of(delimiter));
break;
}
index = line.find_first_of(delimiter);
line = line.substr(index + 1);
count++;
}
}
});
}
System& System::sort(bool unique)
{
exec();
return apply([&unique](std::vector<std::string>& lines) {
std::sort(lines.begin(), lines.end());
if (unique) {
auto last = std::unique(lines.begin(), lines.end());
lines.erase(last, lines.end());
}
});
}
// tail -n
System& System::tail(int32_t number, bool starting)
{
exec();
return apply([&number, &starting](std::vector<std::string>& lines) {
number = abs(number);
if (!starting) {
lines.erase(lines.begin(), lines.end() - number);
}
else {
lines.erase(lines.begin(), lines.begin() + number - 1);
}
});
}
System& System::apply(LineCallback callback)
{
exec();
std::vector<std::string> lines;
auto stream = std::istringstream(m_output);
std::string line;
while (std::getline(stream, line)) {
lines.push_back(line);
}
callback(lines);
m_output.clear();
for (size_t i = 0; i < lines.size(); ++i) {
m_output.append(lines.at(i) + '\n');
}
return *this;
}
void System::print(const std::vector<std::string>& arguments)
{
if (!arguments.size()) {
return;
}
printf("----------\n");
printf("size: %zu\n", arguments.size());
printf("command: ");
for (size_t i = 0; i < arguments.size(); ++i) {
printf("%s ", arguments.at(i).c_str());
}
printf("\n");
printf("----------\n");
}
// -----------------------------------------
System System::exec(std::string input)
{
if (m_arguments.empty()) {
return *this;
}
int stdinFd[2];
int stdoutFd[2];
int stderrFd[2];
if (pipe(stdinFd) < 0) {
perror("\033[31;1mError:\033[0m pipe");
}
if (pipe(stdoutFd) < 0) {
perror("\033[31;1mError:\033[0m pipe");
}
if (pipe(stderrFd) < 0) {
perror("\033[31;1mError:\033[0m pipe");
}
pid_t pid = fork();
switch (pid) {
// Failed
case -1:
perror("\033[31;1mError:\033[0m fork");
break;
// Child
case 0: {
close(stdinFd[WriteFileDescriptor]);
dup2(stdinFd[ReadFileDescriptor], fileno(stdin));
close(stdinFd[ReadFileDescriptor]);
close(stdoutFd[ReadFileDescriptor]);
dup2(stdoutFd[WriteFileDescriptor], fileno(stdout));
close(stdoutFd[WriteFileDescriptor]);
close(stderrFd[ReadFileDescriptor]);
dup2(stderrFd[WriteFileDescriptor], fileno(stderr));
close(stderrFd[WriteFileDescriptor]);
std::vector<char*> charArguments(m_arguments.size() + 1, 0);
for (size_t i = 0; i < m_arguments.size(); ++i) {
charArguments[i] = const_cast<char*>(m_arguments[i].c_str());
}
execvp(charArguments[0], &charArguments[0]);
exit(0);
}
// Parent
default:
m_arguments.clear();
break;
}
close(stdinFd[ReadFileDescriptor]);
if (!input.empty()) {
write(stdinFd[WriteFileDescriptor], input.c_str(), input.size());
}
close(stdinFd[WriteFileDescriptor]);
readFromFileDescriptor(stdoutFd, m_output);
readFromFileDescriptor(stderrFd, m_error);
int result;
do {
result = waitpid(pid, &m_status, 0);
} while (result == -1 && errno == EINTR);
m_status = WEXITSTATUS(m_status);
return *this;
}
void System::readFromFileDescriptor(int fileDescriptor[2], std::string& output)
{
close(fileDescriptor[WriteFileDescriptor]);
constexpr int bufferSize = 4096;
char buffer[bufferSize];
for (;;) {
const ssize_t result = read(fileDescriptor[ReadFileDescriptor], buffer, bufferSize);
if (result > 0) {
output.append(buffer, result);
}
// EOF
if (result == 0) {
break;
}
// Error
else if (result == -1) {
if (errno != EAGAIN && errno != EINTR) {
perror("\033[31;1mError:\033[0m read");
break;
}
}
}
close(fileDescriptor[ReadFileDescriptor]);
}
} // namespace Util

68
src/util/system.h

@ -0,0 +1,68 @@
/*
* Copyright (C) 2022 Riyyi
*
* SPDX-License-Identifier: MIT
*/
#pragma once
#include <cstdint> // int32_t, uint32_t
#include <functional> // function
#include <string>
#include <string_view>
#include <vector>
namespace Util {
using LineCallback = std::function<void(std::vector<std::string>&)>;
class System {
public:
System();
virtual ~System() {}
enum FileDescriptor {
ReadFileDescriptor,
WriteFileDescriptor,
};
System operator()();
System operator()(const char* command);
System operator()(std::string command);
System operator()(std::string_view command);
System operator()(const std::vector<const char*>& arguments);
System operator()(const std::vector<std::string>& arguments);
System operator()(const std::vector<std::string_view>& arguments);
// Operator order
// + -> | -> && -> ||
System operator+(System rhs);
System operator|(System rhs);
System operator&&(System rhs);
System operator||(System rhs);
System& cut(uint32_t field, char delimiter = '\t');
System& sort(bool unique = false);
System& tail(int32_t number, bool starting = false);
System& apply(LineCallback callback);
void print(const std::vector<std::string>& arguments);
const std::vector<std::string>& arguments() const { return m_arguments; }
std::string output() const { return m_output; }
std::string error() const { return m_error; }
int status() const { return m_status; }
private:
System(const std::vector<std::string>& arguments);
System exec(std::string input = "");
void readFromFileDescriptor(int fileDescriptor[2], std::string& output);
std::vector<std::string> m_arguments;
std::string m_output;
std::string m_error;
int m_status { 0 };
};
} // namespace Util

131
src/util/timer.cpp

@ -0,0 +1,131 @@
#include <chrono> // high_resolution_clock, seconds, milliseconds, microseconds, nanoseconds
#include <cstdint> // uint64_t
#include <cstdio> // printf
#include "util/timer.h"
namespace Util {
Timer::Timer()
: m_running(true)
, m_accumulated(TimePoint::min())
, m_start(now())
{
}
Timer::Timer(const TimePoint& timePoint)
: m_running(true)
, m_accumulated(TimePoint::min())
, m_start(timePoint)
{
}
// -----------------------------------------
Timer Timer::operator-(const Timer& timer)
{
return Timer(TimePoint { m_start - timer.start() });
}
TimePoint Timer::now()
{
return std::chrono::high_resolution_clock::now();
}
void Timer::pause()
{
if (!m_running) {
return;
}
m_accumulated += now() - m_start;
m_running = false;
}
void Timer::resume()
{
if (m_running) {
return;
}
m_running = true;
m_start = now();
}
template<typename To, typename From>
To Timer::to(From from)
{
return std::chrono::duration_cast<To>(from);
}
uint64_t Timer::toSeconds()
{
return to<std::chrono::seconds>(m_start.time_since_epoch()).count();
}
uint64_t Timer::toMilliseconds()
{
return to<std::chrono::milliseconds>(m_start.time_since_epoch()).count();
}
uint64_t Timer::toMicroseconds()
{
return to<std::chrono::microseconds>(m_start.time_since_epoch()).count();
}
uint64_t Timer::toNanoseconds()
{
return to<std::chrono::nanoseconds>(m_start.time_since_epoch()).count();
}
template<typename T>
uint64_t Timer::elapsed()
{
uint64_t elapsed = 0;
if (m_running) {
elapsed += std::chrono::duration_cast<T>(now() - m_start).count();
}
elapsed += std::chrono::duration_cast<T>(m_accumulated - TimePoint::min()).count();
return elapsed;
}
uint64_t Timer::elapsedSeconds()
{
return elapsed<std::chrono::seconds>();
}
uint64_t Timer::elapsedMilliseconds()
{
return elapsed<std::chrono::milliseconds>();
}
uint64_t Timer::elapsedMicroseconds()
{
return elapsed<std::chrono::microseconds>();
}
uint64_t Timer::elapsedNanoseconds()
{
return elapsed<std::chrono::nanoseconds>();
}
void Timer::fancyPrint(uint64_t nanoseconds)
{
if (nanoseconds > 999999999) {
printf("%.3fs", nanoseconds / 1000000000.0);
}
else if (nanoseconds > 999999) {
printf("%.0fms", nanoseconds / 1000000.0);
}
else if (nanoseconds > 999) {
printf("%.0fμs", nanoseconds / 1000.0);
}
else {
printf("%luns", nanoseconds);
}
}
} // namespace Util

51
src/util/timer.h

@ -0,0 +1,51 @@
/*
* Copyright (C) 2022 Riyyi
*
* SPDX-License-Identifier: MIT
*/
#pragma once
#include <chrono> // high_resolution_clock
#include <cstdint> // uint64_t
namespace Util {
using TimePoint = std::chrono::high_resolution_clock::time_point;
class Timer {
public:
Timer();
Timer(const TimePoint& timePoint);
Timer operator-(const Timer& timer);
static TimePoint now();
void pause();
void resume();
template<typename To, typename From>
To to(From from);
uint64_t toSeconds();
uint64_t toMilliseconds();
uint64_t toMicroseconds();
uint64_t toNanoseconds();
template<typename T>
uint64_t elapsed();
uint64_t elapsedSeconds();
uint64_t elapsedMilliseconds();
uint64_t elapsedMicroseconds();
uint64_t elapsedNanoseconds();
static void fancyPrint(uint64_t nanoseconds);
const TimePoint& start() const { return m_start; }
private:
bool m_running { true };
TimePoint m_accumulated;
TimePoint m_start;
};
} // namespace Util

79
test/macro.h

@ -0,0 +1,79 @@
#ifndef TEST_H
#define TEST_H
#include <cstdio> // fprintf
#include <iostream> // cerr
#define GET_2TH_ARG(arg1, arg2, ...) arg2
#define GET_3TH_ARG(arg1, arg2, arg3, ...) arg3
#define GET_4TH_ARG(arg1, arg2, arg3, arg4, ...) arg4
#define MACRO_CHOOSER_1(macro, ...) \
GET_2TH_ARG(__VA_ARGS__, macro##_1, )
#define MACRO_CHOOSER_2(macro, ...) \
GET_3TH_ARG(__VA_ARGS__, macro##_2, macro##_1, )
#define MACRO_CHOOSER_3(macro, ...) \
GET_4TH_ARG(__VA_ARGS__, macro##_3, macro##_2, macro##_1, )
// -----------------------------------------
#define EXPECT_IMPL(x, result) \
if (!(x)) { \
fprintf(stderr, " \033[31;1mFAIL:\033[0m %s:%d: EXPECT(%s) failed\n", \
__FILE__, __LINE__, #x); \
Test::TestSuite::the().currentTestCaseFailed(); \
result; \
}
#define EXPECT_1(x) \
EXPECT_IMPL(x, (void)0)
#define EXPECT_2(x, result) \
EXPECT_IMPL(x, result)
#define EXPECT(...) \
MACRO_CHOOSER_2(EXPECT, __VA_ARGS__) \
(__VA_ARGS__)
// -----------------------------------------
#define EXPECT_EQ_IMPL(a, b, result) \
if (a != b) { \
std::cerr << " \033[31;1mFAIL:\033[0m " << __FILE__ << ":" << __LINE__ \
<< ": EXPECT_EQ(" << #a << ", " << #b ") failed with" \
<< " lhs='" << a << "' and rhs='" << b << "'" << std::endl; \
Test::TestSuite::the().currentTestCaseFailed(); \
result; \
}
#define EXPECT_EQ_2(a, b) \
EXPECT_EQ_IMPL(a, b, (void)0)
#define EXPECT_EQ_3(a, b, result) \
EXPECT_EQ_IMPL(a, b, result)
#define EXPECT_EQ(...) \
MACRO_CHOOSER_3(EXPECT_EQ, __VA_ARGS__) \
(__VA_ARGS__)
// -----------------------------------------
#define EXPECT_NE_IMPL(a, b, result) \
if (a == b) { \
std::cerr << " \033[31;1mFAIL:\033[0m " << __FILE__ << ":" << __LINE__ \
<< ": EXPECT_NE(" << #a << ", " << #b ") failed with" \
<< " lhs='" << a << "' and rhs='" << b << "'" << std::endl; \
Test::TestSuite::the().currentTestCaseFailed(); \
result; \
}
#define EXPECT_NE_2(a, b) \
EXPECT_NE_IMPL(a, b, (void)0)
#define EXPECT_NE_3(a, b, result) \
EXPECT_NE_IMPL(a, b, result)
#define EXPECT_NE(...) \
MACRO_CHOOSER_3(EXPECT_NE, __VA_ARGS__) \
(__VA_ARGS__)
#endif // TEST_H

8
test/main.cpp

@ -0,0 +1,8 @@
#include "testsuite.h"
int main(int, const char*[])
{
Test::TestSuite::the().run();
return 0;
}

45
test/testcase.h

@ -0,0 +1,45 @@
#ifndef TEST_CASE_H
#define TEST_CASE_H
#include <functional>
#include <string>
#define __TEST_CASE_FUNCTION(x) __test##x
#define __TEST_CASE_STRUCT(x) __testStruct##x
#define TEST_CASE(x) \
static void __TEST_CASE_FUNCTION(x)(); \
struct __TEST_CASE_STRUCT(x) { \
__TEST_CASE_STRUCT(x) \
() \
{ \
Test::TestSuite::the().addCase( \
{ #x, __TEST_CASE_FUNCTION(x) }); \
} \
}; \
static struct __TEST_CASE_STRUCT(x) __TEST_CASE_STRUCT(x); \
static void __TEST_CASE_FUNCTION(x)()
namespace Test {
using TestFunction = std::function<void()>;
class TestCase {
public:
TestCase(const char* name, TestFunction&& function)
: m_name(name)
, m_function(function)
{
}
const char* name() const { return m_name; }
const TestFunction& function() const { return m_function; }
private:
const char* m_name { nullptr };
TestFunction m_function;
};
} // namespace Test
#endif // TEST_CASE_H

76
test/testsuite.cpp

@ -0,0 +1,76 @@
#include <cstddef> // size_t
#include <cstdio> // fclose, fopen, printf, stdout
#include <string>
#include <vector>
#include "testsuite.h"
#include "util/timer.h"
namespace Test {
TestSuite::TestSuite(s)
{
m_outputStd = stdout;
m_outputErr = stderr;
m_outputNull = fopen("/dev/null", "w"); // Windows: nul
}
TestSuite::~TestSuite()
{
fclose(m_outputNull);
}
void TestSuite::run()
{
const char* escapePass = "\033[42;30;1m";
const char* escapeFail = "\033[41;1m";
const char* escapeGreen = "\033[32m";
const char* escapeGrey = "\033[37m";
const char* escapeRed = "\033[31m";
const char* escapeReset = "\033[0m";
printf("\n");
printf("---- Running %zu Test Cases ----\n", m_cases.size());
Util::Timer totalTimer;
size_t caseFailedCount = 0;
for (size_t i = 0; i < m_cases.size(); ++i) {
printf(" START %s (%zu/%zu)\n", m_cases.at(i).name(), i + 1, m_cases.size());
m_currentTestCasePassed = true;
Util::Timer caseTimer;
m_cases.at(i).function()();
double elapsed = caseTimer.elapsedNanoseconds();
std::string state;
if (m_currentTestCasePassed) {
state.append(escapePass);
state.append(" PASS ");
state.append(escapeReset);
}
else {
caseFailedCount++;
state.append(escapeFail);
state.append(" FAIL ");
state.append(escapeReset);
}
printf("%s %s %s(", state.c_str(), m_cases.at(i).name(), escapeGrey);
Util::Timer::fancyPrint(elapsed);
printf(")%s\n", escapeReset);
}
printf("\n");
printf("Tests: %s%zu failed%s, %s%zu passed%s, %zu total\n",
escapeRed, caseFailedCount, escapeReset,
escapeGreen, m_cases.size() - caseFailedCount, escapeReset,
m_cases.size());
printf("Time: ");
Util::Timer::fancyPrint(totalTimer.elapsedNanoseconds());
printf("\n");
}
} // namespace Test

36
test/testsuite.h

@ -0,0 +1,36 @@
#ifndef TEST_SUITE_H
#define TEST_SUITE_H
#include <cstdio> // FILE
#include <vector>
#include "testcase.h"
#include "util/singleton.h"
namespace Test {
class TestSuite final : public Util::Singleton<TestSuite> {
public:
TestSuite(s);
virtual ~TestSuite();
void run();
void addCase(const TestCase& testCase) { m_cases.push_back(testCase); }
void currentTestCaseFailed() { m_currentTestCasePassed = false; }
FILE* outputStd() const { return m_outputStd; }
FILE* outputErr() const { return m_outputErr; }
FILE* outputNull() const { return m_outputNull; }
private:
bool m_currentTestCasePassed { true };
FILE* m_outputStd { nullptr };
FILE* m_outputErr { nullptr };
FILE* m_outputNull { nullptr };
std::vector<TestCase> m_cases;
};
} // namespace Test
#endif // TEST_SUITE_H

949
test/unit/testutilargparser.cpp

@ -0,0 +1,949 @@
#include <functional> // function
#include <string>
#include <vector>
#include "macro.h"
#include "testcase.h"
#include "testsuite.h"
#include "util/argparser.h"
bool runParser(std::vector<const char*> arguments, std::function<void(Util::ArgParser&)> initializer = {})
{
stdout = Test::TestSuite::the().outputNull();
Util::ArgParser parser;
if (initializer) {
initializer(parser);
}
arguments.insert(arguments.begin(), "app");
auto result = parser.parse(arguments.size(), arguments.data());
stdout = Test::TestSuite::the().outputStd();
return result;
}
// -----------------------------------------
TEST_CASE(NoArguments)
{
auto result = runParser({});
EXPECT_EQ(result, true);
}
// -----------------------------------------
TEST_CASE(NonExistentArguments)
{
auto result = runParser({ "-n", "-e" });
EXPECT_EQ(result, false);
result = runParser({ "--non", "--existent" });
EXPECT_EQ(result, false);
result = runParser({ "-n", "-e", "--non", "--existent" });
EXPECT_EQ(result, false);
result = runParser({ "no", "handling" });
EXPECT_EQ(result, false);
}
// -----------------------------------------
TEST_CASE(RequiredStringArguments)
{
// Single required string argument
std::string stringArg1 = "";
auto result = runParser({ "my-required-argument" }, [&](auto& parser) {
parser.addArgument(stringArg1, nullptr, nullptr, nullptr, Util::ArgParser::Required::Yes);
});
EXPECT_EQ(result, true);
EXPECT_EQ(stringArg1, "my-required-argument");
// Single required string argument, not given
stringArg1 = "";
result = runParser({}, [&](auto& parser) {
parser.addArgument(stringArg1, nullptr, nullptr, nullptr, Util::ArgParser::Required::Yes);
});
EXPECT_EQ(result, false);
EXPECT_EQ(stringArg1, "");
// Multiple required string arguments
stringArg1 = "";
std::string stringArg2 = "";
std::string stringArg3 = "";
result = runParser({ "my", "required", "argument" }, [&](auto& parser) {
parser.addArgument(stringArg1, nullptr, nullptr, nullptr, Util::ArgParser::Required::Yes);
parser.addArgument(stringArg2, nullptr, nullptr, nullptr, Util::ArgParser::Required::Yes);
parser.addArgument(stringArg3, nullptr, nullptr, nullptr, Util::ArgParser::Required::Yes);
});
EXPECT_EQ(result, true);
EXPECT_EQ(stringArg1, "my");
EXPECT_EQ(stringArg2, "required");
EXPECT_EQ(stringArg3, "argument");
// Multiple required string arguments, not given
stringArg1 = "";
stringArg2 = "";
stringArg3 = "";
result = runParser({}, [&](auto& parser) {
parser.addArgument(stringArg1, nullptr, nullptr, nullptr, Util::ArgParser::Required::Yes);
parser.addArgument(stringArg2, nullptr, nullptr, nullptr, Util::ArgParser::Required::Yes);
parser.addArgument(stringArg3, nullptr, nullptr, nullptr, Util::ArgParser::Required::Yes);
});
EXPECT_EQ(result, false);
EXPECT_EQ(stringArg1, "");
EXPECT_EQ(stringArg2, "");
EXPECT_EQ(stringArg3, "");
}
// -----------------------------------------
TEST_CASE(OptionalStringArguments)
{
// Single optional string argument
std::string stringArg1 = "";
auto result = runParser({ "my-optional-argument" }, [&](auto& parser) {
parser.addArgument(stringArg1, nullptr, nullptr, nullptr, Util::ArgParser::Required::Optional);
});
EXPECT_EQ(result, true);
EXPECT_EQ(stringArg1, "my-optional-argument");
// Single optional string argument, not given
stringArg1 = "";
result = runParser({}, [&](auto& parser) {
parser.addArgument(stringArg1, nullptr, nullptr, nullptr, Util::ArgParser::Required::Optional);
});
EXPECT_EQ(result, true);
EXPECT_EQ(stringArg1, "");
// Multiple optional string arguments
stringArg1 = "";
std::string stringArg2 = "";
std::string stringArg3 = "";
result = runParser({ "my", "optional", "argument" }, [&](auto& parser) {
parser.addArgument(stringArg1, nullptr, nullptr, nullptr, Util::ArgParser::Required::Optional);
parser.addArgument(stringArg2, nullptr, nullptr, nullptr, Util::ArgParser::Required::Optional);
parser.addArgument(stringArg3, nullptr, nullptr, nullptr, Util::ArgParser::Required::Optional);
});
EXPECT_EQ(result, true);
EXPECT_EQ(stringArg1, "my");
EXPECT_EQ(stringArg2, "optional");
EXPECT_EQ(stringArg3, "argument");
// Multiple optional string arguments, not given
stringArg1 = "";
stringArg2 = "";
stringArg3 = "";
result = runParser({}, [&](auto& parser) {
parser.addArgument(stringArg1, nullptr, nullptr, nullptr, Util::ArgParser::Required::Optional);
parser.addArgument(stringArg2, nullptr, nullptr, nullptr, Util::ArgParser::Required::Optional);
parser.addArgument(stringArg3, nullptr, nullptr, nullptr, Util::ArgParser::Required::Optional);
});
EXPECT_EQ(result, true);
EXPECT_EQ(stringArg1, "");
EXPECT_EQ(stringArg2, "");
EXPECT_EQ(stringArg3, "");
}
// -----------------------------------------
TEST_CASE(NonRequiredStringArguments)
{
// Single non-required string argument
std::string stringArg1 = "";
auto result = runParser({ "my-non-required-argument" }, [&](auto& parser) {
parser.addArgument(stringArg1, nullptr, nullptr, nullptr, Util::ArgParser::Required::No);
});
EXPECT_EQ(result, true);
EXPECT_EQ(stringArg1, "my-non-required-argument");
// Single non-required string argument, not given
stringArg1 = "";
result = runParser({}, [&](auto& parser) {
parser.addArgument(stringArg1, nullptr, nullptr, nullptr, Util::ArgParser::Required::No);
});
EXPECT_EQ(result, true);
EXPECT_EQ(stringArg1, "");
// Multiple non-required string arguments
stringArg1 = "";
std::string stringArg2 = "";
std::string stringArg3 = "";
result = runParser({ "my", "non-required", "argument" }, [&](auto& parser) {
parser.addArgument(stringArg1, nullptr, nullptr, nullptr, Util::ArgParser::Required::No);
parser.addArgument(stringArg2, nullptr, nullptr, nullptr, Util::ArgParser::Required::No);
parser.addArgument(stringArg3, nullptr, nullptr, nullptr, Util::ArgParser::Required::No);
});
EXPECT_EQ(result, true);
EXPECT_EQ(stringArg1, "my");
EXPECT_EQ(stringArg2, "non-required");
EXPECT_EQ(stringArg3, "argument");
// Multiple non-required string arguments, not given
stringArg1 = "";
stringArg2 = "";
stringArg3 = "";
result = runParser({}, [&](auto& parser) {
parser.addArgument(stringArg1, nullptr, nullptr, nullptr, Util::ArgParser::Required::No);
parser.addArgument(stringArg2, nullptr, nullptr, nullptr, Util::ArgParser::Required::No);
parser.addArgument(stringArg3, nullptr, nullptr, nullptr, Util::ArgParser::Required::No);
});
EXPECT_EQ(result, true);
EXPECT_EQ(stringArg1, "");
EXPECT_EQ(stringArg2, "");
EXPECT_EQ(stringArg3, "");
}
// -----------------------------------------
TEST_CASE(VectorStringArguments)
{
// Required vector string argument, not given
std::vector<std::string> vectorArg1 = {};
auto result = runParser({}, [&](auto& parser) {
parser.addArgument(vectorArg1, nullptr, nullptr, nullptr, Util::ArgParser::Required::Yes);
});
EXPECT_EQ(result, false);
EXPECT_EQ(vectorArg1.size(), 0);
// Required vector string argument, one given
vectorArg1 = {};
result = runParser({ "foo" }, [&](auto& parser) {
parser.addArgument(vectorArg1, nullptr, nullptr, nullptr, Util::ArgParser::Required::Yes);
});
EXPECT_EQ(result, true);
EXPECT_EQ(vectorArg1.size(), 1);
if (vectorArg1.size() == 1) {
EXPECT_EQ(vectorArg1[0], "foo");
}
// Required vector string argument, two given
vectorArg1 = {};
result = runParser({ "hello", "world" }, [&](auto& parser) {
parser.addArgument(vectorArg1, nullptr, nullptr, nullptr, Util::ArgParser::Required::Yes);
});
EXPECT_EQ(result, true);
EXPECT_EQ(vectorArg1.size(), 2);
if (vectorArg1.size() == 2) {
EXPECT_EQ(vectorArg1[0], "hello");
EXPECT_EQ(vectorArg1[1], "world");
}
}
// -----------------------------------------
TEST_CASE(CombinationOfNonRequiredArguments)
{
// Optional arguments, one given
int intArg1 = 0;
double doubleArg1 = 0;
std::string stringArg1 = "";
auto result = runParser({ "optional argument" }, [&](auto& parser) {
parser.addArgument(intArg1, nullptr, nullptr, nullptr, Util::ArgParser::Required::Optional);
parser.addArgument(doubleArg1, nullptr, nullptr, nullptr, Util::ArgParser::Required::Optional);
parser.addArgument(stringArg1, nullptr, nullptr, nullptr, Util::ArgParser::Required::Optional);
});
EXPECT_EQ(result, true);
EXPECT_EQ(intArg1, 0);
EXPECT_EQ(doubleArg1, 0);
EXPECT_EQ(stringArg1, "optional argument");
// Optional arguments, two given
intArg1 = 0;
doubleArg1 = 0;
stringArg1 = "";
result = runParser({ "999.999", "optional argument" }, [&](auto& parser) {
parser.addArgument(intArg1, nullptr, nullptr, nullptr, Util::ArgParser::Required::Optional);
parser.addArgument(doubleArg1, nullptr, nullptr, nullptr, Util::ArgParser::Required::Optional);
parser.addArgument(stringArg1, nullptr, nullptr, nullptr, Util::ArgParser::Required::Optional);
});
EXPECT_EQ(result, true);
EXPECT_EQ(intArg1, 0);
EXPECT_EQ(doubleArg1, 999.999);
EXPECT_EQ(stringArg1, "optional argument");
// Optional arguments, two given, one valid
intArg1 = 0;
doubleArg1 = 0;
stringArg1 = "";
result = runParser({ "999,999", "optional argument" }, [&](auto& parser) {
parser.addArgument(intArg1, nullptr, nullptr, nullptr, Util::ArgParser::Required::Optional);
parser.addArgument(doubleArg1, nullptr, nullptr, nullptr, Util::ArgParser::Required::Optional);
parser.addArgument(stringArg1, nullptr, nullptr, nullptr, Util::ArgParser::Required::Optional);
});
EXPECT_EQ(result, false);
EXPECT_EQ(intArg1, 0);
EXPECT_EQ(doubleArg1, 0);
EXPECT_EQ(stringArg1, "999,999");
// Optional arguments, two given, both valid but wrong order
stringArg1 = "";
intArg1 = 0;
doubleArg1 = 0;
result = runParser({ "999.999", "optional argument" }, [&](auto& parser) {
parser.addArgument(stringArg1, nullptr, nullptr, nullptr, Util::ArgParser::Required::Optional);
parser.addArgument(intArg1, nullptr, nullptr, nullptr, Util::ArgParser::Required::Optional);
parser.addArgument(doubleArg1, nullptr, nullptr, nullptr, Util::ArgParser::Required::Optional);
});
EXPECT_EQ(result, false);
EXPECT_EQ(stringArg1, "999.999");
EXPECT_EQ(intArg1, 0);
EXPECT_EQ(doubleArg1, 0);
}
// -----------------------------------------
TEST_CASE(BoolOptions)
{
// Short option
bool boolOpt1 = false;
auto result = runParser({ "-b" }, [&](auto& parser) {
parser.addOption(boolOpt1, 'b', nullptr, nullptr, nullptr);
});
EXPECT_EQ(result, true);
EXPECT_EQ(boolOpt1, true);
// Short option, not given
boolOpt1 = false;
result = runParser({}, [&](auto& parser) {
parser.addOption(boolOpt1, 'b', nullptr, nullptr, nullptr);
});
EXPECT_EQ(result, true);
EXPECT_EQ(boolOpt1, false);
// Long option
boolOpt1 = false;
result = runParser({ "--bool" }, [&](auto& parser) {
parser.addOption(boolOpt1, '\0', "bool", nullptr, nullptr);
});
EXPECT_EQ(result, true);
EXPECT_EQ(boolOpt1, true);
// Long option, not given
boolOpt1 = false;
result = runParser({}, [&](auto& parser) {
parser.addOption(boolOpt1, '\0', "bool", nullptr, nullptr);
});
EXPECT_EQ(result, true);
EXPECT_EQ(boolOpt1, false);
// Allow both short and long option, provide short
boolOpt1 = false;
result = runParser({ "-b" }, [&](auto& parser) {
parser.addOption(boolOpt1, 'b', "bool", nullptr, nullptr);
});
EXPECT_EQ(result, true);
EXPECT_EQ(boolOpt1, true);
// Allow both short and long option, provide long
boolOpt1 = false;
result = runParser({ "--bool" }, [&](auto& parser) {
parser.addOption(boolOpt1, 'b', "bool", nullptr, nullptr);
});
EXPECT_EQ(result, true);
EXPECT_EQ(boolOpt1, true);
// Allow both short and long option, provide both
boolOpt1 = false;
result = runParser({ "-b", "--bool" }, [&](auto& parser) {
parser.addOption(boolOpt1, 'b', "bool", nullptr, nullptr);
});
EXPECT_EQ(result, true);
EXPECT_EQ(boolOpt1, true);
}
// -----------------------------------------
TEST_CASE(SingleRequiredStringOptions)
{
// Single required string short option
std::string stringOpt1 = "";
auto result = runParser({ "-s", "my-required-argument" }, [&](auto& parser) {
parser.addOption(stringOpt1, 's', nullptr, nullptr, nullptr, nullptr, Util::ArgParser::Required::Yes);
});
EXPECT_EQ(result, true);
EXPECT_EQ(stringOpt1, "my-required-argument");
// Single required string short option, given directly after
stringOpt1 = "";
result = runParser({ "-smy-required-argument" }, [&](auto& parser) {
parser.addOption(stringOpt1, 's', nullptr, nullptr, nullptr, nullptr, Util::ArgParser::Required::Yes);
});
EXPECT_EQ(result, true);
EXPECT_EQ(stringOpt1, "my-required-argument");
// Single required string short option, empty given
stringOpt1 = "";
result = runParser({ "-s" }, [&](auto& parser) {
parser.addOption(stringOpt1, 's', nullptr, nullptr, nullptr, nullptr, Util::ArgParser::Required::Yes);
});
EXPECT_EQ(result, false);
EXPECT_EQ(stringOpt1, "");
// Single required string short option, not given
stringOpt1 = "";
result = runParser({}, [&](auto& parser) {
parser.addOption(stringOpt1, 's', nullptr, nullptr, nullptr, nullptr, Util::ArgParser::Required::Yes);
});
EXPECT_EQ(result, true);
EXPECT_EQ(stringOpt1, "");
// Single required string long option
stringOpt1 = "";
result = runParser({ "--string", "my-required-argument" }, [&](auto& parser) {
parser.addOption(stringOpt1, '\0', "string", nullptr, nullptr, nullptr, Util::ArgParser::Required::Yes);
});
EXPECT_EQ(result, true);
EXPECT_EQ(stringOpt1, "my-required-argument");
// Single required string long option, given directly after
stringOpt1 = "";
result = runParser({ "--string=my-required-argument" }, [&](auto& parser) {
parser.addOption(stringOpt1, '\0', "string", nullptr, nullptr, nullptr, Util::ArgParser::Required::Yes);
});
EXPECT_EQ(result, true);
EXPECT_EQ(stringOpt1, "my-required-argument");
// Single required string long option, empty given
stringOpt1 = "";
result = runParser({ "--string" }, [&](auto& parser) {
parser.addOption(stringOpt1, '\0', "string", nullptr, nullptr, nullptr, Util::ArgParser::Required::Yes);
});
EXPECT_EQ(result, false);
EXPECT_EQ(stringOpt1, "");
// Single required string long option, not given
stringOpt1 = "";
result = runParser({}, [&](auto& parser) {
parser.addOption(stringOpt1, '\0', "string", nullptr, nullptr, nullptr, Util::ArgParser::Required::Yes);
});
EXPECT_EQ(result, true);
EXPECT_EQ(stringOpt1, "");
}
// -----------------------------------------
TEST_CASE(SingleOptionalStringOptions)
{
// Single optional string short option
std::string stringOpt1 = "";
std::string stringArg1 = "";
auto result = runParser({ "-s", "my-optional-argument" }, [&](auto& parser) {
parser.addOption(stringOpt1, 's', nullptr, nullptr, nullptr, nullptr, Util::ArgParser::Required::Optional);
parser.addArgument(stringArg1, nullptr, nullptr, nullptr, Util::ArgParser::Required::No);
});
EXPECT_EQ(result, true);
EXPECT_EQ(stringOpt1, "");
EXPECT_EQ(stringArg1, "my-optional-argument");
// Single optional string short option, given directly after
stringOpt1 = "";
result = runParser({ "-smy-optional-argument" }, [&](auto& parser) {
parser.addOption(stringOpt1, 's', nullptr, nullptr, nullptr, nullptr, Util::ArgParser::Required::Optional);
});
EXPECT_EQ(result, true);
EXPECT_EQ(stringOpt1, "my-optional-argument");
// Single optional string short option, empty given
stringOpt1 = "";
result = runParser({ "-s" }, [&](auto& parser) {
parser.addOption(stringOpt1, 's', nullptr, nullptr, nullptr, nullptr, Util::ArgParser::Required::Optional);
});
EXPECT_EQ(result, true);
EXPECT_EQ(stringOpt1, "");
// Single optional string short option, not given
stringOpt1 = "";
result = runParser({}, [&](auto& parser) {
parser.addOption(stringOpt1, 's', nullptr, nullptr, nullptr, nullptr, Util::ArgParser::Required::Optional);
});
EXPECT_EQ(result, true);
EXPECT_EQ(stringOpt1, "");
// Single optional string long option
stringOpt1 = "";
stringArg1 = "";
result = runParser({ "--string", "my-optional-argument" }, [&](auto& parser) {
parser.addOption(stringOpt1, '\0', "string", nullptr, nullptr, nullptr, Util::ArgParser::Required::Optional);
parser.addArgument(stringArg1, nullptr, nullptr, nullptr, Util::ArgParser::Required::No);
});
EXPECT_EQ(result, true);
EXPECT_EQ(stringOpt1, "");
EXPECT_EQ(stringArg1, "my-optional-argument");
// Single optional string long option, given directly after
stringOpt1 = "";
result = runParser({ "--string=my-optional-argument" }, [&](auto& parser) {
parser.addOption(stringOpt1, '\0', "string", nullptr, nullptr, nullptr, Util::ArgParser::Required::Optional);
});
EXPECT_EQ(result, true);
EXPECT_EQ(stringOpt1, "my-optional-argument");
// Single optional string long option, empty given
stringOpt1 = "";
result = runParser({ "--string" }, [&](auto& parser) {
parser.addOption(stringOpt1, '\0', "string", nullptr, nullptr, nullptr, Util::ArgParser::Required::Optional);
});
EXPECT_EQ(result, true);
EXPECT_EQ(stringOpt1, "");
// Single optional string long option, not given
stringOpt1 = "";
result = runParser({}, [&](auto& parser) {
parser.addOption(stringOpt1, '\0', "string", nullptr, nullptr, nullptr, Util::ArgParser::Required::Optional);
});
EXPECT_EQ(result, true);
EXPECT_EQ(stringOpt1, "");
}
// -----------------------------------------
TEST_CASE(SingleNonRequiredStringOptions)
{
// Single non-required string short option
std::string stringOpt1 = "";
std::string stringArg1 = "";
auto result = runParser({ "-s", "my-non-required-argument" }, [&](auto& parser) {
parser.addOption(stringOpt1, 's', nullptr, nullptr, nullptr, nullptr, Util::ArgParser::Required::No);
parser.addArgument(stringArg1, nullptr, nullptr, nullptr, Util::ArgParser::Required::No);
});
EXPECT_EQ(result, true);
EXPECT_EQ(stringOpt1, "");
EXPECT_EQ(stringArg1, "my-non-required-argument");
// Single non-required string short option, given directly after
stringOpt1 = "";
result = runParser({ "-smy-non-required-argument" }, [&](auto& parser) {
parser.addOption(stringOpt1, 's', nullptr, nullptr, nullptr, nullptr, Util::ArgParser::Required::No);
});
EXPECT_EQ(result, false);
EXPECT_EQ(stringOpt1, "");
// Single non-required string short option, empty given
stringOpt1 = "";
result = runParser({ "-s" }, [&](auto& parser) {
parser.addOption(stringOpt1, 's', nullptr, nullptr, nullptr, nullptr, Util::ArgParser::Required::No);
});
EXPECT_EQ(result, true);
EXPECT_EQ(stringOpt1, "");
// Single non-required string short option, not given
stringOpt1 = "";
result = runParser({}, [&](auto& parser) {
parser.addOption(stringOpt1, 's', nullptr, nullptr, nullptr, nullptr, Util::ArgParser::Required::No);
});
EXPECT_EQ(result, true);
EXPECT_EQ(stringOpt1, "");
// Single non-required string long option
stringOpt1 = "";
stringArg1 = "";
result = runParser({ "--string", "my-non-required-argument" }, [&](auto& parser) {
parser.addOption(stringOpt1, '\0', "string", nullptr, nullptr, nullptr, Util::ArgParser::Required::No);
parser.addArgument(stringArg1, nullptr, nullptr, nullptr, Util::ArgParser::Required::No);
});
EXPECT_EQ(result, true);
EXPECT_EQ(stringOpt1, "");
EXPECT_EQ(stringArg1, "my-non-required-argument");
// Single non-required string long option, given directly after
stringOpt1 = "";
result = runParser({ "--string=my-non-required-argument" }, [&](auto& parser) {
parser.addOption(stringOpt1, '\0', "string", nullptr, nullptr, nullptr, Util::ArgParser::Required::No);
});
EXPECT_EQ(result, false);
EXPECT_EQ(stringOpt1, "");
// Single non-required string long option, empty given
stringOpt1 = "";
result = runParser({ "--string" }, [&](auto& parser) {
parser.addOption(stringOpt1, '\0', "string", nullptr, nullptr, nullptr, Util::ArgParser::Required::No);
});
EXPECT_EQ(result, true);
EXPECT_EQ(stringOpt1, "");
// Single non-required string long option, not given
stringOpt1 = "";
result = runParser({}, [&](auto& parser) {
parser.addOption(stringOpt1, '\0', "string", nullptr, nullptr, nullptr, Util::ArgParser::Required::No);
});
EXPECT_EQ(result, true);
EXPECT_EQ(stringOpt1, "");
}
// -----------------------------------------
TEST_CASE(NumberOptions)
{
// Required int short option
int intOpt1 = 0;
auto result = runParser({ "-i", "2147483647" }, [&](auto& parser) {
parser.addOption(intOpt1, 'i', nullptr, nullptr, nullptr, nullptr, Util::ArgParser::Required::Yes);
});
EXPECT_EQ(result, true);
EXPECT_EQ(intOpt1, 2147483647);
// Required int short option, overflown value given
intOpt1 = 0;
result = runParser({ "-i", "2147483648" }, [&](auto& parser) {
parser.addOption(intOpt1, 'i', nullptr, nullptr, nullptr, nullptr, Util::ArgParser::Required::Yes);
});
EXPECT_EQ(result, false);
EXPECT_EQ(intOpt1, 0);
// Required int short option, empty given
intOpt1 = 0;
result = runParser({ "-i" }, [&](auto& parser) {
parser.addOption(intOpt1, 'i', nullptr, nullptr, nullptr, nullptr, Util::ArgParser::Required::Yes);
});
EXPECT_EQ(result, false);
EXPECT_EQ(intOpt1, 0);
// Required unsigned int short option
unsigned int unsignedIntOpt1 = 0;
result = runParser({ "-u", "4294967295" }, [&](auto& parser) {
parser.addOption(unsignedIntOpt1, 'u', nullptr, nullptr, nullptr, nullptr, Util::ArgParser::Required::Yes);
});
EXPECT_EQ(result, true);
EXPECT_EQ(unsignedIntOpt1, 4294967295);
// Required unsigned int short option, overflown value given
unsignedIntOpt1 = 0;
result = runParser({ "-u", "4294967296" }, [&](auto& parser) {
parser.addOption(unsignedIntOpt1, 'u', nullptr, nullptr, nullptr, nullptr, Util::ArgParser::Required::Yes);
});
EXPECT_EQ(result, false);
EXPECT_EQ(unsignedIntOpt1, 0);
// Required unsigned int short option, empty given
unsignedIntOpt1 = 0;
result = runParser({ "-u" }, [&](auto& parser) {
parser.addOption(unsignedIntOpt1, 'u', nullptr, nullptr, nullptr, nullptr, Util::ArgParser::Required::Yes);
});
EXPECT_EQ(result, false);
EXPECT_EQ(unsignedIntOpt1, 0);
// Required double short option
double doubleOpt1 = 0;
result = runParser({ "-d", "999.999" }, [&](auto& parser) {
parser.addOption(doubleOpt1, 'd', nullptr, nullptr, nullptr, nullptr, Util::ArgParser::Required::Yes);
});
EXPECT_EQ(result, true);
EXPECT_EQ(doubleOpt1, 999.999);
// Required double short option, empty given
doubleOpt1 = 0;
result = runParser({ "-d" }, [&](auto& parser) {
parser.addOption(doubleOpt1, 'd', nullptr, nullptr, nullptr, nullptr, Util::ArgParser::Required::Yes);
});
EXPECT_EQ(result, false);
EXPECT_EQ(doubleOpt1, 0);
}
// -----------------------------------------
TEST_CASE(VectorStringOptions)
{
// Required vector string short option, not given
std::vector<std::string> vectorOpt1 = {};
auto result = runParser({}, [&](auto& parser) {
parser.addOption(vectorOpt1, 'v', nullptr, nullptr, nullptr, nullptr, Util::ArgParser::Required::Yes);
});
EXPECT_EQ(result, true);
EXPECT_EQ(vectorOpt1.size(), 0);
// Required vector string short option, empty given
vectorOpt1 = {};
result = runParser({ "-v" }, [&](auto& parser) {
parser.addOption(vectorOpt1, 'v', nullptr, nullptr, nullptr, nullptr, Util::ArgParser::Required::Yes);
});
EXPECT_EQ(result, false);
EXPECT_EQ(vectorOpt1.size(), 0);
// Required vector string short option, one given
vectorOpt1 = {};
result = runParser({ "-v", "a vector argument!" }, [&](auto& parser) {
parser.addOption(vectorOpt1, 'v', nullptr, nullptr, nullptr, nullptr, Util::ArgParser::Required::Yes);
});
EXPECT_EQ(result, true);
EXPECT_EQ(vectorOpt1.size(), 1);
if (vectorOpt1.size() == 1) {
EXPECT_EQ(vectorOpt1[0], "a vector argument!");
}
// Required vector string short option, two given
vectorOpt1 = {};
result = runParser({ "-v", "hello", "-v", "world" }, [&](auto& parser) {
parser.addOption(vectorOpt1, 'v', nullptr, nullptr, nullptr, nullptr, Util::ArgParser::Required::Yes);
});
EXPECT_EQ(result, true);
EXPECT_EQ(vectorOpt1.size(), 2);
if (vectorOpt1.size() == 2) {
EXPECT_EQ(vectorOpt1[0], "hello");
EXPECT_EQ(vectorOpt1[1], "world");
}
// Required vector string short option, two given directly after
vectorOpt1 = {};
result = runParser({ "-vhello", "-vworld" }, [&](auto& parser) {
parser.addOption(vectorOpt1, 'v', nullptr, nullptr, nullptr, nullptr, Util::ArgParser::Required::Yes);
});
EXPECT_EQ(result, true);
EXPECT_EQ(vectorOpt1.size(), 2);
if (vectorOpt1.size() == 2) {
EXPECT_EQ(vectorOpt1[0], "hello");
EXPECT_EQ(vectorOpt1[1], "world");
}
// Required vector string long option, empty given
vectorOpt1 = {};
result = runParser({ "--vector" }, [&](auto& parser) {
parser.addOption(vectorOpt1, '\0', "vector", nullptr, nullptr, nullptr, Util::ArgParser::Required::Yes);
});
EXPECT_EQ(result, false);
EXPECT_EQ(vectorOpt1.size(), 0);
// Required vector string long option, one given
vectorOpt1 = {};
result = runParser({ "--vector", "a vector argument!" }, [&](auto& parser) {
parser.addOption(vectorOpt1, '\0', "vector", nullptr, nullptr, nullptr, Util::ArgParser::Required::Yes);
});
EXPECT_EQ(result, true);
EXPECT_EQ(vectorOpt1.size(), 1);
if (vectorOpt1.size() == 1) {
EXPECT_EQ(vectorOpt1[0], "a vector argument!");
}
// Required vector string long option, two given
vectorOpt1 = {};
result = runParser({ "--vector", "hello", "--vector", "world" }, [&](auto& parser) {
parser.addOption(vectorOpt1, '\0', "vector", nullptr, nullptr, nullptr, Util::ArgParser::Required::Yes);
});
EXPECT_EQ(result, true);
EXPECT_EQ(vectorOpt1.size(), 2);
if (vectorOpt1.size() == 2) {
EXPECT_EQ(vectorOpt1[0], "hello");
EXPECT_EQ(vectorOpt1[1], "world");
}
// Required vector string long option, two given directly after
vectorOpt1 = {};
result = runParser({ "--vector=hello", "--vector=world" }, [&](auto& parser) {
parser.addOption(vectorOpt1, '\0', "vector", nullptr, nullptr, nullptr, Util::ArgParser::Required::Yes);
});
EXPECT_EQ(result, true);
EXPECT_EQ(vectorOpt1.size(), 2);
if (vectorOpt1.size() == 2) {
EXPECT_EQ(vectorOpt1.at(0), "hello");
EXPECT_EQ(vectorOpt1.at(1), "world");
}
}
// -----------------------------------------
TEST_CASE(MultipleOptions)
{
// Both short options, second is required, with a non-option parameter in-between
bool boolOpt1 = false;
std::string stringOpt1 = "";
std::vector<std::string> vectorArg1 = {};
auto result = runParser({ "-b", "something", "-s", "a-string-value" }, [&](auto& parser) {
parser.addOption(boolOpt1, 'b', nullptr, nullptr, nullptr);
parser.addOption(stringOpt1, 's', nullptr, nullptr, nullptr, nullptr, Util::ArgParser::Required::Yes);
parser.addArgument(vectorArg1, nullptr, nullptr, nullptr);
});
EXPECT_EQ(result, true);
EXPECT_EQ(boolOpt1, true);
EXPECT_EQ(stringOpt1, "a-string-value");
EXPECT_EQ(vectorArg1.size(), 1);
if (vectorArg1.size() == 1) {
EXPECT_EQ(vectorArg1.at(0), "something");
}
}
// -----------------------------------------
TEST_CASE(StopOnDoubleDashOption)
{
// Bool short options, missing
// Expected: The bool options are interpreted as non-option parameters
bool boolOpt1 = false;
bool boolOpt2 = false;
std::vector<std::string> vectorArg1 = {};
auto result = runParser({ "--", "-b", "-c" }, [&](auto& parser) {
parser.addOption(boolOpt1, 'b', nullptr, nullptr, nullptr);
parser.addOption(boolOpt2, 'c', nullptr, nullptr, nullptr);
parser.addArgument(vectorArg1, nullptr, nullptr, nullptr);
});
EXPECT_EQ(result, true);
EXPECT_EQ(boolOpt1, false);
EXPECT_EQ(boolOpt2, false);
EXPECT_EQ(vectorArg1.size(), 2);
if (vectorArg1.size() == 2) {
EXPECT_EQ(vectorArg1.at(0), "-b");
EXPECT_EQ(vectorArg1.at(1), "-c");
}
// Bool short options, one given
// Expected: boolOpt1 is set, one non-option parameter
boolOpt1 = false;
boolOpt2 = false;
vectorArg1 = {};
result = runParser({ "-b", "--", "-c" }, [&](auto& parser) {
parser.addOption(boolOpt1, 'b', nullptr, nullptr, nullptr);
parser.addOption(boolOpt2, 'c', nullptr, nullptr, nullptr);
parser.addArgument(vectorArg1, nullptr, nullptr, nullptr);
});
EXPECT_EQ(result, true);
EXPECT_EQ(boolOpt1, true);
EXPECT_EQ(boolOpt2, false);
EXPECT_EQ(vectorArg1.size(), 1);
if (vectorArg1.size() == 1) {
EXPECT_EQ(vectorArg1.at(0), "-c");
}
// Bool long options, missing
// Expected: The bool options are interpreted as non-option parameters
boolOpt1 = false;
boolOpt2 = false;
vectorArg1 = {};
result = runParser({ "--", "--bool", "--cool" }, [&](auto& parser) {
parser.addOption(boolOpt1, '\0', "bool", nullptr, nullptr);
parser.addOption(boolOpt2, '\0', "cool", nullptr, nullptr);
parser.addArgument(vectorArg1, nullptr, nullptr, nullptr);
});
EXPECT_EQ(result, true);
EXPECT_EQ(boolOpt1, false);
EXPECT_EQ(boolOpt2, false);
EXPECT_EQ(vectorArg1.size(), 2);
if (vectorArg1.size() == 2) {
EXPECT_EQ(vectorArg1.at(0), "--bool");
EXPECT_EQ(vectorArg1.at(1), "--cool");
}
// Bool long options, one given
// Expected: boolOpt1 is set, one non-option parameter
boolOpt1 = false;
boolOpt2 = false;
vectorArg1 = {};
result = runParser({ "--bool", "--", "--cool" }, [&](auto& parser) {
parser.addOption(boolOpt1, '\0', "bool", nullptr, nullptr);
parser.addOption(boolOpt2, '\0', "cool", nullptr, nullptr);
parser.addArgument(vectorArg1, nullptr, nullptr, nullptr);
});
EXPECT_EQ(result, true);
EXPECT_EQ(boolOpt1, true);
EXPECT_EQ(boolOpt2, false);
EXPECT_EQ(vectorArg1.size(), 1);
if (vectorArg1.size() == 1) {
EXPECT_EQ(vectorArg1.at(0), "--cool");
}
}
// -----------------------------------------
TEST_CASE(StopOnFirstNonOption)
{
// Do not stop on first non-option; arguments are in correct order
// Expected: The bool options are set and one non-option parameter
bool boolOpt1 = false;
bool boolOpt2 = false;
std::vector<std::string> vectorArg1 = {};
auto result = runParser({ "-b", "-c", "stopping" }, [&](auto& parser) {
parser.setStopParsingOnFirstNonOption(false);
parser.addOption(boolOpt1, 'b', nullptr, nullptr, nullptr);
parser.addOption(boolOpt2, 'c', nullptr, nullptr, nullptr);
parser.addArgument(vectorArg1, nullptr, nullptr, nullptr);
});
EXPECT_EQ(result, true);
EXPECT_EQ(boolOpt1, true);
EXPECT_EQ(boolOpt2, true);
EXPECT_EQ(vectorArg1.size(), 1);
if (vectorArg1.size() == 1) {
EXPECT_EQ(vectorArg1.at(0), "stopping");
}
// Do not stop on first non-option; arguments are in wrong order
// Expected: The bool options are set and one non-option parameter
boolOpt1 = false;
boolOpt2 = false;
vectorArg1 = {};
result = runParser({ "-b", "stopping", "-c" }, [&](auto& parser) {
parser.setStopParsingOnFirstNonOption(false);
parser.addOption(boolOpt1, 'b', nullptr, nullptr, nullptr);
parser.addOption(boolOpt2, 'c', nullptr, nullptr, nullptr);
parser.addArgument(vectorArg1, nullptr, nullptr, nullptr);
});
EXPECT_EQ(result, true);
EXPECT_EQ(boolOpt1, true);
EXPECT_EQ(boolOpt2, true);
EXPECT_EQ(vectorArg1.size(), 1);
if (vectorArg1.size() == 1) {
EXPECT_EQ(vectorArg1.at(0), "stopping");
}
// Stop on first non option, arguments are in correct order
// Expected: The bool options are set and one non-option parameter
boolOpt1 = false;
boolOpt2 = false;
vectorArg1 = {};
result = runParser({ "-b", "-c", "stopping" }, [&](auto& parser) {
parser.setStopParsingOnFirstNonOption(true);
parser.addOption(boolOpt1, 'b', nullptr, nullptr, nullptr);
parser.addOption(boolOpt2, 'c', nullptr, nullptr, nullptr);
parser.addArgument(vectorArg1, nullptr, nullptr, nullptr);
});
EXPECT_EQ(result, true);
EXPECT_EQ(boolOpt1, true);
EXPECT_EQ(boolOpt2, true);
EXPECT_EQ(vectorArg1.size(), 1);
if (vectorArg1.size() == 1) {
EXPECT_EQ(vectorArg1.at(0), "stopping");
}
// Stop on first non option, arguments are in wrong order
// Expected: boolOpt1 is set and the rest are non-option parameters
boolOpt1 = false;
boolOpt2 = false;
vectorArg1 = {};
result = runParser({ "-b", "stopping", "-c" }, [&](auto& parser) {
parser.setStopParsingOnFirstNonOption(true);
parser.addOption(boolOpt1, 'b', nullptr, nullptr, nullptr);
parser.addOption(boolOpt2, 'c', nullptr, nullptr, nullptr);
parser.addArgument(vectorArg1, nullptr, nullptr, nullptr);
});
EXPECT_EQ(result, true);
EXPECT_EQ(boolOpt1, true);
EXPECT_EQ(boolOpt2, false);
EXPECT_EQ(vectorArg1.size(), 2);
if (vectorArg1.size() == 2) {
EXPECT_EQ(vectorArg1.at(0), "stopping");
EXPECT_EQ(vectorArg1.at(1), "-c");
}
}
// -----------------------------------------
TEST_CASE(ExitOnFirstError)
{
// Do not stop on first error, one non-existing given
// Expected: parsing fails, boolOpt1 is set
bool boolOpt1 = false;
auto result = runParser({ "--this-doesnt-exist", "--this-exist" }, [&](auto& parser) {
parser.setExitOnFirstError(false);
parser.addOption(boolOpt1, '\0', "this-exist", nullptr, nullptr);
});
EXPECT_EQ(result, false);
EXPECT_EQ(boolOpt1, true);
// Stop on first error, one non-existing given
// Expected: parsing fails, boolOpt1 is not set
boolOpt1 = false;
result = runParser({ "--this-doesnt-exist", "--this-exist" }, [&](auto& parser) {
parser.setExitOnFirstError(true);
parser.addOption(boolOpt1, '\0', "this-exist", nullptr, nullptr);
});
EXPECT_EQ(result, false);
EXPECT_EQ(boolOpt1, false);
}

517
test/unit/testutilformat.cpp

@ -0,0 +1,517 @@
/*
* Copyright (C) 2022 Riyyi
*
* SPDX-License-Identifier: MIT
*/
#include <cstddef> // size_t
#include <cstdint> // int8_t, int16_t, int32_t, int64_t, uint8_t, uint16_t, uint32_t
#include <map>
#include <sstream> // stringstream
#include <string>
#include <unordered_map>
#include <vector>
#include "macro.h"
#include "testcase.h"
#include "testsuite.h"
#include "util/format/format.h"
// -----------------------------------------
TEST_CASE(FormatIntegral)
{
std::string result;
// Signed
int8_t i8 = 127; // char
result = Util::format("{}", i8);
EXPECT_EQ(result, "127");
int16_t i16 = 32767;
result = Util::format("{}", i16);
EXPECT_EQ(result, "32767");
int32_t i32 = 68766; // int
result = Util::format("{}", i32);
EXPECT_EQ(result, "68766");
int64_t i64 = 237942768427; // long int
result = Util::format("{}", i64);
EXPECT_EQ(result, "237942768427");
// Unsigned
uint8_t u8 = 255; // unsigned char
result = Util::format("{}", u8);
EXPECT_EQ(result, "255");
uint16_t u16 = 65535;
result = Util::format("{}", u16);
EXPECT_EQ(result, "65535");
uint32_t u32 = 4294967295; // unsigned int
result = Util::format("{}", u32);
EXPECT_EQ(result, "4294967295");
size_t u64 = 18446744073709551615; // long unsigned int
result = Util::format("{}", u64);
EXPECT_EQ(result, "18446744073709551615");
}
TEST_CASE(FormatFloatingPoint)
{
std::string result;
float f32R = 245789.70000;
result = Util::format("{}", f32R);
EXPECT_EQ(result, "245789.703125");
float f32 = 45645.3233;
result = Util::format("{}", f32);
EXPECT_EQ(result, "45645.324219");
double f64 = 87522.300000000;
result = Util::format("{}", f64);
EXPECT_EQ(result, "87522.300000");
double pi = 3.14159265359;
result = Util::format("{}", pi);
EXPECT_EQ(result, "3.141593");
}
TEST_CASE(FormatChar)
{
std::string result;
char character = 'A';
result = Util::format("{}", character);
EXPECT_EQ(result, "A");
bool boolean = true;
result = Util::format("{}", boolean);
EXPECT_EQ(result, "true");
boolean = false;
result = Util::format("{}", boolean);
EXPECT_EQ(result, "false");
}
TEST_CASE(FormatString)
{
std::string result;
result = Util::format("");
EXPECT_EQ(result, "");
const char* cString = "C string";
result = Util::format("{}", cString);
EXPECT_EQ(result, "C string");
std::string string = "string";
result = Util::format("{}", string);
EXPECT_EQ(result, "string");
std::string_view stringView = "string_view";
result = Util::format("{}", stringView);
EXPECT_EQ(result, "string_view");
result = Util::format("{} {}", "Hello", "World");
EXPECT_EQ(result, "Hello World");
result = Util::format("{{escaped braces}}");
EXPECT_EQ(result, "{escaped braces}");
result = Util::format("{{braces{}}}", "Something");
EXPECT_EQ(result, "{bracesSomething}");
}
TEST_CASE(FormatPointer)
{
std::string result;
result = Util::format("{}", nullptr);
EXPECT_EQ(result, "0x0");
int integer = 42;
std::stringstream stream;
stream << &integer;
std::string pointer = stream.str();
result = Util::format("{}", &integer);
EXPECT_EQ(result, pointer);
}
TEST_CASE(FormatSpecifierIntegral)
{
std::string result;
// Fill and Align
result = Util::format("{:+<}", 12345);
EXPECT_EQ(result, "12345");
result = Util::format("{:+^}", 12345);
EXPECT_EQ(result, "12345");
result = Util::format("{:+>}", 12345);
EXPECT_EQ(result, "12345");
// Sign
result = Util::format("{:+}", 12345);
EXPECT_EQ(result, "+12345");
result = Util::format("{:+}", -12345);
EXPECT_EQ(result, "-12345");
result = Util::format("{:-}", 12345);
EXPECT_EQ(result, "12345");
result = Util::format("{:-}", -12345);
EXPECT_EQ(result, "-12345");
result = Util::format("{: }", 12345);
EXPECT_EQ(result, " 12345");
result = Util::format("{: }", -12345);
EXPECT_EQ(result, "-12345");
// AlternativeForm
result = Util::format("{:#}", 12345);
EXPECT_EQ(result, "12345");
// ZeroPadding
result = Util::format("{:0}", 12345);
EXPECT_EQ(result, "12345");
// Width
result = Util::format("{:10}", 12345);
EXPECT_EQ(result, " 12345");
// Width + Fill and Align
result = Util::format("{:+<10}", 12345);
EXPECT_EQ(result, "12345+++++");
result = Util::format("{:+^10}", 12345);
EXPECT_EQ(result, "++12345+++");
result = Util::format("{:+>10}", 12345);
EXPECT_EQ(result, "+++++12345");
// Width + ZeroPadding
result = Util::format("{:010}", 12345);
EXPECT_EQ(result, "0000012345");
// Precision
// Not possible on integral types
// Type
result = Util::format("{:b}", 12345);
EXPECT_EQ(result, "11000000111001");
result = Util::format("{:B}", 12345);
EXPECT_EQ(result, "11000000111001");
result = Util::format("{:c}", 65);
EXPECT_EQ(result, "A");
result = Util::format("{:o}", 12345);
EXPECT_EQ(result, "30071");
result = Util::format("{:x}", 62432);
EXPECT_EQ(result, "f3e0");
result = Util::format("{:X}", 62432);
EXPECT_EQ(result, "F3E0");
// Type + AlternativeForm
result = Util::format("{:#b}", 12345);
EXPECT_EQ(result, "0b11000000111001");
result = Util::format("{:#B}", 12345);
EXPECT_EQ(result, "0B11000000111001");
result = Util::format("{:#c}", 65);
EXPECT_EQ(result, "A");
result = Util::format("{:#o}", 12345);
EXPECT_EQ(result, "030071");
result = Util::format("{:#x}", 62432);
EXPECT_EQ(result, "0xf3e0");
result = Util::format("{:#X}", 62432);
EXPECT_EQ(result, "0XF3E0");
}
TEST_CASE(FormatSpecifierIntegralCombination)
{
std::string result;
// AlternativeForm + ZeroPadding + Width + Type
// ------------------------------
result = Util::format("{:-#010d}", 402);
EXPECT_EQ(result, "0000000402");
// AlternativeForm + Width + Type
// ------------------------------
result = Util::format("{:#10x}", 402);
EXPECT_EQ(result, " 0x192");
// + Fill and Align
result = Util::format("{:^<#10x}", 402);
EXPECT_EQ(result, "0x192^^^^^");
result = Util::format("{:^^#10x}", 402);
EXPECT_EQ(result, "^^0x192^^^");
result = Util::format("{:^>#10x}", 402);
EXPECT_EQ(result, "^^^^^0x192");
// ------------------------------
// + Sign
result = Util::format("{:+#10x}", 402);
EXPECT_EQ(result, " +0x192");
// + Fill and Align + Sign
result = Util::format("{:^<+#10x}", 402);
EXPECT_EQ(result, "+0x192^^^^");
result = Util::format("{:^^+#10x}", 402);
EXPECT_EQ(result, "^^+0x192^^");
result = Util::format("{:^>+#10x}", 402);
EXPECT_EQ(result, "^^^^+0x192");
// ------------------------------
// + ZeroPadding
result = Util::format("{:#010x}", 402);
EXPECT_EQ(result, "0x00000192");
// Fill and Align + ZeroPadding
result = Util::format("{:^<#010x}", 402);
EXPECT_EQ(result, "0x19200000");
result = Util::format("{:^^#010x}", 402);
EXPECT_EQ(result, "000x192000");
result = Util::format("{:^>#010x}", 402);
EXPECT_EQ(result, "000000x192");
// ------------------------------
// + Sign + ZeroPadding
result = Util::format("{:+#010x}", 402);
EXPECT_EQ(result, "+0x0000192");
// + Fill and Align + Sign + ZeroPadding
result = Util::format("{:^<+#010x}", 402);
EXPECT_EQ(result, "+0x1920000");
result = Util::format("{:^^+#010x}", 402);
EXPECT_EQ(result, "00+0x19200");
result = Util::format("{:^>+#010x}", 402);
EXPECT_EQ(result, "0000+0x192");
}
TEST_CASE(FormatSpecifierFloatingPoint)
{
std::string result;
double f64 = 87522.300000000;
result = Util::format("{:.1}", f64);
EXPECT_EQ(result, "87522.3");
double pi = 3.14159265359;
result = Util::format("{}", pi);
EXPECT_EQ(result, "3.141593");
result = Util::format("{:.15}", pi);
EXPECT_EQ(result, "3.141592653590000");
}
TEST_CASE(FormatSpecifierChar)
{
std::string result;
char character = 65;
result = Util::format("{:b}", character);
EXPECT_EQ(result, "1000001");
result = Util::format("{:B}", character);
EXPECT_EQ(result, "1000001");
result = Util::format("{:d}", character);
EXPECT_EQ(result, "65");
result = Util::format("{:o}", character);
EXPECT_EQ(result, "101");
result = Util::format("{:x}", character);
EXPECT_EQ(result, "41");
result = Util::format("{:X}", character);
EXPECT_EQ(result, "41");
bool boolean = true;
result = Util::format("{:b}", boolean);
EXPECT_EQ(result, "1");
result = Util::format("{:B}", boolean);
EXPECT_EQ(result, "1");
result = Util::format("{:d}", boolean);
EXPECT_EQ(result, "1");
result = Util::format("{:o}", boolean);
EXPECT_EQ(result, "1");
result = Util::format("{:x}", boolean);
EXPECT_EQ(result, "1");
result = Util::format("{:X}", boolean);
EXPECT_EQ(result, "1");
boolean = false;
result = Util::format("{:b}", boolean);
EXPECT_EQ(result, "0");
result = Util::format("{:B}", boolean);
EXPECT_EQ(result, "0");
result = Util::format("{:d}", boolean);
EXPECT_EQ(result, "0");
result = Util::format("{:o}", boolean);
EXPECT_EQ(result, "0");
result = Util::format("{:x}", boolean);
EXPECT_EQ(result, "0");
result = Util::format("{:X}", boolean);
EXPECT_EQ(result, "0");
}
TEST_CASE(FormatSpecifierString)
{
std::string result;
std::string string = "my string";
// Fill and Align
result = Util::format("{:+<}", string);
EXPECT_EQ(result, "my string");
result = Util::format("{:+^}", string);
EXPECT_EQ(result, "my string");
result = Util::format("{:+>}", string);
EXPECT_EQ(result, "my string");
// Sign
// Not possible on string types
// AlternativeForm
// Not possible on string types
// ZeroPadding
// Not possible on string types
// Width
result = Util::format("{:15}", string);
EXPECT_EQ(result, "my string ");
// Width + Fill and Align
result = Util::format("{:+<15}", string);
EXPECT_EQ(result, "my string++++++");
result = Util::format("{:+^15}", string);
EXPECT_EQ(result, "+++my string+++");
result = Util::format("{:+>15}", string);
EXPECT_EQ(result, "++++++my string");
// Precision
// Not possible on string types
// Type
result = Util::format("{:s}", string);
EXPECT_EQ(result, "my string");
}
TEST_CASE(FormatSpecifierPointer)
{
std::string result;
int integer = 42;
std::stringstream stream;
stream << &integer;
std::string pointer = stream.str();
// Fill and Align
result = Util::format("{:+<}", &integer);
EXPECT_EQ(result, pointer);
result = Util::format("{:+^}", &integer);
EXPECT_EQ(result, pointer);
result = Util::format("{:+>}", &integer);
EXPECT_EQ(result, pointer);
// Sign
// Not possible on string types
// AlternativeForm
// Not possible on string types
// ZeroPadding
// Not possible on string types
// Width
result = Util::format("{:24}", &integer);
EXPECT_EQ(result, std::string(24 - pointer.length(), ' ') + pointer);
// Width + Fill and Align
result = Util::format("{:+<24}", &integer);
EXPECT_EQ(result, pointer + std::string(24 - pointer.length(), '+'));
result = Util::format("{:+^24}", &integer);
EXPECT_EQ(result, std::string((24 - pointer.length()) / 2, '+') + pointer + std::string((24 - pointer.length()) / 2, '+'));
result = Util::format("{:+>24}", &integer);
EXPECT_EQ(result, std::string(24 - pointer.length(), '+') + pointer);
// Precision
// Not possible on string types
// Type
result = Util::format("{:p}", &integer);
EXPECT_EQ(result, pointer);
}
TEST_CASE(FormatContainers)
{
std::string result;
std::vector<std::string> vector { "thing1", "thing2", "thing3" };
result = Util::format("{}", vector);
EXPECT_EQ(result, "{thing1,thing2,thing3}");
result = Util::format("{:1}", vector);
EXPECT_EQ(result, "{ thing1, thing2, thing3 }");
result = Util::format("{:#4}", vector);
EXPECT_EQ(result, R"({
thing1,
thing2,
thing3
})");
result = Util::format("{:\t<#1}", vector);
EXPECT_EQ(result, R"({
thing1,
thing2,
thing3
})");
std::map<std::string, int> map { { "thing3", 3 }, { "thing2", 2 }, { "thing1", 1 } };
result = Util::format("{}", map);
EXPECT_EQ(result, R"({"thing1":1,"thing2":2,"thing3":3})");
result = Util::format("{:1}", map);
EXPECT_EQ(result, R"({ "thing1": 1, "thing2": 2, "thing3": 3 })");
result = Util::format("{:#4}", map);
EXPECT_EQ(result, R"({
"thing1": 1,
"thing2": 2,
"thing3": 3
})");
result = Util::format("{:\t<#1}", map);
EXPECT_EQ(result, R"({
"thing1": 1,
"thing2": 2,
"thing3": 3
})");
// Multidimensional containers arent supported,
// the user should write a customization point
std::vector<std::vector<std::string>> twoDimensionalVector {
{ "thing1", "thing2", "thing3" },
{ "thing1", "thing2", "thing3" }
};
result = Util::format("{:#4}", twoDimensionalVector);
EXPECT_EQ(result, R"({
{thing1,thing2,thing3},
{thing1,thing2,thing3}
})");
}
// Local Variables:
// lsp-in-cpp-project-cache: nil
// End:

570
test/unit/testutiljson.cpp

@ -0,0 +1,570 @@
/*
* Copyright (C) 2022 Riyyi
*
* SPDX-License-Identifier: MIT
*/
#include <cstddef> // nullptr_t
#include <cstdint> // uint32_t
#include <functional> // function
#include <map>
#include <string>
#include <unordered_map>
#include <vector>
#include "macro.h"
#include "testcase.h"
#include "testsuite.h"
#include "util/json/array.h"
#include "util/json/job.h"
#include "util/json/json.h"
#include "util/json/lexer.h"
#include "util/json/parser.h"
#include "util/json/serializer.h"
#define DONT_PRINT_PARSER_ERRORS
#ifndef DONT_PRINT_PARSER_ERRORS
#define EXEC(x) x
#else
#define EXEC(x) \
stderr = Test::TestSuite::the().outputNull(); \
x; \
stderr = Test::TestSuite::the().outputErr();
#endif
std::vector<Util::JSON::Token> lex(const std::string& input)
{
EXEC(
Util::JSON::Job job(input);
Util::JSON::Lexer lexer(&job);
lexer.analyze(););
return *job.tokens();
}
Util::Json parse(const std::string& input)
{
EXEC(
Util::JSON::Job job(input);
Util::JSON::Lexer lexer(&job);
lexer.analyze(););
if (!job.success()) {
return nullptr;
}
EXEC(
Util::JSON::Parser parser(&job);
Util::Json json = parser.parse(););
if (!job.success()) {
return nullptr;
}
return json;
}
std::string serialize(const std::string& input, uint32_t indent = 0)
{
EXEC(
auto json = Util::Json::parse(input););
return json.dump(indent);
}
// -----------------------------------------
TEST_CASE(JsonLexer)
{
std::vector<Util::JSON::Token> tokens;
// Literal
tokens = lex("true");
EXPECT_EQ(tokens.size(), 1);
EXPECT_EQ(tokens[0].symbol, "true");
EXPECT(tokens[0].type == Util::JSON::Token::Type::Literal);
tokens = lex("false");
EXPECT_EQ(tokens.size(), 1);
EXPECT_EQ(tokens[0].symbol, "false");
EXPECT(tokens[0].type == Util::JSON::Token::Type::Literal);
tokens = lex("null");
EXPECT_EQ(tokens.size(), 1);
EXPECT_EQ(tokens[0].symbol, "null");
EXPECT(tokens[0].type == Util::JSON::Token::Type::Literal);
// Number
tokens = lex("3.14");
EXPECT_EQ(tokens.size(), 1);
EXPECT_EQ(tokens[0].symbol, "3.14");
EXPECT(tokens[0].type == Util::JSON::Token::Type::Number);
tokens = lex("-3.14e+2");
EXPECT_EQ(tokens.size(), 1);
EXPECT_EQ(tokens[0].symbol, "-3.14e+2");
EXPECT(tokens[0].type == Util::JSON::Token::Type::Number);
tokens = lex("+3.14");
EXPECT_EQ(tokens.size(), 1);
EXPECT_EQ(tokens[0].symbol, "+");
EXPECT(tokens[0].type == Util::JSON::Token::Type::None);
// String
tokens = lex(R"("a string")");
EXPECT_EQ(tokens.size(), 1);
EXPECT_EQ(tokens[0].symbol, "a string");
EXPECT(tokens[0].type == Util::JSON::Token::Type::String);
tokens = lex(R"("a string""another string")");
EXPECT_EQ(tokens.size(), 2);
EXPECT_EQ(tokens[0].symbol, "a string");
EXPECT_EQ(tokens[1].symbol, "another string");
EXPECT(tokens[0].type == Util::JSON::Token::Type::String);
tokens = lex("\"a string\nwill break on the newline symbol\"");
EXPECT_EQ(tokens.size(), 1);
EXPECT_EQ(tokens[0].symbol, "a string");
EXPECT(tokens[0].type == Util::JSON::Token::Type::String);
// Array
tokens = lex("[]");
EXPECT_EQ(tokens.size(), 2);
EXPECT_EQ(tokens[0].symbol, "[");
EXPECT_EQ(tokens[1].symbol, "]");
tokens = lex("[\n\n\n]");
EXPECT_EQ(tokens.size(), 2);
EXPECT_EQ(tokens[0].symbol, "[");
EXPECT_EQ(tokens[1].symbol, "]");
// Object
tokens = lex("{}");
EXPECT_EQ(tokens.size(), 2);
EXPECT_EQ(tokens[0].symbol, "{");
EXPECT_EQ(tokens[1].symbol, "}");
tokens = lex("{\n\n\n}");
EXPECT_EQ(tokens.size(), 2);
EXPECT_EQ(tokens[0].symbol, "{");
EXPECT_EQ(tokens[1].symbol, "}");
}
TEST_CASE(JsonParser)
{
Util::Json json;
json = parse("null");
EXPECT_EQ(json.size(), 0);
EXPECT_EQ(json.type(), Util::Json::Type::Null);
json = parse("true");
EXPECT_EQ(json.size(), 1);
EXPECT_EQ(json.type(), Util::Json::Type::Bool);
json = parse("false");
EXPECT_EQ(json.size(), 1);
EXPECT_EQ(json.type(), Util::Json::Type::Bool);
json = parse("3.14");
EXPECT_EQ(json.size(), 1);
EXPECT_EQ(json.type(), Util::Json::Type::Number);
json = parse(R"("a string")");
EXPECT_EQ(json.size(), 1);
EXPECT_EQ(json.type(), Util::Json::Type::String);
// Array
json = parse("[");
EXPECT_EQ(json.size(), 0);
EXPECT_EQ(json.type(), Util::Json::Type::Null);
json = parse("[ 123");
EXPECT_EQ(json.size(), 0);
EXPECT_EQ(json.type(), Util::Json::Type::Null);
json = parse("[ 123,");
EXPECT_EQ(json.size(), 0);
EXPECT_EQ(json.type(), Util::Json::Type::Null);
json = parse("[ 123, ]");
EXPECT_EQ(json.size(), 0);
EXPECT_EQ(json.type(), Util::Json::Type::Null);
json = parse("[ 123 456 ]");
EXPECT_EQ(json.size(), 0);
EXPECT_EQ(json.type(), Util::Json::Type::Null);
json = parse("[]");
EXPECT_EQ(json.size(), 0);
EXPECT_EQ(json.type(), Util::Json::Type::Array);
json = parse(R"([ "element", 3.14 ])");
EXPECT_EQ(json.size(), 2);
EXPECT_EQ(json.type(), Util::Json::Type::Array);
// Object
json = parse("{");
EXPECT_EQ(json.size(), 0);
EXPECT_EQ(json.type(), Util::Json::Type::Null);
json = parse(R"({ "name")");
EXPECT_EQ(json.size(), 0);
EXPECT_EQ(json.type(), Util::Json::Type::Null);
json = parse(R"({ "name":)");
EXPECT_EQ(json.size(), 0);
EXPECT_EQ(json.type(), Util::Json::Type::Null);
json = parse(R"({ "name":,)");
EXPECT_EQ(json.size(), 0);
EXPECT_EQ(json.type(), Util::Json::Type::Null);
json = parse(R"({ "name":"value")");
EXPECT_EQ(json.size(), 0);
EXPECT_EQ(json.type(), Util::Json::Type::Null);
json = parse(R"({ "name":"value",)");
EXPECT_EQ(json.size(), 0);
EXPECT_EQ(json.type(), Util::Json::Type::Null);
json = parse(R"({ "name":"value", })");
EXPECT_EQ(json.size(), 0);
EXPECT_EQ(json.type(), Util::Json::Type::Null);
json = parse(R"({ "name" "value" })");
EXPECT_EQ(json.size(), 0);
EXPECT_EQ(json.type(), Util::Json::Type::Null);
json = parse(R"({ 123 })");
EXPECT_EQ(json.size(), 0);
EXPECT_EQ(json.type(), Util::Json::Type::Null);
json = parse("{}");
EXPECT_EQ(json.size(), 0);
EXPECT_EQ(json.type(), Util::Json::Type::Object);
json = parse(R"({ "name": "value", "name2": 3.14 })");
EXPECT_EQ(json.size(), 2);
EXPECT_EQ(json.type(), Util::Json::Type::Object);
// Multiple root elements
json = parse("54 false");
EXPECT_EQ(json.type(), Util::Json::Type::Null);
json = parse("3.14, 666");
EXPECT_EQ(json.type(), Util::Json::Type::Null);
json = parse("true\nfalse");
EXPECT_EQ(json.type(), Util::Json::Type::Null);
}
TEST_CASE(JsonToJsonValue)
{
Util::Json json;
json = {};
EXPECT_EQ(json.size(), 0);
EXPECT_EQ(json.type(), Util::Json::Type::Null);
json = nullptr;
EXPECT_EQ(json.size(), 0);
EXPECT_EQ(json.type(), Util::Json::Type::Null);
json = true;
EXPECT_EQ(json.size(), 1);
EXPECT_EQ(json.type(), Util::Json::Type::Bool);
json = false;
EXPECT_EQ(json.size(), 1);
EXPECT_EQ(json.type(), Util::Json::Type::Bool);
json = 666;
EXPECT_EQ(json.size(), 1);
EXPECT_EQ(json.type(), Util::Json::Type::Number);
json = 3.14;
EXPECT_EQ(json.size(), 1);
EXPECT_EQ(json.type(), Util::Json::Type::Number);
const char* characters = "my string";
json = characters;
EXPECT_EQ(json.size(), 1);
EXPECT_EQ(json.type(), Util::Json::Type::String);
std::string string = "my string";
json = string;
EXPECT_EQ(json.size(), 1);
EXPECT_EQ(json.type(), Util::Json::Type::String);
// Nested Array with multiple types
json = { "element", 3.14, true, nullptr, { "nested element", { "more nesting", { 1, 2, 3, "yes" } } } };
EXPECT_EQ(json.size(), 5);
EXPECT_EQ(json.type(), Util::Json::Type::Array);
// Nested Object with multiple types
json = { { "name", "value" }, { "name2", 3.14 }, { "name3", true }, { "name4", nullptr }, { "name5", { { "nested name", "value" } } } };
EXPECT_EQ(json.size(), 5);
EXPECT_EQ(json.type(), Util::Json::Type::Object);
// Array with singular type
std::vector<std::string> vector = { "element", "element2", "element3" };
json = vector;
EXPECT_EQ(json.size(), 3);
EXPECT_EQ(json.type(), Util::Json::Type::Array);
// Object with singular type
std::map<std::string, std::string> map = { { "name", "value" }, { "name2", "value2" } };
json = map;
EXPECT_EQ(json.size(), 2);
EXPECT_EQ(json.type(), Util::Json::Type::Object);
// Object with singular type
std::unordered_map<std::string, std::string> unorderedMap = { { "name", "value" }, { "name2", "value2" } };
json = unorderedMap;
EXPECT_EQ(json.size(), 2);
EXPECT_EQ(json.type(), Util::Json::Type::Object);
}
TEST_CASE(JsonFromJsonValue)
{
Util::Json json;
json = nullptr;
EXPECT_EQ(json.get<std::nullptr_t>(), nullptr);
json = true;
EXPECT_EQ(json.get<bool>(), true);
json = false;
EXPECT_EQ(json.get<bool>(), false);
json = 666;
EXPECT_EQ(json.get<int>(), 666);
json = 3.14;
EXPECT_EQ(json.get<double>(), 3.14);
std::string string;
json = "my string";
json.getTo(string);
EXPECT_EQ(string, "my string");
EXPECT_EQ(json.get<std::string>(), "my string");
// Array with singular type
json = { "element", "element2" };
EXPECT_EQ(json[0].get<std::string>(), "element");
EXPECT_EQ(json.at(1).get<std::string>(), "element2");
auto array = json.get<std::vector<std::string>>();
EXPECT_EQ(array.size(), 2);
EXPECT_EQ(array[0], "element");
EXPECT_EQ(array[1], "element2");
// Array with multiple types
json = { "string", 3.14, true, nullptr };
EXPECT_EQ(json[0].get<std::string>(), "string");
EXPECT_EQ(json.at(1).get<double>(), 3.14);
EXPECT_EQ(json[2].get<bool>(), true);
EXPECT_EQ(json[3].get<std::nullptr_t>(), nullptr);
auto valueArray = json.get<std::vector<Util::Json>>();
EXPECT_EQ(valueArray.size(), 4);
EXPECT_EQ(valueArray[0].get<std::string>(), "string");
EXPECT_EQ(valueArray[1].get<double>(), 3.14);
EXPECT_EQ(valueArray[2].get<bool>(), true);
EXPECT_EQ(valueArray[3].get<std::nullptr_t>(), nullptr);
// Nested Array with multiple types
json = {
"value",
{
"thing",
666,
},
{
{
3.14,
},
}
};
EXPECT_EQ(json[0].get<std::string>(), "value");
EXPECT_EQ(json.at(1)[0].get<std::string>(), "thing");
EXPECT_EQ(json[1].at(1).get<int>(), 666);
EXPECT_EQ(json[2][0][0].get<double>(), 3.14);
// Object with singular type
json = { { "name", "value" }, { "name2", "value2" } };
EXPECT_EQ(json["name"].get<std::string>(), "value");
EXPECT_EQ(json.at("name2").get<std::string>(), "value2");
auto object = json.get<std::map<std::string, std::string>>();
EXPECT_EQ(object.size(), 2);
EXPECT_EQ(object["name"], "value");
EXPECT_EQ(object["name2"], "value2");
auto unorderedObject = json.get<std::unordered_map<std::string, std::string>>();
EXPECT_EQ(unorderedObject.size(), 2);
EXPECT_EQ(unorderedObject["name"], "value");
EXPECT_EQ(unorderedObject["name2"], "value2");
// Object with multiple types
json = { { "name", "value" }, { "name2", 3.14 }, { "name3", true }, { "name4", nullptr } };
EXPECT_EQ(json["name"].get<std::string>(), "value");
EXPECT_EQ(json.at("name2").get<double>(), 3.14);
EXPECT_EQ(json["name3"].get<bool>(), true);
EXPECT_EQ(json["name4"].get<std::nullptr_t>(), nullptr);
auto valueObject = json.get<std::map<std::string, Util::Json>>();
EXPECT_EQ(valueObject.size(), 4);
EXPECT_EQ(valueObject["name"].get<std::string>(), "value");
EXPECT_EQ(valueObject["name2"].get<double>(), 3.14);
EXPECT_EQ(valueObject["name3"].get<bool>(), true);
EXPECT_EQ(valueObject["name4"].get<std::nullptr_t>(), nullptr);
// Nested Object with multiple types
json = {
{
"name",
"value",
},
{
"nest 1-deep",
{ {
"number",
1,
} },
},
{
"nest 2-deep",
{ {
"nest 1-deep",
{ {
"bool",
true,
} },
} },
},
};
EXPECT_EQ(json["name"].get<std::string>(), "value");
EXPECT_EQ(json["nest 1-deep"]["number"].get<int>(), 1);
EXPECT_EQ(json["nest 2-deep"]["nest 1-deep"]["bool"].get<bool>(), true);
}
TEST_CASE(JsonImplicitConversion)
{
Util::Json array;
array[0];
EXPECT_EQ(array.type(), Util::Json::Type::Array);
Util::Json arrayEmplace;
arrayEmplace.emplace_back("element");
arrayEmplace.emplace_back({ "nested element" });
EXPECT_EQ(arrayEmplace.type(), Util::Json::Type::Array);
EXPECT_EQ(arrayEmplace[1].type(), Util::Json::Type::Array);
Util::Json object;
object[""];
EXPECT_EQ(object.type(), Util::Json::Type::Object);
Util::Json objectEmplace;
objectEmplace.emplace("name", "value");
objectEmplace.emplace("name2", { { "nested name", "value" } });
EXPECT_EQ(objectEmplace.type(), Util::Json::Type::Object);
EXPECT_EQ(objectEmplace["name2"].type(), Util::Json::Type::Object);
}
TEST_CASE(JsonSerializer)
{
EXPECT_EQ(serialize(""), "null");
EXPECT_EQ(serialize("null"), "null");
EXPECT_EQ(serialize("true"), "true");
EXPECT_EQ(serialize("false"), "false");
EXPECT_EQ(serialize("3.14"), "3.14");
EXPECT_EQ(serialize(R"("string")"), R"("string")");
EXPECT_EQ(serialize("\n\n\n"), "null");
EXPECT_EQ(serialize("null\n"), "null");
EXPECT_EQ(serialize("true\n"), "true");
EXPECT_EQ(serialize("false\n"), "false");
EXPECT_EQ(serialize("3.14\n"), "3.14");
// clang-format off
EXPECT_EQ(serialize(R"("string")" "\n"), R"("string")");
// clang-format on
EXPECT_EQ(serialize("[\n\n\n]"), "[]");
EXPECT_EQ(serialize("[null]"), "[null]");
EXPECT_EQ(serialize("[true]"), "[true]");
EXPECT_EQ(serialize("[false]"), "[false]");
EXPECT_EQ(serialize("[3.14]"), "[3.14]");
EXPECT_EQ(serialize(R"(["string"])"), R"(["string"])");
EXPECT_EQ(serialize("[\n\n\n]", 4), "[\n]");
EXPECT_EQ(serialize("[null]", 4), "[\n null\n]");
EXPECT_EQ(serialize("[true]", 4), "[\n true\n]");
EXPECT_EQ(serialize("[false]", 4), "[\n false\n]");
EXPECT_EQ(serialize("[3.14]", 4), "[\n 3.14\n]");
// clang-format off
EXPECT_EQ(serialize(R"(["string"])", 4), "[\n " R"("string")" "\n]");
// clang-format on
// Check for trailing comma on last array element
EXPECT_EQ(serialize(R"([1])"), R"([1])");
EXPECT_EQ(serialize(R"([1,2])"), R"([1,2])");
EXPECT_EQ(serialize(R"([1,2,3])"), R"([1,2,3])");
// Check for trailing comma on last object member
EXPECT_EQ(serialize(R"({"n1":"v1"})"), R"({"n1":"v1"})");
EXPECT_EQ(serialize(R"({"n1":"v1", "n2":"v2"})"), R"({"n1":"v1","n2":"v2"})");
EXPECT_EQ(serialize(R"({"n1":"v1", "n2":"v2", "n3":"v3"})"), R"({"n1":"v1","n2":"v2","n3":"v3"})");
// clang-format off
EXPECT_EQ(serialize(R"({
"object member one": [
"array element one"
],
"object member two": [
"array element one",
"array element two"
],
"object member three": [
"array element one",
2,
3.0,
4.56,
true,
false,
null
],
"object member four": 3.14,
"object member five": "value five",
"object member six": null,
"object member seven": { "no": 0 }
})", 4), R"({
"object member five": "value five",
"object member four": 3.14,
"object member one": [
"array element one"
],
"object member seven": {
"no": 0
},
"object member six": null,
"object member three": [
"array element one",
2,
3,
4.56,
true,
false,
null
],
"object member two": [
"array element one",
"array element two"
]
})");
// clang-format on
}

39
test/unit/testutilshell.cpp

@ -0,0 +1,39 @@
#include <string>
#include "macro.h"
#include "testcase.h"
#include "testsuite.h"
#include "util/shell.h"
bool runShell(const char* command, std::string* output = nullptr)
{
stdout = Test::TestSuite::the().outputNull();
Util::Shell $;
auto exec = $(command);
if (output) {
*output = exec.output();
}
stdout = Test::TestSuite::the().outputStd();
return !exec.status() ? true : false;
}
// -----------------------------------------
TEST_CASE(ShellCommand)
{
// Pipe test, grep section of the echo command, return true
std::string output = "";
auto result = runShell("echo 'Hello World!' | grep --only-matching 'Hello'", &output);
EXPECT_EQ(result, true);
EXPECT_EQ(output, "Hello\n");
// Return false
result = runShell("exit 1");
EXPECT_EQ(result, false);
// Return false
result = runShell("failure() { return 1; }; failure");
EXPECT_EQ(result, false);
}

98
test/unit/testutilsystem.cpp

@ -0,0 +1,98 @@
#include <functional> // function
#include <string>
#include <vector>
#include "macro.h"
#include "testcase.h"
#include "testsuite.h"
#include "util/system.h"
bool runSystem(std::function<Util::System(Util::System&)> commands, std::string* output = nullptr, std::string* error = nullptr)
{
Util::System system;
auto exec = commands(system);
if (output) {
*output = exec.output();
}
if (error) {
*error = exec.error();
}
return !exec.status() ? true : false;
}
// -----------------------------------------
TEST_CASE(SystemCommand)
{
// Regular echo command, return true
std::string output = "";
std::string error = "";
auto result = runSystem([&](auto& $) {
return $("echo Hello World!")();
}, &output, &error);
EXPECT_EQ(result, true);
EXPECT_EQ(output, "Hello World!\n");
EXPECT_EQ(error, "");
// Apend output of two echo commands, return true
output = "";
error = "";
result = runSystem([&](auto& $) {
return $("echo -n Hello ") + $("echo -n World!");
}, &output, &error);
EXPECT_EQ(result, true);
EXPECT_EQ(output, "Hello World!");
EXPECT_EQ(error, "");
// Pipe test, grep section of the echo command, return true
output = "";
error = "";
result = runSystem([&](auto& $) {
return $("echo Hello World!") | $("grep --only-matching Hello");
}, &output, &error);
EXPECT_EQ(result, true);
EXPECT_EQ(output, "Hello\n");
EXPECT_EQ(error, "");
// Run all commands until first false, return true
output = "";
error = "";
result = runSystem([&](auto& $) {
return $("echo Hello") && $("echo World!");
}, &output, &error);
EXPECT_EQ(result, true);
EXPECT_EQ(output, "Hello\nWorld!\n");
EXPECT_EQ(error, "");
// Run all commands until first true, return true
output = "";
error = "";
result = runSystem([&](auto& $) {
return $("echo Hello") || $("echo World!");
}, &output, &error);
EXPECT_EQ(result, true);
EXPECT_EQ(output, "Hello\n");
EXPECT_EQ(error, "");
// And plus pipe test, grep the middle word, return true
output = "";
error = "";
result = runSystem([&](auto& $) {
return ($("echo -n one ") && $("echo -n two ") && $("echo -n three")) | $("grep --only-matching two");
}, &output, &error);
EXPECT_EQ(result, true);
EXPECT_EQ(output, "two\n");
EXPECT_EQ(error, "");
// FIXME waitpid does not seem to get the right exit status here
// Return false
output = "";
error = "";
result = runSystem([&](auto& $) {
return $("exit 1")();
}, &output, &error);
EXPECT_EQ(result, false);
EXPECT_EQ(output, "");
EXPECT_EQ(error, "");
}
Loading…
Cancel
Save