From af634e19b47ef3b41bb4b9f31a44599457bd9f1a Mon Sep 17 00:00:00 2001 From: "Mark D. Roth" Date: Wed, 3 Aug 2022 16:35:48 -0700 Subject: [PATCH] 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 Co-authored-by: ctiller Co-authored-by: Craig Tiller Co-authored-by: markdroth --- BUILD | 42 + CMakeLists.txt | 38 + Makefile | 2 + build_autogenerated.yaml | 16 + config.m4 | 1 + config.w32 | 1 + gRPC-C++.podspec | 4 + gRPC-Core.podspec | 5 + grpc.gemspec | 3 + grpc.gyp | 2 + package.xml | 3 + src/core/lib/json/json_args.h | 34 + src/core/lib/json/json_channel_args.h | 42 + src/core/lib/json/json_object_loader.cc | 204 ++++ src/core/lib/json/json_object_loader.h | 544 +++++++++++ src/core/lib/json/json_util.cc | 43 +- src/python/grpcio/grpc_core_dependencies.py | 1 + test/core/json/BUILD | 14 + test/core/json/json_object_loader_test.cc | 984 ++++++++++++++++++++ tools/doxygen/Doxyfile.c++.internal | 3 + tools/doxygen/Doxyfile.core.internal | 3 + tools/run_tests/generated/tests.json | 24 + 22 files changed, 1977 insertions(+), 36 deletions(-) create mode 100644 src/core/lib/json/json_args.h create mode 100644 src/core/lib/json/json_channel_args.h create mode 100644 src/core/lib/json/json_object_loader.cc create mode 100644 src/core/lib/json/json_object_loader.h create mode 100644 test/core/json/json_object_loader_test.cc diff --git a/BUILD b/BUILD index 88a3d15feb0..3541025a1fd 100644 --- a/BUILD +++ b/BUILD @@ -7260,10 +7260,52 @@ grpc_cc_library( "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 e02b8782a6b..2f374f2e687 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1025,6 +1025,7 @@ if(gRPC_BUILD_TESTS) 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 @@ add_library(grpc 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 @@ add_library(grpc_unsecure 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 @@ -12623,6 +12626,41 @@ target_link_libraries(join_test ) +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) diff --git a/Makefile b/Makefile index cf54b0cc1a3..9c902fcd849 100644 --- a/Makefile +++ b/Makefile @@ -1548,6 +1548,7 @@ LIBGRPC_SRC = \ 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 @@ LIBGRPC_UNSECURE_SRC = \ 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 049d1f3d507..42686e382e5 100644 --- a/build_autogenerated.yaml +++ b/build_autogenerated.yaml @@ -849,6 +849,8 @@ libs: - 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 @@ libs: - 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 @@ libs: - 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 @@ libs: - 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 @@ targets: - 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 6dd509e5a82..06d35812bf1 100644 --- a/config.m4 +++ b/config.m4 @@ -607,6 +607,7 @@ if test "$PHP_GRPC" != "no"; then 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 46cfab92560..26f612c41af 100644 --- a/config.w32 +++ b/config.w32 @@ -573,6 +573,7 @@ if (PHP_GRPC != "no") { "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 fb0d095f70b..59c6c1e61e7 100644 --- a/gRPC-C++.podspec +++ b/gRPC-C++.podspec @@ -807,6 +807,8 @@ Pod::Spec.new do |s| '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 @@ Pod::Spec.new do |s| '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 456bd60d160..7b998c9dc89 100644 --- a/gRPC-Core.podspec +++ b/gRPC-Core.podspec @@ -1312,6 +1312,9 @@ Pod::Spec.new do |s| '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 @@ Pod::Spec.new do |s| '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 5215d8ca7bf..3e09b77673c 100644 --- a/grpc.gemspec +++ b/grpc.gemspec @@ -1225,6 +1225,9 @@ Gem::Specification.new do |s| 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 a9b7fae6be7..332a579fff2 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 e8547b3360c..2a61574e9b9 100644 --- a/package.xml +++ b/package.xml @@ -1207,6 +1207,9 @@ + + + diff --git a/src/core/lib/json/json_args.h b/src/core/lib/json/json_args.h new file mode 100644 index 00000000000..e975d336234 --- /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 + +#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 00000000000..668cb4aacdc --- /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 + +#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 00000000000..ae327101af1 --- /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 + +#include "src/core/lib/json/json_object_loader.h" + +#include +#include + +#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 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(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(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(dst) = true; + } else if (json.type() == Json::Type::JSON_FALSE) { + *static_cast(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(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(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 00000000000..6ccdc2ebcca --- /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 + +#include +#include +#include +#include +#include + +#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() +// .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 = LoadFromJson(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> field_errors_; + std::vector 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 +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(dst))) { + errors->AddError("failed to parse number"); + } + } +}; + +// Load an unsigned number of type T. +template +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(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(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(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 +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 +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 final : public LoadString {}; +template <> +class AutoLoader final : public LoadDuration {}; +template <> +class AutoLoader final : public TypedLoadSignedNumber {}; +template <> +class AutoLoader final : public TypedLoadSignedNumber {}; +template <> +class AutoLoader final : public TypedLoadUnsignedNumber {}; +template <> +class AutoLoader final : public TypedLoadUnsignedNumber {}; +template <> +class AutoLoader final : public LoadFloat {}; +template <> +class AutoLoader final : public LoadDouble {}; +template <> +class AutoLoader final : public LoadBool {}; +template <> +class AutoLoader final : public LoadUnprocessedJsonObject {}; + +// Specializations of AutoLoader for vectors. +template +class AutoLoader> final : public LoadVector { + private: + void LoadOne(const Json& json, const JsonArgs& args, void* dst, + ErrorList* errors) const final { + auto* vec = static_cast*>(dst); + T value{}; + LoaderForType()->LoadInto(json, args, &value, errors); + vec->push_back(std::move(value)); + } +}; + +// Specializations of AutoLoader for maps. +template +class AutoLoader> 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*>(dst); + T value{}; + LoaderForType()->LoadInto(json, args, &value, errors); + map->emplace(name, std::move(value)); + } +}; + +// Specializations of AutoLoader for absl::optional<>. +template +class AutoLoader> 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*>(dst); + opt->emplace(); + LoaderForType()->LoadInto(json, args, &**opt, errors); + } +}; + +// Implementation of aforementioned LoaderForType. +// Simply keeps a static AutoLoader and returns a pointer to that. +template +const LoaderInterface* LoaderForType() { + static const auto* loader = new AutoLoader(); + return loader; +} + +// Element describes one typed field to be loaded from a JSON object. +struct Element { + Element() = default; + template + Element(const char* name, bool optional, B A::*p, + const LoaderInterface* loader, const char* enable_key) + : loader(loader), + member_offset(static_cast( + reinterpret_cast(&(static_cast(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 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 +class Vec { + public: + Vec(const Vec& 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 +class Vec { + 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