buffet: Add command visibility option to command schema

Add the ability to specify command visibility by using 'visibility'
property in command definition JSON file.

This CL adds parsing code for command visibility as well as the storage
of the property inside CommandDefinition class. The actual usage of this
property will be implemented in subsequent CLs.

BUG=brillo:797
TEST=`FEATURES=test emerge-link buffet`

Change-Id: I3f9b49f8b57f6b63e7c4ae73d042e414c9b6b801
Reviewed-on: https://chromium-review.googlesource.com/266208
Trybot-Ready: Alex Vakulenko <avakulenko@chromium.org>
Tested-by: Alex Vakulenko <avakulenko@chromium.org>
Reviewed-by: Vitaly Buka <vitalybuka@chromium.org>
Commit-Queue: Vitaly Buka <vitalybuka@chromium.org>
diff --git a/buffet/commands/command_definition.cc b/buffet/commands/command_definition.cc
index c5854c2..02b7722 100644
--- a/buffet/commands/command_definition.cc
+++ b/buffet/commands/command_definition.cc
@@ -4,14 +4,70 @@
 
 #include "buffet/commands/command_definition.h"
 
+#include <vector>
+
+#include <chromeos/errors/error.h>
+#include <chromeos/strings/string_utils.h>
+
+#include "buffet/commands/schema_constants.h"
+
 namespace buffet {
 
+bool CommandDefinition::Visibility::FromString(const std::string& str,
+                                               chromeos::ErrorPtr* error) {
+  // This special case is useful for places where we want to make a command
+  // to ALL clients, even if new clients are added in the future.
+  if (str == commands::attributes::kCommand_Visibility_All) {
+    local = true;
+    cloud = true;
+    return true;
+  }
+
+  // Clear any bits first.
+  local = false;
+  cloud = false;
+  if (str == commands::attributes::kCommand_Visibility_None)
+    return true;
+
+  for (const std::string& value : chromeos::string_utils::Split(str, ",")) {
+    if (value == commands::attributes::kCommand_Visibility_Local) {
+      local = true;
+    } else if (value == commands::attributes::kCommand_Visibility_Cloud) {
+      cloud = true;
+    } else {
+      chromeos::Error::AddToPrintf(
+          error, FROM_HERE, errors::commands::kDomain,
+          errors::commands::kInvalidPropValue,
+          "Invalid command visibility value '%s'", value.c_str());
+      return false;
+    }
+  }
+  return true;
+}
+
+std::string CommandDefinition::Visibility::ToString() const {
+  if (local && cloud)
+    return commands::attributes::kCommand_Visibility_All;
+  if (!local && !cloud)
+    return commands::attributes::kCommand_Visibility_None;
+  if (local)
+    return commands::attributes::kCommand_Visibility_Local;
+  return commands::attributes::kCommand_Visibility_Cloud;
+}
+
 CommandDefinition::CommandDefinition(
     const std::string& category,
     std::unique_ptr<const ObjectSchema> parameters,
     std::unique_ptr<const ObjectSchema> results)
         : category_{category},
           parameters_{std::move(parameters)},
-          results_{std::move(results)} {}
+          results_{std::move(results)} {
+  // Set to be available to all clients by default.
+  visibility_ = Visibility::GetAll();
+}
+
+void CommandDefinition::SetVisibility(const Visibility& visibility) {
+  visibility_ = visibility;
+}
 
 }  // namespace buffet
diff --git a/buffet/commands/command_definition.h b/buffet/commands/command_definition.h
index 0cc09b5..74ec09d 100644
--- a/buffet/commands/command_definition.h
+++ b/buffet/commands/command_definition.h
@@ -21,6 +21,29 @@
 // for.
 class CommandDefinition {
  public:
+  struct Visibility {
+    Visibility() = default;
+    Visibility(bool is_local, bool is_cloud)
+        : local{is_local}, cloud{is_cloud} {}
+
+    // Converts a comma-separated string of visibility identifiers into the
+    // Visibility bitset (|str| is a string like "local,cloud").
+    // Special string value "all" is treated as a list of every possible
+    // visibility values and "none" to have all the bits cleared.
+    bool FromString(const std::string& str, chromeos::ErrorPtr* error);
+
+    // Converts the visibility bitset to a string.
+    std::string ToString() const;
+
+    static Visibility GetAll() { return Visibility{true, true}; }
+    static Visibility GetLocal() { return Visibility{true, false}; }
+    static Visibility GetCloud() { return Visibility{false, true}; }
+    static Visibility GetNone() { return Visibility{false, false}; }
+
+    bool local{false};  // Command is available to local clients.
+    bool cloud{false};  // Command is available to cloud clients.
+  };
+
   CommandDefinition(const std::string& category,
                     std::unique_ptr<const ObjectSchema> parameters,
                     std::unique_ptr<const ObjectSchema> results);
@@ -31,11 +54,17 @@
   const ObjectSchema* GetParameters() const { return parameters_.get(); }
   // Gets the object schema for command results.
   const ObjectSchema* GetResults() const { return results_.get(); }
+  // Returns the command visibility.
+  const Visibility& GetVisibility() const { return visibility_; }
+  // Changes the command visibility.
+  void SetVisibility(const Visibility& visibility);
 
  private:
   std::string category_;  // Cmd category. Could be "powerd" for "base.reboot".
   std::unique_ptr<const ObjectSchema> parameters_;  // Command parameters def.
   std::unique_ptr<const ObjectSchema> results_;  // Command results def.
+  Visibility visibility_;  // Available to all by default.
+
   DISALLOW_COPY_AND_ASSIGN(CommandDefinition);
 };
 
diff --git a/buffet/commands/command_definition_unittest.cc b/buffet/commands/command_definition_unittest.cc
index 78520f8..0254fd9 100644
--- a/buffet/commands/command_definition_unittest.cc
+++ b/buffet/commands/command_definition_unittest.cc
@@ -7,15 +7,83 @@
 #include <gtest/gtest.h>
 
 using buffet::ObjectSchema;
+using buffet::CommandDefinition;
+
+TEST(CommandVisibility, DefaultConstructor) {
+  CommandDefinition::Visibility visibility;
+  EXPECT_FALSE(visibility.local);
+  EXPECT_FALSE(visibility.cloud);
+}
+
+TEST(CommandVisibility, InitialState) {
+  auto visibility = CommandDefinition::Visibility::GetAll();
+  EXPECT_TRUE(visibility.local);
+  EXPECT_TRUE(visibility.cloud);
+
+  visibility = CommandDefinition::Visibility::GetLocal();
+  EXPECT_TRUE(visibility.local);
+  EXPECT_FALSE(visibility.cloud);
+
+  visibility = CommandDefinition::Visibility::GetCloud();
+  EXPECT_FALSE(visibility.local);
+  EXPECT_TRUE(visibility.cloud);
+
+  visibility = CommandDefinition::Visibility::GetNone();
+  EXPECT_FALSE(visibility.local);
+  EXPECT_FALSE(visibility.cloud);
+}
+
+TEST(CommandVisibility, FromString) {
+  CommandDefinition::Visibility visibility;
+
+  ASSERT_TRUE(visibility.FromString("local", nullptr));
+  EXPECT_TRUE(visibility.local);
+  EXPECT_FALSE(visibility.cloud);
+
+  ASSERT_TRUE(visibility.FromString("cloud", nullptr));
+  EXPECT_FALSE(visibility.local);
+  EXPECT_TRUE(visibility.cloud);
+
+  ASSERT_TRUE(visibility.FromString("cloud,local", nullptr));
+  EXPECT_TRUE(visibility.local);
+  EXPECT_TRUE(visibility.cloud);
+
+  ASSERT_TRUE(visibility.FromString("none", nullptr));
+  EXPECT_FALSE(visibility.local);
+  EXPECT_FALSE(visibility.cloud);
+
+  ASSERT_TRUE(visibility.FromString("all", nullptr));
+  EXPECT_TRUE(visibility.local);
+  EXPECT_TRUE(visibility.cloud);
+
+  ASSERT_TRUE(visibility.FromString("", nullptr));
+  EXPECT_FALSE(visibility.local);
+  EXPECT_FALSE(visibility.cloud);
+
+  chromeos::ErrorPtr error;
+  ASSERT_FALSE(visibility.FromString("cloud,all", &error));
+  EXPECT_EQ("invalid_parameter_value", error->GetCode());
+  EXPECT_EQ("Invalid command visibility value 'all'", error->GetMessage());
+}
+
+TEST(CommandVisibility, ToString) {
+  EXPECT_EQ("none", CommandDefinition::Visibility::GetNone().ToString());
+  EXPECT_EQ("local", CommandDefinition::Visibility::GetLocal().ToString());
+  EXPECT_EQ("cloud", CommandDefinition::Visibility::GetCloud().ToString());
+  EXPECT_EQ("all", CommandDefinition::Visibility::GetAll().ToString());
+}
 
 TEST(CommandDefinition, Test) {
   std::unique_ptr<const ObjectSchema> params{ObjectSchema::Create()};
   std::unique_ptr<const ObjectSchema> results{ObjectSchema::Create()};
   const ObjectSchema* param_ptr = params.get();
   const ObjectSchema* results_ptr = results.get();
-  buffet::CommandDefinition def{"powerd", std::move(params),
-                                std::move(results)};
+  CommandDefinition def{"powerd", std::move(params), std::move(results)};
   EXPECT_EQ("powerd", def.GetCategory());
   EXPECT_EQ(param_ptr, def.GetParameters());
   EXPECT_EQ(results_ptr, def.GetResults());
+  EXPECT_EQ("all", def.GetVisibility().ToString());
+
+  def.SetVisibility(CommandDefinition::Visibility::GetLocal());
+  EXPECT_EQ("local", def.GetVisibility().ToString());
 }
diff --git a/buffet/commands/command_dictionary.cc b/buffet/commands/command_dictionary.cc
index 68e963c..2469e63 100644
--- a/buffet/commands/command_dictionary.cc
+++ b/buffet/commands/command_dictionary.cc
@@ -69,11 +69,14 @@
 
       const ObjectSchema* base_parameters_def = nullptr;
       const ObjectSchema* base_results_def = nullptr;
+      // By default make it available to all clients.
+      auto visibility = CommandDefinition::Visibility::GetAll();
       if (base_commands) {
         auto cmd = base_commands->FindCommand(full_command_name);
         if (cmd) {
           base_parameters_def = cmd->GetParameters();
           base_results_def = cmd->GetResults();
+          visibility = cmd->GetVisibility();
         }
 
         // If the base command dictionary was provided but the command was not
@@ -111,10 +114,23 @@
       if (!results_schema)
         return false;
 
+      std::string value;
+      using commands::attributes::kCommand_Visibility;
+      if (command_def_json->GetString(kCommand_Visibility, &value)) {
+        if (!visibility.FromString(value, error)) {
+          chromeos::Error::AddToPrintf(
+              error, FROM_HERE, errors::commands::kDomain,
+              errors::commands::kInvalidCommandVisibility,
+              "Error parsing command '%s'", full_command_name.c_str());
+          return false;
+        }
+      }
+
       std::unique_ptr<CommandDefinition> command_def{
         new CommandDefinition{category, std::move(parameters_schema),
                               std::move(results_schema)}
       };
+      command_def->SetVisibility(visibility);
       new_defs.emplace(full_command_name, std::move(command_def));
 
       command_iter.Advance();
@@ -199,8 +215,8 @@
       dict->SetWithoutPathExpansion(package_name, package);
     }
     base::DictionaryValue* command_def = new base::DictionaryValue;
-    command_def->SetWithoutPathExpansion(
-        commands::attributes::kCommand_Parameters, definition.release());
+    command_def->Set(commands::attributes::kCommand_Parameters,
+                     definition.release());
     package->SetWithoutPathExpansion(command_name, command_def);
   }
   return dict;
diff --git a/buffet/commands/command_dictionary.h b/buffet/commands/command_dictionary.h
index 414d79a..db68c8a 100644
--- a/buffet/commands/command_dictionary.h
+++ b/buffet/commands/command_dictionary.h
@@ -73,8 +73,7 @@
   const CommandDefinition* FindCommand(const std::string& command_name) const;
 
  private:
-  using CommandMap =
-      std::map<std::string, std::unique_ptr<const CommandDefinition>>;
+  using CommandMap = std::map<std::string, std::unique_ptr<CommandDefinition>>;
 
   std::unique_ptr<ObjectSchema> BuildObjectSchema(
       const base::DictionaryValue* command_def_json,
diff --git a/buffet/commands/command_dictionary_unittest.cc b/buffet/commands/command_dictionary_unittest.cc
index 0519d88..143b53e 100644
--- a/buffet/commands/command_dictionary_unittest.cc
+++ b/buffet/commands/command_dictionary_unittest.cc
@@ -251,3 +251,167 @@
             "'robot':{'_jump':{'parameters':{'_height':{'type':'integer'}}}}}",
             buffet::unittests::ValueToString(json.get()));
 }
+
+TEST(CommandDictionary, LoadCommandsWithVisibility) {
+  buffet::CommandDictionary dict;
+  auto json = CreateDictionaryValue(R"({
+    'base': {
+      'command1': {
+        'parameters': {},
+        'results': {},
+        'visibility':''
+      },
+      'command2': {
+        'parameters': {},
+        'results': {},
+        'visibility':'local'
+      },
+      'command3': {
+        'parameters': {},
+        'results': {},
+        'visibility':'cloud'
+      },
+      'command4': {
+        'parameters': {},
+        'results': {},
+        'visibility':'all'
+      },
+      'command5': {
+        'parameters': {},
+        'results': {},
+        'visibility':'cloud,local'
+      }
+    }
+  })");
+  EXPECT_TRUE(dict.LoadCommands(*json, "testd", nullptr, nullptr));
+  auto cmd = dict.FindCommand("base.command1");
+  ASSERT_NE(nullptr, cmd);
+  EXPECT_EQ("none", cmd->GetVisibility().ToString());
+
+  cmd = dict.FindCommand("base.command2");
+  ASSERT_NE(nullptr, cmd);
+  EXPECT_EQ("local", cmd->GetVisibility().ToString());
+
+  cmd = dict.FindCommand("base.command3");
+  ASSERT_NE(nullptr, cmd);
+  EXPECT_EQ("cloud", cmd->GetVisibility().ToString());
+
+  cmd = dict.FindCommand("base.command4");
+  ASSERT_NE(nullptr, cmd);
+  EXPECT_EQ("all", cmd->GetVisibility().ToString());
+
+  cmd = dict.FindCommand("base.command5");
+  ASSERT_NE(nullptr, cmd);
+  EXPECT_EQ("all", cmd->GetVisibility().ToString());
+}
+
+TEST(CommandDictionary, LoadCommandsWithVisibility_Inheritance) {
+  buffet::CommandDictionary base_dict;
+  auto json = CreateDictionaryValue(R"({
+    'base': {
+      'command1': {
+        'parameters': {},
+        'results': {},
+        'visibility':''
+      },
+      'command2': {
+        'parameters': {},
+        'results': {},
+        'visibility':'local'
+      },
+      'command3': {
+        'parameters': {},
+        'results': {},
+        'visibility':'cloud'
+      },
+      'command4': {
+        'parameters': {},
+        'results': {},
+        'visibility':'all'
+      },
+      'command5': {
+        'parameters': {},
+        'results': {},
+        'visibility':'local,cloud'
+      }
+    }
+  })");
+  EXPECT_TRUE(base_dict.LoadCommands(*json, "testd", nullptr, nullptr));
+
+  buffet::CommandDictionary dict;
+  json = CreateDictionaryValue(R"({
+    'base': {
+      'command1': {
+        'parameters': {},
+        'results': {}
+      },
+      'command2': {
+        'parameters': {},
+        'results': {}
+      },
+      'command3': {
+        'parameters': {},
+        'results': {}
+      },
+      'command4': {
+        'parameters': {},
+        'results': {}
+      },
+      'command5': {
+        'parameters': {},
+        'results': {}
+      },
+      '_command6': {
+        'parameters': {},
+        'results': {}
+      }
+    }
+  })");
+  EXPECT_TRUE(dict.LoadCommands(*json, "testd", &base_dict, nullptr));
+
+  auto cmd = dict.FindCommand("base.command1");
+  ASSERT_NE(nullptr, cmd);
+  EXPECT_EQ("none", cmd->GetVisibility().ToString());
+
+  cmd = dict.FindCommand("base.command2");
+  ASSERT_NE(nullptr, cmd);
+  EXPECT_EQ("local", cmd->GetVisibility().ToString());
+
+  cmd = dict.FindCommand("base.command3");
+  ASSERT_NE(nullptr, cmd);
+  EXPECT_EQ("cloud", cmd->GetVisibility().ToString());
+
+  cmd = dict.FindCommand("base.command4");
+  ASSERT_NE(nullptr, cmd);
+  EXPECT_EQ("all", cmd->GetVisibility().ToString());
+
+  cmd = dict.FindCommand("base.command5");
+  ASSERT_NE(nullptr, cmd);
+  EXPECT_EQ("all", cmd->GetVisibility().ToString());
+
+  cmd = dict.FindCommand("base._command6");
+  ASSERT_NE(nullptr, cmd);
+  EXPECT_EQ("all", cmd->GetVisibility().ToString());
+}
+
+TEST(CommandDictionary, LoadCommandsWithVisibility_Failures) {
+  buffet::CommandDictionary dict;
+  chromeos::ErrorPtr error;
+
+  auto json = CreateDictionaryValue(R"({
+    'base': {
+      'jump': {
+        'parameters': {},
+        'results': {},
+        'visibility':'foo'
+      }
+    }
+  })");
+  EXPECT_FALSE(dict.LoadCommands(*json, "testd", nullptr, &error));
+  EXPECT_EQ("invalid_command_visibility", error->GetCode());
+  EXPECT_EQ("Error parsing command 'base.jump'", error->GetMessage());
+  EXPECT_EQ("invalid_parameter_value", error->GetInnerError()->GetCode());
+  EXPECT_EQ("Invalid command visibility value 'foo'",
+            error->GetInnerError()->GetMessage());
+  error.reset();
+}
diff --git a/buffet/commands/schema_constants.cc b/buffet/commands/schema_constants.cc
index cd06b32..2868a06 100644
--- a/buffet/commands/schema_constants.cc
+++ b/buffet/commands/schema_constants.cc
@@ -23,6 +23,7 @@
 const char kDuplicateCommandDef[] = "duplicate_command_definition";
 const char kInvalidCommandName[] = "invalid_command_name";
 const char kCommandFailed[] = "command_failed";
+const char kInvalidCommandVisibility[] = "invalid_command_visibility";
 }  // namespace commands
 }  // namespace errors
 
@@ -51,6 +52,12 @@
 const char kCommand_Results[] = "results";
 const char kCommand_State[] = "state";
 const char kCommand_Progress[] = "progress";
+
+const char kCommand_Visibility[] = "visibility";
+const char kCommand_Visibility_None[] = "none";
+const char kCommand_Visibility_Local[] = "local";
+const char kCommand_Visibility_Cloud[] = "cloud";
+const char kCommand_Visibility_All[] = "all";
 }  // namespace attributes
 }  // namespace commands
 
diff --git a/buffet/commands/schema_constants.h b/buffet/commands/schema_constants.h
index 6f5ee71..9740303 100644
--- a/buffet/commands/schema_constants.h
+++ b/buffet/commands/schema_constants.h
@@ -26,6 +26,7 @@
 extern const char kDuplicateCommandDef[];
 extern const char kInvalidCommandName[];
 extern const char kCommandFailed[];
+extern const char kInvalidCommandVisibility[];
 }  // namespace commands
 }  // namespace errors
 
@@ -55,6 +56,12 @@
 extern const char kCommand_Results[];
 extern const char kCommand_State[];
 extern const char kCommand_Progress[];
+
+extern const char kCommand_Visibility[];
+extern const char kCommand_Visibility_None[];
+extern const char kCommand_Visibility_Local[];
+extern const char kCommand_Visibility_Cloud[];
+extern const char kCommand_Visibility_All[];
 }  // namespace attributes
 }  // namespace commands