1

My question has a couple layers to it so please bear with me? I built a module that adds workflows from the Workflow gem to an instance, when you call a method on that instance. It has to be able to receive the description as a Hash or some basic data structure and then turn that into something that puts the described workflow onto the class, at run-time. So everything has to happen at run-time. It's a bit complex to explain what all the crazy requirements are for but it's still a good question, I hope. Anyways, The best I can do to be brief for a context, here, is this:

  1. Build a class and include this module I built.
  2. Create an instance of Your class.
  3. Call the inject_workflow(some_workflow_description) method on the instance. It all must be dynamic.

The tricky part for me is that when I use public_send() or eval() or exec(), I still have to send some nested method calls and it seems like they use 2 different scopes, the class' and Workflow's (the gem). When someone uses the Workflow gem, they hand write these method calls in their class so it scopes everything correctly. The gem gets to have access to the class it creates methods on. The way I'm trying to do it, the user doesn't hand write the methods on the class, they get added to the class via the method shown here. So I wasn't able to get it to work using blocks because I have to do nested block calls e.g.

workflow() do # first method call
  # first nested method call.  can't access my scope from here
  state(:state_name) do 
    # second nested method call.  can't access my scope
    event(:event_name, transitions_to: :transition_to_state)
  end
end

One of the things I'm trying to do is call the Workflow#state() method n number of times, while nesting the Workflow#event(with, custom_params) 0..n times. The problem for me seems to be that I can't get the right scope when I nest the methods like that.

It works just like I'd like it to (I think...) but I'm not too sure I hit the best implementation. In fact, I think I'll probably get some strong words for what I've done. I tried using public_send() and every other thing I could find to avoid using class_eval() to no avail.

Whenever I attempted to use one of the "better" methods, I couldn't quite get the scope right and sometimes, I was invoking methods on the wrong object, altogether. So I think this is where I need the help, yeah?

This is what a few of the attempts were going for but this is more pseudo-code because I could never get this version or any like it to fly.

# Call this as soon as you can, after .new()
def inject_workflow(description)
  public_send :workflow do 
    description[:workflow][:states].each do |state|
      state.map do |name, event|
        public_send name.to_sym do # nested call occurs in Workflow gem
          # nested call occurs in Workflow gem
          public_send :event, event[:name], transitions_to: event[:transitions_to]
        end
      end
    end
  end
end

From what I was trying, all these kinds of attempts ended up in the same result, which was my scope isn't what I need because I'm evaluating code in the Workflow gem, not in the module or user's class.

Anyways, here's my implementation. I would really appreciate it if someone could point me in the right direction!

module WorkflowFactory
# ...
  def inject_workflow(description)       
      # Build up an array of strings that will be used to create exactly what 
      # you would hand-write in your class, if you wanted to use the gem.
      description_string_builder = ['include Workflow', 'workflow do']
      description[:workflow][:states].each do |state|
        state.map do |name, state_description|
          if state_description.nil? # if this is a final state...
            description_string_builder << "state :#{name}"
          else # because it is not a final state, add event information too.
            description_string_builder.concat([
              "state :#{name} do",
              "event :#{state_description[:event]}, transitions_to: :#{state_description[:transitions_to]}",
              "end"
            ])
          end
        end
      end
      description_string_builder << "end\n"
      begin
        # Use class_eval to run that workflow specification by 
        # passing it off to the workflow gem, just like you would when you use 
        # the gem normally.  I'm pretty sure this is where everyone's head pops...
        self.class.class_eval(description_string_builder.join("\n"))
        define_singleton_method(:has_workflow?) { true }
      rescue Exception => e
        define_singleton_method(:has_workflow?) { !!(puts e.backtrace) }
      end
    end 
  end
end


# This is the class in question.
class Job
  include WorkflowFactory
  # ... some interesting code for your class goes here
  def next!
    current_state.events.#somehow choose the correct event
  end
end

# and in some other place where you want your "job" to be able to use a workflow, you have something like this...
job = Job.new
job.done?
# => false
until job.done? do job.next! end
# progresses through the workflow and manages its own state awareness

I started this question off under 300000 lines of text, I swear. Thanks for hanging in there! Here's even more documentation, if you're not asleep yet. module in my gem


Adam
  • 344
  • 1
  • 15
  • Thanks for formatting my question, Makoto! I'll make sure I do a better job at that. – Adam Oct 24 '16 at 00:03
  • can you be a little more specific about what your question is? what's not working? – max pleaner Oct 24 '16 at 00:36
  • I can. So basically I don't know why I couldn't just use `public_send()` to call the workflow methods because that's all you're doing when you use the gem normally, as far as I can make out. I tried to give some more info in an update on the question too. Thanks for looking at it, Max. – Adam Oct 24 '16 at 00:42
  • Oh I see now that your building a string which has ruby code then running eval. This is probably unnecessary, as you can use 'class_exec' or instance_exec to run a block of code in a different scope. You might be making this more complicated then necessary. Can you show a usage example? – max pleaner Oct 24 '16 at 00:45
  • Ok, I hope I haven't run the internet out of ink writing this question but I hope that makes a bit more sense...I'm going to read what I can find for those methods you mentioned, though. I don't think I even checked that set of solutions out. Thanks again, Max! – Adam Oct 24 '16 at 01:15
  • Alright, I think I understand the difference between exec and eval but I'm not sure how I can use it. One of the reasons it was tricky for me was that you do nested method calls but when you use eval and exec, the context is changed. When you hand write them, inside your class, the scope doesn't change to inside the workflow gem but when you use eval or exec, it does. So when I tried to send blocks that contained dynamic amounts and with different parameters, the scope couldn't find the keys and values from the hash in the parameters list. I'll try to update the question. – Adam Oct 24 '16 at 05:46
  • can you just try and refactor your code to use actual code and not strings containing code? It's ok if it doesn't work, but it'll make what you're trying to do clearer. Also, I recognize what problem you're describing (accessing multiple scopes in a `class_exec` or `instance_exec`). There are a few ways to deal with that - you can use class methods which you invoke with an explicit caller (a constant), or you can `include` or `extend` the other code via a module. It looks like you have a module here, so that'd probably be a good approach. – max pleaner Oct 24 '16 at 06:22
  • I think I tried to do what you're suggesting. Maybe I just didn't do it right but I spent a lot of hours trying and several more reading so it must be beyond me. Anyways, what I think you're saying is "instead of building some giant string that looks exactly like a block you want to run, why can't you just create some blocks and run them?" If that's what you meant, I just couldn't get the scope right, like I think you mentioned next. Can you give me some patterns or something to google? I'm still not fully understanding what you were suggesting. Thanks again, by the way. – Adam Oct 24 '16 at 06:37
  • sure, see this https://gist.github.com/MaxPleaner/eb6c3b8b41b482100e8c2463a0b351b7 – max pleaner Oct 24 '16 at 07:14
  • I wish I understood how to use that because it would be much cleaner. I've already used the `klass.extend()` and `klass.include()`, also trying the `send()` methods to get those to work in various ways. There's a line that does that somewhere else to include the module but it still didn't get the methods from workflow on the class vial the module's method call (from anywhere onto the instance). – Adam Oct 24 '16 at 07:30

0 Answers0