Browse Source

refactor: inject parent context into Action

Sam Jaffe 2 years ago
parent
commit
c63c9c7adf

+ 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;
+};
+}

+ 12 - 1
include/program_args/arguments.h

@@ -5,6 +5,8 @@
 #include <string>
 #include <vector>
 
+#include "program_args/any.h"
+
 namespace program {
 
 /**
@@ -30,12 +32,20 @@ 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>(); }
+
 protected:
   /**
    * @brief Define an "action"/"subcommand"/"verb" to be executed. An example of
@@ -145,6 +155,7 @@ private:
       std::function<main_callback_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;

+ 7 - 4
include/program_args/arguments_impl.hpp

@@ -22,8 +22,9 @@ 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);
+      (void)T(argc, argv);
+      return [=](Impl const & args) {
+        return typed_main(args, T(argc, argv, &args));
       };
     });
   }
@@ -179,8 +180,10 @@ Arguments<Impl>::WithDefault<B, V>::operator T() const {
 namespace program {
 
 template <typename Impl>
-Arguments<Impl>::Arguments(int argc, char const * const * const argv) {
-  Impl generator;
+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) {

+ 2 - 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 */,

+ 36 - 5
test/action_test.cpp

@@ -9,6 +9,8 @@
 
 #include "xcode_gtest_helper.h"
 
+using testing::NotNull;
+
 namespace program {
 struct ArgumentTestHelper {
   template <typename Impl> bool has_main(Impl const & args) const {
@@ -24,11 +26,11 @@ 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;
 }
 
@@ -42,6 +44,15 @@ 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 Bad1 : program::Arguments<Bad1> {
   using Arguments::Arguments;
 
@@ -56,15 +67,24 @@ struct Bad2 : program::Arguments<Bad2> {
   Commit commit = action("commit");
 };
 
-struct Git : program::Arguments<Git> {
+class Git : program::Arguments<Git> {
+public:
   using Arguments::Arguments;
 
+  std::string pwd = option('C');
   bool verbose = flag("verbose", 'v');
 
+private:
   Commit commit = action("commit");
   Checkout checkout = action("checkout");
+  Push push = action("push");
 };
 
+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);
@@ -77,5 +97,16 @@ TEST(ActionTest, ActionIsRouted) {
   program::ArgumentTestHelper helper;
   EXPECT_TRUE(helper.has_main(git));
   helper.main(git);
-  EXPECT_THAT(g_type_name, typeid(Commit).name());
+  EXPECT_THAT(g_action.get<Commit>(), NotNull());
+  EXPECT_THAT(g_action.get<Commit>()->message, "this is a message");
+}
+
+TEST(ActionTest, CanFetchParentInfo) {
+  Git git = parse<Git>({"", "-C", "./submodules/X", "push"});
+  program::ArgumentTestHelper helper;
+  EXPECT_TRUE(helper.has_main(git));
+  helper.main(git);
+
+  EXPECT_THAT(g_action.get<Push>(), NotNull());
+  EXPECT_THAT(g_action.get<Push>()->remote, "./submodules/X");
 }