[modular][sessionctl] Add ability to list existing stories

This adds the command list_stories to sessionctl, which will immediately return the names of existing stories.

TEST=puppet_master_impl_unittest
MF-91 #comment [modular][sessionctl] Add ability to list existing stories

Change-Id: I8be5da0b3b34b2f81a7ffb31b83e47459677ac79
diff --git a/bin/sessionctl/logger.cc b/bin/sessionctl/logger.cc
index 1bda9ee..f8dfafa 100644
--- a/bin/sessionctl/logger.cc
+++ b/bin/sessionctl/logger.cc
@@ -10,6 +10,10 @@
 const char kSuccessString[] = "success";
 const char kCommandString[] = "command";
 
+// Key strings for JSON output.
+const char kParamsKeyString[] = "params";
+const char kStoriesKeyString[] = "stories";
+
 Logger::Logger(bool json_out) : json_out_(json_out) {}
 
 void Logger::LogError(const std::string& command,
@@ -29,6 +33,25 @@
 }
 
 void Logger::Log(const std::string& command,
+                 const fidl::VectorPtr<fidl::StringPtr>& params) const {
+  std::stringstream output;
+
+  if (json_out_) {
+    std::cout << GenerateJsonLogString(command, params) << std::endl;
+  } else {
+    if (command == kListStoriesCommandString) {
+      output << "Stories in this session:" << std::endl;
+    }
+
+    for (auto& param : *params) {
+      output << param << std::endl;
+    }
+
+    std::cout << output.str() << std::endl;
+  }
+}
+
+void Logger::Log(const std::string& command,
                  const std::map<std::string, std::string>& params) const {
   if (json_out_) {
     std::cout << GenerateJsonLogString(command, params) << std::endl;
@@ -39,14 +62,36 @@
 
 std::string Logger::GenerateJsonLogString(
     const std::string& command,
-    const std::map<std::string, std::string>& params) const {
-  rapidjson::Document document;
-  document.SetObject();
-  document.AddMember(kSuccessString, true, document.GetAllocator());
-  document.AddMember(kCommandString, command, document.GetAllocator());
+    const fidl::VectorPtr<fidl::StringPtr>& params) const {
+  rapidjson::Document document = GetDocument(command);
 
-  // Generate params string. Ex. "mod_name": "mod1", "story_name": "story1"
-  // std::string params_string;
+  // Generate array of |params| strings.
+  rapidjson::Document stories;
+  stories.SetArray();
+  for (auto& param : *params) {
+    rapidjson::Value story;
+    story.SetString(param.get().data(), param.get().size());
+    stories.PushBack(story, stories.GetAllocator());
+  }
+
+  // Determine what the strings in |params| represent.
+  rapidjson::Value key;
+  if (command == kListStoriesCommandString) {
+    key.SetString(kStoriesKeyString);
+  } else {
+    key.SetString(kParamsKeyString);
+  }
+
+  document.AddMember(key, stories, document.GetAllocator());
+  return JsonValueToPrettyString(document);
+}
+
+std::string Logger::GenerateJsonLogString(
+    const std::string& command,
+    const std::map<std::string, std::string>& params) const {
+  rapidjson::Document document = GetDocument(command);
+
+  // Generate a document containing |params| keys and values.
   rapidjson::Document paramsJson;
   paramsJson.SetObject();
   for (const auto& p : params) {
@@ -58,10 +103,18 @@
     paramsJson.AddMember(name, value, paramsJson.GetAllocator());
   }
 
-  document.AddMember("params", paramsJson, document.GetAllocator());
+  document.AddMember(kParamsKeyString, paramsJson, document.GetAllocator());
   return JsonValueToPrettyString(document);
 }
 
+rapidjson::Document Logger::GetDocument(const std::string& command) const {
+  rapidjson::Document document;
+  document.SetObject();
+  document.AddMember(kSuccessString, true, document.GetAllocator());
+  document.AddMember(kCommandString, command, document.GetAllocator());
+  return document;
+}
+
 std::string Logger::GenerateLogString(
     const std::string& command,
     const std::map<std::string, std::string>& params) const {
diff --git a/bin/sessionctl/logger.h b/bin/sessionctl/logger.h
index c3820c5..09b9d47 100644
--- a/bin/sessionctl/logger.h
+++ b/bin/sessionctl/logger.h
@@ -26,6 +26,9 @@
   void LogError(const std::string& command, const std::string& error) const;
 
   void Log(const std::string& command,
+           const fidl::VectorPtr<fidl::StringPtr>& params) const;
+
+  void Log(const std::string& command,
            const std::map<std::string, std::string>& params) const;
 
  private:
@@ -33,8 +36,14 @@
   // |params| to be logged.
   std::string GenerateJsonLogString(
       const std::string& command,
+      const fidl::VectorPtr<fidl::StringPtr>& params) const;
+
+  std::string GenerateJsonLogString(
+      const std::string& command,
       const std::map<std::string, std::string>& params) const;
 
+  rapidjson::Document GetDocument(const std::string& command) const;
+
   // Returns a string of the executed |command| with respective |params| to be
   // logged.
   std::string GenerateLogString(
diff --git a/bin/sessionctl/main.cc b/bin/sessionctl/main.cc
index ee49a9f..3e0b014 100644
--- a/bin/sessionctl/main.cc
+++ b/bin/sessionctl/main.cc
@@ -102,7 +102,10 @@
     delete_story
       Delete the story.
         required: --story_name
-        optional: --json_out)";
+        optional: --json_out
+    
+    list_stories
+      List all the stories in the current session.)";
 }
 
 PuppetMasterPtr ConnectToPuppetMaster(const ActiveSession& session) {
diff --git a/bin/sessionctl/session_ctl_app.cc b/bin/sessionctl/session_ctl_app.cc
index 2dc71f7..c4504eb 100644
--- a/bin/sessionctl/session_ctl_app.cc
+++ b/bin/sessionctl/session_ctl_app.cc
@@ -24,6 +24,8 @@
     return ExecuteRemoveModCommand(command_line);
   } else if (cmd == kDeleteStoryCommandString) {
     return ExecuteDeleteStoryCommand(command_line);
+  } else if (cmd == kListStoriesCommandString) {
+    return ExecuteListStoriesCommand();
   } else {
     return kGetUsageErrorString;
   }
@@ -149,6 +151,18 @@
   return parsing_error;
 }
 
+std::string SessionCtlApp::ExecuteListStoriesCommand() {
+  async::PostTask(dispatcher_, [this]() mutable {
+    puppet_master_->GetStories(
+        [this](fidl::VectorPtr<fidl::StringPtr> story_names) {
+          logger_.Log(kListStoriesCommandString, std::move(story_names));
+          on_command_executed_();
+        });
+  });
+
+  return "";
+}
+
 fuchsia::modular::StoryCommand SessionCtlApp::MakeFocusStoryCommand() {
   fuchsia::modular::StoryCommand command;
   fuchsia::modular::SetFocusState set_focus_state;
diff --git a/bin/sessionctl/session_ctl_app.h b/bin/sessionctl/session_ctl_app.h
index a24c1cf..58e39e0 100644
--- a/bin/sessionctl/session_ctl_app.h
+++ b/bin/sessionctl/session_ctl_app.h
@@ -46,6 +46,7 @@
   std::string ExecuteAddModCommand(const fxl::CommandLine& command_line);
   std::string ExecuteRemoveModCommand(const fxl::CommandLine& command_line);
   std::string ExecuteDeleteStoryCommand(const fxl::CommandLine& command_line);
+  std::string ExecuteListStoriesCommand();
 
  private:
   // Focus the story to which the mod we are adding belongs.
diff --git a/bin/sessionctl/session_ctl_constants.h b/bin/sessionctl/session_ctl_constants.h
index a95d377..890a223 100644
--- a/bin/sessionctl/session_ctl_constants.h
+++ b/bin/sessionctl/session_ctl_constants.h
@@ -2,8 +2,8 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#ifndef PERIDOT_BIN_SESSIONCTL_COMMAND_NAMES_H_
-#define PERIDOT_BIN_SESSIONCTL_COMMAND_NAMES_H_
+#ifndef PERIDOT_BIN_SESSIONCTL_SESSION_CTL_CONSTANTS_H_
+#define PERIDOT_BIN_SESSIONCTL_SESSION_CTL_CONSTANTS_H_
 
 namespace modular {
 
@@ -11,6 +11,7 @@
 constexpr char kAddModCommandString[] = "add_mod";
 constexpr char kDeleteStoryCommandString[] = "delete_story";
 constexpr char kRemoveModCommandString[] = "remove_mod";
+constexpr char kListStoriesCommandString[] = "list_stories";
 
 // Flags to pass to SessionCtlApp.
 constexpr char kJsonOutFlagString[] = "json_out";
@@ -26,4 +27,4 @@
 constexpr char kGetUsageErrorString[] = "GetUsage";
 }  // namespace modular
 
-#endif  // PERIDOT_BIN_SESSIONCTL_COMMAND_NAMES_H_
+#endif  // PERIDOT_BIN_SESSIONCTL_SESSION_CTL_CONSTANTS_H_
diff --git a/bin/sessionmgr/puppet_master/puppet_master_impl.cc b/bin/sessionmgr/puppet_master/puppet_master_impl.cc
index f20a486..68c466c 100644
--- a/bin/sessionmgr/puppet_master/puppet_master_impl.cc
+++ b/bin/sessionmgr/puppet_master/puppet_master_impl.cc
@@ -4,6 +4,7 @@
 
 #include "peridot/bin/sessionmgr/puppet_master/puppet_master_impl.h"
 
+#include <lib/fxl/functional/make_copyable.h>
 #include <lib/fxl/logging.h>
 
 #include "peridot/bin/sessionmgr/puppet_master/story_puppet_master_impl.h"
@@ -33,13 +34,6 @@
   story_puppet_masters_.AddBinding(std::move(controller), std::move(request));
 }
 
-void PuppetMasterImpl::WatchSession(
-    fidl::InterfaceHandle<fuchsia::modular::SessionWatcher> session_watcher,
-    fuchsia::modular::WatchSessionOptionsPtr options,
-    WatchSessionCallback done) {
-  FXL_NOTIMPLEMENTED();
-}
-
 void PuppetMasterImpl::DeleteStory(fidl::StringPtr story_name,
                                    DeleteStoryCallback done) {
   session_storage_->DeleteStory(story_name)->Then([done = std::move(done)] {
@@ -47,4 +41,18 @@
   });
 }
 
+void PuppetMasterImpl::GetStories(GetStoriesCallback done) {
+  session_storage_->GetAllStoryData()->Then(
+      [done = std::move(done)](
+          fidl::VectorPtr<fuchsia::modular::internal::StoryData>
+              all_story_data) {
+        auto result = fidl::VectorPtr<fidl::StringPtr>::New(0);
+        for (auto& story : *all_story_data) {
+          result.push_back(std::move(story.story_info.id));
+        }
+
+        done(std::move(result));
+      });
+}
+
 }  // namespace modular
diff --git a/bin/sessionmgr/puppet_master/puppet_master_impl.h b/bin/sessionmgr/puppet_master/puppet_master_impl.h
index 178de42..6260956 100644
--- a/bin/sessionmgr/puppet_master/puppet_master_impl.h
+++ b/bin/sessionmgr/puppet_master/puppet_master_impl.h
@@ -35,15 +35,12 @@
                         request) override;
 
   // |PuppetMaster|
-  void WatchSession(
-      fidl::InterfaceHandle<fuchsia::modular::SessionWatcher> session_watcher,
-      fuchsia::modular::WatchSessionOptionsPtr options,
-      WatchSessionCallback done) override;
-
-  // |PuppetMaster|
   void DeleteStory(fidl::StringPtr story_name,
                    DeleteStoryCallback done) override;
 
+  // |PuppetMaster|
+  void GetStories(GetStoriesCallback done) override;
+
   SessionStorage* const session_storage_;  // Not owned.
   StoryCommandExecutor* const executor_;   // Not owned.
 
diff --git a/bin/sessionmgr/puppet_master/puppet_master_impl_unittest.cc b/bin/sessionmgr/puppet_master/puppet_master_impl_unittest.cc
index 5b45b0d..24176e3 100644
--- a/bin/sessionmgr/puppet_master/puppet_master_impl_unittest.cc
+++ b/bin/sessionmgr/puppet_master/puppet_master_impl_unittest.cc
@@ -303,5 +303,27 @@
   RunLoopUntil([&] { return done; });
 }
 
+TEST_F(PuppetMasterTest, GetStories) {
+  // Zero stories to should exist.
+  bool done{};
+  ptr_->GetStories([&](fidl::VectorPtr<fidl::StringPtr> story_names) {
+    EXPECT_EQ(0u, story_names->size());
+    done = true;
+  });
+  RunLoopUntil([&] { return done; });
+
+  // Create a story.
+  storage_->CreateStory("foo", {} /* extra_info */, {} /* story_options */);
+
+  // "foo" should be listed.
+  done = false;
+  ptr_->GetStories([&](fidl::VectorPtr<fidl::StringPtr> story_names) {
+    ASSERT_EQ(1u, story_names->size());
+    EXPECT_EQ("foo", story_names->at(0));
+    done = true;
+  });
+  RunLoopUntil([&] { return done; });
+}
+
 }  // namespace
 }  // namespace modular
diff --git a/public/fidl/fuchsia.modular/story/puppet_master.fidl b/public/fidl/fuchsia.modular/story/puppet_master.fidl
index 0541a98..ed7bbb4 100644
--- a/public/fidl/fuchsia.modular/story/puppet_master.fidl
+++ b/public/fidl/fuchsia.modular/story/puppet_master.fidl
@@ -55,17 +55,13 @@
     // |request| is closed if control cannot be granted.
     //
     // TODO(thatguy): We want story names to be scoped to the client's namespace.
-    1: ControlStory(string story_name, request<StoryPuppetMaster> request);
-
-    // Events on the session will be sent to |watcher| as long as |watcher| is
-    // alive. |options| specifies filters and policy about which events are sent
-    // to |watcher|.
-    //
-    // NOTE: Not implemented yet, pending MF-85.
-    2: WatchSession(SessionWatcher watcher, WatchSessionOptions? options) -> ();
+    ControlStory(string story_name, request<StoryPuppetMaster> request);
 
     // Deletes a story associated to |story_name|.
-    3: DeleteStory(string story_name) -> ();
+    DeleteStory(string story_name) -> ();
+
+    // Returns a list of all the names of stories in the session.
+    GetStories() -> (vector<string> story_names);
 };
 
 struct WatchSessionOptions {
@@ -74,40 +70,25 @@
     int32 placeholder;
 };
 
-interface SessionWatcher {
-    // Some events, in particular those that represent changes to internal
-    // modular framework state, or those that do not pertain to a single story,
-    // are not represented as commands.
-    1: OnStoryRuntimeState(StoryState state);
-
-    // Indicates the story record was permanently deleted from storage.
-    2: OnStoryDeleted(string story_name);
-
-    // Notifies of StoryCommands. If |is_last| == false, more StoryCommands
-    // were executed in a single batch than can be fit into one message and the
-    // client should expect further calls to OnStoryCommands() immediately.
-    3: OnStoryCommands(string story_name, vector<StoryCommand> commands, bool is_last);
-};
-
 interface StoryPuppetMaster {
     // Enqueues the given |commands| in the order they are given.
     // Can be called as many times as necessary to specify the full
     // set of changes to the story.
     //
     // To execute all enqueued commands, call Execute().
-    1: Enqueue(vector<StoryCommand> commands);
+    Enqueue(vector<StoryCommand> commands);
 
     // Executes the commands enqueued so far by Enqueue() in order and as
     // a batch: no other commands from other clients will be interleaved.
     //
     // If an error occurs, execution is stopped and an error code
     // and message are returned in |result|.
-    2: Execute() -> (ExecuteResult result);
+    Execute() -> (ExecuteResult result);
 
     // If this story is new, sets the options for its creation. Must be called
     // before the first call to Execute(). Any subsequent calls (either on the
     // same channel or on a new StoryPuppetMaster for an existing story) are
     // ignored. Some StoryOptions can be changed through specific story commands.
     // See story_command.fidl for details.
-    3: SetCreateOptions(StoryOptions story_options);
+    SetCreateOptions(StoryOptions story_options);
 };