| /* | |
| * Licensed to the Apache Software Foundation (ASF) under one | |
| * or more contributor license agreements. See the NOTICE file | |
| * distributed with this work for additional information | |
| * regarding copyright ownership. The ASF licenses this file | |
| * to you 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. | |
| */ | |
| #pragma once | |
| /// \file iceberg/util/error_collector.h | |
| /// Base class for collecting validation errors in builder patterns | |
| #include <string> | |
| #include <vector> | |
| #include "iceberg/compat/compiler.h" | |
| #include "iceberg/iceberg_export.h" | |
| #include "iceberg/result.h" | |
| // The builder macros name Iceberg symbols with their full namespace, so builders outside | |
| // namespace iceberg can use them. | |
| #define ICEBERG_BUILDER_RETURN_IF_ERROR(result) \ | |
| if (auto&& result_name = result; !result_name) [[unlikely]] { \ | |
| errors_.emplace_back(std::move(result_name.error())); \ | |
| return *this; \ | |
| } | |
| #define ICEBERG_BUILDER_ASSIGN_OR_RETURN_IMPL(result_name, lhs, rexpr) \ | |
| auto&& result_name = (rexpr); \ | |
| ICEBERG_BUILDER_RETURN_IF_ERROR(result_name) \ | |
| lhs = std::move(result_name.value()); | |
| #define ICEBERG_BUILDER_ASSIGN_OR_RETURN(lhs, rexpr) \ | |
| ICEBERG_BUILDER_ASSIGN_OR_RETURN_IMPL( \ | |
| ICEBERG_ASSIGN_OR_RAISE_NAME(result_, __COUNTER__), lhs, rexpr) | |
| #define ICEBERG_BUILDER_ASSIGN_OR_RETURN_WITH_ERROR_IMPL(result_name, lhs, rexpr, ...) \ | |
| auto&& result_name = (rexpr); \ | |
| if (!result_name) [[unlikely]] { \ | |
| AddError(::iceberg::ErrorKind::kInvalidArgument, __VA_ARGS__); \ | |
| return *this; \ | |
| } \ | |
| lhs = std::move(result_name.value()); | |
| #define ICEBERG_BUILDER_ASSIGN_OR_RETURN_WITH_ERROR(lhs, rexpr, ...) \ | |
| ICEBERG_BUILDER_ASSIGN_OR_RETURN_WITH_ERROR_IMPL( \ | |
| ICEBERG_ASSIGN_OR_RAISE_NAME(result_, __COUNTER__), lhs, rexpr, __VA_ARGS__) | |
| #define ICEBERG_BUILDER_CHECK(expr, ...) \ | |
| do { \ | |
| if (!(expr)) [[unlikely]] { \ | |
| AddError(::iceberg::ErrorKind::kInvalidArgument, __VA_ARGS__); \ | |
| return *this; \ | |
| } \ | |
| } while (false) | |
| namespace iceberg { | |
| /// \brief Base class for collecting errors in the builder pattern. | |
| /// | |
| /// This class equips builders with error accumulation capabilities to make it easy | |
| /// for method chaining. Builder methods should call AddError() to accumulate errors | |
| /// and call CheckErrors() before completing the build process. | |
| /// | |
| /// Example usage: | |
| /// \code | |
| /// class MyBuilder : public ErrorCollector { | |
| /// public: | |
| /// MyBuilder& SetValue(int val) { | |
| /// if (val < 0) { | |
| /// AddError(ErrorKind::kInvalidArgument, "Value must be non-negative"); | |
| /// return *this; | |
| /// } | |
| /// value_ = val; | |
| /// return *this; | |
| /// } | |
| /// | |
| /// Result<MyObject> Build() { | |
| /// ICEBERG_RETURN_UNEXPECTED(CheckErrors()); | |
| /// return MyObject{value_}; | |
| /// } | |
| /// | |
| /// private: | |
| /// int value_ = 0; | |
| /// }; | |
| /// \endcode | |
| class ICEBERG_EXPORT ErrorCollector { | |
| public: | |
| ErrorCollector() = default; | |
| virtual ~ErrorCollector() = default; | |
| ErrorCollector(ErrorCollector&&) = default; | |
| ErrorCollector& operator=(ErrorCollector&&) = default; | |
| ErrorCollector(const ErrorCollector&) = default; | |
| ErrorCollector& operator=(const ErrorCollector&) = default; | |
| /// \brief Add a specific error | |
| /// | |
| /// \param kind The kind of error | |
| /// \param fmt The format string | |
| /// \param args The arguments to format the message | |
| /// \return Reference to the concrete derived builder in the default C++23 | |
| /// package, or ErrorCollector& in compatibility mode. | |
| #if ICEBERG_CXX20_COMPAT | |
| template <typename... Args> | |
| ErrorCollector& AddError(ErrorKind kind, const std::format_string<Args...> fmt, | |
| Args&&... args) { | |
| RecordError({kind, std::format(fmt, std::forward<Args>(args)...)}); | |
| return *this; | |
| } | |
| #else | |
| template <typename... Args> | |
| auto& AddError(this auto& self, ErrorKind kind, const std::format_string<Args...> fmt, | |
| Args&&... args) { | |
| static_cast<ErrorCollector&>(self).RecordError( | |
| {kind, std::format(fmt, std::forward<Args>(args)...)}); | |
| return self; | |
| } | |
| #endif | |
| /// \brief Add an existing error object | |
| /// | |
| /// Useful when propagating errors from other components or reusing | |
| /// error objects without deconstructing and reconstructing them. | |
| /// | |
| /// \param err The error to add | |
| /// \return Reference to the concrete derived builder in the default C++23 | |
| /// package, or ErrorCollector& in compatibility mode. | |
| #if ICEBERG_CXX20_COMPAT | |
| ErrorCollector& AddError(Error err) { | |
| RecordError(std::move(err)); | |
| return *this; | |
| } | |
| #else | |
| auto& AddError(this auto& self, Error err) { | |
| static_cast<ErrorCollector&>(self).RecordError(std::move(err)); | |
| return self; | |
| } | |
| #endif | |
| /// \brief Add an unexpected result's error | |
| /// | |
| /// Useful for cases like below: | |
| /// \code | |
| /// AddError(InvalidArgument("Invalid value: {}", value)); | |
| /// return *this; | |
| /// \endcode | |
| /// | |
| /// \param err The unexpected result containing the error to add | |
| /// \return Reference to the concrete derived builder in the default C++23 | |
| /// package, or ErrorCollector& in compatibility mode. | |
| #if ICEBERG_CXX20_COMPAT | |
| ErrorCollector& AddError(::iceberg::unexpected<Error> err) { | |
| RecordError(std::move(err.error())); | |
| return *this; | |
| } | |
| #else | |
| auto& AddError(this auto& self, ::iceberg::unexpected<Error> err) { | |
| static_cast<ErrorCollector&>(self).RecordError(std::move(err.error())); | |
| return self; | |
| } | |
| auto& AddError(this auto& self, std::unexpected<Error> err) { | |
| static_cast<ErrorCollector&>(self).RecordError(std::move(err.error())); | |
| return self; | |
| } | |
| #endif | |
| /// \brief Check if any errors have been collected | |
| /// | |
| /// \return true if there are accumulated errors | |
| [[nodiscard]] bool has_errors() const { return !errors_.empty(); } | |
| /// \brief Get the number of errors collected | |
| /// | |
| /// \return The count of accumulated errors | |
| [[nodiscard]] size_t error_count() const { return errors_.size(); } | |
| /// \brief Check for accumulated errors and return them if any exist | |
| /// | |
| /// This should be called before completing a builder operation (e.g., | |
| /// in Build(), Apply(), or Commit() methods) to validate that no errors | |
| /// were accumulated during the builder method calls. | |
| /// | |
| /// \return Status indicating success if no errors, a RetryableValidationFailed if | |
| /// all accumulated errors are retryable validations, or a ValidationFailed | |
| /// error with all accumulated error messages otherwise | |
| [[nodiscard]] Status CheckErrors() const { | |
| if (!errors_.empty()) { | |
| std::string error_msg = "Validation failed due to the following errors:\n"; | |
| bool all_retryable = true; | |
| for (const auto& [kind, message] : errors_) { | |
| all_retryable &= kind == ErrorKind::kRetryableValidationFailed; | |
| error_msg += " - " + message + "\n"; | |
| } | |
| if (all_retryable) { | |
| return RetryableValidationFailed("{}", error_msg); | |
| } | |
| return ValidationFailed("{}", error_msg); | |
| } | |
| return {}; | |
| } | |
| /// \brief Clear all accumulated errors | |
| /// | |
| /// This can be useful for resetting the error state in tests or | |
| /// when reusing a builder instance. | |
| void ClearErrors() { errors_.clear(); } | |
| /// \brief Get read-only access to all collected errors | |
| [[nodiscard]] const std::vector<Error>& errors() const { return errors_; } | |
| private: | |
| void RecordError(Error err) { errors_.push_back(std::move(err)); } | |
| protected: | |
| std::vector<Error> errors_; | |
| }; | |
| } // namespace iceberg |