Declarative JSON parser (#30442)
* Declarative JSON parser
* Automated change: Fix sanity tests
* fix
* shrinking stuff a little
* static vtables
* separate fns
* simpler?
* make maps work
* windows fixes
* Automated change: Fix sanity tests
* simplify code
* Automated change: Fix sanity tests
* vtable-test
* dont always create vec/map impls for every type
* comments
* make error consistent
* move method private
* progress
* durations!
* Automated change: Fix sanity tests
* fix
* fix
* fix
* Automated change: Fix sanity tests
* post-load
* Automated change: Fix sanity tests
* document JsonPostLoad() and add static_assert
* don't copy field names, to avoid length limitations
* use absl::Status
* accept either string or number for numeric values
* add test for direct data member of another struct type
* remove unused method
* add support for retaining part of the JSON wirthout processing
* update test for changes in Json::Parse() API
* add absl::optional support
* Automated change: Fix sanity tests
* fix tests, improve error messages, and add overload to parse to existing object
* remove overload of LoadFromJson()
* change special case for Json to instead use Json::Object
* fix build
* improve error structure, add missing types, and improve tests
* clang-format
* Automated change: Fix sanity tests
* fix build
* add LoadJsonObjectField(), add LoadFromJson() overload that takes an ErrorList parameter, and add tests for parsing bare top-level types
* fix msan
* Automated change: Fix sanity tests
* fix error message
* Automated change: Fix sanity tests
* add mechanism to conditionally disable individual fields
* fix build
Co-authored-by: Craig Tiller <craig.tiller@gmail.com>
Co-authored-by: ctiller <ctiller@users.noreply.github.com>
Co-authored-by: Craig Tiller <ctiller@google.com>
Co-authored-by: markdroth <markdroth@users.noreply.github.com>
diff --git a/BUILD b/BUILD
index 88a3d15..3541025 100644
--- a/BUILD
+++ b/BUILD
@@ -7260,10 +7260,52 @@
"error",
"gpr_base",
"json",
+ "json_args",
+ "json_object_loader",
"time",
],
)
+grpc_cc_library(
+ name = "json_args",
+ hdrs = ["src/core/lib/json/json_args.h"],
+ external_deps = ["absl/strings"],
+ deps = ["gpr_base"],
+)
+
+grpc_cc_library(
+ name = "json_object_loader",
+ srcs = ["src/core/lib/json/json_object_loader.cc"],
+ hdrs = ["src/core/lib/json/json_object_loader.h"],
+ external_deps = [
+ "absl/meta:type_traits",
+ "absl/status",
+ "absl/status:statusor",
+ "absl/strings",
+ "absl/types:optional",
+ ],
+ deps = [
+ "gpr_base",
+ "json",
+ "json_args",
+ "time",
+ ],
+)
+
+grpc_cc_library(
+ name = "json_channel_args",
+ hdrs = ["src/core/lib/json/json_channel_args.h"],
+ external_deps = [
+ "absl/strings",
+ "absl/types:optional",
+ ],
+ deps = [
+ "channel_args",
+ "gpr",
+ "json_args",
+ ],
+)
+
### UPB Targets
grpc_upb_proto_library(
diff --git a/CMakeLists.txt b/CMakeLists.txt
index e02b878..2f374f2 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1025,6 +1025,7 @@
endif()
add_dependencies(buildtests_cxx istio_echo_server_test)
add_dependencies(buildtests_cxx join_test)
+ add_dependencies(buildtests_cxx json_object_loader_test)
add_dependencies(buildtests_cxx json_test)
add_dependencies(buildtests_cxx json_token_test)
add_dependencies(buildtests_cxx jwt_verifier_test)
@@ -2186,6 +2187,7 @@
src/core/lib/iomgr/wakeup_fd_nospecial.cc
src/core/lib/iomgr/wakeup_fd_pipe.cc
src/core/lib/iomgr/wakeup_fd_posix.cc
+ src/core/lib/json/json_object_loader.cc
src/core/lib/json/json_reader.cc
src/core/lib/json/json_util.cc
src/core/lib/json/json_writer.cc
@@ -2794,6 +2796,7 @@
src/core/lib/iomgr/wakeup_fd_nospecial.cc
src/core/lib/iomgr/wakeup_fd_pipe.cc
src/core/lib/iomgr/wakeup_fd_posix.cc
+ src/core/lib/json/json_object_loader.cc
src/core/lib/json/json_reader.cc
src/core/lib/json/json_util.cc
src/core/lib/json/json_writer.cc
@@ -12626,6 +12629,41 @@
endif()
if(gRPC_BUILD_TESTS)
+add_executable(json_object_loader_test
+ test/core/json/json_object_loader_test.cc
+ third_party/googletest/googletest/src/gtest-all.cc
+ third_party/googletest/googlemock/src/gmock-all.cc
+)
+
+target_include_directories(json_object_loader_test
+ PRIVATE
+ ${CMAKE_CURRENT_SOURCE_DIR}
+ ${CMAKE_CURRENT_SOURCE_DIR}/include
+ ${_gRPC_ADDRESS_SORTING_INCLUDE_DIR}
+ ${_gRPC_RE2_INCLUDE_DIR}
+ ${_gRPC_SSL_INCLUDE_DIR}
+ ${_gRPC_UPB_GENERATED_DIR}
+ ${_gRPC_UPB_GRPC_GENERATED_DIR}
+ ${_gRPC_UPB_INCLUDE_DIR}
+ ${_gRPC_XXHASH_INCLUDE_DIR}
+ ${_gRPC_ZLIB_INCLUDE_DIR}
+ third_party/googletest/googletest/include
+ third_party/googletest/googletest
+ third_party/googletest/googlemock/include
+ third_party/googletest/googlemock
+ ${_gRPC_PROTO_GENS_DIR}
+)
+
+target_link_libraries(json_object_loader_test
+ ${_gRPC_PROTOBUF_LIBRARIES}
+ ${_gRPC_ALLTARGETS_LIBRARIES}
+ grpc_test_util
+)
+
+
+endif()
+if(gRPC_BUILD_TESTS)
+
add_executable(json_test
test/core/json/json_test.cc
third_party/googletest/googletest/src/gtest-all.cc
diff --git a/Makefile b/Makefile
index cf54b0c..9c902fc 100644
--- a/Makefile
+++ b/Makefile
@@ -1548,6 +1548,7 @@
src/core/lib/iomgr/wakeup_fd_nospecial.cc \
src/core/lib/iomgr/wakeup_fd_pipe.cc \
src/core/lib/iomgr/wakeup_fd_posix.cc \
+ src/core/lib/json/json_object_loader.cc \
src/core/lib/json/json_reader.cc \
src/core/lib/json/json_util.cc \
src/core/lib/json/json_writer.cc \
@@ -2020,6 +2021,7 @@
src/core/lib/iomgr/wakeup_fd_nospecial.cc \
src/core/lib/iomgr/wakeup_fd_pipe.cc \
src/core/lib/iomgr/wakeup_fd_posix.cc \
+ src/core/lib/json/json_object_loader.cc \
src/core/lib/json/json_reader.cc \
src/core/lib/json/json_util.cc \
src/core/lib/json/json_writer.cc \
diff --git a/build_autogenerated.yaml b/build_autogenerated.yaml
index 049d1f3..42686e3 100644
--- a/build_autogenerated.yaml
+++ b/build_autogenerated.yaml
@@ -849,6 +849,8 @@
- src/core/lib/iomgr/wakeup_fd_pipe.h
- src/core/lib/iomgr/wakeup_fd_posix.h
- src/core/lib/json/json.h
+ - src/core/lib/json/json_args.h
+ - src/core/lib/json/json_object_loader.h
- src/core/lib/json/json_util.h
- src/core/lib/load_balancing/lb_policy.h
- src/core/lib/load_balancing/lb_policy_factory.h
@@ -1540,6 +1542,7 @@
- src/core/lib/iomgr/wakeup_fd_nospecial.cc
- src/core/lib/iomgr/wakeup_fd_pipe.cc
- src/core/lib/iomgr/wakeup_fd_posix.cc
+ - src/core/lib/json/json_object_loader.cc
- src/core/lib/json/json_reader.cc
- src/core/lib/json/json_util.cc
- src/core/lib/json/json_writer.cc
@@ -2031,6 +2034,8 @@
- src/core/lib/iomgr/wakeup_fd_pipe.h
- src/core/lib/iomgr/wakeup_fd_posix.h
- src/core/lib/json/json.h
+ - src/core/lib/json/json_args.h
+ - src/core/lib/json/json_object_loader.h
- src/core/lib/json/json_util.h
- src/core/lib/load_balancing/lb_policy.h
- src/core/lib/load_balancing/lb_policy_factory.h
@@ -2363,6 +2368,7 @@
- src/core/lib/iomgr/wakeup_fd_nospecial.cc
- src/core/lib/iomgr/wakeup_fd_pipe.cc
- src/core/lib/iomgr/wakeup_fd_posix.cc
+ - src/core/lib/json/json_object_loader.cc
- src/core/lib/json/json_reader.cc
- src/core/lib/json/json_util.cc
- src/core/lib/json/json_writer.cc
@@ -7392,6 +7398,16 @@
- absl/types:variant
- absl/utility:utility
uses_polling: false
+- name: json_object_loader_test
+ gtest: true
+ build: test
+ language: c++
+ headers: []
+ src:
+ - test/core/json/json_object_loader_test.cc
+ deps:
+ - grpc_test_util
+ uses_polling: false
- name: json_test
gtest: true
build: test
diff --git a/config.m4 b/config.m4
index 6dd509e..06d3581 100644
--- a/config.m4
+++ b/config.m4
@@ -607,6 +607,7 @@
src/core/lib/iomgr/wakeup_fd_nospecial.cc \
src/core/lib/iomgr/wakeup_fd_pipe.cc \
src/core/lib/iomgr/wakeup_fd_posix.cc \
+ src/core/lib/json/json_object_loader.cc \
src/core/lib/json/json_reader.cc \
src/core/lib/json/json_util.cc \
src/core/lib/json/json_writer.cc \
diff --git a/config.w32 b/config.w32
index 46cfab9..26f612c 100644
--- a/config.w32
+++ b/config.w32
@@ -573,6 +573,7 @@
"src\\core\\lib\\iomgr\\wakeup_fd_nospecial.cc " +
"src\\core\\lib\\iomgr\\wakeup_fd_pipe.cc " +
"src\\core\\lib\\iomgr\\wakeup_fd_posix.cc " +
+ "src\\core\\lib\\json\\json_object_loader.cc " +
"src\\core\\lib\\json\\json_reader.cc " +
"src\\core\\lib\\json\\json_util.cc " +
"src\\core\\lib\\json\\json_writer.cc " +
diff --git a/gRPC-C++.podspec b/gRPC-C++.podspec
index fb0d095..59c6c1e 100644
--- a/gRPC-C++.podspec
+++ b/gRPC-C++.podspec
@@ -807,6 +807,8 @@
'src/core/lib/iomgr/wakeup_fd_pipe.h',
'src/core/lib/iomgr/wakeup_fd_posix.h',
'src/core/lib/json/json.h',
+ 'src/core/lib/json/json_args.h',
+ 'src/core/lib/json/json_object_loader.h',
'src/core/lib/json/json_util.h',
'src/core/lib/load_balancing/lb_policy.h',
'src/core/lib/load_balancing/lb_policy_factory.h',
@@ -1659,6 +1661,8 @@
'src/core/lib/iomgr/wakeup_fd_pipe.h',
'src/core/lib/iomgr/wakeup_fd_posix.h',
'src/core/lib/json/json.h',
+ 'src/core/lib/json/json_args.h',
+ 'src/core/lib/json/json_object_loader.h',
'src/core/lib/json/json_util.h',
'src/core/lib/load_balancing/lb_policy.h',
'src/core/lib/load_balancing/lb_policy_factory.h',
diff --git a/gRPC-Core.podspec b/gRPC-Core.podspec
index 456bd60..7b998c9 100644
--- a/gRPC-Core.podspec
+++ b/gRPC-Core.podspec
@@ -1312,6 +1312,9 @@
'src/core/lib/iomgr/wakeup_fd_posix.cc',
'src/core/lib/iomgr/wakeup_fd_posix.h',
'src/core/lib/json/json.h',
+ 'src/core/lib/json/json_args.h',
+ 'src/core/lib/json/json_object_loader.cc',
+ 'src/core/lib/json/json_object_loader.h',
'src/core/lib/json/json_reader.cc',
'src/core/lib/json/json_util.cc',
'src/core/lib/json/json_util.h',
@@ -2280,6 +2283,8 @@
'src/core/lib/iomgr/wakeup_fd_pipe.h',
'src/core/lib/iomgr/wakeup_fd_posix.h',
'src/core/lib/json/json.h',
+ 'src/core/lib/json/json_args.h',
+ 'src/core/lib/json/json_object_loader.h',
'src/core/lib/json/json_util.h',
'src/core/lib/load_balancing/lb_policy.h',
'src/core/lib/load_balancing/lb_policy_factory.h',
diff --git a/grpc.gemspec b/grpc.gemspec
index 5215d8c..3e09b77 100644
--- a/grpc.gemspec
+++ b/grpc.gemspec
@@ -1225,6 +1225,9 @@
s.files += %w( src/core/lib/iomgr/wakeup_fd_posix.cc )
s.files += %w( src/core/lib/iomgr/wakeup_fd_posix.h )
s.files += %w( src/core/lib/json/json.h )
+ s.files += %w( src/core/lib/json/json_args.h )
+ s.files += %w( src/core/lib/json/json_object_loader.cc )
+ s.files += %w( src/core/lib/json/json_object_loader.h )
s.files += %w( src/core/lib/json/json_reader.cc )
s.files += %w( src/core/lib/json/json_util.cc )
s.files += %w( src/core/lib/json/json_util.h )
diff --git a/grpc.gyp b/grpc.gyp
index a9b7fae..332a579 100644
--- a/grpc.gyp
+++ b/grpc.gyp
@@ -899,6 +899,7 @@
'src/core/lib/iomgr/wakeup_fd_nospecial.cc',
'src/core/lib/iomgr/wakeup_fd_pipe.cc',
'src/core/lib/iomgr/wakeup_fd_posix.cc',
+ 'src/core/lib/json/json_object_loader.cc',
'src/core/lib/json/json_reader.cc',
'src/core/lib/json/json_util.cc',
'src/core/lib/json/json_writer.cc',
@@ -1339,6 +1340,7 @@
'src/core/lib/iomgr/wakeup_fd_nospecial.cc',
'src/core/lib/iomgr/wakeup_fd_pipe.cc',
'src/core/lib/iomgr/wakeup_fd_posix.cc',
+ 'src/core/lib/json/json_object_loader.cc',
'src/core/lib/json/json_reader.cc',
'src/core/lib/json/json_util.cc',
'src/core/lib/json/json_writer.cc',
diff --git a/package.xml b/package.xml
index e8547b3..2a61574 100644
--- a/package.xml
+++ b/package.xml
@@ -1207,6 +1207,9 @@
<file baseinstalldir="/" name="src/core/lib/iomgr/wakeup_fd_posix.cc" role="src" />
<file baseinstalldir="/" name="src/core/lib/iomgr/wakeup_fd_posix.h" role="src" />
<file baseinstalldir="/" name="src/core/lib/json/json.h" role="src" />
+ <file baseinstalldir="/" name="src/core/lib/json/json_args.h" role="src" />
+ <file baseinstalldir="/" name="src/core/lib/json/json_object_loader.cc" role="src" />
+ <file baseinstalldir="/" name="src/core/lib/json/json_object_loader.h" role="src" />
<file baseinstalldir="/" name="src/core/lib/json/json_reader.cc" role="src" />
<file baseinstalldir="/" name="src/core/lib/json/json_util.cc" role="src" />
<file baseinstalldir="/" name="src/core/lib/json/json_util.h" role="src" />
diff --git a/src/core/lib/json/json_args.h b/src/core/lib/json/json_args.h
new file mode 100644
index 0000000..e975d33
--- /dev/null
+++ b/src/core/lib/json/json_args.h
@@ -0,0 +1,34 @@
+// Copyright 2020 gRPC authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef GRPC_CORE_LIB_JSON_JSON_ARGS_H
+#define GRPC_CORE_LIB_JSON_JSON_ARGS_H
+
+#include <grpc/support/port_platform.h>
+
+#include "absl/strings/string_view.h"
+
+namespace grpc_core {
+
+class JsonArgs {
+ public:
+ JsonArgs() = default;
+ virtual ~JsonArgs() = default;
+
+ virtual bool IsEnabled(absl::string_view /*key*/) const { return true; }
+};
+
+} // namespace grpc_core
+
+#endif // GRPC_CORE_LIB_JSON_JSON_ARGS_H
diff --git a/src/core/lib/json/json_channel_args.h b/src/core/lib/json/json_channel_args.h
new file mode 100644
index 0000000..668cb4a
--- /dev/null
+++ b/src/core/lib/json/json_channel_args.h
@@ -0,0 +1,42 @@
+// Copyright 2022 gRPC authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef GRPC_CORE_LIB_JSON_JSON_CHANNEL_ARGS_H
+#define GRPC_CORE_LIB_JSON_JSON_CHANNEL_ARGS_H
+
+#include <grpc/support/port_platform.h>
+
+#include "absl/strings/string_view.h"
+#include "absl/types/optional.h"
+
+#include "src/core/lib/channel/channel_args.h"
+#include "src/core/lib/json/json_args.h"
+
+namespace grpc_core {
+
+class JsonChannelArgs : public JsonArgs {
+ public:
+ explicit JsonChannelArgs(const ChannelArgs& args) : args_(args) {}
+
+ bool IsEnabled(absl::string_view key) const override {
+ return args_.GetBool(key).value_or(false);
+ }
+
+ private:
+ ChannelArgs args_;
+};
+
+} // namespace grpc_core
+
+#endif // GRPC_CORE_LIB_JSON_JSON_CHANNEL_ARGS_H
diff --git a/src/core/lib/json/json_object_loader.cc b/src/core/lib/json/json_object_loader.cc
new file mode 100644
index 0000000..ae32710
--- /dev/null
+++ b/src/core/lib/json/json_object_loader.cc
@@ -0,0 +1,204 @@
+// Copyright 2020 gRPC authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include <grpc/support/port_platform.h>
+
+#include "src/core/lib/json/json_object_loader.h"
+
+#include <algorithm>
+#include <utility>
+
+#include "absl/status/status.h"
+#include "absl/strings/ascii.h"
+#include "absl/strings/str_cat.h"
+#include "absl/strings/str_join.h"
+#include "absl/strings/strip.h"
+
+namespace grpc_core {
+
+void ErrorList::PushField(absl::string_view ext) {
+ // Skip leading '.' for top-level field names.
+ if (fields_.empty()) absl::ConsumePrefix(&ext, ".");
+ fields_.emplace_back(std::string(ext));
+}
+
+void ErrorList::PopField() { fields_.pop_back(); }
+
+void ErrorList::AddError(absl::string_view error) {
+ field_errors_[absl::StrJoin(fields_, "")].emplace_back(error);
+}
+
+bool ErrorList::FieldHasErrors() const {
+ return field_errors_.find(absl::StrJoin(fields_, "")) != field_errors_.end();
+}
+
+absl::Status ErrorList::status() const {
+ if (field_errors_.empty()) return absl::OkStatus();
+ std::vector<std::string> errors;
+ for (const auto& p : field_errors_) {
+ if (p.second.size() > 1) {
+ errors.emplace_back(absl::StrCat("field:", p.first, " errors:[",
+ absl::StrJoin(p.second, "; "), "]"));
+ } else {
+ errors.emplace_back(
+ absl::StrCat("field:", p.first, " error:", p.second[0]));
+ }
+ }
+ return absl::InvalidArgumentError(absl::StrCat(
+ "errors validating JSON: [", absl::StrJoin(errors, "; "), "]"));
+}
+
+namespace json_detail {
+
+void LoadScalar::LoadInto(const Json& json, const JsonArgs& /*args*/, void* dst,
+ ErrorList* errors) const {
+ // We accept either STRING or NUMBER for numeric values, as per
+ // https://developers.google.com/protocol-buffers/docs/proto3#json.
+ if (json.type() != Json::Type::STRING &&
+ (!IsNumber() || json.type() != Json::Type::NUMBER)) {
+ errors->AddError(
+ absl::StrCat("is not a ", IsNumber() ? "number" : "string"));
+ return;
+ }
+ return LoadInto(json.string_value(), dst, errors);
+}
+
+bool LoadString::IsNumber() const { return false; }
+
+void LoadString::LoadInto(const std::string& value, void* dst,
+ ErrorList*) const {
+ *static_cast<std::string*>(dst) = value;
+}
+
+bool LoadDuration::IsNumber() const { return false; }
+
+void LoadDuration::LoadInto(const std::string& value, void* dst,
+ ErrorList* errors) const {
+ absl::string_view buf(value);
+ if (!absl::ConsumeSuffix(&buf, "s")) {
+ errors->AddError("Not a duration (no s suffix)");
+ return;
+ }
+ buf = absl::StripAsciiWhitespace(buf);
+ auto decimal_point = buf.find('.');
+ int nanos = 0;
+ if (decimal_point != absl::string_view::npos) {
+ absl::string_view after_decimal = buf.substr(decimal_point + 1);
+ buf = buf.substr(0, decimal_point);
+ if (!absl::SimpleAtoi(after_decimal, &nanos)) {
+ errors->AddError("Not a duration (not a number of nanoseconds)");
+ return;
+ }
+ if (after_decimal.length() > 9) {
+ // We don't accept greater precision than nanos.
+ errors->AddError("Not a duration (too many digits after decimal)");
+ return;
+ }
+ for (size_t i = 0; i < (9 - after_decimal.length()); ++i) {
+ nanos *= 10;
+ }
+ }
+ int seconds;
+ if (!absl::SimpleAtoi(buf, &seconds)) {
+ errors->AddError("Not a duration (not a number of seconds)");
+ return;
+ }
+ *static_cast<Duration*>(dst) =
+ Duration::FromSecondsAndNanoseconds(seconds, nanos);
+}
+
+bool LoadNumber::IsNumber() const { return true; }
+
+void LoadBool::LoadInto(const Json& json, const JsonArgs&, void* dst,
+ ErrorList* errors) const {
+ if (json.type() == Json::Type::JSON_TRUE) {
+ *static_cast<bool*>(dst) = true;
+ } else if (json.type() == Json::Type::JSON_FALSE) {
+ *static_cast<bool*>(dst) = false;
+ } else {
+ errors->AddError("is not a boolean");
+ }
+}
+
+void LoadUnprocessedJsonObject::LoadInto(const Json& json, const JsonArgs&,
+ void* dst, ErrorList* errors) const {
+ if (json.type() != Json::Type::OBJECT) {
+ errors->AddError("is not an object");
+ return;
+ }
+ *static_cast<Json::Object*>(dst) = json.object_value();
+}
+
+void LoadVector::LoadInto(const Json& json, const JsonArgs& args, void* dst,
+ ErrorList* errors) const {
+ if (json.type() != Json::Type::ARRAY) {
+ errors->AddError("is not an array");
+ return;
+ }
+ const auto& array = json.array_value();
+ for (size_t i = 0; i < array.size(); ++i) {
+ ScopedField field(errors, absl::StrCat("[", i, "]"));
+ LoadOne(array[i], args, dst, errors);
+ }
+}
+
+void LoadMap::LoadInto(const Json& json, const JsonArgs& args, void* dst,
+ ErrorList* errors) const {
+ if (json.type() != Json::Type::OBJECT) {
+ errors->AddError("is not an object");
+ return;
+ }
+ for (const auto& pair : json.object_value()) {
+ ScopedField field(errors, absl::StrCat("[\"", pair.first, "\"]"));
+ LoadOne(pair.second, args, pair.first, dst, errors);
+ }
+}
+
+bool LoadObject(const Json& json, const JsonArgs& args, const Element* elements,
+ size_t num_elements, void* dst, ErrorList* errors) {
+ if (json.type() != Json::Type::OBJECT) {
+ errors->AddError("is not an object");
+ return false;
+ }
+ for (size_t i = 0; i < num_elements; ++i) {
+ const Element& element = elements[i];
+ if (element.enable_key != nullptr && !args.IsEnabled(element.enable_key)) {
+ continue;
+ }
+ ScopedField field(errors, absl::StrCat(".", element.name));
+ const auto& it = json.object_value().find(element.name);
+ if (it == json.object_value().end()) {
+ if (element.optional) continue;
+ errors->AddError("field not present");
+ continue;
+ }
+ char* field_dst = static_cast<char*>(dst) + element.member_offset;
+ element.loader->LoadInto(it->second, args, field_dst, errors);
+ }
+ return true;
+}
+
+const Json* GetJsonObjectField(const Json::Object& json,
+ absl::string_view field, ErrorList* errors,
+ bool required) {
+ auto it = json.find(std::string(field));
+ if (it == json.end()) {
+ if (required) errors->AddError("field not present");
+ return nullptr;
+ }
+ return &it->second;
+}
+
+} // namespace json_detail
+} // namespace grpc_core
diff --git a/src/core/lib/json/json_object_loader.h b/src/core/lib/json/json_object_loader.h
new file mode 100644
index 0000000..6ccdc2e
--- /dev/null
+++ b/src/core/lib/json/json_object_loader.h
@@ -0,0 +1,544 @@
+// Copyright 2020 gRPC authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef GRPC_CORE_LIB_JSON_JSON_OBJECT_LOADER_H
+#define GRPC_CORE_LIB_JSON_JSON_OBJECT_LOADER_H
+
+#include <grpc/support/port_platform.h>
+
+#include <cstdint>
+#include <cstring>
+#include <map>
+#include <string>
+#include <vector>
+
+#include "absl/meta/type_traits.h"
+#include "absl/status/status.h"
+#include "absl/status/statusor.h"
+#include "absl/strings/numbers.h"
+#include "absl/strings/str_cat.h"
+#include "absl/strings/string_view.h"
+#include "absl/types/optional.h"
+
+#include "src/core/lib/gprpp/time.h"
+#include "src/core/lib/json/json.h"
+#include "src/core/lib/json/json_args.h"
+
+// Provides a means to load JSON objects into C++ objects, with the aim of
+// minimizing object code size.
+//
+// Usage:
+// Given struct Foo:
+// struct Foo {
+// int a;
+// int b;
+// };
+// We add a static JsonLoader() method to Foo to declare how to load the
+// object from JSON, and an optional JsonPostLoad() method to do any
+// necessary post-processing:
+// struct Foo {
+// int a;
+// int b;
+// static const JsonLoaderInterface* JsonLoader() {
+// // Note: Field names must be string constants; they are not copied.
+// static const auto* loader = JsonObjectLoader<Foo>()
+// .Field("a", &Foo::a)
+// .Field("b", &Foo::b)
+// .Finish();
+// return loader;
+// }
+// // Optional; omit if no post-processing needed.
+// void JsonPostLoad(const Json& source, ErrorList* errors) { ++a; }
+// };
+// Now we can load Foo objects from JSON:
+// absl::StatusOr<Foo> foo = LoadFromJson<Foo>(json);
+namespace grpc_core {
+
+// A list of errors that occurred during JSON parsing.
+// If a non-empty list occurs during parsing, the parsing failed.
+class ErrorList {
+ public:
+ // Record that we're reading some field.
+ void PushField(absl::string_view ext) GPR_ATTRIBUTE_NOINLINE;
+ // Record that we've finished reading that field.
+ void PopField() GPR_ATTRIBUTE_NOINLINE;
+
+ // Record that we've encountered an error.
+ void AddError(absl::string_view error) GPR_ATTRIBUTE_NOINLINE;
+ // Returns true if the current field has errors.
+ bool FieldHasErrors() const GPR_ATTRIBUTE_NOINLINE;
+
+ // Returns the resulting status of parsing.
+ absl::Status status() const;
+
+ // Return true if there are no errors.
+ bool ok() const { return field_errors_.empty(); }
+
+ size_t size() const { return field_errors_.size(); }
+
+ private:
+ // TODO(roth): If we don't actually have any fields for which we
+ // report more than one error, simplify this data structure.
+ std::map<std::string /*field_name*/, std::vector<std::string>> field_errors_;
+ std::vector<std::string> fields_;
+};
+
+// Note that we're reading a field, and remove it at the end of the scope.
+class ScopedField {
+ public:
+ ScopedField(ErrorList* error_list, absl::string_view field_name)
+ : error_list_(error_list) {
+ error_list_->PushField(field_name);
+ }
+ ~ScopedField() { error_list_->PopField(); }
+
+ private:
+ ErrorList* error_list_;
+};
+
+namespace json_detail {
+
+// An un-typed JSON loader.
+class LoaderInterface {
+ public:
+ // Convert json value to whatever type we're loading at dst.
+ // If errors occur, add them to error_list.
+ virtual void LoadInto(const Json& json, const JsonArgs& args, void* dst,
+ ErrorList* errors) const = 0;
+
+ protected:
+ virtual ~LoaderInterface() = default;
+};
+
+// Loads a scalar (string or number).
+class LoadScalar : public LoaderInterface {
+ public:
+ void LoadInto(const Json& json, const JsonArgs& args, void* dst,
+ ErrorList* errors) const override;
+
+ protected:
+ ~LoadScalar() override = default;
+
+ private:
+ // true if we're loading a number, false if we're loading a string.
+ // We use a virtual function to store this decision in a vtable instead of
+ // needing an instance variable.
+ virtual bool IsNumber() const = 0;
+
+ virtual void LoadInto(const std::string& json, void* dst,
+ ErrorList* errors) const = 0;
+};
+
+// Load a string.
+class LoadString : public LoadScalar {
+ protected:
+ ~LoadString() override = default;
+
+ private:
+ bool IsNumber() const override;
+ void LoadInto(const std::string& value, void* dst,
+ ErrorList* errors) const override;
+};
+
+// Load a Duration.
+class LoadDuration : public LoadScalar {
+ protected:
+ ~LoadDuration() override = default;
+
+ private:
+ bool IsNumber() const override;
+ void LoadInto(const std::string& value, void* dst,
+ ErrorList* errors) const override;
+};
+
+// Load a number.
+class LoadNumber : public LoadScalar {
+ protected:
+ ~LoadNumber() override = default;
+
+ private:
+ bool IsNumber() const override;
+};
+
+// Load a signed number of type T.
+template <typename T>
+class TypedLoadSignedNumber : public LoadNumber {
+ protected:
+ ~TypedLoadSignedNumber() override = default;
+
+ private:
+ void LoadInto(const std::string& value, void* dst,
+ ErrorList* errors) const override {
+ if (!absl::SimpleAtoi(value, static_cast<T*>(dst))) {
+ errors->AddError("failed to parse number");
+ }
+ }
+};
+
+// Load an unsigned number of type T.
+template <typename T>
+class TypedLoadUnsignedNumber : public LoadNumber {
+ protected:
+ ~TypedLoadUnsignedNumber() override = default;
+
+ private:
+ void LoadInto(const std::string& value, void* dst,
+ ErrorList* errors) const override {
+ if (!absl::SimpleAtoi(value, static_cast<T*>(dst))) {
+ errors->AddError("failed to parse non-negative number");
+ }
+ }
+};
+
+// Load a float.
+class LoadFloat : public LoadNumber {
+ protected:
+ ~LoadFloat() override = default;
+
+ private:
+ void LoadInto(const std::string& value, void* dst,
+ ErrorList* errors) const override {
+ if (!absl::SimpleAtof(value, static_cast<float*>(dst))) {
+ errors->AddError("failed to parse floating-point number");
+ }
+ }
+};
+
+// Load a double.
+class LoadDouble : public LoadNumber {
+ protected:
+ ~LoadDouble() override = default;
+
+ private:
+ void LoadInto(const std::string& value, void* dst,
+ ErrorList* errors) const override {
+ if (!absl::SimpleAtod(value, static_cast<double*>(dst))) {
+ errors->AddError("failed to parse floating-point number");
+ }
+ }
+};
+
+// Load a bool.
+class LoadBool : public LoaderInterface {
+ public:
+ void LoadInto(const Json& json, const JsonArgs& /*args*/, void* dst,
+ ErrorList* errors) const override;
+};
+
+// Loads an unprocessed JSON object value.
+class LoadUnprocessedJsonObject : public LoaderInterface {
+ public:
+ void LoadInto(const Json& json, const JsonArgs& /*args*/, void* dst,
+ ErrorList* errors) const override;
+};
+
+// Load a vector of some type.
+class LoadVector : public LoaderInterface {
+ public:
+ void LoadInto(const Json& json, const JsonArgs& args, void* dst,
+ ErrorList* errors) const override;
+
+ protected:
+ ~LoadVector() override = default;
+
+ private:
+ virtual void LoadOne(const Json& json, const JsonArgs& args, void* dst,
+ ErrorList* errors) const = 0;
+};
+
+// Load a map of string->some type.
+class LoadMap : public LoaderInterface {
+ public:
+ void LoadInto(const Json& json, const JsonArgs& args, void* dst,
+ ErrorList* errors) const override;
+
+ protected:
+ ~LoadMap() override = default;
+
+ private:
+ virtual void LoadOne(const Json& json, const JsonArgs& args,
+ const std::string& name, void* dst,
+ ErrorList* errors) const = 0;
+};
+
+// Fetch a LoaderInterface for some type.
+template <typename T>
+const LoaderInterface* LoaderForType();
+
+// AutoLoader implements LoaderInterface for a type.
+// The default asks the type for its LoaderInterface and then uses that.
+// Classes that load from objects should provide a:
+// static const JsonLoaderInterface* JsonLoader();
+template <typename T>
+class AutoLoader final : public LoaderInterface {
+ public:
+ void LoadInto(const Json& json, const JsonArgs& args, void* dst,
+ ErrorList* errors) const override {
+ T::JsonLoader(args)->LoadInto(json, args, dst, errors);
+ }
+};
+
+// Specializations of AutoLoader for basic types.
+template <>
+class AutoLoader<std::string> final : public LoadString {};
+template <>
+class AutoLoader<Duration> final : public LoadDuration {};
+template <>
+class AutoLoader<int32_t> final : public TypedLoadSignedNumber<int32_t> {};
+template <>
+class AutoLoader<int64_t> final : public TypedLoadSignedNumber<int64_t> {};
+template <>
+class AutoLoader<uint32_t> final : public TypedLoadUnsignedNumber<uint32_t> {};
+template <>
+class AutoLoader<uint64_t> final : public TypedLoadUnsignedNumber<uint64_t> {};
+template <>
+class AutoLoader<float> final : public LoadFloat {};
+template <>
+class AutoLoader<double> final : public LoadDouble {};
+template <>
+class AutoLoader<bool> final : public LoadBool {};
+template <>
+class AutoLoader<Json::Object> final : public LoadUnprocessedJsonObject {};
+
+// Specializations of AutoLoader for vectors.
+template <typename T>
+class AutoLoader<std::vector<T>> final : public LoadVector {
+ private:
+ void LoadOne(const Json& json, const JsonArgs& args, void* dst,
+ ErrorList* errors) const final {
+ auto* vec = static_cast<std::vector<T>*>(dst);
+ T value{};
+ LoaderForType<T>()->LoadInto(json, args, &value, errors);
+ vec->push_back(std::move(value));
+ }
+};
+
+// Specializations of AutoLoader for maps.
+template <typename T>
+class AutoLoader<std::map<std::string, T>> final : public LoadMap {
+ private:
+ void LoadOne(const Json& json, const JsonArgs& args, const std::string& name,
+ void* dst, ErrorList* errors) const final {
+ auto* map = static_cast<std::map<std::string, T>*>(dst);
+ T value{};
+ LoaderForType<T>()->LoadInto(json, args, &value, errors);
+ map->emplace(name, std::move(value));
+ }
+};
+
+// Specializations of AutoLoader for absl::optional<>.
+template <typename T>
+class AutoLoader<absl::optional<T>> final : public LoaderInterface {
+ public:
+ void LoadInto(const Json& json, const JsonArgs& args, void* dst,
+ ErrorList* errors) const override {
+ if (json.type() == Json::Type::JSON_NULL) return;
+ auto* opt = static_cast<absl::optional<T>*>(dst);
+ opt->emplace();
+ LoaderForType<T>()->LoadInto(json, args, &**opt, errors);
+ }
+};
+
+// Implementation of aforementioned LoaderForType.
+// Simply keeps a static AutoLoader<T> and returns a pointer to that.
+template <typename T>
+const LoaderInterface* LoaderForType() {
+ static const auto* loader = new AutoLoader<T>();
+ return loader;
+}
+
+// Element describes one typed field to be loaded from a JSON object.
+struct Element {
+ Element() = default;
+ template <typename A, typename B>
+ Element(const char* name, bool optional, B A::*p,
+ const LoaderInterface* loader, const char* enable_key)
+ : loader(loader),
+ member_offset(static_cast<uint16_t>(
+ reinterpret_cast<uintptr_t>(&(static_cast<A*>(nullptr)->*p)))),
+ optional(optional),
+ name(name),
+ enable_key(enable_key) {}
+ // The loader for this field.
+ const LoaderInterface* loader;
+ // Offset into the destination object to store the field.
+ uint16_t member_offset;
+ // Is this field optional?
+ bool optional;
+ // The name of the field.
+ const char* name;
+ // The key to use with JsonArgs to see if this field is enabled.
+ const char* enable_key;
+};
+
+// Vec<T, kSize> provides a constant array type that can be appended to by
+// copying. It's setup so that most compilers can optimize away all of its
+// operations.
+template <typename T, size_t kSize>
+class Vec {
+ public:
+ Vec(const Vec<T, kSize - 1>& other, const T& new_value) {
+ for (size_t i = 0; i < other.size(); i++) values_[i] = other.data()[i];
+ values_[kSize - 1] = new_value;
+ }
+
+ const T* data() const { return values_; }
+ size_t size() const { return kSize; }
+
+ private:
+ T values_[kSize];
+};
+
+template <typename T>
+class Vec<T, 0> {
+ public:
+ const T* data() const { return nullptr; }
+ size_t size() const { return 0; }
+};
+
+// Given a list of elements, and a destination object, load the elements into
+// the object from some parsed JSON.
+// Returns false if the JSON object was not of type Json::Type::OBJECT.
+bool LoadObject(const Json& json, const JsonArgs& args, const Element* elements,
+ size_t num_elements, void* dst, ErrorList* errors);
+
+// Adaptor type - takes a compile time computed list of elements and implements
+// LoaderInterface by calling LoadObject.
+template <typename T, size_t kElemCount, typename Hidden = void>
+class FinishedJsonObjectLoader final : public LoaderInterface {
+ public:
+ explicit FinishedJsonObjectLoader(const Vec<Element, kElemCount>& elements)
+ : elements_(elements) {}
+
+ void LoadInto(const Json& json, const JsonArgs& args, void* dst,
+ ErrorList* errors) const override {
+ LoadObject(json, args, elements_.data(), elements_.size(), dst, errors);
+ }
+
+ private:
+ GPR_NO_UNIQUE_ADDRESS Vec<Element, kElemCount> elements_;
+};
+
+// Specialization for when the object has a JsonPostLoad function exposed.
+template <typename T, size_t kElemCount>
+class FinishedJsonObjectLoader<T, kElemCount,
+ absl::void_t<decltype(&T::JsonPostLoad)>>
+ final : public LoaderInterface {
+ public:
+ explicit FinishedJsonObjectLoader(const Vec<Element, kElemCount>& elements)
+ : elements_(elements) {}
+
+ void LoadInto(const Json& json, const JsonArgs& args, void* dst,
+ ErrorList* errors) const override {
+ // Call JsonPostLoad() only if json is a JSON object.
+ if (LoadObject(json, args, elements_.data(), elements_.size(), dst,
+ errors)) {
+ static_cast<T*>(dst)->JsonPostLoad(json, args, errors);
+ }
+ }
+
+ private:
+ GPR_NO_UNIQUE_ADDRESS Vec<Element, kElemCount> elements_;
+};
+
+// Builder type for JSON object loaders.
+// Concatenate fields with Field, OptionalField, and then call Finish to obtain
+// an object that implements LoaderInterface.
+template <typename T, size_t kElemCount = 0>
+class JsonObjectLoader final {
+ public:
+ JsonObjectLoader() {
+ static_assert(kElemCount == 0,
+ "Only initial loader step can have kElemCount==0.");
+ }
+
+ FinishedJsonObjectLoader<T, kElemCount>* Finish() const {
+ return new FinishedJsonObjectLoader<T, kElemCount>(elements_);
+ }
+
+ template <typename U>
+ JsonObjectLoader<T, kElemCount + 1> Field(
+ const char* name, U T::*p, const char* enable_key = nullptr) const {
+ return Field(name, false, p, enable_key);
+ }
+
+ template <typename U>
+ JsonObjectLoader<T, kElemCount + 1> OptionalField(
+ const char* name, U T::*p, const char* enable_key = nullptr) const {
+ return Field(name, true, p, enable_key);
+ }
+
+ JsonObjectLoader(const Vec<Element, kElemCount - 1>& elements,
+ Element new_element)
+ : elements_(elements, new_element) {}
+
+ private:
+ template <typename U>
+ JsonObjectLoader<T, kElemCount + 1> Field(const char* name, bool optional,
+ U T::*p,
+ const char* enable_key) const {
+ return JsonObjectLoader<T, kElemCount + 1>(
+ elements_, Element(name, optional, p, LoaderForType<U>(), enable_key));
+ }
+
+ GPR_NO_UNIQUE_ADDRESS Vec<Element, kElemCount> elements_;
+};
+
+const Json* GetJsonObjectField(const Json::Object& json,
+ absl::string_view field, ErrorList* errors,
+ bool required);
+
+} // namespace json_detail
+
+template <typename T>
+using JsonObjectLoader = json_detail::JsonObjectLoader<T>;
+
+using JsonLoaderInterface = json_detail::LoaderInterface;
+
+template <typename T>
+absl::StatusOr<T> LoadFromJson(const Json& json,
+ const JsonArgs& args = JsonArgs()) {
+ ErrorList error_list;
+ T result{};
+ json_detail::LoaderForType<T>()->LoadInto(json, args, &result, &error_list);
+ if (!error_list.ok()) return error_list.status();
+ return std::move(result);
+}
+
+template <typename T>
+T LoadFromJson(const Json& json, const JsonArgs& args, ErrorList* error_list) {
+ T result{};
+ json_detail::LoaderForType<T>()->LoadInto(json, args, &result, error_list);
+ return result;
+}
+
+template <typename T>
+absl::optional<T> LoadJsonObjectField(const Json::Object& json,
+ const JsonArgs& args,
+ absl::string_view field,
+ ErrorList* errors, bool required = true) {
+ ScopedField error_field(errors, absl::StrCat(".", field));
+ const Json* field_json =
+ json_detail::GetJsonObjectField(json, field, errors, required);
+ if (field_json == nullptr) return absl::nullopt;
+ T result{};
+ size_t starting_error_size = errors->size();
+ json_detail::LoaderForType<T>()->LoadInto(*field_json, args, &result, errors);
+ if (errors->size() > starting_error_size) return absl::nullopt;
+ return std::move(result);
+}
+
+} // namespace grpc_core
+
+#endif // GRPC_CORE_LIB_JSON_JSON_OBJECT_LOADER_H
diff --git a/src/core/lib/json/json_util.cc b/src/core/lib/json/json_util.cc
index 0d1b5ce..451a67f 100644
--- a/src/core/lib/json/json_util.cc
+++ b/src/core/lib/json/json_util.cc
@@ -20,46 +20,17 @@
#include "src/core/lib/json/json_util.h"
-#include <string.h>
-
-#include <grpc/support/string_util.h>
-
-#include "src/core/lib/gpr/string.h"
-#include "src/core/lib/gprpp/memory.h"
+#include "src/core/lib/json/json_args.h"
+#include "src/core/lib/json/json_object_loader.h"
namespace grpc_core {
bool ParseDurationFromJson(const Json& field, Duration* duration) {
- if (field.type() != Json::Type::STRING) return false;
- size_t len = field.string_value().size();
- if (field.string_value()[len - 1] != 's') return false;
- if (field.string_value() == Duration::Infinity().ToJsonString()) {
- *duration = Duration::Infinity();
- return true;
- }
- UniquePtr<char> buf(gpr_strdup(field.string_value().c_str()));
- *(buf.get() + len - 1) = '\0'; // Remove trailing 's'.
- char* decimal_point = strchr(buf.get(), '.');
- int nanos = 0;
- if (decimal_point != nullptr) {
- *decimal_point = '\0';
- nanos = gpr_parse_nonnegative_int(decimal_point + 1);
- if (nanos == -1) {
- return false;
- }
- int num_digits = static_cast<int>(strlen(decimal_point + 1));
- if (num_digits > 9) { // We don't accept greater precision than nanos.
- return false;
- }
- for (int i = 0; i < (9 - num_digits); ++i) {
- nanos *= 10;
- }
- }
- int seconds =
- decimal_point == buf.get() ? 0 : gpr_parse_nonnegative_int(buf.get());
- if (seconds == -1) return false;
- *duration = Duration::FromSecondsAndNanoseconds(seconds, nanos);
- return true;
+ json_detail::AutoLoader<Duration> loader;
+ ErrorList errors;
+ static_cast<json_detail::LoaderInterface&>(loader).LoadInto(
+ field, JsonArgs(), duration, &errors);
+ return errors.ok();
}
bool ExtractJsonBool(const Json& json, absl::string_view field_name,
diff --git a/src/python/grpcio/grpc_core_dependencies.py b/src/python/grpcio/grpc_core_dependencies.py
index b4a4373..854bdb9 100644
--- a/src/python/grpcio/grpc_core_dependencies.py
+++ b/src/python/grpcio/grpc_core_dependencies.py
@@ -582,6 +582,7 @@
'src/core/lib/iomgr/wakeup_fd_nospecial.cc',
'src/core/lib/iomgr/wakeup_fd_pipe.cc',
'src/core/lib/iomgr/wakeup_fd_posix.cc',
+ 'src/core/lib/json/json_object_loader.cc',
'src/core/lib/json/json_reader.cc',
'src/core/lib/json/json_util.cc',
'src/core/lib/json/json_writer.cc',
diff --git a/test/core/json/BUILD b/test/core/json/BUILD
index 7f704c3..7eafc56 100644
--- a/test/core/json/BUILD
+++ b/test/core/json/BUILD
@@ -47,3 +47,17 @@
"//test/core/util:grpc_test_util",
],
)
+
+grpc_cc_test(
+ name = "json_object_loader_test",
+ srcs = ["json_object_loader_test.cc"],
+ external_deps = [
+ "gtest",
+ ],
+ language = "C++",
+ uses_polling = False,
+ deps = [
+ "//:json_object_loader",
+ "//test/core/util:grpc_test_util",
+ ],
+)
diff --git a/test/core/json/json_object_loader_test.cc b/test/core/json/json_object_loader_test.cc
new file mode 100644
index 0000000..10b8fb5
--- /dev/null
+++ b/test/core/json/json_object_loader_test.cc
@@ -0,0 +1,984 @@
+// Copyright 2021 gRPC authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/core/lib/json/json_object_loader.h"
+
+#include <cstdint>
+
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+#include "absl/strings/str_join.h"
+
+namespace grpc_core {
+namespace {
+
+template <typename T>
+absl::StatusOr<T> Parse(absl::string_view json,
+ const JsonArgs& args = JsonArgs()) {
+ auto parsed = Json::Parse(json);
+ if (!parsed.ok()) return parsed.status();
+ return LoadFromJson<T>(*parsed, args);
+}
+
+//
+// Signed integer tests
+//
+
+template <typename T>
+class SignedIntegerTest : public ::testing::Test {};
+
+TYPED_TEST_SUITE_P(SignedIntegerTest);
+
+TYPED_TEST_P(SignedIntegerTest, IntegerFields) {
+ struct TestStruct {
+ TypeParam value = 0;
+ TypeParam optional_value = 0;
+ absl::optional<TypeParam> absl_optional_value;
+
+ static const JsonLoaderInterface* JsonLoader(const JsonArgs&) {
+ static const auto* loader =
+ JsonObjectLoader<TestStruct>()
+ .Field("value", &TestStruct::value)
+ .OptionalField("optional_value", &TestStruct::optional_value)
+ .OptionalField("absl_optional_value",
+ &TestStruct::absl_optional_value)
+ .Finish();
+ return loader;
+ }
+ };
+ // Positive number.
+ auto test_struct = Parse<TestStruct>("{\"value\": 5}");
+ ASSERT_TRUE(test_struct.ok()) << test_struct.status();
+ EXPECT_EQ(test_struct->value, 5);
+ EXPECT_EQ(test_struct->optional_value, 0);
+ EXPECT_FALSE(test_struct->absl_optional_value.has_value());
+ // Negative number.
+ test_struct = Parse<TestStruct>("{\"value\": -5}");
+ ASSERT_TRUE(test_struct.ok()) << test_struct.status();
+ EXPECT_EQ(test_struct->value, -5);
+ EXPECT_EQ(test_struct->optional_value, 0);
+ EXPECT_FALSE(test_struct->absl_optional_value.has_value());
+ // Encoded in a JSON string.
+ test_struct = Parse<TestStruct>("{\"value\": \"5\"}");
+ ASSERT_TRUE(test_struct.ok()) << test_struct.status();
+ EXPECT_EQ(test_struct->value, 5);
+ EXPECT_EQ(test_struct->optional_value, 0);
+ EXPECT_FALSE(test_struct->absl_optional_value.has_value());
+ // Fails if required field is not present.
+ test_struct = Parse<TestStruct>("{}");
+ EXPECT_EQ(test_struct.status().code(), absl::StatusCode::kInvalidArgument);
+ EXPECT_EQ(test_struct.status().message(),
+ "errors validating JSON: [field:value error:field not present]")
+ << test_struct.status();
+ // Optional fields present.
+ test_struct = Parse<TestStruct>(
+ "{\"value\": 5, \"optional_value\": 7, "
+ "\"absl_optional_value\": 9}");
+ ASSERT_TRUE(test_struct.ok()) << test_struct.status();
+ EXPECT_EQ(test_struct->value, 5);
+ EXPECT_EQ(test_struct->optional_value, 7);
+ EXPECT_EQ(test_struct->absl_optional_value, 9);
+ // Wrong JSON type.
+ test_struct = Parse<TestStruct>(
+ "{\"value\": [], \"optional_value\": {}, "
+ "\"absl_optional_value\": true}");
+ EXPECT_EQ(test_struct.status().code(), absl::StatusCode::kInvalidArgument);
+ EXPECT_EQ(test_struct.status().message(),
+ "errors validating JSON: ["
+ "field:absl_optional_value error:is not a number; "
+ "field:optional_value error:is not a number; "
+ "field:value error:is not a number]")
+ << test_struct.status();
+}
+
+REGISTER_TYPED_TEST_SUITE_P(SignedIntegerTest, IntegerFields);
+
+using IntegerTypes = ::testing::Types<int32_t, int64_t>;
+INSTANTIATE_TYPED_TEST_SUITE_P(My, SignedIntegerTest, IntegerTypes);
+
+//
+// Unsigned integer tests
+//
+
+template <typename T>
+class UnsignedIntegerTest : public ::testing::Test {};
+
+TYPED_TEST_SUITE_P(UnsignedIntegerTest);
+
+TYPED_TEST_P(UnsignedIntegerTest, IntegerFields) {
+ struct TestStruct {
+ TypeParam value = 0;
+ TypeParam optional_value = 0;
+ absl::optional<TypeParam> absl_optional_value;
+
+ static const JsonLoaderInterface* JsonLoader(const JsonArgs&) {
+ static const auto* loader =
+ JsonObjectLoader<TestStruct>()
+ .Field("value", &TestStruct::value)
+ .OptionalField("optional_value", &TestStruct::optional_value)
+ .OptionalField("absl_optional_value",
+ &TestStruct::absl_optional_value)
+ .Finish();
+ return loader;
+ }
+ };
+ // Positive number.
+ auto test_struct = Parse<TestStruct>("{\"value\": 5}");
+ ASSERT_TRUE(test_struct.ok()) << test_struct.status();
+ EXPECT_EQ(test_struct->value, 5);
+ EXPECT_EQ(test_struct->optional_value, 0);
+ EXPECT_FALSE(test_struct->absl_optional_value.has_value());
+ // Negative number.
+ test_struct = Parse<TestStruct>("{\"value\": -5}");
+ EXPECT_EQ(test_struct.status().code(), absl::StatusCode::kInvalidArgument);
+ EXPECT_EQ(test_struct.status().message(),
+ "errors validating JSON: ["
+ "field:value error:failed to parse non-negative number]")
+ << test_struct.status();
+ // Encoded in a JSON string.
+ test_struct = Parse<TestStruct>("{\"value\": \"5\"}");
+ ASSERT_TRUE(test_struct.ok()) << test_struct.status();
+ EXPECT_EQ(test_struct->value, 5);
+ EXPECT_EQ(test_struct->optional_value, 0);
+ EXPECT_FALSE(test_struct->absl_optional_value.has_value());
+ // Fails if required field is not present.
+ test_struct = Parse<TestStruct>("{}");
+ EXPECT_EQ(test_struct.status().code(), absl::StatusCode::kInvalidArgument);
+ EXPECT_EQ(test_struct.status().message(),
+ "errors validating JSON: [field:value error:field not present]")
+ << test_struct.status();
+ // Optional fields present.
+ test_struct = Parse<TestStruct>(
+ "{\"value\": 5, \"optional_value\": 7, "
+ "\"absl_optional_value\": 9}");
+ ASSERT_TRUE(test_struct.ok()) << test_struct.status();
+ EXPECT_EQ(test_struct->value, 5);
+ EXPECT_EQ(test_struct->optional_value, 7);
+ ASSERT_TRUE(test_struct->absl_optional_value.has_value());
+ EXPECT_EQ(*test_struct->absl_optional_value, 9);
+ // Wrong JSON type.
+ test_struct = Parse<TestStruct>(
+ "{\"value\": [], \"optional_value\": {}, "
+ "\"absl_optional_value\": true}");
+ EXPECT_EQ(test_struct.status().code(), absl::StatusCode::kInvalidArgument);
+ EXPECT_EQ(test_struct.status().message(),
+ "errors validating JSON: ["
+ "field:absl_optional_value error:is not a number; "
+ "field:optional_value error:is not a number; "
+ "field:value error:is not a number]")
+ << test_struct.status();
+}
+
+REGISTER_TYPED_TEST_SUITE_P(UnsignedIntegerTest, IntegerFields);
+
+using UnsignedIntegerTypes = ::testing::Types<uint32_t, uint64_t>;
+INSTANTIATE_TYPED_TEST_SUITE_P(My, UnsignedIntegerTest, UnsignedIntegerTypes);
+
+//
+// Floating-point tests
+//
+
+template <typename T>
+class FloatingPointTest : public ::testing::Test {};
+
+TYPED_TEST_SUITE_P(FloatingPointTest);
+
+TYPED_TEST_P(FloatingPointTest, FloatFields) {
+ struct TestStruct {
+ TypeParam value = 0;
+ TypeParam optional_value = 0;
+ absl::optional<TypeParam> absl_optional_value;
+
+ static const JsonLoaderInterface* JsonLoader(const JsonArgs&) {
+ static const auto* loader =
+ JsonObjectLoader<TestStruct>()
+ .Field("value", &TestStruct::value)
+ .OptionalField("optional_value", &TestStruct::optional_value)
+ .OptionalField("absl_optional_value",
+ &TestStruct::absl_optional_value)
+ .Finish();
+ return loader;
+ }
+ };
+ // Positive number.
+ auto test_struct = Parse<TestStruct>("{\"value\": 5.2}");
+ ASSERT_TRUE(test_struct.ok()) << test_struct.status();
+ EXPECT_NEAR(test_struct->value, 5.2, 0.0001);
+ EXPECT_EQ(test_struct->optional_value, 0);
+ EXPECT_FALSE(test_struct->absl_optional_value.has_value());
+ // Negative number.
+ test_struct = Parse<TestStruct>("{\"value\": -5.2}");
+ ASSERT_TRUE(test_struct.ok()) << test_struct.status();
+ EXPECT_NEAR(test_struct->value, -5.2, 0.0001);
+ EXPECT_EQ(test_struct->optional_value, 0);
+ EXPECT_FALSE(test_struct->absl_optional_value.has_value());
+ // Encoded in a JSON string.
+ test_struct = Parse<TestStruct>("{\"value\": \"5.2\"}");
+ ASSERT_TRUE(test_struct.ok()) << test_struct.status();
+ EXPECT_NEAR(test_struct->value, 5.2, 0.0001);
+ EXPECT_EQ(test_struct->optional_value, 0);
+ EXPECT_FALSE(test_struct->absl_optional_value.has_value());
+ // Fails if required field is not present.
+ test_struct = Parse<TestStruct>("{}");
+ EXPECT_EQ(test_struct.status().code(), absl::StatusCode::kInvalidArgument);
+ EXPECT_EQ(test_struct.status().message(),
+ "errors validating JSON: [field:value error:field not present]")
+ << test_struct.status();
+ // Optional fields present.
+ test_struct = Parse<TestStruct>(
+ "{\"value\": 5.2, \"optional_value\": 7.5, "
+ "\"absl_optional_value\": 9.8}");
+ ASSERT_TRUE(test_struct.ok()) << test_struct.status();
+ EXPECT_NEAR(test_struct->value, 5.2, 0.0001);
+ EXPECT_NEAR(test_struct->optional_value, 7.5, 0.0001);
+ ASSERT_TRUE(test_struct->absl_optional_value.has_value());
+ EXPECT_NEAR(*test_struct->absl_optional_value, 9.8, 0.0001);
+ // Wrong JSON type.
+ test_struct = Parse<TestStruct>(
+ "{\"value\": [], \"optional_value\": {}, "
+ "\"absl_optional_value\": true}");
+ EXPECT_EQ(test_struct.status().code(), absl::StatusCode::kInvalidArgument);
+ EXPECT_EQ(test_struct.status().message(),
+ "errors validating JSON: ["
+ "field:absl_optional_value error:is not a number; "
+ "field:optional_value error:is not a number; "
+ "field:value error:is not a number]")
+ << test_struct.status();
+}
+
+REGISTER_TYPED_TEST_SUITE_P(FloatingPointTest, FloatFields);
+
+using FloatingPointTypes = ::testing::Types<float, double>;
+INSTANTIATE_TYPED_TEST_SUITE_P(My, FloatingPointTest, FloatingPointTypes);
+
+//
+// Boolean tests
+//
+
+TEST(JsonObjectLoader, BooleanFields) {
+ struct TestStruct {
+ bool value = false;
+ bool optional_value = true;
+ absl::optional<bool> absl_optional_value;
+
+ static const JsonLoaderInterface* JsonLoader(const JsonArgs&) {
+ static const auto* loader =
+ JsonObjectLoader<TestStruct>()
+ .Field("value", &TestStruct::value)
+ .OptionalField("optional_value", &TestStruct::optional_value)
+ .OptionalField("absl_optional_value",
+ &TestStruct::absl_optional_value)
+ .Finish();
+ return loader;
+ }
+ };
+ // True.
+ auto test_struct = Parse<TestStruct>("{\"value\": true}");
+ ASSERT_TRUE(test_struct.ok()) << test_struct.status();
+ EXPECT_EQ(test_struct->value, true);
+ EXPECT_EQ(test_struct->optional_value, true); // Unmodified.
+ EXPECT_FALSE(test_struct->absl_optional_value.has_value());
+ // False.
+ test_struct = Parse<TestStruct>("{\"value\": false}");
+ ASSERT_TRUE(test_struct.ok()) << test_struct.status();
+ EXPECT_EQ(test_struct->value, false);
+ EXPECT_EQ(test_struct->optional_value, true); // Unmodified.
+ EXPECT_FALSE(test_struct->absl_optional_value.has_value());
+ // Fails if required field is not present.
+ test_struct = Parse<TestStruct>("{}");
+ EXPECT_EQ(test_struct.status().code(), absl::StatusCode::kInvalidArgument);
+ EXPECT_EQ(test_struct.status().message(),
+ "errors validating JSON: [field:value error:field not present]")
+ << test_struct.status();
+ // Optional fields present.
+ test_struct = Parse<TestStruct>(
+ "{\"value\": true, \"optional_value\": false,"
+ "\"absl_optional_value\": true}");
+ ASSERT_TRUE(test_struct.ok()) << test_struct.status();
+ EXPECT_EQ(test_struct->value, true);
+ EXPECT_EQ(test_struct->optional_value, false);
+ EXPECT_EQ(test_struct->absl_optional_value, true);
+ // Wrong JSON type.
+ test_struct = Parse<TestStruct>(
+ "{\"value\": [], \"optional_value\": {}, "
+ "\"absl_optional_value\": 1}");
+ EXPECT_EQ(test_struct.status().code(), absl::StatusCode::kInvalidArgument);
+ EXPECT_EQ(test_struct.status().message(),
+ "errors validating JSON: ["
+ "field:absl_optional_value error:is not a boolean; "
+ "field:optional_value error:is not a boolean; "
+ "field:value error:is not a boolean]")
+ << test_struct.status();
+}
+
+//
+// String tests
+//
+
+TEST(JsonObjectLoader, StringFields) {
+ struct TestStruct {
+ std::string value;
+ std::string optional_value;
+ absl::optional<std::string> absl_optional_value;
+
+ static const JsonLoaderInterface* JsonLoader(const JsonArgs&) {
+ static const auto* loader =
+ JsonObjectLoader<TestStruct>()
+ .Field("value", &TestStruct::value)
+ .OptionalField("optional_value", &TestStruct::optional_value)
+ .OptionalField("absl_optional_value",
+ &TestStruct::absl_optional_value)
+ .Finish();
+ return loader;
+ }
+ };
+ // Valid string.
+ auto test_struct = Parse<TestStruct>("{\"value\": \"foo\"}");
+ ASSERT_TRUE(test_struct.ok()) << test_struct.status();
+ EXPECT_EQ(test_struct->value, "foo");
+ EXPECT_EQ(test_struct->optional_value, "");
+ EXPECT_FALSE(test_struct->absl_optional_value.has_value());
+ // Fails if required field is not present.
+ test_struct = Parse<TestStruct>("{}");
+ EXPECT_EQ(test_struct.status().code(), absl::StatusCode::kInvalidArgument);
+ EXPECT_EQ(test_struct.status().message(),
+ "errors validating JSON: [field:value error:field not present]")
+ << test_struct.status();
+ // Optional fields present.
+ test_struct = Parse<TestStruct>(
+ "{\"value\": \"foo\", \"optional_value\": \"bar\","
+ "\"absl_optional_value\": \"baz\"}");
+ ASSERT_TRUE(test_struct.ok()) << test_struct.status();
+ EXPECT_EQ(test_struct->value, "foo");
+ EXPECT_EQ(test_struct->optional_value, "bar");
+ EXPECT_EQ(test_struct->absl_optional_value, "baz");
+ // Wrong JSON type.
+ test_struct = Parse<TestStruct>(
+ "{\"value\": [], \"optional_value\": {}, "
+ "\"absl_optional_value\": 1}");
+ EXPECT_EQ(test_struct.status().code(), absl::StatusCode::kInvalidArgument);
+ EXPECT_EQ(test_struct.status().message(),
+ "errors validating JSON: ["
+ "field:absl_optional_value error:is not a string; "
+ "field:optional_value error:is not a string; "
+ "field:value error:is not a string]")
+ << test_struct.status();
+}
+
+//
+// Duration tests
+//
+
+TEST(JsonObjectLoader, DurationFields) {
+ struct TestStruct {
+ Duration value = Duration::Zero();
+ Duration optional_value = Duration::Zero();
+ absl::optional<Duration> absl_optional_value;
+
+ static const JsonLoaderInterface* JsonLoader(const JsonArgs&) {
+ static const auto* loader =
+ JsonObjectLoader<TestStruct>()
+ .Field("value", &TestStruct::value)
+ .OptionalField("optional_value", &TestStruct::optional_value)
+ .OptionalField("absl_optional_value",
+ &TestStruct::absl_optional_value)
+ .Finish();
+ return loader;
+ }
+ };
+ // Valid duration string.
+ auto test_struct = Parse<TestStruct>("{\"value\": \"3s\"}");
+ ASSERT_TRUE(test_struct.ok()) << test_struct.status();
+ EXPECT_EQ(test_struct->value, Duration::Seconds(3));
+ EXPECT_EQ(test_struct->optional_value, Duration::Zero());
+ EXPECT_FALSE(test_struct->absl_optional_value.has_value());
+ // Invalid duration strings.
+ test_struct = Parse<TestStruct>(
+ "{\"value\": \"3sec\", \"optional_value\": \"foos\","
+ "\"absl_optional_value\": \"1.0123456789s\"}");
+ EXPECT_EQ(test_struct.status().code(), absl::StatusCode::kInvalidArgument);
+ EXPECT_EQ(test_struct.status().message(),
+ "errors validating JSON: ["
+ "field:absl_optional_value error:"
+ "Not a duration (too many digits after decimal); "
+ "field:optional_value error:"
+ "Not a duration (not a number of seconds); "
+ "field:value error:Not a duration (no s suffix)]")
+ << test_struct.status();
+ test_struct = Parse<TestStruct>("{\"value\": \"3.xs\"}");
+ EXPECT_EQ(test_struct.status().code(), absl::StatusCode::kInvalidArgument);
+ EXPECT_EQ(test_struct.status().message(),
+ "errors validating JSON: ["
+ "field:value error:Not a duration (not a number of nanoseconds)]")
+ << test_struct.status();
+ // Fails if required field is not present.
+ test_struct = Parse<TestStruct>("{}");
+ EXPECT_EQ(test_struct.status().code(), absl::StatusCode::kInvalidArgument);
+ EXPECT_EQ(test_struct.status().message(),
+ "errors validating JSON: [field:value error:field not present]")
+ << test_struct.status();
+ // Optional fields present.
+ test_struct = Parse<TestStruct>(
+ "{\"value\": \"3s\", \"optional_value\": \"3.2s\", "
+ "\"absl_optional_value\": \"10s\"}");
+ ASSERT_TRUE(test_struct.ok()) << test_struct.status();
+ EXPECT_EQ(test_struct->value, Duration::Seconds(3));
+ EXPECT_EQ(test_struct->optional_value, Duration::Milliseconds(3200));
+ EXPECT_EQ(test_struct->absl_optional_value, Duration::Seconds(10));
+ // Wrong JSON type.
+ test_struct = Parse<TestStruct>(
+ "{\"value\": [], \"optional_value\": {}, "
+ "\"absl_optional_value\": 1}");
+ EXPECT_EQ(test_struct.status().code(), absl::StatusCode::kInvalidArgument);
+ EXPECT_EQ(test_struct.status().message(),
+ "errors validating JSON: ["
+ "field:absl_optional_value error:is not a string; "
+ "field:optional_value error:is not a string; "
+ "field:value error:is not a string]")
+ << test_struct.status();
+}
+
+//
+// Json::Object tests
+//
+
+TEST(JsonObjectLoader, JsonObjectFields) {
+ struct TestStruct {
+ Json::Object value;
+ Json::Object optional_value;
+ absl::optional<Json::Object> absl_optional_value;
+
+ static const JsonLoaderInterface* JsonLoader(const JsonArgs&) {
+ static const auto* loader =
+ JsonObjectLoader<TestStruct>()
+ .Field("value", &TestStruct::value)
+ .OptionalField("optional_value", &TestStruct::optional_value)
+ .OptionalField("absl_optional_value",
+ &TestStruct::absl_optional_value)
+ .Finish();
+ return loader;
+ }
+ };
+ // Valid object.
+ auto test_struct = Parse<TestStruct>("{\"value\": {\"a\":1}}");
+ ASSERT_TRUE(test_struct.ok()) << test_struct.status();
+ EXPECT_EQ(Json{test_struct->value}.Dump(), "{\"a\":1}");
+ EXPECT_EQ(Json{test_struct->optional_value}.Dump(), "{}");
+ EXPECT_FALSE(test_struct->absl_optional_value.has_value());
+ // Fails if required field is not present.
+ test_struct = Parse<TestStruct>("{}");
+ EXPECT_EQ(test_struct.status().code(), absl::StatusCode::kInvalidArgument);
+ EXPECT_EQ(test_struct.status().message(),
+ "errors validating JSON: [field:value error:field not present]")
+ << test_struct.status();
+ // Optional fields present.
+ test_struct = Parse<TestStruct>(
+ "{\"value\": {\"a\":1}, \"optional_value\": {\"b\":2}, "
+ "\"absl_optional_value\": {\"c\":3}}");
+ ASSERT_TRUE(test_struct.ok()) << test_struct.status();
+ EXPECT_EQ(Json{test_struct->value}.Dump(), "{\"a\":1}");
+ EXPECT_EQ(Json{test_struct->optional_value}.Dump(), "{\"b\":2}");
+ ASSERT_TRUE(test_struct->absl_optional_value.has_value());
+ EXPECT_EQ(Json{*test_struct->absl_optional_value}.Dump(), "{\"c\":3}");
+ // Wrong JSON type.
+ test_struct = Parse<TestStruct>(
+ "{\"value\": [], \"optional_value\": true, "
+ "\"absl_optional_value\": 1}");
+ EXPECT_EQ(test_struct.status().code(), absl::StatusCode::kInvalidArgument);
+ EXPECT_EQ(test_struct.status().message(),
+ "errors validating JSON: ["
+ "field:absl_optional_value error:is not an object; "
+ "field:optional_value error:is not an object; "
+ "field:value error:is not an object]")
+ << test_struct.status();
+}
+
+//
+// map<> tests
+//
+
+TEST(JsonObjectLoader, MapFields) {
+ struct TestStruct {
+ std::map<std::string, int32_t> value;
+ std::map<std::string, std::string> optional_value;
+ absl::optional<std::map<std::string, bool>> absl_optional_value;
+
+ static const JsonLoaderInterface* JsonLoader(const JsonArgs&) {
+ static const auto* loader =
+ JsonObjectLoader<TestStruct>()
+ .Field("value", &TestStruct::value)
+ .OptionalField("optional_value", &TestStruct::optional_value)
+ .OptionalField("absl_optional_value",
+ &TestStruct::absl_optional_value)
+ .Finish();
+ return loader;
+ }
+ };
+ // Valid map.
+ auto test_struct = Parse<TestStruct>("{\"value\": {\"a\":1}}");
+ ASSERT_TRUE(test_struct.ok()) << test_struct.status();
+ EXPECT_THAT(test_struct->value,
+ ::testing::ElementsAre(::testing::Pair("a", 1)));
+ EXPECT_THAT(test_struct->optional_value, ::testing::ElementsAre());
+ EXPECT_FALSE(test_struct->absl_optional_value.has_value());
+ // Fails if required field is not present.
+ test_struct = Parse<TestStruct>("{}");
+ EXPECT_EQ(test_struct.status().code(), absl::StatusCode::kInvalidArgument);
+ EXPECT_EQ(test_struct.status().message(),
+ "errors validating JSON: [field:value error:field not present]")
+ << test_struct.status();
+ // Optional fields present.
+ test_struct = Parse<TestStruct>(
+ "{\"value\": {\"a\":1}, \"optional_value\": {\"b\":\"foo\"}, "
+ "\"absl_optional_value\": {\"c\":true}}");
+ ASSERT_TRUE(test_struct.ok()) << test_struct.status();
+ EXPECT_THAT(test_struct->value,
+ ::testing::ElementsAre(::testing::Pair("a", 1)));
+ EXPECT_THAT(test_struct->optional_value,
+ ::testing::ElementsAre(::testing::Pair("b", "foo")));
+ ASSERT_TRUE(test_struct->absl_optional_value.has_value());
+ EXPECT_THAT(*test_struct->absl_optional_value,
+ ::testing::ElementsAre(::testing::Pair("c", true)));
+ // Wrong JSON type.
+ test_struct = Parse<TestStruct>(
+ "{\"value\": [], \"optional_value\": true, "
+ "\"absl_optional_value\": 1}");
+ EXPECT_EQ(test_struct.status().code(), absl::StatusCode::kInvalidArgument);
+ EXPECT_EQ(test_struct.status().message(),
+ "errors validating JSON: ["
+ "field:absl_optional_value error:is not an object; "
+ "field:optional_value error:is not an object; "
+ "field:value error:is not an object]")
+ << test_struct.status();
+ // Wrong JSON type for map value.
+ test_struct = Parse<TestStruct>(
+ "{\"value\": {\"a\":\"foo\"}, \"optional_value\": {\"b\":true}, "
+ "\"absl_optional_value\": {\"c\":1}}");
+ EXPECT_EQ(test_struct.status().code(), absl::StatusCode::kInvalidArgument);
+ EXPECT_EQ(test_struct.status().message(),
+ "errors validating JSON: ["
+ "field:absl_optional_value[\"c\"] error:is not a boolean; "
+ "field:optional_value[\"b\"] error:is not a string; "
+ "field:value[\"a\"] error:failed to parse number]")
+ << test_struct.status();
+}
+
+//
+// vector<> tests
+//
+
+TEST(JsonObjectLoader, VectorFields) {
+ struct TestStruct {
+ std::vector<int32_t> value;
+ std::vector<std::string> optional_value;
+ absl::optional<std::vector<bool>> absl_optional_value;
+
+ static const JsonLoaderInterface* JsonLoader(const JsonArgs&) {
+ static const auto* loader =
+ JsonObjectLoader<TestStruct>()
+ .Field("value", &TestStruct::value)
+ .OptionalField("optional_value", &TestStruct::optional_value)
+ .OptionalField("absl_optional_value",
+ &TestStruct::absl_optional_value)
+ .Finish();
+ return loader;
+ }
+ };
+ // Valid map.
+ auto test_struct = Parse<TestStruct>("{\"value\": [1, 2, 3]}");
+ ASSERT_TRUE(test_struct.ok()) << test_struct.status();
+ EXPECT_THAT(test_struct->value, ::testing::ElementsAre(1, 2, 3));
+ EXPECT_THAT(test_struct->optional_value, ::testing::ElementsAre());
+ EXPECT_FALSE(test_struct->absl_optional_value.has_value());
+ // Fails if required field is not present.
+ test_struct = Parse<TestStruct>("{}");
+ EXPECT_EQ(test_struct.status().code(), absl::StatusCode::kInvalidArgument);
+ EXPECT_EQ(test_struct.status().message(),
+ "errors validating JSON: [field:value error:field not present]")
+ << test_struct.status();
+ // Optional fields present.
+ test_struct = Parse<TestStruct>(
+ "{\"value\": [4, 5, 6], \"optional_value\": [\"foo\", \"bar\"], "
+ "\"absl_optional_value\": [true, false, true]}");
+ ASSERT_TRUE(test_struct.ok()) << test_struct.status();
+ EXPECT_THAT(test_struct->value, ::testing::ElementsAre(4, 5, 6));
+ EXPECT_THAT(test_struct->optional_value,
+ ::testing::ElementsAre("foo", "bar"));
+ ASSERT_TRUE(test_struct->absl_optional_value.has_value());
+ EXPECT_THAT(*test_struct->absl_optional_value,
+ ::testing::ElementsAre(true, false, true));
+ // Wrong JSON type.
+ test_struct = Parse<TestStruct>(
+ "{\"value\": {}, \"optional_value\": true, "
+ "\"absl_optional_value\": 1}");
+ EXPECT_EQ(test_struct.status().code(), absl::StatusCode::kInvalidArgument);
+ EXPECT_EQ(test_struct.status().message(),
+ "errors validating JSON: ["
+ "field:absl_optional_value error:is not an array; "
+ "field:optional_value error:is not an array; "
+ "field:value error:is not an array]")
+ << test_struct.status();
+ // Wrong JSON type for map value.
+ test_struct = Parse<TestStruct>(
+ "{\"value\": [\"foo\", \"bar\"], \"optional_value\": [true, false], "
+ "\"absl_optional_value\": [1, 2]}");
+ EXPECT_EQ(test_struct.status().code(), absl::StatusCode::kInvalidArgument);
+ EXPECT_EQ(test_struct.status().message(),
+ "errors validating JSON: ["
+ "field:absl_optional_value[0] error:is not a boolean; "
+ "field:absl_optional_value[1] error:is not a boolean; "
+ "field:optional_value[0] error:is not a string; "
+ "field:optional_value[1] error:is not a string; "
+ "field:value[0] error:failed to parse number; "
+ "field:value[1] error:failed to parse number]")
+ << test_struct.status();
+}
+
+//
+// Nested struct tests
+//
+
+TEST(JsonObjectLoader, NestedStructFields) {
+ struct NestedStruct {
+ int32_t inner = 0;
+
+ static const JsonLoaderInterface* JsonLoader(const JsonArgs&) {
+ static const auto* loader = JsonObjectLoader<NestedStruct>()
+ .Field("inner", &NestedStruct::inner)
+ .Finish();
+ return loader;
+ }
+ };
+ struct TestStruct {
+ NestedStruct outer;
+ NestedStruct optional_outer;
+ absl::optional<NestedStruct> absl_optional_outer;
+
+ static const JsonLoaderInterface* JsonLoader(const JsonArgs&) {
+ static const auto* loader =
+ JsonObjectLoader<TestStruct>()
+ .Field("outer", &TestStruct::outer)
+ .OptionalField("optional_outer", &TestStruct::optional_outer)
+ .OptionalField("absl_optional_outer",
+ &TestStruct::absl_optional_outer)
+ .Finish();
+ return loader;
+ }
+ };
+ // Valid nested struct.
+ auto test_struct = Parse<TestStruct>("{\"outer\": {\"inner\": 1}}");
+ ASSERT_TRUE(test_struct.ok()) << test_struct.status();
+ EXPECT_EQ(test_struct->outer.inner, 1);
+ EXPECT_EQ(test_struct->optional_outer.inner, 0);
+ EXPECT_FALSE(test_struct->absl_optional_outer.has_value());
+ // Fails if required field is not present.
+ test_struct = Parse<TestStruct>("{}");
+ EXPECT_EQ(test_struct.status().code(), absl::StatusCode::kInvalidArgument);
+ EXPECT_EQ(test_struct.status().message(),
+ "errors validating JSON: [field:outer error:field not present]")
+ << test_struct.status();
+ // Fails if inner required field is not present.
+ test_struct = Parse<TestStruct>("{\"outer\": {}}");
+ EXPECT_EQ(test_struct.status().code(), absl::StatusCode::kInvalidArgument);
+ EXPECT_EQ(
+ test_struct.status().message(),
+ "errors validating JSON: [field:outer.inner error:field not present]")
+ << test_struct.status();
+ // Optional fields present.
+ test_struct = Parse<TestStruct>(
+ "{\"outer\": {\"inner\":1}, \"optional_outer\": {\"inner\":2}, "
+ "\"absl_optional_outer\": {\"inner\":3}}");
+ ASSERT_TRUE(test_struct.ok()) << test_struct.status();
+ EXPECT_EQ(test_struct->outer.inner, 1);
+ EXPECT_EQ(test_struct->optional_outer.inner, 2);
+ ASSERT_TRUE(test_struct->absl_optional_outer.has_value());
+ EXPECT_EQ(test_struct->absl_optional_outer->inner, 3);
+ // Wrong JSON type.
+ test_struct = Parse<TestStruct>(
+ "{\"outer\": \"foo\", \"optional_outer\": true, "
+ "\"absl_optional_outer\": 1}");
+ EXPECT_EQ(test_struct.status().code(), absl::StatusCode::kInvalidArgument);
+ EXPECT_EQ(test_struct.status().message(),
+ "errors validating JSON: ["
+ "field:absl_optional_outer error:is not an object; "
+ "field:optional_outer error:is not an object; "
+ "field:outer error:is not an object]")
+ << test_struct.status();
+ // Wrong JSON type for inner value.
+ test_struct = Parse<TestStruct>(
+ "{\"outer\": {\"inner\":\"foo\"}, \"optional_outer\": {\"inner\":true}, "
+ "\"absl_optional_outer\": {\"inner\":[]}}");
+ EXPECT_EQ(test_struct.status().code(), absl::StatusCode::kInvalidArgument);
+ EXPECT_EQ(test_struct.status().message(),
+ "errors validating JSON: ["
+ "field:absl_optional_outer.inner error:is not a number; "
+ "field:optional_outer.inner error:is not a number; "
+ "field:outer.inner error:failed to parse number]")
+ << test_struct.status();
+}
+
+TEST(JsonObjectLoader, BareString) {
+ auto parsed = Parse<std::string>("\"foo\"");
+ ASSERT_TRUE(parsed.ok()) << parsed.status();
+ EXPECT_EQ(*parsed, "foo");
+}
+
+TEST(JsonObjectLoader, BareDuration) {
+ auto parsed = Parse<Duration>("\"1.5s\"");
+ ASSERT_TRUE(parsed.ok()) << parsed.status();
+ EXPECT_EQ(*parsed, Duration::Milliseconds(1500));
+}
+
+TEST(JsonObjectLoader, BareSignedInteger) {
+ auto parsed = Parse<int32_t>("5");
+ ASSERT_TRUE(parsed.ok()) << parsed.status();
+ EXPECT_EQ(*parsed, 5);
+}
+
+TEST(JsonObjectLoader, BareUnsignedInteger) {
+ auto parsed = Parse<uint32_t>("5");
+ ASSERT_TRUE(parsed.ok()) << parsed.status();
+ EXPECT_EQ(*parsed, 5);
+}
+
+TEST(JsonObjectLoader, BareFloat) {
+ auto parsed = Parse<float>("5.2");
+ ASSERT_TRUE(parsed.ok()) << parsed.status();
+ EXPECT_NEAR(*parsed, 5.2, 0.001);
+}
+
+TEST(JsonObjectLoader, BareBool) {
+ auto parsed = Parse<bool>("true");
+ ASSERT_TRUE(parsed.ok()) << parsed.status();
+ EXPECT_TRUE(*parsed);
+}
+
+TEST(JsonObjectLoader, BareVector) {
+ auto parsed = Parse<std::vector<int32_t>>("[1, 2, 3]");
+ ASSERT_TRUE(parsed.ok()) << parsed.status();
+ EXPECT_THAT(*parsed, ::testing::ElementsAre(1, 2, 3));
+}
+
+TEST(JsonObjectLoader, BareMap) {
+ auto parsed =
+ Parse<std::map<std::string, int32_t>>("{\"a\":1, \"b\":2, \"c\":3}");
+ ASSERT_TRUE(parsed.ok()) << parsed.status();
+ EXPECT_THAT(*parsed, ::testing::ElementsAre(::testing::Pair("a", 1),
+ ::testing::Pair("b", 2),
+ ::testing::Pair("c", 3)));
+}
+
+TEST(JsonObjectLoader, IgnoresUnsupportedFields) {
+ struct TestStruct {
+ int32_t a = 0;
+
+ static const JsonLoaderInterface* JsonLoader(const JsonArgs&) {
+ static const auto* loader =
+ JsonObjectLoader<TestStruct>().Field("a", &TestStruct::a).Finish();
+ return loader;
+ }
+ };
+ auto test_struct = Parse<TestStruct>("{\"a\": 3, \"b\":false}");
+ ASSERT_TRUE(test_struct.ok()) << test_struct.status();
+ EXPECT_EQ(test_struct->a, 3);
+}
+
+TEST(JsonObjectLoader, IgnoresDisabledFields) {
+ class FakeJsonArgs : public JsonArgs {
+ public:
+ FakeJsonArgs() = default;
+
+ bool IsEnabled(absl::string_view key) const override {
+ return key != "disabled";
+ }
+ };
+ struct TestStruct {
+ int32_t a = 0;
+ int32_t b = 0;
+ int32_t c = 0;
+
+ static const JsonLoaderInterface* JsonLoader(const JsonArgs&) {
+ static const auto* loader =
+ JsonObjectLoader<TestStruct>()
+ .Field("a", &TestStruct::a, "disabled")
+ .OptionalField("b", &TestStruct::b, "disabled")
+ .OptionalField("c", &TestStruct::c, "enabled")
+ .Finish();
+ return loader;
+ }
+ };
+ // Fields "a" and "b" have the wrong types, but we ignore them,
+ // because they're disabled.
+ auto test_struct =
+ Parse<TestStruct>("{\"a\":false, \"b\":false, \"c\":1}", FakeJsonArgs());
+ ASSERT_TRUE(test_struct.ok()) << test_struct.status();
+ EXPECT_EQ(test_struct->a, 0);
+ EXPECT_EQ(test_struct->b, 0);
+ EXPECT_EQ(test_struct->c, 1);
+}
+
+TEST(JsonObjectLoader, PostLoadHook) {
+ struct TestStruct {
+ int32_t a = 0;
+
+ static const JsonLoaderInterface* JsonLoader(const JsonArgs&) {
+ static const auto* loader = JsonObjectLoader<TestStruct>()
+ .OptionalField("a", &TestStruct::a)
+ .Finish();
+ return loader;
+ }
+
+ void JsonPostLoad(const Json& /*source*/, const JsonArgs& /*args*/,
+ ErrorList* /*errors*/) {
+ ++a;
+ }
+ };
+ auto test_struct = Parse<TestStruct>("{\"a\": 1}");
+ ASSERT_TRUE(test_struct.ok()) << test_struct.status();
+ EXPECT_EQ(test_struct->a, 2);
+ test_struct = Parse<TestStruct>("{}");
+ ASSERT_TRUE(test_struct.ok()) << test_struct.status();
+ EXPECT_EQ(test_struct->a, 1);
+}
+
+TEST(JsonObjectLoader, CustomValidationInPostLoadHook) {
+ struct TestStruct {
+ int32_t a = 0;
+
+ static const JsonLoaderInterface* JsonLoader(const JsonArgs&) {
+ static const auto* loader =
+ JsonObjectLoader<TestStruct>().Field("a", &TestStruct::a).Finish();
+ return loader;
+ }
+
+ void JsonPostLoad(const Json& /*source*/, const JsonArgs& /*args*/,
+ ErrorList* errors) {
+ ScopedField field(errors, ".a");
+ if (!errors->FieldHasErrors() && a <= 0) {
+ errors->AddError("must be greater than 0");
+ }
+ }
+ };
+ // Value greater than 0.
+ auto test_struct = Parse<TestStruct>("{\"a\": 1}");
+ ASSERT_TRUE(test_struct.ok()) << test_struct.status();
+ EXPECT_EQ(test_struct->a, 1);
+ // Value 0, triggers custom validation.
+ test_struct = Parse<TestStruct>("{\"a\": 0}");
+ EXPECT_EQ(test_struct.status().code(), absl::StatusCode::kInvalidArgument);
+ EXPECT_EQ(test_struct.status().message(),
+ "errors validating JSON: [field:a error:must be greater than 0]")
+ << test_struct.status();
+ // Invalid type, generates built-in parsing error, so custom
+ // validation will not generate a new error.
+ test_struct = Parse<TestStruct>("{\"a\": []}");
+ EXPECT_EQ(test_struct.status().code(), absl::StatusCode::kInvalidArgument);
+ EXPECT_EQ(test_struct.status().message(),
+ "errors validating JSON: [field:a error:is not a number]")
+ << test_struct.status();
+}
+
+TEST(JsonObjectLoader, LoadFromJsonWithErrorList) {
+ struct TestStruct {
+ int32_t a = 0;
+
+ static const JsonLoaderInterface* JsonLoader(const JsonArgs&) {
+ static const auto* loader =
+ JsonObjectLoader<TestStruct>().Field("a", &TestStruct::a).Finish();
+ return loader;
+ }
+ };
+ // Valid.
+ {
+ absl::string_view json_str = "{\"a\":1}";
+ auto json = Json::Parse(json_str);
+ ASSERT_TRUE(json.ok()) << json.status();
+ ErrorList errors;
+ TestStruct test_struct =
+ LoadFromJson<TestStruct>(*json, JsonArgs(), &errors);
+ ASSERT_TRUE(errors.ok()) << errors.status();
+ EXPECT_EQ(test_struct.a, 1);
+ }
+ // Invalid.
+ {
+ absl::string_view json_str = "{\"a\":\"foo\"}";
+ auto json = Json::Parse(json_str);
+ ASSERT_TRUE(json.ok()) << json.status();
+ ErrorList errors;
+ LoadFromJson<TestStruct>(*json, JsonArgs(), &errors);
+ absl::Status status = errors.status();
+ EXPECT_EQ(status.code(), absl::StatusCode::kInvalidArgument);
+ EXPECT_EQ(status.message(),
+ "errors validating JSON: [field:a error:failed to parse number]")
+ << status;
+ }
+}
+
+TEST(JsonObjectLoader, LoadJsonObjectField) {
+ absl::string_view json_str = "{\"int\":1}";
+ auto json = Json::Parse(json_str);
+ ASSERT_TRUE(json.ok()) << json.status();
+ // Load a valid field.
+ {
+ ErrorList errors;
+ auto value = LoadJsonObjectField<int32_t>(json->object_value(), JsonArgs(),
+ "int", &errors);
+ ASSERT_TRUE(value.has_value()) << errors.status();
+ EXPECT_EQ(*value, 1);
+ EXPECT_TRUE(errors.ok());
+ }
+ // An optional field that is not present.
+ {
+ ErrorList errors;
+ auto value = LoadJsonObjectField<int32_t>(json->object_value(), JsonArgs(),
+ "not_present", &errors,
+ /*required=*/false);
+ EXPECT_FALSE(value.has_value());
+ EXPECT_TRUE(errors.ok());
+ }
+ // A required field that is not present.
+ {
+ ErrorList errors;
+ auto value = LoadJsonObjectField<int32_t>(json->object_value(), JsonArgs(),
+ "not_present", &errors);
+ EXPECT_FALSE(value.has_value());
+ auto status = errors.status();
+ EXPECT_THAT(status.code(), absl::StatusCode::kInvalidArgument);
+ EXPECT_EQ(status.message(),
+ "errors validating JSON: ["
+ "field:not_present error:field not present]")
+ << status;
+ }
+ // Value has the wrong type.
+ {
+ ErrorList errors;
+ auto value = LoadJsonObjectField<std::string>(json->object_value(),
+ JsonArgs(), "int", &errors);
+ EXPECT_FALSE(value.has_value());
+ auto status = errors.status();
+ EXPECT_THAT(status.code(), absl::StatusCode::kInvalidArgument);
+ EXPECT_EQ(status.message(),
+ "errors validating JSON: [field:int error:is not a string]")
+ << status;
+ }
+}
+
+} // namespace
+} // namespace grpc_core
+
+int main(int argc, char** argv) {
+ ::testing::InitGoogleTest(&argc, argv);
+ return RUN_ALL_TESTS();
+}
diff --git a/tools/doxygen/Doxyfile.c++.internal b/tools/doxygen/Doxyfile.c++.internal
index 3d4ba13..65d7270 100644
--- a/tools/doxygen/Doxyfile.c++.internal
+++ b/tools/doxygen/Doxyfile.c++.internal
@@ -2208,6 +2208,9 @@
src/core/lib/iomgr/wakeup_fd_posix.cc \
src/core/lib/iomgr/wakeup_fd_posix.h \
src/core/lib/json/json.h \
+src/core/lib/json/json_args.h \
+src/core/lib/json/json_object_loader.cc \
+src/core/lib/json/json_object_loader.h \
src/core/lib/json/json_reader.cc \
src/core/lib/json/json_util.cc \
src/core/lib/json/json_util.h \
diff --git a/tools/doxygen/Doxyfile.core.internal b/tools/doxygen/Doxyfile.core.internal
index 08c036c..e89a845 100644
--- a/tools/doxygen/Doxyfile.core.internal
+++ b/tools/doxygen/Doxyfile.core.internal
@@ -2001,6 +2001,9 @@
src/core/lib/iomgr/wakeup_fd_posix.cc \
src/core/lib/iomgr/wakeup_fd_posix.h \
src/core/lib/json/json.h \
+src/core/lib/json/json_args.h \
+src/core/lib/json/json_object_loader.cc \
+src/core/lib/json/json_object_loader.h \
src/core/lib/json/json_reader.cc \
src/core/lib/json/json_util.cc \
src/core/lib/json/json_util.h \
diff --git a/tools/run_tests/generated/tests.json b/tools/run_tests/generated/tests.json
index 028e370..15ea62f 100644
--- a/tools/run_tests/generated/tests.json
+++ b/tools/run_tests/generated/tests.json
@@ -4178,6 +4178,30 @@
"flaky": false,
"gtest": true,
"language": "c++",
+ "name": "json_object_loader_test",
+ "platforms": [
+ "linux",
+ "mac",
+ "posix",
+ "windows"
+ ],
+ "uses_polling": false
+ },
+ {
+ "args": [],
+ "benchmark": false,
+ "ci_platforms": [
+ "linux",
+ "mac",
+ "posix",
+ "windows"
+ ],
+ "cpu_cost": 1.0,
+ "exclude_configs": [],
+ "exclude_iomgrs": [],
+ "flaky": false,
+ "gtest": true,
+ "language": "c++",
"name": "json_test",
"platforms": [
"linux",