Add Exceptions indicating which line contains error when parsing JSON

This commit is contained in:
Vyn 2024-11-29 10:50:49 +01:00
parent fd034eff71
commit 2639dba60a
9 changed files with 307 additions and 177 deletions

View file

@ -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)

View file

@ -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);

View file

@ -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);

View file

@ -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);
}

View file

@ -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;

View file

@ -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;

View file

@ -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
View 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 '\"'");
}
}

View file

@ -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);