mirror of
https://codeberg.org/vyn/rei-json.git
synced 2025-07-01 09:33:19 +00:00
Add Exceptions indicating which line contains error when parsing JSON
This commit is contained in:
parent
fd034eff71
commit
2639dba60a
9 changed files with 307 additions and 177 deletions
|
@ -21,7 +21,7 @@ Include(FetchContent)
|
|||
FetchContent_Declare(
|
||||
Catch2
|
||||
GIT_REPOSITORY https://github.com/catchorg/Catch2.git
|
||||
GIT_TAG v3.4.0 # or a later release
|
||||
GIT_TAG v3.7.1 # or a later release
|
||||
)
|
||||
|
||||
FetchContent_MakeAvailable(Catch2)
|
||||
|
@ -30,6 +30,7 @@ add_executable(tests
|
|||
tests/usage.cpp
|
||||
tests/parsing.cpp
|
||||
tests/stringify.cpp
|
||||
tests/errors.cpp
|
||||
)
|
||||
target_include_directories(tests PRIVATE "include")
|
||||
target_link_libraries(tests PRIVATE rei-json)
|
||||
|
|
|
@ -14,6 +14,8 @@ namespace rei::json {
|
|||
|
||||
JsonArray();
|
||||
|
||||
JsonArray& push(FieldValue& value);
|
||||
JsonArray& push(FieldValue&& value);
|
||||
JsonArray& push(int value);
|
||||
JsonArray& push(bool value);
|
||||
JsonArray& push(const std::string& value);
|
||||
|
|
|
@ -16,6 +16,8 @@ namespace rei::json {
|
|||
|
||||
bool contains(const std::string& key) const;
|
||||
|
||||
JsonObject& set(const std::string& key, FieldValue& value);
|
||||
JsonObject& set(const std::string& key, FieldValue&& value);
|
||||
JsonObject& set(const std::string& key, int value);
|
||||
JsonObject& set(const std::string& key, bool value);
|
||||
JsonObject& set(const std::string& key, const std::string& value);
|
||||
|
|
|
@ -2,13 +2,27 @@
|
|||
#include "Object.h"
|
||||
#include "Array.h"
|
||||
#include "Field.h"
|
||||
#include <exception>
|
||||
#include <format>
|
||||
#include <string>
|
||||
#include <variant>
|
||||
namespace rei::json {
|
||||
class std::variant<JsonObject, JsonArray> parse(const std::string& jsonStr);
|
||||
|
||||
std::string toString(std::variant<JsonObject, JsonArray>& element);
|
||||
class ParsingError : public std::exception {
|
||||
public:
|
||||
ParsingError(const std::string& reason, int line, int column) {
|
||||
str = std::format("{}:{}: {}", line, column, reason);
|
||||
}
|
||||
|
||||
const char * what() const noexcept override {
|
||||
return str.c_str();
|
||||
}
|
||||
|
||||
private:
|
||||
std::string str;
|
||||
};
|
||||
|
||||
class FieldValue parse(const std::string& jsonStr);
|
||||
|
||||
std::string toString(JsonObject& object);
|
||||
std::string toString(JsonArray& object);
|
||||
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
#include <print>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
namespace rei::json {
|
||||
|
||||
JsonArray::JsonArray() {
|
||||
|
@ -23,6 +24,16 @@ namespace rei::json {
|
|||
return *this;
|
||||
}
|
||||
|
||||
JsonArray& JsonArray::push(FieldValue& value) {
|
||||
elements.push_back(value);
|
||||
return *this;
|
||||
}
|
||||
|
||||
JsonArray& JsonArray::push(FieldValue&& value) {
|
||||
elements.push_back(std::move(value));
|
||||
return *this;
|
||||
}
|
||||
|
||||
JsonArray& JsonArray::push(bool value) {
|
||||
elements.push_back(value);
|
||||
return *this;
|
||||
|
|
|
@ -24,6 +24,16 @@ namespace rei::json {
|
|||
return elements.contains(key);
|
||||
}
|
||||
|
||||
JsonObject& JsonObject::set(const std::string& key, FieldValue& value) {
|
||||
elements.insert_or_assign(key, value);
|
||||
return *this;
|
||||
}
|
||||
|
||||
JsonObject& JsonObject::set(const std::string& key, FieldValue&& value) {
|
||||
elements.insert_or_assign(key, std::move(value));
|
||||
return *this;
|
||||
}
|
||||
|
||||
JsonObject& JsonObject::set(const std::string& key, int value) {
|
||||
elements.insert_or_assign(key, value);
|
||||
return *this;
|
||||
|
|
349
src/parse.cpp
349
src/parse.cpp
|
@ -1,202 +1,209 @@
|
|||
#include "rei-json/Array.h"
|
||||
#include "rei-json/Field.h"
|
||||
#include "rei-json/json.h"
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <cctype>
|
||||
#include <charconv>
|
||||
#include <chrono>
|
||||
#include <memory>
|
||||
#include <ostream>
|
||||
#include <print>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <variant>
|
||||
namespace rei::json {
|
||||
#include <utility>
|
||||
|
||||
void goToNextDoubleQuote(const std::string& str, int* const i) {
|
||||
while (str[*i] != '"') {
|
||||
if (str[*i] == '\\') {
|
||||
++(*i);
|
||||
}
|
||||
++(*i);
|
||||
}
|
||||
}
|
||||
namespace rei::json {
|
||||
|
||||
void skipDigits(const std::string& str, int* const i) {
|
||||
while (isdigit(str[*i]) || str[*i] == '-') {
|
||||
++(*i);
|
||||
}
|
||||
}
|
||||
class StringParser {
|
||||
|
||||
void skipWhiteSpaces(const std::string& str, int* const i) {
|
||||
while (str[*i] == ' ' || str[*i] == '\t' || str[*i] == '\n' || str[*i] == '\v') {
|
||||
++(*i);
|
||||
}
|
||||
}
|
||||
public:
|
||||
|
||||
void parseObject(const std::string &str, int *i, JsonObject* object);
|
||||
void parseArray(const std::string &str, int *i, JsonArray* object);
|
||||
StringParser(const std::string& str) : str(str) { }
|
||||
|
||||
void parseObjectField(const std::string &str, int *i, JsonObject* object) {
|
||||
if (str[*i] != '"') {
|
||||
throw "wrong json: expected opening quote";
|
||||
}
|
||||
++(*i);
|
||||
const int keyStart = *i;
|
||||
goToNextDoubleQuote(str, i);
|
||||
const int keyLength = *i - keyStart;
|
||||
const std::string key = str.substr(keyStart, keyLength);
|
||||
++(*i);
|
||||
skipWhiteSpaces(str, i);
|
||||
if (str[*i] != ':') {
|
||||
throw "wrong json: expect 2 dots";
|
||||
}
|
||||
++(*i);
|
||||
skipWhiteSpaces(str, i);
|
||||
|
||||
if (str[*i] == '"') {
|
||||
++(*i);
|
||||
const int valueStart = *i;
|
||||
goToNextDoubleQuote(str, i);
|
||||
const int valueLength = *i - valueStart;
|
||||
const std::string valueStr = str.substr(valueStart, valueLength);
|
||||
object->set(key, std::move(valueStr));
|
||||
++(*i);
|
||||
} else if (isdigit(str[*i]) || str[*i] == '-') {
|
||||
const int valueStart = *i;
|
||||
skipDigits(str, i);
|
||||
const int valueLength = *i - valueStart;
|
||||
const std::string valueStr = str.substr(valueStart, valueLength);
|
||||
int value;
|
||||
std::from_chars(str.c_str() + valueStart, str.c_str() + valueStart + valueLength, value);
|
||||
object->set(key, value);
|
||||
} else if (std::string_view{str}.substr(*i, 4) == "true") {
|
||||
const int valueStart = *i;
|
||||
*i += 4;
|
||||
const int valueLength = *i - valueStart;
|
||||
object->set(key, true);
|
||||
} else if (std::string_view{str}.substr(*i, 5) == "false") {
|
||||
const int valueStart = *i;
|
||||
*i += 5;
|
||||
const int valueLength = *i - valueStart;
|
||||
object->set(key, false);
|
||||
} else if (std::string_view{str}.substr(*i, 4) == "null") {
|
||||
*i += 4;
|
||||
object->setNull(key);
|
||||
} else if (str[*i] == '{'){
|
||||
JsonObject newObject{};
|
||||
parseObject(str, i, &newObject);
|
||||
object->set(key, std::move(newObject));
|
||||
} else if (str[*i] == '['){
|
||||
JsonArray newArray{};
|
||||
parseArray(str, i, &newArray);
|
||||
object->set(key, std::move(newArray));
|
||||
}
|
||||
}
|
||||
|
||||
void parseArrayField(const std::string &str, int *i, JsonArray* array) {
|
||||
skipWhiteSpaces(str, i);
|
||||
if (str[*i] == '"') {
|
||||
++(*i);
|
||||
const int valueStart = *i;
|
||||
goToNextDoubleQuote(str, i);
|
||||
const int valueLength = *i - valueStart;
|
||||
const std::string valueStr = str.substr(valueStart, valueLength);
|
||||
array->push(std::move(valueStr));
|
||||
++(*i);
|
||||
} else if (isdigit(str[*i])) {
|
||||
const int valueStart = *i;
|
||||
skipDigits(str, i);
|
||||
const int valueLength = *i - valueStart;
|
||||
const std::string valueStr = str.substr(valueStart, valueLength);
|
||||
array->push(std::stoi(valueStr));
|
||||
} else if (std::string_view{str}.substr(*i, 4) == "true") {
|
||||
const int valueStart = *i;
|
||||
*i += 4;
|
||||
const int valueLength = *i - valueStart;
|
||||
array->push(true);
|
||||
} else if (std::string_view{str}.substr(*i, 4) == "null") {
|
||||
*i += 4;
|
||||
array->pushNull();
|
||||
} else if (std::string_view{str}.substr(*i, 5) == "false") {
|
||||
const int valueStart = *i;
|
||||
*i += 5;
|
||||
const int valueLength = *i - valueStart;
|
||||
array->push(false);
|
||||
} else if (str[*i] == '{'){
|
||||
JsonObject newObject{};
|
||||
parseObject(str, i, &newObject);
|
||||
array->push(std::move(newObject));
|
||||
} else if (str[*i] == '['){
|
||||
JsonArray newArray{};
|
||||
parseArray(str, i, &newArray);
|
||||
array->push(std::move(newArray));
|
||||
int currentLine() const {
|
||||
return line;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void parseArray(const std::string &str, int *i, JsonArray* object) {
|
||||
if (str[*i] != '[') {
|
||||
throw "wrong json";
|
||||
int currentColumn() const {
|
||||
return i - lineStartingIndex;
|
||||
}
|
||||
++(*i);
|
||||
skipWhiteSpaces(str, i);
|
||||
while (str[*i] != ']') {
|
||||
parseArrayField(str, i, object);
|
||||
skipWhiteSpaces(str, i);
|
||||
if (str[*i] == ',') {
|
||||
++(*i);
|
||||
skipWhiteSpaces(str, i);
|
||||
if (str[*i] == ']') {
|
||||
throw "wrong json: extra comma on last array field";
|
||||
|
||||
const char& current() const {
|
||||
return str[i];
|
||||
}
|
||||
|
||||
bool is(const char& c) const {
|
||||
return str[i] == c;
|
||||
}
|
||||
|
||||
bool match(const char* c, int length) const {
|
||||
return std::string_view{str}.substr(i, length) == c;
|
||||
}
|
||||
|
||||
bool isDigit() const {
|
||||
return isdigit(str[i]);
|
||||
}
|
||||
|
||||
void skipWhitespaces() {
|
||||
while (str[i] == ' ' || str[i] == '\t' || str[i] == '\n' || str[i] == '\v') {
|
||||
if (str[i] == '\n') {
|
||||
++line;
|
||||
lineStartingIndex = i + 1;
|
||||
}
|
||||
} else if (str[*i] != ']') {
|
||||
throw "wrong json: expected array end";
|
||||
++i;
|
||||
}
|
||||
}
|
||||
++(*i);
|
||||
}
|
||||
|
||||
void parseObject(const std::string &str, int *i, JsonObject* object) {
|
||||
if (str[*i] != '{') {
|
||||
throw "wrong json";
|
||||
}
|
||||
++(*i);
|
||||
skipWhiteSpaces(str, i);
|
||||
while (str[*i] != '}') {
|
||||
if (str[*i] != '"') {
|
||||
throw "wrong json: expect opening key quote";
|
||||
}
|
||||
parseObjectField(str, i, object);
|
||||
skipWhiteSpaces(str, i);
|
||||
if (str[*i] == ',') {
|
||||
++(*i);
|
||||
skipWhiteSpaces(str, i);
|
||||
if (str[*i] == '}') {
|
||||
throw "wrong json: extra comma on last object field";
|
||||
|
||||
void goToNextDoubleQuote() {
|
||||
while (str[i] != '"') {
|
||||
if (str[i] == '\\') {
|
||||
++(i);
|
||||
}
|
||||
} else if (str[*i] != '}') {
|
||||
const std::string sstr = str.substr(*i - 1);
|
||||
throw "wrong json: expected object end";
|
||||
++(i);
|
||||
}
|
||||
}
|
||||
++(*i);
|
||||
|
||||
void skipDigits() {
|
||||
while (isdigit(str[i]) || str[i] == '-') {
|
||||
++(i);
|
||||
}
|
||||
}
|
||||
|
||||
void nextChar(int count = 1) {
|
||||
i += count;
|
||||
}
|
||||
|
||||
int currentIndex() const {
|
||||
return i;
|
||||
}
|
||||
|
||||
std::string substr(int start, int length) const {
|
||||
return str.substr(start, length);
|
||||
}
|
||||
|
||||
const char *c_str() const {
|
||||
return str.c_str();
|
||||
}
|
||||
|
||||
FieldValue parseField(StringParser& parser) {
|
||||
if (parser.is('"')) {
|
||||
parser.nextChar();
|
||||
const int valueStart = parser.currentIndex();
|
||||
parser.goToNextDoubleQuote();
|
||||
const int valueLength = parser.currentIndex() - valueStart;
|
||||
parser.nextChar();
|
||||
return FieldValue{parser.substr(valueStart, valueLength)};
|
||||
} else if (parser.isDigit() || parser.is('-')) {
|
||||
const int valueStart = parser.currentIndex();
|
||||
parser.skipDigits();
|
||||
const int valueLength = parser.currentIndex() - valueStart;
|
||||
int value;
|
||||
std::from_chars(parser.c_str() + valueStart, parser.c_str() + valueStart + valueLength, value);
|
||||
return FieldValue{value};
|
||||
} else if (parser.match("true", 4)) {
|
||||
parser.nextChar(4);
|
||||
return FieldValue{true};
|
||||
} else if (parser.match("false", 5)) {
|
||||
parser.nextChar(5);
|
||||
return FieldValue{false};
|
||||
} else if (parser.match("null", 4)) {
|
||||
parser.nextChar(4);
|
||||
return FieldValue{FieldType::Null};
|
||||
} else if (parser.is('{')){
|
||||
JsonObject newObject{};
|
||||
parseObject(parser, &newObject);
|
||||
return FieldValue{std::move(newObject)};
|
||||
} else if (parser.is('[')){
|
||||
JsonArray newArray{};
|
||||
parseArray(parser, &newArray);
|
||||
return FieldValue{std::move(newArray)};
|
||||
}
|
||||
throw ParsingError("Invalid JSON value", currentLine(), currentColumn());
|
||||
}
|
||||
|
||||
void parseObjectField(StringParser& parser, JsonObject* object) {
|
||||
assert(parser.is('"'));
|
||||
parser.nextChar();
|
||||
const int keyStart = parser.currentIndex();
|
||||
parser.goToNextDoubleQuote();
|
||||
const int keyLength = parser.currentIndex() - keyStart;
|
||||
const std::string key = parser.substr(keyStart, keyLength);
|
||||
parser.nextChar();
|
||||
parser.skipWhitespaces();
|
||||
if (!parser.is(':')) {
|
||||
throw ParsingError("Expected ':'", currentLine(), currentColumn());
|
||||
}
|
||||
parser.nextChar();
|
||||
parser.skipWhitespaces();
|
||||
object->set(key, parseField(parser));
|
||||
}
|
||||
|
||||
void parseArrayField(StringParser& parser, JsonArray* array) {
|
||||
parser.skipWhitespaces();
|
||||
array->push(parseField(parser));
|
||||
}
|
||||
|
||||
void parseArray(StringParser& parser, JsonArray* object) {
|
||||
assert(parser.is('['));
|
||||
parser.nextChar();
|
||||
parser.skipWhitespaces();
|
||||
while (!parser.is(']')) {
|
||||
parseArrayField(parser, object);
|
||||
parser.skipWhitespaces();
|
||||
if (parser.is(',')) {
|
||||
parser.nextChar();
|
||||
parser.skipWhitespaces();
|
||||
if (parser.is(']')) {
|
||||
throw ParsingError("Found extra comma (',')", currentLine(), currentColumn());
|
||||
}
|
||||
} else if (!parser.is(']')) {
|
||||
throw ParsingError("Expected comma (',') or array ending (']')", currentLine(), currentColumn());
|
||||
}
|
||||
}
|
||||
parser.nextChar();
|
||||
}
|
||||
|
||||
}
|
||||
void parseObject(StringParser& parser, JsonObject* object) {
|
||||
assert(parser.is('{'));
|
||||
parser.nextChar();
|
||||
parser.skipWhitespaces();
|
||||
while (!parser.is('}')) {
|
||||
if (!parser.is('"')) {
|
||||
throw ParsingError("Expected '\"'", currentLine(), currentColumn());
|
||||
}
|
||||
parseObjectField(parser, object);
|
||||
parser.skipWhitespaces();
|
||||
if (parser.is(',')) {
|
||||
parser.nextChar();
|
||||
parser.skipWhitespaces();
|
||||
if (parser.is('}')) {
|
||||
throw ParsingError("Found extra comma (',')", currentLine(), currentColumn());
|
||||
}
|
||||
} else if (!parser.is('}')) {
|
||||
throw ParsingError("Expected comma (',') or object ending ('}')", currentLine(), currentColumn());
|
||||
}
|
||||
}
|
||||
parser.nextChar();
|
||||
}
|
||||
|
||||
std::variant<JsonObject, JsonArray> parse(const std::string& jsonStr) {
|
||||
int i = 0;
|
||||
skipWhiteSpaces(jsonStr, &i);
|
||||
if (jsonStr[i] == '{') {
|
||||
private:
|
||||
|
||||
unsigned i = 0;
|
||||
unsigned line = 1;
|
||||
unsigned lineStartingIndex = 1;
|
||||
const std::string str;
|
||||
};
|
||||
|
||||
FieldValue parse(const std::string& jsonStr) {
|
||||
StringParser parser{jsonStr};
|
||||
|
||||
parser.skipWhitespaces();
|
||||
if (parser.is('{')) {
|
||||
JsonObject object{};
|
||||
parseObject(jsonStr, &i, &object);
|
||||
parser.parseObject(parser, &object);
|
||||
return std::move(object);
|
||||
} else if (jsonStr[i] == '[') {
|
||||
} else if (parser.is('[')) {
|
||||
JsonArray array{};
|
||||
parseArray(jsonStr, &i, &array);
|
||||
parser.parseArray(parser, &array);
|
||||
return std::move(array);
|
||||
}
|
||||
throw std::runtime_error("Bad json syntax");
|
||||
throw ParsingError("Expected JSON object or array", parser.currentLine(), parser.currentColumn());
|
||||
}
|
||||
}
|
||||
|
|
83
tests/errors.cpp
Normal file
83
tests/errors.cpp
Normal file
|
@ -0,0 +1,83 @@
|
|||
#include "catch2/catch_test_macros.hpp"
|
||||
#include "catch2/matchers/catch_matchers.hpp"
|
||||
#include "rei-json/json.h"
|
||||
#include <catch2/catch_all.hpp>
|
||||
#include <exception>
|
||||
#include <print>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
TEST_CASE("Parsing errors") {
|
||||
SECTION("Root object") {
|
||||
std::string jsonStr = R"(
|
||||
"keyArray": [
|
||||
42,
|
||||
"elemString",
|
||||
"",
|
||||
true,
|
||||
false,
|
||||
null
|
||||
],
|
||||
"keyBooleanFalse": false,
|
||||
"keyBooleanTrue": true,
|
||||
"keyEmptyString": "",
|
||||
"keyNegativeNumber": -13,
|
||||
"keyNull": null,
|
||||
"keyObject": {
|
||||
"keyNumberOnObject": 42
|
||||
},
|
||||
"keyPositiveNumber": 12,
|
||||
"keyString": "YEP"
|
||||
})";
|
||||
REQUIRE_THROWS_WITH(rei::json::parse(jsonStr), "2:1: Expected JSON object or array");
|
||||
}
|
||||
|
||||
SECTION("Missing comma") {
|
||||
std::string jsonStr = R"({
|
||||
"keyArray": [
|
||||
42,
|
||||
"elemString",
|
||||
"",
|
||||
true,
|
||||
false,
|
||||
null
|
||||
],
|
||||
"keyBooleanFalse": false,
|
||||
"keyBooleanTrue": true
|
||||
"keyEmptyString": "",
|
||||
"keyNegativeNumber": -13,
|
||||
"keyNull": null,
|
||||
"keyObject": {
|
||||
"keyNumberOnObject": 42
|
||||
},
|
||||
"keyPositiveNumber": 12,
|
||||
"keyString": "YEP"
|
||||
})";
|
||||
REQUIRE_THROWS_WITH(rei::json::parse(jsonStr), "12:1: Expected comma (',') or object ending ('}')");
|
||||
}
|
||||
|
||||
SECTION("Missing object key quote") {
|
||||
std::string jsonStr = R"({
|
||||
"keyArray": [
|
||||
42,
|
||||
"elemString",
|
||||
"",
|
||||
true,
|
||||
false,
|
||||
null
|
||||
],
|
||||
"keyBooleanFalse": false,
|
||||
keyBooleanTrue": true,
|
||||
"keyEmptyString": "",
|
||||
"keyNegativeNumber": -13,
|
||||
"keyNull": null,
|
||||
"keyObject": {
|
||||
"keyNumberOnObject": 42
|
||||
},
|
||||
"keyPositiveNumber": 12,
|
||||
"keyString": "YEP"
|
||||
})";
|
||||
REQUIRE_THROWS_WITH(rei::json::parse(jsonStr), "11:1: Expected '\"'");
|
||||
}
|
||||
}
|
|
@ -25,7 +25,7 @@ TEST_CASE("Parsing json object") {
|
|||
"keyString": "YEP"
|
||||
})";
|
||||
auto json = rei::json::parse(jsonStr);
|
||||
auto& objectJson = std::get<rei::json::JsonObject>(json);
|
||||
auto& objectJson = json.asObject();
|
||||
|
||||
REQUIRE(objectJson.getNumber("keyPositiveNumber") == 12);
|
||||
REQUIRE(objectJson.getNumber("keyNegativeNumber") == -13);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue