mirror of https://github.com/grpc/grpc.git
json_object_loader: refactor ErrorList into its own library (#31049)
* json_object_loader: refactor ErrorList into its own library * fix observability_config_test * generate_projects * iwyupull/31071/head
parent
78c7cda232
commit
07df5ff9c7
39 changed files with 609 additions and 240 deletions
@ -0,0 +1,61 @@ |
|||||||
|
// 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/gprpp/validation_errors.h" |
||||||
|
|
||||||
|
#include <algorithm> |
||||||
|
#include <utility> |
||||||
|
|
||||||
|
#include "absl/status/status.h" |
||||||
|
#include "absl/strings/str_cat.h" |
||||||
|
#include "absl/strings/str_join.h" |
||||||
|
#include "absl/strings/strip.h" |
||||||
|
|
||||||
|
namespace grpc_core { |
||||||
|
|
||||||
|
void ValidationErrors::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 ValidationErrors::PopField() { fields_.pop_back(); } |
||||||
|
|
||||||
|
void ValidationErrors::AddError(absl::string_view error) { |
||||||
|
field_errors_[absl::StrJoin(fields_, "")].emplace_back(error); |
||||||
|
} |
||||||
|
|
||||||
|
bool ValidationErrors::FieldHasErrors() const { |
||||||
|
return field_errors_.find(absl::StrJoin(fields_, "")) != field_errors_.end(); |
||||||
|
} |
||||||
|
|
||||||
|
absl::Status ValidationErrors::status(absl::string_view prefix) 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(prefix, ": [", absl::StrJoin(errors, "; "), "]")); |
||||||
|
} |
||||||
|
|
||||||
|
} // namespace grpc_core
|
@ -0,0 +1,110 @@ |
|||||||
|
// 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_GPRPP_VALIDATION_ERRORS_H |
||||||
|
#define GRPC_CORE_LIB_GPRPP_VALIDATION_ERRORS_H |
||||||
|
|
||||||
|
#include <grpc/support/port_platform.h> |
||||||
|
|
||||||
|
#include <stddef.h> |
||||||
|
|
||||||
|
#include <map> |
||||||
|
#include <string> |
||||||
|
#include <vector> |
||||||
|
|
||||||
|
#include "absl/status/status.h" |
||||||
|
#include "absl/strings/string_view.h" |
||||||
|
|
||||||
|
namespace grpc_core { |
||||||
|
|
||||||
|
// Tracks errors that occur during validation of a data structure (e.g.,
|
||||||
|
// a JSON object or protobuf message). Errors are tracked based on
|
||||||
|
// which field they are associated with. If at least one error occurs
|
||||||
|
// during validation, the validation failed.
|
||||||
|
//
|
||||||
|
// Example usage:
|
||||||
|
//
|
||||||
|
// absl::StatusOr<std::string> GetFooBar(const Json::Object& json) {
|
||||||
|
// ValidationErrors errors;
|
||||||
|
// {
|
||||||
|
// ValidationErrors::ScopedField field("foo");
|
||||||
|
// auto it = json.object_value().find("foo");
|
||||||
|
// if (it == json.object_value().end()) {
|
||||||
|
// errors.AddError("field not present");
|
||||||
|
// } else if (it->second.type() != Json::Type::OBJECT) {
|
||||||
|
// errors.AddError("must be a JSON object");
|
||||||
|
// } else {
|
||||||
|
// const Json& foo = it->second;
|
||||||
|
// ValidationErrors::ScopedField field(".bar");
|
||||||
|
// auto it = foo.object_value().find("bar");
|
||||||
|
// if (it == json.object_value().end()) {
|
||||||
|
// errors.AddError("field not present");
|
||||||
|
// } else if (it->second.type() != Json::Type::STRING) {
|
||||||
|
// errors.AddError("must be a JSON string");
|
||||||
|
// } else {
|
||||||
|
// return it->second.string_value();
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// return errors.status("errors validating foo.bar");
|
||||||
|
// }
|
||||||
|
class ValidationErrors { |
||||||
|
public: |
||||||
|
// Pushes a field name onto the stack at construction and pops it off
|
||||||
|
// of the stack at destruction.
|
||||||
|
class ScopedField { |
||||||
|
public: |
||||||
|
ScopedField(ValidationErrors* errors, absl::string_view field_name) |
||||||
|
: errors_(errors) { |
||||||
|
errors_->PushField(field_name); |
||||||
|
} |
||||||
|
~ScopedField() { errors_->PopField(); } |
||||||
|
|
||||||
|
private: |
||||||
|
ValidationErrors* errors_; |
||||||
|
}; |
||||||
|
|
||||||
|
// Records that we've encountered an error associated with the current
|
||||||
|
// field.
|
||||||
|
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(absl::string_view prefix) const; |
||||||
|
|
||||||
|
// Returns true if there are no errors.
|
||||||
|
bool ok() const { return field_errors_.empty(); } |
||||||
|
|
||||||
|
size_t size() const { return field_errors_.size(); } |
||||||
|
|
||||||
|
private: |
||||||
|
// Pushes a field name onto the stack.
|
||||||
|
void PushField(absl::string_view ext) GPR_ATTRIBUTE_NOINLINE; |
||||||
|
// Pops a field name off of the stack.
|
||||||
|
void PopField() GPR_ATTRIBUTE_NOINLINE; |
||||||
|
|
||||||
|
// Errors that we have encountered so far, keyed by field name.
|
||||||
|
// 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_; |
||||||
|
// Stack of field names indicating the field that we are currently
|
||||||
|
// validating.
|
||||||
|
std::vector<std::string> fields_; |
||||||
|
}; |
||||||
|
|
||||||
|
} // namespace grpc_core
|
||||||
|
|
||||||
|
#endif // GRPC_CORE_LIB_GPRPP_VALIDATION_ERRORS_H
|
@ -0,0 +1,111 @@ |
|||||||
|
//
|
||||||
|
// 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.
|
||||||
|
//
|
||||||
|
|
||||||
|
#include "src/core/lib/gprpp/validation_errors.h" |
||||||
|
|
||||||
|
#include <memory> |
||||||
|
#include <thread> |
||||||
|
|
||||||
|
#include <gtest/gtest.h> |
||||||
|
|
||||||
|
#include "test/core/util/test_config.h" |
||||||
|
|
||||||
|
namespace grpc_core { |
||||||
|
namespace testing { |
||||||
|
namespace { |
||||||
|
|
||||||
|
TEST(ValidationErrors, NoErrors) { |
||||||
|
ValidationErrors errors; |
||||||
|
EXPECT_TRUE(errors.ok()); |
||||||
|
EXPECT_EQ(errors.size(), 0); |
||||||
|
{ |
||||||
|
ValidationErrors::ScopedField field(&errors, "foo"); |
||||||
|
{ ValidationErrors::ScopedField field(&errors, ".bar"); } |
||||||
|
} |
||||||
|
EXPECT_TRUE(errors.ok()); |
||||||
|
EXPECT_EQ(errors.size(), 0); |
||||||
|
absl::Status status = errors.status("errors validating config"); |
||||||
|
EXPECT_TRUE(status.ok()) << status; |
||||||
|
} |
||||||
|
|
||||||
|
TEST(ValidationErrors, OneError) { |
||||||
|
ValidationErrors errors; |
||||||
|
{ |
||||||
|
ValidationErrors::ScopedField field(&errors, "foo"); |
||||||
|
{ |
||||||
|
ValidationErrors::ScopedField field(&errors, ".bar"); |
||||||
|
errors.AddError("value smells funny"); |
||||||
|
} |
||||||
|
} |
||||||
|
EXPECT_FALSE(errors.ok()); |
||||||
|
EXPECT_EQ(errors.size(), 1); |
||||||
|
absl::Status status = errors.status("errors validating config"); |
||||||
|
EXPECT_EQ(status.code(), absl::StatusCode::kInvalidArgument); |
||||||
|
EXPECT_EQ( |
||||||
|
status.message(), |
||||||
|
"errors validating config: [field:foo.bar error:value smells funny]") |
||||||
|
<< status; |
||||||
|
} |
||||||
|
|
||||||
|
TEST(ValidationErrors, MultipleErrorsForSameField) { |
||||||
|
ValidationErrors errors; |
||||||
|
{ |
||||||
|
ValidationErrors::ScopedField field(&errors, "foo"); |
||||||
|
{ |
||||||
|
ValidationErrors::ScopedField field(&errors, ".bar"); |
||||||
|
errors.AddError("value smells funny"); |
||||||
|
errors.AddError("value is ugly"); |
||||||
|
} |
||||||
|
} |
||||||
|
EXPECT_FALSE(errors.ok()); |
||||||
|
EXPECT_EQ(errors.size(), 1); |
||||||
|
absl::Status status = errors.status("errors validating config"); |
||||||
|
EXPECT_EQ(status.code(), absl::StatusCode::kInvalidArgument); |
||||||
|
EXPECT_EQ(status.message(), |
||||||
|
"errors validating config: [field:foo.bar errors:[" |
||||||
|
"value smells funny; value is ugly]]") |
||||||
|
<< status; |
||||||
|
} |
||||||
|
|
||||||
|
TEST(ValidationErrors, ErrorsForMultipleFields) { |
||||||
|
ValidationErrors errors; |
||||||
|
{ |
||||||
|
ValidationErrors::ScopedField field(&errors, "foo"); |
||||||
|
{ |
||||||
|
ValidationErrors::ScopedField field(&errors, ".bar"); |
||||||
|
errors.AddError("value smells funny"); |
||||||
|
} |
||||||
|
errors.AddError("too hot"); |
||||||
|
} |
||||||
|
EXPECT_FALSE(errors.ok()); |
||||||
|
EXPECT_EQ(errors.size(), 2); |
||||||
|
absl::Status status = errors.status("errors validating config"); |
||||||
|
EXPECT_EQ(status.code(), absl::StatusCode::kInvalidArgument); |
||||||
|
EXPECT_EQ(status.message(), |
||||||
|
"errors validating config: [" |
||||||
|
"field:foo error:too hot; field:foo.bar error:value smells funny]") |
||||||
|
<< status; |
||||||
|
} |
||||||
|
|
||||||
|
} // namespace
|
||||||
|
} // namespace testing
|
||||||
|
} // namespace grpc_core
|
||||||
|
|
||||||
|
int main(int argc, char** argv) { |
||||||
|
grpc::testing::TestEnvironment env(&argc, argv); |
||||||
|
::testing::InitGoogleTest(&argc, argv); |
||||||
|
return RUN_ALL_TESTS(); |
||||||
|
} |
Loading…
Reference in new issue