feat: add .members() iterator adapter for range-based for loops (#288) (#1679)

* feat: add .members() iterator adapter for range-based for loops (#288)

This adds a zero-allocation iterator adapter to `Json::Value` that enables idiomatic range-based for loops over object members. This allows iterating over key-value pairs without allocating a vector of keys via `getMemberNames()`, and cleanly supports C++17 structured bindings (e.g. `for (const auto& [name, val] : obj.members())`).

Fixes #288

* run ninja format
diff --git a/.github/workflows/abi-compatibility.yml b/.github/workflows/abi-compatibility.yml
index 881c4e8..1351c09 100644
--- a/.github/workflows/abi-compatibility.yml
+++ b/.github/workflows/abi-compatibility.yml
@@ -16,9 +16,17 @@
         shared_libs: [ON, OFF]
         include:
           - jsoncpp_std: 11
+            app_std: 17
+          - jsoncpp_std: 17
+            app_std: 11
+          - jsoncpp_std: 11
             app_std: 23
           - jsoncpp_std: 23
             app_std: 11
+          - jsoncpp_std: 17
+            app_std: 23
+          - jsoncpp_std: 23
+            app_std: 17
 
     steps:
       - name: checkout project
@@ -47,11 +55,12 @@
 
           find_package(jsoncpp REQUIRED CONFIG)
 
-          add_executable(abi_test stringView.cpp)
+          add_executable(abi_test jsontest.cpp fuzz.cpp main.cpp)
           target_link_libraries(abi_test PRIVATE JsonCpp::JsonCpp)
           EOF
 
-          cp $GITHUB_WORKSPACE/example/stringView/stringView.cpp example-app/stringView.cpp
+          cp src/test_lib_json/*.cpp example-app/
+          cp src/test_lib_json/*.h example-app/
 
       - name: build example app (C++${{ matrix.app_std }})
         shell: bash
diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml
index 55452ac..2b2666d 100644
--- a/.github/workflows/cmake.yml
+++ b/.github/workflows/cmake.yml
@@ -12,6 +12,7 @@
       fail-fast: false
       matrix:
         os: [ubuntu-latest, windows-latest, macos-latest]
+        cxx_standard: [11, 17]
         
     steps:
       - name: checkout project
@@ -19,4 +20,6 @@
 
       - name: build project
         uses: threeal/cmake-action@v2.0.0
+        with:
+          options: CMAKE_CXX_STANDARD=${{ matrix.cxx_standard }}
         
diff --git a/include/json/forwards.h b/include/json/forwards.h
index affe33a..2887bdd 100644
--- a/include/json/forwards.h
+++ b/include/json/forwards.h
@@ -37,6 +37,8 @@
 class ValueIteratorBase;
 class ValueIterator;
 class ValueConstIterator;
+class ValueMembersView;
+class ValueConstMembersView;
 
 } // namespace Json
 
diff --git a/include/json/value.h b/include/json/value.h
index 2007e6b..f14a71c 100644
--- a/include/json/value.h
+++ b/include/json/value.h
@@ -682,6 +682,11 @@
   iterator begin();
   iterator end();
 
+  // \brief Returns a view of member pairs for range-based for loops.
+  ValueMembersView members();
+  // \brief Returns a view of member pairs for range-based for loops.
+  ValueConstMembersView members() const;
+
   /// \brief Returns a reference to the first element in the `Value`.
   /// Requires that this value holds an array or json object, with at least one
   /// element.
@@ -1040,6 +1045,131 @@
   pointer operator->() const { return const_cast<pointer>(&deref()); }
 };
 
+/** \brief Proxy struct to enable range-based for loops over object members.
+ */
+struct MemberProxy {
+  const String name;
+  Value& value;
+};
+
+/** \brief Proxy struct to enable range-based for loops over const object
+ * members.
+ */
+struct ConstMemberProxy {
+  const String name;
+  const Value& value;
+};
+
+/** \brief Iterator adapter for range-based for loops.
+ */
+class ValueMembersIterator {
+public:
+  using iterator_category = std::forward_iterator_tag;
+  using value_type = MemberProxy;
+  using difference_type = int;
+  using pointer = MemberProxy*;
+  using reference = MemberProxy;
+
+  ValueMembersIterator() = default;
+  explicit ValueMembersIterator(ValueIterator const& iter) : it_(iter) {}
+
+  ValueMembersIterator& operator++() {
+    ++it_;
+    return *this;
+  }
+  ValueMembersIterator operator++(int) {
+    ValueMembersIterator temp(*this);
+    ++*this;
+    return temp;
+  }
+  bool operator==(ValueMembersIterator const& other) const {
+    return it_ == other.it_;
+  }
+  bool operator!=(ValueMembersIterator const& other) const {
+    return it_ != other.it_;
+  }
+  MemberProxy operator*() const { return MemberProxy{it_.name(), *it_}; }
+
+private:
+  ValueIterator it_;
+};
+
+/** \brief Iterator adapter for range-based for loops.
+ */
+class ValueConstMembersIterator {
+public:
+  using iterator_category = std::forward_iterator_tag;
+  using value_type = ConstMemberProxy;
+  using difference_type = int;
+  using pointer = ConstMemberProxy*;
+  using reference = ConstMemberProxy;
+
+  ValueConstMembersIterator() = default;
+  explicit ValueConstMembersIterator(ValueConstIterator const& iter)
+      : it_(iter) {}
+
+  ValueConstMembersIterator& operator++() {
+    ++it_;
+    return *this;
+  }
+  ValueConstMembersIterator operator++(int) {
+    ValueConstMembersIterator temp(*this);
+    ++*this;
+    return temp;
+  }
+  bool operator==(ValueConstMembersIterator const& other) const {
+    return it_ == other.it_;
+  }
+  bool operator!=(ValueConstMembersIterator const& other) const {
+    return it_ != other.it_;
+  }
+  ConstMemberProxy operator*() const {
+    return ConstMemberProxy{it_.name(), *it_};
+  }
+
+private:
+  ValueConstIterator it_;
+};
+
+/** \brief Range-based for loop adapter for object members.
+ */
+class ValueMembersView {
+public:
+  ValueMembersView(ValueIterator begin, ValueIterator end)
+      : begin_(begin), end_(end) {}
+  ValueMembersIterator begin() const { return ValueMembersIterator(begin_); }
+  ValueMembersIterator end() const { return ValueMembersIterator(end_); }
+
+private:
+  ValueIterator begin_;
+  ValueIterator end_;
+};
+
+/** \brief Range-based for loop adapter for object members.
+ */
+class ValueConstMembersView {
+public:
+  ValueConstMembersView(ValueConstIterator begin, ValueConstIterator end)
+      : begin_(begin), end_(end) {}
+  ValueConstMembersIterator begin() const {
+    return ValueConstMembersIterator(begin_);
+  }
+  ValueConstMembersIterator end() const {
+    return ValueConstMembersIterator(end_);
+  }
+
+private:
+  ValueConstIterator begin_;
+  ValueConstIterator end_;
+};
+
+inline ValueMembersView Value::members() {
+  return ValueMembersView(begin(), end());
+}
+inline ValueConstMembersView Value::members() const {
+  return ValueConstMembersView(begin(), end());
+}
+
 inline void swap(Value& a, Value& b) { a.swap(b); }
 
 inline const Value& Value::front() const { return *begin(); }
diff --git a/src/test_lib_json/main.cpp b/src/test_lib_json/main.cpp
index 4c000fa..501aba1 100644
--- a/src/test_lib_json/main.cpp
+++ b/src/test_lib_json/main.cpp
@@ -3924,6 +3924,54 @@
 
 struct IteratorTest : JsonTest::TestCase {};
 
+JSONTEST_FIXTURE_LOCAL(IteratorTest, members) {
+  Json::Value j;
+  j["k1"] = "a";
+  j["k2"] = "b";
+
+  std::vector<std::string> keys;
+  std::vector<std::string> values;
+
+  for (const auto& member : j.members()) {
+    keys.push_back(member.name);
+    values.push_back(member.value.asString());
+  }
+
+  JSONTEST_ASSERT((keys == std::vector<std::string>{"k1", "k2"}));
+  JSONTEST_ASSERT((values == std::vector<std::string>{"a", "b"}));
+
+  // Test modification through value reference
+  for (const auto& member : j.members()) {
+    member.value = "c";
+  }
+
+  JSONTEST_ASSERT(j["k1"].asString() == "c");
+
+  // Test const members
+  const Json::Value& cj = j;
+  keys.clear();
+  values.clear();
+
+  for (const auto& member : cj.members()) {
+    keys.push_back(member.name);
+    values.push_back(member.value.asString());
+  }
+
+  JSONTEST_ASSERT((keys == std::vector<std::string>{"k1", "k2"}));
+  JSONTEST_ASSERT((values == std::vector<std::string>{"c", "c"}));
+
+#if __cplusplus >= 201703L
+  keys.clear();
+  values.clear();
+  for (auto const& [k, v] : cj.members()) {
+    keys.push_back(k);
+    values.push_back(v.asString());
+  }
+  JSONTEST_ASSERT((keys == std::vector<std::string>{"k1", "k2"}));
+  JSONTEST_ASSERT((values == std::vector<std::string>{"c", "c"}));
+#endif
+}
+
 JSONTEST_FIXTURE_LOCAL(IteratorTest, convert) {
   Json::Value j;
   const Json::Value& cj = j;