Prechádzať zdrojové kódy

feat: add support for recursively defined actions being dispatched to typed_main()

Sam Jaffe 2 rokov pred
rodič
commit
edcfdacb48

+ 17 - 14
include/program_args/arguments.h

@@ -21,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;
@@ -46,6 +49,11 @@ protected:
 
   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
@@ -255,21 +263,16 @@ struct Arguments<Impl>::WithDefault {
 template <typename Args, typename... Actions>
 int typed_main(Args const &, Actions const &...);
 
-#include <boost/preprocessor.hpp>
-
-#define TRY_INVOKE(r, data, elem)                                              \
-  if (self.elem) { return self.elem.invoke(parents..., self); }
-
-#define PROGRAM_ARGS_INVOKE(tname, ...)                                        \
-  template <>                                                                  \
+#define PROGRAM_ARGS_INVOKE_WITH_DEFAULT(default, ...)                         \
   template <typename... Parents>                                               \
-  int program::Arguments<tname>::invoke(Parents const &... parents) const {    \
-    tname const & self = static_cast<tname const &>(*this);                    \
-    BOOST_PP_SEQ_FOR_EACH(TRY_INVOKE, _,                                       \
-                          BOOST_PP_VARIADIC_TO_SEQ(__VA_ARGS__))               \
-    usage();                                                                   \
-    return 1;                                                                  \
-  }
+  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 {                    \

+ 38 - 0
include/program_args/arguments_impl.hpp

@@ -344,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)>{};
+}
 }

+ 51 - 3
test/action_test.cpp

@@ -31,6 +31,12 @@ int typed_main(Args const &, Action const & 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;
+}
+
 struct Checkout : program::Arguments<Checkout> {
   using Arguments::Arguments;
   std::string commitish = argument(0, "commit-ish");
@@ -50,6 +56,27 @@ 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;
 
@@ -65,7 +92,7 @@ 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');
@@ -73,10 +100,9 @@ struct Git : program::Arguments<Git> {
   Commit commit = action("commit");
   Checkout checkout = action("checkout");
   Push push = action("push");
+  Remote remote = action("remote");
 };
 
-PROGRAM_ARGS_INVOKE(Git, commit, checkout, push)
-
 std::string Push::_remote() const {
   if (Git const * git = Arguments::parent<Git>()) { return git->pwd; }
   return "";
@@ -104,3 +130,25 @@ 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());
+}