Compare commits
66
Commits
cfe2987e50
...
4ea1242247
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4ea1242247 | ||
|
|
d6cd90ae4a | ||
|
|
b76223693b | ||
|
|
ad137576a8 | ||
|
|
5835c63bda | ||
|
|
aa283f8c75 | ||
|
|
39329caf22 | ||
|
|
a33f35faab | ||
|
|
aa5b5117ad | ||
|
|
51e788a5ad | ||
|
|
c7e4eb2575 | ||
|
|
55ed70353a | ||
|
|
abb075939f | ||
|
|
a3ab6aecfa | ||
|
|
097be4c012 | ||
|
|
ea7049306e | ||
|
|
fc3dc936fa | ||
|
|
f2ea84be0f | ||
|
|
003db7332d | ||
|
|
8797f695e0 | ||
|
|
74dd24a516 | ||
|
|
5c95288874 | ||
|
|
0c972f420c | ||
|
|
c5bbd7befd | ||
|
|
a209452a68 | ||
|
|
4487c800df | ||
|
|
d9cfd3f7c1 | ||
|
|
0465d5802a | ||
|
|
66774364fd | ||
|
|
3078f62162 | ||
|
|
8e92317f9e | ||
|
|
c3d6af85ba | ||
|
|
5d844554f5 | ||
|
|
a3d1cd1d74 | ||
|
|
ed5efdb0d6 | ||
|
|
4457a711cd | ||
|
|
925fdd474a | ||
|
|
d65f56bf03 | ||
|
|
4501061060 | ||
|
|
0aef4ee2c0 | ||
|
|
c1c9429a9d | ||
|
|
356cdaf051 | ||
|
|
b41bba72c9 | ||
|
|
a818c4489c | ||
|
|
8800ff4f7a | ||
|
|
6161d577c6 | ||
|
|
ddf0a2858b | ||
|
|
8c08b8a594 | ||
|
|
4a7ed92519 | ||
|
|
95e0ddf49a | ||
|
|
fdeba07fd4 | ||
|
|
a14cd1e0a5 | ||
|
|
061ed74d4f | ||
|
|
8fb1a1a8e9 | ||
|
|
bc01b34e58 | ||
|
|
68bc95fdf1 | ||
|
|
13d020a351 | ||
|
|
af678374bc | ||
|
|
e5978310bf | ||
|
|
8bfae9b483 | ||
|
|
9f7fe81ef6 | ||
|
|
aad95de5fd | ||
|
|
c385432bb0 | ||
|
|
5188d57d19 | ||
|
|
0880d98fe0 | ||
|
|
1c676f9548 |
@@ -105,7 +105,6 @@ manafiles will make sure that the contents of the block are commented.
|
||||
- (make) ~cmake~
|
||||
- (make) ~git~
|
||||
- (make) ~gzip~
|
||||
- (make) ~nlohmann-json~ [[https://archlinux.org/packages/community/any/nlohmann-json/][arch]], [[https://packages.debian.org/search?keywords=nlohmann-json][debian]], [[https://packages.ubuntu.com/search?keywords=nlohmann-json][ubuntu]]
|
||||
- (optional) ~grep~
|
||||
- (optional) ~pacman~ + ~pacman-contrib~
|
||||
- (optional) ~apt~ + ~dpkg~
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"ignorePatterns" : [
|
||||
"ignorePatterns": [
|
||||
".git/",
|
||||
"*.md",
|
||||
"manafiles.json",
|
||||
|
||||
+12
-10
@@ -4,6 +4,7 @@
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
#include <cassert> // assert
|
||||
#include <csignal> // raise
|
||||
#include <cstdio> // fprintf
|
||||
#include <filesystem> // current_path, recursive_directory
|
||||
@@ -11,9 +12,8 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include "config.h"
|
||||
#include "util/json/value.h"
|
||||
|
||||
Config::Config(s)
|
||||
: m_workingDirectory(std::filesystem::current_path())
|
||||
@@ -51,7 +51,7 @@ void Config::parseConfigFile()
|
||||
return;
|
||||
}
|
||||
|
||||
nlohmann::json json;
|
||||
Json::Value json;
|
||||
|
||||
std::ifstream file(m_config);
|
||||
if (!file.is_open()) {
|
||||
@@ -72,21 +72,23 @@ void Config::parseConfigFile()
|
||||
|
||||
// -----------------------------------------
|
||||
|
||||
void to_json(nlohmann::json& object, const Settings& settings)
|
||||
void toJson(Json::Value& json, const Settings& settings)
|
||||
{
|
||||
object = nlohmann::json {
|
||||
json = Json::Value {
|
||||
{ "ignorePatterns", settings.ignorePatterns },
|
||||
{ "systemPatterns", settings.systemPatterns }
|
||||
};
|
||||
}
|
||||
|
||||
void from_json(const nlohmann::json& object, Settings& settings)
|
||||
void fromJson(const Json::Value& json, Settings& settings)
|
||||
{
|
||||
if (object.find("ignorePatterns") != object.end()) {
|
||||
object.at("ignorePatterns").get_to(settings.ignorePatterns);
|
||||
assert(json.type() == Json::Value::Type::Object);
|
||||
|
||||
if (json.exists("ignorePatterns")) {
|
||||
json.at("ignorePatterns").getTo(settings.ignorePatterns);
|
||||
}
|
||||
|
||||
if (object.find("systemPatterns") != object.end()) {
|
||||
object.at("systemPatterns").get_to(settings.systemPatterns);
|
||||
if (json.exists("systemPatterns")) {
|
||||
json.at("systemPatterns").getTo(settings.systemPatterns);
|
||||
}
|
||||
}
|
||||
|
||||
+4
-5
@@ -12,9 +12,8 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "nlohmann/json.hpp"
|
||||
|
||||
#include "util/singleton.h"
|
||||
#include "util/json/value.h"
|
||||
|
||||
struct Settings {
|
||||
std::vector<std::string> ignorePatterns {
|
||||
@@ -64,9 +63,9 @@ private:
|
||||
|
||||
// -----------------------------------------
|
||||
|
||||
// nlohmann::json arbitrary type conversion functions
|
||||
// Json arbitrary type conversion functions
|
||||
|
||||
void to_json(nlohmann::json& object, const Settings& settings);
|
||||
void from_json(const nlohmann::json& object, Settings& settings);
|
||||
void toJson(Json::Value& object, const Settings& settings);
|
||||
void fromJson(const Json::Value& object, Settings& settings);
|
||||
|
||||
#endif // CONFIG_H
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright (C) 2022 Riyyi
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
#include "util/json/array.h"
|
||||
#include "util/json/value.h"
|
||||
|
||||
namespace 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 Json
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright (C) 2022 Riyyi
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
#ifndef JSON_ARRAY_H
|
||||
#define JSON_ARRAY_H
|
||||
|
||||
#include <utility> // move
|
||||
#include <vector>
|
||||
|
||||
#include "util/json/parser.h"
|
||||
|
||||
namespace 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)
|
||||
{
|
||||
}
|
||||
|
||||
void emplace_back(Value element);
|
||||
void reserve(size_t size) { m_elements.reserve(size); }
|
||||
|
||||
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); }
|
||||
|
||||
size_t size() const { return m_elements.size(); }
|
||||
const std::vector<Value>& elements() const { return m_elements; }
|
||||
|
||||
private:
|
||||
std::vector<Value> m_elements;
|
||||
};
|
||||
|
||||
} // namespace Json
|
||||
|
||||
#endif // JSON_ARRAY_H
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Copyright (C) 2022 Riyyi
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
#ifndef JSON_CONVERSION_H
|
||||
#define JSON_CONVERSION_H
|
||||
|
||||
namespace Json {
|
||||
|
||||
namespace Detail {
|
||||
|
||||
// Avoid ODR (One Definition Rule) violations
|
||||
template<typename T>
|
||||
constexpr T staticConst {};
|
||||
|
||||
} // namespace Detail
|
||||
|
||||
} // namespace Json
|
||||
|
||||
#endif // JSON_CONVERSION_H
|
||||
@@ -0,0 +1,136 @@
|
||||
/*
|
||||
* Copyright (C) 2022 Riyyi
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
#ifndef JSON_FROM_JSON_H
|
||||
#define JSON_FROM_JSON_H
|
||||
|
||||
#include <algorithm> // transform
|
||||
#include <cassert> // assert
|
||||
#include <cstddef> // nullptr_t
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <utility> // forward
|
||||
#include <vector>
|
||||
|
||||
#include "util/json/array.h"
|
||||
#include "util/json/conversion.h"
|
||||
#include "util/json/object.h"
|
||||
|
||||
namespace 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)
|
||||
{
|
||||
assert(json.type() == Json::Type::Null);
|
||||
null = nullptr;
|
||||
}
|
||||
|
||||
template<typename Json>
|
||||
void fromJson(const Json& json, bool& boolean)
|
||||
{
|
||||
assert(json.type() == Json::Type::Bool);
|
||||
boolean = json.asBool();
|
||||
}
|
||||
|
||||
template<typename Json>
|
||||
void fromJson(const Json& json, int& number)
|
||||
{
|
||||
assert(json.type() == Json::Type::Number);
|
||||
number = (int)json.asDouble();
|
||||
}
|
||||
|
||||
template<typename Json>
|
||||
void fromJson(const Json& json, double& number)
|
||||
{
|
||||
assert(json.type() == Json::Type::Number);
|
||||
number = json.asDouble();
|
||||
}
|
||||
|
||||
template<typename Json>
|
||||
void fromJson(const Json& json, std::string& string)
|
||||
{
|
||||
assert(json.type() == Json::Type::String);
|
||||
string = json.asString();
|
||||
}
|
||||
|
||||
template<typename Json, typename T>
|
||||
void fromJson(const Json& json, std::vector<T>& array)
|
||||
{
|
||||
assert(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)
|
||||
{
|
||||
assert(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)
|
||||
{
|
||||
assert(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 = Detail::staticConst<Detail::fromJsonFunction>; // NOLINT (misc-definitions-in-headers)
|
||||
} // namespace
|
||||
|
||||
} // namespace Json
|
||||
|
||||
#endif // JSON_FROM_JSON_H
|
||||
|
||||
// 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.
|
||||
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* 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 Json {
|
||||
|
||||
Job::Job(const std::string& 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";
|
||||
printf(errorFormat.c_str(),
|
||||
token.line + 1,
|
||||
token.column + 1,
|
||||
message);
|
||||
|
||||
// Get the JSON line that caused the error
|
||||
std::istringstream input(m_input);
|
||||
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";
|
||||
printf(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";
|
||||
printf(arrowFormat.c_str(),
|
||||
std::string(m_lineNumbersWidth, ' ').c_str(),
|
||||
std::string(token.column, ' ').c_str(),
|
||||
std::string(line.length() - token.column, '~').c_str());
|
||||
}
|
||||
|
||||
} // namespace Json
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright (C) 2022 Riyyi
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
#ifndef JSON_JOB_H
|
||||
#define JSON_JOB_H
|
||||
|
||||
#include <cstddef> // size_t
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "util/json/lexer.h"
|
||||
|
||||
namespace Json {
|
||||
|
||||
class Value;
|
||||
|
||||
class Job {
|
||||
public:
|
||||
Job(const std::string& input);
|
||||
virtual ~Job();
|
||||
|
||||
enum class Color {
|
||||
None,
|
||||
Info,
|
||||
Warn,
|
||||
Danger,
|
||||
Success,
|
||||
Comment,
|
||||
};
|
||||
|
||||
Value fire();
|
||||
|
||||
void printErrorLine(Token token, const char* message);
|
||||
|
||||
bool success() const { return m_success; }
|
||||
const std::string& input() const { return m_input; }
|
||||
std::vector<Token>* tokens() { return &m_tokens; }
|
||||
|
||||
private:
|
||||
bool m_success { true };
|
||||
|
||||
std::string m_input;
|
||||
size_t m_lineNumbersWidth { 0 };
|
||||
|
||||
std::vector<Token> m_tokens;
|
||||
};
|
||||
|
||||
} // namespace Json
|
||||
|
||||
#endif // JSON_JOB_H
|
||||
@@ -0,0 +1,230 @@
|
||||
/*
|
||||
* Copyright (C) 2022 Riyyi
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
|
||||
#include "util/json/job.h"
|
||||
#include "util/json/lexer.h"
|
||||
|
||||
namespace Json {
|
||||
|
||||
Lexer::Lexer(Job* job)
|
||||
: m_job(job)
|
||||
, m_tokens(job->tokens())
|
||||
{
|
||||
}
|
||||
|
||||
Lexer::~Lexer()
|
||||
{
|
||||
}
|
||||
|
||||
// -----------------------------------------
|
||||
|
||||
void Lexer::analyze()
|
||||
{
|
||||
while (m_index < m_job->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 (!getString()) {
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case '-':
|
||||
case '0':
|
||||
case '1':
|
||||
case '2':
|
||||
case '3':
|
||||
case '4':
|
||||
case '5':
|
||||
case '6':
|
||||
case '7':
|
||||
case '8':
|
||||
case '9':
|
||||
if (!getNumber()) {
|
||||
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 (!getLiteral()) {
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case ' ':
|
||||
case '\t':
|
||||
break;
|
||||
case '\r':
|
||||
if (peekNext() == '\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;
|
||||
}
|
||||
|
||||
increment();
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------
|
||||
|
||||
char Lexer::peek()
|
||||
{
|
||||
return m_job->input()[m_index];
|
||||
}
|
||||
|
||||
char Lexer::peekNext()
|
||||
{
|
||||
return m_job->input()[m_index + 1];
|
||||
}
|
||||
|
||||
void Lexer::increment()
|
||||
{
|
||||
m_index++;
|
||||
m_column++;
|
||||
}
|
||||
|
||||
void Lexer::decrement()
|
||||
{
|
||||
m_index--;
|
||||
m_column--;
|
||||
}
|
||||
|
||||
char Lexer::consume()
|
||||
{
|
||||
char character = peek();
|
||||
increment();
|
||||
return character;
|
||||
}
|
||||
|
||||
bool Lexer::getString()
|
||||
{
|
||||
size_t column = m_column;
|
||||
std::string symbol = "";
|
||||
std::string breakOnCharacter = std::string() + '"' + '\r' + '\n' + '\0';
|
||||
|
||||
bool escape = false;
|
||||
char character = consume();
|
||||
for (;;) {
|
||||
character = peek();
|
||||
|
||||
if (!escape && character == '\\') {
|
||||
symbol += '\\';
|
||||
increment();
|
||||
escape = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!escape && breakOnCharacter.find(character) != std::string::npos) {
|
||||
break;
|
||||
}
|
||||
|
||||
symbol += character;
|
||||
increment();
|
||||
|
||||
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::getNumberOrLiteral(Token::Type type)
|
||||
{
|
||||
size_t column = m_column;
|
||||
std::string symbol = "";
|
||||
std::string breakOnCharacter = std::string("{}[]:,") + '"' + ' ' + '\t' + '\r' + '\n' + '\0';
|
||||
|
||||
for (char character;;) {
|
||||
character = peek();
|
||||
|
||||
if (breakOnCharacter.find(character) != std::string::npos) {
|
||||
break;
|
||||
}
|
||||
|
||||
symbol += character;
|
||||
increment();
|
||||
}
|
||||
decrement();
|
||||
|
||||
m_tokens->push_back({ type, m_line, column, symbol });
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Lexer::getNumber()
|
||||
{
|
||||
return getNumberOrLiteral(Token::Type::Number);
|
||||
}
|
||||
|
||||
bool Lexer::getLiteral()
|
||||
{
|
||||
return getNumberOrLiteral(Token::Type::Literal);
|
||||
}
|
||||
|
||||
} // namespace Json
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright (C) 2022 Riyyi
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
#ifndef JSON_LEXER_H
|
||||
#define JSON_LEXER_H
|
||||
|
||||
// The JavaScript Object Notation (JSON) Data Interchange Format
|
||||
// https://www.rfc-editor.org/rfc/pdfrfc/rfc8259.txt.pdf
|
||||
|
||||
#include <cstddef> // size_t
|
||||
#include <memory> // shared_ptr
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace Json {
|
||||
|
||||
class Job;
|
||||
|
||||
struct Token {
|
||||
enum class Type {
|
||||
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 {
|
||||
public:
|
||||
Lexer(Job* job);
|
||||
virtual ~Lexer();
|
||||
|
||||
void analyze();
|
||||
|
||||
private:
|
||||
char peek();
|
||||
char peekNext();
|
||||
|
||||
void increment();
|
||||
void decrement();
|
||||
char consume();
|
||||
|
||||
bool getString();
|
||||
bool getNumberOrLiteral(Token::Type type);
|
||||
bool getNumber();
|
||||
bool getLiteral();
|
||||
|
||||
Job* m_job { nullptr };
|
||||
|
||||
size_t m_index { 0 };
|
||||
size_t m_column { 0 };
|
||||
size_t m_line { 0 };
|
||||
|
||||
std::vector<Token>* m_tokens { nullptr };
|
||||
};
|
||||
|
||||
} // namespace Json
|
||||
|
||||
#endif // JSON_LEXER_H
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright (C) 2022 Riyyi
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
#include "util/json/object.h"
|
||||
#include "util/json/value.h"
|
||||
|
||||
namespace 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 Json
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright (C) 2022 Riyyi
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
#ifndef JSON_OBJECT_H
|
||||
#define JSON_OBJECT_H
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <utility> // move
|
||||
|
||||
#include "util/json/parser.h"
|
||||
|
||||
namespace Json {
|
||||
|
||||
class Value;
|
||||
|
||||
class Object {
|
||||
public:
|
||||
Object() {}
|
||||
virtual ~Object() {}
|
||||
|
||||
Object(const Object& other)
|
||||
: m_members(other.m_members)
|
||||
{
|
||||
}
|
||||
|
||||
void emplace(const std::string& name, Value value);
|
||||
|
||||
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); }
|
||||
|
||||
size_t size() const { return m_members.size(); }
|
||||
const std::map<std::string, Value>& members() const { return m_members; }
|
||||
|
||||
private:
|
||||
std::map<std::string, Value> m_members;
|
||||
};
|
||||
|
||||
} // namespace Json
|
||||
|
||||
#endif // JSON_OBJECT_H
|
||||
@@ -0,0 +1,493 @@
|
||||
/*
|
||||
* 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"
|
||||
|
||||
namespace 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 = getLiteral();
|
||||
break;
|
||||
case Token::Type::Number:
|
||||
result = getNumber();
|
||||
break;
|
||||
case Token::Type::String:
|
||||
result = getString();
|
||||
break;
|
||||
case Token::Type::BracketOpen:
|
||||
result = getArray();
|
||||
break;
|
||||
case Token::Type::BraceOpen:
|
||||
result = getObject();
|
||||
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 (!reachedEnd()) {
|
||||
m_job->printErrorLine(peek(), "multiple root elements");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// -----------------------------------------
|
||||
|
||||
bool Parser::reachedEnd()
|
||||
{
|
||||
return m_index >= m_tokens->size();
|
||||
}
|
||||
|
||||
bool Parser::seekForward(Token::Type type)
|
||||
{
|
||||
for (; !reachedEnd(); ++m_index) {
|
||||
if (peek().type == type) {
|
||||
m_index++;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
Token Parser::peek()
|
||||
{
|
||||
return m_tokens->at(m_index);
|
||||
}
|
||||
|
||||
Token Parser::consume()
|
||||
{
|
||||
Token token = peek();
|
||||
m_index++;
|
||||
return token;
|
||||
}
|
||||
|
||||
Value Parser::getLiteral()
|
||||
{
|
||||
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::getNumber()
|
||||
{
|
||||
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 {
|
||||
Int,
|
||||
Fraction,
|
||||
Exponent
|
||||
};
|
||||
|
||||
State state = State::Int;
|
||||
std::string validCharacters = "0123456789-";
|
||||
|
||||
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;
|
||||
validCharacters = "0123456789";
|
||||
fractionPosition = i;
|
||||
continue;
|
||||
}
|
||||
// Int/Fraction -> Exponent
|
||||
else if ((character == 'e' || character == 'E') && state != State::Exponent) {
|
||||
state = State::Exponent;
|
||||
validCharacters = "0123456789+-";
|
||||
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 if (state == State::Fraction) {
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (validCharacters.find(character) == std::string::npos) {
|
||||
reportError(token, std::string() + "invalid number, unexpected '" + character + '\'');
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
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::getString()
|
||||
{
|
||||
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::getArray()
|
||||
{
|
||||
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
|
||||
seekForward(Token::Type::BracketClose);
|
||||
};
|
||||
|
||||
Value array = Value::Type::Array;
|
||||
Token token;
|
||||
for (;;) {
|
||||
// EOF
|
||||
if (reachedEnd()) {
|
||||
reportError(m_tokens->at(m_index - 1), "expecting closing ']' at end");
|
||||
break;
|
||||
}
|
||||
|
||||
token = peek();
|
||||
if (token.type == Token::Type::Literal) {
|
||||
array.emplace_back(getLiteral());
|
||||
}
|
||||
else if (token.type == Token::Type::Number) {
|
||||
array.emplace_back(getNumber());
|
||||
}
|
||||
else if (token.type == Token::Type::String) {
|
||||
array.emplace_back(getString());
|
||||
}
|
||||
else if (token.type == Token::Type::BracketOpen) {
|
||||
array.emplace_back(getArray());
|
||||
}
|
||||
else if (token.type == Token::Type::BraceOpen) {
|
||||
array.emplace_back(getObject());
|
||||
}
|
||||
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 (reachedEnd()) {
|
||||
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::getObject()
|
||||
{
|
||||
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
|
||||
seekForward(Token::Type::BraceClose);
|
||||
};
|
||||
|
||||
Value object = Value::Type::Object;
|
||||
Token token;
|
||||
std::string name;
|
||||
std::map<std::string, uint8_t> unique;
|
||||
for (;;) {
|
||||
// EOF
|
||||
if (reachedEnd()) {
|
||||
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 = getString();
|
||||
if (tmpName.m_type != Value::Type::String) {
|
||||
seekForward(Token::Type::BraceClose);
|
||||
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 (reachedEnd()) {
|
||||
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 (reachedEnd()) {
|
||||
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[name] = getLiteral();
|
||||
}
|
||||
else if (token.type == Token::Type::Number) {
|
||||
object[name] = getNumber();
|
||||
}
|
||||
else if (token.type == Token::Type::String) {
|
||||
object[name] = getString();
|
||||
}
|
||||
else if (token.type == Token::Type::BracketOpen) {
|
||||
object[name] = getArray();
|
||||
}
|
||||
else if (token.type == Token::Type::BraceOpen) {
|
||||
object[name] = getObject();
|
||||
}
|
||||
else {
|
||||
reportError(token, "expecting value, not '" + token.symbol + "'");
|
||||
break;
|
||||
}
|
||||
|
||||
// EOF
|
||||
if (reachedEnd()) {
|
||||
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 Json
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright (C) 2022 Riyyi
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
#ifndef JSON_PARSER_H
|
||||
#define JSON_PARSER_H
|
||||
|
||||
#include <cstddef> // size_t
|
||||
#include <vector>
|
||||
|
||||
#include "util/json/lexer.h"
|
||||
|
||||
namespace Json {
|
||||
|
||||
class Job;
|
||||
class Value;
|
||||
|
||||
class Parser {
|
||||
public:
|
||||
Parser(Job* job);
|
||||
virtual ~Parser();
|
||||
|
||||
Value parse();
|
||||
|
||||
private:
|
||||
bool reachedEnd();
|
||||
bool seekForward(Token::Type type);
|
||||
|
||||
Token peek();
|
||||
Token consume();
|
||||
|
||||
Value getLiteral();
|
||||
Value getNumber();
|
||||
Value getString();
|
||||
Value getArray();
|
||||
Value getObject();
|
||||
|
||||
Job* m_job { nullptr };
|
||||
|
||||
size_t m_index { 0 };
|
||||
|
||||
std::vector<Token>* m_tokens { nullptr };
|
||||
};
|
||||
|
||||
} // namespace Json
|
||||
|
||||
#endif // JSON_PARSER_H
|
||||
@@ -0,0 +1,140 @@
|
||||
/*
|
||||
* Copyright (C) 2022 Riyyi
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
#include <cstdint> // uint32_t
|
||||
#include <iterator> // prev
|
||||
#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 Json {
|
||||
|
||||
Serializer::Serializer(const Value& value, const uint32_t indent, const char indentCharacter)
|
||||
: m_value(value)
|
||||
, m_indent(indent)
|
||||
, m_indentCharacter(indentCharacter)
|
||||
{
|
||||
}
|
||||
|
||||
Serializer::~Serializer()
|
||||
{
|
||||
}
|
||||
|
||||
// ------------------------------------------
|
||||
|
||||
std::string Serializer::dump()
|
||||
{
|
||||
return dumpHelper(m_value);
|
||||
}
|
||||
|
||||
// ------------------------------------------
|
||||
|
||||
std::string Serializer::dumpHelper(const Value& value, const uint32_t indentLevel)
|
||||
{
|
||||
switch (value.m_type) {
|
||||
case Value::Type::Null:
|
||||
return "null";
|
||||
break;
|
||||
case Value::Type::Bool:
|
||||
return value.m_value.boolean ? "true" : "false";
|
||||
break;
|
||||
case Value::Type::Number: {
|
||||
std::ostringstream os;
|
||||
os << value.m_value.number;
|
||||
return os.str();
|
||||
break;
|
||||
}
|
||||
case Value::Type::String:
|
||||
return "\"" + *value.m_value.string + "\"";
|
||||
break;
|
||||
case Value::Type::Array:
|
||||
return dumpArray(value, indentLevel);
|
||||
break;
|
||||
case Value::Type::Object:
|
||||
return dumpObject(value, indentLevel);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
std::string Serializer::dumpArray(const Value& value, const uint32_t indentLevel)
|
||||
{
|
||||
std::string result;
|
||||
|
||||
// Append [
|
||||
result += "[";
|
||||
if (m_indent > 0) {
|
||||
result += '\n';
|
||||
}
|
||||
|
||||
auto values = value.m_value.array->elements();
|
||||
for (auto it = values.begin(); it != values.end(); ++it) {
|
||||
result += std::string(m_indent * (indentLevel + 1), m_indentCharacter);
|
||||
result += dumpHelper(*it, indentLevel + 1);
|
||||
|
||||
// Add comma, except after the last element
|
||||
if (it != std::prev(values.end(), 1)) {
|
||||
result += ",";
|
||||
}
|
||||
if (m_indent > 0) {
|
||||
result += '\n';
|
||||
}
|
||||
}
|
||||
|
||||
// Append indentation
|
||||
result += std::string(m_indent * indentLevel, m_indentCharacter);
|
||||
|
||||
// Append ]
|
||||
result += "]";
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string Serializer::dumpObject(const Value& value, const uint32_t indentLevel)
|
||||
{
|
||||
std::string result;
|
||||
|
||||
// Append {
|
||||
result += "{";
|
||||
if (m_indent > 0) {
|
||||
result += '\n';
|
||||
}
|
||||
|
||||
auto members = value.m_value.object->members();
|
||||
for (auto it = members.begin(); it != members.end(); ++it) {
|
||||
result += std::string(m_indent * (indentLevel + 1), m_indentCharacter);
|
||||
result += "\"" + it->first + "\":";
|
||||
if (m_indent > 0) {
|
||||
result += ' ';
|
||||
}
|
||||
result += dumpHelper(it->second, indentLevel + 1);
|
||||
|
||||
// Add comma, except after the last element
|
||||
if (it != std::prev(members.end(), 1)) {
|
||||
result += ",";
|
||||
}
|
||||
if (m_indent > 0) {
|
||||
result += '\n';
|
||||
}
|
||||
}
|
||||
|
||||
// Append indentation
|
||||
result += std::string(m_indent * indentLevel, m_indentCharacter);
|
||||
|
||||
// Append }
|
||||
result += "}";
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace Json
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright (C) 2022 Riyyi
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
#ifndef JSON_SERIALIZER_H
|
||||
#define JSON_SERIALIZER_H
|
||||
|
||||
#include <cstdint> // uint32_t
|
||||
#include <string>
|
||||
|
||||
#include "util/json/value.h"
|
||||
|
||||
namespace Json {
|
||||
|
||||
class Serializer {
|
||||
public:
|
||||
Serializer(const Value& value, const uint32_t indent = 0, const char indentCharacter = ' ');
|
||||
virtual ~Serializer();
|
||||
|
||||
std::string dump();
|
||||
|
||||
private:
|
||||
std::string dumpHelper(const Value& value, const uint32_t indentLevel = 0);
|
||||
std::string dumpArray(const Value& value, const uint32_t indentLevel = 0);
|
||||
std::string dumpObject(const Value& value, const uint32_t indentLevel = 0);
|
||||
|
||||
Value m_value;
|
||||
|
||||
uint32_t m_indent { 0 };
|
||||
char m_indentCharacter { ' ' };
|
||||
};
|
||||
|
||||
} // namespace Json
|
||||
|
||||
#endif // JSON_SERIALIZER_H
|
||||
@@ -0,0 +1,156 @@
|
||||
/*
|
||||
* Copyright (C) 2022 Riyyi
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
#ifndef JSON_TO_JSON_H
|
||||
#define JSON_TO_JSON_H
|
||||
|
||||
#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/conversion.h"
|
||||
#include "util/json/object.h"
|
||||
|
||||
namespace Json {
|
||||
|
||||
namespace Detail {
|
||||
|
||||
struct jsonConstructor {
|
||||
template<typename Json>
|
||||
static void construct(Json& json, bool boolean)
|
||||
{
|
||||
json.clear();
|
||||
json.m_type = Json::Type::Bool;
|
||||
json.m_value.boolean = boolean;
|
||||
}
|
||||
|
||||
template<typename Json>
|
||||
static void construct(Json& json, int number)
|
||||
{
|
||||
json.clear();
|
||||
json.m_type = Json::Type::Number;
|
||||
json.m_value.number = (double)number;
|
||||
}
|
||||
|
||||
template<typename Json>
|
||||
static void construct(Json& json, double number)
|
||||
{
|
||||
json.clear();
|
||||
json.m_type = Json::Type::Number;
|
||||
json.m_value.number = number;
|
||||
}
|
||||
|
||||
template<typename Json>
|
||||
static void construct(Json& json, const char* string)
|
||||
{
|
||||
json.clear();
|
||||
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.clear();
|
||||
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.clear();
|
||||
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.clear();
|
||||
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.clear();
|
||||
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.clear();
|
||||
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.clear();
|
||||
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 = Detail::staticConst<Detail::toJsonFunction>; // NOLINT (misc-definitions-in-headers)
|
||||
} // namespace
|
||||
|
||||
} // namespace Json
|
||||
|
||||
#endif // JSON_TO_JSON_H
|
||||
|
||||
// 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.
|
||||
@@ -0,0 +1,296 @@
|
||||
/*
|
||||
* Copyright (C) 2022 Riyyi
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
#include <algorithm> // all_of
|
||||
#include <cassert> // assert
|
||||
#include <cstdint> // uint32_t
|
||||
#include <iostream> // istream, ostream
|
||||
#include <string>
|
||||
#include <utility> // move
|
||||
|
||||
#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"
|
||||
|
||||
namespace Json {
|
||||
|
||||
Value::Value(std::nullptr_t)
|
||||
: Value(Type::Null)
|
||||
{
|
||||
}
|
||||
|
||||
Value::Value(Type type)
|
||||
: m_type(type)
|
||||
{
|
||||
create();
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
copyFrom(other);
|
||||
}
|
||||
|
||||
// Assignment operator
|
||||
Value& Value::operator=(const Value& other)
|
||||
{
|
||||
if (this != &other) {
|
||||
clear();
|
||||
copyFrom(other);
|
||||
}
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
// ------------------------------------------
|
||||
|
||||
Value Value::parse(const std::string& input)
|
||||
{
|
||||
Job job(input);
|
||||
Value value = job.fire();
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
std::string Value::dump(const uint32_t indent, const char indentCharacter) const
|
||||
{
|
||||
Serializer serializer(*this, indent, indentCharacter);
|
||||
return serializer.dump();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
assert(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;
|
||||
}
|
||||
|
||||
assert(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
|
||||
{
|
||||
assert(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;
|
||||
}
|
||||
|
||||
assert(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;
|
||||
}
|
||||
|
||||
assert(m_type == Type::Object);
|
||||
return (*m_value.object)[key];
|
||||
}
|
||||
|
||||
const Value& Value::operator[](size_t index) const
|
||||
{
|
||||
assert(m_type == Type::Array);
|
||||
return (*m_value.array)[index];
|
||||
}
|
||||
|
||||
const Value& Value::operator[](const std::string& key) const
|
||||
{
|
||||
assert(m_type == Type::Object);
|
||||
return (*m_value.object)[key];
|
||||
}
|
||||
|
||||
Value& Value::at(size_t index)
|
||||
{
|
||||
assert(m_type == Type::Array);
|
||||
return m_value.array->at(index);
|
||||
}
|
||||
|
||||
Value& Value::at(const std::string& key)
|
||||
{
|
||||
assert(m_type == Type::Object);
|
||||
return m_value.object->at(key);
|
||||
}
|
||||
|
||||
const Value& Value::at(size_t index) const
|
||||
{
|
||||
assert(m_type == Type::Array);
|
||||
return m_value.array->at(index);
|
||||
}
|
||||
|
||||
const Value& Value::at(const std::string& key) const
|
||||
{
|
||||
assert(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::Bool:
|
||||
case Type::Number:
|
||||
case Type::String:
|
||||
return 1;
|
||||
case Type::Array:
|
||||
return m_value.array->size();
|
||||
case Type::Object:
|
||||
return m_value.object->size();
|
||||
default:
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------
|
||||
|
||||
void Value::create()
|
||||
{
|
||||
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;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void Value::clear()
|
||||
{
|
||||
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;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void Value::copyFrom(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;
|
||||
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());
|
||||
|
||||
Job job(inputString);
|
||||
value = job.fire();
|
||||
|
||||
return input;
|
||||
}
|
||||
|
||||
std::ostream& operator<<(std::ostream& output, const Value& value)
|
||||
{
|
||||
return output << value.dump(4);
|
||||
}
|
||||
|
||||
} // namespace Json
|
||||
@@ -0,0 +1,132 @@
|
||||
/*
|
||||
* Copyright (C) 2022 Riyyi
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
#ifndef JSON_VALUE_H
|
||||
#define JSON_VALUE_H
|
||||
|
||||
#include <cstddef> // nullptr_t
|
||||
#include <cstdint> // uint32_t
|
||||
#include <initializer_list>
|
||||
#include <iostream> // istream, ostream
|
||||
#include <string>
|
||||
#include <utility> // forward
|
||||
|
||||
#include "util/json/fromjson.h"
|
||||
#include "util/json/tojson.h"
|
||||
|
||||
namespace Json {
|
||||
|
||||
class Array;
|
||||
class Object;
|
||||
|
||||
class Value {
|
||||
private:
|
||||
friend Detail::jsonConstructor;
|
||||
friend class Parser;
|
||||
friend class Serializer;
|
||||
|
||||
public:
|
||||
enum class Type {
|
||||
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));
|
||||
}
|
||||
|
||||
// Destructor
|
||||
virtual ~Value() { clear(); }
|
||||
|
||||
// Copy constructor
|
||||
Value(const Value& other);
|
||||
|
||||
// Assignment operator
|
||||
Value& operator=(const Value& other);
|
||||
|
||||
// --------------------------------------
|
||||
|
||||
static Value parse(const std::string& input);
|
||||
std::string dump(const uint32_t indent = 0, const char indentCharacter = ' ') const;
|
||||
|
||||
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 create();
|
||||
void clear();
|
||||
void copyFrom(const Value& other);
|
||||
|
||||
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);
|
||||
|
||||
} // namespace Json
|
||||
|
||||
#endif // JSON_VALUE_H
|
||||
@@ -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/lexer.h"
|
||||
#include "util/json/parser.h"
|
||||
#include "util/json/serializer.h"
|
||||
#include "util/json/value.h"
|
||||
|
||||
#define DONT_PRINT_PARSER_ERRORS
|
||||
|
||||
#ifndef DONT_PRINT_PARSER_ERRORS
|
||||
#define EXEC(x) x
|
||||
#else
|
||||
#define EXEC(x) \
|
||||
stdout = Test::TestSuite::the().outputNull(); \
|
||||
x; \
|
||||
stdout = Test::TestSuite::the().outputStd();
|
||||
#endif
|
||||
|
||||
std::vector<Json::Token> lex(const std::string& input)
|
||||
{
|
||||
EXEC(
|
||||
Json::Job job(input);
|
||||
Json::Lexer lexer(&job);
|
||||
lexer.analyze(););
|
||||
return *job.tokens();
|
||||
}
|
||||
|
||||
Json::Value parse(const std::string& input)
|
||||
{
|
||||
EXEC(
|
||||
Json::Job job(input);
|
||||
Json::Lexer lexer(&job);
|
||||
lexer.analyze(););
|
||||
|
||||
if (!job.success()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
EXEC(
|
||||
Json::Parser parser(&job);
|
||||
Json::Value json = parser.parse(););
|
||||
|
||||
if (!job.success()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return json;
|
||||
}
|
||||
|
||||
std::string serialize(const std::string& input, uint32_t indent = 0)
|
||||
{
|
||||
EXEC(
|
||||
auto json = Json::Value::parse(input););
|
||||
return json.dump(indent);
|
||||
}
|
||||
|
||||
// -----------------------------------------
|
||||
|
||||
TEST_CASE(JsonLexer)
|
||||
{
|
||||
std::vector<Json::Token> tokens;
|
||||
|
||||
// Literal
|
||||
|
||||
tokens = lex("true");
|
||||
EXPECT_EQ(tokens.size(), 1);
|
||||
EXPECT_EQ(tokens[0].symbol, "true");
|
||||
EXPECT(tokens[0].type == Json::Token::Type::Literal);
|
||||
|
||||
tokens = lex("false");
|
||||
EXPECT_EQ(tokens.size(), 1);
|
||||
EXPECT_EQ(tokens[0].symbol, "false");
|
||||
EXPECT(tokens[0].type == Json::Token::Type::Literal);
|
||||
|
||||
tokens = lex("null");
|
||||
EXPECT_EQ(tokens.size(), 1);
|
||||
EXPECT_EQ(tokens[0].symbol, "null");
|
||||
EXPECT(tokens[0].type == 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 == 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 == Json::Token::Type::Number);
|
||||
|
||||
tokens = lex("+3.14");
|
||||
EXPECT_EQ(tokens.size(), 1);
|
||||
EXPECT_EQ(tokens[0].symbol, "+");
|
||||
EXPECT(tokens[0].type == 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 == 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 == 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 == 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)
|
||||
{
|
||||
Json::Value json;
|
||||
|
||||
json = parse("null");
|
||||
EXPECT_EQ(json.size(), 0);
|
||||
EXPECT_EQ(json.type(), Json::Value::Type::Null);
|
||||
|
||||
json = parse("true");
|
||||
EXPECT_EQ(json.size(), 1);
|
||||
EXPECT_EQ(json.type(), Json::Value::Type::Bool);
|
||||
|
||||
json = parse("false");
|
||||
EXPECT_EQ(json.size(), 1);
|
||||
EXPECT_EQ(json.type(), Json::Value::Type::Bool);
|
||||
|
||||
json = parse("3.14");
|
||||
EXPECT_EQ(json.size(), 1);
|
||||
EXPECT_EQ(json.type(), Json::Value::Type::Number);
|
||||
|
||||
json = parse(R"("a string")");
|
||||
EXPECT_EQ(json.size(), 1);
|
||||
EXPECT_EQ(json.type(), Json::Value::Type::String);
|
||||
|
||||
// Array
|
||||
|
||||
json = parse("[");
|
||||
EXPECT_EQ(json.size(), 0);
|
||||
EXPECT_EQ(json.type(), Json::Value::Type::Null);
|
||||
|
||||
json = parse("[ 123");
|
||||
EXPECT_EQ(json.size(), 0);
|
||||
EXPECT_EQ(json.type(), Json::Value::Type::Null);
|
||||
|
||||
json = parse("[ 123,");
|
||||
EXPECT_EQ(json.size(), 0);
|
||||
EXPECT_EQ(json.type(), Json::Value::Type::Null);
|
||||
|
||||
json = parse("[ 123, ]");
|
||||
EXPECT_EQ(json.size(), 0);
|
||||
EXPECT_EQ(json.type(), Json::Value::Type::Null);
|
||||
|
||||
json = parse("[ 123 456 ]");
|
||||
EXPECT_EQ(json.size(), 0);
|
||||
EXPECT_EQ(json.type(), Json::Value::Type::Null);
|
||||
|
||||
json = parse("[]");
|
||||
EXPECT_EQ(json.size(), 0);
|
||||
EXPECT_EQ(json.type(), Json::Value::Type::Array);
|
||||
|
||||
json = parse(R"([ "element", 3.14 ])");
|
||||
EXPECT_EQ(json.size(), 2);
|
||||
EXPECT_EQ(json.type(), Json::Value::Type::Array);
|
||||
|
||||
// Object
|
||||
|
||||
json = parse("{");
|
||||
EXPECT_EQ(json.size(), 0);
|
||||
EXPECT_EQ(json.type(), Json::Value::Type::Null);
|
||||
|
||||
json = parse(R"({ "name")");
|
||||
EXPECT_EQ(json.size(), 0);
|
||||
EXPECT_EQ(json.type(), Json::Value::Type::Null);
|
||||
|
||||
json = parse(R"({ "name":)");
|
||||
EXPECT_EQ(json.size(), 0);
|
||||
EXPECT_EQ(json.type(), Json::Value::Type::Null);
|
||||
|
||||
json = parse(R"({ "name":,)");
|
||||
EXPECT_EQ(json.size(), 0);
|
||||
EXPECT_EQ(json.type(), Json::Value::Type::Null);
|
||||
|
||||
json = parse(R"({ "name":"value")");
|
||||
EXPECT_EQ(json.size(), 0);
|
||||
EXPECT_EQ(json.type(), Json::Value::Type::Null);
|
||||
|
||||
json = parse(R"({ "name":"value",)");
|
||||
EXPECT_EQ(json.size(), 0);
|
||||
EXPECT_EQ(json.type(), Json::Value::Type::Null);
|
||||
|
||||
json = parse(R"({ "name":"value", })");
|
||||
EXPECT_EQ(json.size(), 0);
|
||||
EXPECT_EQ(json.type(), Json::Value::Type::Null);
|
||||
|
||||
json = parse(R"({ "name" "value" })");
|
||||
EXPECT_EQ(json.size(), 0);
|
||||
EXPECT_EQ(json.type(), Json::Value::Type::Null);
|
||||
|
||||
json = parse(R"({ 123 })");
|
||||
EXPECT_EQ(json.size(), 0);
|
||||
EXPECT_EQ(json.type(), Json::Value::Type::Null);
|
||||
|
||||
json = parse("{}");
|
||||
EXPECT_EQ(json.size(), 0);
|
||||
EXPECT_EQ(json.type(), Json::Value::Type::Object);
|
||||
|
||||
json = parse(R"({ "name": "value", "name2": 3.14 })");
|
||||
EXPECT_EQ(json.size(), 2);
|
||||
EXPECT_EQ(json.type(), Json::Value::Type::Object);
|
||||
|
||||
// Multiple root elements
|
||||
|
||||
json = parse("54 false");
|
||||
EXPECT_EQ(json.type(), Json::Value::Type::Null);
|
||||
|
||||
json = parse("3.14, 666");
|
||||
EXPECT_EQ(json.type(), Json::Value::Type::Null);
|
||||
|
||||
json = parse("true\nfalse");
|
||||
EXPECT_EQ(json.type(), Json::Value::Type::Null);
|
||||
}
|
||||
|
||||
TEST_CASE(JsonToJsonValue)
|
||||
{
|
||||
Json::Value json;
|
||||
|
||||
json = {};
|
||||
EXPECT_EQ(json.size(), 0);
|
||||
EXPECT_EQ(json.type(), Json::Value::Type::Null);
|
||||
|
||||
json = nullptr;
|
||||
EXPECT_EQ(json.size(), 0);
|
||||
EXPECT_EQ(json.type(), Json::Value::Type::Null);
|
||||
|
||||
json = true;
|
||||
EXPECT_EQ(json.size(), 1);
|
||||
EXPECT_EQ(json.type(), Json::Value::Type::Bool);
|
||||
|
||||
json = false;
|
||||
EXPECT_EQ(json.size(), 1);
|
||||
EXPECT_EQ(json.type(), Json::Value::Type::Bool);
|
||||
|
||||
json = 666;
|
||||
EXPECT_EQ(json.size(), 1);
|
||||
EXPECT_EQ(json.type(), Json::Value::Type::Number);
|
||||
|
||||
json = 3.14;
|
||||
EXPECT_EQ(json.size(), 1);
|
||||
EXPECT_EQ(json.type(), Json::Value::Type::Number);
|
||||
|
||||
const char* characters = "my string";
|
||||
json = characters;
|
||||
EXPECT_EQ(json.size(), 1);
|
||||
EXPECT_EQ(json.type(), Json::Value::Type::String);
|
||||
|
||||
std::string string = "my string";
|
||||
json = string;
|
||||
EXPECT_EQ(json.size(), 1);
|
||||
EXPECT_EQ(json.type(), Json::Value::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(), Json::Value::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(), Json::Value::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(), Json::Value::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(), Json::Value::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(), Json::Value::Type::Object);
|
||||
}
|
||||
|
||||
TEST_CASE(JsonFromJsonValue)
|
||||
{
|
||||
Json::Value 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<Json::Value>>();
|
||||
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, Json::Value>>();
|
||||
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)
|
||||
{
|
||||
Json::Value array;
|
||||
array[0];
|
||||
EXPECT_EQ(array.type(), Json::Value::Type::Array);
|
||||
|
||||
Json::Value arrayEmplace;
|
||||
arrayEmplace.emplace_back("element");
|
||||
arrayEmplace.emplace_back({ "nested element" });
|
||||
EXPECT_EQ(arrayEmplace.type(), Json::Value::Type::Array);
|
||||
EXPECT_EQ(arrayEmplace[1].type(), Json::Value::Type::Array);
|
||||
|
||||
Json::Value object;
|
||||
object[""];
|
||||
EXPECT_EQ(object.type(), Json::Value::Type::Object);
|
||||
|
||||
Json::Value objectEmplace;
|
||||
objectEmplace.emplace("name", "value");
|
||||
objectEmplace.emplace("name2", { { "nested name", "value" } });
|
||||
EXPECT_EQ(objectEmplace.type(), Json::Value::Type::Object);
|
||||
EXPECT_EQ(objectEmplace["name2"].type(), Json::Value::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
|
||||
}
|
||||
Reference in New Issue
Block a user