浏览代码

Merge branch 'refactor/action/recursive'

* refactor/action/recursive:
  feat: add support for recursively defined actions being dispatched to typed_main()
  refactor: show usage instead of throwing on missing action
  feat: enable recursive actions using boost
  refactor: allow filling out Action elements of main args, instead of using them as stubs
  refactor: inject parent context into Action
  refactor: move WithDefault::operator T to impl file
Sam Jaffe 2 年之前
父节点
当前提交
75c2eeed44

+ 27 - 0
include/program_args/any.h

@@ -0,0 +1,27 @@
+//
+//  any.h
+//  program_args
+//
+//  Created by Sam Jaffe on 2/25/23.
+//
+
+#pragma once
+
+#include <typeindex>
+
+namespace program::detail {
+class Any {
+public:
+  Any() = default;
+  template <typename T> Any(T const * ptr) : type_(typeid(T)), ptr_(ptr) {}
+
+  template <typename T> T const * get() const {
+    if (type_ != typeid(T)) { return nullptr; }
+    return static_cast<T const *>(ptr_);
+  }
+
+private:
+  std::type_index type_ = typeid(void);
+  void const * ptr_ = nullptr;
+};
+}

+ 62 - 22
include/program_args/arguments.h

@@ -5,6 +5,8 @@
 #include <string>
 #include <vector>
 
+#include "program_args/any.h"
+
 namespace program {
 
 /**
@@ -19,6 +21,9 @@ struct LongArg {
   std::string str;
 };
 
+constexpr struct usage_on_error_t {
+} usage_on_error;
+
 template <typename Impl> class Arguments {
 private:
   using option_id = std::string;
@@ -30,12 +35,25 @@ private:
 
 public:
   Arguments() = default;
-  Arguments(int argc, char const * const * const argv);
+  Arguments(int argc, char const * const * const argv)
+      : Arguments(argc, argv, (void const *)nullptr) {}
 
   std::vector<std::string> args() const {
     return {arguments.begin() + argument_names.size(), arguments.end()};
   }
 
+protected:
+  template <typename T> Arguments(T const * parent) : parent_(parent) {}
+  template <typename T>
+  Arguments(int argc, char const * const * const argv, T const * parent);
+
+  template <typename T> T const * parent() const { return parent_.get<T>(); }
+
+  template <typename Tuple, typename Default, size_t... Is, typename... Parents>
+  int invoke_action(Default const & default_action, Tuple const & tuple,
+                    std::index_sequence<Is...>,
+                    Parents const &... parents) const;
+
 protected:
   /**
    * @brief Define an "action"/"subcommand"/"verb" to be executed. An example of
@@ -137,26 +155,42 @@ private:
   // A helper for what limited introspection is required for handling actions in
   // test
   friend struct ArgumentTestHelper;
+  template <typename> friend class Arguments;
   friend int main(int, char const * const *);
 
+  operator bool() const { return primed_; }
+
+  template <typename... Parents> int invoke(Parents const &... parents) const {
+    return typed_main(parents..., static_cast<Impl const &>(*this));
+  }
+
+  template <typename T>
+  std::shared_ptr<T> make_action(std::string const & name) const {
+    if (action_name_ != name) { return nullptr; }
+    auto ptr = make_action_(static_cast<Impl const &>(*this));
+    return std::static_pointer_cast<T>(ptr);
+  }
+
 private:
-  using main_callback_t = std::function<int(Impl const &)>;
-  using make_hook_t =
-      std::function<main_callback_t(size_t, char const * const *)>;
+  using make_action_t = std::function<std::shared_ptr<void>(Impl const &)>;
+  using action_hook_t =
+      std::function<make_action_t(size_t, char const * const *)>;
   // Metadata variables
   bool primed_{false};
+  detail::Any parent_;
   std::string rest_name;
   std::map<option_id, std::string> argument_descriptions;
   std::map<size_t, option_id> argument_names;
   std::map<option_id, std::string> option_descriptions;
   std::map<std::string, option_id> option_names;
   std::set<option_id> flag_names;
-  std::map<std::string, make_hook_t> actions;
+  std::map<std::string, action_hook_t> actions;
+  std::string action_name_{""};
+  make_action_t make_action_{nullptr};
   std::map<std::string, std::string> action_descriptions;
 
   // Data/Output variables
   std::string program;
-  main_callback_t main_callback{nullptr};
   constexpr static size_t const no_optional_args{~0ul};
   size_t optional_from{no_optional_args};
   std::vector<std::string> arguments;
@@ -218,31 +252,37 @@ template <typename B, typename V>
 struct Arguments<Impl>::WithDefault {
   B impl;
   V default_value;
-  template <typename T> operator T() const {
-    if (impl) { return impl; }
-    if constexpr (std::is_invocable_r<T, V>{}) {
-      return T(default_value());
-    } else {
-      return T{default_value};
-    }
-  }
+
+  template <typename T> operator T() const;
 };
 
 }
 
 #include "arguments_impl.hpp"
 
-template <typename Args, typename Action>
-int typed_main(Args const &, Action const &);
+template <typename Args, typename... Actions>
+int typed_main(Args const &, Actions const &...);
 
-#define TYPED_MAIN(tname)                                                      \
-  int typed_main(tname const & args);                                          \
+#define PROGRAM_ARGS_INVOKE_WITH_DEFAULT(default, ...)                         \
+  template <typename... Parents>                                               \
+  int invoke(Parents const &... parents) const {                               \
+    return invoke_action(default, program::actions(__VA_ARGS__),               \
+                         program::count(__VA_ARGS__), parents..., *this);      \
+  }                                                                            \
+  using Arguments::Arguments
+
+#define PROGRAM_ARGS_INVOKE(...)                                               \
+  PROGRAM_ARGS_INVOKE_WITH_DEFAULT(program::usage_on_error, ##__VA_ARGS__)
+
+#define PROGRAM_ARGS_MAIN(tname)                                               \
   int main(int argc, char const * const * const argv) try {                    \
-    tname args(argc, argv);                                                    \
-    return (args.main_callback) ? args.main_callback(args) : typed_main(args); \
+    return tname(argc, argv).invoke();                                         \
   } catch (program::ProgramArgumentsError const & pae) {                       \
     std::cerr << "Error in program argument handling: " << pae.what() << "\n"; \
-  }                                                                            \
-  int typed_main(tname const & args)
+  }
+
+#define TYPED_MAIN(tname)                                                      \
+  PROGRAM_ARGS_MAIN(tname)                                                     \
+  template <> int typed_main(tname const & args)
 
 #define PROGRAM_DEFER(...) [this]() { return __VA_ARGS__; }

+ 66 - 6
include/program_args/arguments_impl.hpp

@@ -22,10 +22,12 @@ Arguments<Impl>::Action::operator T() const {
                 "Action can only bind to Arguments");
   if (!primed()) {
     self->actions.emplace(name, [](int argc, char const * const * argv) {
-      return [action = T(argc, argv)](Impl const & args) {
-        return typed_main(args, action);
+      return [=](Impl const & args) {
+        return std::make_shared<T>(T(argc, argv, &args));
       };
     });
+  } else if (auto ptr = self->make_action<T>(name)) {
+    return *ptr;
   }
   return T();
 }
@@ -163,8 +165,26 @@ template <typename Impl> bool Arguments<Impl>::Option::primed() const {
 namespace program {
 
 template <typename Impl>
-Arguments<Impl>::Arguments(int argc, char const * const * const argv) {
-  Impl generator;
+template <typename B, typename V>
+template <typename T>
+Arguments<Impl>::WithDefault<B, V>::operator T() const {
+  if (impl) { return impl; }
+  if constexpr (std::is_invocable_r<T, V>{}) {
+    return T(default_value());
+  } else {
+    return T{default_value};
+  }
+}
+
+}
+
+namespace program {
+
+template <typename Impl>
+template <typename T>
+Arguments<Impl>::Arguments(int argc, char const * const * const argv,
+                           T const * parent) {
+  Impl generator(parent);
   *this = static_cast<Arguments const &>(generator);
   if (argument_names.size() &&
       argument_names.rbegin()->first != argument_names.size() - 1) {
@@ -185,11 +205,13 @@ Arguments<Impl>::Arguments(int argc, char const * const * const argv) {
       arguments.insert(arguments.end(), &next, &argv[argc]);
       break;
     } else if (auto hook = actions[arg]) {
-      main_callback = hook(argc - i, argv + i);
+      action_name_ = arg;
+      make_action_ = hook(argc - i, argv + i);
       return;
     } else if (arg == "help" && next && actions[next]) {
       char const * const help[] = {next, "--help"};
-      main_callback = actions[next](2, help);
+      action_name_ = next;
+      make_action_ = hook(2, help);
       return;
     } else if (arg[0] != '-') {
       arguments.emplace_back(arg);
@@ -322,4 +344,42 @@ auto Arguments<Impl>::option(char abbrev, std::string const & description) {
   return Option{this, {abbrev}, abbrev, description};
 }
 
+template <typename Impl>
+template <typename Tuple, typename Default, size_t... Is, typename... Parents>
+int Arguments<Impl>::invoke_action(Default const & default_action,
+                                   Tuple const & tuple,
+                                   std::index_sequence<Is...>,
+                                   Parents const &... parents) const {
+  // Only actions that were reached in the constructor will ever have the
+  // primed_ flag set to true. Therefore, there is a guarantee that this
+  // function will only call invoke() on a child one or zero times.
+  auto invoke_one = [&parents...](auto const & action) {
+    if (!action.primed_) { return std::make_pair(false, 0); }
+    return std::make_pair(true, action.invoke(parents...));
+  };
+
+  // Because of that, it is safe to use a map from bool => status
+  // Since we only care about the case where an action was invoked.
+  std::map<bool, int> const rval{invoke_one(std::get<Is>(tuple))...};
+  if (rval.count(true)) { return rval.at(true); }
+
+  // If no action was invoked, then we need to handle the fallthrough case
+  if constexpr (std::is_same_v<Default, usage_on_error_t>) {
+    usage();
+    return 1;
+  } else {
+    return default_action.invoke(parents...);
+  }
+}
+
+}
+
+namespace program {
+template <typename... Ts> auto actions(Ts const &... ts) {
+  return std::forward_as_tuple(ts...);
+}
+
+template <typename... Ts> auto count(Ts const &...) {
+  return std::make_index_sequence<sizeof...(Ts)>{};
+}
 }

+ 4 - 0
program_args.xcodeproj/project.pbxproj

@@ -82,6 +82,7 @@
 /* End PBXContainerItemProxy section */
 
 /* Begin PBXFileReference section */
+		CD2A288C29AAC00D009B8C77 /* any.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = any.h; sourceTree = "<group>"; };
 		CD2F0C5325DC9AE000CB394A /* scoped_buffer_capture.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = scoped_buffer_capture.xcodeproj; path = external/scoped_buffer_capture/scoped_buffer_capture.xcodeproj; sourceTree = "<group>"; };
 		CD2F0C6325DC9B3500CB394A /* usage_test.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = usage_test.cpp; sourceTree = "<group>"; };
 		CD8C5A7525D0577E0004A6D9 /* program_args-test.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "program_args-test.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -155,6 +156,7 @@
 		CD8C5A8F25D057C00004A6D9 /* program_args */ = {
 			isa = PBXGroup;
 			children = (
+				CD2A288C29AAC00D009B8C77 /* any.h */,
 				CD8C5A9025D057C00004A6D9 /* arguments.h */,
 				CD8C5A9125D057C00004A6D9 /* utilities.h */,
 				CD8C5A9225D057C00004A6D9 /* arguments_impl.hpp */,
@@ -508,6 +510,7 @@
 				SYSTEM_HEADER_SEARCH_PATHS = (
 					"external/string-utils/include/",
 					external/scoped_buffer_capture/include/,
+					/opt/local/include/,
 				);
 				USER_HEADER_SEARCH_PATHS = include/;
 			};
@@ -565,6 +568,7 @@
 				SYSTEM_HEADER_SEARCH_PATHS = (
 					"external/string-utils/include/",
 					external/scoped_buffer_capture/include/,
+					/opt/local/include/,
 				);
 				USER_HEADER_SEARCH_PATHS = include/;
 			};

+ 86 - 13
test/action_test.cpp

@@ -9,13 +9,12 @@
 
 #include "xcode_gtest_helper.h"
 
+using testing::NotNull;
+
 namespace program {
 struct ArgumentTestHelper {
-  template <typename Impl> bool has_main(Impl const & args) const {
-    return static_cast<bool>(args.main_callback);
-  }
-  template <typename Impl> int main(Impl const & args) const {
-    return args.main_callback(args);
+  template <typename Impl> static int main(Impl const & args) {
+    return args.invoke();
   }
 };
 }
@@ -24,11 +23,17 @@ template <typename T, size_t N> static T parse(char const * const (&argv)[N]) {
   return T(N, argv);
 }
 
-std::string g_type_name;
+program::detail::Any g_action;
 
 template <typename Args, typename Action>
-int typed_main(Args const &, Action const &) {
-  g_type_name = typeid(Action).name();
+int typed_main(Args const &, Action const & action) {
+  g_action = program::detail::Any(&action);
+  return 0;
+}
+
+template <typename Args, typename Action, typename SubAction>
+int typed_main(Args const &, Action const &, SubAction const & action) {
+  g_action = program::detail::Any(&action);
   return 0;
 }
 
@@ -42,6 +47,36 @@ struct Commit : program::Arguments<Commit> {
   std::string message = option("message", 'm');
 };
 
+class Push : public program::Arguments<Push> {
+public:
+  using Arguments::Arguments;
+  std::string remote = argument(0, "repository") = PROGRAM_DEFER(_remote());
+
+private:
+  std::string _remote() const;
+};
+
+struct SetUrl : program::Arguments<SetUrl> {
+  using Arguments::Arguments;
+  bool push = flag("push") = false;
+  std::string name = argument(0, "name");
+  std::string new_url = argument(1, "newurl");
+  std::string old_url = argument(2, "oldurl") = "";
+};
+
+struct Show : program::Arguments<Show> {
+  using Arguments::Arguments;
+  bool use_cached = flag('n') = false;
+};
+
+struct Remote : public program::Arguments<Remote> {
+  PROGRAM_ARGS_INVOKE_WITH_DEFAULT(show, show, set_url);
+
+  bool verbose = flag('v') = false;
+  SetUrl set_url = action("set-url");
+  Show show = action("show");
+};
+
 struct Bad1 : program::Arguments<Bad1> {
   using Arguments::Arguments;
 
@@ -57,14 +92,22 @@ struct Bad2 : program::Arguments<Bad2> {
 };
 
 struct Git : program::Arguments<Git> {
-  using Arguments::Arguments;
+  PROGRAM_ARGS_INVOKE(commit, checkout, push, remote);
 
+  std::string pwd = option('C');
   bool verbose = flag("verbose", 'v');
 
   Commit commit = action("commit");
   Checkout checkout = action("checkout");
+  Push push = action("push");
+  Remote remote = action("remote");
 };
 
+std::string Push::_remote() const {
+  if (Git const * git = Arguments::parent<Git>()) { return git->pwd; }
+  return "";
+}
+
 TEST(ActionTest, CannotMixActionAndArgument) {
   EXPECT_THROW(Bad1(), program::ArgumentMixingError);
   EXPECT_THROW(Bad2(), program::ArgumentMixingError);
@@ -74,8 +117,38 @@ TEST(ActionTest, CanRunWithMultipleActions) { EXPECT_NO_THROW(Git()); }
 
 TEST(ActionTest, ActionIsRouted) {
   Git git = parse<Git>({"", "-v", "commit", "-m", "this is a message"});
-  program::ArgumentTestHelper helper;
-  EXPECT_TRUE(helper.has_main(git));
-  helper.main(git);
-  EXPECT_THAT(g_type_name, typeid(Commit).name());
+  EXPECT_THAT(git.commit.message, "this is a message");
+}
+
+TEST(ActionTest, CanDiveIntoTypedMain) {
+  Git git = parse<Git>({"", "-v", "commit", "-m", "this is a message"});
+  program::ArgumentTestHelper::main(git);
+  EXPECT_THAT(g_action.get<Commit>(), NotNull());
+}
+
+TEST(ActionTest, CanFetchParentInfo) {
+  Git git = parse<Git>({"", "-C", "./submodules/X", "push"});
+  EXPECT_THAT(git.push.remote, "./submodules/X");
+}
+
+TEST(ActionTest, ReturnsFailureOnNoAction) {
+  Git git = parse<Git>({""});
+
+  EXPECT_THAT(program::ArgumentTestHelper::main(git), 1);
+}
+
+TEST(ActionTest, CanRecursivelyPerformActions) {
+  Git git = parse<Git>({"", "remote", "-v", "show", "-n"});
+  EXPECT_TRUE(git.remote.verbose);
+  EXPECT_TRUE(git.remote.show.use_cached);
+
+  program::ArgumentTestHelper::main(git);
+  EXPECT_THAT(g_action.get<Show>(), NotNull());
+}
+
+TEST(ActionTest, CanStoreDefaultAction) {
+  Git git = parse<Git>({"", "remote"});
+
+  program::ArgumentTestHelper::main(git);
+  EXPECT_THAT(g_action.get<Show>(), NotNull());
 }