diff --git a/.gitignore b/.gitignore index 149136d..0f73929 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,11 @@ +# AI +.agents/ +.claude/ +.gemini/ +plans/ +GEMINI.md +CLAUDE.md + # VStudio stuff /.vs /.vscode diff --git a/CHANGELOG.md b/CHANGELOG.md index 80cd47a..0a4b2a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.2.8] - 2026-09-13 + +### Changed +- Incremented version to 0.2.8. +- **Breaking Change**: Conformed empty and whitespace-only document parsing to RFC 4627, RFC 7159, RFC 8259, and JSON5 specifications. Parsing empty strings, whitespace-only content, or empty files now returns `nullptr` (or throws `TJParseException` when `throw_exception = true`) instead of fabricating an empty `TJValueString`. +- `TJ::is_valid("")` now correctly returns `false`. + +### Fixed +- Fixed inconsistent behaviour where JSON5 returned `nullptr` on empty input while RFC 7159/8259 returned a `TJValueString`. + ## [0.2.7] - 2026-06-27 ### Added @@ -22,7 +32,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added warning callback trigger when attempting to retrieve non-existent keys in `TJValueObject` using `try_get_string`, `get_*(..)`, or `get<>(..)` methods. ### Changed -- Incremented version to 0.3.0. +- Incremented version to 0.2.8. ## [0.2.4] - 2026-05-20 diff --git a/README.md b/README.md index 936f419..d116e9e 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![CI](https://github.com/FFMG/TinyJSON/actions/workflows/c-cpp.yml/badge.svg)](https://github.com/FFMG/TinyJSON/actions/workflows/c-cpp.yml) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) -[![Version](https://img.shields.io/badge/version-0.2.7-blue.svg)](src/TinyJSON.h) +[![Version](https://img.shields.io/badge/version-0.2.8-blue.svg)](src/TinyJSON.h) A lightweight and lightning-fast C++ JSON & JSON5 parser designed for high performance and minimal footprint. @@ -131,8 +131,8 @@ The version is set in the `TinyJSON.h` file. ```cpp static const short TJ_VERSION_MAJOR = 0; static const short TJ_VERSION_MINOR = 2; -static const short TJ_VERSION_PATCH = 7; -static const char TJ_VERSION_STRING[] = "0.2.7"; +static const short TJ_VERSION_PATCH = 8; +static const char TJ_VERSION_STRING[] = "0.2.8"; ``` ### Simple Value Access diff --git a/src/TinyJSON.cpp b/src/TinyJSON.cpp index a9a00e1..45b32d5 100644 --- a/src/TinyJSON.cpp +++ b/src/TinyJSON.cpp @@ -4222,15 +4222,16 @@ namespace TinyJSON return nullptr; } - if (value_found == nullptr && parse_options.specification == parse_options::json5_1_0_0) + if (value_found == nullptr) { - // JSON5 requires a value - return nullptr; + parse_result.assign_exception_message(TJCHARPREFIX("The JSON text is empty or contains no value.")); + parse_result.throw_if_exception(); + return nullptr; } if (parse_options.specification == parse_options::rfc4627 && !parse_result.has_exception_message()) { - if (value_found == nullptr || (!value_found->is_array() && !value_found->is_object())) + if (!value_found->is_array() && !value_found->is_object()) { // error: RFC 4627: A JSON text must be either an object or an array. parse_result.assign_exception_message("RFC 4627: A JSON text must be either an object or an array."); @@ -4243,13 +4244,8 @@ namespace TinyJSON } // return if we found anything. - // if we found nothing ... then it is not an error, just an empty string parse_result.throw_if_exception(); - if (value_found == nullptr && parse_result.options().specification == parse_options::json5_1_0_0) - { - return nullptr; - } - return value_found != nullptr ? value_found : new TJValueString(TJCHARPREFIX("")); + return value_found; } /// diff --git a/src/TinyJSON.h b/src/TinyJSON.h index 1eb4ed4..3376abf 100644 --- a/src/TinyJSON.h +++ b/src/TinyJSON.h @@ -49,10 +49,11 @@ // v0.2.5 - added raise warning when key is not found. // v0.2.6 - fixed a bug in the resizing of string buffers. // v0.2.7 - improved parse_file error reporting with specific system errors and file name. +// v0.2.8 - conformed empty input parsing to RFC specifications (returns nullptr / throws instead of empty string). static const short TJ_VERSION_MAJOR = 0; static const short TJ_VERSION_MINOR = 2; -static const short TJ_VERSION_PATCH = 7; -static const char TJ_VERSION_STRING[] = "0.2.7"; +static const short TJ_VERSION_PATCH = 8; +static const char TJ_VERSION_STRING[] = "0.2.8"; #ifndef TJ_USE_CHAR # define TJ_USE_CHAR 1 diff --git a/tests/testtinyjson.cpp b/tests/testtinyjson.cpp index a8e02b2..8b9a756 100644 --- a/tests/testtinyjson.cpp +++ b/tests/testtinyjson.cpp @@ -352,16 +352,12 @@ TEST(TestBasic, NullInStringIsValid) { delete json; } -TEST(TestBasic, NothingIsJustAnEmptyString) { +TEST(TestBasic, EmptyStringReturnsNull) { auto json = TinyJSON::TJ::parse(""); - ASSERT_NE(nullptr, json); - ASSERT_TRUE(json->is_string()); - ASSERT_STREQ("", json->dump_string()); - - delete json; + ASSERT_EQ(nullptr, json); } -TEST(TestBasic, NothingIsJustAnEmptyStringWithSpaces) { +TEST(TestBasic, WhitespaceOnlyStringReturnsNull) { auto json = TinyJSON::TJ::parse(R"( @@ -369,12 +365,10 @@ TEST(TestBasic, NothingIsJustAnEmptyStringWithSpaces) { -)"); - ASSERT_NE(nullptr, json); - ASSERT_TRUE(json->is_string()); - ASSERT_STREQ("", json->dump_string()); - delete json; + +)"); + ASSERT_EQ(nullptr, json); } TEST(TestBasic, StringValueIsValid) { @@ -563,6 +557,14 @@ TEST(TestBasic, ArrayIsValid) { ASSERT_TRUE(TinyJSON::TJ::is_valid("[12,13,14]")); } +TEST(TestBasic, EmptyStringIsNotValid) { + ASSERT_FALSE(TinyJSON::TJ::is_valid("")); +} + +TEST(TestBasic, WhitespaceOnlyIsNotValid) { + ASSERT_FALSE(TinyJSON::TJ::is_valid(" \t\r\n ")); +} + TEST(TestBasic, UserLiteralsArray) { using namespace TinyJSON; diff --git a/tests/testtinyjsonexceptions.cpp b/tests/testtinyjsonexceptions.cpp index b6193bd..320151b 100644 --- a/tests/testtinyjsonexceptions.cpp +++ b/tests/testtinyjsonexceptions.cpp @@ -1,343 +1,457 @@ -// Licensed to Florent Guelfucci under one or more agreements. -// Florent Guelfucci licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. -#include -#define TJ_USE_CHAR 1 -#include "../src/TinyJSON.h" - -TEST(TestException, IfWeHaveNoExceptionWeDoNotThrow) { - TinyJSON::parse_options options = {}; - options.throw_exception = true; - TinyJSON::TJValue* json = nullptr; - EXPECT_NO_THROW( json = TinyJSON::TJ::parse("[12,13,14]", options)); - delete json; -} - -TEST(TestException, ParseExceptionMessageIsSetProperly) { - TinyJSON::parse_options options = {}; - options.throw_exception = true; - TinyJSON::TJValue* json = nullptr; - EXPECT_NO_THROW(json = TinyJSON::TJ::parse("[12,13,14]", options)); - delete json; -} - -TEST(TestException, SourceCannotBeNull) { - TinyJSON::TJParseException ex("Hello"); - EXPECT_STREQ("Hello", ex.what()); -} - -TEST(TestException, OperatorCopyParseException) { - TinyJSON::TJParseException ex1("Hello"); - TinyJSON::TJParseException ex2("World"); - ex2 = ex1; - EXPECT_STREQ("Hello", ex2.what()); -} - -TEST(TestException, CopyConstructorParseException) { - TinyJSON::TJParseException ex1("Hello"); - TinyJSON::TJParseException ex2(ex1); - EXPECT_STREQ("Hello", ex2.what()); -} - - -TEST(TestException, EscapedTabCharacterInString) { - TinyJSON::parse_options options = {}; - options.throw_exception = true; - EXPECT_THROW(TinyJSON::TJ::parse("[\"Tab\tin string\"]", options), TinyJSON::TJParseException); -} - -TEST(TestException, EscapedReturnCharacterInString) { - TinyJSON::parse_options options = {}; - options.throw_exception = true; - EXPECT_THROW(TinyJSON::TJ::parse("[\"Return\rin string\"]", options), TinyJSON::TJParseException); -} - -TEST(TestException, EscapedLineFeedCharacterInString) { - TinyJSON::parse_options options = {}; - options.throw_exception = true; - EXPECT_THROW(TinyJSON::TJ::parse("[\"Line Feed\nin string\"]", options), TinyJSON::TJParseException); -} - -TEST(TestException, EscapedFormFeedCharacterInString) { - TinyJSON::parse_options options = {}; - options.throw_exception = true; - EXPECT_THROW(TinyJSON::TJ::parse("[\"Form Feed\fin string\"]", options), TinyJSON::TJParseException); -} - -TEST(TestException, EscapedBackSpaceCharacterInString) { - TinyJSON::parse_options options = {}; - options.throw_exception = true; - EXPECT_THROW(TinyJSON::TJ::parse("[\"BackSpace\bin string\"]", options), TinyJSON::TJParseException); -} - -TEST(TestException, SingleEscapeCharacterInString) { - TinyJSON::parse_options options = {}; - options.throw_exception = true; - EXPECT_THROW(TinyJSON::TJ::parse("[\"Single Escape \\ \"]", options), TinyJSON::TJParseException); -} - -TEST(TestException, TheStringIsNotClosed) { - TinyJSON::parse_options options = {}; - options.throw_exception = true; - EXPECT_THROW(TinyJSON::TJ::parse("\"Not Closedd", options), TinyJSON::TJParseException); -} - -TEST(TestException, CommaAtTheEndOfObject) { - TinyJSON::parse_options options = {}; - options.throw_exception = true; - EXPECT_THROW(TinyJSON::TJ::parse("{\"a\" : 12,}", options), TinyJSON::TJParseException); -} - -TEST(TestException, CommaAtTheEndOfArray) { - TinyJSON::parse_options options = {}; - options.throw_exception = true; - EXPECT_THROW(TinyJSON::TJ::parse("[12,]", options), TinyJSON::TJParseException); -} - -TEST(TestException, UnExpectedCommaInObject) { - TinyJSON::parse_options options = {}; - options.throw_exception = true; - EXPECT_THROW(TinyJSON::TJ::parse("{,}", options), TinyJSON::TJParseException); -} - -TEST(TestException, UnExpectedCommaInArray) { - TinyJSON::parse_options options = {}; - options.throw_exception = true; - EXPECT_THROW(TinyJSON::TJ::parse("[,]", options), TinyJSON::TJParseException); -} - -TEST(TestException, MissingCommaBetweenItemsInObject) { - TinyJSON::parse_options options = {}; - options.throw_exception = true; - EXPECT_THROW(TinyJSON::TJ::parse("{\"a\" : 12 \"b\" : 12}", options), TinyJSON::TJParseException); -} - -TEST(TestException, MissingCommaBetweenItemsInArray) { - TinyJSON::parse_options options = {}; - options.throw_exception = true; - EXPECT_THROW(TinyJSON::TJ::parse("[12 \"a\"]", options), TinyJSON::TJParseException); -} - -TEST(TestException, UnExpectedCharacterInObject) { - TinyJSON::parse_options options = {}; - options.throw_exception = true; - EXPECT_THROW(TinyJSON::TJ::parse("{ % }", options), TinyJSON::TJParseException); -} - -TEST(TestException, UnExpectedEndOfStringWhileParsingObject) { - TinyJSON::parse_options options = {}; - options.throw_exception = true; - EXPECT_THROW(TinyJSON::TJ::parse("{", options), TinyJSON::TJParseException); -} - -TEST(TestException, UnExpectedEndOfStringWhileParsingArray) { - TinyJSON::parse_options options = {}; - options.throw_exception = true; - EXPECT_THROW(TinyJSON::TJ::parse("[", options), TinyJSON::TJParseException); -} - -TEST(TestException, BadTrue) { - TinyJSON::parse_options options = {}; - options.throw_exception = true; - EXPECT_THROW(TinyJSON::TJ::parse("[tru]", options), TinyJSON::TJParseException); -} - -TEST(TestException, BadFalse) { - TinyJSON::parse_options options = {}; - options.throw_exception = true; - EXPECT_THROW(TinyJSON::TJ::parse("[fals]", options), TinyJSON::TJParseException); -} - -TEST(TestException, BadNull) { - TinyJSON::parse_options options = {}; - options.throw_exception = true; - EXPECT_THROW(TinyJSON::TJ::parse("[nul]", options), TinyJSON::TJParseException); -} - -TEST(TestException, MissingColonInObject) { - TinyJSON::parse_options options = {}; - options.throw_exception = true; - EXPECT_THROW(TinyJSON::TJ::parse(R"({ "Missing colon" null })", options), TinyJSON::TJParseException); -} - -TEST(TestException, InvalidNumberWithLeadingZero) { - TinyJSON::parse_options options = {}; - options.throw_exception = true; - EXPECT_THROW(TinyJSON::TJ::parse("[0123]", options), TinyJSON::TJParseException); -} - -TEST(TestException, ExponentsWithZeroDoNotThrow) { - TinyJSON::parse_options options = {}; - options.throw_exception = true; - TinyJSON::TJValue* json = nullptr; - EXPECT_NO_THROW(json = TinyJSON::TJ::parse("[1e00]", options)); - delete json; -} - -TEST(TestException, WeReachedMaxDepthOfObjects) { - TinyJSON::parse_options options = {}; - options.throw_exception = true; - options.max_depth = 4; - TinyJSON::TJValue* json = nullptr; - EXPECT_THROW(json = TinyJSON::TJ::parse(R"({"a":{"b":{"c":{}}}})", options), TinyJSON::TJParseException); - delete json; -} - -TEST(TestException, WeReachedMaxDepthOfArrays) { - TinyJSON::parse_options options = {}; - options.throw_exception = true; - options.max_depth = 4; - TinyJSON::TJValue* json = nullptr; - EXPECT_THROW(json = TinyJSON::TJ::parse("[12,[13,[14,[]]]]", options), TinyJSON::TJParseException); - delete json; -} - -TEST(TestException, WeReachedMaxDepthMixed) { - TinyJSON::parse_options options = {}; - options.throw_exception = true; - options.max_depth = 4; - TinyJSON::TJValue* json = nullptr; - EXPECT_THROW(json = TinyJSON::TJ::parse(R"({"a":[12,{"c":{}}]})", options), TinyJSON::TJParseException); - delete json; -} - -TEST(TestException, Rfc4627WantsAnObjectOrAnArray) { - TinyJSON::parse_options options = {}; - options.throw_exception = true; - options.specification = TinyJSON::parse_options::rfc4627; - TinyJSON::TJValue* json = nullptr; - EXPECT_THROW(json = TinyJSON::TJ::parse("true", options), TinyJSON::TJParseException); - delete json; -} - -TEST(TestException, WeCannotHaveMoreThanOneItemInRoot) { - TinyJSON::parse_options options = {}; - options.throw_exception = true; - EXPECT_THROW(TinyJSON::TJ::parse("{},[]", options), TinyJSON::TJParseException); -} - -TEST(TestException, UnexpectedTokenWhileLookingForValue) { - TinyJSON::parse_options options = {}; - options.throw_exception = true; - EXPECT_THROW(TinyJSON::TJ::parse(R"({"a" : Value)", options), TinyJSON::TJParseException); -} - -TEST(TestException, ExponentDoesNotHaveANumber) { - TinyJSON::parse_options options = {}; - options.throw_exception = true; - EXPECT_THROW(TinyJSON::TJ::parse("[0e]", options), TinyJSON::TJParseException); -} - -TEST(TestException, ExponentDoesNotHaveANumberButHasNegativeSign) { - TinyJSON::parse_options options = {}; - options.throw_exception = true; - EXPECT_THROW(TinyJSON::TJ::parse("[0e-]", options), TinyJSON::TJParseException); -} - -TEST(TestException, ExponentDoesNotHaveANumberButHasPositiveSign) { - TinyJSON::parse_options options = {}; - options.throw_exception = true; - EXPECT_THROW(TinyJSON::TJ::parse("[0e+]", options), TinyJSON::TJParseException); -} - -TEST(TestException, FractionIsMissingNumberBeforExponent) { - TinyJSON::parse_options options = {}; - options.throw_exception = true; - EXPECT_THROW(TinyJSON::TJ::parse("12.e2", options), TinyJSON::TJParseException); -} - -TEST(TestException, FractionIsMissingNumber) { - TinyJSON::parse_options options = {}; - options.throw_exception = true; - EXPECT_THROW(TinyJSON::TJ::parse("12.", options), TinyJSON::TJParseException); -} - -TEST(TestException, EscapedTabCharacterInStringWillLogAndThrow) { - bool called = false; - TinyJSON::parse_options options = {}; - options.throw_exception = true; - options.callback_function = [&](TinyJSON::parse_options::message_type message_type, const TJCHAR* exception_message) { - EXPECT_EQ(message_type, TinyJSON::parse_options::fatal); - EXPECT_TRUE(nullptr != exception_message); - called = true; - }; - EXPECT_THROW(TinyJSON::TJ::parse("[\"Tab\tin string\"]", options), TinyJSON::TJParseException); - EXPECT_TRUE(called); -} - -TEST(TestException, EscapedTabCharacterInStringWillLogAndNotThrow) { - bool called = false; - TinyJSON::parse_options options = {}; - options.throw_exception = false; - options.callback_function = [&](TinyJSON::parse_options::message_type message_type, const TJCHAR* exception_message) { - EXPECT_EQ(message_type, TinyJSON::parse_options::fatal); - EXPECT_TRUE(nullptr != exception_message); - called = true; - }; - EXPECT_NO_THROW(TinyJSON::TJ::parse("[\"Tab\tin string\"]", options)); - EXPECT_TRUE(called); -} - -TEST(TestException, GettingNonExistentKeyWillLogAndNotThrow) { - bool called = false; - TinyJSON::parse_options options = {}; - options.throw_exception = false; - options.strict = true; - options.callback_function = [&](TinyJSON::parse_options::message_type message_type, const TJCHAR* exception_message) { - EXPECT_EQ(message_type, TinyJSON::parse_options::fatal); - EXPECT_STREQ(exception_message, "The key 'b' was not found!"); - called = true; - }; - TinyJSON::TJValue* json = TinyJSON::TJ::parse("{\"a\" : 12}", options); - ASSERT_TRUE(json != nullptr); - auto* object = static_cast(json); - - // try and get a non existent key - const TJCHAR* value = nullptr; - EXPECT_NO_THROW(value = object->get_string("b", true)); - EXPECT_TRUE(called); - EXPECT_STREQ(value, ""); - delete json; -} - -TEST(TestException, ParseFileNotFoundSmarterError) -{ - bool called = false; - std::string error_msg; - TinyJSON::parse_options options = {}; - options.throw_exception = false; - options.callback_function = [&](TinyJSON::parse_options::message_type message_type, const TJCHAR* exception_message) - { - if (message_type == TinyJSON::parse_options::error) - { - called = true; - error_msg = exception_message; - } - }; - TinyJSON::TJ::parse_file("this_file_does_not_exist_xyz.json", options); - EXPECT_TRUE(called); - EXPECT_EQ("File 'this_file_does_not_exist_xyz.json' could not be found!", error_msg); -} - -TEST(TestException, ParseDirectorySmarterError) -{ - bool called = false; - std::string error_msg; - TinyJSON::parse_options options = {}; - options.throw_exception = false; - options.callback_function = [&](TinyJSON::parse_options::message_type message_type, const TJCHAR* exception_message) - { - if (message_type == TinyJSON::parse_options::error) - { - called = true; - error_msg = exception_message; - } - }; - TinyJSON::TJ::parse_file(".", options); - EXPECT_TRUE(called); - EXPECT_NE(error_msg.find("."), std::string::npos); - bool matched = (error_msg == "File '.' is a directory!" || - error_msg == "Access denied to file '.'!" || - error_msg == "File '.' could not be opened!"); - EXPECT_TRUE(matched) << "Actual message: " << error_msg; -} \ No newline at end of file +// Licensed to Florent Guelfucci under one or more agreements. +// Florent Guelfucci licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. +#include +#include +#define TJ_USE_CHAR 1 +#include "../src/TinyJSON.h" + +TEST(TestException, IfWeHaveNoExceptionWeDoNotThrow) { + TinyJSON::parse_options options = {}; + options.throw_exception = true; + TinyJSON::TJValue* json = nullptr; + EXPECT_NO_THROW( json = TinyJSON::TJ::parse("[12,13,14]", options)); + delete json; +} + +TEST(TestException, ParseExceptionMessageIsSetProperly) { + TinyJSON::parse_options options = {}; + options.throw_exception = true; + TinyJSON::TJValue* json = nullptr; + EXPECT_NO_THROW(json = TinyJSON::TJ::parse("[12,13,14]", options)); + delete json; +} + +TEST(TestException, SourceCannotBeNull) { + TinyJSON::TJParseException ex("Hello"); + EXPECT_STREQ("Hello", ex.what()); +} + +TEST(TestException, OperatorCopyParseException) { + TinyJSON::TJParseException ex1("Hello"); + TinyJSON::TJParseException ex2("World"); + ex2 = ex1; + EXPECT_STREQ("Hello", ex2.what()); +} + +TEST(TestException, CopyConstructorParseException) { + TinyJSON::TJParseException ex1("Hello"); + TinyJSON::TJParseException ex2(ex1); + EXPECT_STREQ("Hello", ex2.what()); +} + + +TEST(TestException, EscapedTabCharacterInString) { + TinyJSON::parse_options options = {}; + options.throw_exception = true; + EXPECT_THROW(TinyJSON::TJ::parse("[\"Tab\tin string\"]", options), TinyJSON::TJParseException); +} + +TEST(TestException, EscapedReturnCharacterInString) { + TinyJSON::parse_options options = {}; + options.throw_exception = true; + EXPECT_THROW(TinyJSON::TJ::parse("[\"Return\rin string\"]", options), TinyJSON::TJParseException); +} + +TEST(TestException, EscapedLineFeedCharacterInString) { + TinyJSON::parse_options options = {}; + options.throw_exception = true; + EXPECT_THROW(TinyJSON::TJ::parse("[\"Line Feed\nin string\"]", options), TinyJSON::TJParseException); +} + +TEST(TestException, EscapedFormFeedCharacterInString) { + TinyJSON::parse_options options = {}; + options.throw_exception = true; + EXPECT_THROW(TinyJSON::TJ::parse("[\"Form Feed\fin string\"]", options), TinyJSON::TJParseException); +} + +TEST(TestException, EscapedBackSpaceCharacterInString) { + TinyJSON::parse_options options = {}; + options.throw_exception = true; + EXPECT_THROW(TinyJSON::TJ::parse("[\"BackSpace\bin string\"]", options), TinyJSON::TJParseException); +} + +TEST(TestException, SingleEscapeCharacterInString) { + TinyJSON::parse_options options = {}; + options.throw_exception = true; + EXPECT_THROW(TinyJSON::TJ::parse("[\"Single Escape \\ \"]", options), TinyJSON::TJParseException); +} + +TEST(TestException, TheStringIsNotClosed) { + TinyJSON::parse_options options = {}; + options.throw_exception = true; + EXPECT_THROW(TinyJSON::TJ::parse("\"Not Closedd", options), TinyJSON::TJParseException); +} + +TEST(TestException, CommaAtTheEndOfObject) { + TinyJSON::parse_options options = {}; + options.throw_exception = true; + EXPECT_THROW(TinyJSON::TJ::parse("{\"a\" : 12,}", options), TinyJSON::TJParseException); +} + +TEST(TestException, CommaAtTheEndOfArray) { + TinyJSON::parse_options options = {}; + options.throw_exception = true; + EXPECT_THROW(TinyJSON::TJ::parse("[12,]", options), TinyJSON::TJParseException); +} + +TEST(TestException, UnExpectedCommaInObject) { + TinyJSON::parse_options options = {}; + options.throw_exception = true; + EXPECT_THROW(TinyJSON::TJ::parse("{,}", options), TinyJSON::TJParseException); +} + +TEST(TestException, UnExpectedCommaInArray) { + TinyJSON::parse_options options = {}; + options.throw_exception = true; + EXPECT_THROW(TinyJSON::TJ::parse("[,]", options), TinyJSON::TJParseException); +} + +TEST(TestException, MissingCommaBetweenItemsInObject) { + TinyJSON::parse_options options = {}; + options.throw_exception = true; + EXPECT_THROW(TinyJSON::TJ::parse("{\"a\" : 12 \"b\" : 12}", options), TinyJSON::TJParseException); +} + +TEST(TestException, MissingCommaBetweenItemsInArray) { + TinyJSON::parse_options options = {}; + options.throw_exception = true; + EXPECT_THROW(TinyJSON::TJ::parse("[12 \"a\"]", options), TinyJSON::TJParseException); +} + +TEST(TestException, UnExpectedCharacterInObject) { + TinyJSON::parse_options options = {}; + options.throw_exception = true; + EXPECT_THROW(TinyJSON::TJ::parse("{ % }", options), TinyJSON::TJParseException); +} + +TEST(TestException, UnExpectedEndOfStringWhileParsingObject) { + TinyJSON::parse_options options = {}; + options.throw_exception = true; + EXPECT_THROW(TinyJSON::TJ::parse("{", options), TinyJSON::TJParseException); +} + +TEST(TestException, UnExpectedEndOfStringWhileParsingArray) { + TinyJSON::parse_options options = {}; + options.throw_exception = true; + EXPECT_THROW(TinyJSON::TJ::parse("[", options), TinyJSON::TJParseException); +} + +TEST(TestException, BadTrue) { + TinyJSON::parse_options options = {}; + options.throw_exception = true; + EXPECT_THROW(TinyJSON::TJ::parse("[tru]", options), TinyJSON::TJParseException); +} + +TEST(TestException, BadFalse) { + TinyJSON::parse_options options = {}; + options.throw_exception = true; + EXPECT_THROW(TinyJSON::TJ::parse("[fals]", options), TinyJSON::TJParseException); +} + +TEST(TestException, BadNull) { + TinyJSON::parse_options options = {}; + options.throw_exception = true; + EXPECT_THROW(TinyJSON::TJ::parse("[nul]", options), TinyJSON::TJParseException); +} + +TEST(TestException, MissingColonInObject) { + TinyJSON::parse_options options = {}; + options.throw_exception = true; + EXPECT_THROW(TinyJSON::TJ::parse(R"({ "Missing colon" null })", options), TinyJSON::TJParseException); +} + +TEST(TestException, InvalidNumberWithLeadingZero) { + TinyJSON::parse_options options = {}; + options.throw_exception = true; + EXPECT_THROW(TinyJSON::TJ::parse("[0123]", options), TinyJSON::TJParseException); +} + +TEST(TestException, ExponentsWithZeroDoNotThrow) { + TinyJSON::parse_options options = {}; + options.throw_exception = true; + TinyJSON::TJValue* json = nullptr; + EXPECT_NO_THROW(json = TinyJSON::TJ::parse("[1e00]", options)); + delete json; +} + +TEST(TestException, WeReachedMaxDepthOfObjects) { + TinyJSON::parse_options options = {}; + options.throw_exception = true; + options.max_depth = 4; + TinyJSON::TJValue* json = nullptr; + EXPECT_THROW(json = TinyJSON::TJ::parse(R"({"a":{"b":{"c":{}}}})", options), TinyJSON::TJParseException); + delete json; +} + +TEST(TestException, WeReachedMaxDepthOfArrays) { + TinyJSON::parse_options options = {}; + options.throw_exception = true; + options.max_depth = 4; + TinyJSON::TJValue* json = nullptr; + EXPECT_THROW(json = TinyJSON::TJ::parse("[12,[13,[14,[]]]]", options), TinyJSON::TJParseException); + delete json; +} + +TEST(TestException, WeReachedMaxDepthMixed) { + TinyJSON::parse_options options = {}; + options.throw_exception = true; + options.max_depth = 4; + TinyJSON::TJValue* json = nullptr; + EXPECT_THROW(json = TinyJSON::TJ::parse(R"({"a":[12,{"c":{}}]})", options), TinyJSON::TJParseException); + delete json; +} + +TEST(TestException, Rfc4627WantsAnObjectOrAnArray) { + TinyJSON::parse_options options = {}; + options.throw_exception = true; + options.specification = TinyJSON::parse_options::rfc4627; + TinyJSON::TJValue* json = nullptr; + EXPECT_THROW(json = TinyJSON::TJ::parse("true", options), TinyJSON::TJParseException); + delete json; +} + +TEST(TestException, WeCannotHaveMoreThanOneItemInRoot) { + TinyJSON::parse_options options = {}; + options.throw_exception = true; + EXPECT_THROW(TinyJSON::TJ::parse("{},[]", options), TinyJSON::TJParseException); +} + +TEST(TestException, UnexpectedTokenWhileLookingForValue) { + TinyJSON::parse_options options = {}; + options.throw_exception = true; + EXPECT_THROW(TinyJSON::TJ::parse(R"({"a" : Value)", options), TinyJSON::TJParseException); +} + +TEST(TestException, ExponentDoesNotHaveANumber) { + TinyJSON::parse_options options = {}; + options.throw_exception = true; + EXPECT_THROW(TinyJSON::TJ::parse("[0e]", options), TinyJSON::TJParseException); +} + +TEST(TestException, ExponentDoesNotHaveANumberButHasNegativeSign) { + TinyJSON::parse_options options = {}; + options.throw_exception = true; + EXPECT_THROW(TinyJSON::TJ::parse("[0e-]", options), TinyJSON::TJParseException); +} + +TEST(TestException, ExponentDoesNotHaveANumberButHasPositiveSign) { + TinyJSON::parse_options options = {}; + options.throw_exception = true; + EXPECT_THROW(TinyJSON::TJ::parse("[0e+]", options), TinyJSON::TJParseException); +} + +TEST(TestException, FractionIsMissingNumberBeforExponent) { + TinyJSON::parse_options options = {}; + options.throw_exception = true; + EXPECT_THROW(TinyJSON::TJ::parse("12.e2", options), TinyJSON::TJParseException); +} + +TEST(TestException, FractionIsMissingNumber) { + TinyJSON::parse_options options = {}; + options.throw_exception = true; + EXPECT_THROW(TinyJSON::TJ::parse("12.", options), TinyJSON::TJParseException); +} + +TEST(TestException, EscapedTabCharacterInStringWillLogAndThrow) { + bool called = false; + TinyJSON::parse_options options = {}; + options.throw_exception = true; + options.callback_function = [&](TinyJSON::parse_options::message_type message_type, const TJCHAR* exception_message) { + EXPECT_EQ(message_type, TinyJSON::parse_options::fatal); + EXPECT_TRUE(nullptr != exception_message); + called = true; + }; + EXPECT_THROW(TinyJSON::TJ::parse("[\"Tab\tin string\"]", options), TinyJSON::TJParseException); + EXPECT_TRUE(called); +} + +TEST(TestException, EscapedTabCharacterInStringWillLogAndNotThrow) { + bool called = false; + TinyJSON::parse_options options = {}; + options.throw_exception = false; + options.callback_function = [&](TinyJSON::parse_options::message_type message_type, const TJCHAR* exception_message) { + EXPECT_EQ(message_type, TinyJSON::parse_options::fatal); + EXPECT_TRUE(nullptr != exception_message); + called = true; + }; + EXPECT_NO_THROW(TinyJSON::TJ::parse("[\"Tab\tin string\"]", options)); + EXPECT_TRUE(called); +} + +TEST(TestException, GettingNonExistentKeyWillLogAndNotThrow) { + bool called = false; + TinyJSON::parse_options options = {}; + options.throw_exception = false; + options.strict = true; + options.callback_function = [&](TinyJSON::parse_options::message_type message_type, const TJCHAR* exception_message) { + EXPECT_EQ(message_type, TinyJSON::parse_options::fatal); + EXPECT_STREQ(exception_message, "The key 'b' was not found!"); + called = true; + }; + TinyJSON::TJValue* json = TinyJSON::TJ::parse("{\"a\" : 12}", options); + ASSERT_TRUE(json != nullptr); + auto* object = static_cast(json); + + // try and get a non existent key + const TJCHAR* value = nullptr; + EXPECT_NO_THROW(value = object->get_string("b", true)); + EXPECT_TRUE(called); + EXPECT_STREQ(value, ""); + delete json; +} + +TEST(TestException, ParseFileNotFoundSmarterError) +{ + bool called = false; + std::string error_msg; + TinyJSON::parse_options options = {}; + options.throw_exception = false; + options.callback_function = [&](TinyJSON::parse_options::message_type message_type, const TJCHAR* exception_message) + { + if (message_type == TinyJSON::parse_options::error) + { + called = true; + error_msg = exception_message; + } + }; + TinyJSON::TJ::parse_file("this_file_does_not_exist_xyz.json", options); + EXPECT_TRUE(called); + EXPECT_EQ("File 'this_file_does_not_exist_xyz.json' could not be found!", error_msg); +} + +TEST(TestException, ParseDirectorySmarterError) +{ + bool called = false; + std::string error_msg; + TinyJSON::parse_options options = {}; + options.throw_exception = false; + options.callback_function = [&](TinyJSON::parse_options::message_type message_type, const TJCHAR* exception_message) + { + if (message_type == TinyJSON::parse_options::error) + { + called = true; + error_msg = exception_message; + } + }; + TinyJSON::TJ::parse_file(".", options); + EXPECT_TRUE(called); + EXPECT_NE(error_msg.find("."), std::string::npos); + bool matched = (error_msg == "File '.' is a directory!" || + error_msg == "Access denied to file '.'!" || + error_msg == "File '.' could not be opened!"); + EXPECT_TRUE(matched) << "Actual message: " << error_msg; +} + +namespace +{ + struct empty_string_callback_tracker + { + bool called = false; + std::string error_msg; + + void operator()(TinyJSON::parse_options::message_type message_type, const TJCHAR* exception_message) + { + if (message_type == TinyJSON::parse_options::fatal || message_type == TinyJSON::parse_options::error) + { + called = true; + if (nullptr != exception_message) + { + error_msg = exception_message; + } + } + } + }; +} + +TEST(TestException, EmptyStringThrowsException) +{ + TinyJSON::parse_options options = {}; + options.throw_exception = true; + EXPECT_THROW(TinyJSON::TJ::parse("", options), TinyJSON::TJParseException); +} + +TEST(TestException, WhitespaceOnlyStringThrowsException) +{ + TinyJSON::parse_options options = {}; + options.throw_exception = true; + EXPECT_THROW(TinyJSON::TJ::parse(" \t\r\n ", options), TinyJSON::TJParseException); +} + +TEST(TestException, EmptyStringThrowsAcrossAllSpecifications) +{ + const enum TinyJSON::parse_options::specification specs[] = { + TinyJSON::parse_options::rfc4627, + TinyJSON::parse_options::rfc7159, + TinyJSON::parse_options::rfc8259, + TinyJSON::parse_options::json5_1_0_0 + }; + + for (const auto spec : specs) + { + TinyJSON::parse_options options = {}; + options.throw_exception = true; + options.specification = spec; + EXPECT_THROW(TinyJSON::TJ::parse("", options), TinyJSON::TJParseException); + } +} + +TEST(TestException, EmptyStringReturnsNullAcrossAllSpecifications) +{ + const enum TinyJSON::parse_options::specification specs[] = { + TinyJSON::parse_options::rfc4627, + TinyJSON::parse_options::rfc7159, + TinyJSON::parse_options::rfc8259, + TinyJSON::parse_options::json5_1_0_0 + }; + + for (const auto spec : specs) + { + TinyJSON::parse_options options = {}; + options.throw_exception = false; + options.specification = spec; + auto* json = TinyJSON::TJ::parse("", options); + EXPECT_EQ(nullptr, json); + } +} + +TEST(TestException, EmptyStringCallbackMessage) +{ + empty_string_callback_tracker tracker; + TinyJSON::parse_options options = {}; + options.throw_exception = false; + options.callback_function = std::ref(tracker); + auto* json = TinyJSON::TJ::parse("", options); + EXPECT_EQ(nullptr, json); + EXPECT_TRUE(tracker.called); + EXPECT_EQ("The JSON text is empty or contains no value.", tracker.error_msg); +} + +TEST(TestException, EmptyFileThrowsException) +{ + const char* empty_filename = "test_empty_file_throws.json"; + { + std::ofstream empty_file(empty_filename); + } + + TinyJSON::parse_options options = {}; + options.throw_exception = true; + EXPECT_THROW(TinyJSON::TJ::parse_file(empty_filename, options), TinyJSON::TJParseException); + + std::remove(empty_filename); +} + +TEST(TestException, EmptyFileReturnsNullWithoutException) +{ + const char* empty_filename = "test_empty_file_null.json"; + { + std::ofstream empty_file(empty_filename); + } + + TinyJSON::parse_options options = {}; + options.throw_exception = false; + auto* json = TinyJSON::TJ::parse_file(empty_filename, options); + EXPECT_EQ(nullptr, json); + + std::remove(empty_filename); +} diff --git a/tests/testtinyjsonversion.cpp b/tests/testtinyjsonversion.cpp index 1c85f6a..ea2abee 100644 --- a/tests/testtinyjsonversion.cpp +++ b/tests/testtinyjsonversion.cpp @@ -17,10 +17,10 @@ TEST(TestVersion, CheckVersionMinor) TEST(TestVersion, CheckVersionPatch) { - ASSERT_EQ(7, TJ_VERSION_PATCH); + ASSERT_EQ(8, TJ_VERSION_PATCH); } TEST(TestVersion, CheckVersionString) { - ASSERT_STREQ("0.2.7", TJ_VERSION_STRING); + ASSERT_STREQ("0.2.8", TJ_VERSION_STRING); }