diff --git a/CMakeLists.txt b/CMakeLists.txt index 5faad17..f269d28 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,9 @@ cmake_minimum_required(VERSION 3.10) -project(ulib LANGUAGES CXX C VERSION 0.1.0) +project( + ulib + LANGUAGES CXX C + VERSION 0.1.0) set(CMAKE_C_STANDARD 11) set(CMAKE_C_STANDARD_REQUIRED ON) set(CMAKE_C_EXTENSIONS OFF) @@ -23,40 +26,41 @@ set(CMAKE_POSITION_INDEPENDENT_CODE ON) if(ULIB_SHARED_LIB) add_library(${PROJECT_NAME} SHARED "") else() - add_library(${PROJECT_NAME} STATIC "" - src/ulib/system/timer.cpp - src/ulib/system/timer.h) + add_library(${PROJECT_NAME} STATIC "" src/ulib/system/timer.cpp + src/ulib/system/timer.h) endif() -target_sources(${PROJECT_NAME} PRIVATE - 3party/mongoose/mongoose.c - src/ulib/base/location.h - src/ulib/base/location.cpp - src/ulib/concorrency/barrier.cpp - src/ulib/concorrency/barrier.h - src/ulib/concorrency/mutex.cpp - src/ulib/concorrency/mutex.h - src/ulib/concorrency/condition_variable.cpp - src/ulib/concorrency/condition_variable.h - src/ulib/concorrency/countdown_latch.cpp - src/ulib/concorrency/countdown_latch.h - src/ulib/concorrency/internal/mutex_impl.cpp - src/ulib/concorrency/internal/mutex_impl.h - src/ulib/concorrency/internal/condition_variable_impl.cpp - src/ulib/concorrency/internal/condition_variable_impl.h - src/ulib/concorrency/event.cpp - src/ulib/concorrency/event.h - src/ulib/system/thread.h - src/ulib/system/thread.cpp - src/ulib/system/thread_pool.h - src/ulib/system/thread_pool.cpp - src/ulib/system/timer.h - src/ulib/system/timer.cpp -) +target_sources( + ${PROJECT_NAME} + PRIVATE 3party/mongoose/mongoose.c + src/ulib/base/location.h + src/ulib/base/location.cpp + src/ulib/status.h + src/ulib/status.cpp + src/ulib/concorrency/barrier.cpp + src/ulib/concorrency/barrier.h + src/ulib/concorrency/mutex.cpp + src/ulib/concorrency/mutex.h + src/ulib/concorrency/condition_variable.cpp + src/ulib/concorrency/condition_variable.h + src/ulib/concorrency/countdown_latch.cpp + src/ulib/concorrency/countdown_latch.h + src/ulib/concorrency/internal/mutex_impl.cpp + src/ulib/concorrency/internal/mutex_impl.h + src/ulib/concorrency/internal/condition_variable_impl.cpp + src/ulib/concorrency/internal/condition_variable_impl.h + src/ulib/concorrency/event.cpp + src/ulib/concorrency/event.h + src/ulib/system/thread.h + src/ulib/system/thread.cpp + src/ulib/system/thread_pool.h + src/ulib/system/thread_pool.cpp + src/ulib/system/timer.h + src/ulib/system/timer.cpp) find_package(Threads REQUIRED) list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") -## CMP0063 +# CMP0063 if(POLICY CMP0063) cmake_policy(SET CMP0063 NEW) endif() @@ -64,9 +68,15 @@ if(POLICY CMP0048) cmake_policy(SET CMP0048 NEW) endif() -set(FMT_USE_CPP11 OFF CACHE BOOL "Use C++11" FORCE) -set(FMT_TEST OFF CACHE BOOL "Build tests" FORCE) -set(FMT_USE_CPP11 OFF CACHE BOOL "Use C++11" FORCE) +set(FMT_USE_CPP11 + OFF + CACHE BOOL "Use C++11" FORCE) +set(FMT_TEST + OFF + CACHE BOOL "Build tests" FORCE) +set(FMT_USE_CPP11 + OFF + CACHE BOOL "Use C++11" FORCE) add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/3party/fmt) add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/3party/sqlpp11) @@ -77,24 +87,22 @@ set(BUILD_SHARED_LIBS OFF) set(BUILD_OBJECT_LIBS OFF) add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/3party/jsoncpp) -target_sources(${PROJECT_NAME} PRIVATE - src/ulib/empty.cpp - src/ulib/log/logger.cpp - src/ulib/log/log.cpp - src/ulib/log/level.cpp -) -target_link_libraries(${PROJECT_NAME} PUBLIC fmt::fmt jsoncpp_static sqlpp11::sqlpp11) +target_sources( + ${PROJECT_NAME} PRIVATE src/ulib/empty.cpp src/ulib/log/logger.cpp + src/ulib/log/log.cpp src/ulib/log/level.cpp) +target_link_libraries(${PROJECT_NAME} PUBLIC fmt::fmt jsoncpp_static + sqlpp11::sqlpp11) target_compile_definitions(${PROJECT_NAME} PRIVATE ULIB_LIBRARY_IMPL) -target_include_directories(${PROJECT_NAME} PUBLIC - 3party/inja - 3party/mongoose - 3party/nlohmann - 3party/nonstd - 3party/sigslot - 3party/rxcpp/Rx/v2/src - 3party/rxcpp/Ix/CPP/src - src -) +target_include_directories( + ${PROJECT_NAME} + PUBLIC 3party/inja + 3party/mongoose + 3party/nlohmann + 3party/nonstd + 3party/sigslot + 3party/rxcpp/Rx/v2/src + 3party/rxcpp/Ix/CPP/src + src) install(TARGETS ${PROJECT_NAME} DESTINATION lib) diff --git a/src/ulib/status.cpp b/src/ulib/status.cpp new file mode 100644 index 0000000..47a037d --- /dev/null +++ b/src/ulib/status.cpp @@ -0,0 +1,249 @@ +// Copyright 2018 Google LLC +// +// 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 +// +// https://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 "status.h" +#include + +namespace ulib { + +namespace { +std::string +StatusWhat(Status const &status) +{ + std::ostringstream os; + os << status; + return std::move(os).str(); +} +}// namespace + +std::string +StatusCodeToString(StatusCode code) +{ + switch (code) { + case StatusCode::kOk: + return "OK"; + case StatusCode::kCancelled: + return "CANCELLED"; + case StatusCode::kUnknown: + return "UNKNOWN"; + case StatusCode::kInvalidArgument: + return "INVALID_ARGUMENT"; + case StatusCode::kDeadlineExceeded: + return "DEADLINE_EXCEEDED"; + case StatusCode::kNotFound: + return "NOT_FOUND"; + case StatusCode::kAlreadyExists: + return "ALREADY_EXISTS"; + case StatusCode::kPermissionDenied: + return "PERMISSION_DENIED"; + case StatusCode::kUnauthenticated: + return "UNAUTHENTICATED"; + case StatusCode::kResourceExhausted: + return "RESOURCE_EXHAUSTED"; + case StatusCode::kFailedPrecondition: + return "FAILED_PRECONDITION"; + case StatusCode::kAborted: + return "ABORTED"; + case StatusCode::kOutOfRange: + return "OUT_OF_RANGE"; + case StatusCode::kUnimplemented: + return "UNIMPLEMENTED"; + case StatusCode::kInternal: + return "INTERNAL"; + case StatusCode::kUnavailable: + return "UNAVAILABLE"; + case StatusCode::kDataLoss: + return "DATA_LOSS"; + default: + return "UNEXPECTED_STATUS_CODE=" + + std::to_string(static_cast(code)); + } +} + +std::ostream & +operator<<(std::ostream &os, StatusCode code) +{ + return os << StatusCodeToString(code); +} + +bool +operator==(ErrorInfo const &a, ErrorInfo const &b) +{ + return a.reason_ == b.reason_ && a.domain_ == b.domain_ + && a.metadata_ == b.metadata_; +} + +bool +operator!=(ErrorInfo const &a, ErrorInfo const &b) +{ + return !(a == b); +} + +// Encapsulates the implementation of a non-OK status. OK Statuses are +// represented by a nullptr Status::impl_, as an optimization for the common +// case of OK Statuses. This class holds all the data associated with a non-OK +// Status so we don't have to worry about bloating the common OK case. +class Status::Impl { +public: + using PayloadType = std::unordered_map; + + explicit Impl(StatusCode code, + std::string message, + ErrorInfo info, + PayloadType payload) + : code_(code), + message_(std::move(message)), + error_info_(std::move(info)), + payload_(std::move(payload)) + {} + + StatusCode code() const { return code_; } + + std::string const &message() const { return message_; } + + ErrorInfo const &error_info() const { return error_info_; } + + PayloadType const &payload() const { return payload_; } + + // Allows mutable access to payload, which is needed in the + // `internal::SetPayload()` function. + PayloadType &payload() { return payload_; } + + friend inline bool operator==(Impl const &a, Impl const &b) + { + return a.code_ == b.code_ && a.message_ == b.message_ + && a.error_info_ == b.error_info_ && a.payload_ == b.payload_; + } + + friend inline bool operator!=(Impl const &a, Impl const &b) + { + return !(a == b); + } + +private: + StatusCode code_; + std::string message_; + ErrorInfo error_info_; + PayloadType payload_; +}; + +Status::Status() = default; +Status::~Status() = default; +Status::Status(Status &&) noexcept = default; +Status &Status::operator=(Status &&) noexcept = default; + +// Deep copy +Status::Status(Status const &other) + : impl_(other.ok() ? nullptr : new auto(*other.impl_)) +{} + +// Deep copy +Status & +Status::operator=(Status const &other) +{ + impl_.reset(other.ok() ? nullptr : new auto(*other.impl_)); + return *this; +} + +// OK statuses have an impl_ == nullptr. Non-OK Statuses get an Impl. +Status::Status(StatusCode code, std::string message, ErrorInfo info) + : impl_( + code == StatusCode::kOk + ? nullptr + : new Status::Impl{code, std::move(message), std::move(info), {}}) +{} + +StatusCode +Status::code() const +{ + return impl_ ? impl_->code() : StatusCode::kOk; +} + +std::string const & +Status::message() const +{ + static auto const *const kEmpty = new std::string{}; + return impl_ ? impl_->message() : *kEmpty; +} + +ErrorInfo const & +Status::error_info() const +{ + static auto const *const kEmpty = new ErrorInfo{}; + return impl_ ? impl_->error_info() : *kEmpty; +} + +bool +Status::Equals(Status const &a, Status const &b) +{ + return (a.ok() && b.ok()) || (a.impl_ && b.impl_ && *a.impl_ == *b.impl_); +} + +std::ostream & +operator<<(std::ostream &os, Status const &s) +{ + if (s.ok()) return os << StatusCode::kOk; + os << s.code() << ": " << s.message(); + auto const &e = s.error_info(); + if (e.reason().empty() && e.domain().empty() && e.metadata().empty()) { + return os; + } + os << " error_info={reason=" << e.reason(); + os << ", domain=" << e.domain(); + os << ", metadata={"; + char const *sep = ""; + for (auto const &e : e.metadata()) { + os << sep << e.first << "=" << e.second; + sep = ", "; + } + return os << "}}"; +} + +namespace internal { + +void +AddMetadata(ErrorInfo &ei, std::string const &key, std::string value) +{ + ei.metadata_[key] = std::move(value); +} + +// Sets the given `payload`, indexed by the given `key`, on the given `Status`, +// IFF the status is not OK. Payloads are considered in equality comparisons. +// The keyspace used here is separate from other keyspaces (e.g., +// `absl::Status`), so we only need to coordinate keys with ourselves. +void +SetPayload(Status &s, std::string key, std::string payload) +{ + if (s.impl_) s.impl_->payload()[std::move(key)] = std::move(payload); +} + +// Returns the payload associated with the given `key`, if available. +ulib::optional +GetPayload(Status const &s, std::string const &key) +{ + if (!s.impl_) return ulib::nullopt; + auto const &payload = s.impl_->payload(); + auto it = payload.find(key); + if (it == payload.end()) return ulib::nullopt; + return it->second; +} + +}// namespace internal + +RuntimeStatusError::RuntimeStatusError(Status status) + : std::runtime_error(StatusWhat(status)), + status_(std::move(status)) +{} + +}// namespace ulib diff --git a/src/ulib/status.h b/src/ulib/status.h new file mode 100644 index 0000000..7134de4 --- /dev/null +++ b/src/ulib/status.h @@ -0,0 +1,401 @@ +// Copyright 2018 Google LLC +// +// 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 +// +// https://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 GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_STATUS_H +#define GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_STATUS_H + +#include "optional.h" +#include +#include +#include +#include + +namespace ulib { + +/** + * Well-known status codes with `grpc::StatusCode`-compatible values. + * + * The semantics of these values are documented in: + * https://grpc.io/grpc/cpp/classgrpc_1_1_status.html + */ +enum class StatusCode { + /// Not an error; returned on success. + kOk = 0, + + /// `kCancelled` (gRPC code `CANCELLED`) indicates the operation was + /// cancelled, typically by the caller. + kCancelled = 1, + + /// `kUnknown` (gRPC code `UNKNOWN`) indicates an unknown error occurred. + /// + /// In general, more specific errors should be raised, if possible. Errors + /// raised by APIs that do not return enough error information may be + /// converted to this error. + kUnknown = 2, + + /// `kInvalidArgument` (gRPC code `INVALID_ARGUMENT`) indicates the caller + /// specified an invalid argument, such as a malformed filename. + /// + /// Note that use of such errors should be narrowly limited to indicate the + /// invalid nature of the arguments themselves. Errors with validly formed + /// arguments that may cause errors with the state of the receiving system + /// should be denoted with `kFailedPrecondition` instead. + kInvalidArgument = 3, + + /// `kDeadlineExceeded` (gRPC code `DEADLINE_EXCEEDED`) indicates a deadline + /// expired before the operation could complete. + /// + /// For operations that may change state within a system, this error may be + /// returned even if the operation has completed successfully. For example, a + /// successful response from a server could have been delayed long enough for + /// the deadline to expire. + kDeadlineExceeded = 4, + + /// `kNotFound` (gRPC code `NOT_FOUND`) indicates some requested entity (such + /// as a file or directory) was not found. + /// + /// `kNotFound` is useful if a request should be denied for an entire class of + /// users, such as during a gradual feature rollout or undocumented allow + /// list. + /// If a request should be denied for specific sets of users, such as through + /// user-based access control, use `kPermissionDenied` instead. + kNotFound = 5, + + /// `kAlreadyExists` (gRPC code `ALREADY_EXISTS`) indicates that the entity a + /// caller attempted to create (such as a file or directory) is already + /// present. + kAlreadyExists = 6, + + /// `kPermissionDenied` (gRPC code `PERMISSION_DENIED`) indicates that the + /// caller does not have permission to execute the specified operation. + /// + /// Note that this error is different than an error due to an + /// *un*authenticated caller. This error code does not imply the request is + /// valid or the requested entity exists or satisfies any other + /// pre-conditions. + /// + /// `kPermissionDenied` must not be used for rejections caused by exhausting + /// some resource. Instead, use `kResourceExhausted` for those errors. + /// `kPermissionDenied` must not be used if the caller cannot be identified. + /// Instead, use `kUnauthenticated` for those errors. + kPermissionDenied = 7, + + /// `kResourceExhausted` (gRPC code `RESOURCE_EXHAUSTED`) indicates some + /// resource has been exhausted. + /// + /// Examples include a per-user quota, or the entire file system being out of + /// space. + kResourceExhausted = 8, + + /// `kFailedPrecondition` (gRPC code `FAILED_PRECONDITION`) indicates that the + /// operation was rejected because the system is not in a state required for + /// the operation's execution. + /// + /// For example, a directory to be deleted may be non-empty, a "rmdir" + /// operation is applied to a non-directory, etc. + /// + /// Some guidelines that may help a service implementer in deciding between + /// `kFailedPrecondition`, `kAborted`, and `kUnavailable`: + /// + /// 1. Use `kUnavailable` if the client can retry just the failing call. + /// 2. Use `kAborted` if the client should retry at a higher transaction + /// level (such as when a client-specified test-and-set fails, indicating + /// the client should restart a read-modify-write sequence). + /// 3. Use `kFailedPrecondition` if the client should not retry until the + /// system state has been explicitly fixed. For example, if a "rmdir" fails + /// because the directory is non-empty, `kFailedPrecondition` should be + /// returned since the client should not retry unless the files are deleted + /// from the directory. + kFailedPrecondition = 9, + + /// `kAborted` (gRPC code `ABORTED`) indicates the operation was aborted. + /// + /// This is typically due to a concurrency issue such as a sequencer check + /// failure or a failed transaction. + /// + /// See the guidelines above for deciding between `kFailedPrecondition`, + /// `kAborted`, and `kUnavailable`. + kAborted = 10, + + /// `kOutOfRange` (gRPC code `OUT_OF_RANGE`) indicates the operation was + /// attempted past the valid range, such as seeking or reading past an + /// end-of-file. + /// + /// Unlike `kInvalidArgument`, this error indicates a problem that may + /// be fixed if the system state changes. For example, a 32-bit file + /// system will generate `kInvalidArgument` if asked to read at an + /// offset that is not in the range [0,2^32-1], but it will generate + /// `kOutOfRange` if asked to read from an offset past the current + /// file size. + /// + /// There is a fair bit of overlap between `kFailedPrecondition` and + /// `kOutOfRange`. We recommend using `kOutOfRange` (the more specific + /// error) when it applies so that callers who are iterating through + /// a space can easily look for an `kOutOfRange` error to detect when + /// they are done. + kOutOfRange = 11, + + /// `kUnimplemented` (gRPC code `UNIMPLEMENTED`) indicates the operation is + /// not implemented or supported in this service. + /// + /// In this case, the operation should not be re-attempted. + kUnimplemented = 12, + + /// `kInternal` (gRPC code `INTERNAL`) indicates an internal error has + /// occurred and some invariants expected by the underlying system have not + /// been satisfied. + /// + /// While this error code is reserved for serious errors, some services return + /// this error under overload conditions. + kInternal = 13, + + /// `kUnavailable` (gRPC code `UNAVAILABLE`) indicates the service is + /// currently unavailable and that this is most likely a transient condition. + /// + /// An error such as this can be corrected by retrying with a backoff scheme. + /// Note that it is not always safe to retry non-idempotent operations. + /// + /// See the guidelines above for deciding between `kFailedPrecondition`, + /// `kAborted`, and `kUnavailable`. + kUnavailable = 14, + + /// `kDataLoss` (gRPC code `DATA_LOSS`) indicates that unrecoverable data loss + /// or corruption has occurred. + /// + /// As this error is serious, proper alerting should be attached to errors + /// such as this. + kDataLoss = 15, + + /// `kUnauthenticated` (gRPC code `UNAUTHENTICATED`) indicates that the + /// request does not have valid authentication credentials for the operation. + /// + /// Correct the authentication and try again. + kUnauthenticated = 16, +}; + +/// Convert @p code to a human readable string. +std::string StatusCodeToString(StatusCode code); + +/// Integration with `std::iostreams`. +std::ostream &operator<<(std::ostream &os, StatusCode code); + +class Status; +class ErrorInfo; + +namespace internal { +void AddMetadata(ErrorInfo &, std::string const &key, std::string value); +void SetPayload(Status &, std::string key, std::string payload); +ulib::optional GetPayload(Status const &, std::string const &key); +}// namespace internal + +/** + * Describes the cause of the error with structured details. + * + * @see https://cloud.google.com/apis/design/errors#error_info + */ +class ErrorInfo { +public: + /** + * Default constructor. + * + * Post-condition: the `reason()`, `domain()`, and `metadata()` fields are + * empty. + */ + ErrorInfo() = default; + + /** + * Constructor. + * + * @param reason initializes the `reason()` value. + * @param domain initializes the `domain()` value. + * @param metadata initializes the `metadata()` value. + */ + explicit ErrorInfo(std::string reason, + std::string domain, + std::unordered_map metadata) + : reason_(std::move(reason)), + domain_(std::move(domain)), + metadata_(std::move(metadata)) + {} + + /** + * The reason of the error. + * + * This is a constant value that identifies the proximate cause of the error. + * Error reasons are unique within a particular domain of errors. This should + * be at most 63 characters and match a regular expression of + * `[A-Z][A-Z0-9_]+[A-Z0-9]`, which represents UPPER_SNAKE_CASE. + */ + std::string const &reason() const { return reason_; } + + /** + * The logical grouping to which the "reason" belongs. + * + * The error domain is typically the registered service name of the tool or + * product that generates the error. Example: "pubsub.googleapis.com". If the + * error is generated by some common infrastructure, the error domain must be + * a globally unique value that identifies the infrastructure. For Google API + * infrastructure, the error domain is "googleapis.com". + * + * For errors generated by the C++ client libraries the domain is + * `gcloud-cpp`. + */ + std::string const &domain() const { return domain_; } + + /** + * Additional structured details about this error. + * + * Keys should match the regular expression `[a-zA-Z0-9-_]` and be limited + * to 64 characters in length. + * + * When identifying the current value of an exceeded limit, the units should + * be contained in the key, not the value. For example, if the client exceeds + * the number of instances that can be created in a single (batch) request + * return `{"instanceLimitPerRequest": "100"}` rather than + * `{"instanceLimit": "100/request"}`. + */ + std::unordered_map const &metadata() const + { + return metadata_; + } + + friend bool operator==(ErrorInfo const &, ErrorInfo const &); + friend bool operator!=(ErrorInfo const &, ErrorInfo const &); + +private: + friend void internal::AddMetadata(ErrorInfo &, + std::string const &key, + std::string value); + + std::string reason_; + std::string domain_; + std::unordered_map metadata_; +}; + +/** + * Represents success or an error with info about the error. + * + * This class is typically used to indicate whether or not a function or other + * operation completed successfully. Success is indicated by an "OK" status. OK + * statuses will have `.code() == StatusCode::kOk` and `.ok() == true`, with + * all other properties having empty values. All OK statuses are equal. Any + * non-OK `Status` is considered an error. Users can inspect the error using + * the member functions, or they can simply stream the `Status` object, and it + * will print itself in some human readable way (the streamed format may change + * over time and you should *not* depend on the specific format of a streamed + * `Status` object remaining unchanged). + * + * This is a regular value type that can be copied, moved, compared for + * equality, and streamed. + */ +class Status { +public: + /// Default constructor, initializes to `StatusCode::kOk`. + Status(); + /// Destructor. + ~Status(); + ///@{ + /** + * @name Copy construction and assignment. + */ + Status(Status const &); + Status &operator=(Status const &); + ///@} + ///@{ + /** + * @name Move construction and assignment. + */ + Status(Status &&) noexcept; + Status &operator=(Status &&) noexcept; + ///@} + + /** + * Construct from a status code, message and (optional) error info. + * + * @param code the status code for the new `Status`. + * @param message the message for the new `Status`, ignored if @p code is + * `StatusCode::kOk`. + * @param info the `ErrorInfo` for the new `Status`, ignored if @p code is + * `SStatusCode::kOk`. + */ + explicit Status(StatusCode code, std::string message, ErrorInfo info = {}); + + /// Returns true if the status code is `StatusCode::kOk`. + bool ok() const { return !impl_; } + + /// Returns the status code. + StatusCode code() const; + + /** + * Returns the message associated with the status. + * + * This is always empty if `code()` is `StatusCode::kOk`. + */ + std::string const &message() const; + + /** + * Returns the additional error info associated with the status. + * + * This is always a default-constructed error info if `code()` is + * `StatusCode::kOk`. + */ + ErrorInfo const &error_info() const; + + friend inline bool operator==(Status const &a, Status const &b) + { + return (a.ok() && b.ok()) || Equals(a, b); + } + + friend inline bool operator!=(Status const &a, Status const &b) + { + return !(a == b); + } + +private: + static bool Equals(Status const &a, Status const &b); + friend void internal::SetPayload(Status &, std::string, std::string); + friend ulib::optional + internal::GetPayload(Status const &, std::string const &); + + class Impl; + // A null `impl_` is an OK status. Only non-OK Statuses allocate an Impl. + std::unique_ptr impl_; +}; + +/** + * Stream @p s to @p os. + * + * This in intended for logging and troubleshooting. Applications should not + * depend on the format of this output. + */ +std::ostream &operator<<(std::ostream &os, Status const &s); + +/** + * A runtime error that wraps a `google::cloud::Status`. + */ +class RuntimeStatusError : public std::runtime_error { +public: + /// Constructor from a `Status`. + explicit RuntimeStatusError(Status status); + + /// Returns the original status. + Status const &status() const { return status_; } + +private: + Status status_; +}; + +}// namespace ulib + +#endif// GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_STATUS_H diff --git a/src/ulib/status_or.h b/src/ulib/status_or.h new file mode 100644 index 0000000..543ae74 --- /dev/null +++ b/src/ulib/status_or.h @@ -0,0 +1,355 @@ +// Copyright 2018 Google LLC +// +// 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 +// +// https://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 GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_STATUS_OR_H +#define GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_STATUS_OR_H + +#include "status.h" +#include +#include +#include + +namespace ulib { + +/** + * Holds a value or a `Status` indicating why there is no value. + * + * `StatusOr` represents either a usable `T` value or a `Status` object + * explaining why a `T` value is not present. Typical usage of `StatusOr` + * looks like usage of a smart pointer, or even a `std::optional`, in that + * you first check its validity using a conversion to bool (or by calling + * `StatusOr::ok()`), then you may dereference the object to access the + * contained value. + * + * It is undefined behavior (UB) to dereference a `StatusOr` that is not + * "ok". For example: + * + * @code + * StatusOr foo = FetchFoo(); + * if (!foo) { // Same as !foo.ok() + * // handle error and probably look at foo.status() + * } else { + * foo->DoSomethingFooey(); // UB if !foo + * } + * @endcode + * + * Alternatively, you may call the `StatusOr::value()` member function, + * which is defined to: (1) throw an exception if there is no `T` value, or (2) + * crash the program if exceptions are disabled. It is never UB to call + * `value()`. + * + * @code + * StatusOr foo = FetchFoo(); + * foo.value().DoSomethingFooey(); // May throw/crash if there is no value + * @endcode + * + * Functions that can fail will often return a `StatusOr` instead of + * returning an error code and taking a `T` out-param, or rather than directly + * returning the `T` and throwing an exception on error. `StatusOr` is used + * so that callers can choose whether they want to explicitly check for errors, + * crash the program, or throw exceptions. + * + * Since constructors do not have a return value, they should be designed in + * such a way that they cannot fail by moving the object's complex + * initialization logic into a separate factory function that itself can return + * a `StatusOr`. For example: + * + * @code + * class Bar { + * public: + * Bar(Arg arg); + * ... + * }; + * StatusOr MakeBar() { + * ... complicated logic that might fail + * return Bar(std::move(arg)); + * } + * @endcode + * + * `StatusOr` supports equality comparisons if the underlying type `T` does. + * + * @tparam T the type of the value. + */ +template +class StatusOr final { +public: + static_assert(!std::is_reference::value, + "StatusOr requires T to **not** be a reference type"); + + /** + * A `value_type` member for use in generic programming. + * + * This is analogous to that of `std::optional::value_type`. + */ + using value_type = T; + + /** + * Initializes with an error status (`StatusCode::kUnknown`). + */ + StatusOr() : StatusOr(MakeDefaultStatus()) {} + + StatusOr(StatusOr const &) = default; + StatusOr &operator=(StatusOr const &) = default; + + // NOLINTNEXTLINE(performance-noexcept-move-constructor) + StatusOr(StatusOr &&other) + : status_(std::move(other.status_)), + value_(std::move(other.value_)) + { + other.status_ = MakeDefaultStatus(); + } + + // NOLINTNEXTLINE(performance-noexcept-move-constructor) + StatusOr &operator=(StatusOr &&other) + { + status_ = std::move(other.status_); + value_ = std::move(other.value_); + other.status_ = MakeDefaultStatus(); + return *this; + } + + /** + * Creates a new `StatusOr` holding the error condition @p rhs. + * + * @par Post-conditions + * `ok() == false` and `status() == rhs`. + * + * @param rhs the status to initialize the object. + * @throws std::invalid_argument if `rhs.ok()`. If exceptions are disabled the + * program terminates via `google::cloud::Terminate()` + */ + // NOLINTNEXTLINE(google-explicit-constructor) + StatusOr(Status rhs) : status_(std::move(rhs)) + { + if (status_.ok()) { + // google::cloud::internal::ThrowInvalidArgument(__func__); + throw std::invalid_argument(__func__); + } + } + + /** + * Assigns the given non-OK Status to this `StatusOr`. + * + * @throws std::invalid_argument if `status.ok()`. If exceptions are disabled + * the program terminates via `google::cloud::Terminate()` + */ + StatusOr &operator=(Status status) + { + *this = StatusOr(std::move(status)); + return *this; + } + + /** + * Assign a `T` (or anything convertible to `T`) into the `StatusOr`. + * + * This function does not participate in overload resolution if `U` is equal + * to `StatusOr` (or to a cv-ref-qualified `StatusOr`). + * + * @return a reference to this object. + * @tparam U a type convertible to @p T. + */ + template>::value, + int> = 0 + /// @endcond + > + StatusOr &operator=(U &&rhs) + { + status_ = Status(); + value_ = std::forward(rhs); + return *this; + } + + /** + * Creates a new `StatusOr` holding the value @p rhs. + * + * @par Post-conditions + * `ok() == true` and `value() == rhs`. + * + * @param rhs the value used to initialize the object. + * + * @throws ... If `T`'s move constructor throws. + */ + // NOLINTNEXTLINE(google-explicit-constructor) + StatusOr(T &&rhs) : value_(std::move(rhs)) {} + + /** + * Creates a new `StatusOr` holding the value @p rhs. + * + * @par Post-conditions + * `ok() == true` and `value() == rhs`. + * + * @param rhs the value used to initialize the object. + * + * @throws ... If `T` copy constructor throws. + */ + // NOLINTNEXTLINE(google-explicit-constructor) + StatusOr(T const &rhs) : value_(rhs) {} + + /// Returns `true` when `this` holds a value. + bool ok() const { return status_.ok(); } + + /// Returns `true` when `this` holds a value. + explicit operator bool() const { return status_.ok(); } + + ///@{ + /** + * @name Dereference operators. + * + * @par Pre-conditions + * @parblock + * `ok() == true` + * + * @warning Using these operators when `ok() == false` results in undefined + * behavior. + * @endparblock + * + * @return A properly ref and const-qualified reference to the underlying + * value. + */ + T &operator*() & { return *value_; } + + T const &operator*() const & { return *value_; } + + T &&operator*() && { return *std::move(value_); } + + T const &&operator*() const && { return *std::move(value_); } + + ///@} + + ///@{ + /** + * @name Member access operators. + * + * @par Pre-conditions + * @parblock + * `ok() == true` + * + * @warning Using these operators when `ok() == false` results in undefined + * behavior. + * @endparblock + * + * @return A properly ref and const-qualified pointer to the underlying value. + */ + T *operator->() & { return &*value_; } + + T const *operator->() const & { return &*value_; } + + ///@} + + ///@{ + /** + * @name Value accessors. + * + * @return All these member functions return a (properly ref and + * const-qualified) reference to the underlying value. + * + * @throws RuntimeStatusError with the contents of `status()` if the object + * does not contain a value, i.e., if `ok() == false`. + */ + T &value() & + { + CheckHasValue(); + return **this; + } + + T const &value() const & + { + CheckHasValue(); + return **this; + } + + T &&value() && + { + CheckHasValue(); + return std::move(**this); + } + + T const &&value() const && + { + CheckHasValue(); + return std::move(**this); + } + + ///@} + + ///@{ + /** + * @name Status accessors. + * + * @return A reference to the contained `Status`. + */ + Status const &status() const & { return status_; } + + Status &&status() && { return std::move(status_); } + + ///@} + +private: + static Status MakeDefaultStatus() + { + return Status{StatusCode::kUnknown, "default"}; + } + + void CheckHasValue() const & + { + if (!ok()) { + // internal::ThrowStatus(status_); + throw RuntimeStatusError(std::move(status_)); + } + } + + // When possible, do not copy the status. + void CheckHasValue() && + { + if (!ok()) { + // internal::ThrowStatus(std::move(status_)); + throw RuntimeStatusError(std::move(status_)); + } + } + + Status status_; + ulib::optional value_; +}; + +// Returns true IFF both `StatusOr` objects hold an equal `Status` or an +// equal instance of `T`. This function requires that `T` supports equality. +template +bool +operator==(StatusOr const &a, StatusOr const &b) +{ + if (!a || !b) return a.status() == b.status(); + return *a == *b; +} + +// Returns true of `a` and `b` are not equal. See `operator==` docs above for +// the definition of equal. +template +bool +operator!=(StatusOr const &a, StatusOr const &b) +{ + return !(a == b); +} + +template +StatusOr +make_status_or(T rhs) +{ + return StatusOr(std::move(rhs)); +} + +}// namespace ulib + +#endif// GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_STATUS_OR_H diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 7c653dd..6bb2763 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -3,23 +3,21 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_C_STANDARD 11) set(CMAKE_C_STANDARD_REQUIRED ON) -add_executable(ulib_test - ulib/base/types_unittest.cpp - ulib/log/log_unittest.cpp - ulib/concorrency/mutex_unittest.cpp - ulib/concorrency/event_unittest.cpp - ulib/concorrency/countdown_latch_unittest.cpp - ulib/system/thread_unittest.cpp - ulib/system/thread_pool_unittest.cpp - ulib/system/timer_unittest.cpp - 3party/inja/inja_unittest.cpp - 3party/optional/optional_unittest.cpp - 3party/sqlpp11/sqlpp11_unittest.cpp - ulib/utils/defer_unittest.cpp -) -target_link_libraries(ulib_test PRIVATE - ulib - gtest - gtest_main) +add_executable( + ulib_test + ulib/base/types_unittest.cpp + ulib/log/log_unittest.cpp + ulib/concorrency/mutex_unittest.cpp + ulib/concorrency/event_unittest.cpp + ulib/concorrency/countdown_latch_unittest.cpp + ulib/system/thread_unittest.cpp + ulib/system/thread_pool_unittest.cpp + ulib/system/timer_unittest.cpp + 3party/inja/inja_unittest.cpp + 3party/optional/optional_unittest.cpp + 3party/sqlpp11/sqlpp11_unittest.cpp + ulib/utils/defer_unittest.cpp + ulib/status_or_unittest.cpp) +target_link_libraries(ulib_test PRIVATE ulib gtest gtest_main) add_test(NAME ulib_test COMMAND ulib_test) diff --git a/tests/ulib/status_or_unittest.cpp b/tests/ulib/status_or_unittest.cpp new file mode 100644 index 0000000..debb80b --- /dev/null +++ b/tests/ulib/status_or_unittest.cpp @@ -0,0 +1,11 @@ +#include +#include + +TEST(StatusOr, EmptyValue) +{ + ulib::StatusOr s; + EXPECT_FALSE(s.ok()); + s = 1; + EXPECT_TRUE(s.ok()); + EXPECT_EQ(s.value(), 1); +}