From fc0483ed21aaa5e02afc6bf28d02dcd604c2da8d Mon Sep 17 00:00:00 2001 From: njp2k5 Date: Wed, 20 May 2026 17:07:12 +0530 Subject: [PATCH 1/3] feat: add operator[] access support for TJValue --- src/TinyJSON.h | 144 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 144 insertions(+) diff --git a/src/TinyJSON.h b/src/TinyJSON.h index eb2e977..c16d6c3 100644 --- a/src/TinyJSON.h +++ b/src/TinyJSON.h @@ -445,6 +445,7 @@ class TJDictionary; class TJHelper; class TJValueArray; class TJValueObject; + class TJValueAccessor; // A simple JSON value, the base of all items in a json class TJValue @@ -671,6 +672,12 @@ class TJDictionary; return get_vector_internal(std::is_integral()); } + template + T as() const + { + return get(); + } + private: template std::vector get_vector_internal(std::true_type) const @@ -705,6 +712,13 @@ class TJDictionary; return const_iterator::make_end(*this); } + TJValueAccessor operator[](const TJCHAR* key) const; + TJValueAccessor operator[](int index) const; + +#if TJ_INCLUDE_STD_STRING == 1 + TJValueAccessor operator[](const std::string& key) const; +#endif + protected: parse_options _parse_options; long long get_raw_number() const; @@ -1615,6 +1629,136 @@ class TJDictionary; void free_values(); }; + class TJValueAccessor + { + public: + TJValueAccessor(const TJValue* owner, const TJCHAR* key, bool case_sensitive = true) : + _owner(owner), + _key(key), + _index(0), + _by_index(false), + _case_sensitive(case_sensitive) + { + } + + TJValueAccessor(const TJValue* owner, int index) : + _owner(owner), + _key(nullptr), + _index(index), + _by_index(true), + _case_sensitive(true) + { + } + + template + T as() const + { + if (_by_index) + { + auto value = get_value_ptr(); + if (value == nullptr) + { + return default_value(); + } + return value->get(); + } + + const auto* object = dynamic_cast(_owner); + if (object == nullptr) + { + return default_value(); + } + return object->get(_key, _case_sensitive); + } + + TJValueAccessor operator[](const TJCHAR* key) const + { + return TJValueAccessor(get_value_ptr(), key, true); + } + + TJValueAccessor operator[](int index) const + { + return TJValueAccessor(get_value_ptr(), index); + } + +#if TJ_INCLUDE_STD_STRING == 1 + TJValueAccessor operator[](const std::string& key) const + { + return TJValueAccessor(get_value_ptr(), key.c_str(), true); + } +#endif + + private: + template + typename std::enable_if::value, T>::type + default_value() const + { + return T(); + } + + template + typename std::enable_if::value, const TJCHAR*>::type + default_value() const + { + return TJCHARPREFIX(""); + } + + const TJValue* get_value_ptr() const + { + if (_owner == nullptr) + { + return nullptr; + } + + if (_by_index) + { + const auto* array = dynamic_cast(_owner); + if (array != nullptr) + { + return array->at(_index); + } + + const auto* object = dynamic_cast(_owner); + if (object != nullptr) + { + auto member = object->at(_index); + return member != nullptr ? member->value() : nullptr; + } + return nullptr; + } + + const auto* object = dynamic_cast(_owner); + if (object == nullptr) + { + return nullptr; + } + return object->try_get_value(_key, _case_sensitive); + } + + const TJValue* _owner; + const TJCHAR* _key; + int _index; + bool _by_index; + bool _case_sensitive; + }; + + inline TJValueAccessor TJValue::operator[](const TJCHAR* key) const + { + return TJValueAccessor(this, key); + } + + inline TJValueAccessor TJValue::operator[](int index) const + { + return TJValueAccessor(this, index); + } + +#if TJ_INCLUDE_STD_STRING == 1 + inline TJValueAccessor TJValue::operator[](const std::string& key) const + { + return TJValueAccessor(this, key.c_str()); + } +#endif + // A string JSon class TJValueString : public TJValue { From 3be7101b3d7559f49d8ba53c551bebfc46909dc7 Mon Sep 17 00:00:00 2001 From: njp2k5 Date: Wed, 20 May 2026 17:20:19 +0530 Subject: [PATCH 2/3] test: add TJValue operator[] access tests --- tests/testtinyjsonvaluesget.cpp | 59 +++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/tests/testtinyjsonvaluesget.cpp b/tests/testtinyjsonvaluesget.cpp index bc2ab5f..6fcda8d 100644 --- a/tests/testtinyjsonvaluesget.cpp +++ b/tests/testtinyjsonvaluesget.cpp @@ -346,3 +346,62 @@ TEST(TestValueGet, GetStrictStringFromArrayWillThrow) delete json; } + +TEST(TestOperatorAccess, AccessByKeyAndIndex) +{ + TinyJSON::parse_options options = {}; + options.throw_exception = true; + options.strict = false; + auto json = TinyJSON::TJ::parse(R"( + { + "a": 1, + "b": "text", + "c": [10, 20, 30], + "d": { "e": 5 } + } + )", options + ); + + ASSERT_NE(nullptr, json); + + ASSERT_EQ(1, (*json)["a"].as()); + ASSERT_EQ(std::string("text"), (*json)["b"].as()); + ASSERT_EQ(20, (*json)["c"][1].as()); + ASSERT_EQ(5, (*json)["d"]["e"].as()); + + ASSERT_EQ(1, (*json)[0].as()); + + delete json; +} + +TEST(TestOperatorAccess, AccessArrayRoot) +{ + TinyJSON::parse_options options = {}; + options.throw_exception = true; + options.strict = false; + auto json = TinyJSON::TJ::parse(R"( + [1, 2, 3] + )", options + ); + + ASSERT_NE(nullptr, json); + ASSERT_EQ(2, (*json)[1].as()); + + delete json; +} + +TEST(TestOperatorAccess, StrictMissingKeyThrows) +{ + TinyJSON::parse_options options = {}; + options.throw_exception = true; + options.strict = true; + auto json = TinyJSON::TJ::parse(R"( + { "a": 1 } + )", options + ); + + ASSERT_NE(nullptr, json); + ASSERT_ANY_THROW((*json)["missing"].as()); + + delete json; +} From b48c6599cbd3bf1003636ea2709dbe039533cf71 Mon Sep 17 00:00:00 2001 From: njp2k5 Date: Wed, 20 May 2026 17:37:55 +0530 Subject: [PATCH 3/3] Fix TJValueAccessor key lifetime handling --- src/TinyJSON.h | 52 ++++++++++++++++++++---- tests/testtinyjsonvaluesget.cpp | 72 +++++++++++++++++++++++++++++++++ 2 files changed, 117 insertions(+), 7 deletions(-) diff --git a/src/TinyJSON.h b/src/TinyJSON.h index c16d6c3..69f9084 100644 --- a/src/TinyJSON.h +++ b/src/TinyJSON.h @@ -713,6 +713,12 @@ class TJDictionary; } TJValueAccessor operator[](const TJCHAR* key) const; + + /// + /// Index access for arrays and objects. + /// For objects, the index follows the non-comment item order (same as TJValueObject::at). + /// Out-of-range access returns an empty accessor; calling as() yields default values. + /// TJValueAccessor operator[](int index) const; #if TJ_INCLUDE_STD_STRING == 1 @@ -1641,6 +1647,30 @@ class TJDictionary; { } +#if TJ_INCLUDE_STD_STRING == 1 + TJValueAccessor(const TJValue* owner, const std::string& key, bool case_sensitive = true) : + _owner(owner), + _key(nullptr), + _index(0), + _by_index(false), + _case_sensitive(case_sensitive), + _key_storage(key) + { + _key = _key_storage.c_str(); + } + + TJValueAccessor(const TJValue* owner, std::string&& key, bool case_sensitive = true) : + _owner(owner), + _key(nullptr), + _index(0), + _by_index(false), + _case_sensitive(case_sensitive), + _key_storage(std::move(key)) + { + _key = _key_storage.c_str(); + } +#endif + TJValueAccessor(const TJValue* owner, int index) : _owner(owner), _key(nullptr), @@ -1673,22 +1703,35 @@ class TJDictionary; TJValueAccessor operator[](const TJCHAR* key) const { + // Chained access uses non-throwing lookup for intermediate nodes. + // Missing intermediates yield empty accessors (as() returns defaults). return TJValueAccessor(get_value_ptr(), key, true); } TJValueAccessor operator[](int index) const { + // For objects, index follows non-comment item order (same as TJValueObject::at). + // Out-of-range yields empty accessor (as() returns defaults). return TJValueAccessor(get_value_ptr(), index); } #if TJ_INCLUDE_STD_STRING == 1 TJValueAccessor operator[](const std::string& key) const { - return TJValueAccessor(get_value_ptr(), key.c_str(), true); + return TJValueAccessor(get_value_ptr(), key, true); } #endif private: + const TJValue* _owner; + const TJCHAR* _key; + int _index; + bool _by_index; + bool _case_sensitive; +#if TJ_INCLUDE_STD_STRING == 1 + std::string _key_storage; +#endif + template typename std::enable_if::value, T>::type default_value() const @@ -1735,11 +1778,6 @@ class TJDictionary; return object->try_get_value(_key, _case_sensitive); } - const TJValue* _owner; - const TJCHAR* _key; - int _index; - bool _by_index; - bool _case_sensitive; }; inline TJValueAccessor TJValue::operator[](const TJCHAR* key) const @@ -1755,7 +1793,7 @@ class TJDictionary; #if TJ_INCLUDE_STD_STRING == 1 inline TJValueAccessor TJValue::operator[](const std::string& key) const { - return TJValueAccessor(this, key.c_str()); + return TJValueAccessor(this, key); } #endif diff --git a/tests/testtinyjsonvaluesget.cpp b/tests/testtinyjsonvaluesget.cpp index 6fcda8d..0bf7e63 100644 --- a/tests/testtinyjsonvaluesget.cpp +++ b/tests/testtinyjsonvaluesget.cpp @@ -405,3 +405,75 @@ TEST(TestOperatorAccess, StrictMissingKeyThrows) delete json; } + +TEST(TestOperatorAccess, StdStringKeyOwnership) +{ + TinyJSON::parse_options options = {}; + options.throw_exception = true; + options.strict = false; + auto json = TinyJSON::TJ::parse(R"( + { + "a": 1, + "b": 2 + } + )", options + ); + + ASSERT_NE(nullptr, json); + + std::string key = "a"; + key.reserve(8); + auto accessor = (*json)[key]; + key[0] = 'b'; + + ASSERT_EQ(1, accessor.as()); + + auto temp_accessor = (*json)[std::string("a")]; + ASSERT_EQ(1, temp_accessor.as()); + + delete json; +} + +TEST(TestOperatorAccess, ChainedAccessDefaultsOnMissingIntermediate) +{ + TinyJSON::parse_options options = {}; + options.throw_exception = true; + options.strict = false; + auto json = TinyJSON::TJ::parse(R"( + { + "a": { "b": 1 } + } + )", options + ); + + ASSERT_NE(nullptr, json); + + ASSERT_EQ(1, (*json)["a"]["b"].as()); + ASSERT_EQ(0, (*json)["missing"]["b"].as()); + + delete json; +} + +TEST(TestOperatorAccess, ObjectIndexOrderingAndBounds) +{ + TinyJSON::parse_options options = {}; + options.throw_exception = true; + options.strict = false; + auto json = TinyJSON::TJ::parse(R"( + { + "first": 1, + "second": 2, + "third": 3 + } + )", options + ); + + ASSERT_NE(nullptr, json); + + ASSERT_EQ(1, (*json)[0].as()); + ASSERT_EQ(2, (*json)[1].as()); + ASSERT_EQ(3, (*json)[2].as()); + ASSERT_EQ(0, (*json)[3].as()); + + delete json; +}