diff --git a/src/TinyJSON.h b/src/TinyJSON.h index eb2e977..69f9084 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,19 @@ class TJDictionary; return const_iterator::make_end(*this); } + 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 + TJValueAccessor operator[](const std::string& key) const; +#endif + protected: parse_options _parse_options; long long get_raw_number() const; @@ -1615,6 +1635,168 @@ 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) + { + } + +#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), + _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 + { + // 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, 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 + { + 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); + } + + }; + + 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); + } +#endif + // A string JSon class TJValueString : public TJValue { diff --git a/tests/testtinyjsonvaluesget.cpp b/tests/testtinyjsonvaluesget.cpp index bc2ab5f..0bf7e63 100644 --- a/tests/testtinyjsonvaluesget.cpp +++ b/tests/testtinyjsonvaluesget.cpp @@ -346,3 +346,134 @@ 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; +} + +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; +}