diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f840ea..ab2c9d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ 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.5] - 2026-06-11 + +### Added +- 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. + ## [0.2.4] - 2026-05-20 ### Added diff --git a/README.md b/README.md index 0e57278..420b151 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.4-blue.svg)](src/TinyJSON.h) +[![Version](https://img.shields.io/badge/version-0.2.5-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 = 4; -static const char TJ_VERSION_STRING[] = "0.2.4"; +static const short TJ_VERSION_PATCH = 5; +static const char TJ_VERSION_STRING[] = "0.2.5"; ``` ### Simple Value Access @@ -169,6 +169,7 @@ if (tj) { * json5_1_0_0 * Callback: (`callback_function:std::function`) Callback function called where there is an error, warning etc. NB: The Callback function is called even if Throw is false. + * If a getter method (`try_get_string`, `get_*(..)`, or `get<>(..)`) is called on a key that does not exist in a `TJValueObject`, a warning message indicating the missing key (e.g. `"The key 'missing' was not found!"`) is passed to the callback function. * trace * debug * info @@ -673,7 +674,7 @@ auto doubles = obj->get>("floats"); #### Strict Get values -Getter methods on `TJValue` (or through `TJValueObject` keys) can be used to retrieve specific types. By default, these methods are non-strict and return default values if the type is incorrect. By setting `strict: true` in `parse_options`, these methods will throw a `TJParseException` instead. +Getter methods on `TJValue` (or through `TJValueObject` keys) can be used to retrieve specific types. By default, these methods are non-strict and return default values if the type is incorrect. By setting `strict: true` in `parse_options`, these methods will throw a `TJParseException` instead. If a key is not found when calling `try_get_string`, `get_*(..)`, or `get<>(..)`, a warning callback is triggered with a message indicating the key that was not found (e.g. `"The key 'missing' was not found!"`) if a callback function is registered in `parse_options`. - get_number() - get_float() diff --git a/src/TinyJSON.cpp b/src/TinyJSON.cpp index 624fc3b..7cc2a67 100644 --- a/src/TinyJSON.cpp +++ b/src/TinyJSON.cpp @@ -5396,16 +5396,68 @@ namespace TinyJSON return object; } + void TJValueObject::raise_key_not_found(const TJCHAR* key, bool is_strict) const + { + if (key == nullptr) + { + return; + } + int key_len = 0; + while (key[key_len] != 0) + { + key_len++; + } + int total_len = 9 + key_len + 17; + TJCHAR* msg = new TJCHAR[total_len + 1]; + + const TJCHAR* prefix = TJCHARPREFIX("The key '"); + for (int i = 0; i < 9; ++i) + { + msg[i] = prefix[i]; + } + + for (int i = 0; i < key_len; ++i) + { + msg[9 + i] = key[i]; + } + + const TJCHAR* suffix = TJCHARPREFIX("' was not found!"); + for (int i = 0; i < 17; ++i) + { + msg[9 + key_len + i] = suffix[i]; + } + + msg[total_len] = 0; + + if (is_strict) + { + _parse_options.callback_function(parse_options::message_type::fatal, msg); + if (_parse_options.throw_exception) + { +#if TJ_USE_CHAR == 1 + TJParseException ex(msg); + delete[] msg; + throw ex; +#else + delete[] msg; + throw TJParseException("The key was not found!"); +#endif + } + } + else + { + _parse_options.callback_function(parse_options::message_type::warning, msg); + } + + delete[] msg; + } + Optional TJValueObject::get_raw_float(const TJCHAR* key, bool case_sensitive) const { auto value = try_get_value(key, case_sensitive); if (nullptr == value) { - if (_parse_options.strict) - { - ParseResult _parse_result(_parse_options); - _parse_result.assign_exception_message_and_throw("The key was not found!"); - } + raise_key_not_found(key, _parse_options.strict); return Optional(); } return Optional(value->get_raw_float()); @@ -5416,11 +5468,7 @@ namespace TinyJSON auto value = try_get_value(key, case_sensitive); if (nullptr == value) { - if (_parse_options.strict) - { - ParseResult _parse_result(_parse_options); - _parse_result.assign_exception_message_and_throw("The key was not found!"); - } + raise_key_not_found(key, _parse_options.strict); return Optional(); } return Optional(value->get_raw_number()); @@ -5431,11 +5479,7 @@ namespace TinyJSON auto value = try_get_value(key, case_sensitive); if (nullptr == value) { - if (_parse_options.strict) - { - ParseResult _parse_result(_parse_options); - _parse_result.assign_exception_message_and_throw("The key was not found!"); - } + raise_key_not_found(key, _parse_options.strict); return Optional>(); } return Optional>(value->get_raw_floats()); @@ -5446,11 +5490,7 @@ namespace TinyJSON auto value = try_get_value(key, case_sensitive); if (nullptr == value) { - if (_parse_options.strict) - { - ParseResult _parse_result(_parse_options); - _parse_result.assign_exception_message_and_throw("The key was not found!"); - } + raise_key_not_found(key, _parse_options.strict); return Optional>(); } return Optional>(value->get_raw_numbers()); @@ -5461,11 +5501,7 @@ namespace TinyJSON auto value = try_get_value(key, case_sensitive); if (nullptr == value) { - if (_parse_options.strict) - { - ParseResult _parse_result(_parse_options); - _parse_result.assign_exception_message_and_throw("The key was not found!"); - } + raise_key_not_found(key, _parse_options.strict); return TJCHARPREFIX(""); } return value->get_string(); @@ -5476,11 +5512,7 @@ namespace TinyJSON auto value = try_get_value(key, case_sensitive); if (nullptr == value) { - if (_parse_options.strict) - { - ParseResult _parse_result(_parse_options); - _parse_result.assign_exception_message_and_throw("The key was not found!"); - } + raise_key_not_found(key, _parse_options.strict); return false; } return value->get_boolean(); @@ -5898,6 +5930,7 @@ namespace TinyJSON auto value = try_get_value(key, case_sensitive); if (nullptr == value) { + raise_key_not_found(key, false); return nullptr; } diff --git a/src/TinyJSON.h b/src/TinyJSON.h index 83198f2..b6c7d8f 100644 --- a/src/TinyJSON.h +++ b/src/TinyJSON.h @@ -45,11 +45,12 @@ // v0.2.1 - added remove_at to TJValueArray. // v0.2.2 - added support for Json5 https://github.com/json5/ // v0.2.3 - added atomic file saving -// v0.2.4 - added operator[] and as() accessors to TJValue +// v0.2.4 - added operator[] and as() accessor to TJValue +// v0.2.5 - added raise warning when key is not found. static const short TJ_VERSION_MAJOR = 0; static const short TJ_VERSION_MINOR = 2; -static const short TJ_VERSION_PATCH = 4; -static const char TJ_VERSION_STRING[] = "0.2.4"; +static const short TJ_VERSION_PATCH = 5; +static const char TJ_VERSION_STRING[] = "0.2.5"; #ifndef TJ_USE_CHAR # define TJ_USE_CHAR 1 @@ -1157,6 +1158,7 @@ class TJDictionary; } private: + void raise_key_not_found(const TJCHAR* key, bool is_strict) const; template std::vector get_vector_internal(const TJCHAR* key, bool case_sensitive, std::true_type) const { diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index ce7d572..d9187c8 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -35,6 +35,7 @@ set( Sources "testtinyjsonstrings.cpp" testtinyjsonvaluesget.cpp testtinyjsonversion.cpp + testtinyjsonwarnings.cpp testtinyjsonconstructors.cpp testtinyjsonwrite.cpp testtinyjsonset.cpp diff --git a/tests/testtinyjsonexceptions.cpp b/tests/testtinyjsonexceptions.cpp index fe55c27..00e519c 100644 --- a/tests/testtinyjsonexceptions.cpp +++ b/tests/testtinyjsonexceptions.cpp @@ -285,7 +285,7 @@ TEST(TestException, GettingNonExistentKeyWillLogAndNotThrow) { 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 was not found!"); + EXPECT_STREQ(exception_message, "The key 'b' was not found!"); called = true; }; TinyJSON::TJValue* json = TinyJSON::TJ::parse("{\"a\" : 12}", options); diff --git a/tests/testtinyjsonversion.cpp b/tests/testtinyjsonversion.cpp index effb28a..7c8ad24 100644 --- a/tests/testtinyjsonversion.cpp +++ b/tests/testtinyjsonversion.cpp @@ -5,18 +5,22 @@ #define TJ_USE_CHAR 1 #include "../src/TinyJSON.h" -TEST(TestVersion, CheckVersionMajor) { +TEST(TestVersion, CheckVersionMajor) +{ ASSERT_EQ(0, TJ_VERSION_MAJOR); } -TEST(TestVersion, CheckVersionMinor) { +TEST(TestVersion, CheckVersionMinor) +{ ASSERT_EQ(2, TJ_VERSION_MINOR); } -TEST(TestVersion, CheckVersionPatch) { - ASSERT_EQ(4, TJ_VERSION_PATCH); +TEST(TestVersion, CheckVersionPatch) +{ + ASSERT_EQ(5, TJ_VERSION_PATCH); } -TEST(TestVersion, CheckVersionString) { - ASSERT_STREQ("0.2.4", TJ_VERSION_STRING); +TEST(TestVersion, CheckVersionString) +{ + ASSERT_STREQ("0.2.5", TJ_VERSION_STRING); } diff --git a/tests/testtinyjsonwarnings.cpp b/tests/testtinyjsonwarnings.cpp new file mode 100644 index 0000000..fccbc7f --- /dev/null +++ b/tests/testtinyjsonwarnings.cpp @@ -0,0 +1,156 @@ +// 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" + +template +void assert_warning_fired(TinyJSON::TJValueObject* obj, int& warning_count, std::string& last_warning_message, F&& func) +{ + warning_count = 0; + last_warning_message = ""; + func(obj); + EXPECT_EQ(1, warning_count); + EXPECT_EQ("The key 'missing' was not found!", last_warning_message); +} + +TEST(TestWarnings, GetterWarnings) +{ + int warning_count = 0; + std::string last_warning_message = ""; + TinyJSON::parse_options options = {}; + options.callback_function = [&](TinyJSON::parse_options::message_type type, const TJCHAR* message) + { + if (type == TinyJSON::parse_options::message_type::warning) + { + warning_count++; + if (message) + { + last_warning_message = message; + } + } + }; + + auto* json = TinyJSON::TJ::parse("{}", options); + auto* obj = dynamic_cast(json); + ASSERT_NE(nullptr, obj); + + // 1. try_get_string + assert_warning_fired(obj, warning_count, last_warning_message, [](TinyJSON::TJValueObject* o) + { + o->try_get_string("missing"); + }); + + // 2. get_boolean + assert_warning_fired(obj, warning_count, last_warning_message, [](TinyJSON::TJValueObject* o) + { + o->get_boolean("missing"); + }); + + // 3. get_string + assert_warning_fired(obj, warning_count, last_warning_message, [](TinyJSON::TJValueObject* o) + { + o->get_string("missing"); + }); + + // 4. get_number (long long) + assert_warning_fired(obj, warning_count, last_warning_message, [](TinyJSON::TJValueObject* o) + { + o->get_number("missing"); + }); + + // 5. get_numbers (std::vector) + assert_warning_fired(obj, warning_count, last_warning_message, [](TinyJSON::TJValueObject* o) + { + o->get_numbers("missing"); + }); + + // 6. get_float (long double) + assert_warning_fired(obj, warning_count, last_warning_message, [](TinyJSON::TJValueObject* o) + { + o->get_float("missing"); + }); + + // 7. get_floats (std::vector) + assert_warning_fired(obj, warning_count, last_warning_message, [](TinyJSON::TJValueObject* o) + { + o->get_floats("missing"); + }); + + // 8. get_number + assert_warning_fired(obj, warning_count, last_warning_message, [](TinyJSON::TJValueObject* o) + { + o->get_number("missing"); + }); + + // 9. get_numbers + assert_warning_fired(obj, warning_count, last_warning_message, [](TinyJSON::TJValueObject* o) + { + o->get_numbers("missing"); + }); + + // 10. get_float + assert_warning_fired(obj, warning_count, last_warning_message, [](TinyJSON::TJValueObject* o) + { + o->get_float("missing"); + }); + + // 11. get_floats + assert_warning_fired(obj, warning_count, last_warning_message, [](TinyJSON::TJValueObject* o) + { + o->get_floats("missing"); + }); + + // 12. get + assert_warning_fired(obj, warning_count, last_warning_message, [](TinyJSON::TJValueObject* o) + { + o->get("missing"); + }); + + // 13. get + assert_warning_fired(obj, warning_count, last_warning_message, [](TinyJSON::TJValueObject* o) + { + o->get("missing"); + }); + + // 14. get + assert_warning_fired(obj, warning_count, last_warning_message, [](TinyJSON::TJValueObject* o) + { + o->get("missing"); + }); + + // 15. get + assert_warning_fired(obj, warning_count, last_warning_message, [](TinyJSON::TJValueObject* o) + { + o->get("missing"); + }); + + // 16. get> + assert_warning_fired(obj, warning_count, last_warning_message, [](TinyJSON::TJValueObject* o) + { + o->get>("missing"); + }); + + // 17. get> + assert_warning_fired(obj, warning_count, last_warning_message, [](TinyJSON::TJValueObject* o) + { + o->get>("missing"); + }); + +#if TJ_INCLUDE_STD_STRING == 1 + // 18. get + assert_warning_fired(obj, warning_count, last_warning_message, [](TinyJSON::TJValueObject* o) + { + o->get("missing"); + }); + + // 19. get with std::string key + assert_warning_fired(obj, warning_count, last_warning_message, [](TinyJSON::TJValueObject* o) + { + o->get(std::string("missing")); + }); +#endif + + delete json; +}