perfetto: add payload APIs to base::Status

This CL allows adding "payloads" to base::Status, allowing attaching
arbitrary key, value arguments. This allows providing additional context about the error (e.g. source locations, human readable messages etc).

Inspiration for these APIs comes directly from absl::Status which has
methods with the same prototypes (but different impls).

Change-Id: Ifb5ab4ebbb2265e69eb7667602c19c7f8b49d4bb
diff --git a/Android.bp b/Android.bp
index b65d3fe..eab5301 100644
--- a/Android.bp
+++ b/Android.bp
@@ -8425,6 +8425,7 @@
         "src/base/scoped_file_unittest.cc",
         "src/base/small_vector_unittest.cc",
         "src/base/status_or_unittest.cc",
+        "src/base/status_unittest.cc",
         "src/base/string_splitter_unittest.cc",
         "src/base/string_utils_unittest.cc",
         "src/base/string_view_unittest.cc",
diff --git a/include/perfetto/base/status.h b/include/perfetto/base/status.h
index 2939357..f059184 100644
--- a/include/perfetto/base/status.h
+++ b/include/perfetto/base/status.h
@@ -17,7 +17,10 @@
 #ifndef INCLUDE_PERFETTO_BASE_STATUS_H_
 #define INCLUDE_PERFETTO_BASE_STATUS_H_
 
+#include <optional>
 #include <string>
+#include <string_view>
+#include <vector>
 
 #include "perfetto/base/compiler.h"
 #include "perfetto/base/export.h"
@@ -30,6 +33,10 @@
 // This can used as the return type of functions which would usually return an
 // bool for success or int for errno but also wants to add some string context
 // (ususally for logging).
+//
+// Similar to absl::Status, an optional "payload" can also be included with more
+// context about the error. This allows passing additional metadata about the
+// error (e.g. location of errors, potential mitigations etc).
 class PERFETTO_EXPORT_COMPONENT Status {
  public:
   Status() : ok_(true) {}
@@ -52,9 +59,49 @@
   const std::string& message() const { return message_; }
   const char* c_message() const { return message_.c_str(); }
 
+  //////////////////////////////////////////////////////////////////////////////
+  // Payload Management APIs
+  //////////////////////////////////////////////////////////////////////////////
+
+  // Payloads can be attached to error statuses to provide additional context.
+  //
+  // Payloads are (key, value) pairs, where the key is a string acting as a
+  // unique "type URL" and the value is an opaque string. The "type URL" should
+  // be unique, follow the format of a URL and, ideally, documentation on how to
+  // interpret its associated data should be available.
+  //
+  // To attach a payload to a status object, call `Status::SetPayload()`.
+  // Similarly, to extract the payload from a status, call
+  // `Status::GetPayload()`.
+  //
+  // Note: the payload APIs are only meaningful to call when the status is an
+  // error. Otherwise, all methods are noops.
+
+  // Gets the payload for the given |type_url| if one exists.
+  //
+  // Will always return std::nullopt if |ok()|.
+  std::optional<std::string_view> GetPayload(std::string_view type_url);
+
+  // Sets the payload for the given key. The key should
+  //
+  // Will always do nothing if |ok()|.
+  void SetPayload(std::string_view type_url, std::string value);
+
+  // Erases the payload for the given string and returns true if the payload
+  // existed and was erased.
+  //
+  // Will always do nothing if |ok()|.
+  bool ErasePayload(std::string_view type_url);
+
  private:
+  struct Payload {
+    std::string type_url;
+    std::string payload;
+  };
+
   bool ok_ = false;
   std::string message_;
+  std::vector<Payload> payloads_;
 };
 
 // Returns a status object which represents the Ok status.
diff --git a/src/base/BUILD.gn b/src/base/BUILD.gn
index 8d48bf0..7dfce6b 100644
--- a/src/base/BUILD.gn
+++ b/src/base/BUILD.gn
@@ -199,6 +199,7 @@
     "scoped_file_unittest.cc",
     "small_vector_unittest.cc",
     "status_or_unittest.cc",
+    "status_unittest.cc",
     "string_splitter_unittest.cc",
     "string_utils_unittest.cc",
     "string_view_unittest.cc",
diff --git a/src/base/status.cc b/src/base/status.cc
index 30ccc47..d3c13e8 100644
--- a/src/base/status.cc
+++ b/src/base/status.cc
@@ -17,6 +17,7 @@
 #include "perfetto/base/status.h"
 
 #include <stdarg.h>
+#include <algorithm>
 
 namespace perfetto {
 namespace base {
@@ -31,5 +32,42 @@
   return status;
 }
 
+std::optional<std::string_view> Status::GetPayload(std::string_view type_url) {
+  if (ok()) {
+    return std::nullopt;
+  }
+  for (const auto& kv : payloads_) {
+    if (kv.type_url == type_url) {
+      return kv.payload;
+    }
+  }
+  return std::nullopt;
+}
+
+void Status::SetPayload(std::string_view type_url, std::string value) {
+  if (ok()) {
+    return;
+  }
+  for (auto& kv : payloads_) {
+    if (kv.type_url == type_url) {
+      kv.payload = value;
+      return;
+    }
+  }
+  payloads_.push_back(Payload{std::string(type_url), std::move(value)});
+}
+
+bool Status::ErasePayload(std::string_view type_url) {
+  if (ok()) {
+    return false;
+  }
+  auto it = std::remove_if(
+      payloads_.begin(), payloads_.end(),
+      [type_url](const Payload& p) { return p.type_url == type_url; });
+  bool erased = it != payloads_.end();
+  payloads_.erase(it, payloads_.end());
+  return erased;
+}
+
 }  // namespace base
 }  // namespace perfetto
diff --git a/src/base/status_unittest.cc b/src/base/status_unittest.cc
new file mode 100644
index 0000000..df42b31
--- /dev/null
+++ b/src/base/status_unittest.cc
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * 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 "perfetto/base/status.h"
+
+#include "test/gtest_and_gmock.h"
+
+namespace perfetto {
+namespace base {
+
+TEST(StatusTest, GetMissingPayload) {
+  base::Status status = base::ErrStatus("Error");
+  ASSERT_EQ(status.GetPayload("test.foo.com/bar"), std::nullopt);
+}
+
+TEST(StatusTest, SetThenGetPayload) {
+  base::Status status = base::ErrStatus("Error");
+  status.SetPayload("test.foo.com/bar", "payload_value");
+  ASSERT_EQ(status.GetPayload("test.foo.com/bar"), "payload_value");
+}
+
+TEST(StatusTest, SetEraseGetPayload) {
+  base::Status status = base::ErrStatus("Error");
+  status.SetPayload("test.foo.com/bar", "payload_value");
+  ASSERT_TRUE(status.ErasePayload("test.foo.com/bar"));
+  ASSERT_EQ(status.GetPayload("test.foo.com/bar"), std::nullopt);
+}
+
+TEST(StatusTest, SetOverride) {
+  base::Status status = base::ErrStatus("Error");
+  status.SetPayload("test.foo.com/bar", "payload_value");
+  status.SetPayload("test.foo.com/bar", "other_value");
+  ASSERT_EQ(status.GetPayload("test.foo.com/bar"), "other_value");
+}
+
+TEST(StatusTest, SetGetOk) {
+  base::Status status = base::OkStatus();
+  status.SetPayload("test.foo.com/bar", "payload_value");
+  ASSERT_EQ(status.GetPayload("test.foo.com/bar"), std::nullopt);
+}
+
+TEST(StatusTest, SetMultipleAndDuplicate) {
+  base::Status status = base::ErrStatus("Error");
+  status.SetPayload("test.foo.com/bar", "payload_value");
+  status.SetPayload("test.foo.com/bar1", "1");
+  status.SetPayload("test.foo.com/bar2", "2");
+  status.SetPayload("test.foo.com/bar", "other_value");
+  ASSERT_EQ(status.GetPayload("test.foo.com/bar"), "other_value");
+  ASSERT_EQ(status.GetPayload("test.foo.com/bar1"), "1");
+  ASSERT_EQ(status.GetPayload("test.foo.com/bar2"), "2");
+}
+
+}  // namespace base
+}  // namespace perfetto